diff --git a/app/common/middleware/AuthTokenMiddleware.php b/app/common/middleware/AuthTokenMiddleware.php new file mode 100644 index 0000000..2a9b0e7 --- /dev/null +++ b/app/common/middleware/AuthTokenMiddleware.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- + +namespace app\common\middleware; + + +use app\Request; +use app\services\supplier\LoginServices; +use crmeb\interfaces\MiddlewareInterface; +use think\facade\Config; + +/** + * Class AuthTokenMiddleware + * @package app\http\middleware\supplier + */ +class AuthTokenMiddleware implements MiddlewareInterface +{ + + /** + * @param Request $request + * @param \Closure $next + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function handle(Request $request, \Closure $next) + { + $token = trim(ltrim($request->header(Config::get('cookie.token_name', 'Authori-zation')), 'Bearer')); + /** @var LoginServices $services */ + $services = app()->make(LoginServices::class); + $outInfo = $services->parseToken($token); + + $request->macro('supplierId', function () use (&$outInfo) { + return (int)$outInfo['id']; + }); + + $request->macro('supplierInfo', function () use (&$outInfo) { + return $outInfo; + }); + + return $next($request); + } +} diff --git a/app/common/middleware/StationOpenMiddleware.php b/app/common/middleware/StationOpenMiddleware.php new file mode 100644 index 0000000..cb99214 --- /dev/null +++ b/app/common/middleware/StationOpenMiddleware.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +namespace app\common\middleware; + + +use app\Request; +use crmeb\interfaces\MiddlewareInterface; + +/** + * 站点升级 + * Class StationOpenMiddleware + * @package app\api\middleware + */ +class StationOpenMiddleware implements MiddlewareInterface +{ + public function handle(Request $request, \Closure $next) + { + if (!systemConfig('station_open', true)) { + return app('json')->make('410010', '站点升级中,请稍候访问'); + } + return $next($request); + } +} diff --git a/app/common/middleware/SupplierCheckRoleMiddleware.php b/app/common/middleware/SupplierCheckRoleMiddleware.php new file mode 100644 index 0000000..dc5ae1f --- /dev/null +++ b/app/common/middleware/SupplierCheckRoleMiddleware.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- + +namespace app\common\middleware; + +use app\Request; + +use app\services\supplier\LoginServices; +use crmeb\exceptions\AuthException; +use crmeb\interfaces\MiddlewareInterface; +use crmeb\utils\ApiErrorCode; + +/** + * 权限规则验证 + * Class SupplierCheckRoleMiddleware + * @package app\http\middleware\supplier + */ +class SupplierCheckRoleMiddleware implements MiddlewareInterface +{ + + public function handle(Request $request, \Closure $next) + { + if (!$request->supplierId() || !$request->supplierInfo()) + throw new AuthException(ApiErrorCode::ERR_ADMINID_VOID); + + if ($request->supplierInfo()['level'] ?? 0) { + /** @var LoginServices $services */ + $services = app()->make(LoginServices::class); + $services->verifiAuth($request); + } + + return $next($request); + } +} diff --git a/app/common/middleware/SupplierLogMiddleware.php b/app/common/middleware/SupplierLogMiddleware.php new file mode 100644 index 0000000..6652d37 --- /dev/null +++ b/app/common/middleware/SupplierLogMiddleware.php @@ -0,0 +1,41 @@ + +// +---------------------------------------------------------------------- + +namespace app\common\middleware; + + +use app\Request; +use app\jobs\system\AdminLogJob; +use crmeb\interfaces\MiddlewareInterface; + +/** + * 操作日志记录 + * Class SupplierLogMiddleware + * @package app\http\middleware\supplier + */ +class SupplierLogMiddleware implements MiddlewareInterface +{ + + /** + * @param Request $request + * @param \Closure $next + * @return mixed + */ + public function handle(Request $request, \Closure $next) + { + $module = app('http')->getName(); + $rule = trim(strtolower($request->rule()->getRule())); + //记录后台日志 + AdminLogJob::dispatch([$request->supplierId(), $request->supplierInfo()['supplier_name'], $module, $rule, $request->ip(), 'supplier']); + + return $next($request); + } +} diff --git a/app/dao/BaseDao.php b/app/dao/BaseDao.php new file mode 100644 index 0000000..7f6f078 --- /dev/null +++ b/app/dao/BaseDao.php @@ -0,0 +1,449 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao; + +use crmeb\basic\BaseAuth; +use crmeb\basic\BaseModel; +use crmeb\traits\dao\CacheDaoTrait; +use think\Model; + +/** + * Class BaseDao + * @package app\dao + */ +abstract class BaseDao +{ + + use CacheDaoTrait; + /** + * 当前表名别名 + * @var string + */ + protected $alias; + + /** + * join表别名 + * @var string + */ + protected $joinAlis; + + + /** + * 获取当前模型 + * @return string + */ + abstract protected function setModel(): string; + + /** + * 设置join链表模型 + * @return string + */ + protected function setJoinModel(): string + { + } + + /** + * 读取数据条数 + * @param array $where + * @return int + */ + public function count(array $where = []): int + { + return $this->search($where)->count(); + } + + /** + * 获取某些条件总数 + * @param array $where + * @return int + */ + public function getCount(array $where) + { + return $this->getModel()->where($where)->count(); + } + + /** + * 获取某些条件去重总数 + * @param array $where + * @param $field + * @param $search + */ + public function getDistinctCount(array $where, $field, $search = true) + { + if ($search) { + return $this->search($where)->field('COUNT(distinct(' . $field . ')) as count')->select()->toArray()[0]['count'] ?? 0; + } else { + return $this->getModel()->where($where)->field('COUNT(distinct(' . $field . ')) as count')->select()->toArray()[0]['count'] ?? 0; + } + } + + /** + * 获取模型 + * @return BaseModel + */ + protected function getModel() + { + return app()->make($this->setModel()); + } + + /** + * 获取主键 + * @return mixed + */ + protected function getPk() + { + return $this->getModel()->getPk(); + } + + /** + * 获取一条数据 + * @param int|array $id + * @param array|null $field + * @param array|null $with + * @return array|Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function get($id, ?array $field = [], ?array $with = []) + { + if (is_array($id)) { + $where = $id; + } else { + $where = [$this->getPk() => $id]; + } + return $this->getModel()::where($where)->when(count($with), function ($query) use ($with) { + $query->with($with); + })->field($field ?? ['*'])->find(); + } + + /** + * 查询一条数据是否存在 + * @param $map + * @param string $field + * @return bool 是否存在 + */ + public function be($map, string $field = '') + { + if (!is_array($map) && empty($field)) $field = $this->getPk(); + $map = !is_array($map) ? [$field => $map] : $map; + return 0 < $this->getModel()->where($map)->count(); + } + + /** + * 根据条件获取一条数据 + * @param array $where + * @param string|null $field + * @param array $with + * @return array|Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOne(array $where, ?string $field = '*', array $with = []) + { + $field = explode(',', $field); + return $this->get($where, $field, $with); + } + + /** + * 获取单个字段值 + * @param array $where + * @param string|null $field + * @return mixed + */ + public function value(array $where, ?string $field = '') + { + $pk = $this->getPk(); + return $this->getModel()::where($where)->value($field ?: $pk); + } + + /** + * 获取某个字段数组 + * @param array $where + * @param string $field + * @param string $key + * @param bool $search + * @return array + */ + public function getColumn(array $where, string $field, string $key = '', bool $search = false) + { + if ($search) { + return $this->search($where)->column($field, $key); + } else { + return $this->getModel()::where($where)->column($field, $key); + } + } + + /** + * 删除 + * @param int|string|array $id + * @return mixed + */ + public function delete($id, ?string $key = null) + { + if (is_array($id)) { + $where = $id; + } else { + $where = [is_null($key) ? $this->getPk() : $key => $id]; + } + return $this->getModel()::where($where)->delete(); + } + + /** + * 更新数据 + * @param int|string|array $id + * @param array $data + * @param string|null $key + * @return mixed + */ + public function update($id, array $data, ?string $key = null) + { + if (is_array($id)) { + $where = $id; + } else { + $where = [is_null($key) ? $this->getPk() : $key => $id]; + } + return $this->getModel()::update($data, $where); + } + + /** + * 批量更新数据 + * @param array $ids + * @param array $data + * @param string|null $key + * @return BaseModel + */ + public function batchUpdate(array $ids, array $data, ?string $key = null) + { + return $this->getModel()::whereIn(is_null($key) ? $this->getPk() : $key, $ids)->update($data); + } + + /** + * 批量更新数据,增加条件 + * @param array $ids + * @param array $data + * @param array $where + * @param string|null $key + * @return BaseModel + */ + public function batchUpdateAppendWhere(array $ids, array $data, array $where, ?string $key = null) + { + return $this->getModel()::whereIn(is_null($key) ? $this->getPk() : $key, $ids)->where($where)->update($data); + } + + /** + * 插入数据 + * @param array $data + * @return BaseModel|Model + */ + public function save(array $data) + { + return $this->getModel()::create($data); + } + + /** + * 插入数据 + * @param array $data + * @return mixed + */ + public function saveAll(array $data) + { + return $this->getModel()::insertAll($data); + } + + /** + * 获取某个字段内的值 + * @param $value + * @param string $filed + * @param string $valueKey + * @param array|string[] $where + * @return mixed + */ + public function getFieldValue($value, string $filed, ?string $valueKey = '', ?array $where = []) + { + return $this->getModel()->getFieldValue($value, $filed, $valueKey, $where); + } + + /** + * 根据搜索器获取搜索内容 + * @param array $withSearch + * @param array|null $data + * @return Model + */ + protected function withSearchSelect(array $withSearch, ?array $data = []) + { + [$with] = app()->make(BaseAuth::class)->________($withSearch, $this->setModel()); + return $this->getModel()->withSearch($with, $data); + } + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed + */ + protected function search(array $where = []) + { + if ($where) { + return $this->withSearchSelect(array_keys($where), $where); + } else { + return $this->getModel(); + } + } + + /** + * 求和 + * @param array $where + * @param string $field + * @param bool $search + * @return float + */ + public function sum(array $where, string $field, bool $search = false) + { + if ($search) { + return $this->search($where)->sum($field); + } else { + return $this->getModel()::where($where)->sum($field); + } + } + + /** + * 高精度加法 + * @param int|string $key + * @param string $incField + * @param string $inc + * @param string|null $keyField + * @param int $acc + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function bcInc($key, string $incField, string $inc, string $keyField = null, int $acc = 2) + { + return $this->bc($key, $incField, $inc, $keyField, 1); + } + + /** + * 高精度 减法 + * @param int|string $uid id + * @param string $decField 相减的字段 + * @param float|int $dec 减的值 + * @param string $keyField id的字段 + * @param bool $minus 是否可以为负数 + * @param int $acc 精度 + * @param bool $dec_return_false 减法 不够减是否返回false || 减为0 + * @return bool + */ + public function bcDec($key, string $decField, string $dec, string $keyField = null, int $acc = 2, bool $dec_return_false = true) + { + return $this->bc($key, $decField, $dec, $keyField, 2, $acc, $dec_return_false); + } + + /** + * 高精度计算并保存 + * @param $key + * @param string $incField + * @param string $inc + * @param string|null $keyField + * @param int $type + * @param int $acc + * @param bool $dec_return_false 减法 不够减是否返回false || 减为0 + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function bc($key, string $incField, string $inc, string $keyField = null, int $type = 1, int $acc = 2, bool $dec_return_false = true) + { + if ($keyField === null) { + $result = $this->get($key); + } else { + $result = $this->getOne([$keyField => $key]); + } + if (!$result) return false; + if ($type === 1) { + $new = bcadd($result[$incField], $inc, $acc); + } else if ($type === 2) { + if ($result[$incField] < $inc) { + if ($dec_return_false) return false; + $new = 0; + } else { + $new = bcsub($result[$incField], $inc, $acc); + } + } + $result->{$incField} = $new; + return false !== $result->save(); + } + + /** + * 减库存加销量 + * @param array $where + * @param int $num + * @return mixed + */ + public function decStockIncSales(array $where, int $num, string $stock = 'stock', string $sales = 'sales') + { + return app()->make(BaseAuth::class)->_____($this->getModel(), $where, $num, $stock, $sales) !== false; + } + + /** + * 加库存减销量 + * @param array $where + * @param int $num + * @return mixed + */ + public function incStockDecSales(array $where, int $num, string $stock = 'stock', string $sales = 'sales') + { + return app()->make(BaseAuth::class)->___($this->getModel(), $where, $num, $stock, $sales) !== false; + } + + /** + * 软删除 + * @param $id + * @param string|null $key + * @return bool + */ + public function destroy($id, ?string $key = null) + { + if (is_array($id)) { + $where = $id; + } else { + $where = [is_null($key) ? $this->getPk() : $key => $id]; + } + return $this->getModel()::destroy($where); + } + + /** + * 自增单个数据 + * @param $where + * @param string $field + * @param int $number + * @return mixed + */ + public function incUpdate($where, string $field, int $number = 1) + { + return $this->getModel()->where(is_array($where) ? $where : [$this->getPk() => $where])->inc($field, $number)->update(); + } + + /** + * 自减单个数据 + * @param $where + * @param string $field + * @param int $number + * @return mixed + */ + public function decUpdate($where, string $field, int $number = 1) + { + return $this->getModel()->where(is_array($where) ? $where : [$this->getPk() => $where])->dec($field, $number)->update(); + } +} diff --git a/app/dao/activity/StoreActivityDao.php b/app/dao/activity/StoreActivityDao.php new file mode 100644 index 0000000..f18a43d --- /dev/null +++ b/app/dao/activity/StoreActivityDao.php @@ -0,0 +1,100 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity; + +use app\dao\BaseDao; +use app\common\model\store\StoreActivity; + +/** + * 活动 + * Class StoreActivityDao + * @package app\dao\activity + */ +class StoreActivityDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreActivity::class; + } + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + protected function search(array $where = []) + { + return parent::search($where)->when(isset($where['activityTime']), function ($query) use ($where) { + [$startTime, $stopTime] = is_array($where['activityTime']) ? $where['activityTime'] : [time(), time() - 86400]; + $query->where('start_day', '<=', $startTime)->where('end_day', '>=', $stopTime); + })->when(isset($where['start_status']) && $where['start_status'] !== '', function ($query) use ($where) { + $time = time(); + switch ($where['start_status']) { + case -1: + $query->where(function ($q) use ($time) { + $q->where('end_day', '<', $time - 86400)->whereOr('status', '0'); + }); + break; + case 0: + $query->where('start_day', '>', $time)->where('status', 1); + break; + case 1: + $query->where('start_day', '<=', $time)->where('end_day', '>=', $time - 86400)->where('status', 1); + break; + } + }); + } + + + /** + * 获取活动列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array $with + * @param string $order + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, array $with = [], string $order = 'start_time asc, id desc') + { + return $this->search($where)->field($field) + ->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order($order)->select()->toArray(); + } + + /** + * 验证是否有活动 + * @param array $where + * @return bool + * @throws \think\db\exception\DbException + */ + public function checkActivity(array $where) + { + return $this->search($where)->where('end_day', '>', time())->count() > 0; + } + + + +} diff --git a/app/dao/activity/bargain/StoreBargainDao.php b/app/dao/activity/bargain/StoreBargainDao.php new file mode 100644 index 0000000..758d80a --- /dev/null +++ b/app/dao/activity/bargain/StoreBargainDao.php @@ -0,0 +1,143 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\bargain; + +use app\dao\BaseDao; +use app\model\activity\bargain\StoreBargain; + +/** + * 砍价商品 + * Class StoreBargainDao + * @package app\dao\activity\bargain + */ +class StoreBargainDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreBargain::class; + } + + /** + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where) + ->when(isset($where['start_status']) && $where['start_status'] !== '', function ($query) use ($where) { + $time = time(); + switch ($where['start_status']) { + case -1: + $query->where('stop_time', '<', $time); + break; + case 0: + $query->where('start_time', '>', $time); + break; + case 1: + $query->where('start_time', '<=', $time)->where('stop_time', '>=', $time); + break; + } + }); + } + + /** + * 获取砍价列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page = 0, int $limit = 0) + { + return $this->search($where)->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id desc')->select()->toArray(); + } + + /** + * 获取活动开启中的砍价id以数组形式返回 + * @param array $ids + * @param array $field + * @return array + */ + public function getBargainIdsArray(array $ids, array $field = []) + { + return $this->search(['is_del' => 0, 'status' => 1])->where('start_time', '<=', time()) + ->where('stop_time', '>=', time())->whereIn('product_id', $ids)->column(implode(',', $field), 'product_id'); + } + + /** + * 根据id获取砍价数据 + * @param array $ids + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function idByBargainList(array $ids, string $field) + { + return $this->getModel()->whereIn('id', $ids)->field($field)->select()->toArray(); + } + + /** + * 正在开启的砍价活动 + * @param int $status + * @return StoreBargain + */ + public function validWhere(int $status = 1) + { + return $this->getModel()->where('is_del', 0)->where('status', $status)->where('start_time', '<', time())->where('stop_time', '>', time()); + + } + + /** + * 砍价列表 + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function bargainList(int $page = 0, int $limit = 0) + { + return $this->search(['is_del' => 0, 'status' => 1]) + ->where('start_time', '<=', time()) + ->where('stop_time', '>=', time()) + ->where('product_id', 'IN', function ($query) { + $query->name('store_product')->where('is_show', 1)->where('is_del', 0)->field('id'); + })->with('product')->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort DESC,id DESC')->select()->toArray(); + } + + /** + * 修改砍价状态 + * @param int $id + * @param string $field + * @return mixed + */ + public function addBargain(int $id, string $field) + { + return $this->getModel()->where('id', $id)->inc($field, 1)->update(); + } +} diff --git a/app/dao/activity/collage/UserCollageDao.php b/app/dao/activity/collage/UserCollageDao.php new file mode 100644 index 0000000..eb67f63 --- /dev/null +++ b/app/dao/activity/collage/UserCollageDao.php @@ -0,0 +1,98 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\activity\collage; + + +use app\dao\BaseDao; +use app\model\activity\collage\UserCollageCode; + +/** + * 拼单 + * Class UserCollageDao + * @package app\dao\collage + */ +class UserCollageDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserCollageCode::class; + } + + /**搜索条件处理 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['serial_number']) && $where['serial_number'] != '', function ($query) use ($where) { + if (substr($where['serial_number'], 0, 2) == 'wx') { + $query->where(function ($que) use ($where) { + $que->where('oid', 'in', function ($q) use ($where) { + $q->name('store_order')->where('order_id', 'LIKE', '%' . $where['serial_number'] . '%')->where('type', 10)->field(['id'])->select(); + }); + }); + } else { + $query->where('serial_number', 'LIKE', '%' . $where['serial_number'] . '%'); + } + })->when(isset($where['status']) && $where['status'], function ($query) use ($where) { + if (is_array($where['status'])) { + $query->whereIn('status', $where['status']); + } else { + $query->where('status', $where['status']); + } + })->when(isset($where['type']) && $where['type'], function ($query) use ($where) { + $query->where('type', $where['type']); + })->when(isset($where['store_id']) && $where['store_id'], function ($query) use ($where) { + $query->where('store_id', $where['store_id']); + })->when(isset($where['staff_id']) && $where['staff_id'] > 0, function ($query) use ($where) { + $query->where(function ($que) use ($where) { + $que->where('oid', 'in', function ($q) use ($where) { + $q->name('store_order')->where('staff_id', $where['staff_id'])->where('type', 10)->field(['id'])->select(); + }); + }); + }); + } + + /**获取当天最大流水号 + * @param $where + * @return mixed + */ + public function getMaxSerialNumber($where) + { + $now_day = strtotime(date('Y-m-d')); + return $this->search($where)->where('add_time', '>', $now_day)->order('add_time desc')->value('serial_number'); + } + + /**桌码订单 + * @param array $where + * @param int $store_id + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function searchTableCodeList(array $where, array $field = ['*'], int $page = 0, int $limit = 0, array $with = []) + { + return $this->search($where)->field($field)->when(count($with), function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(!$page && $limit, function ($query) use ($limit) { + $query->limit($limit); + })->order('add_time desc')->select()->toArray(); + } +} diff --git a/app/dao/activity/collage/UserCollagePartakeDao.php b/app/dao/activity/collage/UserCollagePartakeDao.php new file mode 100644 index 0000000..dffc41e --- /dev/null +++ b/app/dao/activity/collage/UserCollagePartakeDao.php @@ -0,0 +1,106 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\activity\collage; + + +use app\dao\BaseDao; +use app\model\activity\collage\UserCollageCodePartake; + +/** + * 用户参与拼单 + * Class UserCollagePartakeDao + * @package app\dao\collage + */ +class UserCollagePartakeDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserCollageCodePartake::class; + } + + /** + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['uid']) && $where['uid'], function ($query) use ($where) { + $query->where('uid', $where['uid']); + })->when(isset($where['collate_code_id']) && $where['collate_code_id'], function ($query) use ($where) { + $query->where('collate_code_id', $where['collate_code_id']); + })->when(isset($where['store_id']) && $where['store_id'], function ($query) use ($where) { + $query->where('store_id', $where['store_id']); + })->when(isset($where['status']) && $where['status'], function ($query) use ($where) { + $query->where('status', 1); + }); + } + + /** + * 用户拼单商品统计数据 + * @param int $uid + * @param string $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserPartakeList(array $where, string $field = '*', array $with = []) + { + return $this->getModel()->where($where)->when(count($with), function ($query) use ($with) { + $query->with($with); + })->order('add_time DESC')->field($field)->select()->toArray(); + } + + /** + * 获取拼单信息 + * @param array $where + * @param string $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserPartakeProductList(array $where, string $field = '*', array $with = []) + { + return $this->getModel()->where($where)->when(count($with), function ($query) use ($with) { + $query->with($with); + })->order('add_time DESC')->field($field)->select()->toArray(); + } + + /** + * 用户删除拼单、桌码商品 + * @param array $where + * @return bool + */ + public function del(array $where) + { + return $this->getModel()->where($where)->delete(); + } + + /**最后加入的记录 + * @param $where + * @return array|\crmeb\basic\BaseModel|mixed|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserPartake(array $where) + { + return $this->getModel()->where($where)->order('add_time DESC')->find(); + } +} diff --git a/app/dao/activity/combination/StoreCombinationDao.php b/app/dao/activity/combination/StoreCombinationDao.php new file mode 100644 index 0000000..8e6d3b0 --- /dev/null +++ b/app/dao/activity/combination/StoreCombinationDao.php @@ -0,0 +1,153 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\combination; + +use app\dao\BaseDao; +use app\model\activity\combination\StoreCombination; + +/** + * 拼团商品 + * Class StoreCombinationDao + * @package app\dao\activity\combination + */ +class StoreCombinationDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreCombination::class; + } + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['pinkIngTime']), function ($query) use ($where) { + $time = time(); + [$startTime, $stopTime] = is_array($where['pinkIngTime']) ? $where['pinkIngTime'] : [$time, $time]; + $query->where('start_time', '<=', $startTime)->where('stop_time', '>=', $stopTime); + })->when(isset($where['storeProductId']), function ($query) { + $query->where('product_id', 'IN', function ($query) { + $query->name('store_product')->where('is_show', 1)->where('is_del', 0)->field('id'); + }); + })->when(isset($where['start_status']) && $where['start_status'] !== '', function ($query) use ($where) { + $time = time(); + switch ($where['start_status']) { + case -1: + $query->where(function ($query) use ($time) { + $query->where('stop_time', '<', $time)->whereOr('is_show', 0); + }); + break; + case 0: + $query->where('start_time', '>', $time)->where('is_show', 1); + break; + case 1: + $query->where('start_time', '<=', $time)->where('stop_time', '>=', $time)->where('is_show', 1); + break; + } + }); + } + + + /** + * 拼团商品列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page = 0, int $limit = 0) + { + return $this->search($where)->with('getPrice') + ->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id desc')->select()->toArray(); + } + + /** + * 获取正在进行拼团的商品以数组形式返回 + * @param array $ids + * @param array $field + * @return array + */ + public function getPinkIdsArray(array $ids, array $field = []) + { + return $this->search(['is_del' => 0, 'is_show' => 1, 'pinkIngTime' => 1])->whereIn('product_id', $ids)->column(implode(',', $field), 'product_id'); + } + + /** + * 获取拼团列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function combinationList(array $where, int $page = 0, int $limit = 0) + { + return $this->search($where)->with('getPrice')->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id desc')->select()->toArray(); + } + + /** + * 根据id获取拼团数据 + * @param array $ids + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function idCombinationList(array $ids, string $field) + { + return $this->getModel()->whereIn('id', $ids)->field($field)->select()->toArray(); + } + + /** + * 获取一条拼团数据 + * @param int $id + * @param string $field + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function validProduct(int $id, string $field) + { + $where = ['is_show' => 1, 'is_del' => 0, 'pinkIngTime' => true]; + return $this->search($where)->where('id', $id)->with(['total'])->field($field)->order('add_time desc')->find(); + } + + /** + * 获取推荐拼团 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCombinationHost() + { + $where = ['is_del' => 0, 'is_host' => 1, 'is_show' => 1, 'pinkIngTime' => true]; + return $this->search($where)->order('id desc')->select()->toArray(); + } +} diff --git a/app/dao/activity/combination/StorePinkDao.php b/app/dao/activity/combination/StorePinkDao.php new file mode 100644 index 0000000..741b32a --- /dev/null +++ b/app/dao/activity/combination/StorePinkDao.php @@ -0,0 +1,203 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\combination; + +use app\dao\BaseDao; +use app\model\activity\combination\StorePink; + +/** + * 拼团 + * Class StorePinkDao + * @package app\dao\activity\combination + */ +class StorePinkDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StorePink::class; + } + + /** + * 获取拼团数量集合 + * @param array $where + * @return array + */ + public function getPinkCount(array $where = []) + { + return $this->getModel()->where($where)->group('cid')->column('count(*)', 'cid'); + } + + /** + * 获取列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page = 0, int $limit = 0) + { + return $this->search($where)->when($where['k_id'] != 0, function ($query) use ($where) { + $query->whereOr('id', $where['k_id']); + })->with(['getProduct', 'getUser'])->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc')->select()->toArray(); + } + + /** + * 获取正在拼团中的人,取最早写入的一条 + * @param array $where + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPinking(array $where) + { + return $this->search($where)->order('add_time asc')->find(); + } + + /** + * 获取拼团列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function pinkList(array $where) + { + return $this->search($where) + ->where('stop_time', '>', time()) + ->order('add_time desc') + ->select()->toArray(); + } + + /** + * 获取正在拼团的人数 + * @param int $kid + * @return int + */ + public function getPinkPeople(int $kid) + { + return $this->count(['k_id' => $kid, 'is_refund' => 0]) + 1; + } + + /** + * 获取正在拼团的人数 + * @param array $kids + * @return int + */ + public function getPinkPeopleCount(array $kids) + { + $count = $this->getModel()->whereIn('k_id', $kids)->where('is_refund', 0)->group('k_id')->column('COUNT(id) as count', 'k_id'); + $counts = []; + foreach ($kids as &$item) { + if (isset($count[$item])) { + $counts[$item] = $count[$item] + 1; + } else { + $counts[$item] = 1; + } + } + return $counts; + } + + /** + * 获取拼团成功的列表 + * @param int $uid + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function successList(int $uid, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search(['status' => 2, 'is_refund' => 0])->field($field) + ->where('uid', '>', 0) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(!$page && $limit, function ($query) use ($limit) { + $query->limit($limit); + })->order('id desc')->select()->toArray(); + } + + + /** + * 获取拼团完成的个数 + * @return float + */ + public function getPinkOkSumTotalNum() + { + return $this->sum(['status' => 2, 'is_refund' => 0], 'total_num'); + } + + /** + * 是否能继续拼团 + * @param int $id + * @param int $uid + * @return int + */ + public function isPink(int $id, int $uid) + { + return $this->getModel()->where('k_id|id', $id)->where('uid', $uid)->where('is_refund', 0)->count(); + } + + /** + * 获取一条拼团信息 + * @param int $id + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPinkUserOne(int $id) + { + return $this->search()->with('getProduct')->find($id); + } + + /** + * 获取拼团信息 + * @param array $where + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPinkUserList(array $where) + { + return $this->getModel()->where($where)->with('getProduct')->select()->toArray(); + } + + /** + * 获取拼团结束的列表 + * @return \crmeb\basic\BaseModel + */ + public function pinkListEnd() + { + return $this->getModel()->where('stop_time', '<=', time()) + ->where('status', 1) + ->where('k_id', 0) + ->where('is_refund', 0) + ->field('id,people,k_id,order_id_key,uid,stop_time'); + } +} diff --git a/app/dao/activity/coupon/StoreCouponIssueDao.php b/app/dao/activity/coupon/StoreCouponIssueDao.php new file mode 100644 index 0000000..cb69fc9 --- /dev/null +++ b/app/dao/activity/coupon/StoreCouponIssueDao.php @@ -0,0 +1,397 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\coupon; + +use app\dao\BaseDao; +use app\model\activity\coupon\StoreCouponIssue; + + +/** + * 优惠卷 + * Class StoreCouponIssueDao + * @package app\dao\activity\coupon + */ +class StoreCouponIssueDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreCouponIssue::class; + } + + /** + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['receive']) && $where['receive'] != '', function ($query) use ($where) { + if ($where['receive'] == 'send') { + $query->where('receive_type', 3) + ->where('status', 1) + ->where('is_del', 0) + ->where('remain_count > 0 OR is_permanent = 1') + ->where(function ($query1) { + $query1->where(function ($query2) { + $query2->where('start_time', '<=', time())->where('end_time', '>=', time()); + })->whereOr(function ($query3) { + $query3->where('start_time', 0)->where('end_time', 0); + }); + })->where(function ($query4) { + $query4->where(function ($query5) { + $query5->where('coupon_time', 0)->where('end_use_time', '>=', time()); + })->whereOr('coupon_time', '>', 0); + }); + } + })->when(isset($where['receive_type']) && $where['receive_type'], function ($query) use ($where) { + $query->where('receive_type', $where['receive_type']); + }); + } + + /** + * 有效优惠券搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function validSearch(array $where = []) + { + return parent::search($where) + ->where('status', 1) + ->where('is_del', 0) + ->where('remain_count > 0 OR is_permanent = 1') + ->where(function ($query) { + $query->where(function ($query) { + $query->where('start_time', '<=', time())->where('end_time', '>=', time()); + })->whereOr(function ($query) { + $query->where('start_time', 0)->where('end_time', 0); + }); + })->where(function ($query4) { + $query4->where(function ($query5) { + $query5->where('coupon_time', 0)->where('end_use_time', '>=', time()); + })->whereOr('coupon_time', '>', 0); + }); + } + + /** + * 获取列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page = 0, int $limit = 0) + { + return $this->search($where)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + + /** + * 获取有效的优惠券列表 + * @param array $where + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getValidList(array $where = [], int $page = 0, int $limit = 0, array $with = []) + { + return $this->validSearch($where) + ->when(isset($where['not_id']) && $where['not_id'], function($query) use ($where) { + $query->whereNotIn('id', $where['not_id']); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when($with, function ($query) use ($with) { + $query->with($with); + })->order('id desc')->select()->toArray(); + } + + /** + * 获取有效的赠送券 + * @param array $ids + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getValidGiveCoupons(array $ids, string $field = '*' , int $page = 0, int $limit = 0) + { + return $this->validSearch()->field($field) + ->where('receive_type',3) + ->when(count($ids), function ($query) use ($ids) { + $query->whereIn('id', $ids); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 获取商品优惠券 + * @param int $uid + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param string $sort + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + + public function getIssueCouponListNew(int $uid, array $where = [], string $field = '*', int $page = 0, int $limit = 0, string $sort = 'sort desc,id desc') + { + return $this->validSearch()->field($field) + ->whereIn('receive_type', [1, 4]) + ->with(['used' => function ($query) use ($uid) { + $query->where('uid', $uid); + }]) + ->when(isset($where['type']) && $where['type'] != -1, function ($query) use ($where) { + $query->where('type', $where['type']); + }) + ->when(isset($where['type']) && $where['type'] == 1 && isset($where['cate_id']) && $where['cate_id'], function ($query) use ($where) { + $query->where('category_id', 'in', $where['cate_id']); + }) + ->when(isset($where['type']) && $where['type'] == 2 && isset($where['product_id']) && $where['product_id'], function ($query) use ($where) { + $query->whereFindinSet('product_id', $where['product_id']); + }) + ->when(isset($where['type']) && $where['type'] == 3 && isset($where['brand_id']) && $where['brand_id'], function ($query) use ($where) { + $query->where('brand_id', 'in', $where['brand_id']); + }) + ->when((isset($where['product_id']) && $where['product_id']) || (isset($where['cate_id']) && $where['cate_id']) || isset($where['brand_id']) && $where['brand_id'], function ($query) use ($where) { + $query->where(function ($z) use ($where) { + $z->whereOr('type', 0) + ->when(isset($where['product_id']) && $where['product_id'], function ($p) use ($where) { + $p->whereOr(function ($c) use ($where) { + $c->whereFindinSet('product_id', $where['product_id']); + }); + })->when(isset($where['cate_id']) && $where['cate_id'], function ($c) use ($where) { + $c->whereOr('category_id', 'in', $where['cate_id']); + })->when(isset($where['brand_id']) && $where['brand_id'], function ($b) use ($where) { + $b->whereOr('brand_id', 'in', $where['brand_id']); + }); + }); + }) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order($sort)->select()->toArray(); + } + + /** + * 获取优惠券列表 + * @param int $uid 用户ID + * @param int $type 0通用,1分类,2商品 + * @param int $typeId 分类ID或商品ID + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getIssueCouponList(int $uid, int $type, $typeId, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->validSearch()->field($field) + ->whereIn('receive_type', [1, 4]) + ->with(['used' => function ($query) use ($uid) { + $query->where('uid', $uid); + }]) + ->where('type', $type) + ->when($type == 1, function ($query) use ($typeId) { + if ($typeId) $query->where('category_id', 'in', $typeId); + }) + ->when($type == 2, function ($query) use ($typeId) { + if ($typeId) $query->whereFindinSet('product_id', $typeId); + }) + ->when($type == 3, function ($query) use ($typeId) { + if ($typeId) $query->where('brand_id', 'in', $typeId); + }) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id desc')->select()->toArray(); + } + + /** + * PC端获取优惠券 + * @param int $uid + * @param array $cate_ids + * @param int $product_id + * @param string $filed + * @param int $page + * @param int $limit + * @param string $sort + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPcIssueCouponList(int $uid, array $cate_ids = [], int $product_id = 0, string $filed = '*', int $page = 0, int $limit = 0, string $sort = 'sort desc,id desc') + { + return $this->validSearch()->field($filed) + ->whereIn('receive_type', [1, 4]) + ->with(['used' => function ($query) use ($uid) { + $query->where('uid', $uid); + }])->where(function ($query) use ($product_id, $cate_ids) { + if ($product_id != 0 && $cate_ids != []) { + $query->whereFindinSet('product_id', $product_id)->whereOr('category_id', 'in', $cate_ids)->whereOr('type', 0); + } + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(!$page && $limit, function ($query) use ($page, $limit) { + $query->limit($limit); + })->order($sort)->select()->toArray(); + } + + /** + * 获取优惠券数量 + * @param int $productId + * @param int $cateId + * @return mixed + */ + public function getIssueCouponCount($productId = 0, $cateId = 0, $brandId = 0) + { + $count[0] = $this->validSearch()->whereIn('receive_type', [1, 4])->where('type', 0)->count(); + $count[1] = $this->validSearch()->whereIn('receive_type', [1, 4])->where('type', 1)->when($cateId != 0, function ($query) use ($cateId) { + if ($cateId) $query->where('category_id', 'in', $cateId); + })->count(); + $count[2] = $this->validSearch()->whereIn('receive_type', [1, 4])->where('type', 2)->when($productId != 0, function ($query) use ($productId) { + if ($productId) $query->whereFindinSet('product_id', $productId); + })->count(); + $count[3] = $this->validSearch()->whereIn('receive_type', [1, 4])->where('type', 3)->when($productId != 0, function ($query) use ($brandId) { + if ($brandId) $query->where('brand_id', 'in', $brandId); + })->count(); + return $count; + } + + /** + * 获取优惠卷详情 + * @param int $id + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getInfo(int $id) + { + return $this->validSearch()->where('id', $id)->find(); + } + + /** + * 获取金大于额的优惠卷金额 + * @param string $totalPrice + * @return float + */ + public function getUserIssuePrice(string $totalPrice) + { + return $this->search(['status' => 1, 'is_full_give' => 1, 'is_del' => 0]) + ->where('full_reduction', '<=', $totalPrice) + ->sum('coupon_price'); + } + + /** + * 获取新人券 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getNewCoupon() + { + return $this->validSearch()->where('receive_type', 2)->select()->toArray(); + } + + /** + * 获取一条优惠券信息 + * @param int $id + * @return mixed + */ + public function getCouponInfo(int $id) + { + return $this->getModel()->where('id', $id)->where('status', 1)->where('is_del', 0)->find(); + } + + /** + * 获取满赠、下单、关注赠送优惠券 + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGiveCoupon(array $where, string $field = '*') + { + return $this->validSearch()->field($field)->where($where)->select()->toArray(); + } + + /** + * 获取商品优惠卷列表 + * @param $where + * @param $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function productCouponList($where, $field) + { + return $this->getModel()->where($where)->field($field)->select()->toArray(); + } + + /** + * 获取优惠券弹窗列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTodayCoupon($uid) + { + return $this->validSearch() + ->whereIn('receive_type', [1, 4]) + ->when($uid != 0, function ($query) use ($uid) { + $query->with(['used' => function ($query) use ($uid) { + $query->where('uid', $uid); + }]); + })->whereDay('add_time')->order('sort desc,id desc')->select()->toArray(); + } + + /** + * api数据获取优惠券 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getApiIssueList(array $where) + { + return $this->getModel()->where($where)->select()->toArray(); + } + + +} diff --git a/app/dao/activity/coupon/StoreCouponUserDao.php b/app/dao/activity/coupon/StoreCouponUserDao.php new file mode 100644 index 0000000..4174359 --- /dev/null +++ b/app/dao/activity/coupon/StoreCouponUserDao.php @@ -0,0 +1,242 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\coupon; + +use app\dao\BaseDao; +use app\common\model\store\coupon\StoreCouponUser; + +/** + * Class StoreCouponUserDao + * @package app\dao\activity\coupon + */ +class StoreCouponUserDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreCouponUser::class; + } + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['nickname']) && $where['nickname'] != '', function ($query) use ($where) { + $query->where('uid', 'IN' , function($q) use ($where) { + $q->name('user')->where('uid|real_name|nickname|account|phone', 'like', '%' . $where['nickname'] . '%')->field('uid'); + }); + })->when(isset($where['coupon_title']) && $where['coupon_title'] != '', function ($query) use ($where) { + $query->where('coupon_title', 'like', '%' . $where['coupon_title'] . '%'); + }); + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param array $with + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', array $with = ['issue'], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->with($with)->page($page, $limit)->order('id desc')->select()->toArray(); + } + + /** + * 使用优惠券修改优惠券状态 + * @param $id + * @return \think\Model|null + */ + public function useCoupon(int $id) + { + return $this->getModel()->where('id', $id)->update(['status' => 1, 'use_time' => time()]); + } + + /** + * 获取指定商品id下的优惠卷 + * @param array $productIds + * @param int $uid + * @param string $price + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function productIdsByCoupon(array $productIds, int $uid, string $price) + { + return $this->getModel()->whereIn('cid', function ($query) use ($productIds) { + $query->name('store_coupon_issue')->whereIn('id', function ($q) use ($productIds) { + $q->name('store_coupon_product')->whereIn('product_id', $productIds)->field('coupon_id')->select(); + })->field(['id'])->select(); + })->with('issue')->where(['uid' => $uid, 'status' => 0])->order('coupon_price DESC') + ->where('use_min_price', '<=', $price)->select() + ->where('start_time', '<=', time())->where('end_time', '>=', time()) + ->hidden(['status', 'is_fail'])->toArray(); + } + + /** + * 根据商品id获取 + * @param array $cateIds + * @param int $uid + * @param string $price + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function cateIdsByCoupon(array $cateIds, int $uid, string $price) + { + return $this->getModel()->whereIn('cid', function ($query) use ($cateIds) { + $query->name('store_coupon_issue')->whereIn('category_id', $cateIds)->where('type', 1)->field('id')->select(); + })->where(['uid' => $uid, 'status' => 0])->where('use_min_price', '<=', $price) + ->where('start_time', '<=', time())->where('end_time', '>=', time()) + ->order('coupon_price DESC')->with('issue')->select()->hidden(['status', 'is_fail'])->toArray(); + } + + /** + * 获取当前用户可用的优惠卷 + * @param array $ids + * @param int $uid + * @param string $price + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCouponV1(int $uid, string $price = '0', array $ids = []) + { + return $this->getModel()->where(['uid' => $uid, 'status' => 0])->when(count($ids) != 0, function ($query) use ($ids) { + $query->whereNotIn('id', $ids); + })->whereIn('cid', function ($query) { + $query->name('store_coupon_issue')->where('type', 0)->field(['id'])->select(); + })->when($price, function ($query, $price) { + $query->where('use_min_price', '<=', $price); + }) + ->where('start_time', '<=', time())->where('end_time', '>=', time()) + ->order('coupon_price DESC')->with('issue')->select()->hidden(['status', 'is_fail'])->toArray(); + } + + /** + * 获取当前用户可用的优惠卷 + * @param array $ids + * @param int $uid + * @param string $price + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCoupon(array $ids, int $uid, string $price) + { + return $this->getModel()->where(['uid' => $uid, 'status' => 0])->when(count($ids) != 0, function ($query) use ($ids) { + $query->whereNotIn('id', $ids); + })->whereIn('cid', function ($query) { + $query->name('store_coupon_issue')->where('type', 0)->field(['id'])->select(); + })->where('use_min_price', '<=', $price) + ->where('start_time', '<=', time())->where('end_time', '>=', time()) + ->order('coupon_price DESC')->with('issue')->select()->hidden(['status', 'is_fail'])->toArray();; + } + + /** + * 获取当前用户所有可用的优惠卷 + * @param int $uid + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserAllCoupon(int $uid) + { + return $this->getModel()->where(['uid' => $uid, 'status' => 0, 'is_fail' => 0]) + ->where('start_time', '<=', time())->where('end_time', '>=', time()) + ->order('coupon_price DESC')->with('issue')->select()->hidden(['status', 'is_fail'])->toArray(); + } + + /** + * 获取列表带排序 + * @param array $where + * @param $order + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCouponListByOrder(array $where, $order, int $page = 0, int $limit = 0) + { + if (isset($where['status']) && $where['status'] == 1) $where['status'] = [1, 2]; + return $this->search($where)->with('issue')->when($page > 0 && $limit > 0, function ($qeury) use ($page, $limit) { + $qeury->page($page, $limit); + })->when($order != '', function ($query) use ($order) { + $query->order($order); + })->when(isset($where['coupon_ids']), function ($qeury) use ($where) { + $qeury->whereIn('cid', $where['coupon_ids']); + })->select()->toArray(); + } + + /**根据月份查询用户获得的优惠券 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function memberCouponUserGroupBymonth(array $where) + { + return $this->search($where) + ->whereMonth('add_time') + ->whereIn('cid', $where['couponIds']) + ->field('count(id) as num,FROM_UNIXTIME(add_time, \'%Y-%m\') as time') + ->group("FROM_UNIXTIME(add_time, '%Y-%m')") + ->select()->toArray(); + //echo $this->getModel()->getLastSql();die; + + } + + /**根据时间查询 + * @param array $where + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCounponByMonth(array $where) + { + return $this->search($where)->whereMonth('add_time')->find(); + } + + /** + * 获取本月领取的优惠券 + * @param $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getVipCouponList($uid) + { + return $this->getModel()->where('uid', $uid)->whereMonth('add_time')->select()->toArray(); + } +} diff --git a/app/dao/activity/discounts/StoreDiscountsDao.php b/app/dao/activity/discounts/StoreDiscountsDao.php new file mode 100644 index 0000000..c15da50 --- /dev/null +++ b/app/dao/activity/discounts/StoreDiscountsDao.php @@ -0,0 +1,128 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\activity\discounts; + +use app\dao\BaseDao; +use app\model\activity\discounts\StoreDiscounts; + + +/** + * 优惠套餐 + * Class StoreDiscountsDao + * @package app\dao\activity\discounts + */ +class StoreDiscountsDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreDiscounts::class; + } + + public function search(array $where = []) + { + return parent::search($where) + ->when(isset($where['is_time']) && $where['is_time'] == 1, function ($query) { + $query->where(function ($q) { + $q->where(function ($query) { + $query->where('start_time', '<=', time())->where('stop_time', '>=', time()); + })->whereOr(function ($query) { + $query->where('start_time', 0)->where('stop_time', 0); + }); + }); + }); + } + + /** + * 获取列表 + * @param $where + * @param array $with + * @param $page + * @param $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList($where, array $with = [], $page = 0, $limit = 0) + { + return $this->search($where)->where('is_del', 0) + ->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when($with, function ($query) use ($with) { + $query->with($with); + })->order('sort desc,id desc')->select()->toArray(); + } + + /** + * 优惠套餐列表 + * @param int $product_id + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDiscounts(int $product_id, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search(['product_ids' => $product_id])->field($field) + ->where('is_del', 0)->where('status', 1) + ->where(function ($query) { + $query->where(function ($query) { + $query->where('start_time', '<=', time())->where('stop_time', '>=', time()); + })->whereOr(function ($query) { + $query->where('start_time', 0)->where('stop_time', 0); + }); + })->where(function ($query) { + $query->where(function ($query) { + $query->where('is_limit', 0); + })->whereOr(function ($query) { + $query->where('is_limit', 1)->where('limit_num', '>', 0); + }); + })->with(['products']) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id desc')->select()->toArray(); + } + + /** + * 优惠套餐数量 + * @return int + */ + public function getDiscountsCount() + { + return $this->getModel() + ->where('is_del', 0)->where('status', 1) + ->where(function ($query) { + $query->where(function ($query) { + $query->where('start_time', '<=', time())->where('stop_time', '>=', time()); + })->whereOr(function ($query) { + $query->where('start_time', 0)->where('stop_time', 0); + }); + })->count(); + } + + public function decLimitNum($id, $num = 1) + { + return $this->getModel()->where('id', $id)->dec('limit_num', $num)->update(); + } + + public function incLimitNum(int $id, $num = 1) + { + return $this->getModel()->where('id', $id)->inc('limit_num', $num)->update(); + } +} diff --git a/app/dao/activity/discounts/StoreDiscountsProductsDao.php b/app/dao/activity/discounts/StoreDiscountsProductsDao.php new file mode 100644 index 0000000..ed2bef6 --- /dev/null +++ b/app/dao/activity/discounts/StoreDiscountsProductsDao.php @@ -0,0 +1,62 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\activity\discounts; + +use app\dao\BaseDao; +use app\model\activity\discounts\StoreDiscountsProducts; + +/** + * 优惠套餐商品 + * Class StoreDiscountsProductsDao + * @package app\dao\activity\discounts + */ +class StoreDiscountsProductsDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreDiscountsProducts::class; + } + + /** + * 获取商品列表 + * @param $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList($where) + { + return $this->search($where)->select()->toArray(); + } + + /** + * 获取套餐商品 + * @param int $id + * @param string $field + * @param array $with + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDiscountProductInfo(int $id, string $field, array $with = []) + { + return $this->getModel()->where('id', $id)->when($with, function ($query) use ($with) { + $query->with($with); + })->field($field)->find(); + } +} \ No newline at end of file diff --git a/app/dao/activity/integral/StoreIntegralDao.php b/app/dao/activity/integral/StoreIntegralDao.php new file mode 100644 index 0000000..b1b983b --- /dev/null +++ b/app/dao/activity/integral/StoreIntegralDao.php @@ -0,0 +1,83 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\integral; + +use app\dao\BaseDao; +use app\model\activity\integral\StoreIntegral; + +/** + * 积分商品 + * Class StoreIntegralDao + * @package app\dao\activity\integral + */ +class StoreIntegralDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreIntegral::class; + } + + /** + * 积分商品列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page = 0, int $limit = 0, string $field = '*') + { + return $this->search($where)->where('is_del', 0) + ->when(isset($where['integral_time']) && $where['integral_time'] !== '', function ($query) use ($where) { + [$startTime, $endTime] = explode('-', $where['integral_time']); + $query->where('add_time', '>', strtotime($startTime)) + ->where('add_time', '<', strtotime($endTime) + 24 * 3600); + })->when(isset($where['priceOrder']) && $where['priceOrder'] != '', function ($query) use ($where) { + if ($where['priceOrder'] === 'desc') { + $query->order("integral,price desc"); + } else { + $query->order("integral,price asc"); + } + })->when(isset($where['salesOrder']) && $where['salesOrder'] != '', function ($query) use ($where) { + if ($where['salesOrder'] === 'desc') { + $query->order("sales desc"); + } else { + $query->order("sales asc"); + } + })->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->field($field)->order('sort desc,id desc')->select()->toArray(); + } + + /** + * 获取一条积分商品数据 + * @param int $id + * @param string $field + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function validProduct(int $id, string $field) + { + $where = ['is_show' => 1, 'is_del' => 0]; + return $this->search($where)->where('id', $id)->field($field)->order('add_time desc')->find(); + } +} diff --git a/app/dao/activity/integral/StoreIntegralOrderDao.php b/app/dao/activity/integral/StoreIntegralOrderDao.php new file mode 100644 index 0000000..fccc373 --- /dev/null +++ b/app/dao/activity/integral/StoreIntegralOrderDao.php @@ -0,0 +1,160 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\activity\integral; + + +use app\dao\BaseDao; +use app\model\activity\integral\StoreIntegralOrder; + +/** + * 积分订单 + * Class StoreIntegralOrderDao + * @package app\dao\activity\integral + */ +class StoreIntegralOrderDao extends BaseDao +{ + + /** + * 限制精确查询字段 + * @var string[] + */ + protected $withField = ['uid', 'order_id', 'real_name', 'user_phone', 'store_name']; + + /** + * @return string + */ + protected function setModel(): string + { + return StoreIntegralOrder::class; + } + + /** + * 订单搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + $isDel = isset($where['is_del']) && $where['is_del'] !== '' && $where['is_del'] != -1; + $realName = $where['real_name'] ?? ''; + $fieldKey = $where['field_key'] ?? ''; + $fieldKey = $fieldKey == 'all' ? '' : $fieldKey; + return parent::search($where)->when($isDel, function ($query) use ($where) { + $query->where('is_del', $where['is_del']); + })->when(isset($where['is_system_del']), function ($query) { + $query->where('is_system_del', 0); + })->when(isset($where['paid']) && $where['paid'] == 1, function ($query) { + $query->where('paid', 1); + })->when(isset($where['is_price']) && $where['is_price'] == 1, function ($query) { + $query->where('price', '>', 0); + })->when(isset($where['is_integral']) && $where['is_integral'] == 1, function ($query) { + $query->where('integral', '>', 0); + })->when($realName && $fieldKey && in_array($fieldKey, $this->withField), function ($query) use ($where, $realName, $fieldKey) { + if ($fieldKey !== 'store_name') { + $query->where(trim($fieldKey), trim($realName)); + } else { + $query->whereLike('store_name', '%' . $realName . '%'); + } + })->when($realName && !$fieldKey, function ($query) use ($where) { + $query->where(function ($que) use ($where) { + $que->whereLike('order_id|real_name|store_name', '%' . $where['real_name'] . '%')->whereOr('uid', 'in', function ($q) use ($where) { + $q->name('user')->whereLike('nickname|uid|phone', '%' . $where['real_name'] . '%')->field(['uid'])->select(); + }); + }); + }); + } + + /** + * 订单搜索列表 + * @param array $where + * @param array $field + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderList(array $where, array $field, int $page, int $limit, array $with = []) + { + return $this->search($where)->field($field)->with(array_merge(['user'], $with))->page($page, $limit)->order('add_time DESC,id DESC')->select()->toArray(); + } + + /** + * 根据条件获取订单列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getIntegralOrderList(array $where) + { + return $this->search($where)->order('id asc')->select()->toArray(); + } + + /** + * 获取订单总数 + * @param array $where + * @return int + */ + public function count(array $where = []): int + { + return $this->search($where)->count(); + } + + /** + * 查找指定条件下的订单数据以数组形式返回 + * @param array $where + * @param string $field + * @param string $key + * @param string $group + * @return array + */ + public function column(array $where, string $field, string $key = '', string $group = '') + { + return $this->search($where)->when($group, function ($query) use ($group) { + $query->group($group); + })->column($field, $key); + } + + /** + * 获取订单详情 + * @param $uid + * @param $key + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserOrderDetail(string $key, int $uid) + { + return $this->getOne(['order_id' => $key, 'uid' => $uid, 'is_del' => 0]); + } + + /** + * 获取用户已购买此活动商品的个数 + * @param $uid + * @param $productId + * @return int + */ + public function getBuyCount($uid, $productId): int + { + return $this->getModel() + ->where('uid', $uid) + ->where('is_del', 0) + ->where('product_id', $productId) + ->value('sum(total_num)') ?? 0; + } + +} diff --git a/app/dao/activity/integral/StoreIntegralOrderStatusDao.php b/app/dao/activity/integral/StoreIntegralOrderStatusDao.php new file mode 100644 index 0000000..8cb7726 --- /dev/null +++ b/app/dao/activity/integral/StoreIntegralOrderStatusDao.php @@ -0,0 +1,49 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\activity\integral; + + +use app\dao\BaseDao; +use app\model\activity\integral\StoreIntegralOrderStatus; + +/** + * 积分订单状态 + * Class StoreIntegralOrderStatusDao + * @package app\dao\activity\integral + */ +class StoreIntegralOrderStatusDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreIntegralOrderStatus::class; + } + + /** + * 获取订单状态列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStatusList(array $where, int $page, int $limit) + { + return $this->search($where)->page($page, $limit)->select()->toArray(); + } + +} diff --git a/app/dao/activity/lottery/LuckLotteryDao.php b/app/dao/activity/lottery/LuckLotteryDao.php new file mode 100644 index 0000000..1ae54d3 --- /dev/null +++ b/app/dao/activity/lottery/LuckLotteryDao.php @@ -0,0 +1,124 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\lottery; + +use app\dao\BaseDao; +use app\model\activity\lottery\LuckLottery; + +/** + * 抽奖活动 + * Class LuckLotteryDao + * @package app\dao\activity\lottery + */ +class LuckLotteryDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return LuckLottery::class; + } + + public function search(array $data = []) + { + return parent::search($data)->when(isset($data['id']) && $data['id'], function ($query) use ($data) { + $query->where('id', $data['id']); + })->when(isset($data['start']) && $data['start'] !== '', function ($query) use ($data) { + $time = time(); + switch ($data['start']) { + case 0: + $query->where('start_time', '>', $time)->where('status', 1); + break; + case -1: + $query->where(function ($query1) use ($time) { + $query1->where('end_time', '<', $time)->whereOr('status', 0); + }); + break; + case 1: + $query->where('status', 1)->where(function ($query1) use ($time) { + $query1->where(function ($query2) use ($time) { + $query2->where('start_time', '<=', $time)->where('end_time', '>=', $time); + })->whereOr(function ($query3) { + $query3->where('start_time', 0)->where('end_time', 0); + }); + }); + break; + } + }); + } + + /** + * 抽奖活动列表 + * @param array $where + * @param string $field + * @param array $with + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', array $with = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc')->select()->toArray(); + } + + /** + * 获取单个活动 + * @param int $id + * @param string $field + * @param array|string[] $with + * @param bool $is_doing + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLottery(int $id, string $field = '*', array $with = ['prize'], bool $is_doing = false) + { + $where = ['id' => $id]; + $where['is_del'] = 0; + if ($is_doing) $where['start'] = 1; + return $this->search($where)->field($field)->when($with, function ($query) use ($with) { + $query->with($with); + })->find(); + } + + /** + * 获取某个抽奖类型的一条抽奖数据 + * @param int $factor + * @param string $field + * @param array|string[] $with + * @param bool $is_doing + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFactorLottery(int $factor = 1, string $field = '*', array $with = ['prize'], bool $is_doing = true) + { + $where = ['factor' => $factor, 'is_del' => 0]; + if ($is_doing) $where['start'] = 1; + return $this->search($where)->field($field)->when($with, function ($query) use ($with) { + $query->with($with); + })->order('id desc')->find(); + } +} diff --git a/app/dao/activity/lottery/LuckLotteryRecordDao.php b/app/dao/activity/lottery/LuckLotteryRecordDao.php new file mode 100644 index 0000000..6314e7f --- /dev/null +++ b/app/dao/activity/lottery/LuckLotteryRecordDao.php @@ -0,0 +1,69 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\lottery; + +use app\dao\BaseDao; +use app\model\activity\lottery\LuckLotteryRecord; + +/** + * 中奖记录 + * Class LuckLotteryRecordDao + * @package app\dao\activity\lottery + */ +class LuckLotteryRecordDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return LuckLotteryRecord::class; + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param array $with + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, $field = '*', array $with = [], int $page = 0, int $limit = 10) + { + return $this->search($where)->when($with, function ($query) use ($with) { + $query->with($with); + })->field($field)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc')->select()->toArray(); + } + + /** + * @param array $where + * @param string $group + * @return int + */ + public function getCount(array $where, string $group = '') + { + return parent::search($where)->when(isset($where['add_time']) && $where['add_time'], function ($query) use ($where) { + $query->where('add_time','>=', $where['add_time']); + })->when($group, function ($query) use ($group) { + $query->group($group); + })->count(); + } +} diff --git a/app/dao/activity/newcomer/StoreNewcomerDao.php b/app/dao/activity/newcomer/StoreNewcomerDao.php new file mode 100644 index 0000000..7a4c46b --- /dev/null +++ b/app/dao/activity/newcomer/StoreNewcomerDao.php @@ -0,0 +1,88 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\newcomer; + +use app\dao\BaseDao; +use app\model\activity\newcomer\StoreNewcomer; + + +/** + * 新人礼商品 + * Class StoreNewcomerDao + * @package app\dao\activity\newcomer + */ +class StoreNewcomerDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreNewcomer::class; + } + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + protected function search(array $where = []) + { + return parent::search($where)->when(isset($where['storeProductId']), function ($query) { + $query->where('product_id', 'IN', function ($query) { + $query->name('store_product')->where('is_show', 1)->where('is_del', 0)->field('id'); + }); + }); + } + + /** + * 新人专享商品列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, array $with = []) + { + return $this->search($where)->field($field) + ->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when($with, function ($query) use ($with) { + $query->with($with); + })->when(isset($where['priceOrder']) && $where['priceOrder'] != '', function ($query) use ($where) { + if ($where['priceOrder'] === 'desc') { + $query->order("price desc"); + } else { + $query->order("price asc"); + } + })->when(isset($where['salesOrder']) && $where['salesOrder'] != '', function ($query) use ($where) { + if ($where['salesOrder'] === 'desc') { + $query->order("sales desc"); + } else { + $query->order("sales asc"); + } + })->order('id desc')->select()->toArray(); + } + + + + + +} diff --git a/app/dao/activity/promotions/StorePromotionsAuxiliaryDao.php b/app/dao/activity/promotions/StorePromotionsAuxiliaryDao.php new file mode 100644 index 0000000..1c395b2 --- /dev/null +++ b/app/dao/activity/promotions/StorePromotionsAuxiliaryDao.php @@ -0,0 +1,130 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\activity\promotions; + + +use app\dao\BaseDao; +use app\model\activity\promotions\StorePromotions; +use app\model\activity\promotions\StorePromotionsAuxiliary; + + +/** + * 优惠活动辅助表 + */ +class StorePromotionsAuxiliaryDao extends BaseDao +{ + + /** + * @return string + */ + protected function setModel(): string + { + return StorePromotionsAuxiliary::class; + } + + public function joinModel(): string + { + return StorePromotions::class; + } + + /** + * 获取所有的分销员等级 + * @param array $where + * @param string $field + * @param array $with + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where = [], string $field = '*', array $with = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field) + ->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 关联模型 + * @param string $alias + * @param string $join_alias + * @return \crmeb\basic\BaseModel + */ + public function getJoinModel(string $alias = 'a', string $join_alias = 'p', $join = 'left') + { + $this->alias = $alias; + $this->joinAlis = $join_alias; + /** @var StorePromotions $storePromotions */ + $storePromotions = app()->make($this->joinModel()); + $table = $storePromotions->getName(); + return parent::getModel()->alias($alias)->join($table . ' ' . $join_alias, $alias . '.promotions_id = ' . $join_alias . '.id', $join); + } + + /** + * 获取ids + * @param array $product_id + * @param int $product_partake_type + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductsPromotionsIds(array $product_id, int $product_partake_type = 2, string $field = '') + { + $time = time(); + return $this->getJoinModel()->field($field) + ->where($this->joinAlis . '.type', 1) + ->where($this->joinAlis . '.status', 1) + ->where($this->joinAlis . '.is_del', 0) + ->where($this->joinAlis . '.start_time', '<=', $time) + ->where($this->joinAlis . '.stop_time', '>=', $time) + ->where($this->alias . '.type', 1) + ->when(in_array($product_partake_type, [2, 3]), function ($query) use ($product_partake_type, $product_id) { + if ($product_partake_type == 2) { + $query->where(function ($q) use ($product_id) { + $q->whereOr( $this->joinAlis.'.product_partake_type', 1) + ->whereOr(function ($w) use ($product_id) { + $w->where($this->joinAlis.'.product_partake_type', 2)->where(function ($p) use ($product_id) { + if (is_array($product_id)) { + $p->whereIn($this->alias.'.product_id', $product_id); + } else { + $p->where($this->alias.'.product_id', $product_id); + } + }); + })->whereOr(function ($e) use ($product_id) { + $e->where($this->joinAlis.'.product_partake_type', 3)->where($this->alias.'.is_all', 1)->where(function ($p) use ($product_id) { + if (is_array($product_id)) { + $p->whereNotIn($this->alias.'.product_id', $product_id); + } else { + $p->where($this->alias.'.product_id', '<>', $product_id); + } + }); + }); + }); + } else { + $query->where($this->alias.'.product_partake_type', 3)->where($this->alias.'.is_all', 1)->where(function ($p) use ($product_id){ + if(is_array($product_id)){ + $p->whereNotIn($this->alias.'.product_id', $product_id); + }else{ + $p->where($this->alias.'.product_id', $product_id); + } + }); + } + })->select()->toArray(); + } +} diff --git a/app/dao/activity/promotions/StorePromotionsDao.php b/app/dao/activity/promotions/StorePromotionsDao.php new file mode 100644 index 0000000..bdedce5 --- /dev/null +++ b/app/dao/activity/promotions/StorePromotionsDao.php @@ -0,0 +1,155 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\promotions; + +use app\dao\BaseDao; +use app\model\activity\promotions\StorePromotions; + +/** + * 促销活动 + * Class StorePromotionsDao + * @package app\dao\activity\promotions + */ +class StorePromotionsDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StorePromotions::class; + } + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + protected function search(array $where = []) + { + return parent::search($where)->when(isset($where['promotionsTime']), function ($query) use ($where) { + [$startTime, $stopTime] = is_array($where['promotionsTime']) ? $where['promotionsTime'] : [time(), time()]; + $query->where('start_time', '<=', $startTime)->where('stop_time', '>=', $stopTime); + })->when(isset($where['ids']) && $where['ids'], function($query) use($where) { + $query->whereIn('id', $where['ids']); + })->when(isset($where['not_ids']) && $where['not_ids'], function($query) use($where) { + $query->whereNotIn('id', $where['not_ids']); + })->when(isset($where['applicable_type']) && $where['applicable_type'], function($query) use($where) { + $query->whereIn('applicable_type', $where['applicable_type']); + })->when(isset($where['start_status']) && $where['start_status'] !== '', function ($query) use ($where) { + $time = time(); + switch ($where['start_status']) { + case -1: + $query->where(function ($q) use ($time) { + $q->where('stop_time', '<', $time)->whereOr('status', '0'); + }); + break; + case 0: + $query->where('start_time', '>', $time)->where('status', 1); + break; + case 1: + $query->where('start_time', '<=', $time)->where('stop_time', '>=', $time)->where('status', 1); + break; + } + })->when(isset($where['product_id']) && $where['product_id'], function ($query) use ($where) { + $query->where(function ($q) use ($where) { + $q->whereOr('product_partake_type', 1) + ->whereOr(function ($w) use ($where) { + $w->where('product_partake_type', 2)->whereIn('id', function ($a) use ($where) { + $a->name('store_promotions_auxiliary')->field('promotions_id')->where('type', 1)->where('product_partake_type', 2)->where(function ($p) use ($where) { + if(is_array($where['product_id'])){ + $p->whereIn('product_id', $where['product_id']); + } else { + $p->where('product_id', $where['product_id']); + } + }); + }); + })->whereOr(function ($b) use ($where) { + $b->where('product_partake_type', 4)->whereIn('id', function ($a) use ($where) { + $a->name('store_promotions_auxiliary')->field('promotions_id')->where('type', 1)->where('product_partake_type', 2)->where(function ($p) use ($where) { + if(is_array($where['product_id'])){ + $p->whereIn('product_id', $where['product_id']); + } else { + $p->where('product_id', $where['product_id']); + } + }); + }); + }); + }); + }); + } + + /** + * 获取促销活动列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array $with + * @param string $order + * @param string $group + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, array $with = [], string $order = 'update_time desc,id desc', string $group = '') + { + return $this->search($where)->field($field) + ->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when($with, function ($query) use ($with) { + $query->with($with); + })->order($order)->when($group, function($query) use($group) { + $query->group($group); + })->select()->toArray(); + } + + + /** + * 获取一条活动 + * @param int $id + * @param string $field + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function validPromotions(int $id, string $field = '*') + { + $where = ['status' => 1, 'is_del' => 0]; + $time = time(); + return $this->search($where) + ->where('id', $id) + ->where('start_time', '<=', $time) + ->where('stop_time', '>=', $time) + ->field($field)->find(); + } + + /** + * 获取一个活动包含子集的所有ID + * @param int $id + * @return array + */ + public function getOnePromotionsIds(int $id) + { + $result = $this->getModel()->where('id|pid', $id)->column('id'); + $res = []; + if ($result) { + $res = array_column($result, 'id'); + } + return $res; + } +} diff --git a/app/dao/activity/seckill/StoreSeckillDao.php b/app/dao/activity/seckill/StoreSeckillDao.php new file mode 100644 index 0000000..a6a58f5 --- /dev/null +++ b/app/dao/activity/seckill/StoreSeckillDao.php @@ -0,0 +1,218 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\seckill; + +use app\dao\BaseDao; +use app\model\activity\seckill\StoreSeckill; + +/** + * 秒杀商品 + * Class StoreSeckillDao + * @package app\dao\activity\seckill + */ +class StoreSeckillDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreSeckill::class; + } + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + protected function search(array $where = []) + { + return parent::search($where)->when(isset($where['seckllTime']), function ($query) use ($where) { + [$startTime, $stopTime] = is_array($where['seckllTime']) ? $where['seckllTime'] : [time(), time() - 86400]; + $query->where('start_time', '<=', $startTime)->where('stop_time', '>=', $stopTime); + })->when(isset($where['storeProductId']), function ($query) { + $query->where('product_id', 'IN', function ($query) { + $query->name('store_product')->where('is_show', 1)->where('is_del', 0)->field('id'); + }); + })->when(isset($where['start_status']) && $where['start_status'] !== '', function ($query) use ($where) { + $time = time(); + switch ($where['start_status']) { + case -1: + $query->where(function ($q) use ($time) { + $q->where('stop_time', '<', $time - 86400)->whereOr('status', '0'); + }); + break; + case 0: + $query->where('start_time', '>', $time)->where('status', 1); + break; + case 1: + $query->where('start_time', '<=', $time)->where('stop_time', '>=', $time - 86400)->where('status', 1); + break; + } + }); + } + + /** + * 获取某个活动下的秒杀商品ids + * @param array $where + * @return array + */ + public function getActivitySeckillIds(array $where) + { + return $this->search($where)->column('id'); + } + + /** + * 获取秒杀列表 + * @param array $where + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page = 0, int $limit = 0, array $with = []) + { + return $this->search($where) + ->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id desc')->select()->toArray(); + } + + /** + * 根据商品id获取当前正在开启秒杀产品的列表以数组返回 + * @param array $ids + * @param array $field + * @return array + */ + public function getSeckillIdsArray(array $ids, array $field = []) + { + return $this->search(['is_del' => 0, 'status' => 1]) + ->where('start_time', '<=', time()) + ->where('stop_time', '>=', time() - 86400) + ->whereIn('product_id', $ids) + ->field($field)->select()->toArray(); + } + + /** + * 获取某个时间段的秒杀列表 + * @param array $activity_id + * @param array $ids + * @param int $page + * @param int $limit + * @param bool $isStore + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getListByTime(array $activity_id, array $ids = [], int $page = 0, int $limit = 0, bool $isStore = false) + { + if ($activity_id == 0) return []; + return $this->search(['is_del' => 0, 'status' => 1]) + ->where('start_time', '<=', time()) + ->where('stop_time', '>=', time() - 86400) + ->when($activity_id, function ($query) use ($activity_id) { + $query->whereIn('activity_id', $activity_id); + })->where('product_id', 'IN', function ($query) { + $query->name('store_product')->where('is_show', 1)->where('is_del', 0)->field('id'); + })->when($ids, function($query) use($ids) { + $query->whereIn('product_id', $ids); + })->when($isStore, function($query) { + $query->where(function ($q) { + $q->whereOr(function ($c) { + $c->whereFindInSet('delivery_type', 2); + })->whereOr(function ($d) { + $d->whereFindInSet('delivery_type', 3); + }); + }); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id desc')->select()->toArray(); + } + + /** + * 获取正在开启的秒杀总数 + * @param array $activity_id + * @param $ids + * @param $not_ids + * @param bool $isStore + * @return int + * @throws \think\db\exception\DbException + */ + public function getTimeCount(array $activity_id, $ids = [], $not_ids = [], bool $isStore = true) + { + if ($activity_id == 0) return 0; + return $this->search(['is_del' => 0, 'status' => 1]) + ->where('start_time', '<=', time()) + ->where('stop_time', '>=', time() - 86400) + ->when($activity_id, function ($query) use ($activity_id) { + $query->whereIn('activity_id', $activity_id); + })->where('product_id', 'IN', function ($query) { + $query->name('store_product')->where('is_show', 1)->where('is_del', 0)->field('id'); + })->when($ids, function($query) use($ids) { + $query->whereIn('product_id', $ids); + })->when($not_ids, function($query) use($not_ids) { + $query->whereNotIn('product_id', $not_ids); + })->when($isStore, function($query) { + $query->where(function ($q) { + $q->whereOr(function ($c) { + $c->whereFindInSet('delivery_type', 2); + })->whereOr(function ($d) { + $d->whereFindInSet('delivery_type', 3); + }); + }); + })->count(); + } + + /** + * 根据id获取秒杀数据 + * @param array $ids + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function idSeckillList(array $ids, string $field) + { + return $this->getModel()->whereIn('id', $ids)->field($field)->select()->toArray(); + } + + /**获取一条秒杀商品 + * @param $id + * @param $field + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function validProduct($id, $field) + { + $where = ['status' => 1, 'is_del' => 0]; + $time = time(); + return $this->search($where) + ->where('id', $id) + ->where('start_time', '<', $time) + ->where('stop_time', '>', $time - 86400) + ->field($field)->with(['product'])->find(); + } + + +} diff --git a/app/dao/activity/seckill/StoreSeckillTimeDao.php b/app/dao/activity/seckill/StoreSeckillTimeDao.php new file mode 100644 index 0000000..d9b4b3d --- /dev/null +++ b/app/dao/activity/seckill/StoreSeckillTimeDao.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\activity\seckill; + +use app\dao\BaseDao; +use app\common\model\store\StoreSeckillTime; + +/** + * 秒杀时间 + * Class StoreSeckillTimeDao + * @package app\dao\activity\seckill + */ +class StoreSeckillTimeDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreSeckillTime::class; + } + + /** + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where) + ->when(isset($where['start_time']) && $where['start_time'] !== '', function ($query) use ($where) { + $query->whereTime('start_time', '<=', intval($where['start_time'])); + }) + ->when(isset($where['end_time']) && $where['end_time'] !== '', function ($query) use ($where) { + $query->whereTime('end_time', '>=', intval($where['end_time'])); + }); + } + + + /** + * 获取秒杀时间列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param string $order + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, string $order = 'start_time asc,id desc') + { + return $this->search($where)->field($field) + ->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order($order)->select()->toArray(); + } + + + /** + * 开始时间 在别的时间段中 + * @param $time + * @param $id + * @return int + * @throws \think\db\exception\DbException + */ + public function valStartTime($time, $id) + { + return $this->getModel() + ->when($id, function ($query) use ($id) { + $query->where('id', '<>', $id); + })->where('start_time', '<=', $time)->where('end_time', '>', $time)->count(); + } + + /** + * 结束时间在别的时间段中 + * @param $time + * @param $id + * @return int + * @throws \think\db\exception\DbException + */ + public function valEndTime($time, $id) + { + return $this->getModel() + ->when($id, function ($query) use ($id) { + $query->where('id', '<>', $id); + })->where('start_time', '<', $time)->where('end_time', '>=', $time)->count(); + } + + /** + * 时间段包含了别的时间段 + * @param array $data + * @param $id + * @return int + * @throws \think\db\exception\DbException + */ + public function valAllTime(array $data, $id) + { + return $this->getModel() + ->when($id, function ($query) use ($id) { + $query->where('id', '<>', $id); + })->where('start_time', '>', $data['start_time'])->where('end_time', '<=', $data['end_time'])->count(); + } + + + +} diff --git a/app/dao/activity/table/TableQrcodeDao.php b/app/dao/activity/table/TableQrcodeDao.php new file mode 100644 index 0000000..af93107 --- /dev/null +++ b/app/dao/activity/table/TableQrcodeDao.php @@ -0,0 +1,80 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\activity\table; + + +use app\dao\BaseDao; +use app\model\activity\table\TableQrcode; + +/** + * 桌码 + * Class TableQrcodeDao + * @package app\dao\activity\table + */ +class TableQrcodeDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return TableQrcode::class; + } + + /**条件处理 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['store_id']) && $where['store_id'], function ($query) use ($where) { + $query->where('store_id', $where['store_id']); + })->when(isset($where['cate_id']) && $where['cate_id'], function ($query) use ($where) { + $query->where('cate_id', $where['cate_id']); + })->when(isset($where['is_del']), function ($query) use ($where) { + $query->where('is_del', $where['is_del']); + }); + } + + /**获取桌码列表 + * @param array $where + * @param int $storeId + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page, int $limit,array $with) + { + return $this->search($where)->when(count($with), function ($query) use ($with) { + $query->with($with); + })->page($page,$limit)->order('add_time Asc')->select()->toArray(); + } + + /**获取座位信息 + * @param array $where + * @param array $with + * @return array|\crmeb\basic\BaseModel|mixed|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTableCodeOne(array $where, array $with) + { + return $this->getModel()->where($where)->when(count($with), function ($query) use ($with) { + $query->with($with); + })->find(); + } +} diff --git a/app/dao/agent/AgentLevelDao.php b/app/dao/agent/AgentLevelDao.php new file mode 100644 index 0000000..6f162f3 --- /dev/null +++ b/app/dao/agent/AgentLevelDao.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\agent; + +use app\dao\BaseDao; +use app\model\agent\AgentLevel; + +/** + * Class AgentLevelDao + * @package app\dao\agent + */ +class AgentLevelDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return AgentLevel::class; + } + + /** + * 获取所有的分销员等级 + * @param array $where + * @param string $field + * @param array $with + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where = [], string $field = '*', array $with = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('grade asc,id desc')->select()->toArray(); + } +} diff --git a/app/dao/diy/DiyDao.php b/app/dao/diy/DiyDao.php new file mode 100644 index 0000000..8016742 --- /dev/null +++ b/app/dao/diy/DiyDao.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\diy; + +use app\dao\BaseDao; +use app\common\model\system\diy\Diy; + +/** + * + * Class DiyDao + * @package app\dao\diy + */ +class DiyDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return Diy::class; + } + + /** + * 获取DIY列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDiyList(array $where, int $page, int $limit, array $field = ['*']) + { + return $this->search($where)->field($field)->where('is_del', 0)->page($page, $limit)->order('status desc,id desc')->select()->toArray(); + } + + +} diff --git a/app/dao/message/SystemMessageDao.php b/app/dao/message/SystemMessageDao.php new file mode 100644 index 0000000..3557c37 --- /dev/null +++ b/app/dao/message/SystemMessageDao.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\message; + +use app\dao\BaseDao; +use app\model\message\SystemMessage; + +/** + * 站内信 + * Class SystemMessageDao + * @package app\dao\message + */ +class SystemMessageDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SystemMessage::class; + } + + /** + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMessageList(array $where, string $field = '*', int $page = 0, $limit = 0) + { + return $this->getModel()->where($where)->field($field)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc')->select()->toArray(); + } +} diff --git a/app/dao/message/service/StoreServiceAuxiliaryDao.php b/app/dao/message/service/StoreServiceAuxiliaryDao.php new file mode 100644 index 0000000..0857735 --- /dev/null +++ b/app/dao/message/service/StoreServiceAuxiliaryDao.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\message\service; + + +use app\dao\other\queue\QueueAuxiliaryDao; + +/** + * 客服辅助表 + * Class StoreServiceAuxiliaryDao + * @package app\dao\message\service + */ +class StoreServiceAuxiliaryDao extends QueueAuxiliaryDao +{ + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + protected function search(array $where = []) + { + return parent::search($where)->where('type', 0); + } + +} + diff --git a/app/dao/message/service/StoreServiceDao.php b/app/dao/message/service/StoreServiceDao.php new file mode 100644 index 0000000..ed04320 --- /dev/null +++ b/app/dao/message/service/StoreServiceDao.php @@ -0,0 +1,85 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\message\service; + +use app\dao\BaseDao; +use app\common\model\store\service\StoreService; + +/** + * 客服dao + * Class StoreServiceDao + * @package app\dao\message\service + */ +class StoreServiceDao extends BaseDao +{ + + /** + * 不存在的用户直接禁止掉 + * @param array $uids + * @return bool + */ + public function deleteNonExistentService(array $uids = []) + { + if ($uids) { + return $this->getModel()->whereIn('uid', $uids)->update(['status' => 0]); + } else { + return true; + } + } + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreService::class; + } + + /** + * 获取客服列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getServiceList(array $where, int $page, int $limit) + { + return $this->search($where)->with('user')->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(isset($where['noId']), function ($query) use ($where) { + $query->whereNotIn('uid', $where['noId']); + })->with(['workMember' => function ($query) { + $query->field(['uid', 'name', 'position', 'qr_code', 'external_position']); + }])->order('id DESC')->field('id,uid,avatar,nickname as wx_name,status,add_time,phone,account_status')->select()->toArray(); + } + + /** + * 获取接受通知的客服 + * @param int $customer + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreServiceOrderNotice(int $customer = 0, string $field = 'nickname,phone,uid,customer') + { + return $this->getModel()->where(['account_status' => 1, 'status' => 1, 'notify' => 1])->when($customer, function ($query) use ($customer) { + $query->where('customer', $customer); + })->field($field)->select()->toArray(); + } + +} diff --git a/app/dao/message/service/StoreServiceLogDao.php b/app/dao/message/service/StoreServiceLogDao.php new file mode 100644 index 0000000..312c766 --- /dev/null +++ b/app/dao/message/service/StoreServiceLogDao.php @@ -0,0 +1,142 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\message\service; + +use app\dao\BaseDao; +use app\common\model\store\service\StoreServiceLog; + + +/** + * + * Class StoreServiceLogDao + * @package app\dao\service + */ +class StoreServiceLogDao extends BaseDao +{ + + /** + * StoreServiceLogDao constructor. + */ + public function __construct() + { + //清楚去年的聊天记录 +// $this->removeChat(); + $this->removeYesterDayChat(); + } + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreServiceLog::class; + } + + /** + * 获取聊天记录下的uid和to_uid + * @param int $uid + * @return mixed + */ + public function getServiceUserUids(int $uid) + { + return $this->search(['uid' => $uid])->group('uid,to_uid')->field(['uid', 'to_uid'])->select()->toArray(); + } + + /** + * 获取聊天记录并分页 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getServiceList(array $where, int $page, int $limit, array $field = ['*']) + { + return $this->search($where)->with('user')->field($field)->order('add_time DESC')->page($page, $limit)->select()->toArray(); + } + + /** + * 获取聊天记录上翻页 + * @param array $where + * @param int $limit + * @param int $upperId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getChatList(array $where, int $limit = 20, int $upperId = 0) + { + return $this->search($where)->when($upperId, function ($query) use ($upperId, $limit) { + $query->where('id', '<', $upperId)->limit($limit)->order('id DESC'); + })->when(!$upperId, function ($query) use ($limit) { + $query->limit($limit)->order('id DESC'); + })->with(['user', 'service'])->select()->toArray(); + } + + /** + * 清楚去年的聊天记录 + * @return bool + */ + public function removeChat() + { + return $this->search(['time' => 'last year'])->delete(); + } + + /** + * 清楚上周的游客用户聊天记录 + * @return bool + */ + public function removeYesterDayChat() + { + return $this->search(['time' => 'last week', 'is_tourist' => 1])->delete(); + } + + + /** + * 根据条件获取条数 + * @param array $where + * @return int + */ + public function whereByCount(array $where) + { + return $this->search(['uid' => $where['uid']])->order('id DESC')->where('add_time', '<', time() - 300)->count(); + } + + /** + * 获取未读消息条数 + * @param array $where + * @return int + */ + public function getMessageNum(array $where) + { + return $this->getModel()->where($where)->count(); + } + + /** + * 搜索聊天记录 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMessageList(array $where) + { + return $this->search(['chat' => $where['chat']])->when(isset($where['add_time']) && $where['add_time'], function ($query) use ($where) { + $query->where('add_time', '>', $where['add_time']); + })->select()->toArray(); + } +} diff --git a/app/dao/message/service/StoreServiceRecordDao.php b/app/dao/message/service/StoreServiceRecordDao.php new file mode 100644 index 0000000..d6108a2 --- /dev/null +++ b/app/dao/message/service/StoreServiceRecordDao.php @@ -0,0 +1,81 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\message\service; + + +use app\dao\BaseDao; +use app\model\message\service\StoreServiceRecord; + +/** + * Class StoreServiceRecordDao + * @package app\dao\message\service + */ +class StoreServiceRecordDao extends BaseDao +{ + + /** + * StoreServiceRecordDao constructor. + */ + public function __construct() + { + $this->deleteWeekRecord(); + } + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreServiceRecord::class; + } + + /** + * 删除上周游客记录 + */ + protected function deleteWeekRecord() + { + $this->search(['time' => 'last week', 'timeKey' => 'update_time', 'is_tourist' => 1])->delete(); + } + + /** + * 获取客服聊天用户列表 + * @param array $where + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getServiceList(array $where, int $page, int $limit, array $with = []) + { + return $this->search($where)->page($page, $limit)->when(count($with), function ($query) use ($with) { + $query->with($with); + })->order('update_time desc')->select()->toArray(); + } + + /** + * 查询最近和用户聊天的uid用户 + * @param array $where + * @param string $key + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLatelyMsgUid(array $where, string $key) + { + return $this->search($where)->order('update_time DESC')->value($key); + } +} diff --git a/app/dao/order/OtherOrderDao.php b/app/dao/order/OtherOrderDao.php new file mode 100644 index 0000000..2d66d53 --- /dev/null +++ b/app/dao/order/OtherOrderDao.php @@ -0,0 +1,200 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\order; + + +use app\dao\BaseDao; +use app\model\order\OtherOrder; + +class OtherOrderDao extends BaseDao +{ + /** 设置模型 + * @return string + */ + protected function setModel(): string + { + return OtherOrder::class; + } + + /** + * 重写搜索器 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['name']) && $where['name'], function ($query) use ($where) { + $query->where('uid', 'in', function ($que) use ($where) { + $que->name('user')->where('nickname|real_name|phone', 'like', '%' . trim($where['name']) . '%')->field(['uid'])->select(); + }); + }); + } + + /** + * 获取某个时间点一共有多少用户是付费会员状态 + * @param $time + * @param string $channel_type + * @return int|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPayUserCount(int $time, string $channel_type = '') + { + return $this->getModel()->when($channel_type != '', function ($query) use ($channel_type) { + $query->where('channel_type', $channel_type); + })->field('distinct(uid),add_time') + ->group('uid')->having('add_time < ' . $time) + ->order('add_time desc') + ->select()->toArray(); + } + + /** + * 获取VIP曲线 + * @param $time + * @param $type + * @param $timeType + * @param string $str + * @return mixed + */ + public function getTrendData($time, $type, $timeType, $str = 'count(uid)') + { + return $this->getModel()->when($type != '', function ($query) use ($type) { + $query->where('channel_type', $type); + })->where(function ($query) use ($time) { + if ($time[0] == $time[1]) { + $query->whereDay('add_time', $time[0]); + } else { + $time[1] = date('Y/m/d', strtotime($time[1]) + 86400); + $query->whereTime('add_time', 'between', $time); + } + })->field("FROM_UNIXTIME(add_time,'$timeType') as days," . $str . " as num") + ->group('days')->select()->toArray(); + } + + /**合计某字段值 + * @param array $where + * @param string $sumField + * @return float + */ + public function getWhereSumField(array $where, string $sumField) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where) { + $query->whereBetweenTime('pay_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + }) + ->sum($sumField); + } + + /**根据某字段分组查询 + * @param array $where + * @param string $field + * @param string $group + * @return mixed + */ + public function getGroupField(array $where, string $field, string $group) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where, $field, $group) { + $query->whereBetweenTime('pay_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + if ($where['timeKey']['days'] == 1) { + $timeUinx = "%H"; + } elseif ($where['timeKey']['days'] == 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] == 365) { + $timeUinx = "%Y-%m"; + } elseif ($where['timeKey']['days'] > 1 && $where['timeKey']['days'] < 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] > 30 && $where['timeKey']['days'] < 365) { + $timeUinx = "%Y-%m"; + } else { + $timeUinx = "%Y-%m"; + } + $query->field("sum($field) as number,FROM_UNIXTIME($group, '$timeUinx') as time"); + $query->group("FROM_UNIXTIME($group, '$timeUinx')"); + }) + ->order('add_time ASC')->select()->toArray(); + } + + /**根据条件获取单条信息 + * @param array $where + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + + public function getOneByWhere(array $where) + { + return $this->getModel()->where($where)->find(); + } + + /**收银订单 + * @param array $where + * @param int $page + * @param int $limit + * @param string $order + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getScanOrderList(array $where = [], int $page = 0, int $limit = 0, string $order = '') + { + foreach ($where as $k => $v) { + if ($v == "") unset($where[$k]); + } + return $this->search($where) + ->order(($order ? $order . ' ,' : '') . 'id desc') + ->page($page, $limit)->select()->toArray(); + } + + /** + * 获取会员记录 + * @param array $where + * @param string $field + * @param array|string[] $with + * @param int $page + * @param int $limit + * @param string $order + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMemberRecord(array $where = [], string $field = '*', array $with = ['user'], int $page = 0, int $limit = 0, string $order = '') + { + return $this->search($where)->field($field) + ->when($with, function ($query) use ($with) { + $query->with($with); + })->order(($order ? $order . ' ,' : '') . 'id desc') + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 店员统计 + * @param array $where + * @param string $countField + * @param string $sumField + * @param string $groupField + * @return mixed + */ + public function preStaffTotal(array $where, string $countField = 'uid', string $sumField = 'pay_price', string $groupField = 'staff_id') + { + return $this->search($where) + ->field($groupField . ",count(" . $countField . ") as count,sum(`" . $sumField . "`) as price") + ->group($groupField) + ->select()->toArray(); + } +} diff --git a/app/dao/order/StoreCartDao.php b/app/dao/order/StoreCartDao.php new file mode 100644 index 0000000..49cb51d --- /dev/null +++ b/app/dao/order/StoreCartDao.php @@ -0,0 +1,257 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\order; + +use app\dao\BaseDao; +use app\model\order\StoreCart; + +/** + * + * Class StoreCartDao + * @package app\dao\order + */ +class StoreCartDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreCart::class; + } + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['id']) && $where['id'], function ($query) use ($where) { + $query->whereIn('id', $where['id']); + })->when(isset($where['status']), function ($query) use ($where) { + $query->where('status', $where['status']); + }); + } + + /** + * @param array $where + * @param array $unique + * @return array + */ + public function getUserCartNums(array $where, array $unique) + { + return $this->search($where)->whereIn('product_attr_unique', $unique)->column('cart_num', 'product_attr_unique'); + } + + /** + * 获取挂单数据 + * @param string $search + * @param int $storeId + * @param int $staffId + * @param int $page + * @param int $limit + * @return mixed + */ + public function getHangOrder(int $storeId, int $staffId = 0, string $search = '', int $page = 0, int $limit = 0) + { + return $this->getModel()->where([ + 'status' => 1, + 'is_del' => 0, + 'is_pay' => 0, + 'is_new' => 0, + 'store_id' => $storeId + ])->when($staffId, function ($query) use ($staffId) { + $query->where('staff_id', $staffId); + })->when($search !== '', function ($query) use ($search) { + $query->whereIn('uid', function ($query) use ($search) { + $query->name('user')->where('uid|phone|nickname', 'like', "%" . $search . "%")->field('uid'); + }); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->field(['tourist_uid', 'add_time', 'GROUP_CONCAT(id) as cart_id', 'store_id', 'staff_id', 'uid'])->with('user') + ->group('uid,tourist_uid'); + } + + /** + * 根据商品id获取购物车数量 + * @param array $ids + * @param int $uid + * @param int $staff_id + * @return mixed + */ + public function productIdByCartNum(array $ids, int $uid, int $staff_id = 0, int $touristUid = 0, int $storeId = 0) + { + return $this->search([ + 'product_id' => $ids, + 'is_pay' => 0, + 'is_del' => 0, + 'is_new' => 0, + 'tourist_uid' => $touristUid, + 'uid' => $uid, + 'store_id' => $storeId])->when($staff_id, function ($query) use ($staff_id) { + $query->where('staff_id', $staff_id); + })->group('product_attr_unique')->column('cart_num,product_id', 'product_attr_unique'); + } + + /** + * 获取购物车列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCartList(array $where, int $page = 0, int $limit = 0, array $with = []) + { + return $this->search($where)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(count($with), function ($query) use ($with, $where) { + $query->with($with); + })->when(isset($where['is_cashier']), function ($query) use ($where) { + $query->where('is_cashier', $where['is_cashier']); + })->order('add_time DESC')->select()->toArray(); + } + + /** + * 修改购物车数据未已删除 + * @param array $id + * @param array $data + * @return \crmeb\basic\BaseModel + */ + public function updateDel(array $id) + { + return $this->getModel()->whereIn('id', $id)->update(['is_del' => 1]); + } + + /** + * 删除购物车 + * @param int $uid + * @param array $ids + * @return bool + * @throws \Exception + */ + public function removeUserCart(int $uid, array $ids) + { + return $this->getModel()->where('uid', $uid)->whereIn('id', $ids)->delete(); + } + + /** + * 获取购物车数量 + * @param $uid + * @param $type + * @param $numType + */ + public function getUserCartNum($uid, $type, $numType) + { + $model = $this->getModel()->where(['uid' => $uid, 'type' => $type, 'is_pay' => 0, 'is_new' => 0, 'is_del' => 0]); + if ($numType) { + return $model->count(); + } else { + return $model->sum('cart_num'); + } + } + + /** + * 用户购物车统计数据 + * @param int $uid + * @param string $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCartList(array $where, string $field = '*', array $with = []) + { + return $this->getModel()->where(array_merge($where, ['is_pay' => 0, 'is_new' => 0, 'is_del' => 0, 'type' => 0])) + ->when(count($with), function ($query) use ($with) { + $query->with($with); + })->order('add_time DESC')->field($field)->select()->toArray(); + } + + /** + * 修改购物车数量 + * @param $cartId + * @param $cartNum + * @param $uid + */ + public function changeUserCartNum(array $where, int $carNum) + { + return $this->getModel()->update(['cart_num' => $carNum], $where); + } + + /** + * 修改购物车状态 + * @param array $product_ids + * @param int $status + * @return \crmeb\basic\BaseModel + */ + public function changeStatus(array $product_ids, int $status = 0) + { + return $this->getModel()->where('product_id', 'IN', $product_ids)->update(['status' => $status]); + } + + /** + * 删除购物车 + * @param $cartIds + * @return bool + */ + public function deleteCartStatus($cartIds) + { + return $this->getModel()->where('id', 'IN', $cartIds)->delete(); + } + + /** + * 获取购物车最大的id + * @return mixed + */ + public function getCartIdMax() + { + return $this->getModel()->max('id'); + } + + /** + * 求和 + * @param $where + * @param $field + * @return float + */ + public function getSum($where, $field) + { + return $this->search($where)->sum($field); + } + + /** + * 购物车趋势 + * @param $time + * @param $timeType + * @param $str + * @return mixed + */ + public function getProductTrend($time, $timeType, $str) + { + return $this->getModel()->where(function ($query) use ($time) { + if ($time[0] == $time[1]) { + $query->whereDay('add_time', $time[0]); + } else { + $time[1] = date('Y/m/d', strtotime($time[1]) + 86400); + $query->whereTime('add_time', 'between', $time); + } + })->field("FROM_UNIXTIME(add_time,'$timeType') as days,$str as num")->group('days')->select()->toArray(); + } +} diff --git a/app/dao/order/StoreDeliveryOrderDao.php b/app/dao/order/StoreDeliveryOrderDao.php new file mode 100644 index 0000000..31d4a95 --- /dev/null +++ b/app/dao/order/StoreDeliveryOrderDao.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\order; + + +use app\dao\BaseDao; +use app\model\order\StoreDeliveryOrder; + +/** + * 发货订单 + * Class StoreDeliveryOrderDao + * @package app\dao\order + */ +class StoreDeliveryOrderDao extends BaseDao +{ + + /** + * @return string + */ + protected function setModel(): string + { + return StoreDeliveryOrder::class; + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, array $with = []) + { + return $this->search($where)->field($field)->when($with, function($query) use ($with) { + $query->with($with); + })->when($page && $limit, function($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + + + +} diff --git a/app/dao/order/StoreOrderCartInfoDao.php b/app/dao/order/StoreOrderCartInfoDao.php new file mode 100644 index 0000000..fb45d15 --- /dev/null +++ b/app/dao/order/StoreOrderCartInfoDao.php @@ -0,0 +1,106 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\order; + + +use app\dao\BaseDao; +use app\model\order\StoreOrderCartInfo; + +/** + * 订单详情 + * Class StoreOrderCartInfoDao + * @package app\dao\order + * @method saveAll(array $data) + */ +class StoreOrderCartInfoDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreOrderCartInfo::class; + } + + /** + * 获取购物车详情列表 + * @param array $where + * @param array $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCartInfoList(array $where, array $field = ['*']) + { + return $this->search($where)->field($field)->select()->toArray(); + } + + /** + * 获取购物车信息以数组返回 + * @param array $where + * @param string $field + * @param string $key + */ + public function getCartColunm(array $where, string $field, string $key = '') + { + return $this->search($where)->order('id asc')->column($field, $key); + } + + /** + * @param $cart_ids + * @return array + */ + public function getSplitCartNum($cart_ids) + { + $res = $this->getModel()->whereIn('old_cart_id', $cart_ids)->field('sum(cart_num) as num,old_cart_id')->group('old_cart_id')->select()->toArray(); + $data = []; + foreach ($res as $value) { + $data[$value['old_cart_id']] = $value['num']; + } + return $data; + } + + /**临期 卡次 + * @param int $writeEndTime + * @param int $writeTime + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAdventCartInfoList(int $time = 0, int $writeTime = 0) + { + $list = $this->getModel()->where(['is_writeoff' => 0, 'is_advent_sms' => 0])->where('write_start', '>', 0)->where('write_end', '>', 0) + ->where('write_end', '>', $time)->where('write_end', '<=', $writeTime) + ->field('id,oid,product_id,write_times,write_surplus_times,write_start,write_end,cart_info')->select(); + $list = count($list) > 0 ? $list->toArray() : []; + return $list; + } + + /**过期 卡次 + * @param int $writeTime + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExpireCartInfoList(int $writeTime = 0) + { + $list = $this->getModel()->where(['is_writeoff' => 0, 'is_expire_sms' => 0])->where('write_start', '>', 0)->where('write_end', '>', 0) + ->where('write_end', '<', $writeTime) + ->field('id,oid,product_id,write_times,write_surplus_times,write_start,write_end,cart_info')->select(); + $list = count($list) > 0 ? $list->toArray() : []; + return $list; + } +} diff --git a/app/dao/order/StoreOrderDao.php b/app/dao/order/StoreOrderDao.php new file mode 100644 index 0000000..c242997 --- /dev/null +++ b/app/dao/order/StoreOrderDao.php @@ -0,0 +1,1075 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\order; + + +use app\dao\BaseDao; +use app\common\model\store\order\StoreOrder; + +/** + * 订单 + * Class StoreOrderDao + * @package app\dao\order + */ +class StoreOrderDao extends BaseDao +{ + + /** + * 限制精确查询字段 + * @var string[] + */ + protected $withField = ['uid', 'order_id', 'real_name', 'user_phone', 'title', 'total_num']; + + /** + * @return string + */ + protected function setModel(): string + { + return StoreOrder::class; + } + + /** + * 订单搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + if (isset($where['real_name'])) { + $where['real_name'] = trim($where['real_name']); + } + $isDel = isset($where['is_del']) && $where['is_del'] !== '' && $where['is_del'] != -1; + $realName = $where['real_name'] ?? ''; + $fieldKey = $where['field_key'] ?? ''; + $fieldKey = $fieldKey == 'all' ? '' : $fieldKey; + return parent::search($where)->when($isDel, function ($query) use ($where) { + $query->where('is_del', $where['is_del']); + })->when(isset($where['plat_type']) && in_array($where['plat_type'], [-1, 0, 1, 2]), function ($query) use($where) { + switch ($where['plat_type']) { + case -1://所有 + break; + case 0://平台 + $query->where('store_id', 0)->where('supplier_id', 0); + break; + case 1://门店 + $query->where('store_id', '>', 0); + break; + case 2://供应商 + $query->where('supplier_id', '>', 0); + break; + } + })->when(isset($where['is_system_del']), function ($query) { + $query->where('is_system_del', 0); + })->when(isset($where['is_coupon']), function ($query) { + $query->where('coupon_id','>', 0); + })->when(isset($where['staff_id']) && $where['staff_id'], function ($query) use ($where) { + $query->where('staff_id', $where['staff_id']); + })->when(isset($where['status']) && $where['status'] !== '', function ($query) use ($where) { + switch ((int)$where['status']) { + case 0://未支付 + $query->where('paid', 0)->where('status', 0)->where('refund_status', 0)->where('is_del', 0); + break; + case 1://已支付 未发货 + $query->where('paid', 1)->whereIn('status', [0, 4])->whereIn('refund_status', [0, 3])->whereIn('shipping_type', [1, 3, 4])->where('is_del', 0); + break; + case 7://已支付 部分发货 + $query->where('paid', 1)->where('status', 4)->whereIn('refund_status', [0, 3])->where('is_del', 0); + break; + case 2://已支付 待收货 + $query->where('paid', 1)->whereIn('status', [1, 5])->whereIn('refund_status', [0, 3])->where('is_del', 0); + break; + case 3:// 已支付 已收货 待评价 + $query->where('paid', 1)->where('status', 2)->whereIn('refund_status', [0, 3])->where('is_del', 0); + break; + case 4:// 交易完成 + $query->where('paid', 1)->where('status', 3)->whereIn('refund_status', [0, 3])->where('is_del', 0); + break; + case 5://已支付 待核销 + $query->where('paid', 1)->whereIn('status', [0, 1, 5])->whereIn('refund_status', [0, 3])->where('shipping_type', 2)->where('is_del', 0); + break; + case 6://已支付 已核销 没有退款 + $query->where('paid', 1)->where('status', 2)->whereIn('refund_status', [0, 3])->where('shipping_type', 2)->where('is_del', 0); + break; + case 8://已支付 核销订单 + $query->where('paid', 1)->whereIn('status', [0, 1, 2, 5])->whereIn('refund_status', [0, 3])->where('shipping_type', 2)->where('is_del', 0); + break; + case 9://已配送 + $query->where('paid', 1)->whereIn('status', [2, 3])->whereIn('refund_status', [0, 3])->where('is_del', 0); + break; + case -1://退款中 + $query->where('paid', 1)->whereIn('refund_status', [1, 4])->where('is_del', 0); + break; + case -2://已退款 + $query->where('paid', 1)->where('refund_status', 2)->where('is_del', 0); + break; + case -3://退款 + $query->where('paid', 1)->whereIn('refund_status', [1, 2, 4])->where('is_del', 0); + break; + case -4://已删除 + $query->where('is_del', 1); + break; + } + })->when(isset($where['type']) && $where['type'] !== '', function ($query) use ($where) { + switch ($where['type']) { + case 0://普通 + $query->where('type', 0); + break; + case 1://秒杀 + $query->where('type', 1); + break; + case 2://砍价 + $query->where('type', 2); + break; + case 3://拼团 + $query->where('type', 3); + break; + case 4://套餐 + $query->where('type', 5); + break; + case 5://核销订单 + $query->where('shipping_type', 2); + break; + case 6://收银台订单 + $query->where('shipping_type', 4); + break; + case 7://配送订单 + $query->whereIn('shipping_type', [1, 3]); + break; + case 8://预售 + $query->where('type', 6); + break; + case 9://新人专享 + $query->where('type', 7); + break; + case 10://抽奖 + $query->where('type', 8); + break; + case 11://拼单 + $query->where('type', 9); + break; + case 12://桌码 + $query->where('type', 10); + break; + } + })->when(isset($where['order_type']) && $where['order_type'] !== '', function ($query) use ($where) { + switch ($where['order_type']) { + case 5://核销订单 + $query->where('shipping_type', 2); + break; + case 6://收银台订单 + $query->where('shipping_type', 4); + break; + case 7://配送订单 + $query->whereIn('shipping_type', [1, 3]); + break; + } + })->when(isset($where['pay_type']), function ($query) use ($where) { + switch ($where['pay_type']) { + case 1: + $query->where('pay_type', 'weixin'); + break; + case 2: + $query->where('pay_type', 'yue'); + break; + case 3: + $query->where('pay_type', 'offline'); + break; + case 4: + $query->where('pay_type', 'alipay'); + break; + } + })->when($realName && $fieldKey && in_array($fieldKey, $this->withField), function ($query) use ($where, $realName, $fieldKey) { + if ($fieldKey !== 'title') { + $query->where(trim($fieldKey), trim($realName)); + } else { + $query->where('id', 'in', function ($que) use ($where) { + $que->name('store_order_cart_info')->whereIn('product_id', function ($q) use ($where) { + $q->name('store_product')->whereLike('store_name|keyword', '%' . $where['real_name'] . '%')->field(['id'])->select(); + })->field(['oid'])->select(); + }); + } + })->when($realName && !$fieldKey, function ($query) use ($where) { + $query->where(function ($que) use ($where) { + $que->whereLike('order_id|real_name|user_phone|verify_code', '%' . $where['real_name'] . '%') + ->whereOr('uid', 'in', function ($q) use ($where) { + $q->name('user')->whereLike('nickname|uid|phone', '%' . $where['real_name'] . '%')->field(['uid'])->select(); + })->whereOr('uid', 'in', function ($q) use ($where) { + $q->name('user_address')->whereLike('real_name|uid|phone', '%' . $where['real_name'] . '%')->field(['uid'])->select(); + })->whereOr('id', 'in', function ($que) use ($where) { + $que->name('store_order_cart_info')->whereIn('product_id', function ($q) use ($where) { + $q->name('store_product')->whereLike('store_name|keyword', '%' . $where['real_name'] . '%')->field(['id'])->select(); + })->field(['oid'])->select(); + })->whereOr('activity_id', 'in', function ($que) use ($where) { + $que->name('store_seckill')->whereLike('title|info', '%' . $where['real_name'] . '%')->field(['id'])->select(); + })->whereOr('activity_id', 'in', function ($que) use ($where) { + $que->name('store_bargain')->whereLike('title|info', '%' . $where['real_name'] . '%')->field(['id'])->select(); + })->whereOr('activity_id', 'in', function ($que) use ($where) { + $que->name('store_combination')->whereLike('title|info', '%' . $where['real_name'] . '%')->field(['id'])->select(); + }); + }); + })->when(isset($where['unique']), function ($query) use ($where) { + $query->where('unique', $where['unique']); + })->when(isset($where['is_remind']), function ($query) use ($where) { + $query->where('is_remind', $where['is_remind']); + })->when(isset($where['refundTypes']) && $where['refundTypes'] != '', function ($query) use ($where) { + switch ((int)$where['refundTypes']) { + case 1: + $query->where('refund_type', 'in', '1,2'); + break; + case 2: + $query->where('refund_type', 4); + break; + case 3: + $query->where('refund_type', 5); + break; + case 4: + $query->where('refund_type', 6); + break; + } + })->when(isset($where['is_refund']) && $where['is_refund'] !== '', function ($query) use ($where) { + if ($where['is_refund'] == 1) { + $query->where('refund_status', 2); + } else { + $query->where('refund_status', 0); + } + }); + } + + /** + * 获取某一个月订单数量 + * @param array $where + * @param string $month + * @return int + */ + public function getMonthCount(array $where, string $month) + { + return $this->search($where)->whereMonth('add_time', $month)->count(); + } + + /** + * 获取某一个月订单金额 + * @param array $where + * @param string $month + * @param string $field + * @return float + */ + public function getMonthMoneyCount(array $where, string $month, string $field) + { + return $this->search($where)->whereMonth('add_time', $month)->sum($field); + } + + /** + * 获取购买历史用户 + * @param int $storeId + * @param int $staffId + * @param int $limit + * @return mixed + */ + public function getOrderHistoryList(int $storeId, int $staffId, array $uid = [], int $limit = 20) + { + return $this->search(['store_id' => $storeId, 'staff_id' => $staffId])->when($uid, function ($query) use ($uid) { + $query->whereNotIn('uid', $uid); + })->where('uid', '<>', 0)->with('user')->limit($limit) + ->group('uid')->order('add_time', 'desc')->field(['uid', 'store_id', 'staff_id'])->select()->toArray(); + } + + /** + * 订单搜索列表 + * @param array $where + * @param array $field + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, array $field, int $page = 0, int $limit = 0, array $with = []) + { + return $this->search($where)->field($field)->with($with)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('pay_time DESC,id DESC')->select()->toArray(); + } + + /** + * 获取待核销的订单列表 + * @param array $where + * @param array|string[] $field + * @return mixed + */ + public function getUnWirteOffList(array $where, array $field = ['*']) + { + return $this->search($where)->field($field)->where('paid', 1)->whereIn('status', [0, 1, 5])->whereIn('refund_status', [0, 3])->where('is_del', 0)->where('is_system_del', 0) + ->where(function ($query) { + $query->where('shipping_type', 2)->whereOr('delivery_type', 'send'); + })->order('pay_time DESC,id DESC')->select()->toArray(); + } + + /** + * 订单搜索列表 + * @param array $where + * @param array $field + * @param int $page + * @param int $limit + * @param array $with + * @param string $order + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderList(array $where, array $field, int $page = 0, int $limit = 0, array $with = [], string $order = 'add_time DESC,id DESC') + { + return $this->search($where)->field($field)->with(array_merge(['user', 'spread', 'refund'], $with))->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(!$page && $limit, function ($query) use ($limit) { + $query->limit($limit); + })->order($order)->select()->toArray(); + } + + + /** + * 聚合查询 + * @param array $where + * @param string $field + * @param string $together + * @return int + */ + public function together(array $where, string $field, string $together = 'sum') + { + if (!in_array($together, ['sum', 'max', 'min', 'avg'])) { + return 0; + } + return $this->search($where)->{$together}($field); + } + + /** + * 查找指定条件下的订单数据以数组形式返回 + * @param array $where + * @param string $field + * @param string $key + * @param string $group + * @return array + */ + public function column(array $where, string $field, string $key = '', string $group = '') + { + return $this->search($where)->when($group, function ($query) use ($group) { + $query->group($group); + })->column($field, $key); + } + + /** + * 获取订单id下没有删除的订单数量 + * @param array $ids + * @return int + */ + public function getOrderIdsCount(array $ids) + { + return $this->getModel()->whereIn('id', $ids)->where('is_del', 0)->count(); + } + + /** + * 获取一段时间订单统计数量、金额 + * @param array $where + * @param array $time + * @param string $timeType + * @param bool $is_pid + * @param string $countField + * @param string $sumField + * @param string $groupField + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function orderAddTimeList(array $where, array $time, string $timeType = "week", bool $is_pid = true, string $countField = '*', string $sumField = 'pay_price', string $groupField = 'add_time') + { + return $this->getModel()->where($where)->when($is_pid, function ($query) { + $query->where('pid', '>=', 0); + })->when(!isset($where['refund_status']), function ($query) { + $query->whereIn('refund_status', [0, 3]); + })->where('paid', 1)->where('is_del', 0)->where('is_system_del', 0) + ->where(isset($where['timekey']) && $where['timekey'] ? $where['timekey'] : 'add_time', 'between time', $time) + ->when($timeType, function ($query) use ($timeType, $countField, $sumField, $groupField) { + switch ($timeType) { + case "hour": + $timeUnix = "%H"; + break; + case "day" : + $timeUnix = "%Y-%m-%d"; + break; + case "week" : + $timeUnix = "%w"; + break; + case "month" : + $timeUnix = "%d"; + break; + case "weekly" : + $timeUnix = "%W"; + break; + case "year" : + $timeUnix = "%Y-%m"; + break; + default: + $timeUnix = "%Y-%m-%d"; + break; + } + $query->field("FROM_UNIXTIME(`" . $groupField . "`,'$timeUnix') as day,count(" . $countField . ") as count,sum(`" . $sumField . "`) as price"); + $query->group('day'); + })->order('add_time asc')->select()->toArray(); + } + + /** + * 统计总数上期 + * @param array $where + * @param array $time + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function preTotalFind(array $where, array $time, string $sumField = 'pay_price', string $groupField = 'add_time') + { + return $this->getModel()->where($where)->where('pid', '>=', 0)->where('paid', 1)->whereIn('refund_status', [0, 3])->where('is_del', 0)->where('is_system_del', 0) + ->where($groupField, 'between time', $time) + ->field("count(*) as count,sum(`" . $sumField . "`) as price") + ->find(); + } + + /** + * 新订单ID + * @param $status + * @param int $store_id + * @return array + */ + public function newOrderId($status, int $store_id = 0) + { + return $this->search(['status' => $status, 'is_remind' => 0])->where('store_id', $store_id)->column('order_id', 'id'); + } + + /** + * 获取订单数量 + * @param int $store_id + * @param int $type + * @param string $field + * @return int + */ + public function storeOrderCount(int $val = 0, int $type = -1, string $field = 'store_id') + { + $where = ['pid' => 0, 'status' => 1]; + if ($type != -1) $where['type'] = $type; + return $this->search($where)->when($field && $val > 0, function ($query) use ($field, $val) { + $query->where($field, $val); + })->count(); + } + + /** + * 总销售额 + * @param $time + * @param int $store_id + * @return float + */ + public function totalSales($time, int $store_id = 0) + { + return $this->search(['pid' => 0, 'paid' => 1, 'is_del' => 0, 'refund_status' => [0, 3], 'time' => $time ?: 'today', 'timekey' => 'pay_time'])->where('store_id', $store_id)->sum('pay_price'); + } + + /** + * 获取特定时间内订单量 + * @param $time + * @return int + */ + public function totalOrderCount($time) + { + return $this->search(['pid' => 0, 'time' => $time ?: 'today', 'timeKey' => 'add_time'])->where('paid', 1)->whereIn('refund_status', [0, 3])->where('store_id', 0)->count(); + } + + /** + * 获取订单详情 + * @param string $key + * @param int $uid + * @param array $with + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserOrderDetail(string $key, int $uid, array $with = []) + { + return $this->getOne(['order_id|id|unique' => $key, 'uid' => $uid, 'is_del' => 0], '*', $with); + } + + /** + * 获取用户推广订单 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStairOrderList(array $where, string $field, int $page, int $limit, array $with = []) + { + return $this->search($where)->with($with)->field($field)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id DESC')->select()->toArray(); + } + + /** + * 订单每月统计数据 + * @param int $page + * @param int $limit + * @return array + */ + public function getOrderDataPriceCount(array $where, array $field, int $page, int $limit) + { + return $this->search($where) + ->field($field)->group("FROM_UNIXTIME(add_time, '%Y-%m-%d')") + ->order('add_time DESC')->page($page, $limit)->select()->toArray(); + } + + /** + * 获取当前时间到指定时间的支付金额 管理员 + * @param $start + * @param $stop + * @param int $store_id + * @return mixed + */ + public function chartTimePrice($start, $stop, int $store_id = 0) + { + return $this->search(['pid' => 0, 'is_del' => 0, 'paid' => 1, 'refund_status' => [0, 3], 'is_system_del' => 0]) + ->where('store_id', $store_id) + ->where('add_time', '>=', $start) + ->where('add_time', '<', $stop) + ->field('sum(pay_price) as num,FROM_UNIXTIME(add_time, \'%Y-%m-%d\') as time') + ->group("FROM_UNIXTIME(add_time, '%Y-%m-%d')") + ->order('add_time ASC')->select()->toArray(); + } + + /** + * 获取当前时间到指定时间的支付订单数 管理员 + * @param $start + * @param $stop + * @param int $store_id + * @return mixed + */ + public function chartTimeNumber($start, $stop, int $store_id = 0) + { + return $this->search(['pid' => 0, 'is_del' => 0, 'paid' => 1, 'refund_status' => [0, 3]]) + ->where('store_id', $store_id) + ->where('add_time', '>=', $start) + ->where('add_time', '<', $stop) + ->field('count(id) as num,FROM_UNIXTIME(add_time, \'%Y-%m-%d\') as time') + ->group("FROM_UNIXTIME(add_time, '%Y-%m-%d')") + ->order('add_time ASC')->select()->toArray(); + } + + /** + * 获取用户已购买此活动商品的个数 + * @param $uid + * @param $type + * @param $activity_id + * @return int + */ + public function getBuyCount($uid, $type, $activity_id): int + { + return $this->getModel() + ->where('uid', $uid) + ->where('type', $type) + ->where('activity_id', $activity_id) + ->whereIn('pid', [0, -1]) + ->where(function ($query) { + $query->where('paid', 1)->whereOr(function ($query1) { + $query1->where('paid', 0)->where('is_del', 0); + }); + })->value('sum(total_num)') ?? 0; + } + + /** + * 获取没有支付的订单列表 + * @param int $store_id + * @param int $page + * @param int $limit + * @return \crmeb\basic\BaseModel + */ + public function getOrderUnPaid(int $store_id = 0, int $page = 0, int $limit = 0) + { + return $this->getModel() + ->where(['pid' => 0, 'paid' => 0, 'is_del' => 0, 'status' => 0, 'refund_status' => 0]) + ->where('store_id', $store_id) + ->where('pay_type', '<>', 'offline') + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + }); + } + + + /** + * 用户趋势数据 + * @param $time + * @param $type + * @param $timeType + * @return mixed + */ + public function getTrendData($time, $type, $timeType, $str) + { + return $this->getModel()->when($type != '', function ($query) use ($type) { + $query->where('channel_type', $type); + })->where('paid', 1)->where('pid', '>=', 0)->where(function ($query) use ($time) { + if ($time[0] == $time[1]) { + $query->whereDay('pay_time', $time[0]); + } else { + $time[1] = date('Y/m/d', strtotime($time[1]) + 86400); + $query->whereTime('pay_time', 'between', $time); + } + })->field("FROM_UNIXTIME(pay_time,'$timeType') as days,$str as num") + ->group('days')->select()->toArray(); + } + + /** + * 用户地域数据 + * @param $time + * @param $userType + * @return mixed + */ + public function getRegion($time, $userType) + { + return $this->getModel()->when($userType != '', function ($query) use ($userType) { + $query->where('channel_type', $userType); + })->where('pid', '>=', 0)->where('store_id', 0)->where(function ($query) use ($time) { + if ($time[0] == $time[1]) { + $query->whereDay('pay_time', $time[0]); + } else { + $time[1] = date('Y/m/d', strtotime($time[1]) + 86400); + $query->whereTime('pay_time', 'between', $time); + } + })->field('pay_price as payPrice,substring_index(user_address, " ", 1) as province')->select()->toArray(); + } + + /** + * 商品趋势 + * @param $time + * @param $timeType + * @param $field + * @param $str + * @return mixed + */ + public function getProductTrend($time, $timeType, $field, $str, $orderStatus = '') + { + return $this->getModel()->where(function ($query) use ($field, $orderStatus) { + if ($field == 'pay_time') { + $query->where('paid', 1); + } elseif ($field == 'refund_reason_time') { + $query->where('paid', 1)->where('refund_status', '>', 0); + } elseif ($field == 'add_time') { + if ($orderStatus == 'pay') { + $query->where('paid', 1)->where('pid', '>=', 0)->where('refund_status', 0); + } elseif ($orderStatus == 'refund') { + $query->where('paid', 1)->where('pid', '>=', 0)->where('refund_status', '>', 0); + } elseif ($orderStatus == 'coupon') { + $query->where('paid', 1)->where('pid', '>=', 0)->where('coupon_id', '>', 0); + } + } + })->where('pid', '>=', 0)->where('store_id', 0)->where(function ($query) use ($time, $field) { + if ($time[0] == $time[1]) { + $query->whereDay($field, $time[0]); + } else { + $time[1] = date('Y/m/d', strtotime($time[1]) + 86400); + $query->whereTime($field, 'between', $time); + } + })->field("FROM_UNIXTIME($field,'$timeType') as days,$str as num")->group('days')->select()->toArray(); + } + + + /** + * 按照支付时间统计支付金额 + * @param array $where + * @param string $sumField + * @return mixed + */ + public function getDayTotalMoney(array $where, string $sumField) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where) { + $query->whereBetweenTime('pay_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + }) + ->sum($sumField); + } + + /** + * 时间段订单数统计 + * @param array $where + * @param string $countField + * @return int + */ + public function getDayOrderCount(array $where, string $countField = "*") + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where) { + $query->whereBetweenTime('pay_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + }) + ->count($countField); + } + + /** + * 时间分组订单付款金额统计 + * @param array $where + * @param string $sumField + * @return mixed + */ + public function getDayGroupMoney(array $where, string $sumField, string $group) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where, $sumField, $group) { + $query->whereBetweenTime('pay_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + if ($where['timeKey']['days'] == 1) { + $timeUinx = "%H"; + } elseif ($where['timeKey']['days'] == 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] == 365) { + $timeUinx = "%Y-%m"; + } elseif ($where['timeKey']['days'] > 1 && $where['timeKey']['days'] < 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] > 30 && $where['timeKey']['days'] < 365) { + $timeUinx = "%Y-%m"; + } else { + $timeUinx = "%Y-%m"; + } + $query->field("sum($sumField) as number,FROM_UNIXTIME($group, '$timeUinx') as time"); + $query->group("FROM_UNIXTIME($group, '$timeUinx')"); + }) + ->order('pay_time ASC')->select()->toArray(); + } + + /** + * 时间分组订单数统计 + * @param array $where + * @param string $sumField + * @return mixed + */ + public function getOrderGroupCount(array $where, string $sumField = "*") + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where, $sumField) { + $query->whereBetweenTime('pay_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + if ($where['timeKey']['days'] == 1) { + $timeUinx = "%H"; + } elseif ($where['timeKey']['days'] == 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] == 365) { + $timeUinx = "%Y-%m"; + } elseif ($where['timeKey']['days'] > 1 && $where['timeKey']['days'] < 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] > 30 && $where['timeKey']['days'] < 365) { + $timeUinx = "%Y-%m"; + } else { + $timeUinx = "%Y-%m"; + } + $query->field("count($sumField) as number,FROM_UNIXTIME(pay_time, '$timeUinx') as time"); + $query->group("FROM_UNIXTIME(pay_time, '$timeUinx')"); + }) + ->order('pay_time ASC')->select()->toArray(); + } + + /** + * 时间段支付订单人数 + * @param $where + * @return mixed + */ + public function getPayOrderPeople($where) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where) { + $query->whereBetweenTime('pay_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + }) + ->field('uid') + ->distinct(true) + ->select()->toArray(); + } + + /** + * 时间段分组统计支付订单人数 + * @param $where + * @return mixed + */ + public function getPayOrderGroupPeople($where) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where) { + $query->whereBetweenTime('pay_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + if ($where['timeKey']['days'] == 1) { + $timeUinx = "%H"; + } elseif ($where['timeKey']['days'] == 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] == 365) { + $timeUinx = "%Y-%m"; + } elseif ($where['timeKey']['days'] > 1 && $where['timeKey']['days'] < 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] > 30 && $where['timeKey']['days'] < 365) { + $timeUinx = "%Y-%m"; + } else { + $timeUinx = "%Y-%m"; + } + $query->field("count(distinct uid) as number,FROM_UNIXTIME(pay_time, '$timeUinx') as time"); + $query->group("FROM_UNIXTIME(pay_time, '$timeUinx')"); + }) + ->order('pay_time ASC')->select()->toArray(); + } + + + /** + * 获取批量打印电子面单数据 + * @param array $where + * @param array $ids + * @param string $filed + * @param int $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderDumpData(array $where, array $ids = [], $filed = "*", int $store_id = 0) + { + $where['pid'] = 0; + $where['status'] = 1; + $where['refund_status'] = 0; + $where['paid'] = 1; + $where['is_del'] = 0; + $where['shipping_type'] = 1; + $where['is_system_del'] = 0; + $where['store_id'] = $store_id; + return $this->search($where)->when($ids, function ($query) use ($ids) { + $query->whereIn('id', $ids); + })->field($filed)->with(['pink'])->select()->toArray(); + } + + /** + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderListByWhere(array $where, $field = "*") + { + return $this->search($where)->field($field)->select()->toArray(); + } + + /** + * 批量修改订单 + * @param array $ids + * @param array $data + * @param string|null $key + * @return \crmeb\basic\BaseModel + */ + public function batchUpdateOrder(array $ids, array $data, ?string $key = null) + { + return $this->getModel()::whereIn(is_null($key) ? $this->getPk() : $key, $ids)->update($data); + } + + /** + * 获取拆单之后的子订单 + * @param int $id + * @param string $field + * @param int $type 1:不包含自己2:包含自己 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSonOrder(int $id, string $field = '*', int $type = 1) + { + return in_array($type, [1, 2]) ? $this->getModel()::field($field)->when($type, function ($query) use ($id, $type) { + if ($type == 1) { + $query->where('pid', $id); + } else { + $query->where('pid', $id)->whereOr('id', $id); + } + })->select()->toArray() : []; + } + + /** + * 查询退款订单 + * @param $where + * @param $page + * @param $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRefundList($where, $page = 0, $limit = 0) + { + $model = $this->getModel() + ->where('is_system_del', 0) + ->where('paid', 1) + ->when(isset($where['store_id']), function ($query) use ($where) { + $query->where('store_id', $where['store_id']); + })->when(isset($where['refund_type']) && $where['refund_type'] !== '', function ($query) use ($where) { + if ($where['refund_type'] == 0) { + $query->where('refund_type', '>', 0); + } else { + $query->where('refund_type', $where['refund_type']); + } + })->when(isset($where['not_pid']), function ($query) { + $query->where('pid', '<>', -1); + })->when($where['order_id'] != '', function ($query) use ($where) { + $query->where('order_id', $where['order_id']); + })->when(is_array($where['refund_reason_time']), function ($query) use ($where) { + $query->whereBetween('refund_reason_time', [strtotime($where['refund_reason_time'][0]), strtotime($where['refund_reason_time'][1]) + 86400]); + })->with(array_merge(['user', 'spread'])); + $count = $model->count(); + $list = $model->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('refund_reason_time desc')->select()->toArray(); + return compact('list', 'count'); + } + + /** + * 门店线上支付订单详情 + * @param int $store_id + * @param int $uid + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function payCashierOrder(int $store_id, int $uid) + { + return $this->getModel()->where('uid', $uid)->where('store_id', $store_id)->where('paid', 0)->where('is_del', 0)->where('is_system_del', 0) + ->where('shipping_type', 4) + ->order('add_time desc,id desc') + ->find(); + } + + /** + * 商品趋势 + * @param $time + * @param $timeType + * @param $field + * @param $str + * @param $orderStatus + * @return mixed + */ + public function getOrderStatistics($where, $time, $timeType, $field, $str, $orderStatus = '') + { + return $this->getModel()->where($where)->where(function ($query) use ($field, $orderStatus) { + if ($field == 'pay_time') { + $query->where('paid', 1); + } elseif ($field == 'refund_reason_time') { + $query->where('paid', 1)->where('refund_status', '>', 0); + } elseif ($field == 'add_time') { + if ($orderStatus == 'pay') { + $query->where('paid', 1)->where('pid', '>=', 0)->whereIn('refund_status', [0, 3]); + } elseif ($orderStatus == 'refund') { + $query->where('paid', 1)->where('pid', '>=', 0)->where('refund_type', 6); + } + } + })->where(function ($query) use ($time, $field) { + if ($time[0] == $time[1]) { + $query->whereDay($field, $time[0]); + } else { + $time[1] = date('Y/m/d', (!is_numeric($time[1]) ? strtotime($time[1]) : $time[1]) + 86400); + $query->whereTime($field, 'between', $time); + } + })->where('is_del', 0)->where('is_system_del', 0) + ->field("FROM_UNIXTIME($field,'$timeType') as days,$str as num")->group('days')->select()->toArray(); + } + + /** + * 获取活动订单列表: + * @param int $id + * @param int $type 0:普通、1:秒杀、2:砍价、3:拼团、4:积分、5:套餐、6:预售、7:新人礼 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function activityStatisticsOrder(int $id, int $type = 1,array $where = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->where('pid', 'in', [0, -1])->where('paid', 1)->where('type', $type)->where('activity_id', $id) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->field(['order_id', 'real_name', 'status', 'pay_price', 'total_num', 'add_time', 'pay_time', 'paid', 'shipping_type', 'refund_status', 'is_del', 'is_system_del'])->select()->toArray(); + } + + + /** + * 秒杀参与人统计 + * @param int $id + * @param string $keyword + * @param int $page + * @param int $limit + * @return mixed + */ + public function seckillPeople(int $id, string $keyword, int $page = 0, int $limit = 0) + { + return $this->getModel() + ->when($id != 0, function ($query) use ($id) { + $query->where('type', 1)->where('activity_id', $id); + })->when($keyword != '', function ($query) use ($keyword) { + $query->where('real_name|uid|user_phone', 'like', '%' . $keyword . '%'); + })->where('pid', 'in', [0, -1])->where('paid', 1)->field([ + 'real_name', + 'uid', + 'SUM(total_num) as goods_num', + 'COUNT(id) as order_num', + 'SUM(pay_price) as total_price', + 'add_time' + ])->group('uid')->order("add_time desc")->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * @param int $pid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSubOrderNotSendList(int $pid) + { + return $this->getModel()->where('pid', $pid)->where('status', 1)->select()->toArray(); + } + + /** + * @param int $pid + * @param int $order_id + * @return int + * @throws \think\db\exception\DbException + */ + public function getSubOrderNotSend(int $pid, int $order_id) + { + return $this->getModel()->where('pid', $pid)->where('status', 0)->where('id', '<>', $order_id)->count(); + } + + /** + * @param int $pid + * @param int $order_id + * @return int + * @throws \think\db\exception\DbException + */ + public function getSubOrderNotTake(int $pid, int $order_id) + { + return $this->getModel()->where('pid', $pid)->where('status', 1)->where('id', '<>', $order_id)->count(); + } + +} diff --git a/app/dao/order/StoreOrderRefundDao.php b/app/dao/order/StoreOrderRefundDao.php new file mode 100644 index 0000000..8e95897 --- /dev/null +++ b/app/dao/order/StoreOrderRefundDao.php @@ -0,0 +1,191 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\order; + + +use app\dao\BaseDao; +use app\model\order\StoreOrderRefund; + +/** + * 退款订单 + * Class StoreOrderInvoiceDao + * @package app\dao\order + */ +class StoreOrderRefundDao extends BaseDao +{ + + protected function setModel(): string + { + return StoreOrderRefund::class; + } + + /** + * 搜索器 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + $realName = $where['real_name'] ?? ''; + $fieldKey = $where['field_key'] ?? ''; + $fieldKey = $fieldKey == 'all' ? '' : $fieldKey; + return parent::search($where)->when(isset($where['refund_type']) && $where['refund_type'] !== '', function ($query) use ($where) { + if ($where['refund_type'] == 0) { + $query->where('refund_type', '>', 0); + } else { + if (is_array($where['refund_type'])) { + $query->whereIn('refund_type', $where['refund_type']); + } else { + $query->where('refund_type', $where['refund_type']); + } + } + })->when(isset($where['order_id']) && $where['order_id'] != '', function ($query) use ($where) { + $query->where(function ($q) use ($where) { + $q->whereLike('order_id', '%' . $where['order_id'] . '%')->whereOr('store_order_id', 'IN', function ($orderModel) use ($where) { + $orderModel->name('store_order')->field('id')->whereLike('order_id|uid|user_phone|staff_id', '%' . $where['order_id'] . '%'); + }); + }); + })->when($realName && !$fieldKey, function ($query) use ($where) { + $query->where(function ($que) use ($where) { + $que->whereLike('order_id', '%' . $where['real_name'] . '%')->whereOr(function ($q) use ($where) { + $q->whereOr('uid', 'in', function ($q) use ($where) { + $q->name('user')->whereLike('nickname|uid|phone', '%' . $where['real_name'] . '%')->field(['uid'])->select(); + })->whereOr('store_order_id', 'in', function ($que) use ($where) { + $que->name('store_order_cart_info')->whereIn('product_id', function ($q) use ($where) { + $q->name('store_product')->whereLike('store_name|keyword', '%' . $where['real_name'] . '%')->field(['id'])->select(); + })->field(['oid'])->select(); + })->whereOr('store_order_id', 'in', function ($orderModel) use ($where) { + $orderModel->name('store_order')->field('id')->whereLike('order_id', '%' . $where['real_name'] . '%'); + }); + }); + }); + })->when(isset($where['refundTypes']) && $where['refundTypes'] != '', function ($query) use ($where) { + switch ((int)$where['refundTypes']) { + case 1: + $query->where('refund_type', 'in', '1,2'); + break; + case 2: + $query->where('refund_type', 4); + break; + case 3: + $query->where('refund_type', 5); + break; + case 4: + $query->where('refund_type', 6); + break; + } + })->when(isset($where['supplier']) && $where['supplier'] !== '', function ($query) use ($where) { + $query->where('supplier_id', '>', 0); + }); + } + + /** + * 查询退款订单 + * @param $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRefundList($where, string $file = '*', array $with = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($file)->with(array_merge(['user'], $with)) + ->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc')->select()->toArray(); + } + + /** + * 获取退款金额按照时间分组 + * @param array $time + * @param string $timeType + * @param string $field + * @param string $str + * @return mixed + */ + public function getUserRefundPriceList(array $time, string $timeType, string $str, string $field = 'add_time') + { + return $this->getModel()->where('is_cancel', 0)->where(function ($query) use ($time, $field) { + if ($time[0] == $time[1]) { + $query->whereDay($field, $time[0]); + } else { + $time[1] = date('Y/m/d', strtotime($time[1]) + 86400); + $query->whereTime($field, 'between', $time); + } + })->field("FROM_UNIXTIME($field,'$timeType') as days,$str as num,GROUP_CONCAT(store_order_id) as link_ids")->group('days')->select()->toArray(); + } + + /** + * 根据时间获取 + * @param array $where + * @return float|int + */ + public function getOrderRefundMoneyByWhere(array $where, string $sum_field, string $selectType, string $group = "") + { + + switch ($selectType) { + case "sum" : + return $this->getDayTotalMoney($where, $sum_field); + case "group" : + return $this->getDayGroupMoney($where, $sum_field, $group); + } + } + + /** + * 按照支付时间统计支付金额 + * @param array $where + * @param string $sumField + * @return mixed + */ + public function getDayTotalMoney(array $where, string $sumField) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where) { + $query->whereBetweenTime('add_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + }) + ->sum($sumField); + } + + + /** + * 时间分组订单付款金额统计 + * @param array $where + * @param string $sumField + * @return mixed + */ + public function getDayGroupMoney(array $where, string $sumField, string $group) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where, $sumField, $group) { + $query->whereBetweenTime('add_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + if ($where['timeKey']['days'] == 1) { + $timeUinx = "%H"; + } elseif ($where['timeKey']['days'] == 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] == 365) { + $timeUinx = "%Y-%m"; + } elseif ($where['timeKey']['days'] > 1 && $where['timeKey']['days'] < 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] > 30 && $where['timeKey']['days'] < 365) { + $timeUinx = "%Y-%m"; + } else { + $timeUinx = "%Y-%m"; + } + $query->field("sum($sumField) as number,FROM_UNIXTIME($group, '$timeUinx') as time"); + $query->group("FROM_UNIXTIME($group, '$timeUinx')"); + }) + ->order('add_time ASC')->select()->toArray(); + } +} diff --git a/app/dao/order/StoreOrderStatusDao.php b/app/dao/order/StoreOrderStatusDao.php new file mode 100644 index 0000000..aa06f9b --- /dev/null +++ b/app/dao/order/StoreOrderStatusDao.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\order; + + +use app\dao\BaseDao; +use app\common\model\store\order\StoreOrderStatus; + +/** + * 订单状态 + * Class StoreOrderStatusDao + * @package app\dao\order + */ +class StoreOrderStatusDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreOrderStatus::class; + } + + /** + * 获取订单状态列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStatusList(array $where, int $page = 0, int $limit = 0) + { + return $this->search($where) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('change_time desc')->select()->toArray(); + } + +} diff --git a/app/dao/other/CacheDao.php b/app/dao/other/CacheDao.php new file mode 100644 index 0000000..579bbba --- /dev/null +++ b/app/dao/other/CacheDao.php @@ -0,0 +1,41 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\other; + + +use app\dao\BaseDao; +use app\common\model\system\Cache; + +/** + * Class CacheDao + * @package app\dao\other + */ +class CacheDao extends BaseDao +{ + + /** + * @return string + */ + public function setModel(): string + { + return Cache::class; + } + + /** + * 清除过期缓存 + * @throws \Exception + */ + public function delectDeOverdueDbCache() + { + $this->getModel()->where('expire_time', '<>', 0)->where('expire_time', '<', time())->delete(); + } +} diff --git a/app/dao/other/CategoryDao.php b/app/dao/other/CategoryDao.php new file mode 100644 index 0000000..a9ead55 --- /dev/null +++ b/app/dao/other/CategoryDao.php @@ -0,0 +1,103 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\other; + + +use app\dao\BaseDao; +use app\model\other\Category; + +/** + * 分类 + * Class CategoryDao + * @package app\dao\other + */ +class CategoryDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return Category::class; + } + + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['name']) && $where['name'], function ($query) use ($where) { + $query->where(function ($q) use ($where) { + $q->whereOr('id|name', 'like', '%' . $where['name'] . '%') + ->when(!empty($where['product_label']), function ($query) use ($where) { + $query->whereOr('id', 'IN', function ($l) use ($where) { + $l->name('store_product_label')->whereLike('id|label_name', '%' . $where['name'] . '%')->field('label_cate'); + }); + }); + }); + })->when(!empty($where['nowName']), function ($query) use ($where) { + $query->where('name', $where['nowName']); + }); + } + + /** + * 获取分类 + * @param array $where + * @param int $page + * @param int $limit + * @param array|string[] $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCateList(array $where, int $page = 0, int $limit = 0, array $field = ['*'], array $with = []) + { + $other = false; + if (isset($where['other'])) { + $other = true; + unset($where['other']); + } + return $this->search($where)->field($field)->when(count($with), function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when($other, function ($query) { + $query->where('other', '<>', ''); + })->order('sort DESC,id DESC')->select()->toArray(); + } + + /** + * 获取全部标签分类 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAll(array $where = [], array $with = []) + { + return $this->search($where)->when(count($with), function ($query) use ($with) { + $query->with($with); + })->order('sort DESC,id DESC')->select()->toArray(); + } + + /** + * 添加修改选择上级分类列表 + * @param array $where + * @param string $field + * @return array + */ + public function getMenus(array $where, string $field = 'id,pid,name') + { + return $this->search($where)->order('sort desc,id desc')->column($field); + } + +} diff --git a/app/dao/other/CityAreaDao.php b/app/dao/other/CityAreaDao.php new file mode 100644 index 0000000..7db55dd --- /dev/null +++ b/app/dao/other/CityAreaDao.php @@ -0,0 +1,109 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\other; + + +use app\dao\BaseDao; +use app\common\model\store\CityArea; + +/** + * Class CityAreaDao + * @package app\dao\other + */ +class CityAreaDao extends BaseDao +{ + + /** + * @return string + */ + protected function setModel(): string + { + return CityArea::class; + } + + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['pid']) && $where['pid'] !== '', function ($query) use ($where) { + $query->where('parent_id', $where['pid']); + })->when(isset($where['address']) && $where['address'] !== '', function ($query) use ($where) { + $address = explode('/', trim($where['address'], '/')); + if (isset($address[0]) && isset($address[1]) && $address[0] == $address[1]) {//直辖市:北京市北京市朝阳区 + array_shift($address); + } + $p = array_shift($address); + if (mb_strlen($p) - 1 === mb_strpos($p, '市')) { + $p = mb_substr($p, 0, -1); + } elseif (mb_strlen($p) - 1 === mb_strpos($p, '省')) { + $p = mb_substr($p, 0, -1); + } elseif (mb_strlen($p) - 3 === mb_strpos($p, '自治区')) { + $p = mb_substr($p, 0, -3); + } + $pcity = $this->getModel()->where('name', $p)->value('id'); + $path = ['', $pcity]; + $street = $p; + $i = 0; + foreach ($address as $item) { + //县级市,只有三级地址;市和县相同 + if ($item == ($address[$i-1] ?? '')) continue; + $pcity = $this->getModel()->whereLike('path', implode('/', $path) . '/%')->where('name', $item)->value('id'); + if (!$pcity) { + break; + } + $path[] = $pcity; + $street = $item; + $i++; + } + array_pop($path); + $query->whereLike('path', implode('/', $path) . '/%')->where('name', $street); + }); + + } + + /** + * 搜索某个地址 + * @param array $where + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function searchCity(array $where) + { + return $this->search($where)->order('id DESC')->find(); + } + + /** + * 获取地址 + * @param array $where + * @param string $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCityList(array $where, string $field = '*', array $with = []) + { + return $this->getModel()->where($where)->field($field)->with($with)->order('id asc')->select()->toArray(); + } + + /** + * 删除上级城市和当前城市id + * @param int $cityId + * @return bool + * @throws \Exception + */ + public function deleteCity(int $cityId) + { + return $this->getModel()->where('id', $cityId)->whereOr('parent_id', $cityId)->delete(); + } +} diff --git a/app/dao/other/ExpressDao.php b/app/dao/other/ExpressDao.php new file mode 100644 index 0000000..624dca2 --- /dev/null +++ b/app/dao/other/ExpressDao.php @@ -0,0 +1,76 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\other; + +use app\dao\BaseDao; +use app\common\model\store\shipping\Express; + +/** + * 物流信息 + * Class ExpressDao + * @package app\dao\other + */ +class ExpressDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return Express::class; + } + + /** + * 获取物流列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExpressList(array $where, string $field, int $page, int $limit) + { + return $this->search($where)->field($field)->order('sort DESC,id DESC') + ->when($page > 0 && $limit > 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 指定的条件获取物流信息以数组返回 + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getExpress(array $where, string $field, string $key) + { + return $this->search($where)->order('id DESC')->column($field, $key); + } + + /** + * 通过code获取一条信息 + * @param string $code + * @param string $field + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExpressByCode(string $code, string $field = '*') + { + return $this->getModel()->field($field)->where('code', $code)->find(); + } +} diff --git a/app/dao/other/QrcodeDao.php b/app/dao/other/QrcodeDao.php new file mode 100644 index 0000000..0c725f7 --- /dev/null +++ b/app/dao/other/QrcodeDao.php @@ -0,0 +1,59 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\other; + +use app\dao\BaseDao; +use app\model\other\Qrcode; + +/** + * + * Class QrcodeDao + * @package app\dao\other + */ +class QrcodeDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return Qrcode::class; + } + + /** + * 获取一条二维码 + * @param $id + * @param string $type + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getQrcode($id, $type = 'id') + { + return $this->getModel()->where($type, $id)->find(); + } + + /** + * 修改二维码使用状态 + * @param $id + * @param string $type + * @return mixed + */ + public function scanQrcode($id, $type = 'id') + { + return $this->getModel()->where($type, $id)->inc('scan')->update(); + } +} diff --git a/app/dao/other/SystemCityDao.php b/app/dao/other/SystemCityDao.php new file mode 100644 index 0000000..adc3195 --- /dev/null +++ b/app/dao/other/SystemCityDao.php @@ -0,0 +1,90 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\other; + + +use app\dao\BaseDao; +use app\common\model\store\shipping\City as SystemCity; + +/** + * 城市数据 + * Class SystemCityDao + * @package app\dao\other + */ +class SystemCityDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SystemCity::class; + } + + /** + * 获取城市数据列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCityList(array $where, string $field = '*') + { + return $this->search($where)->field($field)->select()->toArray(); + } + + /** + * 获取城市数据以数组形式返回 + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getCityArray(array $where, string $field, string $key) + { + return $this->search($where)->column($field, $key); + } + + /** + * 删除上级城市和当前城市id + * @param int $cityId + * @return bool + * @throws \Exception + */ + public function deleteCity(int $cityId) + { + return $this->getModel()->where('city_id', $cityId)->whereOr('parent_id', $cityId)->delete(); + } + + /** + * 获取city_id的最大值 + * @return mixed + */ + public function getCityIdMax() + { + return $this->getModel()->max('city_id'); + } + + /** + * 获取运费模板城市选择 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getShippingCity() + { + return $this->getModel()->with('children')->where('parent_id', 0)->order('id asc')->select()->toArray(); + } +} diff --git a/app/dao/other/queue/QueueAuxiliaryDao.php b/app/dao/other/queue/QueueAuxiliaryDao.php new file mode 100644 index 0000000..aca9e6e --- /dev/null +++ b/app/dao/other/queue/QueueAuxiliaryDao.php @@ -0,0 +1,108 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\other\queue; + + +use app\dao\BaseDao; +use app\model\other\queue\QueueAuxiliary; + +/** + * 队列辅助 + * Class QueueAuxiliaryDao + * @package app\dao\other\queue + */ +class QueueAuxiliaryDao extends BaseDao +{ + + /** + * @return string + */ + protected function setModel(): string + { + return QueueAuxiliary::class; + } + + /** + * 添加订单缓存记录 + * @param array $data + * @return int|string + */ + public function saveOrderCacheLog(array $data) + { + return $this->getModel()->insertGetId($data); + } + + /** + * 获取发货缓存数据列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderExpreList(array $where, int $page = 0, int $limit = 0) + { + return $this->search($where)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time asc')->select()->toArray(); + } + + /** + * 查询单条订单缓存数据 + * @param array $where + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderCacheOne(array $where) + { + return $this->search($where)->find(); + } + + /** + * 获取发货记录 + * @param array $where + * @param int $page + * @param int $limit + * @param string $order + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function deliveryLogList(array $where, int $page = 0, int $limit = 0, string $order = '') + { + foreach ($where as $k => $v) { + if ($v == "") unset($where[$k]); + } + return $this->search($where) + ->order(($order ? $order . ' ,' : '') . 'add_time desc') + ->page($page, $limit)->select()->toArray(); + } + + /** + * 获取某个队列的数据缓存 + * @param $bindingId + * @param $type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCacheOidList($bindingId, $type) + { + return $this->search(['binding_id' => $bindingId, 'type' => $type])->select()->toArray(); + } +} diff --git a/app/dao/other/queue/QueueDao.php b/app/dao/other/queue/QueueDao.php new file mode 100644 index 0000000..286cb45 --- /dev/null +++ b/app/dao/other/queue/QueueDao.php @@ -0,0 +1,128 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\other\queue; + + +use app\dao\BaseDao; +use app\model\other\queue\Queue; + +/** + * 队列 + * Class QueueDao + * @package app\dao\other + */ +class QueueDao extends BaseDao +{ + + /** + * @return string + */ + public function setModel(): string + { + return Queue::class; + } + + + /** + * 队列任务列表 + * @param array $where + * @param int $page + * @param int $limit + * @param string $order + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page = 0, int $limit = 0, string $order = '') + { + foreach ($where as $k => $v) { + if ($v == "") unset($where[$k]); + } + return $this->search($where) + ->order(($order ? $order . ' ,' : '') . 'add_time desc') + ->page($page, $limit)->select()->toArray(); + } + + /** + * 获取单个队列详情 + * @param array $where + * @return array|bool|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getQueueOne(array $where) + { + if (!$where) return false; + return $this->search($where)->order('id desc')->find(); + } + + /** + * 加入队列数据表 + * @param string $queueName + * @param int $queueDataNum + * @param int $type + * @param string $redisKey + * @param string $source + * @return mixed + */ + public function addQueueList(string $queueName, int $queueDataNum, int $type, string $redisKey, string $source = "admin") + { + $data = [ + 'type' => $type, + 'source' => $source, + 'execute_key' => $redisKey ? $redisKey : '', + 'title' => $queueName, + 'status' => 0, + 'surplus_num' => $queueDataNum, + 'total_num' => $queueDataNum, + 'add_time' => time(), + ]; + return $this->getModel()->insertGetId($data); + } + + + /** + * 将队列置为正在执行状态 + * @param $queueInValue + * @param $queueId + * @return bool|mixed + */ + public function setQueueDoing($queueInValue, $queueId, bool $is_again = false) + { + $saveData['queue_in_value'] = is_array($queueInValue) ? json_encode($queueInValue) : $queueInValue; + $saveData['status'] = 1; + if ($is_again) { + $saveData['again_time'] = time(); + } else { + $saveData['first_time'] = time(); + } + return $this->getModel()->update($saveData, ['id' => $queueId]); + } + + /** + * 停止队列 + * @param $queueId + * @return \crmeb\basic\BaseModel + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function stopWrongQueue($queueId) + { + $queueInfo = $this->getModel()->where(['id' => $queueId])->find(); + if (!$queueInfo) return false; + return $this->getModel()->update(['id' => $queueId], ['status' => 3]); + } + +} diff --git a/app/dao/product/category/StoreProductCategoryDao.php b/app/dao/product/category/StoreProductCategoryDao.php new file mode 100644 index 0000000..3abd72d --- /dev/null +++ b/app/dao/product/category/StoreProductCategoryDao.php @@ -0,0 +1,238 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\category; + +use app\dao\BaseDao; +use app\model\product\category\StoreProductCategory; + +/** + * Class StoreProductCategoryDao + * @package app\dao\product\product + */ +class StoreProductCategoryDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductCategory::class; + } + + /** + * 获取分类列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->with('children')->when($page && $limit, function ($query) use($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id desc')->select()->toArray(); + } + + /** + * + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTierList(array $where = [], array $field = ['*']) + { + return $this->search($where)->field($field)->order('sort desc,id desc')->select()->toArray(); + } + + /** + * 添加修改选择上级分类列表 + * @param array $where + * @return array + */ + public function getMenus(array $where) + { + return $this->search($where)->order('sort desc,id desc')->column('cate_name,id'); + } + + /** + * 根据id获取分类 + * @param string $cateIds + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCateArray(string $cateIds) + { + return $this->search(['id' => $cateIds])->field('cate_name,id')->select()->toArray(); + } + + /** + * 前端分类页面分离列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCategory() + { + return $this->getModel()->with('children')->where('is_show', 1)->where('pid', 0)->order('sort desc,id desc')->hidden(['add_time', 'is_show', 'sort', 'children.sort', 'children.add_time', 'children.pid', 'children.is_show'])->select()->toArray(); + } + + /** + * 根据分类id获取上级id + * @param array $cateId + * @return array + */ + public function cateIdByPid(array $cateId) + { + return $this->getModel()->whereIn('id', $cateId)->column('pid'); + } + + /** + * 获取首页展示的二级分类 排序默认降序 + * @param int $limit + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function byIndexList($limit = 4, $field = 'id,cate_name,pid,pic') + { + return $this->getModel()->where('pid', '>', 0)->where('is_show', 1)->field($field)->order('sort DESC')->limit($limit)->select()->toArray(); + } + + /** + * 获取一级分类和二级分类组成的集合 + * @param $cateId + * @return mixed + */ + public function getCateParentAndChildName(string $cateId) + { + return $this->getModel()->where('id', 'IN', $cateId)->field('cate_name as two,id,pid') + ->with(['parentCate' => function ($query) { + $query->field('id,pid,cate_name')->bind(['one' => 'cate_name']); + }])->select()->toArray(); + } + + /** + * 按照个数获取一级分类下有商品的分类ID + * @param $page + * @param $limit + * @return array + */ + public function getCid($page, $limit) + { + return $this->getModel() + ->where('is_show', 1) + ->where('pid', 0) + ->where('id', 'in', function ($query) { + $query->name('store_product_relation')->where('type', 1)->where('status', 1)->group('relation_pid')->field('relation_pid')->select()->toArray(); + }) + ->page($page, $limit) + ->order('sort DESC,id DESC') + ->select()->toArray(); + } + + /** + * 按照个数获取一级分类下有商品的分类个数 + * @param $page + * @param $limit + * @return int + */ + public function getCidCount() + { + return $this->getModel() + ->where('is_show', 1) + ->where('pid', 0) + ->where('id', 'in', function ($query) { + $query->name('store_product_relation')->where('type', 1)->where('status', 1)->group('relation_pid')->field('relation_pid')->select()->toArray(); + }) + ->count(); + } + + /** + * 通过分类id 获取(自己以及下级)的所有分类 + * @param int $id + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAllById(int $id, string $field = 'id') + { + return $this->getModel()->where(function ($query) use ($id) { + $query->where(function ($q) use ($id) { + $q->where('id', $id)->whereOr('pid', $id); + }); + })->where('is_show', 1)->field($field)->select()->toArray(); + } + + /** + * 通过分类ids 获取(自己以及下级)的所有分类 + * @param int $ids + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAllByIds(array $ids, string $field = 'id') + { + return $this->getModel()->where(function ($query) use ($ids) { + $query->where(function ($q) use ($ids) { + $q->where('id', 'in', $ids)->whereOr('pid', 'in', $ids); + }); + })->where('is_show', 1)->field($field)->select()->toArray(); + } + + /** + * 可以搜索的获取所有二级分类 + * @param array $where + * @param string $field + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getALlByIndex(array $where, string $field = 'id,cate_name,pid,pic', $limit = 0) + { + $pid = $where['pid'] ?? -1; + return $this->getModel()->where('is_show', 1)->field($field) + ->when(in_array($pid, [0, -1]), function ($query) use ($pid) { + switch ($pid) { + case -1://所有一级 + $query->where('pid', 0); + break; + case 0://所有二级 + $query->where('pid', '>', 0); + break; + } + })->when((int)$pid > 0, function ($query) use ($pid) { + $query->where('pid', $pid); + })->when(isset($where['name']) && $where['name'], function ($query) use ($where) { + $query->whereLike('id|cate_name', '%' . $where['name'] . '%'); + })->when($limit > 0, function ($query) use ($limit) { + $query->limit($limit); + })->order('sort DESC')->select()->toArray(); + } +} diff --git a/app/dao/product/ensure/StoreProductEnsureDao.php b/app/dao/product/ensure/StoreProductEnsureDao.php new file mode 100644 index 0000000..9448e85 --- /dev/null +++ b/app/dao/product/ensure/StoreProductEnsureDao.php @@ -0,0 +1,55 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\ensure; + + +use app\dao\BaseDao; +use app\model\product\ensure\StoreProductEnsure; + + +/** + * 商品保障服务 + * Class StoreProductEnsureDao + * @package app\dao\product\label + */ +class StoreProductEnsureDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductEnsure::class; + } + + /** + * 获取保障服务列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id desc')->select()->toArray(); + } + + +} diff --git a/app/dao/product/label/StoreProductLabelAuxiliaryDao.php b/app/dao/product/label/StoreProductLabelAuxiliaryDao.php new file mode 100644 index 0000000..6b43631 --- /dev/null +++ b/app/dao/product/label/StoreProductLabelAuxiliaryDao.php @@ -0,0 +1,32 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\label; + + +use app\dao\BaseDao; +use app\model\product\label\StoreProductLabelAuxiliary; + +/** + * Class StoreProductLabelAuxiliaryDao + * @package app\dao\product\label + */ +class StoreProductLabelAuxiliaryDao extends BaseDao +{ + + /** + * @return string + */ + protected function setModel(): string + { + return StoreProductLabelAuxiliary::class; + } +} diff --git a/app/dao/product/label/StoreProductLabelDao.php b/app/dao/product/label/StoreProductLabelDao.php new file mode 100644 index 0000000..e8ae3fd --- /dev/null +++ b/app/dao/product/label/StoreProductLabelDao.php @@ -0,0 +1,53 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\label; + + +use app\dao\BaseDao; +use app\common\model\store\product\ProductLabel as StoreProductLabel; + + +/** + * Class StoreProductLabelDao + * @package app\dao\product\label + */ +class StoreProductLabelDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductLabel::class; + } + + /** + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + +} diff --git a/app/dao/product/product/StoreDescriptionDao.php b/app/dao/product/product/StoreDescriptionDao.php new file mode 100644 index 0000000..3474faa --- /dev/null +++ b/app/dao/product/product/StoreDescriptionDao.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\product; + +use app\dao\BaseDao; +use app\model\product\product\StoreDescription; + +/** + * Class StoreDescriptionDao + * @package app\dao\product\product + */ +class StoreDescriptionDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreDescription::class; + } + + /** + * 根据条件获取商品详情 + * @param array $where + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDescription(array $where) + { + return $this->getOne($where); + } +} diff --git a/app/dao/product/product/StoreProductCategoryBrandDao.php b/app/dao/product/product/StoreProductCategoryBrandDao.php new file mode 100644 index 0000000..252accf --- /dev/null +++ b/app/dao/product/product/StoreProductCategoryBrandDao.php @@ -0,0 +1,78 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\product; + +use app\dao\BaseDao; +use app\model\product\product\StoreProductCategoryBrand; +use app\model\product\product\StoreProductRelation; + +/** + * 商品关联关系dao + * Class StoreProductCategoryBrandDao + * @package app\dao\product\product + */ +class StoreProductCategoryBrandDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductCategoryBrand::class; + } + + /** + * 获取所有的分销员等级 + * @param array $where + * @param string $field + * @param array $with + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where = [], string $field = '*', array $with = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field) + ->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 保存数据 + * @param array $data + * @return mixed|void + */ + public function saveAll(array $data) + { + $this->getModel()->insertAll($data); + } + + /** + * 设置 + * @param array $ids + * @param int $is_show + * @param string $key + * @return \crmeb\basic\BaseModel + */ + public function setShow(array $ids, int $is_show = 1, string $key = 'product_id') + { + return $this->getModel()->whereIn($key, $ids)->update(['status' => $is_show]); + } + +} diff --git a/app/dao/product/product/StoreProductDao.php b/app/dao/product/product/StoreProductDao.php new file mode 100644 index 0000000..54a88e7 --- /dev/null +++ b/app/dao/product/product/StoreProductDao.php @@ -0,0 +1,476 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\product; + + +use app\dao\BaseDao; +use app\common\model\store\product\Product as StoreProduct; +use think\facade\Config; + +/** + * Class StoreProductDao + * @package app\dao\product\product + */ +class StoreProductDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProduct::class; + } + + + /** + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where) + ->when(isset($where['collate_code_id']) && $where['collate_code_id'], function ($query) use ($where) { + $query->where('product_type', 0)->where('is_presale_product', 0)->where('system_form_id', 0); + })->when(isset($where['is_integral']) && $where['is_integral'], function ($query) use ($where) { + $query->where('product_type', 'in', [0, 1, 2, 3]); + }) + ->when(isset($where['store_name']) && $where['store_name'], function ($query) use ($where) { + if (isset($where['field_key']) && $where['field_key'] && in_array($where['field_key'], ['product_id', 'bar_code'])) { + if ($where['field_key'] == 'product_id') { + $query->where('id', trim($where['store_name'])); + } else { + $query->where(function ($query) use ($where) { + $query->where('bar_code', trim($where['store_name']))->whereOr('id', 'IN', function ($q) use ($where) { + $q->name('store_product_attr_value')->field('product_id')->where('bar_code', trim($where['store_name']))->select(); + }); + }); + } + } else { + $query->where(function ($q) use ($where) { + $q->where('id|store_name|bar_code|keyword', 'LIKE', '%' . trim($where['store_name']) . '%')->whereOr('id', 'IN', function ($q) use ($where) { + $q->name('store_product_attr_value')->field('product_id')->where('bar_code', trim($where['store_name']))->select(); + }); + }); + } + })->when(isset($where['sid']) && $where['sid'], function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('store_product_relation')->where('type', 1)->where('relation_id', $where['sid'])->field('product_id')->select(); + }); + })->when(isset($where['cid']) && $where['cid'], function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('store_product_relation')->where('type', 1)->whereIn('relation_id', function ($query) use ($where) { + $query->name('store_product_category')->where('id|pid', $where['cid'])->field('id')->select(); + })->field('product_id')->select(); + }); + })->when(isset($where['brand_id']) && $where['brand_id'], function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('store_product_relation')->where('type', 2)->whereIn('relation_id', $where['brand_id'])->field('product_id')->select(); + }); + })->when(isset($where['store_label_id']) && $where['store_label_id'], function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('store_product_relation')->where('type', 3)->whereIn('relation_id', $where['store_label_id'])->field('product_id')->select(); + }); + })->when(isset($where['is_live']) && $where['is_live'] == 1, function ($query) use ($where) { + $query->whereNotIn('id', function ($query) { + $query->name('live_goods')->where('is_del', 0)->where('audit_status', '<>', 3)->field('product_id')->select(); + }); + })->when(isset($where['is_supplier']) && in_array((int)$where['is_supplier'], [0, 1]), function ($query) use ($where) { + if ($where['is_supplier'] == 1) {//查询供应商商品 + $query->where('type', 2); + } else { + $query->where('type', '<>', 2); + } + }); + } + + /** + * 条件获取数量 + * @param array $where + * @return int + */ + public function getCount(array $where) + { + return $this->search($where) + ->when(isset($where['unit_name']) && $where['unit_name'] !== '', function ($query) use ($where) { + $query->where('unit_name', $where['unit_name']); + })->when(isset($where['ids']) && $where['ids'], function ($query) use ($where) { + if (!isset($where['type'])) $query->where('id', 'in', $where['ids']); + })->when(isset($where['not_ids']) && $where['not_ids'], function ($query) use ($where) { + $query->whereNotIn('id', $where['not_ids']); + })->when(isset($where['pids']) && $where['pids'], function ($query) use ($where) { + if ((isset($where['priceOrder']) && $where['priceOrder'] != '') || (isset($where['salesOrder']) && $where['salesOrder'] != '')) { + $query->whereIn('pid', $where['pids']); + } else { + $query->whereIn('pid', $where['pids'])->orderField('pid', $where['pids'], 'asc'); + } + })->when(isset($where['not_pids']) && $where['not_pids'], function ($query) use ($where) { + $query->whereNotIn('pid', $where['not_pids']); + })->count(); + } + + /** + * 获取商品列表 + * @param array $where + * @param int $page + * @param int $limit + * @param string $order + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page = 0, int $limit = 0, string $order = '', array $with = []) + { + $prefix = Config::get('database.connections.' . Config::get('database.default') . '.prefix'); + return $this->search($where)->order(($order ? $order . ' ,' : '') . 'sort desc,id desc') + ->when(count($with), function ($query) use ($with) { + $query->with($with); + })->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(isset($where['store_id']) && $where['store_id'], function ($query) use ($where) { + $query->with(['storeBranchProduct' => function ($querys) use ($where) { + $querys->where('store_id', $where['store_id'])->bind([ + 'branch_sales' => 'sales', + 'branch_stock' => 'stock', + 'branch_is_show' => 'is_show' + ]); + }]); + })->when(isset($where['ids']), function ($query) use ($where) { + $query->where('id', 'in', $where['ids']); + })->field([ + '*', + '(SELECT count(*) FROM `' . $prefix . 'user_relation` WHERE `relation_id` = `' . $prefix . 'store_product`.`id` AND `category` = \'product\' AND `type` = \'collect\') as collect', + '(SELECT count(*) FROM `' . $prefix . 'user_relation` WHERE `relation_id` = `' . $prefix . 'store_product`.`id` AND `category` = \'product\' AND `type` = \'like\') as likes', + '(SELECT SUM(stock) FROM `' . $prefix . 'store_product_attr_value` WHERE `product_id` = `' . $prefix . 'store_product`.`id` AND `type` = 0) as stock', + // '(SELECT SUM(sales) FROM `' . $prefix . 'store_product_attr_value` WHERE `product_id` = `' . $prefix . 'store_product`.`id` AND `type` = 0) as sales', + '(SELECT count(*) FROM `' . $prefix . 'store_visit` WHERE `product_id` = `' . $prefix . 'store_product`.`id` AND `product_type` = \'product\') as visitor', + ])->select()->toArray(); + } + + /** + * 获取商品详情 + * @param int $id + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getInfo(int $id) + { + return $this->search()->with('coupons')->find($id); + } + + /** + * 获取门店商品 + * @param $where + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getBranchProduct($where) + { + return $this->search($where)->find(); + } + + /** + * 条件获取商品列表 + * @param array $where + * @param int $page + * @param int $limit + * @param array $field + * @param string $order + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSearchList(array $where, int $page = 0, int $limit = 0, array $field = ['*'], string $order = '', array $with = ['couponId', 'descriptions']) + { + if (isset($where['star'])) $with[] = 'star'; + return $this->search($where)->with($with)->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(isset($where['ids']) && $where['ids'], function ($query) use ($where) { + if ((isset($where['priceOrder']) && $where['priceOrder'] != '') || (isset($where['salesOrder']) && $where['salesOrder'] != '')) { + $query->whereIn('id', $where['ids']); + } else { + $query->whereIn('id', $where['ids'])->orderField('id', $where['ids'], 'asc'); + } + })->when(isset($where['not_ids']) && $where['not_ids'], function ($query) use ($where) { + $query->whereNotIn('id', $where['not_ids']); + })->when(isset($where['pids']) && $where['pids'], function ($query) use ($where) { + if ((isset($where['priceOrder']) && $where['priceOrder'] != '') || (isset($where['salesOrder']) && $where['salesOrder'] != '')) { + $query->whereIn('pid', $where['pids']); + } else { + $query->whereIn('pid', $where['pids'])->orderField('pid', $where['pids'], 'asc'); + } + })->when(isset($where['not_pids']) && $where['not_pids'], function ($query) use ($where) { + $query->whereNotIn('pid', $where['not_pids']); + })->when(isset($where['priceOrder']) && $where['priceOrder'] != '', function ($query) use ($where) { + if ($where['priceOrder'] === 'desc') { + $query->order("price desc"); + } else { + $query->order("price asc"); + } + })->when(isset($where['salesOrder']) && $where['salesOrder'] != '', function ($query) use ($where) { + if ($where['salesOrder'] === 'desc') { + $query->order("sales desc"); + } else { + $query->order("sales asc"); + } + })->when(!isset($where['ids']), function ($query) use ($where, $order) { + if (isset($where['timeOrder']) && $where['timeOrder'] == 1) { + $query->order('id desc'); + } else if ($order == 'rand') { + $query->orderRand(); + } else if ($order) { + $query->orderRaw($order); + } else { + $query->order('sort desc,id desc'); + } + })->when(isset($where['use_min_price']) && $where['use_min_price'], function ($query) use ($where) { + if (is_array($where['use_min_price']) && count($where['use_min_price']) == 2) { + $query->where('price', $where['use_min_price'][0] ?? '=', $where['use_min_price'][1] ?? 0); + } + })->when(!$page && $limit, function ($query) use ($limit) { + $query->limit($limit); + })->field($field)->select()->toArray(); + } + + /**商品列表 + * @param array $where + * @param $limit + * @param $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductLimit(array $where, $limit, $field) + { + return $this->search($where)->field($field)->order('val', 'desc')->limit($limit)->select()->toArray(); + + } + + /** + * 根据id获取商品数据 + * @param array $ids + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function idByProductList(array $ids, string $field) + { + return $this->getModel()->whereIn('id', $ids)->field($field)->select()->toArray(); + } + + /** + * 获取推荐商品 + * @param array $where + * @param array $field + * @param int $num + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRecommendProduct(array $where, array $field = ['*'], int $num = 0, int $page = 0, int $limit = 0, array $with = ['couponId', 'star']) + { + return $this->search($where)->field($field) + ->when(count($with), function ($query) use ($with) { + $query->with($with); + })->when($num, function ($query) use ($num) { + $query->limit($num); + })->when($page, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when($limit, function ($query) use ($limit) { + $query->limit($limit); + })->order('sort DESC, id DESC')->select()->toArray(); + } + + /** + * 获取加入购物车的商品 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductCartList(array $where, int $page, int $limit, array $field = ['*']) + { + $where['is_verify'] = 1; + $where['is_show'] = 1; + $where['is_del'] = 0; + return $this->search($where)->when($page, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->field($field)->order('sort DESC,id DESC')->select()->toArray(); + } + + /** + * 获取用户购买热销榜单 + * @param array $where + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserProductHotSale(array $where, int $limit = 20) + { + return $this->search($where)->field(['IFNULL(sales,0) + IFNULL(ficti,0) as sales', 'store_name', 'image', 'id', 'price', 'ot_price', 'stock'])->limit($limit)->order('sales desc')->select()->toArray(); + } + + /** + * 通过商品id获取商品分类 + * @param array $productIds + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function productIdByCateId(array $productIds) + { + return $this->search(['id' => $productIds])->with('cateName')->field('id')->select()->toArray(); + } + + /** + * @param array $where + * @param $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductListByWhere(array $where, $field) + { + return $this->search($where)->field($field)->select()->toArray(); + } + + /** + * 搜索条件获取字段column + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getColumnList(array $where, string $field = 'brand_id', string $key = 'id') + { + return $this->search($where) + ->when(isset($where['sid']) && $where['sid'], function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('store_product_relation')->where('type', 1)->where('relation_id', $where['sid'])->field('product_id')->select(); + }); + })->when(isset($where['cid']) && $where['cid'], function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('store_product_relation')->where('type', 1)->whereIn('relation_id', function ($query) use ($where) { + $query->name('store_product_category')->where('id|pid', $where['cid'])->field('id')->select(); + })->field('product_id')->select(); + }); + })->when(isset($where['ids']) && $where['ids'], function ($query) use ($where) { + $query->whereIn('id', $where['ids']); + })->field($field)->column($field, $key); + } + + /** + * 自动上下架 + * @param int $is_show + * @return \crmeb\basic\BaseModel + */ + public function overUpperShelves($is_show = 0) + { + return $this->getModel()->where(['is_del' => 0])->where('is_verify', 1) + ->when(in_array($is_show, [0, 1]), function ($query) use ($is_show) { + if ($is_show == 1) { + $query->where('is_show', 0)->where('auto_on_time', '<>', 0)->where('auto_on_time', '<=', time()); + } else { + $query->where('is_show', 1)->where('auto_off_time', '<>', 0)->where('auto_off_time', '<', time()); + } + })->update(['is_show' => $is_show]); + } + + /** + * 获取预售列表 + * @param array $where + * @param int $page + * @param int $limit + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPresaleList(array $where, int $page, int $limit, string $field = '*') + { + $model = $this->getModel()->field($field) + ->where('is_presale_product', 1)->where('is_del', 0)->where('is_show', 1) + ->where(function ($query) use ($where) { + switch ($where['time_type']) { + case 1: + $query->where('presale_start_time', '>', time()); + break; + case 2: + $query->where('presale_start_time', '<=', time())->where('presale_end_time', '>=', time()); + break; + case 3: + $query->where('presale_end_time', '<', time()); + break; + } + if ($where['type']) $query->whereIn('type', $where['type']); + }); + $count = $model->count(); + $list = $model->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc')->select()->toArray(); + return compact('list', 'count'); + } + + /** + * 获取使用某服务保障商品数量 + * @param int $ensure_id + * @return int + */ + public function getUseEnsureCount(int $ensure_id) + { + return $this->getModel()->whereFindInSet('ensure_id', $ensure_id)->count(); + } + + /** + * 保存数据 + * @param array $data + * @return mixed|\think\Collection + * @throws \Exception + */ + public function saveAll(array $data) + { + return $this->getModel()->saveAll($data); + } + + /** + * 同步商品保存获取id + * @param $data + * @return int|string + */ + public function ErpProductSave($data) + { + return $this->getModel()->insertGetId($data); + } +} diff --git a/app/dao/product/product/StoreProductLogDao.php b/app/dao/product/product/StoreProductLogDao.php new file mode 100644 index 0000000..36987c6 --- /dev/null +++ b/app/dao/product/product/StoreProductLogDao.php @@ -0,0 +1,79 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\product; + + +use app\dao\BaseDao; +use app\model\product\product\StoreProductLog; +use think\facade\Config; + +class StoreProductLogDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductLog::class; + } + + public function getRanking($where) + { +// $prefix = Config::get('database.connections.' . Config::get('database.default') . '.prefix'); + $sortField = $where['sort'] ?? 'visit'; + return $this->search($where)->with('storeName') + ->field([ + 'product_id', + 'SUM(visit_num) as visit', + 'COUNT(distinct(uid)) as user', + 'SUM(cart_num) as cart', + 'SUM(order_num) as orders', + 'SUM(pay_num) as pay', + 'SUM(pay_price * pay_num) as price', + 'SUM(cost_price) as cost', + 'ROUND((SUM(pay_price)-SUM(cost_price))/SUM(cost_price),2) as profit', + 'SUM(collect_num) as collect', + 'ROUND((COUNT(distinct(pay_uid))-1)/COUNT(distinct(uid)),2) as changes', +// '(select count(*) from (SELECT COUNT(`pay_uid`) as p,product_id FROM `' . $prefix . 'store_product_log` WHERE `product_id` = `' . $prefix . 'store_product_log`.`product_id` AND `type` = \'pay\' GROUP BY `pay_uid` HAVING p>1) u WHERE `product_id` = `' . $prefix . 'store_product_log`.`product_id`) as aaaa', + 'COUNT(distinct(pay_uid))-1 as repeats' + //->having($sortField . ' > 0') + ])->group('product_id')->order($sortField . ' desc')->limit(20)->select()->toArray(); + } + + public function getRepeats($where, $product_id) + { + return count($this->search($where)->where('type', 'pay')->where('product_id', $product_id)->field('count(pay_uid) as p')->group('pay_uid')->having('p>1')->select()); + } + + /** + * 列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param string $group + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, string $group = '') + { + return $this->search($where)->with(['storeName'])->field($field) + ->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when($group, function ($query) use ($group) { + $query->group($group); + })->order('add_time desc')->select()->toArray(); + } +} diff --git a/app/dao/product/product/StoreProductRelationDao.php b/app/dao/product/product/StoreProductRelationDao.php new file mode 100644 index 0000000..7f093a9 --- /dev/null +++ b/app/dao/product/product/StoreProductRelationDao.php @@ -0,0 +1,105 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\product; + +use app\dao\BaseDao; +use app\model\product\product\StoreProductRelation; + +/** + * 商品关联关系dao + * Class StoreProductRelationDao + * @package app\dao\product\product + */ +class StoreProductRelationDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductRelation::class; + } + + /** + * 获取所有 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where = [], string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 保存数据 + * @param array $data + * @return mixed|void + */ + public function saveAll(array $data) + { + $this->getModel()->insertAll($data); + } + + /** + * 根据商品id获取分类id + * @param array $productId + * @return array + */ + public function productIdByCateId(array $productId) + { + return $this->getModel()->whereIn('product_id', $productId)->where('type', 1)->column('relation_id'); + } + + /** + * 根据分类获取商品id + * @param array $cate_id + * @return array + */ + public function cateIdByProduct(array $cate_id) + { + return $this->getModel()->whereIn('relation_id', $cate_id)->where('type', 1)->column('product_id'); + } + + /** + * 设置 + * @param array $ids + * @param int $is_show + * @param int $type + * @param string $key + * @return \crmeb\basic\BaseModel + */ + public function setShow(array $ids, int $is_show = 1, int $type = 1, string $key = 'product_id') + { + return $this->getModel()->whereIn($key, $ids)->where('type', $type)->update(['status' => $is_show]); + } + + /** + * 根据条件获取商品ID, 或者relation_id + * @param array $where + * @param string $field + * @return array + */ + public function getIdsByWhere(array $where, string $field = 'product_id') + { + return $this->search($where)->column($field); + } +} diff --git a/app/dao/product/product/StoreProductReplyCommentDao.php b/app/dao/product/product/StoreProductReplyCommentDao.php new file mode 100644 index 0000000..55e2918 --- /dev/null +++ b/app/dao/product/product/StoreProductReplyCommentDao.php @@ -0,0 +1,55 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\product; + + +use app\dao\BaseDao; +use app\model\product\product\StoreProductReplyComment; +use crmeb\traits\SearchDaoTrait; + +/** + * Class StoreProductReplyCommentDao + * @package app\dao\product\product + */ +class StoreProductReplyCommentDao extends BaseDao +{ + + use SearchDaoTrait; + + /** + * @return string + */ + protected function setModel(): string + { + return StoreProductReplyComment::class; + } + + /** + * 更新点赞 + * @param int $id + * @return mixed + */ + public function updatePraise(int $id) + { + return $this->getModel()->where('id', $id)->inc('praise', 1)->update(); + } + + /** + * 获取评论回复条数 + * @param array $replyId + * @return mixed + */ + public function getReplyCommentCountList(array $replyId) + { + return $this->getModel()->whereIn('reply_id', $replyId)->where('pid', 0)->group('reply_id')->field(['reply_id', 'count(*) as sum'])->select()->toArray(); + } +} diff --git a/app/dao/product/product/StoreProductReplyDao.php b/app/dao/product/product/StoreProductReplyDao.php new file mode 100644 index 0000000..dba73cb --- /dev/null +++ b/app/dao/product/product/StoreProductReplyDao.php @@ -0,0 +1,124 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\product; + +use app\dao\BaseDao; +use app\common\model\store\product\ProductReply as StoreProductReply; + +/** + * Class StoreProductReplyDao + * @package app\dao\product\product + */ +class StoreProductReplyDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductReply::class; + } + + /** + * 后台评论列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function sysPage(array $where, int $page, int $limit) + { + return $this->search($where)->page($page, $limit)->select()->toArray(); + } + + /** + * 获取最近一条最好的评论 + * @param int $productId + * @param string $field + * @param int $page + * @param $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductReply(int $productId, string $field = '*', int $page = 0, $limit = 0) + { + return $this->search(['product_id' => $productId, 'is_del' => 0]) + ->field($field) + ->with(['cartInfo', 'userInfo']) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time DESC,product_score DESC,service_score DESC,add_time DESC') + ->select()->toArray(); + } + + /** + * 评论条件 + * @param int $id + * @param int $type + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function replyWhere(int $id, int $type = 0) + { + return $this->search(['product_id' => $id, 'is_del' => 0]) + ->when($type == 1, function ($query) { + $query->where('product_score', 5)->where('service_score', 5); + })->when($type == 2, function ($query) { + $query->where(function ($query0) { + $query0->where(function ($query1) { + $query1->where('product_score', '<>', 5)->whereOr('service_score', '<>', 5); + })->where(function ($query2) { + $query2->where('service_score', '>', 2)->where('product_score', '>', 2); + }); + }); + })->when($type == 3, function ($query) { + $query->where(function ($query0) { + $query0->where('product_score', '<=', 2)->whereOr('service_score', '<=', 2); + }); + }); + } + + /** + * 评论条数 + * @param int $id + * @param int $type + * @return int + */ + public function replyCount(int $id, int $type = 0) + { + return $this->replyWhere($id, $type)->count(); + } + + /** + * 评论内容 + * @param int $id + * @param int $type + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function replyList(int $id, int $type = 0, int $page = 0, int $limit = 0, array $with = ['cartInfo', 'userInfo']) + { + return $this->replyWhere($id, $type)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->where('is_del', 0)->with($with)->order('add_time desc')->select()->toArray(); + } +} diff --git a/app/dao/product/product/StoreProductStockRecordDao.php b/app/dao/product/product/StoreProductStockRecordDao.php new file mode 100644 index 0000000..53ccc24 --- /dev/null +++ b/app/dao/product/product/StoreProductStockRecordDao.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\product; + + +use app\dao\BaseDao; +use app\model\product\product\StoreProductStockRecord; + +/** + * 库存记录 + * Class StoreProductStockRecordDao + * @package app\dao\product\product + */ +class StoreProductStockRecordDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductStockRecord::class; + } + + +} diff --git a/app/dao/product/product/StoreVisitDao.php b/app/dao/product/product/StoreVisitDao.php new file mode 100644 index 0000000..a8e4d1a --- /dev/null +++ b/app/dao/product/product/StoreVisitDao.php @@ -0,0 +1,65 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\product; + +use app\dao\BaseDao; +use app\model\product\product\StoreVisit; + +/** + * Class StoreVisitDao + * @package app\dao\product\product + */ +class StoreVisitDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreVisit::class; + } + + /** + * + * @param int $uid + * @return array + */ + public function getUserVisitProductId(int $uid) + { + return $this->getModel()->where('uid', $uid)->column('product_id'); + } + + public function getSum($where, $field) + { + return $this->search($where)->sum($field); + } + + /** + * 商品趋势 + * @param $time + * @param $timeType + * @param $str + * @return mixed + */ + public function getProductTrend($time, $timeType, $str) + { + return $this->getModel()->where(function ($query) use ($time) { + if ($time[0] == $time[1]) { + $query->whereDay('add_time', $time[0]); + } else { + $time[1] = date('Y/m/d', strtotime($time[1]) + 86400); + $query->whereTime('add_time', 'between', $time); + } + })->field("FROM_UNIXTIME(add_time,'$timeType') as days,$str as num")->group('days')->select()->toArray(); + } +} diff --git a/app/dao/product/shipping/ShippingTemplatesDao.php b/app/dao/product/shipping/ShippingTemplatesDao.php new file mode 100644 index 0000000..7da0025 --- /dev/null +++ b/app/dao/product/shipping/ShippingTemplatesDao.php @@ -0,0 +1,81 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\product\shipping; + +use app\dao\BaseDao; +use app\model\product\shipping\ShippingTemplates; + +/** + * 运费模版 + * Class ShippingTemplatesDao + * @package app\dao\product\shipping + */ +class ShippingTemplatesDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return ShippingTemplates::class; + } + + /** + * 获取选择模板列表 + * @param array $where + * @return array + */ + public function getSelectList(array $where = []) + { + return $this->search($where)->order('sort DESC,id DESC')->column('id,name'); + } + + /** + * 获取 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getShippingList(array $where, int $page, int $limit) + { + return $this->search($where)->order('sort DESC,id DESC')->page($page, $limit)->select()->toArray(); + } + + /** + * 插入数据返回主键id + * @param array $data + * @return int|string + */ + public function insertGetId(array $data) + { + return $this->getModel()->insertGetId($data); + } + + /** + * 获取运费模板指定条件下的数据 + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getShippingColumn(array $where, string $field, string $key) + { + return $this->search($where)->column($field, $key); + } +} diff --git a/app/dao/product/shipping/ShippingTemplatesFreeDao.php b/app/dao/product/shipping/ShippingTemplatesFreeDao.php new file mode 100644 index 0000000..94a6419 --- /dev/null +++ b/app/dao/product/shipping/ShippingTemplatesFreeDao.php @@ -0,0 +1,124 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\shipping; + + +use app\dao\BaseDao; +use app\model\product\shipping\ShippingTemplatesFree; + +/** + * 包邮 + * Class ShippingTemplatesFreeDao + * @package app\dao\product\shipping + */ +class ShippingTemplatesFreeDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return ShippingTemplatesFree::class; + } + + /** + * 获取运费模板列表并按照指定字段进行分组 + * @param array $where + * @param string $group + * @param string $field + * @param string $key + * @return mixed + */ + public function getShippingGroupArray(array $where, string $group, string $field, string $key) + { + return $this->search($where)->group($group)->column($field, $key); + } + + /** + * 获取列表 + * @param array $where + * @param array|string[] $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getShippingList(array $where, array $field = ['*']) + { + return $this->search($where)->field($field)->select()->toArray(); + } + + /** + * 获取运费模板列表 + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getShippingArray(array $where, string $field, string $key) + { + return $this->search($where)->column($field, $key); + } + + /** + * 是否可以满足包邮 + * @param $tempId + * @param $cityid + * @param $number + * @param $price + * @param $type + * @return int + */ + public function isFree($tempId, $cityid, $number, $price, $type = 0) + { + return $this->getModel()->where('temp_id', $tempId) + ->where('city_id', $cityid) + ->when($type, function ($query) use ($type, $number) { + if ($type == 1) {//数量 + $query->where('number', '<=', $number); + } else {//重量、体积 + $query->where('number', '>=', $number); + } + })->where('price', '<=', $price)->count(); + } + + /** + * 是否包邮模版数据列表 + * @param $tempId + * @param $cityid + * @param int $price + * @param string $field + * @param string $key + * @return array + */ + public function isFreeList($tempId, $cityid, $price = 0, string $field = '*', string $key = '') + { + return $this->getModel() + ->when($cityid, function ($query) use ($cityid) { + if (is_array($cityid)) { + $query->whereIn('city_id', $cityid); + } else { + $query->where('city_id', $cityid); + } + })->when($tempId, function ($query) use ($tempId) { + if (is_array($tempId)) { + $query->whereIn('temp_id', $tempId); + } else { + $query->where('temp_id', $tempId); + } + })->when($price, function ($query) use ($price) { + $query->where('price', '<=', $price); + })->column($field, $key); + } + +} diff --git a/app/dao/product/shipping/ShippingTemplatesNoDeliveryDao.php b/app/dao/product/shipping/ShippingTemplatesNoDeliveryDao.php new file mode 100644 index 0000000..efa789f --- /dev/null +++ b/app/dao/product/shipping/ShippingTemplatesNoDeliveryDao.php @@ -0,0 +1,99 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\shipping; + + +use app\dao\BaseDao; +use app\model\product\shipping\ShippingTemplatesNoDelivery; + +/** + * 不送达 + * Class ShippingTemplatesNoDeliveryDao + * @package app\dao\shipping + */ +class ShippingTemplatesNoDeliveryDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return ShippingTemplatesNoDelivery::class; + } + + /** + * 获取运费模板列表并按照指定字段进行分组 + * @param array $where + * @param string $group + * @param string $field + * @param string $key + * @return mixed + */ + public function getShippingGroupArray(array $where, string $group, string $field, string $key) + { + return $this->search($where)->group($group)->column($field, $key); + } + + /** + * 获取列表 + * @param array $where + * @param array|string[] $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getShippingList(array $where, array $field = ['*']) + { + return $this->search($where)->field($field)->select()->toArray(); + } + + /** + * 获取运费模板列表 + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getShippingArray(array $where, string $field, string $key) + { + return $this->search($where)->column($field, $key); + } + + /** + * 是否不送达 + * @param $tempId + * @param $cityid + * @param string $field + * @param string $key + * @return array + */ + public function isNoDelivery($tempId, $cityid, string $field = 'temp_id', string $key = '') + { + return $this->getModel() + ->when($cityid, function ($query) use ($cityid) { + if (is_array($cityid)) { + $query->whereIn('city_id', $cityid); + } else { + $query->where('city_id', $cityid); + } + })->when($tempId, function ($query) use ($tempId) { + if (is_array($tempId)) { + $query->whereIn('temp_id', $tempId); + } else { + $query->where('temp_id', $tempId); + } + })->column($field, $key); + } + +} diff --git a/app/dao/product/shipping/ShippingTemplatesRegionDao.php b/app/dao/product/shipping/ShippingTemplatesRegionDao.php new file mode 100644 index 0000000..e48044c --- /dev/null +++ b/app/dao/product/shipping/ShippingTemplatesRegionDao.php @@ -0,0 +1,85 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\shipping; + + +use app\dao\BaseDao; +use app\model\product\shipping\ShippingTemplatesRegion; + +/** + * 指定邮费 + * Class ShippingTemplatesRegionDao + * @package app\dao\product\shipping + */ +class ShippingTemplatesRegionDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return ShippingTemplatesRegion::class; + } + + /** + * 获取运费模板列表并按照指定字段进行分组 + * @param array $where + * @param string $group + * @param string $field + * @param string $key + * @return mixed + */ + public function getShippingGroupArray(array $where, string $group, string $field, string $key) + { + return $this->search($where)->group($group)->column($field, $key); + } + + /** + * 获取运费模板列表 + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getShippingArray(array $where, string $field, string $key) + { + return $this->search($where)->column($field, $key); + } + + /** + * 根据运费模板id和城市id获得包邮数据列表 + * @param array $tempIds + * @param array $cityId + * @param string $field + * @param string $key + * @return array + */ + public function getTempRegionList(array $tempIds, array $cityId, string $field = '*', string $key = '*') + { + return $this->getModel()->whereIn('temp_id', $tempIds)->whereIn('city_id', $cityId)->order('city_id asc')->column($field, $key); + } + + /** + * 获取列表 + * @param array $where + * @param array|string[] $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getShippingList(array $where, array $field = ['*']) + { + return $this->search($where)->field($field)->select()->toArray(); + } +} diff --git a/app/dao/product/sku/StoreProductAttrDao.php b/app/dao/product/sku/StoreProductAttrDao.php new file mode 100644 index 0000000..199957b --- /dev/null +++ b/app/dao/product/sku/StoreProductAttrDao.php @@ -0,0 +1,80 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\sku; + +use app\dao\BaseDao; +use app\common\model\store\product\ProductAttr as StoreProductAttr; + +/** + * Class StoreProductAttrDao + * @package app\dao\product\sku + */ +class StoreProductAttrDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductAttr::class; + } + + /** + * 删除sku + * @param int $id + * @param int $type + * @return bool + * @throws \Exception + */ + public function del(int $id, int $type) + { + return $this->search(['product_id' => $id, 'type' => $type])->delete(); + } + + /** + * 保存sku + * @param array $data + * @return mixed|\think\Collection + * @throws \Exception + */ + public function saveAll(array $data) + { + return $this->getModel()->saveAll($data); + } + + /** + * 获取商品sku + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductAttr(array $where) + { + return $this->search($where)->select()->toArray(); + } + + /** + * 获取商品sku + * @param array $where + * @return \think\Collection + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductValue(array $where) + { + return $this->search($where)->select(); + } +} diff --git a/app/dao/product/sku/StoreProductAttrResultDao.php b/app/dao/product/sku/StoreProductAttrResultDao.php new file mode 100644 index 0000000..ba00560 --- /dev/null +++ b/app/dao/product/sku/StoreProductAttrResultDao.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\sku; + +use app\dao\BaseDao; +use app\model\product\sku\StoreProductAttrResult; + +/** + * Class StoreProductAttrResultDao + * @package app\dao\product\sku + */ +class StoreProductAttrResultDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductAttrResult::class; + } + + /** + * 删除指定条件下的sku + * @param int $id + * @param int $type + * @return bool + * @throws \Exception + */ + public function del(int $id, int $type) + { + return $this->search(['product_id' => $id, 'type' => $type])->delete(); + } +} diff --git a/app/dao/product/sku/StoreProductAttrValueDao.php b/app/dao/product/sku/StoreProductAttrValueDao.php new file mode 100644 index 0000000..6772274 --- /dev/null +++ b/app/dao/product/sku/StoreProductAttrValueDao.php @@ -0,0 +1,316 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\sku; + +use app\dao\BaseDao; +use app\model\product\sku\StoreProductAttrValue; + +/** + * Class StoreProductAttrValueDao + * @package app\dao\product\sku + */ +class StoreProductAttrValueDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductAttrValue::class; + } + + /** + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, $limit = 0) + { + return $this->search($where)->field($field)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id asc')->select()->toArray(); + } + + /** + * 减库存 + * @param array $where + * @param int $num + * @param string $stock + * @param string $sales + * @return bool|mixed + */ + public function decStockIncSales(array $where, int $num, string $stock = 'stock', string $sales = 'sales') + { + $isQuota = false; + if (isset($where['type']) && $where['type']) { + $isQuota = true; + if (count($where) == 2) { + unset($where['type']); + } + } + $field = $isQuota ? 'stock,quota' : 'stock'; + $product = $this->getModel()->where($where)->field($field)->find(); + if ($product) { + return $this->getModel()->where($where)->when($isQuota, function ($query) use ($num) { + $query->dec('quota', $num); + })->dec($stock, $num)->dec('sum_stock', $num)->inc($sales, $num)->update(); + } + return true; + // return $this->getModel()->where($where)->dec($stock, $num)->inc($sales, $num)->dec('sum_stock', $num)->update(); + } + + /** + * 加库存 + * @param array $where + * @param int $num + * @param string $stock + * @param string $sales + * @return bool|mixed + */ + public function incStockDecSales(array $where, int $num, string $stock = 'stock', string $sales = 'sales') + { + $isQuota = false; + if (isset($where['type']) && $where['type']) { + $isQuota = true; + if (count($where) == 2) { + unset($where['type']); + } + } + $salesOne = $this->getModel()->where($where)->value($sales); + if ($salesOne) { + $salesNum = $num; + if ($num > $salesOne) { + $salesNum = $salesOne; + } + return $this->getModel()->where($where)->when($isQuota, function ($query) use ($num) { + $query->inc('quota', $num); + })->inc($stock, $num)->inc('sum_stock', $num)->dec($sales, $salesNum)->update(); + } + return true; +// $salesOne = $this->getModel()->where($where)->value($sales); +// if ($salesOne) { +// $salesNum = $num; +// if ($num > $salesOne) { +// $salesNum = $salesOne; +// } +// return $this->getModel()->where($where)->inc($stock, $num)->inc('sum_stock', $num)->dec($sales, $salesNum)->update(); +// }; +// return true; + } + + /** + * 根据条件获取规格value + * @param array $where + * @param string $field + * @param string $key + * @param bool $search + * @return array + */ + public function getColumn(array $where, string $field = '*', string $key = 'suk', bool $search = false) + { + if ($search) { + return $this->search($where) + ->when(isset($where['store_id']) && $where['store_id'], function ($query) use ($where) { + $query->with(['storeBranch' => function ($querys) use ($where) { + $querys->where(['store_id' => $where['store_id'], 'product_id' => $where['product_id']]); + }]); + }) + ->column($field, $key); + } else { + return $this->getModel()::where($where) + ->when(isset($where['product_id']) && $where['product_id'], function ($query) use ($where, $field) { + if (is_array($where['product_id'])) { + $query->whereIn('product_id', $where['product_id']); + } else { + $query->where('product_id', $where['product_id']); + } + }) + ->column($field, $key); + } + + } + + /** + * 根据条件删除规格value + * @param int $id + * @param int $type + * @param array $suk + * @return bool + */ + public function del(int $id, int $type, array $suk = []) + { + return $this->search(['product_id' => $id, 'type' => $type, 'suk' => $suk])->delete(); + } + + /** + * 保存数据 + * @param array $data + * @return mixed|\think\Collection + * @throws \Exception + */ + public function saveAll(array $data) + { + return $this->getModel()->saveAll($data); + } + + /** + * 根据条件获取规格数据列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductAttrValue(array $where) + { + return $this->search($where)->order('id asc')->select()->toArray(); + } + + /** + * 获取属性列表 + * @return mixed + */ + public function attrValue() + { + return $this->search()->field('product_id,sum(sales * price) as val')->with(['product'])->group('product_id')->limit(20)->select()->toArray(); + } + + /** + * 获取属性库存 + * @param string $unique + * @return int + */ + public function uniqueByStock(string $unique) + { + return $this->search(['unique' => $unique])->value('stock') ?: 0; + } + + /** + * 减库存加销量减限购 + * @param array $where + * @param int $num + * @return mixed + */ + public function decStockIncSalesDecQuota(array $where, int $num) + { + return $this->getModel()->where($where)->dec('stock', $num)->dec('quota', $num)->inc('sales', $num)->update(); + } + + /** + * 根据unique获取一条规格数据 + * @param string $unique + * @param int $type + * @param string $field + * @param array $with + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function uniqueByField(string $unique, int $type = 0, string $field = '*', array $with = []) + { + return $this->search(['unique' => $unique, 'type' => $type])->field($field)->with($with)->find(); + } + + /** + * 根据商品id获取对应规格库存 + * @param int $pid + * @param int $type + * @return float + */ + public function pidBuStock(int $pid, $type = 0) + { + return $this->getModel()->where(['product_id' => $pid, 'type' => $type])->sum('stock'); + } + + /** + * 获取门店规格 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function storeBranchAttr(array $where) + { + return $this->search($where) + ->when(isset($where['store_id']) && $where['store_id'], function ($query) use ($where) { + $query->with(['storeBranch' => function ($querys) use ($where) { + $querys->where('store_id', $where['store_id']); + }]); + })->select()->toArray(); + } + + /** + * 根据条形码获取一条商品规格信息 + * @param string $bar_code + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAttrByBarCode(string $bar_code) + { + return $this->getModel()->where('bar_code', $bar_code)->order('id desc')->find(); + } + + /** + * 根据规格信息获取商品库存 + * @param array $ids + * @return array|\think\Model|null + */ + public function getProductStockByValues(array $ids) + { + return $this->getModel()->whereIn('product_id', $ids)->where('type', 0) + ->field('`product_id` AS `id`, SUM(`stock`) AS `stock`')->group("product_id")->select()->toArray(); + } + + /** + * 分组查询 + * @param string $file + * @param string $group_id + * @param array $where + * @param string $having + * @return mixed + */ + public function getGroupData(string $file, string $group_id, array $where, string $having = '') + { + return $this->getModel()->when($where, function ($query) use ($where) { + $query->where($where); + })->field($file)->group($group_id)->when($having, function ($query) use ($having) { + $query->having($having); + })->select(); + } + + /** + * 库存警戒查询 + * @author 等风来 + * @email 136327134@qq.com + * @date 2023/4/18 + * @param array $where + * @return int + * @throws \think\db\exception\DbException + */ + public function getPolice(array $where) + { + return $this->getModel()->when($where, function ($query) use ($where) { + $query->where($where); + })->count(); + } + +} diff --git a/app/dao/product/sku/StoreProductRuleDao.php b/app/dao/product/sku/StoreProductRuleDao.php new file mode 100644 index 0000000..00b1b73 --- /dev/null +++ b/app/dao/product/sku/StoreProductRuleDao.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\sku; + +use app\dao\BaseDao; +use app\model\product\sku\StoreProductRule; + +/** + * Class StoreProductRuleDao + * @package app\dao\product\sku + */ +class StoreProductRuleDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductRule::class; + } + + /** + * 获取列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where = [], int $page = 0, int $limit = 0) + { + return $this->search($where) + ->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + + /** + * 删除数据 + * @param string $ids + * @throws \Exception + */ + public function del(string $ids) + { + return $this->getModel()->whereIn('id', $ids)->delete(); + } + + + /** + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductRuleList(array $where, $field = "*"): array + { + + return $this->search($where)->field($field)->select()->toArray(); + } +} diff --git a/app/dao/product/sku/StoreProductVirtualDao.php b/app/dao/product/sku/StoreProductVirtualDao.php new file mode 100644 index 0000000..08b60f8 --- /dev/null +++ b/app/dao/product/sku/StoreProductVirtualDao.php @@ -0,0 +1,41 @@ +search($where)->field($field) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(!$page && $limit, function ($query) use ($limit) { + $query->limit($limit); + })->order('id asc')->select()->toArray(); + } +} \ No newline at end of file diff --git a/app/dao/product/specs/StoreProductSpecsDao.php b/app/dao/product/specs/StoreProductSpecsDao.php new file mode 100644 index 0000000..9841c85 --- /dev/null +++ b/app/dao/product/specs/StoreProductSpecsDao.php @@ -0,0 +1,55 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\product\specs; + + +use app\dao\BaseDao; +use app\model\product\specs\StoreProductSpecs; + + +/** + * 商品参数 + * Class StoreProductSpecsDao + * @package app\dao\product\specs + */ +class StoreProductSpecsDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreProductSpecs::class; + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id desc')->select()->toArray(); + } + + +} diff --git a/app/dao/store/DeliveryServiceDao.php b/app/dao/store/DeliveryServiceDao.php new file mode 100644 index 0000000..752e3d9 --- /dev/null +++ b/app/dao/store/DeliveryServiceDao.php @@ -0,0 +1,69 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\store; + +use app\dao\BaseDao; +use app\model\store\DeliveryService; + +/** + * 配送dao + * Class DeliveryServiceDao + * @package app\dao\store + */ +class DeliveryServiceDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return DeliveryService::class; + } + + /** + * 获取配送员列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getServiceList(array $where, int $page = 0, int $limit = 0) + { + $realName = $where['keyword'] ?? ''; + $fieldKey = $where['field_key'] ?? ''; + $fieldKey = $fieldKey == 'all' ? '' : $fieldKey; + return $this->search($where)->with('user')->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when($realName && $fieldKey && in_array($fieldKey, ['id', 'phone']), function ($query) use ($where, $realName, $fieldKey) { + $query->whereLike($fieldKey, '%' . $where['keyword'] . '%'); + })->when($realName && !$fieldKey, function ($query) use ($where) { + $query->whereLike('uid|id|nickname|phone', '%' . $where['keyword'] . '%'); + })->when(isset($where['noId']), function ($query) use ($where) { + $query->where('id', '<>', $where['noId']); + })->order('id DESC')->field('id,uid,avatar,nickname as wx_name,status,add_time,phone')->select()->toArray(); + } + + /** + * 获取配送员select + * @param array $where + * @return array + */ + public function getSelectList(array $where) + { + return $this->search($where)->field('uid,nickname')->select()->toArray(); + } + +} diff --git a/app/dao/store/StoreConfigDao.php b/app/dao/store/StoreConfigDao.php new file mode 100644 index 0000000..9ada227 --- /dev/null +++ b/app/dao/store/StoreConfigDao.php @@ -0,0 +1,48 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\store; + + +use app\dao\BaseDao; +use app\model\store\StoreConfig; + +//use crmeb\traits\SearchDaoTrait; + +/** + * Class StoreConfigDao + * @package app\dao\store + */ +class StoreConfigDao extends BaseDao +{ + +// use SearchDaoTrait; + + /** + * @return string + */ + protected function setModel(): string + { + return StoreConfig::class; + } + + /** + * 获取门店配置 + * @param array $keys + * @param int $type + * @param int $relation_id + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function searchs(array $keys, int $type = 0, int $relation_id = 0) + { + return parent::search()->whereIn('key_name', $keys)->where('type', $type)->where('relation_id', $relation_id); + } +} diff --git a/app/dao/store/StoreUserDao.php b/app/dao/store/StoreUserDao.php new file mode 100644 index 0000000..95f1ad3 --- /dev/null +++ b/app/dao/store/StoreUserDao.php @@ -0,0 +1,98 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\store; + + +use app\dao\BaseDao; +use app\model\store\StoreUser; + +/** + * 门店用户 + * Class StoreUserDao + * @package app\dao\store + */ +class StoreUserDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreUser::class; + } + + + /** + * 获取用户列表 + * @param array $where + * @param string $field + * @param array $with + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', array $with = [], int $page = 0, int $limit = 0): array + { + return $this->search($where)->field($field)->when($with, function ($query) use ($with) { + $query->with(['label']); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 获取一段时间内新增人数 + * @param array $where + * @param array $time + * @param string $timeType + * @param string $countField + * @param string $sumField + * @param string $groupField + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function userTimeList(array $where, array $time, string $timeType = "week", string $countField = '*', string $sumField = 'pay_price', string $groupField = 'add_time') + { + return $this->getModel()->where($where) + ->where($groupField, 'between time', $time) + ->when($timeType, function ($query) use ($timeType, $countField, $sumField, $groupField) { + switch ($timeType) { + case "hour": + $timeUnix = "%H"; + break; + case "week" : + $timeUnix = "%w"; + break; + case "month" : + $timeUnix = "%d"; + break; + case "weekly" : + $timeUnix = "%W"; + break; + case "year" : + $timeUnix = "%Y-%m"; + break; + default: + $timeUnix = "%m-%d"; + break; + } + $query->field("FROM_UNIXTIME(`" . $groupField . "`,'$timeUnix') as day,count(" . $countField . ") as count"); + $query->group('day'); + })->order('add_time asc')->select()->toArray(); + } +} diff --git a/app/dao/store/SystemStoreDao.php b/app/dao/store/SystemStoreDao.php new file mode 100644 index 0000000..77c9bae --- /dev/null +++ b/app/dao/store/SystemStoreDao.php @@ -0,0 +1,212 @@ + +// +---------------------------------------------------------------------- +namespace app\dao\store; + +use app\dao\BaseDao; +use app\model\store\SystemStore; + +/** + * 门店dao + * Class SystemStoreDao + * @package app\dao\system\store + */ +class SystemStoreDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SystemStore::class; + } + + /** + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['ids']) && $where['ids'], function ($query) use ($where) { + $query->whereIn('id', $where['ids']); + })->when(isset($where['province']) && $where['province'], function ($query) use ($where) { + $query->where('province', $where['province']); + })->when(isset($where['city']) && $where['city'], function ($query) use ($where) { + $query->where('city', $where['city']); + })->when(isset($where['area']) && $where['area'], function ($query) use ($where) { + $query->where('area', $where['area']); + })->when(isset($where['street']) && $where['street'], function ($query) use ($where) { + $query->where('street', $where['street']); + }); + } + + /** + * 经纬度排序计算 + * @param string $latitude + * @param string $longitude + * @return string + */ + public function distance(string $latitude, string $longitude, bool $type = false) + { + if ($type) { + return "(round(6378137 * 2 * asin(sqrt(pow(sin(((latitude * pi()) / 180 - ({$latitude} * pi()) / 180) / 2), 2) + cos(({$latitude} * pi()) / 180) * cos((latitude * pi()) / 180) * pow(sin(((longitude * pi()) / 180 - ({$longitude} * pi()) / 180) / 2), 2)))))"; + } else { + return "(round(6378137 * 2 * asin(sqrt(pow(sin(((latitude * pi()) / 180 - ({$latitude} * pi()) / 180) / 2), 2) + cos(({$latitude} * pi()) / 180) * cos((latitude * pi()) / 180) * pow(sin(((longitude * pi()) / 180 - ({$longitude} * pi()) / 180) / 2), 2))))) AS distance"; + } + } + + /** + * 获取列表 + * @param array $where + * @param array $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, array $field = ['*'], int $page = 0, int $limit = 0) + { + return $this->search($where)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->field($field)->select()->toArray(); + } + + /** + * 获取 + * @param array $where + * @param array $field + * @param int $page + * @param int $limit + * @param string $latitude + * @param string $longitude + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreList(array $where, array $field = ['*'], int $page = 0, int $limit = 0, array $with = [], string $latitude = '', string $longitude = '', int $order = 0) + { + return $this->search($where)->when($with, function ($query) use ($with) { + $query->with($with); + })->when($latitude && $longitude, function ($query) use ($longitude, $latitude, $order) { + $query->field(['*', $this->distance($latitude, $longitude)]); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(isset($order), function ($query) use ($order) { + if ($order == 1) { + $query->order('distance ASC'); + } else { + $query->order('id desc'); + } + })->field($field)->select()->toArray(); + } + + /** + * 获取有效门店 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function getValidSerch(array $where = []) + { + $validWhere = [ + 'is_show' => 1, + 'is_del' => 0, + ]; + return $this->search($where)->where($validWhere); + } + + /** + * 获取最近距离距离内的一个门店 + * @param string $latitude + * @param string $longitude + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDistanceShortStore(string $latitude = '', string $longitude = '') + { + return $this->getValidSerch()->when($longitude && $longitude, function ($query) use ($longitude, $latitude) { + $query->field(['*', $this->distance($latitude, $longitude)])->order('distance ASC'); + })->order('id desc')->find(); + } + + /** + * 距离排序符合配送范围门店 + * @param string $latitude + * @param string $longitude + * @param string $field + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDistanceShortStoreList(string $latitude = '', string $longitude = '', string $field = '*', int $limit = 0) + { + return $this->getValidSerch()->field($field)->when($longitude && $longitude, function ($query) use ($longitude, $latitude, $field) { + $query->field([$field, $this->distance($latitude, $longitude)])->where('valid_range', 'EXP', '>' . $this->distance($latitude, $longitude, true))->order('distance ASC'); + })->when($limit, function ($query) use ($limit) { + $query->limit($limit); + })->order('id desc')->select()->toArray(); + } + + /** + * 根据地址区、街道信息获取门店列表 + * @param string $addressInfo + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreByAddressInfo(string $addressInfo = '', array $where = [], string $field = '*', int $page = 0, int $limit = 0) + { + return $this->getValidSerch($where)->field($field)->when($addressInfo, function ($query) use ($addressInfo) { + $query->whereLike('address', '%' . $addressInfo . '%'); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(!$page && $limit, function ($query) use ($limit) { + $query->limit($limit); + })->order('id desc')->select()->toArray(); + } + + /** + * 获取门店不分页 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStore(array $where) + { + return $this->search($where)->order('add_time DESC')->field(['id', 'name'])->select()->toArray(); + } + + /** + * 获取ERP店铺 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getErpStore(array $where, array $field = ['id','name','erp_shop_id']) + { + return $this->search(['type' => 0])->where($where)->field($field)->select()->toArray(); + } +} diff --git a/app/dao/store/SystemStoreStaffDao.php b/app/dao/store/SystemStoreStaffDao.php new file mode 100644 index 0000000..9ef4aa4 --- /dev/null +++ b/app/dao/store/SystemStoreStaffDao.php @@ -0,0 +1,123 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\store; + + +use app\dao\BaseDao; +use app\model\store\SystemStoreStaff; + +/** + * 门店店员 + * Class SystemStoreStaffDao + * @package app\dao\system\store + */ +class SystemStoreStaffDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SystemStoreStaff::class; + } + + /** + * @return \crmeb\basic\BaseModel + */ + public function getWhere() + { + return $this->getModel(); + } + + /** + * 门店店员搜索器 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['keyword']) && $where['keyword'], function ($query) use ($where) { + if (!isset($where['field_key']) || $where['field_key'] == '') { + $query->whereLike('id|uid|staff_name|phone', '%' . $where['keyword'] . '%'); + } else { + $query->where($where['field_key'], $where['keyword']); + } + }); + } + + /** + * 获取门店管理员列表 + * @param array $where + * @param int $page + * @param int $limit + * @param array|string[] $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreAdminList(array $where, int $page = 0, int $limit = 0, array $with = ['user']) + { + return $this->search($where)->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time DESC')->select()->toArray(); + } + + + /** + * 获取店员列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array|string[] $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreStaffList(array $where, string $field = '*', int $page = 0, int $limit = 0, array $with = ['store', 'user']) + { + return $this->search($where)->field($field)->when($with, function ($query) use ($with) { + $query->with(array_merge($with, ['store', 'user'])); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(isset($where['notId']), function ($query) use ($where) { + $query->where('id', '<>', $where['notId']); + })->when(isset($where['store_id']), function ($query) use ($where) { + $query->where('store_id', $where['store_id']); + })->order('add_time DESC')->select()->toArray(); + } + + /** + * 获取店员select + * @param array $where + * @return array + */ + public function getSelectList(array $where) + { + return $this->search($where)->field('id,staff_name')->select()->toArray(); + } + + /** + * 用户注销删除门店店员 + * @param int $uid + * @return \crmeb\basic\BaseModel + */ + public function cancelUserDel(int $uid) + { + return $this->getModel()->where('uid', $uid)->where('level', '>', 0)->update(['is_del' => 1]); + } +} diff --git a/app/dao/store/finance/StoreFinanceFlowDao.php b/app/dao/store/finance/StoreFinanceFlowDao.php new file mode 100644 index 0000000..74d8818 --- /dev/null +++ b/app/dao/store/finance/StoreFinanceFlowDao.php @@ -0,0 +1,181 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\store\finance; + + +use app\dao\BaseDao; +use app\model\store\finance\StoreFinanceFlow; + +/** + * 门店流水 + * Class StoreExtractDao + * @package app\dao\store\finance + */ +class StoreFinanceFlowDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return StoreFinanceFlow::class; + } + + + /** + * 获取提现列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, array $with = []) + { + return $this->search($where) + ->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->field($field)->order('id desc')->select()->toArray(); + } + + /** + * + * @param array $where + * @return \crmeb\basic\BaseModel|int|mixed|\think\Model + */ + public function getCount(array $where = []) + { + return $this->search($where)->count(); + } + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where) + ->when(isset($where['keyword']) && $where['keyword'] !== '', function ($query) use ($where) { + $query->where(function ($que) use ($where) { + $que->whereLike('order_id', '%' . $where['keyword'] . '%')->whereOr('uid', 'in', function ($q) use ($where) { + $q->name('user')->whereLike('nickname|uid', '%' . $where['keyword'] . '%')->field(['uid'])->select(); + }); + }); + }); + } + + /** + * 门店账单 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFundRecord(array $where = [], int $page = 0, int $limit = 0) + { + + $model = parent::search($where) + ->when(isset($where['timeType']) && $where['timeType'] !== '', function ($query) use ($where) { + $timeUnix = '%Y-%m-%d'; + switch ($where['timeType']) { + case "day" : + $timeUnix = "%Y-%m-%d"; + break; + case "week" : + $timeUnix = "%Y-%u"; + break; + case "month" : + $timeUnix = "%Y-%m"; + break; + } + $query->field("FROM_UNIXTIME(add_time,'$timeUnix') as day,sum(if(pm = 1,number,0)) as income_num,sum(if(pm = 0,number,0)) as exp_num,add_time,group_concat(id) as ids"); + $query->group("FROM_UNIXTIME(add_time, '$timeUnix')"); + }); + $count = $model->count(); + $list = $model->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc')->select()->toArray(); + return compact('list', 'count'); + } + + /** + * 店员交易统计头部数据 + * @param array $where + * @param string $group + * @param string $field + * @return mixed + */ + public function getStatisticsHeader(array $where = [], $group = 'staff_id', string $field = 'number') + { + return parent::search($where)->with(['systemStoreStaff']) + ->field("*,sum(`" . $field . "`) as total_number,count(*) as order_count") + ->group($group) + ->order('total_number desc') + ->select()->toArray(); + } + + /** + * 获取一段时间订单统计数量、金额 + * @param array $where + * @param array $time + * @param string $timeType + * @param string $countField + * @param string $sumField + * @param string $groupField + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function orderAddTimeList(array $where, array $time, string $timeType = "week", string $countField = '*', string $sumField = 'number', string $groupField = 'add_time') + { + return parent::search($where) + ->where(isset($where['timekey']) && $where['timekey'] ? $where['timekey'] : 'add_time', 'between time', $time) + ->when($timeType, function ($query) use ($timeType, $countField, $sumField, $groupField) { + switch ($timeType) { + case "hour": + $timeUnix = "%H"; + break; + case "day" : + $timeUnix = "%Y-%m-%d"; + break; + case "week" : + $timeUnix = "%Y-%w"; + break; + case "month" : + $timeUnix = "%Y-%d"; + break; + case "weekly" : + $timeUnix = "%W"; + break; + case "year" : + $timeUnix = "%Y-%m"; + break; + default: + $timeUnix = "%m-%d"; + break; + } + $query->field("FROM_UNIXTIME(`" . $groupField . "`,'$timeUnix') as day,count(" . $countField . ") as count,sum(`" . $sumField . "`) as price"); + $query->group('day'); + })->order('add_time asc')->select()->toArray(); + } +} diff --git a/app/dao/supplier/SupplierTicketPrintDao.php b/app/dao/supplier/SupplierTicketPrintDao.php new file mode 100644 index 0000000..698cfec --- /dev/null +++ b/app/dao/supplier/SupplierTicketPrintDao.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\supplier; + +use app\dao\BaseDao; +use app\model\supplier\SupplierTicketPrint; + +/** + * 供应商小票打印 + * Class SystemSupplierDao + * @package app\dao\system\store + */ +class SupplierTicketPrintDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SupplierTicketPrint::class; + } + + /** + * 获取列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where = [], int $page = 0, int $limit = 0) + { + return $this->search($where) + ->when($page != 0 && $limit != 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } +} diff --git a/app/dao/supplier/SystemSupplierDao.php b/app/dao/supplier/SystemSupplierDao.php new file mode 100644 index 0000000..771603e --- /dev/null +++ b/app/dao/supplier/SystemSupplierDao.php @@ -0,0 +1,53 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\supplier; + +use app\dao\BaseDao; +use app\model\supplier\SystemSupplier; + +/** + * 供应商 + * Class SystemSupplierDao + * @package app\dao\system\store + */ +class SystemSupplierDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SystemSupplier::class; + } + + /** + * 列表 + * @param array $where + * @param array $field + * @param int $page + * @param int $limit + * @param string $order + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSupplierList(array $where, array $field, int $page = 0, int $limit = 10, string $order = 'sort desc,id desc'): array + { + return $this->search($where)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(isset($order), function ($query) use ($order) { + $query->order($order); + })->field($field)->select()->toArray(); + } +} diff --git a/app/dao/supplier/finance/SupplierExtractDao.php b/app/dao/supplier/finance/SupplierExtractDao.php new file mode 100644 index 0000000..65c9cbf --- /dev/null +++ b/app/dao/supplier/finance/SupplierExtractDao.php @@ -0,0 +1,65 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\supplier\finance; + + +use app\dao\BaseDao; +use app\model\supplier\finance\SupplierExtract; + +/** + * 门店提现 + * Class StoreExtractDao + * @package app\dao\store\finance + */ +class SupplierExtractDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SupplierExtract::class; + } + + + /** + * 获取提现列表 + * @param array $where + * @param string $field + * @param array $with + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExtractList(array $where, string $field = '*', array $with = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + + /** + * @param array $where + * @param string $field + * @return float + */ + public function getExtractMoneyByWhere(array $where, string $field) + { + return $this->search($where)->sum($field); + } +} diff --git a/app/dao/supplier/finance/SupplierFlowingWaterDao.php b/app/dao/supplier/finance/SupplierFlowingWaterDao.php new file mode 100644 index 0000000..d5e39bf --- /dev/null +++ b/app/dao/supplier/finance/SupplierFlowingWaterDao.php @@ -0,0 +1,167 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\supplier\finance; + + +use app\dao\BaseDao; +use app\model\supplier\finance\SupplierFlowingWater; + +/** + * 供应商流水 + * Class SupplierFlowingWaterDao + * @package app\dao\supplier\finance + */ +class SupplierFlowingWaterDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SupplierFlowingWater::class; + } + + + /** + * 获取供应商流水列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, array $with = []) + { + return $this->search($where) + ->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->field($field)->order('id desc')->select()->toArray(); + } + + /** + *供应商流水数量 + * @param array $where + * @return \crmeb\basic\BaseModel|int|mixed|\think\Model + */ + public function getCount(array $where = []) + { + return $this->search($where)->count(); + } + + /** + * 搜索 + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where) + ->when(isset($where['type']) && $where['type'] !== '', function ($query) use ($where) { + $query->where('type', $where['type']); + })->when(isset($where['keyword']) && $where['keyword'] !== '', function ($query) use ($where) { + $query->where(function ($que) use ($where) { + $que->whereLike('order_id', '%' . $where['keyword'] . '%')->whereOr('uid', 'in', function ($q) use ($where) { + $q->name('user')->whereLike('nickname|uid', '%' . $where['keyword'] . '%')->field(['uid'])->select(); + }); + }); + }); + } + + /** + * 供应商账单 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFundRecord(array $where = [], int $page = 0, int $limit = 0) + { + + $model = parent::search($where) + ->when(isset($where['timeType']) && $where['timeType'] !== '', function ($query) use ($where) { + $timeUnix = '%Y-%m-%d'; + switch ($where['timeType']) { + case "day" : + $timeUnix = "%Y-%m-%d"; + break; + case "week" : + $timeUnix = "%Y-%u"; + break; + case "month" : + $timeUnix = "%Y-%m"; + break; + } + $query->field("FROM_UNIXTIME(add_time,'$timeUnix') as day,sum(if(pm = 1,number,0)) as income_num,sum(if(pm = 0,number,0)) as exp_num,add_time,group_concat(id) as ids"); + $query->group("FROM_UNIXTIME(add_time, '$timeUnix')"); + }); + $count = $model->count(); + $list = $model->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc')->select()->toArray(); + return compact('list', 'count'); + } + + /** + * 获取一段时间订单统计数量、金额 + * @param array $where + * @param array $time + * @param string $timeType + * @param string $countField + * @param string $sumField + * @param string $groupField + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function orderAddTimeList(array $where, array $time, string $timeType = "week", string $countField = '*', string $sumField = 'number', string $groupField = 'add_time') + { + return parent::search($where) + ->where(isset($where['timekey']) && $where['timekey'] ? $where['timekey'] : 'add_time', 'between time', $time) + ->when($timeType, function ($query) use ($timeType, $countField, $sumField, $groupField) { + switch ($timeType) { + case "hour": + $timeUnix = "%H"; + break; + case "day" : + $timeUnix = "%Y-%m-%d"; + break; + case "week" : + $timeUnix = "%Y-%w"; + break; + case "month" : + $timeUnix = "%Y-%d"; + break; + case "weekly" : + $timeUnix = "%W"; + break; + case "year" : + $timeUnix = "%Y-%m"; + break; + default: + $timeUnix = "%m-%d"; + break; + } + $query->field("FROM_UNIXTIME(`" . $groupField . "`,'$timeUnix') as day,count(" . $countField . ") as count,sum(`" . $sumField . "`) as price"); + $query->group('day'); + })->order('add_time asc')->select()->toArray(); + } +} diff --git a/app/dao/supplier/finance/SupplierTransactionsDao.php b/app/dao/supplier/finance/SupplierTransactionsDao.php new file mode 100644 index 0000000..b4684d5 --- /dev/null +++ b/app/dao/supplier/finance/SupplierTransactionsDao.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\supplier\finance; + + +use app\dao\BaseDao; +use app\model\supplier\finance\SupplierTransactions; + +/** + * 门店流水 + * Class StoreExtractDao + * @package app\dao\store\finance + */ +class SupplierTransactionsDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SupplierTransactions::class; + } + + +} diff --git a/app/dao/system/CapitalFlowDao.php b/app/dao/system/CapitalFlowDao.php new file mode 100644 index 0000000..e3a2ad3 --- /dev/null +++ b/app/dao/system/CapitalFlowDao.php @@ -0,0 +1,72 @@ +search($where)->field($field)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + + /** + * 账单记录 + * @param $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRecordList($where, $page = 0, $limit = 0) + { + $model = $this->search($where) + ->when(isset($where['type']) && $where['type'] !== '', function ($query) use ($where) { + $timeUnix = '%Y-%m-%d'; + switch ($where['type']) { + case "day" : + $timeUnix = "%Y-%m-%d"; + break; + case "week" : + $timeUnix = "%Y-%u"; + break; + case "month" : + $timeUnix = "%Y-%m"; + break; + } + $query->field("FROM_UNIXTIME(add_time,'$timeUnix') as day,sum(if(price >= 0,price,0)) as income_price,sum(if(price < 0,price,0)) as exp_price,add_time,group_concat(id) as ids"); + $query->group("FROM_UNIXTIME(add_time, '$timeUnix')"); + }); + $count = $model->count(); + $list = $model->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc')->select()->toArray(); + return compact('list', 'count'); + } +} diff --git a/app/dao/system/SystemRoleDao.php b/app/dao/system/SystemRoleDao.php new file mode 100644 index 0000000..1ebbc4d --- /dev/null +++ b/app/dao/system/SystemRoleDao.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\system; + +use app\dao\BaseDao; +use app\common\model\system\auth\Role as SystemRole; + +/** + * Class SystemRoleDao + * @package app\dao\system\admin + */ +class SystemRoleDao extends BaseDao +{ + /** + * 设置模型名 + * @return string + */ + protected function setModel(): string + { + return SystemRole::class; + } + + /** + * 获取权限 + * @param string $field + * @param string $key + * @return mixed + */ + public function getRoule(array $where = [], ?string $field = null, ?string $key = null) + { + return $this->search($where)->column($field ?: 'role_name', $key ?: 'id'); + } + + /** + * 获取身份列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRouleList(array $where, int $page, int $limit) + { + return $this->search($where)->page($page, $limit)->select()->toArray(); + } +} diff --git a/app/dao/system/SystemUserApplyDao.php b/app/dao/system/SystemUserApplyDao.php new file mode 100644 index 0000000..6bcfe42 --- /dev/null +++ b/app/dao/system/SystemUserApplyDao.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\system; + +use app\dao\BaseDao; +use app\model\system\SystemUserApply; + +/** + * Class SystemRoleDao + * @package app\dao\system\admin + */ +class SystemUserApplyDao extends BaseDao +{ + /** + * 设置模型名 + * @return string + */ + protected function setModel(): string + { + return SystemUserApply::class; + } + + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array $typeWhere + * @return array + */ + public function getList(array $where, string $field = '*', array $with = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + +} diff --git a/app/dao/system/admin/SystemAdminDao.php b/app/dao/system/admin/SystemAdminDao.php new file mode 100644 index 0000000..f9bb6fd --- /dev/null +++ b/app/dao/system/admin/SystemAdminDao.php @@ -0,0 +1,110 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\system\admin; + +use app\dao\BaseDao; +use app\common\model\system\admin\Admin as SystemAdmin; + +/** + * Class SystemAdminDao + * @package app\dao\system\admin + */ +class SystemAdminDao extends BaseDao +{ + protected function setModel(): string + { + return SystemAdmin::class; + } + + /** + * 获取列表 + * @param array $where + * @param int $page + * @param int $limit + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page = 0, int $limit = 0, string $field = '*') + { + return $this->search($where)->field($field)->when($page && $limit, function ($query) use($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 用管理员名查找管理员信息 + * @param string $account + * @param int $adminType + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function accountByAdmin(string $account, int $adminType) + { + return $this->search(['account' => $account, 'is_del' => 0, 'status' => 1, 'admin_type' => $adminType])->find(); + } + + /** + * 用电话查找管理员信息 + * @param string $phone + * @param int $adminType + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function phoneByAdmin(string $phone, int $adminType = 1) + { + return $this->search(['phone' => $phone, 'is_del' => 0, 'status' => 1, 'admin_type' => $adminType])->find(); + } + + /** + * 当前账号是否可用 + * @param string $account + * @param int $id + * @param int $admin_type + * @return int + * @throws \think\db\exception\DbException + */ + public function isAccountUsable(string $account, int $id, int $admin_type = 1) + { + return $this->search(['account' => $account, 'is_del' => 0])->where('admin_type', $admin_type)->where('id', '<>', $id)->count(); + } + + /** + * 获取adminid + * @param int $level + * @return array + */ + public function getAdminIds(int $level) + { + return $this->getModel()->where('level', '>=', $level)->column('id', 'id'); + } + + /** + * 获取低于等级的管理员名称和id + * @param string $field + * @param int $level + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrdAdmin(string $field = 'real_name,id', int $level = 0) + { + return $this->getModel()->where('level', '>=', $level)->field($field)->select()->toArray(); + } +} diff --git a/app/dao/system/attachment/SystemAttachmentDao.php b/app/dao/system/attachment/SystemAttachmentDao.php new file mode 100644 index 0000000..9761b1c --- /dev/null +++ b/app/dao/system/attachment/SystemAttachmentDao.php @@ -0,0 +1,122 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\system\attachment; + +use app\dao\BaseDao; +use app\common\model\system\attachment\Attachment as SystemAttachment; + +/** + * + * Class SystemAttachmentDao + * @package app\dao\attachment + */ +class SystemAttachmentDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SystemAttachment::class; + } + /** + * 搜索附件分类search + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['name']) && $where['name']!='', function ($query) use ($where) { + $query->where('att_id|real_name|name', 'LIKE', '%' . trim($where['name']) . '%'); + }); + } + /** + * 获取图片列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page, int $limit) + { + return $this->search($where)->where('module_type', 1)->page($page, $limit)->order('att_id DESC')->select()->toArray(); + } + + /** + * 移动图片 + * @param array $data + * @return \crmeb\basic\BaseModel + */ + public function move(array $data) + { + return $this->getModel()->whereIn('att_id', $data['images'])->update(['pid' => $data['pid']]); + } + + /** + * 获取名称 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLikeNameList(array $where, int $page, int $limit) + { + return $this->search($where)->page($page, $limit)->order('att_id desc')->select()->toArray(); + } + + /** + * 获取昨日系统生成 + * @param int $type + * @param int $relationId + * @return \think\Collection + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getYesterday(int $type = 1, $relationId = 0) + { + return $this->getModel()->where('type', $type)->when($relationId, function ($query) use ($relationId) { + $query->where('relation_id', $relationId); + })->whereTime('time', 'yesterday')->where('module_type', 2)->field(['name', 'att_dir', 'att_id', 'image_type'])->select(); + } + + /** + * 删除昨日生成海报 + * @throws \Exception + */ + public function delYesterday() + { + $this->getModel()->whereTime('time', 'yesterday')->where('module_type', 2)->delete(); + } + + /** + * 获取扫码上传的图片数据 + * @param $scan_token + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function scanUploadImage($scan_token) + { + return $this->getModel()->where('scan_token', $scan_token)->field('att_dir,att_id')->select()->toArray(); + } +} diff --git a/app/dao/system/config/SystemConfigDao.php b/app/dao/system/config/SystemConfigDao.php new file mode 100644 index 0000000..1eefeef --- /dev/null +++ b/app/dao/system/config/SystemConfigDao.php @@ -0,0 +1,141 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\system\config; + +use app\dao\BaseDao; +use app\common\model\system\config\SystemConfig; + +/** + * 系统配置 + * Class SystemConfigDao + * @package app\dao\system\config + */ +class SystemConfigDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SystemConfig::class; + } + + /** + * 获取某个系统配置 + * @param string $configNmae + * @param int $storeId + * @return mixed + */ + public function getConfigValue(string $configNmae, int $storeId = 0) + { + return $this->withSearchSelect(['menu_name', 'store_id'], ['menu_name' => $configNmae, 'store_id' => $storeId])->value('value'); + } + + /** + * 获取所有配置 + * @param array $configName + * @param int $storeId + * @return array + */ + public function getConfigAll(array $configName = [], int $storeId = 0) + { + if ($configName) { + return $this->withSearchSelect(['menu_name', 'store_id'], ['menu_name' => $configName, 'store_id' => $storeId])->column('value', 'menu_name'); + } else { + return $this->getModel()->column('value', 'menu_name'); + } + } + + /** + * @param array $configName + * @param int $storeId + * @param array $field + * @return array + */ + public function getConfigAllField(array $configName = [], int $storeId = 0, array $field = []) + { + return $this->withSearchSelect(['menu_name', 'store_id'], ['menu_name' => $configName, 'store_id' => $storeId])->column(implode(',', $field), 'menu_name'); + } + + /** + * 获取配置列表分页 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getConfigList(array $where, int $page, int $limit) + { + return $this->search($where)->page($page, $limit)->order('sort desc,id desc')->select()->toArray(); + } + + /** + * 获取某些分类配置下的配置列表 + * @param int $tabId + * @param int $status + * @param int $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getConfigTabAllList(int $tabId, int $status = 1, int $type = 1, int $relation_id = 0) + { + $where['tab_id'] = $tabId; + if ($status == 1) $where['status'] = $status; + if ($relation_id != 0) $where['is_store'] = 1; + return $this->search($where) + ->when(isset($relation_id) && $relation_id != 0, function ($query) use ($type, $relation_id) { + $query->with(['storeConfig' => function ($querys) use ($type, $relation_id) { + $querys->where('type', $type)->where('relation_id', $relation_id); + }]); + }) + ->order('sort desc')->select()->toArray(); + } + + /** + * 根据条件获取配置 + * @param array $where + * @param int $type + * @param int $relation_id + * @param array $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getConfigAllListByWhere(array $where, int $type = 1, int $relation_id = 0, array $field = ['*']) + { + if ($relation_id != 0) $where['is_store'] = 1; + return $this->search($where)->field($field) + ->when(isset($relation_id) && $relation_id != 0, function ($query) use ($type, $relation_id) { + $query->with(['storeConfig' => function ($querys) use ($type, $relation_id) { + $querys->where('type', $type)->where('relation_id', $relation_id); + }]); + }) + ->order('sort desc')->select()->toArray(); + } + + /** + * 获取上传配置中的上传类型 + * @param string $configName + * @return array + */ + public function getUploadTypeList(string $configName) + { + return $this->search(['menu_name' => $configName])->column('upload_type', 'type'); + } +} diff --git a/app/dao/system/config/SystemGroupDao.php b/app/dao/system/config/SystemGroupDao.php new file mode 100644 index 0000000..8079187 --- /dev/null +++ b/app/dao/system/config/SystemGroupDao.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\system\config; + +use app\dao\BaseDao; +use app\common\model\system\groupData\SystemGroup; + +/** + * Class SystemGroupDao + * @package app\dao\system\config + */ +class SystemGroupDao extends BaseDao +{ + /** + * @return string + */ + protected function setModel(): string + { + return SystemGroup::class; + } + + /** + * 获取组合数据分页列表 + * @param array $where + * @param array $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGroupList(array $where, array $field = ['*'], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 根据配置名称获取配置id + * @param string $configName + * @return mixed + */ + public function getConfigNameId(string $configName) + { + return $this->value(['config_name' => $configName]); + } +} diff --git a/app/dao/system/config/SystemGroupDataDao.php b/app/dao/system/config/SystemGroupDataDao.php new file mode 100644 index 0000000..51181de --- /dev/null +++ b/app/dao/system/config/SystemGroupDataDao.php @@ -0,0 +1,99 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\system\config; + +use app\dao\BaseDao; +use app\common\model\system\groupData\SystemGroupData; + +/** + * 组合数据 + * Class SystemGroupDataDao + * @package app\dao\system\config + */ +class SystemGroupDataDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SystemGroupData::class; + } + + /** + * 获取组合数据列表 + * @param array $where + * @param int $page + * @param int $limit + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGroupDataList(array $where, int $page, int $limit) + { + return $this->search($where)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('sort desc,id DESC')->select()->toArray(); + } + + /** + * 获取某个gid下的组合数据 + * @param int $gid + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGroupDate(int $gid, int $limit = 0) + { + return $this->search(['gid' => $gid, 'status' => 1])->when($limit, function ($query) use ($limit) { + $query->limit($limit); + })->field('value,id')->order('sort DESC,id DESC')->select()->toArray(); + } + + /** + * 根据gid删除组合数据 + * @param int $gid + * @return bool + */ + public function delGroupDate(int $gid) + { + return $this->getModel()->where('gid', $gid)->delete(); + } + + /** + * 批量保存 + * @param array $data + * @return mixed|\think\Collection + * @throws \Exception + */ + public function saveAll(array $data) + { + return $this->getModel()->saveAll($data); + } + + /** + * 根据id获取秒杀数据 + * @param array $ids + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function idByGroupList(array $ids, string $field) + { + return $this->getModel()->whereIn('id', $ids)->field($field)->select()->toArray(); + } +} diff --git a/app/dao/system/form/SystemFormDao.php b/app/dao/system/form/SystemFormDao.php new file mode 100644 index 0000000..033ba07 --- /dev/null +++ b/app/dao/system/form/SystemFormDao.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\system\form; + +use app\dao\BaseDao; +use app\model\system\form\SystemForm; + +/** + * + * Class SystemFormDao + * @package app\dao\system\form + */ +class SystemFormDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return SystemForm::class; + } + + + /** + * 获取系统表单 + * @param array $where + * @param array $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFormList(array $where, array $field = ['*'], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->where('is_del', 0) + ->when($page && $limit, function($query) use ($page, $limit){$query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + +} diff --git a/app/dao/user/UserAddressDao.php b/app/dao/user/UserAddressDao.php new file mode 100644 index 0000000..1020b9a --- /dev/null +++ b/app/dao/user/UserAddressDao.php @@ -0,0 +1,103 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\common\model\user\User; +use app\common\model\user\UserAddress; + +/** + * 用户收获地址 + * Class UserAddressDao + * @package app\dao\user + */ +class UserAddressDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserAddress::class; + } + + protected function JoinModel() : string + { + return User::class; + } + + public function getJoinModel(string $alias = 'a', string $join_alias = 'u', $join = 'left') + { + $this->alias = $alias; + $this->joinAlis = $join_alias; + /** @var User $user */ + $user = app()->make($this->joinModel()); + $table = $user->getName(); + return parent::getModel()->alias($alias)->join($table . ' ' . $join_alias, $alias . '.uid = ' . $join_alias . '.uid', $join); + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0): array + { + return $this->search($where)->field($field)->page($page, $limit)->order('is_default DESC')->select()->toArray(); + } + + /** + * 地域全部用户 + * @param $time + * @param $userType + * @return mixed + */ + public function getRegionAll($time, $userType) + { + return $this->getJoinModel()->when($userType != '', function ($query) use ($userType) { + $query->where($this->joinAlis . '.user_type', $userType); + })->where(function ($query) use ($time) { + $query->whereTime($this->joinAlis . '.add_time', '<', strtotime($time[1]) + 86400)->whereOr($this->joinAlis . '.add_time', NULL); + })->field('count(distinct(' . $this->alias . '.uid)) as allNum,' . $this->alias . '.province') + ->group($this->alias . '.province')->select()->toArray(); + } + + /** + * 地域新增用户 + * @param $time + * @param $userType + * @return mixed + */ + public function getRegionNew($time, $userType) + { + return $this->getJoinModel()->when($userType != '', function ($query) use ($userType) { + $query->where($this->joinAlis . '.user_type', $userType); + })->where(function ($query) use ($time) { + if ($time[0] == $time[1]) { + $query->whereDay($this->joinAlis . '.add_time', $time[0]); + } else { + $time[1] = date('Y/m/d', strtotime($time[1]) + 86400); + $query->whereTime($this->joinAlis . '.add_time', 'between', $time); + } + })->field('count(distinct(' . $this->alias . '.uid)) as newNum,' . $this->alias . '.province') + ->group($this->alias . '.province')->select()->toArray(); + } + +} diff --git a/app/dao/user/UserBillDao.php b/app/dao/user/UserBillDao.php new file mode 100644 index 0000000..b2f7e25 --- /dev/null +++ b/app/dao/user/UserBillDao.php @@ -0,0 +1,279 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\common\model\user\UserBill; + +/** + * 积分&经验 + * Class UserBilldao + * @package app\dao\user + */ +class UserBilldao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserBill::class; + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array $typeWhere + * @return array + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, array $typeWhere = []) + { + return $this->search($where)->when(count($typeWhere) > 0, function ($query) use ($typeWhere) { + $query->where($typeWhere); + })->field($field)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + */ + public function getBillList(array $where, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->with([ + 'user' => function ($query) { + $query->field('uid,nickname'); + }])->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + + /** + * 获取某个条件总数 + * @param array $where + */ + public function getBillSum(array $where) + { + return $this->search($where)->sum('number'); + } + + + /** + * 获取某个条件总条数 + * @param array $where + */ + public function getBillCount(array $where) + { + return $this->getModel()->where($where)->count(); + } + + /** + * 获取某些条件的bill总数 + * @param array $where + * @return mixed + */ + public function getBillSumColumn(array $where) + { + if (isset($where['uid']) && is_array($where['uid'])) { + return $this->search($where)->group('uid')->column('sum(number) as num', 'uid'); + } else + return $this->search($where)->sum('number'); + } + + /** + * 获取类型 + * @param array $where + * @param string $filed + * @return mixed + */ + public function getType(array $where, string $filed = 'title,type') + { + return $this->search($where)->distinct(true)->field($filed)->group('type')->select(); + } + + /** + * 获取签到用户数量 + * @param array $where + * @return mixed + */ + public function getUserSignPoint(array $where) + { + return $this->search($where)->count(); + } + + /** + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserBillList(array $where) + { + return $this->search($where)->select()->toArray(); + } + + /** + * 获取佣金排行 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function brokerageRankList(array $where, int $page = 0, int $limit = 0) + { + return $this->search($where)->field('uid,SUM(IF(pm=1,`number`,-`number`)) as brokerage_price')->with(['user' => function ($query) { + $query->field('uid,avatar,nickname'); + }])->order('brokerage_price desc')->group('uid')->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 时间分组 + * @param array $where + * @param string $filed + * @param string $group + * @param int $page + * @param int $limit + * @return mixed + */ + public function getUserBillListByGroup(array $where, string $filed, string $group, int $page, int $limit) + { + return $this->search($where)->field($filed)->where('number', '>', 0)->order('add_time desc')->group($group)->page($page, $limit)->select()->toArray(); + } + + /** + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getBalanceRecord(array $where, int $page, int $limit) + { + return $this->search($where)->order('add_time desc')->page($page, $limit)->select()->toArray(); + } + + /** + * 计算某个条件下订单内商品总数 + * @param $where + * @return float|int + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTotalSum(array $where) + { + $list = $this->search($where)->with('order')->select()->toArray(); + if (count($list)) { + $sum = 0; + foreach ($list as $item) { + $sum += $item['total_num']; + } + return $sum; + } else { + return 0; + } + } + + /** + * 获取某个字段总和 + * @param array $where + * @param string $field + * @return float + */ + public function getWhereSumField(array $where, string $field) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where) { + $query->whereBetweenTime('add_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + }) + ->sum($field); + } + + /** + * 根据某字段分组查询 + * @param array $where + * @param string $field + * @param string $group + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGroupField(array $where, string $field, string $group) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where, $field, $group) { + $query->whereBetweenTime('add_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + if ($where['timeKey']['days'] == 1) { + $timeUinx = "%H"; + } elseif ($where['timeKey']['days'] == 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] == 365) { + $timeUinx = "%Y-%m"; + } elseif ($where['timeKey']['days'] > 1 && $where['timeKey']['days'] < 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] > 30 && $where['timeKey']['days'] < 365) { + $timeUinx = "%Y-%m"; + } else { + $timeUinx = "%Y-%m"; + } + $query->field("sum($field) as number,FROM_UNIXTIME($group, '$timeUinx') as time"); + $query->group("FROM_UNIXTIME($group, '$timeUinx')"); + }) + ->order('add_time ASC')->select()->toArray(); + } + + /** + * 积分趋势 + * @param $time + * @param $timeType + * @param $field + * @param $str + * @return mixed + */ + public function getPointTrend($time, $timeType, $field, $str, $orderStatus = '') + { + return $this->getModel()->where(function ($query) use ($field, $orderStatus) { + $query->where('category', 'integral'); + if ($orderStatus == 'add') { + $query->where('pm', 1); + } elseif ($orderStatus == 'sub') { + $query->where('pm', 0); + } + })->where(function ($query) use ($time, $field) { + if ($time[0] == $time[1]) { + $query->whereDay($field, $time[0]); + } else { + $query->whereTime($field, 'between', $time); + } + })->field("FROM_UNIXTIME($field,'$timeType') as days,$str as num")->group('days')->select()->toArray(); + } +} diff --git a/app/dao/user/UserBrokerageDao.php b/app/dao/user/UserBrokerageDao.php new file mode 100644 index 0000000..91a49e1 --- /dev/null +++ b/app/dao/user/UserBrokerageDao.php @@ -0,0 +1,162 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\common\model\user\UserBrokerage; + +/** + * 用户佣金 + * Class UserBrokerageDao + * @package app\dao\user + */ +class UserBrokerageDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserBrokerage::class; + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array $typeWhere + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, array $typeWhere = [], array $with = []) + { + return $this->search($where)->when(count($typeWhere) > 0, function ($query) use ($typeWhere) { + $query->where($typeWhere); + })->field($field)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(!empty($with), function ($query) use ($with) { + $query->with($with); + })->order('id desc')->select()->toArray(); + } + + /** + * 获取佣金排行 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function brokerageRankList(array $where, int $page = 0, int $limit = 0) + { + $where['not_type'] = ['extract_fail', 'refund']; + return $this->search($where)->where('pm', 1)->field('uid,SUM(number) as brokerage_price')->with(['user' => function ($query) { + $query->field('uid,avatar,nickname'); + }])->order('brokerage_price desc')->group('uid')->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserBrokerageList(array $where) + { + return $this->search($where)->select()->toArray(); + } + + /** + * 获取佣金记录类型 + * @param array $where + * @param string $filed + * @return mixed + */ + public function getType(array $where, string $filed = 'title,type') + { + return $this->search($where)->distinct(true)->field($filed)->group('type')->select(); + } + + /** + * 修改收货状态 + * @param int $uid + * @param int $id + * @return \crmeb\basic\BaseModel + */ + public function takeUpdate(int $uid, int $id) + { + return $this->getModel()->where('uid', $uid)->where('link_id', $id)->where('type', 'IN', ['one_brokerage', 'two_brokerage'])->update(['take' => 1]); + } + + /** + * 获取某个账户下的冻结佣金 + * @param int $uid + * @return float + */ + public function getUserFrozenPrice(int $uid) + { + return $this->search(['uid' => $uid, 'status' => 1, 'pm' => 1])->where('frozen_time', '>', time())->sum('number'); + } + + /** + * 获取某个条件总数 + * @param array $where + */ + public function getBrokerageSum(array $where) + { + return $this->search($where)->sum('number'); + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + */ + public function getBrokerageList(array $where, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->with([ + 'user' => function ($query) { + $query->field('uid,nickname'); + }])->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + + /** + * 获取某些条件的bill总数 + * @param array $where + * @return mixed + */ + public function getBrokerageSumColumn(array $where) + { + if (isset($where['uid']) && is_array($where['uid'])) { + return $this->search($where)->group('uid')->column('sum(number) as num', 'uid'); + } else + return $this->search($where)->sum('number'); + } +} diff --git a/app/dao/user/UserCardDao.php b/app/dao/user/UserCardDao.php new file mode 100644 index 0000000..db9a77e --- /dev/null +++ b/app/dao/user/UserCardDao.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\model\user\UserCard; + +/** + * 用户领取卡券 + * Class UserCardDao + * @package app\dao\user + */ +class UserCardDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserCard::class; + } + + /** + * 获取激活会员卡列表 + * @param array $where + * @param string $field + * @param array $with + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', array $with = [], int $page = 0, $limit = 0) + { + return $this->search($where)->field($field) + ->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc,id desc')->select()->toArray(); + } +} diff --git a/app/dao/user/UserDao.php b/app/dao/user/UserDao.php new file mode 100644 index 0000000..69af3f0 --- /dev/null +++ b/app/dao/user/UserDao.php @@ -0,0 +1,364 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\common\model\user\User; + +/** + * 用户 + * Class UserDao + * @package app\dao\user + */ +class UserDao extends BaseDao +{ + + protected function setModel(): string + { + return User::class; + } + + /** + * @param array $where + * @return \crmeb\basic\BaseModel|mixed|\think\Model + */ + public function search(array $where = []) + { + return parent::search($where)->when(isset($where['label_id']) && $where['label_id'], function($query) use ($where) { + $query->whereIn('uid', function ($q) use ($where) { + if (is_array($where['label_id'])) { + $q->name('user_label_relation')->whereIn('label_id', $where['label_id'])->field('uid')->select(); + } else { + if (strpos($where['label_id'], ',') !== false) { + $q->name('user_label_relation')->whereIn('label_id', explode(',', $where['label_id']))->field('uid')->select(); + } else { + $q->name('user_label_relation')->where('label_id', $where['label_id'])->field('uid')->select(); + } + } + }); + }); + } + + /** + * 是否存在 + * @param int $uid + * @return bool + */ + public function exist(int $uid) + { + return !!$this->getModel()->where('uid', $uid)->count(); + } + + /** + * @param array $where + * @return mixed + */ + public function getWithTrashedCount(array $where = [], array $time = []) + { + return $this->getModel()->withTrashed()->where($where)->whereTime('add_time', 'between', $time)->count(); + } + + /** + * 获取删除和没有删除的用户信息 + * @param int $uid + * @param $field + * @return mixed + */ + public function getUserWithTrashedInfo(int $uid, $field = '*') + { + return $this->getModel()->withTrashed()->field($field)->find($uid); + } + + /** + * 获取用户列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0): array + { + return $this->search($where)->field($field)->with(['label'])->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 获取特定条件的总数 + * @param array $where + * @param bool $is_list + * @return array|int + */ + public function getCount(array $where, bool $is_list = false) + { + if ($is_list) + return $this->getModel()->where($where)->group('uid')->fetchSql(true)->column('count(*) as user_count', 'uid'); + else + return $this->getModel()->where($where)->count(); + } + + /** + * 用户支付成功个数增加 + * @param int $uid + * @return mixed + */ + public function incPayCount(int $uid) + { + event('user.update', [$uid]); + return $this->getModel()->where('uid', $uid)->inc('pay_count', 1)->update(); + } + + /** + * 某个字段累加某个数值 + * @param string $field + * @param int $num + */ + public function incField(int $uid, string $field, int $num = 1) + { + event('user.update', [$uid]); + return $this->getModel()->where('uid', $uid)->inc($field, $num)->update(); + } + + /** + * @param $uid + * @return \think\Collection + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserLabel($uid, $field = '*') + { + return $this->search(['uid' => $uid])->field($field)->with(['label'])->select()->toArray(); + } + + /** + * 获取分销用户 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAgentUserList(array $where, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->with([ + 'extract' => function ($query) { + $query->field('sum(extract_price + extract_fee) as extract_count_price,count(id) as extract_count_num,uid')->where('status', '1')->group('uid'); + }, 'order' => function ($query) { + $query->field('sum(pay_price) as order_price,count(id) as order_count,uid')->where('pid', '>=', 0)->where('paid', 1)->where('is_del', 0)->where('is_system_del', 0)->where('refund_status', 'IN', [0, 3])->group('uid'); + }, 'brokerage' => function ($query) { + $query->field('sum(number) as brokerage_money,uid')->where('status', 1)->where('pm', 1)->group('uid'); + }, 'spreadCount' => function ($query) { + $query->field('count(`uid`) as spread_count,spread_uid')->group('spread_uid'); + }, 'spreadUser' => function ($query) { + $query->field('uid,phone,nickname'); + }, 'agentLevel' => function ($query) { + $query->field('id,name'); + } + ])->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('uid desc')->select()->toArray(); + } + + /** + * 获取推广人列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSairList(array $where, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->with([ + 'order' => function ($query) { + $query->field('sum(pay_price) as order_price,count(id) as order_count,uid')->where('paid', 1)->where('refund_status', 0)->group('uid'); + }, 'spreadCount' => function ($query) { + $query->field('count(`uid`) as spread_count,spread_uid')->group('spread_uid'); + }, 'spreadUser' => function ($query) { + $query->field('uid,phone,nickname'); + } + ])->page($page, $limit)->order('uid desc')->select()->toArray(); + } + + /** + * 获取推广人排行 + * @param array $time + * @param string $field + * @param int $page + * @param int $limit + */ + public function getAgentRankList(array $time, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->getModel()->alias('t0') + ->field($field) + ->join('user t1', 't0.uid = t1.spread_uid', 'LEFT') + ->where('t1.spread_uid', '<>', 0) + ->order('count desc') + ->order('t0.uid desc') + ->where('t1.spread_time', 'BETWEEN', $time) + ->page($page, $limit) + ->group('t0.uid') + ->select()->toArray(); + } + + /** + * 获取推广员ids + * @param array $where + * @return array + */ + public function getAgentUserIds(array $where) + { + return $this->search($where)->column('uid'); + } + + /** + * 某个条件 用户某个字段总和 + * @param array $where + * @param string $filed + * @return float + */ + public function getWhereSumField(array $where, string $filed) + { + return $this->search($where)->sum($filed); + } + + /** + * 根据条件查询对应的用户信息以数组形式返回 + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getUserInfoArray(array $where, string $field, string $key) + { + return $this->search($where)->column($field, $key); + } + + /** + * 获取特定时间用户访问量 + * @param $time + * @param $week + * @return int + */ + public function todayLastVisit($time, $week) + { + switch ($week) { + case 1: + return $this->search(['time' => $time ?: 'today', 'timeKey' => 'last_time'])->count(); + case 2: + return $this->search(['time' => $time ?: 'week', 'timeKey' => 'last_time'])->count(); + } + } + + /** + * 获取特定时间用户访问量 + * @param $time + * @return int + */ + public function totalUserCount($time) + { + return $this->search(['time' => $time ?: 'today', 'timeKey' => 'add_time'])->count(); + } + + /** + * 获取特定时间内用户列表 + * @param $starday + * @param $yesterday + * @return mixed + */ + public function userList($starday, $yesterday) + { + return $this->getModel()->where('add_time', 'between time', [$starday, $yesterday]) + ->field("FROM_UNIXTIME(add_time,'%Y-%m-%d') as day,count(*) as count") + ->group("FROM_UNIXTIME(add_time, '%Y%m%d')") + ->order('add_time asc') + ->select()->toArray(); + } + + /** + * 购买量范围的用户数量 + * @param $status + * @return int + */ + public function userCount($status) + { + switch ($status) { + case 1: + return $this->getModel()->where('pay_count', '>', 1)->where('pay_count', '<=', 4)->count(); + case 2: + return $this->getModel()->where('pay_count', '>', 4)->count(); + } + } + + /** + * 获取用户统计数据 + * @param $time + * @param $type + * @param $timeType + * @return mixed + */ + public function getTrendData($time, $type, $timeType) + { + return $this->getModel()->when($type != '', function ($query) use ($type) { + $query->where('user_type', $type); + })->where(function ($query) use ($time) { + if ($time[0] == $time[1]) { + $query->whereDay('add_time', $time[0]); + } else { + $time[1] = date('Y/m/d', strtotime($time[1]) + 86400); + $query->whereTime('add_time', 'between', $time); + } + })->field("FROM_UNIXTIME(add_time,'$timeType') as days,count(uid) as num")->group('days')->select()->toArray(); + } + + /** + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserInfoList(array $where, $field = "*"): array + { + return $this->search($where)->field($field)->select()->toArray(); + } + + /** + * 获取用户会员数量 + * @param $where (time type) + * @return int + */ + public function getMemberCount($where, int $overdue_time = 0) + { + if (!$overdue_time) $overdue_time = time(); + return $this->search($where)->where('is_ever_level', 1)->whereOr(function ($qeury) use ($overdue_time) { + $qeury->where('is_money_level', '>', 0)->where('overdue_time', '>', $overdue_time); + })->count(); + } + + public function getOutOne(int $uid, $field = "*") + { + return $this->getModel()->where('uid|phone', $uid)->field($field)->find(); + } +} diff --git a/app/dao/user/UserExtractDao.php b/app/dao/user/UserExtractDao.php new file mode 100644 index 0000000..ae34a20 --- /dev/null +++ b/app/dao/user/UserExtractDao.php @@ -0,0 +1,149 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\common\model\user\UserExtract; + +/** + * + * Class UserExtractDao + * @package app\dao\user + */ +class UserExtractDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserExtract::class; + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array $typeWhere + * @return array + */ + public function getList(array $where, string $field = '*', array $with = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + + /** + * 获取某个条件的提现总和 + * @param array $where + * @return float + */ + public function getWhereSum(array $where) + { + return $this->search($where)->field('(extract_price + extract_fee) as extract_price')->sum('extract_price'); + } + + /** + * 获取某些条件总数组合列表 + * @param array $where + * @param string $field + * @param string $key + * @return mixed + */ + public function getWhereSumList(array $where, string $field = 'extract_price', string $key = 'uid') + { + return $this->search($where)->group($key)->column('(sum(extract_price) + sum(extract_fee)) as extract_price', $key); + } + + /** + * 获取提现列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExtractList(array $where, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->with([ + 'user' => function ($query) { + $query->field('uid,nickname'); + }])->page($page, $limit)->order('id desc')->select()->toArray(); + } + + /** + * 获取某个字段总和 + * @param array $where + * @param string $field + * @return float + */ + public function getWhereSumField(array $where, string $field) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where) { + $query->whereBetweenTime('add_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + }) + ->sum($field); + } + + /** + * 根据某字段分组查询 + * @param array $where + * @param string $field + * @param string $group + * @return mixed + */ + public function getGroupField(array $where, string $field, string $group) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where, $field, $group) { + $query->whereBetweenTime('add_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + if ($where['timeKey']['days'] == 1) { + $timeUinx = "%H"; + } elseif ($where['timeKey']['days'] == 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] == 365) { + $timeUinx = "%Y-%m"; + } elseif ($where['timeKey']['days'] > 1 && $where['timeKey']['days'] < 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] > 30 && $where['timeKey']['days'] < 365) { + $timeUinx = "%Y-%m"; + } else { + $timeUinx = "%Y-%m"; + } + $query->field("sum($field) as number,FROM_UNIXTIME($group, '$timeUinx') as time"); + $query->group("FROM_UNIXTIME($group, '$timeUinx')"); + }) + ->order('add_time ASC')->select()->toArray(); + } + + /** + * @param array $where + * @param string $field + * @return float + */ + public function getExtractMoneyByWhere(array $where, string $field) + { + return $this->search($where)->sum($field); + } +} diff --git a/app/dao/user/UserFriendsDao.php b/app/dao/user/UserFriendsDao.php new file mode 100644 index 0000000..2e2410a --- /dev/null +++ b/app/dao/user/UserFriendsDao.php @@ -0,0 +1,46 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\user; + + +use app\dao\BaseDao; +use app\model\user\UserFriends; + +class UserFriendsDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserFriends::class; + } + + /** + * 获取好友关系 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFriendList(array $where, int $page, int $limit, array $with = []) + { + return $this->search($where)->when($with, function ($query) use ($with) { + $query->with($with); + })->page($page, $limit)->select()->toArray(); + } +} diff --git a/app/dao/user/UserInvoiceDao.php b/app/dao/user/UserInvoiceDao.php new file mode 100644 index 0000000..09cdad9 --- /dev/null +++ b/app/dao/user/UserInvoiceDao.php @@ -0,0 +1,65 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user; + + +use app\dao\BaseDao; +use app\model\user\UserInvoice; +use think\exception\ValidateException; + +/** + * Class UserInvoiceDao + * @package app\dao\user + */ +class UserInvoiceDao extends BaseDao +{ + + protected function setModel(): string + { + return UserInvoice::class; + } + + /** + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->page($page, $limit)->order('is_default desc,id desc')->select()->toArray(); + } + + /** + * 设置默认(个人普通|企业普通|企业专用) + * @param int $uid + * @param int $id + * @param $header_type + * @param $type + * @return bool + */ + public function setDefault(int $uid, int $id, $header_type, $type) + { + if (false === $this->getModel()->where('uid', $uid)->where('header_type', $header_type)->where('type', $type)->update(['is_default' => 0])) { + return false; + } + if (false === $this->getModel()->where('id', $id)->update(['is_default' => 1])) { + return false; + } + return true; + } +} diff --git a/app/dao/user/UserMoneyDao.php b/app/dao/user/UserMoneyDao.php new file mode 100644 index 0000000..2364f20 --- /dev/null +++ b/app/dao/user/UserMoneyDao.php @@ -0,0 +1,153 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\model\user\UserMoney; + +/** + * 用户余额 + * Class UserMoneyDao + * @package app\dao\user + */ +class UserMoneyDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserMoney::class; + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', int $page = 0, int $limit = 0, array $with = []) + { + return $this->search($where)->field($field)->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } + + + /** + * 获取某一个月数量 + * @param array $where + * @param string $month + * @return int + */ + public function getMonthCount(array $where, string $month) + { + return $this->search($where)->whereMonth('add_time', $month)->count(); + } + + /** + * 获取余额记录类型 + * @param array $where + * @param string $filed + * @return mixed + */ + public function getMoneyType(array $where, string $filed = 'title,type') + { + return $this->search($where)->distinct(true)->field($filed)->group('type')->select(); + } + + /** + * 余额趋势 + * @param $time + * @param $timeType + * @param $field + * @param $str + * @return mixed + */ + public function getBalanceTrend($time, $timeType, $field, $str, $orderStatus = '') + { + return $this->getModel()->where(function ($query) use ($field, $orderStatus) { + if ($orderStatus == 'add') { + $query->where('pm', 1); + } elseif ($orderStatus == 'sub') { + $query->where('pm', 0); + } + })->where(function ($query) use ($time, $field) { + if ($time[0] == $time[1]) { + $query->whereDay($field, $time[0]); + } else { + $query->whereTime($field, 'between', $time); + } + })->field("FROM_UNIXTIME($field,'$timeType') as days,$str as num")->group('days')->select()->toArray(); + } + + /** + * 获取某个字段总和 + * @param array $where + * @param string $field + * @return float + */ + public function getWhereSumField(array $where, string $field) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where) { + $query->whereBetweenTime('add_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + }) + ->sum($field); + } + + /** + * 根据某字段分组查询 + * @param array $where + * @param string $field + * @param string $group + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGroupField(array $where, string $field, string $group) + { + return $this->search($where) + ->when(isset($where['timeKey']), function ($query) use ($where, $field, $group) { + $query->whereBetweenTime('add_time', $where['timeKey']['start_time'], $where['timeKey']['end_time']); + if ($where['timeKey']['days'] == 1) { + $timeUinx = "%H"; + } elseif ($where['timeKey']['days'] == 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] == 365) { + $timeUinx = "%Y-%m"; + } elseif ($where['timeKey']['days'] > 1 && $where['timeKey']['days'] < 30) { + $timeUinx = "%Y-%m-%d"; + } elseif ($where['timeKey']['days'] > 30 && $where['timeKey']['days'] < 365) { + $timeUinx = "%Y-%m"; + } else { + $timeUinx = "%Y-%m"; + } + $query->field("sum($field) as number,FROM_UNIXTIME($group, '$timeUinx') as time"); + $query->group("FROM_UNIXTIME($group, '$timeUinx')"); + }) + ->order('add_time ASC')->select()->toArray(); + } + +} diff --git a/app/dao/user/UserRelationDao.php b/app/dao/user/UserRelationDao.php new file mode 100644 index 0000000..005166d --- /dev/null +++ b/app/dao/user/UserRelationDao.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\common\model\user\UserRelation; + +/** + * Class UserRelationDao + * @package app\dao\user + */ +class UserRelationDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserRelation::class; + } + + /** + * 获取收藏列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', array $with = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->when($with, function($query) use ($with) { + $query->with($with); + })->when($page && $limit, function($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('add_time desc')->select()->toArray(); + } +} diff --git a/app/dao/user/UserSearchDao.php b/app/dao/user/UserSearchDao.php new file mode 100644 index 0000000..53b5ab4 --- /dev/null +++ b/app/dao/user/UserSearchDao.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\model\user\UserSearch; + +/** + * 用户搜索 + * Class UserSearchDao + * @package app\dao\user + */ +class UserSearchDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserSearch::class; + } + + /** + * 获取列表 + * @param array $where + * @param string $order + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where = [], string $order = 'id desc', int $page = 0, int $limit = 0): array + { + return $this->search($where)->order($order)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * * 获取全局|用户某个关键词搜素结果 + * @param int $uid + * @param string $keyword 关键词 + * @param int $preTime 多长时间内认为结果集有效 + * @return array|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getKeywordResult(int $uid, string $keyword, int $preTime = 7200) + { + if (!$keyword) return []; + $where = ['keyword' => $keyword]; + if ($uid) $where['uid'] = $uid; + return $this->search($where)->when($uid && $preTime == 0, function ($query) { + $query->where('is_del', 0); + })->when($preTime > 0, function ($query) use ($preTime) { + $query->where('add_time', '>', time() - $preTime); + })->order('add_time desc,id desc')->find() ?? []; + } +} diff --git a/app/dao/user/UserSignDao.php b/app/dao/user/UserSignDao.php new file mode 100644 index 0000000..b7c1608 --- /dev/null +++ b/app/dao/user/UserSignDao.php @@ -0,0 +1,66 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\common\model\user\UserSign; + +/** + * + * Class UserSignDao + * @package app\dao\user + */ +class UserSignDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserSign::class; + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field, int $page, int $limit) + { + return $this->search($where)->field($field)->order('id desc')->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getListGroup(array $where, string $field, int $page, int $limit, string $group) + { + return $this->search($where)->field($field)->order('id desc')->group($group)->page($page, $limit)->select()->toArray(); + } +} diff --git a/app/dao/user/UserSpreadDao.php b/app/dao/user/UserSpreadDao.php new file mode 100644 index 0000000..c937596 --- /dev/null +++ b/app/dao/user/UserSpreadDao.php @@ -0,0 +1,125 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user; + +use app\dao\BaseDao; +use app\model\user\UserSpread; + +/** + * Class UserSpreadDao + * @package app\dao\user + */ +class UserSpreadDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserSpread::class; + } + + /** + * 获取推广列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $field = '*', array $with = [], int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field) + ->when($with, function ($query) use ($with) { + $query->with($with); + })->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('spread_time desc,id desc')->select()->toArray(); + } + + /** + * 获取推广uids + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSpreadUids(array $where) + { + return $this->search($where)->order('spread_time desc,id desc')->column('uid'); + } + + /** + * 获取一段时间内推广人数 + * @param $datebefor + * @param $dateafter + * @return mixed + */ + public function spreadTimeList(array $where, array $time, string $timeType = "week", string $countField = '*', string $sumField = 'pay_price', string $groupField = 'add_time') + { + return $this->getModel()->where($where) + ->where($groupField, 'between time', $time) + ->when($timeType, function ($query) use ($timeType, $countField, $sumField, $groupField) { + switch ($timeType) { + case "hour": + $timeUnix = "%H"; + break; + case "day" : + $timeUnix = "%m-%d"; + break; + case "week" : + $timeUnix = "%w"; + break; + case "month" : + $timeUnix = "%d"; + break; + case "year" : + $timeUnix = "%m"; + break; + default: + $timeUnix = "%m-%d"; + break; + } + $query->field("FROM_UNIXTIME(`" . $groupField . "`,'$timeUnix') as day,count(`" . $countField . "`) as count,sum(`" . $sumField . "`) as price"); + $query->group("FROM_UNIXTIME($groupField, '$timeUnix')"); + })->order('add_time asc')->select()->toArray(); + } + + /** + * 获取推广人排行 + * @param array $time + * @param string $field + * @param int $page + * @param int $limit + */ + public function getAgentRankList(array $time, string $field = '*', int $page = 0, int $limit = 0) + { + return $this->getModel()->with(['spreadUser']) + ->field($field) + ->order('count desc') + ->order('uid desc') + ->where('spread_time', 'BETWEEN', $time) + ->page($page, $limit) + ->group('spread_uid') + ->select()->toArray(); + } +} diff --git a/app/dao/user/group/UserGroupDao.php b/app/dao/user/group/UserGroupDao.php new file mode 100644 index 0000000..9a48fbb --- /dev/null +++ b/app/dao/user/group/UserGroupDao.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user\group; + +use app\dao\BaseDao; +use app\common\model\user\UserGroup; + +/** + * 用户分组 + * Class UserGroupDao + * @package app\dao\user\group + */ +class UserGroupDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserGroup::class; + } + + /** + * 获取列表 + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where = [], string $field = '*', int $page = 0, int $limit = 0) + { + return $this->search($where)->field($field)->when($page, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->order('id desc')->select()->toArray(); + } +} diff --git a/app/dao/user/label/UserLabelRelationDao.php b/app/dao/user/label/UserLabelRelationDao.php new file mode 100644 index 0000000..bd547aa --- /dev/null +++ b/app/dao/user/label/UserLabelRelationDao.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user\label; + +use app\dao\BaseDao; +use app\model\user\label\UserLabelRelation; + +/** + * 用户关联标签 + * Class UserLabelRelationDao + * @package app\dao\user\label + */ +class UserLabelRelationDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserLabelRelation::class; + } + + /** + * 获取用户个标签列表按照用户id进行分组 + * @param array $uids + * @param int $type + * @param int $relation_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLabelList(array $uids, int $type = 0, int $relation_id = 0) + { + return $this->search(['uid' => $uids])->where('type', $type)->where('relation_id', $relation_id)->with('label')->select()->toArray(); + } + +} diff --git a/app/dao/user/level/UserLevelDao.php b/app/dao/user/level/UserLevelDao.php new file mode 100644 index 0000000..be26972 --- /dev/null +++ b/app/dao/user/level/UserLevelDao.php @@ -0,0 +1,61 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\user\level; + +use app\dao\BaseDao; +use app\model\user\level\UserLevel; + +/** + * 用户等技 + * Class UserLevelDao + * @package app\dao\user\level + */ +class UserLevelDao extends BaseDao +{ + + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return UserLevel::class; + } + + /** + * 根据uid 获取用户会员等级详细信息 + * @param int $uid + * @param string $field + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserLevel(int $uid, string $field = '*') + { + return $this->getModel()->where('uid', $uid)->where('is_del', 0)->where('status', 1)->field($field)->with(['levelInfo'])->order('grade desc,add_time desc')->find(); + } + + /** + * 获取用户等级折扣 + * @param int $uid + * @return mixed + */ + public function getDiscount(int $uid) + { + $level = $this->getModel()->where(['uid' => $uid, 'is_del' => 0, 'status' => 1])->with(['levelInfo' => function ($query) { + $query->field('id,discount')->bind(['discount_num' => 'discount']); + }])->order('id desc')->find(); + return $level ? $level->toArray()['discount_num'] : NULL; + } +} diff --git a/app/dao/user/member/MemberCardDao.php b/app/dao/user/member/MemberCardDao.php new file mode 100644 index 0000000..49d8a13 --- /dev/null +++ b/app/dao/user/member/MemberCardDao.php @@ -0,0 +1,67 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\user\member; + + +use app\dao\BaseDao; +use app\model\user\member\MemberCard; + +/** + * Class MemberCardDao + * @package app\dao\user\member + */ +class MemberCardDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return MemberCard::class; + } + + /** + * 获取列表 + * @param array $where + * @param int $page + * @param int $limit + * @param array|string[] $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSearchList(array $where, int $page = 0, int $limit = 0, array $field = ['*']) + { + return $this->search($where)->order('use_time desc,id desc') + ->field($field) + ->when($page > 0 || $limit > 0, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + + /** + * 获取当条会员卡信息 + * @param array $where + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOneByWhere(array $where) + { + return $this->getModel()->where($where)->find(); + } + + +} diff --git a/app/dao/user/member/MemberRightDao.php b/app/dao/user/member/MemberRightDao.php new file mode 100644 index 0000000..983f965 --- /dev/null +++ b/app/dao/user/member/MemberRightDao.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\user\member; + + +use app\dao\BaseDao; +use app\model\user\member\MemberRight; + +/** + * Class MemberRightDao + * @package app\dao\user\member + */ +class MemberRightDao extends BaseDao +{ + /** 设置模型 + * @return string + */ + protected function setModel(): string + { + return MemberRight::class; + } + + /**获取会员权益接口 + * @param array $where + * @param int $page + * @param int $limit + * @param array $field + * @return \think\Collection + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSearchList(array $where, int $page = 0, int $limit = 0, array $field = ['*']) + { + return $this->search($where)->order('sort desc,id desc') + ->field($field) + ->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + }) + ->select()->toArray(); + + } + + +} diff --git a/app/dao/user/member/MemberShipDao.php b/app/dao/user/member/MemberShipDao.php new file mode 100644 index 0000000..6f09532 --- /dev/null +++ b/app/dao/user/member/MemberShipDao.php @@ -0,0 +1,67 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\user\member; + + +use app\dao\BaseDao; +use app\model\user\member\MemberShip; + +/** + * Class MemberShipDao + * @package app\dao\user\member + */ +class MemberShipDao extends BaseDao +{ + /** + * 设置模型 + * @return string + */ + protected function setModel(): string + { + return MemberShip::class; + } + + /** + * 后台获取会员卡类型接口 + * @param array $where + * @param int $page + * @param int $limit + * @param array $field + * @return \think\Collection + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSearchList(array $where, int $page = 0, int $limit = 0, array $field = ['*']) + { + return $this->search($where)->order('sort desc,id desc') + ->field($field) + ->page($page, $limit) + ->select(); + + } + + /** + * 获取会员类型api接口 + * @param array $where + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getApiList(array $where) + { + return $this->search()->where($where)->order('sort desc')->select()->toArray(); + } + + +} diff --git a/app/dao/wechat/WechatCardDao.php b/app/dao/wechat/WechatCardDao.php new file mode 100644 index 0000000..a01f876 --- /dev/null +++ b/app/dao/wechat/WechatCardDao.php @@ -0,0 +1,48 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\dao\wechat; + +use think\model; +use app\dao\BaseDao; +use app\model\wechat\WechatCard; + +/** + * 微信卡券 + * Class WechatCardDao + * @package app\dao\wechat + */ +class WechatCardDao extends BaseDao +{ + protected function setModel(): string + { + return WechatCard::class; + } + + /** + * 获取卡券列表 + * @param array $where + * @param int $page + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, int $page = 0, int $limit = 0) + { + return $this->search($where)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->select()->toArray(); + } + +} diff --git a/app/dao/work/WorkClientDao.php b/app/dao/work/WorkClientDao.php new file mode 100644 index 0000000..c9a8dfa --- /dev/null +++ b/app/dao/work/WorkClientDao.php @@ -0,0 +1,105 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\work; + +use app\dao\BaseDao; +use app\model\work\WorkClient; +use crmeb\basic\BaseAuth; +use crmeb\traits\SearchDaoTrait; + +/** + * 企业微信客户 + * Class WorkClientDao + * @package app\dao\work + */ +class WorkClientDao extends BaseDao +{ + + use SearchDaoTrait; + + /** + * @return string + */ + protected function setModel(): string + { + return WorkClient::class; + } + + /** + * @param array $where + * @param bool $authWhere + * @return \crmeb\basic\BaseModel + */ + public function searchWhere(array $where, bool $authWhere = true) + { + [$with, $whereKey] = app()->make(BaseAuth::class)->________(array_keys($where), $this->setModel()); + $whereData = []; + foreach ($whereKey as $key) { + if (isset($where[$key])) { + $whereData[$key] = $where[$key]; + } + } + + return $this->getModel()->withSearch($with, $where)->when(!empty($where['label']) || !empty($where['notLabel']), function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('work_client_follow')->whereIn('id', function ($query) use ($where) { + $query->name('work_client_follow_tags')->when(!empty($where['label']), function ($query) use ($where) { + $query->whereIn('tag_id', $where['label']); + })->when(!empty($where['notLabel']), function ($query) use ($where) { + $query->whereNotIn('tag_id', $where['notLabel']); + })->field('follow_id'); + })->field('client_id'); + }); + })->when(!empty($where['userid']), function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('work_client_follow')->when(!empty($where['state']), function ($query) use ($where) { + $query->where('state', '<>', ''); + })->whereIn('userid', $where['userid'])->field('client_id'); + }); + })->when(isset($where['name']) && '' !== $where['name'], function ($query) use ($where) { + $query->whereLike('name', '%' . $where['name'] . '%'); + })->when(!empty($where['corp_id']), function ($query) use ($where) { + $query->where('corp_id', $where['corp_id']); + })->when(!empty($where['gender']), function ($query) use ($where) { + $query->where('gender', $where['gender']); + })->when(!empty($where['state']) && empty($where['userid']), function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('work_client_follow')->where('state', '<>', '')->field('client_id'); + }); + })->when(!empty($where['status']), function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('work_client_follow')->where('is_del_user', $where['status'])->field('client_id'); + }); + })->when(!empty($where['notUserid']), function ($query) use ($where) { + $query->whereNotIn('external_userid', $where['notUserid']); + }); + } + + /** + * @param array $where + * @return int + */ + public function getClientCount(array $where) + { + return $this->searchWhere($where)->count(); + } + + /** + * 获取客户userid + * @param array $where + * @return array + */ + public function getClientUserIds(array $where) + { + return $this->searchWhere($where)->column('external_userid'); + } +} diff --git a/app/dao/work/WorkGroupChatAuthDao.php b/app/dao/work/WorkGroupChatAuthDao.php new file mode 100644 index 0000000..29ff2ee --- /dev/null +++ b/app/dao/work/WorkGroupChatAuthDao.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\work; + + +use app\dao\BaseDao; +use app\model\work\WorkGroupChatAuth; +use crmeb\traits\SearchDaoTrait; + +/** + * 企业微信自动拉群 + * Class WorkGroupChatAuthDao + * @package app\dao\work + */ +class WorkGroupChatAuthDao extends BaseDao +{ + + use SearchDaoTrait; + + /** + * @return string + */ + protected function setModel(): string + { + return WorkGroupChatAuth::class; + } +} diff --git a/app/dao/work/WorkMediaDao.php b/app/dao/work/WorkMediaDao.php new file mode 100644 index 0000000..eec805f --- /dev/null +++ b/app/dao/work/WorkMediaDao.php @@ -0,0 +1,45 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\work; + + +use app\dao\BaseDao; +use app\model\work\WorkMedia; + +/** + * Class WorkMediaDao + * @package app\dao\work + */ +class WorkMediaDao extends BaseDao +{ + + /** + * @return string + */ + protected function setModel(): string + { + return WorkMedia::class; + } + + /** + * 删除时效的media_id + * @return \crmeb\basic\BaseModel + */ + public function deleteValidFile() + { + return $this->getModel() + ->where('temporary', 1) + ->where('valid_time', '<>', 0) + ->where('valid_time', '<', time()) + ->update(['media_id' => '', 'valid_time' => 0]); + } +} diff --git a/app/dao/work/WorkMemberDao.php b/app/dao/work/WorkMemberDao.php new file mode 100644 index 0000000..141731c --- /dev/null +++ b/app/dao/work/WorkMemberDao.php @@ -0,0 +1,55 @@ + +// +---------------------------------------------------------------------- + +namespace app\dao\work; + + +use app\dao\BaseDao; +use app\model\work\WorkMember; +use crmeb\basic\BaseAuth; +use crmeb\traits\SearchDaoTrait; + +/** + * 企业微信成员 + * Class WorkMemberDao + * @package app\dao\work + */ +class WorkMemberDao extends BaseDao +{ + use SearchDaoTrait; + + /** + * @return string + */ + protected function setModel(): string + { + return WorkMember::class; + } + + /** + * 搜索 + * @param array $where + * @param bool $authWhere + * @return \crmeb\basic\BaseModel + */ + public function searchWhere(array $where, bool $authWhere = true) + { + [$with] = app()->make(BaseAuth::class)->________(array_keys($where), $this->setModel()); + return $this->getModel()->withSearch($with, $where) + ->when(!empty($where['name']), function ($query) use ($where) { + $query->where('id|name|mobile', 'like', '%' . $where['name'] . '%'); + })->when(!empty($where['department']), function ($query) use ($where) { + $query->whereIn('id', function ($query) use ($where) { + $query->name('work_member_relation')->where('department', $where['department'])->field(['member_id']); + }); + }); + } +} diff --git a/app/jobs/BatchHandleJob.php b/app/jobs/BatchHandleJob.php new file mode 100644 index 0000000..c38cc2e --- /dev/null +++ b/app/jobs/BatchHandleJob.php @@ -0,0 +1,111 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs; + + +use app\services\other\queue\QueueServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 批量任务队列 + * Class BatchHandleJob + * @package app\jobs + */ +class BatchHandleJob extends BaseJobs +{ + use QueueTrait; + + /** + * @return mixed + */ + public static function queueName() + { + $default = config('queue.default'); + return config('queue.connections.' . $default . '.batch_queue'); + } + + /** + * 批量任务队列 + * @param false $data + * @param $type + * @param array $other + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function doJob($data = false, $type, array $other = []) + { + /** @var QueueServices $queueServices */ + $queueServices = app()->make(QueueServices::class); + $re = true; + try { + switch ($type) { + case 1://批量发放优惠券 + if (!$data) { + return true; + } + $re = $queueServices->sendCoupon($data, $type); + break; + case 2://批量设置用户分组 + if (!$data) { + return true; + } + $re = $queueServices->setUserGroup($data, $type); + break; + case 3://批量设置用户标签 + if (!$data) { + return true; + } + $re = $queueServices->setUserLabel($data, $type, $other); + break; + case 4://批量上下架商品 + $re = $queueServices->setProductShow($data, $type); + break; + case 5://批量删除商品规格 + $re = $queueServices->delProductRule($type); + break; + case 6://批量删除用户已删除订单 + $re = $queueServices->delOrder($type); + break; + case 7://批量手动发货 + case 8://批量电子面单发货 + case 9://批量配送 + case 10://批量虚拟发货 + $re = $queueServices->orderDelivery($data, $other); + break; + default: + $re = false; + break; + } + } catch (\Throwable $e) { + $queueName = $queueServices->queue_type_name[$type] ?? ''; + Log::error($queueName . '失败,原因' . $e->getMessage()); + $re = false; + } + if ($re === false) $queueServices->delWrongQueue(0, $type, false); + + //清除缓存 + /** @var StoreProductServices $productService */ + $productService = app()->make(StoreProductServices::class); + /** @var StoreProductAttrServices $productAttrService */ + $productAttrService = app()->make(StoreProductAttrServices::class); + $productService->cacheTag()->clear(); + $productAttrService->cacheTag()->clear(); + + return true; + } +} diff --git a/app/jobs/activity/StorePromotionsJob.php b/app/jobs/activity/StorePromotionsJob.php new file mode 100644 index 0000000..2212928 --- /dev/null +++ b/app/jobs/activity/StorePromotionsJob.php @@ -0,0 +1,174 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\activity; + +use app\services\user\UserServices; +use app\services\user\UserBillServices; +use app\services\user\label\UserLabelRelationServices; +use app\services\activity\coupon\StoreCouponIssueServices; +use app\services\activity\promotions\StorePromotionsAuxiliaryServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 营销:优惠活动 + * Class StorePromotionsJob + * @package app\jobs\activity + */ +class StorePromotionsJob extends BaseJobs +{ + + use QueueTrait; + + /** + * 赠送 + * @param $orderInfo + * @return bool + */ + public function give($orderInfo) + { + $uid = (int)$orderInfo['uid']; + $promotions_give = []; + if (isset($orderInfo['promotions_give']) && $orderInfo['promotions_give']) { + $promotions_give = is_string($orderInfo['promotions_give']) ? json_decode($promotions_give, true) : $orderInfo['promotions_give']; + } + $give_integral = $promotions_give['give_integral'] ?? 0; + if ($give_integral) { + try { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $balance = bcadd((string)$userInfo['integral'], (string)$give_integral, 0); + $userServices->update(['uid' => $userInfo['uid']], ['integral' => $balance]); + $userBillServices->income('order_promotions_give_integral', $uid, (int)$give_integral, (int)$balance, $orderInfo['id']); + } catch (\Throwable $e) { + Log::error('优惠活动下单赠送积分失败,失败原因:' . $e->getMessage()); + } + + } + $give_coupon = $promotions_give['give_coupon'] ?? []; + $this->giveCoupon($uid, $give_coupon); + return true; + } + + /** + * 赠送优惠券 + * @param int $uid + * @param array $give_coupon + * @return bool + */ + public function giveCoupon(int $uid, array $give_coupon) + { + if ($give_coupon) { + try { + /** @var StoreCouponIssueServices $storeCoupon */ + $storeCoupon = app()->make(StoreCouponIssueServices::class); + $storeCoupon->orderPayGiveCoupon($uid, $give_coupon); + } catch (\Throwable $e) { + Log::error('优惠活动下单赠送优惠券失败,失败原因:' . $e->getMessage()); + } + } + return true; + } + + /** + * 扣除优惠活动赠品限量 + * @param array $promotions_give + * @param bool $isDec + * @rerunt bool + */ + public function changeGiveLimit(array $promotions_give, bool $isDec = true) + { + if ($promotions_give){ + try { + $promotionsArr = $promotions_give['promotions'] ?? []; + if ($promotionsArr) { + /** @var StorePromotionsAuxiliaryServices $storePromotionsAuxiliaryServices */ + $storePromotionsAuxiliaryServices = app()->make(StorePromotionsAuxiliaryServices::class); + $giveCoupon = $promotions_give['give_coupon'] ?? []; + $getPromotionsId = function($id, $key) use ($promotionsArr) { + $pid = 0; + foreach($promotionsArr as $promotions) { + $k = $key == 'coupon_id' ? 'giveCoupon' : 'giveProducts'; + $arr = $promotions[$k] ?? []; + $ids = []; + if ($arr) $ids = array_column($arr, $key); + if ($ids && in_array($id, $ids)) $pid = $promotions['id'] ?? $promotionsArr['id'] ?? 0; + } + return $pid; + }; + if ($giveCoupon) { + foreach ($giveCoupon as $coupon_id) { + $promotions_id = $getPromotionsId($coupon_id, 'coupon_id'); + if ($promotions_id) $storePromotionsAuxiliaryServices->updateLimit([$promotions_id], 2, (int)$coupon_id, $isDec); + } + } + $giveProduct = $promotions_give['give_product'] ?? []; + if ($giveProduct) { + foreach ($giveProduct as $give) { + $promotions_id = (int)$give['promotions_id'] ?? 0; + $product_id = (int)$give['product_id'] ?? 0; + $unique = $give['unique'] ?? ''; + $cart_num = (int)$give['cart_num'] ?? 1; + if ($promotions_id && $product_id && $unique) $storePromotionsAuxiliaryServices->updateLimit([$promotions_id], 3, (int)$product_id, $isDec, $unique, $cart_num); + } + } + } + + } catch (\Throwable $e) { + Log::error('订单创建优惠活动赠品限量扣除失败,失败原因:' . $e->getMessage()); + } + + } + return true; + } + + + /** + * 设置用户购买的标签 + * @param $orderInfo + */ + public function setUserLabel($orderInfo) + { + try { + $promotions_give = []; + if (isset($orderInfo['promotions_give']) && $orderInfo['promotions_give']) { + $promotions_give = is_string($orderInfo['promotions_give']) ? json_decode($promotions_give, true) : $orderInfo['promotions_give']; + } + $promotions = $promotions_give['promotions'] ?? []; + if (!$promotions) { + return true; + } + $labelIds = []; + foreach ($promotions as $key => $value) { + $label_id = is_string($value['label_id']) ? explode(',', $value['label_id']) : $value['label_id']; + $labelIds = array_merge($labelIds, $label_id); + } + if (!$labelIds) { + return true; + } + $labelIds = array_unique($labelIds); + $store_id = $orderInfo['store_id'] ?? 0; + $type = $store_id ? 1 : 0; + /** @var UserLabelRelationServices $labelServices */ + $labelServices = app()->make(UserLabelRelationServices::class); + $labelServices->setUserLable([$orderInfo['uid']], $labelIds, $type, $store_id); + } catch (\Throwable $e) { + Log::error('用户标签添加失败,失败原因:' . $e->getMessage()); + } + return true; + } + +} diff --git a/app/jobs/activity/pink/AuthPinkFail.php b/app/jobs/activity/pink/AuthPinkFail.php new file mode 100644 index 0000000..e2529a7 --- /dev/null +++ b/app/jobs/activity/pink/AuthPinkFail.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\activity\pink; + + +use app\services\activity\combination\StorePinkServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\db\exception\DataNotFoundException; +use think\db\exception\ModelNotFoundException; + +/** + * 分页处理拼团 + * Class AuthPinkFail + * @package app\jobs\pink + */ +class AuthPinkFail extends BaseJobs +{ + use QueueTrait; + + /** + * @return string + */ + protected static function queueName() + { + return 'CRMEB_PRO_TASK'; + } + + /** + * @param $page + * @param $limit + * @return bool + * @throws DataNotFoundException + * @throws ModelNotFoundException + */ + public function doJob($page, $limit) + { + /** @var StorePinkServices $service */ + $service = app()->make(StorePinkServices::class); + return $service->statusPink((int)$page, (int)$limit); + } + +} diff --git a/app/jobs/activity/pink/PinkJob.php b/app/jobs/activity/pink/PinkJob.php new file mode 100644 index 0000000..c177a4c --- /dev/null +++ b/app/jobs/activity/pink/PinkJob.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\activity\pink; + + +use app\services\activity\combination\StorePinkServices; +use app\services\order\StoreOrderRefundServices; +use app\services\order\StoreOrderServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 拼团失败 + * Class PinkJob + * @package app\jobs + */ +class PinkJob extends BaseJobs +{ + use QueueTrait; + + public function doJob($pinkId) + { + try { + /** @var StorePinkServices $pinkService */ + $pinkService = app()->make(StorePinkServices::class); + $info = $pinkService->get((int)$pinkId); + if (!$info) { + return true; + } + //已经成功 || 失败处理 + if (in_array($info['status'], [2, 3])) { + return true; + } + [$pinkAll, $pinkT, $count, $idAll, $uidAll] = $pinkService->getPinkMemberAndPinkK($info); + $pinkService->pinkFail($pinkAll, $pinkT, 0); + + } catch (\Throwable $e) { + Log::error('拼团超时处理失败,原因:' . $e->getMessage()); + } + return true; + } + + /** + * 创建拼团 + * @param $orderInfo + * @return bool + */ + public function createPink($orderInfo) + { + if (!$orderInfo) { + return true; + } + try { + /** @var StorePinkServices $pinkServices */ + $pinkServices = app()->make(StorePinkServices::class); + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $resPink = $pinkServices->createPink($orderServices->tidyOrder($orderInfo, true));//创建拼团 + } catch (\Throwable $e) { + Log::error('创建拼团失败失败,原因:' . $e->getMessage()); + } + return true; + } +} diff --git a/app/jobs/agent/AutoAgentJob.php b/app/jobs/agent/AutoAgentJob.php new file mode 100644 index 0000000..c7364e8 --- /dev/null +++ b/app/jobs/agent/AutoAgentJob.php @@ -0,0 +1,53 @@ + +// +---------------------------------------------------------------------- +namespace app\jobs\agent; + +use crmeb\basic\BaseJobs; +use app\services\agent\AgentManageServices; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 自动解除上下级 + * Class AutoAgentJob + * @package app\jobs\user + */ +class AutoAgentJob extends BaseJobs +{ + use QueueTrait; + + /** + * @return string + */ + protected static function queueName() + { + return 'CRMEB_PRO_TASK'; + } + + /** + * @param $page + * @param $limit + * @param $where + */ + public function doJob($page, $limit, $where) + { + //自动解绑上级绑定 + try { + /** @var AgentManageServices $agentManage */ + $agentManage = app()->make(AgentManageServices::class); + return $agentManage->startRemoveSpread($page, $limit, $where); + } catch (\Throwable $e) { + Log::error('自动解除上级绑定失败,失败原因:[' . class_basename($this) . ']' . $e->getMessage()); + } + } + + +} diff --git a/app/jobs/agent/SystemJob.php b/app/jobs/agent/SystemJob.php new file mode 100644 index 0000000..4fb5bef --- /dev/null +++ b/app/jobs/agent/SystemJob.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\agent; + + +use app\services\agent\AgentManageServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; + +/** + * 重置分销有效期 + * Class SystemJob + * @package app\jobs + */ +class SystemJob extends BaseJobs +{ + + use QueueTrait; + + + public function resetSpreadTime() + { + /** @var AgentManageServices $agentManage */ + $agentManage = app()->make(AgentManageServices::class); + $agentManage->resetSpreadTime(); + } +} diff --git a/app/jobs/notice/EnterpriseWechatJob.php b/app/jobs/notice/EnterpriseWechatJob.php new file mode 100644 index 0000000..fc9fdbb --- /dev/null +++ b/app/jobs/notice/EnterpriseWechatJob.php @@ -0,0 +1,52 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\notice; + +use crmeb\basic\BaseJobs; +use crmeb\services\HttpService; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +class EnterpriseWechatJob extends BaseJobs +{ + use QueueTrait; + + + /** + * 给企业微信群发送消息 + */ + public function doJob($data, $url, $ent_wechat_text) + { + try { + $str = $ent_wechat_text; + foreach ($data as $key => $item) { + $str = str_replace('{' . $key . '}', $item, $str); + } + $s = explode('\n', $str); + $d = ''; + foreach ($s as $item) { + $d .= $item . "\n>"; + } + $d = substr($d, 0, strlen($d) - 2); + $datas = [ + 'msgtype' => 'markdown', + 'markdown' => ['content' => $d] + ]; + HttpService::postRequest($url, json_encode($datas)); + return true; + } catch (\Throwable $e) { + Log::error('发送企业群消息失败,失败原因:' . $e->getMessage()); + } + + } + +} diff --git a/app/jobs/notice/PrintJob.php b/app/jobs/notice/PrintJob.php new file mode 100644 index 0000000..88b8de5 --- /dev/null +++ b/app/jobs/notice/PrintJob.php @@ -0,0 +1,94 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\notice; + + +use app\services\activity\collage\UserCollageServices; +use app\services\activity\integral\StoreIntegralOrderServices; +use app\services\order\StoreOrderServices; +use crmeb\basic\BaseJobs; +use crmeb\services\printer\Printer; +use crmeb\traits\QueueTrait; +use think\facade\Log; + + +/** + * 小票打印 + * Class PrintJob + * @package app\jobs\notice + */ +class PrintJob extends BaseJobs +{ + use QueueTrait; + + /** + * 小票打印 + * @param $name + * @param $configData + * @param $order + * @param $product + * @return bool|void + */ + public function doJob($id, $isTable = true) + { + try { + if (!$id) { + return true; + } + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $orderServices->orderPrint((int)$id, -1, -1, !!$isTable); + } catch (\Throwable $e) { + Log::error('小票打印失败失败,失败原因:' . $e->getMessage()); + } + return true; + } + + /** + * 桌码流水小票打印 + * @param $tableId + * @param $store_id + * @return bool + */ + public function tableDoJob($tableId, $store_id) + { + try { + if (!(int)$tableId || !(int)$store_id) { + return true; + } + /** @var UserCollageServices $collageServices */ + $collageServices = app()->make(UserCollageServices::class); + $collageServices->tablePrint((int)$tableId, (int)$store_id); + } catch (\Throwable $e) { + Log::error('桌码流水小票打印失败失败,失败原因:' . $e->getMessage()); + } + return true; + } + + /** + * 积分订单小票打印 + * @param $id + * @return bool + */ + public function IntegralDoJob($id) + { + try { + /** @var StoreIntegralOrderServices $storeIntegralOrderServices */ + $storeIntegralOrderServices = app()->make(StoreIntegralOrderServices::class); + $storeIntegralOrderServices->orderPrint((int)$id); + } catch (\Throwable $e) { + Log::error('桌码流水小票打印失败失败,失败原因:' . $e->getMessage()); + } + return true; + } + +} diff --git a/app/jobs/notice/SmsAdminJob.php b/app/jobs/notice/SmsAdminJob.php new file mode 100644 index 0000000..18be771 --- /dev/null +++ b/app/jobs/notice/SmsAdminJob.php @@ -0,0 +1,99 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\notice; + + +use app\services\message\sms\SmsSendServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 短信通知管理员 + * Class SmsAdminJob + * @package app\jobs + */ +class SmsAdminJob extends BaseJobs +{ + use QueueTrait; + + /** + * 退款发送管理员消息任务 + * @param $switch + * @param $adminList + * @param $order + * @return bool + */ + public function sendAdminRefund($switch, $adminList, $order) + { + if (!$switch) { + return true; + } + try { + /** @var SmsSendServices $smsServices */ + $smsServices = app()->make(SmsSendServices::class); + foreach ($adminList as $item) { + $data = ['order_id' => $order['order_id'], 'admin_name' => $item['nickname']]; + $smsServices->send(true, $item['phone'], $data, 'ADMIN_RETURN_GOODS_CODE'); + } + } catch (\Throwable $e) { + Log::error('退款发送管理员消息失败,原因:' . $e->getMessage()); + } + return true; + } + + /** + * 用户确认收货管理员短信提醒 + * @param $switch + * @param $adminList + * @param $order + * @return bool + */ + public function sendAdminConfirmTakeOver($switch, $adminList, $order) + { + if (!$switch) { + return true; + } + try { + /** @var SmsSendServices $smsServices */ + $smsServices = app()->make(SmsSendServices::class); + foreach ($adminList as $item) { + $data = ['order_id' => $order['order_id'], 'admin_name' => $item['nickname']]; + $smsServices->send(true, $item['phone'], $data, 'ADMIN_TAKE_DELIVERY_CODE'); + } + } catch (\Throwable $e) { + Log::error('用户确认收货管理员短信提醒失败,原因:' . $e->getMessage()); + } + return true; + } + + /** + * 下单成功给客服管理员发送短信 + * @param $switch + * @param $adminList + * @param $order + * @return bool + */ + public function sendAdminPaySuccess($switch, $adminList, $order) + { + if (!$switch) { + return true; + } + /** @var SmsSendServices $smsServices */ + $smsServices = app()->make(SmsSendServices::class); + foreach ($adminList as $item) { + $data = ['order_id' => $order['order_id'], 'admin_name' => $item['nickname']]; + $smsServices->send(true, $item['phone'], $data, 'ADMIN_PAY_SUCCESS_CODE'); + } + return true; + } +} diff --git a/app/jobs/notice/SmsJob.php b/app/jobs/notice/SmsJob.php new file mode 100644 index 0000000..93849bf --- /dev/null +++ b/app/jobs/notice/SmsJob.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\notice; + + +use app\services\message\sms\SmsSendServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 短信 + * Class SmsJob + * @package app\jobs\notice + */ +class SmsJob extends BaseJobs +{ + use QueueTrait; + + /** + * 发送短信 + * @param $switch + * @param $adminList + * @param $order + * @return bool + */ + public function doJob($phone, array $data, string $template) + { + + try { + /** @var SmsSendServices $smsServices */ + $smsServices = app()->make(SmsSendServices::class); + $smsServices->send(true, $phone, $data, $template); + return true; + } catch (\Throwable $e) { + Log::error('发送短信消息失败,失败原因:' . $e->getMessage()); + } + + } + +} diff --git a/app/jobs/order/AutoOrderUnpaidCancelJob.php b/app/jobs/order/AutoOrderUnpaidCancelJob.php new file mode 100644 index 0000000..02cb27b --- /dev/null +++ b/app/jobs/order/AutoOrderUnpaidCancelJob.php @@ -0,0 +1,51 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\order; + + +use app\services\order\StoreOrderServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; + +/** + * 自动取消未支付订单 + * Class AutoOrderUnpaidCancelJob + * @package app\jobs\order + */ +class AutoOrderUnpaidCancelJob extends BaseJobs +{ + use QueueTrait; + + /** + * @return string + */ + protected static function queueName() + { + return 'CRMEB_PRO_TASK'; + } + + /** + * @param $page + * @param $limit + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function doJob($page, $limit) + { + /** @var StoreOrderServices $service */ + $service = app()->make(StoreOrderServices::class); + return $service->runOrderUnpaidCancel($page, $limit); + } + +} diff --git a/app/jobs/order/AutoTakeOrderJob.php b/app/jobs/order/AutoTakeOrderJob.php new file mode 100644 index 0000000..26ba3a3 --- /dev/null +++ b/app/jobs/order/AutoTakeOrderJob.php @@ -0,0 +1,49 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\order; + + +use app\services\order\StoreOrderTakeServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; + +/** + * 自动执行确认收货 + * Class AutoTakeOrderJob + * @package app\jobs\order + */ +class AutoTakeOrderJob extends BaseJobs +{ + + use QueueTrait; + + /** + * @return string + */ + protected static function queueName() + { + return 'CRMEB_PRO_TASK'; + } + + /** + * @param $where + * @param $page + * @param $limit + * @return bool + */ + public function doJob($where, $page, $limit) + { + /** @var StoreOrderTakeServices $service */ + $service = app()->make(StoreOrderTakeServices::class); + return $service->runAutoTakeOrder($where, $page, $limit); + } +} diff --git a/app/jobs/order/OrderTakeJob.php b/app/jobs/order/OrderTakeJob.php new file mode 100644 index 0000000..294143f --- /dev/null +++ b/app/jobs/order/OrderTakeJob.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\order; + + +use app\services\order\StoreOrderStatusServices; +use app\services\order\StoreOrderTakeServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 订单收货任务 + * Class OrderTakeJob + * @package app\jobs + */ +class OrderTakeJob extends BaseJobs +{ + use QueueTrait; + + /** + * @return string + */ + protected static function queueName() + { + return 'CRMEB_PRO_TASK'; + } + + public function doJob($order) + { + if (!$order) return true; + /** @var StoreOrderTakeServices $service */ + $service = app()->make(StoreOrderTakeServices::class); + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $res = $service->update($order['id'], ['status' => 2]) && $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'user_take_delivery', + 'change_message' => '用户已收货', + 'change_time' => time() + ]); + $order = $service->get((int)$order['id']); + $res = $res && $service->storeProductOrderUserTakeDelivery($order); + if (!$res) { + Log::error('收货失败:' . $order['order_id']); + } + return true; + } +} diff --git a/app/jobs/order/SpliteStoreOrderJob.php b/app/jobs/order/SpliteStoreOrderJob.php new file mode 100644 index 0000000..ca31d59 --- /dev/null +++ b/app/jobs/order/SpliteStoreOrderJob.php @@ -0,0 +1,55 @@ +make(StoreOrderCartInfoServices::class); + $storeName = $orderInfoServices->getCarIdByProductTitle((int)$orderInfo['id']); + $orderInfo['storeName'] = substrUTf8($storeName, 20, 'UTF-8', ''); + $orderInfo['send_name'] = $orderInfo['real_name']; + //分配完成用户推送消息事件(门店小票打印) + event('notice.notice', [$orderInfo, 'order_pay_success']); + } + if (isset($orderInfo['store_id']) && $orderInfo['store_id']) { + //记录门店用户 + /** @var StoreUserServices $storeUserServices */ + $storeUserServices = app()->make(StoreUserServices::class); + $storeUserServices->setStoreUser((int)$orderInfo['uid'], (int)$orderInfo['store_id']); + } + event('notice.notice', [$orderInfo, 'admin_pay_success_code']); + return true; + } + +} diff --git a/app/jobs/product/ProductCategoryBrandJob.php b/app/jobs/product/ProductCategoryBrandJob.php new file mode 100644 index 0000000..ff1f611 --- /dev/null +++ b/app/jobs/product/ProductCategoryBrandJob.php @@ -0,0 +1,86 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\product; + + +use app\services\product\label\StoreProductLabelAuxiliaryServices; +use app\services\product\product\StoreProductCategoryBrandServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 商品、分类、品牌 + * Class ProductCategoryBrandJob + * @package app\jobs\product + */ +class ProductCategoryBrandJob extends BaseJobs +{ + use QueueTrait; + + /** + * @param int $id + * @param array $cate_id + * @param array $brand_id + * @return bool + */ + public function doJob(int $id, array $cate_id, array $brand_id, int $status = 1) + { + if (!$id) { + return true; + } + try { + /** @var StoreProductCategoryBrandServices $services */ + $services = app()->make(StoreProductCategoryBrandServices::class); + //标签关联 + $services->saveRelation($id, $cate_id, $brand_id, $status); + } catch (\Throwable $e) { + Log::error('写入商品、分类、品牌关联发生错误,错误原因:' . $e->getMessage()); + } + return true; + } + + /** + * 处理关联数据 + * @param int $id + * @param string $field + * @param string $updateField + * @param int $status + * @return bool + */ + public function setShow(int $id, string $field = 'cate_id', string $updateField = 'status', int $status = 1) + { + if (!$id || !$field) { + return true; + } + try { + if (!in_array($field, ['product_id', 'cate_id', 'brand_id'])) { + return true; + } + if (!in_array($updateField, ['status', 'is_del'])) { + return true; + } + /** @var StoreProductCategoryBrandServices $services */ + $services = app()->make(StoreProductCategoryBrandServices::class); + $where = [$field => $id]; + if ($updateField == 'is_del') { + $services->delete($where); + } else { + $services->update($where, [$updateField => $status]); + } + } catch (\Throwable $e) { + Log::error('修改商品、分类、品牌关联发生错误,错误原因:' . $e->getMessage()); + } + return true; + } + +} diff --git a/app/jobs/product/ProductLogJob.php b/app/jobs/product/ProductLogJob.php new file mode 100644 index 0000000..75f9f6f --- /dev/null +++ b/app/jobs/product/ProductLogJob.php @@ -0,0 +1,60 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\product; + + +use app\services\product\product\StoreProductLogServices; +use app\services\product\product\StoreVisitServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 商品记录 + * Class ProductLogJob + * @package app\jobs + */ +class ProductLogJob extends BaseJobs +{ + use QueueTrait; + + /** + * @return mixed + */ + public static function queueName() + { + return 'CRMEB_PRO_LOG'; + } + + /** + * @param $type 'visit','cart','order','pay','collect','refund' + * @param $data + * @return bool + */ + public function doJob($type, $data, $productType = 'product') + { + try { + /** @var StoreProductLogServices $productLogServices */ + $productLogServices = app()->make(StoreProductLogServices::class); + $productLogServices->createLog($type, $data); + if ($type == 'visit') { + /** @var StoreVisitServices $storeVisit */ + $storeVisit = app()->make(StoreVisitServices::class); + $storeVisit->setView($data['uid'] ?? 0, $data['id'] ?? 0, $productType, $data['product_id'] ?? [], 'view'); + } + } catch (\Throwable $e) { + Log::error('写入商品记录发生错误,错误原因:' . $e->getMessage()); + } + return true; + } + +} diff --git a/app/jobs/product/ProductStockTips.php b/app/jobs/product/ProductStockTips.php new file mode 100644 index 0000000..b459235 --- /dev/null +++ b/app/jobs/product/ProductStockTips.php @@ -0,0 +1,60 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\product; + + +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrValueServices; +// use app\webscoket\SocketPush; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; + +/** + * 商品库存警戒提示 + * Class ProductStockTips + * @package app\jobs\product + */ +class ProductStockTips extends BaseJobs +{ + use QueueTrait; + + + public function doJob($productId, $send = 1) + { + /** @var StoreProductServices $make */ + $make = app()->make(StoreProductServices::class); + $product = $make->get(['id' => $productId], ['stock', 'id', 'is_police', 'is_sold']); + $store_stock = sys_config('store_stock') ?? 0;//库存预警界限 + /** @var StoreProductAttrValueServices $storeValueService */ + $storeValueService = app()->make(StoreProductAttrValueServices::class); + $count = $storeValueService->getPolice([ + ['type', '=', 0], + ['stock', '<=', $store_stock], + ['product_id', '=', $productId] + ]); + $product->is_sold = $storeValueService->value(['type' => 0, 'product_id' => $productId], 'stock') === 0 ? 1 : 0; + if ($store_stock >= $product['stock'] || $count) { + $product->is_police = 1; + if ($send) { + try { + // SocketPush::admin()->type('STORE_STOCK')->data(['id' => $productId])->push(); + } catch (\Exception $e) { + } + } + } else { + $product->is_police = 0; + } + $product->save(); + return true; + } + +} diff --git a/app/jobs/product/ProductStockValueTips.php b/app/jobs/product/ProductStockValueTips.php new file mode 100644 index 0000000..e01372e --- /dev/null +++ b/app/jobs/product/ProductStockValueTips.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\product; + + +use app\services\product\sku\StoreProductAttrValueServices; +// use app\webscoket\SocketPush; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; + +/** + * 库存提醒 + * Class ProductStockValueTips + * @package app\jobs\product + */ +class ProductStockValueTips extends BaseJobs +{ + use QueueTrait; + + public function doJob($productId, $unique, $type) + { + /** @var StoreProductAttrValueServices $make */ + $make = app()->make(StoreProductAttrValueServices::class); + $stock = $make->value([ + 'product_id' => $productId, + 'unique' => $unique, + 'type' => $type + ], 'stock'); + $store_stock = sys_config('store_stock') ?? 0;//库存预警界限 + if ($store_stock >= $stock) { + try { + // SocketPush::admin()->data(['id' => $productId])->push(); + } catch (\Exception $e) { + } + } + return true; + } +} diff --git a/app/jobs/product/ProductSyncStoreJob.php b/app/jobs/product/ProductSyncStoreJob.php new file mode 100644 index 0000000..ec67055 --- /dev/null +++ b/app/jobs/product/ProductSyncStoreJob.php @@ -0,0 +1,110 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\product; + + + +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\product\StoreProductServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 商品同步到门店 + * Class ProductSyncStoreJob + * @package app\jobs\product + */ +class ProductSyncStoreJob extends BaseJobs +{ + use QueueTrait; + + /** + * 同步某个商品到某个门店 + * @param $product_id + * @param $store_id + * @return bool + */ + public function syncProduct($product_id, $store_id) + { + $product_id = (int)$product_id; + $store_id = (int)$store_id; + if (!$product_id || !$store_id) { + return true; + } + try { + /** @var StoreBranchProductServices $storeBranchProductServices */ + $storeBranchProductServices = app()->make(StoreBranchProductServices::class); + $storeBranchProductServices->syncProduct($product_id, $store_id); + } catch (\Throwable $e) { + Log::error('同步商品到门店发生错误,错误原因:' . $e->getMessage()); + } + return true; + } + + /** + * 新增门店:同步平台商品(选择多个,或者所有) + * @param $store_id + * @param $product_id + * @return bool + */ + public function syncProducts($store_id, $product_id = []) + { + if (!$store_id) { + return true; + } + try { + $where = ['is_show' => 1, 'is_del' => 0, 'type' => 0, 'product_type' => [0, 4], 'is_verify' => 1, 'pid' => 0]; + if ($product_id) {//同步某些商品 + $where['id'] = is_array($product_id) ? $product_id : explode(',', $product_id); + } + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $products = $productServices->getSearchList($where, 0, 0, ['id'], '', []); + if ($products) { + $productIds = array_column($products, 'id'); + foreach ($productIds as $id) { + ProductSyncStoreJob::dispatchDo('syncProduct', [$id, $store_id]); + } + } + } catch (\Throwable $e) { + Log::error('同步商品到门店发生错误,错误原因:' . $e->getMessage()); + } + return true; + } + + /** + * 同步一个商品到多个门店 + * @param $product_id + * @param $store_ids + * @return bool + */ + public function syncProductToStores($product_id, $applicable_type = 0, $store_ids = []) + { + $product_id = (int)$product_id; + if (!$product_id) { + return true; + } + try { + if ($store_ids) {//同步门店 + $store_ids = is_array($store_ids) ? $store_ids : explode(',', $product_id); + } + /** @var StoreBranchProductServices $storeBranchProductServices */ + $storeBranchProductServices = app()->make(StoreBranchProductServices::class); + $storeBranchProductServices->syncProductToStores($product_id, $applicable_type, $store_ids); + } catch (\Throwable $e) { + Log::error('同步商品到门店发生错误,错误原因:' . $e->getMessage()); + } + return true; + } + +} diff --git a/app/jobs/store/StoreFinanceJob.php b/app/jobs/store/StoreFinanceJob.php new file mode 100644 index 0000000..ccbc1b3 --- /dev/null +++ b/app/jobs/store/StoreFinanceJob.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\store; + + +use app\services\store\finance\StoreFinanceFlowServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 门店资金流水记录 + * Class StoreFinanceJob + * @package app\jobs + */ +class StoreFinanceJob extends BaseJobs +{ + use QueueTrait; + + /** + * 门店流水 + * @param array $order + * @param int $type + * @param int $price + * @return bool + */ + public function doJob(array $order, int $type, $price = 0) + { + try { + /** @var StoreFinanceFlowServices $storeFinanceFlowServices */ + $storeFinanceFlowServices = app()->make(StoreFinanceFlowServices::class); + $storeFinanceFlowServices->setFinance($order, $type, $price); + } catch (\Throwable $e) { + Log::error('记录流水失败:' . $e->getMessage()); + } + return true; + } +} diff --git a/app/jobs/system/AdminLogJob.php b/app/jobs/system/AdminLogJob.php new file mode 100644 index 0000000..ac53ae3 --- /dev/null +++ b/app/jobs/system/AdminLogJob.php @@ -0,0 +1,54 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\system; + +use app\services\system\log\SystemLogServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; + +/** + * 后台日志 + * Class AdminLogJob + * @package app\jobs\system + */ +class AdminLogJob extends BaseJobs +{ + use QueueTrait; + + /** + * @return mixed + */ + public static function queueName() + { + return 'CRMEB_PRO_LOG'; + } + + /** + * @param $adminId + * @param $adminName + * @param $module + * @param $rule + * @param $ip + * @param $type + */ + public function doJob($adminId, $adminName, $module, $rule, $ip, $type) + { + try { + /** @var SystemLogServices $services */ + $services = app()->make(SystemLogServices::class); + $services->recordAdminLog((int)$adminId, $adminName, $module, $rule, $ip, $type); + } catch (\Exception $e) { + + } + return true; + } +} diff --git a/app/jobs/system/CapitalFlowJob.php b/app/jobs/system/CapitalFlowJob.php new file mode 100644 index 0000000..2a54004 --- /dev/null +++ b/app/jobs/system/CapitalFlowJob.php @@ -0,0 +1,121 @@ +make(UserServices::class); + $userInfo = $userServices->get($order['uid'], ['uid', 'nickname']); + $data = [ + 'order_id' => $order['order_id'], + 'store_id' => $order['store_id'] ?? 0, + 'uid' => $order['uid'] ?? 0, + 'nickname' => $userInfo['nickname'] ?? '游客' . time(), + 'phone' => $order['phone'] ?? '', + 'price' => $order['pay_price'], + 'pay_type' => $order['pay_type'], + ]; + } + break; + case 'refund'://退款 + //退款写入资金流水 + if (in_array($order['pay_type'], ['weixin', 'alipay', 'offline'])) { + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->get($order['uid'], ['uid', 'nickname', 'phone']); + //记录资金流水队列 + $data = [ + 'order_id' => $order['order_id'], + 'store_id' => $order['store_id'], + 'uid' => $order['uid'], + 'nickname' => $userInfo['nickname'], + 'phone' => $userInfo['phone'], + 'price' => $order['refund_price'], + 'pay_type' => $order['pay_type'], + ]; + } + break; + case 'pay_member'://购买付费会员 + if ($order['pay_type'] != 'yue') { + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->get($order['uid'], ['uid', 'nickname', 'phone']); + $data = [ + 'order_id' => $order['order_id'], + 'store_id' => 0, + 'uid' => $order['uid'], + 'nickname' => $userInfo['nickname'], + 'phone' => $userInfo['phone'], + 'price' => $order['pay_price'], + 'pay_type' => $order['pay_type'], + ]; + } + break; + case 'recharge'://充值 + $data = [ + 'order_id' => $order['order_id'], + 'store_id' => $order['store_id'] ?? 0, + 'uid' => $order['uid'], + 'nickname' => $order['nickname'], + 'phone' => $order['phone'], + 'price' => $order['price'], + 'pay_type' => $order['recharge_type'] ?? 'weixin', + 'add_time' => time(), + ]; + break; + case 'refund_recharge'://充值退款 + $data = [ + 'order_id' => $order['order_id'], + 'store_id' => $order['store_id'] ?? 0, + 'uid' => $order['uid'], + 'nickname' => $order['nickname'], + 'phone' => $order['phone'], + 'price' => $order['price'], + 'pay_type' => $order['recharge_type'] ?? 'weixin', + ]; + break; + case 'extract'://提现 + $data = $order; + break; + case 'luck'://抽奖 + $data = $order; + break; + } + if ($data) { + /** @var CapitalFlowServices $capitalFlowServices */ + $capitalFlowServices = app()->make(CapitalFlowServices::class); + $capitalFlowServices->setFlow($data, $type); + } + } catch (\Throwable $e) { + Log::error('写入资金流水错误:[' . class_basename($this) . ']' . $e->getMessage()); + } + return true; + + } +} diff --git a/app/jobs/system/ExportExcelJob.php b/app/jobs/system/ExportExcelJob.php new file mode 100644 index 0000000..d75d467 --- /dev/null +++ b/app/jobs/system/ExportExcelJob.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\system; + +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use crmeb\services\SpreadsheetExcelService; +use think\facade\Log; + +/** + * 导出数据队列 + * Class ExportExcelJob + * @package app\jobs + */ +class ExportExcelJob extends BaseJobs +{ + use QueueTrait; + + /** + * 分批导出excel + * @param $order + * @return bool + */ + public function doJob(array $export = [], string $filename = '', array $header = [], array $title_arr = [], string $suffix = 'xlsx', bool $is_save = false) + { + if (!$export) { + return true; + } + try { + if ($header && $title_arr) { + $title = isset($title_arr[0]) && !empty($title_arr[0]) ? $title_arr[0] : '导出数据'; + $name = isset($title_arr[1]) && !empty($title_arr[1]) ? $title_arr[1] : '导出数据'; + $info = isset($title_arr[2]) && !empty($title_arr[2]) ? $title_arr[2] : date('Y-m-d H:i:s', time()); + SpreadsheetExcelService::instance() + ->setExcelHeader($header) + ->setExcelTile($title, $name, $info) + ->setExcelContent($export) + ->excelSave($filename, $suffix, $is_save); + } else { + SpreadsheetExcelService::instance() + ->setExcelContent($export) + ->excelSave($filename, $suffix, $is_save); + } + } catch (\Throwable $e) { + Log::error('导出excel' . $title . '失败,原因:' . $e->getMessage()); + } + return true; + } +} diff --git a/app/jobs/user/MicroPayOrderJob.php b/app/jobs/user/MicroPayOrderJob.php new file mode 100644 index 0000000..91b04ae --- /dev/null +++ b/app/jobs/user/MicroPayOrderJob.php @@ -0,0 +1,126 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\user; + + +use app\services\order\OtherOrderServices; +use app\services\order\StoreCartServices; +use app\services\order\StoreOrderCreateServices; +use app\services\order\StoreOrderSuccessServices; +use app\services\pay\PayServices; +use app\services\user\UserRechargeServices; +use crmeb\basic\BaseJobs; +use crmeb\services\wechat\Payment; +use crmeb\traits\QueueTrait; +use think\facade\Log; +// use app\webscoket\SocketPush; + +/** + * 付款码支付 + * Class MicroPayOrderJob + * @package app\jobs\user + */ +class MicroPayOrderJob extends BaseJobs +{ + + use QueueTrait; + + + public function doJob(string $outTradeNo, int $type = 1, int $num = 1) + { + if ($type == 2) { + /** @var OtherOrderServices $OtherOrderServices */ + $make = app()->make(OtherOrderServices::class); + } else if ($type == 1) { + /** @var UserRechargeServices $make */ + $make = app()->make(UserRechargeServices::class); + } else { + /** @var StoreOrderSuccessServices $make */ + $make = app()->make(StoreOrderSuccessServices::class); + } + $orderInfo = $make->get(['order_id' => $outTradeNo]); + if (!$orderInfo) { + return true; + } + if ($orderInfo->paid) { + return true; + } + if (!$type && $orderInfo->is_del) { + return true; + } + try { + //查询订单支付状态 + $respones = Payment::queryOrder($outTradeNo); + if ($respones['paid'] && ($respones['payInfo']['trade_state'] ?? '') == 'SUCCESS') { + if ($type == 2) { + $orderInfo = $orderInfo->toArray(); + $make->paySuccess($orderInfo, $orderInfo['pay_type']); + } else if ($type == 1) { + $make->rechargeSuccess($outTradeNo); + } else { + //删除购物车 + /** @var StoreCartServices $cartServices */ + $cartServices = app()->make(StoreCartServices::class); + $cartServices->deleteCartStatus($orderInfo['cart_id'] ?? []); + //修改支付状态 + $make->paySuccess($orderInfo->toArray(), PayServices::WEIXIN_PAY, [ + 'trade_no' => $respones['payInfo']['transaction_id'] ?? '' + ]); + + if ($orderInfo->staff_id) { + //发送消息 + try { + // SocketPush::instance()->to($orderInfo->staff_id)->setUserType('cashier')->type('changSuccess')->push(); + } catch (\Throwable $e) { + } + } + } + } else { + //15秒后还是状态异常直接取消订单 + if ($num >= 3) { + try { + $respones = Payment::reverseOrder($outTradeNo); + if ($type) { + /** @var StoreOrderCreateServices $service */ + $service = app()->make(StoreOrderCreateServices::class); + $make->update( + [ + 'order_id' => $outTradeNo + ], + [ + $type ? 'remarks' : 'remark' => '支付状态异常自动撤销订单,并从新生成订单号', + 'order_id' => $type ? $make->getOrderId() : ($type == 2 ? $service->getNewOrderId('hy') : $service->getNewOrderId()), + ] + ); + } + + } catch (\Throwable $e) { + Log::error([ + 'message' => '撤销订单失败,订单号:' . $outTradeNo . ';错误原因:' . $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + return true; + } + $secs = 5; + if (isset($respones['payInfo']['err_code']) && $respones['payInfo']['err_code'] === 'USERPAYING') { + $secs = 10; + } + self::dispatchSece($secs, [$outTradeNo, $type, $num + 1]); + } + } catch (\Throwable $e) { + + } + return true; + } +} diff --git a/app/jobs/user/UserFriendsJob.php b/app/jobs/user/UserFriendsJob.php new file mode 100644 index 0000000..e4e466a --- /dev/null +++ b/app/jobs/user/UserFriendsJob.php @@ -0,0 +1,49 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\user; + + +use app\services\user\UserFriendsServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 用户好友关系 + * Class UserFriendsJob + * @package app\jobs\user + */ +class UserFriendsJob extends BaseJobs +{ + use QueueTrait; + + /** + * 记录用户好友关系 + * @param int $uid + * @param int $spreadUid + * @return bool + */ + public function doJob(int $uid, int $spreadUid) + { + if (!$uid || !$spreadUid || $uid == $spreadUid) { + return true; + } + try { + /** @var UserFriendsServices $serviceFriend */ + $serviceFriend = app()->make(UserFriendsServices::class); + $serviceFriend->saveFriend($uid, $spreadUid); + } catch (\Throwable $e) { + Log::error('记录好友关系失败,失败原因:' . $e->getMessage()); + } + return true; + } +} diff --git a/app/jobs/user/UserIntegralJob.php b/app/jobs/user/UserIntegralJob.php new file mode 100644 index 0000000..b106331 --- /dev/null +++ b/app/jobs/user/UserIntegralJob.php @@ -0,0 +1,82 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\user; + + +use app\services\user\UserIntegralServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 清除到期积分 + * Class UserIntegralJob + * @package app\jobs + */ +class UserIntegralJob extends BaseJobs +{ + use QueueTrait; + + /** + * 执行清除到期积分 + * @param $openids + * @return bool + */ + public function doJob($uids) + { + if (!$uids || !is_array($uids)) { + return true; + } + try { + /** @var UserIntegralServices $userIntegralServices */ + $userIntegralServices = app()->make(UserIntegralServices::class); + $userIntegralServices->doClearExpireIntegral($uids); + } catch (\Throwable $e) { + Log::error('清除用户到期积分失败,失败原因:' . $e->getMessage()); + } + return true; + } + + /** + * 赠送新人礼积分 + * @param $uid + * @return bool + */ + public function newcomerGiveIntegral($uid) + { + try { + /** @var UserIntegralServices $userIntegralServices */ + $userIntegralServices = app()->make(UserIntegralServices::class); + $userIntegralServices->newcomerGiveIntegral((int)$uid); + } catch (\Throwable $e) { + Log::error('赠送新人礼积分失败,失败原因:' . $e->getMessage()); + } + return true; + } + + /** + * 会员卡激活赠送积分 + * @param $uid + * @return bool + */ + public function levelGiveIntegral($uid) + { + try { + /** @var UserIntegralServices $userIntegralServices */ + $userIntegralServices = app()->make(UserIntegralServices::class); + $userIntegralServices->levelGiveIntegral((int)$uid); + } catch (\Throwable $e) { + Log::error('会员卡激活赠送积分失败,失败原因:' . $e->getMessage()); + } + return true; + } +} diff --git a/app/jobs/user/UserJob.php b/app/jobs/user/UserJob.php new file mode 100644 index 0000000..f9b48bd --- /dev/null +++ b/app/jobs/user/UserJob.php @@ -0,0 +1,235 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\user; + + +use app\services\activity\lottery\LuckLotteryServices; +use app\services\activity\newcomer\StoreNewcomerServices; +use app\services\message\wechat\MessageServices; +use app\services\user\UserServices; +use app\services\wechat\WechatUserServices; +use app\services\work\WorkClientServices; +use app\services\work\WorkMemberServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 同步用户 + * Class UserJob + * @package app\jobs + */ +class UserJob extends BaseJobs +{ + use QueueTrait; + + /** + * 执行同步数据后 + * @param $openids + * @return bool + */ + public function doJob($openids) + { + if (!$openids || !is_array($openids)) { + return true; + } + $noBeOpenids = []; + try { + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + $noBeOpenids = $wechatUser->syncWechatUser($openids); + } catch (\Throwable $e) { + Log::error('更新wechatUser用户信息失败,失败原因:' . $e->getMessage()); + } + if (!$noBeOpenids) { + return true; + } + try { + /** @var UserServices $user */ + $user = app()->make(UserServices::class); + $user->importUser($noBeOpenids); + } catch (\Throwable $e) { + Log::error('新增用户失败,失败原因:' . $e->getMessage()); + } + return true; + } + + /** + * 关注推官新用户(发送抽奖消息 、推广用户增加抽奖次数) + * @param int $uid + * @param string $openid + * @param int $spread_uid + * @return bool + */ + public function subscribeSpreadLottery(int $uid, string $openid, int $spread_uid) + { + /** @var LuckLotteryServices $lotteryServices */ + $lotteryServices = app()->make(LuckLotteryServices::class); + $lottery = $lotteryServices->getFactorLottery(5); + if (!$lottery) { + return true; + } + try { + /** @var MessageServices $messageServices */ + $messageServices = app()->make(MessageServices::class); + $messageServices->wechatEvent(['third_type' => 'luckLottery-' . $uid, 'lottery' => $lottery], $openid); + } catch (\Throwable $e) { + Log::error('发送关注抽奖消息失败,原因:' . $e->getMessage()); + } + if (!$spread_uid) { + return true; + } + try { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $spreadUser = $userServices->getUserInfo($spread_uid, 'uid,spread_lottery'); + //增加推广用户抽奖次数 + if (!$spreadUser) { + return true; + } + if ($lottery['lottery_num_term'] == 1) { + $where = ['time' => 'today', 'timeKey' => 'spread_time']; + } else { + $where = ['spread_uid' => $spreadUser['uid']]; + } + $spreadCount = $userServices->count($where); + if ($spreadCount) { + $lotterySum = bcmul((string)$spreadCount, (string)$lottery['spread_num'], 0); + if ($lotterySum < $lottery['lottery_num']) { + $update_lottery_num = $spreadUser['spread_lottery'] + (int)$lottery['spread_num']; + if ($update_lottery_num > $lottery['lottery_num']) { + $update_lottery_num = $lottery['lottery_num']; + } + $userServices->update($spreadUser['uid'], ['spread_lottery' => $update_lottery_num], 'uid'); + } + } + } catch (\Throwable $e) { + Log::error('增加推广用户抽奖次数失败,原因:' . $e->getMessage()); + } + + return true; + + } + + /** + * 绑定企业微信成员 + * @param $uid + * @param $unionid + * @return bool + */ + public function bindWorkMember($uid, $unionid) + { + if (!$unionid) { + return true; + } + /** @var WorkClientServices $service */ + $service = app()->make(WorkClientServices::class); + $clienInfo = $service->get(['unionid' => $unionid], ['id', 'external_userid', 'uid'], ['followOne']); + if ($clienInfo) { + if (!$clienInfo->uid) { + $clienInfo->uid = $uid; + $clienInfo->save(); + } + if (!empty($clienInfo->followOne->userid)) { + $userId = $clienInfo->followOne->userid; + /** @var WorkMemberServices $memberService */ + $memberService = app()->make(WorkMemberServices::class); + $menber = $memberService->get(['userid' => $userId], ['uid', 'mobile']); + if (!$menber) { + Log::error([ + 'message' => '绑定企业微信成员失败:没有查询到成员身份', + 'request' => ['uid' => $uid, 'unionid' => $unionid], + ]); + return true; + } + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + if (!$menber->uid && $menber->mobile) { + $menberUid = $userService->value(['phone' => $menber->mobile], 'uid'); + if ($menberUid && $menberUid != $uid) { + $menber->uid = $menberUid; + $menber->save(); + } + } + $userInfo = $userService->get($uid, ['uid', 'work_uid', 'work_userid']); + if (!$userInfo) { + return true; + } + if (!$userInfo->work_userid) { + $userInfo->work_userid = $userId; + $userInfo->work_uid = $menber->uid; + $userInfo->save(); + } + } + } + return true; + } + + /** + * 设置用户会员卡激活状态 + * @param $uid + * @return bool + */ + public function setUserLevel($uid) + { + if (!(int)$uid) { + return true; + } + try { + /** @var StoreNewcomerServices $newcomerServices */ + $newcomerServices = app()->make(StoreNewcomerServices::class); + $newcomerServices->setUserLevel((int)$uid); + } catch (\Throwable $e) { + Log::error('设置用户会员卡激活状态失败,失败原因:' . $e->getMessage()); + } + return true; + } + + /** + * 设置用户首单优惠、新人专享 + * @param $uid + * @return bool + */ + public function setUserNewcomer($uid) + { + if (!(int)$uid) { + return true; + } + try { + /** @var StoreNewcomerServices $newcomerServices */ + $newcomerServices = app()->make(StoreNewcomerServices::class); + $newcomerServices->setUserNewcomer((int)$uid); + } catch (\Throwable $e) { + Log::error('设置用户首单优惠、新人专享失败,失败原因:' . $e->getMessage()); + } + return true; + } + + /** + * 下单修改 + * @param $uid + * @return bool|void + */ + public function updateUserNewcomer($uid, $order) + { + if (!(int)$uid) { + return true; + } + try { + /** @var StoreNewcomerServices $newcomerServices */ + $newcomerServices = app()->make(StoreNewcomerServices::class); + $newcomerServices->updateUserNewcomer((int)$uid, $order); + } catch (\Throwable $e) { + Log::error('修改用户首单优惠、新人专享使用状态失败,失败原因:' . $e->getMessage()); + } + } +} diff --git a/app/jobs/user/UserLabelJob.php b/app/jobs/user/UserLabelJob.php new file mode 100644 index 0000000..51b03af --- /dev/null +++ b/app/jobs/user/UserLabelJob.php @@ -0,0 +1,66 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\user; + + +use app\services\user\label\UserLabelServices; +use app\services\work\WorkGroupChatAuthServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; + +/** + * 用户标签 + * Class UserLabelJob + * @package app\jobs\user + */ +class UserLabelJob extends BaseJobs +{ + use QueueTrait; + + /** + * 同步后台标签到企业微信客户后台 + * @param $cateId + * @param $groupName + * @return bool + */ + public function authLabel($cateId, $groupName) + { + /** @var UserLabelServices $make */ + $make = app()->make(UserLabelServices::class); + return $make->addCorpClientLabel($cateId, $groupName); + } + + /** + * 同步企业微信客户标签到平台 + * @return bool + */ + public function authWorkLabel() + { + /** @var UserLabelServices $make */ + $make = app()->make(UserLabelServices::class); + return $make->authWorkLabel(); + } + + /** + * 编辑客户标签 + * @param $userid + * @param $groupAuthId + * @return mixed + */ + public function clientAddLabel($userid, $externalUserID, $groupAuthId) + { + /** @var WorkGroupChatAuthServices $chatAuthService */ + $chatAuthService = app()->make(WorkGroupChatAuthServices::class); + return $chatAuthService->clientAddLabel((int)$groupAuthId, $userid, $externalUserID); + } + +} diff --git a/app/jobs/user/UserLevelJob.php b/app/jobs/user/UserLevelJob.php new file mode 100644 index 0000000..0bebee3 --- /dev/null +++ b/app/jobs/user/UserLevelJob.php @@ -0,0 +1,41 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\user; + + +use app\services\user\level\UserLevelServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 检测会员等级 + * Class UserLevelJob + * @package app\jobs\user + */ +class UserLevelJob extends BaseJobs +{ + + use QueueTrait; + + public function doJob($uid) + { + try { + /** @var UserLevelServices $levelServices */ + $levelServices = app()->make(UserLevelServices::class); + $levelServices->detection((int)$uid); + } catch (\Throwable $e) { + Log::error('会员等级升级失败,失败原因:' . $e->getMessage()); + } + return true; + } +} diff --git a/app/jobs/user/UserSpreadJob.php b/app/jobs/user/UserSpreadJob.php new file mode 100644 index 0000000..f665a54 --- /dev/null +++ b/app/jobs/user/UserSpreadJob.php @@ -0,0 +1,55 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\user; + + +use app\services\user\UserServices; +use app\services\user\UserSpreadServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 用户推广关系 + * Class UserSpreadJob + * @package app\jobs\user + */ +class UserSpreadJob extends BaseJobs +{ + use QueueTrait; + + /** + * 记录用户推广关系 + * @param int $uid + * @param int $spreadUid + * @return bool + */ + public function doJob(int $uid, int $spreadUid, int $spread_time = 0, int $admin_id = 0) + { + if (!$uid || !$spreadUid || $uid == $spreadUid) { + return true; + } + try { + /** @var UserSpreadServices $userSpreadServices */ + $userSpreadServices = app()->make(UserSpreadServices::class); + //记录 + $userSpreadServices->setSpread($uid, $spreadUid, $spread_time, $admin_id); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + //增加推广人数 + $userServices->incField($spreadUid, 'spread_count', 1); + } catch (\Throwable $e) { + Log::error('记录用户推广失败,失败原因:' . $e->getMessage()); + } + return true; + } +} diff --git a/app/jobs/user/UserSvipJob.php b/app/jobs/user/UserSvipJob.php new file mode 100644 index 0000000..5c8e5b8 --- /dev/null +++ b/app/jobs/user/UserSvipJob.php @@ -0,0 +1,50 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\user; + + +use app\services\user\UserServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; +use think\facade\Log; + +/** + * 清除到期svip + * Class UserSvipJob + * @package app\jobs\user + */ +class UserSvipJob extends BaseJobs +{ + use QueueTrait; + + /** + * 执行清除到期积分 + * @param $openids + * @return bool + */ + public function doJob($uids) + { + if (!$uids || !is_array($uids)) { + return true; + } + try { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + foreach ($uids as $uid) { + $userServices->offMemberLevel($uid); + } + } catch (\Throwable $e) { + Log::error('清除到期用户svip失败,失败原因:' . $e->getMessage()); + } + return true; + } +} diff --git a/app/jobs/work/WorkClientJob.php b/app/jobs/work/WorkClientJob.php new file mode 100644 index 0000000..a10c05c --- /dev/null +++ b/app/jobs/work/WorkClientJob.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\work; + + +use app\services\work\WorkClientServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; + +/** + * 同步客户 + * Class WorkClientJob + * @package app\jobs\work + */ +class WorkClientJob extends BaseJobs +{ + + use QueueTrait; + + /** + * @param $page + * @param $cursor + * @return bool + */ + public function authClient($page, $cursor) + { + /** @var WorkClientServices $make */ + $make = app()->make(WorkClientServices::class); + $make->authGetExternalcontact($page, $cursor); + return true; + } + + /** + * 同步客户信息 + * @param $corpId + * @param $externalUserID + * @param $userId + */ + public function saveClientInfo($corpId, $externalUserID, $userId) + { + /** @var WorkClientServices $make */ + $make = app()->make(WorkClientServices::class); + $make->saveOrUpdateClient($corpId, $externalUserID, $userId); + return true; + } + + /** + * 设置客户标签 + * @param $markTag + * @return bool + */ + public function setLabel($markTag) + { + /** @var WorkClientServices $make */ + $make = app()->make(WorkClientServices::class); + $make->setClientMarkTag($markTag); + return true; + } +} diff --git a/app/jobs/work/WorkMemberJob.php b/app/jobs/work/WorkMemberJob.php new file mode 100644 index 0000000..8329bad --- /dev/null +++ b/app/jobs/work/WorkMemberJob.php @@ -0,0 +1,53 @@ + +// +---------------------------------------------------------------------- + +namespace app\jobs\work; + + +use app\services\work\WorkMemberServices; +use crmeb\basic\BaseJobs; +use crmeb\traits\QueueTrait; + +/** + * 企业微信成员 + * Class WorkMemberJob + * @package app\jobs\work + */ +class WorkMemberJob extends BaseJobs +{ + + use QueueTrait; + + /** + * 执行部门同步 + * @param $id + * @return bool + */ + public function run($id) + { + /** @var WorkMemberServices $make */ + $make = app()->make(WorkMemberServices::class); + $make->authUpdataMember((int)$id); + return true; + } + + /** + * 保存数据 + * @param $member + * @return bool + */ + public function save($member) + { + /** @var WorkMemberServices $make */ + $make = app()->make(WorkMemberServices::class); + return $make->saveMember($member); + } +} diff --git a/app/model/activity/bargain/StoreBargain.php b/app/model/activity/bargain/StoreBargain.php new file mode 100644 index 0000000..5eb3dda --- /dev/null +++ b/app/model/activity/bargain/StoreBargain.php @@ -0,0 +1,296 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\bargain; + +use app\model\product\product\StoreDescription; +use app\common\model\store\product\Product as StoreProduct; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 砍价商品Model + * Class StoreBargain + * @package app\model\activity\bargain + */ +class StoreBargain extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_bargain'; + + + public function getImagesAttr($value) + { + return json_decode($value, true) ?? []; + } + + /** + * 配送信息 + * @param $value + * @return false|string + */ + protected function setDeliveryTypeAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 配送信息 + * @param $value + * @param $data + * @return mixed + */ + protected function getDeliveryTypeAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 配送信息 + * @param $value + * @param $data + * @return mixed + */ + protected function getCustomFormAttr($value) + { + if ($value) { + return is_string($value) ? json_decode($value, true) : $value; + } + return []; + } + + /** + * 商品标签 + * @param $value + * @return array|mixed + */ + protected function getLabelIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * @param $value + * @return array|mixed + */ + public function getStoreLabelIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + /** + * 商品保障服务 + * @param $value + * @return array|mixed + */ + protected function getEnsureIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + /** + * 参数信息 + * @param $value + * @return array|mixed + */ + protected function getSpecsAttr($value) + { + if ($value) { + return is_string($value) ? json_decode($value, true) : $value; + } + return []; + } + + /** + * 适用门店信息 + * @param $value + * @return string + */ + protected function setApplicableStoreIdAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 适用门店信息 + * @param $value + * @return array|false|string[] + */ + protected function getApplicableStoreIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + /** + * 一对一关联 + * 商品关联商品商品详情 + * @return \think\model\relation\HasOne + */ + public function descriptions() + { + return $this->hasOne(StoreDescription::class, 'product_id', 'id')->where('type', 2)->bind(['description']); + } + + /** + * 原价 + * @return \think\model\relation\HasOne + */ + public function product() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id')->field(['id', 'ot_price'])->bind(['ot_price']); + } + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getAddTimeAttr($value) + { + if ($value) return date('Y-m-d H:i:s', (int)$value); + return ''; + } + + /** + * 砍价商品名称搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStoreNameAttr($query, $value, $data) + { + if ($value != '') $query->where('title|id', 'like', '%' . $value . '%'); + } + + /** + * 状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value, $data) + { + if ($value != '') $query->where('status', $value ?? 1); + } + + /** + * 是否推荐搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsHotAttr($query, $value, $data) + { + $query->where('is_hot', $value ?? 0); + } + + /** + * 是否删除搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsDelAttr($query, $value, $data) + { + $query->where('is_del', $value ?? 0); + } + + /** + * 商品ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProductIdAttr($query, $value, $data) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + } + + /** + * 活动有效时间搜索器 + * @param $query + * @param $value + */ + public function searchBargainTimeAttr($query, $value) + { + if ($value == 1) { + $time = time(); + $query->where('start_time', '<=', $time)->where('stop_time', '>=', $time); + } + } + + /** + * 系统表单搜索器 + * @param Model $query + * @param $value + */ + public function searchSystemFormIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('system_form_id', $value); + } else { + if ($value !== '') $query->where('system_form_id', $value); + } + } + + /** + * 适用门店类型搜索器 + * @param Model $query + * @param $value + */ + public function searchApplicableTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('applicable_type', $value); + } else { + if ($value !== '') $query->where('applicable_type', $value); + } + } +} diff --git a/app/model/activity/collage/UserCollageCode.php b/app/model/activity/collage/UserCollageCode.php new file mode 100644 index 0000000..e85e013 --- /dev/null +++ b/app/model/activity/collage/UserCollageCode.php @@ -0,0 +1,67 @@ + +// +---------------------------------------------------------------------- +namespace app\model\activity\collage; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use app\common\model\store\order\StoreOrder; +use app\model\activity\table\TableQrcode; + +/** + * 拼单Model + * Class UserCollage + * @package app\model\collage + */ +class UserCollageCode extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'user_collage_code'; + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getAddTimeAttr($value) + { + if ($value) return date('Y-m-d H:i:s', (int)$value); + return ''; + } + + /**一对一关联 + * 关联订单信息 + * @return \think\model\relation\HasOne + */ + public function orderId() + { + return $this->hasOne(StoreOrder::class, 'id', 'oid')->field(['id','order_id','pay_type','total_price','pay_price','paid','refund_status','staff_id', 'is_del', 'is_system_del']); + } + + /**一对一关联 + * 关联桌码信息 + * @return \think\model\relation\HasOne + */ + public function qrcode() + { + return $this->hasOne(TableQrcode::class, 'id', 'qrcode_id')->field(['id','cate_id','table_number','is_using','is_del']); + } +} diff --git a/app/model/activity/collage/UserCollageCodePartake.php b/app/model/activity/collage/UserCollageCodePartake.php new file mode 100644 index 0000000..630e683 --- /dev/null +++ b/app/model/activity/collage/UserCollageCodePartake.php @@ -0,0 +1,69 @@ + +// +---------------------------------------------------------------------- +namespace app\model\activity\collage; + +use app\common\model\store\product\Product as StoreProduct; +use app\common\model\user\User; +use app\common\model\store\product\ProductAttrValue as StoreProductAttrValue; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * 用户参与拼单Model + * Class UserCollagePartake + * @package app\model\collage + */ +class UserCollageCodePartake extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'user_collage_code_partake'; + + /** + * 一对一关联 + * 购物车关联商品商品详情 + * @return \think\model\relation\HasOne + */ + public function productInfo() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id'); + } + + /** + * 一对一关联 + * 购物车关联商品商品规格 + * @return \think\model\relation\HasOne + */ + public function attrInfo() + { + return $this->hasOne(StoreProductAttrValue::class, 'unique', 'product_attr_unique'); + } + + /** + * 一对一关联 + * 关联用户信息 + * @return \think\model\relation\HasOne + */ + public function userInfo() + { + return $this->hasOne(User::class, 'uid', 'uid'); + } +} diff --git a/app/model/activity/combination/StoreCombination.php b/app/model/activity/combination/StoreCombination.php new file mode 100644 index 0000000..8f01307 --- /dev/null +++ b/app/model/activity/combination/StoreCombination.php @@ -0,0 +1,300 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\combination; + +use app\model\product\product\StoreDescription; +use app\common\model\store\product\Product as StoreProduct; +use crmeb\traits\ModelTrait; +use crmeb\basic\BaseModel; +use think\Model; + +/** + * 拼团商品Model + * Class StoreCombination + * @package app\model\activity\combination + */ +class StoreCombination extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_combination'; + + + /** + * 配送信息 + * @param $value + * @return false|string + */ + protected function setDeliveryTypeAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 配送信息 + * @param $value + * @param $data + * @return mixed + */ + protected function getDeliveryTypeAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 配送信息 + * @param $value + * @param $data + * @return mixed + */ + protected function getCustomFormAttr($value) + { + if ($value) { + return is_string($value) ? json_decode($value, true) : $value; + } + return []; + } + + /** + * @param $value + * @return array|mixed + */ + public function getStoreLabelIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + /** + * 商品标签 + * @param $value + * @return array|mixed + */ + protected function getLabelIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 商品保障服务 + * @param $value + * @return array|mixed + */ + protected function getEnsureIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 参数信息 + * @param $value + * @return array|mixed + */ + protected function getSpecsAttr($value) + { + if ($value) { + return is_string($value) ? json_decode($value, true) : $value; + } + return []; + } + + /** + * 适用门店信息 + * @param $value + * @return string + */ + protected function setApplicableStoreIdAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 适用门店信息 + * @param $value + * @return array|false|string[] + */ + protected function getApplicableStoreIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + /** + * 一对一获取原价 + * @return \think\model\relation\HasOne + */ + public function getPrice() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id')->field(['id', 'ot_price', 'price'])->bind(['ot_price', 'product_price' => 'price']); + } + + /** + * 一对一关联 + * 商品关联商品商品详情 + * @return \think\model\relation\HasOne + */ + public function total() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id')->where('is_show', 1)->where('is_del', 0)->field(['SUM(sales+ficti) as total', 'id', 'price'])->bind([ + 'total' => 'total', 'product_price' => 'price' + ]); + } + + /** + * 一对一关联 + * 商品关联商品商品详情 + * @return \think\model\relation\HasOne + */ + public function descriptions() + { + return $this->hasOne(StoreDescription::class, 'product_id', 'id')->where('type', 3)->bind(['description']); + } + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getAddTimeAttr($value) + { + if ($value) return date('Y-m-d H:i:s', (int)$value); + return ''; + } + + /** + * 轮播图获取器 + * @param $value + * @return mixed + */ + public function getImagesAttr($value) + { + return json_decode($value, true) ?? []; + } + + /** + * 拼团商品名称搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStoreNameAttr($query, $value, $data) + { + if ($value) $query->where('title|id', 'like', '%' . $value . '%'); + } + + /** + * 是否推荐搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsHostAttr($query, $value, $data) + { + $query->where('is_host', $value ?? 1); + } + + /** + * 状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsShowAttr($query, $value, $data) + { + if ($value != '') $query->where('is_show', $value ?: 0); + } + + /** + * 是否删除搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsDelAttr($query, $value, $data) + { + $query->where('is_del', $value ?? 0); + } + + /** + * 商品ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProductIdAttr($query, $value, $data) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + } + + /** + * 系统表单搜索器 + * @param Model $query + * @param $value + */ + public function searchSystemFormIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('system_form_id', $value); + } else { + if ($value !== '') $query->where('system_form_id', $value); + } + } + + /** + * 适用门店类型搜索器 + * @param Model $query + * @param $value + */ + public function searchApplicableTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('applicable_type', $value); + } else { + if ($value !== '') $query->where('applicable_type', $value); + } + } +} diff --git a/app/model/activity/combination/StorePink.php b/app/model/activity/combination/StorePink.php new file mode 100644 index 0000000..627cf4a --- /dev/null +++ b/app/model/activity/combination/StorePink.php @@ -0,0 +1,130 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\combination; + +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 拼团Model + * Class StorePink + * @package app\model\activity\combination + */ +class StorePink extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_pink'; + + use ModelTrait; + + /** + * 用户一对一关联 + * @return \think\model\relation\HasOne + */ + public function getUser() + { + return $this->hasOne(User::class, 'uid', 'uid', false)->bind(['delete_time']); + } + + public function getProduct() + { + return $this->hasOne(StoreCombination::class, 'id', 'cid')->bind(['title']); + } + + /** + * 订单号搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchOrderIdAttr($query, $value, $data) + { + $query->where('order_id', $value); + } + + /** + * 订单编号搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchOrderIdKeyAttr($query, $value, $data) + { + $query->where('order_id_key', $value); + } + + /** + * 拼团商品ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchCidAttr($query, $value, $data) + { + $query->where('cid', $value); + } + + /** + * 商品ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchPidAttr($query, $value, $data) + { + $query->where('pid', $value); + } + + /** + * 是否团长搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchKIdAttr($query, $value, $data) + { + if ($value !== '') $query->where('k_id', $value); + } + + /** + * 是否退款搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsRefundAttr($query, $value, $data) + { + if ($value !== '') $query->where('is_refund', $value ?? 0); + } + + /** + * 状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value, $data) + { + if ($value != '') $query->where('status', $value); + } +} diff --git a/app/model/activity/coupon/StoreCouponIssue.php b/app/model/activity/coupon/StoreCouponIssue.php new file mode 100644 index 0000000..c0dd149 --- /dev/null +++ b/app/model/activity/coupon/StoreCouponIssue.php @@ -0,0 +1,204 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\coupon; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 发布优惠券Model + * Class StoreCouponIssue + * @package app\model\activity\coupon + */ +class StoreCouponIssue extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_coupon_issue'; + + + /** + * 适用门店信息 + * @param $value + * @return string + */ + protected function setApplicableStoreIdAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 适用门店信息 + * @param $value + * @return array|false|string[] + */ + protected function getApplicableStoreIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + /** + * 用户是否拥有 + * @return \think\model\relation\HasOne + */ + public function used() + { + return $this->hasOne(StoreCouponUser::class, 'cid', 'id')->field('id,cid'); + } + + /** + * id + * @param Model $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) + $query->whereIn('id', $value); + else + $query->where('id', $value); + } + + /** + * 优惠券模板搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchCidAttr($query, $value, $data) + { + if ($value != '') $query->where('cid', $value); + } + + /** + * type + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) { + if (is_array($value)) + $query->whereIn('type', $value); + else + $query->where('type', $value); + } + } + + /** + * 优惠类型搜索器 + * @param Model $query + * @param $value + */ + public function searchCouponTypeAttr($query, $value) + { + if ($value != '') $query->where('coupon_type', $value); + } + + /** + * receive_type + * @param Model $query + * @param $value + */ + public function searchReceiveTypeAttr($query, $value) + { + if ($value) { + if (is_array($value)) + $query->whereIn('receive_type', $value); + else + $query->where('receive_type', $value); + } + } + + /** + * 优惠券是否不限量 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsPermanentAttr($query, $value, $data) + { + if ($value !== '') $query->where('is_permanent', $value); + } + + /** + * 优惠券是否新人券 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsGiveSubscribeAttr($query, $value, $data) + { + if ($value !== '') $query->where('is_give_subscribe', $value); + } + + /** + * 优惠券是否满赠 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsFullGiveAttr($query, $value, $data) + { + if ($value !== '') $query->where('is_full_give', $value); + } + + /** + * 优惠券状态 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value, $data) + { + if ($value != '') $query->where('status', $value); + } + + /** + * 优惠券是否删除 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsDelAttr($query, $value, $data) + { + if ($value !== '') $query->where('is_del', $value ?? 0); + } + + /** + * 优惠券名称 + * @param Model $query + * @param $value + * @param $data + */ + public function searchCouponTitleAttr($query, $value, $data) + { + if ($value) $query->whereLike('coupon_title', '%' . $value . '%'); + } +} diff --git a/app/model/activity/discounts/StoreDiscounts.php b/app/model/activity/discounts/StoreDiscounts.php new file mode 100644 index 0000000..422ff9e --- /dev/null +++ b/app/model/activity/discounts/StoreDiscounts.php @@ -0,0 +1,103 @@ + +// +---------------------------------------------------------------------- + + +namespace app\model\activity\discounts; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 优惠套餐 + * Class StoreDiscounts + * @package app\model\activity\discounts + */ +class StoreDiscounts extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_discounts'; + + use ModelTrait; + + /** + * 套餐商品关联 + * @return \think\model\relation\HasMany + */ + public function products() + { + return $this->hasMany(StoreDiscountsProducts::class, 'discount_id', 'id'); + } + + /** + * 类型搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchTypeAttr($query, $value) + { + if ($value != '') $query->where('type', $value); + } + + /** + * 名称搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchTitleAttr($query, $value) + { + if ($value != '') $query->where('title', 'like', '%' . $value . '%'); + } + + /** + * 状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value) + { + if ($value !== '') $query->where('status', $value); + } + + /** + * 是否删除搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } + + /** + * 商品id搜索器 + * @param Model $query + * @param $value + */ + public function searchProductIdsAttr($query, $value) + { + if ($value != '') $query->whereFindInSet('product_ids', $value); + } +} \ No newline at end of file diff --git a/app/model/activity/discounts/StoreDiscountsProducts.php b/app/model/activity/discounts/StoreDiscountsProducts.php new file mode 100644 index 0000000..ae84ab7 --- /dev/null +++ b/app/model/activity/discounts/StoreDiscountsProducts.php @@ -0,0 +1,69 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\discounts; + +use app\common\model\store\product\Product as StoreProduct; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 优惠套餐商品 + * Class StoreDiscountsProducts + * @package app\model\activity\discounts + */ +class StoreDiscountsProducts extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_discounts_products'; + + use ModelTrait; + + /** + * 一对一关联商品表 + * @return \think\model\relation\HasOne + */ + public function product() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id')->field(['id', 'pid', 'type', 'cate_id', 'relation_id', 'freight', 'postage', 'temp_id', 'delivery_type'])->bind([ + 'pid', + 'cate_id', + 'plat_type' => 'type', + 'relation_id' => 'relation_id', + 'freight' => 'freight', + 'postage' => 'postage', + 'p_temp_id' => 'temp_id', + 'delivery_type' + ]); + } + + /** + * 状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchDiscountIdAttr($query, $value) + { + if ($value != '') $query->where('discount_id', $value); + } + +} \ No newline at end of file diff --git a/app/model/activity/integral/StoreIntegral.php b/app/model/activity/integral/StoreIntegral.php new file mode 100644 index 0000000..31ee748 --- /dev/null +++ b/app/model/activity/integral/StoreIntegral.php @@ -0,0 +1,288 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\integral; + +use app\model\product\product\StoreDescription; +use app\common\model\store\product\Product as StoreProduct; +use crmeb\traits\ModelTrait; +use crmeb\basic\BaseModel; +use think\Model; + +/** + * 积分商品Model + * Class StoreIntegral + * @package app\model\activity\integral + */ +class StoreIntegral extends BaseModel +{ + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_integral'; + + use ModelTrait; + + /** + * 配送信息 + * @param $value + * @return false|string + */ + protected function setDeliveryTypeAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 配送信息 + * @param $value + * @param $data + * @return mixed + */ + protected function getDeliveryTypeAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 配送信息 + * @param $value + * @param $data + * @return mixed + */ + protected function getCustomFormAttr($value) + { + if ($value) { + return is_string($value) ? json_decode($value, true) : $value; + } + return []; + } + + /** + * @param $value + * @return array|mixed + */ + public function getStoreLabelIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + /** + * 商品标签 + * @param $value + * @return array|mixed + */ + protected function getLabelIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 商品保障服务 + * @param $value + * @return array|mixed + */ + protected function getEnsureIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 参数信息 + * @param $value + * @return array|mixed + */ + protected function getSpecsAttr($value) + { + if ($value) { + return is_string($value) ? json_decode($value, true) : $value; + } + return []; + } + + /** + * 适用门店信息 + * @param $value + * @return string + */ + protected function setApplicableStoreIdAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 适用门店信息 + * @param $value + * @return array|false|string[] + */ + protected function getApplicableStoreIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + /** + * 一对一获取原价 + * @return \think\model\relation\HasOne + */ + public function getPrice() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id')->bind(['ot_price', 'product_price' => 'price']); + } + + /** + * 一对一关联 + * 商品关联商品商品详情 + * @return \think\model\relation\HasOne + */ + public function descriptions() + { + return $this->hasOne(StoreDescription::class, 'product_id', 'id')->where('type', 4)->bind(['description']); + } + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getAddTimeAttr($value) + { + if ($value) return date('Y-m-d H:i:s', (int)$value); + return ''; + } + + /** + * 轮播图获取器 + * @param $value + * @return mixed + */ + public function getImagesAttr($value) + { + return json_decode($value, true) ?? []; + } + + /** + * 积分商品名称搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStoreNameAttr($query, $value, $data) + { + if ($value) $query->where('title|id', 'like', '%' . $value . '%'); + } + + /** + * 是否推荐搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsHostAttr($query, $value, $data) + { + $query->where('is_host', $value ?? 1); + } + + /** + * 状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsShowAttr($query, $value, $data) + { + if ($value != '') $query->where('is_show', $value ?: 0); + } + + /** + * 是否删除搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsDelAttr($query, $value, $data) + { + $query->where('is_del', $value ?? 0); + } + + /** + * 商品ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProductIdAttr($query, $value, $data) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + } + + /** + * 系统表单搜索器 + * @param Model $query + * @param $value + */ + public function searchSystemFormIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('system_form_id', $value); + } else { + if ($value !== '') $query->where('system_form_id', $value); + } + } + + /** + * 适用门店类型搜索器 + * @param Model $query + * @param $value + */ + public function searchApplicableTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('applicable_type', $value); + } else { + if ($value !== '') $query->where('applicable_type', $value); + } + } + +} diff --git a/app/model/activity/integral/StoreIntegralOrder.php b/app/model/activity/integral/StoreIntegralOrder.php new file mode 100644 index 0000000..32e0ca2 --- /dev/null +++ b/app/model/activity/integral/StoreIntegralOrder.php @@ -0,0 +1,207 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\integral; + +use app\model\product\sku\StoreProductVirtual; +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 订单Model + * Class StoreIntegralOrder + * @package app\model\activity\integral + */ +class StoreIntegralOrder extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_integral_order'; + + protected $insert = ['add_time']; + + /** + * 更新时间 + * @var bool | string | int + */ + protected $updateTime = false; + + /** + * 创建时间修改器 + * @return int + */ + protected function setAddTimeAttr() + { + return time(); + } + + /** + * 购物车信息获取器 + * @param $value + * @return array|mixed + */ + public function getCartInfoAttr($value) + { + return is_string($value) ? json_decode($value, true) ?? [] : []; + } + + /** + * 配送信息 + * @param $value + * @param $data + * @return mixed + */ + protected function getCustomFormAttr($value) + { + if ($value) { + return is_string($value) ? json_decode($value, true) : $value; + } + return []; + } + + /** + * 一对一关联用户表 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid')->field(['uid', 'nickname', 'phone', 'spread_uid'])->bind([ + 'nickname' => 'nickname', + 'phone' => 'phone', + 'spread_uid' => 'spread_uid', + ]); + } + + /** + * 关联卡密 + * @return \think\model\relation\HasMany + */ + public function virtual() + { + return $this->hasMany(StoreProductVirtual::class, 'order_id', 'order_id')->where('order_type', 2); + } + + /** + * 关联订单记录 + * @return \think\model\relation\HasMany + */ + public function orderStatus() + { + return $this->hasMany(StoreIntegralOrderStatus::class, 'oid', 'id'); + } + + /** + * 订单ID搜索器 + * @param Model $query + * @param $value + */ + public function searchOrderIdAttr($query, $value) + { + $query->where('order_id', $value); + } + + /** + * 订单状态搜索器 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value !== '') { + $query->where('status', $value); + } + } + + /** + * 商品id搜索器 + * @param Model $query + * @param $value + */ + public function searchProductIdAttr($query, $value) + { + if ($value !== '') { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + } + + /** + * 核销码搜索器 + * @param Model $query + * @param $value + */ + public function searchVerifyCodeAttr($query, $value) + { + $query->where('verify_code', $value); + } + + /** + * @param Model $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('id', $value); + } else { + $query->where('id', $value); + } + } + + /** + * 订单id或者用户名搜索器 + * @param $query + * @param $value + */ + public function searchOrderIdRealNameAttr($query, $value) + { + $query->where('order_id|real_name', $value); + } + + /** + * 用户ID搜索器 + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if (is_array($value)) + $query->whereIn('uid', $value); + else + $query->where('uid', $value); + } + + /** + * 用户来源 + * @param Model $query + * @param $value + */ + public function searchChannelTypeAttr($query, $value) + { + if ($value != '') $query->where('channel_type', $value); + } + +} diff --git a/app/model/activity/integral/StoreIntegralOrderStatus.php b/app/model/activity/integral/StoreIntegralOrderStatus.php new file mode 100644 index 0000000..4fd5922 --- /dev/null +++ b/app/model/activity/integral/StoreIntegralOrderStatus.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\integral; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 订单修改状态记录Model + * Class StoreIntegralOrderStatus + * @package app\model\activity\integral + */ +class StoreIntegralOrderStatus extends BaseModel +{ + use ModelTrait; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_integral_order_status'; + + protected $autoWriteTimestamp = 'int'; + + protected $createTime = 'change_time'; + + /** + * 订单ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchOidAttr($query, $value, $data) + { + $query->where('oid', $value); + } + + /** + * 变动类型搜索器 + * @param Model $query + * @param $value + */ + public function searchChangeTypeAttr($query, $value) + { + $query->where('change_type', $value); + } +} diff --git a/app/model/activity/lottery/LuckLottery.php b/app/model/activity/lottery/LuckLottery.php new file mode 100644 index 0000000..a9763d0 --- /dev/null +++ b/app/model/activity/lottery/LuckLottery.php @@ -0,0 +1,147 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\model\activity\lottery; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 抽奖活动 + * Class LuckLottery + * @package app\model\activity\lottery + */ +class LuckLottery extends BaseModel +{ + + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'luck_lottery'; + + /** + * 抽奖用户等级修改器 + * @param $value + * @return false|string + */ + protected function setUserLevelAttr($value) + { + if ($value) { + return is_array($value) ? json_encode($value) : $value; + } + return ''; + } + + /** + * 抽奖用户等级获取器 + * @param $value + * @param $data + * @return mixed + */ + protected function getUserLevelAttr($value) + { + return $value ? json_decode($value, true) : []; + } + + /** + * 抽奖用户标签修改器 + * @param $value + * @return false|string + */ + protected function setUserLabelAttr($value) + { + if ($value) { + return is_array($value) ? json_encode($value) : $value; + } + return ''; + } + + /** + * 抽奖用户标签获取器 + * @param $value + * @param $data + * @return mixed + */ + protected function getUserLabelAttr($value) + { + return $value ? json_decode($value, true) : []; + } + + /** + * 关联奖品 + * @return \think\model\relation\HasOne + */ + public function prize() + { + return $this->hasMany(LuckPrize::class, 'lottery_id', 'id')->where('status', 1)->where('is_del', 0)->order('sort asc,id asc'); + } + + /** + * 关键词搜索器 + * @param $query Model + * @param $value + */ + public function searchKeywordAttr($query, $value) + { + if ($value !== '') $query->where('id|name|desc|content', 'like', '%' . $value . '%'); + } + + /** + * 抽奖形式搜索器 + * @param $query Model + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) $query->where('type', $value); + } + + /** + * 抽奖类型搜索器 + * @param $query Model + * @param $value + */ + public function searchFactorAttr($query, $value) + { + if ($value !== '') $query->where('factor', $value); + } + + /** + * 状态搜索器 + * @param $query Model + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value !== '') $query->where('status', $value); + } + + /** + * 是否删除搜索器 + * @param $query Model + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } +} diff --git a/app/model/activity/lottery/LuckLotteryRecord.php b/app/model/activity/lottery/LuckLotteryRecord.php new file mode 100644 index 0000000..c2252d9 --- /dev/null +++ b/app/model/activity/lottery/LuckLotteryRecord.php @@ -0,0 +1,218 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\model\activity\lottery; + +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * + * Class LuckLotteryRecordDao + * @package app\model\activity\lottery + */ +class LuckLotteryRecord extends BaseModel +{ + + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'luck_lottery_record'; + + /** + * 收货信息修改器 + * @param $value + * @return false|string + */ + protected function setReceiveInfoAttr($value) + { + if ($value) { + return is_array($value) ? json_encode($value) : $value; + } + return ''; + } + + /** + * 收货信息获取器 + * @param $value + * @param $data + * @return mixed + */ + protected function getReceiveInfoAttr($value) + { + return $value ? json_decode($value, true) : []; + } + + /** + * 发货信息修改器 + * @param $value + * @return false|string + */ + protected function setDeliverInfoAttr($value) + { + if ($value) { + return is_array($value) ? json_encode($value) : $value; + } + return ''; + } + + /** + * 发货信息获取器 + * @param $value + * @param $data + * @return mixed + */ + protected function getDeliverInfoAttr($value) + { + return $value ? json_decode($value, true) : []; + } + + /** + * 关联抽奖 + * @return \think\model\relation\HasOne + */ + public function lottery() + { + return $this->hasOne(LuckLottery::class, 'id', 'lottery_id'); + } + + /** + * 关联奖品 + * @return \think\model\relation\HasOne + */ + public function prize() + { + return $this->hasOne(LuckPrize::class, 'id', 'prize_id'); + } + + /** + * 关联用户 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid', false)->field('uid,real_name,nickname,phone,delete_time'); + } + + /** + * 用户uid搜索器 + * @param $query Model + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value) $query->where('uid', $value); + } + + /** + * 关键词搜索器 + * @param $query Model + * @param $value + */ + public function searchKeywordAttr($query, $value) + { + if ($value !== '') { + $query->where(function ($query1) use ($value) { + $query1->where('id|uid|lottery_id|prize_id', 'LIKE', '%' . $value . '%')->whereOr('uid', 'IN', function ($query1) use ($value) { + $query1->name('user')->field('uid')->where('account|nickname|phone|real_name|uid', 'LIKE', "%$value%")->select(); + })->whereOr('lottery_id', 'IN', function ($query1) use ($value) { + $query1->name('luck_lottery')->field('id')->where('name|desc|content', 'LIKE', "%$value%")->select(); + })->whereOr('prize_id', 'IN', function ($query1) use ($value) { + $query1->name('luck_prize')->field('id')->where('name|prompt', 'LIKE', "%$value%")->select(); + }); + }); + } + } + + /** + * 抽奖id搜索器 + * @param $query Model + * @param $value + */ + public function searchLotteryIdAttr($query, $value) + { + if ($value) $query->where('lottery_id', $value); + } + + /** + * 奖品id搜索器 + * @param $query Model + * @param $value + */ + public function searchPrizeIdAttr($query, $value) + { + if ($value) $query->where('prize_id', $value); + } + + /** + * 奖品类型搜索器 + * @param $query Model + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('type', $value); + } else { + $query->where('type', $value); + } + } + } + + /** + * 奖品不再这个类型中搜索器 + * @param $query Model + * @param $value + */ + public function searchNotTypeAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereNotIn('type', $value); + } else { + $query->where('type', '<>', $value); + } + } + } + + /** + * 是否领取 + * @param $query Model + * @param $value + */ + public function searchIsReceiveAttr($query, $value) + { + if ($value !== '') $query->where('is_receive', $value); + } + + /** + * 是否发货处理 + * @param $query Model + * @param $value + */ + public function searchIsDeliverAttr($query, $value) + { + if ($value !== '') $query->where('is_deliver', $value); + } +} diff --git a/app/model/activity/newcomer/StoreNewcomer.php b/app/model/activity/newcomer/StoreNewcomer.php new file mode 100644 index 0000000..0dc3970 --- /dev/null +++ b/app/model/activity/newcomer/StoreNewcomer.php @@ -0,0 +1,104 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\newcomer; + +use app\model\product\product\StoreDescription; +use app\common\model\store\product\Product as StoreProduct; +use app\model\product\sku\StoreProductAttrValue; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 新人礼商品Model + * Class StoreNewcomer + * @package app\model\activity\newcomer + */ +class StoreNewcomer extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_newcomer'; + + protected $updateTime = false; + + + /** + * 一对一关联 + * 商品关联商品商品详情 + * @return \think\model\relation\HasOne + */ + public function descriptions() + { + return $this->hasOne(StoreDescription::class, 'product_id', 'product_id')->where('type', 1)->bind(['description']); + } + + /** + * 一对一关联 + * 商品关联商品商品详情 + * @return \think\model\relation\HasOne + */ + public function product() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id')->where('is_show', 1)->where('is_del', 0)->where('is_verify', 1); + } + + /** + * sku一对多 + * @return \think\model\relation\HasMany + */ + public function attrValue() + { + return $this->hasMany(StoreProductAttrValue::class, 'product_id', 'id')->where('type', 7); + } + + + /** + * 商品ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProductIdAttr($query, $value, $data) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + } + + /** + * 是否删除 + * @param $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') { + $query->where('is_del', $value); + } + } + +} diff --git a/app/model/activity/promotions/StorePromotions.php b/app/model/activity/promotions/StorePromotions.php new file mode 100644 index 0000000..1e336a0 --- /dev/null +++ b/app/model/activity/promotions/StorePromotions.php @@ -0,0 +1,449 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\promotions; + +use app\model\activity\coupon\StoreCouponIssue; +use app\common\model\store\product\Product as StoreProduct; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 促销活动活动 + * Class StorePromotions + * @package app\model\activity\promotions + */ +class StorePromotions extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_promotions'; + + protected $updateTime = false; + + /** + * 优惠叠加 + * @param $value + * @return string + */ + protected function setDiscountUseAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 优惠叠加 + * @param $value + * @return array|false|string[] + */ + protected function getDiscountUseAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 标签 + * @param $value + * @return array|mixed + */ + protected function getLabelIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 关联商品 + * @param $value + * @return array|mixed + */ + protected function getProductIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 叠加使用 + * @param $value + * @return array|mixed + */ + protected function getOverlayAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 赠送优惠券 + * @param $value + * @return array|mixed + */ + protected function getIGiveCouponIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 赠送商品 + * @param $value + * @return array|mixed + */ + protected function getIGiveProductIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 赠送商品 + * @param $value + * @return array|mixed + */ + protected function getIGiveProductUniqueAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getAddTimeAttr($value) + { + if ($value) return date('Y-m-d H:i:s', (int)$value); + return ''; + } + + /** + * 适用门店信息 + * @param $value + * @return string + */ + protected function setApplicableStoreIdAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 适用门店信息 + * @param $value + * @return array|false|string[] + */ + protected function getApplicableStoreIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + + /** + * 关联商品 + * @return \think\model\relation\HasMany + */ + public function products() + { + return $this->hasMany(StorePromotionsAuxiliary::class, 'promotions_id', 'id')->where('type',1)->where('product_partake_type', 2); + } + + /** + * 关联优惠券 + * @return \think\model\relation\HasOne + */ + public function giveCoupon() + { + return $this->hasMany(StorePromotionsAuxiliary::class, 'promotions_id', 'id')->where('type',2); + } + + /** + * 赠送商品 + * @return \think\model\relation\HasOne + */ + public function giveProducts() + { + return $this->hasMany(StorePromotionsAuxiliary::class, 'promotions_id', 'id')->where('type',3); + } + + /** + * 阶梯优惠 + * @return \think\model\relation\HasMany + */ + public function promotions() + { + return $this->hasMany(self::class, 'pid', 'id')->order('id asc'); + } + + /** + * 关联品牌 + * @return \think\model\relation\HasMany + */ + public function brands() + { + return $this->hasMany(StorePromotionsAuxiliary::class, 'promotions_id', 'id')->where('type',1)->where('product_partake_type', 4); + } + + + /** + * 关联商品标签 + * @return \think\model\relation\HasMany + */ + public function productLabels() + { + return $this->hasMany(StorePromotionsAuxiliary::class, 'promotions_id', 'id')->where('type',1)->where('product_partake_type', 5); + } + + /** + * ID搜索器 + * @param Model $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('id', $value); + } else { + if ($value) $query->where('id', $value); + } + } + + /** + * pid搜索器 + * @param Model $query + * @param $value + */ + public function searchPidAttr($query, $value) + { + if ($value !== '') $query->where('pid', $value); + } + + /** + * type搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) $query->where('type', $value); + } + + + /** + * store_id搜索器 + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') $query->where('store_id', $value); + } + + /** + * promotions_type搜索器 + * @param $query + * @param $value + */ + public function searchPromotionsTypeAttr($query, $value) + { + if ($value !== '') { + if (is_array($value)) { + if ($value) $query->whereIn('promotions_type', $value); + } else { + if ($value) $query->where('promotions_type', $value); + } + } + } + + /** + * promotions_cate搜索器 + * @param Model $query + * @param $value + */ + public function searchPromotionsCateAttr($query, $value) + { + if ($value !== '') $query->where('promotions_cate', $value); + } + + /** + * name搜索器 + * @param Model $query + * @param $value + */ + public function searchNameAttr($query, $value) + { + if ($value !== '') $query->whereLike('id|name|desc', "%" . $value . "%"); + } + + /** + * threshold_type搜索器 + * @param Model $query + * @param $value + */ + public function searchThresholdTypeAttr($query, $value) + { + if ($value !== '') $query->where('threshold_type', $value); + } + + /** + * discount_type搜索器 + * @param Model $query + * @param $value + */ + public function searchDiscountTypeAttr($query, $value) + { + if ($value !== '') $query->where('discount_type', $value); + } + + /** + * n_piece_n_discount搜索器 + * @param Model $query + * @param $value + */ + public function searchNPieceNDiscountAttr($query, $value) + { + if ($value !== '') $query->where('n_piece_n_discount', $value); + } + + /** + * product_partake_type搜索器 + * @param Model $query + * @param $value + */ + public function searchProductPartakeTypeAttr($query, $value) + { + if ($value !== '') { + if (is_array($value)) { + if ($value) $query->whereIn('product_partake_type', $value); + } else { + if ($value) $query->where('product_partake_type', $value); + } + } + } + + /** + * 是否删除搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsDelAttr($query, $value, $data) + { + $query->where('is_del', $value ?? 0); + } + + /** + * 状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value, $data) + { + if ($value != '') $query->where('status', $value); + } + + /** + * 适用门店类型搜索器 + * @param Model $query + * @param $value + */ + public function searchApplicableTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('applicable_type', $value); + } else { + if ($value !== '') $query->where('applicable_type', $value); + } + } + + /** + * 活动事件搜索器 + * @param $query + * @param $value + * @param $data + * @return void + */ + public function searchActivityTimeAttr($query, $value, $data) + { + if ($value) { + $startTime = $endTime = 0; + if (is_array($value)) { + $startTime = trim($value[0] ?? 0); + $endTime = trim($value[1] ?? 0); + } elseif (is_string($value)) { + if (strstr($value, '-') !== false) { + [$startTime, $endTime] = explode('-', $value); + $startTime = trim($startTime); + $endTime = trim($endTime); + } + } + if ($startTime || $endTime) { + try { + date('Y-m-d', $startTime); + } catch (\Throwable $e) { + $startTime = strtotime($startTime); + } + try { + date('Y-m-d', $endTime); + } catch (\Throwable $e) { + $endTime = strtotime($endTime); + } + if ($startTime == $endTime || $endTime == strtotime(date('Y-m-d', $endTime))) { + $endTime = $endTime + 86399; + } + $query->where(function ($b) use($startTime, $endTime) { + $b->whereBetween('start_time', [$startTime, $endTime])->whereOr(function ($q) use ($startTime, $endTime) { + $q->whereBetween('stop_time', [$startTime, $endTime]); + })->whereOr(function ($q) use ($startTime, $endTime) { + $q->where('start_time', '<=', $startTime)->where('stop_time', '>=', $endTime); + }); + }); + } + } + } + +} diff --git a/app/model/activity/promotions/StorePromotionsAuxiliary.php b/app/model/activity/promotions/StorePromotionsAuxiliary.php new file mode 100644 index 0000000..b9f7cbc --- /dev/null +++ b/app/model/activity/promotions/StorePromotionsAuxiliary.php @@ -0,0 +1,189 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\promotions; + + +use app\model\activity\coupon\StoreCouponIssue; +use app\common\model\store\StoreBrand; +use app\common\model\store\product\ProductLabel as StoreProductLabel; +use app\common\model\store\product\Product as StoreProduct; +use app\model\product\sku\StoreProductAttrValue; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * 优惠活动辅助表 + */ +class StorePromotionsAuxiliary extends BaseModel +{ + use ModelTrait; + + /** + * @var string + */ + protected $key = 'id'; + + /** + * @var string + */ + protected $name = 'store_promotions_auxiliary'; + + /** + * 商品规格 + * @param $value + * @return array|mixed + */ + protected function getUniqueAttr($value, $data) + { + if ($value) { + return is_string($value) && (!isset($data['type']) || $data['type'] != 3) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 关联优惠券 + * @return \think\model\relation\HasOne + */ + public function coupon() + { + return $this->hasOne(StoreCouponIssue::class, 'id', 'coupon_id'); + } + + /** + * 关联商品 + * @return \think\model\relation\HasOne + */ + public function productInfo() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id'); + } + + /** + * 赠送sku一对一 + * @return \think\model\relation\hasOne + */ + public function giveAttrValue() + { + return $this->hasOne(StoreProductAttrValue::class, 'unique', 'unique')->where('type', 0); + } + + /** + * sku一对多 + * @return \think\model\relation\HasMany + */ + public function attrValue() + { + return $this->hasMany(StoreProductAttrValue::class, 'product_id', 'product_id')->where('type', 0); + } + + /** + * 关联商品品牌 + * @return \think\model\relation\HasOne + */ + public function brandInfo() + { + return $this->hasOne(StoreBrand::class, 'id', 'brand_id'); + } + + /** + * 关联商品标签 + * @return \think\model\relation\HasOne + */ + public function productLabelInfo() + { + return $this->hasOne(StoreProductLabel::class, 'id', 'store_label_id'); + } + + + /** + * type搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) $query->where('type', $value); + } + + /** + * product_partake_type搜索器 + * @param Model $query + * @param $value + */ + public function searchProductPartakeTypeAttr($query, $value) + { + if ($value !== '') { + if (is_array($value)) { + if ($value) $query->whereIn('product_partake_type', $value); + } else { + if ($value) $query->where('product_partake_type', $value); + } + } + } + + /** + * promotions_id搜索器 + * @param $query + * @param $value + */ + public function searchPromotionsIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('promotions_id', $value); + } else { + if ($value !== '') $query->where('promotions_id', $value); + } + } + + /** + * coupon_id搜索器 + * @param $query + * @param $value + */ + public function searchCouponIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('coupon_id', $value); + } else { + if ($value !== '') $query->where('coupon_id', $value); + } + } + + + /** + * product_id搜索器 + * @param $query + * @param $value + */ + public function searchProductIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('product_id', $value); + } else { + if ($value !== '') $query->where('product_id', $value); + } + } + + /** + * is_all搜索器 + * @param $query + * @param $value + */ + public function searchIsAllAttr($query, $value) + { + if ($value !== '') { + $query->where('is_all', $value); + } + } + +} diff --git a/app/model/activity/seckill/StoreSeckill.php b/app/model/activity/seckill/StoreSeckill.php new file mode 100644 index 0000000..54189c9 --- /dev/null +++ b/app/model/activity/seckill/StoreSeckill.php @@ -0,0 +1,387 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\activity\seckill; + +use app\common\model\store\StoreActivity; +use app\model\product\product\StoreDescription; +use app\common\model\store\product\Product as StoreProduct; +use app\model\product\sku\StoreProductAttrValue; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 秒杀商品Model + * Class StoreSeckill + * @package app\model\activity\seckill + */ +class StoreSeckill extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_seckill'; + + /** + * 配送信息 + * @param $value + * @return false|string + */ + protected function setDeliveryTypeAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 配送信息 + * @param $value + * @param $data + * @return mixed + */ + protected function getDeliveryTypeAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 配送信息 + * @param $value + * @param $data + * @return mixed + */ + protected function getCustomFormAttr($value) + { + if ($value) { + return is_string($value) ? json_decode($value, true) : $value; + } + return []; + } + + /** + * 商品标签 + * @param $value + * @return array|mixed + */ + protected function getStoreLabelIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 商品保障服务 + * @param $value + * @return array|mixed + */ + protected function getEnsureIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * 参数信息 + * @param $value + * @return array|mixed + */ + protected function getSpecsAttr($value) + { + if ($value) { + return is_string($value) ? json_decode($value, true) : $value; + } + return []; + } + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getAddTimeAttr($value) + { + if ($value) return date('Y-m-d H:i:s', (int)$value); + return ''; + } + + /** + * 图片获取器 + * @param $value + * @return array|mixed + */ + protected function getImagesAttr($value) + { + return json_decode($value, true) ?: []; + } + + /** + * 适用门店信息 + * @param $value + * @return string + */ + protected function setApplicableStoreIdAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 适用门店信息 + * @param $value + * @return array|false|string[] + */ + protected function getApplicableStoreIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + /** + * 时间段ID + * @param $value + * @return string + */ + protected function setTimeIdAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 时间段ID + * @param $value + * @return array|false|string[] + */ + protected function getTimeIdAttr($value) + { + if ($value) { + return is_string($value) ? array_map('intval', array_filter(explode(',', $value))) : $value; + } + return []; + } + + /** + * 一对一关联 + * 商品关联商品商品详情 + * @return \think\model\relation\HasOne + */ + public function descriptions() + { + return $this->hasOne(StoreDescription::class, 'product_id', 'id')->where('type', 1)->bind(['description']); + } + + /** + * 一对一关联 + * 商品关联商品商品详情 + * @return \think\model\relation\HasOne + */ + public function product() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id')->where('is_show', 1)->where('is_del', 0)->field(['SUM(sales+ficti) as total', 'id'])->bind([ + 'total' => 'total' + ]); + } + + /** + * sku一对多 + * @return \think\model\relation\HasMany + */ + public function attrValue() + { + return $this->hasMany(StoreProductAttrValue::class, 'product_id', 'id')->where('type', 1); + } + + /** + * 一对一关联 + * 商品关联活动 + * @return \think\model\relation\HasOne + */ + public function activity() + { + return $this->hasOne(StoreActivity::class, 'id', 'activity_id'); + } + + /** + * 一对一关联 + * 商品关联活动 + * @return \think\model\relation\HasOne + */ + public function activityName() + { + return $this->hasOne(StoreActivity::class, 'id', 'activity_id')->field(['id', 'name'])->bind([ + 'activity_name' => 'name' + ]); + } + + /** + * 活动ID搜索器 + * @param Model $query + * @param $value + */ + public function searchActivityIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('activity_id', $value); + } else { + $query->where('activity_id', $value); + } + } + } + + /** + * 秒杀时间段 + * @param $query + * @param $value + */ + public function searchTimeIdAttr($query, $value) + { + if ($value) $query->where('find_in_set(' . $value . ',`time_id`)'); + } + + /** + * 秒杀商品名称搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStoreNameAttr($query, $value, $data) + { + if ($value) $query->where('title|id', 'like', '%' . $value . '%'); + } + + /** + * 是否推荐搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsHotAttr($query, $value, $data) + { + $query->where('is_hot', $value ?? 1); + } + + /** + * 状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsShowAttr($query, $value, $data) + { + $query->where('is_show', $value ?? 1); + } + + /** + * 是否删除搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsDelAttr($query, $value, $data) + { + $query->where('is_del', $value ?? 0); + } + + /** + * 状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value, $data) + { + if ($value != '') $query->where('status', $value); + } + + /** + * 商品ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProductIdAttr($query, $value, $data) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + } + + /** + * 活动有效时间搜索器 + * @param $query + * @param $value + */ + public function searchSeckillTimeAttr($query, $value) + { + if ($value == 1) { + $time = time(); + $query->where('start_time', '<=', $time)->where('stop_time', '>=', $time - 86400); + } + } + + /** + * 系统表单搜索器 + * @param Model $query + * @param $value + */ + public function searchSystemFormIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('system_form_id', $value); + } else { + if ($value !== '') $query->where('system_form_id', $value); + } + } + + /** + * 适用门店类型搜索器 + * @param Model $query + * @param $value + */ + public function searchApplicableTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('applicable_type', $value); + } else { + if ($value !== '') $query->where('applicable_type', $value); + } + } +} diff --git a/app/model/activity/table/TableQrcode.php b/app/model/activity/table/TableQrcode.php new file mode 100644 index 0000000..cb0e36e --- /dev/null +++ b/app/model/activity/table/TableQrcode.php @@ -0,0 +1,88 @@ + +// +---------------------------------------------------------------------- +namespace app\model\activity\table; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use app\model\other\Category; +use app\model\store\SystemStore; +use app\model\activity\collage\UserCollageCode; + +/** + * 拼单Model + * Class TableSeats + * @package app\model\activity\table + */ +class TableQrcode extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'table_qrcode'; + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getAddTimeAttr($value) + { + if ($value) return date('Y-m-d H:i:s', (int)$value); + return ''; + } + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getOrderTimeAttr($value) + { + if ($value) return date('m-d H:i', (int)$value); + return ''; + } + + /**一对一关联 + * 获取分类 + * @return \think\model\relation\HasOne + */ + public function category() + { + return $this->hasOne(Category::class, 'id', 'cate_id')->where(['group' => 6, 'type' => 1])->field(['id', 'name']); + } + + /**一对一关联 + * 获取门店名称 + * @return \think\model\relation\HasOne + */ + public function storeName() + { + return $this->hasOne(SystemStore::class, 'id', 'store_id')->field(['id','name as storeName']); + } + + /**一对一关联 + * 获取桌码记录信息 + * @return \think\model\relation\HasOne + */ + public function collateCode() + { + return $this->hasOne(UserCollageCode::class, 'qrcode_id', 'id')->field(['qrcode_id','serial_number','number_diners']); + } +} diff --git a/app/model/agent/AgentLevel.php b/app/model/agent/AgentLevel.php new file mode 100644 index 0000000..3228961 --- /dev/null +++ b/app/model/agent/AgentLevel.php @@ -0,0 +1,89 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\model\agent; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 分销员等级 + * Class AgentLevel + * @package app\model\agent + */ +class AgentLevel extends BaseModel +{ + + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'agent_level'; + + /** + * 关联等级任务 + * @return \think\model\relation\HasMany + */ + public function task() + { + return $this->hasMany(AgentLevelTask::class, 'level_id', 'id')->where('is_del', 0); + } + + /** + * 关键词搜索 + * @param $query + * @param $value + */ + public function searchKeywordAttr($query, $value) + { + if ($value !== '') $query->whereLike('id|name', "%" . trim($value) . "%"); + } + + /** + * 等级搜索器 + * @param $query Model + * @param $value + */ + public function searchGradeAttr($query, $value) + { + if ($value !== '') $query->where('grade', $value); + } + + /** + * 状态搜索器 + * @param $query Model + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value !== '') $query->where('status', $value); + } + + /** + * 是否删除搜索器 + * @param $query Model + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } +} diff --git a/app/model/message/SystemMessage.php b/app/model/message/SystemMessage.php new file mode 100644 index 0000000..904b52c --- /dev/null +++ b/app/model/message/SystemMessage.php @@ -0,0 +1,87 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\message; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 站内信 + * Class SystemMessage + * @package app\model\message + */ +class SystemMessage extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + + /** + * 模型名称 + * @var string + */ + protected $name = 'system_message'; + + + protected $insert = ['add_time']; + + /** + * ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIdAttr($query, $value, $data) + { + $query->where('id', $value); + } + + /** + * UID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchUidAttr($query, $value, $data) + { + $query->where('uid', $value); + } + + /** + * Look搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchLookAttr($query, $value, $data) + { + $query->where('look', $value); + } + + /** + * del搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsDelAttr($query, $value, $data) + { + $query->where('is_del', $value); + } + +} diff --git a/app/model/message/service/StoreServiceRecord.php b/app/model/message/service/StoreServiceRecord.php new file mode 100644 index 0000000..93a9316 --- /dev/null +++ b/app/model/message/service/StoreServiceRecord.php @@ -0,0 +1,106 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\message\service; + + +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 客服聊天用户记录 + * Class StoreServiceRecord + * @package app\model\message\service + */ +class StoreServiceRecord extends BaseModel +{ + use ModelTrait; + + protected $name = 'store_service_record'; + + protected $pk = 'id'; + + /** + * 更新时间 + * @var bool | string | int + */ + protected $updateTime = false; + + /** + * 用户关联 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'to_uid')->field(['nickname', 'uid', 'avatar'])->bind([ + 'wx_nickname' => 'nickname', + 'wx_avatar' => 'avatar', + ]); + } + + /** + * 客服用户 + * @return \think\model\relation\HasOne + */ + public function service() + { + return $this->hasOne(StoreService::class, 'uid', 'to_uid')->field(['nickname', 'uid', 'avatar'])->bind([ + 'kefu_nickname' => 'nickname', + 'kefu_avatar' => 'avatar', + ]); + } + + /** + * 发送者id搜索器 + * @param Model $query + * @param $value + */ + public function searchUserIdAttr($query, $value) + { + $query->where('user_id', $value); + } + + /** + * 送达人uid搜索器 + * @param Model $query + * @param $value + */ + public function searchToUidAttr($query, $value) + { + $query->where('to_uid', $value); + } + + /** + * 用户昵称搜索器 + * @param Model $query + * @param $value + */ + public function searchTitleAttr($query, $value) + { + if ($value) { + $query->whereIn('to_uid', function ($query) use ($value) { + $query->name('user')->whereLike('nickname|uid', '%' . $value . '%')->field('uid'); + }); + } + } + + /** + * 是否游客 + * @param Model $query + * @param $value + */ + public function searchIsTouristAttr($query, $value) + { + $query->where('is_tourist', $value); + } +} diff --git a/app/model/order/OtherOrder.php b/app/model/order/OtherOrder.php new file mode 100644 index 0000000..94c0af5 --- /dev/null +++ b/app/model/order/OtherOrder.php @@ -0,0 +1,211 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\order; + + +use app\model\store\SystemStoreStaff; +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +class OtherOrder extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'other_order'; + + protected $insert = ['add_time']; + + // protected $hidden = ['add_time', 'is_del', 'uid']; + + + /** + * 门店ID + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') { + if ($value == -1) {//所有门店 + $query->where('store_id', '>', 0); + } else { + $query->where('store_id', $value); + } + } + } + + /** + * 门店店员ID + * @param $query + * @param $value + */ + public function searchStaffIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('staff_id', $value); + } else { + $query->where('staff_id', $value); + } + } + } + + /** + * 订单类型 + * @param $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->where('type', 'in', $value); + } else { + $query->where('type', $value); + } + } + } + + public function searchPaidAttr($query, $value) + { + $query->where('paid', $value); + } + + /** + * 支付方式不属于 + * @param $query + * @param $value + */ + public function searchPayTypeNoAttr($query, $value) + { + $query->where('pay_type', '<>', $value); + } + + /** + * 用户来源 + * @param Model $query + * @param $value + */ + public function searchChannelTypeAttr($query, $value) + { + if ($value != '') $query->where('channel_type', $value); + } + + /** + * 订单id搜索器 + * @param $query + * @param $value + */ + public function searchOrderIdAttr($query, $value) + { + if ($value != "") { + $query->where('order_id', $value); + } + + } + + /** + * 一对一关联用户表 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid')->field(['uid', 'nickname', 'avatar', 'phone', 'spread_uid', 'overdue_time', 'now_money', 'integral']); + } + + /** + * @return model\relation\HasOne + */ + public function staff() + { + return $this->hasOne(SystemStoreStaff::class, 'id', 'staff_id') + ->field(['id', 'uid', 'store_id', 'staff_name']) + ->bind([ + 'staff_uid' => 'uid', + 'staff_store_id' => 'store_id', + 'staff_name' => 'staff_name' + ]);; + } + + /** + * 会员类型 + * @param $query + * @param $value + */ + public function searchMemberTypeAttr($query, $value) + { + if ($value && $value != 'card' && $value != 'free') { + $query->where('member_type', $value); + } elseif ($value == 'card') { + $query->where('member_type', 'free')->where('code', '<>', ''); + } elseif ($value == 'free') { + $query->where('member_type', 'free')->where('code', ''); + } + } + + /** + * 支付方式 + * @param $query + * @param $value + */ + public function searchPayTypeAttr($query, $value) + { + if ($value) { + if ($value == "free") { + $query->where(function ($query) { + $query->where('type', 'in', [0, 2, 4])->whereOr(function ($query) { + $query->where(['type' => 1, 'is_free' => 1]); + }); + }); + } else { + $query->where('pay_type', $value); + } + + } + } + + /** + * @param $query + * @param $value + */ + public function searchAddTimeAttr($query, $value) + { + if ($value) { + $query->whereTime('add_time', 'between', $value); + } + } + + /** + * @param $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value) { + $query->where('uid', 'in', $value); + } + } + + +} diff --git a/app/model/order/StoreCart.php b/app/model/order/StoreCart.php new file mode 100644 index 0000000..d6d6722 --- /dev/null +++ b/app/model/order/StoreCart.php @@ -0,0 +1,252 @@ + +// +---------------------------------------------------------------------- +namespace app\model\order; + +use app\model\product\branch\StoreBranchProductAttrValue; +use app\common\model\store\product\Product as StoreProduct; +use app\common\model\store\product\ProductAttrValue as StoreProductAttrValue; +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 购物车Model + * Class StoreCart + * @package app\model\order + */ +class StoreCart extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_cart'; + + /** + * 自动添加字段 + * @var string[] + */ + protected $insert = ['add_time']; + + /** + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid')->bind([ + 'nickname' => 'nickname', + 'avatar' => 'avatar' + ]); + } + + /** + * 添加时间修改器 + * @return int + */ + protected function setAddTimeAttr() + { + return time(); + } + + /** + * 一对一关联 + * 购物车关联商品商品详情 + * @return \think\model\relation\HasOne + */ + public function productInfo() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id'); + } + + /** + * 一对一关联 + * 购物车关联商品商品规格 + * @return \think\model\relation\HasOne + */ + public function attrInfo() + { + return $this->hasOne(StoreProductAttrValue::class, 'unique', 'product_attr_unique'); + } + + /** + * 门店规格表 + * @return \think\model\relation\HasOne + */ + public function storeBranchProductAttr() + { + return $this->hasOne(StoreBranchProductAttrValue::class, 'unique', 'product_attr_unique'); + } + + /** + * 类型搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchTypeAttr($query, $value, $data) + { + $query->where('type', $value); + } + + /** + * 购物车ID搜索器 + * @param $query + * @param $value + */ + public function searchCartIdsAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('id', $value); + } else { + $query->where('id', $value); + } + } + } + + /** + * 是否支付 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsPayAttr($query, $value, $data) + { + $query->where('is_pay', $value); + } + + /** + * 是否删除 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsDelAttr($query, $value, $data) + { + $query->where('is_del', $value); + } + + /** + * 是否立即支付 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsNewAttr($query, $value, $data) + { + $query->where('is_new', $value); + } + + /** + * 查询用户购物车 + * @param Model $query + * @param $value + * @param $data + */ + public function searchUidAttr($query, $value, $data) + { + $query->where('uid', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchTouristUidAttr($query, $value) + { + if ($value) { + $query->where('tourist_uid', $value); + } + } + + /** + * 商品ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProductIdAttr($query, $value, $data) + { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + + /** + * 商品类型搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProductTypeAttr($query, $value, $data) + { + if (is_array($value)) { + $query->whereIn('product_type', $value); + } else { + $query->where('product_type', $value); + } + } + + /** + * 商品规格唯一值搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProductAttrUniqueAttr($query, $value, $data) + { + $query->where('product_attr_unique', $value); + } + + /** + * store_id搜索器 + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') $query->where('store_id', $value); + } + + /** + * 店员搜索器 + * @param $query + * @param $value + */ + public function searchStaffIdAttr($query, $value) + { + if ($value !== '') { + $query->where('staff_id', $value); + } + } + + /** + * 一对多关联 + * 商品关联优惠卷模板id + * @return \think\model\relation\HasMany + */ + public function product() + { + return $this->hasMany(StoreProduct::class, 'id', 'product_id'); + + } +} diff --git a/app/model/order/StoreDeliveryOrder.php b/app/model/order/StoreDeliveryOrder.php new file mode 100644 index 0000000..78807e3 --- /dev/null +++ b/app/model/order/StoreDeliveryOrder.php @@ -0,0 +1,155 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\order; + + +use app\model\store\SystemStore; +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + + +class StoreDeliveryOrder extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_delivery_order'; + + protected $updateTime = false; + + /** + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid'); + } + + /** + * @return \think\model\relation\HasOne + */ + public function orderInfo() + { + return $this->hasOne(StoreOrder::class, 'id', 'oid'); + } + + /** + * @return \think\model\relation\HasOne + */ + public function storeInfo() + { + return $this->hasOne(SystemStore::class, 'id', 'relation_id')->where(['is_show' => 1, 'is_del' => 0]); + } + + /** + * @param $query + * @param $value + * @return void + */ + public function searchKeywordAttr($query, $value) + { + $query->where(''); + } + + /** + * 商户搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value !== '') $query->where('type', $value); + } + } + + /** + * 关联门店ID、供应商ID搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value); + } else { + if ($value !== '') $query->where('relation_id', $value); + } + } + + /** + * 订单ID搜索器 + * @param Model $query + * @param $value + */ + public function searchOidAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('oid', $value); + } else { + if ($value !== '') $query->where('oid', $value); + } + } + + /** + * UID搜索器 + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('uid', $value); + } else { + if ($value !== '') $query->where('uid', $value); + } + } + + /** + * 平台类型搜索器 + * @param $query + * @param $value + * @return void + */ + public function searchStationTypeAttr($query, $value) + { + if ($value !== '') $query->where('station_type', $value); + } + + /** + * status搜索器 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('status', $value); + } else { + if ($value !== '') $query->where('status', $value); + } + } + +} diff --git a/app/model/order/StoreOrderCartInfo.php b/app/model/order/StoreOrderCartInfo.php new file mode 100644 index 0000000..7ce3c12 --- /dev/null +++ b/app/model/order/StoreOrderCartInfo.php @@ -0,0 +1,188 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\order; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 订单记录Model + * Class StoreOrderCartInfo + * @package app\model\order + */ +class StoreOrderCartInfo extends BaseModel +{ + use ModelTrait; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_order_cart_info'; + + /** + * 购物车信息获取器 + * @param $value + * @return array|mixed + */ + public function getCartInfoAttr($value) + { + return is_string($value) ? json_decode($value, true) ?? [] : []; + } + + /** + * 订单ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchOidAttr($query, $value, $data) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('oid', $value); + } else { + $query->where('oid', $value); + } + } + } + + /** + * UID搜索器 + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('uid', $value); + } else { + $query->where('uid', $value); + } + } + } + + /** + * product_id搜索器 + * @param Model $query + * @param $value + */ + public function searchProductIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + } + + /** + * 优惠活动ID搜索器 + * @param Model $query + * @param $value + */ + public function searchPromotionsIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->where(function($q) use ($value) { + foreach ($value as $key => $v) { + $q->whereOr(function ($c) use ($v) { + $c->whereFindInSet('promotions_id', $v); + }); + } + }); + } else { + $query->whereFindInSet('promotions_id', $value); + } + } + } + + /** + * 购物车ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchCartIdAttr($query, $value, $data) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('cart_id', $value); + } else { + $query->where('cart_id', $value); + } + } + } + + /** + * 原购物车ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchOldCartIdAttr($query, $value, $data) + { + if (is_array($value)) { + $query->whereIn('old_cart_id', $value); + } else { + $query->where('old_cart_id', $value); + } + } + + /** + * 拆分状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchSplitStatusAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('split_status', $value); + } else { + if (in_array($value, [0, 1, 2])) { + $query->where('split_status', $value); + } + } + } + + /** + * 是否赠送搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsGiftAttr($query, $value) + { + if ($value !== '') { + $query->where('is_gift', $value); + } + } + + /** + * 是否核销搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsWriteoffAttr($query, $value) + { + if ($value !== '') { + $query->where('is_writeoff', $value); + } + } +} diff --git a/app/model/order/StoreOrderRefund.php b/app/model/order/StoreOrderRefund.php new file mode 100644 index 0000000..6a8f254 --- /dev/null +++ b/app/model/order/StoreOrderRefund.php @@ -0,0 +1,183 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\order; + + +use app\model\supplier\SystemSupplier; +use app\common\model\user\User; +use app\common\model\store\order\StoreOrder; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * Class StoreOrderRefund + * @package app\model\order + */ +class StoreOrderRefund extends BaseModel +{ + use ModelTrait; + + protected $pk = 'id'; + + protected $name = 'store_order_refund'; + + /** + * 购物车信息获取器 + * @param $value + * @return array|mixed + */ + public function getCartInfoAttr($value) + { + return is_string($value) ? json_decode($value, true) ?? [] : []; + } + + /** + * 图片获取器 + * @param $value + * @return array|mixed + */ + public function getRefundImgAttr($value) + { + return is_string($value) ? json_decode($value, true) ?? [] : []; + } + + /** + * 图片获取器 + * @param $value + * @return array|mixed + */ + public function getRefundGoodsImgAttr($value) + { + return is_string($value) ? json_decode($value, true) ?? [] : []; + } + + /** + * 一对一关联订单表 + * @return StoreOrderRefund|\think\model\relation\HasOne + */ + public function order() + { + return $this->hasOne(StoreOrder::class, 'id', 'store_order_id'); + } + + /** + * 一对一关联用户表 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid', false)->field(['uid', 'avatar', 'nickname', 'phone', 'now_money', 'integral', 'delete_time'])->bind([ + 'avatar' => 'avatar', + 'nickname' => 'nickname', + 'phone' => 'phone', + 'now_money' => 'now_money', + 'integral' => 'integral', + 'delete_time' => 'delete_time', + ]); + } + + /** + * 订单ID搜索器 + * @param $query + * @param $value + */ + public function searchStoreOrderIdAttr($query, $value) + { + if ($value !== '') { + if (is_array($value)) { + $query->whereIn('store_order_id', $value); + } else { + $query->where('store_order_id', $value); + } + } + } + + /** + * 门店ID + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') { + if ($value == -1) {//所有门店 + $query->where('store_id', '>', 0); + } else { + $query->where('store_id', $value); + } + } + } + + + /** + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value !== '' && !is_null($value)) { + if (is_array($value)) { + $query->whereIn('uid', $value); + } else { + $query->where('uid', $value); + } + } + } + + /** + * is_cancel + * @param Model $query + * @param $value + */ + public function searchIsCancelAttr($query, $value) + { + if ($value !== '' && !is_null($value)) $query->where('is_cancel', $value); + } + + /** + * is_del搜索器 + * @param Model $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '' && !is_null($value)) $query->where('is_del', $value); + } + + /** + * 供应商ID + * @param $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if ($value !== '') { + if ($value == -1) { + $query->where('supplier_id', '>', 0); + } else { + $query->where('supplier_id', $value); + } + } + } + + /** + * 一对一关联供应商 + * @return \think\model\relation\HasOne + */ + public function supplier() + { + return $this->hasOne(SystemSupplier::class, 'id', 'supplier_id')->field(['id', 'supplier_name'])->bind([ + 'supplier_name' + ]); + } +} diff --git a/app/model/other/Category.php b/app/model/other/Category.php new file mode 100644 index 0000000..c2fc3c2 --- /dev/null +++ b/app/model/other/Category.php @@ -0,0 +1,212 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\other; + + +use app\common\model\store\product\ProductLabel as StoreProductLabel; +use app\model\product\specs\StoreProductSpecs; +use app\common\model\user\UserLabel; +use app\model\activity\table\TableQrcode; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 分类表 + * Class Category + * @package app\model\other + */ +class Category extends BaseModel +{ + + use ModelTrait; + + /** + * 表名 + * @var string + */ + protected $name = 'category'; + + /** + * 主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 用户标签 + * @return \think\model\relation\HasMany + */ + public function label() + { + return $this->hasMany(UserLabel::class, 'label_cate', 'id'); + } + + /** + * 商品标签 + * @return \think\model\relation\HasMany + */ + public function productLabel() + { + return $this->hasMany(StoreProductLabel::class, 'label_cate', 'id'); + } + + /** + * 商品参数 + * @return \think\model\relation\HasMany + */ + public function specs() + { + return $this->hasMany(StoreProductSpecs::class, 'temp_id', 'id'); + } + + /** + * 获取子集分类查询条件 + * @return \think\model\relation\HasMany + */ + public function children() + { + return $this->hasMany(self::class, 'pid', 'id')->where('is_show', 1)->order('sort DESC,id DESC'); + } + + /**分类关联桌码 一对多 + * @return \think\model\relation\HasMany + */ + public function tableQrcode() + { + return $this->hasMany(TableQrcode::class, 'cate_id', 'id')->where('is_using', 1)->where('is_del', 0)->order('table_number ASC,id ASC'); + } + + /** + * ID搜索器 + * @param Model $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('id', $value); + } else { + if ($value) $query->where('id', $value); + } + } + + /** + * PID搜索器 + * @param Model $query + * @param $value + */ + public function searchPidAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('pid', $value); + } else { + if ($value !== '') $query->where('pid', $value); + } + } + + /** + * 归属人 + * @param Model $query + * @param $value + */ + public function searchOwnerIdAttr($query, $value) + { + $query->where('owner_id', $value); + } + + /** + * 商户搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value !== '') $query->where('type', $value); + } + } + + /** + * 关联门店ID、供应商ID搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value); + } else { + if ($value !== '') $query->where('relation_id', $value); + } + } + + /** + * 供应商 + * @param Model $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 2); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 2); + } + } + + /** + * 门店 + * @param Model $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 1); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 1); + } + } + + /** + * 标签类型0:用户1:客服话术 + * @param Model $query + * @param $value + */ + public function searchGroupAttr($query, $value) + { + if ($value !== '') $query->where('group', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchNotIdAttr($query, $value) + { + $query->where('id', '<>', $value); + } + + /** + * 分类是否显示搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsShowAttr($query, $value, $data) + { + if ($value !== '') $query->where('is_show', $value); + } +} diff --git a/app/model/other/Qrcode.php b/app/model/other/Qrcode.php new file mode 100644 index 0000000..f43c773 --- /dev/null +++ b/app/model/other/Qrcode.php @@ -0,0 +1,88 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\other; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * Class Qrcode + * @package app\model\other + */ +class Qrcode extends BaseModel +{ + + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'qrcode'; + + /** + * type 搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value != '') { + $query->whereLike('type', $value); + } + } + + /** + * status 搜索器 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value != '') { + $query->whereLike('status', $value); + } + } + + /** + * third_type 搜索器 + * @param Model $query + * @param $value + */ + public function searchThirdTypeAttr($query, $value) + { + if ($value != '') { + $query->whereLike('third_type', $value); + } + } + + /** + * third_id 搜索器 + * @param Model $query + * @param $value + */ + public function searchThirdIdAttr($query, $value) + { + if ($value != '') { + $query->whereLike('third_id', $value); + } + } + +} diff --git a/app/model/other/queue/Queue.php b/app/model/other/queue/Queue.php new file mode 100644 index 0000000..63d1048 --- /dev/null +++ b/app/model/other/queue/Queue.php @@ -0,0 +1,92 @@ + +// +---------------------------------------------------------------------- +namespace app\model\other\queue; + +use crmeb\traits\ModelTrait; +use crmeb\basic\BaseModel; +use think\Model; + +/** + * 队列model + * Class Queue + * @package app\model\other\queue + */ +class Queue extends BaseModel +{ + use ModelTrait; + + const EXPIRE = 0; + + /** + * 主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'queue_list'; + + /** + * 缓存KEY搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchExecuteKeyAttr($query, $value, $data) + { + if ($value) $query->where('execute_key', $value); + } + + /** + * 队列类型搜索器 + * @param $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value) $query->where('type', $value); + } + } + } + + /** + * 队列主键搜索器 + * @param $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if ($value) $query->where('id', $value); + } + + /** + * 队列状态搜索器 + * @param $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('status', $value); + } else { + $query->where('status', $value); + } + } + } +} diff --git a/app/model/other/queue/QueueAuxiliary.php b/app/model/other/queue/QueueAuxiliary.php new file mode 100644 index 0000000..7bbda60 --- /dev/null +++ b/app/model/other/queue/QueueAuxiliary.php @@ -0,0 +1,89 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\other\queue; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * 队列辅助表 + * Class QueueAuxiliary + * @package app\model\other\queue + */ +class QueueAuxiliary extends BaseModel +{ + + use ModelTrait; + + /** + * 表明 + * @var string + */ + protected $name = 'queue_auxiliary'; + + protected $insert = ['add_time']; + + protected $autoWriteTimestamp = false; + + /** + * 主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 类型搜索器 + * @param $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + $query->where('type', $value); + } + + /** + * 类型绑定id搜索器 + * @param $query + * @param $value + */ + public function searchBindingIdAttr($query, $value) + { + $query->where('binding_id', $value); + } + + /** + * 类型状态搜索器 + * @param $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('status', $value); + } else { + $query->where('status', $value); + } + } + } + + /** + * 类型关联id搜索器 + * @param $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + $query->where('relation_id', $value); + } +} diff --git a/app/model/product/branch/StoreBranchProductAttrValue.php b/app/model/product/branch/StoreBranchProductAttrValue.php new file mode 100644 index 0000000..52827b4 --- /dev/null +++ b/app/model/product/branch/StoreBranchProductAttrValue.php @@ -0,0 +1,77 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\branch; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * Class StoreBranchProductAttrValue + * @package app\model\product\branch + */ +class StoreBranchProductAttrValue extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_branch_product_attr_value'; + + /** + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') $query->where('store_id', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchProductIdAttr($query, $value) + { + $query->where('product_id', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + $query->where('type', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchUniqueAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('unique', $value); + } else { + $query->where('unique', $value); + } + } +} diff --git a/app/model/product/category/StoreProductCategory.php b/app/model/product/category/StoreProductCategory.php new file mode 100644 index 0000000..75e07ad --- /dev/null +++ b/app/model/product/category/StoreProductCategory.php @@ -0,0 +1,136 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\category; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 商品分类Model + * Class StoreProductCategory + * @package app\model\product\category + */ +class StoreProductCategory extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_category'; + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getAddTimeAttr($value) + { + return date('Y-m-d H:i:s', $value); + } + + /** + * 父级 + * @return \think\model\relation\HasOne + */ + public function parentCate() + { + return $this->hasOne(self::class, 'id', 'pid')->where('is_show', 1); + } + + /** + * 获取子集分类查询条件 + * @return \think\model\relation\HasMany + */ + public function children() + { + return $this->hasMany(self::class, 'pid', 'id')->where('is_show', 1)->order('sort DESC,id DESC'); + } + + /** + * ID搜索器 + * @param Model $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('id', $value); + } else { + if ($value) $query->where('id', $value); + } + } + + /** + * 分类是否显示搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsShowAttr($query, $value, $data) + { + if ($value !== '') $query->where('is_show', $value); + } + + /** + * 分类是否显示搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchPidAttr($query, $value, $data) + { + if ($value !== '') $query->where('pid', $value); + } + + /** + * 分类是否显示搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchCateNameAttr($query, $value, $data) + { + if ($value !== '') $query->where('cate_name', 'like', '%' . $value . '%'); + } + + /** + * 类型搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchTypeAttr($query, $value) + { + if ($value !== '') $query->where('type', $value); + } + + + /** + * 关联ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchRelationIdAttr($query, $value) + { + if ($value !== '') $query->where('relation_id', $value); + } +} diff --git a/app/model/product/ensure/StoreProductEnsure.php b/app/model/product/ensure/StoreProductEnsure.php new file mode 100644 index 0000000..50accea --- /dev/null +++ b/app/model/product/ensure/StoreProductEnsure.php @@ -0,0 +1,140 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\ensure; + +use app\common\model\store\product\Product as StoreProduct; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 商品保障服务 + * Class StoreProductEnsure + * @package app\model\product\label + */ +class StoreProductEnsure extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_ensure'; + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getAddTimeAttr($value) + { + return date('Y-m-d H:i:s', $value); + } + + /** + * @param Model $query + * @param $value + */ + public function searchNameAttr($query, $value) + { + $query->whereLike('id|name', '%' . $value . '%'); + } + + + /** + * 商户搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value !== '') $query->where('type', $value); + } + } + + /** + * 关联门店ID、供应商ID搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value); + } else { + if ($value !== '') $query->where('relation_id', $value); + } + } + + /** + * 供应商 + * @param Model $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 2); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 2); + } + } + + /** + * 门店 + * @param Model $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 1); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 1); + } + } + + /** + * status搜索器 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value !== '') $query->where('status', $value); + } + + /** + * @param $query + * @param $value + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/2 + */ + public function searchIdsAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('id', $value); + } + } + +} diff --git a/app/model/product/label/StoreProductLabelAuxiliary.php b/app/model/product/label/StoreProductLabelAuxiliary.php new file mode 100644 index 0000000..544bf5d --- /dev/null +++ b/app/model/product/label/StoreProductLabelAuxiliary.php @@ -0,0 +1,38 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\label; + + +use app\model\product\sku\StoreProductAttrValue; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * 标签辅助表 + * Class StoreProductLabelAuxiliary + * @package app\model\product\label + */ +class StoreProductLabelAuxiliary extends BaseModel +{ + use ModelTrait; + + /** + * @var string + */ + protected $key = 'id'; + + /** + * @var string + */ + protected $name = 'store_product_label_auxiliary'; + +} diff --git a/app/model/product/product/StoreDescription.php b/app/model/product/product/StoreDescription.php new file mode 100644 index 0000000..127203d --- /dev/null +++ b/app/model/product/product/StoreDescription.php @@ -0,0 +1,56 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\product; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * 商品详情Model + * Class StoreDescription + * @package app\model\product\product + */ +class StoreDescription extends BaseModel +{ + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_description'; + + use ModelTrait; + + public function getDescriptionAttr($value) + { + return htmlspecialchars_decode($value); + } + + /** + * 商品ID搜索器 + * @param $query + * @param $value + */ + public function searchProductIdAttr($query, $value) + { + if ($value) $query->where('product_id', $value); + } + + /** + * 类型搜索器 + * @param $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + $query->where('type', $value); + } +} diff --git a/app/model/product/product/StoreProductCategoryBrand.php b/app/model/product/product/StoreProductCategoryBrand.php new file mode 100644 index 0000000..1c324ba --- /dev/null +++ b/app/model/product/product/StoreProductCategoryBrand.php @@ -0,0 +1,105 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\product; + +use app\common\model\store\StoreBrand; +use app\model\product\category\StoreProductCategory; +use app\model\product\ensure\StoreProductEnsure; +use app\common\model\store\product\ProductLabel as StoreProductLabel; +use app\model\product\specs\StoreProductSpecs; +use app\common\model\user\UserLabel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 商品、分类、品牌 + * Class StoreProductCategoryBrand + * @package app\model\product\product + */ +class StoreProductCategoryBrand extends Model +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_category_brand'; + + + /** + * 商品ID搜索器 + * @param Model $query + * @param $value + */ + public function searchProductIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + } + + /** + * 分类ID搜索器 + * @param Model $query + * @param $value + */ + public function searchCateIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('cate_id', $value); + } else { + $query->where('cate_id', $value); + } + } + } + + /** + * 品牌ID搜索器 + * @param Model $query + * @param $value + */ + public function searchBrandIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('brand_id', $value); + } else { + $query->where('brand_id', $value); + } + } + } + + /** + * 状态搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchStatusAttr($query, $value, $data) + { + if ($value != '') $query->where('status', $value); + } + + +} diff --git a/app/model/product/product/StoreProductLog.php b/app/model/product/product/StoreProductLog.php new file mode 100644 index 0000000..bce2cb1 --- /dev/null +++ b/app/model/product/product/StoreProductLog.php @@ -0,0 +1,115 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\product; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; +use think\model\concern\SoftDelete; + +class StoreProductLog extends BaseModel +{ + use ModelTrait; + use SoftDelete; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_log'; + + protected $autoWriteTimestamp = 'int'; + + protected $createTime = 'add_time'; + + protected $deleteTime = 'delete_time'; + + /** + * 添加时间修改器 + * @return int + */ + public function setAddTimeAttr() + { + return time(); + } + + /** + * 一对一关联 + * 商品记录关联商品名称 + * @return \think\model\relation\HasOne + */ + public function storeName() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id')->bind([ + 'store_name', + 'image', + 'product_price'=>'price', + 'stock', + 'is_show' + ]); + } + + /** + * 分类搜索器 + * @param Model $query + * @param int $value + */ + public function searchCateIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('product_id', function ($query) use ($value) { + $query->name('store_product_relation')->where('type', 1)->where('relation_id', 'IN', $value)->field('product_id')->select(); + }); + } else { + $query->whereFindInSet('cate_id', $value); + } + } + } + + /** + * 记录类型搜索器 + * @param $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value != '') $query->where('type', $value); + } + + /** + * 商品ID搜索器 + * @param $query + * @param $value + */ + public function searchProductIdAttr($query, $value) + { + if ($value != '') $query->where('product_id', $value); + } + + /** + * 用户ID搜索器 + * @param $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value != '') $query->where('uid', $value); + } +} diff --git a/app/model/product/product/StoreProductRelation.php b/app/model/product/product/StoreProductRelation.php new file mode 100644 index 0000000..3169a7e --- /dev/null +++ b/app/model/product/product/StoreProductRelation.php @@ -0,0 +1,157 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\product; + +use app\common\model\store\StoreBrand; +use app\model\product\category\StoreProductCategory; +use app\model\product\ensure\StoreProductEnsure; +use app\common\model\store\product\ProductLabel as StoreProductLabel; +use app\model\product\specs\StoreProductSpecs; +use app\common\model\user\UserLabel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 商品关联模型 + * Class StoreProductRelation + * @package app\model\product\product + */ +class StoreProductRelation extends Model +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_relation'; + + /** + * 一对一关联获取分类名称 + * @return \think\model\relation\HasOne + */ + public function cateName() + { + return $this->hasOne(StoreProductCategory::class, 'id', 'relation_id')->bind([ + 'cate_name' => 'cate_name' + ]); + } + + /** + * 一对一关联获取分类名称 + * @return \think\model\relation\HasOne + */ + public function cate() + { + return $this->hasOne(StoreProductCategory::class, 'id', 'relation_id'); + } + + /** + * 品牌 + * @return \think\model\relation\HasOne + */ + public function brand() + { + return $this->hasOne(StoreBrand::class, 'id', 'relation_id'); + } + + /** + * 商品标签 + * @return \think\model\relation\HasOne + */ + public function productLabel() + { + return $this->hasOne(StoreProductLabel::class, 'id', 'relation_id'); + } + + /** + * 用户标签 + * @return \think\model\relation\HasOne + */ + public function userLabel() + { + return $this->hasOne(UserLabel::class, 'id', 'relation_id'); + } + + /** + * 用户保障服务 + * @return \think\model\relation\HasOne + */ + public function ensure() + { + return $this->hasOne(StoreProductEnsure::class, 'id', 'relation_id'); + } + + /** + * 用户保障服务 + * @return \think\model\relation\HasOne + */ + public function specs() + { + return $this->hasOne(StoreProductSpecs::class, 'id', 'relation_id'); + } + + /** + * 商品ID搜索器 + * @param Model $query + * @param $value + */ + public function searchProductIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + } + + /** + * 关联搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('relation_id', $value); + } else { + $query->where('relation_id', $value); + } + } + } + + /** + * 类型搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('type', $value); + } else { + $query->where('type', $value); + } + } + } + +} diff --git a/app/model/product/product/StoreProductReplyComment.php b/app/model/product/product/StoreProductReplyComment.php new file mode 100644 index 0000000..cc41381 --- /dev/null +++ b/app/model/product/product/StoreProductReplyComment.php @@ -0,0 +1,144 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\product; + + +use app\common\model\user\User; +use app\common\model\user\UserRelation; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\model\relation\HasOne; + +/** + * 商品评价回复表 + * Class StoreProductReplyComment + * @package app\model\product\product + */ +class StoreProductReplyComment extends BaseModel +{ + + use ModelTrait; + + /** + * @var string + */ + protected $name = 'store_product_reply_comment'; + + /** + * @var string + */ + protected $key = 'id'; + + /** + * @var bool + */ + protected $autoWriteTimestamp = false; + + /** + * @return HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid')->field(['uid', 'avatar', 'nickname']); + } + + /** + * @return HasOne + */ + public function children() + { + return $this->hasOne(self::class, 'pid', 'id'); + } + + /** + * 点赞关联 + * @return \think\model\relation\HasOne + */ + public function productRelation() + { + return $this->hasOne(UserRelation::class, 'relation_id', 'id'); + } + + /** + * 评论列表搜索 + * @param $query + * @param $value + */ + public function searchReplyIdAttr($query, $value) + { + $query->where('reply_id', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + $query->where('uid', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchNotUidAttr($query, $value) + { + $query->where('uid', '<>', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') $query->where('store_id', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchPidAttr($query, $value) + { + $query->where('pid', $value); + } + + /** + * 时间搜索 + * @param $query + * @param $value + */ + public function searchCreatetimeAttr($query, $value) + { + $this->searchTimeAttr($query, $value, ['timeKey' => 'create_time']); + } + + /** + * @param $value + * @return false|string + */ + public function getCreateTimeAttr($value) + { + return date('Y-m-d H:i:s', $value); + } + + /** + * @param $value + * @return false|string + */ + public function getUpdateTimeAttr($value) + { + return date('Y-m-d H:i:s', $value); + } +} diff --git a/app/model/product/product/StoreProductStockRecord.php b/app/model/product/product/StoreProductStockRecord.php new file mode 100644 index 0000000..1eee409 --- /dev/null +++ b/app/model/product/product/StoreProductStockRecord.php @@ -0,0 +1,106 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\product; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * 库存记录 + * Class StoreProductStockRecord + * @package app\model\product\product + */ +class StoreProductStockRecord extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_stock_record'; + + protected $autoWriteTimestamp = 'int'; + + protected $createTime = 'add_time'; + + /** + * 添加时间修改器 + * @return int + */ + public function setAddTimeAttr() + { + return time(); + } + + /** + * 一对一关联 + * 商品记录关联商品名称 + * @return \think\model\relation\HasOne + */ + public function storeName() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id')->bind([ + 'store_name', + 'image', + 'product_price' => 'price' + ]); + } + + /** + * 门店ID搜索器 + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') $query->where('store_id', $value); + } + + + /** + * 商品ID搜索器 + * @param $query + * @param $value + */ + public function searchProductIdAttr($query, $value) + { + if ($value != '') $query->where('product_id', $value); + } + + /** + * unique搜索器 + * @param $query + * @param $value + */ + public function searchUniqueAttr($query, $value) + { + if ($value != '') $query->where('unique', $value); + } + + /** + * pm搜索器 + * @param $query + * @param $value + */ + public function searchPmAttr($query, $value) + { + if ($value != '') $query->where('pm', $value); + } +} diff --git a/app/model/product/product/StoreVisit.php b/app/model/product/product/StoreVisit.php new file mode 100644 index 0000000..e762acc --- /dev/null +++ b/app/model/product/product/StoreVisit.php @@ -0,0 +1,37 @@ + +// +---------------------------------------------------------------------- +namespace app\model\product\product; + +use crmeb\traits\ModelTrait; +use crmeb\basic\BaseModel; + +/** + * 商品浏览分析Model + * Class StoreVisit + * @package app\model\product\product + */ +class StoreVisit extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_visit'; + +} diff --git a/app/model/product/shipping/ShippingTemplates.php b/app/model/product/shipping/ShippingTemplates.php new file mode 100644 index 0000000..3eaf0bf --- /dev/null +++ b/app/model/product/shipping/ShippingTemplates.php @@ -0,0 +1,177 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\shipping; + +use crmeb\traits\ModelTrait; +use crmeb\basic\BaseModel; +use think\Model; + +/** + * 运费模板Model + * Class ShippingTemplates + * @package app\model\product\shipping + */ +class ShippingTemplates extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'shipping_templates'; + + /** + * 是否开启包邮获取器 + * @param $value + * @return string + */ + public function getAppointAttr($value) + { + $status = [1 => '开启', 0 => '关闭']; + return $status[$value]; + } + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + public function getAddTimeAttr($value) + { + $value = date('Y-m-d H:i:s', $value); + return $value; + } + + /** + * 运费地区一对多关联 + * @return \think\model\relation\HasMany + */ + public function region() + { + return $this->hasMany(ShippingTemplatesRegion::class, 'temp_id', 'id'); + } + + /** + * 包邮地区一对多关联 + * @return \think\model\relation\HasMany + */ + public function free() + { + return $this->hasMany(ShippingTemplatesFree::class, 'temp_id', 'id'); + } + + /** + * ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('id', $value); + } else { + $query->where('id', $value); + } + } + + /** + * 模板名称搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchNameAttr($query, $value) + { + if ($value) { + $query->where('name', 'like', '%' . $value . '%'); + } + } + + /** + * 计费方式搜索器 + * @param Model $query + * @param $value + */ + public function searchGroupAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('group', $value); + } else { + if ($value !== '') $query->where('group', $value); + } + } + + /** + * 商户搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value !== '') $query->where('type', $value); + } + } + + /** + * 关联门店ID、供应商ID搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value); + } else { + if ($value !== '') $query->where('relation_id', $value); + } + } + + /** + * 供应商 + * @param Model $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 2); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 2); + } + } + + /** + * 门店 + * @param Model $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 1); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 1); + } + } + +} diff --git a/app/model/product/shipping/ShippingTemplatesFree.php b/app/model/product/shipping/ShippingTemplatesFree.php new file mode 100644 index 0000000..e9cb5ad --- /dev/null +++ b/app/model/product/shipping/ShippingTemplatesFree.php @@ -0,0 +1,73 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\shipping; + +use crmeb\traits\ModelTrait; +use crmeb\basic\BaseModel; +use think\Model; + +/** + * 包邮Model + * Class ShippingTemplatesFree + * @package app\model\product\shipping + */ +class ShippingTemplatesFree extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'shipping_templates_free'; + + /** + * 城市ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchCityIdAttr($query, $value) + { + $query->where('city_id', $value); + } + + /** + * 模板id搜索 + * @param Model $query + * @param $value + */ + public function searchTempIdAttr($query, $value) + { + $query->where('temp_id', $value); + } + + /** + * uniqid 搜索器 + * @param Model $query + * @param $value + */ + public function searchUniqidAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('uniqid', $value); + } else { + $query->where('uniqid', $value); + } + } +} diff --git a/app/model/product/shipping/ShippingTemplatesNoDelivery.php b/app/model/product/shipping/ShippingTemplatesNoDelivery.php new file mode 100644 index 0000000..bfa0f63 --- /dev/null +++ b/app/model/product/shipping/ShippingTemplatesNoDelivery.php @@ -0,0 +1,84 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\shipping; + +use crmeb\traits\ModelTrait; +use crmeb\basic\BaseModel; +use think\Model; + +/** + * 不送达Model + * Class ShippingTemplatesNoDelivery + * @package app\model\product\shipping + */ +class ShippingTemplatesNoDelivery extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'shipping_templates_no_delivery'; + + /** + * 省ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProvinceIdAttr($query, $value) + { + $query->where('province_id', $value); + } + + /** + * 城市ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchCityIdAttr($query, $value) + { + $query->where('city_id', $value); + } + + /** + * 模板id搜索 + * @param Model $query + * @param $value + */ + public function searchTempIdAttr($query, $value) + { + $query->where('temp_id', $value); + } + + /** + * uniqid 搜索器 + * @param Model $query + * @param $value + */ + public function searchUniqidAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('uniqid', $value); + } else { + $query->where('uniqid', $value); + } + } +} diff --git a/app/model/product/shipping/ShippingTemplatesRegion.php b/app/model/product/shipping/ShippingTemplatesRegion.php new file mode 100644 index 0000000..b4edd29 --- /dev/null +++ b/app/model/product/shipping/ShippingTemplatesRegion.php @@ -0,0 +1,72 @@ + +// +---------------------------------------------------------------------- +namespace app\model\product\shipping; + +use crmeb\traits\ModelTrait; +use crmeb\basic\BaseModel; +use think\Model; + +/** + * 地区运费Model + * Class ShippingTemplatesRegion + * @package app\model\product\shipping + */ +class ShippingTemplatesRegion extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'shipping_templates_region'; + + /** + * 城市ID搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchCityIdAttr($query, $value) + { + $query->where('city_id', $value); + } + + /** + * uniqid 搜索器 + * @param Model $query + * @param $value + */ + public function searchUniqidAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('uniqid', $value); + } else { + $query->where('uniqid', $value); + } + } + + /** + * 模板id搜索 + * @param Model $query + * @param $value + */ + public function searchTempIdAttr($query, $value) + { + $query->where('temp_id', $value); + } +} diff --git a/app/model/product/shipping/StoreProductAttrResult.php b/app/model/product/shipping/StoreProductAttrResult.php new file mode 100644 index 0000000..bdc4efc --- /dev/null +++ b/app/model/product/shipping/StoreProductAttrResult.php @@ -0,0 +1,81 @@ + +// +---------------------------------------------------------------------- +namespace app\model\product\sku; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * Class StoreProductAttrResult + * @package app\common\model\product + */ +class StoreProductAttrResult extends BaseModel +{ + + use ModelTrait; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_attr_result'; + + protected $insert = ['change_time']; + + /** + * 自动增加改变时间 + * @param $value + * @return int + */ + protected static function setChangeTimeAttr($value) + { + return time(); + } + + /** + * 数据json化 + * @param $value + * @return false|string + */ + protected static function setResultAttr($value) + { + return is_array($value) ? json_encode($value) : $value; + } + + /** + * 商品搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProductIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + } + + /** + * 商品类型搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchTypeAttr($query, $value) + { + $query->where('type', $value); + } +} diff --git a/app/model/product/sku/StoreProductAttrValue.php b/app/model/product/sku/StoreProductAttrValue.php new file mode 100644 index 0000000..7a7a11c --- /dev/null +++ b/app/model/product/sku/StoreProductAttrValue.php @@ -0,0 +1,146 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\sku; + +use app\model\activity\integral\StoreIntegral; +use app\model\product\branch\StoreBranchProductAttrValue as ProductAttrValue; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; +use app\common\model\store\product\Product as StoreProduct; + +/** + * Class StoreProductAttrValue + * @package app\common\model\product + */ +class StoreProductAttrValue extends BaseModel +{ + use ModelTrait; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_attr_value'; + + protected $insert = ['unique']; + + /** + * sku 字段写入 + * @param $value + * @return string + */ + public function setSukAttr($value) + { + return is_array($value) ? implode(',', $value) : $value; + } + + /** + * Unique字段写入 + * @param $value + * @param $data + * @return mixed + */ + public function setUniqueAttr($value, $data) + { + if (is_array($data['suk'])) { + $data['suk'] = $this->setSukAttr($data['suk']); + } + return $data['unique'] ?: substr(md5($data['product_id'] . $data['suk'] . uniqid(true)), 12, 8); + } + + /** + * 门店规格表 + * @return \think\model\relation\HasOne + */ + public function storeBranch() + { + return $this->hasOne(ProductAttrValue::class, 'unique', 'unique')->bind([ + 'branch_sales' => 'sales', + 'branch_stock' => 'stock' + ]);; + } + + /** + * 关联积分商城表 + * @return \think\model\relation\HasOne + */ + public function storeIntegral() + { + return $this->hasOne(StoreIntegral::class, 'id', 'product_id')->where('is_show', 1)->where('is_del', 0); + } + + + /** + * 商品搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchProductIdAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('product_id', $value); + } else { + $query->where('product_id', $value); + } + } + + /** + * 商品类型搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchTypeAttr($query, $value) + { + $query->where('type', $value); + } + + /** + * 商品属性名称搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchSukAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('suk', $value); + } else { + if ($value !== '') $query->where('suk', $value); + } + } + + /** + * 规格唯一值搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchUniqueAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('unique', $value); + } else { + if ($value) { + $query->where('unique', $value); + } + } + } + + public function product() + { + return $this->hasOne(StoreProduct::class, 'id', 'product_id')->field('store_name,id')->bind(['store_name']); + } + +} diff --git a/app/model/product/sku/StoreProductRule.php b/app/model/product/sku/StoreProductRule.php new file mode 100644 index 0000000..25f1fd7 --- /dev/null +++ b/app/model/product/sku/StoreProductRule.php @@ -0,0 +1,98 @@ + +// +---------------------------------------------------------------------- +namespace app\model\product\sku; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 商品规则 + * Class StoreProductRule + * @package app\common\model\product + */ +class StoreProductRule extends BaseModel +{ + use ModelTrait; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_rule'; + + /** + * 商户搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value !== '') $query->where('type', $value); + } + } + + /** + * 关联门店ID、供应商ID搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value); + } else { + if ($value !== '') $query->where('relation_id', $value); + } + } + + /** + * 供应商 + * @param Model $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 2); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 2); + } + } + + /** + * 门店 + * @param Model $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 1); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 1); + } + } + + /** + * 属性模板名称搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchRuleNameAttr($query, $value) + { + $query->where('rule_name', 'like', '%' . $value . '%'); + } +} diff --git a/app/model/product/sku/StoreProductVirtual.php b/app/model/product/sku/StoreProductVirtual.php new file mode 100644 index 0000000..a889cc7 --- /dev/null +++ b/app/model/product/sku/StoreProductVirtual.php @@ -0,0 +1,85 @@ +where('card_no', $value); + } + + /** + * 卡密搜索器 + * @param $query + * @param $value + */ + public function searchCardPwdAttr($query, $value) + { + $query->where('card_pwd', $value); + } + + /** + * 商品搜索器 + * @param $query + * @param $value + */ + public function searchProductIdAttr($query, $value) + { + $query->where('product_id', $value); + } + + /** + * 用户搜索器 + * @param $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + $query->where('uid', $value); + } + + /** + * 订单搜索器 + * @param $query + * @param $value + */ + public function searchOrderIdAttr($query, $value) + { + $query->where('order_id', $value); + } + + /** + * 唯一值搜索器 + * @param $query + * @param $value + */ + public function searchAttrUniqueAttr($query, $value) + { + $query->where('attr_unique', $value); + } +} diff --git a/app/model/product/specs/StoreProductSpecs.php b/app/model/product/specs/StoreProductSpecs.php new file mode 100644 index 0000000..d847c8c --- /dev/null +++ b/app/model/product/specs/StoreProductSpecs.php @@ -0,0 +1,153 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\product\specs; + +use app\common\model\store\product\Product as StoreProduct; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 商品参数 + * Class StoreProductSpecs + * @package app\model\product\specs + */ +class StoreProductSpecs extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_product_specs'; + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + protected function getAddTimeAttr($value) + { + return date('Y-m-d H:i:s', $value); + } + + /** + * id搜索器 + * @param Model $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('id', $value); + } else { + if ($value !== '') $query->where('id', $value); + } + } + + /** + * @param Model $query + * @param $value + */ + public function searchNameAttr($query, $value) + { + $query->whereLike('name', '%' . $value . '%'); + } + + /** + * temp_id搜索器 + * @param Model $query + * @param $value + */ + public function searchTempIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('temp_id', $value); + } else { + if ($value !== '') $query->where('temp_id', $value); + } + } + + /** + * status搜索器 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value !== '') $query->where('status', $value); + } + + /** + * 商户搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value !== '') $query->where('type', $value); + } + } + + /** + * 关联门店ID、供应商ID搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value); + } else { + if ($value !== '') $query->where('relation_id', $value); + } + } + + /** + * 供应商 + * @param Model $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 2); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 2); + } + } + + /** + * 门店 + * @param Model $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 1); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 1); + } + } + +} diff --git a/app/model/store/DeliveryService.php b/app/model/store/DeliveryService.php new file mode 100644 index 0000000..b8a4fc8 --- /dev/null +++ b/app/model/store/DeliveryService.php @@ -0,0 +1,176 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\store; + + +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 配送员 + * Class DeliveryService + * @package app\model\store + */ +class DeliveryService extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'delivery_service'; + + /** + * @var bool + */ + protected $updateTime = false; + + + protected function getAddTimeAttr($value) + { + if ($value) return date('Y-m-d H:i:s', $value); + return $value; + } + + /** + * 用户名一对多关联 + * @return mixed + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid')->field(['uid', 'nickname'])->bind([ + 'nickname' => 'nickname' + ]); + } + + /** + * uid搜索器 + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + $query->where('uid', $value); + } + + /** + * 商户搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value !== '') $query->where('type', $value); + } + } + + /** + * 关联门店ID、供应商ID搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value); + } else { + if ($value !== '') $query->where('relation_id', $value); + } + } + + /** + * 供应商 + * @param Model $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 2); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 2); + } + } + + /** + * 门店 + * @param Model $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 1); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 1); + } + } + + /** + * status搜索器 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + $query->where('status', $value); + } + + /** + * customer + * @param Model $query + * @param $value + */ + public function searchCustomerAttr($query, $value) + { + $query->where('customer', $value); + } + + /** + * 用户昵称搜索器 + * @param Model $query + * @param $value + */ + public function searchNicknameAttr($query, $value) + { + $query->whereLike('nickname', '%' . $value . '%'); + } + + public function searchPhoneAttr($query, $value) + { + $query->where('phone', $value); + } + + /** + * 是否删除 + * @param $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') { + $query->where('is_del', $value); + } + } +} diff --git a/app/model/store/StoreConfig.php b/app/model/store/StoreConfig.php new file mode 100644 index 0000000..fa707b3 --- /dev/null +++ b/app/model/store/StoreConfig.php @@ -0,0 +1,109 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\store; + +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 门店配置 + * Class StoreConfig + * @package app\model\store + */ +class StoreConfig extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_config'; + + /** + * 商户搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value !== '') $query->where('type', $value); + } + } + + /** + * 关联门店ID、供应商ID搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value); + } else { + if ($value !== '') $query->where('relation_id', $value); + } + } + + /** + * 供应商 + * @param Model $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 2); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 2); + } + } + + /** + * 门店 + * @param Model $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 1); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 1); + } + } + + /** + * 关键字 + * @param $query + * @param $value + */ + public function searchKeyNameAttr($query, $value) + { + if ($value !== '') { + $query->where('key_name', $value); + } + } + + +} diff --git a/app/model/store/StoreUser.php b/app/model/store/StoreUser.php new file mode 100644 index 0000000..c8a6048 --- /dev/null +++ b/app/model/store/StoreUser.php @@ -0,0 +1,106 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\store; + +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 门店用户 + * Class StoreUser + * @package app\model\store + */ +class StoreUser extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_user'; + + /** + * user用户表一对一关联 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid'); + } + + /** + * 门店id搜索器 + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value && $value != -1) { + $query->where('store_id', $value); + } + } + + /** + * 关联标签修改器 + * @param $value + * @return false|string + */ + protected function setLabelIdAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * 关联标签获取器 + * @param $value + * @param $data + * @return mixed + */ + protected function getLabelIdAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + + /** + * uid搜索器 + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('uid', $value); + } else { + if ($value) { + $query->where('uid', $value); + } + } + } + + +} diff --git a/app/model/store/SystemStore.php b/app/model/store/SystemStore.php new file mode 100644 index 0000000..428af95 --- /dev/null +++ b/app/model/store/SystemStore.php @@ -0,0 +1,165 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\store; + +use app\model\other\Category; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 门店列表 + * Class SystemStore + * @package app\model\store + */ +class SystemStore extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'system_store'; + + /** + * @return \think\model\relation\HasOne + */ + public function categoryName() + { + return $this->hasOne(Category::class, 'id', 'cate_id')->where('group', 5)->field('id,pid,name')->bind([ + 'cate_name' => 'name' + ]); + } + + /** + * @return \think\model\relation\HasOne + */ + public function category() + { + return $this->hasOne(Category::class, 'id', 'cate_id')->where('group', 5); + } + + /** + * 经纬度获取器 + * @param $value + * @param $data + * @return string + */ + public static function getLatlngAttr($value, $data) + { + return $data['latitude'] . ',' . $data['longitude']; + } + + /** + * id + * @param Model $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) + $query->whereIn('id', $value); + else + $query->where('id', $value); + } + } + + /** + * cate_id + * @param Model $query + * @param $value + */ + public function searchCateIdAttr($query, $value) + { + if ($value) { + if (is_array($value)) + $query->whereIn('cate_id', $value); + else + $query->where('cate_id', $value); + } + } + + /** + * 店铺类型搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value !== '') { + switch ((int)$value) { + case 1://营业中 + case 0://休息中 + $query->where(['is_del' => 0, 'is_show' => 1]); + break; + case -1://已停业 + $query->where(['is_del' => 0, 'is_show' => 0]); + break; + default: + $query->where(['is_del' => 0]); + break; + } + } + } + + /** + * is_show搜索器 + * @param $query + * @param $value + */ + public function searchIsShowAttr($query, $value) + { + if ($value !== '') $query->where('is_show', $value); + } + + /** + * is_del搜索器 + * @param $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } + + /** + * is_store搜索器 + * @param $query + * @param $value + */ + public function searchIsStoreAttr($query, $value) + { + if ($value !== '') $query->where('is_store', $value); + } + + /** + * 手机号,id,昵称搜索器 + * @param Model $query + * @param $value + */ + public function searchKeywordsAttr($query, $value) + { + if ($value != '') { + $query->where('id|name|introduction|phone|detailed_address|address', 'LIKE', "%$value%"); + } + } + + +} diff --git a/app/model/store/SystemStoreStaff.php b/app/model/store/SystemStoreStaff.php new file mode 100644 index 0000000..14b2ab2 --- /dev/null +++ b/app/model/store/SystemStoreStaff.php @@ -0,0 +1,238 @@ + +// +---------------------------------------------------------------------- +namespace app\model\store; + +use app\common\model\user\User; +use app\model\work\WorkMember; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 店员模型 + * Class SystemStoreStaff + * @package app\model\store + */ +class SystemStoreStaff extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + /** + * 模型名称 + * @var string + */ + protected $name = 'system_store_staff'; + /** + * @var string[] + */ + protected $hidden = [ + 'last_ip' + ]; + + /** + * @return \think\model\relation\HasOne + */ + public function workMember() + { + return $this->hasOne(WorkMember::class, 'uid', 'uid'); + } + + /** + * 规则修改器 + * @param $value + * @return string + */ + public function setRolesAttr($value) + { + if ($value) { + return is_array($value) ? implode(',', $value) : $value; + } + return ''; + } + + /** + * @param $value + * @return array|false|string[] + */ + public function getRolesAttr($value) + { + if ($value) { + return is_string($value) ? explode(',', $value) : $value; + } + return []; + } + + /** + * user用户表一对一关联 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid', false)->field(['uid', 'nickname', 'delete_time'])->bind([ + 'nickname' => 'nickname', + 'delete_time' => 'delete_time', + ]); + } + + /** + * 门店表一对一关联 + * @return \think\model\relation\HasOne + */ + public function store() + { + return $this->hasOne(SystemStore::class, 'id', 'store_id')->field(['id', 'name'])->bind([ + 'name' => 'name' + ]); + } + + /** + * 时间戳获取器转日期 + * @param $value + * @return false|string + */ + public static function getAddTimeAttr($value) + { + return date('Y-m-d H:i:s', $value); + } + + public function searchAccountAttr($query, $value) + { + if ($value) $query->where('account', $value); + } + + public function searchPhoneAttr($query, $value) + { + if ($value) $query->where('phone', $value); + } + + /** + * 权限规格状态搜索器 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value != '') { + $query->where('status', $value); + } + } + + /** + * 权限等级搜索器 + * @param Model $query + * @param $value + */ + public function searchLevelAttr($query, $value) + { + if (is_array($value)) { + $query->where('level', $value[0], $value[1]); + } else { + $query->where('level', $value); + } + } + + /** + * 是否有核销权限搜索器 + * @param Model $query + * @param $value 用户uid + */ + public function searchIsStatusAttr($query, $value) + { + $query->where(['uid' => $value, 'status' => 1, 'verify_status' => 1]); + } + + /** + * uid搜索器 + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + $query->where('uid', $value); + } + + /** + * 门店id搜索器 + * @param Model $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') { + $query->where('store_id', $value); + } + } + + /** + * 角色 + * @param $query + * @param $value + */ + public function searchRolesAttr($query, $value) + { + if ($value) { + $query->where('find_in_set(' . $value . ',`roles`)'); + } + } + + /** + * 是否是管理员 + * @param $query + * @param $value + */ + public function searchIsAdminAttr($query, $value) + { + if ($value !== '') { + $query->where('is_admin', $value); + } + } + + /** + * 是否是店员 + * @param $query + * @param $value + */ + public function searchIsStoreAttr($query, $value) + { + if ($value !== '') { + $query->where('is_store', $value); + } + } + + /** + * 是否删除 + * @param $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') { + $query->where('is_del', $value); + } + } + + /** + * 是否接收通知 + * @param $query + * @param $value + */ + public function searchNotifyAttr($query, $value) + { + if ($value !== '') { + $query->where('notify', $value); + } + } +} diff --git a/app/model/store/finance/StoreFinanceFlow.php b/app/model/store/finance/StoreFinanceFlow.php new file mode 100644 index 0000000..d3095ad --- /dev/null +++ b/app/model/store/finance/StoreFinanceFlow.php @@ -0,0 +1,219 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\store\finance; + +use app\model\store\SystemStore; +use app\model\store\SystemStoreStaff; +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 流水 + * Class StoreFinanceFlow + * @package app\model\store\finance + */ +class StoreFinanceFlow extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'store_finance_flow'; + + /** + * 一对一关联用户表 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid')->field(['uid', 'nickname'])->bind([ + 'user_nickname' => 'nickname', + ]); + } + + /** + * 一对一关联店员 + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function systemStoreStaff() + { + return $this->hasOne(SystemStoreStaff::class, 'id', 'staff_id')->field(['id', 'staff_name'])->bind([ + 'staff_name' => 'staff_name' + ]); + } + + /** + * 一对一关联店员 + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function systemStore() + { + return $this->hasOne(SystemStore::class, 'id', 'store_id'); + } + + /** + * id搜索器 + * @param $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('id', $value); + } else { + $query->where('id', $value); + } + } + + /** + * 门店id搜索器 + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') { + $query->where('store_id', $value); + } + } + + /** + * 用户id + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value) $query->where('uid', $value); + } + + /** + * 用户id + * @param Model $query + * @param $value + */ + public function searchTradeTypeAttr($query, $value) + { + if ($value) $query->where('trade_type', $value); + } + + /** + * 排除type + * @param Model $query + * @param $value + */ + public function searchNoTypeAttr($query, $value) + { + if ($value) $query->where('type', '<>', $value); + } + + /** + * 店员id + * @param Model $query + * @param $value + */ + public function searchStaffIdAttr($query, $value) + { + if ($value) { + if ($value == -1) {//所有店员 + $query->where('staff_id', '>', 0); + } else { + $query->where('staff_id', $value); + } + } + } + + /** + * 交易单号 + * @param Model $query + * @param $value + */ + public function searchOrderIdAttr($query, $value) + { + if ($value !== '') { + $query->where('order_id', 'LIKE', "%$value%"); + } + } + + /** + * 关联订单号 + * @param Model $query + * @param $value + */ + public function searchLinkIdAttr($query, $value) + { + if ($value !== '') $query->where('link_id', $value); + } + + /** + * 支出获取 + * @param Model $query + * @param $value + */ + public function searchPmAttr($query, $value) + { + if ($value !== '') $query->where('pm', $value); + } + + /** + * 类型 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->where('type', 'in', $value); + } else { + $query->where('type', $value); + } + } + } + + /** + * 支付类型 + * @param Model $query + * @param $value + */ + public function searchPayTypeAttr($query, $value) + { + if ($value !== '') $query->where('pay_type', $value); + } + + /** + * 删除 + * @param Model $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } + +} diff --git a/app/model/supplier/SupplierTicketPrint.php b/app/model/supplier/SupplierTicketPrint.php new file mode 100644 index 0000000..be21c8d --- /dev/null +++ b/app/model/supplier/SupplierTicketPrint.php @@ -0,0 +1,39 @@ + +// +---------------------------------------------------------------------- +namespace app\model\supplier; + +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 小票模型模型 + * Class SystemSupplier + * @package app\model\store + */ +class SupplierTicketPrint extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + /** + * 模型名称 + * @var string + */ + protected $name = 'supplier_ticket_print'; + + +} diff --git a/app/model/supplier/SystemSupplier.php b/app/model/supplier/SystemSupplier.php new file mode 100644 index 0000000..8e407d4 --- /dev/null +++ b/app/model/supplier/SystemSupplier.php @@ -0,0 +1,75 @@ + +// +---------------------------------------------------------------------- +namespace app\model\supplier; + +use app\common\model\system\admin\Admin; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 供应商模型 + * Class SystemSupplier + * @package app\model\store + */ +class SystemSupplier extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'system_supplier'; + + /** + * 管理员 + * @return \think\model\relation\HasOne + */ + public function admin() + { + return $this->hasOne(Admin::class, 'id', 'admin_id')->field(['id', 'account', 'pwd', 'admin_type', 'is_del', 'level', 'roles'])->bind([ + 'account', + 'pwd', + 'admin_type', + 'level', + 'roles', + 'admin_is_del' => 'is_del', + ]); + } + /** + * 手机号,id,昵称搜索器 + * @param Model $query + * @param $value + */ + public function searchKeywordsAttr($query, $value) + { + if ($value != '') { + $query->where('supplier_name', 'LIKE', "%$value%"); + } + } + + /** + * 是否删除搜索器 + * @param Model $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + $query->where('is_del', $value ?? 0); + } +} diff --git a/app/model/supplier/finance/SupplierExtract.php b/app/model/supplier/finance/SupplierExtract.php new file mode 100644 index 0000000..d989dbf --- /dev/null +++ b/app/model/supplier/finance/SupplierExtract.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\supplier\finance; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use app\model\supplier\SystemSupplier; +use think\Model; + +/** + * 门店列表 + * Class SystemStore + * @package app\model\store + */ +class SupplierExtract extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + /** + * 模型名称 + * @var string + */ + protected $name = 'supplier_extract'; + + + + /** + * 状态 + * @var string[] + */ + protected static $status = [ + -1 => '未通过', + 0 => '审核中', + 1 => '已提现' + ]; + + /** + * 供应商一对一关联 + * @return \think\model\relation\HasOne + */ + public function supplier() + { + return $this->hasOne(SystemSupplier::class, 'id', 'supplier_id')->hidden(['bank_code,bank_address', 'alipay_account', 'alipay_qrcode_url', 'wechat', 'wechat_qrcode_url']); + } + + /** + * 门店id搜索器 + * @param $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if ($value !== '') { + $query->where('supplier_id', $value); + } + } + + /** + * 提现方式 + * @param Model $query + * @param $value + */ + public function searchExtractTypeAttr($query, $value) + { + if ($value != '') $query->where('extract_type', $value); + } + + /** + * 审核状态 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value !== '') { + $query->where('status', $value); + } + } + + /** + * 转账状态 + * @param Model $query + * @param $value + */ + public function searchPayStatusAttr($query, $value) + { + if ($value !== '') { + $query->where('pay_status', $value); + } + } + + /** + * 状态驳回 + * @param Model $query + * @param $value + */ + public function searchNotStatusAttr($query, $value) + { + if ($value !== '') { + $query->where('status', '<>', $value); + } + } + +} diff --git a/app/model/supplier/finance/SupplierFlowingWater.php b/app/model/supplier/finance/SupplierFlowingWater.php new file mode 100644 index 0000000..e947957 --- /dev/null +++ b/app/model/supplier/finance/SupplierFlowingWater.php @@ -0,0 +1,177 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\supplier\finance; + +use app\model\supplier\SystemSupplier; +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 流水 + * Class SupplierFlowingWater + * @package app\model\supplier\finance + */ +class SupplierFlowingWater extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + /** + * 模型名称 + * @var string + */ + protected $name = 'supplier_flowing_water'; + + + + /** + * 一对一关联用户表 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid')->field(['uid', 'nickname'])->bind([ + 'user_nickname' => 'nickname', + ]); + } + + /** + * 一对一关联供应商表 + * @return \think\model\relation\HasOne + */ + public function supplier() + { + return $this->hasOne(SystemSupplier::class, 'id', 'supplier_id'); + } + + + /** + * id搜索器 + * @param $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('id', $value); + } else { + $query->where('id', $value); + } + } + + /** + * 供应商id搜索器 + * @param $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if ($value !== '') { + $query->where('supplier_id', $value); + } + } + + /** + * 用户id + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value) $query->where('uid', $value); + } + + /** + * 排除type + * @param Model $query + * @param $value + */ + public function searchNoTypeAttr($query, $value) + { + if ($value) $query->where('type', '<>', $value); + } + + /** + * 交易单号 + * @param Model $query + * @param $value + */ + public function searchOrderIdAttr($query, $value) + { + if ($value !== '') { + $query->where('order_id', 'LIKE', "%$value%"); + } + } + + /** + * 关联订单号 + * @param Model $query + * @param $value + */ + public function searchLinkIdAttr($query, $value) + { + if ($value !== '') $query->where('link_id', $value); + } + + /** + * 支出获取 + * @param Model $query + * @param $value + */ + public function searchPmAttr($query, $value) + { + if ($value !== '') $query->where('pm', $value); + } + + /** + * 类型 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->where('type', 'in', $value); + } else { + $query->where('type', $value); + } + } + } + + /** + * 支付类型 + * @param Model $query + * @param $value + */ + public function searchPayTypeAttr($query, $value) + { + if ($value !== '') $query->where('pay_type', $value); + } + + /** + * 删除 + * @param Model $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } + +} diff --git a/app/model/supplier/finance/SupplierTransactions.php b/app/model/supplier/finance/SupplierTransactions.php new file mode 100644 index 0000000..ab10c1a --- /dev/null +++ b/app/model/supplier/finance/SupplierTransactions.php @@ -0,0 +1,163 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\supplier\finance; + +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 交易 + * Class SupplierTransactions + * @package app\model\store\finance + */ +class SupplierTransactions extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + /** + * 模型名称 + * @var string + */ + protected $name = 'supplier_transactions'; + + /** + * 一对一关联用户表 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid')->field(['uid', 'nickname'])->bind([ + 'user_nickname' => 'nickname', + ]); + } + /** + * id搜索器 + * @param $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('id', $value); + } else { + $query->where('id', $value); + } + } + + /** + * 供应商id搜索器 + * @param $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if ($value !== '') { + $query->where('supplier_id', $value); + } + } + + /** + * 用户id + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value) $query->where('uid', $value); + } + + /** + * 排除type + * @param Model $query + * @param $value + */ + public function searchNoTypeAttr($query, $value) + { + if ($value) $query->where('type', '<>', $value); + } + + /** + * 交易单号 + * @param Model $query + * @param $value + */ + public function searchOrderIdAttr($query, $value) + { + if ($value !== '') { + $query->where('order_id', 'LIKE', "%$value%"); + } + } + + /** + * 关联订单号 + * @param Model $query + * @param $value + */ + public function searchLinkIdAttr($query, $value) + { + if ($value !== '') $query->where('link_id', $value); + } + + /** + * 支出获取 + * @param Model $query + * @param $value + */ + public function searchPmAttr($query, $value) + { + if ($value !== '') $query->where('pm', $value); + } + + /** + * 类型 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value) { + if (is_array($value)) { + $query->where('type', 'in', $value); + } else { + $query->where('type', $value); + } + } + } + + /** + * 支付类型 + * @param Model $query + * @param $value + */ + public function searchPayTypeAttr($query, $value) + { + if ($value !== '') $query->where('pay_type', $value); + } + + /** + * 删除 + * @param Model $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } + +} diff --git a/app/model/system/CapitalFlow.php b/app/model/system/CapitalFlow.php new file mode 100644 index 0000000..70431c9 --- /dev/null +++ b/app/model/system/CapitalFlow.php @@ -0,0 +1,91 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\system; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +class CapitalFlow extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'capital_flow'; + + /** + * 交易类型搜索器 + * @param $query + * @param $value + */ + public function searchTradingTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('trading_type', $value); + } else { + if ($value) $query->where('trading_type', $value); + } + } + + /** + * 门店搜索器 + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') $query->where('store_id', $value); + } + + /** + * 关键字搜索器 + * @param $query + * @param $value + */ + public function searchKeywordsAttr($query, $value) + { + if ($value !== '') $query->where('order_id|uid|nickname|phone', 'like', '%' . $value . '%'); + } + + /** + * 批量id搜索器 + * @param $query + * @param $value + */ + public function searchIdsAttr($query, $value) + { + if ($value != '') $query->whereIn('id', $value); + } + + /** + * UID搜索器 + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('uid', $value); + } else { + if ($value !== '') $query->where('uid', $value); + } + } +} diff --git a/app/model/system/SystemUserApply.php b/app/model/system/SystemUserApply.php new file mode 100644 index 0000000..5d630ea --- /dev/null +++ b/app/model/system/SystemUserApply.php @@ -0,0 +1,165 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\system; + +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 用户申请 + * Class SystemUserApply + * @package app\model\system\admin + */ +class SystemUserApply extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'system_user_apply'; + + /** + * 多图 + * @param $value + * @return string + */ + public function setImagesAttr($value) + { + if ($value) { + return is_array($value) ? json_encode($value) : $value; + } + return ''; + } + + /** + * 多图获取器 + * @param $value + * @return array|mixed + */ + public function getImagesAttr($value) + { + return is_string($value) ? json_decode($value, true) : []; + } + + /** + * 关联user + * @return model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid'); + } + + + /** + * id搜索器 + * @param Model $query + * @param $value + */ + public function searchIdAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('id', $value); + } else { + $query->where('id', $value); + } + } + + /** + * 关键词搜索器 + * @param $query Model + * @param $value + */ + public function searchKeywordAttr($query, $value) + { + if ($value !== '') $query->where('id|uid|name|phone|system_name|fail_msg|mark', 'like', '%' . $value . '%'); + } + + /** + * uid搜索器 + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('uid', $value); + } else { + if($value !== '') $query->where('uid', $value); + } + } + + /** + * 商户搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value !== '') $query->where('type', $value); + } + } + + /** + * 关联门店ID、供应商ID搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value); + } else { + if ($value !== '') $query->where('relation_id', $value); + } + } + + + + /** + * 权限规格状态搜索器 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value != '') { + $query->where('status', $value); + } + } + + /** + * 是否删除搜索器 + * @param Model $query + * @param $value + * @param $data + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } + + +} diff --git a/app/model/system/form/SystemForm.php b/app/model/system/form/SystemForm.php new file mode 100644 index 0000000..d959e52 --- /dev/null +++ b/app/model/system/form/SystemForm.php @@ -0,0 +1,113 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\system\form; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + + +class SystemForm extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'system_form'; + + protected $updateTime = false; + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + public function getAddTimeAttr($value) + { + return date('Y-m-d H:i:s', $value); + } + + /** + * 修改时间获取器 + * @param $value + * @return false|string + */ + public function getUpdateTimeAttr($value) + { + return date('Y-m-d H:i:s', $value); + } + + /** + * 类型搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value != '') { + if (is_array($value)) { + $query->whereIn('type', $value); + } else { + $query->where('type', $value); + } + } + } + + /** + * 版本号搜索器 + * @param Model $query + * @param $value + */ + public function searchVersionAttr($query, $value) + { + if ($value != '') $query->where('version', $value); + } + + /** + * 是否使用搜索器 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value != '') $query->where('status', $value); + } + + /** + * 名称搜索器 + * @param Model $query + * @param $value + */ + public function searchNameAttr($query, $value) + { + if ($value != '') $query->where('name', $value); + } + + /** + * 是否删除搜索器 + * @param Model $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } +} diff --git a/app/model/user/UserCard.php b/app/model/user/UserCard.php new file mode 100644 index 0000000..e8f6e50 --- /dev/null +++ b/app/model/user/UserCard.php @@ -0,0 +1,141 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * 用户领取卡券 + * Class UserCard + * @package app\model\user + */ +class UserCard extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'user_card'; + + /** + * 用户 + * @return \think\model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid')->field('uid,nickname,avatar')->bind([ + 'nickname' => 'nickname', + 'avatar' => 'avatar' + ]); + } + + /** + * uid + * @param $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value !== '') $query->where('uid', $value); + } + + /** + * wechat_card_id + * @param $query + * @param $value + */ + public function searchWechatCardIdAttr($query, $value) + { + if (in_array($value)) { + $query->whereIn('wechat_card_id', $value); + } else { + if ($value !== '') $query->where('wechat_card_id', $value); + } + } + + /** + * card_id + * @param $query + * @param $value + */ + public function searchCardIdAttr($query, $value) + { + if ($value) $query->where('card_id', $value); + } + + /** + * 门店ID + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') { + if ($value == -1) {//所有门店 + $query->where('store_id', '>', 0); + } else { + $query->where('store_id', $value); + } + } + } + + /** + * 门店店员ID + * @param $query + * @param $value + */ + public function searchStaffIdAttr($query, $value) + { + if ($value) $query->where('staff_id', $value); + } + + /** + * openid + * @param $query + * @param $value + */ + public function searchOpenidAttr($query, $value) + { + if ($value) $query->where('openid', $value); + } + + /** + * is_submit + * @param $query + * @param $value + */ + public function searchIsSubmitAttr($query, $value) + { + if ($value !== '') $query->where('is_submit', $value); + } + + /** + * is_del + * @param $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } + + +} diff --git a/app/model/user/UserFriends.php b/app/model/user/UserFriends.php new file mode 100644 index 0000000..12376e2 --- /dev/null +++ b/app/model/user/UserFriends.php @@ -0,0 +1,76 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +class UserFriends extends BaseModel +{ + + use ModelTrait; + + /** + * 表明 + * @var string + */ + protected $name = 'user_friends'; + + /** + * 主键 + * @var string + */ + protected $pk = 'id'; + + /** + * + * @return \think\model\relation\HasOne + */ + public function level() + { + return $this->hasOne(User::class, 'uid', 'uid')->field(['uid', 'level'])->bind([ + 'level' => 'level' + ]); + } + + /** + * @return \think\model\relation\HasOne + */ + public function nickname() + { + return $this->hasOne(User::class, 'uid', 'uid')->field(['uid', 'nickname'])->bind([ + 'nickname' => 'nickname' + ]); + } + + /** + * uid搜索器 + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + $query->where('uid', $value); + } + + /** + * 修改添加时间 + * @param $value + * @return false|string + */ + public function getAddTimeAttr($value) + { + return date('Y-m-d H:i:s', $value); + } +} diff --git a/app/model/user/UserInvoice.php b/app/model/user/UserInvoice.php new file mode 100644 index 0000000..8e13f56 --- /dev/null +++ b/app/model/user/UserInvoice.php @@ -0,0 +1,99 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * Class UserInvoice + * @package app\model\live + */ +class UserInvoice extends BaseModel +{ + use ModelTrait; + + protected $pk = 'id'; + + protected $name = 'user_invoice'; + + protected $autoWriteTimestamp = 'int'; + + protected $createTime = 'add_time'; + + protected function setAddTimeAttr() + { + return time(); + } + + /** + * 添加时间获取器 + * @param $value + * @return false|string + */ + public function getAddTimeAttr($value) + { + if (!empty($value)) { + return date('Y-m-d H:i:s', (int)$value); + } + return ''; + } + + + /** + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value !== '') $query->where('uid', $value); + } + + /** + * @param Model $query + * @param $value + */ + public function searchHeaderTypeAttr($query, $value) + { + if ($value !== '') $query->where('header_type', $value); + } + + /** + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if ($value !== '') $query->where('type', $value); + } + + /** + * @param Model $query + * @param $value + */ + public function searchIsDefaultAttr($query, $value) + { + if ($value !== '') $query->whereLike('is_default', $value); + } + + /** + * @param Model $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } + +} diff --git a/app/model/user/UserMoney.php b/app/model/user/UserMoney.php new file mode 100644 index 0000000..97b1286 --- /dev/null +++ b/app/model/user/UserMoney.php @@ -0,0 +1,167 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use app\common\model\store\order\StoreOrder; +use think\model; + +/** + * 用户余额 + * Class UserMoney + * @package app\model\user + */ +class UserMoney extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'user_money'; + + /** + * 关联订单表 + * @return UserBill|model\relation\HasOne + */ + public function order() + { + return $this->hasOne(StoreOrder::class, 'id', 'link_id')->field(['id', 'total_num'])->bind(['total_num']); + } + + /** + * 关联用户 + * @return model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid'); + } + + /** + * 用户uid + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if ($value !== '') { + if (is_array($value)) + $query->whereIn('uid', $value); + else + $query->where('uid', $value); + } + } + + /** + * 关联id + * @param Model $query + * @param $value + */ + public function searchLinkIdAttr($query, $value) + { + if (is_array($value)) + $query->whereIn('link_id', $value); + else + $query->where('link_id', $value); + } + + /** + * 支出|获得 + * @param Model $query + * @param $value + */ + public function searchPmAttr($query, $value) + { + if ($value !== '') $query->where('pm', $value); + } + + /** + * 类型 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) + $query->whereIn('type', $value); + else + $query->where('type', $value); + } + + /** + * @param Model $query + * @param $value + */ + public function searchNotTypeAttr($query, $value) + { + if (is_array($value)) + $query->whereNotIn('type', $value); + else + $query->where('type', '<>', $value); + } + + /** + * 状态 0:带确定 1:有效 -1:无效 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + $query->where('status', $value); + } + + /** + * 是否收货 0:未收货 1:已收货 + * @param Model $query + * @param $value + */ + public function searchTakeAttr($query, $value) + { + $query->where('take', $value); + } + + /** + * 模糊搜索 + * @param Model $query + * @param $value + */ + public function searchLikeAttr($query, $value) + { + $query->where(function ($query) use ($value) { + $query->where('uid|title', 'like', "%$value%")->whereOr('uid', 'in', function ($query) use ($value) { + $query->name('user')->whereLike('uid|account|nickname|phone', '%' . $value . '%')->field('uid')->select(); + }); + }); + } + + /** + * 时间 + * @param Model $query + * @param $value + */ + public function searchAddTimeAttr($query, $value) + { + if (is_string($value)) $query->whereTime($query, $value); + if (is_array($value) && count($value) == 2) $query->whereTime('add_time', 'between', $value); + } + +} diff --git a/app/model/user/UserSearch.php b/app/model/user/UserSearch.php new file mode 100644 index 0000000..9055b34 --- /dev/null +++ b/app/model/user/UserSearch.php @@ -0,0 +1,63 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * Class UserSearch + * @package app\model\user + */ +class UserSearch extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'user_search'; + + + /** + * 获取搜索结果 + * @param $value + * @return array|mixed + */ + public function getResultAttr($value) + { + return json_decode($value, true) ?? []; + } + + public function searchUidAttr($query, $value) + { + if ($value !== '') $query->where('uid', $value); + } + + public function searchKeywordAttr($query, $value) + { + if ($value !== '') $query->where('keyword', $value); + } + + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } + +} diff --git a/app/model/user/UserSpread.php b/app/model/user/UserSpread.php new file mode 100644 index 0000000..d7e6969 --- /dev/null +++ b/app/model/user/UserSpread.php @@ -0,0 +1,122 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user; + + +use app\common\model\system\admin\Admin as SystemAdmin; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\model; + +/** + * Class UserSpread + * @package app\model\user + */ +class UserSpread extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'user_spread'; + + /** + * 用户 + * @return model\relation\HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid')->field('uid,nickname,avatar')->bind([ + 'nickname' => 'nickname', + 'avatar' => 'avatar' + ]); + } + + /** + * 推荐人 + * @return model\relation\HasOne + */ + public function spreadUser() + { + return $this->hasOne(User::class, 'uid', 'spread_uid')->field('uid,nickname,avatar')->bind([ + 'nickname' => 'nickname', + 'avatar' => 'avatar' + ]); + } + + /** + * 管理员姓名 + * @return model\relation\HasOne + */ + public function admin() + { + return $this->hasOne(SystemAdmin::class, 'id', 'admin_id')->field('id,real_name')->bind([ + 'real_name' => 'real_name' + ]); + } + + /** + * 用户uid + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if (is_array($value)) + $query->whereIn('uid', $value); + else + $query->where('uid', $value); + + } + + /** + * 门店ID + * @param $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if ($value !== '') $query->where('store_id', $value); + } + + /** + * 门店店员ID + * @param $query + * @param $value + */ + public function searchStaffIdAttr($query, $value) + { + if ($value) $query->where('staff_id', $value); + } + + /** + * 推广人uid + * @param Model $query + * @param $value + */ + public function searchSpreadUidAttr($query, $value) + { + if (is_array($value)) + $query->whereIn('spread_uid', $value); + else + $query->where('spread_uid', $value); + + } +} diff --git a/app/model/user/label/UserLabelRelation.php b/app/model/user/label/UserLabelRelation.php new file mode 100644 index 0000000..c27f17f --- /dev/null +++ b/app/model/user/label/UserLabelRelation.php @@ -0,0 +1,108 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user\label; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * 用户关联标签 + * Class UserLabelRelation + * @package app\model\user\label + */ +class UserLabelRelation extends BaseModel +{ + use ModelTrait; + + /** + * 模型名称 + * @var string + */ + protected $name = 'user_label_relation'; + + /** + * @return \think\model\relation\HasOne + */ + public function label() + { + return $this->hasOne(UserLabel::class, 'id', 'label_id')->bind([ + 'label_name' => 'label_name' + ]); + } + + /** + * uid搜索器 + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + $query->whereIn('uid', $value); + } + + /** + * 商户搜索器 + * @param Model $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('type', $value); + } else { + if ($value !== '') $query->where('type', $value); + } + } + + /** + * 关联门店ID、供应商ID搜索器 + * @param Model $query + * @param $value + */ + public function searchRelationIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value); + } else { + if ($value !== '') $query->where('relation_id', $value); + } + } + + /** + * 供应商 + * @param Model $query + * @param $value + */ + public function searchSupplierIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 2); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 2); + } + } + + /** + * 门店 + * @param Model $query + * @param $value + */ + public function searchStoreIdAttr($query, $value) + { + if (is_array($value)) { + if ($value) $query->whereIn('relation_id', $value)->where('type', 1); + } else { + if ($value !== '') $query->where('relation_id', $value)->where('type', 1); + } + } +} diff --git a/app/model/user/level/UserLevel.php b/app/model/user/level/UserLevel.php new file mode 100644 index 0000000..99bc494 --- /dev/null +++ b/app/model/user/level/UserLevel.php @@ -0,0 +1,103 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user\level; + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\model; + +/** + * 用户等级 + * Class UserLevel + * @package app\model\user\level + */ +class UserLevel extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'user_level'; + + public function levelInfo() + { + return $this->hasOne(SystemUserLevel::class, 'id', 'level_id')->where('is_del', 0); + } + + /** + * 用户uid + * @param Model $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + $query->where('uid', $value); + } + + /** + * 是否永久 + * @param Model $query + * @param $value + */ + public function searchIsForeverAttr($query, $value) + { + $query->where('is_forever', $value); + } + + /** + * 过期时间 + * @param Model $query + * @param $value + */ + public function searchValidTimeAttr($query, $value) + { + $query->where('valid_time', '>', $value); + } + + /** + * 状态 + * @param Model $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + $query->where('status', $value); + } + + /** + * 是否通知 + * @param Model $query + * @param $value + */ + public function searchRemindAttr($query, $value) + { + $query->where('remind', $value); + } + + /** + * 是否删除 + * @param Model $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + $query->where('is_del', $value); + } +} diff --git a/app/model/user/member/MemberCard.php b/app/model/user/member/MemberCard.php new file mode 100644 index 0000000..2d2af09 --- /dev/null +++ b/app/model/user/member/MemberCard.php @@ -0,0 +1,119 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user\member; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * Class MemberCard + * @package app\model\user\member + */ +class MemberCard extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'member_card'; + + protected $insert = ['add_time', 'update_time']; + + protected $hidden = ['update_time']; + + protected $updateTime = false; + + /** + * 卡号搜索器 + * @param Model $query + * @param $value + */ + public function searchCardNumberAttr($query, $value) + { + if ($value) { + $query->whereLike('card_number', '%' . $value . '%'); + } + + } + + /** + * 用户uid搜索器 + * @param Model $query + * @param $value + */ + public function searchUseUidAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('use_uid', $value); + } else { + $query->where('use_uid', $value); + } + } + + /** + * 手机号搜索器 + * @param Model $query + * @param $value + */ + public function searchPhoneAttr($query, $value) + { + if ($value) { + $query->whereIn('use_uid', function ($query) use ($value) { + $query->name('user')->whereLike('phone', $value . '%')->field('uid')->select(); + }); + } + } + + /** + * 批次id搜索器 + * @param Model $query + * @param $value + */ + public function searchBatchCardIdAttr($query, $value) + { + $query->where('card_batch_id', $value); + } + + /** + * 用户use_time搜索器 + * @param Model $query + * @param $value + */ + public function searchUseTimeAttr($query, $value) + { + if ($value > 0) { + $query->where('use_time', '>', 0); + } + if ($value == 0) { + $query->where('use_time', 0); + } + + } + + public function searchIsStatusAttr($query, $value) + { + if ($value) { + $query->where('status', $value); + } + + } +} diff --git a/app/model/user/member/MemberRight.php b/app/model/user/member/MemberRight.php new file mode 100644 index 0000000..411a414 --- /dev/null +++ b/app/model/user/member/MemberRight.php @@ -0,0 +1,61 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user\member; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * Class MemberRight + * @package app\model\user\member + */ +class MemberRight extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'member_right'; + + /** + * 状态搜索器 + * @param $query + * @param $value + */ + public function searchStatusAttr($query, $value) + { + if ($value) { + $query->where('status', $value); + } + } + + /** + * 状态搜索器 + * @param $query + * @param $value + */ + public function searchRightTypeAttr($query, $value) + { + if ($value) { + $query->where('right_type', $value); + } + } +} diff --git a/app/model/user/member/MemberShip.php b/app/model/user/member/MemberShip.php new file mode 100644 index 0000000..bf7c6d2 --- /dev/null +++ b/app/model/user/member/MemberShip.php @@ -0,0 +1,47 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\user\member; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\Model; + +/** + * Class MemberShip + * @package app\model\user\member + */ +class MemberShip extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'member_ship'; + + /** + * @param Model $query + * @param $value + */ + public function searchIsDelAttr($query, $value) + { + if ($value !== '') $query->where('is_del', $value); + } +} diff --git a/app/model/wechat/WechatCard.php b/app/model/wechat/WechatCard.php new file mode 100644 index 0000000..a0bb9f3 --- /dev/null +++ b/app/model/wechat/WechatCard.php @@ -0,0 +1,62 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\wechat; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * 关键词model + * Class WechatKey + * @package app\model\wechat + */ +class WechatCard extends BaseModel +{ + use ModelTrait; + + /** + * 数据表主键 + * @var string + */ + protected $pk = 'id'; + + /** + * 模型名称 + * @var string + */ + protected $name = 'wechat_card'; + + /** + * 特别信息修改器 + * @param $value + * @return false|string + */ + protected function setEspecialAttr($value) + { + if ($value) { + return is_array($value) ? json_encode($value) : $value; + } + return ''; + } + + /** + * 特别信息获取器 + * @param $value + * @param $data + * @return mixed + */ + protected function getEspecialAttr($value) + { + return $value ? json_decode($value, true) : []; + } +} diff --git a/app/model/work/WorkClient.php b/app/model/work/WorkClient.php new file mode 100644 index 0000000..6fba3ad --- /dev/null +++ b/app/model/work/WorkClient.php @@ -0,0 +1,127 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\work; + + +use app\common\model\user\User; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\model\concern\SoftDelete; +use think\model\relation\HasMany; +use think\model\relation\HasManyThrough; +use think\model\relation\HasOne; + +/** + * 企业微信客户 + * Class WorkClient + * @package app\model\work + */ +class WorkClient extends BaseModel +{ + + use ModelTrait, SoftDelete; + + /** + * @var string + */ + protected $name = 'work_client'; + + /** + * @var string + */ + protected $key = 'id'; + + /** + * @var string + */ + protected $autoWriteTimestamp = 'int'; + + /** + * @return HasMany + */ + public function follow() + { + return $this->hasMany(WorkClientFollow::class, 'client_id', 'id'); + } + + /** + * @return HasOne + */ + public function followOne() + { + return $this->hasOne(WorkClientFollow::class, 'client_id', 'id')->order('createtime', 'asc'); + } + + /** + * 商城用户关联 + * @return HasOne + */ + public function user() + { + return $this->hasOne(User::class, 'uid', 'uid'); + } + + public function chat() + { + return $this->hasOne(WorkGroupChatMember::class, 'userid', 'external_userid')->where('type', 2); + } + + /** + * @return HasManyThrough + */ + public function tags() + { + return $this->hasManyThrough( + WorkClientFollowTags::class, + WorkClientFollow::class, + 'follow_id', + 'id', + 'client_id', + 'id' + ); + } + + /** + * @param $query + * @param $value + */ + public function searchExternalUseridAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('external_userid', $value); + } else { + $query->where('external_userid', $value); + } + } + + /** + * @param $query + * @param $value + */ + public function searchCorpIdAttr($query, $value) + { + $query->where('corp_id', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchUidAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('uid', $value); + } else { + $query->where('uid', $value); + } + } +} diff --git a/app/model/work/WorkGroupChatAuth.php b/app/model/work/WorkGroupChatAuth.php new file mode 100644 index 0000000..0346185 --- /dev/null +++ b/app/model/work/WorkGroupChatAuth.php @@ -0,0 +1,137 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\work; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\model\concern\SoftDelete; + +/** + * 企业微信自动拉群 + * Class WorkGroupChatAuth + * @package app\model\work + */ +class WorkGroupChatAuth extends BaseModel +{ + + use ModelTrait, SoftDelete; + + /** + * @var string + */ + protected $name = 'work_group_chat_auth'; + + /** + * @var string + */ + protected $key = 'id'; + + /** + * @var string + */ + protected $autoWriteTimestamp = 'int'; + + /** + * @param $value + * @return false|string + */ + public function setWelcomeWordsAttr($value) + { + return json_encode($value); + } + + /** + * @param $value + * @return mixed + */ + public function getWelcomeWordsAttr($value) + { + return json_decode($value, true); + } + + /** + * @param $value + * @return false|string + */ + public function setAdminUserAttr($value) + { + return json_encode($value); + } + + /** + * @param $value + * @return mixed + */ + public function getAdminUserAttr($value) + { + return json_decode($value, true); + } + + /** + * @param $value + * @return false|string + */ + public function setChatIdAttr($value) + { + return json_encode($value); + } + + /** + * @param $value + * @return mixed + */ + public function getChatIdAttr($value) + { + return json_decode($value, true); + } + + /** + * @param $value + * @return false|string + */ + public function setLabelAttr($value) + { + return json_encode($value); + } + + /** + * @param $value + * @return mixed + */ + public function getLabelAttr($value) + { + return json_decode($value, true); + } + + /** + * @param $query + * @param $value + */ + public function searchNameAttr($query, $value) + { + if ('' !== $value) { + $query->whereLike('name', '%' . $value . '%'); + } + } + + /** + * @param $query + * @param $value + */ + public function searchCreateTimeAttr($query, $value) + { + if ('' !== $value) { + $this->searchTimeAttr($query, $value, ['timeKey' => 'create_time']); + } + } +} diff --git a/app/model/work/WorkMedia.php b/app/model/work/WorkMedia.php new file mode 100644 index 0000000..61764e0 --- /dev/null +++ b/app/model/work/WorkMedia.php @@ -0,0 +1,60 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\work; + + +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; + +/** + * Class WorkMedia + * @package app\model\work + */ +class WorkMedia extends BaseModel +{ + + use ModelTrait; + + /** + * @var string + */ + protected $name = 'work_media'; + + /** + * @var string + */ + protected $key = 'id'; + + /** + * @var string + */ + protected $autoWriteTimestamp = 'int'; + + /** + * @param $query + * @param $value + */ + public function searchMd5PathAttr($query, $value) + { + $query->where('md5_path', $value); + } + + /** + * @param $query + * @param $value + */ + public function searchTypeAttr($query, $value) + { + $query->where('type', $value); + } + +} diff --git a/app/model/work/WorkMember.php b/app/model/work/WorkMember.php new file mode 100644 index 0000000..e3674eb --- /dev/null +++ b/app/model/work/WorkMember.php @@ -0,0 +1,140 @@ + +// +---------------------------------------------------------------------- + +namespace app\model\work; + + +use app\common\model\user\UserLabel; +use app\model\user\label\UserLabelRelation; +use crmeb\basic\BaseModel; +use crmeb\traits\ModelTrait; +use think\model\relation\HasManyThrough; +use think\model\relation\HasOne; + +/** + * 企业微信成员 + * Class WorkMember + * @package app\model\work + */ +class WorkMember extends BaseModel +{ + + use ModelTrait; + + /** + * @var string + */ + protected $name = 'work_member'; + + /** + * @var string + */ + protected $key = 'id'; + + /** + * @var string + */ + protected $autoWriteTimestamp = 'int'; + + public function department() + { + return $this->hasManyThrough(WorkDepartment::class, WorkMemberRelation::class, 'member_id', 'department_id', 'id', 'department'); + } + + /** + * 主部门 + * @return HasOne + */ + public function mastareDepartment() + { + return $this->hasOne(WorkDepartment::class, 'department_id', 'main_department')->field(['department_id', 'name'])->bind(['mastare_department_name' => 'name']); + } + + /** + * @return \think\model\relation\HasMany + */ + public function departmentRelation() + { + return $this->hasMany(WorkMemberRelation::class, 'member_id', 'id'); + } + + /** + * @return HasOne + */ + public function chat() + { + return $this->hasOne(WorkGroupChatMember::class, 'userid', 'userid')->where('type', 1); + } + + /** + * @return HasOne + */ + public function clientFollow() + { + return $this->hasOne(WorkClientFollow::class, 'userid', 'userid'); + } + + /** + * @return HasManyThrough + */ + public function tags() + { + return $this->hasManyThrough( + UserLabel::class, + UserLabelRelation::class, + 'phone', + 'id', + 'mobile', + 'id' + ); + } + + /** + * @param $value + * @return false|string + */ + public function setDirectLeaderAttr($value) + { + return is_string($value) ? $value : json_encode($value); + } + + /** + * @param $value + * @return mixed + */ + public function getDirectLeaderAttr($value) + { + return json_decode($value, true); + } + + /** + * @param $query + * @param $value + */ + public function searchUseridAttr($query, $value) + { + if (is_array($value)) { + $query->whereIn('userid', $value); + } else { + $query->where('userid', $value); + } + } + + /** + * 企业id查询 + * @param $query + * @param $value + */ + public function searchCorpIdAttr($query, $value) + { + $query->where('corp_id', $value); + } +} diff --git a/app/services/BaseServices.php b/app/services/BaseServices.php new file mode 100644 index 0000000..878e693 --- /dev/null +++ b/app/services/BaseServices.php @@ -0,0 +1,424 @@ + +// +---------------------------------------------------------------------- + +namespace app\services; + +use crmeb\utils\JwtAuth; +use think\facade\Db; +use think\facade\Config; +use think\facade\Route as Url; + +/** + * Class BaseServices + * @package app\services + */ +abstract class BaseServices +{ + /** + * 模型注入 + * @var object + */ + protected $dao; + + /** + * 获取分页配置 + * @param bool $isPage + * @param bool $isRelieve + * @return int[] + */ + public function getPageValue(bool $isPage = true, bool $isRelieve = true) + { + $page = $limit = 0; + if ($isPage) { + $page = app()->request->param(Config::get('database.page.pageKey', 'page') . '/d', 0); + $limit = app()->request->param(Config::get('database.page.limitKey', 'limit') . '/d', 0); + } + $limitMax = Config::get('database.page.limitMax'); + $defaultLimit = Config::get('database.page.defaultLimit', 10); + if ($limit > $limitMax && $isRelieve) { + $limit = $limitMax; + } + return [(int)$page, (int)$limit, (int)$defaultLimit]; + } + + /** + * 数据库事务操作 + * @param callable $closure + * @return mixed + */ + public function transaction(callable $closure, bool $isTran = true) + { + return $isTran ? Db::transaction($closure) : $closure(); + } + + /** + * 创建token + * @param int $id + * @param $type + * @return array + */ + public function createToken(int $id, $type, string $pwd = '') + { + /** @var JwtAuth $jwtAuth */ + $jwtAuth = app()->make(JwtAuth::class); + return $jwtAuth->createToken($id, $type, ['auth' => md5($pwd)]); + } + + /** + * 获取路由地址 + * @param string $path + * @param array $params + * @param bool $suffix + * @param bool $isDomain + * @return string + */ + public function url(string $path, array $params = [], bool $suffix = false, bool $isDomain = false) + { + return Url::buildUrl($path, $params)->suffix($suffix)->domain($isDomain)->build(); + } + + /** + * 密码hash加密 + * @param string $password + * @return false|string|null + */ + public function passwordHash(string $password) + { + return password_hash($password, PASSWORD_BCRYPT); + } + + /** + * 格式化时间 + * @param $time + * @param bool $is_time_key + * @return array + */ + public function timeHandle($time, bool $is_time_key = false) + { + switch ($time) { + case 'today': + $start = date('Y-m-d 00:00:00'); + $end = date('Y-m-d 23:59:59'); + break; + case 'yesterday': + $start = date('Y-m-d 00:00:00', strtotime("-1 day")); + $end = date('Y-m-d 23:59:59', strtotime("-1 day")); + break; + case 'sevenday': + $start = date('Y-m-d 00:00:00', strtotime('-6 day')); + $end = date('Y-m-d 23:59:59'); + break; + case 'thirtyday': + $start = date('Y-m-d 00:00:00', strtotime('-29 day')); + $end = date('Y-m-d 23:59:59'); + break; + case 'last month': + $start = date('Y-m-01 00:00:00', strtotime('first Day of last month 00:00:00')); + $end = date("Y-m-d 23:59:59", strtotime('first Day of this month 00:00:00 -1second')); + break; + case 'month': + $start = date('Y-m-01 00:00:00'); + $end = date('Y-m-d 23:59:59', mktime(23, 59, 59, date('m'), date('t'), date('Y'))); + break; + case 'year': + $start = date('Y-01-01 00:00:00'); + $end = date('Y-12-31 23:59:59'); + break; + default: + $start = date("Y/m/d", strtotime("-30 days", time())); + $end = date("Y/m/d", time()); + if (strstr($time, '-') !== false) { + [$start_r, $end_r] = explode('-', $time); + if ($start_r || $end_r) { + $start = $start_r; + $end = $end_r; + $end_time = strtotime($end_r); + //没选具体时分秒 加上86400 + if ($end_time == strtotime(date('Y/m/d', $end_time))) { + $end = date('Y/m/d H:i:s', $end_time + 86399); + } + } + } + break; + } + $start = $start ? strtotime($start) : 0; + $end = $end ? strtotime($end) : 0; + if ($is_time_key) { + $dayCount = ceil(($end - $start) / 86400); + $s_start = $start; + $timeKey = []; + if ($dayCount == 1) { + $timeType = 'hour'; + $timeKey = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']; + } elseif ($dayCount <= 31) { + $timeType = 'day'; + for ($i = 0; $i < $dayCount; $i++) { + $timeKey[] = date('Y-m-d', $s_start); + $s_start = strtotime("+1 day", $s_start); + } + } elseif ($dayCount <= 92) { + $timeType = 'weekly'; + for ($i = 0; $i < $dayCount; $i = $i + 7) { + $timeKey[] = '第' . date('W', $s_start) . '周'; + $s_start = strtotime("+1 week", $s_start); + } + } else { + $timeType = 'year'; + while ($s_start <= $end) { + $timeKey[] = date('Y-m', $s_start); + $s_start = strtotime("+1 month", $s_start); + } + } + return [$start, $end, $timeType, $timeKey]; + } + return [$start, $end]; + } + + /** + * 计算环比增长率 + * @param $nowValue + * @param $lastValue + * @return float|int|string + */ + public function countRate($nowValue, $lastValue) + { + if ($lastValue == 0 && $nowValue == 0) return 0; + if ($lastValue == 0) return round(bcmul(bcdiv($nowValue, 1, 4), 100, 2), 2); + if ($nowValue == 0) return -round(bcmul(bcdiv($lastValue, 1, 4), 100, 2), 2); + return bcmul(bcdiv((bcsub($nowValue, $lastValue, 2)), $lastValue, 4), 100, 2); + } + + /** + * tree处理 分类、标签数据(这一类数据) + * @param array $cate + * @param array $label + * @return array + */ + public function get_tree_children(array $cate, array $label) + { + if ($cate) { + foreach ($cate as $key => $value) { + if ($label) { + foreach ($label as $k => $item) { + if ($value['id'] == $item['label_cate']) { + $cate[$key]['children'][] = $item; + unset($label[$k]); + } + } + } else { + $cate[$key]['children'] = []; + } + } + } + return $cate; + } + + /** + * ip转城市 + * @param $ip + * @return array|string|string[]|null + */ + public function convertIp($ip) + { + try { + $ip1num = 0; + $ip2num = 0; + $ipAddr1 = ""; + $ipAddr2 = ""; + $dat_path = public_path() . 'statics/ip.dat'; + if (!preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/", $ip)) { + return ''; + } + if (!$fd = @fopen($dat_path, 'rb')) { + return ''; + } + $ip = explode('.', $ip); + $ipNum = $ip[0] * 16777216 + $ip[1] * 65536 + $ip[2] * 256 + $ip[3]; + $DataBegin = fread($fd, 4); + $DataEnd = fread($fd, 4); + $ipbegin = implode('', unpack('L', $DataBegin)); + if ($ipbegin < 0) $ipbegin += pow(2, 32); + $ipend = implode('', unpack('L', $DataEnd)); + if ($ipend < 0) $ipend += pow(2, 32); + $ipAllNum = ($ipend - $ipbegin) / 7 + 1; + $BeginNum = 0; + $EndNum = $ipAllNum; + while ($ip1num > $ipNum || $ip2num < $ipNum) { + $Middle = intval(($EndNum + $BeginNum) / 2); + fseek($fd, $ipbegin + 7 * $Middle); + $ipData1 = fread($fd, 4); + if (strlen($ipData1) < 4) { + fclose($fd); + return ''; + } + $ip1num = implode('', unpack('L', $ipData1)); + if ($ip1num < 0) $ip1num += pow(2, 32); + + if ($ip1num > $ipNum) { + $EndNum = $Middle; + continue; + } + $DataSeek = fread($fd, 3); + if (strlen($DataSeek) < 3) { + fclose($fd); + return ''; + } + $DataSeek = implode('', unpack('L', $DataSeek . chr(0))); + fseek($fd, $DataSeek); + $ipData2 = fread($fd, 4); + if (strlen($ipData2) < 4) { + fclose($fd); + return ''; + } + $ip2num = implode('', unpack('L', $ipData2)); + if ($ip2num < 0) $ip2num += pow(2, 32); + if ($ip2num < $ipNum) { + if ($Middle == $BeginNum) { + fclose($fd); + return ''; + } + $BeginNum = $Middle; + } + } + $ipFlag = fread($fd, 1); + if ($ipFlag == chr(1)) { + $ipSeek = fread($fd, 3); + if (strlen($ipSeek) < 3) { + fclose($fd); + return ''; + } + $ipSeek = implode('', unpack('L', $ipSeek . chr(0))); + fseek($fd, $ipSeek); + $ipFlag = fread($fd, 1); + } + if ($ipFlag == chr(2)) { + $AddrSeek = fread($fd, 3); + if (strlen($AddrSeek) < 3) { + fclose($fd); + return ''; + } + $ipFlag = fread($fd, 1); + if ($ipFlag == chr(2)) { + $AddrSeek2 = fread($fd, 3); + if (strlen($AddrSeek2) < 3) { + fclose($fd); + return ''; + } + $AddrSeek2 = implode('', unpack('L', $AddrSeek2 . chr(0))); + fseek($fd, $AddrSeek2); + } else { + fseek($fd, -1, SEEK_CUR); + } + while (($char = fread($fd, 1)) != chr(0)) + $ipAddr2 .= $char; + $AddrSeek = implode('', unpack('L', $AddrSeek . chr(0))); + fseek($fd, $AddrSeek); + while (($char = fread($fd, 1)) != chr(0)) + $ipAddr1 .= $char; + } else { + fseek($fd, -1, SEEK_CUR); + while (($char = fread($fd, 1)) != chr(0)) + $ipAddr1 .= $char; + $ipFlag = fread($fd, 1); + if ($ipFlag == chr(2)) { + $AddrSeek2 = fread($fd, 3); + if (strlen($AddrSeek2) < 3) { + fclose($fd); + return ''; + } + $AddrSeek2 = implode('', unpack('L', $AddrSeek2 . chr(0))); + fseek($fd, $AddrSeek2); + } else { + fseek($fd, -1, SEEK_CUR); + } + while (($char = fread($fd, 1)) != chr(0)) { + $ipAddr2 .= $char; + } + } + fclose($fd); + if (preg_match('/http/i', $ipAddr2)) { + $ipAddr2 = ''; + } + $ipaddr = $ipAddr1; + $ipaddr = preg_replace('/CZ88.NET/is', '', $ipaddr); + $ipaddr = preg_replace('/^s*/is', '', $ipaddr); + $ipaddr = preg_replace('/s*$/is', '', $ipaddr); + + if (preg_match('/http/i', $ipaddr) || $ipaddr == '') { + $ipaddr = ''; + } + return $this->strToUtf8($ipaddr); + + } catch (\Throwable $e) { + return ''; + } + } + + /** + * 文字格式转utf8 + * @param $str + * @return array|false|string|string[]|null + */ + public function strToUtf8($str) + { + $encode = mb_detect_encoding($str, array("ASCII", 'UTF-8', "GB2312", "GBK", 'BIG5')); + if ($encode == 'UTF-8') { + return $str; + } else { + return mb_convert_encoding($str, 'UTF-8', $encode); + } + } + + /** + * 处理城市数据 + * @param $address + * @return array + */ + public function addressHandle($address) + { + if ($address) { + try { + preg_match('/(.*?(省|自治区|北京市|天津市|上海市|重庆市|澳门特别行政区|香港特别行政区))/', $address, $matches); + if (count($matches) > 1) { + $province = $matches[count($matches) - 2]; + $address = preg_replace('/(.*?(省|自治区|北京市|天津市|上海市|重庆市|澳门特别行政区|香港特别行政区))/', '', $address, 1); + } + preg_match('/(.*?(市|自治州|地区|区划|县))/', $address, $matches); + if (count($matches) > 1) { + $city = $matches[count($matches) - 2]; + $address = str_replace($city, '', $address); + } + preg_match('/(.*?(区|县|镇|乡|街道))/', $address, $matches); + if (count($matches) > 1) { + $area = $matches[count($matches) - 2]; + $address = str_replace($area, '', $address); + } + } catch (\Throwable $e) { + } + } + return [ + 'province' => $province ?? '', + 'city' => $city ?? '', + 'district' => $area ?? '', + "address" => $address + ]; + } + + /** + * @param $name + * @param $arguments + * @return mixed + */ + public function __call($name, $arguments) + { + return call_user_func_array([$this->dao, $name], $arguments); + } +} diff --git a/app/services/activity/StoreActivityServices.php b/app/services/activity/StoreActivityServices.php new file mode 100644 index 0000000..7b83cdb --- /dev/null +++ b/app/services/activity/StoreActivityServices.php @@ -0,0 +1,355 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity; + + +use app\dao\activity\StoreActivityDao; +use app\services\activity\seckill\StoreSeckillServices; +use app\services\activity\seckill\StoreSeckillTimeServices; +use app\services\BaseServices; +use app\services\product\product\StoreDescriptionServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrResultServices; +use app\services\product\sku\StoreProductAttrServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\store\SystemStoreServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; + +/** + * 活动 + * Class StoreActivityServices + * @package app\services\activity + * @mixin StoreActivityDao + */ +class StoreActivityServices extends BaseServices +{ + + /** + * @var string[] + */ + public $typeName = [ + 1 => '秒杀', + ]; + + /** + * StoreActivityServices constructor. + * @param StoreActivityDao $dao + */ + public function __construct(StoreActivityDao $dao) + { + $this->dao = $dao; + } + + /** + * 活动列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function systemPage(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit, ['seckill' => function ($query) { + $query->field('id,activity_id'); + }]); + $count = $this->dao->count($where); + /** @var StoreSeckillTimeServices $seckillTimeServices */ + $seckillTimeServices = app()->make(StoreSeckillTimeServices::class); + $timeList = $seckillTimeServices->time_list(); + if ($timeList) { + $timeList = array_combine(array_column($timeList, 'id'), $timeList); + } + foreach ($list as &$item) { + $item['product_count'] = count($item['seckill']); + $item['time_id'] = is_string($item['time_id']) ? explode(',', $item['time_id']) : $item['time_id']; + $item['time_list'] = []; + foreach ($item['time_id'] as $time_id) { + if (isset($timeList[$time_id])) $item['time_list'][] = $timeList[$time_id]; + } + if ($item['status']) { + if ($item['start_day'] > time()) + $item['start_name'] = '未开始'; + else if (bcadd((string)$item['end_day'], '86400') < time()) + $item['start_name'] = '已结束'; + else if (bcadd((string)$item['end_day'], '86400') > time() && $item['start_day'] < time()) { + $item['start_name'] = '进行中'; + } + } else $item['start_name'] = '已结束'; + switch ($item['type']) { + case 1://秒杀 + $item['start_time'] = substr_replace($item['start_time'], ':', 2, 0); + $item['end_time'] = substr_replace($item['end_time'], ':', 2, 0); + break; + } + $item['start_day'] = date('Y-m-d', $item['start_day']); + $item['end_day'] = date('Y-m-d', $item['end_day']); +// $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + } + return compact('list', 'count'); + } + + /** + * 获取一条活动信息 + * @param int $id + * @param array $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getInfo(int $id, array $field = ['*']) + { + $info = $this->dao->get($id, $field); + if (!$info) { + throw new AdminException('数据不存在'); + } + $info = $info->toArray(); + return $info; + } + + /** + * 获取一条秒杀活动 + * @param int $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSeckillInfo(int $id) + { + $info = $this->getInfo($id); + /** @var StoreSeckillServices $seckillServices */ + $seckillServices = app()->make(StoreSeckillServices::class); + $seckill = $seckillServices->getList(['activity_id' => $id, 'is_del' => 0], 0, 0, ['attrValue']); + $info['section_data'] = [date('Y-m-d', $info['start_day']), date('Y-m-d', $info['end_day'])]; + $productList = []; + if ($seckill) { + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $productList = $productServices->searchList(['id' => array_column($seckill, 'product_id'), 'is_del' => 0, 'is_verify' => 1]); + $productList = $productList['list'] ?? []; + $seckill = array_combine(array_column($seckill, 'product_id'), $seckill); + //放入秒杀商品价格 + foreach ($productList as &$product) { + $seckillInfo = $seckill[$product['id']] ?? []; + $attrValue = $product['attrValue'] ?? []; + if ($seckillInfo && $attrValue) { + $product['status'] = $seckillInfo['status']; + $seckillAttrValue = $seckillInfo['attrValue'] ?? []; + if ($seckillAttrValue) { + $seckillAttrValue = array_combine(array_column($seckillAttrValue, 'suk'), $seckillAttrValue); + foreach ($attrValue as &$value) { + $value['quota'] = $seckillAttrValue[$value['suk']]['quota'] ?? 0; + $value['quota_show'] = $seckillAttrValue[$value['suk']]['quota_show'] ?? 0; + $value['price'] = $seckillAttrValue[$value['suk']]['price'] ?? 0; + $value['cost'] = $seckillAttrValue[$value['suk']]['cost'] ?? 0; + $value['ot_price'] = $seckillAttrValue[$value['suk']]['ot_price'] ?? 0; + } + $product['attrValue'] = $attrValue; + } + } + } + } + $info['productList'] = $productList; + //适用门店 + $info['stores'] = []; + if (isset($info['applicable_type']) && ($info['applicable_type'] == 1 || ($info['applicable_type'] == 2 && isset($info['applicable_store_id']) && $info['applicable_store_id']))) {//查询门店信息 + $where = ['is_del' => 0]; + if ($info['applicable_type'] == 2) { + $store_ids = is_array($info['applicable_store_id']) ? $info['applicable_store_id'] : explode(',', $info['applicable_store_id']); + $where['id'] = $store_ids; + } + $field = ['id', 'cate_id', 'name', 'phone', 'address', 'detailed_address', 'image', 'is_show', 'day_time', 'day_start', 'day_end']; + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeData = $storeServices->getStoreList($where, $field, '', '', 0, ['categoryName']); + $info['stores'] = $storeData['list'] ?? []; + } + return $info; + } + + + /** + * 保存活动信息 + * @param int $id + * @param array $data + * @param array $expend + * @param int $type + * @return bool + */ + public function saveData(int $id, array $data, array $expend, int $type = 1) + { + if (!$data || !$expend) { + throw new AdminException('缺少活动数据'); + } + $section_data = $data['section_data']; + $data['start_day'] = strtotime($section_data[0]); + $data['end_day'] = strtotime($section_data[1]); + unset($data['section_data']); + $this->transaction( function () use ($id, $type, $data, $section_data, $expend) { + if ($id) { + $this->getInfo($id); + $this->dao->update($id, $data); + $this->clearAcivityAttr($id, $type); + } else { + $data['add_time'] = time(); + $res = $this->dao->save($data); + $id = (int)$res->id; + } + //处理活动商品 + switch ($type) { + case 1: + $data['section_data'] = $section_data; + $this->saveActivitySeckill($id, $expend, $data); + break; + } + }); + + return true; + } + + /** + * 保存秒杀商品 + * @param int $id + * @param array $data + * @param array $activity_data + * @return bool + */ + public function saveActivitySeckill(int $id, array $data, array $activity_data) + { + $productIds = array_column($data, 'id'); + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $productList = $productServices->searchList(['id' => $productIds, 'is_del' => 0, 'is_verify' => 1]); + $productList = $productList['list'] ?? []; + //放入秒杀商品价格 + /** @var StoreSeckillServices $seckillServices */ + $seckillServices = app()->make(StoreSeckillServices::class); + $data = array_combine($productIds, $data); + foreach ($productList as &$product) { + $attrInfo = $productServices->getProductRules((int)$product['id'], 0); + $seckillData = []; + $seckillData['activity_id'] = $id; + $seckillData['product_id'] = $product['id'] ?? 0; + $seckillData['title'] = $product['store_name'] ?? ''; + $seckillData['info'] = $product['store_info'] ?? ''; + $seckillData['unit_name'] = $product['unit_name'] ?? ''; + $seckillData['section_time'] = $activity_data['section_data']; + $seckillData['images'] = $product['slider_image'] ?? ''; + $seckillData['description'] = $product['description'] ?? ''; + $seckillData['status'] = $data[$product['id']]['status'] ?? 1; + $seckillData['time_id'] = $activity_data['time_id'] ?? []; + $seckillData['num'] = $activity_data['num'] ?? 0; + $seckillData['once_num'] = $activity_data['once_num'] ?? 0; + $seckillData['delivery_type'] = $product['delivery_type'] ?? ''; + $seckillData['temp_id'] = $product['temp_id'];//运费设置 + $seckillData['freight'] = $product['freight'];//运费设置 + $seckillData['postage'] = $product['freight'];//邮费 + $seckillData['custom_form'] = $product['custom_form'];//自定义表单 + $seckillData['system_form_id'] = $product['system_form_id'];//系统表单ID + $seckillData['product_type'] = $product['product_type'];//商品类型 + $seckillData['applicable_type'] = $activity_data['applicable_type'];//适用门店类型 + $seckillData['applicable_store_id'] = $activity_data['applicable_store_id'];//适用门店IDS + + $seckillData['items'] = $attrInfo['items']; + $attrs = $attrInfo['attrs'] ?? []; + if ($attrs) { + $seckillAttrValue = $data[$product['id']]['attrValue'] ?? []; + if (!$seckillAttrValue) { + throw new AdminException('请选择商品规格'); + } + foreach ($seckillAttrValue as $sattr) { + if (!isset($sattr['price']) || !$sattr['price']) { + throw new AdminException('请填写商品('.$product['store_name'] .')活动价'); + } + if (!isset($sattr['quota']) || !$sattr['quota']) { + throw new AdminException('请填写商品('.$product['store_name'] .')限量'); + } + } + $seckillAttrValue = array_combine(array_column($seckillAttrValue, 'suk'), $seckillAttrValue); + foreach ($attrs as $attr) { + $sku = implode(',', $attr['detail']); + if(!isset($seckillAttrValue[$sku])) { + throw new AdminException('请重新选择商品规格'); + } + if (($seckillAttrValue[$sku][''] ?? 0) > $attr['stock']) { + throw new AdminException('限量超过了商品库存'); + } + $attr['quota'] = $attr['quota_show'] = $seckillAttrValue[$sku]['quota'] ?? 0; + $attr['price'] = $seckillAttrValue[$sku]['price'] ?? 0; + $attr['cost'] = $seckillAttrValue[$sku]['cost'] ?? 0; + $attr['ot_price'] = $seckillAttrValue[$sku]['ot_price'] ?? 0; + $seckillData['attrs'][] = $attr; + } + } + $seckillServices->saveData(0, $seckillData); + } + return true; + } + + /** + * 清空活动商品数据 + * @param int $id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function clearAcivityAttr(int $id, int $type = 1) + { + /** @var StoreSeckillServices $seckillServices */ + $seckillServices = app()->make(StoreSeckillServices::class); + $seckill = $seckillServices->getList(['activity_id' => $id, 'is_del' => 0]); + if ($seckill) { + $seckillIds = array_column($seckill, 'id'); + /** @var StoreProductAttrResultServices $storeProductAttrResultServices */ + $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class); + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $where = ['product_id' => $seckillIds, 'type' => $type]; + $storeProductAttrResultServices->delete($where); + $storeDescriptionServices->delete($where); + $storeProductAttrServices->delete($where); + $storeProductAttrValueServices->delete($where); + $seckillServices->delete(['activity_id' => $id, 'is_del' => 0]); + $seckillServices->cacheTag()->clear(); + } + CacheService::redisHandler('product_attr')->clear(); + return true; + } + + + /** + * 判断时间段是否存在秒杀活动 + * @param array $where + * @param int $type + * @return bool + * @throws \think\db\exception\DbException + */ + public function existenceActivity(array $where, int $type = 1): bool + { + $where['is_del'] = 0; + if (!isset($where['type'])) $where['type'] = $type; + return $this->dao->checkActivity($where); + } + + + +} diff --git a/app/services/activity/bargain/StoreBargainServices.php b/app/services/activity/bargain/StoreBargainServices.php new file mode 100644 index 0000000..343c549 --- /dev/null +++ b/app/services/activity/bargain/StoreBargainServices.php @@ -0,0 +1,956 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\bargain; + +use app\dao\activity\bargain\StoreBargainDao; +use app\services\BaseServices; +use app\services\diy\DiyServices; +use app\services\order\StoreOrderServices; +use app\services\other\QrcodeServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\ensure\StoreProductEnsureServices; +use app\services\product\label\StoreProductLabelServices; +use app\services\product\product\StoreDescriptionServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrResultServices; +use app\services\product\sku\StoreProductAttrServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\system\attachment\SystemAttachmentServices; +use app\services\user\UserServices; +use app\jobs\product\ProductLogJob; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use crmeb\services\UploadService; +use crmeb\services\UtilService; +use crmeb\services\wechat\MiniProgram; +use GuzzleHttp\Psr7\Utils; +use think\exception\ValidateException; + +/** + * Class StoreBargainServices + * @package app\services\activity\bargain + * @mixin StoreBargainDao + */ +class StoreBargainServices extends BaseServices +{ + + const DRNCCGFB = '$2y$10'; + + /** + * 商品活动类型 + */ + const TYPE = 3; + + /** + * StoreCombinationServices constructor. + * @param StoreBargainDao $dao + */ + public function __construct(StoreBargainDao $dao) + { + $this->dao = $dao; + } + + /** + * @param array $productIds + * @return mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/28 + */ + public function getBargainIdsArrayCache(array $productIds) + { + return $this->dao->cacheTag()->remember(md5('Bargain_ids_' . json_encode($productIds)), function () use ($productIds) { + return $this->dao->getBargainIdsArray($productIds, ['id']); + }); + } + + /** + * 判断砍价商品是否开启 + * @param int $bargainId + * @return int|string + */ + public function validBargain($bargainId = 0) + { + $where = []; + $time = time(); + $where[] = ['is_del', '=', 0]; + $where[] = ['status', '=', 1]; + $where[] = ['start_time', '<', $time]; + $where[] = ['stop_time', '>', $time - 85400]; + if ($bargainId) $where[] = ['id', '=', $bargainId]; + return $this->dao->getCount($where); + } + + /** + * 获取后台列表 + * @param array $where + * @return array + */ + public function getStoreBargainList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit); + $count = $this->dao->count($where); + /** @var StoreBargainUserServices $storeBargainUserServices */ + $storeBargainUserServices = app()->make(StoreBargainUserServices::class); + $ids = array_column($list, 'id'); + $countAll = $storeBargainUserServices->getAllCount([['bargain_id', 'in', $ids]]); + $countSuccess = $storeBargainUserServices->getAllCount([ + ['status', '=', 3], + ['bargain_id', 'in', $ids] + ]); + /** @var StoreBargainUserHelpServices $storeBargainUserHelpServices */ + $storeBargainUserHelpServices = app()->make(StoreBargainUserHelpServices::class); + $countHelpAll = $storeBargainUserHelpServices->getHelpAllCount([['bargain_id', 'in', $ids]]); + foreach ($list as &$item) { + $item['count_people_all'] = $countAll[$item['id']] ?? 0;//参与人数 + $item['count_people_help'] = $countHelpAll[$item['id']] ?? 0;//帮忙砍价人数 + $item['count_people_success'] = $countSuccess[$item['id']] ?? 0;//砍价成功人数 + $item['stop_status'] = $item['stop_time'] < time() ? 1 : 0; + if ($item['start_time'] > time()) + $item['start_name'] = '未开始'; + else if ($item['stop_time'] < time()) + $item['start_name'] = '已结束'; + else if ($item['stop_time'] > time() && $item['start_time'] < time()) { + $item['start_name'] = '进行中'; + } + } + return compact('list', 'count'); + } + + /** + * 保存数据 + * @param int $id + * @param array $data + */ + public function saveData(int $id, array $data) + { + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $productInfo = $storeProductServices->getOne(['is_show' => 1, 'is_del' => 0, 'is_verify' => 1, 'id' => $data['product_id']]); + if (!$productInfo) { + throw new AdminException('原商品已下架或移入回收站'); + } + if ($productInfo['is_vip_product'] || $productInfo['is_presale_product']) { + throw new AdminException('该商品是预售或svip专享'); + } + $data['product_type'] = $productInfo['product_type']; + if ($data['product_type'] == 4 && !$data['delivery_type']) { + $data['delivery_type'] = $productInfo['delivery_type']; + } + $data['type'] = $productInfo['type'] ?? 0; + $data['relation_id'] = $productInfo['relation_id'] ?? 0; + $custom_form = $productInfo['custom_form'] ?? []; + $data['custom_form'] = is_array($custom_form) ? json_encode($custom_form) : $custom_form; + $data['system_form_id'] = $productInfo['system_form_id'] ?? 0; + $store_label_id = $productInfo['store_label_id'] ?? []; + $data['store_label_id'] = is_array($store_label_id) ? implode(',', $store_label_id) : $store_label_id; + $ensure_id = $productInfo['ensure_id'] ?? []; + $data['ensure_id'] = is_array($ensure_id) ? implode(',', $ensure_id) : $ensure_id; + $specs = $productInfo['specs'] ?? []; + $data['specs'] = is_array($specs) ? json_encode($specs) : $specs; + if (in_array($data['product_type'], [1, 2, 3])) { + $data['freight'] = 2; + $data['temp_id'] = 0; + $data['postage'] = 0; + } else { + if ($data['freight'] == 1) { + $data['temp_id'] = 0; + $data['postage'] = 0; + } elseif ($data['freight'] == 2) { + $data['temp_id'] = 0; + } elseif ($data['freight'] == 3) { + $data['postage'] = 0; + } + if ($data['freight'] == 2 && !$data['postage']) { + throw new AdminException('请设置运费金额'); + } + if ($data['freight'] == 3 && !$data['temp_id']) { + throw new AdminException('请选择运费模版'); + } + } + $description = $data['description']; + $detail = $data['attrs']; + $items = $data['items']; + $data['start_time'] = strtotime($data['section_time'][0]); + $data['stop_time'] = strtotime($data['section_time'][1]); + $data['image'] = $data['images'][0] ?? ''; + $data['images'] = json_encode($data['images']); + $data['stock'] = $detail[0]['stock']; + $data['quota'] = $detail[0]['quota']; + $data['quota_show'] = $detail[0]['quota']; + $data['price'] = $detail[0]['price']; + $data['min_price'] = $detail[0]['min_price']; + + if ($detail[0]['min_price'] < 0 || $detail[0]['price'] <= 0 || $detail[0]['min_price'] === '' || $detail[0]['price'] === '') throw new ValidateException('金额不能小于0'); + if ($detail[0]['min_price'] >= $detail[0]['price']) throw new ValidateException('砍价最低价不能大于或等于起始金额'); + if ($detail[0]['quota'] > $detail[0]['stock']) throw new ValidateException('限量不能超过商品库存'); + + //按照能砍掉的金额计算最大设置人数,并判断填写的砍价人数是否大于最大设置人数 + $bNum = bcmul(bcsub((string)$data['price'], (string)$data['min_price'], 2), '100'); + if ($data['people_num'] > $bNum) throw new ValidateException('商品砍去金额(每人最少0.01元)'); + + unset($data['section_time'], $data['description'], $data['attrs'], $data['items'], $detail[0]['min_price'], $detail[0]['_index'], $detail[0]['_rowKey']); + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + $this->transaction(function () use ($id, $data, $description, $detail, $items, $storeDescriptionServices, $storeProductAttrServices) { + if ($id) { + $res = $this->dao->update($id, $data); + if (!$res) throw new AdminException('修改失败'); + } else { + $data['add_time'] = time(); + $res = $this->dao->save($data); + if (!$res) throw new AdminException('添加失败'); + $id = (int)$res->id; + } + $storeDescriptionServices->saveDescription((int)$id, $description, 2); + $skuList = $storeProductAttrServices->validateProductAttr($items, $detail, (int)$id, 2); + $valueGroup = $storeProductAttrServices->saveProductAttr($skuList, (int)$id, 2); + + $res = true; + foreach ($valueGroup as $item) { + $res = $res && CacheService::setStock($item['unique'], (int)$item['quota_show'], 2); + } + if (!$res) { + throw new AdminException('占用库存失败'); + } + }); + + $this->dao->cacheTag()->clear(); + } + + /** + * 获取砍价详情 + * @param int $id + * @return array|\think\Model|null + */ + public function getInfo(int $id) + { + $info = $this->dao->get($id); + if ($info) { + if ($info['start_time']) + $start_time = date('Y-m-d H:i:s', $info['start_time']); + + if ($info['stop_time']) + $stop_time = date('Y-m-d H:i:s', $info['stop_time']); + if (isset($start_time) && isset($stop_time)) + $info['section_time'] = [$start_time, $stop_time]; + else + $info['section_time'] = []; + unset($info['start_time'], $info['stop_time']); + } + + $info['give_integral'] = intval($info['give_integral']); + $info['price'] = floatval($info['price']); + $info['postage'] = floatval($info['postage']); + $info['cost'] = floatval($info['cost']); + $info['bargain_max_price'] = floatval($info['bargain_max_price']); + $info['bargain_min_price'] = floatval($info['bargain_min_price']); + $info['min_price'] = floatval($info['min_price']); + $info['weight'] = floatval($info['weight']); + $info['volume'] = floatval($info['volume']); + if (!$info['delivery_type']) { + $info['delivery_type'] = [1]; + } + if ($info['postage']) { + $info['freight'] = 2; + } elseif ($info['temp_id']) { + $info['freight'] = 3; + } else { + $info['freight'] = 1; + } + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + $info['description'] = $storeDescriptionServices->getDescription(['product_id' => $id, 'type' => 2]); + $info['attrs'] = $this->attrList($id, $info['product_id']); + return $info; + } + + /** + * 获取规格 + * @param int $id + * @param int $pid + * @return mixed + */ + public function attrList(int $id, int $pid) + { + /** @var StoreProductAttrResultServices $storeProductAttrResultServices */ + $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class); + $bargainResult = $storeProductAttrResultServices->value(['product_id' => $id, 'type' => 2], 'result'); + $items = json_decode($bargainResult, true)['attr']; + $productAttr = $this->getattr($items, $pid, 0); + $bargainAttr = $this->getattr($items, $id, 2); + foreach ($productAttr as $pk => $pv) { + foreach ($bargainAttr as &$sv) { + if ($pv['detail'] == $sv['detail']) { + $productAttr[$pk] = $sv; + } + } + $productAttr[$pk]['detail'] = json_decode($productAttr[$pk]['detail']); + } + $attrs['items'] = $items; + $attrs['value'] = $productAttr; + foreach ($items as $key => $item) { + $header[] = ['title' => $item['value'], 'key' => 'value' . ($key + 1), 'align' => 'center', 'minWidth' => 80]; + } + $header[] = ['title' => '图片', 'slot' => 'pic', 'align' => 'center', 'minWidth' => 120]; + $header[] = ['title' => '砍价起始金额', 'slot' => 'price', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '砍价最低价', 'slot' => 'min_price', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '成本价', 'key' => 'cost', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '原价', 'key' => 'ot_price', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '库存', 'key' => 'stock', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '限量', 'slot' => 'quota', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '重量(KG)', 'key' => 'weight', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '体积(m³)', 'key' => 'volume', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '商品条形码', 'key' => 'bar_code', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '商品编号', 'key' => 'code', 'align' => 'center', 'minWidth' => 80]; + $attrs['header'] = $header; + return $attrs; + } + + /** + * 获取规格 + * @param $attr + * @param $id + * @param $type + * @return array + */ + public function getattr($attr, $id, $type) + { + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $value = attr_format($attr)[1]; + $valueNew = []; + $count = 0; + if ($type == 2) { + $min_price = $this->dao->value(['id' => $id], 'min_price'); + } else { + $min_price = 0; + } + foreach ($value as $key => $item) { + $detail = $item['detail']; +// sort($item['detail'], SORT_STRING); + $suk = implode(',', $item['detail']); + $sukValue = $storeProductAttrValueServices->getSkuArray(['product_id' => $id, 'type' => $type, 'suk' => $suk], 'bar_code,code,cost,price,ot_price,stock,image as pic,weight,volume,brokerage,brokerage_two,quota,quota_show', 'suk'); + if (count($sukValue)) { + foreach (array_values($detail) as $k => $v) { + $valueNew[$count]['value' . ($k + 1)] = $v; + } + $valueNew[$count]['detail'] = json_encode($detail); + $valueNew[$count]['pic'] = $sukValue[$suk]['pic'] ?? ''; + $valueNew[$count]['price'] = $sukValue[$suk]['price'] ? floatval($sukValue[$suk]['price']) : 0; + $valueNew[$count]['min_price'] = $min_price ? floatval($min_price) : 0; + $valueNew[$count]['cost'] = $sukValue[$suk]['cost'] ? floatval($sukValue[$suk]['cost']) : 0; + $valueNew[$count]['ot_price'] = isset($sukValue[$suk]['ot_price']) ? floatval($sukValue[$suk]['ot_price']) : 0; + $valueNew[$count]['stock'] = $sukValue[$suk]['stock'] ? intval($sukValue[$suk]['stock']) : 0; +// $valueNew[$count]['quota'] = $sukValue[$suk]['quota'] ? intval($sukValue[$suk]['quota']) : 0; + $valueNew[$count]['quota'] = isset($sukValue[$suk]['quota_show']) && $sukValue[$suk]['quota_show'] ? intval($sukValue[$suk]['quota_show']) : 0; + $valueNew[$count]['code'] = $sukValue[$suk]['code'] ?? ''; + $valueNew[$count]['bar_code'] = $sukValue[$suk]['bar_code'] ?? ''; + $valueNew[$count]['weight'] = $sukValue[$suk]['weight'] ? floatval($sukValue[$suk]['weight']) : 0; + $valueNew[$count]['volume'] = $sukValue[$suk]['volume'] ? floatval($sukValue[$suk]['volume']) : 0; + $valueNew[$count]['brokerage'] = $sukValue[$suk]['brokerage'] ? floatval($sukValue[$suk]['brokerage']) : 0; + $valueNew[$count]['brokerage_two'] = $sukValue[$suk]['brokerage_two'] ? floatval($sukValue[$suk]['brokerage_two']) : 0; + $valueNew[$count]['opt'] = $type != 0; + $count++; + } + } + return $valueNew; + } + + /** + * 砍价列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getBargainList() + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->BargainList($page, $limit); + if ($list) { + $bargainIds = array_column($list, 'id'); + /** @var StoreBargainUserHelpServices $bargainHelp */ + $bargainHelp = app()->make(StoreBargainUserHelpServices::class); + $bargainPeople = $bargainHelp->getBargainPeople([['bargain_id', 'in', $bargainIds], ['type', '=', 1]], 'bargain_id,count(*) as people', 'bargain_id'); + foreach ($list as &$item) { + $item['people'] = $bargainPeople[$item['id']]['people'] ?? 0; + $item['price'] = floatval($item['price']); + } + } + return $list; + } + + /** + * 获取单条砍价 + * @param int $uid + * @param int $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getBargain(int $uid, int $id) + { + $storeInfo = $this->dao->getOne(['id' => $id], '*', ['descriptions']); + if (!$storeInfo) throw new ValidateException('砍价商品不存在'); + $this->dao->addBargain($id, 'look'); + $storeInfo['time'] = time(); + if ($storeInfo['stop_time'] < time()) throw new ValidateException('砍价已结束'); + $user = []; + if ($uid) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid, 'uid,nickname,avatar'); + if (!$user) { + throw new ValidateException('用户信息获取失败或者登录失效'); + } + } + $data['userInfo']['uid'] = $uid; + $data['userInfo']['nickname'] = $user['nickname'] ?? ''; + $data['userInfo']['avatar'] = $user['avatar'] ?? ''; + + /** @var DiyServices $diyServices */ + $diyServices = app()->make(DiyServices::class); + $infoDiy = $diyServices->getProductDetailDiy(); + //diy控制参数 + if (!isset($infoDiy['is_specs']) || !$infoDiy['is_specs']) { + $storeInfo['specs'] = []; + } + //品牌名称 + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $storeInfo['brand_name'] = $storeProductServices->productIdByBrandName($storeInfo['product_id']); + $storeInfo['store_label'] = $storeInfo['ensure'] = []; + if ($storeInfo['store_label_id']) { + /** @var StoreProductLabelServices $storeProductLabelServices */ + $storeProductLabelServices = app()->make(StoreProductLabelServices::class); + $storeInfo['store_label'] = $storeProductLabelServices->getLabelCache($storeInfo['store_label_id'], ['id', 'label_name']); + } + if ($storeInfo['ensure_id'] && isset($infoDiy['is_ensure']) && $infoDiy['is_ensure']) { + /** @var StoreProductEnsureServices $storeProductEnsureServices */ + $storeProductEnsureServices = app()->make(StoreProductEnsureServices::class); + $storeInfo['ensure'] = $storeProductEnsureServices->getEnsurCache($storeInfo['ensure_id'], ['id', 'name', 'image', 'desc']); + } + + /** + * 判断配送方式 + */ + $storeInfo['delivery_type'] = $storeProductServices->getDeliveryType((int)$storeInfo['id'], (int)$storeInfo['type'], (int)$storeInfo['relation_id'], $storeInfo['delivery_type']); + + //是否自己还能参与砍价 + $count = 0; + $data['bargainSumCount'] = 0; + $data['userBargainStatus'] = 0; + if ($uid) { + /** @var StoreBargainUserServices $bargainUserServices */ + $bargainUserServices = app()->make(StoreBargainUserServices::class); + $count = $bargainUserServices->count(['uid' => $uid, 'bargain_id' => $id]); + /** @var StoreOrderServices $orderService */ + $orderService = app()->make(StoreOrderServices::class); + $data['bargainSumCount'] = $orderService->count(['type' => 2, 'activity_id' => $id, 'uid' => $uid]);//只要用户生成订单,就算作用电一次砍价的机会 + /** @var StoreBargainUserServices $storeInfoUserService */ + $storeInfoUserService = app()->make(StoreBargainUserServices::class); + $data['userBargainStatus'] = $storeInfoUserService->count(['bargain_id' => $id, 'uid' => $uid, 'is_del' => 0]); + } + $storeInfo['is_bargain'] = $count >= $storeInfo['num']; + + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + [$productAttr, $productValue] = $storeProductAttrServices->getProductAttrDetail($id, $uid, 0, 2, $storeInfo['product_id']); + foreach ($productValue as $v) { + $storeInfo['attr'] = $v; + } + $data['bargain'] = get_thumb_water($storeInfo); + $storeInfoNew = get_thumb_water($storeInfo, 'small'); + $data['bargain']['small_image'] = $storeInfoNew['image']; + $data['site_name'] = sys_config('site_name'); + $data['share_qrcode'] = sys_config('share_qrcode', 0); + //浏览记录 + ProductLogJob::dispatch(['visit', ['uid' => $uid, 'id' => $id, 'product_id' => $storeInfo['product_id']], 'bargain']); + return $data; + } + + /** + * 验证砍价是否能支付 + * @param int $bargainId + * @param int $uid + */ + public function checkBargainUser(int $bargainId, int $uid) + { + /** @var StoreBargainUserServices $bargainUserServices */ + $bargainUserServices = app()->make(StoreBargainUserServices::class); + $bargainUserInfo = $bargainUserServices->getOne(['uid' => $uid, 'bargain_id' => $bargainId, 'status' => 1, 'is_del' => 0]); + if (!$bargainUserInfo) + throw new ValidateException('砍价失败'); + $bargainUserTableId = $bargainUserInfo['id']; + if ($bargainUserInfo['bargain_price_min'] < bcsub((string)$bargainUserInfo['bargain_price'], (string)$bargainUserInfo['price'], 2)) { + throw new ValidateException('砍价未成功'); + } + if ($bargainUserInfo['status'] == 3) + throw new ValidateException('砍价已支付'); + + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + $res = $attrValueServices->getOne(['product_id' => $bargainId, 'type' => 2]); + if (!$this->validBargain($bargainId) || !$res) { + throw new ValidateException('该商品已下架或删除'); + } + $StoreBargainInfo = $this->dao->get($bargainId); + if (1 > $res['quota']) { + throw new ValidateException('该商品库存不足'); + } + $product_stock = $attrValueServices->value(['product_id' => $StoreBargainInfo['product_id'], 'suk' => $res['suk'], 'type' => 0], 'stock'); + if ($product_stock < 1) { + throw new ValidateException('该商品库存不足'); + } + return true; + } + + /** + * 修改砍价状态 + * @param int $bargainId + * @param int $uid + * @param int $bargainUserTableId + * @return \crmeb\basic\BaseModel|false + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setBargainUserStatus(int $bargainId, int $uid, int $bargainUserTableId = 0) + { + if (!$bargainId || !$uid) return false; + if (!$bargainUserTableId) { + /** @var StoreBargainUserServices $bargainUserServices */ + $bargainUserServices = app()->make(StoreBargainUserServices::class); + $bargainUserInfo = $bargainUserServices->getOne(['uid' => $uid, 'bargain_id' => $bargainId, 'status' => 1, 'is_del' => 0]); + if (!$bargainUserInfo) + throw new ValidateException('砍价失败'); + $bargainUserTableId = $bargainUserInfo['id']; + } + /** @var StoreBargainUserServices $bargainUserServices */ + $bargainUserServices = app()->make(StoreBargainUserServices::class); + $bargainUser = $bargainUserServices->getOne(['id' => $bargainUserTableId, 'uid' => $uid, 'bargain_id' => $bargainId, 'status' => 1], 'price,bargain_price,bargain_price_min'); + if (!$bargainUser) { + return false; + } + $userPrice = $bargainUser['price']; + $price = bcsub($bargainUser['bargain_price'], $bargainUser['bargain_price_min'], 2); + if (bcsub($price, $userPrice, 2) > 0) { + return false; + } + return $bargainUserServices->updateBargainStatus($bargainUserTableId); + } + + /** + * 参与砍价 + * @param int $uid + * @param int $bargainId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setBargain(int $uid, int $bargainId) + { + $bargainInfo = $this->dao->getOne([ + ['is_del', '=', 0], + ['status', '=', 1], + ['start_time', '<', time()], + ['stop_time', '>', time()], + ['id', '=', $bargainId], + ['quota', '>', 0], + ]); + if (!$bargainInfo) throw new ValidateException('砍价已结束'); + $bargainInfo = $bargainInfo->toArray(); + /** @var StoreBargainUserServices $bargainUserService */ + $bargainUserService = app()->make(StoreBargainUserServices::class); + $count = $bargainUserService->count(['bargain_id' => $bargainId, 'uid' => $uid, 'is_del' => 0, 'status' => 1]); + if ($count === false) { + throw new ValidateException('参数错误'); + } else { + $count = $bargainUserService->count(['uid' => $uid, 'bargain_id' => $bargainId, 'type' => 1]); + if ($count >= $bargainInfo['num']) throw new ValidateException('您不能再发起此件商品砍价'); + [$bargainUserInfo, $price] = $this->transaction(function () use ($uid, $bargainId, $bargainInfo, $bargainUserService) { + $res = $bargainUserService->setBargain($bargainId, $uid, $bargainInfo); + if (!$res) { + throw new ValidateException('参与失败'); + } + /** @var StoreBargainUserHelpServices $bargainUserHelpService */ + $bargainUserHelpService = app()->make(StoreBargainUserHelpServices::class); + $price = $bargainUserHelpService->setBargainUserHelp($bargainId, (int)$res['id'], $uid, $bargainInfo); + $bargainUserInfo = $bargainUserService->get((int)$res['id']); + return [$bargainUserInfo, $price]; + }); + $status = $this->getBargainStatus((int)$bargainUserInfo['id'], $bargainUserInfo); + return compact('bargainUserInfo', 'price', 'status'); + } + } + + /** + * 获取砍价状态 + * @param int $bargainUserTableId + * @param $bargainUserInfo + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getBargainStatus(int $bargainUserTableId, $bargainUserInfo) + { + if (!$bargainUserInfo) { + /** @var StoreBargainUserServices $bargainUserService */ + $bargainUserService = app()->make(StoreBargainUserServices::class); + $bargainUserInfo = $bargainUserService->get($bargainUserTableId);// 获取用户参与砍价信息 + } + if (bcsub($bargainUserInfo['bargain_price'], $bargainUserInfo['price'], 2) == $bargainUserInfo['bargain_price_min']) { + $status = true; + } else { + $status = false; + } + return $status; + } + + /** + * 保存砍价记录返回砍价金额 + * @param int $uid + * @param int $bargainId + * @param int $bargainUserUid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setHelpBargain(int $uid, int $bargainId, int $bargainUserUid) + { + if (!$bargainId || !$bargainUserUid) throw new ValidateException('参数错误'); + $bargainInfo = $this->dao->getOne([ + ['is_del', '=', 0], + ['status', '=', 1], + ['start_time', '<', time()], + ['stop_time', '>', time()], + ['id', '=', $bargainId], + ['quota', '>', 0], + ]); + if (!$bargainInfo) throw new ValidateException('砍价已结束'); + $bargainInfo = $bargainInfo->toArray(); + /** @var StoreBargainUserServices $bargainUserService */ + $bargainUserService = app()->make(StoreBargainUserServices::class); + $bargainUserTableId = $bargainUserService->getBargainUserTableId($bargainId, $bargainUserUid); + if (!$bargainUserTableId) throw new ValidateException('该分享未开启砍价'); + /** @var StoreBargainUserHelpServices $userHelpService */ + $userHelpService = app()->make(StoreBargainUserHelpServices::class); + $count = $userHelpService->isBargainUserHelpCount($bargainId, $bargainUserTableId, $uid); + $bargainUserInfo = $bargainUserService->get($bargainUserTableId); + if (!$count) { + throw new ValidateException('您已经帮砍过此砍价'); + } + $price = $userHelpService->setBargainUserHelp($bargainId, $bargainUserTableId, $uid, $bargainInfo); + if (!(float)$price) { + [$coverPrice, $alreadyPrice, $surplusPrice, $pricePercent, $bargainUserInfo] = $bargainUserService->getSurplusPrice($bargainUserTableId); + if (0.00 === (float)$surplusPrice) { + $bargainUserInfo = $bargainUserService->get($bargainUserTableId);// 获取用户参与砍价信息 + //用户发送消息 + event('notice.notice', [['uid' => $bargainUserUid, 'bargainInfo' => $bargainInfo, 'bargainUserInfo' => $bargainUserInfo,], 'bargain_success']); + } + } + $status = $this->getBargainStatus($bargainUserTableId, $bargainUserInfo); + return compact('bargainUserInfo', 'price', 'status'); + } + + /** + * 减库存加销量 + * @param int $num + * @param int $bargainId + * @param string $unique + * @param int $store_id + * @return bool + */ + public function decBargainStock(int $num, int $bargainId, string $unique, int $store_id = 0) + { + //平台商品ID + $product_id = $this->dao->value(['id' => $bargainId], 'product_id'); + $res = true; + if ($product_id) { + if ($unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //减去砍价商品sku的库存增加销量 + $res = $res && $skuValueServices->decProductAttrStock($bargainId, $unique, $num, 2); + + //砍价商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $bargainId, 'type' => 2], 'suk'); + //平台商品sku unique + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //减掉普通商品sku的库存加销量 + $res = $res && $services->decProductStock($num, (int)$product_id, (string)$productUnique, $store_id); + } + //减去砍价商品的库存和销量 + $res = $res && $this->dao->decStockIncSales(['id' => $bargainId, 'type' => 2], $num); + } + return $res; + } + + /** + * 减销量加库存 + * @param int $num + * @param int $bargainId + * @param string $unique + * @param int $store_id + * @return bool + */ + public function incBargainStock(int $num, int $bargainId, string $unique, int $store_id = 0) + { + $product_id = $this->dao->value(['id' => $bargainId], 'product_id'); + $res = true; + if ($product_id) { + if ($unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //减去砍价商品sku的销量,增加库存和限购数量 + $res = false !== $skuValueServices->incProductAttrStock($bargainId, $unique, $num, 2); + + //砍价商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $bargainId, 'type' => 2], 'suk'); + //平台商品sku unique + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //减掉普通商品的销量加库存 + $res = $res && $services->incProductStock($num, (int)$product_id, (string)$productUnique, $store_id); + } + //减去砍价商品的销量,增加库存 + $res = $res && $this->dao->incStockDecSales(['id' => $bargainId, 'type' => 2], $num); + } + return $res; + } + + /** + * 获取砍价海报信息 + * @param int $bargainId + * @param $user + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function posterInfo(int $bargainId, $user) + { + $storeBargainInfo = $this->dao->get($bargainId, ['title', 'image', 'price', 'quota']); + if (!$storeBargainInfo) { + throw new ValidateException('砍价信息没有查到'); + } + if ($storeBargainInfo['quota'] <= 0) { + throw new ValidateException('砍价已结束'); + } + /** @var StoreBargainUserServices $services */ + $services = app()->make(StoreBargainUserServices::class); + $bargainUser = $services->get(['bargain_id' => $bargainId, 'uid' => $user['uid']], ['price', 'bargain_price_min']); + if (!$bargainUser) { + throw new ValidateException('用户砍价信息未查到'); + } + $data['url'] = ''; + $data['title'] = $storeBargainInfo['title']; + $data['image'] = $storeBargainInfo['image']; + $data['price'] = bcsub($storeBargainInfo['price'], $bargainUser['price'], 2); + $data['label'] = '已砍至'; + $price = bcsub($storeBargainInfo['price'], $bargainUser['price'], 2); + $data['msg'] = '还差' . (bcsub($price, $bargainUser['bargain_price_min'], 2)) . '元即可砍价成功'; + //只有在小程序端,才会生成二维码 + if (\request()->isRoutine()) { + try { + /** @var SystemAttachmentServices $systemAttachmentServices */ + $systemAttachmentServices = app()->make(SystemAttachmentServices::class); + //小程序 + $name = $bargainId . '_' . $user['uid'] . '_' . $user['is_promoter'] . '_bargain_share_routine.jpg'; + $siteUrl = sys_config('site_url'); + $imageInfo = $systemAttachmentServices->getInfo(['name' => $name]); + if (!$imageInfo) { + $valueData = 'id=' . $bargainId . '&spid=' . $user['uid']; + $res = MiniProgram::appCodeUnlimit($valueData, 'pages/activity/goods_bargain_details/index', 280); + if (!$res) throw new ValidateException('二维码生成失败'); + $uploadType = (int)sys_config('upload_type', 1); + $upload = UploadService::init($uploadType); + $res = (string)Utils::streamFor($res); + $res = $upload->to('routine/activity/bargain/code')->validate()->stream($res, $name); + if ($res === false) { + throw new ValidateException($upload->getError()); + } + $imageInfo = $upload->getUploadInfo(); + $imageInfo['image_type'] = $uploadType; + if ($imageInfo['image_type'] == 1) $remoteImage = UtilService::remoteImage($siteUrl . $imageInfo['dir']); + else $remoteImage = UtilService::remoteImage($imageInfo['dir']); + if (!$remoteImage['status']) throw new ValidateException($remoteImage['msg']); + $systemAttachmentServices->save([ + 'name' => $imageInfo['name'], + 'att_dir' => $imageInfo['dir'], + 'satt_dir' => $imageInfo['thumb_path'], + 'att_size' => $imageInfo['size'], + 'att_type' => $imageInfo['type'], + 'image_type' => $imageInfo['image_type'], + 'module_type' => 2, + 'time' => time(), + 'pid' => 1, + 'type' => 1 + ]); + $url = $imageInfo['dir']; + } else $url = $imageInfo['att_dir']; + if ($imageInfo['image_type'] == 1) { + $data['url'] = $siteUrl . $url; + } else { + $data['url'] = $url; + } + } catch (\Throwable $e) { + } + } else { + if (sys_config('share_qrcode', 0) && request()->isWechat()) { + /** @var QrcodeServices $qrcodeService */ + $qrcodeService = app()->make(QrcodeServices::class); + $data['url'] = $qrcodeService->getTemporaryQrcode('bargain-' . $bargainId . '-' . $user['uid'], $user['uid'])->url; + } + } + return $data; + } + + /** + * 验证砍价下单库存限量 + * @param int $uid + * @param int $bargainId + * @param int $cartNum + * @param string $unique + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkBargainStock(int $uid, int $bargainId, int $cartNum = 1, string $unique = '') + { + if (!$this->validBargain($bargainId)) { + throw new ValidateException('该商品已下架或删除'); + } + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + $attrInfo = $attrValueServices->getOne(['product_id' => $bargainId, 'type' => 2]); + if (!$attrInfo || $attrInfo['product_id'] != $bargainId) { + throw new ValidateException('请选择有效的商品属性'); + } + $productInfo = $this->dao->get($bargainId, ['*', 'title as store_name']); + /** @var StoreBargainUserServices $bargainUserService */ + $bargainUserService = app()->make(StoreBargainUserServices::class); + $bargainUserInfo = $bargainUserService->getOne(['uid' => $uid, 'bargain_id' => $bargainId, 'status' => 1, 'is_del' => 0]); + if (!$bargainUserInfo || $bargainUserInfo['bargain_price_min'] < bcsub((string)$bargainUserInfo['bargain_price'], (string)$bargainUserInfo['price'], 2)) { + throw new ValidateException('砍价未成功'); + } + $unique = $attrInfo['unique']; + if ($cartNum > $attrInfo['quota']) { + throw new ValidateException('该商品库存不足' . $cartNum); + } + return [$attrInfo, $unique, $productInfo, $bargainUserInfo]; + } + + /** + * 砍价统计 + * @param $id + * @return array + */ + public function bargainStatistics(int $id) + { + /** @var StoreBargainUserServices $bargainUser */ + $bargainUser = app()->make(StoreBargainUserServices::class); + /** @var StoreBargainUserHelpServices $bargainUserHelp */ + $bargainUserHelp = app()->make(StoreBargainUserHelpServices::class); + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $people_count = $bargainUserHelp->count(['bargain_id' => $id]); + $spread_count = $bargainUserHelp->count(['bargain_id' => $id, 'type' => 0]); + $start_count = $bargainUser->count(['bargain_id' => $id]); + $success_count = $bargainUser->count(['bargain_id' => $id, 'status' => 3]); + $pay_price = $orderServices->sum(['type' => 2, 'activity_id' => $id, 'paid' => 1], 'pay_price', true); + $pay_count = $orderServices->count(['type' => 2, 'activity_id' => $id, 'paid' => 1]); + $pay_rate = $start_count > 0 ? bcmul(bcdiv((string)$pay_count, (string)$start_count, 2), '100', 2) : 0; + return compact('people_count', 'spread_count', 'start_count', 'success_count', 'pay_price', 'pay_count', 'pay_rate'); + } + + /** + * 砍价统计列表 + * @param $id + * @param array $where + * @return array + */ + public function bargainStatisticsList(int $id, array $where = []) + { + /** @var StoreBargainUserServices $bargainUser */ + $bargainUser = app()->make(StoreBargainUserServices::class); + $where['bargain_id'] = $id; + return $bargainUser->bargainUserList($where); + } + + /** + * 砍价统计订单 + * @param $id + * @param array $where + * @return array + */ + public function bargainStatisticsOrder(int $id, array $where = []) + { + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + [$page, $limit] = $this->getPageValue(); + $list = $orderServices->activityStatisticsOrder($id, 2, $where, $page, $limit); + $where['type'] = 2; + $where['activity_id'] = $id; + $count = $orderServices->count($where); + foreach ($list as &$item) { + if ($item['is_del'] || $item['is_system_del']) { + $item['status'] = '已删除'; + } else if ($item['paid'] == 0 && $item['status'] == 0) { + $item['status'] = '未支付'; + } else if ($item['paid'] == 1 && $item['status'] == 4 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status'] = '部分发货'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 2) { + $item['status'] = '已退款'; + } else if ($item['paid'] == 1 && $item['status'] == 5 && $item['refund_status'] == 0) { + $item['status'] = $item['shipping_type'] == 2 ? '部分核销' : '部分收货'; + $item['_status'] = 12;//已支付 部分核销 + } else if ($item['paid'] == 1 && $item['refund_status'] == 1) { + $item['status'] = '申请退款'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 4) { + $item['status'] = '退款中'; + } else if ($item['paid'] == 1 && $item['status'] == 0 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status'] = '未发货'; + $item['_status'] = 2;//已支付 未发货 + } else if ($item['paid'] == 1 && in_array($item['status'], [0, 1]) && $item['shipping_type'] == 2 && $item['refund_status'] == 0) { + $item['status'] = '未核销'; + } else if ($item['paid'] == 1 && in_array($item['status'], [1, 5]) && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status'] = '待收货'; + } else if ($item['paid'] == 1 && $item['status'] == 2 && $item['refund_status'] == 0) { + $item['status'] = '待评价'; + } else if ($item['paid'] == 1 && $item['status'] == 3 && $item['refund_status'] == 0) { + $item['status'] = '已完成'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 3) { + $item['status'] = '部分退款'; + } else { + $item['status'] = '未知'; + } + $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']); + $item['pay_time'] = $item['pay_time'] ? date('Y-m-d H:i:s', $item['pay_time']) : ''; + } + return compact('list', 'count'); + } +} diff --git a/app/services/activity/collage/UserCollagePartakeServices.php b/app/services/activity/collage/UserCollagePartakeServices.php new file mode 100644 index 0000000..7114d9d --- /dev/null +++ b/app/services/activity/collage/UserCollagePartakeServices.php @@ -0,0 +1,863 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\collage; + +use app\services\BaseServices; +use app\dao\activity\collage\UserCollagePartakeDao; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\store\SystemStoreServices; +use app\services\order\StoreCartServices; +use app\services\user\level\SystemUserLevelServices; +use app\services\user\member\MemberCardServices; +use app\services\user\UserServices; +use think\exception\ValidateException; + +/** + * + * Class UserCollagePartakeServices + * @package app\services\activity\collage + * @mixin UserCollagePartakeDao + */ +class UserCollagePartakeServices extends BaseServices +{ + /** + * UserCollagePartakeServices constructor. + * @param UserCollagePartakeDao $dao + */ + public function __construct(UserCollagePartakeDao $dao) + { + $this->dao = $dao; + } + + /**用户拼单/桌码商品统计 + * @param array $where + * @param string $numType + * @param int $collage_id + * @param int $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserPartakeCount(array $where, string $numType = '0', int $collage_id = 0, int $store_id = 0) + { + $count = 0; + $ids = []; + $cartNums = []; + $cartList = $this->dao->getUserPartakeList($where, 'id,cart_num,product_id'); + if ($cartList) { + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $productInfos = $storeProductServices->getColumn([['id', 'in', array_column($cartList, 'product_id')]], 'id,pid,type,relation_id', 'id'); + foreach ($cartList as $cart) { + $productInfo = $productInfos[$cart['product_id']] ?? []; + if (!$productInfo) continue; + if (in_array($productInfo['type'], [0, 2]) || ($productInfo['type'] == 1 && $productInfo['relation_id'] == $store_id) || ($productInfo['type'] == 1 && $productInfo['pid'] > 0)) { + $ids[] = $cart['id']; + $cartNums[] = $cart['cart_num']; + } + } + if ($numType) { + $count = count($ids); + } else { + $count = array_sum($cartNums); + } + } + return compact('count', 'ids'); + } + + /**获取购物车列表 + * @param array $where + * @param int $page + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPartakeList(array $where, int $page = 0, int $limit = 0, array $with = []) + { + return $this->search($where)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(count($with), function ($query) use ($with, $where) { + $query->with($with); + })->order('add_time DESC')->select()->toArray(); + } + + /** + * 处理购物车数据 + * @param int $uid + * @param array $cartList + * @param array $addr + * @param int $shipping_type + * @param int $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function handleCartList(int $uid, array $cartList, int $shipping_type = 1, int $store_id = 0) + { + if (!$cartList) { + return [$cartList, [], [], [], 0, [], []]; + } + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + /** @var MemberCardServices $memberCardService */ + $memberCardService = app()->make(MemberCardServices::class); + $vipStatus = $memberCardService->isOpenMemberCardCache('vip_price', false); + $tempIds = []; + $userInfo = []; + $discount = 100; + $productIds = $allStock = $attrUniquesArr = []; + if ($uid) { + /** @var UserServices $user */ + $user = app()->make(UserServices::class); + $userInfo = $user->getUserCacheInfo($uid); + //用户等级是否开启 + if (sys_config('member_func_status', 1) && $userInfo) { + $userInfo = $userInfo->toArray(); + /** @var SystemUserLevelServices $systemLevel */ + $systemLevel = app()->make(SystemUserLevelServices::class); + $discount = $systemLevel->getDiscount($uid, (int)$userInfo['level'] ?? 0); + } + } + if ($store_id) {//平台商品,在门店购买 验证门店库存 + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + /** @var StoreBranchProductServices $branchProductServics */ + $branchProductServics = app()->make(StoreBranchProductServices::class); + foreach ($cartList as $cart) { + $productInfo = $cart['productInfo'] ?? []; + if (!$productInfo) continue; + if ($productInfo['type'] == 1) {//平台、供应商商品验证门店库存 + continue; + } + $productIds[] = $cart['product_id']; + $suk = $skuValueServices->value(['unique' => $cart['product_attr_unique'], 'product_id' => $cart['product_id'], 'type' => 0], 'suk'); + $branchProductInfo = $branchProductServics->isValidStoreProduct((int)$cart['product_id'], $store_id); + if (!$branchProductInfo) { + continue; + } + $attrValue = $skuValueServices->get(['suk' => $suk, 'product_id' => $branchProductInfo['id'], 'type' => 0]); + if (!$attrValue) { + continue; + } + $allStock[$attrValue['unique']] = $attrValue['stock']; + $attrUniquesArr[$cart['product_attr_unique']] = $attrValue['unique']; + } + } else { + $productIds = array_unique(array_column($cartList, 'product_id')); + } + + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeInfo = $storeServices->getNearbyStore(['id' => $store_id], '', '', '', 1); + $valid = $invalid = []; + foreach ($cartList as &$item) { + if (isset($item['productInfo']['delivery_type'])) { + $item['productInfo']['delivery_type'] = is_string($item['productInfo']['delivery_type']) ? explode(',', $item['productInfo']['delivery_type']) : $item['productInfo']['delivery_type']; + } else { + $item['productInfo']['delivery_type'] = []; + } + $item['productInfo']['express_delivery'] = in_array(1, $item['productInfo']['delivery_type']); + $item['productInfo']['store_mention'] = in_array(2, $item['productInfo']['delivery_type']); + $item['productInfo']['store_delivery'] = in_array(3, $item['productInfo']['delivery_type']); + + if (isset($item['attrInfo']) && $item['attrInfo'] && (!isset($item['productInfo']['attrInfo']) || !$item['productInfo']['attrInfo'])) { + $item['productInfo']['attrInfo'] = $item['attrInfo'] ?? []; + } + $item['attrStatus'] = isset($item['productInfo']['attrInfo']['stock']) && $item['productInfo']['attrInfo']['stock']; + $item['productInfo']['attrInfo']['image'] = $item['productInfo']['attrInfo']['image'] ?? $item['productInfo']['image'] ?? ''; + $item['productInfo']['attrInfo']['suk'] = $item['productInfo']['attrInfo']['suk'] ?? '已失效'; + if (isset($item['productInfo']['attrInfo'])) { + $item['productInfo']['attrInfo'] = get_thumb_water($item['productInfo']['attrInfo']); + } + $item['productInfo'] = get_thumb_water($item['productInfo']); + $productInfo = $item['productInfo']; + //门店独立商品 + $isBranchProduct = isset($productInfo['type']) && isset($productInfo['pid']) && $productInfo['type'] == 1 && !$productInfo['pid']; + $product_store_id = $isBranchProduct ? $productInfo['relation_id'] : 0; + if (isset($productInfo['attrInfo']['product_id']) && $item['product_attr_unique']) { + $item['costPrice'] = $productInfo['attrInfo']['cost'] ?? 0; + $item['trueStock'] = $item['branch_stock'] = $productInfo['attrInfo']['stock'] ?? 0; + $item['branch_sales'] = $productInfo['attrInfo']['sales'] ?? 0; + $item['truePrice'] = $productInfo['attrInfo']['price'] ?? 0; + $item['sum_price'] = $productInfo['attrInfo']['price'] ?? 0; + if (!$isBranchProduct) { + [$truePrice, $vip_truePrice, $type] = $productServices->setLevelPrice($productInfo['attrInfo']['price'] ?? 0, $uid, $userInfo, $vipStatus, $discount, $productInfo['attrInfo']['vip_price'] ?? 0, $productInfo['is_vip'] ?? 0, true); + $item['truePrice'] = $truePrice; + $item['vip_truePrice'] = $vip_truePrice; + $item['price_type'] = $type; + } + } else { + $item['costPrice'] = $item['productInfo']['cost'] ?? 0; + $item['trueStock'] = $item['branch_sales'] = $item['productInfo']['stock'] ?? 0; + $item['branch_sales'] = $item['productInfo']['sales'] ?? 0; + $item['truePrice'] = $item['productInfo']['price'] ?? 0; + $item['sum_price'] = $item['productInfo']['price'] ?? 0; + if (!$isBranchProduct) { + [$truePrice, $vip_truePrice, $type] = $productServices->setLevelPrice($item['productInfo']['price'] ?? 0, $uid, $userInfo, $vipStatus, $discount, $item['productInfo']['vip_price'] ?? 0, $item['productInfo']['is_vip'] ?? 0, true); + $item['truePrice'] = $truePrice; + $item['vip_truePrice'] = $vip_truePrice; + $item['price_type'] = $type; + } + } + $item['is_true_stock'] = $item['trueStock'] >= $item['cart_num'] ? true : false; + $item['total_price'] = bcmul((string)$item['truePrice'], (string)$item['cart_num'], 2); + $item['sum_price'] = bcmul((string)$item['sum_price'], (string)$item['cart_num'], 2); + if (isset($item['status']) && $item['status'] == 0) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品已失效'; + $invalid[] = $item; + } elseif (($item['productInfo']['type'] ?? 0) == 1 && ($item['productInfo']['pid'] ?? 0) == 0 && $storeInfo && ($item['productInfo']['relation_id'] ?? 0) != $storeInfo['id']) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif ((isset($item['productInfo']['delivery_type']) && !$item['productInfo']['delivery_type']) || in_array($item['productInfo']['product_type'], [1, 2, 3])) { + $item['is_valid'] = 1; + $valid[] = $item; + } else { + $condition = !in_array(isset($item['productInfo']['product_id']) ? $item['productInfo']['product_id'] : $item['productInfo']['id'], $productIds) || $item['cart_num'] > ($allStock[$attrUniquesArr[$item['product_attr_unique']] ?? ''] ?? 0); + switch ($shipping_type) { + case -1://购物车列表展示 + if ($isBranchProduct && $store_id && ($store_id != $product_store_id || !in_array(3, $item['productInfo']['delivery_type']))) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } else { + $item['is_valid'] = 1; + $valid[] = $item; + } + break; + case 1: + //不送达 + if (in_array($item['productInfo']['temp_id'], $tempIds) || (isset($item['productInfo']['delivery_type']) && !in_array(1, $item['productInfo']['delivery_type']) && !in_array(3, $item['productInfo']['delivery_type']))) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif ($isBranchProduct && $store_id && ($store_id != $product_store_id || !in_array(3, $item['productInfo']['delivery_type']))) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif (in_array($productInfo['type'], [0, 2]) && $store_id && ($condition || (!in_array(2, $item['productInfo']['delivery_type']) && !in_array(3, $item['productInfo']['delivery_type'])))) {//平台商品 在门店购买 验证门店库存 + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } else { + $item['is_valid'] = 1; + $valid[] = $item; + } + break; + case 2: + //不支持到店自提 + if (isset($item['productInfo']['delivery_type']) && $item['productInfo']['delivery_type'] && !in_array(2, $item['productInfo']['delivery_type'])) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif ($isBranchProduct && $store_id && $store_id != $product_store_id) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif ($item['productInfo']['product_type'] == 1) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif (in_array($productInfo['type'], [0, 2]) && $store_id && $condition) {//平台、供应商商品 在门店购买 验证门店库存 + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } else { + $item['is_valid'] = 1; + $valid[] = $item; + } + break; + case 4: + //无库存||下架 + if ($isBranchProduct && $store_id && $store_id != $product_store_id) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif (in_array($productInfo['type'], [0, 2]) && $store_id && $condition) { + $item['is_valid'] = 0; + $invalid[] = $item; + } else { + $item['is_valid'] = 1; + $valid[] = $item; + } + break; + default: + $item['is_valid'] = 1; + $valid[] = $item; + break; + } + } + unset($item['attrInfo']); + } + return [$cartList, $valid, $invalid]; + } + + /**用户添加拼单商品 + * @param int $uid + * @param int $productId + * @param int $cart_num + * @param string $product_attr_unique + * @param int $collageId + * @param int $storeId + * @param int $type + * @param int $isAdd + * @return bool|\crmeb\basic\BaseModel|mixed|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function addUserPartakeProduct(int $uid, int $productId, int $cart_num = 1, string $product_attr_unique = '', int $collageId = 0, int $storeId = 0, int $type = 0, int $isAdd = 1) + { + if ($cart_num < 1) $cart_num = 1; + + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + + if ($product_attr_unique == '') { + $product_attr_unique = $attrValueServices->value(['product_id' => $productId, 'type' => 0], 'unique'); + } + //该商品已选择库存 + $sum_cart_num = $this->dao->sum(['collate_code_id' => $collageId, 'product_id' => $productId, 'product_attr_unique' => $product_attr_unique, 'store_id' => $storeId, 'status'=>1],'cart_num'); + + //检测库存限量 + /** @var StoreCartServices $cartServices */ + $cartServices = app()->make(StoreCartServices::class); + [$attrInfo, $product_attr_unique, $bargainPriceMin, $cart_num, $productInfo] = $cartServices->checkProductStock( + $uid, + $productId, + $cart_num, + $storeId, + $product_attr_unique, + false, + $type, + 0, + 0, + $sum_cart_num + ); + $product_type = $productInfo['product_type']; + $cart = $this->dao->getOne(['uid' => $uid, 'collate_code_id' => $collageId, 'product_id' => $productId, 'product_attr_unique' => $product_attr_unique, 'store_id' => $storeId, 'status'=>1]); + if ($cart) { + if ($isAdd) { + $cart->cart_num = $cart->cart_num + $cart_num; + } else { + $cart->cart_num = $cart->cart_num - $cart_num; + } + if ($cart->cart_num == 0) { + $res = $this->dao->delete($cart->id); + } else { + $cart->add_time = time(); + $res = $cart->save(); + } + } else { + $data = [ + 'uid' => $uid, + 'collate_code_id' => $collageId, + 'product_id' => $productId, + 'product_attr_unique' => $product_attr_unique, + 'product_type' => $product_type, + 'store_id' => $storeId, + 'cart_num' => $cart_num, + 'add_time' => time() + ]; + $res = $this->dao->save($data); + } + return $res; + } + + /**重组数组 + * @param array $array + * @param int $uid + * @param int $sponsor_uid + * @return array + */ + public function array_val_chunk(array $array, int $uid, int $sponsor_uid, int $status) + { + $result = []; + foreach ($array as $key => &$value) { + if (!$value['userInfo']) { + $value['userInfo'] = [ + 'uid' => 0, + 'nickname' => '该用户已注销' + ]; + } + $result [$value ['uid']]['userInfo'] = $value['userInfo']; + unset($value['userInfo']); + $result [$value ['uid']]['goods'][] = $value; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (array_key_exists($uid, $result)) { + $undata = $result[$uid]; + } else { + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + $userInfo = [ + 'uid' => 0, + 'nickname' => '该用户已注销' + ]; + } + if ($status >= 2) { + $undata = []; + } else { + $undata = [ + 'userInfo' => $userInfo, + 'goods' => [] + ]; + } + } + if (array_key_exists($sponsor_uid, $result)) { + $sponsordata = $result[$sponsor_uid]; + } else { + $userInfo = $userServices->getUserInfo($sponsor_uid); + if (!$userInfo) { + $userInfo = [ + 'uid' => 0, + 'nickname' => '该用户已注销' + ]; + } + $sponsordata = [ + 'userInfo' => $userInfo, + 'goods' => [] + ]; + } + foreach ($result as $key => $item) { + if ($key == $uid || $key == $sponsor_uid) { + unset($result[$key]); + } + } + if ($uid == $sponsor_uid) { + array_unshift($result, $undata); + } else { + if ($undata) { + array_unshift($result, $undata, $sponsordata); + } else { + array_unshift($result, $sponsordata); + } + } + foreach ($result as $key => $item) { + $truePrices = array_column($item['goods'], 'sum_price'); + $sum = array_sum($truePrices); + $result[$key]['sum_price'] = $sum; + $result[$key]['sumPrice'] = number_format($sum, 2); + } + return $result; + } + + /** + * 获取所有人拼单商品 + * @param int $collage_id + * @param int $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserPartakeProduct(int $collage_id, int $uid) + { + /** @var UserCollageServices $collageServices */ + $collageServices = app()->make(UserCollageServices::class); + $collage = $collageServices->get($collage_id); + $where = ['collate_code_id' => $collage_id, 'status' => 1]; + [$cartList, $valid, $invalid] = $this->getUserCashierTablePartakeProduct($collage['uid'], $collage['store_id'], $where, '', ['userInfo', 'productInfo', 'attrInfo']); + $cartList = $this->array_val_chunk($cartList, $uid, $collage['uid'], $collage['status']); + return $cartList; + } + + /** + * 用户清空拼单 + * @param int $collage_id + * @param int $uid + * @return bool + */ + public function emptyUserCollagePartake(int $collage_id, int $uid) + { + return $this->dao->del(['collate_code_id' => $collage_id, 'uid' => $uid]); + } + + /** + * 复制他人拼单商品 + * @param int $collage_id + * @param int $c_uid + * @param int $uid + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function duplicateUserCollagePartake(int $collage_id, int $c_uid, int $uid) + { + $data = $this->dao->getUserPartakeList(['uid' => $c_uid, 'collate_code_id' => $collage_id, 'status' => 1], 'product_id,product_type,store_id,product_attr_unique,cart_num'); + foreach ($data as $key => $item) { + $res = $this->addUserPartakeProduct($uid, (int)$item['product_id'], $item['cart_num'], $item['product_attr_unique'], $collage_id, $item['store_id'], 9, 1); + if(!$res) continue; + } + return true; + } + + /** + * 拼单商品写入购物车 + * @param int $collage_id + * @param int $uid + * @param int $type + * @return false|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function allUserSettleAccountsCollage(int $collage_id, int $uid, int $type) + { + $data = $this->dao->getUserPartakeList(['collate_code_id' => $collage_id, 'status' => 1], 'id,product_id,product_type,store_id,product_attr_unique,cart_num,status,is_settle'); + if (count($data) <= 0) return false; + /** @var StoreCartServices $cartServices */ + $cartServices = app()->make(StoreCartServices::class); + $cartIds = []; + foreach ($data as $key => $item) { + [$key, $cart_num] = $cartServices->setCart($uid, (int)$item['product_id'], (int)$item['cart_num'], $item['product_attr_unique'], $type, true, $collage_id, 0); + if ($key && !$item['is_settle']) { + $this->dao->update($item['id'], ['is_settle' => 1]); + } + $cartIds[] = $key; + } + if (count($cartIds) < 0) return false; + /** @var UserCollageServices $collageServices */ + $collageServices = app()->make(UserCollageServices::class); + $res = $collageServices->userUpdate($collage_id, ['status' => 1]); + if (!$res) return false; + $cartIds = array_unique($cartIds); + $cartIds = implode(',', $cartIds); + return $cartIds; + } + + /** + * 根据商品id获取购物车数量 + * @param array $ids + * @param int $uid + * @param int $storeId + * @param int $collate_code_id + * @return mixed + */ + public function productIdByCartNum(array $ids, int $uid, int $storeId = 0, int $collate_code_id = 0) + { + return $this->search(['product_id' => $ids, 'uid' => $uid, 'store_id' => $storeId, 'collate_code_id' => $collate_code_id])->group('product_attr_unique')->column('cart_num,product_id', 'product_attr_unique'); + } + + /** + * 用户注销信息删除拼单商品 + * @param int $uid + * @return bool + */ + public function logOffUserCollagePartake(int $uid) + { + return $this->dao->del(['uid' => $uid, 'is_settle' => 0]); + } + + /** + * 获取桌码商品 + * @param int $tableId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserTablePartakeProduct(int $tableId) + { + /** @var UserCollageServices $collageServices */ + $collageServices = app()->make(UserCollageServices::class); + $table = $collageServices->get($tableId); + if ($table['status'] == -1) { + throw new ValidateException('桌码已取消'); + } + $where = ['collate_code_id' => $tableId, 'status' => 1]; + [$cartList, $valid, $invalid] = $this->getUserCashierTablePartakeProduct($table['uid'], $table['store_id'], $where, '', ['userInfo', 'productInfo', 'attrInfo']); + $result = []; + foreach ($cartList as $key => &$value) { + if (!$value['userInfo']) { + $value['userInfo'] = [ + 'uid' => 0, + 'nickname' => '该用户已注销' + ]; + } + $result [$value ['uid']]['userInfo'] = $value['userInfo']; + $result [$value ['uid']]['order_time'] = date('H:i', $value['add_time']); + unset($value['userInfo']); + $result [$value ['uid']]['goods'][] = $value; + } + return $result; + } + + /** + * 订单商品信息 + * @param int $uid + * @param int $store_id + * @param array $where + * @param string $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCashierTablePartakeProduct(int $uid, int $store_id, array $where, string $field = '*', array $with = []) + { + $cartList = $this->dao->getUserPartakeProductList($where, $field, $with); + return $this->handleCartList($uid, $cartList, -1, $store_id); + } + + /** + * 获取订单商品信息 + * @param int $uid + * @param int $store_id + * @param array $where + * @param string $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCashierTablePartakeProduct(array $where, string $field = '*', array $with = []) + { + $cartList = $this->dao->getUserPartakeProductList($where, $field, $with); + if (!$cartList) return []; + $data = $this->dataPartake($cartList, true); + return $data; + } + + /** + * 桌码商品处理 + * @param int $uid + * @param array $where + * @param int $store_id + * @return mixed + */ + public function getTableCatePartakeList(int $uid, array $where, int $store_id) + { + $data = $this->getPartakeList($where, 0, 0, ['productInfo', 'attrInfo']); + $data = $this->dataPartake($data, false); + [$data, $valid, $invalid] = $this->handleCartList($uid, $data['cart'], -1, $store_id); + return $valid; + } + + /** + * 数据处理 + * @param array $data + * @param bool $is_price + * @return array + */ + public function dataPartake(array $data, bool $is_price) + { + $cart = []; + $sum_price = 0; + $cart_num = 0; + foreach ($data as $key => $item) { + if (isset($item['id'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['id'] = $item['id']; + } + if (isset($item['uid'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['uid'] = $item['uid']; + } + if (isset($item['collate_code_id'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['collate_code_id'] = $item['collate_code_id']; + } + if (isset($item['product_id'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['product_id'] = $item['product_id']; + } + if (isset($item['product_type'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['product_type'] = $item['product_type']; + } + if (isset($item['product_attr_unique'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['product_attr_unique'] = $item['product_attr_unique']; + } + if (isset($item['status'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['status'] = $item['status']; + } + if (isset($item['is_print'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['is_print'] = $item['is_print']; + } + if (isset($item['is_settle'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['is_settle'] = $item['is_settle']; + } + if (isset($item['add_time'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['add_time'] = $item['add_time']; + } + if (isset($item['productInfo'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['productInfo'] = $item['productInfo']; + } + if (isset($item['attrInfo']) && isset($item['productInfo'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['productInfo']['attrInfo'] = $item['attrInfo']; + unset($item['attrInfo']); + } + if (isset($cart[$item['product_id'] . $item['product_attr_unique']]['cart_num'])) { + $cart[$item['product_id'] . $item['product_attr_unique']]['cart_num'] += $item['cart_num']; + } else { + $cart[$item['product_id'] . $item['product_attr_unique']]['cart_num'] = $item['cart_num']; + } + $cart_num += $item['cart_num']; + if ($is_price && isset($item['productInfo'])) { + $sum_price = bcadd((string)$sum_price, bcmul((string)$item['cart_num'], (string)$item['productInfo']['price'], 2), 2); + } + } + return compact('cart', 'sum_price', 'cart_num'); + } + + /** + * 桌码商品写入购物车 + * @param int $tableId + * @param int $uid + * @param int $type + * @return false|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function allUserSettleAccountsTableCode(int $tableId, int $uid, int $type) + { + $data = $this->dao->getUserPartakeList(['collate_code_id' => $tableId, 'status' => 1], 'id,product_id,product_type,store_id,product_attr_unique,cart_num,status,is_settle'); + if (count($data) <= 0) return false; + /** @var StoreCartServices $cartServices */ + $cartServices = app()->make(StoreCartServices::class); + $cartIds = []; + $data = $this->dataPartake($data, false); + foreach ($data['cart'] as $key => $item) { + try { + [$key, $cart_num] = $cartServices->setCart($uid, (int)$item['product_id'], $item['cart_num'], $item['product_attr_unique'], $type, true, $tableId, 0); + if ($key && !$item['is_settle']) { + $this->dao->update($item['id'], ['is_settle' => 1]); + } + $cartIds[] = $key; + } catch (\Exception $e) { + continue; + } + } + if (count($cartIds) < 0) return false; + $cartIds = array_unique($cartIds); + $cartIds = implode(',', $cartIds); + return $cartIds; + } + + /** + * 获取最后一条记录 + * @param array $where + * @return array|\crmeb\basic\BaseModel|mixed|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMaxTime(array $where) + { + return $this->dao->getUserPartake($where); + } + + /** + * 获取用户信息 + * @param $where + * @param $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function tableCodeUserAll($where, $store_id) + { + $uids = $this->dao->getUserPartakeList(['collate_code_id' => $where['table_id'], 'store_id' => $store_id], 'uid', ['userInfo']); + foreach ($uids as $key => $item) { + if (!$item['userInfo']) { + unset($uids[$key]); + } + } + $uids = array_unique($uids, SORT_REGULAR); + return array_merge($uids); + } + + /** + * 获取打印订单的商品信息 + * @param array $table + * @return array + */ + public function getCartInfoPrintProduct(array $table) + { + $where = ['collate_code_id' => $table['id'], 'is_print' => 0, 'status' => 1]; + $data = $this->getCashierTablePartakeProduct($where, '', ['productInfo', 'attrInfo']); + $product = []; + if (!$data || !isset($data['cart'])) return $product; + foreach ($data['cart'] as $item) { + $value = $item; + $value['productInfo']['store_name'] = $value['productInfo']['store_name'] ?? ""; + $value['productInfo']['store_name'] = substrUTf8($value['productInfo']['store_name'], 10, 'UTF-8', ''); + $product[] = $value; + } + return $product; + } + + /** + * 用户清空购物车 + * @param int $table_id + * @return bool + */ + public function emptyUserTablePartake(int $table_id) + { + return $this->dao->del(['collate_code_id' => $table_id]); + } + + /** + * 收银台购物车数量操作 + * @param array $where + * @param int $store_id + * @return bool|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function editTableCartProduct(array $where, int $store_id) + { + if ($where['cartNum'] < 1) { + $cart_num = 1; + } else { + $cart_num = $where['cartNum']; + } + $cart = $this->dao->getOne(['collate_code_id' => $where['tableId'], 'product_id' => $where['productId'], 'product_attr_unique' => $where['uniqueId'], 'store_id' => $store_id]); + if (!$cart) return false; + if ($where['isAdd']) { + $cart->cart_num = $cart->cart_num + $cart_num; + } else { + $cart->cart_num = $cart->cart_num - $cart_num; + } + if ($cart->cart_num == 0) { + $res = $this->dao->delete($cart->id); + } else { + $res = $cart->save(); + } + return $res; + } + + /** + * 用户删除拼单、桌码商品 + * @param int $collate_code_id + * @param int $storeId + * @param int $productId + * @param string $uniqueId + * @return bool + */ + public function delUserCatePartake(int $collate_code_id, int $storeId, int $productId, string $uniqueId) + { + return $this->dao->del(['collate_code_id' => $collate_code_id, 'store_id' => $storeId, 'product_id' => $productId, 'product_attr_unique' => $uniqueId]); + } +} diff --git a/app/services/activity/collage/UserCollageServices.php b/app/services/activity/collage/UserCollageServices.php new file mode 100644 index 0000000..e8b0b44 --- /dev/null +++ b/app/services/activity/collage/UserCollageServices.php @@ -0,0 +1,379 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\collage; + +use app\jobs\notice\PrintJob; +use app\services\activity\table\TableQrcodeServices; +use app\services\BaseServices; +use app\dao\activity\collage\UserCollageDao; +use app\services\message\NoticeService; +use app\services\system\config\ConfigServices; +use crmeb\services\printer\Printer; +use think\exception\ValidateException; +use app\services\other\CategoryServices; +use crmeb\services\SystemConfigService; +use crmeb\utils\Arr; +use think\facade\Log; +use app\services\user\UserServices; +use app\services\user\UserBillServices; + +/** + * + * Class UserCollageServices + * @package app\services\activity\collage + * @mixin UserCollageDao + */ +class UserCollageServices extends BaseServices +{ + + /** + * UserCollageServices constructor. + * @param UserCollageDao $dao + */ + public function __construct(UserCollageDao $dao) + { + $this->dao = $dao; + } + + /** + * 验证拼单是否开启 + * @return bool + */ + public function checkCollageStatus() + { + //门店是否开启 + if (!sys_config('store_func_status', 1)) { + return false; + } + //桌码是否开启 + if (!sys_config('store_splicing_switch', 1)) { + return false; + } + return true; + } + + /** + * 验证桌码功能是否开启 + * @return bool + */ + public function checkTabldeCodeStatus(int $store_id) + { + //门店是否开启 + if (!sys_config('store_func_status', 1)) { + return false; + } + //桌码是否开启 + if (!store_config($store_id, 'store_code_switch', 1)) { + return false; + } + return true; + } + + /** + * 拼单、桌码状态 + * @param $id + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function collageStatus($id) + { + return $this->dao->get($id, ['status']); + } + + /** + * 记录发起拼单 + * @param int $uid + * @param int $store_id + * @param int $address_id + * @param int $shipping_type + * @return \crmeb\basic\BaseModel|\think\Model + */ + public function setUserCollage(int $uid, int $store_id, int $address_id, int $shipping_type) + { + if ($this->dao->be(['uid' => $uid, 'type' => 9, 'store_id' => $store_id, 'address_id' => $address_id, 'shipping_type' => $shipping_type, 'status' => [0, 1]])) throw new ValidateException('您已在拼单中,不能再次拼单!'); + $data = [ + 'uid' => $uid, + 'type' => 9, + 'store_id' => $store_id, + 'address_id' => $address_id, + 'shipping_type' => $shipping_type, + 'add_time' => time() + ]; + $res = $this->dao->save($data); + return $res; + } + + /** + * 获取拼单/桌码信息 + * @param array $where + * @param string|null $field + * @param array $with + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCollage(array $where, ?string $field = '*', array $with = []) + { + return $this->dao->getOne($where, $field, $with); + } + + /** + * 修改拼单 + * @param int $id + * @param array $where + * @return mixed + */ + public function userUpdate(int $id, array $where) + { + return $this->dao->update($id, $where); + } + + /** + * 记录桌码 + * @param int $uid + * @param int $store_id + * @param int $qrcode_id + * @param int $number + * @return bool|\crmeb\basic\BaseModel|\think\Model + */ + public function setUserTableCode(int $uid, int $store_id, int $qrcode_id, int $number) + { + //1=>合并结账 2=>单独结账 + $store_checkout_method = store_config($store_id, 'store_checkout_method', 1); + if ($store_checkout_method == 1) { + $where = ['store_id' => $store_id, 'qrcode_id' => $qrcode_id, 'checkout_method' => $store_checkout_method, 'type' => 10, 'status' => [0, 1]]; + } else { + $where = ['uid' => $uid, 'store_id' => $store_id, 'qrcode_id' => $qrcode_id, 'checkout_method' => $store_checkout_method, 'type' => 10, 'status' => [0, 1]]; + } + $table = $this->dao->getOne($where); + if ($table) return $table; + $max = $this->dao->getMaxSerialNumber(['store_id' => $store_id, 'type' => 10]); + $data = [ + 'uid' => $uid, + 'type' => 10, + 'store_id' => $store_id, + 'checkout_method' => $store_checkout_method, + 'qrcode_id' => $qrcode_id, + 'number_diners' => $number, + 'serial_number' => $max ? '00' . ($max + 1) : '001', + 'add_time' => time() + ]; + return $this->dao->save($data); + } + + /** + * 检查是否换桌 code 0 :换桌 1 :未换桌 2:无记录 + * @param int $uid + * @param int $store_id + * @param int $qrcode_id + * @param int $store_checkout_method + * @return array|int[] + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function isUserChangingTables(int $uid, int $store_id, int $qrcode_id, int $store_checkout_method) + { + $where = ['store_id' => $store_id, 'uid' => $uid, 'checkout_method' => $store_checkout_method, 'type' => 10, 'status' => [0, 1]]; + $whereTo = ['store_id' => $store_id, 'qrcode_id' => $qrcode_id, 'checkout_method' => $store_checkout_method, 'type' => 10, 'status' => [0, 1]]; + $table = $this->dao->getOne($where); + $table1 = $this->dao->getOne($whereTo); + if ($table) { + if ($table['qrcode_id'] != $qrcode_id) { + return ['code' => 0, 'tableId' => $table['id']]; + } else { + return ['code' => 1, 'tableId' => $table['id']]; + } + } else { + if ($store_checkout_method == 1) { + /** @var UserCollagePartakeServices $partakeService */ + $partakeService = app()->make(UserCollagePartakeServices::class); + $partake_where = ['uid' => $uid, 'store_id' => $store_id, 'status' => 1]; + $partake = $partakeService->getMaxTime($partake_where); + if (!$partake) { + return ['code' => 2, 'tableId' => $table1 ? $table1['id'] : 0]; + } + $table2 = $this->dao->get($partake['collate_code_id']); + if (!$table2) { + return ['code' => 2, 'tableId' => $table1 ? $table1['id'] : 0]; + } + if (in_array($table2['status'], [0, 1])) { + if ($table2['qrcode_id'] != $qrcode_id) { + return ['code' => 0, 'tableId' => $table2['id']]; + } else { + return ['code' => 1, 'tableId' => $table2['id']]; + } + } else { + return ['code' => 2, 'tableId' => $table1 ? $table1['id'] : 0]; + } + } else { + return ['code' => 1, 'tableId' => 0]; + } + } + } + + /** + * 更换座位 处理原有商品 + * @param int $tableId + * @param int $y_tableId + * @return bool + */ + public function userChangingTables(int $tableId, int $y_tableId) + { + /** @var UserCollagePartakeServices $partakeService */ + $partakeService = app()->make(UserCollagePartakeServices::class); + $res = $partakeService->update(['collate_code_id' => $y_tableId], ['collate_code_id' => $tableId]); + $table = $this->dao->get($y_tableId); + /** @var TableQrcodeServices $qrcodeService */ + $qrcodeService = app()->make(TableQrcodeServices::class); + $res1 = $qrcodeService->update($table['qrcode_id'], ['is_use' => 0, 'eat_number' => 0, 'order_time' => 0]); + $res2 = $this->dao->update($tableId, ['number_diners' => $table['number_diners']]); + $res3 = $this->dao->delete($y_tableId); + return $res && $res1 && $res2 && $res3; + } + + /** + * 桌码订单 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreTableCodeList(array $where, array $field = ['*']) + { + [$page, $limit] = $this->getPageValue(); + $data = $this->dao->searchTableCodeList($where, $field, $page, $limit, ['qrcode', 'orderId']); + $count = $this->dao->count($where); + /** @var UserCollagePartakeServices $partakeService */ + $partakeService = app()->make(UserCollagePartakeServices::class); + foreach ($data as $key => &$datum) { + $datum['qrcode']['category'] = []; + if (isset($datum['qrcode']['cate_id']) && $datum['qrcode']['cate_id']) { + /** @var CategoryServices $categoryService */ + $categoryService = app()->make(CategoryServices::class); + $datum['qrcode']['category'] = $categoryService->get((int)$datum['qrcode']['cate_id'], ['name']); + } else { + $datum['qrcode']['category'] = ['name' => '已删除']; + $datum['qrcode']['table_number'] = ''; + } + $dataPartake = $partakeService->getCashierTablePartakeProduct(['collate_code_id' => $datum['id'], 'status' => 1], '', ['productInfo']); + $datum['cartList'] = $dataPartake['cart'] ?? []; + $datum['sum_price'] = $dataPartake['sum_price'] ?? 0; + $datum['cart_num'] = $dataPartake['cart_num'] ?? 0; + } + return compact('data', 'count'); + } + + /** + * 确认下单 + * @param int $tableId + * @param int $store_id + * @return bool + */ + public function userTablePlaceOrder(int $tableId, int $store_id) + { + $this->dao->update($tableId, ['status' => 1]); + $print = store_config($store_id, 'store_printing_timing'); + if ($print && is_array($print) && in_array(1, $print)) { + PrintJob::dispatch('tableDoJob', [$tableId, $store_id]); + } + return true; + } + + /** + * 桌码、拼单长期未操作取消桌码、拼单记录 + * @return bool|void + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function tableCodeNotOperating(int $type = 10) + { + $where = ['type' => $type, 'status' => [0, 1]]; + $list = $this->dao->searchTableCodeList($where); + //系统预设取消订单时间段 + $keyValue = ['table_code_not_operating_time', 'collate_not_operating_time']; + //获取配置 + $systemValue = SystemConfigService::more($keyValue); + //格式化数据 + $systemValue = Arr::setValeTime($keyValue, is_array($systemValue) ? $systemValue : []); + $table_code_not_operating_time = $systemValue['table_code_not_operating_time']; + $collate_not_operating_time = $systemValue['collate_not_operating_time']; + $not_operating_time = $type == 10 ? $table_code_not_operating_time : $collate_not_operating_time; + /** @var UserCollagePartakeServices $partakeService */ + $partakeService = app()->make(UserCollagePartakeServices::class); + $not_operating_time = (int)bcmul((string)$not_operating_time, '3600', 0); + foreach ($list as $key => $item) { + if ((strtotime($item['add_time']) + $not_operating_time) < time()) { + try { + $this->transaction(function () use ($item, $partakeService) { + //修改记录状态 + $res = $this->dao->update($item['id'], ['status' => -1]); + $res = $res && $partakeService->update(['collate_code_id' => $item['id']], ['status' => 0]); + }); + } catch (\Throwable $e) { + $msg = $type == 10 ? '桌码长期未操作取消桌码记录失败' : '拼单长期未操作取消拼单记录失败'; + Log::error($msg . ',失败原因:' . $e->getMessage(), $e->getTrace()); + } + } + } + return true; + } + + /** + * 桌码打印订单 + * @param int $tableId + * @param int $store_id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function tablePrint(int $tableId, int $store_id) + { + $table = $this->get($tableId); + if($table['status']== -1){ + throw new ValidateException('桌码已取消'); + } + /** @var UserCollagePartakeServices $partakeService */ + $partakeService = app()->make(UserCollagePartakeServices::class); + $product = $partakeService->getCartInfoPrintProduct($table->toArray()); + if (!$product) { + return true; + } + $partakeService->update(['collate_code_id' => $tableId], ['is_print' => 1]); + + /** @var ConfigServices $configServices */ + $configServices = app()->make(ConfigServices::class); + [$switch, $name, $configData] = $configServices->getPrintingConfig(1, $store_id); + if (!$switch) { + throw new ValidateException('请先开启小票打印'); + } + foreach ($configData as $value) { + if (!$value) { + throw new ValidateException('请先配置小票打印开发者'); + } + } + $printer = new Printer($name, $configData); + $printer->setPrinterTableContent([ + 'name' => sys_config('site_name'), + 'tableInfo' => is_object($table) ? $table->toArray() : $table, + 'product' => $product + ])->startPrinter(); + return true; + } +} diff --git a/app/services/activity/combination/StoreCombinationServices.php b/app/services/activity/combination/StoreCombinationServices.php new file mode 100644 index 0000000..b0167c2 --- /dev/null +++ b/app/services/activity/combination/StoreCombinationServices.php @@ -0,0 +1,786 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\combination; + +use app\Request; +use app\services\BaseServices; +use app\dao\activity\combination\StoreCombinationDao; +use app\services\diy\DiyServices; +use app\services\order\StoreOrderServices; +use app\services\other\QrcodeServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\ensure\StoreProductEnsureServices; +use app\services\product\label\StoreProductLabelServices; +use app\services\product\product\StoreDescriptionServices; +use app\services\user\UserRelationServices; +use app\services\product\product\StoreProductReplyServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrResultServices; +use app\services\product\sku\StoreProductAttrServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\jobs\product\ProductLogJob; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use crmeb\services\SystemConfigService; +use think\exception\ValidateException; + +/** + * 拼团商品 + * Class StoreCombinationServices + * @package app\services\activity\combination + * @mixin StoreCombinationDao + */ +class StoreCombinationServices extends BaseServices +{ + const THODLCEG = 'ykGUKB'; + + /** + * 商品活动类型 + */ + const TYPE = 2; + + /** + * StoreCombinationServices constructor. + * @param StoreCombinationDao $dao + */ + public function __construct(StoreCombinationDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取指定条件下的条数 + * @param array $where + */ + public function getCount(array $where) + { + $this->dao->count($where); + } + + /** + * @param array $productIds + * @return mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/28 + */ + public function getPinkIdsArrayCache(array $productIds) + { + return $this->dao->cacheTag()->remember(md5('pink_ids_' . json_encode($productIds)), function () use ($productIds) { + return $this->dao->getPinkIdsArray($productIds, ['id']); + }); + } + + /** + * 获取是否有拼团商品 + * */ + public function validCombination() + { + return $this->dao->count([ + 'is_del' => 0, + 'is_show' => 1, + 'pinkIngTime' => true + ]); + } + + /** + * 拼团商品添加 + * @param int $id + * @param array $data + */ + public function saveData(int $id, array $data) + { + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $productInfo = $storeProductServices->getOne(['is_show' => 1, 'is_del' => 0, 'is_verify' => 1, 'id' => $data['product_id']]); + if (!$productInfo) { + throw new AdminException('原商品已下架或移入回收站'); + } + if ($productInfo['is_vip_product'] || $productInfo['is_presale_product']) { + throw new AdminException('该商品是预售或svip专享'); + } + $data['product_type'] = $productInfo['product_type']; + if ($data['product_type'] == 4 && !$data['delivery_type']) { + $data['delivery_type'] = $productInfo['delivery_type']; + } + $data['type'] = $productInfo['type'] ?? 0; + $data['relation_id'] = $productInfo['relation_id'] ?? 0; + $custom_form = $productInfo['custom_form'] ?? []; + $data['custom_form'] = is_array($custom_form) ? json_encode($custom_form) : $custom_form; + $data['system_form_id'] = $productInfo['system_form_id'] ?? 0; + $store_label_id = $productInfo['store_label_id'] ?? []; + $data['store_label_id'] = is_array($store_label_id) ? implode(',', $store_label_id) : $store_label_id; + $ensure_id = $productInfo['ensure_id'] ?? []; + $data['ensure_id'] = is_array($ensure_id) ? implode(',', $ensure_id) : $ensure_id; + $specs = $productInfo['specs'] ?? []; + $data['specs'] = is_array($specs) ? json_encode($specs) : $specs; + if (in_array($data['product_type'], [1, 2, 3])) { + $data['freight'] = 2; + $data['temp_id'] = 0; + $data['postage'] = 0; + } else { + if ($data['freight'] == 1) { + $data['temp_id'] = 0; + $data['postage'] = 0; + } elseif ($data['freight'] == 2) { + $data['temp_id'] = 0; + } elseif ($data['freight'] == 3) { + $data['postage'] = 0; + } + if ($data['freight'] == 2 && !$data['postage']) { + throw new AdminException('请设置运费金额'); + } + if ($data['freight'] == 3 && !$data['temp_id']) { + throw new AdminException('请选择运费模版'); + } + } + $description = $data['description']; + $detail = $data['attrs']; + $items = $data['items']; + $data['start_time'] = strtotime($data['section_time'][0]); + $data['stop_time'] = strtotime($data['section_time'][1]); + if ($data['stop_time'] < strtotime(date('Y-m-d', time()))) throw new AdminException('结束时间不能小于今天'); + $data['image'] = $data['images'][0] ?? ''; + $data['images'] = json_encode($data['images']); + $data['price'] = min(array_column($detail, 'price')); + $data['quota'] = $data['quota_show'] = array_sum(array_column($detail, 'quota')); + if ($data['quota'] > $productInfo['stock']) { + throw new ValidateException('限量不能超过商品库存'); + } + $data['stock'] = array_sum(array_column($detail, 'stock')); + unset($data['section_time'], $data['description'], $data['attrs'], $data['items']); + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + + $this->transaction(function () use ($id, $data, $description, $detail, $items, $storeDescriptionServices, $storeProductAttrServices) { + if ($id) { + $res = $this->dao->update($id, $data); + if (!$res) throw new AdminException('修改失败'); + } else { + $data['add_time'] = time(); + $res = $this->dao->save($data); + if (!$res) throw new AdminException('添加失败'); + $id = (int)$res->id; + } + $storeDescriptionServices->saveDescription((int)$id, $description, 3); + $skuList = $storeProductAttrServices->validateProductAttr($items, $detail, (int)$id, 3); + $valueGroup = $storeProductAttrServices->saveProductAttr($skuList, (int)$id, 3); + + $res = true; + foreach ($valueGroup as $item) { + $res = $res && CacheService::setStock($item['unique'], (int)$item['quota_show'], 3); + } + if (!$res) { + throw new AdminException('占用库存失败'); + } + }); + + $this->dao->cacheTag()->clear(); + } + + /** + * 拼团列表 + * @param array $where + * @return array + */ + public function systemPage(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit); + $count = $this->dao->count($where); + /** @var StorePinkServices $storePinkServices */ + $storePinkServices = app()->make(StorePinkServices::class); + $countAll = $storePinkServices->getPinkCount([]); + $countTeam = $storePinkServices->getPinkCount(['k_id' => 0, 'status' => 2]); + $countPeople = $storePinkServices->getPinkCount(['k_id' => 0]); + foreach ($list as &$item) { + $item['count_people'] = $countPeople[$item['id']] ?? 0;//拼团数量 + $item['count_people_all'] = $countAll[$item['id']] ?? 0;//参与人数 + $item['count_people_pink'] = $countTeam[$item['id']] ?? 0;//成团数量 + $item['stop_status'] = $item['stop_time'] < time() ? 1 : 0; + if ($item['is_show']) { + if ($item['start_time'] > time()) + $item['start_name'] = '未开始'; + else if ($item['stop_time'] < time()) + $item['start_name'] = '已结束'; + else if ($item['stop_time'] > time() && $item['start_time'] < time()) { + $item['start_name'] = '进行中'; + } + } else $item['start_name'] = '已结束'; + } + return compact('list', 'count'); + } + + /** + * 获取详情 + * @param int $id + * @return array|\think\Model|null + */ + public function getInfo(int $id) + { + $info = $this->dao->get($id); + if (!$info) { + throw new ValidateException('查看的商品不存在!'); + } + if ($info->is_del) { + throw new ValidateException('您查看的团团商品已被删除!'); + } + if ($info['start_time']) + $start_time = date('Y-m-d H:i:s', $info['start_time']); + + if ($info['stop_time']) + $stop_time = date('Y-m-d H:i:s', $info['stop_time']); + if (isset($start_time) && isset($stop_time)) + $info['section_time'] = [$start_time, $stop_time]; + else + $info['section_time'] = []; + unset($info['start_time'], $info['stop_time']); + $info['price'] = floatval($info['price']); + $info['postage'] = floatval($info['postage']); + $info['weight'] = floatval($info['weight']); + $info['volume'] = floatval($info['volume']); + if (!$info['delivery_type']) { + $info['delivery_type'] = [1]; + } + if ($info['postage']) { + $info['freight'] = 2; + } elseif ($info['temp_id']) { + $info['freight'] = 3; + } else { + $info['freight'] = 1; + } + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + $info['description'] = $storeDescriptionServices->getDescription(['product_id' => $id, 'type' => 3]); + $info['attrs'] = $this->attrList($id, $info['product_id']); + return $info; + } + + /** + * 获取规格 + * @param int $id + * @param int $pid + * @return mixed + */ + public function attrList(int $id, int $pid) + { + /** @var StoreProductAttrResultServices $storeProductAttrResultServices */ + $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class); + $combinationResult = $storeProductAttrResultServices->value(['product_id' => $id, 'type' => 3], 'result'); + $items = json_decode($combinationResult, true)['attr']; + $productAttr = $this->getAttr($items, $pid, 0); + $combinationAttr = $this->getAttr($items, $id, 3); + foreach ($productAttr as $pk => &$pv) { + $pv['r_price'] = $pv['price']; + foreach ($combinationAttr as &$sv) { + if ($pv['detail'] == $sv['detail']) { + $sv['r_price'] = $pv['price']; + $pv = $sv; + } + } + $pv['detail'] = json_decode($pv['detail']); + } + $attrs['items'] = $items; + $attrs['value'] = $productAttr; + foreach ($items as $key => $item) { + $header[] = ['title' => $item['value'], 'key' => 'value' . ($key + 1), 'align' => 'center', 'minWidth' => 80]; + } + $header[] = ['title' => '图片', 'slot' => 'pic', 'align' => 'center', 'minWidth' => 120]; + $header[] = ['title' => '拼团价', 'key' => 'price', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '成本价', 'key' => 'cost', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '日常售价', 'key' => 'r_price', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '库存', 'key' => 'stock', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '限量', 'key' => 'quota', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '重量(KG)', 'key' => 'weight', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '体积(m³)', 'key' => 'volume', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '商品条形码', 'key' => 'bar_code', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '商品编号', 'key' => 'code', 'align' => 'center', 'minWidth' => 80]; + $attrs['header'] = $header; + return $attrs; + } + + /** + * 获得规格 + * @param $attr + * @param $id + * @param $type + * @return array + */ + public function getAttr($attr, $id, $type) + { + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $value = attr_format($attr)[1]; + $valueNew = []; + $count = 0; + foreach ($value as $key => $item) { + $detail = $item['detail']; +// sort($item['detail'], SORT_STRING); + $suk = implode(',', $item['detail']); + $sukValue = $storeProductAttrValueServices->getSkuArray(['product_id' => $id, 'type' => $type, 'suk' => $suk], 'bar_code,code,cost,price,ot_price,stock,image as pic,weight,volume,brokerage,brokerage_two,quota,quota_show', 'suk'); + if (count($sukValue)) { + foreach (array_values($detail) as $k => $v) { + $valueNew[$count]['value' . ($k + 1)] = $v; + } + $valueNew[$count]['detail'] = json_encode($detail); + $valueNew[$count]['pic'] = $sukValue[$suk]['pic'] ?? ''; + $valueNew[$count]['price'] = $sukValue[$suk]['price'] ? floatval($sukValue[$suk]['price']) : 0; + $valueNew[$count]['cost'] = $sukValue[$suk]['cost'] ? floatval($sukValue[$suk]['cost']) : 0; + $valueNew[$count]['ot_price'] = isset($sukValue[$suk]['ot_price']) ? floatval($sukValue[$suk]['ot_price']) : 0; + $valueNew[$count]['stock'] = $sukValue[$suk]['stock'] ? intval($sukValue[$suk]['stock']) : 0; +// $valueNew[$count]['quota'] = $sukValue[$suk]['quota'] ? intval($sukValue[$suk]['quota']) : 0; + $valueNew[$count]['quota'] = isset($sukValue[$suk]['quota_show']) && $sukValue[$suk]['quota_show'] ? intval($sukValue[$suk]['quota_show']) : 0; + $valueNew[$count]['code'] = $sukValue[$suk]['code'] ?? ''; + $valueNew[$count]['bar_code'] = $sukValue[$suk]['bar_code'] ?? ''; + $valueNew[$count]['weight'] = $sukValue[$suk]['weight'] ? floatval($sukValue[$suk]['weight']) : 0; + $valueNew[$count]['volume'] = $sukValue[$suk]['volume'] ? floatval($sukValue[$suk]['volume']) : 0; + $valueNew[$count]['brokerage'] = $sukValue[$suk]['brokerage'] ? floatval($sukValue[$suk]['brokerage']) : 0; + $valueNew[$count]['brokerage_two'] = $sukValue[$suk]['brokerage_two'] ? floatval($sukValue[$suk]['brokerage_two']) : 0; + $valueNew[$count]['_checked'] = $type != 0; + $count++; + } + } + return $valueNew; + } + + /** + * 根据id获取拼团数据列表 + * @param array $ids + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + + public function getCombinationList() + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->combinationList(['is_del' => 0, 'is_show' => 1, 'pinkIngTime' => true, 'storeProductId' => true], $page, $limit); + foreach ($list as &$item) { + $item['image'] = set_file_url($item['image']); + $item['price'] = floatval($item['price']); + $item['product_price'] = floatval($item['product_price']); + } + return $list; + } + + /** + * 拼团商品详情 + * @param Request $request + * @param int $id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function combinationDetail(Request $request, int $id) + { + $uid = (int)$request->uid(); + $storeInfo = $this->dao->cacheTag()->remember('' . $id, function () use ($id) { + $storeInfo = $this->dao->getOne(['id' => $id], '*', ['descriptions', 'total']); + if (!$storeInfo) { + throw new ValidateException('商品不存在'); + } else { + $storeInfo = $storeInfo->toArray(); + } + return $storeInfo; + }, 600); + /** @var DiyServices $diyServices */ + $diyServices = app()->make(DiyServices::class); + $infoDiy = $diyServices->getProductDetailDiy(); + //diy控制参数 + if (!isset($infoDiy['is_specs']) || !$infoDiy['is_specs']) { + $storeInfo['specs'] = []; + } + $configData = SystemConfigService::more(['site_url', 'routine_contact_type', 'site_name', 'share_qrcode', 'store_self_mention', 'store_func_status', 'product_poster_title']); + $siteUrl = $configData['site_url'] ?? ''; + $storeInfo['image'] = set_file_url($storeInfo['image'], $siteUrl); + $storeInfo['image_base'] = set_file_url($storeInfo['image'], $siteUrl); + $storeInfo['sale_stock'] = 0; + if ($storeInfo['stock'] > 0) $storeInfo['sale_stock'] = 1; + + //品牌名称 + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $productInfo = $storeProductServices->getCacheProductInfo((int)$storeInfo['product_id']); + $storeInfo['brand_name'] = $storeProductServices->productIdByBrandName($storeInfo['product_id'], $productInfo); + $delivery_type = $storeInfo['delivery_type'] ?? $productInfo['delivery_type']; + $storeInfo['delivery_type'] = is_string($delivery_type) ? explode(',', $delivery_type) : $delivery_type; + /** + * 判断配送方式 + */ + $storeInfo['delivery_type'] = $storeProductServices->getDeliveryType((int)$storeInfo['id'], (int)$storeInfo['type'], (int)$storeInfo['relation_id'], $storeInfo['delivery_type']); + $storeInfo['store_label'] = $storeInfo['ensure'] = []; + if ($storeInfo['store_label_id']) { + /** @var StoreProductLabelServices $storeProductLabelServices */ + $storeProductLabelServices = app()->make(StoreProductLabelServices::class); + $storeInfo['store_label'] = $storeProductLabelServices->getLabelCache($storeInfo['store_label_id'], ['id', 'label_name']); + } + if ($storeInfo['ensure_id'] && isset($infoDiy['is_ensure']) && $infoDiy['is_ensure']) { + /** @var StoreProductEnsureServices $storeProductEnsureServices */ + $storeProductEnsureServices = app()->make(StoreProductEnsureServices::class); + $storeInfo['ensure'] = $storeProductEnsureServices->getEnsurCache($storeInfo['ensure_id'], ['id', 'name', 'image', 'desc']); + } + + /** @var UserRelationServices $userRelationServices */ + $userRelationServices = app()->make(UserRelationServices::class); + $storeInfo['userCollect'] = $userRelationServices->isProductRelation(['uid' => $uid, 'relation_id' => $id, 'type' => 'collect', 'category' => UserRelationServices::CATEGORY_PRODUCT]); + $storeInfo['userLike'] = 0; + + /** @var QrcodeServices $qrcodeService */ + $qrcodeService = app()->make(QrcodeServices::class); + if (($configData['share_qrcode'] ?? 0) && request()->isWechat()) { + $storeInfo['code_base'] = $qrcodeService->getTemporaryQrcode('combination-' . $id, $uid)->url; + } else { + $storeInfo['code_base'] = $qrcodeService->getWechatQrcodePath($id . '_product_combination_detail_wap.jpg', '/pages/activity/goods_combination_details/index?id=' . $id); + } + $storeInfo['small_image'] = get_thumb_water($storeInfo['image']); + $data['storeInfo'] = $storeInfo; + + /** @var StorePinkServices $pinkService */ + $pinkService = app()->make(StorePinkServices::class); + [$pink, $pindAll] = $pinkService->getPinkList($id, true, 1);//拼团进行中列表 + $data['pink_ok_list'] = $pinkService->getPinkOkList($uid); + $data['pink_ok_sum'] = $pinkService->getPinkOkSumTotalNum(); + $data['pink'] = $pink; + $data['pindAll'] = $pindAll; + + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $data['buy_num'] = $storeOrderServices->getBuyCount($uid, 3, $id); + + $data['reply'] = []; + $data['replyChance'] = $data['replyCount'] = 0; + if (isset($infoDiy['is_reply']) && $infoDiy['is_reply']) { + /** @var StoreProductReplyServices $storeProductReplyService */ + $storeProductReplyService = app()->make(StoreProductReplyServices::class); + $reply = $storeProductReplyService->getRecProductReplyCache((int)$storeInfo['product_id'], (int)($infoDiy['reply_num'] ?? 1)); + $data['reply'] = $reply ? get_thumb_water($reply, 'small', ['pics']) : []; + [$replyCount, $goodReply, $replyChance] = $storeProductReplyService->getProductReplyData((int)$storeInfo['product_id']); + $data['replyChance'] = $replyChance; + $data['replyCount'] = $replyCount; + } + + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + [$productAttr, $productValue] = $storeProductAttrServices->getProductAttrDetailCache($id, $uid, 0, 3, $storeInfo['product_id'], $productInfo); + $data['productAttr'] = $productAttr; + $data['productValue'] = $productValue; + $data['routine_contact_type'] = sys_config('routine_contact_type', 0); + $data['store_func_status'] = (int)($configData['store_func_status'] ?? 1);//门店是否开启 + $data['store_self_mention'] = $data['store_func_status'] ? (int)($configData['store_self_mention'] ?? 0) : 0;//门店自提是否开启 + $data['site_name'] = sys_config('site_name'); + $data['share_qrcode'] = sys_config('share_qrcode', 0); + $data['product_poster_title'] = $configData['product_poster_title'] ?? ''; + //浏览记录 + ProductLogJob::dispatch(['visit', ['uid' => $uid, 'id' => $id, 'product_id' => $storeInfo['product_id']], 'combination']); + return $data; + } + + /** + * 修改销量和库存 + * @param int $num + * @param int $CombinationId + * @param string $unique + * @param int $store_id + * @return bool + */ + public function decCombinationStock(int $num, int $CombinationId, string $unique, int $store_id = 0) + { + $product_id = $this->dao->value(['id' => $CombinationId], 'product_id'); + $res = true; + if ($product_id) { + if ($unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //减去拼团商品的sku库存增加销量 + $res = $res && $skuValueServices->decProductAttrStock($CombinationId, $unique, $num, 3); + + //拼团商品sku + $sku = $skuValueServices->value(['product_id' => $CombinationId, 'unique' => $unique, 'type' => 3], 'suk'); + //平台普通商品sku unique + $productUnique = $skuValueServices->value(['suk' => $sku, 'product_id' => $product_id, 'type' => 0], 'unique'); + + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //减去当前普通商品、sku的库存增加销量 + $res = $res && $services->decProductStock($num, (int)$product_id, (string)$productUnique, $store_id); + } + //减去拼团商品库存 + $res = $res && $this->dao->decStockIncSales(['id' => $CombinationId, 'type' => 3], $num); + } + return $res; + } + + /** + * 加库存减销量 + * @param int $num + * @param int $CombinationId + * @param string $unique + * @param int $store_id + * @return bool + */ + public function incCombinationStock(int $num, int $CombinationId, string $unique, int $store_id = 0) + { + $product_id = $this->dao->value(['id' => $CombinationId], 'product_id'); + $res = false; + if ($product_id) { + if ($unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //增加拼团商品的sku库存,减去销量 + $res = false !== $skuValueServices->incProductAttrStock($CombinationId, $unique, $num, 3); + + //拼团商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $CombinationId, 'type' => 3], 'suk'); + //平台商品sku unique + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //增加当前普通商品sku的库存,减去销量 + $res = $res && $services->incProductStock($num, (int)$product_id, (string)$productUnique, $store_id); + } + //增加拼团库存 + $res = $res && $this->dao->incStockDecSales(['id' => $CombinationId, 'type' => 3], $num); + } + return $res; + } + + /** + * 获取一条拼团数据 + * @param $id + * @return mixed + */ + public function getCombinationOne($id, $field = '*') + { + return $this->dao->validProduct($id, $field); + } + + /** + * 获取拼团详情 + * @param int $uid + * @param int $id + * @param array $user + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPinkInfo(int $uid, int $id, array $user = []) + { + if (!$id || !$uid) throw new ValidateException('参数错误'); + + $is_ok = 0;//判断拼团是否完成 + $userBool = 0;//判断当前用户是否在团内 0未在 1在 + $pinkBool = 0;//判断拼团是否成功 0未在 1在 + if (!$user) { + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserCacheInfo($uid); + if (!$user) { + throw new ValidateException('参数错误'); + } + $user = $user->toArray(); + } + /** @var StorePinkServices $pinkService */ + $pinkService = app()->make(StorePinkServices::class); + $pink = $pinkService->getPinkUserOne($id); + if (!$pink) throw new ValidateException('参数错误'); + $pink = $pink->toArray(); + if (isset($pink['is_refund']) && $pink['is_refund']) { + if ($pink['is_refund'] != $pink['id']) { + $id = $pink['is_refund']; + return $this->getPinkInfo($uid, $id, $user); + } else { + throw new ValidateException('订单已退款'); + } + } + [$pinkAll, $pinkT, $count, $idAll, $uidAll] = $pinkService->getPinkMemberAndPinkK($pink); + if ($pinkT['status'] == 2) { + $pinkBool = 1; + $is_ok = 1; + } else if ($pinkT['status'] == 3) { + $pinkBool = -1; + $is_ok = 0; + } else { + if ($count < 1) {//组团完成 + $is_ok = 1; + $pinkBool = $pinkService->pinkComplete($uidAll, $idAll, $user['uid'], $pinkT); + } else { + $pinkBool = $pinkService->pinkFail($pinkAll, $pinkT, $pinkBool); + } + //更新pinkT 数据 可能成功或失败 + $pinkT = $pinkService->getPinkUserOne($pinkT['id']); + } + if (!empty($pinkAll)) { + foreach ($pinkAll as $v) { + if ($v['uid'] == $user['uid']) $userBool = 1; + } + } + if ($pinkT['uid'] == $user['uid']) $userBool = 1; + $combinationOne = $this->getCombinationOne($pink['cid']); + if (!$combinationOne) { + throw new ValidateException('拼团不存在或已下架,请手动申请退款!'); + } + $combinationOne = $combinationOne->hidden(['mer_id', 'images', 'attr', 'info', 'sort', 'sales', 'stock', 'add_time', 'is_host', 'is_show', 'is_del', 'combination', 'mer_use', 'is_postage', 'postage', 'start_time', 'stop_time', 'cost', 'browse', 'product_price'])->toArray(); + + $data['userInfo']['uid'] = $user['uid']; + $data['userInfo']['nickname'] = $user['nickname']; + $data['userInfo']['avatar'] = $user['avatar']; + $data['is_ok'] = $is_ok; + $data['userBool'] = $userBool; + $data['pinkBool'] = $pinkBool; + $delivery_type = $combinationOne['delivery_type'] ?? []; + $combinationOne['delivery_type'] = is_string($delivery_type) ? explode(',', $delivery_type) : $delivery_type; + $data['store_combination'] = $combinationOne; + $data['pinkT'] = $pinkT; + $data['pinkAll'] = $pinkAll; + $data['count'] = $count <= 0 ? 0 : $count; + $data['store_combination_host'] = $this->dao->getCombinationHost(); + $data['current_pink_order'] = $pinkService->getCurrentPink($id, $user['uid']); + + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + + [$productAttr, $productValue] = $storeProductAttrServices->getProductAttrDetail($combinationOne['id'], $user['uid'], 0, 3, $combinationOne['product_id']); + foreach ($productValue as $k => $v) { + $productValue[$k]['product_stock'] = $storeProductAttrValueServices->value(['product_id' => $combinationOne['product_id'], 'suk' => $v['suk'], 'type' => 0], 'stock'); + } + $data['store_combination']['productAttr'] = $productAttr; + $data['store_combination']['productValue'] = $productValue; + $data['store_func_status'] = (int)(sys_config('store_func_status', 1)); + $data['store_self_mention'] = $data['store_func_status'] ? (int)(sys_config('store_self_mention', 0)) : 0;//门店自提是否开启 + return $data; + } + + /** + * 验证拼团下单库存限量 + * @param int $uid + * @param int $combinationId + * @param int $cartNum + * @param string $unique + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkCombinationStock(int $uid, int $combinationId, int $cartNum = 1, string $unique = '') + { + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + if ($unique == '') { + $unique = $attrValueServices->value(['product_id' => $combinationId, 'type' => 3], 'unique'); + } + $attrInfo = $attrValueServices->getOne(['product_id' => $combinationId, 'unique' => $unique, 'type' => 3]); + if (!$attrInfo || $attrInfo['product_id'] != $combinationId) { + throw new ValidateException('请选择有效的商品属性'); + } + $StoreCombinationInfo = $productInfo = $this->getCombinationOne($combinationId, '*,title as store_name'); + if (!$StoreCombinationInfo) { + throw new ValidateException('该商品已下架或删除'); + } + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $userBuyCount = $orderServices->getBuyCount($uid, 3, $combinationId); + if ($StoreCombinationInfo['once_num'] < $cartNum) { + throw new ValidateException('每个订单限购' . $StoreCombinationInfo['once_num'] . '件'); + } + if ($StoreCombinationInfo['num'] < ($userBuyCount + $cartNum)) { + throw new ValidateException('每人总共限购' . $StoreCombinationInfo['num'] . '件'); + } + + if ($cartNum > $attrInfo['quota']) { + throw new ValidateException('该商品库存不足' . $cartNum); + } + return [$attrInfo, $unique, $productInfo]; + } + + /** + * 拼团统计 + * @param $id + * @return array + */ + public function combinationStatistics(int $id) + { + /** @var StorePinkServices $pinkServices */ + $pinkServices = app()->make(StorePinkServices::class); + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $people_count = $pinkServices->getDistinctCount([['cid', '=', $id]], 'uid', false); + $spread_count = $pinkServices->getDistinctCount([['cid', '=', $id], ['k_id', '>', 0]], 'uid', false); + $start_count = $pinkServices->count(['cid' => $id, 'k_id' => 0]); + $success_count = $pinkServices->count(['cid' => $id, 'k_id' => 0, 'status' => 2]); + $pay_price = $orderServices->sum(['type' => 3, 'activity_id' => $id, 'paid' => 1, 'pid' => [0, -1]], 'pay_price', true); + $pay_count = $orderServices->getDistinctCount(['type' => 3, 'activity_id' => $id, 'paid' => 1, 'pid' => [0, -1]], 'uid'); + return compact('people_count', 'spread_count', 'start_count', 'success_count', 'pay_price', 'pay_count'); + } + + /** + * 拼团订单 + * @param $id + * @param array $where + * @return array + */ + public function combinationStatisticsOrder(int $id, array $where = []) + { + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + [$page, $limit] = $this->getPageValue(); + $list = $orderServices->activityStatisticsOrder($id, 3, $where, $page, $limit); + $where['type'] = 3; + $where['activity_id'] = $id; + $count = $orderServices->count($where); + foreach ($list as &$item) { + if ($item['is_del'] || $item['is_system_del']) { + $item['status'] = '已删除'; + } else if ($item['paid'] == 0 && $item['status'] == 0) { + $item['status'] = '未支付'; + } else if ($item['paid'] == 1 && $item['status'] == 4 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status'] = '部分发货'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 2) { + $item['status'] = '已退款'; + } else if ($item['paid'] == 1 && $item['status'] == 5 && $item['refund_status'] == 0) { + $item['status'] = $item['shipping_type'] == 2 ? '部分核销' : '部分收货'; + $item['_status'] = 12;//已支付 部分核销 + } else if ($item['paid'] == 1 && $item['refund_status'] == 1) { + $item['status'] = '申请退款'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 4) { + $item['status'] = '退款中'; + } else if ($item['paid'] == 1 && $item['status'] == 0 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status'] = '未发货'; + $item['_status'] = 2;//已支付 未发货 + } else if ($item['paid'] == 1 && in_array($item['status'], [0, 1]) && $item['shipping_type'] == 2 && $item['refund_status'] == 0) { + $item['status'] = '未核销'; + } else if ($item['paid'] == 1 && in_array($item['status'], [1, 5]) && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status'] = '待收货'; + } else if ($item['paid'] == 1 && $item['status'] == 2 && $item['refund_status'] == 0) { + $item['status'] = '待评价'; + } else if ($item['paid'] == 1 && $item['status'] == 3 && $item['refund_status'] == 0) { + $item['status'] = '已完成'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 3) { + $item['status'] = '部分退款'; + } else { + $item['status'] = '未知'; + } + $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']); + $item['pay_time'] = $item['pay_time'] ? date('Y-m-d H:i:s', $item['pay_time']) : ''; + } + return compact('list', 'count'); + } +} diff --git a/app/services/activity/combination/StorePinkServices.php b/app/services/activity/combination/StorePinkServices.php new file mode 100644 index 0000000..090e17e --- /dev/null +++ b/app/services/activity/combination/StorePinkServices.php @@ -0,0 +1,744 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\combination; + +use app\jobs\activity\pink\AuthPinkFail; +use app\services\BaseServices; +use app\dao\activity\combination\StorePinkDao; +use app\services\order\StoreOrderRefundServices; +use app\services\order\StoreOrderServices; +use app\services\other\QrcodeServices; +use app\services\user\UserServices; +use app\services\system\attachment\SystemAttachmentServices; +use app\jobs\activity\pink\PinkJob; +use crmeb\services\CacheService; +use crmeb\services\UploadService; +use crmeb\services\UtilService; +use crmeb\services\wechat\MiniProgram; +use GuzzleHttp\Psr7\Utils; +use think\exception\ValidateException; + +/** + * 拼团 + * Class StorePinkServices + * @package app\services\activity\combination + * @mixin StorePinkDao + */ +class StorePinkServices extends BaseServices +{ + + const OXPJOHRW = '/G6B.Z'; + + /** + * StorePinkServices constructor. + * @param StorePinkDao $dao + */ + public function __construct(StorePinkDao $dao) + { + $this->dao = $dao; + } + + /** + * @param array $where + * @return array + */ + public function systemPage(array $where) + { + $where['k_id'] = 0; + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit); + $time = time(); + foreach ($list as &$item) { + $item['count_people'] = $this->dao->count(['k_id' => $item['id']]) + 1; + //状态为进行中,实际已经结束的 + if ($item['status'] == 1 && $item['stop_time'] < $time) { + $item['status'] = $item['count_people'] >= $item['people'] ? 2 : 3; + } + $item['_add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', (int)$item['add_time']) : ''; + $item['_stop_time'] = $item['stop_time'] ? date('Y-m-d H:i:s', (int)$item['stop_time']) : ''; + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 拼团列表头部 + * @return array + */ + public function getStatistics() + { + $res = [ + ['col' => 6, 'count' => $this->dao->count(), 'name' => '参与人数(人)', 'className' => 'ios-speedometer-outline'], + ['col' => 6, 'count' => $this->dao->count(['k_id' => 0, 'status' => 2]), 'name' => '成团数量(个)', 'className' => 'md-rose'], + ]; + return compact('res'); + } + + /** + * 参团人员 + * @param int $id + * @return array + */ + public function getPinkMember(int $id) + { + return $this->dao->getList(['k_id' => $id]); + } + + /** + * 拼团退款 + * @param $id + * @return bool + */ + public function setRefundPink($order) + { + $res = true; + if ($order['pink_id']) { + $id = $order['pink_id']; + } else { + return true; + } + //正在拼团 团长 + $count = $this->dao->getOne(['id' => $id, 'uid' => $order['uid']]); + //正在拼团 团员 + $countY = $this->dao->getOne(['k_id' => $id, 'uid' => $order['uid']]); + if (!$count && !$countY) { + return $res; + } + if ($count) {//团长 + //判断团内是否还有其他人 如果有 团长为第二个进团的人 + $kCount = $this->dao->getPinking(['k_id' => $id]); + if ($kCount) { + $res11 = $this->dao->update($id, ['k_id' => $kCount['id']], 'k_id'); + $res12 = $this->dao->update($kCount['id'], ['stop_time' => $count['add_time'] + 86400]); + $res1 = $res11 && $res12; + $res2 = $this->dao->update($id, ['stop_time' => time() - 1, 'is_refund' => $kCount['id']]); + } else { + $res1 = true; + $res2 = $this->dao->update($id, ['stop_time' => time() - 1, 'is_refund' => $id, 'status' => 3]); + } + //修改结束时间为前一秒 + $res = $res1 && $res2; + } else if ($countY) {//团员 + $res = $this->dao->update($countY['id'], ['stop_time' => time() - 1, 'is_refund' => $id, 'status' => 3]); + } + if ($res) { + CacheService::setStock(md5((string)$id), 1, 3, false); + } + return $res; + } + + /** + * 拼团详情查看拼团列表 + * @param int $id + * @param bool $type + * @param int $status + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPinkList(int $id, bool $type, int $status = -1) + { + $where['cid'] = $id; + $where['k_id'] = 0; + $where['is_refund'] = 0; + if ($status !== -1) { + $where['status'] = $status; + } + $list = $this->dao->pinkList($where); + $ids = array_column($list, 'id'); + $counts = $this->dao->getPinkPeopleCount($ids); + if ($type) { + $pinkAll = []; + foreach ($list as &$v) { + $v['count'] = $v['people'] - $counts[$v['id']]; + $v['h'] = date('H', (int)$v['stop_time']); + $v['i'] = date('i', (int)$v['stop_time']); + $v['s'] = date('s', (int)$v['stop_time']); + $pinkAll[] = $v['id'];//开团团长ID + $v['stop_time'] = (int)$v['stop_time']; + } + return [$list, $pinkAll]; + } + return $list; + } + + /** + * 获取成团列表信息 + * @param int $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPinkOkList(int $uid) + { + $list = $this->dao->successList($uid, 'id,nickname', 0, 100); + $msg = []; + foreach ($list as &$item) { + if (isset($item['nickname'])) $msg[] = $item['nickname'] .= '拼团成功'; + } + return $msg; + } + + /** + * 查找拼团信息 + * @param $pink + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPinkMemberAndPinkK($pink) + { + //查团长 + if ($pink['k_id']) {//团员 + $pinkT = $this->dao->getPinkUserOne($pink['k_id']); + } else { + $pinkT = $pink; + } + //所有团员 + $pinkAll = $this->dao->getPinkUserList(['k_id' => $pinkT['id'], 'is_refund' => 0]); + $count = count($pinkAll) + 1; + $count = $pinkT['people'] - $count; + //收集拼团用户id和拼团id + $idAll = $uidAll = []; + if ($pinkAll) { + $idAll = array_column($pinkAll, 'id'); + $uidAll = array_column($pinkAll, 'uid'); + } + $idAll[] = $pinkT['id']; + $uidAll[] = $pinkT['uid']; + return [$pinkAll, $pinkT, $count, $idAll, $uidAll]; + } + + /** + * 拼团失败 + * @param $pinkAll + * @param $pinkT + * @param $pinkBool + * @param bool $isRunErr + * @param bool $isIds + * @return array|int + */ + public function pinkFail($pinkAll, $pinkT, $pinkBool, $isRunErr = true, $isIds = false) + { + $pinkIds = []; + try { + if ($pinkT['stop_time'] < time()) {//拼团时间超时 退款 + $virtual = $this->virtualCombination($pinkT['id']); + if ($virtual) return 1; + $pinkBool = -1; + array_push($pinkAll, $pinkT); + $oids = array_column($pinkAll, 'order_id_key'); + /** @var StoreOrderServices $orderService */ + $orderService = app()->make(StoreOrderServices::class); + $orders = $orderService->getColumn([['id', 'in', $oids]], '*', 'id'); + $refundData = [ + 'refund_reason' => '拼团时间超时', + 'refund_explain' => '拼团时间超时', + 'refund_img' => json_encode([]), + ]; + /** @var StoreOrderRefundServices $orderRefundService */ + $orderRefundService = app()->make(StoreOrderRefundServices::class); + $refundeOrder = $orderRefundService->getColumn([ + ['store_order_id', 'IN', $oids], + ['refund_type', 'in', [1, 2, 4, 5]], + ['is_cancel', '=', 0], + ['is_del', '=', 0] + ], 'id,store_order_id', 'store_order_id'); + foreach ($pinkAll as $v) { + if (isset($orders[$v['order_id_key']]) && $order = $orders[$v['order_id_key']]) { + if (in_array($v['order_id_key'], $refundeOrder)) { + continue; + } + $res1 = $res2 = true; + try { + $res1 = $orderRefundService->applyRefund((int)$order['id'], (int)$order['uid'], $order, [], 1, (float)$order['pay_price'], $refundData); + } catch (\Throwable $e) { + + } + $res2 = $this->dao->getCount([['uid', '=', $v['uid']], ['is_tpl', '=', 0], ['k_id|id', '=', $pinkT['id']]]); + if ($res1 && $res2) { + if ($isIds) array_push($pinkIds, $v['id']); + $this->orderPinkAfterNo($pinkT['uid'], $pinkT['id'], false, $orders[$v['order_id_key']]['is_channel']); + } else { + if ($isRunErr) return $pinkBool; + } + } + } + } + if ($isIds) return $pinkIds; + } catch (\Exception $e) { + \think\facade\Log::error('拼团超时处理失败,原因:' . $e->getMessage()); + } + return $pinkBool; + } + + /** + * 失败发送消息和修改状态 + * @param $uid + * @param $pid + * @param bool $isRemove + * @param $channel + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function orderPinkAfterNo($uid, $pid, $isRemove = false, $channel = '') + { + $this->dao->update([['id|k_id', '=', $pid]], ['status' => 3, 'stop_time' => time()]); + $pink = $this->dao->getOne([['id|k_id', '=', $pid], ['uid', '=', $uid]], '*', ['getProduct']); + if ($isRemove) { + event('notice.notice', [['uid' => $uid, 'pink' => $pink, 'user_type' => $channel], 'send_order_pink_clone']); + } else { + event('notice.notice', [['uid' => $uid, 'pink' => $pink, 'user_type' => $channel], 'send_order_pink_fial']); + } + } + + + /** + * 判断拼团状态 + * @param $pinkId + * @return bool + */ + public function isPinkStatus($pinkId) + { + if (!$pinkId) return false; + $stopTime = $this->dao->value(['id' => $pinkId], 'stop_time'); + if ($stopTime < time()) return true; //拼团结束 + else return false;//拼团未结束 + } + + /** + * 获取拼团order_id + * @param int $id + * @param int $uid + * @return mixed + */ + public function getCurrentPink(int $id, int $uid) + { + $oid = $this->dao->value(['id' => $id, 'uid' => $uid], 'order_id_key'); + if (!$oid) $oid = $this->dao->value(['k_id' => $id, 'uid' => $uid], 'order_id_key'); + /** @var StoreOrderServices $orderService */ + $orderService = app()->make(StoreOrderServices::class); + return $orderService->value(['id' => $oid], 'order_id'); + } + + /** + * 拼团成功 + * @param $uidAll + * @param $idAll + * @param $uid + * @param $pinkT + * @return int + */ + public function pinkComplete($uidAll, $idAll, $uid, $pinkT) + { + $pinkBool = 6; + try { + if (!$this->dao->getCount([['id', 'in', $idAll], ['is_refund', '=', 1]])) { + $this->dao->update([['id', 'in', $idAll]], ['stop_time' => time(), 'status' => 2]); + if (in_array($uid, $uidAll)) { + if ($this->dao->getCount([['uid', 'in', $uidAll], ['is_tpl', '=', 0], ['k_id|id', '=', $pinkT['id']]])) + $this->orderPinkAfter($uidAll, $pinkT['id']); + $pinkBool = 1; + } else $pinkBool = 3; + } + return $pinkBool; + } catch (\Exception $e) { + return $pinkBool; + } + } + + /** + * 拼团成功修改 + * @param $uidAll + * @param $pid + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function orderPinkAfter($uidAll, $pid) + { + //发送消息之前去除虚拟用户 + foreach ($uidAll as $key => $uid) { + if ($uid == 0) unset($uidAll[$key]); + } + /** @var StoreCombinationServices $storeCombinationServices */ + $storeCombinationServices = app()->make(StoreCombinationServices::class); + $pinkInfo = $this->dao->get((int)$pid, ['cid', 'nickname', 'uid']); + $title = $storeCombinationServices->value(['id' => $pinkInfo['cid']], 'title'); + $pinkList = $this->dao->getColumn([['id|k_id', '=', $pid], ['uid', '<>', 0]], '*', 'uid'); + $order_ids = array_column($pinkList, 'order_id'); + /** @var StoreOrderServices $orderService */ + $orderService = app()->make(StoreOrderServices::class); + $order_channels = $orderService->getColumn([['order_id', 'in', $order_ids]], 'is_channel', 'order_id'); + if (!$pinkList) return false; + foreach ($pinkList as $item) { + //用户发送消息 + event('notice.notice', [ + [ + 'list' => $item, + 'nickname' => $pinkInfo['nickname'], + 'title' => $title, + 'user_type' => $order_channels[$item['order_id']], + 'url' => '/pages/users/order_details/index?order_id=' . $item['order_id'] + ], 'order_user_groups_success']); + } + $this->dao->update([['uid', 'in', $uidAll], ['id|k_id', '=', $pid]], ['is_tpl' => 1]); + } + + /** + * 创建拼团 + * @param $order + * @return mixed + */ + public function createPink(array $orderInfo) + { + /** @var StoreCombinationServices $services */ + $services = app()->make(StoreCombinationServices::class); + $product = $services->getOne(['id' => $orderInfo['activity_id']], 'effective_time,title,people'); + if (!$product) { + return false; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->get($orderInfo['uid']); + if ($orderInfo['pink_id']) { + //拼团存在 + $res = false; + $pink['uid'] = $orderInfo['uid'];//用户id + $pink['nickname'] = $userInfo['nickname']; + $pink['avatar'] = $userInfo['avatar']; + if ($this->isPinkBe($pink, $orderInfo['pink_id'])) return false; + $pink['order_id'] = $orderInfo['order_id'];//订单id 生成 + $pink['order_id_key'] = $orderInfo['id'];//订单id 数据库id + $pink['total_num'] = $orderInfo['total_num'];//购买个数 + $pink['total_price'] = $orderInfo['pay_price'];//总金额 + $pink['k_id'] = $orderInfo['pink_id'];//拼团id + foreach ($orderInfo['cartInfo'] as $v) { + $pink['cid'] = $v['activity_id'];//拼团商品id + $pink['pid'] = $v['product_id'];//商品id + $pink['people'] = $product['people'];//几人拼团 + $pink['price'] = $v['productInfo']['price'];//单价 + $pink['stop_time'] = 0;//结束时间 + $pink['add_time'] = time();//开团时间 + $res = $this->save($pink); + } + // 拼团团成功发送模板消息 + event('notice.notice', [['orderInfo' => $orderInfo, 'title' => $product['title'], 'pink' => $pink], 'can_pink_success']); + //处理拼团完成 + [$pinkAll, $pinkT, $count, $idAll, $uidAll] = $this->getPinkMemberAndPinkK($pink); + if ($pinkT['status'] == 1) { + if (!$count)//组团完成 + $this->pinkComplete($uidAll, $idAll, $pink['uid'], $pinkT); + else + $this->pinkFail($pinkAll, $pinkT, 0); + } + + if ($res) return true; + else return false; + } else { + //创建拼团 + $res = false; + $pink['uid'] = $orderInfo['uid'];//用户id + $pink['nickname'] = $userInfo['nickname']; + $pink['avatar'] = $userInfo['avatar']; + $pink['order_id'] = $orderInfo['order_id'];//订单id 生成 + $pink['order_id_key'] = $orderInfo['id'];//订单id 数据库id + $pink['total_num'] = $orderInfo['total_num'];//购买个数 + $pink['total_price'] = $orderInfo['pay_price'];//总金额 + $pink['k_id'] = 0;//拼团id + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + foreach ($orderInfo['cartInfo'] as $v) { + $pink['cid'] = $v['activity_id'];//拼团商品id + $pink['pid'] = $v['product_id'];//商品id + $pink['people'] = $product['people'];//几人拼团 + $pink['price'] = $v['productInfo']['price'];//单价 + $pink['stop_time'] = time() + $product->effective_time * 3600;//结束时间 + $pink['add_time'] = time();//开团时间 + $res1 = $this->dao->save($pink); + $res2 = $orderServices->update($orderInfo['id'], ['pink_id' => $res1['id']]); + $res = $res1 && $res2; + $pink['id'] = $res1['id']; + } + $number = (int)bcsub((string)$product['people'], '1', 0); + if ($number) CacheService::setStock(md5((string)$pink['id']), $number, 3); + + PinkJob::dispatchSece(($product->effective_time * 3600) + 60, [$pink['id']]); + event('notice.notice', [['orderInfo' => $orderInfo, 'title' => $product['title'], 'pink' => $pink], 'open_pink_success']); + if ($res) return true; + else return false; + } + } + + /** + * 是否拼团 + * @param array $data + * @param int $id + * @return int + */ + public function isPinkBe(array $data, int $id) + { + $data['id'] = $id; + $count = $this->dao->getCount($data); + if ($count) return $count; + $data['k_id'] = $id; + $count = $this->dao->getCount($data); + if ($count) return $count; + else return 0; + } + + /** + * 取消拼团 + * @param int $uid + * @param int $cid + * @param int $pink_id + * @param null $nextPinkT + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function removePink(int $uid, int $cid, int $pink_id, $nextPinkT = null) + { + $pinkT = $this->dao->getOne([ + ['uid', '=', $uid], + ['id', '=', $pink_id], + ['cid', '=', $cid], + ['k_id', '=', 0], + ['is_refund', '=', 0], + ['status', '=', 1], + ['stop_time', '>', time()], + ]); + if (!$pinkT) throw new ValidateException('未查到拼团信息,无法取消'); + [$pinkAll, $pinkT, $count, $idAll, $uidAll] = $this->getPinkMemberAndPinkK($pinkT); + if (count($pinkAll)) { + $count = $pinkT['people'] - ($this->dao->count(['k_id' => $pink_id, 'is_refund' => 0]) + 1); + if ($count) { + //拼团未完成,拼团有成员取消开团取 紧跟团长后拼团的人 + if (isset($pinkAll[0])) $nextPinkT = $pinkAll[0]; + } else { + //拼团完成 + $this->PinkComplete($uidAll, $idAll, $uid, $pinkT); + throw new ValidateException('拼团已完成,无法取消'); + } + } + /** @var StoreOrderServices $orderService */ + $orderService = app()->make(StoreOrderServices::class); + /** @var StoreOrderRefundServices $orderRefundService */ + $orderRefundService = app()->make(StoreOrderRefundServices::class); + //取消开团 + $order = $orderService->get($pinkT['order_id_key']); + $refundData = [ + 'refund_reason' => '用户手动取消拼团', + 'refund_explain' => '用户手动取消拼团', + 'refund_img' => json_encode([]), + ]; + try { + $res1 = $orderRefundService->applyRefund((int)$order['id'], (int)$order['uid'], $order, [], 1, (float)$order['pay_price'], $refundData); + } catch (\Throwable $e) { + $res1 = true; + } + $res2 = $this->dao->getCount([['uid', '=', $pinkT['uid']], ['k_id|id', '=', $pinkT['id']]]); + if ($res1 && $res2) { + $this->orderPinkAfterNo($pinkT['uid'], $pinkT['id'], true, $order->is_channel); + } + //当前团有人的时候 + if (is_array($nextPinkT)) { + $this->dao->update($nextPinkT['id'], ['k_id' => 0, 'status' => 1, 'stop_time' => $pinkT['stop_time']]); + $this->dao->update($pinkT['id'], ['k_id' => $nextPinkT['id']], 'k_id'); + $orderService->update($nextPinkT['order_id'], ['pink_id' => $nextPinkT['id']], 'order_id'); + } + return true; + } + + /** + * 修改到期的拼团状态 + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function statusPink(int $page, int $limit) + { + $pinkListEnd = $this->dao->pinkListEnd()->page($page, $limit)->select()->toArray(); + foreach ($pinkListEnd as $key => $pink) { + [$pinkAll, $pinkT, $count, $idAll, $uidAll] = $this->getPinkMemberAndPinkK($pink); + $this->pinkFail($pinkAll, $pinkT, 0); + } + return true; + } + + /** + * 放入自动取消拼团队列 + */ + public function useStatusPink() + { + $pinkEndCount = $this->dao->pinkListEnd()->count(); + if (!$pinkEndCount) { + return true; + } + $pages = ceil($pinkEndCount / 100); + for ($i = 1; $i <= $pages; $i++) { + AuthPinkFail::dispatch([$i, 100]); + } + return true; + } + + /** + * 虚拟拼团 + * @param $pinkId + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function virtualCombination($pinkId) + { + $pinkInfo = $this->dao->get($pinkId); + $people = $pinkInfo['people']; + $count = $this->dao->count(['k_id' => $pinkId]) + 1; + $percent1 = bcdiv((string)$count, (string)$people, 2) * 100; + /** @var StoreCombinationServices $services */ + $services = app()->make(StoreCombinationServices::class); + $percent2 = $services->value(['id' => $pinkInfo['cid']], 'virtual'); + if ($percent1 >= $percent2) { + $time = time(); + $num = $people - $count; + $data = []; + $defaultData = [ + 'uid' => 0, + 'avatar' => sys_config('h5_avatar'), + 'order_id' => 0, + 'order_id_key' => 0, + 'total_num' => 0, + 'total_price' => 0, + 'cid' => $pinkInfo['cid'], + 'pid' => $pinkInfo['pid'], + 'people' => $people, + 'price' => 0, + 'add_time' => $time, + 'stop_time' => $time, + 'k_id' => $pinkInfo['id'], + 'is_tpl' => 1, + 'is_refund' => 0, + 'status' => 2, + 'is_virtual' => 1 + ]; + mt_srand(); + for ($i = 0; $i < $num; $i++) { + $defaultData['nickname'] = substr(md5(time() . rand(1000, 9999)), 0, 12); + $data[$i] = $defaultData; + } + //添加虚拟团员 + $this->dao->saveAll($data); + //更改团员状态为拼团成功 + $this->dao->update($pinkId, ['stop_time' => $time, 'status' => 2], 'k_id'); + //更改团长为拼团成功 + $this->dao->update($pinkId, ['stop_time' => $time, 'status' => 2]); + $uidAll = $this->dao->getColumn([['id|k_id', '=', $pinkId]], 'uid'); + $this->orderPinkAfter($uidAll, $pinkId); + return true; + } else { + return false; + } + } + + /** + * 获取拼团海报详情信息 + * @param int $id + * @param $user + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function posterInfo(int $id, $user) + { + $pinkInfo = $this->dao->get($id); + /** @var StoreCombinationServices $combinationService */ + $combinationService = app()->make(StoreCombinationServices::class); + $storeCombinationInfo = $combinationService->getOne(['id' => $pinkInfo['cid']], '*', ['getPrice']); + $data['title'] = $storeCombinationInfo['title']; + $data['url'] = ''; + $data['image'] = $storeCombinationInfo['image']; + $data['price'] = $pinkInfo['price']; + $data['label'] = $pinkInfo['people'] . '人团'; + if ($pinkInfo['k_id']) $pinkAll = $this->getPinkMember($pinkInfo['k_id']); + else $pinkAll = $this->getPinkMember($pinkInfo['id']); + $count = count($pinkAll); + $data['msg'] = '原价¥' . $storeCombinationInfo['product_price'] . ' 还差' . ($pinkInfo['people'] - $count) . '人拼团成功'; + + /** @var SystemAttachmentServices $systemAttachmentServices */ + $systemAttachmentServices = app()->make(SystemAttachmentServices::class); + + try { + $siteUrl = sys_config('site_url'); + if (request()->isRoutine()) { + //小程序 + $name = $id . '_' . $user['uid'] . '_' . $user['is_promoter'] . '_pink_share_routine.jpg'; + $imageInfo = $systemAttachmentServices->getInfo(['name' => $name]); + if (!$imageInfo) { + $valueData = 'id=' . $id; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if ($userServices->checkUserPromoter((int)$user['uid'], $user)) { + $valueData .= '&spid=' . $user['uid']; + } + $res = MiniProgram::appCodeUnlimit($valueData, 'pages/activity/goods_combination_status/index', 280); + if (!$res) throw new ValidateException('二维码生成失败'); + $uploadType = (int)sys_config('upload_type', 1); + $upload = UploadService::init($uploadType); + $res = (string)Utils::streamFor($res); + $res = $upload->to('routine/activity/pink/code')->validate()->stream($res, $name); + if ($res === false) { + throw new ValidateException($upload->getError()); + } + $imageInfo = $upload->getUploadInfo(); + $imageInfo['image_type'] = $uploadType; + if ($imageInfo['image_type'] == 1) $remoteImage = UtilService::remoteImage($siteUrl . $imageInfo['dir']); + else $remoteImage = UtilService::remoteImage($imageInfo['dir']); + if (!$remoteImage['status']) throw new ValidateException($remoteImage['msg']); + $systemAttachmentServices->save([ + 'name' => $imageInfo['name'], + 'att_dir' => $imageInfo['dir'], + 'satt_dir' => $imageInfo['thumb_path'], + 'att_size' => $imageInfo['size'], + 'att_type' => $imageInfo['type'], + 'image_type' => $imageInfo['image_type'], + 'module_type' => 2, + 'time' => time(), + 'pid' => 1, + 'type' => 1 + ]); + $url = $imageInfo['dir']; + } else $url = $imageInfo['att_dir']; + $data['url'] = $url; + if ($imageInfo['image_type'] == 1) + $data['url'] = $siteUrl . $url; + } else { + if (sys_config('share_qrcode', 0) && request()->isWechat()) { + /** @var QrcodeServices $qrcodeService */ + $qrcodeService = app()->make(QrcodeServices::class); + $data['url'] = $qrcodeService->getTemporaryQrcode('pink-' . $id, $user['uid'])->url; + } + } + } catch (\Throwable $e) { + } + return $data; + } +} diff --git a/app/services/activity/coupon/StoreCouponIssueServices.php b/app/services/activity/coupon/StoreCouponIssueServices.php new file mode 100644 index 0000000..5d49f62 --- /dev/null +++ b/app/services/activity/coupon/StoreCouponIssueServices.php @@ -0,0 +1,990 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\coupon; + +use app\services\BaseServices; +use app\dao\activity\coupon\StoreCouponIssueDao; +use app\services\order\StoreCartServices; +use app\services\other\queue\QueueServices; +use app\services\product\brand\StoreBrandServices; +use app\services\product\category\StoreProductCategoryServices; +use app\services\product\product\StoreProductServices; +use app\services\user\member\MemberCardServices; +use app\services\user\member\MemberRightServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder; +use think\exception\ValidateException; + +/** + * + * Class StoreCouponIssueServices + * @package app\services\activity\coupon + * @mixin StoreCouponIssueDao + */ +class StoreCouponIssueServices extends BaseServices +{ + + public $_couponType = [0 => "通用券", 1 => "品类券", 2 => '商品券']; + + /** + * StoreCouponIssueServices constructor. + * @param StoreCouponIssueDao $dao + */ + public function __construct(StoreCouponIssueDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取已发布列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCouponIssueList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $where['is_del'] = 0; + $list = $this->dao->getList($where, $page, $limit); + foreach ($list as &$item) { + if (!$item['coupon_time']) { + $item['coupon_time'] = ceil(($item['end_use_time'] - $item['start_use_time']) / '86400'); + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取会员优惠券列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMemberCouponIssueList(array $where) + { + return $this->dao->getApiIssueList($where); + } + + /** + * 新增优惠券 + * @param $data + * @return bool + */ + public function saveCoupon($data) + { + $data['start_use_time'] = strtotime((string)$data['start_use_time']); + $data['end_use_time'] = strtotime((string)$data['end_use_time']); + $data['start_time'] = strtotime((string)$data['start_time']); + $data['end_time'] = strtotime((string)$data['end_time']); + $data['title'] = $data['coupon_title']; + $data['remain_count'] = $data['total_count']; + $data['add_time'] = time(); + $res = $this->dao->save($data); + if ($data['product_id'] !== '' && $res) { + $productIds = explode(',', $data['product_id']); + $couponData = []; + foreach ($productIds as $product_id) { + $couponData[] = ['product_id' => $product_id, 'coupon_id' => $res->id]; + } + /** @var StoreCouponProductServices $storeCouponProductService */ + $storeCouponProductService = app()->make(StoreCouponProductServices::class); + $storeCouponProductService->saveAll($couponData); + } + if (!$res) throw new AdminException('添加优惠券失败!'); + return true; + } + + + /** + * 修改状态 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createForm(int $id) + { + $issueInfo = $this->dao->get($id); + if (-1 == $issueInfo['status'] || 1 == $issueInfo['is_del']) return $this->fail('状态错误,无法修改'); + $f = [FormBuilder::radio('status', '是否开启', $issueInfo['status'])->options([['label' => '开启', 'value' => 1], ['label' => '关闭', 'value' => 0]])]; + return create_form('状态修改', $f, $this->url('/marketing/coupon/released/status/' . $id), 'PUT'); + } + + /** + * 领取记录 + * @param int $id + * @return array + */ + public function issueLog(int $id) + { + $coupon = $this->dao->get($id); + if (!$coupon) { + throw new ValidateException('优惠券不存在'); + } + if ($coupon['receive_type'] != 4) { + /** @var StoreCouponIssueUserServices $storeCouponIssueUserService */ + $storeCouponIssueUserService = app()->make(StoreCouponIssueUserServices::class); + return $storeCouponIssueUserService->issueLog(['issue_coupon_id' => $id]); + } else {//会员券 + /** @var StoreCouponUserServices $storeCouponUserService */ + $storeCouponUserService = app()->make(StoreCouponUserServices::class); + return $storeCouponUserService->issueLog(['cid' => $id]); + } + + } + + /** + * 保存赠送给用户优惠券 + * @param int $uid + * @param array $couponList + * @param string $type + * @return array + */ + public function saveGiveCoupon(int $uid, array $couponList, string $type = 'get') + { + if (!$uid || !$couponList) { + return []; + } + $couponData = []; + $issueUserData = []; + $time = time(); + /** @var StoreCouponIssueUserServices $issueUser */ + $issueUser = app()->make(StoreCouponIssueUserServices::class); + foreach ($couponList as $item) { + $data['cid'] = $item['id']; + $data['uid'] = $uid; + $data['coupon_title'] = $item['title']; + $data['coupon_price'] = $item['coupon_price']; + $data['use_min_price'] = $item['use_min_price']; + if ($item['coupon_time']) { + $data['add_time'] = $time; + $data['end_time'] = $data['add_time'] + $item['coupon_time'] * 86400; + } else { + $data['add_time'] = $item['start_use_time']; + $data['end_time'] = $item['end_use_time']; + } + $data['type'] = $type; + $issue['uid'] = $uid; + $issue['issue_coupon_id'] = $item['id']; + $issue['add_time'] = $time; + $issueUserData[] = $issue; + $couponData[] = $data; + unset($data); + unset($issue); + } + if ($couponData) { + /** @var StoreCouponUserServices $storeCouponUser */ + $storeCouponUser = app()->make(StoreCouponUserServices::class); + if (!$storeCouponUser->saveAll($couponData)) { + throw new AdminException('发劵失败'); + } + } + if ($issueUserData) { + if (!$issueUser->saveAll($issueUserData)) { + throw new AdminException('发劵失败'); + } + } + return $couponData; + } + + /** + * 新人礼赠送优惠券 + * @param int $uid + */ + public function newcomerGiveCoupon(int $uid) + { + if (!sys_config('newcomer_status')) { + return false; + } + $status = sys_config('register_coupon_status'); + if (!$status) {//未开启 + return true; + } + $couponIds = sys_config('register_give_coupon', []); + if (!$couponIds) { + return true; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + return true; + } + $couponList = $this->dao->getGiveCoupon([['id', 'IN', $couponIds]]); + if ($couponList) { + $this->saveGiveCoupon($uid, $couponList, 'newcomer'); + } + return true; + } + + /** + * 会员卡激活赠送优惠券 + * @param int $uid + */ + public function levelGiveCoupon(int $uid) + { + $status = sys_config('level_activate_status'); + if (!$status) {//是否需要激活 + return true; + } + $status = sys_config('level_coupon_status'); + if (!$status) {//未开启 + return true; + } + $couponIds = sys_config('level_give_coupon', []); + if (!$couponIds) { + return true; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + return true; + } + $couponList = $this->dao->getGiveCoupon([['id', 'IN', $couponIds]]); + if ($couponList) { + $this->saveGiveCoupon($uid, $couponList, 'activate_level'); + } + return true; + } + + /** + * 关注送优惠券 + * @param int $uid + */ + public function userFirstSubGiveCoupon(int $uid) + { + $couponList = $this->dao->getGiveCoupon(['receive_type' => 2]); + if ($couponList) { + $this->saveGiveCoupon($uid, $couponList, 'user_first'); + } + return true; + } + + /** + * 订单金额达到预设金额赠送优惠卷 + * @param $uid + */ + public function userTakeOrderGiveCoupon($uid, $total_price) + { + $couponList = $this->dao->getGiveCoupon([['is_full_give', '=', 1], ['full_reduction', '<=', $total_price]]); + if ($couponList) { + $res = []; + foreach ($couponList as $item) { + if ($total_price >= $item['full_reduction']) { + $res[] = $item; + } + } + $this->saveGiveCoupon($uid,$res); + } + return true; + } + + /** + * 下单之后赠送 + * @param $uid + */ + public function orderPayGiveCoupon($uid, $coupon_issue_ids) + { + if (!$coupon_issue_ids) return []; + $couponList = $this->dao->getGiveCoupon([['id', 'IN', $coupon_issue_ids]]); + $couponData = []; + if ($couponList) { + $couponData = $this->saveGiveCoupon($uid, $couponList, 'order'); + } + return $couponData; + } + + /** + * 获取商品关联 + * @param int $uid + * @param int $product_id + * @param string $field + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductCouponList(int $uid, int $product_id, string $field = '*', int $limit = 0) + { + if ($limit) { + $page = 1; + } else { + [$page, $limit] = $this->getPageValue(); + } + /** @var StoreProductServices $storeProductService */ + $storeProductService = app()->make(StoreProductServices::class); + /** @var StoreProductCategoryServices $storeCategoryService */ + $storeCategoryService = app()->make(StoreProductCategoryServices::class); + + $productInfo = $storeProductService->getCacheProductInfo($product_id); + $cateId = explode(',', $productInfo['cate_id']); + $cateId = array_merge($cateId, $storeCategoryService->cateIdByPid($cateId)); + $cateId = array_unique(array_diff($cateId, [0])); + $brandIds = []; + if ($productInfo['brand_id']) { + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $brandInfo = $storeBrandServices->get((int)$productInfo['brand_id'], ['id', 'pid']); + if ($brandInfo) { + $brandIds = $brandInfo->toArray(); + $brandIds = array_diff($brandIds, [0]); + } + } + $where = ['product_id' => $productInfo['pid'] ? : $product_id, 'cate_id' => $cateId, 'brand_id' => $brandIds]; + $list = $this->dao->getIssueCouponListNew($uid, $where, $field, $page, $limit); + + //门店ID + $store_id = $productInfo['type'] == 1 ? ($productInfo['relation_id'] ?? 0) : 0; + $result = []; + foreach ($list as &$item) { + if ($store_id && isset($item['applicable_type']) && isset($item['applicable_store_id'])) { + $applicable_store_id = is_array($item['applicable_store_id']) ? $item['applicable_store_id'] : explode(',', $item['applicable_store_id']); + //活动不适用该门店 + if ($item['applicable_type'] == 0 || ($item['applicable_type'] == 2 && !in_array($store_id, $applicable_store_id))) { + continue; + } + } + $item['coupon_price'] = floatval($item['coupon_price']); + $item['use_min_price'] = floatval($item['use_min_price']); + $item['is_use'] = $uid ? isset($item['used']) : false; + if (!$item['end_time']) { + $item['start_time'] = ''; + $item['end_time'] = '不限时'; + } else { + $item['start_time'] = date('Y/m/d', $item['start_time']); + $item['end_time'] = $item['end_time'] ? date('Y/m/d', $item['end_time']) : date('Y/m/d', time() + 86400); + } + $result[] = $item; + } + return $result; + } + + /** + * 获取优惠券列表 + * @param int $uid + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getIssueCouponList(int $uid, array $where, string $field = '*', bool $is_product = false) + { + [$page, $limit] = $this->getPageValue(); + /** @var StoreProductServices $storeProductService */ + $storeProductService = app()->make(StoreProductServices::class); + /** @var StoreProductCategoryServices $storeCategoryService */ + $storeCategoryService = app()->make(StoreProductCategoryServices::class); + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $typeId = 0; + $cateId = 0; + $bandId = 0; + $store_id = 0; + $product_id = 0; + if ($where['product_id'] == 0) { + if ($where['type'] == -1) { // PC端获取优惠券 + $list = $this->dao->getIssueCouponListNew($uid, $where, $field); + } else { + $list = $this->dao->getIssueCouponListNew($uid, $where, $field, $page, $limit); + if (!$list) $list = $this->dao->getIssueCouponListNew($uid, ['type' => 1], $field, $page, $limit); + if (!$list) $list = $this->dao->getIssueCouponListNew($uid, ['type' => 2], $field, $page, $limit); + if (!$list) $list = $this->dao->getIssueCouponListNew($uid, ['type' => 3], $field, $page, $limit); + } + } else { + $productInfo = $storeProductService->getOne(['id' => $where['product_id']], 'id,type,relation_id,cate_id,brand_id'); + //门店ID + $store_id = $productInfo['type'] == 1 ? ($productInfo['relation_id'] ?? 0) : 0; + $product_id = $productInfo['pid'] ? : $where['product_id']; + if ($productInfo) { + $cateId = $productInfo['cateId']; + if ($cateId) { + $cateId = explode(',', $cateId); + $cateId = array_merge($cateId, $storeCategoryService->cateIdByPid($cateId)); + $cateId = array_diff($cateId, [0]); + } + if ($productInfo['brand_id']) { + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $brandInfo = $storeBrandServices->get((int)$productInfo['brand_id'], ['id', 'pid']); + if ($brandInfo) { + $bandId = $brandInfo->toArray(); + $bandId = array_diff($bandId, [0]); + } + } + } + + if ($where['type'] == -1) { // PC端获取优惠券 + $list = $this->dao->getIssueCouponListNew($uid, ['product_id' => $product_id, 'cate_id' => $cateId, 'brand_id' => $bandId], $field); + } else { + $coupon_where = ['type' => $where['type']]; + if ($where['type'] == 1) { + $category_type['cate_id'] = $cateId; + } elseif ($where['type'] == 2) { + $category_type['product_id'] = $where['product_id']; + } elseif ($where['type'] == 3) { + $category_type['brand_id'] = $where['brand_id']; + } + $list = $this->dao->getIssueCouponListNew($uid, $coupon_where, $field, $page, $limit); + } + } + $result = []; + if ($list) { + $limit = 3; + $field = ['id', 'image', 'store_name', 'price', 'IFNULL(sales,0) + IFNULL(ficti,0) as sales']; + $category = $storeCategoryService->getColumn([], 'pid,cate_name', 'id'); + $brands = $storeBrandServices->getColumn([], 'id,pid', 'id'); + foreach ($list as &$item) { + if ($store_id && isset($item['applicable_type']) && isset($item['applicable_store_id'])) { + $applicable_store_id = is_array($item['applicable_store_id']) ? $item['applicable_store_id'] : explode(',', $item['applicable_store_id']); + //活动不适用该门店 + if ($item['applicable_type'] == 0 || ($item['applicable_type'] == 2 && !in_array($store_id, $applicable_store_id))) { + continue; + } + } + $item['coupon_price'] = floatval($item['coupon_price']); + $item['use_min_price'] = floatval($item['use_min_price']); + $item['is_use'] = $uid ? isset($item['used']) : false; + if (!$item['end_time']) { + $item['start_time'] = ''; + $item['end_time'] = '不限时'; + } else { + $item['start_time'] = date('Y/m/d', $item['start_time']); + $item['end_time'] = $item['end_time'] ? date('Y/m/d', $item['end_time']) : date('Y/m/d', time() + 86400); + } + if (!$is_product) {//不需要返回优惠券使用商品 + $result[] = $item; + continue; + } + $product_where = []; + $product_where['is_show'] = 1; + $product_where['is_del'] = 0; + $product_where['use_min_price'] = $item['use_min_price']; + switch ($item['type']) { + case 0: + break; + case 1://品类券 + $product_where['salesOrder'] = 'desc'; + $category_type = ($category[$item['category_id'] ?? 0]['pid'] ?? 0) == 0 ? 1 : 2; + if ($category_type == 1) { + $product_where['cid'] = $item['category_id']; + } else { + $product_where['sid'] = $item['category_id']; + } + break; + case 2://商品劵 + $product_where['salesOrder'] = 'desc'; + $product_where['ids'] = $item['product_id']; + break; + case 3://品牌券 + $product_where['salesOrder'] = 'desc'; + $product_where['brand_id'] = $brands[$item['brand_id']] ?? []; + break; + default: + $item['products'] = []; + } + $item['products'] = get_thumb_water($storeProductService->getSearchList($product_where, 0, $limit, $field, $item['type'] == 0 ? 'rand' : '', [])); + + $result[] = $item; + } + } + $data['list'] = $result; + $data['count'] = $this->dao->getIssueCouponCount($product_id, $cateId, $bandId); + return $data; + } + + /** + * 给用户发送优惠券 + * @param $id + * @param $user + * @param bool $more + * @param string $type + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function issueUserCoupon($id, $user, bool $more = true, string $type = 'get') + { + $issueCouponInfo = $this->dao->getInfo((int)$id); + $uid = $user['uid']; + if (!$issueCouponInfo) throw new ValidateException('领取的优惠劵已领完或已过期!'); + /** @var MemberRightServices $memberRightService */ + $memberRightService = app()->make(MemberRightServices::class); + if ($issueCouponInfo->receive_type == 4 && (!$user['is_money_level'] || !$memberRightService->getMemberRightStatus("coupon"))) { + if (!$user['is_money_level']) throw new ValidateException('您不是付费会员!'); + if (!$memberRightService->getMemberRightStatus("coupon")) throw new ValidateException('暂时无法领取!'); + } + /** @var StoreCouponIssueUserServices $issueUserService */ + $issueUserService = app()->make(StoreCouponIssueUserServices::class); + /** @var StoreCouponUserServices $couponUserService */ + $couponUserService = app()->make(StoreCouponUserServices::class); + //是否多次领取 + if (!$more) { + if ($issueUserService->getOne(['uid' => $uid, 'issue_coupon_id' => $id])) { + throw new ValidateException('已领取过该优惠劵!'); + } + } + if ($issueCouponInfo->remain_count <= 0 && !$issueCouponInfo->is_permanent) throw new ValidateException('抱歉优惠券已经领取完了!'); + $userCounpon = $this->transaction(function () use ($issueUserService, $uid, $id, $couponUserService, $issueCouponInfo, $type) { + $issueUserService->save(['uid' => $uid, 'issue_coupon_id' => $id, 'add_time' => time()]); + $res = $couponUserService->addUserCoupon($uid, $issueCouponInfo, $type); + if ($issueCouponInfo['total_count'] > 0) { + $issueCouponInfo['remain_count'] -= 1; + $issueCouponInfo->save(); + } + return $res; + }); + return $userCounpon; + } + + /** + * 会员发放优惠期券 + * @param $id + * @param $uid + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function memberIssueUserCoupon($id, $uid) + { + $issueCouponInfo = $this->dao->getInfo((int)$id); + if ($issueCouponInfo) { + /** @var StoreCouponIssueUserServices $issueUserService */ + $issueUserService = app()->make(StoreCouponIssueUserServices::class); + /** @var StoreCouponUserServices $couponUserService */ + $couponUserService = app()->make(StoreCouponUserServices::class); + if ($issueCouponInfo->remain_count >= 0 || $issueCouponInfo->is_permanent) { + $this->transaction(function () use ($issueUserService, $uid, $id, $couponUserService, $issueCouponInfo) { + //$issueUserService->save(['uid' => $uid, 'issue_coupon_id' => $id, 'add_time' => time()]); + $couponUserService->addMemberUserCoupon($uid, $issueCouponInfo, "send"); + // 如果会员劵需要限制数量时打开 + if ($issueCouponInfo['total_count'] > 0) { + $issueCouponInfo['remain_count'] -= 1; + $issueCouponInfo->save(); + } + }); + } + + } + + } + + /** + * 用户优惠劵列表 + * @param int $uid + * @param $types + * @return array + */ + public function getUserCouponList(int $uid, $types) + { + /** @var StoreCouponUserServices $storeConponUser */ + $storeConponUser = app()->make(StoreCouponUserServices::class); + return $storeConponUser->getUserCounpon($uid, $types); + } + + /** + * 后台发送优惠券 + * @param $coupon + * @param $user + * @return bool + */ + public function setCoupon($coupon, $user, $redisKey, $queueId) + { + if (!$redisKey || !$queueId || !$user) return false; + $data = []; + $issueData = []; + /** @var StoreCouponUserServices $storeCouponUser */ + $storeCouponUser = app()->make(StoreCouponUserServices::class); + /** @var StoreCouponIssueUserServices $storeCouponIssueUser */ + $storeCouponIssueUser = app()->make(StoreCouponIssueUserServices::class); + /** @var QueueServices $queueService */ + $queueService = app()->make(QueueServices::class); + $i = 0; + $time = time(); + foreach ($user as $k => $v) { + $data[$i]['cid'] = $coupon['id']; + $data[$i]['uid'] = $v; + $data[$i]['coupon_title'] = $coupon['title']; + $data[$i]['coupon_price'] = $coupon['coupon_price']; + $data[$i]['use_min_price'] = $coupon['use_min_price']; + $data[$i]['add_time'] = $time; + if ($coupon['coupon_time']) { + $data[$i]['start_time'] = $time; + $data[$i]['end_time'] = $time + $coupon['coupon_time'] * 86400; + } else { + $data[$i]['start_time'] = $coupon['start_use_time']; + $data[$i]['end_time'] = $coupon['end_use_time']; + } + $data[$i]['type'] = 'send'; + $issueData[$i]['uid'] = $v; + $issueData[$i]['issue_coupon_id'] = $coupon['id']; + $issueData[$i]['add_time'] = time(); + $i++; + } + $res = $this->transaction(function () use ($data, $issueData, $storeCouponUser, $storeCouponIssueUser) { + $res1 = $res2 = true; + if ($data) { + $res1 = $storeCouponUser->saveAll($data); + } + if ($issueData) { + $res2 = $storeCouponIssueUser->saveAll($issueData); + } + return $res1 && $res2; + }); + if (!$res) { + //发券失败后将队列状态置为失败 + $queueService->setQueueFail($queueId['id'], $redisKey); + } else { + //发券成功的用户踢出集合 + $queueService->doSuccessSremRedis($user, $redisKey, $queueId['type']); + } + return true; + } + + /** + * 获取下单可使用的优惠券列表 + * @param int $uid + * @param $cartId + * @param bool $new + * @param int $shipping_type + * @param int $store_id + * @return array + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function beUsableCouponList(int $uid, $cartId, bool $new, int $shipping_type = 1, int $store_id = 0) + { + /** @var StoreCartServices $services */ + $services = app()->make(StoreCartServices::class); + $cartGroup = $services->getUserProductCartListV1($uid, $cartId, $new, [], $shipping_type, $store_id); + /** @var StoreCouponUserServices $coupServices */ + $coupServices = app()->make(StoreCouponUserServices::class); + return $coupServices->getUsableCouponList($uid, $cartGroup, $store_id); + } + + /**获取单个优惠券类型 + * @param array $where + * @return mixed + */ + public function getOne(array $where) + { + if (!$where) throw new AdminException('参数缺失!'); + return $this->dao->getOne($where); + + } + + /** + * 俩时间相差月份 + * @param $date1 + * @param $date2 + * @return float|int + */ + public function getMonthNum($date1, $date2) + { + $date1_stamp = strtotime($date1); + $date2_stamp = strtotime($date2); + [$date_1['y'], $date_1['m']] = explode("-", date('Y-m', $date1_stamp)); + [$date_2['y'], $date_2['m']] = explode("-", date('Y-m', $date2_stamp)); + return abs($date_1['y'] - $date_2['y']) * 12 + $date_2['m'] - $date_1['m']; + } + + /** + * 给会员发放优惠券 + * @param $uid + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function sendMemberCoupon($uid, $couponId = 0) + { + if (!$uid) return false; + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + /** @var StoreCouponUserServices $couponUserService */ + $couponUserService = app()->make(StoreCouponUserServices::class); + /** @var MemberRightServices $memberRightService */ + $memberRightService = app()->make(MemberRightServices::class); + /** @var MemberCardServices $memberCardService */ + $memberCardService = app()->make(MemberCardServices::class); + //看付费会员是否开启 + $isOpenMember = $memberCardService->isOpenMemberCardCache(); + if (!$isOpenMember) return false; + $userInfo = $userService->getUserInfo((int)$uid); + //看是否会员过期 + if ($userInfo['is_ever_level'] == 0 && $userInfo['is_money_level'] > 0 && $userInfo['overdue_time'] < time()) { + return false; + } + //看是否开启会员送券 + $isSendCoupon = $memberRightService->getMemberRightStatus("coupon"); + if (!$isSendCoupon) return false; + if ($userInfo && (($userInfo['is_money_level'] > 0) || $userInfo['is_ever_level'] == 1)) { + $monthNum = $this->getMonthNum(date('Y-m-d H:i:s', time()), date('Y-m-d H:i:s', $userInfo['overdue_time'])); + if ($couponId) {//手动点击领取 + $couponWhere['id'] = $couponId; + $couponInfo = $this->getMemberCouponIssueList($couponWhere); + } else {//主动批量发放 + $couponWhere['status'] = 1; + $couponWhere['receive_type'] = 4; + $couponWhere['is_del'] = 0; + $couponInfo = $this->getMemberCouponIssueList($couponWhere); + } + if ($couponInfo) { + $couponIds = array_column($couponInfo, 'id'); + $couponUserMonth = $couponUserService->memberCouponUserGroupBymonth(['uid' => $uid, 'couponIds' => $couponIds]); + $getTime = array(); + if ($couponUserMonth) { + $getTime = array_column($couponUserMonth, 'num', 'time'); + } + // 判断这个月是否领取过,而且领全了 + //if (in_array(date('Y-m', time()), $getTime)) return false; + $timeKey = date('Y-m', time()); + if (array_key_exists($timeKey, $getTime) && $getTime[$timeKey] == count($couponIds)) return false; + //判断是否领完所有月份 + if (count($getTime) >= $monthNum && (array_key_exists($timeKey, $getTime) && $getTime[$timeKey] == count($couponIds)) && $userInfo['is_ever_level'] != 1 && $monthNum > 0) return false; + foreach ($couponInfo as $cv) { + //看之前是否手动领取过某一张,领取过就不再领取。 + $couponUser = $couponUserService->getUserCounponByMonth(['uid' => $uid, 'cid' => $cv['id']]); + if (!$couponUser) { + $this->memberIssueUserCoupon($cv['id'], $uid); + } + } + + } + } + return true; + } + + /** + * 获取今日新增优惠券 + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTodayCoupon($uid) + { + $list = $this->dao->getTodayCoupon($uid); + foreach ($list as $key => &$item) { + $item['start_time'] = $item['start_time'] ? date('Y/m/d', $item['start_time']) : 0; + $item['end_time'] = $item['end_time'] ? date('Y/m/d', $item['end_time']) : 0; + $item['coupon_price'] = floatval($item['coupon_price']); + $item['use_min_price'] = floatval($item['use_min_price']); + if (isset($item['used']) && $item['used']) { + unset($list[$key]); + } + } + return array_merge($list); + } + + /** + * 获取新人券 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getNewCoupon() + { + $list = $this->dao->getNewCoupon(); + foreach ($list as &$item) { + $item['start_time'] = $item['start_time'] ? date('Y/m/d', $item['start_time']) : 0; + $item['end_time'] = $item['end_time'] ? date('Y/m/d', $item['end_time']) : 0; + $item['coupon_price'] = floatval($item['coupon_price']); + $item['use_min_price'] = floatval($item['use_min_price']); + } + return $list; + } + + /** + * 获取可以使用优惠券 + * @param int $uid + * @param array $cartList + * @param array $promotions + * @param int $store_id + * @param bool $isMax + * @return array|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCanUseCoupon(int $uid, array $cartList = [], array $promotions = [], int $store_id = 0, bool $isMax = true) + { + $userInfo = []; + if ($uid) { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $userInfo = $userService->getUserCacheInfo($uid); + } + if ($userInfo && isset($userInfo['is_money_level']) && $userInfo['is_money_level'] > 0) { + $where = ['receive_type' => [1, 4]]; + } else { + $where = ['receive_type' => 1]; + } + /** @var StoreCouponUserServices $coupServices */ + $coupServices = app()->make(StoreCouponUserServices::class); + //用户持有有效券 + $userCoupons = $coupServices->getUserAllCoupon($uid); + if ($userCoupons) { + $where['not_id'] = array_column($userCoupons, 'cid'); + } + $counpons = $this->dao->getValidList($where, 0, 0, ['used' => function ($query) use ($uid) { + $query->where('uid', $uid); + }]); + //用户已经领取的 && 没有领取的 + $counpons = array_merge($counpons, $userCoupons); + $result = []; + if ($counpons) { + $promotionsList = []; + if($promotions){ + $promotionsList = array_combine(array_column($promotions, 'id'), $promotions); + } + //验证是否适用门店 + $isApplicableStore = function ($couponInfo) use ($store_id) { + if ($store_id && isset($couponInfo['applicable_type']) && isset($couponInfo['applicable_store_id'])) { + $applicable_store_id = is_array($couponInfo['applicable_store_id']) ? $couponInfo['applicable_store_id'] : explode(',', $couponInfo['applicable_store_id']); + //活动不适用该门店 + if ($couponInfo['applicable_type'] == 0 || ($couponInfo['applicable_type'] == 2 && !in_array($store_id, $applicable_store_id))) { + return false; + } + } + return true; + }; + // + $isOverlay = function($cart) use ($promotionsList) { + $productInfo = $cart['productInfo'] ?? []; + if (!$productInfo) { + return false; + } + //门店独立商品 不使用优惠券 + $isBranchProduct = isset($productInfo['type']) && isset($productInfo['pid']) && $productInfo['type'] == 1 && !$productInfo['pid']; + if ($isBranchProduct) { + return false; + } + if (isset($cart['promotions_id']) && $cart['promotions_id']) { + foreach ($cart['promotions_id'] as $key => $promotions_id) { + $promotions = $promotionsList[$promotions_id] ?? []; + if ($promotions && $promotions['promotions_type'] != 4){ + $overlay = is_string($promotions['overlay']) ? explode(',', $promotions['overlay']) : $promotions['overlay']; + if (!in_array(5, $overlay)) { + return false; + } + } + } + } + return true; + }; + /** @var StoreProductCategoryServices $storeCategoryServices */ + $storeCategoryServices = app()->make(StoreProductCategoryServices::class); + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + foreach ($counpons as $coupon) { + if (isset($coupon['used']) && $coupon['used']) {//所有优惠券中 已经领取跳过 + continue; + } + if (!$isApplicableStore($coupon)) {//不适用门店跳过 + continue; + } + $price = 0; + $count = 0; + if (isset($coupon['coupon_applicable_type'])) {//已经领取 有效优惠券 + $coupon['type'] = $coupon['coupon_applicable_type']; + $coupon['used'] = ['id' => $coupon['id'], 'cid' => $coupon['cid']]; + } + //&& in_array($promotions['promotions_type'], $overlay) + switch ($coupon['type']) { + case 0: + foreach ($cartList as $cart) { + if (!$isOverlay($cart)) continue; + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + break; + case 1://品类券 + $cateGorys = $storeCategoryServices->getAllById((int)$coupon['category_id']); + if ($cateGorys) { + $cateIds = array_column($cateGorys, 'id'); + foreach ($cartList as $cart) { + if (!$isOverlay($cart)) continue; + if (isset($cart['productInfo']['cate_id']) && array_intersect(explode(',', $cart['productInfo']['cate_id']), $cateIds)) { + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + } + break; + case 2://商品券 + foreach ($cartList as $cart) { + if (!$isOverlay($cart)) continue; + $product_id = isset($cart['productInfo']['pid']) && $cart['productInfo']['pid'] ? $cart['productInfo']['pid'] : ($cart['product_id'] ?? 0); + if ($product_id && in_array($product_id, explode(',', $coupon['product_id']))) { + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + break; + case 3://品牌券 + $brands = $storeBrandServices->getAllById((int)$coupon['brand_id']); + if ($brands) { + $brandIds = array_column($brands, 'id'); + foreach ($cartList as $cart) { + if (!$isOverlay($cart)) continue; + if (isset($cart['productInfo']['brand_id']) && in_array($cart['productInfo']['brand_id'], $brandIds)) { + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + } + break; + } + if ($count && $coupon['use_min_price'] <= $price) { + //满减券 + if ($coupon['coupon_type'] == 1) { + $couponPrice = $coupon['coupon_price'] > $price ? $price : $coupon['coupon_price']; + } else { + if ($coupon['coupon_price'] <= 0) {//0折 + $couponPrice = $price; + } else if ($coupon['coupon_price'] >= 100) { + $couponPrice = 0; + } else { + $truePrice = (float)bcmul((string)$price, bcdiv((string)$coupon['coupon_price'], '100', 2), 2); + $couponPrice = (float)bcsub((string)$price, (string)$truePrice, 2); + } + } + $coupon['coupon_price'] = floatval($coupon['coupon_price']); + $coupon['use_min_price'] = floatval($coupon['use_min_price']); + $coupon['true_coupon_price'] = $couponPrice; + $result[] = $coupon; + } + } + } + if ($result) { + $trueCouponPrice = array_column($result, 'true_coupon_price'); + array_multisort($trueCouponPrice, SORT_DESC, $result); + } + //最优的一个 + if ($isMax) { + $useCoupon = $result[0] ?? []; + return $useCoupon; + } + return $result; + } +} diff --git a/app/services/activity/coupon/StoreCouponUserServices.php b/app/services/activity/coupon/StoreCouponUserServices.php new file mode 100644 index 0000000..fe4fea5 --- /dev/null +++ b/app/services/activity/coupon/StoreCouponUserServices.php @@ -0,0 +1,607 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\coupon; + +use app\services\activity\promotions\StorePromotionsServices; +use app\services\BaseServices; +use app\services\product\brand\StoreBrandServices; +use app\services\product\product\StoreProductRelationServices; +use app\services\user\UserServices; +use app\dao\activity\coupon\StoreCouponUserDao; +use app\services\product\category\StoreProductCategoryServices; +use crmeb\utils\Arr; + +/** + * Class StoreCouponUserServices + * @package app\services\activity\coupon + * @mixin StoreCouponUserDao + */ +class StoreCouponUserServices extends BaseServices +{ + + /** + * StoreCouponUserServices constructor. + * @param StoreCouponUserDao $dao + */ + public function __construct(StoreCouponUserDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取列表 + * @param array $where + * @return array + */ + public function issueLog(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, 'uid,add_time', ['userInfo'], $page, $limit); + foreach ($list as &$item) { + $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']); + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function systemPage(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', ['issue'], $page, $limit); + $count = 0; + if ($list) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userAll = $userServices->getColumn([['uid', 'IN', array_column($list, 'uid')]], 'uid,nickname', 'uid'); + foreach ($list as &$item) { + $item['nickname'] = $userAll[$item['uid']]['nickname'] ?? ''; + } + $count = $this->dao->count($where); + } + return compact('list', 'count'); + } + + /** + * 获取用户优惠券 + * @param int $id + * @param int $status + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCouponList(int $id, int $status = -1) + { + [$page, $limit] = $this->getPageValue(); + $where = ['uid' => $id]; + if ($status != -1) $where['status'] = $status; + $list = $this->dao->getList($where, '*', ['issue'], $page, $limit); + foreach ($list as &$item) { + $item['_add_time'] = date('Y-m-d H:i:s', $item['add_time']); + $item['_end_time'] = date('Y-m-d H:i:s', $item['end_time']); + if (!$item['coupon_time']) { + $item['coupon_time'] = ceil(($item['end_use_time'] - $item['start_use_time']) / '86400'); + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 恢复优惠券 + * @param int $id + * @return bool|mixed + */ + public function recoverCoupon(int $id) + { + $status = $this->dao->value(['id' => $id], 'status'); + if ($status) return $this->dao->update($id, ['status' => 0, 'use_time' => 0]); + else return true; + } + + /** + * 过期优惠卷失效 + */ + public function checkInvalidCoupon() + { + $this->dao->update([['end_time', '<', time()], ['status', '=', '0']], ['status' => 2]); + } + + /** + * 获取用户有效优惠劵数量 + * @param int $uid + * @return int + */ + public function getUserValidCouponCount(int $uid) + { + $this->checkInvalidCoupon(); + return $this->dao->getCount(['uid' => $uid, 'status' => 0]); + } + + /** + * 下单页面显示可用优惠券 + * @param int $uid + * @param array $cartGroup + * @param int $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUsableCouponList(int $uid, array $cartGroup, int $store_id = 0) + { + $userCoupons = $this->dao->getUserAllCoupon($uid); + $result = []; + if ($userCoupons) { + $cartInfo = $cartGroup['valid']; + $promotions = $cartGroup['promotions'] ?? []; + $promotionsList = []; + if($promotions){ + $promotionsList = array_combine(array_column($promotions, 'id'), $promotions); + } + //验证是否适用门店 + $isApplicableStore = function ($couponInfo) use ($store_id) { + if ($store_id && isset($couponInfo['applicable_type']) && isset($couponInfo['applicable_store_id'])) { + $applicable_store_id = is_array($couponInfo['applicable_store_id']) ? $couponInfo['applicable_store_id'] : explode(',', $couponInfo['applicable_store_id']); + //活动不适用该门店 + if ($couponInfo['applicable_type'] == 0 || ($couponInfo['applicable_type'] == 2 && !in_array($store_id, $applicable_store_id))) { + return false; + } + } + return true; + }; + $isOverlay = function ($cart) use ($promotionsList) { + $productInfo = $cart['productInfo'] ?? []; + if (!$productInfo) { + return false; + } + //门店独立商品 不使用优惠券 + $isBranchProduct = isset($productInfo['type']) && isset($productInfo['pid']) && $productInfo['type'] == 1 && !$productInfo['pid']; + if ($isBranchProduct) { + return false; + } + if (isset($cart['promotions_id']) && $cart['promotions_id']) { + foreach ($cart['promotions_id'] as $key => $promotions_id) { + $promotions = $promotionsList[$promotions_id] ?? []; + if ($promotions && $promotions['promotions_type'] != 4){ + $overlay = is_string($promotions['overlay']) ? explode(',', $promotions['overlay']) : $promotions['overlay']; + if (!in_array(5, $overlay)) { + return false; + } + } + } + } + return true; + }; + /** @var StoreProductCategoryServices $storeCategoryServices */ + $storeCategoryServices = app()->make(StoreProductCategoryServices::class); + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + foreach ($userCoupons as $coupon) { + if (!$isApplicableStore($coupon)) {//不适用门店跳过 + continue; + } + $price = 0; + $count = 0; + switch ($coupon['coupon_applicable_type']) { + case 0: + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + break; + case 1://品类券 + $cateGorys = $storeCategoryServices->getAllById((int)$coupon['category_id']); + if ($cateGorys) { + $cateIds = array_column($cateGorys, 'id'); + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + if (isset($cart['productInfo']['cate_id']) && array_intersect(explode(',', $cart['productInfo']['cate_id']), $cateIds)) { + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + } + break; + case 2://商品 + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + $product_id = isset($cart['productInfo']['pid']) && $cart['productInfo']['pid'] ? $cart['productInfo']['pid'] : ($cart['product_id'] ?? 0); + if ($product_id && in_array($product_id, explode(',', $coupon['product_id']))) { + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + break; + case 3://品牌 + $brands = $storeBrandServices->getAllById((int)$coupon['brand_id']); + if ($brands) { + $brandIds = array_column($brands, 'id'); + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + if (isset($cart['productInfo']['brand_id']) && in_array($cart['productInfo']['brand_id'], $brandIds)) { + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + } + break; + } + if ($count && $coupon['use_min_price'] <= $price) { + $coupon['start_time'] = $coupon['start_time'] ? date('Y/m/d', $coupon['start_time']) : date('Y/m/d', $coupon['add_time']); + $coupon['add_time'] = date('Y/m/d', $coupon['add_time']); + $coupon['end_time'] = date('Y/m/d', $coupon['end_time']); + $coupon['title'] = $coupon['coupon_title']; + $coupon['type'] = $coupon['coupon_applicable_type']; + $coupon['use_min_price'] = floatval($coupon['use_min_price']); + $coupon['coupon_price'] = floatval($coupon['coupon_price']); + $result[] = $coupon; + } + } + } + return $result; + } + + /** + * 下单页面显示可用优惠券 + * @param $uid + * @param $cartGroup + * @param $price + * @return array + */ + public function getOldUsableCouponList(int $uid, array $cartGroup) + { + $cartPrice = $cateIds = []; + $productId = Arr::getUniqueKey($cartGroup['valid'], 'product_id'); + foreach ($cartGroup['valid'] as $value) { + $cartPrice[] = bcmul((string)$value['truePrice'], (string)$value['cart_num'], 2); + } + $maxPrice = count($cartPrice) ? max($cartPrice) : 0; + if ($productId) { + /** @var StoreProductRelationServices $storeProductRelationServices */ + $storeProductRelationServices = app()->make(StoreProductRelationServices::class); + $cateId = $storeProductRelationServices->productIdByCateId($productId); + + if ($cateId) { + /** @var StoreProductCategoryServices $cateServices */ + $cateServices = app()->make(StoreProductCategoryServices::class); + $catePids = $cateServices->cateIdByPid($cateId); + $cateIds = array_merge($cateId, $catePids); + } else { + $cateIds = $cateId; + } + + } + $productCouponList = $this->dao->productIdsByCoupon($productId, $uid, (string)$maxPrice); + $cateCouponList = $this->dao->cateIdsByCoupon($cateIds, $uid, (string)$maxPrice); + $list = array_merge($productCouponList, $cateCouponList); + $couponIds = Arr::getUniqueKey($list, 'id'); + $sumCartPrice = array_sum($cartPrice); + $list1 = $this->dao->getUserCoupon($couponIds, $uid, (string)$sumCartPrice); + $list = array_merge($list, $list1); + foreach ($list as &$item) { + $item['add_time'] = date('Y/m/d', $item['add_time']); + $item['end_time'] = date('Y/m/d', $item['end_time']); + $item['title'] = $item['coupon_title']; + $item['type'] = $item['coupon_applicable_type'] ?? 0; + } + return $list; + } + + /** + * 用户领取优惠券 + * @param $uid + * @param $issueCouponInfo + * @param string $type + * @return mixed + */ + public function addUserCoupon($uid, $issueCouponInfo, string $type = 'get') + { + $data = []; + $data['cid'] = $issueCouponInfo['id']; + $data['uid'] = $uid; + $data['coupon_title'] = $issueCouponInfo['title']; + $data['coupon_price'] = $issueCouponInfo['coupon_price']; + $data['use_min_price'] = $issueCouponInfo['use_min_price']; + $data['add_time'] = time(); + if ($issueCouponInfo['coupon_time']) { + $data['start_time'] = $data['add_time']; + $data['end_time'] = $data['add_time'] + $issueCouponInfo['coupon_time'] * 86400; + } else { + $data['start_time'] = $issueCouponInfo['start_use_time']; + $data['end_time'] = $issueCouponInfo['end_use_time']; + } + $data['type'] = $type; + return $this->dao->save($data); + } + + /**会员领取优惠券 + * @param $uid + * @param $issueCouponInfo + * @param string $type + * @return mixed + */ + public function addMemberUserCoupon($uid, $issueCouponInfo, $type = 'get') + { + $data = []; + $data['cid'] = $issueCouponInfo['id']; + $data['uid'] = $uid; + $data['coupon_title'] = $issueCouponInfo['title']; + $data['coupon_price'] = $issueCouponInfo['coupon_price']; + $data['use_min_price'] = $issueCouponInfo['use_min_price']; + $data['add_time'] = time(); + /* if ($issueCouponInfo['coupon_time']) { + $data['start_time'] = $data['add_time']; + $data['end_time'] = $data['add_time'] + $issueCouponInfo['coupon_time'] * 86400; + } else { + $data['start_time'] = $issueCouponInfo['start_use_time']; + $data['end_time'] = $issueCouponInfo['end_use_time']; + }*/ + $data['start_time'] = strtotime(date('Y-m-d 00:00:00', time())); + $data['end_time'] = strtotime(date('Y-m-d 23:59:59', strtotime('+30 day'))); + $data['type'] = $type; + return $this->dao->save($data); + } + + /** + * 获取用户已领取的优惠卷 + * @param int $uid + * @param $type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCounpon(int $uid, $type) + { + $where = []; + $where['uid'] = $uid; + switch ($type) { + case 0: + case '': + break; + case 1: + $where['status'] = 0; + break; + case 2: + $where['status'] = 1; + break; + default: + $where['status'] = 1; + break; + } + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getCouponListByOrder($where, 'status ASC,add_time DESC', $page, $limit); + /** @var StoreProductCategoryServices $categoryServices */ + $categoryServices = app()->make(StoreProductCategoryServices::class); + $category = $categoryServices->getColumn([], 'pid,cate_name', 'id'); + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $brand = $storeBrandServices->getColumn([], 'id,pid,brand_name', 'id'); + foreach ($list as &$item) { + $item['applicable_type'] = $item['coupon_applicable_type']; + if ($item['category_id'] && isset($category[$item['category_id']])) { + $item['category_type'] = $category[$item['category_id']]['pid'] == 0 ? 1 : 2; + $item['category_name'] = $category[$item['category_id']]['cate_name']; + } else { + $item['category_type'] = ''; + $item['category_name'] = ''; + } + if ($item['brand_id'] && isset($brand[$item['brand_id']])) { + $item['category_name'] = $brand[$item['brand_id']]['brand_name']; + } else { + $item['brand_name'] = ''; + } + } + return $list ? $this->tidyCouponList($list) : []; + } + + /** + * 格式化优惠券 + * @param $couponList + * @return mixed + */ + public function tidyCouponList($couponList) + { + $time = time(); + foreach ($couponList as &$coupon) { + if ($coupon['status'] == '已使用') { + $coupon['_type'] = 0; + $coupon['_msg'] = '已使用'; + $coupon['pc_type'] = 0; + $coupon['pc_msg'] = '已使用'; + } else if ($coupon['status'] == '已过期') { + $coupon['is_fail'] = 1; + $coupon['_type'] = 0; + $coupon['_msg'] = '已过期'; + $coupon['pc_type'] = 0; + $coupon['pc_msg'] = '已过期'; + } else if ($coupon['end_time'] < $time) { + $coupon['is_fail'] = 1; + $coupon['_type'] = 0; + $coupon['_msg'] = '已过期'; + $coupon['pc_type'] = 0; + $coupon['pc_msg'] = '已过期'; + } else if ($coupon['start_time'] > $time) { + $coupon['_type'] = 0; + $coupon['_msg'] = '未开始'; + $coupon['pc_type'] = 1; + $coupon['pc_msg'] = '未开始'; + } else { + if ($coupon['start_time'] + 3600 * 24 > $time) { + $coupon['_type'] = 2; + $coupon['_msg'] = '立即使用'; + $coupon['pc_type'] = 1; + $coupon['pc_msg'] = '可使用'; + } else { + $coupon['_type'] = 1; + $coupon['_msg'] = '立即使用'; + $coupon['pc_type'] = 1; + $coupon['pc_msg'] = '可使用'; + } + } + $coupon['add_time'] = $coupon['_add_time'] = $coupon['start_time'] ? date('Y/m/d', $coupon['start_time']) : date('Y/m/d', $coupon['add_time']); + $coupon['end_time'] = $coupon['_end_time'] = date('Y/m/d', $coupon['end_time']); + $coupon['use_min_price'] = floatval($coupon['use_min_price']); + $coupon['coupon_price'] = floatval($coupon['coupon_price']); + } + return $couponList; + } + + /** + * 获取会员优惠券列表 + * @param $uid + * @return array|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMemberCoupon($uid) + { + if (!$uid) return []; + /** @var StoreCouponIssueServices $couponIssueService */ + $couponIssueService = app()->make(StoreCouponIssueServices::class); + //$couponWhere['status'] = 1; + $couponWhere['receive_type'] = 4; + //$couponWhere['is_del'] = 0; + $couponInfo = $couponIssueService->getMemberCouponIssueList($couponWhere); + $couponList = []; + if ($couponInfo) { + $couponIds = array_column($couponInfo, 'id'); + $couponType = array_column($couponInfo, 'type', 'id'); + $couponList = $this->dao->getCouponListByOrder(['uid' => $uid, 'coupon_ids' => $couponIds], 'add_time desc'); + if ($couponList) { + foreach ($couponList as $k => $v) { + $couponList[$k]['type_name'] = $couponIssueService->_couponType[$couponType[$v['cid']]]; + } + } + } + return $couponList ? $this->tidyCouponList($couponList) : []; + } + + /** + * 根据月分组看会员发放优惠券情况 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function memberCouponUserGroupBymonth(array $where) + { + return $this->dao->memberCouponUserGroupBymonth($where); + } + + /** + * 会员券失效 + * @param $coupon_user + * @return false|mixed + */ + public function memberCouponIsFail($coupon_user) + { + if (!$coupon_user) return false; + if ($coupon_user['use_time'] == 0) { + return $this->dao->update($coupon_user['id'], ['is_fail' => 1, 'status' => 2]); + } + + } + + /** + * 根据id查询会员优惠劵 + * @param int $id + * @param string $filed + * @param array $with + * @return array|false|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCouponUserOne(int $id, string $filed = '', array $with = []) + { + if (!$id) return false; + return $this->dao->getOne(['id' => $id], $filed, $with); + } + + /**根据时间查询用户优惠券 + * @param array $where + * @return array|bool|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCounponByMonth(array $where) + { + if (!$where) return false; + return $this->dao->getUserCounponByMonth($where); + } + + /** + * 检查付费会员是否领取了会员券 + * @param $uid + * @param $vipCouponIds + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkHave($uid, $vipCouponIds) + { + $list = $this->dao->getVipCouponList($uid); + $have = []; + foreach ($list as $item) { + if ($vipCouponIds && in_array($item['cid'], $vipCouponIds)) { + $have[$item['cid']] = true; + } else { + $have[$item['cid']] = false; + } + } + return $have; + } + + /** + * 使用优惠券验证 + * @param int $couponId + * @param int $uid + * @param array $cartInfo + * @param array $promotions + * @param int $store_id + * @return bool + */ + public function useCoupon(int $couponId, int $uid, array $cartInfo, array $promotions = [], int $store_id = 0) + { + if (!$couponId || !$uid || !$cartInfo) { + return true; + } + /** @var StorePromotionsServices $promotionsServices */ + $promotionsServices = app()->make(StorePromotionsServices::class); + try { + [$couponInfo, $couponPrice] = $promotionsServices->useCoupon($couponId, $uid, $cartInfo, $promotions, $store_id); + } catch (\Throwable $e) { + $couponInfo = []; + $couponPrice = 0; + } + if ($couponInfo) { + $this->dao->useCoupon($couponId); + } + return true; + } +} diff --git a/app/services/activity/discounts/StoreDiscountsProductsServices.php b/app/services/activity/discounts/StoreDiscountsProductsServices.php new file mode 100644 index 0000000..b267c1b --- /dev/null +++ b/app/services/activity/discounts/StoreDiscountsProductsServices.php @@ -0,0 +1,85 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\activity\discounts; + +use app\dao\activity\discounts\StoreDiscountsProductsDao; +use app\services\BaseServices; +use app\services\product\sku\StoreProductAttrResultServices; +use app\services\product\sku\StoreProductAttrServices; +use app\services\product\sku\StoreProductAttrValueServices; +use think\exception\ValidateException; + + +/** + * 优惠套餐商品 + * Class StoreDiscountsProductsServices + * @package app\services\activity\discounts + * @mixin StoreDiscountsProductsDao + */ +class StoreDiscountsProductsServices extends BaseServices +{ + public function __construct(StoreDiscountsProductsDao $dao) + { + $this->dao = $dao; + } + + /** + * 删除规格和优惠商品 + * @param $discount_id + * @return mixed + */ + public function del($discount_id) + { + /** @var StoreProductAttrResultServices $storeProductAttrResultServices */ + $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class); + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + + $ids = $this->dao->getColumn(['discount_id' => $discount_id], 'id'); + + foreach ($ids as $id) { + $storeProductAttrServices->del($id, 5); + $storeProductAttrResultServices->del($id, 5); + $storeProductAttrValueServices->del($id, 5); + } + return $this->dao->delete($discount_id, 'discount_id'); + } + + /** + * 下单|加入购物车验证套餐商品库存 + * @param int $uid + * @param int $discount_product_id + * @param int $cartNum + * @param string $unique + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkDiscountsStock(int $uid, int $discount_product_id, int $cartNum = 1, string $unique = '') + { + $discountProductInfo = $this->dao->getDiscountProductInfo($discount_product_id, '*,title as store_name', ['product']); + if (!$discountProductInfo) { + throw new ValidateException('该商品已下架或删除'); + } + $discountProductInfo['temp_id'] = $discountProductInfo['p_temp_id'] ?? 0; + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + $attrInfo = $attrValueServices->getOne(['product_id' => $discount_product_id, 'unique' => $unique, 'type' => 5]); + if (!$attrInfo || $attrInfo['product_id'] != $discount_product_id) { + throw new ValidateException('请选择有效的商品属性'); + } + return [$attrInfo, $unique, $discountProductInfo]; + } +} \ No newline at end of file diff --git a/app/services/activity/discounts/StoreDiscountsServices.php b/app/services/activity/discounts/StoreDiscountsServices.php new file mode 100644 index 0000000..0692459 --- /dev/null +++ b/app/services/activity/discounts/StoreDiscountsServices.php @@ -0,0 +1,415 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\activity\discounts; + +use app\dao\activity\discounts\StoreDiscountsDao; +use app\services\BaseServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrResultServices; +use app\services\product\sku\StoreProductAttrServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\user\label\UserLabelServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; + +/** + * 优惠套餐 + * Class StoreDiscountsServices + * @package app\services\activity\discounts + * @mixin StoreDiscountsDao + */ +class StoreDiscountsServices extends BaseServices +{ + /** + * 商品活动类型 + */ + const TYPE = 5; + + /** + * @param StoreDiscountsDao $dao + */ + public function __construct(StoreDiscountsDao $dao) + { + $this->dao = $dao; + } + + /** + * 保存数据 + * @param $data + * @return mixed + */ + public function saveDiscounts($data) + { + if ($data['freight'] == 2 && !$data['postage']) { + throw new AdminException('请设置运费金额'); + } + if ($data['freight'] == 3 && !$data['temp_id']) { + throw new AdminException('请选择运费模版'); + } + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + /** @var StoreDiscountsProductsServices $storeDiscountsProductsServices */ + $storeDiscountsProductsServices = app()->make(StoreDiscountsProductsServices::class); + return $this->transaction(function () use ($data, $storeProductAttrServices, $storeDiscountsProductsServices) { + //添加优惠套餐 + $discountsData['title'] = $data['title']; + $discountsData['image'] = $data['image']; + $discountsData['type'] = $data['type']; + $discountsData['is_limit'] = $data['is_limit']; + $discountsData['limit_num'] = $data['is_limit'] ? $data['limit_num'] : 0; + $discountsData['link_ids'] = implode(',', $data['link_ids']); + $discountsData['is_time'] = $data['is_time']; + $discountsData['start_time'] = $data['is_time'] ? strtotime($data['time'][0]) : 0; + $discountsData['stop_time'] = $data['is_time'] ? strtotime($data['time'][1]) + 86399 : 0; + $discountsData['sort'] = $data['sort']; + $discountsData['add_time'] = time(); + $discountsData['free_shipping'] = $data['free_shipping']; + $discountsData['status'] = $data['status']; + $discountsData['freight'] = $data['freight']; + $discountsData['postage'] = $data['postage']; + $discountsData['custom_form'] = json_encode($data['custom_form']); + $discountsData['system_form_id'] = $data['system_form_id'] ?? 0; + $product_ids = []; + foreach ($data['products'] as $product) { + if ($product['type']) { + $product_ids = []; + $product_ids[] = $product['product_id']; + break; + } else { + $product_ids[] = $product['product_id']; + } + } + $discountsData['product_ids'] = implode(',', $product_ids); + if ($data['id']) { + unset($discountsData['add_time']); + $this->dao->update($data['id'], $discountsData); + $storeDiscountsProductsServices->del($data['id']); + $discountsId = $data['id']; + } else { + $discountsId = $this->dao->save($discountsData)->id; + } + if (!$discountsId) throw new AdminException('添加失败'); + if ($discountsData['is_limit']) CacheService::setStock(md5($discountsId), (int)$discountsData['limit_num'], 5); + //添加优惠套餐内商品 + /** @var StoreProductServices $productService */ + $productService = app()->make(StoreProductServices::class); + foreach ($data['products'] as $item) { + $stock = $productService->value(['id' => $item['product_id'], 'is_del' => 0, 'is_show' => 1], 'stock') ?? 0; + if ((int)$stock <= 0) { + throw new AdminException('商品库存不足,请选择其他商品'); + } + $productData = []; + $productData['discount_id'] = $discountsId; + $productData['product_id'] = $item['product_id']; + $productData['product_type'] = $item['product_type'] ?? 0; + $productData['title'] = $item['store_name']; + $productData['image'] = $item['image']; + $productData['type'] = $item['type']; + $productData['temp_id'] = $item['temp_id']; + $discountsProducts = $storeDiscountsProductsServices->save($productData); + $skuList = $storeProductAttrServices->validateProductAttr($item['items'], $item['attr'], (int)$discountsProducts->id, 5); + $valueGroup = $storeProductAttrServices->saveProductAttr($skuList, (int)$discountsProducts->id, 5); + if (!$discountsProducts || !$valueGroup) throw new AdminException('添加失败'); + } + return true; + }); + } + + /** + * 获取列表 + * @param array $where + * @return array + */ + public function getList($where = []) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, ['products'], $page, $limit); + $count = $this->dao->count($where + ['is_del' => 0]); + $time = time(); + foreach ($list as &$item) { + if (!$this->checkDiscount($item, 0) || ($item['stop_time'] && $item['stop_time'] < $time)) { + $item['status'] = 0; + $this->dao->update(['id' => $item['id']], ['status' => 0]); + } + $item['start_time'] = $item['start_time'] ? date('Y-m-d', $item['start_time']) : 0; + $item['stop_time'] = $item['stop_time'] ? date('Y-m-d', $item['stop_time']) : 0; + $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']); + } + return compact('list', 'count'); + } + + /** + * 获取详情 + * @param $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getInfo($id) + { + $discounts = $this->dao->get($id); + if ($discounts) { + $discounts = $discounts->toArray(); + } else { + throw new AdminException('套餐商品未找到!'); + } + /** @var StoreDiscountsProductsServices $discountsProducts */ + $discountsProducts = app()->make(StoreDiscountsProductsServices::class); + /** @var UserLabelServices $userLabelServices */ + $userLabelServices = app()->make(UserLabelServices::class); + $discounts['products'] = $discountsProducts->dao->getList(['discount_id' => $discounts['id']]); + foreach ($discounts['products'] as &$item) { + /** @var StoreProductAttrResultServices $storeProductAttrResultServices */ + $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class); + $discountsResult = $storeProductAttrResultServices->value(['product_id' => $item['id'], 'type' => 5], 'result'); + $item['items'] = json_decode($discountsResult, true)['attr']; + $item['attr'] = $this->getattr($item['items'], $item['id'], 5); + $item['store_name'] = $item['title']; + } + if ($discounts['start_time']) { + $discounts['time'] = [date('Y-m-d', $discounts['start_time']), date('Y-m-d', $discounts['stop_time'])]; + } else { + $discounts['time'] = []; + } + $link_ids = explode(',', $discounts['link_ids']); + $discounts['link_ids'] = $userLabelServices->getLabelList(['ids' => $link_ids], ['id', 'label_name']); + return $discounts; + } + + /** + * 获取规格 + * @param $attr + * @param $id + * @param $type + * @return array + */ + public function getattr($attr, $id, $type) + { + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $value = attr_format($attr)[1]; + $valueNew = []; + $count = 0; + foreach ($value as $key => $item) { + $detail = $item['detail']; + $suk = implode(',', $item['detail']); + $sukValue = $storeProductAttrValueServices->getSkuArray(['product_id' => $id, 'type' => $type, 'suk' => $suk], 'bar_code,code,cost,price,ot_price,stock,image as pic,weight,volume,brokerage,brokerage_two,quota,quota_show', 'suk'); + if (count($sukValue)) { + $valueNew[$count]['value'] = ''; + foreach (array_values($detail) as $k => $v) { + $valueNew[$count]['value' . ($k + 1)] = $v; + $valueNew[$count]['value'] .= $valueNew[$count]['value'] == '' ? $v : ',' . $v; + } + $valueNew[$count]['detail'] = $detail; + $valueNew[$count]['pic'] = $sukValue[$suk]['pic'] ?? ''; + $valueNew[$count]['price'] = $sukValue[$suk]['price'] ? floatval($sukValue[$suk]['price']) : 0; + $valueNew[$count]['cost'] = $sukValue[$suk]['cost'] ? floatval($sukValue[$suk]['cost']) : 0; + $valueNew[$count]['ot_price'] = isset($sukValue[$suk]['ot_price']) ? floatval($sukValue[$suk]['ot_price']) : 0; + $valueNew[$count]['stock'] = $sukValue[$suk]['stock'] ? intval($sukValue[$suk]['stock']) : 0; +// $valueNew[$count]['quota'] = $sukValue[$suk]['quota'] ? intval($sukValue[$suk]['quota']) : 0; + $valueNew[$count]['quota'] = isset($sukValue[$suk]['quota_show']) && $sukValue[$suk]['quota_show'] ? intval($sukValue[$suk]['quota_show']) : 0; + $valueNew[$count]['bar_code'] = $sukValue[$suk]['bar_code'] ?? ''; + $valueNew[$count]['code'] = $sukValue[$suk]['code'] ?? ''; + $valueNew[$count]['weight'] = $sukValue[$suk]['weight'] ? floatval($sukValue[$suk]['weight']) : 0; + $valueNew[$count]['volume'] = $sukValue[$suk]['volume'] ? floatval($sukValue[$suk]['volume']) : 0; + $valueNew[$count]['brokerage'] = $sukValue[$suk]['brokerage'] ? floatval($sukValue[$suk]['brokerage']) : 0; + $valueNew[$count]['brokerage_two'] = $sukValue[$suk]['brokerage_two'] ? floatval($sukValue[$suk]['brokerage_two']) : 0; + $count++; + } + } + return $valueNew; + } + + /** + * 修改状态 + * @param $id + * @param $status + * @return mixed + */ + public function setStatus($id, $status) + { + $discounts = $this->dao->get($id, ['*'], ['products']); + if ($discounts) { + $discounts = $discounts->toArray(); + } else { + throw new AdminException('套餐商品未找到!'); + } + //上架 + if ($status) { + if ($discounts['stop_time'] && $discounts['stop_time'] < time()) { + throw new AdminException('套餐活动已结束'); + } + if (!$this->checkDiscount($discounts, 0)) { + throw new AdminException('套餐内商品已下架或者库存不足'); + } + } + return $this->dao->update($id, ['status' => $status]); + } + + /** + * 删除商品 + * @param $id + * @return mixed + */ + public function del($id) + { + return $this->dao->update($id, ['is_del' => 1]); + } + + /** + * 获取优惠套餐列表 + * @param int $product_id + * @param int $uid + * @param int $limit + * @return array + */ + public function getDiscounts(int $product_id, int $uid, int $limit = 0) + { + $page = $limit ? 1 : 0; + $list = $this->dao->getDiscounts($product_id, '*', $page, $limit); + foreach ($list as $key => &$discounts) { + $discounts = $this->checkDiscount($discounts, $uid); + if (!$discounts) unset($list[$key]); + } + return $list; + } + + /** + * 购买|退款处理套餐限量 + * @param int $discountId + * @param bool $is_dec + * @return bool + */ + public function changeDiscountLimit(int $discountId, bool $is_dec = true) + { + $is_limit = $this->dao->value(['id' => $discountId], 'is_limit'); + $res = true; + //开启限量 + if ($is_limit) { + if ($is_dec) $res = $res && $this->dao->decLimitNum($discountId); + else $res = $res && $this->dao->incLimitNum($discountId); + } + return $res; + } + + /** + * 优惠套餐库存减少 + * @param int $num + * @param int $discountId + * @param int $discount_product_id + * @param int $product_id + * @param string $unique + * @param int $store_id + * @return bool + */ + public function decDiscountStock(int $num, int $discountId, int $discount_product_id, int $product_id, string $unique, int $store_id = 0) + { + if (!$discountId || !$discount_product_id || !$product_id) return false; + $res = true; + if ($unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //套餐商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $discount_product_id, 'type' => 5], 'suk'); + //平台商品sku unique + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //减掉普通商品sku的库存加销量 + $res = false !== $services->decProductStock($num, $product_id, (string)$productUnique, $store_id); + } + return $res; + } + + /** + * 加库存减销量 + * @param int $num + * @param int $discountId 套餐id + * @param int $discount_product_id 套餐商品id + * @param int $product_id 原商品ID + * @param string $unique + * @return bool + */ + public function incDiscountStock(int $num, int $discountId, int $discount_product_id, int $product_id, string $unique, int $store_id = 0) + { + if (!$discountId || !$discount_product_id || !$product_id) return false; + $res = true; + if ($unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //套餐商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $discount_product_id, 'type' => 5], 'suk'); + //平台普通商品sku unique + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //增加当前普通商品sku的库存,减去销量 + $res = $res && $services->incProductStock($num, $product_id, (string)$productUnique, $store_id); + } + return $res; + } + + /** + * 判断优惠套餐显示 + * @param $discount + * @param $uid + * @return false + */ + public function checkDiscount($discount, $uid) + { + $discount = is_object($discount) ? $discount->toArray() : $discount; + if ($discount['is_limit'] && $discount['limit_num'] <= 0) return false; + if (isset($discount['products']) && $discount['products']) { + /** @var StoreProductServices $productService */ + $productService = app()->make(StoreProductServices::class); + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + $minPrice = 0; + foreach ($discount['products'] as $key => &$item) { + $stock = $productService->value(['id' => $item['product_id'], 'is_del' => 0, 'is_show' => 1], 'stock') ?? 0; + try { + [$productAttr, $productValue] = $storeProductAttrServices->getProductAttrDetailCache($item['id'], $uid, 0, 5, $item['product_id']); + } catch(\Throwable $e) { + return false; + } + if ($discount['type']) { + if ($item['type']) { + if ($stock == 0) { + return false; + } else { + if (!array_sum(array_column($productValue, 'product_stock'))) return false; + } + } else { + if (!array_sum(array_column($productValue, 'product_stock'))) unset($discount['products'][$key]); + } + } else { + if ($stock == 0) { + return false; + } else { + if (!array_sum(array_column($productValue, 'product_stock'))) return false; + } + } + $item['productAttr'] = $productAttr; + $item['productValue'] = $productValue; + $minPrice += min(array_column($productValue, 'price')); + } + if (!count($discount['products'])) return false; + $discount['min_price'] = $minPrice; + $discount['products'] = array_merge($discount['products']); + } + return $discount; + } +} diff --git a/app/services/activity/integral/StoreIntegralOrderServices.php b/app/services/activity/integral/StoreIntegralOrderServices.php new file mode 100644 index 0000000..24fd801 --- /dev/null +++ b/app/services/activity/integral/StoreIntegralOrderServices.php @@ -0,0 +1,961 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\activity\integral; + +use app\dao\activity\integral\StoreIntegralOrderDao; +use app\jobs\notice\PrintJob; +use app\services\activity\lottery\LuckLotteryServices; +use app\services\BaseServices; +use app\services\diy\DiyServices; +use app\services\message\notice\NoticeSmsService; +use app\services\message\SystemMessageServices; +use app\services\pay\PayServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\product\sku\StoreProductVirtualServices; +use app\services\serve\ServeServices; +use app\services\other\ExpressServices; +use app\services\system\config\ConfigServices; +use app\services\system\form\SystemFormServices; +use app\services\user\UserServices; +use app\services\user\UserAddressServices; +use app\services\user\UserBillServices; +use crmeb\services\FormBuilder as Form; +use crmeb\services\printer\Printer; +use crmeb\services\SystemConfigService; +use crmeb\traits\ServicesTrait; +use crmeb\utils\Arr; +use think\exception\ValidateException; +use think\facade\Log; + +/** + * Class StoreIntegralOrderServices + * @package app\services\activity\integral + * @mixin StoreIntegralOrderDao + */ +class StoreIntegralOrderServices extends BaseServices +{ + + use ServicesTrait; + + /** + * 发货类型 + * @var string[] + */ + public $deliveryType = ['send' => '商家配送', 'express' => '快递配送', 'fictitious' => '虚拟发货']; + + /** + * StoreIntegralOrderServices constructor. + * @param StoreIntegralOrderDao $dao + */ + public function __construct(StoreIntegralOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderList(array $where, array $field = ['*'], array $with = []) + { + [$page, $limit] = $this->getPageValue(); + $data = $this->dao->getOrderList($where, $field, $page, $limit, $with); + $count = $this->dao->count($where); + $data = $this->tidyOrderList($data); + $batch_url = "file/upload/1"; + return compact('data', 'count', 'batch_url'); + } + + /** + * 获取导出数据 + * @param array $where + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExportList(array $where, int $limit = 0) + { + if ($limit) { + [$page] = $this->getPageValue(); + } else { + [$page, $limit] = $this->getPageValue(); + } + $data = $this->dao->getOrderList($where, ['*'], $page, $limit); + $data = $this->tidyOrderList($data); + return $data; + } + + /** + * 前端订单列表 + * @param array $where + * @param array|string[] $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderApiList(array $where, array $field = ['*'], array $with = []) + { + [$page, $limit] = $this->getPageValue(); + $data = $this->dao->getOrderList($where, $field, $page, $limit, $with); + $data = $this->tidyOrderList($data); + return $data; + } + + /** + * 订单详情数据格式化 + * @param $order + * @return mixed + */ + public function tidyOrder($order) + { + $order['_add_time'] = $order['add_time'] = date('Y-m-d H:i:s', $order['add_time']); + if ($order['status'] == 1) { + $order['status_name'] = '未发货'; + } else if ($order['status'] == 2) { + $order['status_name'] = '待收货'; + } else if ($order['status'] == 3) { + $order['status_name'] = '已完成'; + } + return $order; + } + + /** + * 数据转换 + * @param array $data + * @return array + */ + public function tidyOrderList(array $data) + { + foreach ($data as &$item) { + $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']); + if ($item['status'] == 1) { + $item['status_name'] = '未发货'; + } else if ($item['status'] == 2) { + $item['status_name'] = '待收货'; + } else if ($item['status'] == 3) { + $item['status_name'] = '已完成'; + } + } + return $data; + } + + /** + * 创建订单 + * @param $uid + * @param $addressId + * @param string $mark + * @param $user + * @param $num + * @param $productInfo + * @throws \Exception + */ + public function createOrder($uid, $addressId, $payType, $mark, $userInfo, $num, $productInfo, $customForm) + { + if ($productInfo['product_type'] == 0 && !$addressId) { + throw new ValidateException('请选择收货地址!'); + } + if ($addressId) { + /** @var UserAddressServices $addressServices */ + $addressServices = app()->make(UserAddressServices::class); + if (!$addressInfo = $addressServices->getOne(['uid' => $uid, 'id' => $addressId, 'is_del' => 0])) + throw new ValidateException('地址选择有误!'); + $addressInfo = $addressInfo->toArray(); + } else { + $addressInfo = []; + } + + $total_price = bcmul($productInfo['attrInfo']['price'], $num, 2); + $total_integral = bcmul($productInfo['attrInfo']['integral'], $num, 2); + if ($total_integral > $userInfo['integral']) throw new ValidateException('积分不足!'); + if ($total_price <= 0) { + $paid = 1; + $orderInfo['pay_time'] = time(); + $payType = 'integral'; + } else { + $paid = 0; + } + $productInfo['cart_num'] = $num; + $orderInfo = [ + 'uid' => $uid, + 'order_id' => $this->getNewOrderId(), + 'real_name' => $addressInfo['real_name'] ?? '', + 'user_phone' => $addressInfo['phone'] ?? '', + 'user_address' => $addressInfo ? $addressInfo['province'] . ' ' . $addressInfo['city'] . ' ' . $addressInfo['district'] . ' ' . $addressInfo['detail'] : '', + 'product_id' => $productInfo['id'], + 'image' => $productInfo['attrInfo']['image'] ?? '', + 'store_name' => $productInfo['title'], + 'suk' => $productInfo['attrInfo']['suk'] ?? '', + 'unique' => $productInfo['attrInfo']['unique'] ?? '', + 'total_num' => $num, + 'price' => $productInfo['attrInfo']['price'] ?? '', + 'total_price' => $total_price, + 'integral' => $productInfo['attrInfo']['integral'] ?? '', + 'total_integral' => $total_integral, + 'paid' => $paid, + 'pay_type' => $payType, + 'add_time' => time(), + 'status' => 1, + 'mark' => $mark, + 'channel_type' => $userInfo['user_type'], + 'product_type' => $productInfo['product_type'], + 'custom_form' => json_encode($customForm), + 'cart_info' => json_encode([$productInfo]) + ]; + $order = $this->transaction(function () use ($orderInfo, $userInfo, $productInfo, $uid, $num, $total_integral, $total_price) { + //创建订单 + $order = $this->dao->save($orderInfo); + if (!$order) { + throw new ValidateException('订单生成失败!'); + } + //扣库存 + $this->decGoodsStock($productInfo, $num); + //减积分 + $this->deductIntegral($userInfo, $total_integral, (int)$userInfo['uid'], $order->id); + if ($total_price <= 0) { + //卡密发放 + if ($orderInfo['product_type']) { + $this->sendCard((int)$order->id); + //修改订单状态 + $this->dao->update((int)$order->id, ['status' => 3]); + } + } + return $order; + }); + /** @var StoreIntegralOrderStatusServices $statusService */ + $statusService = app()->make(StoreIntegralOrderStatusServices::class); + $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'cache_key_create_order', + 'change_message' => '订单生成', + 'change_time' => time() + ]); + return $order; + } + + /** + * 抵扣积分 + * @param array $userInfo + * @param bool $useIntegral + * @param array $priceData + * @param int $uid + * @param string $key + */ + public function deductIntegral(array $userInfo, $priceIntegral, int $uid, string $orderId) + { + $res2 = true; + if ($userInfo['integral'] > 0) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if ($userInfo['integral'] > $priceIntegral) { + $integral = bcsub((string)$userInfo['integral'], (string)$priceIntegral); + } else { + $integral = 0; + } + $res2 = $userServices->update($uid, ['integral' => $integral]); + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $res3 = $userBillServices->income('storeIntegral_use_integral', $uid, (int)$priceIntegral, (int)$integral, $orderId); + $res2 = $res2 && false != $res3; + } + if (!$res2) { + throw new ValidateException('使用积分抵扣失败!'); + } + } + + /**未支付积分订单退积分 + * @return bool|void + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function rebatePointsOrders() + { + $where['is_del'] = 0; + $where['is_system_del'] = 0; + $where['paid'] = 0; + $where['is_price'] = 1; + $where['is_integral'] = 1; + $list = $this->dao->getIntegralOrderList($where); + //系统预设取消订单时间段 + $keyValue = ['rebate_points_orders_time', 'order_cancel_time']; + //获取配置 + $systemValue = SystemConfigService::more($keyValue); + //格式化数据 + $systemValue = Arr::setValeTime($keyValue, is_array($systemValue) ? $systemValue : []); + $order_cancel_time = $systemValue['rebate_points_orders_time'] ?: $systemValue['order_cancel_time']; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $order_cancel_time = (int)bcmul((string)$order_cancel_time, '3600', 0); + foreach ($list as $key => $item) { + if (($item['add_time'] + $order_cancel_time) < time()) { + try { + $this->transaction(function () use ($item, $userServices, $userBillServices) { + $user_integral = $userServices->value(['uid' => $item['uid']], 'integral'); + $res = $userServices->incUpdate(['uid' => $item['uid']], 'integral', (int)$item['integral']); + $integral = bcadd((string)$user_integral, (string)$item['integral']); + $res1 = $userBillServices->income('integral_refund', $item['uid'], (int)$item['integral'], (int)$integral, $item['id']); + //修改订单状态 + $res = $res && $res1 && $this->dao->update($item['id'], ['is_del' => 1, 'is_system_del' => 1, 'mark' => '积分订单未支付已超过系统预设时间']); + }); + } catch (\Throwable $e) { + Log::error('未支付积分订单退积分失败,失败原因:' . $e->getMessage(), $e->getTrace()); + } + } + } + return true; + } + + /** + * 扣库存 + * @param array $cartInfo + * @param int $combinationId + * @param int $seckillId + * @param int $bargainId + */ + public function decGoodsStock(array $productInfo, int $num) + { + $res5 = true; + /** @var StoreIntegralServices $StoreIntegralServices */ + $StoreIntegralServices = app()->make(StoreIntegralServices::class); + try { + $res5 = $res5 && $StoreIntegralServices->decIntegralStock((int)$num, $productInfo['attrInfo']['product_id'] ?? 0, $productInfo['attrInfo']['unique'] ?? ''); + if (!$res5) { + throw new ValidateException('库存不足!'); + } + } catch (\Throwable $e) { + throw new ValidateException('库存不足!'); + } + } + + /** + * 发送卡密 + * @param int $id + * @param array $orderInfo + * @return bool + */ + public function sendCard(int $id, $orderInfo = []) + { + if (!$id && !$orderInfo) return false; + + if (!$orderInfo) { + $orderInfo = $this->dao->get($id); + } + if ($orderInfo['fictitious'] && $orderInfo['fictitious'] == 'fictitious') { + return true; + } + try { + switch ($orderInfo['product_type']) { + case 1: + /** @var SystemMessageServices $SystemMessageServices */ + $SystemMessageServices = app()->make(SystemMessageServices::class); + /** @var StoreIntegralOrderStatusServices $statusService */ + $statusService = app()->make(StoreIntegralOrderStatusServices::class); + $orderInfo['cart_info'] = is_string($orderInfo['cart_info']) ? json_decode($orderInfo['cart_info'], true) : $orderInfo['cart_info']; + + $title = $content = $disk_info = $virtual_info = ''; + $unique = $orderInfo['unique']; + //活动订单共用原商品规格卡密 + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + $attrValue = $skuValueServices->getUniqueByActivityUnique($unique, (int)$orderInfo['product_id'], 4, ['unique', 'disk_info']); + if ($attrValue) { + $disk_info = $attrValue['disk_info'] ?? ''; + $unique = $attrValue['unique'] ?? ''; + } + if ($disk_info) { + $title = '虚拟密钥发放'; + $content = '您购买的密钥商品已支付成功,'; + if ($orderInfo['total_price'] > 0) { + $content .= '支付金额' . $orderInfo['total_price'] . '元,'; + } elseif ($orderInfo['total_integral'] > 0) { + $content .= '支付积分' . $orderInfo['total_integral']; + } + $content .= ',订单号:' . $orderInfo['order_id'] . ',密钥:' . $disk_info . ',感谢您的光临!'; + $virtual_info = '密钥自动发放:' . $disk_info; + $value = '密钥:' . $disk_info; +// $remark = '密钥自动发放:' . $disk_info; + } else { + /** @var StoreProductVirtualServices $virtualService */ + $virtualService = app()->make(StoreProductVirtualServices::class); + $cardList = $virtualService->getOrderCardList(['store_id' => $orderInfo['store_id'], 'attr_unique' => $unique, 'uid' => 0], (int)$orderInfo['total_num']); + $title = '虚拟卡密发放'; + $virtual_info = '卡密已自动发放'; + $value = ''; +// $remark = '卡密已自动发放'; + if ($cardList) { + $content = '您购买的密钥商品已支付成功,'; + if ($orderInfo['total_price'] > 0) { + $content .= '支付金额' . $orderInfo['total_price'] . '元,'; + } elseif ($orderInfo['total_integral'] > 0) { + $content .= '支付积分' . $orderInfo['total_integral']; + } + $content .= ',订单号:' . $orderInfo['order_id']; + $update = []; + $update['order_id'] = $orderInfo['order_id']; + $update['uid'] = $orderInfo['uid']; + $update['order_type'] = 2; + foreach ($cardList as $virtual) { + $virtualService->update($virtual['id'], $update); + $content .= ',卡号:' . $virtual['card_no'] . ';密码:' . $virtual['card_pwd'] . "\n"; + $virtual_info .= ',卡号:' . $virtual['card_no'] . ';密码:' . $virtual['card_pwd'] . ';'; + $value .= '卡号:' . $virtual['card_no'] . ';密码:' . $virtual['card_pwd']; +// $remark .= ',卡号:' . $virtual['card_no'] . ';密码:' . $virtual['card_pwd'] . ';'; + } + $content .= ',感谢您的光临!'; + } + } + //修改订单虚拟备注 + $this->dao->update(['id' => $orderInfo['id']], ['status' => 1, 'delivery_type' => 'fictitious', 'virtual_info' => $virtual_info]); + $data['id'] = $orderInfo['id']; + $data['uid'] = $orderInfo['uid']; + $data['order_id'] = $orderInfo['order_id']; + $data['title'] = $title; + $data['value'] = $value; + $data['content'] = $content; + $data['is_integral'] = 1; + event('notice.notice', [$data, 'kami_deliver_goods_code']); + $statusService->save([ + 'oid' => $orderInfo['id'], + 'change_type' => 'delivery_fictitious', + 'change_message' => '卡密自动发货', + 'change_time' => time() + ]); + break; + } + } catch (\Throwable $e) { + Log::error('订单虚拟商品自动发放失败,原因:' . $e->getMessage()); + } + return true; + } + + /** + * 使用雪花算法生成订单ID + * @return string + * @throws \Exception + */ + public function getNewOrderId(string $prefix = 'wx') + { + $snowflake = new \Godruoyi\Snowflake\Snowflake(); + //32位 + if (PHP_INT_SIZE == 4) { + $id = abs($snowflake->id()); + } else { + $id = $snowflake->setStartTimeStamp(strtotime('2020-06-05') * 1000)->id(); + } + return $prefix . $id; + } + + /** + *获取订单数量 + * @param array $where + * @return mixed + */ + public function orderCount(array $where) + { + //全部订单 + $data['statusAll'] = (string)$this->dao->count($where + ['is_system_del' => 0]); + //未发货 + $data['unshipped'] = (string)$this->dao->count($where + ['status' => 1, 'is_system_del' => 0]); + //待收货 + $data['untake'] = (string)$this->dao->count($where + ['status' => 2, 'is_system_del' => 0]); + //待评价 +// $data['unevaluate'] = (string)$this->dao->count(['status' => 3, 'time' => $where['time'], 'is_system_del' => 0]); + //交易完成 + $data['complete'] = (string)$this->dao->count($where + ['status' => 3, 'is_system_del' => 0]); + return $data; + } + + + /** + * 打印订单 + * @param int $id + * @return bool|mixed|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function orderPrint(int $id) + { + $order = $this->dao->get($id); + if (!$order) { + throw new ValidateException('订单没有查到,无法打印!'); + } + /** @var ConfigServices $configServices */ + $configServices = app()->make(ConfigServices::class); + [$switch, $name, $configData] = $configServices->getPrintingConfig(); + if (!$switch) { + throw new ValidateException('请先开启小票打印'); + } + foreach ($configData as $value) { + if (!$value) { + throw new ValidateException('请先配置小票打印开发者'); + } + } + $printer = new Printer($name, $configData); + $res = $printer->setIntegralPrinterContent([ + 'name' => sys_config('site_name'), + 'orderInfo' => is_object($order) ? $order->toArray() : $order, + ])->startPrinter(); + if (!$res) { + throw new ValidateException($printer->getError()); + } + return $res; + } + + /** + * 获取订单确认数据 + * @param array $user + * @param $cartId + * @return mixed + */ + public function getOrderConfirmData(array $user, $unique, $num) + { + /** @var StoreProductAttrValueServices $StoreProductAttrValueServices */ + $StoreProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $attrValue = $StoreProductAttrValueServices->uniqueByField($unique, 4); + if (!$attrValue) { + throw new ValidateException('请重新选择商品规格'); + } + /** @var StoreIntegralServices $storeIntrgralServices */ + $storeIntrgralServices = app()->make(StoreIntegralServices::class); + $productInfo = $storeIntrgralServices->getIntegralOne((int)$attrValue['product_id']); + if (!$productInfo) { + throw new ValidateException('该商品已下架'); + } + if ($productInfo['once_num'] < $num && $productInfo['once_num'] != -1) { + throw new ValidateException('每个订单限购' . $productInfo['once_num'] . '件'); + } + $data = []; + $attrValue = is_object($attrValue) ? $attrValue->toArray() : $attrValue; + $productInfo['attrInfo'] = $attrValue; + $productInfo['unique'] = $attrValue['unique']; + $productInfo['store_name'] = $productInfo['title']; + $data['now_money'] = $user['now_money']; + $data['integral'] = $user['integral']; + $data['yue_pay_status'] = (int)sys_config('balance_func_status') && (int)sys_config('yue_pay_status') == 1 ? (int)1 : (int)2;//余额支付 1 开启 2 关闭 + $data['pay_weixin_open'] = (int)sys_config('pay_weixin_open') ?? 0;//微信支付 1 开启 0 关闭 + $data['ali_pay_status'] = (bool)sys_config('ali_pay_status');//支付包支付 1 开启 0 关闭 + $data['num'] = $num; + $data['total_integral'] = bcmul($num, $attrValue['integral'], 0); + $data['total_price'] = bcmul($num, $attrValue['price'], 2); + $data['productInfo'] = $productInfo; + $data['custom_form'] = []; + if (isset($productInfo['system_form_id']) && $productInfo['system_form_id']) { + /** @var SystemFormServices $systemFormServices */ + $systemFormServices = app()->make(SystemFormServices::class); + $formInfo = $systemFormServices->value(['id' => $productInfo['system_form_id']], 'value'); + if ($formInfo) { + $data['custom_form'] = is_string($formInfo) ? json_decode($formInfo, true) : $formInfo; + } + } + return $data; + } + + /** + * 删除订单 + * @param $uni + * @param $uid + * @return bool + */ + public function removeOrder(string $order_id, int $uid) + { + $order = $this->getUserOrderDetail($order_id, $uid); + if ($order['status'] != 3) + throw new ValidateException('该订单无法删除!'); + + $order->is_del = 1; + /** @var StoreIntegralOrderStatusServices $statusService */ + $statusService = app()->make(StoreIntegralOrderStatusServices::class); + $res = $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'remove_order', + 'change_message' => '删除订单', + 'change_time' => time() + ]); + if ($order->save() && $res) { + return true; + } else + throw new ValidateException('订单删除失败!'); + } + + /** + * 订单发货 + * @param int $id + * @param array $data + * @return bool + */ + public function delivery(int $id, array $data) + { + $orderInfo = $this->dao->get($id); + if (!$orderInfo) { + throw new ValidateException('订单未能查到,不能发货!'); + } + if ($orderInfo['paid'] != 1) { + throw new ValidateException('订单未支付'); + } + if ($orderInfo->is_del) { + throw new ValidateException('订单已删除,不能发货!'); + } + if ($orderInfo->status != 1) { + throw new ValidateException('订单已发货请勿重复操作!'); + } + $type = (int)$data['type']; + unset($data['type']); + switch ($type) { + case 1: + //发货 + $this->orderDeliverGoods($id, $data, $orderInfo); + break; + case 2: + $this->orderDelivery($id, $data, $orderInfo); + break; + case 3: + $this->orderVirtualDelivery($id, $data, $orderInfo); + break; + default: + throw new ValidateException('暂时不支持其他发货类型'); + } + // 小程序订单管理 + event('order.routine.shipping', ['integral', $orderInfo, $type, $data['delivery_id'], $data['delivery_name']]); + return true; + } + + /** + * 虚拟发货 + * @param int $id + * @param array $data + */ + public function orderVirtualDelivery(int $id, array $data) + { + $data['delivery_type'] = 'fictitious'; + $data['status'] = 2; + unset($data['sh_delivery_name'], $data['sh_delivery_id'], $data['delivery_name'], $data['delivery_id']); + //保存信息 + /** @var StoreIntegralOrderStatusServices $services */ + $services = app()->make(StoreIntegralOrderStatusServices::class); + $this->transaction(function () use ($id, $data, $services) { + $this->dao->update($id, $data); + $services->save([ + 'oid' => $id, + 'change_type' => 'delivery_fictitious', + 'change_message' => '已虚拟发货', + 'change_time' => time() + ]); + }); + } + + /** + * 订单配送 + * @param int $id + * @param array $data + */ + public function orderDelivery(int $id, array $data, $orderInfo) + { + $data['delivery_type'] = 'send'; + $data['delivery_name'] = $data['sh_delivery_name']; + $data['delivery_id'] = $data['sh_delivery_id']; + $data['delivery_uid'] = $data['sh_delivery_uid']; + //获取核销码 + $data['verify_code'] = $this->getStoreCode(); + unset($data['sh_delivery_name'], $data['sh_delivery_id'], $data['sh_delivery_uid']); + if (!$data['delivery_name']) { + throw new ValidateException('请输入送货人姓名'); + } + if (!$data['delivery_id']) { + throw new ValidateException('请输入送货人电话号码'); + } + if (!$data['delivery_uid']) { + throw new ValidateException('请输入送货人信息'); + } + if (!check_phone($data['delivery_id'])) { + throw new ValidateException('请输入正确的送货人电话号码'); + } + $data['status'] = 2; + $orderInfo->delivery_type = $data['delivery_type']; + $orderInfo->delivery_name = $data['delivery_name']; + $orderInfo->delivery_id = $data['delivery_id']; + $orderInfo->status = $data['status']; + /** @var StoreIntegralOrderStatusServices $services */ + $services = app()->make(StoreIntegralOrderStatusServices::class); + $this->transaction(function () use ($id, $data, $services) { + $this->dao->update($id, $data); + //记录订单状态 + $services->save([ + 'oid' => $id, + 'change_type' => 'delivery', + 'change_time' => time(), + 'change_message' => '已配送 发货人:' . $data['delivery_name'] . ' 发货人电话:' . $data['delivery_id'] + ]); + }); + return true; + } + + /** + * 订单快递发货 + * @param int $id + * @param array $data + */ + public function orderDeliverGoods(int $id, array $data, $orderInfo) + { + if (!$data['delivery_name']) { + throw new ValidateException('请选择快递公司'); + } + $data['delivery_type'] = 'express'; + if ($data['express_record_type'] == 2) {//电子面单 + if (!$data['delivery_code']) { + throw new ValidateException('快递公司编缺失'); + } + if (!$data['express_temp_id']) { + throw new ValidateException('请选择电子面单模板'); + } + if (!$data['to_name']) { + throw new ValidateException('请填写寄件人姓名'); + } + if (!$data['to_tel']) { + throw new ValidateException('请填写寄件人电话'); + } + if (!$data['to_addr']) { + throw new ValidateException('请填写寄件人地址'); + } + /** @var ServeServices $ServeServices */ + $ServeServices = app()->make(ServeServices::class); + $expData['com'] = $data['delivery_code']; + $expData['to_name'] = $orderInfo->real_name; + $expData['to_tel'] = $orderInfo->user_phone; + $expData['to_addr'] = $orderInfo->user_address; + $expData['from_name'] = $data['to_name']; + $expData['from_tel'] = $data['to_tel']; + $expData['from_addr'] = $data['to_addr']; + $expData['siid'] = sys_config('config_export_siid'); + $expData['temp_id'] = $data['express_temp_id']; + $expData['count'] = $orderInfo->total_num; + $expData['cargo'] = $orderInfo->store_name . '(' . $orderInfo->suk . ')*' . $orderInfo->total_num; + $expData['order_id'] = $orderInfo->order_id; + if (!sys_config('config_export_open', 0)) { + throw new ValidateException('系统通知:电子面单已关闭,请选择其他发货方式!'); + } + $dump = $ServeServices->express()->dump($expData); + $orderInfo->delivery_id = $dump['kuaidinum']; + $data['express_dump'] = json_encode([ + 'com' => $expData['com'], + 'from_name' => $expData['from_name'], + 'from_tel' => $expData['from_tel'], + 'from_addr' => $expData['from_addr'], + 'temp_id' => $expData['temp_id'], + 'cargo' => $expData['cargo'], + ]); + $data['delivery_id'] = $dump['kuaidinum']; + } else { + if (!$data['delivery_id']) { + throw new ValidateException('请输入快递单号'); + } + $orderInfo->delivery_id = $data['delivery_id']; + } + $data['status'] = 2; + $orderInfo->delivery_type = $data['delivery_type']; + $orderInfo->delivery_name = $data['delivery_name']; + $orderInfo->status = $data['status']; + /** @var StoreIntegralOrderStatusServices $services */ + $services = app()->make(StoreIntegralOrderStatusServices::class); + $this->transaction(function () use ($id, $data, $services) { + $res = $this->dao->update($id, $data); + $res = $res && $services->save([ + 'oid' => $id, + 'change_time' => time(), + 'change_type' => 'delivery_goods', + 'change_message' => '已发货 快递公司:' . $data['delivery_name'] . ' 快递单号:' . $data['delivery_id'] + ]); + if (!$res) { + throw new ValidateException('发货失败:数据保存不成功'); + } + }); + return true; + } + + /** + * 核销订单生成核销码 + * @return false|string + */ + public function getStoreCode() + { + mt_srand(); + [$msec, $sec] = explode(' ', microtime()); + $num = time() + mt_rand(10, 999999) . '' . substr($msec, 2, 3);//生成随机数 + if (strlen($num) < 12) + $num = str_pad((string)$num, 12, 0, STR_PAD_RIGHT); + else + $num = substr($num, 0, 12); + if ($this->dao->count(['verify_code' => $num])) { + return $this->getStoreCode(); + } + return $num; + } + + /** + * 获取修改配送信息表单结构 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function distributionForm(int $id) + { + if (!$orderInfo = $this->dao->get($id)) + throw new ValidateException('订单不存在'); + + $f[] = Form::input('order_id', '订单号', $orderInfo->getData('order_id'))->disabled(1); + + switch ($orderInfo['delivery_type']) { + case 'send': + $f[] = Form::input('delivery_name', '送货人姓名', $orderInfo->getData('delivery_name'))->required('请输入送货人姓名'); + $f[] = Form::input('delivery_id', '送货人电话', $orderInfo->getData('delivery_id'))->required('请输入送货人电话'); + break; + case 'express': + /** @var ExpressServices $expressServices */ + $expressServices = app()->make(ExpressServices::class); + $f[] = Form::select('delivery_name', '快递公司', (string)$orderInfo->getData('delivery_name'))->setOptions(array_map(function ($item) { + $item['value'] = $item['label']; + return $item; + }, $expressServices->expressSelectForm(['is_show' => 1])))->required('请选择快递公司'); + $f[] = Form::input('delivery_id', '快递单号', $orderInfo->getData('delivery_id'))->required('请填写快递单号'); + break; + } + return create_form('配送信息', $f, $this->url('/marketing/integral/order/distribution/' . $id), 'PUT'); + } + + /** + * 用户订单收货 + * @param string $order_id + * @param int $uid + * @return array|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function takeOrder(string $order_id, int $uid) + { + $order = $this->dao->getUserOrderDetail($order_id, $uid); + if (!$order) { + throw new ValidateException('订单不存在!'); + } + if ($order['status'] != 2) { + throw new ValidateException('订单状态错误!'); + } + $order->status = 3; + /** @var StoreIntegralOrderStatusServices $statusService */ + $statusService = app()->make(StoreIntegralOrderStatusServices::class); + $res = $order->save() && $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'user_take_delivery', + 'change_message' => '用户已收货', + 'change_time' => time() + ]); + if (!$res) { + throw new ValidateException('收货失败'); + } + return $order; + } + + /** + * 修改配送信息 + * @param int $id 订单id + * @return mixed + */ + public function updateDistribution(int $id, array $data) + { + $order = $this->dao->get($id); + if (!$order) { + throw new ValidateException('数据不存在!'); + } + switch ($order['delivery_type']) { + case 'send': + if (!$data['delivery_name']) { + throw new ValidateException('请输入送货人姓名'); + } + if (!$data['delivery_id']) { + throw new ValidateException('请输入送货人电话号码'); + } + if (!check_phone($data['delivery_id'])) { + throw new ValidateException('请输入正确的送货人电话号码'); + } + break; + case 'express': + if (!$data['delivery_name']) { + throw new ValidateException('请选择快递公司'); + } + if (!$data['delivery_id']) { + throw new ValidateException('请输入快递单号'); + } + break; + default: + throw new ValidateException('未发货,请先发货再修改配送信息'); + break; + } + /** @var StoreIntegralOrderStatusServices $statusService */ + $statusService = app()->make(StoreIntegralOrderStatusServices::class); + $statusService->save([ + 'oid' => $id, + 'change_type' => 'distribution', + 'change_message' => '修改发货信息为' . $data['delivery_name'] . '号' . $data['delivery_id'], + 'change_time' => time() + ]); + return $this->dao->update($id, $data); + } + + /** + * 支付成功 + * @param array $orderInfo + * @param string $paytype + * @return bool + */ + public function paySuccess(array $orderInfo, string $paytype = PayServices::WEIXIN_PAY, array $other = []) + { + $updata = ['paid' => 1, 'pay_type' => $paytype, 'pay_time' => time()]; + if ($other && isset($other['trade_no'])) { + $updata['trade_no'] = $other['trade_no']; + } + $res1 = $this->dao->update($orderInfo['id'], $updata); + if ($res1) { + //卡密发放 + if ($orderInfo['product_type']) { + $this->sendCard((int)$orderInfo['id']); + //修改订单状态 + $this->dao->update((int)$orderInfo['id'], ['status' => 3]); + } + } + $res = $res1; + PrintJob::dispatchDo('IntegralDoJob', [$orderInfo['id']]); + return false !== $res; + } + + /** + * 获取全部积分商品订单 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAllIntegralOrderList(array $where) + { + $list = $this->dao->getIntegralOrderList($where); + return $list; + } +} diff --git a/app/services/activity/integral/StoreIntegralOrderStatusServices.php b/app/services/activity/integral/StoreIntegralOrderStatusServices.php new file mode 100644 index 0000000..ca2ae1d --- /dev/null +++ b/app/services/activity/integral/StoreIntegralOrderStatusServices.php @@ -0,0 +1,58 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\activity\integral; + + +use app\dao\activity\integral\StoreIntegralOrderStatusDao; +use app\services\BaseServices; +use crmeb\traits\ServicesTrait; + +/** + * 订单状态 + * Class StoreIntegralOrderStatusServices + * @package app\services\activity\integral + * @mixin StoreIntegralOrderStatusDao + */ +class StoreIntegralOrderStatusServices extends BaseServices +{ + use ServicesTrait; + + /** + * 构造方法 + * StoreIntegralOrderStatusServices constructor. + * @param StoreIntegralOrderStatusDao $dao + */ + public function __construct(StoreIntegralOrderStatusDao $dao) + { + $this->dao = $dao; + } + + /** + * 订单状态分页 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStatusList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getStatusList($where, $page, $limit); + foreach ($list as &$item) { + if (is_int($item['change_time'])) $item['change_time'] = date('Y-m-d H:i:s', $item['change_time']); + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + +} diff --git a/app/services/activity/integral/StoreIntegralServices.php b/app/services/activity/integral/StoreIntegralServices.php new file mode 100644 index 0000000..300afc3 --- /dev/null +++ b/app/services/activity/integral/StoreIntegralServices.php @@ -0,0 +1,482 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\integral; + +use app\Request; +use app\services\BaseServices; +use app\dao\activity\integral\StoreIntegralDao; +use app\services\diy\DiyServices; +use app\services\product\ensure\StoreProductEnsureServices; +use app\services\product\label\StoreProductLabelServices; +use app\services\product\product\StoreDescriptionServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrResultServices; +use app\services\product\sku\StoreProductAttrServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\jobs\product\ProductLogJob; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use think\exception\ValidateException; + +/** + * + * Class StoreIntegralServices + * @package app\services\activity\integral + * @mixin StoreIntegralDao + */ +class StoreIntegralServices extends BaseServices +{ + const THODLCEG = 'ykGUKB'; + + /** + * 商品活动类型 + */ + const TYPE = 4; + + /** + * StoreIntegralServices constructor. + * @param StoreIntegralDao $dao + */ + public function __construct(StoreIntegralDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取指定条件下的条数 + * @param array $where + */ + public function getCount(array $where) + { + $this->dao->count($where); + } + + /** + * 积分商品添加 + * @param int $id + * @param array $data + */ + public function saveData(int $id, array $data) + { + if ($data['freight'] == 2 && !$data['postage']) { + throw new AdminException('请设置运费金额'); + } + if ($data['freight'] == 3 && !$data['temp_id']) { + throw new AdminException('请选择运费模版'); + } + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $productInfo = $storeProductServices->getOne(['is_show' => 1, 'is_del' => 0, 'is_verify' => 1, 'id' => $data['product_id']]); + if (!$productInfo) { + throw new AdminException('原商品已下架或移入回收站'); + } + if ($productInfo['is_vip_product'] || $productInfo['is_presale_product']) { + throw new AdminException('该商品是预售或svip专享'); + } + $data['product_type'] = $productInfo['product_type']; + if (!$data['delivery_type']) { + $data['delivery_type'] = $productInfo['delivery_type']; + } + $data['type'] = $productInfo['type'] ?? 0; + $data['relation_id'] = $productInfo['relation_id'] ?? 0; + $custom_form = $productInfo['custom_form'] ?? []; + $data['custom_form'] = is_array($custom_form) ? json_encode($custom_form) : $custom_form; + $data['system_form_id'] = $productInfo['system_form_id'] ?? 0; + $store_label_id = $productInfo['store_label_id'] ?? []; + $data['store_label_id'] = is_array($store_label_id) ? implode(',', $store_label_id) : $store_label_id; + $ensure_id = $productInfo['ensure_id'] ?? []; + $data['ensure_id'] = is_array($ensure_id) ? implode(',', $ensure_id) : $ensure_id; + $specs = $productInfo['specs'] ?? []; + $data['specs'] = is_array($specs) ? json_encode($specs) : $specs; + $description = $data['description']; + $detail = $data['attrs']; + if ($detail) { + foreach ($detail as $attr) { + if ($attr['quota'] > $attr['stock']) { + throw new AdminException('限量超过了商品库存'); + } + } + } + $items = $data['items']; + $data['image'] = $data['images'][0] ?? ''; + $data['images'] = json_encode($data['images']); + $integral_data = array_column($detail, 'integral', 'price'); + $data['integral'] = $integral_data ? (int)min($integral_data) : 0; + $data['price'] = array_search($data['integral'], $integral_data); + $data['quota'] = $data['quota_show'] = array_sum(array_column($detail, 'quota')); + if ($data['quota'] > $storeProductServices->value(['id' => $data['product_id']], 'stock')) { + throw new ValidateException('限量不能超过商品库存'); + } + $data['stock'] = array_sum(array_column($detail, 'stock')); + unset($data['section_time'], $data['description'], $data['attrs'], $data['items']); + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + $this->transaction(function () use ($id, $data, $description, $detail, $items, $storeDescriptionServices, $storeProductAttrServices, $storeProductServices) { + if ($id) { + $res = $this->dao->update($id, $data); + if (!$res) throw new AdminException('修改失败'); + } else { + if (!$storeProductServices->getOne(['is_show' => 1, 'is_del' => 0, 'is_verify' => 1, 'id' => $data['product_id']])) { + throw new AdminException('原商品已下架或移入回收站'); + } + $data['add_time'] = time(); + $res = $this->dao->save($data); + if (!$res) throw new AdminException('添加失败'); + $id = (int)$res->id; + } + $storeDescriptionServices->saveDescription((int)$id, $description, 4); + $skuList = $storeProductAttrServices->validateProductAttr($items, $detail, (int)$id, 4); + $valueGroup = $storeProductAttrServices->saveProductAttr($skuList, (int)$id, 4); + + $res = true; + foreach ($valueGroup as $item) { + $res = $res && CacheService::setStock($item['unique'], (int)$item['quota_show'], 4); + } + if (!$res) { + throw new AdminException('占用库存失败'); + } + }); + } + + /** + * 批量添加商品 + * @param array $data + */ + public function saveBatchData(array $data) + { + /** @var StoreProductServices $service */ + $service = app()->make(StoreProductServices::class); + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + /** @var StoreProductAttrResultServices $storeProductAttrResultServices */ + $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class); + if (!$data) { + throw new ValidateException('请先添加产品!'); + } + $attrs = []; + foreach ($data['attrs'] as $k => $v) { + $attrs[$v['product_id']][] = $v; + } + foreach ($attrs as $k => $v) { + $productInfo = $service->getOne(['id' => $k]); + $productInfo = is_object($productInfo) ? $productInfo->toArray() : []; + if ($productInfo) { + $product = []; + $result = $storeProductAttrResultServices->getResult(['product_id' => $productInfo['id'], 'type' => 0]); + $product['product_id'] = $productInfo['id']; + $product['description'] = $storeDescriptionServices->getDescription(['product_id' => $productInfo['id'], 'type' => 0]); + $product['attrs'] = $v; + $product['items'] = $result['attr']; + $product['is_show'] = isset($data['is_show']) ? $data['is_show'] : 0; + $product['title'] = $productInfo['store_name']; + $product['unit_name'] = $productInfo['unit_name']; + $product['image'] = $productInfo['image']; + $product['images'] = $productInfo['slider_image']; + $product['num'] = 0; + $product['is_host'] = 0; + $product['once_num'] = 0; + $product['sort'] = 0; + $this->saveData(0, $product); + } + } + return true; + } + + /** + * 积分商品列表 + * @param array $where + * @return array + */ + public function systemPage(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取详情 + * @param int $id + * @return array|\think\Model|null + */ + public function getInfo(int $id) + { + $info = $this->dao->get($id); + if (!$info) { + throw new ValidateException('查看的商品不存在!'); + } + if ($info->is_del) { + throw new ValidateException('您查看的积分商品已被删除!'); + } + $info['price'] = floatval($info['price']); + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + $info['description'] = $storeDescriptionServices->getDescription(['product_id' => $id, 'type' => 4]); + $info['attrs'] = $this->attrList($id, $info['product_id']); + return $info; + } + + /** + * 获取规格 + * @param int $id + * @param int $pid + * @return mixed + */ + public function attrList(int $id, int $pid) + { + /** @var StoreProductAttrResultServices $storeProductAttrResultServices */ + $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class); + $combinationResult = $storeProductAttrResultServices->value(['product_id' => $id, 'type' => 4], 'result'); + $items = json_decode($combinationResult, true)['attr']; + $productAttr = $this->getAttr($items, $pid, 0); + $combinationAttr = $this->getAttr($items, $id, 4); + foreach ($productAttr as $pk => $pv) { + foreach ($combinationAttr as &$sv) { + if ($pv['detail'] == $sv['detail']) { + $productAttr[$pk] = $sv; + } + } + $productAttr[$pk]['detail'] = json_decode($productAttr[$pk]['detail']); + } + $attrs['items'] = $items; + $attrs['value'] = $productAttr; + foreach ($items as $key => $item) { + $header[] = ['title' => $item['value'], 'key' => 'value' . ($key + 1), 'align' => 'center', 'minWidth' => 80]; + } + $header[] = ['title' => '图片', 'slot' => 'pic', 'align' => 'center', 'minWidth' => 120]; + $header[] = ['title' => '兑换积分', 'key' => 'integral', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '金额', 'key' => 'price', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '库存', 'key' => 'stock', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '兑换次数', 'key' => 'quota', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '重量(KG)', 'key' => 'weight', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '体积(m³)', 'key' => 'volume', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '商品编号', 'key' => 'bar_code', 'align' => 'center', 'minWidth' => 80]; + $attrs['header'] = $header; + return $attrs; + } + + /** + * 获得规格 + * @param $attr + * @param $id + * @param $type + * @return array + */ + public function getAttr($attr, $id, $type) + { + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $value = attr_format($attr)[1]; + $valueNew = []; + $count = 0; + foreach ($value as $key => $item) { + $detail = $item['detail']; + $suk = implode(',', $item['detail']); + $sukValue = $storeProductAttrValueServices->getSkuArray(['product_id' => $id, 'type' => $type, 'suk' => $suk], 'bar_code,cost,price,integral,ot_price,stock,image as pic,weight,volume,brokerage,brokerage_two,quota,quota_show', 'suk'); + if (count($sukValue)) { + foreach (array_values($detail) as $k => $v) { + $valueNew[$count]['value' . ($k + 1)] = $v; + } + $valueNew[$count]['detail'] = json_encode($detail); + $valueNew[$count]['pic'] = $sukValue[$suk]['pic'] ?? ''; + $valueNew[$count]['integral'] = isset($sukValue[$suk]['integral']) ? floatval($sukValue[$suk]['integral']) : 0; + $valueNew[$count]['price'] = $sukValue[$suk]['price'] ? floatval($sukValue[$suk]['price']) : 0; + $valueNew[$count]['cost'] = $sukValue[$suk]['cost'] ? floatval($sukValue[$suk]['cost']) : 0; + $valueNew[$count]['ot_price'] = isset($sukValue[$suk]['ot_price']) ? floatval($sukValue[$suk]['ot_price']) : 0; + $valueNew[$count]['stock'] = $sukValue[$suk]['stock'] ? intval($sukValue[$suk]['stock']) : 0; +// $valueNew[$count]['quota'] = $sukValue[$suk]['quota'] ? intval($sukValue[$suk]['quota']) : 0; + $valueNew[$count]['quota'] = isset($sukValue[$suk]['quota_show']) && $sukValue[$suk]['quota_show'] ? intval($sukValue[$suk]['quota_show']) : 0; + $valueNew[$count]['bar_code'] = $sukValue[$suk]['bar_code'] ?? ''; + $valueNew[$count]['weight'] = $sukValue[$suk]['weight'] ? floatval($sukValue[$suk]['weight']) : 0; + $valueNew[$count]['volume'] = $sukValue[$suk]['volume'] ? floatval($sukValue[$suk]['volume']) : 0; + $valueNew[$count]['brokerage'] = $sukValue[$suk]['brokerage'] ? floatval($sukValue[$suk]['brokerage']) : 0; + $valueNew[$count]['brokerage_two'] = $sukValue[$suk]['brokerage_two'] ? floatval($sukValue[$suk]['brokerage_two']) : 0; + $valueNew[$count]['_checked'] = $type != 0; + $count++; + } + } + return $valueNew; + } + + /** + * 积分商品详情 + * @param Request $request + * @param int $id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function integralDetail(Request $request, int $id) + { + $storeInfo = $this->dao->getOne(['id' => $id], '*', ['getPrice']); + if (!$storeInfo) { + throw new ValidateException('商品不存在'); + } else { + $storeInfo = $storeInfo->toArray(); + } + /** @var DiyServices $diyServices */ + $diyServices = app()->make(DiyServices::class); + $infoDiy = $diyServices->getProductDetailDiy(); + //diy控制参数 + if (!isset($infoDiy['is_specs']) || !$infoDiy['is_specs']) { + $storeInfo['specs'] = []; + } + $siteUrl = sys_config('site_url'); + $storeInfo['image'] = set_file_url($storeInfo['image'], $siteUrl); + $storeInfo['image_base'] = set_file_url($storeInfo['image'], $siteUrl); + $storeInfo['sale_stock'] = 0; + if ($storeInfo['stock'] > 0) $storeInfo['sale_stock'] = 1; + $uid = $request->uid(); + /** @var StoreDescriptionServices $storeDescriptionService */ + $storeDescriptionService = app()->make(StoreDescriptionServices::class); + $storeInfo['description'] = $storeDescriptionService->getDescription(['product_id' => $id, 'type' => 4]); + $storeInfo['store_label'] = $storeInfo['ensure'] = []; + if ($storeInfo['store_label_id']) { + /** @var StoreProductLabelServices $storeProductLabelServices */ + $storeProductLabelServices = app()->make(StoreProductLabelServices::class); + $storeInfo['store_label'] = $storeProductLabelServices->getColumn([['id', 'in', $storeInfo['store_label_id']]], 'id,label_name'); + } + if ($storeInfo['ensure_id'] && isset($infoDiy['is_ensure']) && $infoDiy['is_ensure']) { + /** @var StoreProductEnsureServices $storeProductEnsureServices */ + $storeProductEnsureServices = app()->make(StoreProductEnsureServices::class); + $storeInfo['ensure'] = $storeProductEnsureServices->getColumn([['id', 'in', $storeInfo['ensure_id']]], 'id,name,image,desc'); + } + $storeInfo['small_image'] = get_thumb_water($storeInfo['image']); + $data['storeInfo'] = $storeInfo; + + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + [$productAttr, $productValue] = $storeProductAttrServices->getProductAttrDetail($id, $uid, 0, 4, $storeInfo['product_id']); + $data['productAttr'] = $productAttr; + $data['productValue'] = $productValue; + //浏览记录 + ProductLogJob::dispatch(['visit', ['uid' => $uid, 'id' => $id, 'product_id' => $storeInfo['product_id']], 'integral']); + return $data; + } + + /** + * 修改销量和库存 + * @param $num + * @param $integralId + * @return bool + */ + public function decIntegralStock(int $num, int $integralId, string $unique) + { + $product_id = $this->dao->value(['id' => $integralId], 'product_id'); + if ($unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //减去积分商品的sku库存增加销量 + $res = false !== $skuValueServices->dao->decStockIncSalesDecQuota(['product_id' => $integralId, 'unique' => $unique, 'type' => 4], $num); + //减去积分商品库存 + $res = $res && $this->dao->decStockIncSales(['id' => $integralId, 'type' => 4], $num); + //获取拼团的sku + $sku = $skuValueServices->value(['product_id' => $integralId, 'unique' => $unique, 'type' => 4], 'suk'); + //减去当前普通商品sku的库存增加销量 + $res = $res && $skuValueServices->decStockIncSales(['product_id' => $product_id, 'suk' => $sku, 'type' => 0], $num); + } else { + $res = false !== $this->dao->decStockIncSales(['id' => $integralId, 'type' => 4], $num); + } + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //减去普通商品库存 + $res = $res && $services->decProductStock($num, $product_id); + return $res; + } + + /** + * 获取一条积分商品 + * @param $id + * @return mixed + */ + public function getIntegralOne($id) + { + return $this->dao->validProduct($id, '*'); + } + + /** + * 验证积分商品下单库存限量 + * @param int $uid + * @param int $integralId + * @param int $num + * @param string $unique + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkoutProductStock(int $uid, int $integralId, int $num = 1, string $unique = '') + { + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + if ($unique == '') { + $unique = $attrValueServices->value(['product_id' => $integralId, 'type' => 4], 'unique'); + } + $StoreIntegralInfo = $this->getIntegralOne($integralId); + if (!$StoreIntegralInfo) { + throw new ValidateException('该商品已下架或删除'); + } + /** @var StoreIntegralOrderServices $orderServices */ + $orderServices = app()->make(StoreIntegralOrderServices::class); + $userBuyCount = $orderServices->getBuyCount($uid, $integralId); + if ($StoreIntegralInfo['once_num'] < $num && $StoreIntegralInfo['once_num'] != -1) { + throw new ValidateException('每个订单限购' . $StoreIntegralInfo['once_num'] . '件'); + } + if ($StoreIntegralInfo['num'] < ($userBuyCount + $num) && $StoreIntegralInfo['num'] != -1) { + throw new ValidateException('每人总共限购' . $StoreIntegralInfo['num'] . '件'); + } + $attrInfo = $attrValueServices->getOne(['product_id' => $integralId, 'unique' => $unique, 'type' => 4]); + if ($num > $attrInfo['quota']) { + throw new ValidateException('该商品库存不足' . $num); + } + $product_stock = $attrValueServices->value(['product_id' => $StoreIntegralInfo['product_id'], 'suk' => $attrInfo['suk'], 'type' => 0], 'stock'); + if ($product_stock < $num) { + throw new ValidateException('该商品库存不足' . $num); + } + if (!CacheService::checkStock($unique, $num, 4)) { + throw new ValidateException('该商品库存不足' . $num); + } + return [$attrInfo, $unique, $StoreIntegralInfo]; + } + + /** + * 获取推荐积分商品 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getIntegralList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit, 'id,image,title,integral,price,sales'); + return $list; + } + + /** + * 获取全部积分商品 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAllIntegralList(array $where) + { + $list = $this->dao->getList($where, 0, 0, 'id,image,title,integral,price,sales'); + return $list; + } +} diff --git a/app/services/activity/lottery/LuckLotteryRecordServices.php b/app/services/activity/lottery/LuckLotteryRecordServices.php new file mode 100644 index 0000000..95fca33 --- /dev/null +++ b/app/services/activity/lottery/LuckLotteryRecordServices.php @@ -0,0 +1,338 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\lottery; + +use app\jobs\system\CapitalFlowJob; +use app\services\BaseServices; +use app\dao\activity\lottery\LuckLotteryRecordDao; +use app\services\activity\coupon\StoreCouponIssueServices; +use app\services\order\StoreOrderCreateServices; +use app\services\user\UserBillServices; +use app\services\user\UserMoneyServices; +use app\services\user\UserServices; +use app\services\wechat\WechatUserServices; +use crmeb\services\wechat\Payment; +use think\exception\ValidateException; +use think\facade\Log; + +/** + * 抽奖记录 + * Class LuckLotteryRecordServices + * @package app\services\activity\lottery + * @mixin LuckLotteryRecordDao + */ +class LuckLotteryRecordServices extends BaseServices +{ + + /** + * LuckLotteryRecordServices constructor. + * @param LuckLotteryRecordDao $dao + */ + public function __construct(LuckLotteryRecordDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取抽奖记录列表 + * @param array $where + * @return array + */ + public function getList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $where['not_type'] = 1; + if (isset($where['factor']) && $where['factor']) { + /** @var LuckLotteryServices $luckServices */ + $luckServices = app()->make(LuckLotteryServices::class); + $where['lottery_id'] = $luckServices->value(['factor' => $where['factor'], 'status' => 1], 'id'); + if (!$where['lottery_id']) { + $list = []; + $count = 0; + return compact('list', 'count'); + } + unset($where['factor']); + } + $list = $this->dao->getList($where, '*', ['lottery', 'prize', 'user'], $page, $limit); + foreach ($list as &$item) { + if (isset($item['prize_info']) && $item['prize_info']) { + $prize = json_decode($item['prize_info'], true); + } else { + $prize = $item['prize']; + } + $item['prize'] = $prize; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取中奖记录 + * @param array $where + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getWinList(array $where, int $limit = 20) + { + $where = $where + ['not_type' => 1]; + $list = $this->dao->getList($where, 'id,uid,prize_id,lottery_id,receive_time,prize_info,add_time', ['user', 'prize'], 0, $limit); + foreach ($list as &$item) { + if (isset($item['prize_info']) && $item['prize_info']) { + $prize = json_decode($item['prize_info'], true); + } else { + $prize = $item['prize']; + } + $item['prize'] = $prize; + $item['receive_time'] = $item['receive_time'] ? date('Y-m-d H:i:s', $item['receive_time']) : ''; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i', $item['add_time']) : ''; + } + return $list; + } + + /** + * 参与抽奖数据统计 + * @param int $lottery_id + * @return int[] + */ + public function getLotteryRecordData(int $lottery_id) + { + $data = ['all' => 0, 'people' => 0, 'win' => 0]; + if ($lottery_id) { + $where = [['lottery_id', '=', $lottery_id]]; + $data['all'] = $this->dao->getCount($where); + $data['people'] = $this->dao->getCount($where, 'uid'); + $data['win'] = $this->dao->getCount($where + [['type', '>', 1]], 'uid'); + } + return $data; + } + + /** + * 写入中奖纪录 + * @param int $uid + * @param array $prize + * @return mixed + */ + public function insertPrizeRecord(int $uid, array $prize, array $userInfo = []) + { + if (!$userInfo) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + } + if (!$userInfo) { + throw new ValidateException('用户不存在'); + } + if (!$prize) { + throw new ValidateException('奖品不存在'); + } + $data = []; + $data['uid'] = $uid; + $data['lottery_id'] = $prize['lottery_id']; + $data['prize_id'] = $prize['id']; + $data['type'] = $prize['type']; + $data['prize_info'] = json_encode($prize); + $data['add_time'] = time(); + if (!$res = $this->dao->save($data)) { + throw new ValidateException('写入中奖记录失败'); + } + return $res; + } + + /** + * 领取奖品 + * @param int $uid + * @param int $lottery_record_id + * @param string $receive_info + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function receivePrize(int $uid, int $lottery_record_id, array $receive_info = []) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + throw new ValidateException('用户不存在'); + } + $lotteryRecord = $this->dao->get($lottery_record_id, ['*'], ['prize']); + if (!$lotteryRecord || !isset($lotteryRecord['prize'])) { + throw new ValidateException('请继续参与活动抽奖'); + } + if ($lotteryRecord['is_receive'] == 1) { + throw new ValidateException('已经领取成功'); + } + $data = ['is_receive' => 1, 'receive_time' => time(), 'receive_info' => $receive_info]; + if (isset($lotteryRecord['prize_info']) && $lotteryRecord['prize_info']) { + $prize = json_decode($lotteryRecord['prize_info'], true); + } else { + $prize = $lotteryRecord['prize']; + } + + $this->transaction(function () use ($uid, $userInfo, $lottery_record_id, $data, $prize, $userServices, $receive_info) { + //奖品类型1:未中奖2:积分3:余额4:红包5:优惠券6:站内商品7:等级经验8:用户等级 9:svip天数 + switch ($prize['type']) { + case 1: + break; + case 2: + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $integral = bcadd((string)$userInfo['integral'], (string)$prize['num'], 0); + $userBillServices->income('lottery_give_integral', $uid, $prize['num'], (int)$integral, $prize['id']); + $userServices->update($uid, ['integral' => $integral], 'uid'); + break; + case 3: + /** @var UserMoneyServices $userMoneyServices */ + $userMoneyServices = app()->make(UserMoneyServices::class); + $now_money = bcadd((string)$userInfo['now_money'], (string)$prize['num'], 2); + $userMoneyServices->income('lottery_give_money', $uid, $prize['num'], $now_money, $prize['id']); + $userServices->update($uid, ['now_money' => $now_money], 'uid'); + break; + case 4: + /** @var WechatUserServices $wechatServices */ + $wechatServices = app()->make(WechatUserServices::class); + $openid = $wechatServices->getWechatOpenid($uid, 'wechat'); + if ($openid) {//公众号用户 + $type = Payment::WEB; + } else {//小程序用户 + $openid = $wechatServices->getWechatOpenid($uid, 'routine'); + $type = Payment::MINI; + } + //app微信用户 + if (!$openid) { + $openid = $wechatServices->getWechatOpenid($uid, 'app'); + $type = Payment::APP; + } + if ($openid) { + /** @var StoreOrderCreateServices $services */ + $services = app()->make(StoreOrderCreateServices::class); + $wechat_order_id = $services->getNewOrderId(); + //记录资金流水 + CapitalFlowJob::dispatch([['order_id' => $wechat_order_id, 'store_id' => 0, 'uid' => $uid, 'price' => $prize['num'], 'pay_type' => 'weixin', 'nickname' => $userInfo['nickname'], 'phone' => $userInfo['phone']], 'luck']); + Payment::merchantPay($openid, $wechat_order_id, (string)$prize['num'], '抽奖中奖红包', $type); + } + break; + case 5: + /** @var StoreCouponIssueServices $couponIssueService */ + $couponIssueService = app()->make(StoreCouponIssueServices::class); + try { + $couponIssueService->issueUserCoupon($prize['coupon_id'], $userInfo, true, 'luck_lottery'); + } catch (\Throwable $e) { + Log::error('抽奖领取优惠券失败,原因:' . $e->getMessage()); + } + break; + case 6: + if (!$receive_info['name'] || !$receive_info['phone'] || !$receive_info['address']) { + throw new ValidateException('请输入收货人信息'); + } + if (!check_phone($receive_info['phone'])) { + throw new ValidateException('请输入正确的收货人电话'); + } + break; + case 7: + break; + case 8: + break; + case 9: + break; + } + $this->dao->update($lottery_record_id, $data, 'id'); + }); + return true; + } + + /** + * 发货、备注 + * @param int $lottery_record_id + * @param array $data + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setDeliver(int $lottery_record_id, array $data) + { + $lotteryRecord = $this->dao->get($lottery_record_id); + if (!$lotteryRecord) { + throw new ValidateException('抽奖记录不存在'); + } + $deliver_info = $lotteryRecord['deliver_info']; + $edit = []; + //备注 + if (!$data['deliver_name'] && !$data['deliver_number']) { + $deliver_info['mark'] = $data['mark']; + } else { + if ($lotteryRecord['type'] != 6 && ($data['deliver_name'] || $data['deliver_number'])) { + throw new ValidateException('该奖品不需要发货'); + } + if ($lotteryRecord['type'] == 6 && (!$data['deliver_name'] || !$data['deliver_number'])) { + throw new ValidateException('请选择快递公司或输入快递单号'); + } + $deliver_info['deliver_name'] = $data['deliver_name']; + $deliver_info['deliver_number'] = $data['deliver_number']; + $edit['is_deliver'] = 1; + $edit['deliver_time'] = time(); + } + $edit['deliver_info'] = $deliver_info; + if (!$this->dao->update($lottery_record_id, $edit, 'id')) { + throw new ValidateException('处理失败'); + } + return true; + } + + /** + * 获取中奖记录 + * @param int $uid + * @return array + */ + public function getRecord(int $uid, $where = []) + { + if (!$where) { + $where['uid'] = $uid; + $where['not_type'] = 1; + } + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', ['prize'], $page, $limit); + foreach ($list as &$item) { + if (isset($item['prize_info']) && $item['prize_info']) { + $prize = $item['prize_info'] = json_decode($item['prize_info'], true); + } else { + $prize = $item['prize']; + } + $item['prize'] = $prize; + $item['deliver_time'] = $item['deliver_time'] ? date('Y-m-d H:i:s', $item['deliver_time']) : ''; + $item['receive_time'] = $item['receive_time'] ? date('Y-m-d H:i:s', $item['receive_time']) : ''; + } + return $list; + } + + /**获取抽奖次数 + * @param int $uid + * @param int $lottery_id + * @return array + */ + public function getLotteryNum(int $uid, int $lottery_id) + { + $where['uid'] = $uid; + $where['lottery_id'] = $lottery_id; + $now_day = strtotime(date('Y-m-d'));//今日 + $todayCount = $this->dao->getCount($where + ['add_time'=> $now_day]); + $totalCount = $this->dao->getCount($where); + return compact('todayCount','totalCount'); + } +} diff --git a/app/services/activity/lottery/LuckLotteryServices.php b/app/services/activity/lottery/LuckLotteryServices.php new file mode 100644 index 0000000..64ea6f6 --- /dev/null +++ b/app/services/activity/lottery/LuckLotteryServices.php @@ -0,0 +1,727 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\lottery; + +use app\services\BaseServices; +use app\dao\activity\lottery\LuckLotteryDao; +use app\services\activity\coupon\StoreCouponIssueServices; +use app\services\product\product\StoreProductServices; +use app\services\user\label\UserLabelServices; +use app\services\user\UserBillServices; +use app\services\user\label\UserLabelRelationServices; +use app\services\user\UserMoneyServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use think\exception\ValidateException; + +/** + * + * Class LuckLotteryServices + * @package app\services\activity\lottery + * @mixin LuckLotteryDao + */ +class LuckLotteryServices extends BaseServices +{ + /** + * 抽奖形式,奖品数量 + * @var int[] + */ + protected $lottery_type = [ + '1' => 8 //九宫格 + ]; + /** + * 抽奖类型 + * @var string[] + */ + protected $lottery_factor = [ + '1' => '积分', + '2' => '余额', + '3' => '下单支付成功', + '4' => '订单评价', + '5' => '关注公众号' + ]; + + /** + * 抽奖次数缓存时间 + * @var int + */ + public $luck_cache_time = 120; + + /** + * LuckLotteryServices constructor. + * @param LuckLotteryDao $dao + */ + public function __construct(LuckLotteryDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取抽奖缓存 + * @param int $factor + * @return mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/16 + */ + public function getFactorLotteryCache(int $factor) + { + return $this->dao->cacheTag()->remember('' . $factor, function () use ($factor) { + $lottery = $this->dao->getFactorLottery($factor); + return $lottery ? $lottery->toArray() : null; + }); + } + + /** + * 获取列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $where['is_del'] = 0; + $list = $this->dao->getList($where, '*', ['prize'], $page, $limit); + $lottery_factor = $this->lottery_factor; + /** @var LuckLotteryRecordServices $luckLotteryRecordServices */ + $luckLotteryRecordServices = app()->make(LuckLotteryRecordServices::class); + foreach ($list as &$item) { + $item['lottery_type'] = $lottery_factor[$item['factor']] ?? ''; + $data = $luckLotteryRecordServices->getLotteryRecordData((int)$item['id']); + $item['lottery_all'] = $data['all'] ?? 0; + $item['lottery_people'] = $data['people'] ?? 0; + $item['lottery_win'] = $data['win'] ?? 0; + if ($item['status']) { + if ($item['start_time'] == 0 && $item['end_time'] == 0) { + $item['lottery_status'] = '进行中'; + } else { + if ($item['start_time'] > time()) + $item['lottery_status'] = '未开始'; + else if ($item['end_time'] < time()) + $item['lottery_status'] = '已结束'; + else if ($item['end_time'] > time() && $item['start_time'] < time()) { + $item['lottery_status'] = '进行中'; + } + } + } else $item['lottery_status'] = '已结束'; + $item['start_time'] = $item['start_time'] ? date('Y-m-d H:i:s', $item['start_time']) : ''; + $item['end_time'] = $item['end_time'] ? date('Y-m-d H:i:s', $item['end_time']) : ''; + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取抽奖详情 + * @param int $id + * @return array|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getlotteryInfo(int $id) + { + $lottery = $this->dao->getLottery($id, '*', ['prize']); + if (!$lottery) { + throw new ValidateException('活动不存在或已删除'); + } + $lottery = $lottery->toArray(); + if (isset($lottery['prize']) && $lottery['prize']) { + $products = $coupons = []; + $product_ids = array_unique(array_column($lottery['prize'], 'product_id')); + $coupon_ids = array_unique(array_column($lottery['prize'], 'coupon_id')); + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $products = $productServices->getColumn([['id', 'in', $product_ids]], 'id,store_name,image', 'id'); + /** @var StoreCouponIssueServices $couponServices */ + $couponServices = app()->make(StoreCouponIssueServices::class); + $coupons = $couponServices->getColumn([['id', 'in', $coupon_ids]], 'id,coupon_title', 'id'); + foreach ($lottery['prize'] as &$prize) { + $prize['coupon_title'] = $prize['goods_image'] = ''; + if ($prize['type'] == 6) { + $prize['goods_image'] = $products[$prize['product_id']]['image'] ?? ''; + } + if ($prize['type'] == 5) { + $prize['coupon_title'] = $coupons[$prize['coupon_id']]['coupon_title'] ?? ''; + } + } + } + return $lottery; + } + + /** + * 根据类型获取数据 + * @param int $factor + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getlotteryFactorInfo(int $factor) + { + $lottery = $this->dao->getFactorLottery($factor, '*', ['prize'], false); + if (!$lottery) { + return []; + } + $lottery = $lottery->toArray(); + if (isset($lottery['prize']) && $lottery['prize']) { + $products = $coupons = []; + $product_ids = array_unique(array_column($lottery['prize'], 'product_id')); + $coupon_ids = array_unique(array_column($lottery['prize'], 'coupon_id')); + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $products = $productServices->getColumn([['id', 'in', $product_ids]], 'id,store_name,image', 'id'); + /** @var StoreCouponIssueServices $couponServices */ + $couponServices = app()->make(StoreCouponIssueServices::class); + $coupons = $couponServices->getColumn([['id', 'in', $coupon_ids]], 'id,coupon_title', 'id'); + foreach ($lottery['prize'] as &$prize) { + $prize['coupon_title'] = $prize['goods_image'] = ''; + if ($prize['type'] == 6) { + $prize['goods_image'] = $products[$prize['product_id']]['image'] ?? ''; + } + if ($prize['type'] == 5) { + $prize['coupon_title'] = $coupons[$prize['coupon_id']]['coupon_title'] ?? ''; + } + } + /** @var UserLabelServices $labelService */ + $labelService = app()->make(UserLabelServices::class); + $lottery['user_label'] = $lottery['user_label'] ? $labelService->getColumn([ + ['id', 'in', $lottery['user_label']], + ], 'id,label_name') : []; + } + return $lottery; + } + + /** + * 添加抽奖活动以及奖品 + * @param array $data + * @return mixed + */ + public function add(array $data) + { + $prizes = $data['prize']; + $prize_num = $this->lottery_type[1]; + if (count($prizes) != $prize_num) { + throw new ValidateException('请选择' . $prize_num . '个奖品'); + } + unset($data['prize']); + $this->transaction(function () use ($data, $prizes) { + $time = time(); + $data['add_time'] = $time; + if (!$lottery = $this->dao->save($data)) { + throw new ValidateException('添加抽奖活动失败'); + } + if ($data['status']) { + $this->setStatus((int)$lottery->id, $data['status']); + } + /** @var LuckPrizeServices $luckPrizeServices */ + $luckPrizeServices = app()->make(LuckPrizeServices::class); + $sort = 1; + $typeTrue = false; + foreach ($prizes as $prize) { + $prize = $luckPrizeServices->checkPrizeData($prize); + if ($prize['type'] == 1) { + $typeTrue = true; + } + $prize['lottery_id'] = $lottery->id; + unset($prize['id']); + $prize['add_time'] = $time; + $prize['sort'] = $sort; + + $res = $luckPrizeServices->save($prize); + if (!$res) { + throw new ValidateException('添加抽奖奖品失败'); + } + $sort++; + //抽奖商品库存加入redis + CacheService::setStock((string)$res->id, (int)$prize['total'], 6); + } + if (!$typeTrue) { + throw new ValidateException('添加抽奖奖品至少需要一个未中奖奖项'); + } + + return true; + }); + $this->dao->cacheTag()->clear(); + return true; + } + + /** + * 修改抽奖活动以及奖品 + * @param int $id + * @param array $data + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function edit(int $id, array $data) + { + $lottery = $this->dao->getLottery($id); + if (!$lottery) { + throw new ValidateException('抽奖活动不存在'); + } + $newPrizes = $data['prize']; + unset($data['prize'], $data['id']); + $prize_num = $this->lottery_type[1]; + if (count($newPrizes) != $prize_num) { + throw new ValidateException('请选择' . $prize_num . '个奖品'); + } + /** @var LuckPrizeServices $luckPrizeServices */ + $luckPrizeServices = app()->make(LuckPrizeServices::class); + $prizes = $luckPrizeServices->getLotteryPrizeList($id); + $this->transaction(function () use ($id, $lottery, $data, $newPrizes, $prizes, $luckPrizeServices) { + $updateIds = array_column($newPrizes, 'id'); + $oldIds = array_column($prizes, 'id'); + $delIds = array_merge(array_diff($oldIds, $updateIds)); + $insert = []; + $time = time(); + $sort = 1; + foreach ($newPrizes as $prize) { + $prize = $luckPrizeServices->checkPrizeData($prize); + $prize['sort'] = $sort; + if (isset($prize['id']) && $prize['id']) { + $prizeId = $prize['id']; + if (!$prize['lottery_id']) { + throw new ValidateException('缺少活动ID'); + } + if (!$luckPrizeServices->update($prize['id'], $prize, 'id')) { + throw new ValidateException('修改奖品失败'); + } + } else { + unset($prize['id']); + $prize['lottery_id'] = $id; + $prize['add_time'] = $time; + $prize['sort'] = $sort; + $res = $luckPrizeServices->save($prize); + if (!$res) { + throw new ValidateException('新增奖品失败'); + } + $prizeId = $res->id; + } + $sort++; + + //抽奖商品库存加入redis + CacheService::setStock((string)$prizeId, (int)$prize['total'], 6); + } + if ($delIds) { + if (!$luckPrizeServices->update([['id', 'in', $delIds]], ['is_del' => 1])) { + throw new ValidateException('删除奖品失败'); + } + } + if (!$this->dao->update($id, $data)) { + throw new ValidateException('修改失败'); + } + //上架 + if (!$lottery['status'] && $data['status']) { + $this->setStatus($id, $data['status']); + } + return true; + }); + $this->dao->cacheTag()->clear(); + return true; + } + + /** + * 获取用户某个抽奖活动剩余抽奖次数 + * @param int $uid + * @param int $lottery_id + * @param array $userInfo + * @param array $lottery + * @return false|float|int|mixed + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLotteryNum(int $uid, int $lottery_id, array $userInfo = [], array $lottery = []) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$userInfo) { + $userInfo = $userServices->getUserInfo($uid); + } + if (!$userInfo) { + throw new ValidateException('用户不存在'); + } + if (!$lottery) { + $lottery = $this->dao->getLottery($lottery_id, '*', [], true); + } + if (!$lottery) { + throw new ValidateException('该活动已经下架,请持续关注'); + } + //抽奖类型:1:积分2:余额3:下单支付成功4:订单评价5:拉新人 + switch ($lottery['factor']) { + case 1: + return $userInfo['integral'] > 0 && $lottery['factor_num'] > 0 ? floor($userInfo['integral'] / $lottery['factor_num']) : 0; + break; + case 2: + return $userInfo['now_money'] > 0 && $lottery['factor_num'] > 0 ? floor($userInfo['now_money'] / $lottery['factor_num']) : 0; + break; + case 3: + return $this->getCacheLotteryNum($uid, 'order'); + break; + case 4: + return $this->getCacheLotteryNum($uid, 'comment'); + break; + case 5: + return $userInfo['spread_lottery'] ?? 0; + break; + default: + throw new ValidateException('暂未有该类型活动'); + break; + } + } + + /** + * 验证用户抽奖资格(用户等级、付费会员、用户标签) + * @param int $uid + * @param int $lottery_id + * @param array $userInfo + * @param array $lottery + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkoutUserAuth(int $uid, int $lottery_id, array $userInfo = [], array $lottery = []) + { + if (!$userInfo) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + } + if (!$userInfo) { + throw new ValidateException('用户不存在'); + } + if (!$lottery) { + $lottery = $this->dao->getLottery($lottery_id, '*', [], true); + } + if (!$lottery) { + throw new ValidateException('该活动已经下架,请持续关注'); + } + //部分用户参与 + if ($lottery['attends_user'] == 2) { + //用户等级 + if ($lottery['user_level'] && !in_array($userInfo['level'], $lottery['user_level'])) { + throw new ValidateException('您暂时无法参与该活动'); + } + //用户标签 + if ($lottery['user_label']) { + /** @var UserLabelRelationServices $userlableRelation */ + $userlableRelation = app()->make(UserLabelRelationServices::class); + $user_labels = $userlableRelation->getUserLabels($uid); + if (!array_intersect($lottery['user_label'], $user_labels)) { + throw new ValidateException('您暂时无法参与该活动'); + } + } + //是否是付费会员 + if ($lottery['is_svip'] != -1) { + if (($lottery['is_svip'] == 1 && $userInfo['is_money_level'] <= 0) || ($lottery['is_svip'] == 0 && $userInfo['is_money_level'] > 0)) { + throw new ValidateException('您暂时无法参与该活动'); + } + } + } + return true; + } + + /** + * 抽奖 + * @param int $uid + * @param int $lottery_id + * @return mixed + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function luckLottery(int $uid, int $lottery_id) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + throw new ValidateException('用户不存在'); + } + + $lottery = $this->dao->getLottery($lottery_id, '*', [], true); + if (!$lottery) { + throw new ValidateException('该活动已经下架,请持续关注'); + } + $userInfo = $userInfo->toArray(); + $lottery = $lottery->toArray(); + //验证用户身份 + $this->checkoutUserAuth($uid, $lottery_id, $userInfo, $lottery); + + /** @var LuckPrizeServices $lotteryPrizeServices */ + $lotteryPrizeServices = app()->make(LuckPrizeServices::class); + $lotteryPrize = $lotteryPrizeServices->getPrizeList($lottery_id); + if (!$lotteryPrize) { + throw new ValidateException('该活动状态有误,请联系管理员'); + } + if ($this->getLotteryNum($uid, $lottery_id, $userInfo, $lottery) < 1) { + //抽奖类型:1:积分2:余额3:下单支付成功4:订单评价5:拉新人 + switch ($lottery['factor']) { + case 1: + throw new ValidateException('积分不足,没有更多抽奖次数'); + break; + case 2: + throw new ValidateException('余额不足,没有更多抽奖次数'); + break; + case 3: + throw new ValidateException('购买商品之后获得更多抽奖次数'); + break; + case 4: + throw new ValidateException('订单完成评价之后获得更多抽奖次数'); + break; + case 5: + throw new ValidateException('邀请更多好友获取抽奖次数'); + break; + default: + throw new ValidateException('暂未有该类型活动'); + break; + } + } + if ($lottery['factor'] == 1) {//积分抽奖 + /** @var LuckLotteryRecordServices $lotteryRecordServices */ + $lotteryRecordServices = app()->make(LuckLotteryRecordServices::class); + $data = $lotteryRecordServices->getLotteryNum($uid, (int)$lottery['id']); + $lotteryData['todayCount'] = (int)max(bcsub((string)$lottery['lottery_num'], (string)$data['todayCount'], 0), 0); + $lotteryData['totalCount'] = (int)max(bcsub((string)$lottery['total_lottery_num'], (string)$data['totalCount'], 0), 0); + if ($lotteryData['todayCount'] <= 0) { + throw new ValidateException('本次活动当天抽奖次数已用完!'); + } + if ($lotteryData['totalCount'] <= 0) { + throw new ValidateException('本次活动总抽奖次数已用完!'); + } + } + /** @var LuckPrizeServices $luckPrizeServices */ + $luckPrizeServices = app()->make(LuckPrizeServices::class); + //随机抽奖 + $prize = $luckPrizeServices->getLuckPrize($lotteryPrize); + if (!$prize) { + throw new ValidateException('该活动状态有误,请联系管理员'); + } + + try { + $res = $this->transaction(function () use ($prize, $luckPrizeServices, $uid, $lotteryPrize, $userInfo, $lottery) { + //中奖扣除积分、余额 + $this->lotteryFactor($uid, $userInfo, $lottery); + //中奖减少奖品数量 + $luckPrizeServices->decPrizeNum($prize['id'], $prize); + /** @var LuckLotteryRecordServices $lotteryRecordServices */ + $lotteryRecordServices = app()->make(LuckLotteryRecordServices::class); + //中奖写入记录 + $record = $lotteryRecordServices->insertPrizeRecord($uid, $prize, $userInfo); + //不是站内商品直接领奖 + if ($prize['type'] != 6) { + $lotteryRecordServices->receivePrize($uid, (int)$record->id); + } + $prize['lottery_record_id'] = $record->id; + return $prize; + }); + } catch (\Throwable $e) { + throw new ValidateException($e->getMessage()); + //发生错误回退库存 + CacheService::setStock((string)$prize['id'], 1, 6, false); + } + + return $res; + } + + /** + * 抽奖消耗扣除用户积分、余额等 + * @param int $uid + * @param array $userInfo + * @param array $lottery + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function lotteryFactor(int $uid, array $userInfo, array $lottery) + { + if (!$userInfo || !$lottery) { + return true; + } + //抽奖类型:1:积分2:余额3:下单支付成功4:订单评价5:拉新人 + switch ($lottery['factor']) { + case 1: + if ($userInfo['integral'] > $lottery['factor_num']) { + $integral = bcsub((string)$userInfo['integral'], (string)$lottery['factor_num'], 0); + } else { + $integral = 0; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $userBillServices->income('lottery_use_integral', $uid, (int)$lottery['factor_num'], (int)$integral, $lottery['id']); + if (!$userServices->update($uid, ['integral' => $integral], 'uid')) { + throw new ValidateException('抽奖扣除用户积分失败'); + } + break; + case 2: + if ($userInfo['now_money'] >= $lottery['factor_num']) { + $now_money = bcsub((string)$userInfo['now_money'], (string)$lottery['factor_num'], 2); + } else { + throw new ValidateException('抽奖失败,余额不足!'); + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + /** @var UserMoneyServices $userMoneyServices */ + $userMoneyServices = app()->make(UserMoneyServices::class); + $userMoneyServices->income('lottery_use_money', $uid, $lottery['factor_num'], $now_money, $lottery['id']); + if (!$userServices->update($uid, ['now_money' => $now_money], 'uid')) { + throw new ValidateException('抽奖扣除用户余额失败'); + } + break; + case 3: + case 4: + //销毁抽奖次数缓存 + $this->delCacheLotteryNum($uid, $lottery['factor'] == 3 ? 'order' : 'comment'); + break; + case 5: + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $spread_lottery = 0; + if ($userInfo['spread_lottery'] > 1) { + $spread_lottery = $userInfo['spread_lottery'] - 1; + } + if (!$userServices->update($uid, ['spread_lottery' => $spread_lottery], 'uid')) { + throw new ValidateException('抽奖扣除用户推广获取抽奖次数失败'); + } + break; + default: + throw new ValidateException('暂未有该类型活动'); + break; + } + return true; + } + + /** + * 删除 + * @param int $id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function delLottery(int $id) + { + if ($lottery = $this->dao->getLottery($id)) { + if (!$this->dao->update(['id' => $id], ['is_del' => 1])) { + throw new AdminException('删除失败,请稍候重试'); + } + } + return true; + } + + /** + * 设置抽奖活动状态 + * @param int $id + * @param $status + * @return false|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setStatus(int $id, $status) + { + if (!$id) return false; + $lottery = $this->dao->getLottery($id, 'id,factor'); + if (!$lottery) { + return false; + } + //每一种抽奖类型只有一个上架 + if ($status) { + $this->dao->update(['factor' => $lottery['factor']], ['status' => 0]); + } + return $this->dao->update($id, ['status' => $status], 'id'); + } + + /** + * 下单支付、评论缓存抽奖次数 + * @param int $uid + * @param string $type + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function setCacheLotteryNum(int $uid, string $type = 'order') + { + $factor = $type == 'order' ? 3 : 4; + $lottery = $this->dao->getFactorLottery($factor, 'id,factor_num'); + if (!$lottery || !$lottery['factor_num']) { + return true; + } + $key = 'user_' . $type . '_luck_lottery_' . $uid; + $timeKey = 'expire_time_user_' . $type . '_luck_lottery_' . $uid; + $cache = CacheService::redisHandler(); +// $num = (int)$this->getCacheLotteryNum($uid, $type); + $cache->set($timeKey, bcadd((string)time(), (string)$this->luck_cache_time), bcadd('10', (string)$this->luck_cache_time)); + return $cache->set($key, $lottery['factor_num'], $this->luck_cache_time); + } + + /** + * 获取抽奖到期时间 + * @param int $uid + * @param string $type + * @return int|mixed + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getCacheLotteryExpireTime(int $uid, string $type = 'order') + { + $timeKey = 'expire_time_user_' . $type . '_luck_lottery_' . $uid; + $time = CacheService::redisHandler()->get($timeKey); + return empty($time) ? 0 : $time; + } + + /** + * 取出下单支付、评论得到的抽奖此处 + * @param int $uid + * @param string $type + * @return int|mixed + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getCacheLotteryNum(int $uid, string $type = 'order') + { + $key = 'user_' . $type . '_luck_lottery_' . $uid; + $num = CacheService::redisHandler()->get($key); + return empty($num) ? 0 : $num; + } + + /** + * 抽奖之后销毁缓存 + * @param int $uid + * @param string $type + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function delCacheLotteryNum(int $uid, string $type = 'order') + { + $key = 'user_' . $type . '_luck_lottery_' . $uid; + $timeKey = 'expire_time_user_' . $type . '_luck_lottery_' . $uid; + $num = $this->getCacheLotteryNum($uid, $type); + $cache = CacheService::redisHandler(); + if ($num > 1) { + $cache->set($timeKey, bcadd((string)time(), (string)$this->luck_cache_time), bcadd('10', (string)$this->luck_cache_time)); + $cache->set($key, $num - 1, $this->luck_cache_time); + } else { + $cache->delete($timeKey); + $cache->delete($key); + } + return true; + } +} diff --git a/app/services/activity/newcomer/StoreNewcomerServices.php b/app/services/activity/newcomer/StoreNewcomerServices.php new file mode 100644 index 0000000..0494b41 --- /dev/null +++ b/app/services/activity/newcomer/StoreNewcomerServices.php @@ -0,0 +1,639 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\newcomer; + +use app\dao\activity\newcomer\StoreNewcomerDao; +use app\jobs\product\ProductLogJob; +use app\services\BaseServices; +use app\services\diy\DiyServices; +use app\services\order\StoreOrderServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\ensure\StoreProductEnsureServices; +use app\services\product\label\StoreProductLabelServices; +use app\services\product\product\StoreDescriptionServices; +use app\services\product\product\StoreProductReplyServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\user\UserRelationServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\SystemConfigService; +use think\exception\ValidateException; + +/** + * Class StoreNewcomerServices + * @package app\services\activity\newcomer + * @mixin StoreNewcomerDao + */ +class StoreNewcomerServices extends BaseServices +{ + + /** + * 商品活动类型 + */ + const TYPE = 7; + + /** + * StoreNewcomerServices constructor. + * @param StoreNewcomerDao $dao + */ + public function __construct(StoreNewcomerDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取新人礼商品详情 + * @param int $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getInfo(int $id) + { + $info = $this->dao->get($id, ['*'], ['product']); + $res = []; + if ($info) { + $res = $info->toArray(); + $product = $res['product'] ?? []; + unset($res['product'], $product['id'], $product['price']); + $res = array_merge($res, $product); + } + return $res; + } + + /** + * 获取新人专享商品 + * @param array $where + * @param string $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCustomerProduct(array $where = [], string $field = '*', array $with = ['product', 'attrValue']) + { + [$page, $limit] = $this->getPageValue(); + $where['is_del'] = 0; + $list = $this->dao->getList($where, $field, $page, $limit, $with); + $res = []; + if ($list) { + foreach ($list as &$item) { + $product = $item['product'] ?? []; + if ($product) { + unset($item['product'], $product['id'], $product['price']); + $item = array_merge($item, $product); + $res[] = $item; + } + } + } + return $res; + } + + /** + * 保存新人礼专属商品 + * @param $data + * @return bool + */ + public function saveNewcomer($data) + { + $productIds = []; + $oldProductIds = $this->dao->getColumn(['is_del' => 0], 'id,product_id'); + if ($oldProductIds) $oldProductIds = array_column($oldProductIds, 'product_id'); + if ($data) { + $productIds = array_column($data, 'product_id'); + $this->transaction(function () use ($data) { + foreach ($data as $product) { + $this->saveData(0, $product); + } + }); + } + $deleteIds = array_merge(array_diff($oldProductIds, $productIds)); + //清空现有新人礼商品 + if ($deleteIds) { + $this->dao->update(['is_del' => 0, 'product_id' => $deleteIds], ['is_del' => 1 ,'update_time' => time()]); + } + return true; + } + + /** + * 保存数据 + * @param int $id + * @param array $info + */ + public function saveData(int $id, array $info) + { + if (!$info || !isset($info['product_id']) || !$info['product_id']) { + throw new ValidateException('请重新选择新人专享商品'); + } + $attr = $info['attr']; + if ($attr) { + foreach ($attr as $a) { + if (!isset($a['unique']) || !isset($a['price'])) throw new ValidateException('请重新选择新人专享商品'); + if (!$a['price']) { + throw new ValidateException('请填写商品专享价'); + } + } + } else { + if (!$info['price']) { + throw new ValidateException('请填写商品专享价'); + } + } + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $productInfo = $storeProductServices->getOne(['is_show' => 1, 'is_del' => 0, 'is_verify' => 1, 'id' => $info['product_id']]); + if (!$productInfo) { + throw new AdminException('原商品已下架或移入回收站'); + } + if ($productInfo['is_vip_product'] || $productInfo['is_presale_product']) { + throw new AdminException('该商品是预售或svip专享'); + } + if (!$id) { + $newcomer = $this->dao->getOne(['product_id' => $info['product_id'], 'is_del' => 0]); + $id = $newcomer['id'] ?? 0; + } + + $data = []; + $data['product_id'] = $productInfo['id']; + $data['type'] = $productInfo['type'] ?? 0; + $data['product_type'] = $productInfo['product_type']; + $data['relation_id'] = $productInfo['relation_id'] ?? 0; + $data['price'] = $info['price'] ?? 0; + + if ($attr) $data['price'] = min(array_column($attr, 'price')); + + /** @var StoreProductAttrValueServices $productAttrValueServices */ + $productAttrValueServices = app()->make(StoreProductAttrValueServices::class); + /** @var StoreProductAttrServices $productAttrServices */ + $productAttrServices = app()->make(StoreProductAttrServices::class); + $attrValue = $productAttrValueServices->getList(['product_id' => $info['product_id'], 'type' => 0]); + $skus = array_column($attr, 'unique'); + $attr = array_combine($skus, $attr); + + $this->transaction(function () use ($id, $data, $attrValue, $skus, $attr, $productAttrValueServices, $productAttrServices) { + $newcomerAttrValue = []; + if ($id) { + $data['update_time'] = time(); + $res = $this->dao->update($id, $data); + $newcomerAttrValue = $productAttrValueServices->getList(['product_id' => $id, 'type' => 7]); + if (!$res) throw new AdminException('修改失败'); + } else { + $data['add_time'] = time(); + $res = $this->dao->save($data); + if (!$res) throw new AdminException('添加失败'); + $id = (int)$res->id; + } + $detail = []; + foreach ($attrValue as $item) { + if (in_array($item['unique'], $skus)) { + $item['product_id'] = $id; + $item['type'] = 7; + if (isset($attr[$item['unique']]['price'])) $item['price'] = $attr[$item['unique']]['price']; + $item['unique'] = $productAttrServices->createAttrUnique((int)$id, $item['suk']); + unset($item['id']); + $detail[] = $item; + } + } + if ($newcomerAttrValue) { + foreach ($newcomerAttrValue as $item) { + if (in_array($item['unique'], $skus)) { + $item['product_id'] = $id; + $item['type'] = 7; + if (isset($attr[$item['unique']]['price'])) $item['price'] = $attr[$item['unique']]['price']; + unset($item['id']); + $detail[] = $item; + } + } + } + $productAttrValueServices->delete(['product_id' => $id, 'type' => 7]); + if ($detail) $productAttrValueServices->saveAll($detail); + }); + } + + /** + * 获取新人专享商品详情 + * @param int $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function newcomerDetail(int $uid, int $id) + { + $storeInfo = $this->getInfo($id); + if (!$storeInfo) { + throw new ValidateException('新人商品已下架或删除'); + } + /** @var DiyServices $diyServices */ + $diyServices = app()->make(DiyServices::class); + $infoDiy = $diyServices->getProductDetailDiy(); + //diy控制参数 + if (!isset($infoDiy['is_specs']) || !$infoDiy['is_specs']) { + $storeInfo['specs'] = []; + } + + $configData = SystemConfigService::more(['site_url', 'routine_contact_type', 'site_name', 'share_qrcode', 'store_self_mention', 'store_func_status', 'product_poster_title']); + $siteUrl = $configData['site_url'] ?? ''; + $storeInfo['image'] = set_file_url($storeInfo['image'], $siteUrl); + $storeInfo['image_base'] = set_file_url($storeInfo['image'], $siteUrl); + /** @var StoreDescriptionServices $descriptionServices */ + $descriptionServices = app()->make(StoreDescriptionServices::class); + $storeInfo['description'] = $descriptionServices->getDescription(['product_id' => $storeInfo['product_id']]); + + //品牌名称 + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $productInfo = $storeProductServices->getCacheProductInfo((int)$storeInfo['product_id']); + $storeInfo['brand_name'] = $storeProductServices->productIdByBrandName((int)$storeInfo['product_id'], $productInfo); + $delivery_type = $storeInfo['delivery_type'] ?? $productInfo['delivery_type']; + $storeInfo['delivery_type'] = is_string($delivery_type) ? explode(',', $delivery_type) : $delivery_type; + /** + * 判断配送方式 + */ + $storeInfo['delivery_type'] = $storeProductServices->getDeliveryType((int)$storeInfo['type'], (int)$storeInfo['type'], (int)$storeInfo['relation_id'],$storeInfo['delivery_type']); + $storeInfo['total'] = $productInfo['sales'] + $productInfo['ficti']; + $storeInfo['store_label'] = $storeInfo['ensure'] = []; + if ($storeInfo['store_label_id']) { + /** @var StoreProductLabelServices $storeProductLabelServices */ + $storeProductLabelServices = app()->make(StoreProductLabelServices::class); + $storeInfo['store_label'] = $storeProductLabelServices->getColumn([['id', 'in', $storeInfo['store_label_id']]], 'id,label_name'); + } + if ($storeInfo['ensure_id'] && isset($infoDiy['is_ensure']) && $infoDiy['is_ensure']) { + /** @var StoreProductEnsureServices $storeProductEnsureServices */ + $storeProductEnsureServices = app()->make(StoreProductEnsureServices::class); + $storeInfo['ensure'] = $storeProductEnsureServices->getColumn([['id', 'in', $storeInfo['ensure_id']]], 'id,name,image,desc'); + } + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $data['buy_num'] = $storeOrderServices->getBuyCount($uid, 7, $id); + + /** @var UserRelationServices $userRelationServices */ + $userRelationServices = app()->make(UserRelationServices::class); + $storeInfo['userCollect'] = $userRelationServices->isProductRelation(['uid' => $uid, 'relation_id' => $storeInfo['product_id'], 'type' => 'collect', 'category' => UserRelationServices::CATEGORY_PRODUCT]); + $storeInfo['userLike'] = 0; + + $storeInfo['uid'] = $uid; + + //商品详情 + $storeInfo['small_image'] = get_thumb_water($storeInfo['image']); + $data['storeInfo'] = $storeInfo; + + $data['reply'] = []; + $data['replyChance'] = $data['replyCount'] = 0; + if (isset($infoDiy['is_reply']) && $infoDiy['is_reply']) { + /** @var StoreProductReplyServices $storeProductReplyService */ + $storeProductReplyService = app()->make(StoreProductReplyServices::class); + $reply = $storeProductReplyService->getRecProductReply((int)$storeInfo['product_id'], (int)($infoDiy['reply_num'] ?? 1)); + $data['reply'] = $reply ? get_thumb_water($reply, 'small', ['pics']) : []; + [$replyCount, $goodReply, $replyChance] = $storeProductReplyService->getProductReplyData((int)$storeInfo['product_id']); + $data['replyChance'] = $replyChance; + $data['replyCount'] = $replyCount; + } + + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + [$productAttr, $productValue] = $storeProductAttrServices->getProductAttrDetail($id, $uid, 0, 7, $storeInfo['product_id']); + $data['productAttr'] = $productAttr; + $data['productValue'] = $productValue; + $data['routine_contact_type'] = $configData['routine_contact_type'] ?? 0; + $data['store_func_status'] = (int)($configData['store_func_status'] ?? 1);//门店是否开启 + $data['store_self_mention'] = $data['store_func_status'] ? (int)($configData['store_self_mention'] ?? 0) : 0;//门店自提是否开启 + $data['site_name'] = $configData['site_name'] ?? ''; + $data['share_qrcode'] = $configData['share_qrcode'] ?? 0; + $data['product_poster_title'] = $configData['product_poster_title'] ?? ''; + //浏览记录 + ProductLogJob::dispatch(['visit', ['uid' => $uid, 'id' => $id, 'product_id' => $storeInfo['product_id']], 'newcomer']); + + return $data; + } + + + /** + * 修改秒杀库存 + * @param int $num + * @param int $newcomerId + * @param string $unique + * @param int $store_id + * @return bool + */ + public function decNewcomerStock(int $num, int $newcomerId, string $unique = '', int $store_id = 0) + { + $product_id = $this->dao->value(['id' => $newcomerId], 'product_id'); + $res = true; + if ($product_id && $unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //新人商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $newcomerId, 'type' => 7], 'suk'); + //平台普通商品sku unique + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //减去当前普通商品sku的库存增加销量 + $res = false !== $services->decProductStock($num, (int)$product_id, (string)$productUnique, $store_id); + } + + return $res; + } + + /** + * 加库存减销量 + * @param int $num + * @param int $newcomerId + * @param string $unique + * @param int $store_id + * @return bool + */ + public function incNewcomerStock(int $num, int $newcomerId, string $unique = '', int $store_id = 0) + { + $product_id = $this->dao->value(['id' => $newcomerId], 'product_id'); + $res = true; + if ($product_id && $unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //新人商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $newcomerId, 'type' => 7], 'suk'); + //平台商品sku unique + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //减去当前普通商品sku的库存增加销量 + $res = $services->incProductStock($num, (int)$product_id, (string)$productUnique, $store_id); + } + return $res; + } + + + /** + * 下单|加入购物车验证秒杀商品库存 + * @param int $uid + * @param int $newcomerId + * @param int $cartNum + * @param string $unique + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkNewcomerStock(int $uid, int $newcomerId, int $cartNum = 1, string $unique = '') + { + if (!$this->checkUserNewcomer($uid)) { + throw new ValidateException('您已无法享受新人专享价'); + } + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + if ($unique == '') { + $unique = $attrValueServices->value(['product_id' => $newcomerId, 'type' => 7], 'unique'); + } + //检查商品活动状态 + $newcomerInfo = $this->getInfo($newcomerId); + if (!$newcomerInfo) { + throw new ValidateException('该活动已下架'); + } + $attrInfo = $attrValueServices->getOne(['product_id' => $newcomerId, 'unique' => $unique, 'type' => 7]); + if (!$attrInfo || $attrInfo['product_id'] != $newcomerId) { + throw new ValidateException('请选择有效的商品属性'); + } + $suk = $attrInfo['suk']; + $productAttrInfo = $attrValueServices->getOne(['suk' => $suk, 'product_id' => $newcomerInfo['product_id'], 'type' => 0]); + if (!$productAttrInfo) { + throw new ValidateException('请选择有效的商品属性'); + } + //库存要验证愿商品库存 + if ($cartNum > $productAttrInfo['stock']) { + throw new ValidateException('该商品库存不足' . $cartNum); + } + return [$attrInfo, $unique, $newcomerInfo]; + } + + /** + * 验证用户是否可以享受首单优惠 + * @param int $uid + * @param $userInfo + * @return array + */ + public function checkUserFirstDiscount(int $uid, $userInfo = []) + { + if (!$uid) { + return []; + } + //开启新人礼 + if (!sys_config('newcomer_status')) { + return []; + } + //开启首单优惠 + if (!sys_config('first_order_status')) { + return []; + } + if (!$userInfo) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + return []; + } + } + if (isset($userInfo['is_first_order']) && $userInfo['is_first_order'] != 0) { + return []; + } + $timeStatus = sys_config('newcomer_limit_status', 1); + if ($timeStatus) {//设置限时,且超过时间不再享受 + $time = sys_config('newcomer_limit_time'); + if ($time) { + $time = (int)bcsub((string)time(), (string)bcmul((string)$time, '86400')); + if ($time > $userInfo['add_time']) { + return []; + } + } + } + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $count = $storeOrderServices->getCount([['type', 'not in', [7]], ['uid', '=', $uid]]); + if ($count) {//有过订单 + return []; + } + $discount = sys_config('first_order_discount', 100); + $discount_limit = sys_config('first_order_discount_limit', 0); + return [$discount, $discount_limit]; + } + + /** + * 验证用户是否可以购买新人专享 + * @param int $uid + * @param $userInfo + * @return bool + */ + public function checkUserNewcomer(int $uid, $userInfo = []) + { + if (!$uid) { + return false; + } + //开启新人礼 + if (!sys_config('newcomer_status')) { + return false; + } + //开启新人专享 + if (!sys_config('register_price_status')) { + return false; + } + if (!$userInfo) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + return false; + } + } + if (isset($userInfo['is_newcomer']) && $userInfo['is_newcomer'] != 0) { + return false; + } + $timeStatus = sys_config('newcomer_limit_status', 1); + if ($timeStatus) {//设置限时,且超过时间不再享受 + $time = sys_config('newcomer_limit_time'); + if ($time) { + $time = (int)bcsub((string)time(), (string)bcmul((string)$time, '86400')); + if ($time > $userInfo['add_time']) { + return false; + } + } + } + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $count = $orderServices->count(['uid' => $uid, 'type' => 9]); + if ($count) { + return false; + } + return true; + } + + /** + * 用户会员卡激活状态 + * @param int $uid + * @return bool|mixed + */ + public function setUserLevel(int $uid) + { + if (!$uid) { + return false; + } + //开启会员卡 且不需要激活 直接设置为已激活状态 + if (sys_config('member_func_status') && !sys_config('level_activate_status')) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + return $userServices->update($uid, ['level_status' => 1]); + } + return true; + } + + /** + * 用户注册设置:首单优惠、新人专享是否可用 -1:不可用 0:未使用 1:已使用 + * @param int $uid + * @return false|mixed + */ + public function setUserNewcomer(int $uid) + { + if (!$uid) { + return false; + } + //是否开启新人礼 + $newcomer_status = sys_config('newcomer_status'); + $update = []; + if ($newcomer_status) { + //开启首单优惠 + $first_order = sys_config('first_order_status'); + $update['is_first_order'] = $first_order ? 0 : -1; + //开启新人专享 + $is_newcomer = sys_config('register_price_status'); + $update['is_newcomer'] = $is_newcomer ? 0 : -1; + } else {//不可用,数据记录已使用 + $update = ['is_first_order' => -1, 'is_newcomer' => -1]; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + return $userServices->update($uid, $update); + } + + /** + * 下单修改用户首单优惠 + * @param int $uid + * @param $orderInfo + * @return bool + */ + public function updateUserNewcomer(int $uid, $orderInfo) + { + if (!$uid || !$orderInfo) { + return false; + } + $update = []; + //首单优惠 + if (isset($orderInfo['first_order_price']) && $orderInfo['first_order_price'] > 0) { + $update['is_first_order'] = 1; + } + //新人专享 + if (isset($orderInfo['type']) && $orderInfo['type'] == 7) { + $update['is_newcomer'] = 1; + } + if ($update) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userServices->update($uid, $update); + $userServices->cacheTag()->clear(); + } + return true; + } + + /** + * 获取新人专享商品 + * @param int $uid + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDiyNewcomerList(int $uid = 0, array $where = []) + { + if ($uid) {//验证权限 + //验证用户是否享受过新人礼 + if (!$this->checkUserNewcomer($uid)) { + return []; + } + } else {//验证是否开启 无登录:如果开启新人礼默认要展示 + //开启新人礼 || 开启新人专享 + if (!sys_config('newcomer_status') || !sys_config('register_price_status')) { + return []; + } + } + [$page, $limit] = $this->getPageValue(); + $where['is_del'] = 0; + $list = $this->dao->getList($where, '*', $page, $limit, ['product']); + $res = []; + if ($list) { + foreach ($list as &$item) { + $product = $item['product'] ?? []; + if ($product) { + unset($item['product'], $product['id'], $product['price'], $product['sales']); + $item = array_merge($item, $product); + $res[] = $item; + } + } + } + return $res; + } + +} diff --git a/app/services/activity/promotions/StorePromotionsAuxiliaryServices.php b/app/services/activity/promotions/StorePromotionsAuxiliaryServices.php new file mode 100644 index 0000000..95beb4f --- /dev/null +++ b/app/services/activity/promotions/StorePromotionsAuxiliaryServices.php @@ -0,0 +1,279 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\activity\promotions; + + +use app\dao\activity\promotions\StorePromotionsAuxiliaryDao; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\BaseServices; + +/** + * 优惠活动辅助表 + * Class StorePromotionsAuxiliaryServices + * @package app\services\activity\promotions + * @mixin StorePromotionsAuxiliaryDao + */ +class StorePromotionsAuxiliaryServices extends BaseServices +{ + + /** + * @param StorePromotionsAuxiliaryDao $dao + */ + public function __construct(StorePromotionsAuxiliaryDao $dao) + { + $this->dao = $dao; + } + + /** + * 优惠活动关联保存 + * @param int $promotionsId + * @param int $type + * @param array $promotionsAuxiliaryData + * @param array $couponData + * @param array $giveProductData + * @return bool + */ + public function savePromotionsRelation(int $promotionsId, int $type, array $promotionsAuxiliaryData, array $couponData = [], array $giveProductData = [], bool $isAttr = true) + { + $this->dao->delete(['promotions_id' => $promotionsId]); + if ($promotionsAuxiliaryData) $this->savePromotionsProducts($promotionsId, $type, $promotionsAuxiliaryData, $isAttr); + if ($couponData) $this->savePromotionsGiveCoupon($promotionsId, $couponData); + if ($giveProductData) $this->savePromotionsGiveProducts($promotionsId, $giveProductData); + return true; + } + + /** + * 设置活动关联商品 + * @param int $promotionsId + * @param int $type + * @param array $productIds + * @return bool + */ + public function savePromotionsProducts(int $promotionsId, int $type, array $promotionsAuxiliaryData, bool $isAttr = true) + { + if ($promotionsAuxiliaryData) { + $data = []; + $unitData = ['type' => 1, 'promotions_id' => $promotionsId ,'product_partake_type' => $type]; + switch ($type) { + case 1://所有商品 + $data[] = $unitData; + break; + case 2: + case 3: + if ($isAttr) { + $productIds = array_column($promotionsAuxiliaryData, 'product_id'); + $promotionsAuxiliaryData = array_combine($productIds, $promotionsAuxiliaryData); + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + foreach ($productIds as $productId) { + $unique = $promotionsAuxiliaryData[$productId]['unique'] ?? []; + $skuCount = $skuValueServices->count(['product_id' => $productId, 'type' => 0]); + $unitData['product_id'] = $productId; + $unitData['is_all'] = count($unique) >= $skuCount ? 1 : 0; + $unitData['unique'] = implode(',', $unique); + $data[] = $unitData; + } + } else { + $productIds = $promotionsAuxiliaryData['product_id'] ?? []; + foreach ($productIds as $productId) { + $unitData['product_id'] = $productId; + $data[] = $unitData; + } + } + break; + case 4://品牌 + $brandIds = $promotionsAuxiliaryData['brand_id'] ?? []; + if ($brandIds) { + foreach ($brandIds as $id) { + $unitData['brand_id'] = $id; + $data[] = $unitData; + } + } + break; + case 5://标签 + $storeLabelIds = $promotionsAuxiliaryData['store_label_id'] ?? []; + if ($storeLabelIds) { + foreach ($storeLabelIds as $id) { + $unitData['store_label_id'] = $id; + $data[] = $unitData; + } + } + break; + } + if ($data) $this->dao->saveAll($data); + $this->setPromotionsAuxiliaryCache($promotionsId, $data); + } + return true; + } + + /** + * 设置活动关联赠送优惠券 + * @param int $promotionsId + * @param array $couponData + * @return bool + */ + public function savePromotionsGiveCoupon(int $promotionsId, array $couponData) + { + if ($couponData) { + $data = []; + $couponIds = array_column($couponData, 'give_coupon_id'); + $couponData = array_combine($couponIds, $couponData); + foreach ($couponIds as $couponId) { + $data[] = [ + 'type' => 2, + 'promotions_id' => $promotionsId, + 'coupon_id' => $couponId, + 'limit_num' => $couponData[$couponId]['give_coupon_num'] ?? 0, + 'surplus_num' => $couponData[$couponId]['give_coupon_num'] ?? 0 + ]; + } + $this->dao->saveAll($data); + } + return true; + } + + /** + * 设置活动关联赠送商品 + * @param int $promotionsId + * @param array $giveProductData + * @return bool + */ + public function savePromotionsGiveProducts(int $promotionsId, array $giveProductData) + { + if ($giveProductData) { + $data = []; + foreach ($giveProductData as $product) { + $data[] = [ + 'type' => 3, + 'promotions_id' => $promotionsId, + 'product_id' => $product['give_product_id'], + 'limit_num' => $product['give_product_num'] ?? 0, + 'surplus_num' => $product['give_product_num'] ?? 0, + 'unique' => $product['unique'] ?? '' + ]; + } + $this->dao->saveAll($data); + } + return true; + } + + + /** + * 优惠活动关联赠品限量处理 + * @param array $promotions_id + * @param int $type + * @param int $id + * @param bool $is_dec + * @param string $unique + * @param int $num + * @return bool + */ + public function updateLimit(array $promotions_id, int $type, int $id, bool $is_dec = true, string $unique = '', int $num = 1) + { + if (!$promotions_id) return true; + $where = ['promotions_id' => $promotions_id, 'type' => $type]; + if ($type == 2) { + $where['coupon_id'] = $id; + } else { + $where['product_id'] = $id; + $where['unique'] = $unique; + } + $info = $this->dao->get($where); + if ($info) { + if ($is_dec) { + if ($info['surplus_num'] < $num) { + $surplus_num = 0; + } else { + $surplus_num = bcsub((string)$info['surplus_num'], (string)$num, 0); + } + } else { + $surplus_num = bcadd((string)$info['surplus_num'], (string)$num, 0); + } + + $this->dao->update($info['id'], ['surplus_num' => $surplus_num]); + } + return true; + } + + /** + * 设置优惠活动关联缓存 + * @param int $promotions_id + * @param array $data + * @param int $product_partake_type + * @return array + */ + public function setPromotionsAuxiliaryCache(int $promotions_id, array $data, int $product_partake_type = 1) + { + $key ='cache_promotions_auxiliary_' . $promotions_id; + $cacheData = []; + if ($data) { + switch ($product_partake_type) { + case 1://所有商品 + break; + case 2: + case 3: + $cacheData = array_unique(array_column($data, 'product_id')); + break; + case 4://品牌 + $cacheData = array_unique(array_column($data, 'brand_id')); + break; + case 5://标签 + $cacheData = array_unique(array_column($data, 'store_label_id')); + break; + } + } + + $this->dao->cacheHander()->delete($key); + $this->dao->cacheTag()->set($key, $cacheData); + return $cacheData; + } + + /** + * 更新缓存 + * @param int $promotions_id + * @param string $key + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updatePromotionsAuxiliaryCache(int $promotions_id) + { + $data = $this->dao->getList(['promotions_id' => $promotions_id, 'type' => 1]); + $cacheData = []; + if ($data) { + $product_partake_type = $data[0]['product_partake_type'] ?? 0; + $cacheData = $this->setPromotionsAuxiliaryCache($promotions_id, $data, $product_partake_type); + } + return $cacheData; + } + + /** + * 获取商品关联缓存 + * @param int $product_id + * @param array $type + * @param bool $isCache + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPromotionsAuxiliaryCache(int $promotions_id, bool $isCache = false) + { + $key = 'cache_promotions_auxiliary_' . $promotions_id; + $data = $this->dao->cacheHander()->get($key); + if (!$data || $isCache) { + $data = $this->updatePromotionsAuxiliaryCache($promotions_id); + } + return $data; + } +} diff --git a/app/services/activity/promotions/StorePromotionsServices.php b/app/services/activity/promotions/StorePromotionsServices.php new file mode 100644 index 0000000..e28c7f2 --- /dev/null +++ b/app/services/activity/promotions/StorePromotionsServices.php @@ -0,0 +1,2296 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\promotions; + +use app\dao\activity\promotions\StorePromotionsDao; +use app\services\activity\coupon\StoreCouponIssueServices; +use app\services\activity\coupon\StoreCouponUserServices; +use app\services\order\StoreOrderCreateServices; +use app\services\order\StoreOrderComputedServices; +use app\services\BaseServices; +use app\services\product\brand\StoreBrandServices; +use app\services\product\label\StoreProductLabelServices; +use app\services\product\product\StoreProductRelationServices; +use app\services\product\product\StoreProductServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\store\SystemStoreServices; +use app\services\user\label\UserLabelServices; +use app\services\order\StoreOrderServices; +use app\services\order\StoreOrderCartInfoServices; +use app\services\order\StoreCartServices; +use app\services\product\category\StoreProductCategoryServices; +use crmeb\exceptions\AdminException; +use think\exception\ValidateException; +use \crmeb\traits\OptionTrait; + + +/** + * 促销活动 + * Class StorePromotionsServices + * @package app\services\activity\promotions + * @mixin StorePromotionsDao + */ +class StorePromotionsServices extends BaseServices +{ + use OptionTrait; + + /** + * 活动类型 + * @var string[] + */ + protected $promotionsType = [ + 1 => '限时折扣', + 2 => '第N件N折', + 3 => '满减满折', + 4 => '满送', + ]; + + /** + * 优惠内容数据 + * @var array + */ + protected $promotionsData = [ + 'threshold_type' => 1,//门槛类型1:满N元2:满N件 + 'threshold' => 0,//优惠门槛 + 'discount_type' => 1,//优惠类型1:满减2:满折 + 'n_piece_n_discount' => 3,//n件n折类型:1:第二件半件2:买1送1 3:自定义 + 'discount' => 0,//优惠 + 'give_integral' => 0,//赠送积分 + 'give_coupon_id' => [],//赠送优惠券ID + 'give_product_id' => [],//赠送商品ID + ]; + + /** + * StorePromotionsServices constructor. + * @param StorePromotionsDao $dao + */ + public function __construct(StorePromotionsDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取打折折扣 || 优惠折扣 + * @param $num + * @param int $unit + * @param int $type 1:打折折扣 2:优惠折扣 + * @return float + */ + public function computedDiscount($num, int $unit = 100, int $type = 1) + { + if ((float)$num < 0) { + $num = 0; + } elseif ((float)$num > 100) { + $num = 100; + } + $discount = bcdiv((string)$num, (string)$unit, 2); + if ($type == 2) {//优惠折扣 打9折扣优惠就是1折 + $discount = bcsub('1', (string)$discount, 2); + } + return (float)$discount; + } + + + /** + * 获取优惠活动标题,内容详情 + * @param int $type + * @param int $promotions_cate + * @param array $promotions + * @return array + */ + public function getPromotionsDesc(int $type, int $promotions_cate, array $promotions) + { + $title = ''; + $desc = []; + if ($promotions) { + switch ($type) { + case 1: + $title = '限时折扣'; + $base = '限时打' . $this->computedDiscount($promotions[0]['discount'] ?? 0, 10) . '折'; + if (isset($promotions['is_limit']) && isset($promotions['limit_num']) && (int)$promotions['is_limit'] && (int)$promotions['limit_num']) { + $base .= ',每人限购' . (int)$promotions['limit_num'] . '件'; + } + $desc[] = $base; + break; + case 2: + switch ($promotions[0]['n_piece_n_discount'] ?? 3) { + case 1: + $title = '第二件半价'; + break; + case 2: + $title = '买1送1'; + break; + case 3: + $title = '第' . floatval($promotions[0]['threshold'] ?? 0) . '件' . $this->computedDiscount($promotions[0]['discount'] ?? 0, 10) . '折'; + break; + } + $desc[] = '买' . floatval($promotions[0]['threshold'] ?? 0) . '件商品,其中一件享' . $this->computedDiscount($promotions[0]['discount'] ?? 0, 10) . '折优惠'; + break; + case 3: + $title = '满减满折'; + foreach ($promotions as $p) { + if ($promotions_cate == 2) { + $give = '每满' . floatval($p['threshold'] ?? 0); + } else { + $give = '满' . floatval($p['threshold'] ?? 0); + } + $give .= $p['threshold_type'] == 1 ? '元' : '件'; + $give .= $p['discount_type'] == 1 ? ('减' . floatval($p['discount'] ?? 0) . '元') : '打' . $this->computedDiscount($p['discount'] ?? 0, 10) . '折'; + $desc[] = $give; + } + break; + case 4: + $title = '满送活动'; + foreach ($promotions as $p) { + if ($promotions_cate == 2) { + $base = '每满' . floatval($p['threshold'] ?? 0); + } else { + $base = '满' . floatval($p['threshold'] ?? 0); + } + $base .= $p['threshold_type'] == 1 ? '元送' : '件送'; + if ($p['give_integral']) { + $desc[] = $base . floatval($p['give_integral'] ?? 0) . '积分'; + } + if ($p['give_coupon_id']) { + $desc[] = $base . '优惠券'; + } + if ($p['give_product_id']) { + $desc[] = $base . '赠品'; + } + } + break; + default: + break; + } + } + return [$title, $desc]; + } + + /** + * 返回目前进行中的活动ids + * @param array $promotions_type + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAllShowActivityIds(array $promotions_type = [], string $field = 'id') + { + $where = ['type' => 1, 'store_id' => 0, 'pid' => 0, 'is_del' => 0, 'status' => 1, 'promotionsTime' => true]; + if ($promotions_type) $where['promotions_type'] = $promotions_type; + $promotions = $this->dao->getList($where, $field); + $ids = []; + if ($promotions) { + if ($field == 'id') { + $ids = array_column($promotions, 'id'); + } else { + $ids = $promotions; + } + } + return $ids; + } + + /** + * 获取商品所属活动 + * @param array $productIds + * @param array $promotions_type + * @param string $field + * @param array $with + * @param string $group + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductsPromotions(array $productIds, array $promotions_type = [], string $field = '*', array $with = [], string $group = '', int $store_id = 0) + { + if (!$productIds) { + return [[], []]; + } + $promotionsIds = $this->getAllShowActivityIds($promotions_type, 'id,promotions_type,product_partake_type,applicable_type,applicable_store_id'); + if (!$promotionsIds) { + return [[], []]; + } + $ids = []; + $productArr = []; + /** @var StoreProductRelationServices $productRelationServices */ + $productRelationServices = app()->make(StoreProductRelationServices::class); + foreach ($productIds as $productId) { + $productArr[$productId] = $productRelationServices->getProductRelationCache((int)$productId, [2, 3]); + } + /** @var StorePromotionsAuxiliaryServices $promotionsAuxiliaryServices */ + $promotionsAuxiliaryServices = app()->make(StorePromotionsAuxiliaryServices::class); + $preType = []; + foreach ($promotionsIds as $info) { + $pid = (int)$info['id']; + $applicable_store_id = is_array($info['applicable_store_id']) ? $info['applicable_store_id'] : explode(',', $info['applicable_store_id']); + //活动不适用该门店 + if ($store_id && ($info['applicable_type'] == 0 || ($info['applicable_type'] == 2 && !in_array($store_id, $applicable_store_id)))) { + continue; + } + if ($group && in_array($info['promotions_type'], $preType)) { + continue; + } + if ($info['product_partake_type'] == 1) {//所有商品 + $ids[] = $pid; + $preType[] = $info['promotions_type']; + } else { + $promotionsAuxiliaryData = $promotionsAuxiliaryServices->getPromotionsAuxiliaryCache($pid); + if ($info['product_partake_type'] == 2) { + if (array_intersect($promotionsAuxiliaryData, $productIds)) { + $ids[] = $pid; + $preType[] = $info['promotions_type']; + } + } else { + foreach ($productArr as $productInfo) { + $data = []; + switch ($info['product_partake_type']) { + case 4://品牌 + $data = $productInfo[2] ?? []; + break; + case 5://商品标签 + $data = $productInfo[3] ?? []; + break; + } + if (array_intersect($promotionsAuxiliaryData, $data)) {//一个商品满足活动 + $ids[] = $pid; + $preType[] = $info['promotions_type']; + break; + } + } + } + + } + } + $ids = array_unique($ids); + $result = []; + if ($ids) { + $order = 'promotions_type asc,update_time desc'; + $promotions = $this->dao->getList(['ids' => $ids], $field, 0, 0, $with, $order); + if ($promotions) { + $data = $this->promotionsData; + $data['giveCoupon'] = []; + $data['giveProducts'] = []; + foreach ($promotions as &$item) { + if (!isset($item['promotions'])) { + $item['promotions'] = []; + } + $first = array_merge($data, array_intersect_key($item, $data)); + array_unshift($item['promotions'], $first); + $item['promotions'] = $this->handelPromotions($item['promotions']); + $result[] = $item; + } + } + } + return [$result, $productArr]; + } + + /** + * 获取商品所有优惠活动 所属活动详情 + * @param array $productIds + * @param string $field + * @param array $with + * @param array $promotions_type + * @param string $group + * @param int $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductsPromotionsDetail(array $productIds, string $field = '*', array $with = [], array $promotions_type = [], string $group = '', int $store_id = 0) + { + $productDetails = []; + $promotionsDetails = []; + $promotions = []; + if ($productIds) { + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $newProductIds = $productServices->getColumn([['id', 'in', $productIds]], 'id,pid,type,relation_id', 'id'); + //处理门店商品活动 + $newIds = []; + if ($newProductIds) { + foreach ($newProductIds as $item) { + //门店自营商品 不参与活动 + if ($item['type'] == 1) { + if ($item['pid']) {//平台共享到门店商品 + $newIds[] = $item['pid']; + } + } else { + $newIds[] = $item['id']; + } + } + } + [$promotions, $productRelation] = $this->getProductsPromotions($newIds, $promotions_type, $field, $with, $group); + if ($promotions) { + /** @var StorePromotionsAuxiliaryServices $promotionsAuxiliaryServices */ + $promotionsAuxiliaryServices = app()->make(StorePromotionsAuxiliaryServices::class); + foreach ($promotions as $info) { + $id = (int)$info['id']; + $promotionsAuxiliaryData = $promotionsAuxiliaryServices->getPromotionsAuxiliaryCache($id); + $applicable_store_id = $info['applicable_store_id'] ? (is_string($info['applicable_store_id']) ? explode(',', $info['applicable_store_id']) : $info['applicable_store_id']) : []; + foreach ($productIds as $productId) { + $newProductId = $productId; + $detail = $newProductIds[$productId] ?? []; + if (!$detail) continue; + if ($detail['type'] == 1) {//门店商品 + if ($detail['pid']) {//平台共享到门店商品 + $newProductId = $detail['pid']; + } else {//门店自营商品 不参与活动 + continue; + } + $product_store_id = $detail['relation_id']; + } else {//平台、供应商商品 存在在门店购买情况 需要验证 + $product_store_id = $store_id; + } + //活动不适用该门店 + if ($product_store_id && ($info['applicable_type'] == 0 || ($info['applicable_type'] == 2 && !in_array($product_store_id, $applicable_store_id)))) { + continue; + } + $products = $info['products'] ?? []; + $pIds = $products ? array_unique(array_column($products, 'product_id')) : []; + switch ($info['product_partake_type']) { + case 1://全部商品 + $productDetails[$productId][] = $id; + $promotionsDetails[$id][] = $productId; + break; + case 2://选中商品参与 + if (in_array($newProductId, $pIds)) { + $productDetails[$productId][] = $id; + $promotionsDetails[$id][] = $productId; + } + break; + case 3://选中商品不参与 + $products = $info['products'] ?? []; + if ($products) $products = array_combine(array_column($products, 'product_id'), $products); + if (!in_array($newProductId, $pIds) || (isset($products[$newProductId]['is_all']) && $products[$newProductId]['is_all'] == 0)) { + $productDetails[$productId][] = $id; + $promotionsDetails[$id][] = $productId; + } + break; + case 4://品牌 + $data = $productRelation[$newProductId][2] ?? []; + if (array_intersect($promotionsAuxiliaryData, $data)) {//一个商品满足活动 + $productDetails[$productId][] = $id; + $promotionsDetails[$id][] = $productId; + } + break; + case 5://商品标签 + $data = $productRelation[$newProductId][3] ?? []; + if (array_intersect($promotionsAuxiliaryData, $data)) {//一个商品满足活动 + $productDetails[$productId][] = $id; + $promotionsDetails[$id][] = $productId; + } + break; + } + } + } + } + } + return [$promotions, $productDetails, $promotionsDetails]; + } + + /** + * 检测活动内容,格式数据 + * @param int $type + * @param array $data + * @return array + */ + public function checkPromotions(int $type, array $data) + { + if (!$data) { + throw new AdminException('请添加活动优惠内容'); + } + $data = array_merge($this->promotionsData, array_intersect_key($data, $this->promotionsData)); + switch ($type) { + case 1: + case 2: + $data['promotions_cate'] = 1; + $data['discount_type'] = 2; + $data['give_coupon_id'] = $data['give_product_id'] = []; + if ($type == 2) { + $data['threshold_type'] = 2; + if (!$data['threshold']) { + throw new AdminException('请输入打折门槛'); + } + } + if ($data['discount'] === '') { + throw new AdminException('请添加折扣'); + } + if ($data['discount'] < 0 || $data['discount'] > 100) { + throw new AdminException('折扣必须为0~99数字'); + } + break; + case 3: + $data['give_coupon_id'] = $data['give_product_id'] = []; + if (!$data['threshold']) { + throw new AdminException('请输入优惠门槛'); + } + if ($data['discount'] === '') { + throw new AdminException($data['discount_type'] == 1 ? '请输入优惠金额' : '请输入打折折扣'); + } + if ($data['discount_type'] == 2 && ($data['discount'] < 0 || $data['discount'] > 100)) { + throw new AdminException('折扣必须为0~99数字'); + } + break; + case 4: + if (!$data['threshold']) { + throw new AdminException('请输入优惠门槛'); + } + if (!$data['give_integral'] && !$data['give_coupon_id'] && !$data['give_product_id']) { + throw new AdminException('请至少选择一项赠送内容'); + } + if ($data['give_coupon_id']) { + $couponsIds = array_column($data['give_coupon_id'], 'give_coupon_id'); + $giveCoupon = array_combine($couponsIds, $data['give_coupon_id']); + /** @var StoreCouponIssueServices $storeCouponServices */ + $storeCouponServices = app()->make(StoreCouponIssueServices::class); + $coupons = $storeCouponServices->getValidGiveCoupons($couponsIds, 'id,is_permanent,remain_count'); + if (!$coupons || count($coupons) != count($couponsIds)) { + throw new AdminException('优惠券已失效请重新选择'); + } + foreach ($coupons as $coupon) { + if (!isset($giveCoupon[$coupon['id']]['give_coupon_num']) || !$giveCoupon[$coupon['id']]['give_coupon_num']) { + throw new AdminException('请输入赠送优惠券数量'); + } + if ($coupon['is_permanent'] == 0 && $coupon['remain_count'] < $giveCoupon[$coupon['id']]['give_coupon_num']) { + throw new AdminException('赠送优惠券数量不能超出优惠券限量'); + } + } + } + if ($data['give_product_id']) { + $productIds = array_column($data['give_product_id'], 'give_product_id'); + $giveProduct = array_combine($productIds, $data['give_product_id']); + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $products = $storeProductServices->getSearchList(['ids' => $productIds], 0, 0, ['id,stock']); + if (!$products || count($products) != count(array_unique($productIds))) { + throw new AdminException('商品已失效请重新选择'); + } + foreach ($products as $product) { + if (!isset($giveProduct[$product['id']]['give_product_num']) || !$giveProduct[$product['id']]['give_product_num']) { + throw new AdminException('请输入赠送商品数量'); + } + if ($product['stock'] < $giveProduct[$product['id']]['give_product_num']) { + throw new AdminException('赠送商品数量不能超出商品库存'); + } + } + } + break; + default: + throw new AdminException('暂不支持该类型优惠活动'); + } + return $data; + } + + /**获取门店适用的活动 + * @param $where + * @param $storeId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getgetPromotionListInfo($where, $storeId) + { + $list = $this->dao->getList($where); + $listInfo = []; + foreach ($list as $key => &$info) { + if ($info['applicable_type'] == 2) { + $store_ids = is_array($info['applicable_store_id']) ? $info['applicable_store_id'] : explode(',', $info['applicable_store_id']); + if (!in_array($storeId, $store_ids)) continue; + } + if ($info['start_time']) + $start_time = $info['promotions_type'] == 1 ? date('Y-m-d H:i', (int)$info['start_time']) : date('Y-m-d', (int)$info['start_time']); + if ($info['stop_time']) + $stop_time = $info['promotions_type'] == 1 ? date('Y-m-d H:i', (int)$info['stop_time']) : date('Y-m-d', (int)$info['stop_time']); + if (isset($start_time) && isset($stop_time)) + $info['section_time'] = [$start_time, $stop_time]; + else + $info['section_time'] = []; + unset($info['start_time'], $info['stop_time']); + $info['is_label'] = $info['label_id'] ? 1 : 0; + + $info['threshold'] = floatval($info['threshold']); + $info['discount'] = floatval($info['discount']); + $info['give_integral'] = intval($info['give_integral']); + $info['is_overlay'] = $info['overlay'] ? 1 : 0; + $info['promotions'] = $info['promotions'] ?? []; + $data = $this->promotionsData; + $first = array_merge($data, array_intersect_key($info, $data)); + array_unshift($info['promotions'], $first); + + $info['promotions'] = $this->handelPromotions($info['promotions']); + $listInfo[] = $info; + } + return $listInfo; + } + + /** + * 获取列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function systemPage(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit, ['giveProducts' => function ($query) { + $query->field('promotions_id,product_id,limit_num,surplus_num')->with(['productInfo' => function ($query) { + $query->field('id,store_name'); + }]); + }, 'giveCoupon' => function ($query) { + $query->field('promotions_id,coupon_id,limit_num,surplus_num')->with(['coupon' => function ($query) { + $query->field('id,type,coupon_type,coupon_title,coupon_price,use_min_price'); + }]); + }, 'promotions' => function ($query) { + $query->field('id,pid,promotions_type,promotions_cate,threshold_type,threshold,discount_type,n_piece_n_discount,discount,give_integral,give_coupon_id,give_product_id,give_product_unique')->with(['giveProducts' => function ($query) { + $query->field('promotions_id, product_id,limit_num,surplus_num')->with(['productInfo' => function ($query) { + $query->field('id,store_name'); + }]); + }, 'giveCoupon' => function ($query) { + $query->field('promotions_id, coupon_id,limit_num,surplus_num')->with(['coupon' => function ($query) { + $query->field('id,type,coupon_type,coupon_title,coupon_price,use_min_price'); + }]); + }]); + }]); + $count = 0; + if ($list) { + $count = $this->dao->count($where); + $data = $this->promotionsData; + $data['giveCoupon'] = []; + $data['giveProducts'] = []; + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + /** @var StoreProductRelationServices $storeProductRelationServices */ + $storeProductRelationServices = app()->make(StoreProductRelationServices::class); + /** @var StorePromotionsAuxiliaryServices $promotionsAuxiliaryServices */ + $promotionsAuxiliaryServices = app()->make(StorePromotionsAuxiliaryServices::class); + $getPromotions = function ($cartList, $oids) { + $promotionsPrice = 0; + $uids = []; + $oldUids = []; + $ids = []; + foreach ($cartList as $key => $cart) { + if (!in_array($cart['oid'], $oids)) continue; + $info = is_string($cart['cart_info']) ? json_decode($cart['cart_info'], true) : $cart['cart_info']; + $promotionsPrice = bcadd((string)$promotionsPrice, (string)bcmul((string)($info['promotions_true_price'] ?? 0), (string)$info['cart_num'], 2), 2); + if (!in_array($cart['oid'], $ids)) { + $ids[] = $cart['oid']; + if (!in_array($cart['uid'], $uids)) { + $uids[] = $cart['uid']; + } else { + $oldUids[] = $cart['uid']; + } + } + } + return [$promotionsPrice, $uids, $oldUids]; + }; + foreach ($list as &$item) { + if ($item['status']) { + if ($item['start_time'] > time()) + $item['start_name'] = '未开始'; + else if (bcadd((string)$item['stop_time'], '86400') < time()) + $item['start_name'] = '已结束'; + else if (bcadd((string)$item['stop_time'], '86400') > time() && $item['start_time'] < time()) { + $item['start_name'] = '进行中'; + } + } else $item['start_name'] = '已结束'; + $end_time = $item['stop_time'] ? date('Y/m/d', (int)$item['stop_time']) : ''; + $item['_stop_time'] = $end_time; + $item['stop_status'] = $item['stop_time'] + 86400 < time() ? 1 : 0; + + $item['sum_pay_price'] = 0.00; + $item['sum_promotions_price'] = 0.00; + $item['sum_order'] = 0; + $item['sum_user'] = 0; + $item['old_user'] = 0; + $item['new_user'] = 0; + $pids = array_merge([$item['id']], array_column($item['promotions'], 'id')); + $cartInfos = $storeOrderCartInfoServices->getColumn(['promotions_id' => $pids], 'oid,uid,cart_info', 'id', true); + if ($cartInfos) { + $oids = $storeOrderServices->getColumn(['id' => array_unique(array_column($cartInfos, 'oid')), 'is_del' => 0, 'pid' => 0, 'is_system_del' => 0, 'refund_status' => [0, 3]], 'id', ''); + $item['sum_pay_price'] = $storeOrderServices->sum(['id' => $oids, 'is_del' => 0, 'pid' => 0, 'is_system_del' => 0, 'refund_status' => [0, 3]], 'pay_price', true); + [$promotionsPrice, $uids, $oldUids] = $getPromotions($cartInfos, $oids); + $item['sum_promotions_price'] = $promotionsPrice; + $item['sum_order'] = $storeOrderServices->count(['id' => $oids, 'is_del' => 0, 'pid' => 0, 'is_system_del' => 0, 'refund_status' => [0, 3]]); + $item['sum_user'] = count($uids); + $item['old_user'] = count(array_unique($oldUids)); + $item['new_user'] = bcsub((string)$item['sum_user'], (string)$item['old_user'], 0); + } + + $first = array_merge($data, array_intersect_key($item, $data)); + array_unshift($item['promotions'], $first); + $item['promotions'] = $this->handelPromotions($item['promotions']); + $promotionsAuxiliaryData = $promotionsAuxiliaryServices->getPromotionsAuxiliaryCache($item['id']); + switch ($item['product_partake_type']) { + case 1://所有商品 + $item['product_count'] = $storeProductServices->count(['is_show' => 1, 'is_del' => 0, 'type' => [0, 2], 'is_verify' => 1]); + break; + case 2://选中商品参与 + $product_ids = $promotionsAuxiliaryData; + $item['product_count'] = $product_ids ? $storeProductServices->count(['is_show' => 1, 'is_del' => 0, 'id' => $product_ids, 'is_verify' => 1]) : 0; + break; + case 3: + $item['product_count'] = 0; + break; + case 4://品牌 + $product_ids = $promotionsAuxiliaryData ? $storeProductRelationServices->getIdsByWhere(['type' => 2, 'relation_id' => $promotionsAuxiliaryData]) : []; + $item['product_count'] = $product_ids ? $storeProductServices->count(['is_show' => 1, 'is_del' => 0, 'id' => $product_ids, 'type' => [0, 2], 'is_verify' => 1]) : 0; + break; + case 5://商品标签 + $product_ids = $promotionsAuxiliaryData ? $storeProductRelationServices->getIdsByWhere(['type' => 3, 'relation_id' => $promotionsAuxiliaryData]) : []; + $item['product_count'] = $product_ids ? $storeProductServices->count(['is_show' => 1, 'is_del' => 0, 'id' => $product_ids, 'type' => [0, 2], 'is_verify' => 1]) : 0; + break; + } + } + } + return compact('list', 'count'); + } + + /** + * 获取信息 + * @param int $id + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getInfo(int $id) + { + $info = $this->dao->get($id, ['*'], ['products' => function ($query) { + $query->field('promotions_id,product_id,unique')->with(['productInfo', 'attrValue']); + }, 'brands' => function ($query) { + $query->field('promotions_id,brand_id')->with(['brandInfo' => function ($q) { + $q->field('id,brand_name'); + }]); + }, 'productLabels' => function ($query) { + $query->field('promotions_id,store_label_id')->with(['productLabelInfo' => function ($q) { + $q->field('id,label_name'); + }]); + }, 'giveProducts' => function ($query) { + $query->field('type,promotions_id,product_id,limit_num,unique')->with(['productInfo' => function ($q) { + $q->field('id,store_name'); + }, 'giveAttrValue']); + }, 'giveCoupon' => function ($query) { + $query->field('type,promotions_id,coupon_id,limit_num')->with(['coupon']); + }, 'promotions' => function ($query) { + $query->field('id,pid,promotions_type,promotions_cate,threshold_type,threshold,discount_type,n_piece_n_discount,discount,give_integral,give_coupon_id,give_product_id,give_product_unique')->with(['giveProducts' => function ($query) { + $query->field('type,promotions_id, product_id,limit_num,unique')->with(['productInfo' => function ($q) { + $q->field('id,store_name'); + }, 'giveAttrValue']); + }, 'giveCoupon' => function ($query) { + $query->field('type,promotions_id, coupon_id,limit_num')->with(['coupon']); + }]); + }]); + if (!$info) { + throw new AdminException('数据不存在'); + } + $info = $info->toArray(); + if ($info['start_time']) + $start_time = $info['promotions_type'] == 1 ? date('Y-m-d H:i', (int)$info['start_time']) : date('Y-m-d', (int)$info['start_time']); + if ($info['stop_time']) + $stop_time = $info['promotions_type'] == 1 ? date('Y-m-d H:i', (int)$info['stop_time']) : date('Y-m-d', (int)$info['stop_time']); + if (isset($start_time) && isset($stop_time)) + $info['section_time'] = [$start_time, $stop_time]; + else + $info['section_time'] = []; + unset($info['start_time'], $info['stop_time']); + $info['is_label'] = $info['label_id'] ? 1 : 0; + if ($info['is_label']) { + $label_id = is_array($info['label_id']) ? $info['label_id'] : explode(',', $info['label_id']); + /** @var UserLabelServices $userLabelServices */ + $userLabelServices = app()->make(UserLabelServices::class); + $info['label_id'] = $userLabelServices->getLabelList(['ids' => $label_id], ['id', 'label_name']); + } else { + $info['label_id'] = []; + } + + $info['threshold'] = floatval($info['threshold']); + $info['discount'] = floatval($info['discount']); + $info['give_integral'] = intval($info['give_integral']); + $info['is_overlay'] = $info['overlay'] ? 1 : 0; + $info['promotions'] = $info['promotions'] ?? []; + $info['products'] = $info['products'] ?? []; + $info['giveCoupon'] = $info['giveCoupon'] ?? []; + $info['giveProducts'] = $info['giveProducts'] ?? []; + if ($info['products']) { + /** @var StoreProductLabelServices $storeProductLabelServices */ + $storeProductLabelServices = app()->make(StoreProductLabelServices::class); + $products = []; + foreach ($info['products'] as &$item) { + $product = is_object($item) ? $item->toArray() : $item; + $product = array_merge($product, $product['productInfo'] ?? []); + $product['store_label'] = ''; + if (isset($product['store_label_id']) && $product['store_label_id']) { + $storeLabelList = $storeProductLabelServices->getColumn([['relation_id', '=', 0], ['type', '=', 0], ['id', 'IN', $product['store_label_id']]], 'id,label_name'); + $product['store_label'] = $storeLabelList ? implode(',', array_column($storeLabelList, 'label_name')) : ''; + } + $unique = is_string($product['unique']) ? explode(',', $product['unique']) : $product['unique']; + foreach ($product['attrValue'] as $key => $value) { + if (!in_array($value['unique'], $unique)) { + unset($product['attrValue'][$key]); + } + } + $product['attrValue'] = array_merge($product['attrValue']); + unset($product['productInfo']); + $products[] = $product; + } + if ($products) { + $cateIds = implode(',', array_column($products, 'cate_id')); + /** @var StoreProductCategoryServices $categoryService */ + $categoryService = app()->make(StoreProductCategoryServices::class); + $cateList = $categoryService->getCateParentAndChildName($cateIds); + foreach ($products as $key => &$item) { + $item['cate_name'] = ''; + if (isset($item['cate_id']) && $item['cate_id']) { + $cate_ids = explode(',', $item['cate_id']); + $cate_name = $categoryService->getCateName($cate_ids, $cateList); + if ($cate_name) { + $item['cate_name'] = is_array($cate_name) ? implode(',', $cate_name) : ''; + } + } + foreach ($item['attrValue'] as $key => &$value) { + $value['store_label'] = $item['store_label'] ?? ''; + $value['cate_name'] = $item['cate_name'] ?? ''; + } + } + } + + unset($info['products']); + $info['products'] = $products; + } + $data = $this->promotionsData; + $data['giveCoupon'] = []; + $data['giveProducts'] = []; + $first = array_merge($data, array_intersect_key($info, $data)); + array_unshift($info['promotions'], $first); + + $info['promotions'] = $this->handelPromotions($info['promotions']); + $info['brand_id'] = isset($info['brands']) && $info['brands'] ? array_column($info['brands'], 'brand_id') : []; + $info['store_label_id'] = []; + if (isset($info['productLabels']) && $info['productLabels']) { + foreach ($info['productLabels'] as $label) { + if (isset($label['productLabelInfo']) && $label['productLabelInfo']) { + $info['store_label_id'][] = $label['productLabelInfo']; + } + } + } + //适用门店 + $info['stores'] = []; + if (isset($info['applicable_type']) && ($info['applicable_type'] == 1 || ($info['applicable_type'] == 2 && isset($info['applicable_store_id']) && $info['applicable_store_id']))) {//查询门店信息 + $where = ['is_del' => 0]; + if ($info['applicable_type'] == 2) { + $store_ids = is_array($info['applicable_store_id']) ? $info['applicable_store_id'] : explode(',', $info['applicable_store_id']); + $where['id'] = $store_ids; + } + $field = ['id', 'cate_id', 'name', 'phone', 'address', 'detailed_address', 'image', 'is_show', 'day_time', 'day_start', 'day_end']; + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeData = $storeServices->getStoreList($where, $field, '', '', 0, ['categoryName']); + $info['stores'] = $storeData['list'] ?? []; + } + unset($info['brands'], $info['productLabels']); + return $info; + } + + /** + * 处理阶梯优惠赠送商品、优惠券 + * @param array $promotions + * @return array + */ + public function handelPromotions(array $promotions) + { + if ($promotions) { + foreach ($promotions as &$p) { + $p['threshold'] = (float)$p['threshold']; + $p['discount'] = (float)$p['discount']; + if (isset($p['giveCoupon']) && $p['giveCoupon']) { + $coupons = []; + foreach ($p['giveCoupon'] as &$coupon) { + $coupon = is_object($coupon) ? $coupon->toArray() : $coupon; + $coupon = array_merge($coupon, $coupon['coupon'] ?? []); + unset($coupon['coupon']); + $coupons[] = $coupon; + } + unset($p['giveCoupon']); + $p['giveCoupon'] = $coupons; + } + if (isset($p['giveProducts']) && $p['giveProducts']) { + $products = []; + foreach ($p['giveProducts'] as &$product) { + $product = is_object($product) ? $product->toArray() : $product; + $product = array_merge($product, $product['productInfo'] ?? []); + $product = array_merge($product, $product['giveAttrValue'] ?? []); + unset($product['productInfo'], $product['giveAttrValue']); + $products[] = $product; + } + unset($p['giveProducts']); + $p['giveProducts'] = $products; + } + } + } + return $promotions; + } + + /** + * 保存促销活动 + * @param int $id + * @param array $data + * @return bool + */ + public function saveData(int $id, array $data) + { + if (!$data['section_time'] || count($data['section_time']) != 2) { + throw new AdminException('请选择活动时间'); + } + [$start_time, $end_time] = $data['section_time']; + if (strtotime($end_time) < time()) { + throw new AdminException('活动结束时间不能小于当前时间'); + } + if ($id) { + $info = $this->dao->get((int)$id); + if (!$info) { + throw new AdminException('数据不存在'); + } + } + $data['start_time'] = strtotime($start_time); + $data['stop_time'] = $data['promotions_type'] == 1 ? strtotime($end_time) : strtotime($end_time) + 86399; + $data['label_id'] = $data['label_id'] ? implode(',', $data['label_id']) : ''; + $data['overlay'] = $data['overlay'] ? implode(',', $data['overlay']) : ''; + $promotionsAuxiliaryData = []; + switch ($data['product_partake_type']) { + case 1://全部 + $data['product_id'] = []; + break; + case 2://指定ID参与 + case 3://指定ID不参与 + $promotionsAuxiliaryData = $productData = $data['product_id']; + $productIds = $productData ? array_column($productData, 'product_id') : []; + $data['product_id'] = $productIds ? implode(',', $productIds) : ''; + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $count = $storeProductServices->count(['is_show' => 1, 'is_del' => 0, 'id' => $productIds]); + $productCount = count(array_unique($productIds)); + if ($count != $productCount) { + throw new AdminException('选择商品中有已下架或移入回收站'); + } + break; + case 4://指定品牌 + $data['brand_id'] = array_unique($data['brand_id']); + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $brandCount = $storeBrandServices->count(['is_show' => 1, 'is_del' => 0, 'id' => $data['brand_id']]); + if (count(array_unique($data['brand_id'])) != $brandCount) { + throw new AdminException('选择商品品牌中有已下架或删除的'); + } + $promotionsAuxiliaryData['brand_id'] = $data['brand_id']; + break; + case 5://指定商品标签 + $data['store_label_id'] = array_unique($data['store_label_id']); + /** @var StoreProductLabelServices $storeProductLabelServices */ + $storeProductLabelServices = app()->make(StoreProductLabelServices::class); + $labelCount = $storeProductLabelServices->count(['id' => $data['store_label_id']]); + if (count(array_unique($data['store_label_id'])) != $labelCount) { + throw new ValidateException('选择商品标签中有已下架或删除的'); + } + $promotionsAuxiliaryData['store_label_id'] = $data['store_label_id']; + break; + default: + throw new ValidateException('暂不支持该类型商品'); + break; + } + + $promotions = $data['promotions']; + $threshold = -1; + foreach ($promotions as &$value) { + if ($threshold != -1 && $value['threshold'] <= $threshold) { + throw new AdminException('优惠门槛只能递增(例如二级必须大于一级优惠)'); + } + $threshold = $value['threshold']; + $value = $this->checkPromotions((int)$data['promotions_type'], $value); + $value['threshold_type'] = $data['threshold_type']; + } + [$title, $desc] = $this->getPromotionsDesc((int)$data['promotions_type'], (int)$data['promotions_cate'], $data['promotions_type'] == 1 ? array_merge($promotions, ['is_limit' => $data['is_limit'], 'limit_num' => $data['limit_num']]) : $promotions); + $data['title'] = $title; + $data['desc'] = implode(',', $desc); + + $first = array_shift($promotions); + $giveCoupon = $first['give_coupon_id'] ?? []; + $giveProduct = $first['give_product_id'] ?? []; + unset($first['give_coupon_id'], $first['give_product_id']); + $first['give_coupon_id'] = $giveCoupon ? implode(',', array_unique(array_column($giveCoupon, 'give_coupon_id'))) : ''; + $first['give_product_id'] = $giveProduct ? implode(',', array_unique(array_column($giveProduct, 'give_product_id'))) : ''; + $first['give_product_unique'] = $giveProduct ? implode(',', array_unique(array_column($giveProduct, 'unique'))) : ''; + $data = array_merge($data, $first); + + unset($data['section_time'], $data['promotions']); + $this->transaction(function () use ($id, $data, $promotions, $promotionsAuxiliaryData, $giveCoupon, $giveProduct) { + $time = time(); + $data['update_time'] = $time; + if ($id) { + $this->dao->update($id, $data); + //删除之前阶梯数据 + $this->dao->delete(['pid' => $id]); + } else { + $data['add_time'] = $time; + $res = $this->dao->save($data); + $id = $res->id; + } + /** @var StorePromotionsAuxiliaryServices $storePromotionsAuxiliaryServices */ + $storePromotionsAuxiliaryServices = app()->make(StorePromotionsAuxiliaryServices::class); + $storePromotionsAuxiliaryServices->savePromotionsRelation((int)$id, (int)$data['product_partake_type'], $promotionsAuxiliaryData, $giveCoupon, $giveProduct); + + if ($promotions) { + foreach ($promotions as $item) { + $giveCoupon = $item['give_coupon_id'] ?? []; + $giveProduct = $item['give_product_id'] ?? []; + unset($item['give_coupon_id'], $item['give_product_id']); + $item['give_coupon_id'] = $giveCoupon ? implode(',', array_unique(array_column($giveCoupon, 'give_coupon_id'))) : ''; + $item['give_product_id'] = $giveProduct ? implode(',', array_unique(array_column($giveProduct, 'give_product_id'))) : ''; + $item['give_product_unique'] = $giveProduct ? implode(',', array_unique(array_column($giveProduct, 'unique'))) : ''; + + $item['pid'] = $id; + $item['promotions_type'] = (int)$data['promotions_type']; + $item['promotions_cate'] = (int)$data['promotions_cate']; + $item['add_time'] = $time; + $res = $this->dao->save($item); + $storePromotionsAuxiliaryServices->savePromotionsRelation((int)$res->id, (int)$data['product_partake_type'], $promotionsAuxiliaryData, $giveCoupon, $giveProduct); + } + } + + }); + return true; + } + + /** + * 验证购买商品规格是否在优惠活动适用商品选择规格中 + * @param array $productIds + * @param array $cartList + * @param array $promotions + * @return array + */ + public function checkProductCanUsePromotions(array $productIds, array $cartList, array $promotions) + { + $ids = $uniques = []; + if (!$productIds || !$cartList || !$promotions) return [$ids, $uniques]; + $useProducts = $promotions['products'] ?? []; + if ($useProducts) { + $useProducts = array_combine(array_column($useProducts, 'product_id'), $useProducts); + } + foreach ($cartList as $cart) { + if (!in_array($cart['product_id'], $productIds)) continue; + $productUnique = $cart['product_attr_unique'] ?? ''; + $productInfo = $cart['productInfo'] ?? []; + $product_id = $cart['product_id'] ?? 0; + $useUniques = $useProducts[$product_id]['unique'] ?? []; + if (!$productUnique || !$productInfo) continue; + if ($productInfo['type'] == 1 && $productInfo['pid'] > 0) {//平台共享到门店商品 查询平台商品unique + $unique = $this->getProductUnique($productUnique, (int)$productInfo['id'], (int)$productInfo['pid']); + $productUnique = $unique ?: $productUnique; + $useUniques = $useProducts[$productInfo['pid']]['unique'] ?? []; + } + if (in_array($promotions['product_partake_type'], [2, 3])) { + if ($promotions['product_partake_type'] == 2) { + $uniques[$product_id] = $useUniques; + if (!in_array($productUnique, $useUniques)) { + continue; + } + } else { + $uniques[$product_id] = $useUniques; + if (in_array($productUnique, $useUniques)) { + continue; + } + } + } + $ids[] = $product_id; + } + return [$ids, $uniques]; + } + + /** + * 检查优惠活动限量 + * @param StoreOrderCartInfoServices $storeOrderCartInfoServices + * @param int $uid + * @param int $id + * @param array $productIds + * @param array $promotions + * @return array + */ + public function checkPromotionsLimit(StoreOrderCartInfoServices $storeOrderCartInfoServices, int $uid, int $id, array $productIds, array $promotions) + { + if (!$productIds) { + return []; + } + if ($uid && $promotions['promotions_type'] == 1 && $promotions['is_limit']) {//限时折扣 存在限量 + //获取包含子级获取ids + $ids = array_unique(array_merge([$id], array_column($promotions['promotions'] ?? [], 'id'))); + $data = []; + foreach ($productIds as $key => $product_id) { + if ($storeOrderCartInfoServices->count(['uid' => $uid, 'product_id' => $product_id, 'promotions_id' => $ids]) < $promotions['limit_num']) { + $data[] = $product_id; + } + } + return $data; + } + return $productIds; + } + + /** + * 计算几个商品总金额 并返回商品信息 + * @param int $promotions_type + * @param array $productIds + * @param array $cartList + * @param array $uniquesArr + * @param int $product_partake_type + * @param bool $isGive + * @return array + */ + protected function getPromotionsProductInfo(int $promotions_type, array $productIds, array $cartList, array $uniquesArr = [], int $product_partake_type = 1, bool $isGive = false) + { + $sumPrice = $sumCount = 0; + $p = []; + if (!$cartList || !$productIds) { + return [$sumPrice, $sumCount, $p]; + } + $productComputedArr = $this->getItem('productComputedArr', []); + $computedArr = $this->getItem('computedArr', []); + foreach ($cartList as $product) { + if (!in_array($product['product_id'], $productIds)) continue; + $cart_num = $product['cart_num'] ?? 1; + $unique = $product['product_attr_unique'] ?? ''; + $product_id = $product['product_id']; + $productInfo = $product['productInfo'] ?? []; + if (!$unique || !$product_id || !$productInfo) continue; + if (!$this->checkProductUnque($unique, $productInfo, $uniquesArr[$product_id] ?? [], $product_partake_type)) { + continue; + } + if ($isGive) { + $key = 'true_price'; + } else { + if ($promotions_type == 1) { + $key = 'price'; + } else { + if (isset($productComputedArr[$product_id]['typeArr']) && in_array(1, $productComputedArr[$product_id]['typeArr'])) { + $key = 'price'; + } else { + $key = 'true_price'; + } + } + } + if ($key == 'price') { + $price = isset($product['productInfo']['attrInfo']['price']) ? $product['productInfo']['attrInfo']['price'] : ($product['productInfo']['price'] ?? 0); + } else { + $price = $product['truePrice']; + } + $isOverlay = $productComputedArr[$product_id]['is_overlay'] ?? false; + if ($isOverlay && $computedArr) { + foreach ($computedArr as $key => $computedDetail) { + if (isset($computedDetail[$unique]['promotions_true_price'])) { + $price = bcsub((string)$price, (string)$computedDetail[$unique]['promotions_true_price'], 2); + } + } + } + $product['price'] = floatval($price) > 0 ? $price : 0; + $sumPrice = bcadd((string)$sumPrice, (string)bcmul((string)$price, (string)$cart_num, 2), 2); + if ($isGive && isset($product['coupon_price'])) { + $sumPrice = bcsub((string)$sumPrice, (string)$product['coupon_price'], 2); + } + $sumCount = bcadd((string)$sumCount, (string)$cart_num, 0); + + $p[] = $product; + } + return [$sumPrice, $sumCount, $p]; + } + + /** + * 验证是否叠加其他活动 + * @param int $promotions_type + * @param array $overlay + * @return bool + */ + public function checkOverlay(int $promotions_type, array $overlay) + { + $data = [1, 2, 3]; + return boolval(array_intersect($overlay, array_diff($data, [$promotions_type]))); + } + + /** + * 获取商品活动是否叠加计算 + * @param array $promotionsList + * @param array $productDetails + * @param array $promotionsDetail + * @return array[] + */ + public function getProductComputedPromotions(array $promotionsList, array $productDetails, array $promotionsDetail) + { + $productArr = []; + $promotionsArr = []; + $productComputedArr = []; + if (!$promotionsList) { + return [$productArr, $promotionsArr, $productComputedArr]; + } + //验证是否能叠加使用 + foreach ($productDetails as $product_id => $promotionsIds) { + $pIds = []; + $overlayPIds = []; + $unOverlayPIds = []; + $prevType = []; + $preOverlay = []; + $isOverlay = true; + foreach ($promotionsIds as $id) { + $promotions = $promotionsList[$id] ?? []; + if (!$promotions) continue; + $overlay = is_string($promotions['overlay']) ? explode(',', $promotions['overlay']) : $promotions['overlay']; + //同一个商品 同一类型活动取最新一个 + if (!in_array($promotions['promotions_type'], $prevType)) { + if (!$prevType) { + $overlayPIds[] = $unOverlayPIds[] = $id; + $prevType[] = $promotions['promotions_type']; + $preOverlay[$promotions['promotions_type']] = $overlay; + } else { + //有限时折扣 + if (isset($preOverlay[1]) && !$this->checkOverlay(1, $preOverlay[1])) { + if ($promotions['promotions_type'] == 4) { + $prevType[] = $promotions['promotions_type']; + $preOverlay[$promotions['promotions_type']] = $overlay; + $unOverlayPIds[] = $id; + } + } else { + if ($promotions['promotions_type'] == 4) { + $prevType[] = $promotions['promotions_type']; + $preOverlay[$promotions['promotions_type']] = $overlay; + $overlayPIds[] = $id; + $unOverlayPIds[] = $id; + } else { + foreach ($preOverlay as $key => $value) { + if (in_array($key, $overlay) && in_array($promotions['promotions_type'], $value)) { + $overlayPIds[] = $id; + } else { + $unOverlayPIds[] = $id; + } + } + } + $prevType[] = $promotions['promotions_type']; + $preOverlay[$promotions['promotions_type']] = $overlay; + } + } + } + } + $overlayPIds = array_unique($overlayPIds); + $unOverlayPIds = array_unique($unOverlayPIds); + if (count($overlayPIds) > 1) { + $isOverlay = true; + $pIds = $overlayPIds; + } else if (count($overlayPIds) == 1 && count($unOverlayPIds) == 1) { + $isOverlay = false; + $pIds = $overlayPIds; + } else { + $isOverlay = false; + $pIds = $unOverlayPIds; + } + + $productComputedArr[$product_id]['is_overlay'] = $pIds && $isOverlay; + $typeArr = []; + //重新整理商品关联ids数据 + foreach ($pIds as $id) { + $promotions = $promotionsList[$id] ?? []; + if ($promotions) { + $productArr[$product_id][] = $id; + $typeArr[] = $promotions['promotions_type']; + } + } + //存在限时折扣 不叠加 + if (count($typeArr) > 1 && in_array(1, $typeArr) && !$productComputedArr[$product_id]['is_overlay']) { + $typeArr = []; + $productArr[$product_id] = []; + foreach ($pIds as $id) { + $promotions = $promotionsList[$id] ?? []; + if ($promotions) { + if (in_array($promotions['promotions_type'], [1, 4])) { + $productArr[$product_id][] = $id; + $typeArr[] = $promotions['promotions_type']; + } + } + } + } + $productComputedArr[$product_id]['typeArr'] = $typeArr; + } + //重新整理活动关联商品ids数据 + foreach ($promotionsDetail as $promotions_id => $productIds) { + foreach ($productIds as $pid) { + $pIds = $productArr[$pid] ?? []; + if ($pIds && in_array($promotions_id, $pIds)) { + $promotionsArr[$promotions_id][] = $pid; + } + } + } + return [$productArr, $promotionsArr, $productComputedArr]; + } + + /** + * 组合使用的优惠活动详情 + * @param int $uid + * @param int $store_id + * @param array $usePromotionsIds + * @param array $promotionsArr + * @param array $computedArr + * @return array + */ + public function getUsePromotiosnInfo(int $uid, int $store_id, array $usePromotionsIds, array $promotionsArr, array $computedArr) + { + $usePromotions = []; + if (!$usePromotionsIds || !$promotionsArr || !$computedArr) { + return $usePromotions; + } + $giveCoupon = $giveProduct = $giveCartList = []; + $giveIntegral = 0; + foreach ($usePromotionsIds as $id) { + $promotionsInfo = $promotionsArr[$id] ?? []; + if (!$promotionsInfo) continue; + $details = $computedArr[$id] ?? []; + $promotionsInfo['details'] = $details; + $promotionsInfo = array_merge($promotionsInfo, $details['give'] ?? []); + $promotionsInfo['is_valid'] = $details['is_valid'] ?? 0; + $promotionsInfo['reach_threshold'] = $details['reach_threshold'] ?? 0; + $promotionsInfo['sum_promotions_price'] = $details['sum_promotions_price'] ?? 0; + $promotionsInfo['differ_threshold'] = $details['differ_threshold'] ?? 0;//下一级优惠差多少元|件 + $promotionsInfo['differ_price'] = $details['differ_price'] ?? 0;//下一级优惠金额 + $promotionsInfo['differ_discount'] = $details['differ_discount'] ?? 0;//下一级享受折扣 + $giveProductIds = $giveCart = []; + if (isset($promotionsInfo['give_product']) && $promotionsInfo['give_product']) { + $giveCart = $this->createGiveProductCart($uid, (int)$id, $promotionsInfo['give_product'], $promotionsInfo, $store_id); + if ($giveCart) { + $giveProductIds = array_column($giveProductIds, 'product_id'); + $giveCartList = array_merge($giveCartList, $giveCart); + } + } + $promotionsInfo['product_ids'] = $promotionsDetail[$id] ?? []; + $promotionsInfo['product_ids'] = array_merge($promotionsInfo['product_ids'], $giveProductIds); + + $promotionsInfo['give_product'] = $giveProductIds; + + $giveCoupon = array_merge($giveCoupon, $promotionsInfo['give_coupon'] ?? []); + $giveProduct = array_merge($giveProduct, $promotionsInfo['give_product'] ?? []); + $giveIntegral = bcadd((string)$giveIntegral, (string)($promotionsInfo['give_integral'] ?? 0), 0); + $usePromotions[] = $promotionsInfo; + } + return [$usePromotions, $giveIntegral, $giveCoupon, $giveCartList]; + } + + /** + * 根据门店|平台商品 unique 获取另一端商品unique + * @param string $unique + * @param int $product_id + * @param int $type + * @return false|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductUnique(string $unique, int $product_id, int $new_product_id, int $type = 0) + { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $product_id, 'type' => $type], 'suk'); + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $new_product_id, 'type' => $type], 'unique'); + return $productUnique ?: ''; + } + + /** + * 验证购物车商品规格是否在活动中 + * @param string $unique + * @param array $productInfo + * @param array $uniques + * @param int $product_partake_type + * @return bool + */ + public function checkProductUnque(string $unique, array $productInfo, array $uniques, int $product_partake_type = 1) + { + if (!$unique) { + return false; + } + if ((in_array($product_partake_type, [2, 3]) && !$uniques) || !is_array($uniques)) { + return false; + } + if ($productInfo['type'] == 1 && $productInfo['pid'] > 0) {//平台共享到门店商品 查询平台商品unique + $productUnique = $this->getProductUnique($unique, (int)$productInfo['id'], (int)($productInfo['pid'] ?? 0)); + $unique = $productUnique ?: $unique; + } + switch ($product_partake_type) { + case 1: + case 4: + case 5: + break; + case 2: + if (!$uniques) { + return true; + } + if (!in_array($unique, $uniques)) { + return false; + } + break; + case 3: + if (!$uniques) { + return true; + } + if (in_array($unique, $uniques)) { + return false; + } + break; + default: + return false; + break; + } + return true; + } + + + /** + * 计算商品优惠价格 + * @param int $uid + * @param array $cartList + * @param int $store_id + * @param int $couponId + * @param bool $isCart + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function computedPromotions(int $uid, array $cartList, int $store_id = 0, int $couponId = 0, bool $isCart = false) + { + $giveIntegral = $couponPrice = 0; + $giveCoupon = $giveCartList = $usePromotions = $useCounpon = []; + if ($cartList) { + $productIds = array_column($cartList, 'product_id'); + // $productArr = array_combine($productIds, $cartList); + $with = ['products' => function ($query) { + $query->field('promotions_id,product_id,is_all,unique'); + }, 'giveProducts' => function ($query) { + $query->field('type,promotions_id,product_id,limit_num,surplus_num,unique')->with(['productInfo' => function ($query) { + $query->field('id,store_name'); + }]); + }, 'giveCoupon' => function ($query) { + $query->field('type,promotions_id,coupon_id,limit_num,surplus_num')->with(['coupon' => function ($query) { + $query->field('id,type,coupon_type,coupon_title,coupon_price,use_min_price,remain_count,is_permanent'); + }]); + }, 'promotions' => function ($query) { + $query->field('id,pid,promotions_type,promotions_cate,threshold_type,threshold,discount_type,n_piece_n_discount,discount,give_integral,give_coupon_id,give_product_id,give_product_unique')->with(['giveProducts' => function ($query) { + $query->field('type,promotions_id,product_id,limit_num,surplus_num,unique')->with(['productInfo' => function ($query) { + $query->field('id,store_name'); + }]); + }, 'giveCoupon' => function ($query) { + $query->field('type,promotions_id, coupon_id,limit_num,surplus_num')->with(['coupon' => function ($query) { + $query->field('id,type,coupon_type,coupon_title,coupon_price,use_min_price,remain_count,is_permanent'); + }]); + }]); + }]; + //获取购物车商品所有活动 + [$promotionsArr, $productDetails, $promotionsDetail] = $this->getProductsPromotionsDetail($productIds, '*', $with, [1, 2, 3, 4], '', $store_id); + $computedArr = []; + $usePromotionsIds = []; + if ($promotionsArr) { + $promotionsArr = array_combine(array_column($promotionsArr, 'id'), $promotionsArr); + //获取商品活动是否叠加计算 + [$productDetails, $promotionsDetail, $productComputedArr] = $this->getProductComputedPromotions($promotionsArr, $productDetails, $promotionsDetail); + //计算优惠金额 + $computedArr = $this->doComputeV1($uid, $cartList, $promotionsDetail, $promotionsArr, $productComputedArr); + foreach ($cartList as &$cart) { + $sum_promotions_true_price = 0; + $product_id = (int)($cart['product_id'] ?? 0); + $unique = $cart['product_attr_unique'] ?? ''; + $productInfo = $cart['productInfo'] ?? []; + $promotionsIds = $productDetails[$product_id] ?? []; + $cart['promotions_id'] = []; + if (!$promotionsIds || !$unique || !$productInfo) { + continue; + } + $price = isset($cart['productInfo']['attrInfo']['price']) ? $cart['productInfo']['attrInfo']['price'] : ($cart['productInfo']['price'] ?? 0); + //叠加 + $typeArr = $productComputedArr[$product_id]['typeArr'] ?? []; + $isOverly = isset($productComputedArr[$product_id]['is_overlay']) && $productComputedArr[$product_id]['is_overlay']; + foreach ($promotionsIds as $promotions_id) { + $promotionsInfo = $promotionsArr[$promotions_id] ?? []; + $trueDetail = $computedArr[$promotions_id] ?? []; + if (!$promotionsInfo || !$trueDetail) continue; + if (!$this->checkProductUnque($unique, $productInfo, $trueDetail['uniques'][$product_id] ?? [], (int)$promotionsInfo['product_partake_type'])) { + continue; + } + $trueArr = $trueDetail[$unique] ?? []; + if (!isset($trueDetail['is_valid']) || $trueDetail['is_valid'] == 0) { + if ($isCart) {//购物车不满足也展示 + $cart['promotions_id'][] = $promotions_id; + } + } else { + //活动叠加商品 单件总计优惠金额 + if ($isOverly) { + $cart['promotions_id'][] = $promotions_id; + $sum_promotions_true_price = bcadd((string)$sum_promotions_true_price, (string)($trueArr['promotions_true_price'] ?? 0), 2); + } else { //不叠加取最优惠 + if ($isCart) { + $cart['promotions_id'][] = $promotions_id; + } + if ($sum_promotions_true_price < ($trueArr['promotions_true_price'] ?? 0)) { + $sum_promotions_true_price = $trueArr['promotions_true_price'] ?? 0; + if (!$isCart) { + $cart['promotions_id'] = [$promotions_id]; + } + } + } + } + } + if ($sum_promotions_true_price) { + //是否有限时折扣 + if (in_array(1, $typeArr)) { + $true_price = (float)bcsub((string)$price, (string)$sum_promotions_true_price, 2); + if ($true_price < 0) { + $true_price = 0; + } + //比较与用户等级、svip优惠后金额 + if ($true_price && $cart['truePrice'] > $true_price) { + $cart['truePrice'] = $true_price; + $cart['promotions_true_price'] = $sum_promotions_true_price; + $cart['price_type'] = 'promotions'; + $cart['vip_truePrice'] = 0; + } else { //使用了用户等级、svip价格 去掉优惠活动关联 + $cart['promotions_id'] = []; + } + } else {//svip 用户等级价格上继续优惠 + $true_price = (float)bcsub((string)$cart['truePrice'], (string)$sum_promotions_true_price, 2); + $cart['truePrice'] = $true_price > 0 ? $true_price : 0; + $cart['promotions_true_price'] = $sum_promotions_true_price; + $cart['price_type'] = 'promotions'; + } + } + $usePromotionsIds = array_unique(array_merge($usePromotionsIds, $cart['promotions_id'])); + //排出不叠加 去最优的其他优惠活动金额 + foreach ($promotionsIds as $promotions_id) { + //使用了优惠会跳过 + if (in_array($promotions_id, $cart['promotions_id'])) continue; + $trueDetail = $computedArr[$promotions_id] ?? []; + $trueArr = $trueDetail[$unique] ?? []; + if (!$trueDetail || !$trueArr) { + continue; + } + $true_price = $trueArr['promotions_true_price'] ?? 0; + $sum_promotions_price = bcsub((string)($trueDetail['sum_promotions_price'] ?? '0'), (string)bcmul((string)$true_price, (string)$cart['cart_num'], 2), 2); + $computedArr[$promotions_id]['sum_promotions_price'] = $sum_promotions_price >= 0 ? $sum_promotions_price : 0; + //这个商品的没有使用优惠活动 + unset($computedArr[$promotions_id][$unique]); + } + } + } + //使用优惠券 + $coupon_id = 0; + if ($uid) { + if ($usePromotionsIds) [$usePromotions, $giveIntegral, $giveCoupon, $giveCartList] = $this->getUsePromotiosnInfo($uid, $store_id, $usePromotionsIds, $promotionsArr, $computedArr); + if ($couponId) { + [$useCounpon, $couponPrice] = $this->useCoupon($couponId, $uid, $cartList, $usePromotions, $store_id); + $coupon_id = $couponId; + } else { + //获取最优优惠券 + if ($isCart) { + /** @var StoreCouponIssueServices $couponServices */ + $couponServices = app()->make(StoreCouponIssueServices::class); + $useCounpon = $couponServices->getCanUseCoupon($uid, $cartList, $usePromotions, $store_id); + $couponPrice = $useCounpon['true_coupon_price'] ?? 0; + $coupon_id = $useCounpon['used']['id'] ?? 0; + } + } + //计算每一件商品优惠券优惠金额 + if ($coupon_id && $useCounpon && $couponPrice) { + /** @var StoreOrderComputedServices $computedServices */ + $computedServices = app()->make(StoreOrderComputedServices::class); + $payPrice = $computedServices->getOrderSumPrice($cartList); + if ($couponPrice > $payPrice) { + $couponPrice = $payPrice; + } + /** @var StoreOrderCreateServices $createServices */ + $createServices = app()->make(StoreOrderCreateServices::class); + $priceData = ['coupon_id' => $coupon_id, 'coupon_price' => $couponPrice]; + $cartList = $createServices->computeOrderProductCoupon($cartList, $priceData, $usePromotions, $store_id); + } + } + + //获取赠送积分、优惠券、商品 + [$cartList, $computedArr, $useGivePromotionsIds] = $this->getPromotionsGive($uid, $cartList, $computedArr, $promotionsDetail, $productDetails, $promotionsArr, $isCart); + $usePromotionsIds = array_unique(array_merge($usePromotionsIds, $useGivePromotionsIds)); + //整合返回数据 + if ($usePromotionsIds) [$usePromotions, $giveIntegral, $giveCoupon, $giveCartList] = $this->getUsePromotiosnInfo($uid, $store_id, $usePromotionsIds, $promotionsArr, $computedArr); + } + return [$cartList, $couponPrice, $useCounpon, $usePromotions, $giveIntegral, $giveCoupon, $giveCartList]; + } + + /** + * 实际计算商品优惠金额 + * @param int $uid + * @param array $cartList + * @param array $promotionsDetail + * @param array $promotionsArr + * @param array $productComputedArr + * @return array + */ + public function doComputeV1(int $uid, array $cartList, array $promotionsDetail, array $promotionsArr, array $productComputedArr) + { + $computedArr = []; + if (!$cartList || !$promotionsDetail) { + return $computedArr; + } + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + foreach ($promotionsDetail as $promotions_id => $productIds) { + $promotions = $promotionsArr[$promotions_id] ?? []; + $productCount = count($productIds); + if (!$promotions || !$productCount || $promotions['promotions_type'] == 4) continue; + //验证商品规格是否满足活动 + [$productIds, $uniques] = $this->checkProductCanUsePromotions($productIds, $cartList, $promotions); + if (!$productIds) { + continue; + } + //验证限量 + if ($uid) { + $productIds = $this->checkPromotionsLimit($storeOrderCartInfoServices, $uid, (int)$promotions_id, $productIds, $promotions); + if (!$productIds) { + continue; + } + } + $promotions_type = (int)$promotions['promotions_type'] ?? 1; + $this->setItem('productComputedArr', $productComputedArr)->setItem('computedArr', $computedArr); + [$sumPrice, $sumCount, $promotionsProductArr] = $this->getPromotionsProductInfo($promotions_type, $productIds, $cartList, $uniques, (int)$promotions['product_partake_type']); + $this->reset(); + $compute_price = $sum_promotions_true_price = $sum_promotions_price = 0; + $data = ['is_valid' => 0, 'reach_threshold' => 0, 'differ_threshold' => 0, 'promotions_type' => $promotions['promotions_type'], 'sum_promotions_price' => 0, 'product_id' => [], 'uniques' => $uniques]; + switch ($promotions['promotions_type']) { + case 1://限制折扣 + $p = $promotions['promotions'][0] ?? []; + if ($p) { + $promotionsDiscount = $this->computedDiscount($p['discount'], 100, 2); + $sum_promotions_price = 0; + + if ($promotions['is_limit']) {//是否限量 + $limit_num = $promotions['limit_num']; + if ($limit_num <= 0) break; + $sumCount = 0; + foreach ($promotionsProductArr as $product) { + $product_id = $product['product_id']; + $unique = $product['product_attr_unique']; + $price = $product['price']; + $cartNum = $newCartNum = $product['cart_num'] ?? 1; + if ($sumCount >= $limit_num) { + break; + } + if (bcadd((string)$sumCount, (string)$cartNum) > $limit_num) {//加上下一个商品数量 大于限购数量 + $newCartNum = bcsub((string)$limit_num, (string)$sumCount); + $sumCount = $limit_num; + } + $promotions_true_price = (float)bcmul((string)$price, (string)$promotionsDiscount, 2); + if ($promotions_true_price < 0.01) {//实际优惠小于0.01 就不计算优惠 + $promotions_true_price = 0; + } + //部分享受折扣 + if ($cartNum != $newCartNum) { + $promotions_true_price = bcdiv((string)bcmul((string)$promotions_true_price, (string)$newCartNum, 4), (string)$cartNum, 2); + } + $data[$unique] = ['product_id' => $product_id, 'uniqid' => $unique, 'price' => $price, 'promotions_true_price' => $promotions_true_price]; + $sum_promotions_price = bcadd((string)$sum_promotions_price, (string)bcmul((string)$promotions_true_price, (string)$cartNum, 2), 2); + } + + } else { + foreach ($promotionsProductArr as $product) { + $product_id = $product['product_id']; + $unique = $product['product_attr_unique']; + $price = $product['price']; + $promotions_true_price = (float)bcmul((string)$price, (string)$promotionsDiscount, 2); + if ($promotions_true_price < 0.01) {//实际优惠小于0.01 就不计算优惠 + $promotions_true_price = 0; + } + $data[$unique] = ['product_id' => $product_id, 'uniqid' => $unique, 'price' => $price, 'promotions_true_price' => $promotions_true_price]; + $sum_promotions_price = bcadd((string)$sum_promotions_price, (string)bcmul((string)$promotions_true_price, (string)($product['cart_num'] ?? 1), 2), 2); + } + } + $data['is_valid'] = 1; + $data['sum_promotions_price'] = $sum_promotions_price; + } + break; + case 2://n件n折 + $p = $promotions['promotions'][0] ?? []; + if ($p) { + $promotionsDiscount = $this->computedDiscount($p['discount'], 100, 2); + if ($sumCount >= $p['threshold']) {//满足 + $useProductArr = $promotionsProductArr; + //商品价格升序 + array_multisort(array_column($promotionsProductArr, 'price'), SORT_ASC, $promotionsProductArr); + $minPrice = $promotionsProductArr[0]['price'] ?? 0; + //算出n件实际优惠金额 + $sum_promotions_price = (float)bcmul((string)$minPrice, (string)$promotionsDiscount, 2); + if ($sum_promotions_price < 0.01) { + $sum_promotions_price = 0; + } + if ($sum_promotions_price) { + $useCount = $productCount; + $useSumPrice = $sumPrice; + $count = count($useProductArr); + $compute_price = 0; + array_multisort(array_column($useProductArr, 'cart_num'), SORT_DESC, $useProductArr); + foreach ($useProductArr as $value) { + $product_id = $value['product_id']; + $unique = $value['product_attr_unique']; + $price = $value['price']; + if ($count > 1) { + $promotions_true_price = bcmul((string)bcdiv((string)$price, (string)$useSumPrice, 4), (string)$sum_promotions_price, 2); + $compute_price = bcadd((string)$compute_price, (string)bcmul((string)$promotions_true_price, (string)$value['cart_num'], 2), 2); + } else { + $one_promotions_sum_price = bcsub((string)$sum_promotions_price, (string)$compute_price, 2); + $promotions_true_price = bcdiv((string)$one_promotions_sum_price, (string)$value['cart_num'], 2); + } + $data[$unique] = ['product_id' => $product_id, 'uniqid' => $unique, 'price' => $price, 'promotions_true_price' => $promotions_true_price]; + $count--; + } + + } + $data['is_valid'] = 1; + $data['reach_threshold'] = $p['threshold']; + $data['sum_promotions_price'] = $sum_promotions_price; + } else { + $data['differ_discount'] = $p['discount']; + $data['differ_threshold'] = bcsub((string)$p['threshold'], (string)$sumCount, 0); + } + } + break; + case 3://满减折 + if ($promotions['promotions_cate'] == 1) {//阶梯 享受最高 + $valid = $invalid = []; + foreach ($promotions['promotions'] as $key => $p) { + if (($p['threshold_type'] == 1 ? $sumPrice : $sumCount) >= $p['threshold']) { + $valid = $p; + } else { + $invalid = $p; + break; + } + } + if ($valid) { + if ($valid['discount_type'] == 1) {//免减 + $sum_promotions_price = (string)$valid['discount']; + } else {//折扣 + $promotionsDiscount = $this->computedDiscount($valid['discount'], 100, 2); + $sum_promotions_price = 0; + } + array_multisort(array_column($promotionsProductArr, 'cart_num'), SORT_DESC, $promotionsProductArr); + foreach ($promotionsProductArr as $product) { + $product_id = $product['product_id']; + $unique = $product['product_attr_unique']; + $price = $product['price']; + if ($valid['discount_type'] == 1) { + if ($productCount > 1) { + $promotions_true_price = bcmul((string)bcdiv((string)$price, (string)$sumPrice, 4), (string)$sum_promotions_price, 2); + $compute_price = bcadd((string)$compute_price, (string)bcmul((string)$promotions_true_price, (string)$product['cart_num'], 2), 2); + } else { + $one_promotions_sum_price = bcsub((string)$sum_promotions_price, (string)$compute_price, 2); + $promotions_true_price = bcdiv((string)$one_promotions_sum_price, (string)$product['cart_num'], 2); + } + } else { + $promotions_true_price = (float)bcmul((string)$price, (string)$promotionsDiscount, 2); + if ($promotions_true_price < 0.01) {//实际优惠小于0.01 就不计算优惠 + $promotions_true_price = 0; + } + $sum_promotions_price = bcadd((string)$sum_promotions_price, (string)bcmul((string)$promotions_true_price, (string)($product['cart_num'] ?? 1), 2), 2); + } + $data[$unique] = ['product_id' => $product_id, 'uniqid' => $unique, 'price' => $price, 'promotions_true_price' => $promotions_true_price]; + $productCount--; + } + $data['is_valid'] = 1; + $data['reach_threshold'] = $valid['threshold']; + $data['sum_promotions_price'] = $sum_promotions_price; + } + if ($invalid) { + if ($valid) $data['is_valid'] = 2; + if ($invalid['discount_type'] == 1) {//免减 + $data['differ_price'] = $invalid['discount']; + } else { + $data['differ_discount'] = $invalid['discount']; + } + $data['differ_threshold'] = bcsub((string)$invalid['threshold'], (string)($invalid['threshold_type'] == 1 ? $sumPrice : $sumCount), 0); + } + } else {//循环 + $p = $promotions['promotions'][0] ?? []; + $validCount = floor(bcdiv((string)($p['threshold_type'] == 1 ? $sumPrice : $sumCount), (string)$p['threshold'], 2)); + if ($validCount) {//满足次数 + $promotionsDiscount = $this->computedDiscount($p['discount'], 100, 2); + $sum_promotions_price = 0; + $useProductArr = $promotionsProductArr; + if ($p['discount_type'] == 2 && $p['threshold_type'] == 2) {//满n件 打n折 + // $suprplusDiscount = bcsub((string)$productCount, bcmul((string)$validCount, (string)$p['threshold'], 0), 0); + //商品价格升序 + array_multisort(array_column($promotionsProductArr, 'price'), SORT_ASC, $promotionsProductArr); + $minPrice = $promotionsProductArr[0]['price'] ?? 0; + //算出n件实际优惠金额 + $sum_promotions_price = (float)bcmul((string)$minPrice, (string)$promotionsDiscount, 2); + if ($sum_promotions_price < 0.01) { + $sum_promotions_price = 0; + } + $sum_promotions_price = bcmul((string)$validCount, (string)$sum_promotions_price, 2); + } else { + if ($p['discount_type'] == 1) {//免减(n元、n件) + $sum_promotions_price = bcmul((string)$validCount, (string)$p['discount'], 2); + } else {//打折 + if ($p['threshold_type'] == 1) {//满n元 + $sum_promotions_price = bcmul((string)bcmul((string)$validCount, (string)$p['threshold'], 2), (string)$promotionsDiscount, 2); + } + } + } + $useCount = 0; + array_multisort(array_column($useProductArr, 'cart_num'), SORT_DESC, $useProductArr); + foreach ($useProductArr as $product) { + $price = $product['price']; + $product_id = $product['product_id']; + $unique = $product['product_attr_unique']; + // if ($p['discount_type'] == 2 && $p['threshold_type'] == 2) { //满n件打n折 + // if ($p['threshold'] > $useCount) { + // $promotions_true_price = (float)bcmul((string)$price, (string)$promotionsDiscount, 2); + // if ($promotions_true_price < 0.01) {//实际优惠小于0.01 就不计算优惠 + // $promotions_true_price = 0; + // } + // } else { + // $promotions_true_price = 0; + // } + // $sum_promotions_price = bcadd((string)$sum_promotions_price, (string)$promotions_true_price, 2); + // $useCount = (int)bcadd((string)$useCount, (string)$product['cart_num']); + // } else { + if ($productCount > 1) { + $promotions_true_price = bcmul((string)bcdiv((string)$price, (string)$sumPrice, 4), (string)$sum_promotions_price, 2); + $compute_price = bcadd((string)$compute_price, (string)bcmul((string)$promotions_true_price, (string)$product['cart_num'], 2), 2); + } else { + $one_promotions_sum_price = bcsub((string)$sum_promotions_price, (string)$compute_price, 2); + $promotions_true_price = bcdiv((string)$one_promotions_sum_price, (string)$product['cart_num'], 2); + } + // } + $data[$unique] = ['product_id' => $product_id, 'uniqid' => $unique, 'price' => $price, 'promotions_true_price' => $promotions_true_price]; + $productCount--; + } + $data['is_valid'] = 1; + $data['reach_threshold'] = $p['threshold']; + $data['sum_promotions_price'] = $sum_promotions_price; + } else { + if ($p['discount_type'] == 1) {//免减 + $data['differ_price'] = $p['discount']; + } else { + $data['differ_discount'] = $p['discount']; + } + $data['differ_threshold'] = bcsub((string)$p['threshold'], (string)($p['threshold_type'] == 1 ? $sumPrice : $sumCount), 0); + } + } + break; + case 4://满送 + + break; + default: + break; + } + $data['give'] = ['give_integral' => 0, 'give_coupon' => [], 'give_product' => []]; + $computedArr[$promotions_id] = $data; + } + return $computedArr; + } + + /** + * 使用优惠卷 + * @param int $couponId + * @param int $uid + * @param array $cartInfo + * @param array $promotions + * @param int $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function useCoupon(int $couponId, int $uid, array $cartInfo, array $promotions, int $store_id = 0) + { + //使用优惠劵 + $couponPrice = 0; + $couponInfo = []; + if ($couponId && $cartInfo) { + /** @var StoreCouponUserServices $couponServices */ + $couponServices = app()->make(StoreCouponUserServices::class); + $couponInfo = $couponServices->getOne([['id', '=', $couponId], ['uid', '=', $uid], ['is_fail', '=', 0], ['status', '=', 0], ['start_time', '<=', time()], ['end_time', '>=', time()]], '*', ['issue']); + if (!$couponInfo) { + throw new ValidateException('选择的优惠劵无效!'); + } + $type = $couponInfo['coupon_applicable_type'] ?? 0; + $flag = false; + $price = 0; + $count = 0; + $promotionsList = []; + if ($promotions) { + $promotionsList = array_combine(array_column($promotions, 'id'), $promotions); + } + //验证是否适用门店 + if ($store_id && isset($couponInfo['applicable_type']) && isset($couponInfo['applicable_store_id'])) { + $applicable_store_id = is_array($couponInfo['applicable_store_id']) ? $couponInfo['applicable_store_id'] : explode(',', $couponInfo['applicable_store_id']); + //活动不适用该门店 + if ($couponInfo['applicable_type'] == 0 || ($couponInfo['applicable_type'] == 2 && !in_array($store_id, $applicable_store_id))) { + throw new ValidateException('优惠劵暂不支持该门店使用!'); + } + } + $isOverlay = function ($cart) use ($promotionsList) { + $productInfo = $cart['productInfo'] ?? []; + if (!$productInfo) { + return false; + } + //门店独立商品 不使用优惠券 + $isBranchProduct = isset($productInfo['type']) && isset($productInfo['pid']) && $productInfo['type'] == 1 && !$productInfo['pid']; + if ($isBranchProduct) { + return false; + } + if (isset($cart['promotions_id']) && $cart['promotions_id']) { + foreach ($cart['promotions_id'] as $key => $promotions_id) { + $promotions = $promotionsList[$promotions_id] ?? []; + if ($promotions && $promotions['promotions_type'] != 4) { + $overlay = is_string($promotions['overlay']) ? explode(',', $promotions['overlay']) : $promotions['overlay']; + if (!in_array(5, $overlay)) { + return false; + } + } + } + } + return true; + }; + switch ($type) { + case 0: + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + $price = bcadd((string)$price, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + break; + case 1://品类券 + /** @var StoreProductCategoryServices $storeCategoryServices */ + $storeCategoryServices = app()->make(StoreProductCategoryServices::class); + $cateGorys = $storeCategoryServices->getAllById((int)$couponInfo['category_id']); + if ($cateGorys) { + $cateIds = array_column($cateGorys, 'id'); + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + if (isset($cart['productInfo']['cate_id']) && array_intersect(explode(',', $cart['productInfo']['cate_id']), $cateIds)) { + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + } + break; + case 2: + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + $product_id = isset($cart['productInfo']['pid']) && $cart['productInfo']['pid'] ? $cart['productInfo']['pid'] : ($cart['product_id'] ?? 0); + if ($product_id && in_array($product_id, explode(',', $couponInfo['product_id']))) { + $price = bcadd((string)$price, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + case 3: + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $brands = $storeBrandServices->getAllById((int)$couponInfo['brand_id']); + if ($brands) { + $brandIds = array_column($brands, 'id'); + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + if (isset($cart['productInfo']['brand_id']) && in_array($cart['productInfo']['brand_id'], $brandIds)) { + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + } + break; + } + if ($count && $couponInfo['use_min_price'] <= $price) { + $flag = true; + } + if (!$flag) { + return [[], 0]; +// throw new ValidateException('不满足优惠劵的使用条件!'); + } + //满减券 + if ($couponInfo['coupon_type'] == 1) { + $couponPrice = $couponInfo['coupon_price'] > $price ? $price : $couponInfo['coupon_price']; + } else { + if ($couponInfo['coupon_price'] <= 0) {//0折 + $couponPrice = $price; + } else if ($couponInfo['coupon_price'] >= 100) { + $couponPrice = 0; + } else { + $truePrice = (float)bcmul((string)$price, bcdiv((string)$couponInfo['coupon_price'], '100', 2), 2); + $couponPrice = (float)bcsub((string)$price, (string)$truePrice, 2); + } + } + } + return [$couponInfo, $couponPrice]; + } + + /** + * 实际获取优惠活动赠送赠品 + * @param int $uid + * @param array $cartList + * @param array $computedArr + * @param array $promotionsDetail + * @param array $productDetails + * @param array $promotionsArr + * @param bool $isCart + * @return array + */ + public function getPromotionsGive(int $uid, array $cartList, array $computedArr, array $promotionsDetail, array $productDetails, array $promotionsArr, bool $isCart = false) + { + $usePromotionsIds = []; + if (!$cartList || !$promotionsDetail) { + return [$cartList, $computedArr, $usePromotionsIds]; + } + foreach ($promotionsDetail as $promotions_id => $productIds) { + $promotions = $promotionsArr[$promotions_id] ?? []; + $productCount = count($productIds); + if (!$promotions || !$productCount || $promotions['promotions_type'] != 4) continue; + //验证商品规格是否满足活动 + [$productIds, $uniques] = $this->checkProductCanUsePromotions($productIds, $cartList, $promotions); + if (!$productIds) { + continue; + } + $promotions_type = (int)$promotions['promotions_type'] ?? 1; + [$sumPrice, $sumCount, $promotionsProductArr] = $this->getPromotionsProductInfo($promotions_type, $productIds, $cartList, $uniques, (int)$promotions['product_partake_type'], true); + + $give_product_id = []; + $give_coupon_ids = []; + $give_integral = 0; + $data = ['is_valid' => 0, 'reach_threshold' => 0, 'differ_threshold' => 0, 'promotions_type' => $promotions['promotions_type'], 'sum_promotions_price' => 0, 'product_id' => []]; + if ($promotions['promotions_cate'] == 1) {//阶梯 + $valid = $invalid = []; + foreach ($promotions['promotions'] as $key => $p) { + if (($p['threshold_type'] == 1 ? $sumPrice : $sumCount) >= $p['threshold']) { + $valid = $p; + } else { + $invalid = $p; + break; + } + } + if ($valid) { + if ($valid['give_integral']) { + $give_integral = bcadd((string)$give_integral, (string)$valid['give_integral'], 0); + } + if ($valid['give_coupon_id']) { + $coupon_ids = is_string($valid['give_coupon_id']) ? explode(',', $valid['give_coupon_id']) : $valid['give_coupon_id']; + $give_coupon_ids = array_merge($give_coupon_ids, $coupon_ids); + } + if ($valid['giveProducts']) { + foreach ($valid['giveProducts'] as $value) { + if (isset($give_product_id[$value['unique']])) { + $give_product_id[$value['unique']]['cart_num'] = $give_product_id[$value['unique']]['cart_num'] + 1; + } else { + $give_product_id[$value['unique']] = ['promotions_id' => $value['promotions_id'], 'unique' => $value['unique'], 'product_id' => $value['product_id'], 'cart_num' => 1]; + } + } + } + $data['is_valid'] = 1; + $data['reach_threshold'] = $valid['threshold']; + } + if ($invalid) { + if ($valid) $data['is_valid'] = 2; + $data['differ_threshold'] = bcsub((string)$invalid['threshold'], (string)($invalid['threshold_type'] == 1 ? $sumPrice : $sumCount), 0); + } + } else {//循环 + $p = $promotions['promotions'][0] ?? []; + $validCount = floor(bcdiv((string)($p['threshold_type'] == 1 ? $sumPrice : $sumCount), (string)$p['threshold'], 2)); + if ($validCount) {//满足次数 + if ($p['give_integral']) { + $give_integral = bcadd((string)$give_integral, (string)bcmul((string)$validCount, (string)$p['give_integral'], 0), 0); + } + if ($p['give_coupon_id']) { + $coupon_ids = is_string($p['give_coupon_id']) ? explode(',', $p['give_coupon_id']) : $p['give_coupon_id']; + $give_coupon_ids = array_merge($give_coupon_ids, $coupon_ids); + } + if ($p['giveProducts']) { + foreach ($p['giveProducts'] as $value) { + if (isset($give_product_id[$value['unique']])) { + $give_product_id[$value['unique']]['cart_num'] = bcadd((string)$give_product_id[$value['unique']]['cart_num'], (string)$validCount); + } else { + $give_product_id[$value['unique']] = ['promotions_id' => $value['promotions_id'], 'unique' => $value['unique'], 'product_id' => $value['product_id'], 'cart_num' => $validCount]; + } + } + } + $data['is_valid'] = 1; + $data['reach_threshold'] = $p['threshold']; + } else { + $data['differ_threshold'] = bcsub((string)$p['threshold'], (string)($p['threshold_type'] == 1 ? $sumPrice : $sumCount), 0); + } + } + $ids = []; + //验证优惠券限量 + if ($give_coupon_ids) { + foreach ($give_coupon_ids as $give_coupon_id) { + foreach ($promotions['promotions'] as $value) { + $giveCoupon = $value['giveCoupon'] ?? []; + if ($giveCoupon) $giveCoupon = array_combine(array_column($giveCoupon, 'coupon_id'), $giveCoupon); + if ($giveCoupon && ($giveCoupon[$give_coupon_id]['surplus_num'] ?? 0) >= 1) { + $ids[] = $give_coupon_id; + break; + } + } + } + } + $data['give'] = ['give_integral' => $give_integral, 'give_coupon' => $ids, 'give_product' => $give_product_id]; + $computedArr[$promotions_id] = $data; + if (!$isCart && $data['is_valid'] > 0) { + $usePromotionsIds[] = $promotions_id; + } elseif ($isCart) { + $usePromotionsIds[] = $promotions_id; + } + } + foreach ($cartList as &$cart) { + $product_id = $cart['product_id'] ?? 0; + $promotionsIds = $productDetails[$product_id] ?? []; + if (!$promotionsIds) { + continue; + } + $useIds = array_intersect($promotionsIds, $usePromotionsIds); + $cart['promotions_id'] = array_unique(array_merge($cart['promotions_id'] ?? [], $useIds)); + } + return [$cartList, $computedArr, $usePromotionsIds]; + } + + /** + * 生成赠送商品购物车数据 + * @param int $uid + * @param int $promotions_id + * @param array $giveProduct + * @param array promotions + * @param int $store_id + * @return array + */ + public function createGiveProductCart(int $uid, int $promotions_id, array $giveProduct, array $promotions, int $store_id = 0) + { + $cart = []; + if ($giveProduct) { + /** @var StoreOrderCreateServices $storeOrderCreateService */ + $storeOrderCreateService = app()->make(StoreOrderCreateServices::class); + /** @var StoreCartServices $storeCartServices */ + $storeCartServices = app()->make(StoreCartServices::class); + $promotionsArr = $promotions['promotions'] ?? []; + foreach ($giveProduct as $unique => $give) { + $product_id = $give['product_id'] ?? 0; + $cart_num = $give['cart_num'] ?? 1; + if (!$product_id) { + continue; + } + try { + [$attrInfo, $product_attr_unique, $bargainPriceMin, $cart_num, $productInfo] = $storeCartServices->checkProductStock($uid, (int)$product_id, (int)$cart_num, $store_id, $unique, true); + } catch (\Throwable $e) { + continue; + } + $is_limit = false; + foreach ($promotionsArr as $key => $value) { + $giveProducts = $value['giveProducts'] ?? []; + if ($giveProducts) $giveProducts = array_combine(array_column($giveProducts, 'product_id'), $giveProducts); + if ($giveProducts && $cart_num <= ($giveProducts[$product_id]['surplus_num'] ?? 0)) { + $is_limit = true; + break; + } + } + if (!$is_limit) { + continue; + } + + $key = $storeOrderCreateService->getNewOrderId((string)$uid); + $info['id'] = $key; + $info['type'] = 0; + $info['product_type'] = $productInfo['product_type']; + $info['promotions_id'] = [$give['promotions_id'] ?? $promotions_id]; + $info['activity_id'] = 0; + $info['discount_product_id'] = 0; + $info['product_id'] = $product_id; + $info['is_gift'] = 1; + $info['is_valid'] = 1; + $info['product_attr_unique'] = $product_attr_unique; + $info['cart_num'] = $cart_num; + $info['productInfo'] = $productInfo ? $productInfo->toArray() : []; + $info['productInfo']['express_delivery'] = false; + $info['productInfo']['store_mention'] = false; + $info['productInfo']['store_delivery'] = false; + if (isset($info['productInfo']['delivery_type'])) { + $info['productInfo']['delivery_type'] = is_string($info['productInfo']['delivery_type']) ? explode(',', $info['productInfo']['delivery_type']) : $info['productInfo']['delivery_type']; + if (in_array(1, $info['productInfo']['delivery_type'])) { + $info['productInfo']['express_delivery'] = true; + } + if (in_array(2, $info['productInfo']['delivery_type'])) { + $info['productInfo']['store_mention'] = true; + } + if (in_array(3, $info['productInfo']['delivery_type'])) { + $info['productInfo']['store_delivery'] = true; + } + } + $info['productInfo']['attrInfo'] = $attrInfo->toArray(); + $info['attrStatus'] = (bool)$info['productInfo']['attrInfo']; + $info['sum_price'] = $info['productInfo']['attrInfo']['price'] ?? $info['productInfo']['price'] ?? 0; + $info['truePrice'] = 0; + $info['vip_truePrice'] = 0; + $info['trueStock'] = $info['productInfo']['attrInfo']['stock'] ?? 0; + $info['costPrice'] = $info['productInfo']['attrInfo']['cost'] ?? 0; + $info['limit_num'] = $giveProducts[$product_id]['limit_num'] ?? 0; + + $cart[] = $info; + } + } + return $cart; + } + + /** + * 根据类型获取商品列表 + * @param int $type + * @param int $uid + * @param int $promotions_id + * @param int $store_id + * @param int $staff_id + * @param int $tourist_uid + * @return array|array[]|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTypeList(int $type, int $uid, int $promotions_id = 0) + { + $store_id = (int)$this->getItem('store_id', 0); + $staff_id = (int)$this->getItem('staff_id', 0); + $tourist_uid = (int)$this->getItem('tourist_uid', 0); + $store_name = $this->getItem('store_name', ''); + $ids = $this->getItem('ids', []); + $not_ids = $this->getItem('not_ids', []); + $where = []; + $where['type'] = 1; + $where['store_id'] = 0; + $where['pid'] = 0; + $where['is_del'] = 0; + $where['status'] = 1; + $where['promotionsTime'] = true; + $where['promotions_type'] = $type; + if ($promotions_id) { + $where['id'] = $promotions_id; + } + $product_where = []; + $product_where['store_name'] = $store_name; + $product_where['pids'] = $ids; + $product_where['not_pids'] = $not_ids; + //门店不展示卡密商品 + $product_where['product_type'] = [0, 2, 4]; + //存在一个全部商品折扣优惠活动 直接返回商品 + + if ($ids && !$this->dao->count($where + ['product_partake_type' => 1])) { + //正选并集 + $mergeIds = function ($promotions) { + $data = []; + foreach ($promotions as $item) { + $productIds = is_string($item['product_id']) ? explode(',', $item['product_id']) : $item['product_id']; + $data = array_merge($data, $productIds); + } + return $data; + }; + $promotions = $this->getList($where + ['product_partake_type' => 2], 'id,product_id'); + $product_where['pids'] = $promotions ? $mergeIds($promotions) : []; + $notPromotions = $this->getList($where + ['product_partake_type' => 3], 'id,product_id'); + //反选交集 + /** @var StorePromotionsAuxiliaryServices $auxiliaryService */ + $auxiliaryService = app()->make(StorePromotionsAuxiliaryServices::class); + $intersectIds = function ($promotions) use ($auxiliaryService) { + $data = []; + foreach ($promotions as $item) { + $productIds = is_string($item['product_id']) ? explode(',', $item['product_id']) : $item['product_id']; + if (!$productIds) { + continue; + } + $productIds = $auxiliaryService->getColumn(['promotions_id' => $item['id'], 'type' => 1, 'is_all' => 1, 'product_id' => $productIds], 'product_id', '', true); + if (!$productIds) { + continue; + } + if ($data) { + $data = array_intersect($data, $productIds); + } else { + $data = $productIds; + } + } + return $data; + }; + $product_where['not_pids'] = array_merge($product_where['not_pids'], $notPromotions ? $intersectIds($notPromotions) : []); + } + $list = []; + $count = 0; + if ($product_where['pids'] && $this->dao->count($where)) { + $product_where['pids'] = array_merge(array_diff($product_where['pids'], $product_where['not_pids'])); + unset($product_where['not_pids']); + /** @var StoreBranchProductServices $branchProductServices */ + $branchProductServices = app()->make(StoreBranchProductServices::class); + return $branchProductServices->getCashierProductListV2($product_where, $store_id, $uid, $staff_id, $tourist_uid); + } + return compact('list', 'count'); + } + + /** + * 获取凑单商品ids + * @param int $promotions_id + * @return array + */ + public function collectOrderProduct(int $promotions_id) + { + $product_where = []; + $promotions = $this->dao->get($promotions_id, ['*']); + if ($promotions) { + $promotions = $promotions->toArray(); + /** @var StoreProductRelationServices $storeProductRelationServices */ + $storeProductRelationServices = app()->make(StoreProductRelationServices::class); + /** @var StorePromotionsAuxiliaryServices $promotionsAuxiliaryServices */ + $promotionsAuxiliaryServices = app()->make(StorePromotionsAuxiliaryServices::class); + $promotionsAuxiliaryData = $promotionsAuxiliaryServices->getPromotionsAuxiliaryCache($promotions_id); + switch ($promotions['product_partake_type']) { + case 1://所有商品 + break; + case 2://选中商品参与 + $product_ids = $promotionsAuxiliaryData; + $product_where['ids'] = $product_ids; + break; + case 3: + $ids = is_string($promotions['product_id']) ? explode(',', $promotions['product_id']) : $promotions['product_id']; + if ($ids) {//商品全部规格 不参与 才不显示该商品 + /** @var StorePromotionsAuxiliaryServices $auxiliaryService */ + $auxiliaryService = app()->make(StorePromotionsAuxiliaryServices::class); + $ids = $auxiliaryService->getColumn(['promotions_id' => $promotions['id'], 'type' => 1, 'is_all' => 1, 'product_id' => $ids], 'product_id', '', true); + } + $product_where['not_ids'] = $ids; + break; + case 4://品牌 + $product_ids = $promotionsAuxiliaryData ? $storeProductRelationServices->getIdsByWhere(['type' => 2, 'relation_id' => $promotionsAuxiliaryData]) : []; + $product_where['ids'] = $product_ids; + break; + case 5://商品标签 + $product_ids = $promotionsAuxiliaryData ? $storeProductRelationServices->getIdsByWhere(['type' => 3, 'relation_id' => $promotionsAuxiliaryData]) : []; + $product_where['ids'] = $product_ids; + break; + } + } + return $product_where; + } + +} diff --git a/app/services/activity/seckill/StoreSeckillServices.php b/app/services/activity/seckill/StoreSeckillServices.php new file mode 100644 index 0000000..a809e39 --- /dev/null +++ b/app/services/activity/seckill/StoreSeckillServices.php @@ -0,0 +1,909 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\seckill; + + +use app\Request; +use app\services\activity\StoreActivityServices; +use app\services\BaseServices; +use app\dao\activity\seckill\StoreSeckillDao; +use app\services\diy\DiyServices; +use app\services\order\StoreOrderServices; +use app\services\other\QrcodeServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\ensure\StoreProductEnsureServices; +use app\services\product\label\StoreProductLabelServices; +use app\services\product\product\StoreDescriptionServices; +use app\services\store\SystemStoreServices; +use app\services\user\UserRelationServices; +use app\services\product\product\StoreProductReplyServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrResultServices; +use app\services\product\sku\StoreProductAttrServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\jobs\product\ProductLogJob; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use crmeb\services\SystemConfigService; +use think\exception\ValidateException; + +/** + * Class StoreSeckillServices + * @package app\services\activity\seckill + * @method getSeckillIdsArray(array $ids, array $field) + * @mixin StoreSeckillDao + */ +class StoreSeckillServices extends BaseServices +{ + const OPOXMWTJ = 'k8kkOJ'; + + /** + * 商品活动类型 + */ + const TYPE = 1; + + /** + * StoreSeckillServices constructor. + * @param StoreSeckillDao $dao + */ + public function __construct(StoreSeckillDao $dao) + { + $this->dao = $dao; + } + + /** + * @param array $productIds + * @return array + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/14 + */ + public function getSeckillIdsArrayCache(array $productIds) + { + $list = $this->dao->cacheList(); + if (!$list) { + return $this->dao->getSeckillIdsArray($productIds, ['id', 'time_id', 'product_id']); + } else { + $seckill = []; + $time = time(); + foreach ($list as $item) { + if ($item['is_del'] == 0 && $item['status'] == 1 && $item['start_time'] <= $time && $item['stop_time'] >= ($time - 86400) && in_array($item['product_id'], $productIds)) { + $seckill[] = [ + 'id' => $item['id'], + 'time_id' => $item['time_id'], + 'product_id' => $item['product_id'], + ]; + } + } + return $seckill; + } + } + + public function getCount(array $where) + { + $this->dao->count($where); + } + + /** + * 秒杀是否存在 + * @param int $id + * @param string $field + * @return array|int|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSeckillCount(int $id = 0, string $field = 'time_id') + { + $where = []; + $where[] = ['is_del', '=', 0]; + $where[] = ['status', '=', 1];$config = []; + $currentHour = date('Hi'); + /** @var StoreSeckillTimeServices $storeSeckillTimeServices */ + $storeSeckillTimeServices = app()->make(StoreSeckillTimeServices::class); + if ($id) { + $time = time(); + $where[] = ['id', '=', $id]; + $where[] = ['start_time', '<=', $time]; + $where[] = ['stop_time', '>=', $time - 86400]; + $seckill_one = $this->dao->getOne($where, $field); + if (!$seckill_one) { + throw new ValidateException('活动已结束'); + } + $time_id = $seckill_one['time_id'] ?? []; + if ($time_id) { + $time_id = is_string($time_id) ? explode(',', $time_id) : $time_id; + $timeList = $storeSeckillTimeServices->getList(['id' => $time_id, 'status' => 1]); + foreach ($timeList as $value) { + $value['start_time'] = $start = str_replace(':', '', $value['start_time']); + $value['end_time'] = $end = str_replace(':', '', $value['end_time']); + if ($currentHour >= $start && $currentHour < $end) { + $config = $value; + break; + } + } + } + if (!$config) { + throw new ValidateException('活动已结束'); + } + //获取秒杀商品状态 + if ($config['start_time'] <= $currentHour && $config['end_time'] > $currentHour) { + return $seckill_one; + } else if ($config['start_time'] > $currentHour) { + throw new ValidateException('活动未开始'); + } else { + throw new ValidateException('活动已结束'); + } + } else { + $timeList = $storeSeckillTimeServices->getList(['status' => 1]); + foreach ($timeList as $value) { + $start = str_replace(':', '', $value['start_time']); + $end = str_replace(':', '', $value['end_time']); + if ($currentHour >= $start && $currentHour < $end) { + $config = $value; + break; + } + } + if (!$config) return 0; + + //获取秒杀商品状态 + $start = substr_replace($config['start_time'], ':', 2, 0); + $end = substr_replace($config['end_time'], ':', 2, 0); + + $startTime = strtotime(date('Y-m-d') . ' '. $start); + $stopTime = strtotime(date('Y-m-d') . ' '. $end); + + $where[] = ['start_time', '<', $startTime]; + $where[] = ['stop_time', '>', $stopTime]; + return $this->dao->getCount($where); + } + } + + + /** + * 保存数据 + * @param int $id + * @param array $data + */ + public function saveData(int $id, array $data) + { + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $productInfo = $storeProductServices->getOne(['is_show' => 1, 'is_del' => 0, 'is_verify' => 1, 'id' => $data['product_id']]); + if (!$productInfo) { + throw new AdminException('原商品已下架或移入回收站'); + } + if ($productInfo['is_vip_product'] || $productInfo['is_presale_product']) { + throw new AdminException('该商品是预售或svip专享'); + } + $data['product_type'] = $productInfo['product_type']; + $data['type'] = $productInfo['type'] ?? 0; + $data['relation_id'] = $productInfo['relation_id'] ?? 0; + $custom_form = $productInfo['custom_form'] ?? []; + $data['custom_form'] = is_array($custom_form) ? json_encode($custom_form) : $custom_form; + $data['system_form_id'] = $productInfo['system_form_id'] ?? 0; + $store_label_id = $productInfo['store_label_id'] ?? []; + $data['store_label_id'] = is_array($store_label_id) ? implode(',', $store_label_id) : $store_label_id; + $ensure_id = $productInfo['ensure_id'] ?? []; + $data['ensure_id'] = is_array($ensure_id) ? implode(',', $ensure_id) : $ensure_id; + $specs = $productInfo['specs'] ?? []; + $data['specs'] = is_array($specs) ? json_encode($specs) : $specs; + if (in_array($data['product_type'], [1, 2, 3])) { + $data['freight'] = 2; + $data['temp_id'] = 0; + $data['postage'] = 0; + } else { + if ($data['freight'] == 1) { + $data['temp_id'] = 0; + $data['postage'] = 0; + } elseif ($data['freight'] == 2) { + $data['temp_id'] = 0; + } elseif ($data['freight'] == 3) { + $data['postage'] = 0; + } + if ($data['freight'] == 2 && !$data['postage']) { + throw new AdminException('请设置运费金额'); + } + if ($data['freight'] == 3 && !$data['temp_id']) { + throw new AdminException('请选择运费模版'); + } + } + $description = $data['description']; + $detail = $data['attrs']; + $items = $data['items']; + $data['start_time'] = strtotime($data['section_time'][0]); + $data['stop_time'] = strtotime($data['section_time'][1]); + $data['image'] = $data['images'][0] ?? ''; + $data['images'] = json_encode($data['images']); + $data['price'] = min(array_column($detail, 'price')); + $data['ot_price'] = min(array_column($detail, 'ot_price')); + $data['quota'] = $data['quota_show'] = array_sum(array_column($detail, 'quota')); + if ($data['quota'] > $productInfo['stock']) { + throw new ValidateException('限量不能超过商品库存'); + } + $data['stock'] = array_sum(array_column($detail, 'stock')); + unset($data['section_time'], $data['description'], $data['attrs'], $data['items']); + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + $id = $this->transaction(function () use ($id, $data, $description, $detail, $items, $storeDescriptionServices, $storeProductAttrServices) { + if ($id) { + $res = $this->dao->update($id, $data); + if (!$res) throw new AdminException('修改失败'); + } else { + $data['add_time'] = time(); + $res = $this->dao->save($data); + if (!$res) throw new AdminException('添加失败'); + $id = (int)$res->id; + } + $storeDescriptionServices->saveDescription((int)$id, $description, 1); + $skuList = $storeProductAttrServices->validateProductAttr($items, $detail, (int)$id, 1); + $valueGroup = $storeProductAttrServices->saveProductAttr($skuList, (int)$id, 1); + + $res = true; + foreach ($valueGroup as $item) { + $res = $res && CacheService::setStock($item['unique'], (int)$item['quota_show']); + } + if (!$res) { + throw new AdminException('占用库存失败'); + } + return $id; + }); + $this->dao->cacheTag()->clear(); + //保存 + $seckill = $this->dao->get($id, ['*'], ['descriptions', 'activity' => function ($query) { + $query->field('id,start_day,end_day,time_id'); + }]); + $this->dao->cacheUpdate($seckill->toArray()); + CacheService::redisHandler('product_attr')->clear(); + } + + /** + * 获取列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function systemPage(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit, ['activityName']); + $count = $this->dao->count($where); + foreach ($list as &$item) { + $item['store_name'] = $item['title']; + if ($item['status']) { + if ($item['start_time'] > time()) + $item['start_name'] = '未开始'; + else if (bcadd($item['stop_time'], '86400') < time()) + $item['start_name'] = '已结束'; + else if (bcadd($item['stop_time'], '86400') > time() && $item['start_time'] < time()) { + $item['start_name'] = '进行中'; + } + } else $item['start_name'] = '已结束'; + $end_time = $item['stop_time'] ? (date('Y-m-d', (int)$item['stop_time']) . ' 23:59:59') : ''; + $item['_stop_time'] = $end_time; + $item['stop_status'] = $item['stop_time'] + 86400 < time() ? 1 : 0; + } + return compact('list', 'count'); + } + + /** + * 获取秒杀详情 + * @param int $id + * @return array|\think\Model|null + */ + public function getInfo(int $id) + { + $info = $this->dao->get($id); + if ($info) { + if ($info['start_time']) + $start_time = date('Y-m-d', (int)$info['start_time']); + + if ($info['stop_time']) + $stop_time = date('Y-m-d', (int)$info['stop_time']); + if (isset($start_time) && isset($stop_time)) + $info['section_time'] = [$start_time, $stop_time]; + else + $info['section_time'] = []; + unset($info['start_time'], $info['stop_time']); + $info['give_integral'] = intval($info['give_integral']); + $info['price'] = floatval($info['price']); + $info['ot_price'] = floatval($info['ot_price']); + $info['postage'] = floatval($info['postage']); + $info['cost'] = floatval($info['cost']); + $info['weight'] = floatval($info['weight']); + $info['volume'] = floatval($info['volume']); + if (!$info['delivery_type']) { + $info['delivery_type'] = [1]; + } + if ($info['postage']) { + $info['freight'] = 2; + } elseif ($info['temp_id']) { + $info['freight'] = 3; + } else { + $info['freight'] = 1; + } + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + $info['description'] = $storeDescriptionServices->getDescription(['product_id' => $id, 'type' => 1]); + $info['attrs'] = $this->attrList($id, $info['product_id']); + //适用门店 + $info['stores'] = []; + if (isset($info['applicable_type']) && ($info['applicable_type'] == 1 || ($info['applicable_type'] == 2 && isset($info['applicable_store_id']) && $info['applicable_store_id']))) {//查询门店信息 + $where = ['is_del' => 0]; + if ($info['applicable_type'] == 2) { + $store_ids = is_array($info['applicable_store_id']) ? $info['applicable_store_id'] : explode(',', $info['applicable_store_id']); + $where['id'] = $store_ids; + } + $field = ['id', 'cate_id', 'name', 'phone', 'address', 'detailed_address', 'image', 'is_show', 'day_time', 'day_start', 'day_end']; + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeData = $storeServices->getStoreList($where, $field, '', '', 0, ['categoryName']); + $info['stores'] = $storeData['list'] ?? []; + } + } + return $info; + } + + /** + * 获取规格 + * @param int $id + * @param int $pid + * @return mixed + */ + public function attrList(int $id, int $pid) + { + /** @var StoreProductAttrResultServices $storeProductAttrResultServices */ + $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class); + $seckillResult = $storeProductAttrResultServices->value(['product_id' => $id, 'type' => 1], 'result'); + $items = json_decode($seckillResult, true)['attr']; + $productAttr = $this->getAttr($items, $pid, 0); + $seckillAttr = $this->getAttr($items, $id, 1); + foreach ($productAttr as $pk => $pv) { + foreach ($seckillAttr as &$sv) { + if ($pv['detail'] == $sv['detail']) { + $productAttr[$pk] = $sv; + } + } + $productAttr[$pk]['detail'] = json_decode($productAttr[$pk]['detail']); + } + $attrs['items'] = $items; + $attrs['value'] = $productAttr; + foreach ($items as $key => $item) { + $header[] = ['title' => $item['value'], 'key' => 'value' . ($key + 1), 'align' => 'center', 'minWidth' => 80]; + } + $header[] = ['title' => '图片', 'slot' => 'pic', 'align' => 'center', 'minWidth' => 120]; + $header[] = ['title' => '秒杀价', 'key' => 'price', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '成本价', 'key' => 'cost', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '原价', 'key' => 'ot_price', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '库存', 'key' => 'stock', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '限量', 'key' => 'quota', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '重量(KG)', 'key' => 'weight', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '体积(m³)', 'key' => 'volume', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '商品条形码', 'key' => 'bar_code', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '商品编号', 'key' => 'code', 'align' => 'center', 'minWidth' => 80]; + $attrs['header'] = $header; + return $attrs; + } + + /** + * 获取规格 + * @param $attr + * @param $id + * @param $type + * @return array + */ + public function getAttr($attr, $id, $type) + { + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $value = attr_format($attr)[1]; + $valueNew = []; + $count = 0; + foreach ($value as $key => $item) { + $detail = $item['detail']; +// sort($item['detail'], SORT_STRING); + $suk = implode(',', $item['detail']); + $sukValue = $storeProductAttrValueServices->getSkuArray(['product_id' => $id, 'type' => $type, 'suk' => $suk], 'bar_code,code,cost,price,ot_price,stock,image as pic,weight,volume,brokerage,brokerage_two,quota,quota_show', 'suk'); + if (count($sukValue)) { + foreach (array_values($detail) as $k => $v) { + $valueNew[$count]['value' . ($k + 1)] = $v; + } + $valueNew[$count]['detail'] = json_encode($detail); + $valueNew[$count]['pic'] = $sukValue[$suk]['pic'] ?? ''; + $valueNew[$count]['price'] = $sukValue[$suk]['price'] ? floatval($sukValue[$suk]['price']) : 0; + $valueNew[$count]['cost'] = $sukValue[$suk]['cost'] ? floatval($sukValue[$suk]['cost']) : 0; + $valueNew[$count]['ot_price'] = isset($sukValue[$suk]['ot_price']) ? floatval($sukValue[$suk]['ot_price']) : 0; + $valueNew[$count]['stock'] = $sukValue[$suk]['stock'] ? intval($sukValue[$suk]['stock']) : 0; +// $valueNew[$count]['quota'] = $sukValue[$suk]['quota'] ? intval($sukValue[$suk]['quota']) : 0; + $valueNew[$count]['quota'] = isset($sukValue[$suk]['quota_show']) && $sukValue[$suk]['quota_show'] ? intval($sukValue[$suk]['quota_show']) : 0; + $valueNew[$count]['code'] = $sukValue[$suk]['code'] ?? ''; + $valueNew[$count]['bar_code'] = $sukValue[$suk]['bar_code'] ?? ''; + $valueNew[$count]['weight'] = $sukValue[$suk]['weight'] ? floatval($sukValue[$suk]['weight']) : 0; + $valueNew[$count]['volume'] = $sukValue[$suk]['volume'] ? floatval($sukValue[$suk]['volume']) : 0; + $valueNew[$count]['brokerage'] = $sukValue[$suk]['brokerage'] ? floatval($sukValue[$suk]['brokerage']) : 0; + $valueNew[$count]['brokerage_two'] = $sukValue[$suk]['brokerage_two'] ? floatval($sukValue[$suk]['brokerage_two']) : 0; + $valueNew[$count]['_checked'] = $type != 0; + $count++; + } + } + return $valueNew; + } + + /** + * 获取某个时间段的秒杀列表 + * @param int $time + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getListByTime(int $time, array $ids = [], bool $isStore = false) + { + [$page, $limit] = $this->getPageValue(); + /** @var StoreActivityServices $storeActivityServices */ + $storeActivityServices = app()->make(StoreActivityServices::class); + $activityList = $storeActivityServices->getList(['time_id' => $time, 'type' => 1, 'status' => 1, 'is_del' => 0, 'activityTime' => true], 'id,image'); + $seckillInfo = []; + if ($activityList) { + $activityIds = array_column($activityList, 'id'); + $seckillInfo = $this->dao->getListByTime($activityIds, $ids, $page, $limit, $isStore); + if (count($seckillInfo)) { + $activityList = array_combine($activityIds, $activityList); + foreach ($seckillInfo as $key => &$item) { + if ($item['quota'] > 0) { + $percent = (float)sprintf("%.1f", (($item['quota_show'] - $item['quota']) / $item['quota_show'] * 100)); + $item['percent'] = $percent; + $item['stock'] = $item['quota']; + } else { + $item['percent'] = 100; + $item['stock'] = 0; + } + $item['price'] = floatval($item['price']); + $item['ot_price'] = floatval($item['ot_price']); + $item['discount_num'] = $item['ot_price'] ? (float)bcmul(bcdiv((string)$item['price'], (string)$item['ot_price'], 4), '10', 1) : 10; + $item['activity_image'] = $activityList[$item['activity_id']]['image'] ?? ''; + } + } + } + + return $seckillInfo; + } + + /** + * 获取某个时间段的秒杀商品数量 + * @param int $time + * @param array $ids + * @param array $not_ids + * @param bool $isStore + * @return int + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCountByTime(int $time, array $ids = [], array $not_ids = [], bool $isStore = false) + { + /** @var StoreActivityServices $storeActivityServices */ + $storeActivityServices = app()->make(StoreActivityServices::class); + $activityList = $storeActivityServices->getList(['time_id' => $time, 'type' => 1, 'status' => 1, 'is_del' => 0, 'activityTime' => true], 'id'); + $count = 0; + if ($activityList) { + $activityIds = array_column($activityList, 'id'); + $count = $this->dao->getTimeCount($activityIds, $ids, $not_ids, $isStore); + } + return $count; + } + + /** + * 获取秒杀详情 + * @param Request $request + * @param int $id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function seckillDetail(Request $request, int $id) + { + $uid = (int)$request->uid(); + //读取秒杀商品缓存信息 + $storeInfo = $this->dao->cacheRemember($id, function () use ($id) { + $storeInfo = $this->dao->getOne(['id' => $id], '*', ['descriptions']); + if (!$storeInfo) { + throw new ValidateException('商品不存在'); + } else { + $storeInfo = $storeInfo->toArray(); + } + return $storeInfo; + }); + $storeInfo['activity'] = []; + if ($storeInfo['activity_id']) { + $activityServices = app()->make(StoreActivityServices::class); + $storeInfo['activity'] = $activityServices->getInfo((int)$storeInfo['activity_id'], ['id', 'start_day', 'end_day', 'time_id']); + } + /** @var DiyServices $diyServices */ + $diyServices = app()->make(DiyServices::class); + $infoDiy = $diyServices->getProductDetailDiy(); + //diy控制参数 + if (!isset($infoDiy['is_specs']) || !$infoDiy['is_specs']) { + $storeInfo['specs'] = []; + } + $configData = SystemConfigService::more(['site_url', 'routine_contact_type', 'site_name', 'share_qrcode', 'store_self_mention', 'store_func_status', 'product_poster_title']); + $siteUrl = $configData['site_url'] ?? ''; + $storeInfo['image'] = set_file_url($storeInfo['image'], $siteUrl); + $storeInfo['image_base'] = set_file_url($storeInfo['image'], $siteUrl); + + //品牌名称 + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $productInfo = $storeProductServices->getCacheProductInfo((int)$storeInfo['product_id']); + $storeInfo['brand_name'] = $storeProductServices->productIdByBrandName($storeInfo['product_id'], $productInfo); + $delivery_type = $storeInfo['delivery_type'] ?? $productInfo['delivery_type']; + $storeInfo['delivery_type'] = is_string($delivery_type) ? explode(',', $delivery_type) : $delivery_type; + + /** + * 判断配送方式 + */ + $storeInfo['delivery_type'] = $storeProductServices->getDeliveryType((int)$storeInfo['type'], (int)$storeInfo['type'], (int)$storeInfo['relation_id'], $storeInfo['delivery_type']); + $storeInfo['total'] = $productInfo['sales'] + $productInfo['ficti']; + $storeInfo['store_label'] = $storeInfo['ensure'] = []; + if ($storeInfo['store_label_id']) { + /** @var StoreProductLabelServices $storeProductLabelServices */ + $storeProductLabelServices = app()->make(StoreProductLabelServices::class); + $storeInfo['store_label'] = $storeProductLabelServices->getLabelCache($storeInfo['store_label_id'], ['id', 'label_name']); + } + if ($storeInfo['ensure_id'] && isset($infoDiy['is_ensure']) && $infoDiy['is_ensure']) { + /** @var StoreProductEnsureServices $storeProductEnsureServices */ + $storeProductEnsureServices = app()->make(StoreProductEnsureServices::class); + $storeInfo['ensure'] = $storeProductEnsureServices->getEnsurCache($storeInfo['ensure_id'], ['id', 'name', 'image', 'desc']); + } + + /** @var QrcodeServices $qrcodeService */ + $qrcodeService = app()->make(QrcodeServices::class); + $time = $request->param('time', ''); + $status = $request->param('status', ''); + $time_id = (int)$request->param('time_id', ''); + if (($configData['share_qrcode'] ?? 0) && request()->isWechat()) { + $storeInfo['code_base'] = $qrcodeService->getTemporaryQrcode('seckill-' . $id . '-' . $time . '-' . $status, $uid)->url; + } else { + $storeInfo['code_base'] = $qrcodeService->getWechatQrcodePath($id . '_product_seckill_detail_wap.jpg', '/pages/activity/goods_seckill_details/index?id=' . $id . '&time=' . $time . '&status=' . $status); + } + + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $data['buy_num'] = $storeOrderServices->getBuyCount($uid, 1, $id); + + /** @var UserRelationServices $userRelationServices */ + $userRelationServices = app()->make(UserRelationServices::class); + $storeInfo['userCollect'] = $userRelationServices->isProductRelation(['uid' => $uid, 'relation_id' => $storeInfo['product_id'], 'type' => 'collect', 'category' => UserRelationServices::CATEGORY_PRODUCT]); + $storeInfo['userLike'] = 0; + + $storeInfo['uid'] = $uid; + if ($storeInfo['quota'] > 0) { + $percent = (float)sprintf("%.1f", ($storeInfo['quota_show'] - $storeInfo['quota']) / $storeInfo['quota_show'] * 100); + $storeInfo['percent'] = $percent; + $storeInfo['stock'] = $storeInfo['quota']; + } else { + $storeInfo['percent'] = 100; + $storeInfo['stock'] = 0; + } + $storeInfo['last_time'] = 0; + $time_id = $time_id ?: ($storeInfo['activity']['time_id'] ?? []); + if ($time_id) { + $time_id = is_string($time_id) ? explode(',', $time_id) : $time_id; + /** @var StoreSeckillTimeServices $storeSeckillTimeServices */ + $storeSeckillTimeServices = app()->make(StoreSeckillTimeServices::class); + $timeList = $storeSeckillTimeServices->getList(['id' => $time_id, 'status' => 1]); + $config = []; + $today = date('Y-m-d'); + $currentHour = date('Hi'); + if (count($timeList) <= 1) { + $config = $timeList[0] ?? []; + } else { + foreach ($timeList as $value) { + $start = str_replace(':', '', $value['start_time']); + $end = str_replace(':', '', $value['end_time']); + if ($currentHour >= $start && $currentHour < $end) { + $config = $value; + break; + } + } + } + + //获取秒杀商品状态 + if ($storeInfo['status'] == 1) { + if ($config) {//正在进行中的 + $start = str_replace(':', '', $config['start_time']); + $end = str_replace(':', '', $config['end_time']); + if ($start <= $currentHour && $end > $currentHour) { + $storeInfo['status'] = 1; + $storeInfo['last_time'] = strtotime($today. ' '. $config['end_time']); + } else if ($start > $currentHour) { + $storeInfo['status'] = 2; + } else { + $storeInfo['status'] = 0; + } + } else { + $storeInfo['status'] = 0; + } + } + } + + //商品详情 + $storeInfo['small_image'] = get_thumb_water($storeInfo['image']); + $data['storeInfo'] = $storeInfo; + + $data['reply'] = []; + $data['replyChance'] = $data['replyCount'] = 0; + if (isset($infoDiy['is_reply']) && $infoDiy['is_reply']) { + /** @var StoreProductReplyServices $storeProductReplyService */ + $storeProductReplyService = app()->make(StoreProductReplyServices::class); + $reply = $storeProductReplyService->getRecProductReplyCache((int)$storeInfo['product_id'], (int)($infoDiy['reply_num'] ?? 1)); + $data['reply'] = $reply ? get_thumb_water($reply, 'small', ['pics']) : []; + [$replyCount, $goodReply, $replyChance] = $storeProductReplyService->getProductReplyData((int)$storeInfo['product_id']); + $data['replyChance'] = $replyChance; + $data['replyCount'] = $replyCount; + } + + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + [$productAttr, $productValue] = $storeProductAttrServices->getProductAttrDetailCache($id, $uid, 0, 1, $storeInfo['product_id'], $productInfo); + $data['productAttr'] = $productAttr; + $data['productValue'] = $productValue; + $data['routine_contact_type'] = $configData['routine_contact_type'] ?? 0; + $data['store_func_status'] = (int)($configData['store_func_status'] ?? 1);//门店是否开启 + $data['store_self_mention'] = $data['store_func_status'] ? (int)($configData['store_self_mention'] ?? 0) : 0;//门店自提是否开启 + $data['site_name'] = $configData['site_name'] ?? ''; + $data['share_qrcode'] = $configData['share_qrcode'] ?? 0; + $data['product_poster_title'] = $configData['product_poster_title'] ?? ''; + //浏览记录 + ProductLogJob::dispatch(['visit', ['uid' => $uid, 'id' => $id, 'product_id' => $storeInfo['product_id']], 'seckill']); + return $data; + } + + /** + * 修改秒杀库存 + * @param int $num + * @param int $seckillId + * @param string $unique + * @param int $store_id + * @return bool + */ + public function decSeckillStock(int $num, int $seckillId, string $unique = '', int $store_id = 0) + { + $product_id = $this->dao->value(['id' => $seckillId], 'product_id'); + $res = true; + if ($product_id) { + if ($unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //减去秒杀商品的sku库存增加销量 + $res = $res && $skuValueServices->decProductAttrStock($seckillId, $unique, $num, 1); + + //减去当前普通商品sku的库存增加销量 + //秒杀商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $seckillId, 'type' => 1], 'suk'); + //平台商品sku unique + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //减去普通商品库存 + $res = $res && $services->decProductStock($num, (int)$product_id, (string)$productUnique, $store_id); + } + //减去秒杀库存 + $res = $res && $this->dao->decStockIncSales(['id' => $seckillId, 'type' => 1], $num); + } + //更新单个缓存 + $info = $this->dao->getOne(['id' => $seckillId], '*', ['descriptions']); + if ($info) { + $info = $info->toArray(); + $this->dao->cacheUpdate($info); + } + return $res; + } + + /** + * 加库存减销量 + * @param int $num + * @param int $seckillId + * @param string $unique + * @param int $store_id + * @return bool + */ + public function incSeckillStock(int $num, int $seckillId, string $unique = '', int $store_id = 0) + { + $product_id = $this->dao->value(['id' => $seckillId], 'product_id'); + $res = true; + if ($product_id) { + if ($unique) { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //增加秒杀商品的sku库存减少销量 + $res = $res && $skuValueServices->incProductAttrStock($seckillId, $unique, $num, 1); + + //减去当前普通商品sku的库存增加销量 + //秒杀商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $seckillId, 'type' => 1], 'suk'); + //平台商品sku unique + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + //减去普通商品库存 + $res = $res && $services->incProductStock($num, (int)$product_id, (string)$productUnique, $store_id); + } + //增加秒杀库存减去销量 + $res = $res && $this->dao->incStockDecSales(['id' => $seckillId, 'type' => 1], $num); + } + //更新单个缓存 + $info = $this->dao->getOne(['id' => $seckillId], '*', ['descriptions']); + if ($info) { + $info = $info->toArray(); + $this->dao->cacheUpdate($info); + } + return $res; + } + + /** + * 下单|加入购物车验证秒杀商品库存 + * @param int $uid + * @param int $seckillId + * @param int $cartNum + * @param string $unique + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkSeckillStock(int $uid, int $seckillId, int $cartNum = 1, int $store_id = 0, string $unique = '') + { + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + if ($unique == '') { + $unique = $attrValueServices->value(['product_id' => $seckillId, 'type' => 1], 'unique'); + } + //检查商品活动状态 + $storeSeckillinfo = $this->getSeckillCount($seckillId, '*,title as store_name'); + if (!$storeSeckillinfo) { + throw new ValidateException('该活动已下架'); + } + if ($storeSeckillinfo['once_num'] < $cartNum) { + throw new ValidateException('每个订单限购' . $storeSeckillinfo['once_num'] . '件'); + } + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $userBuyCount = $orderServices->getBuyCount($uid, 1, $seckillId); + if ($storeSeckillinfo['num'] < ($userBuyCount + $cartNum)) { + throw new ValidateException('每人总共限购' . $storeSeckillinfo['num'] . '件'); + } + if ($storeSeckillinfo['num'] < $cartNum) { + throw new ValidateException('每人限购' . $storeSeckillinfo['num'] . '件'); + } + $attrInfo = $attrValueServices->getOne(['product_id' => $seckillId, 'unique' => $unique, 'type' => 1]); + if (!$attrInfo || $attrInfo['product_id'] != $seckillId) { + throw new ValidateException('请选择有效的商品属性'); + } + if ($cartNum > $attrInfo['quota']) { + throw new ValidateException('该商品库存不足' . $cartNum); + } + + if ($store_id) { + /** @var StoreBranchProductServices $branchProductServices */ + $branchProductServices = app()->make(StoreBranchProductServices::class); + $branchProductInfo = $branchProductServices->isValidStoreProduct((int)$storeSeckillinfo['product_id'], $store_id); + if (!$branchProductInfo) { + throw new ValidateException('门店该商品已下架'); + } + $suk = $attrValueServices->value(['unique' => $unique, 'product_id' => $seckillId, 'type' => 1], 'suk'); + $brandAttrInfo = $attrValueServices->getOne(['product_id' => $branchProductInfo['id'], 'suk' => $suk, 'type' => 0]); + if (!$brandAttrInfo) { + throw new ValidateException('请选择有效的商品属性'); + } + if ($cartNum > $brandAttrInfo['stock']) { + throw new ValidateException('该商品库存不足' . $cartNum); + } + } + return [$attrInfo, $unique, $storeSeckillinfo]; + } + + /** + * 秒杀统计 + * @return array + */ + public function seckillStatistics($id) + { + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $pay_count = $orderServices->getDistinctCount(['type' => 1, 'activity_id' => $id, 'paid' => 1, 'pid' => [0, -1]], 'uid'); + $order_count = $orderServices->getDistinctCount(['type' => 1, 'activity_id' => $id, 'pid' => [0, -1]], 'uid'); + $all_price = $orderServices->sum(['type' => 1, 'activity_id' => $id, 'paid' => 1,'refund_type' => [0, 3], 'pid' => [0, -1]], 'pay_price'); + $seckillInfo = $this->dao->get($id); + $pay_rate = $seckillInfo['quota'] . '/' . $seckillInfo['quota_show']; + return compact('pay_count', 'order_count', 'all_price', 'pay_rate'); + } + + /** + * 秒杀参与人统计 + * @param $id + * @param string $keyword + * @return array + */ + public function seckillPeople($id, $keyword = '') + { + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + [$page, $limit] = $this->getPageValue(); + $list = $orderServices->seckillPeople($id, $keyword, $page, $limit); + $count = $orderServices->getDistinctCount([['paid', '=', 1],['type', '=', 1],['activity_id', '=', $id], ['pid', 'in', [0, -1]], ['real_name|uid|user_phone', 'like', '%' . $keyword . '%']], 'uid', false); + foreach ($list as &$item) { + $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']); + } + return compact('list', 'count'); + } + + /** + * 秒杀订单统计 + * @param $id + * @param array $where + * @return array + */ + public function seckillOrder(int $id, array $where = []) + { + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + [$page, $limit] = $this->getPageValue(); + $list = $orderServices->activityStatisticsOrder($id, 1, $where, $page, $limit); + $where['type'] = 1; + $where['activity_id'] = $id; + $count = $orderServices->count($where); + foreach ($list as &$item) { + if ($item['is_del'] || $item['is_system_del']) { + $item['status'] = '已删除'; + } else if ($item['paid'] == 0 && $item['status'] == 0) { + $item['status'] = '未支付'; + } else if ($item['paid'] == 1 && $item['status'] == 4 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status'] = '部分发货'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 2) { + $item['status'] = '已退款'; + } else if ($item['paid'] == 1 && $item['status'] == 5 && $item['refund_status'] == 0) { + $item['status'] = $item['shipping_type'] == 2 ? '部分核销' : '部分收货'; + $item['_status'] = 12;//已支付 部分核销 + } else if ($item['paid'] == 1 && $item['refund_status'] == 1) { + $item['status'] = '申请退款'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 4) { + $item['status'] = '退款中'; + } else if ($item['paid'] == 1 && $item['status'] == 0 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status'] = '未发货'; + $item['_status'] = 2;//已支付 未发货 + } else if ($item['paid'] == 1 && in_array($item['status'], [0, 1]) && $item['shipping_type'] == 2 && $item['refund_status'] == 0) { + $item['status'] = '未核销'; + } else if ($item['paid'] == 1 && in_array($item['status'], [1, 5]) && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status'] = '待收货'; + } else if ($item['paid'] == 1 && $item['status'] == 2 && $item['refund_status'] == 0) { + $item['status'] = '待评价'; + } else if ($item['paid'] == 1 && $item['status'] == 3 && $item['refund_status'] == 0) { + $item['status'] = '已完成'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 3) { + $item['status'] = '部分退款'; + } else { + $item['status'] = '未知'; + } + $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']); + $item['pay_time'] = $item['pay_time'] ? date('Y-m-d H:i:s', $item['pay_time']) : ''; + } + return compact('list', 'count'); + } +} diff --git a/app/services/activity/seckill/StoreSeckillTimeServices.php b/app/services/activity/seckill/StoreSeckillTimeServices.php new file mode 100644 index 0000000..bd5c731 --- /dev/null +++ b/app/services/activity/seckill/StoreSeckillTimeServices.php @@ -0,0 +1,184 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\seckill; + + +use app\dao\activity\seckill\StoreSeckillTimeDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use FormBuilder\Factory\Iview; +use think\facade\Route as Url; + +/** + * 秒杀时间 + * Class StoreSeckillTimeServices + * @package app\services\activity\seckill + * @mixin StoreSeckillTimeDao + */ +class StoreSeckillTimeServices extends BaseServices +{ + + /** + * StoreSeckillTimeServices constructor. + * @param StoreSeckillTimeDao $dao + */ + public function __construct(StoreSeckillTimeDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取当前的秒杀时间time + * @return int + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSeckillTime() + { + $seckillTime = $this->time_list(); + $currentHour = date('Hi'); + $time = 0; + foreach ($seckillTime as $value) { + $start = str_replace(':', '', $value['start_time']); + $end = str_replace(':', '', $value['end_time']); + if ($currentHour >= $start && $currentHour < $end) { + $time = $value['id']; + } + } + return (int)$time; + } + + /** + * 获取列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function systemPage(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit); + $count = $this->dao->count($where); + foreach ($list as &$item) { + $item['start_time'] = substr_replace($item['start_time'], ':', 2, 0); + $item['end_time'] = substr_replace($item['end_time'], ':', 2, 0); + } + return compact('list', 'count'); + } + + /** + * 可用秒杀时间段 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function time_list() + { + $list = $this->dao->getList(['status' => 1], 'id,title,start_time,end_time,pic'); + foreach ($list as &$item) { + $item['start_time'] = substr_replace($item['start_time'], ':', 2, 0); + $item['end_time'] = substr_replace($item['end_time'], ':', 2, 0); + $item['slide'] = $item['pic']; + }; + return $list; + } + + /** + * 验证时间是否重复 + * @param array $where + * @param int $id + * @return bool + * @throws \think\db\exception\DbException + */ + public function checkTime(array $where, int $id) + { + if (!$this->dao->valStartTime($where['start_time'], $id) && !$this->dao->valEndTime($where['end_time'], $id) && !$this->dao->valAllTime($where, $id)) return true; + return false; + } + + /** + * 获取秒杀时间 + * @param int $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getInfo(int $id) + { + $info = $this->dao->get($id); + if (!$info) { + throw new AdminException('数据不存在'); + } + $info = $info->toArray(); + $info['time'] = [ + substr_replace($info['start_time'], ':', 2, 0) . ':00', + substr_replace($info['end_time'], ':', 2, 0) . ':00' + ]; + return $info; + } + + + /** + * 添加、编辑表单 + * @param int $id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createForm(int $id) + { + $info = []; + if ($id) { + $info = $this->getInfo($id); + } + $f[] = Form::input('title', '标题', $info['title'] ?? '')->required(); + $f[] = Form::timeRange('time', '时间选择', $info['time'][0] ?? '', $info['time'][1] ?? '')->prop('picker-options', ['format' => 'HH:mm'])->appendValidate(Iview::validateArr()->required()->message('请选择时间')); + $f[] = Form::frameImage('pic', '图片', Url::buildUrl('admin/widget.images/index', array('fodder' => 'pic')), $info['pic'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true])->appendValidate(Iview::validateStr()->required()->message('请选择图片')); + $f[] = Form::radio('status', '状态', $info['status'] ?? 1)->options([['label' => '显示', 'value' => 1], ['label' => '隐藏', 'value' => 0]]); + $f[] = Form::input('describe', '描述',$Info['describe'] ?? '')->required(); + return create_form($id ? '编辑' : '添加秒杀时间', $f, $this->url('/marketing/seckill/time/' . $id), 'POST'); + } + + /** + * 组合4位秒杀时间 + * @param $time + * @return array|string|string[] + */ + public function changeTime($time) + { + $str_time = ''; + switch (strlen($time)) { + case 1: + $str_time = '0' . $time . '00'; + break; + case 2: + $str_time = $time . '00'; + break; + case 3: + $str_time = '0' . $time; + break; + case 4: + $str_time = $time; + break; + } + return substr_replace($str_time, ':', 2, 0); + } + +} diff --git a/app/services/activity/table/TableQrcodeServices.php b/app/services/activity/table/TableQrcodeServices.php new file mode 100644 index 0000000..17b40bf --- /dev/null +++ b/app/services/activity/table/TableQrcodeServices.php @@ -0,0 +1,94 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\activity\table; + +use app\services\BaseServices; +use app\dao\activity\table\TableQrcodeDao; +use app\services\other\QrcodeServices; +use think\exception\ValidateException; + +/** + * + * Class TableQrcodeServices + * @package app\services\activity\table + * @mixin TableQrcodeDao + */ +class TableQrcodeServices extends BaseServices +{ + + /** + * TableQrcodeServices constructor. + * @param TableQrcodeDao $dao + */ + public function __construct(TableQrcodeDao $dao) + { + $this->dao = $dao; + } + + /**桌码列表 + * @param array $where + * @param int $storeId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function tableQrcodeyList(array $where, int $storeId) + { + [$page, $limit] = $this->getPageValue(); + $where['store_id'] = $storeId; + $where['is_del'] = 0; + $list = $this->dao->getList($where, $page, $limit, ['category']); + foreach ($list as $key => &$item) { + if (!$item['qrcode']) { + $item['qrcode'] = $this->setQrcodey((int)$item['id'], (int)$item['store_id'], (int)$item['table_number']); + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /**生成桌码 + * @param int $id + * @param int $store_id + * @param int $table_number + * @return bool|string + */ + public function setQrcodey(int $id, int $store_id, int $table_number) + { + /** @var QrcodeServices $qrcodeServices */ + $qrcodeServices = app()->make(QrcodeServices::class); + $parame['store_id'] = $store_id; + $parame['table_number'] = $table_number; + $qrcode = $qrcodeServices->getRoutineQrcodePath($id, 0, 9, $parame); + $this->dao->update($id, ['qrcode' => $qrcode]); + return $qrcode; + } + + /**获取桌码信息 + * @param int $id + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getQrcodeyInfo(int $id, array $with = []) + { + $Info = $this->dao->getTableCodeOne(['id' => $id, 'is_using' => 1], $with); + if (!$Info) { + throw new ValidateException('获取信息失败'); + } + return $Info->toArray(); + } +} diff --git a/app/services/agent/AgentLevelServices.php b/app/services/agent/AgentLevelServices.php new file mode 100644 index 0000000..71289a7 --- /dev/null +++ b/app/services/agent/AgentLevelServices.php @@ -0,0 +1,402 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\agent; + +use app\dao\agent\AgentLevelDao; +use app\services\BaseServices; +use app\services\order\StoreOrderServices; +use app\services\user\UserBrokerageServices; +use app\services\user\UserExtractServices; +use app\services\user\UserServices; +use app\services\user\UserSpreadServices; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use FormBuilder\Factory\Iview; +use think\exception\ValidateException; +use think\facade\Route as Url; + + +/** + * 分销等级 + * Class AgentLevelServices + * @package app\services\agent + * @mixin AgentLevelDao + */ +class AgentLevelServices extends BaseServices +{ + /** + * AgentLevelServices constructor. + * @param AgentLevelDao $dao + */ + public function __construct(AgentLevelDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取某一个等级信息 + * @param int $id + * @param string $field + * @param array $with + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLevelInfo(int $id, string $field = '*', array $with = []) + { + return $this->dao->getOne(['id' => $id, 'is_del' => 0], $field, $with); + } + + /** + * 获取等级列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLevelList(array $where) + { + $where['is_del'] = 0; + [$page, $limit] = $this->getPageValue(); + $count = $this->dao->count($where); + $list = []; + if ($count) { + $list = $this->dao->getList($where, '*', ['task' => function ($query) { + $query->field('count(*) as sum'); + }], $page, $limit); + $store_brokerage_ratio = sys_config('store_brokerage_ratio'); + $store_brokerage_two = sys_config('store_brokerage_two'); + foreach ($list as &$item) { + $item['one_brokerage_ratio'] = $this->compoteBrokerage($store_brokerage_ratio, $item['one_brokerage']); + $item['two_brokerage_ratio'] = $this->compoteBrokerage($store_brokerage_two, $item['two_brokerage']); + } + } + return compact('count', 'list'); + } + + /** + * 商城获取分销员等级列表 + * @param int $uid + * @return array + */ + public function getUserlevelList(int $uid) + { + //商城分销是否开启 + if (!sys_config('brokerage_func_status')) { + return []; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserCacheInfo($uid); + if (!$user) { + throw new ValidateException('没有此用户'); + } + try { + //检测升级 + $this->checkUserLevelFinish($uid); + } catch (\Throwable $e) { + + } + $list = $this->dao->getList(['is_del' => 0, 'status' => 1]); + $agent_level = $user['agent_level'] ?? 0; + //没等级默认最低等级 + if (!$agent_level) { + $levelInfo = []; + } else { + $levelInfo = $this->getLevelInfo($agent_level) ?: []; + } + $sum_task = $finish_task = 0; + if ($levelInfo) { + /** @var AgentLevelTaskServices $levelTaskServices */ + $levelTaskServices = app()->make(AgentLevelTaskServices::class); + $sum_task = $levelTaskServices->count(['level_id' => $levelInfo['id'], 'is_del' => 0, 'status' => 1]); + /** @var AgentLevelTaskRecordServices $levelTaskRecordServices */ + $levelTaskRecordServices = app()->make(AgentLevelTaskRecordServices::class); + $finish_task = $levelTaskRecordServices->count(['level_id' => $levelInfo['id'], 'uid' => $uid]); + } + $levelInfo['sum_task'] = $sum_task; + $levelInfo['finish_task'] = $finish_task; + + //推广订单总数 + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $user['spread_order_count'] = $orderServices->count(['type' => 0, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0, 'spread_or_uid' => $uid]); + + //冻结佣金和可提现金额 + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $user['broken_commission'] = $userBrokerageServices->getUserFrozenPrice($uid); + $user['commissionCount'] = bcsub($user['brokerage_price'], $user['broken_commission'], 2); + //佣金排行 + $user['position_count'] = $userBrokerageServices->getUserBrokerageRank($uid, 'week'); + //推广人排行 + $startTime = strtotime('this week Monday'); + $endTime = time(); + $field = 'spread_uid,count(uid) AS count,spread_time'; + /** @var UserSpreadServices $userSpreadServices */ + $userSpreadServices = app()->make(UserSpreadServices::class); + $rankList = $userSpreadServices->getAgentRankList([$startTime, $endTime], $field); + $rank = 0; + foreach ($rankList as $key => $item) { + if ($item['spread_uid'] == $uid) $rank = $key + 1; + } + $user['rank_count'] = $rank; + $user['spread_count'] = $userServices->count(['spread_uid' => $uid]); + /** @var UserExtractServices $extractService */ + $extractService = app()->make(UserExtractServices::class); + $user['extract_price'] = $extractService->sum(['uid' => $uid, 'status' => 1], 'extract_price'); + return ['user' => $user, 'level_list' => $list, 'level_info' => $levelInfo]; + } + + /** + * 检测用户是否能升级 + * @param int $uid + * @param array $uids + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkUserLevelFinish(int $uid, array $uids = []) + { + //商城分销是否开启 + if (!sys_config('brokerage_func_status')) { + return false; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserCacheInfo($uid); + if (!$userInfo) { + return false; + } + $list = $this->dao->getList(['is_del' => 0, 'status' => 1]); + if (!$list) { + return false; + } + if (!$uids) { + //获取上级uid || 开启自购返回自己uid + $spread_uid = $userServices->getSpreadUid($uid, $userInfo); + $two_spread_uid = 0; + if ($spread_uid > 0 && $one_user_info = $userServices->getUserCacheInfo($spread_uid)) { + $two_spread_uid = $userServices->getSpreadUid($spread_uid, $one_user_info, false); + } + $uids = array_unique([$uid, $spread_uid, $two_spread_uid]); + } + foreach ($uids as $uid) { + if ($uid <= 0) continue; + if ($uid != $userInfo['uid']) { + $userInfo = $userServices->getUserCacheInfo($uid); + if (!$userInfo) + continue; + } + + $now_grade = 0; + if ($userInfo['agent_level']) { + $now_grade = $this->dao->value(['id' => $userInfo['agent_level']], 'grade') ?: 0; + } + foreach ($list as $levelInfo) { + if (!$levelInfo || $levelInfo['grade'] <= $now_grade) { + continue; + } + /** @var AgentLevelTaskServices $levelTaskServices */ + $levelTaskServices = app()->make(AgentLevelTaskServices::class); + $task_list = $levelTaskServices->getTaskList(['level_id' => $levelInfo['id'], 'is_del' => 0, 'status' => 1]); + if (!$task_list) { + continue; + } + foreach ($task_list as $task) { + $levelTaskServices->checkLevelTaskFinish($uid, (int)$task['id'], $task); + } + /** @var AgentLevelTaskRecordServices $levelTaskRecordServices */ + $levelTaskRecordServices = app()->make(AgentLevelTaskRecordServices::class); + $ids = array_column($task_list, 'id'); + $finish_task = $levelTaskRecordServices->count(['level_id' => $levelInfo['id'], 'uid' => $uid, 'task_id' => $ids]); + //任务完成升这一等级 + if ($finish_task >= count($task_list)) { + $userServices->update($uid, ['agent_level' => $levelInfo['id']]); + } else { + break; + } + } + } + + return true; + } + + /** + * 分销等级上浮 + * @param int $uid + * @param array $userInfo + * @return array|int[] + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAgentLevelBrokerage(int $uid, $userInfo = []) + { + $one_brokerage_up = $two_brokerage_up = $spread_uid = $spread_two_uid = 0; + $data = [$one_brokerage_up, $two_brokerage_up, $spread_uid, $spread_two_uid]; + if (!$uid) { + return $data; + } + //商城分销是否开启 + if (!sys_config('brokerage_func_status')) { + return $data; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$userInfo) { + $userInfo = $userServices->getUserCacheInfo($uid); + } + if (!$userInfo) { + return $data; + } + //获取上级uid || 开启自购返回自己uid + $spread_uid = $userServices->getSpreadUid($uid, $userInfo); + $one_agent_level = 0; + $two_agent_level = 0;; + if ($spread_uid > 0 && $one_user_info = $userServices->getUserInfo($spread_uid)) { + $one_agent_level = $one_user_info['agent_level'] ?? 0; + $spread_two_uid = $userServices->getSpreadUid($spread_uid, $one_user_info, false); + if ($spread_two_uid > 0 && $two_user_info = $userServices->getUserInfo($spread_two_uid)) { + $two_agent_level = $two_user_info['agent_level'] ?? 0; + } + } + $one_brokerage_up = $one_agent_level ? ($this->getLevelInfo($one_agent_level)['one_brokerage'] ?? 0) : 0; + $two_brokerage_up = $two_agent_level ? ($this->getLevelInfo($two_agent_level)['two_brokerage'] ?? 0) : 0; + return [$one_brokerage_up, $two_brokerage_up, $spread_uid, $spread_two_uid]; + } + + + /** + * 计算一二级返佣比率上浮 + * @param $ratio + * @param $brokerage + * @return int|string + */ + public function compoteBrokerage($ratio, $brokerage) + { + if (!$ratio) { + return 0; + } + $brokerage = bcdiv((string)$brokerage, '100', 4); + if ($brokerage) { + return bcmul((string)$ratio, bcadd('1', (string)$brokerage, 4), 2); + } + return $brokerage; + } + + /** + * 添加等级表单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createForm() + { + $store_brokerage_ratio = sys_config('store_brokerage_ratio'); + $store_brokerage_two = sys_config('store_brokerage_two'); + + $field[] = Form::input('name', '等级名称')->col(24); + $field[] = Form::number('grade', '等级', 0)->min(0)->precision(0); + $field[] = Form::frameImage('image', '背景图', Url::buildUrl(config('admin.admin_prefix') . '/widget.images/index', array('fodder' => 'image')))->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true])->appendValidate(Iview::validateStr()->required()->message('请选择背景图')); + $field[] = Form::number('one_brokerage', '一级上浮', 0)->info('在分销一级佣金基础上浮(0-1000之间整数)百分比,目前一级返佣比率:'. $store_brokerage_ratio . '%,例如上浮10%,则返佣比率:一级返佣比率 * (1 + 一级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_ratio, 10) .'%')->min(0)->max(1000); + $field[] = Form::number('two_brokerage', '二级上浮', 0)->info('在分销二级佣金基础上浮(0-1000之间整数)百分比,目前二级返佣比率:'. $store_brokerage_two . '%,例如上浮10%,则返佣比率:二级返佣比率 * (1 + 二级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_two, 10). '%')->min(0)->max(1000); + $field[] = Form::radio('status', '是否显示', 1)->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]); + return create_form('添加分销员等级', $field, Url::buildUrl('/agent/level'), 'POST'); + } + + + /** + * 获取修改等级表单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function editForm(int $id) + { + $store_brokerage_ratio = sys_config('store_brokerage_ratio'); + $store_brokerage_two = sys_config('store_brokerage_two'); + $levelInfo = $this->getLevelInfo($id); + if (!$levelInfo) + throw new AdminException('数据不存在'); + $field = []; + $field[] = Form::hidden('id', $id); + $field[] = Form::input('name', '等级名称', $levelInfo['name'])->col(24); + $field[] = Form::number('grade', '等级', $levelInfo['grade'])->min(0)->precision(0); + $field[] = Form::frameImage('image', '背景图', Url::buildUrl(config('admin.admin_prefix') . '/widget.images/index', array('fodder' => 'image')), $levelInfo['image'])->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true])->appendValidate(Iview::validateStr()->required()->message('请选择背景图')); + $field[] = Form::number('one_brokerage', '一级上浮', $levelInfo['one_brokerage'])->info('在分销一级佣金基础上浮(0-1000之间整数)百分比,目前一级返佣比率:'. $store_brokerage_ratio . '%,上浮'. $levelInfo['one_brokerage'] . '%,则返佣比率:一级返佣比率 * (1 + 一级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_ratio, $levelInfo['one_brokerage']) .'%')->min(0)->max(1000); + $field[] = Form::number('two_brokerage', '二级上浮', $levelInfo['two_brokerage'])->info('在分销二级佣金基础上浮(0-1000之间整数)百分比,目前二级返佣比率:'. $store_brokerage_two . '%,上浮' . $levelInfo['two_brokerage'] . '%,则返佣比率:二级返佣比率 * (1 + 二级上浮比率) = ' . $this->compoteBrokerage($store_brokerage_two, $levelInfo['two_brokerage']). '%')->min(0)->max(1000); + $field[] = Form::radio('status', '是否显示', $levelInfo['status'])->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]); + + return create_form('编辑分销员等级', $field, Url::buildUrl('/agent/level/' . $id), 'PUT'); + } + + /** + * 赠送分销等级表单 + * @param int $uid + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function levelForm(int $uid) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + throw new AdminException('分销员不存在'); + } + $levelList = $this->dao->getList(['is_del' => 0, 'status' => 1]); + $setOptionLabel = function () use ($levelList) { + $menus = []; + foreach ($levelList as $level) { + $menus[] = ['value' => $level['id'], 'label' => $level['name']]; + } + return $menus; + }; + $field[] = Form::hidden('uid', $uid); + $field[] = Form::select('id', '分销等级', $userInfo['agent_level'] ?? 0)->setOptions(Form::setOptions($setOptionLabel))->filterable(true); + return create_form('赠送分销等级', $field, Url::buildUrl('/agent/give_level'), 'post'); + } + + /** + * 赠送分销等级 + * @param int $uid + * @param int $id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function givelevel(int $uid, int $id) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + throw new AdminException('分销员不存在'); + } + $levelInfo = $this->getLevelInfo($id); + if (!$levelInfo) { + throw new AdminException('分销等级不存在'); + } + if ($userServices->update($uid, ['agent_level' => $id]) === false) { + throw new AdminException('赠送失败'); + } + return true; + } +} diff --git a/app/services/agent/AgentManageServices.php b/app/services/agent/AgentManageServices.php new file mode 100644 index 0000000..dbddfa5 --- /dev/null +++ b/app/services/agent/AgentManageServices.php @@ -0,0 +1,423 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\agent; + +use app\jobs\agent\AutoAgentJob; +use app\services\BaseServices; +use app\services\order\StoreOrderServices; +use app\services\order\StoreOrderStatusServices; +use app\services\other\QrcodeServices; +use app\services\system\attachment\SystemAttachmentServices; +use app\services\user\UserBrokerageServices; +use app\services\user\UserExtractServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\{QrcodeService, UploadService, wechat\MiniProgram}; +use think\exception\ValidateException; + +/** + * 分销员 + * Class AgentManageServices + * @package app\services\agent + */ +class AgentManageServices extends BaseServices +{ + + /** + * @param array $where + * @return array + */ + public function agentSystemPage(array $where, $is_page = true) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $data = $userServices->getAgentUserList($where, '*', $is_page); + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + foreach ($data['list'] as &$item) { + $item['headimgurl'] = $item['avatar']; + $item['extract_count_price'] = $item['extract'][0]['extract_count_price'] ?? 0; + $item['extract_count_num'] = $item['extract'][0]['extract_count_num'] ?? 0; + $item['spread_name'] = $item['spreadUser']['nickname'] ?? ''; + if ($item['spread_name']) { + $item['spread_name'] .= '/' . $item['spread_uid']; + } + $item['spread_count'] = $item['spreadCount'][0]['spread_count'] ?? 0; + $item['order_price'] = $item['order'][0]['order_price'] ?? 0; + $item['order_count'] = $item['order'][0]['order_count'] ?? 0; + $item['broken_commission'] = $userBrokerageServices->getUserFrozenPrice((int)$item['uid']); + if ($item['broken_commission'] < 0) + $item['broken_commission'] = 0; + $item['brokerage_money'] = $item['brokerage'][0]['brokerage_money'] ?? 0; + if ($item['brokerage_price'] > $item['broken_commission']) + $item['brokerage_money'] = bcsub($item['brokerage_price'], $item['broken_commission'], 2); + else + $item['brokerage_money'] = 0; + $item['new_money'] = $item['brokerage_price']; + unset($item['extract'], $item['order'], $item['bill'], $item['spreadUser'], $item['spreadCount']); + } + return $data; + } + + /** + * 分销头部信息 + * @param $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSpreadBadge($where) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $uids = $userServices->getAgentUserIds($where); + + //分销员人数 + $data['uids'] = $uids; + $data['sum_count'] = count($uids); + $data['spread_sum'] = 0; + $data['extract_price'] = 0; + if ($data['sum_count']) { + //发展会员人数 + $data['spread_sum'] = $userServices->getCount([['spread_uid', 'in', $uids]]); + //获取某个用户可提现金额 + $data['extract_price'] = $userServices->getSumBrokerage(['uid' => $uids]); + } + //分销员人数 + $data['order_count'] = 0; + $data['pay_price'] = 0; + $data['pay_price'] = 0; + $data['extract_count'] = 0; + if ($data['sum_count']) { + /** @var StoreOrderServices $storeOrder */ + $storeOrder = app()->make(StoreOrderServices::class); + $order_where = ['uid' => $uids, 'pid' => 0, 'paid' => 1, 'refund_status' => [0, 3]]; + //订单总数 + $data['order_count'] = $storeOrder->count($order_where); + //订单金额 + $data['pay_price'] = $storeOrder->sum($order_where, 'pay_price', true); + //提现次数 + $data['extract_count'] = app()->make(UserExtractServices::class)->getCount([['uid', 'in', $uids], ['status', '=', 1]]); + } + return [ + [ + 'name' => '分销员人数(人)', + 'count' => $data['sum_count'], + 'className' => 'md-contacts', + 'col' => 6, + ], + [ + 'name' => '发展会员人数(人)', + 'count' => $data['spread_sum'], + 'className' => 'md-contact', + 'col' => 6, + ], + [ + 'name' => '订单数(单)', + 'count' => $data['order_count'], + 'className' => 'md-cart', + 'col' => 6, + ], + [ + 'name' => '订单金额(元)', + 'count' => $data['pay_price'], + 'className' => 'md-bug', + 'col' => 6, + ], + [ + 'name' => '提现次数(次)', + 'count' => $data['extract_count'], + 'className' => 'md-basket', + 'col' => 6, + ], + [ + 'name' => '未提现金额(元)', + 'count' => $data['extract_price'], + 'className' => 'ios-at-outline', + 'col' => 6, + ], + ]; + } + + /** + * 推广人列表 + * @param array $where + * @return mixed + */ + public function getStairList(array $where) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $data = $userServices->getSairList($where); + $store_brokerage_statu = sys_config('store_brokerage_statu'); + foreach ($data['list'] as &$item) { + $item['spread_count'] = $item['spreadCount'][0]['spread_count'] ?? 0; + $item['order_count'] = $item['order'][0]['order_count'] ?? 0; + $item['promoter_name'] = $item['is_promoter'] || $store_brokerage_statu == 2 ? '是' : '否'; + $item['add_time'] = $item['add_time'] ? date("Y-m-d H:i:s", $item['add_time']) : ''; + } + return $data; + } + + /** + * 推广人头部信息 + * @param array $where + * @return array[] + */ + public function getSairBadge(array $where) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $data['number'] = $userServices->getSairCount($where); + $where['type'] = 1; + $data['one_number'] = $userServices->getSairCount($where); + $where['type'] = 2; + $data['two_number'] = $userServices->getSairCount($where); + + $col = $data['two_number'] > 0 ? 4 : 6; + return [ + [ + 'name' => '总人数(人)', + 'count' => $data['number'], + 'col' => $col, + ], + [ + 'name' => '一级人数(人)', + 'count' => $data['one_number'], + 'col' => $col, + ], + [ + 'name' => '二级人数(人)', + 'count' => $data['two_number'], + 'col' => $col, + ], + ]; + } + + /** + * 推广订单 + * @param array $where + * @return array + */ + public function getStairOrderList(int $uid, array $where) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + return ['count' => 0, 'list' => []]; + } + /** @var StoreOrderServices $storeOrder */ + $storeOrder = app()->make(StoreOrderServices::class); + $data = $storeOrder->getUserStairOrderList($uid, $where); + if ($data['list']) { + $uids = array_unique(array_column($data['list'], 'uid')); + $userList = []; + if ($uids) { + $userList = $userServices->getColumn([['uid', 'IN', $uids]], 'nickname,phone,avatar,real_name', 'uid'); + } + $orderIds = array_column($data['list'], 'id'); + $orderChangTimes = []; + if ($orderIds) { + /** @var StoreOrderStatusServices $storeOrderStatus */ + $storeOrderStatus = app()->make(StoreOrderStatusServices::class); + $orderChangTimes = $storeOrderStatus->getColumn([['oid', 'IN', $orderIds], ['change_type', '=', 'user_take_delivery']], 'change_time', 'oid'); + } + foreach ($data['list'] as &$item) { + $user = $userList[$item['uid']] ?? []; + $item['user_info'] = ''; + $item['avatar'] = ''; + if (count($user)) { + $item['user_info'] = $user['nickname'] . '|' . ($user['phone'] ? $user['phone'] . '|' : '') . $user['real_name']; + $item['avatar'] = $user['avatar']; + } + $item['brokerage_price'] = $item['spread_uid'] == $uid ? $item['one_brokerage'] : $item['two_brokerage']; + $item['_pay_time'] = $item['pay_time'] ? date('Y-m-d H:i:s', $item['pay_time']) : ''; + $item['_add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + $item['take_time'] = ($change_time = $orderChangTimes[$item['id']] ?? '') ? date('Y-m-d H:i:s', $change_time) : '暂无'; + } + } + return $data; + } + + /** + * 获取永久二维码 + * @param $type + * @param $id + * @return array|false|\PDOStatement|string|\think\Model + */ + public function wechatCode(int $uid) + { + /** @var QrcodeServices $qrcode */ + $qrcode = app()->make(QrcodeServices::class); + $code = $qrcode->getForeverQrcode('spread', $uid); + if (!$code['ticket']) exception('永久二维码获取错误'); + return $code; + } + + /** + * 查看小程序推广二维码 + * @param string $uid + */ + public function lookXcxCode(int $uid) + { + $userInfo = app()->make(UserServices::class)->getUserInfo($uid); + if (!$userInfo) { + throw new AdminException('数据不存在'); + } + $name = $userInfo['uid'] . '_' . $userInfo['is_promoter'] . '_user.jpg'; + /** @var SystemAttachmentServices $systemAttachmentModel */ + $systemAttachmentModel = app()->make(SystemAttachmentServices::class); + $imageInfo = $systemAttachmentModel->getInfo(['name' => $name]); + if (!$imageInfo) { + /** @var QrcodeServices $qrcode */ + $qrcode = app()->make(QrcodeServices::class); + $resForever = $qrcode->qrCodeForever($uid, 'spread_routine'); + if ($resForever) { + $data = 'id=' . $resForever->id . '&spid=0'; + $resCode = MiniProgram::appCodeUnlimit($data, '', 280); + $res = ['res' => $resCode, 'id' => $resForever->id]; + } else { + $res = false; + } + if (!$res) throw new ValidateException('二维码生成失败'); + $uploadType = (int)sys_config('upload_type', 1); + $upload = UploadService::init($uploadType); + if ($upload->to('routine/spread/code')->validate()->stream((string)$res['res'], $name) === false) { + return $upload->getError(); + } + $imageInfo = $upload->getUploadInfo(); + $imageInfo['image_type'] = $uploadType; + $systemAttachmentModel->attachmentAdd($imageInfo['name'], $imageInfo['size'], $imageInfo['type'], $imageInfo['dir'], $imageInfo['thumb_path'], 1, $imageInfo['image_type'], $imageInfo['time'], 2); + $qrcode->update($res['id'], ['status' => 1, 'time' => time(), 'qrcode_url' => $imageInfo['dir']]); + $urlCode = $imageInfo['dir']; + } else $urlCode = $imageInfo['att_dir']; + return ['code_src' => $urlCode]; + } + + /** + * 查看H5推广二维码 + * @param string $uid + * @return mixed|string + */ + public function lookH5Code(int $uid) + { + $userInfo = app()->make(UserServices::class)->getUserInfo($uid); + if (!$userInfo) { + throw new AdminException('数据不存在'); + } + $name = $userInfo['uid'] . '_h5_' . $userInfo['is_promoter'] . '_user.jpg'; + /** @var SystemAttachmentServices $systemAttachmentModel */ + $systemAttachmentModel = app()->make(SystemAttachmentServices::class); + $imageInfo = $systemAttachmentModel->getInfo(['name' => $name]); + if (!$imageInfo) { + $urlCode = QrcodeService::getWechatQrcodePath($uid . '_h5_' . $userInfo['is_promoter'] . '_user.jpg', '?spread=' . $uid); + } else $urlCode = $imageInfo['att_dir']; + return ['code_src' => $urlCode]; + } + + /** + * 清除推广关系 + * @param int $uid + * @return mixed + */ + public function delSpread(int $uid) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$userServices->userExist($uid)) { + throw new AdminException('数据不存在'); + } + if ($userServices->update($uid, ['spread_uid' => 0]) !== false) + return true; + else + throw new AdminException('解除失败'); + } + + /** + * 取消推广资格 + * @param int $uid + * @return mixed + */ + public function delSystemSpread(int $uid) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$userServices->userExist($uid)) { + throw new AdminException('数据不存在'); + } + if ($userServices->update($uid, ['spread_uid' => 0, 'spread_time' => 0]) !== false) + return true; + else + throw new AdminException('取消失败'); + } + + /** + * @param $page + * @param $limit + * @param $where + */ + public function startRemoveSpread($page, $limit, $where) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $list = $userServices->getList($where, 'uid,spread_uid,spread_time', $page, $limit); + foreach ($list as $userInfo) { + $userServices->update($userInfo['uid'], ['spread_uid' => 0, 'spread_time' => 0], 'uid'); + } + return true; + } + + /** + * 取消绑定上级 + * @return bool + */ + public function removeSpread() + { + //商城分销功能是否开启 0关闭1开启 + if (!sys_config('brokerage_func_status')) return true; + + //绑定类型 + $store_brokergae_binding_status = sys_config('store_brokerage_binding_status', 1); + if ($store_brokergae_binding_status == 1 || $store_brokergae_binding_status == 3) { + return true; + } else { + //分销绑定类型为时间段且没过期 + $store_brokerage_binding_time = (int)sys_config('store_brokerage_binding_time', 30) * 24 * 3600; + $spread_time = bcsub((string)time(), (string)$store_brokerage_binding_time, 0); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $where = ['not_spread_uid' => 0, 'status' => 1, 'time' => [0, $spread_time], 'time_key' => 'spread_time']; + $count = $userServices->count($where); + $pages = ceil($count / 100); + for ($i = 1; $i <= $pages; $i++) { + AutoAgentJob::dispatch([$i, 100, $where]); + } + } + return true; + } + + /** + * 配置绑定类型切换重置绑定时间 + * @return bool + */ + public function resetSpreadTime() + { + //商城分销功能是否开启 0关闭1开启 + if (!sys_config('brokerage_func_status')) return true; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userServices->update(['not_spread_uid' => 0, 'status' => 1], ['spread_time' => time()]); + return true; + } +} diff --git a/app/services/diy/DiyServices.php b/app/services/diy/DiyServices.php new file mode 100644 index 0000000..c21ca6d --- /dev/null +++ b/app/services/diy/DiyServices.php @@ -0,0 +1,515 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\diy; + +use app\services\BaseServices; +use app\dao\diy\DiyDao; +use app\services\other\QrcodeServices; +use app\services\system\config\SystemGroupDataServices; +use app\services\system\config\SystemGroupServices; +use crmeb\exceptions\AdminException; +use crmeb\services\SystemConfigService; +use think\exception\ValidateException; + +/** + * 首页diy + * Class DiyServices + * @package app\services\diy + * @mixin DiyDao + */ +class DiyServices extends BaseServices +{ + /** + /** + * @var string + */ + protected $cacheKey = 'diy_cache'; + + /** + * 数据类型 + * @var array + */ + public $type = [ + 1 => 'DIY数据', + 3 => [ + 'member' => '个人中心', + 'category' => '分类样式', + 'color_change' => '主题风格', + ], + 4 => '系统表单数据' + ]; + + /** + * 商品详情diy默认数据 + * @var array + */ + protected $productDetail = [ + 'price_type' => [1],//展示1:svip价2:会员价 + 'is_share' => true,//分享 + 'is_name' => true, //商品名称 + 'is_brand' => true,//品牌 + 'is_ot_price' => false,//原价 + 'is_sales' => true,//销量 + 'is_stock' => true,//库存 + 'is_coupon' => true,//优惠券 + 'is_activity' => true,//活动 + 'is_promotions' => true,//优惠活动 + 'is_specs' => true,//参数 + 'is_ensure' => true,//保障服务 + 'is_sku' => true,//规格选择 + 'sku_style' => 1,//规格样式 + 'is_store' => true,//门店 + 'is_reply' => true,//是否站评论 + 'reply_num' => 1,//评论数量 + 'is_discounts' => true,//是否展示搭配购 + 'discounts_num' => 3,//搭配狗数量 + 'is_recommend' => true,//是否展示优品推荐 + 'recommend_num' => 6,//优惠推荐数量 + 'is_description' => true//是否展示商品详情 + ]; + + /** + * DiyServices constructor. + * @param DiyDao $dao + */ + public function __construct(DiyDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取DIY列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDiyList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getDiyList($where, $page, $limit, ['is_diy', 'template_name', 'id', 'title', 'name', 'type', 'add_time', 'update_time', 'status', 'cover_image']); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 保存资源 + * @param int $id + * @param array $data + * @return int + */ + public function saveData(int $id = 0, array $data = []) + { + if ($id) { + if ($data['type'] === '') { + unset($data['type']); + } + $data['update_time'] = time(); + $res = $this->dao->update($id, $data); + if (!$res) throw new AdminException('修改失败'); + } else { + $data['add_time'] = time(); + $data['update_time'] = time(); + $data['is_diy'] = 1; + $res = $this->dao->save($data); + if (!$res) throw new AdminException('保存失败'); + $id = $res->id; + } + + $this->dao->cacheTag()->clear(); + $this->dao->cacheTag()->set('index_diy_' . $id, $data['version']); + $this->updateCacheDiyVersion(); + + event('diy.update'); + return $id; + } + + /** + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/9 + */ + public function updateCacheDiyVersion() + { + $diyInfo = $this->dao->get(['status' => 1, 'type' => 1, 'is_diy' => 1], ['id', 'version']); + if (!$diyInfo) { + $this->dao->cacheHander()->delete('index_diy_default'); + } else { + $this->dao->cacheTag()->set('index_diy_default', $diyInfo['version']); + } + } + + /** + * @return mixed + * @throws \Throwable + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/9 + */ + public function getCacheDiyVersion() + { + return $this->dao->cacheTag()->remember('index_diy_default', function () { + return $this->dao->value(['status' => 1, 'type' => 1, 'is_diy' => 1], 'version'); + }); + } + + /** + * 删除DIY模板 + * @param int $id + */ + public function del(int $id) + { + if ($id == 1) throw new AdminException('默认模板不能删除'); + $count = $this->dao->getCount(['id' => $id, 'status' => 1]); + if ($count) throw new AdminException('该模板使用中,无法删除'); + $res = $this->dao->update($id, ['is_del' => 1]); + if (!$res) throw new AdminException('删除失败,请稍后再试'); + + $this->dao->cacheTag()->clear(); + $this->updateCacheDiyVersion(); + $this->dao->cacheHander()->delete('index_diy_' . $id); + + event('diy.update'); + } + + /** + * 设置模板使用 + * @param int $id + */ + public function setStatus(int $id) + { + $info = $this->dao->get($id); + if (!$info) { + throw new AdminException('默认不存在'); + } + if (!$info['is_show']) { + throw new AdminException('请编辑开启页面状态后,在设置为首页'); + } + $this->dao->update($info['type'], ['status' => 0], 'type'); + $this->dao->update($id, ['status' => 1, 'update_time' => time()]); + + $this->dao->cacheTag()->clear(); + $this->updateCacheDiyVersion(); + + event('diy.update'); + return ['status' => 1, 'msg' => '设置成功']; + } + + /** + * 获取页面数据 + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDiy(string $name) + { + $data = []; + if ($name == '') { + $info = $this->dao->getOne(['status' => 1, 'type' => 1]); + } else { + $info = $this->dao->getOne(['template_name' => $name]); + } + if ($info) { + $info = $info->toArray(); + $data = json_decode($info['value'], true); + } + return $data; + } + + /** + * 返回当前diy版本 + * @param int $id + * @return mixed + * @throws \Throwable + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/9 + */ + public function getDiyVersion(int $id) + { + if ($id) { + return $this->dao->cacheTag()->remember('index_diy_' . $id, function () use ($id) { + return $this->dao->value(['id' => $id], 'version'); + }); + } else { + return $this->getCacheDiyVersion(); + } + } + + /** + * 获取diy详细数据 + * @param int $id + * @return array|object + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDiyInfo(int $id) + { + return $this->dao->cacheTag()->remember('diy_info_' . $id, function () use ($id) { + $field = ['title', 'value', 'is_show', 'is_bg_color', 'color_picker', 'bg_pic', 'bg_tab_val', 'is_bg_pic', 'order_status']; + $diyInfo = $this->dao->get($id ?: ['status' => 1, 'type' => 1, 'is_diy' => 1], $field); + if (!$diyInfo) { + $diyInfo = $this->dao->get(['template_name' => 'default'], $field); + } + if ($diyInfo) { + $diyInfo = $diyInfo->toArray(); + $diyInfo['value'] = json_decode($diyInfo['value'], true); + $value = []; + foreach ($diyInfo['value'] as $key => $item) { + if ($item['name'] == 'customerService') { + $item['routine_contact_type'] = SystemConfigService::get('routine_contact_type', 0); + } + if ($item['name'] == 'promotionList') { + unset($item['titleShow']['title'], + $item['opriceShow']['title'], + $item['priceShow']['title'], + $item['couponShow']['title']); + } + if ($item['name'] == 'activeParty') { + unset($item['titleConfig']['place'], + $item['titleConfig']['max'], + $item['desConfig']['place'], + $item['desConfig']['max'] + ); + if (isset($item['menuConfig']['list']['info'])) { + foreach ($item['menuConfig']['list']['info'] as $k => $v) { + unset($v['tips'], $v['max']); + $item['menuConfig']['list']['info'][$k] = $v; + } + } + } + + if ($item['name'] == 'pageFoot' && !$id) { + continue; + } + $value[$key] = $item; + } + $diyInfo['value'] = $value; + return $diyInfo; + } else { + return []; + } + }); + } + + /** + * 获取底部导航 + * @param string $template_name + * @return array|mixed + */ + public function getNavigation(string $template_name) + { + return $this->dao->cacheTag()->remember('navigation_' . $template_name, function () use ($template_name) { + $value = $this->dao->value($template_name ? ['template_name' => $template_name] : ['status' => 1, 'type' => 1], 'value'); + if (!$value) { + $value = $this->dao->value(['template_name' => 'default'], 'value'); + } + $navigation = []; + if ($value) { + $value = json_decode($value, true); + foreach ($value as $item) { + if (isset($item['name']) && strtolower($item['name']) === 'pagefoot') { + $navigation = $item; + break; + } + } + } + return $navigation; + }); + } + + /** + * 获取换色/分类数据 + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getColorChange(string $name) + { + $key = 'color_change_' . $name . '_3'; + return $this->dao->cacheStrRemember($key, function () use ($name) { + return $this->dao->value(['template_name' => $name, 'type' => 3], 'value'); + }); + } + + /** + * 取单个diy小程序预览二维码 + * @param int $id + * @return string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRoutineCode(int $id) + { + $diy = $this->dao->getOne(['id' => $id, 'is_del' => 0]); + if (!$diy) { + throw new AdminException('数据不存在'); + } + $type = $diy['status'] ? 8 : 6; + /** @var QrcodeServices $QrcodeService */ + $QrcodeService = app()->make(QrcodeServices::class); + $image = $QrcodeService->getRoutineQrcodePath($id, 0, $type, [], true); + return $image; + } + + /** + * 获取个人中心数据 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMemberData() + { + $key = 'diy_data_member_3'; + $info = $this->dao->cacheRemember($key, function () { + $info = $this->dao->get(['template_name' => 'member', 'type' => 3]); + if ($info) { + return $info->toArray(); + } else { + return null; + } + }); + if (!$info) { + throw new ValidateException('数据不存在'); + } + $value = json_decode($info['value'], true); + $data = []; + $data['status'] = (int)($value['status'] ?? 1); + $data['vip_type'] = (int)($value['vip_type'] ?? 1); + $data['newcomer_status'] = (int)($value['newcomer_status'] ?? 1); + $data['newcomer_style'] = (int)($value['newcomer_style'] ?? 1); + $data['order_status'] = $info['order_status'] ? (int)$info['order_status'] : 1; + $data['menu_status'] = $info['menu_status'] ? (int)$info['menu_status'] : 1; + $data['service_status'] = $info['service_status'] ? (int)$info['service_status'] : 1; + $data['my_banner_status'] = !!((int)$info['my_banner_status'] ?? 1); + $data['color_change'] = (int)$this->getColorChange('color_change'); + /** @var SystemGroupDataServices $systemGroupDataServices */ + $systemGroupDataServices = app()->make(SystemGroupDataServices::class); + /** @var SystemGroupServices $systemGroupServices */ + $systemGroupServices = app()->make(SystemGroupServices::class); + $menus_gid = $systemGroupServices->value(['config_name' => 'routine_my_menus'], 'id'); + $banner_gid = $systemGroupServices->value(['config_name' => 'routine_my_banner'], 'id'); + $routine_my_menus = $systemGroupDataServices->getGroupDataList(['gid' => $menus_gid]); + $routine_my_menus = $routine_my_menus['list'] ?? []; + $url = ['/kefu/mobile_list', '/pages/store_spread/index', '/pages/admin/order_cancellation/index', '/pages/admin/order/index']; + foreach ($routine_my_menus as &$item) { + if (!isset($item['type']) || !$item['type']) { + $item['type'] = in_array($item['url'], $url) ? 2 : 1; + } + } + $data['routine_my_menus'] = $routine_my_menus; + $routine_my_banner = $systemGroupDataServices->getGroupDataList(['gid' => $banner_gid]); + $routine_my_banner = $routine_my_banner['list'] ?? []; + $data['routine_my_banner'] = $routine_my_banner; + return $data; + } + + /** + * 保存个人中心数据配置 + * @param array $data + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function memberSaveData(array $data) + { + $key = 'diy_data_member_3'; + /** @var SystemGroupDataServices $systemGroupDataServices */ + $systemGroupDataServices = app()->make(SystemGroupDataServices::class); + if (!isset($data['status']) || !$data['status']) throw new AdminException('参数错误'); + $info = $this->dao->get(['template_name' => 'member', 'type' => 3]); + $routine_my_banner = $data['routine_my_banner'] ?? []; + $routine_my_menus = $data['routine_my_menus'] ?? []; + $value = ['status' => $data['status'], 'vip_type' => $data['vip_type'] ?? 1, 'newcomer_status' => $data['newcomer_status'] ?? 1, 'newcomer_style' => $data['newcomer_style'] ?? 1]; + $data['value'] = json_encode($value); + $data['my_banner_status'] = $data['my_banner_status'] && $data['routine_my_banner'] ? 1 : 0; + unset($data['status'], $data['routine_my_banner'], $data['routine_my_menus']); + if ($info) { + $data['update_time'] = time(); + if (!$this->dao->update($info['id'], $data)) { + throw new AdminException('编辑保存失败'); + } + $data = array_merge($info->toArray(), $data); + $this->dao->cacheUpdate($data, $key); + } else { + throw new AdminException('个人中心模板不存在'); + } + $systemGroupDataServices->saveAllData($routine_my_banner, 'routine_my_banner'); + $systemGroupDataServices->saveAllData($routine_my_menus, 'routine_my_menus'); + return true; + } + + /** + * 获取商品diy数据 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductDetailDiy() + { + $name = 'product_detail'; + $key = $this->cacheKey . '_' . $name; + $default = $this->productDetail; + return $this->dao->cacheTag()->remember($key, function () use ($name, $default) { + $info = $this->dao->get(['template_name' => $name, 'type' => 3]); + if ($info) { + $result = json_decode($info['value'], true); + $result = array_merge($default, array_intersect_key($result, $default)); + } else { + $result = $default; + } + return $result; + }); + } + + /** + * 保存商品diy数据 + * @param array $content + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveProductDetailDiy(array $content) + { + $name = 'product_detail'; + $key = $this->cacheKey . '_' . $name; + $info = $this->dao->get(['template_name' => $name, 'type' => 3]); + $data = []; + $data['value'] = json_encode($content); + $time = time(); + if ($info) { + $data['update_time'] = $time; + if (!$this->dao->update($info['id'], $data)) { + throw new AdminException('编辑保存失败'); + } + } else { + $data['template_name'] = 'product_detail'; + $data['type'] = 3; + $data['add_time'] = $time; + $this->dao->save($data); + } + $this->dao->cacheTag()->clear(); + return true; + } +} diff --git a/app/services/kefu/KefuServices.php b/app/services/kefu/KefuServices.php new file mode 100644 index 0000000..ae22bf8 --- /dev/null +++ b/app/services/kefu/KefuServices.php @@ -0,0 +1,230 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\kefu; + + +use app\services\BaseServices; +use app\dao\message\service\StoreServiceDao; +use app\services\message\service\StoreServiceAuxiliaryServices; +use app\services\message\service\StoreServiceServices; +use app\services\user\UserServices; +use app\services\wechat\WechatUserServices; +// use app\webscoket\SocketPush; +use think\exception\ValidateException; +use app\services\message\service\StoreServiceLogServices; +use app\services\message\service\StoreServiceRecordServices; +use think\facade\Log; + +/** + * Class KefuServices + * @package app\services\kefu + * @mixin StoreServiceDao + */ +class KefuServices extends BaseServices +{ + const GECJUROK = 'gElnUk'; + + /** + * KefuServices constructor. + * @param StoreServiceDao $dao + */ + public function __construct(StoreServiceDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取客服列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getServiceList(array $where, array $noId) + { + $where['status'] = 1; + $where['account_status'] = 1; + $where['noId'] = $noId; + $where['online'] = 1; + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getServiceList($where, $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取聊天记录 + * @param int $uid + * @param int $toUid + * @param int $upperId + * @param int $is_tourist + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getChatList(int $uid, int $toUid, int $upperId, int $is_tourist = 0) + { + /** @var StoreServiceLogServices $service */ + $service = app()->make(StoreServiceLogServices::class); + [$page, $limit] = $this->getPageValue(); + return array_reverse($service->tidyChat($service->getServiceChatList(['chat' => [$uid, $toUid], 'is_tourist' => $is_tourist], $limit, $upperId))); + } + + /** + * 转移客服 + * @param int $kfuUid + * @param int $uid + * @param int $toUid + * @return mixed + */ + public function setTransfer(int $kfuUid, int $uid, int $kfuToUid) + { + if ($uid === $kfuToUid) { + throw new ValidateException('自己不能转接给自己'); + } + /** @var StoreServiceAuxiliaryServices $auxiliaryServices */ + $auxiliaryServices = app()->make(StoreServiceAuxiliaryServices::class); + /** @var StoreServiceLogServices $service */ + $service = app()->make(StoreServiceLogServices::class); + $addTime = $auxiliaryServices->value(['binding_id' => $kfuUid, 'relation_id' => $uid], 'update_time'); + $list = $service->getMessageList(['chat' => [$kfuUid, $uid], 'add_time' => $addTime]); + $data = []; + foreach ($list as $item) { + if ($item['to_uid'] == $kfuUid) { + $item['to_uid'] = $kfuToUid; + } + if ($item['uid'] == $kfuUid) { + $item['uid'] = $kfuToUid; + } + $item['add_time'] = time(); + unset($item['id']); + $data[] = $item; + } + $record = $this->transaction(function () use ($data, $service, $kfuUid, $uid, $kfuToUid, $auxiliaryServices) { + if ($data) { + $num = count($data) - 1; + $messageData = $data[$num] ?? []; + $res = $service->saveAll($data); + } else { + $num = 0; + $res = true; + $messageData = []; + } + /** @var StoreServiceRecordServices $serviceRecord */ + $serviceRecord = app()->make(StoreServiceRecordServices::class); + $info = $serviceRecord->get(['user_id' => $kfuUid, 'to_uid' => $uid], ['type', 'message_type', 'is_tourist', 'avatar', 'nickname']); + $record = $serviceRecord->saveRecord($uid, $kfuToUid, $messageData['msn'] ?? '', $info['type'] ?? 1, $messageData['message_type'] ?? 1, $num, $info['is_tourist'] ?? 0, $info['nickname'] ?? "", $info['avatar'] ?? ''); + $res = $res && $auxiliaryServices->saveAuxliary(['binding_id' => $kfuUid, 'relation_id' => $uid]); + if (!$res && !$record) { + throw new ValidateException('转接客服失败'); + } + return $record; + }); + try { + if (!$record['is_tourist']) { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $_userInfo = $userService->getUserInfo($uid, 'nickname,avatar'); + $record['nickname'] = $_userInfo['nickname']; + $record['avatar'] = $_userInfo['avatar']; + } + $keufInfo = $this->dao->get(['uid' => $kfuUid], ['avatar', 'nickname']); + if ($keufInfo) { + $keufInfo = $keufInfo->toArray(); + } else { + $keufInfo = (object)[]; + } + //给转接的客服发送消息通知 + // SocketPush::kefu()->type('transfer')->to($kfuToUid)->data(['recored' => $record, 'kefuInfo' => $keufInfo])->push(); + //告知用户对接此用户聊天 + $keufToInfo = $this->dao->get(['uid' => $kfuToUid], ['avatar', 'nickname']); + // SocketPush::user()->type('to_transfer')->to($uid)->data(['toUid' => $kfuToUid, 'avatar' => $keufToInfo['avatar'] ?? '', 'nickname' => $keufToInfo['nickname'] ?? ''])->push(); + } catch (\Exception $e) { + } + return true; + } + + /** + * 关键字回复,没有默认关键词会自动发送给客服 + * @param string $reply + * @param string $openId + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function replyTransferService(string $reply, string $openId) + { + /** @var WechatUserServices $userServices */ + $userServices = app()->make(WechatUserServices::class); + $userInfo = $userServices->get(['openid' => $openId], ['uid', 'nickname', 'headimgurl as avatar']); + if (!$userInfo) { + return true; + } + /** @var StoreServiceServices $kfServices */ + $kfServices = app()->make(StoreServiceServices::class); + $serviceInfoList = $kfServices->getServiceList(['status' => 1, 'online' => 1]); + if (!count($serviceInfoList)) { + return true; + } + $uids = array_column($serviceInfoList['list'], 'uid'); + if (!$uids) { + return true; + } + /** @var StoreServiceRecordServices $recordServices */ + $recordServices = app()->make(StoreServiceRecordServices::class); + //上次聊天客服优先对话 + $toUid = $recordServices->getLatelyMsgUid(['to_uid' => $userInfo['uid']], 'user_id'); + //如果上次聊天的客不在当前客服中从新获取新的客服人员 + if (!in_array($toUid, $uids)) { + $toUid = 0; + } + if (!$toUid) { + mt_srand(); + $toUid = $uids[array_rand($uids)] ?? 0; + } + if (!$toUid) { + return true; + } + /** @var StoreServiceLogServices $logServices */ + $logServices = app()->make(StoreServiceLogServices::class); + $num = $logServices->getMessageNum(['uid' => $userInfo['uid'], 'to_uid' => $toUid, 'type' => 0, 'is_tourist' => 0]); + $record = $recordServices->saveRecord($userInfo['uid'], $toUid, $reply, 1, 1, $num, 0, $userInfo['nickname'] ?? "", $userInfo['avatar'] ?? ''); + + $data = [ + 'add_time' => time(), + 'is_tourist' => 0, + 'to_uid' => $toUid, + 'msn' => $reply, + 'uid' => $userInfo['uid'], + 'type' => 0 + ]; + $data = $logServices->save($data); + $data = $data->toArray(); + $data['_add_time'] = $data['add_time']; + $data['add_time'] = strtotime($data['add_time']); + $data['record'] = $record; + + try { + SocketPush::kefu()->type('mssage_num')->to($toUid)->data([ + 'uid' => $userInfo['uid'], + 'num' => $num, + 'recored' => $data['record'] + ])->push(); + SocketPush::admin()->to($toUid)->data($data)->type('reply')->push(); + } catch (\Throwable $e) { + Log::error('没有开启长连接无法推送消息,消息内容为:' . $reply); + } + } +} diff --git a/app/services/message/NoticeService.php b/app/services/message/NoticeService.php new file mode 100644 index 0000000..339167c --- /dev/null +++ b/app/services/message/NoticeService.php @@ -0,0 +1,108 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\message; + + +use app\jobs\notice\EnterpriseWechatJob; +use app\jobs\notice\PrintJob; +use app\services\activity\collage\UserCollagePartakeServices; +use app\services\activity\collage\UserCollageServices; +use app\services\BaseServices; +use app\services\order\StoreOrderCartInfoServices; +use app\services\order\StoreOrderServices; +use app\services\system\config\ConfigServices; +use app\services\wechat\WechatUserServices; +use crmeb\services\CacheService; +use think\exception\ValidateException; + + +class NoticeService extends BaseServices +{ + + /** + * 发送消息类型 + * @var array + */ +// protected $type = [ +// 'is_sms' => NoticeSmsService::class, +// 'is_system' => SystemSendServices::class, +// 'is_wechat' => WechatTemplateService::class, +// 'is_routine' => RoutineTemplateServices::class, +// 'is_ent_wechat' => EntWechatServices::class, +// ]; + + /** + * @var array + */ + protected $noticeInfo = []; + + /** + * @var string + */ + protected $event; + + /** + * @param string $event + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setEvent(string $event) + { + if ($this->event != $event) { + $this->noticeInfo = CacheService::redisHandler('NOTCEINFO')->remember('NOTCE_' . $event, function () use ($event) { + /** @var SystemNotificationServices $services */ + $services = app()->make(SystemNotificationServices::class); + $noticeInfo = $services->getOneNotce(['mark' => $event]); + if ($noticeInfo) { + return $noticeInfo->toArray(); + } else { + return []; + } + }); + $this->event = $event; + } + return $this; + } + + + /** + * @param array $notceinfo + * @param $data + * @param string $msgtype + */ + //企业微信群机器人 + public function EnterpriseWechatSend($data) + { + if ($this->noticeInfo['is_ent_wechat'] == 1 && $this->noticeInfo['url'] !== '') { + $url = $this->noticeInfo['url']; + $ent_wechat_text = $this->noticeInfo['ent_wechat_text']; + EnterpriseWechatJob::dispatchDo('doJob', [$data, $url, $ent_wechat_text]); + + } + } + + /** + * 根据UID,user_type获取openid + * @param int $uid + * @param string $userType + * @return mixed + */ + public function getOpenidByUid(int $uid, string $userType = 'wechat') + { + /** @var WechatUserServices $wechatServices */ + $wechatServices = app()->make(WechatUserServices::class); + return $wechatServices->uidToOpenid($uid, $userType); + } + + +} diff --git a/app/services/message/SystemMessageServices.php b/app/services/message/SystemMessageServices.php new file mode 100644 index 0000000..5229df0 --- /dev/null +++ b/app/services/message/SystemMessageServices.php @@ -0,0 +1,139 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\message; + +use app\dao\message\SystemMessageDao; +use app\services\BaseServices; +use think\exception\ValidateException; + +/** + * 站内信 + * Class SystemMessageServices + * @package app\services\message + * @mixin SystemMessageDao + */ +class SystemMessageServices extends BaseServices +{ + + /** + * SystemMessageServices constructor. + * @param SystemMessageDao $dao + */ + public function __construct(SystemMessageDao $dao) + { + $this->dao = $dao; + } + + /** + * 移动端消息列表 + * @param $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMessageSystemList($uid) + { + [$page, $limit] = $this->getPageValue(); + $where['is_del'] = 0; + $where['uid'] = $uid; + $list = $this->dao->getMessageList($where, '*', $page, $limit); + $count = $this->dao->getCount($where); + if (!$list) return ['list' => [], 'count' => 0]; + foreach ($list as &$item) { + $item['add_time'] = time_tran($item['add_time']); + } + $message = $this->dao->count(['uid' => $uid, 'look' => 0, 'is_del' => 0]); + return ['list' => $list, 'count' => $count, 'service_num' => $message]; + } + + /** + * 消息详情 + * @param int $id + * @param int $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getInfo(int $id, int $uid = 0) + { + $info = $this->dao->getOne(['id' => $id]); + if (!$info || $info['is_del'] == 1) { + throw new ValidateException('数据不存在'); + } + if ($uid && $info['uid'] != $uid) { + throw new ValidateException('数据不存在'); + } + $info = $info->toArray(); + if ($info['look'] == 0) { + $this->update($info['id'], ['look' => 1]); + } + $info['add_time'] = time_tran($info['add_time']); + return $info; + } + + /** + * 站内信发放 + * @param int $uid + * @param array $noticeInfo + * @return \crmeb\basic\BaseModel|\think\Model + */ + public function systemSend(int $uid, array $noticeInfo) + { + $data = []; + $data['mark'] = $noticeInfo['mark']; + $data['uid'] = $uid; + $data['title'] = $noticeInfo['title']; + $data['content'] = $noticeInfo['content']; + $data['type'] = 1; + $data['add_time'] = time(); + return $this->dao->save($data); + } + + /** + * 一键已读 + * @param int $uid + * @return bool + */ + public function setAllRead(int $uid) + { + if (!$this->dao->count(['uid' => $uid, 'is_del' => 0, 'look' => 0])) { + throw new ValidateException('暂无未读消息'); + } + $this->dao->update(['uid' => $uid], ['look' => 1]); + return true; + } + + /** + * 删除消息 + * @param int $uid + * @param array $ids + * @return bool + */ + public function setBatchDel(int $uid, array $ids = []) + { + if (!$this->dao->count(['uid' => $uid, 'is_del' => 0])) { + throw new ValidateException('暂无消息'); + } + $where = ['uid' => $uid]; + if ($ids) {//单个删除验证数据 + if (count($ids) == 1) $this->getInfo((int)$ids[0], $uid); + $where['id'] = $ids; + } + $this->dao->update($where, ['is_del' => 1]); + return true; + } + + +} diff --git a/app/services/message/notice/NoticeSmsService.php b/app/services/message/notice/NoticeSmsService.php new file mode 100644 index 0000000..27c86f4 --- /dev/null +++ b/app/services/message/notice/NoticeSmsService.php @@ -0,0 +1,143 @@ + +// +---------------------------------------------------------------------- +namespace app\services\message\notice; + +use app\jobs\notice\SmsJob; +use app\services\message\NoticeService; +use app\services\message\service\StoreServiceServices; +use app\services\store\SystemStoreStaffServices; +use app\services\system\admin\SystemAdminServices;use think\facade\Log; + +/** + * 短信发送消息列表 + * Created by PhpStorm. + * User: xurongyao <763569752@qq.com> + * Date: 2021/9/22 1:23 PM + */ +class NoticeSmsService extends NoticeService +{ + /** + * 判断是否开启权限 + * @var bool + */ + private $isopend = true; + + /** + * 是否开启权限 + * @param string $mark + * @return $this + */ + public function isOpen(string $mark) + { + $this->isopend = $this->noticeInfo['is_sms'] === 1; + return $this; + } + + /** + * 发送短信消息 + * @param $phone + * @param array $data + * @param string $template + * @return bool|void + */ + public function sendSms($phone, array $data, string $template) + { + try { + $this->isopend = $this->noticeInfo['is_sms'] === 1; + if ($this->isopend && $phone) { + SmsJob::dispatch('doJob', [$phone, $data, $template]); + } + } catch (\Exception $e) { + Log::error($e->getMessage()); + return true; + } + } + + /** + * 退款发送管理员消息任务 + * @param $order + * @return bool + */ + public function sendAdminRefund($order, $store_id = 0) + { + if ($store_id != 0) { + /** @var SystemStoreStaffServices $systemStoreStaffServices */ + $systemStoreStaffServices = app()->make(SystemStoreStaffServices::class); + $adminList = $systemStoreStaffServices->getNotifyStoreStaffList($store_id, 'phone,staff_name as nickname'); + } elseif (isset($order['supplier_id']) && $order['supplier_id']) { + /** @var SystemAdminServices $systemAdminServices */ + $systemAdminServices = app()->make(SystemAdminServices::class); + $adminList = $systemAdminServices->getNotifySupplierList((int)$order['supplier_id'], 'phone,real_name as nickname'); + } else { + /** @var StoreServiceServices $StoreServiceServices */ + $StoreServiceServices = app()->make(StoreServiceServices::class); + $adminList = $StoreServiceServices->getStoreServiceOrderNotice(); + } + if ($adminList) { + foreach ($adminList as $item) { + $data = ['order_id' => $order['order_id'], 'admin_name' => $item['nickname'] ?? '']; + $this->sendSms($item['phone'], $data, 'ADMIN_RETURN_GOODS_CODE'); + } + } + return true; + } + + + /** + * 用户确认收货管理员短信提醒 + * @param $switch + * @param $adminList + * @param $order + * @return bool + */ + public function sendAdminConfirmTakeOver($order) + { + /** @var StoreServiceServices $StoreServiceServices */ + $StoreServiceServices = app()->make(StoreServiceServices::class); + $adminList = $StoreServiceServices->getStoreServiceOrderNotice(); + foreach ($adminList as $item) { + $data = ['order_id' => $order['order_id'], 'admin_name' => $item['nickname']]; + $this->sendSms($item['phone'], $data, 'ADMIN_TAKE_DELIVERY_CODE'); + } + return true; + } + + /** + * 下单成功给客服管理员发送短信 + * @param $switch + * @param $adminList + * @param $order + * @return bool + */ + public function sendAdminPaySuccess($order, $store_id = 0) + { + if ($store_id != 0) { + /** @var SystemStoreStaffServices $systemStoreStaffServices */ + $systemStoreStaffServices = app()->make(SystemStoreStaffServices::class); + $adminList = $systemStoreStaffServices->getNotifyStoreStaffList($store_id, 'phone,staff_name as nickname'); + } elseif (isset($order['supplier_id']) && $order['supplier_id']) { + /** @var SystemAdminServices $systemAdminServices */ + $systemAdminServices = app()->make(SystemAdminServices::class); + $adminList = $systemAdminServices->getNotifySupplierList((int)$order['supplier_id'], 'phone,real_name as nickname'); + } else { + /** @var StoreServiceServices $StoreServiceServices */ + $StoreServiceServices = app()->make(StoreServiceServices::class); + $adminList = $StoreServiceServices->getStoreServiceOrderNotice(); + } + if ($adminList) { + foreach ($adminList as $item) { + $data = ['order_id' => $order['order_id'], 'admin_name' => $item['nickname'] ?? '']; + $this->sendSms($item['phone'], $data, 'ADMIN_PAY_SUCCESS_CODE'); + } + } + return true; + } +} diff --git a/app/services/message/service/StoreServiceAuxiliaryServices.php b/app/services/message/service/StoreServiceAuxiliaryServices.php new file mode 100644 index 0000000..cf68aaa --- /dev/null +++ b/app/services/message/service/StoreServiceAuxiliaryServices.php @@ -0,0 +1,59 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\message\service; + + +use app\dao\message\service\StoreServiceAuxiliaryDao; +use app\services\BaseServices; + +/** + * Class StoreServiceAuxiliaryServices + * @package app\services\message\service + * @mixin StoreServiceAuxiliaryDao + */ +class StoreServiceAuxiliaryServices extends BaseServices +{ + /** + * StoreServiceAuxiliaryServices constructor. + * @param StoreServiceAuxiliaryDao $dao + */ + public function __construct(StoreServiceAuxiliaryDao $dao) + { + $this->dao = $dao; + } + + /** + * 保存转接信息 + * @param array $data + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveAuxliary(array $data) + { + $auxliaryInfo = $this->dao->get(['type' => 0, 'binding_id' => $data['binding_id'], 'relation_id' => $data['relation_id']]); + if ($auxliaryInfo) { + $auxliaryInfo->update_time = time(); + return $auxliaryInfo->save(); + } else { + return $this->dao->save([ + 'type' => 0, + 'binding_id' => $data['binding_id'], + 'relation_id' => $data['relation_id'], + 'update_time' => time(), + 'add_time' => time(), + ]); + } + } + +} diff --git a/app/services/message/service/StoreServiceLogServices.php b/app/services/message/service/StoreServiceLogServices.php new file mode 100644 index 0000000..5e70061 --- /dev/null +++ b/app/services/message/service/StoreServiceLogServices.php @@ -0,0 +1,234 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\message\service; + + +use app\dao\message\service\StoreServiceLogDao; +use app\services\BaseServices; +use app\services\order\StoreOrderRefundServices; +use app\services\order\StoreOrderServices; +use app\services\product\product\StoreProductServices; + +/** + * 客服聊天记录 + * Class StoreServiceLogServices + * @package app\services\message\service + * @mixin StoreServiceLogDao + */ +class StoreServiceLogServices extends BaseServices +{ + /** + * 消息类型 + * @var array 1=文字 2=表情 3=图片 4=语音 5 = 商品链接 6 = 订单类型 + */ + const MSN_TYPE = [1, 2, 3, 4, 5, 6, 7]; + + /** + * 商品链接消息类型 + */ + const MSN_TYPE_GOODS = 5; + + /** + * 订单信息消息类型 + */ + const MSN_TYPE_ORDER = 6; + + /** + * 退款订单消息类型 + */ + const MSN_TYPE_REFUND_ORDER = 7; + + /** + * 构造方法 + * StoreServiceLogServices constructor. + * @param StoreServiceLogDao $dao + */ + public function __construct(StoreServiceLogDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取聊天记录中的uid和to_uid + * @param int $uid + * @return array + */ + public function getChatUserIds(int $uid) + { + $list = $this->dao->getServiceUserUids($uid); + $arr_user = $arr_to_user = []; + foreach ($list as $key => $value) { + array_push($arr_user, $value["uid"]); + array_push($arr_to_user, $value["to_uid"]); + } + $uids = array_merge($arr_user, $arr_to_user); + $uids = array_flip(array_flip($uids)); + $uids = array_flip($uids); + unset($uids[$uid]); + return array_flip($uids); + } + + /** + * 获取某个用户的客服聊天记录 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getChatLogList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getServiceList($where, $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取聊天记录列表 + * @param array $where + * @param int $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getChatList(array $where, int $uid) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getServiceList($where, $page, $limit); + return $this->tidyChat($list); + } + + /** + * 聊天列表格式化 + * @param array $list + * @param int $uid + * @return array + */ + public function tidyChat(array $list) + { + $productIds = $orderIds = $productList = $orderInfo = $toUser = $user = $orderIds_refund = []; + $toUid = $list[0]['to_uid'] ?? 0; + $uid = $list[0]['uid'] ?? 0; + foreach ($list as &$item) { + $item['_add_time'] = $item['add_time']; + $item['add_time'] = strtotime($item['_add_time']); + $item['productInfo'] = $item['orderInfo'] = []; + if ($item['msn_type'] == self::MSN_TYPE_GOODS && $item['msn']) { + $productIds[] = $item['msn']; + } elseif ($item['msn_type'] == self::MSN_TYPE_ORDER && $item['msn']) { + $orderIds[] = $item['msn']; + } elseif ($item['msn_type'] == self::MSN_TYPE_REFUND_ORDER && $item['msn']) { + $orderIds_refund[] = $item['msn']; + } + } + if ($productIds) { + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $where = [ + ['id', 'in', $productIds], + ['is_del', '=', 0], + ['is_show', '=', 1], + ]; + $productList = get_thumb_water($productServices->getProductArray($where, '*', 'id')); + } + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + if ($orderIds) { + $orderWhere = [ + ['order_id|unique', 'in', $orderIds], + ['is_del', '=', 0], + ]; + $orderInfo = $orderServices->getColumn($orderWhere, '*', 'order_id'); + } + /** @var StoreOrderRefundServices $orderRefundService */ + $orderRefundService = app()->make(StoreOrderRefundServices::class); + if ($orderIds_refund) { + $orderWhere = [ + ['order_id', 'in', $orderIds_refund], + ['is_del', '=', 0], + ]; + $orderInfo_refund = $orderRefundService->getColumn($orderWhere, '*', 'order_id'); + } + if ($toUid && $uid) { + /** @var StoreServiceRecordServices $recordServices */ + $recordServices = app()->make(StoreServiceRecordServices::class); + $toUser = $recordServices->get(['user_id' => $uid, 'to_uid' => $toUid], ['nickname', 'avatar']); + $user = $recordServices->get(['user_id' => $toUid, 'to_uid' => $uid], ['nickname', 'avatar']); + } + + foreach ($list as &$item) { + if ($item['msn_type'] == self::MSN_TYPE_GOODS && $item['msn']) { + $item['productInfo'] = $productList[$item['msn']] ?? []; + } elseif ($item['msn_type'] == self::MSN_TYPE_ORDER && $item['msn']) { + $order = $orderInfo[$item['msn']] ?? null; + if ($order) { + $order = $orderServices->tidyOrder($order, true, true); + $order['add_time_y'] = date('Y-m-d', $order['add_time']); + $order['add_time_h'] = date('H:i:s', $order['add_time']); + $item['orderInfo'] = $order; + } else { + $item['orderInfo'] = []; + } + } elseif ($item['msn_type'] == self::MSN_TYPE_REFUND_ORDER && $item['msn']) { + $order_refund = $orderInfo_refund[$item['msn']] ?? null; + if ($order_refund) { + $order_refund['cartInfo'] = json_decode($order_refund['cart_info'], true); + $order_refund['add_time_y'] = date('Y-m-d', $order_refund['add_time']); + $order_refund['add_time_h'] = date('H:i:s', $order_refund['add_time']); + $order_refund['total_num'] = $order_refund['refund_num']; + $order_refund['total_price'] = $order_refund['refund_price']; + $order_refund['pay_price'] = $order_refund['refund_price']; + $item['orderInfo'] = $order_refund; + } else { + $item['orderInfo'] = []; + } + } + $item['msn_type'] = (int)$item['msn_type']; + if (!isset($item['nickname'])) { + $item['nickname'] = ''; + } + if (!isset($item['avatar'])) { + $item['avatar'] = ''; + } + + if (!$item['avatar'] && !$item['nickname']) { + if ($item['uid'] == $uid && $item['to_uid'] == $toUid) { + $item['nickname'] = $user['nickname'] ?? ''; + $item['avatar'] = $user['avatar'] ?? ''; + } + if ($item['uid'] == $toUid && $item['to_uid'] == $uid) { + $item['nickname'] = $toUser['nickname'] ?? ''; + $item['avatar'] = $toUser['avatar'] ?? ''; + } + } + } + return $list; + } + + /** + * 获取聊天记录 + * @param array $where + * @param int $page + * @param int $limit + * @param bool $isUp + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getServiceChatList(array $where, int $limit, int $upperId) + { + return $this->dao->getChatList($where, $limit, $upperId); + } +} diff --git a/app/services/message/service/StoreServiceRecordServices.php b/app/services/message/service/StoreServiceRecordServices.php new file mode 100644 index 0000000..3958469 --- /dev/null +++ b/app/services/message/service/StoreServiceRecordServices.php @@ -0,0 +1,125 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\message\service; + + +use app\dao\message\service\StoreServiceRecordDao; +use app\services\BaseServices; +use crmeb\traits\ServicesTrait; + +/** + * Class StoreServiceRecordServices + * @package app\services\message\service + * @mixin StoreServiceRecordDao + */ +class StoreServiceRecordServices extends BaseServices +{ + + use ServicesTrait; + + /** + * StoreServiceRecordServices constructor. + * @param StoreServiceRecordDao $dao + */ + public function __construct(StoreServiceRecordDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取客服用户聊天列表 + * @param int $userId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getServiceList(int $userId, string $nickname, int $isTourist = 0) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getServiceList(['user_id' => $userId, 'title' => $nickname, 'is_tourist' => $isTourist], $page, $limit, ['user', 'service']); + foreach ($list as &$item) { + if ($item['message_type'] == 1) { + $item['message'] = substrUTf8($item['message'], '10', 'UTF-8', ''); + } + if (isset($item['kefu_nickname']) && $item['kefu_nickname']) { + $item['nickname'] = $item['kefu_nickname']; + } + if (isset($item['wx_nickname']) && $item['wx_nickname'] && !$item['nickname']) { + $item['nickname'] = $item['wx_nickname']; + } + if (isset($item['kefu_avatar']) && $item['kefu_avatar']) { + $item['avatar'] = $item['kefu_avatar']; + } + if (isset($item['wx_avatar']) && $item['wx_avatar'] && !$item['avatar']) { + $item['avatar'] = $item['wx_avatar']; + } + $item['_update_time'] = date('Y-m-d H:i', $item['update_time']); + } + return $list; + } + + /** + * 更新客服用户信息 + * @param int $uid + * @param array $data + * @return mixed + */ + public function updateRecord(array $where, array $data) + { + return $this->dao->update($where, $data); + } + + /** + * 写入聊天相关人数据 + * @param int $uid + * @param int $toUid + * @param string $message + * @param int $type + * @param int $messageType + * @param int $num + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveRecord(int $uid, int $toUid, string $message, int $type, int $messageType, int $num, int $isTourist = 0, string $nickname = '', string $avatar = '') + { + $info = $this->dao->get(['user_id' => $toUid, 'to_uid' => $uid]); + if ($info) { + $info->type = $type; + $info->message = $message; + $info->message_type = $messageType; + $info->update_time = time(); + $info->mssage_num = $num; + if ($avatar) $info->avatar = $avatar; + if ($nickname) $info->nickname = $nickname; + $info->save(); + $this->dao->update(['user_id' => $uid, 'to_uid' => $toUid], ['message' => $message, 'message_type' => $messageType]); + return $info->toArray(); + } else { + return $this->dao->save([ + 'user_id' => $toUid, + 'to_uid' => $uid, + 'type' => $type, + 'message' => $message, + 'avatar' => $avatar, + 'nickname' => $nickname, + 'message_type' => $messageType, + 'mssage_num' => $num, + 'add_time' => time(), + 'update_time' => time(), + 'is_tourist' => $isTourist + ])->toArray(); + } + } +} diff --git a/app/services/message/service/StoreServiceServices.php b/app/services/message/service/StoreServiceServices.php new file mode 100644 index 0000000..ce76244 --- /dev/null +++ b/app/services/message/service/StoreServiceServices.php @@ -0,0 +1,241 @@ + +// +---------------------------------------------------------------------- +namespace app\services\message\service; + + +use app\dao\message\service\StoreServiceDao; +use app\services\BaseServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; + +/** + * 客服 + * Class StoreServiceServices + * @package app\services\message\service + * @mixin StoreServiceDao + */ +class StoreServiceServices extends BaseServices +{ + use ServicesTrait; + + /** + * 创建form表单 + * @var Form + */ + protected $builder; + + /** + * 构造方法 + * StoreServiceServices constructor. + * @param StoreServiceDao $dao + * @param FormBuilder $builder + */ + public function __construct(StoreServiceDao $dao, FormBuilder $builder) + { + $this->dao = $dao; + $this->builder = $builder; + } + + /** + * 获取客服列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getServiceList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getServiceList($where, $page, $limit); + $prefix = config('admin.kefu_prefix'); + foreach ($list as &$item) { + if (!isset($item['workMember'])) { + $item['workMember'] = []; + } + if (isset($item['wx_name']) && !isset($item['nickname'])) { + $item['nickname'] = $item['wx_name']; + } + $item['prefix'] = $prefix; + } + $this->updateNonExistentService(array_column($list, 'uid')); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * @param array $uids + * @return bool + */ + public function updateNonExistentService(array $uids = []) + { + if (!$uids) { + return true; + } + /** @var UserServices $services */ + $services = app()->make(UserServices::class); + $userUids = $services->getColumn([['uid', 'in', $uids]], 'uid'); + $unUids = array_diff($uids, $userUids); + return $this->dao->deleteNonExistentService($unUids); + } + + /** + * 创建客服表单 + * @param array $formData + * @return mixed + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createServiceForm(array $formData = []) + { + if ($formData) { + $field[] = $this->builder->frameImage('avatar', '客服头像', $this->url(config('admin.admin_prefix') . '/widget.images/index', ['fodder' => 'avatar'], true), $formData['avatar'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + } else { + $field[] = $this->builder->frameImage('image', '商城用户', $this->url(config('admin.admin_prefix') . '/system.user/list', ['fodder' => 'image'], true))->icon('ios-add')->width('960px')->height('550px')->modal(['footer-hide' => true])->Props(['srcKey' => 'image']); + $field[] = $this->builder->hidden('uid', 0); + $field[] = $this->builder->hidden('avatar', ''); + } + $field[] = $this->builder->input('nickname', '客服名称', $formData['nickname'] ?? '')->col(24)->required(); + $field[] = $this->builder->input('phone', '手机号码', $formData['phone'] ?? '')->col(24)->required(); + if ($formData) { + $field[] = $this->builder->input('account', '客服账号', $formData['account'] ?? '')->col(24)->required(); + $field[] = $this->builder->input('password', '客服密码')->type('password')->col(24); + $field[] = $this->builder->input('true_password', '确认密码')->type('password')->col(24); + } else { + $field[] = $this->builder->input('account', '客服账号')->col(24)->required(); + $field[] = $this->builder->input('password', '客服密码')->type('password')->col(24)->required(); + $field[] = $this->builder->input('true_password', '确认密码')->type('password')->col(24)->required(); + } + $field[] = $this->builder->switches('account_status', '账号状态', (int)($formData['account_status'] ?? 0))->appendControl(1, [ + $this->builder->switches('status', '客服状态', (int)($formData['status'] ?? 0))->falseValue(0)->trueValue(1)->openStr('打开')->closeStr('关闭')->size('large'), + $this->builder->switches('customer', '手机订单管理', $formData['customer'] ?? 0)->falseValue(0)->trueValue(1)->openStr('打开')->closeStr('关闭')->size('large'), + $this->builder->switches('notify', '订单通知', $formData['notify'] ?? 0)->falseValue(0)->trueValue(1)->openStr('打开')->closeStr('关闭')->size('large'), + ])->falseValue(0)->trueValue(1)->openStr('开启')->closeStr('关闭')->size('large'); + return $field; + } + + /** + * 创建客服获取表单 + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function create() + { + return create_form('添加客服', $this->createServiceForm(), $this->url('/app/wechat/kefu'), 'POST'); + } + + /** + * 编辑获取表单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function edit(int $id) + { + $serviceInfo = $this->dao->get($id); + if (!$serviceInfo) { + throw new AdminException('数据不存在!'); + } + return create_form('编辑客服', $this->createServiceForm($serviceInfo->toArray()), $this->url('/app/wechat/kefu/' . $id), 'PUT'); + } + + /** + * 获取某人的聊天记录用户列表 + * @param int $uid + * @return array|array[] + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getChatUser(int $uid) + { + /** @var StoreServiceLogServices $serviceLog */ + $serviceLog = app()->make(StoreServiceLogServices::class); + /** @var UserServices $serviceUser */ + $serviceUser = app()->make(UserServices::class); + $uids = $serviceLog->getChatUserIds($uid); + if (!$uids) { + return []; + } + return $serviceUser->getUserList(['uid' => $uids], 'nickname,uid,avatar as headimgurl'); + } + + /** + * 检查用户是否是客服 + * @param array $where + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkoutIsService(array $where) + { + return (bool)$this->dao->count($where); + } + + /** + * 查询聊天记录和获取客服uid + * @param int $uid 当前用户uid + * @param int $uidTo 上翻页id + * @param int $limit 展示条数 + * @param int $toUid 客服uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRecord(int $uid, int $uidTo, int $limit = 10, int $toUid = 0) + { + if (!$toUid) { + $serviceInfoList = $this->getServiceList(['noId' => [$uid], 'status' => 1, 'account_status' => 1, 'online' => 1]); + if (!count($serviceInfoList)) { + throw new ValidateException('暂无客服人员在线,请稍后联系'); + } + $uids = array_column($serviceInfoList['list'], 'uid'); + if (!$uids) { + throw new ValidateException('暂无客服人员在线,请稍后联系'); + } + /** @var StoreServiceRecordServices $recordServices */ + $recordServices = app()->make(StoreServiceRecordServices::class); + //上次聊天客服优先对话 + $toUid = $recordServices->getLatelyMsgUid(['to_uid' => $uid], 'user_id'); + //如果上次聊天的客不在当前客服中从新 + if (!in_array($toUid, $uids)) { + $toUid = 0; + } + if (!$toUid) { + mt_srand(); + $toUid = $uids[array_rand($uids)] ?? 0; + } + if (!$toUid) { + throw new ValidateException('暂无客服人员在线,请稍后联系'); + } + } + $userInfo = $this->dao->get(['uid' => $toUid], ['nickname', 'avatar']); + if (!$userInfo) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->get(['uid' => $toUid], ['nickname', 'avatar']); + if (!$userInfo) { + $userInfo['nickname'] = ''; + $userInfo['avatar'] = ''; + } + } + /** @var StoreServiceLogServices $logServices */ + $logServices = app()->make(StoreServiceLogServices::class); + $result = ['serviceList' => [], 'uid' => $toUid, 'nickname' => $userInfo['nickname'], 'avatar' => $userInfo['avatar']]; + $serviceLogList = $logServices->getServiceChatList(['chat' => [$uid, $toUid], 'is_tourist' => 0], $limit, $uidTo); + $result['serviceList'] = array_reverse($logServices->tidyChat($serviceLogList)); + return $result; + } +} diff --git a/app/services/message/sms/SmsRecordServices.php b/app/services/message/sms/SmsRecordServices.php new file mode 100644 index 0000000..2a8bde9 --- /dev/null +++ b/app/services/message/sms/SmsRecordServices.php @@ -0,0 +1,72 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\message\sms; + +use app\common\dao\system\sms\SmsRecordDao; +use app\services\BaseServices; +use app\services\serve\ServeServices; + +/** + * 短信发送记录 + * Class SmsRecordServices + * @package app\services\message\sms + * @mixin SmsRecordDao + */ +class SmsRecordServices extends BaseServices +{ + /** + * 构造方法 + * SmsRecordServices constructor. + * @param SmsRecordDao $dao + */ + public function __construct(SmsRecordDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取短信发送列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRecordList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $data = $this->dao->getRecordList($where, $page, $limit); + $count = $this->dao->count($where); + return compact('data', 'count'); + } + + /** + * 修改短信发送记录短信状态 + */ + public function modifyResultCode() + { + $recordIds = $this->dao->getCodeNull(); + if (count($recordIds)) { + /** @var ServeServices $smsHandle */ + $smsHandle = app()->make(ServeServices::class); + $codeLists = $smsHandle->sms()->getStatus($recordIds); + foreach ($codeLists as $item) { + if (isset($item['id']) && isset($item['resultcode'])) { + if ($item['resultcode'] == '' || $item['resultcode'] == null) $item['resultcode'] = 134; + $this->dao->update($item['id'], ['resultcode' => $item['resultcode']], 'record_id'); + } + } + return true; + } + return true; + } +} diff --git a/app/services/message/sms/SmsSendServices.php b/app/services/message/sms/SmsSendServices.php new file mode 100644 index 0000000..3465fe0 --- /dev/null +++ b/app/services/message/sms/SmsSendServices.php @@ -0,0 +1,48 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\message\sms; + +use app\services\BaseServices; +use app\services\serve\ServeServices; +use think\exception\ValidateException; + +/** + * 短信发送 + * Class SmsSendServices + * @package app\services\message\sms + */ +class SmsSendServices extends BaseServices +{ + /** + * 发送短信 + * @param bool $switch + * @param $phone + * @param array $data + * @param string $template + * @return bool + */ + public function send(bool $switch, $phone, array $data, string $template) + { + if ($switch && $phone) { + /** @var ServeServices $services */ + $services = app()->make(ServeServices::class); + $res = $services->sms()->send($phone, $template, $data); + if ($res === false) { + throw new ValidateException($services->getError()); + } + return true; + } else { + return false; + } + } + +} diff --git a/app/services/message/wechat/MessageServices.php b/app/services/message/wechat/MessageServices.php new file mode 100644 index 0000000..6d51036 --- /dev/null +++ b/app/services/message/wechat/MessageServices.php @@ -0,0 +1,467 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\message\wechat; + + +use app\services\activity\bargain\StoreBargainServices; +use app\services\activity\combination\StoreCombinationServices; +use app\services\activity\combination\StorePinkServices; +use app\services\activity\seckill\StoreSeckillServices; +use app\services\BaseServices; +use app\services\other\QrcodeServices; +use app\services\product\product\StoreProductServices; +use app\services\user\LoginServices; +use app\services\user\UserCardServices; +use app\services\user\UserServices; +use app\services\wechat\WechatQrcodeServices; +use app\services\wechat\WechatReplyServices; +use app\services\wechat\WechatUserServices; +use crmeb\services\CacheService; +use crmeb\services\SystemConfigService; +use crmeb\services\wechat\Messages; +use crmeb\services\wechat\OfficialAccount; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\Kernel\Exceptions\RuntimeException; +use EasyWeChat\Kernel\Messages\Image; +use EasyWeChat\Kernel\Messages\News; +use EasyWeChat\Kernel\Messages\Text; +use EasyWeChat\Kernel\Messages\Transfer; +use EasyWeChat\Kernel\Messages\Voice; +use think\facade\Log; + +/** + * Class MessageServices + * @package app\services\message\wechat + */ +class MessageServices extends BaseServices +{ + /** + * 事件处理 + * @param $qrInfo + * @param $openid + * @return array|Image|News|Text|Transfer|Voice|string + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws RuntimeException + */ + public function wechatEvent($qrInfo, $openid) + { + $response = Messages::transfer(); + $thirdType = explode('-', $qrInfo['third_type']); + $baseUrl = sys_config('site_url'); + switch (strtolower($thirdType[0])) { + case 'spread': + try { + $spreadUid = $qrInfo['third_id']; + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + $uid = $wechatUser->getFieldValue($openid, 'openid', 'uid'); + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $userInfo = $userService->get($uid); + if ($userInfo && $spreadUid != $uid && !$userInfo['spread_uid']) { + /** @var LoginServices $loginService */ + $loginService = app()->make(LoginServices::class); + if ($loginService->updateUserInfo(['spread_uid' => $spreadUid], $userInfo)) { + $response = '绑定推荐人失败!'; + } + } + $data = SystemConfigService::more(['site_name', 'site_logo', 'site_name']); + $wechatNews['title'] = $data['site_name'] ?? ''; + $wechatNews['image'] = $data['site_logo'] ?? ''; + $wechatNews['description'] = $data['site_name'] ?? ''; + $wechatNews['url'] = $baseUrl . '/pages/index/index'; + $messages = Messages::newMessage($wechatNews); + OfficialAccount::staffService()->message($messages)->to($openid)->send(); + } catch (\Exception $e) { + $response = $e->getMessage(); + } + break; + case 'reply': + /** @var WechatReplyServices $replyServices */ + $replyServices = app()->make(WechatReplyServices::class); + $data = $replyServices->get($qrInfo['third_id']); + if ($data) { + $response = $replyServices->replyDataByMessage($data->toArray()); + } + break; + case 'product': + /** @var StoreProductServices $productService */ + $productService = app()->make(StoreProductServices::class); + $productInfo = $productService->get($thirdType[1] ?? 0); + $wechatNews['title'] = $productInfo->store_name; + $wechatNews['image'] = $productInfo->image; + $wechatNews['description'] = $productInfo->store_info; + $wechatNews['url'] = $baseUrl . '/pages/goods_details/index?id=' . $thirdType[1]; + $messages = Messages::newMessage($wechatNews); + OfficialAccount::staffService()->message($messages)->to($openid)->send(); + break; + case 'combination': + /** @var StoreCombinationServices $combinationService */ + $combinationService = app()->make(StoreCombinationServices::class); + $productInfo = $combinationService->get($thirdType[1] ?? 0); + $wechatNews['title'] = $productInfo->title; + $wechatNews['image'] = $productInfo->image; + $wechatNews['description'] = $productInfo->info; + $wechatNews['url'] = $baseUrl . '/pages/activity/goods_combination_details/index?id=' . $thirdType[1]; + $messages = Messages::newMessage($wechatNews); + OfficialAccount::staffService()->message($messages)->to($openid)->send(); + break; + case 'seckill': + /** @var StoreSeckillServices $seckillService */ + $seckillService = app()->make(StoreSeckillServices::class); + $productInfo = $seckillService->get($thirdType[1] ?? 0); + $wechatNews['title'] = $productInfo->title; + $wechatNews['image'] = $productInfo->image; + $wechatNews['description'] = $productInfo->info; + $wechatNews['url'] = $baseUrl . '/pages/activity/goods_seckill_details/index?id=' . $thirdType[1] . '&time=' . $thirdType[2] . '&status=' . $thirdType[3]; + $messages = Messages::newMessage($wechatNews); + OfficialAccount::staffService()->message($messages)->to($openid)->send(); + break; + case 'bargain': + /** @var StoreBargainServices $bargainService */ + $bargainService = app()->make(StoreBargainServices::class); + $productInfo = $bargainService->get($thirdType[1] ?? 0); + $wechatNews['title'] = $productInfo->title; + $wechatNews['image'] = $productInfo->image; + $wechatNews['description'] = $productInfo->info; + $wechatNews['url'] = $baseUrl . '/pages/activity/goods_bargain_details/index?id=' . $thirdType[1] . '&bargain=' . $thirdType[2]; + $messages = Messages::newMessage($wechatNews); + OfficialAccount::staffService()->message($messages)->to($openid)->send(); + break; + case 'pink': + /** @var StorePinkServices $pinkService */ + $pinkService = app()->make(StorePinkServices::class); + /** @var StoreCombinationServices $combinationService */ + $combinationService = app()->make(StoreCombinationServices::class); + $pinktInfo = $pinkService->get($thirdType[1]); + $productInfo = $combinationService->get($pinktInfo->cid); + $wechatNews['title'] = $productInfo->title; + $wechatNews['image'] = $productInfo->image; + $wechatNews['description'] = $productInfo->info; + $wechatNews['url'] = $baseUrl . '/pages/activity/goods_combination_status/index?id=' . $thirdType[1]; + $messages = Messages::newMessage($wechatNews); + OfficialAccount::staffService()->message($messages)->to($openid)->send(); + break; + case 'lucklottery': + try { + $lottery = $qrInfo['lottery'] ?? []; + $wechatNews['title'] = $lottery['name'] ?? '关注成功,立即参与抽奖'; + $wechatNews['image'] = $lottery['image'] ?? ''; + $wechatNews['description'] = $lottery['name'] ?? '关注成功,获得一次抽奖机会'; + $wechatNews['url'] = $baseUrl . '/pages/goods/lottery/grids/index?type=5&spread=' . $thirdType[1] ?? ''; + $messages = Messages::newMessage($wechatNews); + OfficialAccount::staffService()->message($messages)->to($openid)->send(); + } catch (\Exception $e) { + \think\facade\Log::error('发送关注抽奖失败:' . $e->getMessage()); + $response = $e->getMessage(); + } + break; + case 'wechatqrcode'://渠道码 + /** @var WechatQrcodeServices $wechatQrcodeService */ + $wechatQrcodeService = app()->make(WechatQrcodeServices::class); + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + /** @var LoginServices $loginService */ + $loginService = app()->make(LoginServices::class); + try { + //wechatqrcode类型的二维码数据中,third_id为渠道码的id + $qrcodeInfo = $wechatQrcodeService->qrcodeInfo($qrInfo['third_id']); + $spreadUid = $qrcodeInfo['uid']; + $spreadInfo = $userService->get($spreadUid); + $is_new = $wechatUser->saveUser($openid); + $uid = $wechatUser->getFieldValue($openid, 'openid', 'uid', ['user_type', '<>', 'h5']); + $userInfo = $userService->get($uid); + + if ($qrcodeInfo['status'] == 0 || $qrcodeInfo['is_del'] == 1 || ($qrcodeInfo['end_time'] < time() && $qrcodeInfo['end_time'] > 0)) { + $response = '二维码已失效'; + } else if ($spreadUid == $uid) { + $response = '自己不能推荐自己'; + } else if (!$userInfo) { + $response = '用户不存在'; + } else if (!$spreadInfo) { + $response = '上级用户不存在'; + } else if ($loginService->updateUserInfo(['code' => $spreadUid], $userInfo, $is_new)) { + //写入扫码记录,返回内容 + $response = $wechatQrcodeService->wechatQrcodeRecord($qrcodeInfo, $userInfo, $spreadInfo, 1); + } + } catch (\Exception $e) { + $response = $e->getMessage(); + } + break; + } + return $response; + } + + /** + * 扫码发送图文消息 + * @param $title + * @param $image + * @param $info + * @param $url + * @param $openId + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws RuntimeException + */ + public function sendMessage($title, $image, $info, $url, $openId) + { + $wechatNews['title'] = $title; + $wechatNews['image'] = $image; + $wechatNews['description'] = $info; + $wechatNews['url'] = $url; + $message = Messages::newMessage($wechatNews); + OfficialAccount::staffService()->message($message)->to($openId)->send(); + } + + /** + * 扫码 + * @param $message + * @return array|Image|News|Text|Transfer|Voice|string + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws RuntimeException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function wechatEventScan($message) + { + /** @var QrcodeServices $qrcodeService */ + $qrcodeService = app()->make(QrcodeServices::class); + /** @var WechatReplyServices $wechatReplyService */ + $wechatReplyService = app()->make(WechatReplyServices::class); + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + /** @var LoginServices $loginService */ + $loginService = app()->make(LoginServices::class); + + $qrInfo = []; + $response = $wechatReplyService->reply('subscribe', $message['FromUserName']); + if ($message['EventKey'] && ($qrInfo = $qrcodeService->getQrcode($message['Ticket'], 'ticket'))) { + $qrcodeService->scanQrcode($message['Ticket'], 'ticket'); + $response = $this->wechatEvent($qrInfo, $message['FromUserName']); + } + if ($qrInfo && strtolower($qrInfo['third_type']) == 'wechatqrcode') { + /** @var WechatQrcodeServices $wechatQrcodeService */ + $wechatQrcodeService = app()->make(WechatQrcodeServices::class); + try { + //wechatqrcode类型的二维码数据中,third_id为渠道码的id + $qrcodeInfo = $wechatQrcodeService->qrcodeInfo($qrInfo['third_id']); + $spreadUid = $qrcodeInfo['uid']; + $spreadInfo = $userService->get($spreadUid); + $is_new = $wechatUser->saveUser($message['FromUserName']); + $uid = $wechatUser->getFieldValue($message['FromUserName'], 'openid', 'uid', ['user_type', '<>', 'h5']); + $userInfo = $userService->get($uid); + + if ($qrcodeInfo['status'] == 0 || $qrcodeInfo['is_del'] == 1 || ($qrcodeInfo['end_time'] < time() && $qrcodeInfo['end_time'] > 0)) { + $response = '二维码已失效'; + } else if ($spreadUid == $uid) { + $response = '自己不能推荐自己'; + } else if (!$userInfo) { + $response = '用户不存在'; + } else if (!$spreadInfo) { + $response = '上级用户不存在'; + } else if ($loginService->updateUserInfo(['code' => $spreadUid], $userInfo, $is_new)) { + //写入扫码记录,返回内容 + $response = $wechatQrcodeService->wechatQrcodeRecord($qrcodeInfo, $userInfo, $spreadInfo, 1); + } + } catch (\Exception $e) { + $response = $e->getMessage(); + } + } + $ticket = $message['EventKey']; + if (strpos($ticket, 'wechat_scan_login:') === 0) { + $openId = $message['FromUserName']; + $key = str_replace('wechat_scan_login:', '', $ticket); + /** @var WechatUserServices $wechatUserSerives */ + $wechatUserSerives = app()->make(WechatUserServices::class); + $wechatUser = $wechatUserSerives->getWechatUserInfo(['openid' => $openId, 'is_del' => 0]); + if ($wechatUser) { + CacheService::set('wechat_scan_login:' . $key, $wechatUser['uid']); + } + + } + return $response; + } + + /** + * 取消关注 + * @param $message + */ + public function wechatEventUnsubscribe($message) + { + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + $wechatUser->unSubscribe($message['FromUserName']); + } + + /** + * 公众号关注 + * @param $message + * @param $spread_uid + * @return array|Image|News|Text|Transfer|Voice|string + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws RuntimeException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function wechatEventSubscribe($message, $spread_uid) + { + /** @var WechatReplyServices $wechatReplyService */ + $wechatReplyService = app()->make(WechatReplyServices::class); + $response = $wechatReplyService->reply('subscribe', $message['FromUserName']); + if (isset($message['EventKey'])) { + /** @var QrcodeServices $qrcodeService */ + $qrcodeService = app()->make(QrcodeServices::class); + if ($message['EventKey'] && ($qrInfo = $qrcodeService->getQrcode($message['Ticket'], 'ticket'))) { + $qrcodeService->scanQrcode($message['Ticket'], 'ticket'); + $response = $this->wechatEvent($qrInfo, $message['FromUserName']); + } + } + //是否开启 + if (sys_config('create_wechat_user', 1)) { + try { + /** @var WechatUserServices $wechatUserSerives */ + $wechatUserSerives = app()->make(WechatUserServices::class); + $wechatUserSerives->saveUser($message['FromUserName'], $spread_uid); + } catch (\Throwable $e) { + Log::error('关注公众号生成用户失败,原因:' . $e->getMessage() . $e->getFile() . $e->getLine()); + } + } + return $response; + } + + /** + * 位置 事件 + * @param $message + * @return string + */ + public function wechatEventLocation($message) + { + //return 'location'; + } + + /** + * 跳转URL 事件 + * @param $message + * @return string + */ + public function wechatEventView($message) + { + //return 'view'; + } + + /** + * 图片 消息 + * @param $message + * @return string + */ + public function wechatMessageImage($message) + { + //return 'image'; + } + + /** + * 语音 消息 + * @param $message + * @return string + */ + public function wechatMessageVoice($message) + { + //return 'voice'; + } + + /** + * 视频 消息 + * @param $message + * @return string + */ + public function wechatMessageVideo($message) + { + //return 'video'; + } + + /** + * 位置 消息 + */ + public function wechatMessageLocation($message) + { + //return 'location'; + } + + /** + * 链接 消息 + * @param $message + * @return string + */ + public function wechatMessageLink($message) + { + //return 'link'; + } + + /** + * 其它消息 消息 + */ + public function wechatMessageOther($message) + { + //return 'other'; + } + + /** + * 领取卡券 + * @param $message + */ + public function wechatEventUserGetCard($message) + { + try { + /** @var UserCardServices $userCardServices */ + $userCardServices = app()->make(UserCardServices::class); + $userCardServices->userGetCard($message); + } catch (\Throwable $e) { + Log::error('领取微信卡券失败,原因:' . $e->getMessage() . $e->getFile() . $e->getLine()); + } + } + + /** + * 激活卡券 + * @param $message + */ + public function wechatEventSubmitMembercardUserInfo($message) + { + try { + /** @var UserCardServices $userCardServices */ + $userCardServices = app()->make(UserCardServices::class); + $userCardServices->userSubmitCard($message); + } catch (\Throwable $e) { + Log::error('激活微信卡券失败,原因:' . $e->getMessage() . $e->getFile() . $e->getLine()); + } + } + + /** + * 删除卡券 + * @param $message + */ + public function wechatEventUserDelCard($message) + { + /** @var UserCardServices $userCardServices */ + $userCardServices = app()->make(UserCardServices::class); + $userCardServices->userDelCard($message); + } +} diff --git a/app/services/order/OtherOrderServices.php b/app/services/order/OtherOrderServices.php new file mode 100644 index 0000000..a29d9f2 --- /dev/null +++ b/app/services/order/OtherOrderServices.php @@ -0,0 +1,678 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order; + + +use app\dao\order\OtherOrderDao; +use app\services\BaseServices; +use app\services\pay\OrderPayServices; +use app\services\pay\PayServices; +use app\services\statistic\TradeStatisticServices; +use app\services\store\finance\StoreFinanceFlowServices; +use app\services\user\member\MemberShipServices; +use app\services\user\UserServices; +use app\services\user\member\MemberCardServices; +use crmeb\services\AliPayService; +use crmeb\services\wechat\Payment; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; +use app\jobs\user\MicroPayOrderJob; + +/** + * Class OtherOrderServices + * @package app\services\order + * @mixin OtherOrderDao + */ +class OtherOrderServices extends BaseServices +{ + + use ServicesTrait; + + /** + * 订单类型 + * @var string[] + */ + protected $type = [ + 0 => '免费领取', + 1 => '购买会员卡', + 2 => '卡密激活', + 3 => '收银订单', + 4 => '赠送' + ]; + + /** + * 初始化,获得dao层句柄 + * OtherOrderServices constructor. + * @param OtherOrderDao $dao + */ + public function __construct(OtherOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * @param int $storeId + * @return int + */ + public function getvipOrderCount(int $storeId) + { + return $this->dao->count(['store_id' => $storeId, 'paid' => 1, 'type' => [0, 1, 2, 4]]); + } + + /** + * 生成会员购买订单数据 + * @param array $data + * @param int $type + * @param string $changeType + * @return \crmeb\basic\BaseModel|\think\Model + */ + public function addOtherOrderData(array $data, int $type = 1, string $changeType = 'create_member_order') + { + if (!$data) throw new ValidateException('数据不能为空'); + $add = [ + 'uid' => $data['uid'], + 'store_id' => $data['store_id'] ?? 0, + 'staff_id' => $data['staff_id'] ?? 0, + 'type' => $data['type'] ?? 1, + 'order_id' => $data['order_id'], + 'channel_type' => $data['channel_type'], + 'pay_type' => $data['pay_type'] ?? 0, + 'member_type' => $data['member_type'] ?? 0, + 'member_price' => $data['member_price'] ?? 0.00, + 'pay_price' => $data['pay_price'] ?? 0.00, + 'code' => $data['member_code'] ?? '', + 'vip_day' => $data['vip_day'] ?? 0, + 'is_permanent' => $data['is_permanent'] ?? 0, + 'is_free' => $data['is_free'] ?? 0, + 'overdue_time' => $data['overdue_time'] ?? 0, + 'status' => 0, + 'paid' => $data['paid'] ?? 0, + 'pay_time' => $data['pay_time'] ?? 0, + 'money' => $data['money'] ?? 0, + 'add_time' => time(), + ]; + $res = $this->dao->save($add); + if (!$res) { + throw new ValidateException('订单创建失败'); + } + /** @var OtherOrderStatusServices $statusService */ + $statusService = app()->make(OtherOrderStatusServices::class); + $statusService->save([ + 'oid' => $res['id'], + 'change_type' => $changeType, + 'change_message' => '订单生成', + 'change_time' => time(), + 'shop_type' => $type, + ]); + return $res; + } + + /** + * 能否领取免费 + * @param int $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function isCanGetFree(int $uid) + { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + /** @var MemberShipServices $memberShipService */ + $memberShipService = app()->make(MemberShipServices::class); + /** @var TradeStatisticServices $tradeService */ + $tradeService = app()->make(TradeStatisticServices::class); + /** @var StoreOrderEconomizeServices $economizeService */ + $economizeService = app()->make(StoreOrderEconomizeServices::class); + $freeDay = $memberShipService->getVipDay(['type' => "free"]); + $freeConfig = array(); + $freeConfig['price'] = 0; + $freeConfig['pre_price'] = 0; + $freeConfig['title'] = "免费会员"; + $freeConfig['type'] = "free"; + $freeConfig['vip_day'] = $freeDay ? $freeDay : 0; + $userInfo = $userService->get($uid); + if ($freeConfig) { + $freeConfig['is_record'] = 0; + $record = $this->dao->getOneByWhere(['uid' => $uid, 'is_free' => 1]); + if ($record) { + $freeConfig['is_record'] = 1; + } + } + $registerTime = $tradeService->TimeConvert(['start_time' => date('Y-m-d H:i:s', $userInfo['add_time']), 'end_time' => date('Y-m-d H:i:s', time())]); + $userInfo['register_days'] = $registerTime['days']; + $userInfo['economize_money'] = $economizeService->sumEconomizeMoney($uid); + $userInfo['shop_name'] = sys_config('site_name'); + $freeConfig['user_info'] = $userInfo; + return $freeConfig; + } + + + /** + * 查询会员卡订单数据 + * @param array $where + * @param string $field + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOne(array $where, string $field = '*') + { + return $this->dao->getOne($where, $field); + } + + /** + * 创建订单 + * @param int $uid + * @param int $memberId + * @param string $payPrice + * @param string $channelType + * @param string $payType + * @param $type + * @param int $store_id + * @param int $staff_id + * @return \crmeb\basic\BaseModel|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createOrder(int $uid, int $memberId, string $payPrice = '0', string $channelType, string $payType = 'weixin', $type = 1, int $store_id = 0, int $staff_id = 0) + { + /** @var StoreOrderCreateServices $storeOrderCreateService */ + $storeOrderCreateService = app()->make(StoreOrderCreateServices::class); + $orderInfo = [ + 'uid' => $uid, + 'order_id' => $storeOrderCreateService->getNewOrderId('hy'), + 'pay_type' => $payType, + 'channel_type' => $channelType, + 'member_code' => "", + 'store_id' => $store_id, + 'staff_id' => $staff_id + ]; + if ($type != 3) { //区别 0:免费领取会员 1:购买会员 2:卡密领取会员 3:线下付款 + [$memberPrice, $isFree, $isPermanent, $overdueTime, $memberInfo] = $this->checkPayMemberType($uid, $memberId); + $orderInfo['member_price'] = $memberPrice; + $orderInfo['pay_price'] = $memberInfo['pre_price']; + $orderInfo['money'] = $memberPrice; + $orderInfo['vip_day'] = $memberInfo['vip_day']; + $orderInfo['member_type'] = $memberInfo['id']; + $orderInfo['overdue_time'] = $overdueTime; + $orderInfo['is_permanent'] = $isPermanent; + $orderInfo['is_free'] = $isFree; + $orderInfo['type'] = $type; + $changeType = "create_member_order"; + } else { + $orderInfo['type'] = $type; + $orderInfo['member_code'] = ""; + $changeType = "create_offline_scan_order"; + $orderInfo['money'] = $payPrice; + } + return $this->addOtherOrderData($orderInfo, $type, $changeType); + } + + /** + * + * @param int $uid + * @param $price + * @param $merberId + * @param $type + * @param $from + * @param array $staffinfo + * @param string $authCode 扫码code + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public function payMember(int $uid, int $memberId, float $price, int $payType, string $from, array $staffinfo = [], string $authCode = '') + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('用户数据不存在'); + } + if (!$staffinfo) { + throw new ValidateException('请稍后重试'); + } + /** @var StoreOrderCreateServices $storeOrderCreateService */ + $storeOrderCreateService = app()->make(StoreOrderCreateServices::class); + $orderInfo = [ + 'uid' => $uid, + 'order_id' => $storeOrderCreateService->getNewOrderId('hy'), + 'channel_type' => $user['user_type'], + 'member_code' => "", + 'store_id' => $staffinfo['store_id'], + 'staff_id' => $staffinfo['id'] + ]; + [$memberPrice, $isFree, $isPermanent, $overdueTime, $memberInfo] = $this->checkPayMemberType($uid, $memberId); + $type = $memberInfo['type'] == 'free' ? 0 : 1; + $orderInfo['member_price'] = $memberPrice; + $orderInfo['pay_price'] = $memberInfo['pre_price']; + $orderInfo['money'] = $memberPrice; + $orderInfo['vip_day'] = $memberInfo['vip_day']; + $orderInfo['member_type'] = $memberInfo['id']; + $orderInfo['overdue_time'] = $overdueTime; + $orderInfo['is_permanent'] = $isPermanent; + $orderInfo['is_free'] = $isFree; + $orderInfo['type'] = $type; + $changeType = "create_member_order"; + switch ((int)$payType) { + case 2://门店充值-用户扫码付款 + case 3://门店充值-付款码付款 + //自动判定支付方式 + if ($authCode) { + $orderInfo['auth_code'] = $authCode; + if (Payment::isWechatAuthCode($authCode)) { + $orderInfo['pay_type'] = PayServices::WEIXIN_PAY; + } else if (AliPayService::isAliPayAuthCode($authCode)) { + $orderInfo['pay_type'] = PayServices::ALIAPY_PAY; + } else { + throw new ValidateException('付款二维码错误'); + } + } else { + $orderInfo['pay_type'] = $from; + } + $memberOrder = $this->addOtherOrderData($orderInfo, $type, $changeType); + $memberOrder = $memberOrder->toArray(); + + try { + /** @var OrderPayServices $payServices */ + $payServices = app()->make(OrderPayServices::class); + $order_info = $payServices->otherRecharge($memberOrder,$memberOrder['pay_type'], $authCode); + if ($payType == 3) { + if ($order_info['paid'] === 1) { + //修改支付状态 + $this->paySuccess($memberOrder,$memberOrder['pay_type']); + return [ + 'msg' => $order_info['message'], + 'status' => 'SUCCESS', + 'type' => $from, + 'payInfo' => [], + 'data' => [ + 'jsConfig' => [], + 'order_id' => $memberOrder['order_id'] + ] + ]; + } else { + //发起支付但是还没有支付,需要在5秒后查询支付状态 + if ($memberOrder['pay_type'] === PayServices::WEIXIN_PAY) { + if (isset($order_info['payInfo']['err_code']) && in_array($order_info['payInfo']['err_code'], ['AUTH_CODE_INVALID', 'NOTENOUGH'])) { + return ['status' => 'ERROR', 'msg' => '支付失败', 'payInfo' => $order_info]; + } + $secs = 5; + if (isset($order_info['payInfo']['err_code']) && $order_info['payInfo']['err_code'] === 'USERPAYING') { + $secs = 10; + } + MicroPayOrderJob::dispatchSece($secs, [$memberOrder['order_id'],2]); + } + return [ + 'msg' => $order_info['message'] ?? '等待支付中', + 'status' => 'PAY_ING', + 'type' => $from, + 'payInfo' => $order_info, + 'data' => [ + 'jsConfig' => [], + 'order_id' => $memberOrder['order_id'] + ] + ]; + } + } + } catch (\Exception $e) { + \think\facade\Log::error('充值失败,原因:' . $e->getMessage()); + throw new ValidateException('充值失败:' . $e->getMessage()); + } + return ['msg' => '', 'status' => 'PAY', 'type' => $from, 'data' => ['jsConfig' => $order_info, 'order_id' => $memberOrder['order_id']]]; + break; + case 4: //现金支付 + $orderInfo['pay_type'] = PayServices::CASH_PAY; + $memberOrder = $this->addOtherOrderData($orderInfo, $type, $changeType); + if (!$memberOrder) { + throw new ValidateException('订单生成失败!'); + } + $memberOrder = $memberOrder->toArray(); + try { + //修改支付状态 + $this->paySuccess($memberOrder,$memberOrder['pay_type']); + } catch (\Exception $e) { + throw new ValidateException($e->getMessage()); + } + return [ + 'msg' => '', + 'status' => 'SUCCESS', + 'type' => $from, + 'payInfo' => [], + 'data' => [ + 'jsConfig' => [], + 'order_id' => $memberOrder['order_id'] + ] + ]; + break; + default: + throw new ValidateException('缺少参数'); + break; + } + } + /** + * 免费卡领取支付 + * @param $orderInfo + * @return bool + */ + public function zeroYuanPayment($orderInfo) + { + if ($orderInfo['paid']) { + throw new ValidateException('该订单已支付!'); + } + /** @var MemberShipServices $memberShipServices */ + $memberShipServices = app()->make(MemberShipServices::class); + $member_type = $memberShipServices->value(['id' => $orderInfo['member_type']], 'type'); + if ($member_type != 'free') { + throw new ValidateException('支付失败!'); + } + $res = $this->paySuccess($orderInfo, 'yue');//余额支付成功 + return $res; + + } + + /** + * 会员卡支付成功 + * @param array $orderInfo + * @param string $paytype + * @return bool + */ + public function paySuccess(array $orderInfo, string $paytype = PayServices::WEIXIN_PAY, array $other = []) + { + /** @var OtherOrderStatusServices $statusService */ + $statusService = app()->make(OtherOrderStatusServices::class); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + /** @var MemberShipServices $memberShipServices */ + $memberShipServices = app()->make(MemberShipServices::class); + $orderInfo['member_type'] = $memberShipServices->value(['id' => $orderInfo['member_type']], 'type'); + switch ($orderInfo['type']) { + case 0 : + case 1: + case 2 : + $res1 = $userServices->setMemberOverdueTime($orderInfo['vip_day'], $orderInfo['uid'], 1, $orderInfo['member_type']); + break; + case 3: + $res1 = true; + break; + } + $update = []; + if (isset($other['trade_no'])) { + $update['trade_no'] = $other['trade_no']; + } + $update['paid'] = 1; + $update['pay_type'] = $paytype; + $update['pay_time'] = time(); + $res2 = $this->dao->update($orderInfo['id'], $update); + $res3 = $statusService->save([ + 'oid' => $orderInfo['id'], + 'change_type' => 'pay_success', + 'change_message' => '用户付款成功', + 'shop_type' => $orderInfo['type'], + 'change_time' => time() + ]); + //记录流水账单 + if ($orderInfo['store_id'] > 0) { + /** @var StoreFinanceFlowServices $storeFinanceFlowServices */ + $storeFinanceFlowServices = app()->make(StoreFinanceFlowServices::class); + $orderInfo['pay_type'] = $paytype; + $orderInfo['pay_time'] = time(); + $storeFinanceFlowServices->setFinance($orderInfo, 3); + } + + //支付成功后发送消息 + event('user.vipPay', [$orderInfo]); + $res = $res1 && $res2 && $res3; + return false !== $res; + } + + /** + * 修改 + * @param array $where + * @param array $data + * @return mixed + */ + public function update(array $where, array $data) + { + return $this->dao->update($where, $data); + } + + /** + * 购买会员卡数据校验 + * @param int $uid + * @param int $memberId + * @param string $payPrice + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkPayMemberType(int $uid, int $memberId) + { + /** @var MemberShipServices $memberShipService */ + $memberShipService = app()->make(MemberShipServices::class); + $memberInfo = $memberShipService->getMemberInfo($memberId); + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $userInfo = $userService->get($uid); + if ($userInfo['is_money_level'] > 0 && $userInfo['is_ever_level'] > 0) throw new ValidateException('您已是永久会员无需再购买!'); + $memberTypes = $memberInfo['type'] ?? ''; + $price = $memberInfo['pre_price']; + if ($memberTypes == 'free' && $memberInfo['vip_day'] <= 0) throw new ValidateException('网络错误!'); + if ($userInfo['overdue_time'] > time()) { + $time = $userInfo['overdue_time']; + } else { + $time = time(); + } + switch ($memberTypes) { + case "free"://免费会员 + $isCanGetFree = $this->isCanGetFree($uid); + if ($isCanGetFree['is_record'] == 1) throw new ValidateException('您已经领取过免费会员!'); + $memberPrice = 0.00; //会员卡价格 + $isFree = 1;//代表免费 + $isPermanent = 0;//代表非永久 + $overdueTime = bcadd(bcmul(abs($memberInfo['vip_day']), "86400", 0), $time, 0); + break; + case "ever": + $memberPrice = $price; + $isFree = 0; + $isPermanent = 1; + $overdueTime = -1; + break; + default: + $memberPrice = $price; + $isFree = 0; + $isPermanent = 0; + $overdueTime =$memberShipService->getOverdueTime($uid, $memberId, $userInfo, $memberInfo); + break; + } + return [$memberPrice, $isFree, $isPermanent, $overdueTime, $memberInfo]; + } + + /** + * 根据查询用户购买会员金额 + * @param array $where + * @return mixed + */ + public function getMemberMoneyByWhere(array $where, string $sumField, string $selectType, string $group = "") + { + switch ($selectType) { + case "sum" : + return $this->dao->getWhereSumField($where, $sumField); + case "group" : + return $this->dao->getGroupField($where, $sumField, $group); + } + } + + /** + * 线下收银列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getScanOrderList(array $where) + { + $where['type'] = 3; + $where['paid'] = 1; + [$page, $limit] = $this->getPageValue(); + if ($where['add_time']) { + [$startTime, $endTime] = explode('-', $where['add_time']); + if ($startTime || $endTime) { + $startTime = strtotime($startTime); + $endTime = strtotime($endTime . ' 23:59:59'); + $where['add_time'] = [$startTime, $endTime]; + } + } + if ($where['name']) { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $userInfo = $userService->getUserInfoList(['nickname' => $where['name']], "uid"); + if ($userInfo) $where['uid'] = array_column($userInfo, 'uid'); + } + $list = $this->dao->getScanOrderList($where, $page, $limit); + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + if ($list) { + $userInfos = $userService->getColumn([['uid', 'IN', array_unique(array_column($list, 'uid'))]], 'uid,phone,nickname', 'uid'); + foreach ($list as &$v) { + $v['add_time'] = date('Y-m-d H:i:s', $v['add_time']); + $v['pay_time'] = $v['pay_time'] ? date('Y-m-d H:i:s', $v['pay_time']) : ''; + $v['phone'] = $userInfos[$v['uid']]['phone'] ?? ''; + $v['nickname'] = $userInfos[$v['uid']]['nickname'] ?? ''; + switch ($v['pay_type']) { + case "yue" : + $v['pay_type'] = "余额"; + break; + case "weixin" : + $v['pay_type'] = "微信"; + break; + case "alipay" : + $v['pay_type'] = "支付宝"; + break; + case "offline" : + $v['pay_type'] = "线下支付"; + break; + case "cash" : + $v['pay_type'] = "现金支付"; + break; + } + $v['true_price'] = bcsub($v['money'], $v['pay_price'], 2); + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取会员记录 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMemberRecord(array $where, int $limit = 0) + { + $where['type'] = [0, 1, 2, 4]; + if (isset($where['add_time']) && $where['add_time']) { + $where['time'] = $where['add_time']; + unset($where['add_time']); + } + if ($limit) { + [$page] = $this->getPageValue(); + } else { + [$page, $limit] = $this->getPageValue(); + } + $list = $this->dao->getMemberRecord($where, '*', ['user', 'staff'], $page, $limit); + if ($list) { + /** @var MemberShipServices $memberShipService */ + $memberShipService = app()->make(MemberShipServices::class); + $shipInfo = $memberShipService->getColumn([], 'title,type', 'id'); + foreach ($list as &$v) { + $v['overdue_time'] = $v['member_type'] == 'ever' || ($shipInfo[$v['member_type']]['type'] ?? '') == 'ever' ? '永久' : ($v['overdue_time'] ? date('Y-m-d H:i:s', $v['overdue_time']) : ''); + $v['vip_day'] = $v['member_type'] == 'ever' || ($shipInfo[$v['member_type']]['type'] ?? '') == 'ever' ? '永久' : $v['vip_day']; + $v['member_type'] = $v['member_type'] ? ($shipInfo[$v['member_type']]['title'] ?? '') : ($this->type[$v['type']] ?? '其他'); + $v['pay_time'] = $v['pay_time'] ? date('Y-m-d H:i:s', $v['pay_time']) : ''; + $v['add_time'] = date('Y-m-d H:i:s', $v['add_time']); + switch ($v['pay_type']) { + case "yue" : + $v['pay_type'] = "余额"; + break; + case "weixin" : + $v['pay_type'] = "微信"; + break; + case "alipay" : + $v['pay_type'] = "支付宝"; + break; + case "admin" : + $v['pay_type'] = "后台赠送"; + break; + case "offline" : + $v['pay_type'] = "线下支付"; + break; + case "cash" : + $v['pay_type'] = "现金支付"; + break; + } + if ($v['type'] == 0) $v['pay_type'] = "免费领取"; + if ($v['type'] == 2) { + $v['pay_type'] = "卡密领取"; + $v['member_type'] = "卡密激活"; + } + if ($v['type'] == 1 && $v['is_free'] == 1) $v['pay_type'] = "免费领取"; + $v['user']['overdue_time'] = isset($v['user']['overdue_time']) ? (date('Y-m-d', $v['user']['overdue_time']) == "1970-01-01" ? "" : date('Y-m-d H:i:s', $v['user']['overdue_time'])) : ''; + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 门店付费会员统计详情列表 + * @param int $store_id + * @param int $staff_id + * @param array $time + * @return array|array[] + */ + public function time(int $store_id, int $staff_id, array $time = []) + { + if (!$time) { + return [[], []]; + } + [$start, $stop, $front, $front_stop] = $time; + $where = ['store_id' => $store_id, 'paid' => 1, 'type' => [0, 1, 2, 4]]; + if ($staff_id) { + $where['staff_id'] = $staff_id; + } + $frontPrice = $this->dao->sum($where + ['time' => [$front, $front_stop]], 'pay_price', true); + $nowPrice = $this->dao->sum($where + ['time' => [$start, $stop]], 'pay_price', true); + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getMemberRecord($where + ['time' => [$start, $stop]], 'id,order_id,uid,pay_price,add_time', ['user' => function ($query) { + $query->field(['uid', 'avatar', 'nickname', 'phone'])->bind([ + 'avatar' => 'avatar', + 'nickname' => 'nickname', + 'phone' => 'phone' + ]); + }], $page, $limit); + foreach ($list as &$item) { + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + } + return [[$nowPrice, $frontPrice], $list]; + } + +} diff --git a/app/services/order/StoreCartServices.php b/app/services/order/StoreCartServices.php new file mode 100644 index 0000000..3a0a599 --- /dev/null +++ b/app/services/order/StoreCartServices.php @@ -0,0 +1,1505 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\order; + +use app\dao\order\StoreCartDao; +use app\services\activity\discounts\StoreDiscountsProductsServices; +use app\services\activity\bargain\StoreBargainServices; +use app\services\activity\combination\StoreCombinationServices; +use app\services\activity\integral\StoreIntegralServices; +use app\services\activity\lottery\LuckLotteryRecordServices; +use app\services\activity\newcomer\StoreNewcomerServices; +use app\services\activity\promotions\StorePromotionsServices; +use app\services\activity\seckill\StoreSeckillServices; +use app\services\BaseServices; +use app\services\other\CityAreaServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\product\shipping\ShippingTemplatesServices; +use app\services\product\shipping\ShippingTemplatesNoDeliveryServices; +use app\services\store\SystemStoreServices; +use app\services\user\level\SystemUserLevelServices; +use app\services\user\member\MemberCardServices; +use app\services\user\UserAddressServices; +use app\services\user\UserServices; +use app\jobs\product\ProductLogJob; +use crmeb\services\CacheService; +use crmeb\traits\OptionTrait; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; + +/** + * + * Class StoreCartServices + * @package app\services\order + * @mixin StoreCartDao + */ +class StoreCartServices extends BaseServices +{ + + use OptionTrait, ServicesTrait; + + //库存字段比对 + const STOCK_FIELD = 'sum_stock'; + //购物车最大数量 + protected $maxCartNum = 100; + + /** + * StoreCartServices constructor. + * @param StoreCartDao $dao + */ + public function __construct(StoreCartDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取某个用户下的购物车数量 + * @param array $unique + * @param int $productId + * @param int $uid + * @param string $userKey + * @return array + */ + public function getUserCartNums(array $unique, int $productId, int $uid, string $userKey = 'uid') + { + $where['is_pay'] = 0; + $where['is_del'] = 0; + $where['is_new'] = 0; + $where['product_id'] = $productId; + $where[$userKey] = $uid; + return $this->dao->getUserCartNums($where, $unique); + } + + /** + * 计算首单优惠 + * @param int $uid + * @param array $cartInfo + * @param array $newcomerArr + * @return array + */ + public function computedFirstDiscount(int $uid, array $cartInfo, array $newcomerArr = []) + { + $first_order_price = $first_discount = $first_discount_limit = 0; + if ($uid && $cartInfo) { + if (!$newcomerArr) { + /** @var StoreNewcomerServices $newcomerServices */ + $newcomerServices = app()->make(StoreNewcomerServices::class); + $newcomerArr = $newcomerServices->checkUserFirstDiscount($uid); + } + if ($newcomerArr) {//首单优惠 + [$first_discount, $first_discount_limit] = $newcomerArr; + /** @var StoreOrderComputedServices $orderServices */ + $orderServices = app()->make(StoreOrderComputedServices::class); + $totalPrice = $orderServices->getOrderSumPrice($cartInfo, 'truePrice');//获取订单svip、用户等级优惠之后总金额 + $first_discount = bcsub('1', (string)bcdiv($first_discount, '100', 2), 2); + $first_order_price = (float)bcmul((string)$totalPrice, (string)$first_discount, 2); + $first_order_price = min($first_order_price, $first_discount_limit, $totalPrice); + } + } + return [$cartInfo, $first_order_price, $first_discount, $first_discount_limit]; + } + + /** + * 获取用户下的购物车列表 + * @param int $uid + * @param $cartIds + * @param bool $new + * @param array $addr + * @param int $shipping_type + * @param int $store_id + * @param int $coupon_id + * @param bool $isCart + * @return array + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserProductCartListV1(int $uid, $cartIds, bool $new, array $addr = [], int $shipping_type = 1, int $store_id = 0, int $coupon_id = 0, bool $isCart = false) + { + if ($new) { + $cartIds = $cartIds && is_string($cartIds) ? explode(',', $cartIds) : (is_array($cartIds) ? $cartIds : []); + $cartInfo = []; + if ($cartIds) { + foreach ($cartIds as $key) { + $info = CacheService::redisHandler()->get($key); + if ($info) { + $cartInfo[] = $info; + } + } + } + } else { + $cartInfo = $this->dao->getCartList(['uid' => $uid, 'status' => 1, 'id' => $cartIds], 0, 0, ['productInfo', 'attrInfo']); + } + if (!$cartInfo) { + throw new ValidateException('获取购物车信息失败'); + } + foreach ($cartInfo as $cart) { + //检查限购 + if (isset($cart['type']) && $cart['type'] != 8) { + $this->checkLimit($uid, $cart['product_id'] ?? 0, $cart['cart_num'] ?? 1, true, $store_id); + } + } + + [$cartInfo, $valid, $invalid] = $this->handleCartList($uid, $cartInfo, $addr, $shipping_type, $store_id); + $type = array_unique(array_column($cartInfo, 'type')); + $product_type = array_unique(array_column($cartInfo, 'product_type')); + $activity_id = array_unique(array_column($cartInfo, 'activity_id')); + $collate_code_id = array_unique(array_column($cartInfo, 'collate_code_id')); + $deduction = ['product_type' => $product_type[0] ?? 0, 'type' => $type[0] ?? 0, 'activity_id' => $activity_id[0] ?? 0, 'collate_code_id' => $collate_code_id[0] ?? 0]; + $promotions = $giveCoupon = $giveCartList = $useCoupon = $giveProduct = []; + $giveIntegral = $couponPrice = $firstOrderPrice = 0; + if (!$deduction['activity_id'] && $deduction['type'] != 6) { + /** @var StoreNewcomerServices $newcomerServices */ + $newcomerServices = app()->make(StoreNewcomerServices::class); + $newcomerArr = $newcomerServices->checkUserFirstDiscount($uid); + if ($newcomerArr) {//首单优惠 + //计算首单优惠 + [$valid, $firstOrderPrice, $first_discount, $first_discount_limit] = $this->computedFirstDiscount($uid, $valid, $newcomerArr); + } else { + /** @var StorePromotionsServices $storePromotionsServices */ + $storePromotionsServices = app()->make(StorePromotionsServices::class); + //计算相关优惠活动 + [$valid, $couponPrice, $useCoupon, $promotions, $giveIntegral, $giveCoupon, $giveCartList] = $storePromotionsServices->computedPromotions($uid, $valid, $store_id, $coupon_id, $isCart); + if ($giveCartList) { + foreach ($giveCartList as $key => $give) { + $giveProduct[] = [ + 'promotions_id' => $give['promotions_id'][0] ?? 0, + 'product_id' => $give['product_id'] ?? 0, + 'unique' => $give['product_attr_unique'] ?? '', + 'cart_num' => $give['cart_num'] ?? 1, + ]; + } + } + } + } + return compact('cartInfo', 'valid', 'invalid', 'deduction', 'couponPrice', 'useCoupon', 'promotions', 'giveCartList', 'giveIntegral', 'giveCoupon', 'giveProduct', 'firstOrderPrice'); + } + + /** + * 验证库存 + * @param int $uid + * @param int $productId + * @param int $cartNum + * @param int $store_id + * @param string $unique + * @param bool $new + * @param int $type + * @param int $activity_id + * @param int $discount_product_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkProductStock(int $uid, int $productId, int $cartNum = 1, int $store_id = 0, string $unique = '', bool $new = false, int $type = 0, int $activity_id = 0, $discount_product_id = 0, $sum_cart_num = 0) + { + //验证限量 + $this->checkLimit($uid, $productId, $cartNum, $new, $store_id); + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + $isSet = $this->getItem('is_set', 0); + switch ($type) { + case 0://普通 + if ($unique == '') { + $unique = $attrValueServices->value(['product_id' => $productId, 'type' => 0], 'unique'); + } + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $productInfo = $productServices->isValidProduct($productId); + if (!$productInfo) { + throw new ValidateException('该商品已下架或删除'); + } + if ($productInfo['is_vip_product']) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $is_vip = $userServices->value(['uid' => $uid], 'is_money_level'); + if (!$is_vip) { + throw new ValidateException('该商品为付费会员专享商品'); + } + } + //预售商品 + if ($productInfo['is_presale_product']) { + if ($productInfo['presale_start_time'] > time()) throw new ValidateException('预售活动未开始'); + if ($productInfo['presale_end_time'] < time()) throw new ValidateException('预售活动已结束'); + } + + $attrInfo = $attrValueServices->getOne(['unique' => $unique, 'type' => 0]); + if (!$unique || !$attrInfo || $attrInfo['product_id'] != $productId) { + throw new ValidateException('请选择有效的商品属性'); + } + $nowStock = $attrInfo['stock'];//现有平台库存 + if ($cartNum > $nowStock) { + throw new ValidateException('该商品库存不足' . $cartNum); + } + //直接设置购物车商品数量 + if ($isSet) { + $stockNum = 0; + } else { + $stockNum = $this->dao->value(['product_id' => $productId, 'product_attr_unique' => $unique, 'uid' => $uid, 'status' => 1, 'store_id' => $store_id], 'cart_num') ?: 0; + } + if ($nowStock < ($cartNum + $stockNum)) { + if ($store_id) { + throw new ValidateException('该商品库存不足'); + } + $surplusStock = $nowStock - $cartNum;//剩余库存 + if ($surplusStock < $stockNum) { + $this->dao->update(['product_id' => $productId, 'product_attr_unique' => $unique, 'uid' => $uid, 'status' => 1, 'store_id' => $store_id], ['cart_num' => $surplusStock]); + } + } + break; + case 1://秒杀 + /** @var StoreSeckillServices $seckillService */ + $seckillService = app()->make(StoreSeckillServices::class); + [$attrInfo, $unique, $productInfo] = $seckillService->checkSeckillStock($uid, $activity_id, $cartNum, $store_id, $unique); + break; + case 2://砍价 + /** @var StoreBargainServices $bargainService */ + $bargainService = app()->make(StoreBargainServices::class); + [$attrInfo, $unique, $productInfo, $bargainUserInfo] = $bargainService->checkBargainStock($uid, $activity_id, $cartNum, $unique); + break; + case 3://拼团 + /** @var StoreCombinationServices $combinationService */ + $combinationService = app()->make(StoreCombinationServices::class); + [$attrInfo, $unique, $productInfo] = $combinationService->checkCombinationStock($uid, $activity_id, $cartNum, $unique); + break; + case 4://积分 + /** @var StoreIntegralServices $storeIntegralServices */ + $storeIntegralServices = app()->make(StoreIntegralServices::class); + [$attrInfo, $unique, $productInfo] = $storeIntegralServices->checkoutProductStock($uid, $activity_id, $cartNum, $unique); + break; + case 5://套餐 + /** @var StoreDiscountsProductsServices $discountProduct */ + $discountProduct = app()->make(StoreDiscountsProductsServices::class); + [$attrInfo, $unique, $productInfo] = $discountProduct->checkDiscountsStock($uid, $discount_product_id, $cartNum, $unique); + break; + case 7://新人专享 + if ($cartNum > 1) { + throw new ValidateException('新人专享商品限购一件'); + } + /** @var StoreNewcomerServices $newcomerServices */ + $newcomerServices = app()->make(StoreNewcomerServices::class); + [$attrInfo, $unique, $productInfo] = $newcomerServices->checkNewcomerStock($uid, $activity_id, $cartNum, $unique); + break; + case 8://抽奖 + if (!$activity_id) { + throw new ValidateException('缺少中奖信息,请返回刷新重试'); + } + /** @var LuckLotteryRecordServices $luckRecordServices */ + $luckRecordServices = app()->make(LuckLotteryRecordServices::class); + $record = $luckRecordServices->get($activity_id); + if (!$record) { + throw new ValidateException('缺少中奖信息,请返回刷新重试'); + } + if ($record['oid']) { + throw new ValidateException('已经领取成功,不要重复领取'); + } + if ($unique == '') { + $unique = $attrValueServices->value(['product_id' => $productId, 'type' => 0], 'unique'); + } + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $productInfo = $productServices->isValidProduct($productId); + if (!$productInfo) { + throw new ValidateException('该商品已下架或删除'); + } + $attrInfo = $attrValueServices->getOne(['unique' => $unique, 'type' => 0]); + if (!$unique || !$attrInfo || $attrInfo['product_id'] != $productId) { + throw new ValidateException('请选择有效的商品属性'); + } + $nowStock = $attrInfo['stock'];//现有平台库存 + if ($cartNum > $nowStock) { + throw new ValidateException('该商品库存不足' . $cartNum); + } + break; + case 9://拼单 + if ($unique == '') { + $unique = $attrValueServices->value(['product_id' => $productId, 'type' => 0], 'unique'); + } + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $productInfo = $productServices->isValidProduct($productId); + if (!$productInfo) { + throw new ValidateException('该商品已下架或删除'); + } + $attrInfo = $attrValueServices->getOne(['unique' => $unique, 'type' => 0]); + if (!$unique || !$attrInfo || $attrInfo['product_id'] != $productId) { + throw new ValidateException('请选择有效的商品属性'); + } + $nowStock = $attrInfo['stock'];//现有平台库存 + if (bcadd((string)$cartNum, (string)$sum_cart_num, 0) > $nowStock) { + throw new ValidateException('拼单中该商品库存不足' . bcadd((string)$cartNum, (string)$sum_cart_num, 0)); + } + break; + case 10://桌码 + if ($unique == '') { + $unique = $attrValueServices->value(['product_id' => $productId, 'type' => 0], 'unique'); + } + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $productInfo = $productServices->isValidProduct($productId); + if (!$productInfo) { + throw new ValidateException('该商品已下架或删除'); + } + $attrInfo = $attrValueServices->getOne(['unique' => $unique, 'type' => 0]); + if (!$unique || !$attrInfo || $attrInfo['product_id'] != $productId) { + throw new ValidateException('请选择有效的商品属性'); + } + $nowStock = $attrInfo['stock'];//现有平台库存 + if (bcadd((string)$cartNum, (string)$sum_cart_num, 0) > $nowStock) { + throw new ValidateException('桌码中该商品库存不足' . bcadd((string)$cartNum, (string)$sum_cart_num, 0)); + } + break; + default: + throw new ValidateException('请刷新后重试'); + break; + } + if (in_array($type, [1, 2, 3])) { + //根商品规格库存 + $product_stock = $attrValueServices->value(['product_id' => $productInfo['product_id'], 'suk' => $attrInfo['suk'], 'type' => 0], 'stock'); + if ($product_stock < $cartNum) { + throw new ValidateException('商品库存不足' . $cartNum); + } + if (!CacheService::checkStock($unique, (int)$cartNum, $type)) { + throw new ValidateException('商品库存不足' . $cartNum . ',无法购买请选择其他商品!'); + } + } + return [$attrInfo, $unique, $bargainUserInfo['bargain_price_min'] ?? 0, $cartNum, $productInfo]; + } + + /** + * 添加购物车 + * @param int $uid + * @param int $product_id + * @param int $cart_num + * @param string $product_attr_unique + * @param int $type + * @param bool $new + * @param int $activity_id + * @param int $discount_product_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setCart(int $uid, int $product_id, int $cart_num = 1, string $product_attr_unique = '', int $type = 0, bool $new = true, int $activity_id = 0, int $discount_product_id = 0) + { + if ($cart_num < 1) $cart_num = 1; + //检测库存限量 + $store_id = $this->getItem('store_id', 0); + $staff_id = $this->getItem('staff_id', 0); + $tourist_uid = $this->getItem('tourist_uid', ''); + [$attrInfo, $product_attr_unique, $bargainPriceMin, $cart_num, $productInfo] = $this->checkProductStock( + $uid, + $product_id, + $cart_num, + $store_id, + $product_attr_unique, + $new, + $type, $activity_id, + $discount_product_id + ); + $product_type = $productInfo['product_type']; + if ($new) { + /** @var StoreOrderCreateServices $storeOrderCreateService */ + $storeOrderCreateService = app()->make(StoreOrderCreateServices::class); + $key = $storeOrderCreateService->getNewOrderId((string)$uid); + //普通订单 && 商品是预售商品 订单类型改为预售订单 + if ($type == 0 && $productInfo['is_presale_product']) { + $type = 6; + } + $info['id'] = $key; + $info['type'] = $type; + $info['store_id'] = $store_id; + $info['tourist_uid'] = $tourist_uid; + $info['product_type'] = $product_type; + if ($type == 10 || $type == 9) { + $info['collate_code_id'] = $activity_id; + $activity_id = 0; + } + $info['activity_id'] = $activity_id; + $info['discount_product_id'] = $discount_product_id; + $info['product_id'] = $product_id; + $info['product_attr_unique'] = $product_attr_unique; + $info['cart_num'] = $cart_num; + $info['productInfo'] = []; + if ($productInfo) { + $info['productInfo'] = is_object($productInfo) ? $productInfo->toArray() : $productInfo; + } + $info['attrInfo'] = $attrInfo->toArray(); + $info['productInfo']['attrInfo'] = $info['attrInfo']; + $info['sum_price'] = $info['productInfo']['attrInfo']['price'] ?? $info['productInfo']['price'] ?? 0; + //砍价 + if ($type == 2 && $activity_id) { + $info['truePrice'] = $bargainPriceMin; + $info['productInfo']['attrInfo']['price'] = $bargainPriceMin; + } else { + $info['truePrice'] = $info['productInfo']['attrInfo']['price'] ?? $info['productInfo']['price'] ?? 0; + } + //活动商品不参与会员价 + if ($type > 0 && $activity_id) { + $info['truePrice'] = $info['productInfo']['attrInfo']['price'] ?? 0; + $info['vip_truePrice'] = 0; + } + if ($type == 8) $info['is_luck'] = true; + $info['trueStock'] = $info['productInfo']['attrInfo']['stock'] ?? 0; + $info['costPrice'] = $info['productInfo']['attrInfo']['cost'] ?? 0; + try { + CacheService::redisHandler()->set($key, $info, 3600); + } catch (\Throwable $e) { + throw new ValidateException($e->getMessage()); + } + return [$key, $cart_num]; + } else {//加入购物车记录 + ProductLogJob::dispatch(['cart', ['uid' => $uid, 'product_id' => $product_id, 'cart_num' => $cart_num]]); + $cart = $this->dao->getOne(['type' => $type, 'uid' => $uid, 'tourist_uid' => $tourist_uid, 'product_id' => $product_id, 'product_attr_unique' => $product_attr_unique, 'is_del' => 0, 'is_new' => 0, 'is_pay' => 0, 'status' => 1, 'store_id' => $store_id, 'staff_id' => $staff_id]); + if ($cart) { + $cart->cart_num = $cart_num + $cart->cart_num; + $cart->add_time = time(); + $cart->save(); + return [$cart->id, $cart->cart_num]; + } else { + $add_time = time(); + $id = $this->dao->save(compact('uid', 'tourist_uid', 'store_id', 'staff_id', 'product_id', 'product_type', 'cart_num', 'product_attr_unique', 'type', 'activity_id', 'add_time'))->id; + event('cart.add', [$uid, $tourist_uid, $store_id, $staff_id]); + return [$id, $cart_num]; + } + + } + } + +// /**拼单/桌码商品写入购物车 +// * @param int $uid +// * @param int $product_id +// * @param int $cart_num +// * @param string $product_attr_unique +// * @param int $type +// * @param bool $new +// * @param int $activity_id +// * @param int $store_id +// * @param int $discount_product_id +// * @return mixed +// * @throws \think\db\exception\DataNotFoundException +// * @throws \think\db\exception\DbException +// * @throws \think\db\exception\ModelNotFoundException +// */ +// public function addCollageCart(int $uid, int $product_id, int $cart_num = 1, string $product_attr_unique = '', int $type = 0, bool $new = true, int $activity_id = 0, int $collate_code_id = 0, int $store_id = 0, int $is_settle = 0) +// { +// if ($cart_num < 1) $cart_num = 1; +// //检测库存限量 +// $staff_id = $this->getItem('staff_id', 0); +// $tourist_uid = $this->getItem('tourist_uid', ''); +// [$attrInfo, $product_attr_unique, $bargainPriceMin, $cart_num, $productInfo] = $this->checkProductStock( +// $uid, +// $product_id, +// $cart_num, +// $store_id, +// $product_attr_unique, +// $new, +// $type, +// $activity_id, +// 0 +// ); +// $product_type = $productInfo['product_type']; +// ProductLogJob::dispatch(['cart', ['uid' => $uid, 'product_id' => $product_id, 'cart_num' => $cart_num]]); +// $cart = $this->dao->getOne(['type' => $type, 'uid' => $uid, 'product_id' => $product_id, 'product_attr_unique' => $product_attr_unique, 'activity_id' => $activity_id, 'collate_code_id' => $collate_code_id, 'is_del' => 0, 'is_new' => 0, 'is_pay' => 0, 'status' => 1, 'store_id' => $store_id]); +// if ($cart) { +// if (!$is_settle) { +// $cart->cart_num = $cart_num + $cart->cart_num; +// } +// $cart->add_time = time(); +// $cart->save(); +// return $cart->id; +// } else { +// $add_time = time(); +// $id = $this->dao->save(compact('uid', 'store_id', 'staff_id', 'product_id', 'product_type', 'cart_num', 'product_attr_unique', 'type', 'activity_id', 'collate_code_id', 'add_time'))->id; +// event('cart.add', [$uid, $tourist_uid, $store_id, $staff_id]); +// return $id; +// } +// } + + /** + * 移除购物车商品 + * @param int $uid + * @param array $ids + * @return StoreCartDao|bool + */ + public function removeUserCart(int $uid, array $ids) + { + return $this->dao->removeUserCart($uid, $ids) !== false; + } + + /** + * 购物车 修改商品数量 + * @param $id + * @param $number + * @param $uid + * @return bool|\crmeb\basic\BaseModel + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function changeUserCartNum($id, $number, $uid) + { + if (!$id || !$number) return false; + $where = ['uid' => $uid, 'id' => $id]; + $carInfo = $this->dao->getOne($where, 'product_id,type,activity_id,product_attr_unique,cart_num'); + /** @var StoreProductServices $StoreProduct */ + $StoreProduct = app()->make(StoreProductServices::class); + $stock = $StoreProduct->getProductStock($carInfo->product_id, $carInfo->product_attr_unique); + if (!$stock) throw new ValidateException('暂无库存'); + if (!$number) throw new ValidateException('库存错误'); + if ($stock < $number) throw new ValidateException('库存不足' . $number); + if ($carInfo->cart_num == $number) return true; + $this->checkProductStock($uid, (int)$carInfo->product_id, (int)$number, 0, $carInfo->product_attr_unique, true); + return $this->dao->changeUserCartNum(['uid' => $uid, 'id' => $id], (int)$number); + } + + /** + * 获取购物车列表 + * @param int $uid + * @param int $status + * @param array $cartIds + * @param int $storeId + * @param int $staff_id + * @param int $shipping_type + * @param int $touristUid + * @param int $numType + * @param bool $new + * @param bool $isCart + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCartList(int $uid, int $status, array $cartIds = [], int $storeId = 0, int $staff_id = 0, int $shipping_type = -1, int $touristUid = 0, int $numType = 0, bool $new = false, bool $isCart = true) + { + // [$page, $limit] = $this->getPageValue(); + if ($new) { + $cartIds = $cartIds && is_string($cartIds) ? explode(',', $cartIds) : (is_array($cartIds) ? $cartIds : []); + $list = []; + if ($cartIds) { + foreach ($cartIds as $key) { + $info = CacheService::redisHandler()->get($key); + if ($info) { + $list[] = $info; + } + } + } + } else { + $where = ['uid' => $uid, 'store_id' => $storeId, 'tourist_uid' => $touristUid, 'cart_ids' => $cartIds]; + //有店员就证明在收银台中 + if ($staff_id) { + $where['staff_id'] = $staff_id; + } + if ($status != -1) $where = array_merge($where, ['status' => $status]); + + $list = $this->dao->getCartList($where, 0, 0, ['productInfo', 'attrInfo']); + } + $count = $promotionsPrice = $coupon_price = $firstOrderPrice = 0; + $cartList = $valid = $promotions = $coupon = $invalid = $type = $activity_id = []; + if ($list) { + [$list, $valid, $invalid] = $this->handleCartList($uid, $list, [], $shipping_type, $storeId); + $activity_id = array_unique(array_column($list, 'activity_id')); + $type = array_unique(array_column($list, 'type')); + + if (!($activity_id[0] ?? 0)) { + /** @var StoreNewcomerServices $newcomerServices */ + $newcomerServices = app()->make(StoreNewcomerServices::class); + $newcomerArr = $newcomerServices->checkUserFirstDiscount($uid); + if ($newcomerArr) { + //计算首单优惠 + [$valid, $firstOrderPrice, $first_discount, $first_discount_limit] = $this->computedFirstDiscount($uid, $valid, $newcomerArr); + } else { + /** @var StorePromotionsServices $storePromotionsServices */ + $storePromotionsServices = app()->make(StorePromotionsServices::class); + //计算相关优惠活动 + [$valid, $coupon_price, $coupon, $promotions, $giveIntegral, $giveCoupon, $giveCartList] = $storePromotionsServices->computedPromotions($uid, $valid, $storeId, 0, $isCart); + $cartList = array_merge($valid, $giveCartList); + foreach ($cartList as $key => $cart) { + if (isset($cart['promotions_true_price']) && isset($cart['price_type']) && $cart['price_type'] == 'promotions') { + $promotionsPrice = bcadd((string)$promotionsPrice, (string)bcmul((string)$cart['promotions_true_price'], (string)$cart['cart_num'], 2), 2); + } + } + } + } + if ($numType) { + $count = count($valid); + } else { + $count = array_sum(array_column($valid, 'cart_num')); + } + } + $deduction = ['type' => $type[0] ?? 0, 'activity_id' => $activity_id[0] ?? 0]; + $deduction['promotions_price'] = $promotionsPrice; + $deduction['coupon_price'] = $coupon_price; + $deduction['first_order_price'] = $firstOrderPrice; + + $user_store_id = $this->getItem('store_id', 0); + $invalid_key = 'invalid_' . $user_store_id . '_' . $uid; + //写入缓存 + if ($status == 1) { + CacheService::redisHandler()->delete($invalid_key); + if ($invalid) CacheService::redisHandler()->set($invalid_key, $invalid, 60); + } + //读取缓存 + if ($status == 0) { + $other_invalid = CacheService::redisHandler()->get($invalid_key); + if ($other_invalid) $invalid = array_merge($invalid, $other_invalid); + } + + return ['promotions' => $promotions, 'coupon' => $coupon, 'valid' => $valid, 'invalid' => $invalid, 'deduction' => $deduction, 'count' => $count]; + } + + /** + * 购物车重选 + * @param int $cart_id + * @param int $product_id + * @param string $unique + */ + public function modifyCart(int $cart_id, int $product_id, string $unique) + { + /** @var StoreProductAttrValueServices $attrService */ + $attrService = app()->make(StoreProductAttrValueServices::class); + $stock = $attrService->value(['product_id' => $product_id, 'unique' => $unique, 'type' => 0], 'stock'); + if ($stock > 0) { + $this->dao->update($cart_id, ['product_attr_unique' => $unique, 'cart_num' => 1]); + } else { + throw new ValidateException('选择的规格库存不足'); + } + } + + /** + * 重选购物车 + * @param $id + * @param $uid + * @param $productId + * @param $unique + * @param $num + * @param int $store_id + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function resetCart($id, $uid, $productId, $unique, $num, int $store_id = 0) + { + $res = $this->dao->getOne(['uid' => $uid, 'product_id' => $productId, 'product_attr_unique' => $unique, 'store_id' => $store_id]); + if ($res) { + /** @var StoreProductServices $StoreProduct */ + $StoreProduct = app()->make(StoreProductServices::class); + $stock = $StoreProduct->getProductStock((int)$productId, $unique); + $cart_num = $res->cart_num + $num; + if ($cart_num > $stock) { + $cart_num = $stock; + } + $res->cart_num = $cart_num; + $res->save(); + if ($res['id'] != $id) $this->dao->delete($id); + } else { + $this->dao->update($id, ['product_attr_unique' => $unique, 'cart_num' => $num]); + } + } + + /** + * 首页加入购物车 + * @param int $uid + * @param int $productId + * @param int $num + * @param string $unique + * @param int $type + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setCartNum(int $uid, int $productId, int $num, string $unique, int $type) + { + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + + if ($unique == '') { + $unique = $attrValueServices->value(['product_id' => $productId, 'type' => 0], 'unique'); + } + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $productInfo = $productServices->isValidProduct((int)$productId); + if (!$productInfo) { + throw new ValidateException('该商品已下架或删除'); + } + if (!($unique && $attrValueServices->getAttrvalueCount($productId, $unique, 0))) { + throw new ValidateException('请选择有效的商品属性'); + } + $stock = $productServices->getProductStock((int)$productId, $unique); + if ($stock < $num) { + throw new ValidateException('该商品库存不足' . $num); + } + //预售商品 + if ($productInfo['is_presale_product']) { + if ($productInfo['presale_start_time'] > time()) throw new ValidateException('预售活动未开始'); + if ($productInfo['presale_end_time'] < time()) throw new ValidateException('预售活动已结束'); + } + //检查限购 + if ($type != 0) $this->checkLimit($uid, $productId, $num); + + $cart = $this->dao->getOne(['uid' => $uid, 'product_id' => $productId, 'product_attr_unique' => $unique, 'store_id' => 0]); + if ($cart) { + if ($type == -1) { + $cart->cart_num = $num; + } elseif ($type == 0) { + $cart->cart_num = $cart->cart_num - $num; + } elseif ($type == 1) { + if ($cart->cart_num >= $stock) { + throw new ValidateException('该商品库存只有' . $stock); + } + $new_cart_num = $cart->cart_num + $num; + if ($new_cart_num > $stock) { + $new_cart_num = $stock; + } + $cart->cart_num = $new_cart_num; + } + if ($cart->cart_num === 0) { + return $this->dao->delete($cart->id); + } else { + $cart->add_time = time(); + $cart->save(); + return $cart->id; + } + } else { + $data = [ + 'uid' => $uid, + 'product_id' => $productId, + 'product_type' => $productInfo['product_type'], + 'cart_num' => $num, + 'product_attr_unique' => $unique, + 'type' => 0, + 'add_time' => time() + ]; + $id = $this->dao->save($data)->id; + event('cart.add', [$uid, 0, 0, 0]); + return $id; + } + } + + /** + * 用户购物车商品统计 + * @param int $uid + * @param string $numType + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserCartCount(int $uid, string $numType = '0', int $store_id = 0) + { + $count = 0; + $ids = []; + $cartNums = []; + $sum_price = 0; + $cartList = $this->dao->getUserCartList(['uid' => $uid, 'status' => 1, 'store_id' => 0], 'id,cart_num,product_id,product_attr_unique'); + if ($cartList) { + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $productInfos = $storeProductServices->getColumn([['id', 'in', array_column($cartList, 'product_id')]], 'id,pid,type,relation_id', 'id'); + /** @var StoreProductAttrValueServices $storePrdouctAttrValueServices */ + $storePrdouctAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $attrInfos = $storePrdouctAttrValueServices->getColumn([['unique', 'in', array_column($cartList, 'product_attr_unique')]], 'id,unique', 'unique'); + foreach ($cartList as $cart) { + $productInfo = $productInfos[$cart['product_id']] ?? []; + if (!$productInfo) continue; + $attrInfo = $attrInfos[$cart['product_attr_unique']] ?? []; + if (!$attrInfo) continue; + if ($store_id) {//某门店加入购物车商品数量 + if (in_array($productInfo['type'], [0, 2]) || ($productInfo['type'] == 1 && $productInfo['relation_id'] == $store_id) || ($productInfo['type'] == 1 && $productInfo['pid'] > 0)) { + $ids[] = $cart['id']; + $cartNums[] = $cart['cart_num']; + } + } else { + if (in_array($productInfo['type'], [0, 2]) || ($productInfo['type'] == 1 && $productInfo['pid'] > 0)) { + $ids[] = $cart['id']; + $cartNums[] = $cart['cart_num']; + } + } + } + if ($numType) { + $count = count($ids); + } else { + $count = array_sum($cartNums); + } + } + return compact('count', 'ids', 'sum_price'); + } + + /** + * 处理购物车数据 + * @param int $uid + * @param array $cartList + * @param array $addr + * @param int $shipping_type + * @param int $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function handleCartList(int $uid, array $cartList, array $addr = [], int $shipping_type = 1, int $store_id = 0) + { + if (!$cartList) { + return [$cartList, [], [], [], 0, [], []]; + } + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + /** @var MemberCardServices $memberCardService */ + $memberCardService = app()->make(MemberCardServices::class); + $vipStatus = $memberCardService->isOpenMemberCardCache('vip_price', false); + $tempIds = []; + $userInfo = []; + $discount = 100; + $productIds = $allStock = $attrUniquesArr = []; + if ($uid) { + /** @var UserServices $user */ + $user = app()->make(UserServices::class); + $userInfo = $user->getUserCacheInfo($uid); + if (!$userInfo) { + throw new ValidateException('用户不存在'); + } + $userInfo = $userInfo->toArray(); + //用户等级是否开启 + if (sys_config('member_func_status', 1) && $userInfo) { + /** @var SystemUserLevelServices $systemLevel */ + $systemLevel = app()->make(SystemUserLevelServices::class); + $discount = $systemLevel->getDiscount($uid, (int)$userInfo['level'] ?? 0); + } + } + //不送达运费模板 + if ($shipping_type == 1 && $addr) { + $cityId = (int)($addr['city_id'] ?? 0); + if ($cityId) { + /** @var CityAreaServices $cityAreaServices */ + $cityAreaServices = app()->make(CityAreaServices::class); + $cityIds = $cityAreaServices->getRelationCityIds($cityId); + foreach ($cartList as $item) { + $tempIds[] = $item['productInfo']['temp_id']; + } + $tempIds = array_unique($tempIds); + /** @var ShippingTemplatesServices $shippingService */ + $shippingService = app()->make(ShippingTemplatesServices::class); + $tempIds = $shippingService->getColumn([['id', 'in', $tempIds], ['no_delivery', '=', 1]], 'id'); + if ($tempIds) { + /** @var ShippingTemplatesNoDeliveryServices $noDeliveryServices */ + $noDeliveryServices = app()->make(ShippingTemplatesNoDeliveryServices::class); + $tempIds = $noDeliveryServices->isNoDelivery($tempIds, $cityIds); + } + } + } + $latitude = $this->getItem('latitude', ''); + $longitude = $this->getItem('longitude', ''); + $user_store_id = $this->getItem('store_id', 0); + $store_id = $store_id ?: $user_store_id; + //平台是否开启门店自提 + $store_self_mention = sys_config('store_func_status', 1) && sys_config('store_self_mention'); + $cart_type = 0; + if ($store_id) {//平台商品,在门店购买 验证门店库存 + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + /** @var StoreBranchProductServices $branchProductServics */ + $branchProductServics = app()->make(StoreBranchProductServices::class); + foreach ($cartList as $cart) { + $productInfo = $cart['productInfo'] ?? []; + if (!$productInfo) continue; + $product_id = 0; + if (in_array($productInfo['type'], [0, 2])) { + $product_id = $productInfo['id']; + } else {//门店商品 + if ($productInfo['pid'] && $productInfo['relation_id'] != $store_id) {//平台共享商品到另一个门店购买 + $product_id = $productInfo['pid']; + } + } + if (!$product_id) {//自己门店购买不用再次验证库存 + continue; + } + $productIds[] = $cart['product_id']; + $suk = ''; + $cart_type = $cart['type']; + //类型 0:普通、1:秒杀、2:砍价、3:拼团、4:积分、5:套餐、6:预售、7:新人礼、8:抽奖、9:拼单、10:桌码 + switch ($cart['type']) { + case 0: + case 6: + case 8: + case 9: + case 10: + $suk = $skuValueServices->value(['unique' => $cart['product_attr_unique'], 'product_id' => $cart['product_id'], 'type' => 0], 'suk'); + break; + case 1: + case 2: + case 3: + case 5: + case 7: + if ($cart['type'] == 5 && isset($cart['discount_product_id'])) { + $product_id = $cart['discount_product_id']; + } else { + $product_id = $cart['activity_id']; + } + $suk = $skuValueServices->value(['unique' => $cart['product_attr_unique'], 'product_id' => $product_id, 'type' => $cart['type']], 'suk'); + break; + } + $branchProductInfo = $branchProductServics->isValidStoreProduct((int)$cart['product_id'], $store_id); + if (!$branchProductInfo) { + continue; + } + $attrValue = ''; + if ($suk) { + $attrValue = $skuValueServices->get(['suk' => $suk, 'product_id' => $branchProductInfo['id'], 'type' => 0]); + } + if (!$attrValue) { + continue; + } + $allStock[$attrValue['unique']] = $attrValue['stock']; + $attrUniquesArr[$cart['product_attr_unique']] = $attrValue['unique']; + } + } else { + $productIds = array_unique(array_column($cartList, 'product_id')); + } + + $storeInfo = []; + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + if ($user_store_id) { + $storeInfo = $storeServices->getNearbyStore(['id' => $user_store_id], '', '', '', 1); + } else if ($latitude && $longitude) { + $storeInfo = $storeServices->getNearbyStore([], $latitude, $longitude, '', 1); + } + $valid = $invalid = []; + foreach ($cartList as &$item) { + $item['is_gift'] = 0; + if (isset($item['productInfo']['delivery_type'])) { + $item['productInfo']['delivery_type'] = is_string($item['productInfo']['delivery_type']) ? explode(',', $item['productInfo']['delivery_type']) : $item['productInfo']['delivery_type']; + } else { + $item['productInfo']['delivery_type'] = []; + } + $item['productInfo']['express_delivery'] = in_array(1, $item['productInfo']['delivery_type']); + $item['productInfo']['store_mention'] = in_array(2, $item['productInfo']['delivery_type']); + $item['productInfo']['store_delivery'] = in_array(3, $item['productInfo']['delivery_type']); + + if (isset($item['attrInfo']) && $item['attrInfo'] && (!isset($item['productInfo']['attrInfo']) || !$item['productInfo']['attrInfo'])) { + $item['productInfo']['attrInfo'] = $item['attrInfo'] ?? []; + } + $item['attrStatus'] = isset($item['productInfo']['attrInfo']['stock']) && $item['productInfo']['attrInfo']['stock']; + $item['productInfo']['attrInfo']['image'] = $item['productInfo']['attrInfo']['image'] ?? $item['productInfo']['image'] ?? ''; + $item['productInfo']['attrInfo']['suk'] = $item['productInfo']['attrInfo']['suk'] ?? '已失效'; + if (isset($item['productInfo']['attrInfo'])) { + $item['productInfo']['attrInfo'] = get_thumb_water($item['productInfo']['attrInfo']); + } + $item['productInfo'] = get_thumb_water($item['productInfo']); + $productInfo = $item['productInfo']; + $item['vip_truePrice'] = 0; + //门店独立商品 + $isBranchProduct = isset($productInfo['type']) && isset($productInfo['pid']) && $productInfo['type'] == 1 && !$productInfo['pid']; + $product_store_id = $isBranchProduct ? $productInfo['relation_id'] : 0; + + if (isset($productInfo['attrInfo']['product_id']) && $item['product_attr_unique']) { + $item['costPrice'] = $productInfo['attrInfo']['cost'] ?? 0; + $item['trueStock'] = $item['branch_stock'] = $productInfo['attrInfo']['stock'] ?? 0; + $item['branch_sales'] = $productInfo['attrInfo']['sales'] ?? 0; + $item['truePrice'] = $productInfo['attrInfo']['price'] ?? 0; + $item['sum_price'] = $productInfo['attrInfo']['price'] ?? 0; + if ((!$item['type'] || !$item['activity_id']) && !$isBranchProduct) { + [$truePrice, $vip_truePrice, $type] = $productServices->setLevelPrice($productInfo['attrInfo']['price'] ?? 0, $uid, $userInfo, $vipStatus, $discount, $productInfo['attrInfo']['vip_price'] ?? 0, $productInfo['is_vip'] ?? 0, true); + $item['truePrice'] = $truePrice; + $item['vip_truePrice'] = $vip_truePrice; + $item['price_type'] = $type; + } + } else { + $item['costPrice'] = $item['productInfo']['cost'] ?? 0; + $item['trueStock'] = $item['branch_sales'] = $item['productInfo']['stock'] ?? 0; + $item['branch_sales'] = $item['productInfo']['sales'] ?? 0; + $item['truePrice'] = $item['productInfo']['price'] ?? 0; + $item['sum_price'] = $item['productInfo']['price'] ?? 0; + if ((!$item['type'] || !$item['activity_id']) && !$isBranchProduct) { + [$truePrice, $vip_truePrice, $type] = $productServices->setLevelPrice($item['productInfo']['price'] ?? 0, $uid, $userInfo, $vipStatus, $discount, $item['productInfo']['vip_price'] ?? 0, $item['productInfo']['is_vip'] ?? 0, true); + $item['truePrice'] = $truePrice; + $item['vip_truePrice'] = $vip_truePrice; + $item['price_type'] = $type; + } + } + $applicable_type = $item['productInfo']['applicable_type'] ?? 1; + $applicable_store_id = []; + if (isset($item['productInfo']['applicable_store_id'])) { + $applicable_store_id = is_string($item['productInfo']['applicable_store_id']) ? explode(',', $item['productInfo']['applicable_store_id']) : $item['productInfo']['applicable_store_id']; + } + $applicableStatus = !$store_id || $applicable_type == 1 || ($applicable_type == 2 && in_array($store_id, $applicable_store_id)); + if (isset($item['status']) && $item['status'] == 0) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品已失效'; + $invalid[] = $item; + } elseif (($item['productInfo']['type'] ?? 0) == 1 && ($item['productInfo']['pid'] ?? 0) == 0 && $storeInfo && ($item['productInfo']['relation_id'] ?? 0) != $storeInfo['id'] && $cart_type != 10) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif ((isset($item['productInfo']['delivery_type']) && !$item['productInfo']['delivery_type']) || in_array($item['productInfo']['product_type'], [1, 2, 3])) { + $item['is_valid'] = 1; + $valid[] = $item; + } elseif (!$applicableStatus) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } else { + $condition = !in_array(isset($item['productInfo']['product_id']) ? $item['productInfo']['product_id'] : $item['productInfo']['id'], $productIds) || $item['cart_num'] > ($allStock[$attrUniquesArr[$item['product_attr_unique']] ?? ''] ?? 0); + switch ($shipping_type) { + case -1://购物车列表展示 + if ($isBranchProduct && $store_id && ($store_id != $product_store_id)) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } else { + $item['is_valid'] = 1; + $valid[] = $item; + } + break; + case 1: + //不送达 + if (in_array($item['productInfo']['temp_id'], $tempIds) || (isset($item['productInfo']['delivery_type']) && !in_array(1, $item['productInfo']['delivery_type']) && !in_array(3, $item['productInfo']['delivery_type']))) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif ($isBranchProduct && $store_id && ($store_id != $product_store_id || !in_array(3, $item['productInfo']['delivery_type']))) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif ((in_array($productInfo['type'], [0, 2]) || $productInfo['relation_id'] != $store_id) && $store_id && ($condition || (!in_array(2, $item['productInfo']['delivery_type']) && !in_array(3, $item['productInfo']['delivery_type'])))) {//平台商品 在门店购买 验证门店库存 + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif (!$storeServices->checkStoreDeliveryScope($store_id, $addr, $latitude, $longitude)) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } else { + $item['is_valid'] = 1; + $valid[] = $item; + } + break; + case 2: + //不支持到店自提 + if (!$store_self_mention) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '平台/门店已关闭自提'; + $invalid[] = $item; + } elseif (isset($item['productInfo']['delivery_type']) && $item['productInfo']['delivery_type'] && !in_array(2, $item['productInfo']['delivery_type'])) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif ($isBranchProduct && $store_id && $store_id != $product_store_id) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif ($item['productInfo']['product_type'] == 1) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif ((in_array($productInfo['type'], [0, 2]) || $productInfo['relation_id'] != $store_id) && $store_id && $condition) {//平台、供应商商品 在门店购买 验证门店库存 + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } else { + $item['is_valid'] = 1; + $valid[] = $item; + } + break; + case 4: + //无库存||下架 + if ($isBranchProduct && $store_id && $store_id != $product_store_id) { + $item['is_valid'] = 0; + $item['invalid_desc'] = '此商品超出配送/自提范围'; + $invalid[] = $item; + } elseif (in_array($productInfo['type'], [0, 2]) && $store_id && $condition) { + $item['is_valid'] = 0; + $invalid[] = $item; + } else { + $item['is_valid'] = 1; + $valid[] = $item; + } + break; + default: + $item['is_valid'] = 1; + $valid[] = $item; + break; + } + } + unset($item['attrInfo']); + } + + + return [$cartList, $valid, $invalid]; + } + + + /** + * 门店给用户加入购物车 + * @param int $uid + * @param int $productId + * @param int $cartNum + * @param string $unique + * @param int $staff_id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function addCashierCart(int $uid, int $productId, int $cartNum, string $unique, int $staff_id = 0) + { + $store_id = $this->getItem('store_id', 0); + $tourist_uid = $this->getItem('tourist_uid', ''); + if (!$store_id) { + throw new ValidateException('缺少门店ID'); + } + + [$nowStock, $unique, $bargainPriceMin, $cart_num, $productInfo] = $this->checkProductStock($uid, $productId, $cartNum, $store_id, $unique, true); + + ProductLogJob::dispatch(['cart', ['uid' => $uid, 'product_id' => $productId, 'cart_num' => $cartNum]]); + $cart = $this->dao->getOne([ + 'uid' => $uid, + 'product_id' => $productId, + 'product_attr_unique' => $unique, + 'store_id' => $store_id, + 'staff_id' => $staff_id, + 'tourist_uid' => $tourist_uid, + 'is_del' => 0, + 'is_new' => 0, + 'is_pay' => 0, + 'status' => 1 + ]); + if ($cart) { + if ($nowStock < ($cartNum + $cart['cart_num'])) { + $cartNum = $nowStock - $cartNum;//剩余库存 + } + if ($cartNum == 0) throw new ValidateException('库存不足'); + $cart->cart_num = $cartNum + $cart->cart_num; + $cart->add_time = time(); + $cart->save(); + return $cart->id; + } else { + $add_time = time(); + $data = compact('uid', 'store_id', 'add_time', 'tourist_uid'); + $data['type'] = 0; + $data['product_id'] = $productId; + $data['product_type'] = $productInfo['product_type']; + $data['cart_num'] = $cartNum; + $data['product_attr_unique'] = $unique; + $data['store_id'] = $store_id; + $data['staff_id'] = $staff_id; + $id = $this->dao->save($data)->id; + event('cart.add', [$uid, $tourist_uid, $store_id, $staff_id]); + return $id; + } + } + + /** + * @param int $id + * @param int $number + * @param int $uid + * @param int $storeId + * @return bool|\crmeb\basic\BaseModel + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function changeCashierCartNum(int $id, int $number, int $uid, int $storeId) + { + if (!$id || !$number) return false; + $where = ['uid' => $uid, 'id' => $id, 'store_id' => $storeId]; + $carInfo = $this->dao->getOne($where, 'product_id,product_attr_unique,cart_num'); + /** @var StoreBranchProductServices $storeProduct */ + $storeProduct = app()->make(StoreBranchProductServices::class); + $stock = $storeProduct->getProductStock($carInfo->product_id, $storeId, $carInfo->product_attr_unique); + if (!$stock) throw new ValidateException('暂无库存'); + if ($stock < $number) throw new ValidateException('库存不足' . $number); + if ($carInfo->cart_num == $number) return true; + $this->setItem('is_set', 1); + $this->checkProductStock($uid, (int)$carInfo->product_id, $number, $storeId, $carInfo->product_attr_unique, true); + $this->reset(); + return $this->dao->changeUserCartNum(['uid' => $uid, 'id' => $id], (int)$number); + } + + /** + * 购物车重选 + * @param int $cart_id + * @param int $product_id + * @param string $unique + */ + public function modifyCashierCart(int $storeId, int $cart_id, int $product_id, string $unique) + { + /** @var StoreProductAttrValueServices $attrService */ + $attrService = app()->make(StoreProductAttrValueServices::class); + $stock = $attrService->value(['product_id' => $product_id, 'unique' => $unique, 'type' => 0], 'stock'); + if ($stock > 0) { + $this->dao->update($cart_id, ['product_attr_unique' => $unique, 'cart_num' => 1]); + } else { + throw new ValidateException('选择的规格库存不足'); + } + } + + /** + * 批量加入购物车 + * @param array $cart + * @param int $storeId + * @param int $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function batchAddCart(array $cart, int $storeId, int $uid) + { + $this->setItem('store_id', $storeId); + $cartIds = []; + foreach ($cart as $item) { + if (!isset($item['productId'])) { + throw new ValidateException('缺少商品ID'); + } + if (!isset($item['cartNum'])) { + throw new ValidateException('缺少购买商品数量'); + } + if (!isset($item['uniqueId'])) { + throw new ValidateException('缺少唯一值'); + } + $cartIds[] = $this->addCashierCart($uid, (int)$item['productId'], (int)$item['cartNum'], $item['uniqueId']); + } + $this->reset(); + return $cartIds; + } + + /** + * 组合前端购物车需要的数据结构 + * @param array $cartList + * @param array $protmoions + * @return array + */ + public function getReturnCartList(array $cartList, array $promotions) + { + $result = []; + if ($cartList) { + if ($promotions) $promotions = array_combine(array_column($promotions, 'id'), $promotions); + $i = 0; + foreach ($cartList as $key => $cart) { + $data = ['promotions' => [], 'pids' => [], 'cart' => []]; + if ($result && isset($cart['promotions_id']) && $cart['promotions_id'] && (!isset($cart['collate_code_id']) || $cart['collate_code_id'] <= 0)) { + $isTure = false; + foreach ($result as $key => &$res) { + if (array_intersect($res['pids'], $cart['promotions_id'])) { + $res['pids'] = array_unique(array_merge($res['pids'], $cart['promotions_id'] ?? [])); + $res['cart'][] = $cart; + $isTure = true; + break; + } + } + if (!$isTure) { + $data['cart'][] = $cart; + $data['pids'] = array_unique($cart['promotions_id'] ?? []); + $result[$i] = $data; + $i++; + } + } else { + $data['cart'][] = $cart; + $data['pids'] = array_unique($cart['promotions_id'] ?? []); + $result[$i] = $data; + $i++; + } + } + + foreach ($result as $key => &$item) { + if ($item['pids']) { + foreach ($item['pids'] as $key => $id) { + $item['promotions'][] = $promotions[$id] ?? []; + } + } + } + } + return $result; + } + + + /** + * 控制购物车加入商品最大数量 + * @param int $uid + * @param int $tourist_uid + * @param int $store_id + * @param int $staff_id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function controlCartNum(int $uid, int $tourist_uid = 0, int $store_id = 0, int $staff_id = 0) + { + $maxCartNum = $this->maxCartNum; + $where = [ + 'is_del' => 0, + 'is_new' => 0, + 'is_pay' => 0, + 'status' => 1 + ]; + if ($uid) $where['uid'] = $uid; + if ($tourist_uid) $where['tourist_uid'] = $tourist_uid; + if ($store_id) $where['store_id'] = $store_id; + if ($staff_id) $where['staff_id'] = $staff_id; + try { + $count = $this->dao->count($where); + if ($count >= $maxCartNum) {//删除一个最早加入购物车商品 + $one = $this->dao->search($where)->order('id asc')->find(); + if ($one) { + $this->dao->delete($one['id']); + } + } + } catch (\Throwable $e) { + \think\facade\Log::error('自动控制购物车数量,删除最早加入商品失败:' . $e->getMessage()); + } + return true; + } + + /** + * 检测限购 + * @param int $uid + * @param int $product_id + * @param int $num + * @param bool $new + * @param int $store_id + * @return bool + */ + public function checkLimit(int $uid, int $product_id, int $num, bool $new = false, int $store_id = 0) + { + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + /** @var StoreOrderCartInfoServices $orderCartServices */ + $orderCartServices = app()->make(StoreOrderCartInfoServices::class); + + $limitInfo = $productServices->get($product_id, ['id', 'pid', 'is_limit', 'limit_type', 'limit_num']); + if (!$limitInfo) throw new ValidateException('商品不存在'); + $limitInfo = $limitInfo->toArray(); + if (!$limitInfo['is_limit']) return true; + $cartNum = 0; + //收银台游客限购 + $tourist_uid = 0; + if (!$uid) { + $tourist_uid = $this->getItem('tourist_uid', ''); + } + $pid = $limitInfo['pid'] ? $limitInfo['pid'] : $limitInfo['id']; + $product_ids = $productServices->getColumn(['pid' => $pid], 'id'); + $product_ids[] = $pid; + if (!$new) {// 购物车商品数量 + $cartNum = $this->dao->sum(['uid' => $uid, 'tourist_uid' => $tourist_uid, 'product_id' => $product_ids, 'store_id' => $store_id, 'status' => 1, 'is_del' => 0], 'cart_num', true); + } + if ($limitInfo['limit_type'] == 1) { + if (($num + $cartNum) > $limitInfo['limit_num']) { + throw new ValidateException('单次购买不能超过 ' . $limitInfo['limit_num'] . ' 件'); + } + } else if ($limitInfo['limit_type'] == 2) { + $orderPayNum = $orderCartServices->sum(['uid' => $uid, 'product_id' => $product_ids], 'cart_num'); + $orderRefundNum = $orderCartServices->sum(['uid' => $uid, 'product_id' => $product_ids], 'refund_num'); + $orderNum = $cartNum + $orderPayNum - $orderRefundNum; + if (($num + $orderNum) > $limitInfo['limit_num']) { + throw new ValidateException('该商品限购 ' . $limitInfo['limit_num'] . ' 件,您已经购买 ' . $orderNum . ' 件'); + } + } + return true; + } + + /** + * 计算用户购物车商品(优惠活动、最优优惠券) + * @param array $user + * @param $cartId + * @param bool $new + * @param int $addressId + * @param int $shipping_type + * @param int $store_id + * @return array + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @throws \throwable + */ + public function computeUserCart(array $user, $cartId, bool $new, int $addressId, int $shipping_type = 1, int $store_id = 0) + { + $addr = $data = []; + $uid = (int)$user['uid']; + /** @var UserAddressServices $addressServices */ + $addressServices = app()->make(UserAddressServices::class); + if ($addressId) { + $addr = $addressServices->getAdderssCache($addressId); + } + //没传地址id或地址已删除未找到 ||获取默认地址 + if (!$addr) { + $addr = $addressServices->getUserDefaultAddressCache($uid); + } + $data['upgrade_addr'] = 0; + if ($addr) { + $addr = is_object($addr) ? $addr->toArray() : $addr; + if (isset($addr['upgrade']) && $addr['upgrade'] == 0) { + $data['upgrade_addr'] = 1; + } + } else { + $addr = []; + } + if ($store_id) { + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeServices->getStoreInfo($store_id); + } + //获取购物车信息 + $cartGroup = $this->getUserProductCartListV1($uid, $cartId, $new, $addr, $shipping_type, $store_id, 0, true); + $storeFreePostage = floatval(sys_config('store_free_postage')) ?: 0;//满额包邮金额 + $valid = $cartGroup['valid'] ?? []; + /** @var StoreOrderComputedServices $computedServices */ + $computedServices = app()->make(StoreOrderComputedServices::class); + $priceGroup = $computedServices->getOrderPriceGroup($valid, $addr, $user, $storeFreePostage); + + + $invalid = $cartGroup['invalid'] ?? []; + $deduction = $cartGroup['deduction'] ?? []; + $coupon = $cartGroup['useCoupon'] ?? []; + $promotions = $cartGroup['promotions'] ?? []; + $giveCartList = $cartGroup['giveCartList'] ?? []; + $couponPrice = $cartGroup['couponPrice'] ?? 0; + $firstOrderPrice = $cartGroup['firstOrderPrice']; + + $cartList = array_merge($valid, $giveCartList); + $promotionsPrice = 0; + if ($cartList) { + foreach ($cartList as $key => $cart) { + if (isset($cart['promotions_true_price']) && isset($cart['price_type']) && $cart['price_type'] == 'promotions') { + $promotionsPrice = bcadd((string)$promotionsPrice, (string)bcmul((string)$cart['promotions_true_price'], (string)$cart['cart_num'], 2), 2); + } + } + } + $deduction['promotions_price'] = $promotionsPrice; + $deduction['coupon_price'] = $couponPrice; + $deduction['first_order_price'] = $firstOrderPrice; + $deduction['sum_price'] = $priceGroup['sumPrice']; + $deduction['vip_price'] = $priceGroup['vipPrice']; + + $payPrice = (float)$priceGroup['totalPrice']; + if ($couponPrice < $payPrice) {//优惠券金额 + $payPrice = bcsub((string)$payPrice, (string)$couponPrice, 2); + } else { + $payPrice = 0; + } + if ($firstOrderPrice < $payPrice) {//首单优惠金额 + $payPrice = bcsub((string)$payPrice, (string)$firstOrderPrice, 2); + } else { + $payPrice = 0; + } + $deduction['pay_price'] = $payPrice; + return compact('promotions', 'coupon', 'deduction'); + } + +} diff --git a/app/services/order/StoreDeliveryOrderServices.php b/app/services/order/StoreDeliveryOrderServices.php new file mode 100644 index 0000000..4ecb787 --- /dev/null +++ b/app/services/order/StoreDeliveryOrderServices.php @@ -0,0 +1,682 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order; + + +use app\dao\order\StoreDeliveryOrderDao; +use app\jobs\order\OrderTakeJob; +use app\jobs\store\StoreFinanceJob; +use app\services\BaseServices; +use app\services\other\CityAreaServices; +use app\services\store\SystemStoreServices; +use app\services\supplier\SystemSupplierServices; +use crmeb\services\DeliverySevices; +use crmeb\services\FormBuilder as Form; +use crmeb\services\SystemConfigService; +use crmeb\traits\OptionTrait; +use think\exception\ValidateException; +use think\facade\Log; +use think\facade\Route as Url; + +/** + * 发货单 + * Class StoreDeliveryOrderServices + * @package app\services\order + * @mixin StoreDeliveryOrderDao + */ +class StoreDeliveryOrderServices extends BaseServices +{ + use OptionTrait; + + protected $statusData = [ + 2 => '待取货', + 3 => '配送中', + 4 => '已完成', + -1 => '已取消', + 9 => '物品返回中', + 10 => '物品返回完成', + 100 => '骑士到店', + ]; + + /** + * 平台达达门店 + * @var string + */ + public $platCityShopId = 'plat_delivery_city_shop_001'; + + /** + * 构造方法 + * StoreDeliveryOrderServices constructor. + * @param StoreDeliveryOrderDao $dao + */ + public function __construct(StoreDeliveryOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * 配送信息 + * @return string[] + */ + public function getStatusMsg() + { + return $this->statusData; + } + + /** + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function systemPage(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit, ['orderInfo', 'storeInfo']); + $count = $this->dao->count($where); + if ($list) { + foreach ($list as &$item) { + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s') : ''; + $item['distance'] = $item['distance'] ? bcdiv($item['distance'], '1000', 3) : 0; + } + } + return compact('count', 'list'); + } + + /** + * 生成订单号 + * @return string + * @throws \Exception + */ + public function getOrderSn(string $key = '') + { + [$msec, $sec] = explode(' ', microtime()); + $msectime = number_format((floatval($msec) + floatval($sec)) * 1000, 0, '', ''); + $orderId = $key . $msectime . random_int(10000, max(intval($msec * 10000) + 10000, 98369)); + return $orderId; + } + + /** + * 地址转经纬度 + * @param $region + * @param $address + * @return mixed|null + */ + public function lbs_address($region, $address) + { + $key = sys_config('tengxun_map_key', ''); + if (!$key) { + throw new ValidateException('请先配置地图KEY'); + } + $locationOption = new \Joypack\Tencent\Map\Bundle\AddressOption($key); + $locationOption->setAddress($address); + $locationOption->setRegion($region); + $location = new \Joypack\Tencent\Map\Bundle\Address($locationOption); + $res = $location->request(); + if ($res->error) { + throw new ValidateException($res->error); + } + if ($res->status) { + throw new ValidateException($res->message); + } + if (!$res->result) { + throw new ValidateException('获取失败'); + } + return $res->result; + } + + + /** + * 处理订单数据 + * @param array $station + * @param array $order + * @param int $type + * @return array + */ + public function getPriceParams(array $station, array $order, int $type) + { + $data = []; + $type = (int)$type; + + switch ($type) { + case 1: + $city = DeliverySevices::init(DeliverySevices::DELIVERY_TYPE_DADA)->getCity([]); + $res = []; + foreach ($city as $item) { + $res[$item['label']] = $item['key']; + } + //达达城市数据:西安 + $city_name = str_replace(['市','自治州','地区','区划','县'], '',$station['city_name'] ?? ''); + $data = [ + 'shop_no' => $station['origin_shop_id'], + 'city_code' => $res[$city_name] ?? '西安', + 'cargo_price' => $order['pay_price'], + 'is_prepay' => 0, + 'receiver_name' => $order['real_name'], + 'receiver_address' => $order['user_address'], + 'cargo_weight' => 0, + 'receiver_phone' => $order['user_phone'], + 'is_finish_code_needed' => 1, + ]; + break; + case 2://uu城市数据:西安市 + $business = DeliverySevices::init(DeliverySevices::DELIVERY_TYPE_UU)->getBusiness(); + $business = array_combine(array_column($business, 'key'), $business); + $data = [ + 'from_address' => $station['station_address'], + 'to_address' => $order['user_address'], + 'city_name' => $station['city_name'] ?? '西安', + 'goods_type' => $business[$station['business'] ?? 1]['label'], + 'send_type' =>'0', + 'to_lat' => $order['latitude'], + 'to_lng' => $order['longitude'], + 'from_lat' => $station['lat'], + 'from_lng' => $station['lng'], + ]; + break; + } + return $data; + } + + /** + * 创建配送单 + * @param int $id + * @param array $data + * @param int $type + * @param $order + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function create(int $id, array $data, int $type = 1, $order = []) + { + if (!$order) { + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $order = $orderServices->get($id); + } + if (!$order) { + throw new ValidateException('订单信息获取失败'); + } + if (isset($order['user_address']) && !$order['user_address']) { + throw new ValidateException('自提订单不支持同城配送'); + } + $order = is_object($order) ? $order->toArray() : $order; + //处理地址定位 + if (isset($order['user_location']) && $order['user_location']) { + [$longitude, $latitude] = explode(' ', $order['user_location']); + $order['longitude'] = $longitude; + $order['latitude'] = $latitude; + if (!$order['longitude'] || !$order['latitude']) { + $addressArr = $this->addressHandle($order['user_address']); + $city_name = $addressArr['city'] ?? ''; + try { + $addres = $this->lbs_address($city_name, $order['user_address']); + $order['latitude'] = $addres['location']['lat'] ?? ''; + $order['longitude'] = $addres['location']['lng'] ?? ''; + } catch (\Exception $e) { + throw new ValidateException('获取经纬度失败'); + } + } + } + if ($order['store_id']) {//门店 + $resType = 1; + $relationId = (int)$order['store_id']; + } elseif ($order['supplier_id']) {//供应商 + $resType = 2; + $relationId = (int)$order['supplier_id']; + } else {//平台信息 + $resType = 0; + $relationId = 0; + } + //获取发货信息 + $station = $this->syncCityShop($relationId, $resType, $type); + if (!$station || !isset($station['lat']) || !$station['lat'] || !isset($station['lng']) || !$station['lng']) { + throw new ValidateException('获取发货信息失败'); + } + $getPriceParams = $this->getPriceParams($station, $order, $type); + $orderSn = $this->getOrderSn($type == 1 ? 'dd' : 'uu'); + $getPriceParams['origin_id'] = $orderSn; + $getPriceParams['cargo_weight'] = $data['cargo_weight'] ?? ''; + + $service = DeliverySevices::init($type); + try { + //计算价格 + $priceData = $service->getOrderPrice($getPriceParams); + if ($type == DeliverySevices::DELIVERY_TYPE_UU) { //uu + $priceData['receiver'] = $order['real_name']; + $priceData['receiver_phone'] = $order['user_phone']; + $priceData['note'] = $data['delivery_remark']; + $priceData['push_type'] = 2; + $priceData['special_type'] = $data['special_type'] ?? 0; + } + + $res = $service->addOrder($priceData); + $ret = [ + 'type' => $resType, + 'relation_id' => $relationId, + 'oid' => $id, + 'order_id' => $orderSn, + 'delivery_no' => $type == 2 ? $res['ordercode'] : $priceData['deliveryNo'] ?? '', + 'city_code' => $station['city_name'] ?? '西安', + 'receiver_phone' => $order['user_phone'], + 'user_name' => $order['real_name'], + 'from_address' => $station['station_address'] ?? '', + 'to_address' => $order['user_address'], + 'info' => $data['delivery_remark'], + 'status' => $res['status'] ?? 0, + 'station_type' => $type, + 'to_lat' => $order['latitude'], + 'to_lng' => $order['longitude'], + 'from_lat' => $station['lat'] ?? '', + 'from_lng' => $station['lng'] ?? '', + 'distance' => $priceData['distance'], + 'fee' => $priceData['fee'] ?? $priceData['need_paymoney'] ?? 0, + 'mark' => $data['delivery_remark'], + 'uid' => $order['uid'], + 'add_time' => time() + ]; + //入库操作 + $this->dao->save($ret); + //记录门店流水 + if ($resType == 1) { + StoreFinanceJob::dispatch([$ret, 6, $ret['fee']]); + } + return true; + } catch (\Throwable $e) { + if (isset($res['status']) && $res['status'] == 'success'){ + $error['origin_id'] = $orderSn; + $error['reason'] = $type == 1 ? 36 : '信息错误'; + $error['delivery_no'] = $type == 2 ? $res['ordercode'] : $priceData['deliveryNo']; + $service->cancelOrder($error); + } + throw new ValidateException($e->getMessage()); + } + + } + + /** + * 配送订单详情 + * @param int $id + * @return array|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function detail(int $id) + { + $res = $this->dao->get($id, ['orderInfo']); + if (!$res) { + throw new ValidateException('配送订单不存在,或已取消'); + } + $res = $res->toArray(); + $order = DeliverySevices::init((int)$res['station_type'])->getOrderDetail($res); + if (!$res) throw new ValidateException('订单不存在'); + $res['data'] = [ + 'order_id' => $order['order_code'], + 'to_address' => $order['to_address'], + 'from_address' => $order['from_address'], + 'state' => $order['state'], + 'note' => $order['note'], + 'order_price' => $order['order_price'], + 'distance' => round(($order['distance'] / 1000),2) . ' km', + ]; + return $res; + } + + /** + * 删除 + * @param int $id + * @return mixed + */ + public function delete(int $id) + { + $res = $this->dao->get($id); + if (!$res) throw new ValidateException('订单不存在'); + return $this->dao->delete($id); + } + + /** + * @param $id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function cancelForm($id) + { + $order = $this->dao->get($id); + if (!$order) throw new ValidateException('订单不存在'); + if ($order['status'] == -1) throw new ValidateException('订单已取消,无法操作'); + $field = []; + if ($order['station_type'] == 1) { + $options = DeliverySevices::init(1)->reasons(); + $field[] = Form::select('reason', '取消原因')->setOptions(Form::setOptions($options))->filterable(1)->col(12); + $field[] = Form::input('cancel_reason', '其他原因说明')->required('请输入原因'); + } else { + $field[] = Form::input('reason', '取消原因')->required('请输入原因'); + } + return create_form('取消同城配送订单', $field, Url::buildUrl('/order/delivery_order/cancel/'. $id), 'POST'); + } + + /** + * 取消订单 + * @param int $id + * @param array $reason + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function cancel(int $id, array $reason) + { + $order = $this->dao->get($id); + if (!$order) throw new ValidateException('配送订单不存在'); + if ($order['status'] == -1) throw new ValidateException('请勿重复操作'); + $data = [ + 'origin_id' => $order['order_id'], + 'order_code'=> $order['delivery_no'], + 'reason' => $reason['reason'], + 'cancel_reason' => $reason['cancel_reason'], + ]; + $this->transaction(function () use($id, $order, $data){ + $mark = $data['reason']; + $delivery = DeliverySevices::init((int)$order['station_type']); + if ($order['station_type'] == DeliverySevices::DELIVERY_TYPE_DADA) { + $options = $delivery->reasons(); + if ($options) $options = array_combine(array_column($options, 'value'), $options); + $mark = $options[$data['reason']]['label'] ?? ''; + } + if ($data['cancel_reason']) $mark .= ','.$data['cancel_reason']; + $res = $delivery->cancelOrder($data); + $deduct_fee = $res['deduct_fee'] ?? 0; + $this->doCancel($id, $order, $deduct_fee, $mark); + + }); + return true; + } + + /** + * 执行取消订单 + * @param int $id + * @param $deliveryOrder + * @param $deduct_fee + * @param $mark + * @return bool + */ + public function doCancel(int $id , $deliveryOrder, $deduct_fee, $mark) + { + //修改配送单 + $this->dao->update($id, ['status' => 1, 'mark' => $mark, 'deduct_fee' => $deduct_fee]); + //修改愿订单 + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $storeOrderServices->update($deliveryOrder['oid'], ['status' => 0, 'delivery_type' => '', 'delivery_name' => '', 'delivery_id' => '', 'delivery_uid' => '']); + /** @var StoreOrderStatusServices $statusServices */ + $statusServices = app()->make(StoreOrderStatusServices::class); + $statusServices->save([ + 'oid' => $deliveryOrder['oid'], + 'change_type' => 'city_delivery_cancel', + 'change_message' => '同城配送取消', + 'change_time' => time() + ]); + //记录门店流水 + if ($deliveryOrder['type'] == 1) { + StoreFinanceJob::dispatch([$deliveryOrder, 7, $deliveryOrder['fee']]); + } + return true; + } + + + /** + * TODO 回调 + * @param $data + * @author Qinii + * @day 2/17/22 + */ + public function notify($data) + { + //达达 + /** + * 订单状态(待接单=1,待取货=2,配送中=3,已完成=4,已取消=5, 指派单=8,妥投异常之物品返回中=9, 妥投异常之物品返回完成=10, 骑士到店=100,创建达达运单失败=1000 可参考文末的状态说明) + */ + Log::info('同城回调参数:'.var_export(['=======',$data,'======='],1)); + if (isset($data['data'])) { + $data = json_decode($data['data'], 1); + } + + $reason = ''; + $deductFee = 0; + $delivery = []; + if (isset($data['order_status'])){ + $order_id = $data['order_id']; + if ($data['order_status'] == 1) { + $orderData = $this->dao->getOne(['order_id' => $data['order_id']]); + if (!$orderData['finish_code']) { + $orderData->finish_code = $data['finish_code']; + $orderData->save(); + } + return ; + } else if (in_array( $data['order_status'],[2,3,4,5,9,10,100])){ + $status = $data['order_status']; + if ($data['order_status'] == 5){ + $msg = [ + '取消:', + '达达配送员取消:', + '商家主动取消:', + '系统或客服取消:', + ]; + //1:达达配送员取消;2:商家主动取消;3:系统或客服取消;0:默认值 + $status = -1; + $reason = $msg[$data['cancel_from']].$data['cancel_reason']; + } + $deductFee = $data['deductFee'] ?? 0; + if (isset($data['dm_name']) && $data['dm_name']) { + $delivery = [ + 'delivery_name' => $data['dm_name'], + 'delivery_id' => $data['dm_mobile'], + ]; + } + + } + } else if (isset($data['state'])){ //uu + if (!$data['origin_id']) $deliveryOrder = $this->dao->getOne(['delivery_no' => $data['order_code']]); + $order_id = $data['origin_id'] ?: $deliveryOrder['order_id'] ; + //当前状态 1下单成功 3跑男抢单 4已到达 5已取件 6到达目的地 10收件人已收货 -1订单取消 + switch ($data['state']) { + case 3: + $status = 2; + break; + case 4: + $status = 100; + break; + case 5: + $status = 3; + break; + case 10: + $status = 4; + break; + case -1: + $status = -1; + $reason = $data['state_text']; + break; + default: + break; + } + if (isset($data['driver_name']) && $data['driver_name']) { + $delivery = [ + 'delivery_name' => $data['driver_name'], + 'delivery_id' => $data['driver_mobile'], + ]; + } + } + + if (isset($order_id) && isset($status)){ + $deliveryOrder = $this->dao->getOne(['order_id' => $order_id]); + if ($deliveryOrder) { + $this->notifyAfter($status, $reason, $deliveryOrder, $delivery, $deductFee); + }else { + Log::info('同城配送回调,未查询到订单:'.$order_id); + } + } + } + + /** + * @param $status + * @param $reason + * @param $res + * @param $data + * @param $deductFee + * @return bool + */ + public function notifyAfter($status, $reason, $deliveryOrder, $data, $deductFee) + { + if (!isset($this->statusData[$status])) return true; + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $orderData = $orderServices->get($deliveryOrder['oid']); + /** @var StoreOrderStatusServices $statusServices */ + $statusServices = app()->make(StoreOrderStatusServices::class); + $change_type = 'city_delivery_' . $status; + $oid = (int)$deliveryOrder['oid']; + + if ($orderData['status'] != $status && !$statusServices->count(['oid' => $oid, 'change_type' => $change_type])) { + $order = $orderServices->get($oid); + //修改配送单 + $this->dao->update($deliveryOrder['id'], ['status' => $status, 'reason' => $reason]); + //增加 + $message = '订单已配送【'. $this->statusData[$status].'】'; + $statusServices->save([ + 'oid' => $oid, + 'change_type' => $change_type, + 'change_message' => $message, + 'change_time' => time() + ]); + switch ($status) { + case 2: + if (!empty($data)) $orderServices->update($oid, $data); + break; + case 4: + //订单收货 + if ($order['status'] != 2) { + OrderTakeJob::dispatch([$order]); + } + break; + case -1: + $this->doCancel((int)$deliveryOrder['id'], $deliveryOrder, $deductFee , $reason); + break; + } + } + return true; + } + + /** + * 同步达达门店信息 + * @param int $id + * @param bool $is_new + * @param int $type + * @param int $station_type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function syncCityShop(int $id, int $type = 1, int $station_type = 1) + { + $data = []; + if ($station_type == 1) { + $status = sys_config('dada_delivery_status'); + } else { + $status = sys_config('uu_delivery_status'); + } + if (!$status) {//未开启 + return $data; + } + if ($type == 1) {//门店 + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $station = $storeServices->getStoreInfo($id); + /** @var CityAreaServices $cityArea */ + $cityArea = app()->make(CityAreaServices::class); + $station['city_name'] = $cityArea->value(['id' => $station['city']], 'name'); + $station['address'] = $station['detailed_address']; + if (!$station['city_shop_id']) { + $station['city_shop_id'] = 'deliver_store_' . $station['id'] . '_'. $this->getOrderSn(); + $storeServices->update($id, ['city_shop_id' => $station['city_shop_id']]); + } + } elseif ($type == 2){//供应商 + /** @var SystemSupplierServices $services */ + $services = app()->make(SystemSupplierServices::class); + $station = $services->getSupplierInfo($id); + if ($station) { + $station['name'] = $station['supplier_name'] ?? ''; + } + } else {//平台 + $station = SystemConfigService::more(['refund_address', 'refund_name', 'refund_phone']); + $address = $station['refund_address']; + if ($address) { + $station['address'] = $station['refund_address']; + $station['name'] = $station['refund_name']; + $station['phone'] = $station['refund_phone']; + $station['city_shop_id'] = $this->platCityShopId; + } + } + if (!isset($station['latitude'])) {//地址转经纬度 + $addressArr = $this->addressHandle($station['address']); + if (!$addressArr['province'] || !$addressArr['city']) { + throw new ValidateException($type == 0 ? '请检查(设置->商城设置->交易设置->退货收货人地址)完整性' : '请检查该订单关联供应商地址信息是否正确'); + } + $station['city_name'] = $addressArr['city'] ?? ''; + try { + $addres = $this->lbs_address($station['city_name'], $station['address']); + $station['latitude'] = $addres['location']['lat'] ?? ''; + $station['longitude'] = $addres['location']['lng'] ?? ''; + } catch (\Exception $e) { + throw new ValidateException('获取经纬度失败'); + } + } + $data = [ + 'lng' => (float)($station['longitude'] ?? 0), + 'lat' => (float)($station['latitude'] ?? 0), + 'phone' => $station['phone'] ?? '', + 'business' => (int)($station['business'] ?? 5), + 'contact_name' => $station['name'] ?? '', + 'station_name' => $station['name'], + 'station_address' => $station['address'] ?? '', + 'status' => 1, + 'origin_shop_id' => $station['city_shop_id'], + ]; + if ($data) { + $serve = DeliverySevices::init($station_type); + try { + $shop = $serve->getShopDetail($data['origin_shop_id']); + } catch (\Throwable $e) { + $shop = []; + } + try { + if (!$shop) { + $serve->addShop($data); + } else { + $serve->updateShop($data); + } + } catch (\Throwable $e) { + throw new ValidateException('创建达达门店失败,原因:' . $e->getMessage()); + } + } + $data['city_name'] = $station['city_name']; + return $data; + } + + +} diff --git a/app/services/order/StoreOrderCartInfoServices.php b/app/services/order/StoreOrderCartInfoServices.php new file mode 100644 index 0000000..8d69bb4 --- /dev/null +++ b/app/services/order/StoreOrderCartInfoServices.php @@ -0,0 +1,384 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order; + +use app\dao\order\StoreOrderCartInfoDao; +use app\services\BaseServices; +use crmeb\services\CacheService; +use crmeb\services\SystemConfigService; +use crmeb\traits\OptionTrait; +use crmeb\traits\ServicesTrait; +use crmeb\utils\Arr; +use think\exception\ValidateException; + +/** + * Class StoreOrderCartInfoServices + * @package app\services\order + * @mixin StoreOrderCartInfoDao + */ +class StoreOrderCartInfoServices extends BaseServices +{ + use OptionTrait; + use ServicesTrait; + + /** + * StorePinkServices constructor. + * @param StoreOrderCartInfoDao $dao + */ + public function __construct(StoreOrderCartInfoDao $dao) + { + $this->dao = $dao; + } + + /** + * 清空订单商品缓存 + * @param int $oid + * @return bool + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function clearOrderCartInfo(int $oid) + { + return CacheService::delete(md5('store_order_cart_info_' . $oid)); + } + + /** + * 获取指定订单下的商品详情 + * @param int $oid + * @return array|mixed + */ + public function getOrderCartInfo(int $oid) + { + $cartInfo = CacheService::get(md5('store_order_cart_info_' . $oid)); + if ($cartInfo) return $cartInfo; + $cart_info = $this->dao->getCartColunm(['oid' => $oid], 'cart_info', 'cart_id'); + $info = []; + foreach ($cart_info as $k => $v) { + $_info = is_string($v) ? json_decode($v, true) : $v; + if (!isset($_info['productInfo'])) $_info['productInfo'] = []; + //缩略图处理 + if (isset($_info['productInfo']['attrInfo'])) { + $_info['productInfo']['attrInfo'] = get_thumb_water($_info['productInfo']['attrInfo']); + } + $_info['product_type'] = $_info['productInfo']['product_type'] ?? 0; + $_info['supplier_id'] = $_info['productInfo']['supplier_id'] ?? 0; + $_info['is_support_refund'] = $_info['productInfo']['is_support_refund'] ?? 1; + $_info['productInfo'] = get_thumb_water($_info['productInfo']); + $_info['refund_num'] = $this->dao->sum(['cart_id' => $_info['id']], 'refund_num'); + $info[$k]['cart_info'] = $_info; + unset($_info); + } + CacheService::set(md5('store_order_cart_info_' . $oid), $info); + return $info; + } + + /** 获取指定订单下的商品详情 供应商 + * @param int $oid + * @return array + */ + public function getOrderCartInfoSettlePrice(int $oid) + { + $cart_info = $this->dao->getCartColunm(['oid' => $oid], 'cart_num,refund_num,cart_info', 'id'); + $settlePrice = 0; + $refundSettlePrice = 0; + foreach ($cart_info as $k => &$v) { + $_info = is_string($v['cart_info']) ? json_decode($v['cart_info'], true) : $v['cart_info']; + $v['cart_info'] = $_info; + if (!isset($_info['productInfo'])) $_info['productInfo'] = []; + $settle_price = $_info['productInfo']['attrInfo']['settle_price'] ?? 0; + $_info['settlePrice'] = bcmul((string)$settle_price, (string)$v['cart_num'], 2); //购买结算价格 + $settlePrice = bcadd($_info['settlePrice'], $settlePrice, 2); + $_info['refundSettlePrice'] = bcmul((string)$settle_price, (string)$v['refund_num'], 2);//退款结算价格 + $refundSettlePrice = bcadd($_info['refundSettlePrice'], $refundSettlePrice, 2); + } + return ['settlePrice' => $settlePrice, 'refundSettlePrice' => $refundSettlePrice, 'info'=> $cart_info]; + } + + /** + * 查找购物车里的所有商品标题 + * @param int $oid + * @param false $goodsNum + * @return bool|mixed|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCarIdByProductTitle(int $oid, bool $goodsNum = false) + { + $key = md5('store_order_cart_product_title_' . $oid); + $title = CacheService::get($key); + if (!$title) { + $orderCart = $this->dao->getCartInfoList(['oid' => $oid], ['cart_info']); + foreach ($orderCart as $item) { + if (isset($item['cart_info']['productInfo']['store_name'])) { + if ($goodsNum && isset($item['cart_info']['cart_num'])) { + $title .= $item['cart_info']['productInfo']['store_name'] . ' * ' . $item['cart_info']['cart_num'] . ' | '; + } else { + $title .= $item['cart_info']['productInfo']['store_name'] . '|'; + } + } + } + if ($title) { + $title = substr($title, 0, strlen($title) - 1); + } + CacheService::set($key, $title); + } + return $title ? $title : ''; + } + + /** + * 获取打印订单的商品信息 + * @param int $oid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCartInfoPrintProduct(int $oid) + { + $cartInfo = $this->dao->getCartInfoList(['oid' => $oid], ['is_gift', 'cart_info']); + $product = []; + foreach ($cartInfo as $item) { + $value = is_string($item['cart_info']) ? json_decode($item['cart_info'], true) : $item['cart_info']; + $value['productInfo']['store_name'] = $value['productInfo']['store_name'] ?? ""; + $value['productInfo']['store_name'] = substrUTf8($value['productInfo']['store_name'], 10, 'UTF-8', ''); + $value['is_gift'] = $item['is_gift']; + $product[] = $value; + } + return $product; + } + + /** + * 保存购物车info + * @param $oid + * @param array $cartInfo + * @param $uid + * @param array $promotions + * @return int + */ + public function setCartInfo($oid, array $cartInfo, $uid, array $promotions = []) + { + $group = []; + foreach ($cartInfo as $cart) { + $group[] = [ + 'oid' => $oid, + 'uid' => $uid, + 'cart_id' => $cart['id'], + 'type' => $cart['productInfo']['type'] ?? 0, + 'relation_id' => $cart['productInfo']['relation_id'] ?? 0, + 'product_id' => $cart['product_id'] ?? $cart['productInfo']['id'],//原商品ID + 'product_type' => $cart['productInfo']['product_type'] ?? 0, + 'sku_unique' => $cart['product_attr_unique'] ?? '', + 'promotions_id' => implode(',', $cart['promotions_id'] ?? []), + 'is_gift' => isset($cart['is_gift']) && $cart['is_gift'] ? 1 : 0, + 'is_support_refund' => isset($cart['is_gift']) && $cart['is_gift'] ? 0 : ($cart['productInfo']['is_support_refund'] ?? 1), + 'cart_info' => json_encode($cart), + 'cart_num' => $cart['cart_num'], + 'surplus_num' => $cart['cart_num'], + 'split_surplus_num' => $cart['cart_num'], + 'write_times' => bcmul((string)$cart['cart_num'], (string)(max($cart['productInfo']['attrInfo']['write_times'] ?? 1, 1)), 0), + 'write_surplus_times' => bcmul((string)$cart['cart_num'], (string)(max($cart['productInfo']['attrInfo']['write_times'] ?? 1, 1)), 0), + 'unique' => md5($cart['id'] . '' . $oid) + ]; + } + if ($promotions) { + /** @var StoreOrderPromotionsServices $services */ + $services = app()->make(StoreOrderPromotionsServices::class); + $services->setPromotionsDetail((int)$uid, (int)$oid, $cartInfo, $promotions); + } + return $this->dao->saveAll($group); + } + + /** + * 订单创建成功之后计算订单(实际优惠、积分、佣金、上级、上上级) + * @param $oid + * @param array $cartInfo + * @return bool + */ + public function updateCartInfo($oid, array $cartInfo) + { + foreach ($cartInfo as $cart) { + $group = [ + 'cart_info' => json_encode($cart) + ]; + $this->dao->update(['oid' => $oid, 'cart_id' => $cart['id']], $group); + } + return true; + } + + /** + * 商品编号 + * @param int $oid + * @return array + */ + public function getCartIdsProduct(int $oid) + { + $where = [ + 'oid' => $oid + ]; + return $this->dao->getColumn($where, 'product_id', 'oid', true); + } + + /** + * 检测这些商品是否还可以拆分 + * @param int $oid + * @param array $cart_data + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkCartIdsIsSplit(int $oid, array $cart_data) + { + if (!$cart_data) return false; + $ids = array_unique(array_column($cart_data, 'cart_id')); + if ($this->dao->getCartInfoList(['oid' => $oid, 'cart_id' => $ids, 'split_status' => 2], ['cart_id'])) { + throw new ValidateException('您选择的商品已经拆分完成,请刷新或稍后重新选择'); + } + $cartInfo = $this->getSplitCartList($oid, 'surplus_num,split_surplus_num,cart_info,cart_num', 'cart_id'); + if (!$cartInfo) { + throw new ValidateException('该订单已发货完成'); + } + foreach ($cart_data as $cart) { + $surplus_num = $cartInfo[$cart['cart_id']]['surplus_num'] ?? 0; + if (!$surplus_num) {//兼容之前老数据 + $_info = $cartInfo[$cart['cart_id']]['cart_info'] ?? []; + $surplus_num = $_info['cart_num'] ?? 0; + } + if ($cart['cart_num'] > $surplus_num) { + throw new ValidateException('您选择商品拆分数量大于购买数量'); + } + } + return true; + } + + /** + * 获取可退款商品 + * @param int $oid + * @param string $field + * @param string $key + * @return array + */ + public function getRefundCartList(int $oid, string $field = '*', string $key = '') + { + $cartInfo = $this->dao->getColumn([['oid', '=', $oid]], $field, $key); + foreach ($cartInfo as $key => &$item) { + if ($field == 'cart_info') { + $item = is_string($item) ? json_decode($item, true) : $item; + } else { + if (isset($item['cart_info'])) $item['cart_info'] = is_string($item['cart_info']) ? json_decode($item['cart_info'], true) : $item['cart_info']; + if (isset($item['cart_num']) && !$item['cart_num']) {//兼容之前老数据 + $item['cart_num'] = $item['cart_info']['cart_num'] ?? 0; + } + } + $surplus = (int)bcsub((string)$item['cart_num'], (string)$item['refund_num'], 0); + if ($surplus > 0) { + $item['surplus_num'] = $surplus; + } else { + unset($cartInfo[$key]); + } + } + return array_merge($cartInfo); + } + + /** + * 获取某个订单还可以拆分商品 split_status 0:未拆分1:部分拆分2:拆分完成 + * @param int $oid + * @param string $field + * @param string $key + * @return array + */ + public function getSplitCartList(int $oid, string $field = '*', string $key = '') + { + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $order = $orderServices->get($oid); + if (!$order) { + throw new ValidateException('订单不存在'); + } + $store_id = $this->getItem('store_id', 0); + $supplier_id = $this->getItem('supplier_id', 0); + + //拆分完整主订单查询未发货子订单 + if ($order['pid'] == -1) { + $oid = $orderServices->value(['pid' => $oid, 'status' => 0, 'supplier_id' => $supplier_id, 'store_id' => $store_id, 'refund_type' => [0, 3]], 'id'); + } + $cartInfo = $this->dao->getColumn([['oid', '=', $oid], ['split_status', 'IN', [0, 1]]], $field, $key); + foreach ($cartInfo as &$item) { + if ($field == 'cart_info') { + $item = is_string($item) ? json_decode($item, true) : $item; + } else { + if (isset($item['cart_info'])) $item['cart_info'] = is_string($item['cart_info']) ? json_decode($item['cart_info'], true) : $item['cart_info']; + if (isset($item['cart_num']) && !$item['cart_num']) {//兼容之前老数据 + $item['cart_num'] = $item['cart_info']['cart_num'] ?? 0; + } + $item['surplus_num'] = $item['split_surplus_num']; + } + } + return $cartInfo; + } + + /** 次卡商品未核销短信提醒 + * @return bool + */ + public function reminderUnverifiedRemind() + { + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + // 临期 + //系统预设取消订单时间段 + $keyValue = ['reminder_deadline_second_card_time']; + //获取配置 + $systemValue = SystemConfigService::more($keyValue); + //格式化数据 + $systemValue = Arr::setValeTime($keyValue, is_array($systemValue) ? $systemValue : []); + $reminder_deadline_second_card_time = $systemValue['reminder_deadline_second_card_time']; + $reminder_deadline_second_card_time = (int)bcmul((string)$reminder_deadline_second_card_time, '3600', 0); + $writeTime = time() + $reminder_deadline_second_card_time; + $adventList = $this->dao->getAdventCartInfoList(time(), $writeTime); + if ($adventList) { + foreach ($adventList as $key => $item) { + $cart_info = is_string($item['cart_info']) ? json_decode($item['cart_info'], true) : $item['cart_info']; + $store_name = substrUTf8($cart_info['productInfo']['store_name'], 10, 'UTF-8', ''); + $data['store_name'] = $store_name; + $order = $orderServices->get($item['oid'], ['id', 'uid', 'pay_time', 'user_phone']); + if (!$order) { + continue; + } + $data['uid'] = $order['uid']; + $data['end_time'] = date('Y-m-d H:i', $item['write_end']); + $data['pay_time'] = date('Y-m-d H:i', $order['pay_time']); + $data['phone'] = $order['user_phone']; + event('notice.notice', [$data, 'reminder_brink_death']); + $this->dao->update($item['id'], ['is_advent_sms' => 1]); + } + } + // 过期 + $expireList = $this->dao->getExpireCartInfoList(time()); + if ($expireList) { + foreach ($expireList as $key => $item) { + $cart_info = is_string($item['cart_info']) ? json_decode($item['cart_info'], true) : $item['cart_info']; + $store_name = substrUTf8($cart_info['productInfo']['store_name'], 10, 'UTF-8', ''); + $data['store_name'] = $store_name; + $data['end_time'] = date('Y-m-d H:i', $item['write_end']); + $order = $orderServices->get($item['oid'], ['id', 'uid', 'pay_time', 'user_phone']); + if (!$order) { + continue; + } + $data['uid'] = $order['uid']; + $data['phone'] = $order['user_phone']; + event('notice.notice', [$data, 'expiration_reminder']); + $this->dao->update($item['id'], ['is_expire_sms' => 1]); + } + } + return true; + } +} diff --git a/app/services/order/StoreOrderComputedServices.php b/app/services/order/StoreOrderComputedServices.php new file mode 100644 index 0000000..3245137 --- /dev/null +++ b/app/services/order/StoreOrderComputedServices.php @@ -0,0 +1,708 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order; + +use app\services\activity\discounts\StoreDiscountsServices; +use app\services\BaseServices; +use app\common\dao\store\order\StoreOrderDao; +use app\services\other\CityAreaServices; +use app\services\pay\PayServices; +use app\services\product\brand\StoreBrandServices; +use app\services\product\category\StoreProductCategoryServices; +use app\services\user\member\MemberCardServices; +use app\services\user\UserServices; +use think\exception\ValidateException; +use app\services\user\UserAddressServices; +use app\services\activity\coupon\StoreCouponUserServices; +use app\services\product\shipping\ShippingTemplatesFreeServices; +use app\services\product\shipping\ShippingTemplatesRegionServices; +use app\services\product\shipping\ShippingTemplatesServices; +use function Swoole\Coroutine\batch; + +/** + * 订单计算金额 + * Class StoreOrderComputedServices + * @package app\services\order + * @mixin StoreOrderDao + */ +class StoreOrderComputedServices extends BaseServices +{ + /** + * 支付类型 + * @var string[] + */ + public $payType = ['weixin' => '微信支付', 'yue' => '余额支付', 'offline' => '线下支付', 'pc' => 'pc']; + + /** + * 额外参数 + * @var array + */ + protected $paramData = []; + + /** + * StoreOrderComputedServices constructor. + * @param StoreOrderDao $dao + */ + public function __construct(StoreOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * 设置额外参数 + * @param array $paramData + * @return $this + */ + public function setParamData(array $paramData) + { + $this->paramData = $paramData; + return $this; + } + + /** + * 计算订单金额 + * @param int $uid + * @param array $userInfo + * @param array $cartGroup + * @param int $addressId + * @param string $payType + * @param bool $useIntegral + * @param int $couponId + * @param int $shippingType + * @return array + */ + public function computedOrder(int $uid, array $userInfo = [], array $cartGroup, int $addressId, string $payType, bool $useIntegral = false, int $couponId = 0, int $shippingType = 1) + { + $offlinePayStatus = (int)sys_config('offline_pay_status') ?? (int)2; + $systemPayType = PayServices::PAY_TYPE; + if ($offlinePayStatus == 2) unset($systemPayType['offline']); + if ($payType && !array_key_exists($payType, $systemPayType)) { + throw new ValidateException('选择支付方式有误'); + } + if (!$userInfo) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserCacheInfo($uid); + if (!$userInfo) { + throw new ValidateException('用户不存在!'); + } + } + $cartInfo = $cartGroup['cartInfo']; + $priceGroup = $cartGroup['priceGroup']; + $deduction = $cartGroup['deduction']; + $other = $cartGroup['other']; + $promotions = $other['promotions'] ?? []; + $payPrice = (float)$priceGroup['totalPrice']; + $couponPrice = (float)$priceGroup['couponPrice']; + $firstOrderPrice = (float)$priceGroup['firstOrderPrice']; + $addr = $cartGroup['addr'] ?? []; + $postage = $priceGroup; + if (!$addr || $addr['id'] != $addressId) { + /** @var UserAddressServices $addressServices */ + $addressServices = app()->make(UserAddressServices::class); + $addr = $addressServices->getAdderssCache($addressId); + //改变地址重新计算邮费 + $postage = []; + } + $combinationId = $this->paramData['combinationId'] ?? 0; + $seckillId = $this->paramData['seckill_id'] ?? 0; + $bargainId = $this->paramData['bargainId'] ?? 0; + $newcomerId = $this->paramData['newcomerId'] ?? 0; + $isActivity = $combinationId || $seckillId || $bargainId || $newcomerId; + $type = (int)$deduction['type'] ?? 0; + $results = batch([ + // 'coupon' => function() use ($couponId, $uid, $cartInfo, $payPrice, $isActivity, $promotions) { + // if (!$isActivity) { + // try{ + // return $this->useCouponId($couponId, $uid, $cartInfo, $payPrice, $promotions); + // }catch (\Throwable $e){ + // return $e->getMessage(); + // } + // } + // return [$payPrice, 0]; + // }, + 'promotions' => function () use ($cartInfo, $type) { + $promotionsPrice = 0; + if ($type == 8) return $promotionsPrice; + foreach ($cartInfo as $key => $cart) { + if (isset($cart['promotions_true_price']) && isset($cart['price_type']) && $cart['price_type'] == 'promotions') { + $promotionsPrice = bcadd((string)$promotionsPrice, (string)bcmul((string)$cart['promotions_true_price'], (string)$cart['cart_num'], 2), 2); + } + } + return $promotionsPrice; + }, + 'postage' => function () use ($shippingType, $payType, $cartInfo, $addr, $payPrice, $postage, $other, $userInfo, $type) { + if ($type == 8 || $type == 10) $shippingType = 2; + return $this->computedPayPostage($shippingType, $payType, $cartInfo, $addr, $payPrice, $postage, $other, $userInfo); + }, + ]); + + // if ($results['coupon'] && is_string($results['coupon'])) { + // throw new ValidateException($results['coupon']); + // } + $promotionsDetail = []; + if ($promotions) { + foreach ($promotions as $key => $value) { + if (isset($value['details']['sum_promotions_price']) && $value['details']['sum_promotions_price']) { + $promotionsDetail[] = ['id' => $value['id'], 'name' => $value['name'], 'title' => $value['title'], 'desc' => $value['desc'], 'promotions_price' => $value['details']['sum_promotions_price'], 'promotions_type' => $value['promotions_type']]; + } + } + if ($promotionsDetail) { + $typeArr = array_column($promotionsDetail, 'promotions_type'); + array_multisort($typeArr, SORT_ASC, $promotionsDetail); + } + } + + // [$p, $couponPrice] = $results['coupon']; + [$p, $payPostage, $storePostageDiscount, $storeFreePostage, $isStoreFreePostage] = $results['postage']; + if ($couponPrice < $payPrice) {//优惠券金额 + $payPrice = bcsub((string)$payPrice, (string)$couponPrice, 2); + } else { + $payPrice = 0; + } + if ($type == 8) { + $firstOrderPrice = 0; + $payPrice = 0; + } + if ($firstOrderPrice < $payPrice) {//首单优惠金额 + $payPrice = bcsub((string)$payPrice, (string)$firstOrderPrice, 2); + } else { + $payPrice = 0; + } + if (sys_config('integral_ratio_status') && !$isActivity) { + //使用积分 + [$payPrice, $deductionPrice, $usedIntegral, $SurplusIntegral] = $this->useIntegral($useIntegral, $userInfo, $payPrice, $other); + } + $payPrice = (float)bcadd((string)$payPrice, (string)$payPostage, 2); + foreach ($cartInfo as &$item) { + $item['invalid'] = false; + if ($shippingType === 2 && $item['productInfo']['store_mention']) { + $item['invalid'] = true; + } + } + + $result = [ + 'total_price' => $priceGroup['totalPrice'], + 'pay_price' => max($payPrice, 0), + 'total_postage' => bcadd((string)$payPostage, (string)($storePostageDiscount ?? 0), 2), + 'pay_postage' => $payPostage, + 'first_order_price' => $firstOrderPrice ?? 0, + 'coupon_price' => $couponPrice ?? 0, + 'promotions_price' => $results['promotions'] ?? 0, + 'promotions_detail' => $promotionsDetail, + 'deduction_price' => $deductionPrice ?? 0, + 'usedIntegral' => $usedIntegral ?? 0, + 'SurplusIntegral' => $SurplusIntegral ?? 0, + 'storePostageDiscount' => $storePostageDiscount ?? 0, + 'isStoreFreePostage' => $isStoreFreePostage ?? false, + 'storeFreePostage' => $storeFreePostage ?? 0, + 'cartInfo' => $cartInfo + ]; + $this->paramData = []; + return $result; + } + + /** + * 使用优惠卷 + * @param int $couponId + * @param int $uid + * @param $cartInfo + * @param $payPrice + */ + public function useCouponId(int $couponId, int $uid, $cartInfo, $payPrice, $promotions) + { + //使用优惠劵 + $couponPrice = 0; + if ($couponId && $cartInfo) { + /** @var StoreCouponUserServices $couponServices */ + $couponServices = app()->make(StoreCouponUserServices::class); + $couponInfo = $couponServices->getOne([['id', '=', $couponId], ['uid', '=', $uid], ['is_fail', '=', 0], ['status', '=', 0], ['start_time', '<=', time()], ['end_time', '>=', time()]], '*', ['issue']); + if (!$couponInfo) { + throw new ValidateException('选择的优惠劵无效!'); + } + $type = $couponInfo['applicable_type'] ?? 0; + $flag = false; + $price = 0; + $count = 0; + $promotionsList = []; + if ($promotions) { + $promotionsList = array_combine(array_column($promotions, 'id'), $promotions); + } + $isOverlay = function ($cart) use ($promotionsList) { + $productInfo = $cart['productInfo'] ?? []; + if (!$productInfo) { + return false; + } + //门店独立商品 不使用优惠券 + $isBranchProduct = isset($productInfo['type']) && isset($productInfo['pid']) && $productInfo['type'] == 1 && !$productInfo['pid']; + if ($isBranchProduct) { + return false; + } + if (isset($cart['promotions_id']) && $cart['promotions_id']) { + foreach ($cart['promotions_id'] as $key => $promotions_id) { + $promotions = $promotionsList[$promotions_id] ?? []; + if ($promotions && $promotions['promotions_type'] != 4) { + $overlay = is_string($promotions['overlay']) ? explode(',', $promotions['overlay']) : $promotions['overlay']; + if (!in_array(5, $overlay)) { + return false; + } + } + } + } + return true; + }; + switch ($type) { + case 0: + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + $price = bcadd($price, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + break; + case 1://品类券 + /** @var StoreProductCategoryServices $storeCategoryServices */ + $storeCategoryServices = app()->make(StoreProductCategoryServices::class); + $cateGorys = $storeCategoryServices->getAllById((int)$couponInfo['category_id']); + if ($cateGorys) { + $cateIds = array_column($cateGorys, 'id'); + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + if (isset($cart['productInfo']['cate_id']) && array_intersect(explode(',', $cart['productInfo']['cate_id']), $cateIds)) { + $price = bcadd($price, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + } + break; + case 2: + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + $product_id = isset($cart['productInfo']['pid']) && $cart['productInfo']['pid'] ? $cart['productInfo']['pid'] : ($cart['product_id'] ?? 0); + if ($product_id && in_array($product_id, explode(',', $couponInfo['product_id']))) { + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + case 3: + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $brands = $storeBrandServices->getAllById((int)$couponInfo['brand_id']); + if ($brands) { + $brandIds = array_column($brands, 'id'); + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart)) continue; + if (isset($cart['productInfo']['brand_id']) && in_array($cart['productInfo']['brand_id'], $brandIds)) { + $price = bcadd((string)$price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2); + $count++; + } + } + } + break; + } + if ($count && $couponInfo['use_min_price'] <= $price) { + $flag = true; + } + if (!$flag) { + return [$payPrice, 0]; +// throw new ValidateException('不满足优惠劵的使用条件!'); + } + //满减券 + if ($couponInfo['coupon_type'] == 1) { + $couponPrice = $couponInfo['coupon_price']; + } else { + if ($couponInfo['coupon_price'] <= 0) {//0折 + $couponPrice = $price; + } else if ($couponInfo['coupon_price'] >= 100) { + $couponPrice = 0; + } else { + $truePrice = (float)bcmul((string)$price, bcdiv((string)$couponInfo['coupon_price'], '100', 2), 2); + $couponPrice = (float)bcsub((string)$price, (string)$truePrice, 2); + } + } + if ($couponPrice < $payPrice) { + $payPrice = (float)bcsub((string)$payPrice, (string)$couponPrice, 2); + } else { + $couponPrice = $payPrice; + $payPrice = 0; + } + } + return [$payPrice, $couponPrice]; + } + + + /** + * 使用积分 + * @param $useIntegral + * @param $userInfo + * @param $payPrice + * @param $other + * @return array + */ + public function useIntegral(bool $useIntegral, $userInfo, string $payPrice, array $other) + { + $SurplusIntegral = 0; + $deductionPrice = 0; + $usedIntegral = 0; + if ($userInfo && $useIntegral && $userInfo['integral'] > 0 && $payPrice) { + $integralMaxType = sys_config('integral_max_type', 1);//积分抵用上限类型1:积分、2:订单金额比例 + if ($integralMaxType == 1) {//最多抵用积分 + $integralMaxNum = sys_config('integral_max_num', 200); + if ($integralMaxNum > 0 && $userInfo['integral'] > $integralMaxNum) { + $integral = $integralMaxNum; + } else { + $integral = $userInfo['integral']; + } + $deductionPrice = (float)bcmul((string)$integral, (string)$other['integralRatio'], 2); + if ($deductionPrice < $payPrice) { + $payPrice = bcsub((string)$payPrice, (string)$deductionPrice, 2); + $usedIntegral = $integral; + } else { + if ($other['integralRatio']) { + $deductionPrice = $payPrice; + $usedIntegral = (int)ceil(bcdiv((string)$payPrice, (string)$other['integralRatio'], 2)); + } + $payPrice = 0; + } + } else {//最高抵用比率 + $integralMaxRate = sys_config('integral_max_rate', 0); + $deductionPrice = (float)bcmul((string)$userInfo['integral'], (string)$other['integralRatio'], 2); + if ($integralMaxRate > 0 && $integralMaxRate <= 100) { + $integralMaxPrice = (float)bcmul((string)$payPrice, (string)bcdiv((string)$integralMaxRate, '100', 2), 2); + } else { + $integralMaxPrice = $payPrice; + } + $deductionPrice = min($deductionPrice, $integralMaxPrice); + $payPrice = bcsub((string)$payPrice, (string)$deductionPrice, 2); + $usedIntegral = ceil(bcdiv((string)$deductionPrice, (string)$other['integralRatio'], 2)); + } + if ($payPrice <= 0) $payPrice = 0; + } + $SurplusIntegral = (int)bcsub((string)$userInfo['integral'], (string)$usedIntegral, 0); + return [$payPrice, $deductionPrice, $usedIntegral, $SurplusIntegral]; + } + + /** + * 计算邮费 + * @param int $shipping_type + * @param string $payType + * @param array $cartInfo + * @param array $addr + * @param string $payPrice + * @param array $postage + * @param array $other + * @param array $userInfo + * @return array + */ + public function computedPayPostage(int $shipping_type, string $payType, array $cartInfo, array $addr, string $payPrice, array $postage = [], array $other, $userInfo = []) + { + $storePostageDiscount = 0; + $storeFreePostage = $postage['storeFreePostage'] ?? 0; + $isStoreFreePostage = false; + if (!$storeFreePostage) { + $storeFreePostage = floatval(sys_config('store_free_postage')) ?: 0;//满额包邮金额 + } + if (!$addr && !isset($addr['id']) || !$cartInfo) { + $payPostage = 0; + } else { + //$shipping_type = 1 快递发货 $shipping_type = 2 门店自提 + if ($shipping_type == 2) { + if (!sys_config('store_func_status', 1) || !sys_config('store_self_mention', 1)) $shipping_type = 1; + } + //门店自提 || (线下支付 && 线下支付包邮) 没有邮费支付 + if ($shipping_type === 2 || ($payType == 'offline' && ((isset($other['offlinePostage']) && $other['offlinePostage']) || sys_config('offline_postage')) == 1)) { + $payPostage = 0; + } else { + if (!$postage || !isset($postage['storePostage']) || !isset($postage['storePostageDiscount'])) { + $postage = $this->getOrderPriceGroup($cartInfo, $addr, $userInfo, $storeFreePostage); + } + $payPostage = $postage['storePostage']; + $storePostageDiscount = $postage['storePostageDiscount']; + $isStoreFreePostage = $postage['isStoreFreePostage'] ?? false; + + $payPrice = (float)bcadd((string)$payPrice, (string)$payPostage, 2); + } + } + return [$payPrice, $payPostage, $storePostageDiscount, $storeFreePostage, $isStoreFreePostage]; + } + + + /** + * 运费计算,总金额计算 + * @param $cartInfo + * @param $addr + * @param array $userInfo + * @param null $storeFreePostage + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderPriceGroup($cartInfo, $addr, $userInfo = [], $storeFreePostage = null) + { + $storePostage = 0; + $storePostageDiscount = 0; + $isStoreFreePostage = false;//是否满额包邮 + if (is_null($storeFreePostage)) { + $storeFreePostage = floatval(sys_config('store_free_postage')) ?: 0;//满额包邮金额 + } + $sumPrice = $this->getOrderSumPrice($cartInfo, 'sum_price');//获取订单原总金额 + $totalPrice = $this->getOrderSumPrice($cartInfo, 'truePrice');//获取订单svip、用户等级优惠之后总金额 + $costPrice = $this->getOrderSumPrice($cartInfo, 'costPrice');//获取订单成本价 + $vipPrice = $this->getOrderSumPrice($cartInfo, 'vip_truePrice');//获取订单会员优惠金额 + + //如果满额包邮等于0 + $free_shipping = 0; + $postageArr = []; + if (isset($cartInfo[0]['productInfo']['product_type']) && in_array($cartInfo[0]['productInfo']['product_type'], [1, 2])) { + $storePostage = 0; + } elseif ($cartInfo && $addr) { + //优惠套餐包邮判断 + if (isset($cartInfo[0]['type']) && $cartInfo[0]['type'] == 5 && isset($cartInfo[0]['activity_id']) && $cartInfo[0]['activity_id']) { + /** @var StoreDiscountsServices $discountService */ + $discountService = app()->make(StoreDiscountsServices::class); + $free_shipping = $discountService->value(['id' => $cartInfo[0]['activity_id']], 'free_shipping'); + } + if ($free_shipping) { + $storePostage = 0; + } else if ($storeFreePostage && $sumPrice >= $storeFreePostage) {//如果总价大于等于满额包邮 邮费等于0 + $isStoreFreePostage = true; + $storePostage = 0; + } else { + + // 判断商品包邮和固定运费 + foreach ($cartInfo as &$item) { + if (!isset($item['productInfo']['freight'])) continue; + if ($item['productInfo']['freight'] == 1) { + $item['postage_price'] = 0; + } elseif ($item['productInfo']['freight'] == 2) { + $item['postage_price'] = bcmul((string)$item['productInfo']['postage'], (string)$item['cart_num'], 2); + $storePostage = bcadd((string)$storePostage, (string)$item['postage_price'], 2); + } + } + + //按照运费模板计算每个运费模板下商品的件数/重量/体积以及总金额 按照首重倒序排列 + $cityId = (int)($addr['city_id'] ?? 0); + $ids = []; + if ($cityId) { + /** @var CityAreaServices $cityAreaServices */ + $cityAreaServices = app()->make(CityAreaServices::class); + $ids = $cityAreaServices->getRelationCityIds($cityId); + } + $cityIds = array_merge([0], $ids); + + $tempIds[] = 1; + foreach ($cartInfo as $key_c => $item_c) { + if (isset($item_c['productInfo']['freight']) && $item_c['productInfo']['freight'] == 3) { + $tempIds[] = $item_c['productInfo']['temp_id']; + } + } + $tempIds = array_unique($tempIds); + /** @var ShippingTemplatesServices $shippServices */ + $shippServices = app()->make(ShippingTemplatesServices::class); + $temp = $shippServices->getShippingColumnCache(['id' => $tempIds], 'appoint,group', 'id'); + /** @var ShippingTemplatesRegionServices $regionServices */ + $regionServices = app()->make(ShippingTemplatesRegionServices::class); + $regions = $regionServices->getTempRegionListCache($tempIds, $cityIds); + $temp_num = []; + foreach ($cartInfo as $cart) { + if (isset($cart['productInfo']['freight']) && in_array($cart['productInfo']['freight'], [1, 2])) { + continue; + } + $tempId = $cart['productInfo']['temp_id'] ?? 1; + $group = isset($temp[$tempId]['group']) ? $temp[$tempId]['group'] : $temp[1]['group']; + if ($group == 1) { + $num = $cart['cart_num']; + } elseif ($group == 2) { + $num = $cart['cart_num'] * $cart['productInfo']['attrInfo']['weight']; + } else { + $num = $cart['cart_num'] * $cart['productInfo']['attrInfo']['volume']; + } + $region = isset($regions[$tempId]) ? $regions[$tempId] : ($regions[1] ?? []); + if (!$region) { + continue; + } + if (!isset($temp_num[$tempId])) { + $temp_num[$tempId] = [ + 'number' => $num, + 'group' => $group, + 'price' => bcmul($cart['cart_num'], $cart['truePrice'], 2), + 'first' => $region['first'], + 'first_price' => $region['first_price'], + 'continue' => $region['continue'], + 'continue_price' => $region['continue_price'], + 'temp_id' => $tempId + ]; + } else { + $temp_num[$tempId]['number'] += $num; + $temp_num[$tempId]['price'] += bcmul($cart['cart_num'], $cart['truePrice'], 2); + } + } + if ($temp_num) { + /** @var ShippingTemplatesFreeServices $freeServices */ + $freeServices = app()->make(ShippingTemplatesFreeServices::class); + $freeList = $freeServices->isFreeListCache($tempIds, $cityIds); + if ($freeList) { + foreach ($temp_num as $k => $v) { + if (isset($temp[$v['temp_id']]['appoint']) && $temp[$v['temp_id']]['appoint'] && isset($freeList[$v['temp_id']])) { + $free = $freeList[$v['temp_id']]; + $condition = $free['number'] <= $v['number']; + if ($free['price'] <= $v['price'] && $condition) { + unset($temp_num[$k]); + } + } + } + } + //首件运费最大值 + $maxFirstPrice = $temp_num ? max(array_column($temp_num, 'first_price')) : 0; + //初始运费为0 + $storePostage_arr = []; + + $i = 0; + //循环运费数组 + foreach ($temp_num as $fk => $fv) { + //找到首件运费等于最大值 + if ($fv['first_price'] == $maxFirstPrice) { + //每次循环设置初始值 + $tempArr = $temp_num; + $Postage = 0; + //计算首件运费 + if ($fv['number'] <= $fv['first']) { + $Postage = bcadd($Postage, $fv['first_price'], 2); + } else { + if ($fv['continue'] <= 0) { + $Postage = $Postage; + } else { + $Postage = bcadd(bcadd($Postage, $fv['first_price'], 2), bcmul(ceil(bcdiv(bcsub($fv['number'], $fv['first'], 2), $fv['continue'] ?? 0, 2)), $fv['continue_price'], 4), 2); + } + } + $postageArr[$i]['data'][$fk] = $Postage; + + //删除计算过的首件数据 + unset($tempArr[$fk]); + //循环计算剩余运费 + foreach ($tempArr as $ck => $cv) { + if ($cv['continue'] <= 0) { + $Postage = $Postage; + } else { + $one_postage = bcmul(ceil(bcdiv($cv['number'], $cv['continue'] ?? 0, 2)), $cv['continue_price'], 2); + $Postage = bcadd($Postage, $one_postage, 2); + $postageArr[$i]['data'][$ck] = $one_postage; + } + } + $postageArr[$i]['sum'] = $Postage; + $storePostage_arr[] = $Postage; + $i++; + } + } + $maxStorePostage = $storePostage_arr ? max($storePostage_arr) : 0; +// //获取运费计算中的最大值 + $storePostage = bcadd((string)$storePostage, (string)$maxStorePostage, 2); + } + } + } + + //会员邮费享受折扣 + if ($storePostage) { + $express_rule_number = 0; + if (!$userInfo) { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $userInfo = $userService->getUserCacheInfo($addr['uid']); + } + if ($userInfo && isset($userInfo['is_money_level']) && $userInfo['is_money_level'] > 0) { + //看是否开启会员折扣奖励 + /** @var MemberCardServices $memberCardService */ + $memberCardService = app()->make(MemberCardServices::class); + $express_rule_number = $memberCardService->isOpenMemberCardCache('express'); + $express_rule_number = $express_rule_number <= 0 ? 0 : $express_rule_number; + } + + $truePostageArr = []; + foreach ($postageArr as $postageitem) { + if ($postageitem['sum'] == ($maxStorePostage ?? 0)) { + $truePostageArr = $postageitem['data']; + break; + } + } + $cartAlready = []; + foreach ($cartInfo as &$item) { + if (isset($item['productInfo']['freight']) && in_array($item['productInfo']['freight'], [1, 2])) { + if (isset($item['postage_price']) && $item['postage_price'] && $express_rule_number && $express_rule_number < 100) { + $item['postage_price'] = bcmul($item['postage_price'], bcdiv($express_rule_number, 100, 4), 2); + } + continue; + } + $tempId = $item['productInfo']['temp_id'] ?? 0; + $tempPostage = $truePostageArr[$tempId] ?? 0; + $tempNumber = $temp_num[$tempId]['number'] ?? 0; + if (!$tempId || !$tempPostage) continue; + $group = $temp_num[$tempId]['group']; + + if ($group == 1) { + $num = $item['cart_num']; + } elseif ($group == 2) { + $num = $item['cart_num'] * $item['productInfo']['attrInfo']['weight']; + } else { + $num = $item['cart_num'] * $item['productInfo']['attrInfo']['volume']; + } + + if ((($cartAlready[$tempId]['number'] ?? 0) + $num) >= $tempNumber) { + $price = isset($cartAlready[$tempId]['price']) ? bcsub((string)$tempPostage, (string)$cartAlready[$tempId]['price'], 6) : $tempPostage; + } else { + $price = bcmul((string)$tempPostage, bcdiv((string)$num, (string)$tempNumber, 6), 6); + } + $cartAlready[$tempId]['number'] = bcadd((string)($cartAlready[$tempId]['number'] ?? 0), (string)$num, 4); + $cartAlready[$tempId]['price'] = bcadd((string)($cartAlready[$tempId]['price'] ?? 0.00), (string)$price, 4); + + if ($express_rule_number && $express_rule_number < 100) { + $price = bcmul($price, bcdiv($express_rule_number, 100, 4), 4); + } + $price = sprintf("%.2f", $price); + $item['postage_price'] = $price; + } + if ($express_rule_number && $express_rule_number < 100) { + $storePostageDiscount = $storePostage; + $storePostage = bcmul($storePostage, bcdiv($express_rule_number, 100, 4), 2); + $storePostageDiscount = bcsub($storePostageDiscount, $storePostage, 2); + } else { + $storePostageDiscount = 0; + $storePostage = $storePostage; + } + + } + return compact('storePostage', 'storeFreePostage', 'isStoreFreePostage', 'sumPrice', 'totalPrice', 'costPrice', 'vipPrice', 'storePostageDiscount', 'cartInfo'); + } + + /** + * 获取某个字段总金额 + * @param $cartInfo + * @param string $key + * @param bool $is_unit + * @return int|string + */ + public function getOrderSumPrice($cartInfo, $key = 'truePrice', $is_unit = true) + { + $SumPrice = 0; + foreach ($cartInfo as $cart) { + if (isset($cart['cart_info'])) $cart = $cart['cart_info']; + if (isset($cart['is_gift']) && $cart['is_gift']) { + continue; + } + if ($is_unit) { + $SumPrice = bcadd($SumPrice, bcmul($cart['cart_num'] ?? 1, $cart[$key] ?? 0, 2), 2); + } else { + $SumPrice = bcadd($SumPrice, $cart[$key] ?? 0, 2); + } + } + return $SumPrice; + } +} diff --git a/app/services/order/StoreOrderCreateServices.php b/app/services/order/StoreOrderCreateServices.php new file mode 100644 index 0000000..6eab672 --- /dev/null +++ b/app/services/order/StoreOrderCreateServices.php @@ -0,0 +1,941 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order; + + +use app\jobs\activity\StorePromotionsJob; +use app\services\activity\discounts\StoreDiscountsServices; +use app\services\activity\newcomer\StoreNewcomerServices; +use app\services\agent\AgentLevelServices; +use app\services\activity\coupon\StoreCouponUserServices; +use app\services\other\CityAreaServices; +use app\services\pay\PayServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\brand\StoreBrandServices; +use app\services\product\category\StoreProductCategoryServices; +use app\services\product\shipping\ShippingTemplatesFreeServices; +use app\services\product\shipping\ShippingTemplatesRegionServices; +use app\services\product\shipping\ShippingTemplatesServices; +use app\services\wechat\WechatUserServices; +use app\services\BaseServices; +use crmeb\services\CacheService; +use app\common\dao\store\order\StoreOrderDao; +use app\services\user\UserServices; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; +use app\services\user\UserBillServices; +use app\services\user\UserAddressServices; +use app\services\activity\bargain\StoreBargainServices; +use app\services\activity\seckill\StoreSeckillServices; +use app\services\store\SystemStoreServices; +use app\services\activity\combination\StoreCombinationServices; +use app\services\product\product\StoreProductServices; +use app\services\activity\collage\UserCollageServices; +use app\services\activity\collage\UserCollagePartakeServices; +use think\facade\Cache; +use think\facade\Log; + +/** + * 订单创建 + * Class StoreOrderCreateServices + * @package app\services\order + * @mixin StoreOrderDao + */ +class StoreOrderCreateServices extends BaseServices +{ + use ServicesTrait; + + /** + * StoreOrderCreateServices constructor. + * @param StoreOrderDao $dao + */ + public function __construct(StoreOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * 使用雪花算法生成订单ID + * @return string + * @throws \Exception + */ + public function getNewOrderId(string $prefix = 'wx') + { + $snowflake = new \Godruoyi\Snowflake\Snowflake(); + $is_callable = function ($currentTime) { +// if (is_win()) { + $redis = Cache::store('redis'); + $swooleSequenceResolver = new \Godruoyi\Snowflake\RedisSequenceResolver($redis->handler()); + return $swooleSequenceResolver->sequence($currentTime); +// } else { +// $swooleSequenceResolver = new \Godruoyi\Snowflake\SwooleSequenceResolver(); +// return $swooleSequenceResolver->sequence($currentTime); +// } + }; + //32位 + if (PHP_INT_SIZE == 4) { + $id = abs($snowflake->setSequenceResolver($is_callable)->id()); + } else { + $id = $snowflake->setStartTimeStamp(strtotime('2020-06-05') * 1000)->setSequenceResolver($is_callable)->id(); + } + return $prefix . $id; + } + + /** + * 核销订单生成核销码 + * @return false|string + */ + public function getStoreCode() + { + mt_srand(); + [$msec, $sec] = explode(' ', microtime()); + $num = time() + mt_rand(10, 999999) . '' . substr($msec, 2, 3);//生成随机数 + if (strlen($num) < 12) + $num = str_pad((string)$num, 12, 0, STR_PAD_RIGHT); + else + $num = substr($num, 0, 12); + if ($this->dao->count(['verify_code' => $num])) { + return $this->getStoreCode(); + } + return $num; + } + + /** + * 创建订单 + * @param int $uid + * @param string $key + * @param array $cartGroup + * @param int $addressId + * @param string $payType + * @param array $addressInfo + * @param array $userInfo + * @param bool $useIntegral + * @param int $couponId + * @param string $mark + * @param int $pinkId + * @param int $isChannel + * @param int $shippingType + * @param int $storeId + * @param false $news + * @param array $customForm + * @param int $invoice_id + * @param string $from + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createOrder(int $uid, string $key, array $cartGroup, int $addressId, string $payType, array $addressInfo, array $userInfo = [], bool $useIntegral = false, $couponId = 0, $mark = '', $pinkId = 0, $isChannel = 0, $shippingType = 1, $storeId = 0, $news = false, $customForm = [], int $invoice_id = 0, string $from = '', int $collate_code_id = 0) + { + /** @var StoreOrderComputedServices $computedServices */ + $computedServices = app()->make(StoreOrderComputedServices::class); + $priceData = $computedServices->computedOrder($uid, $userInfo, $cartGroup, $addressId, $payType, $useIntegral, $couponId, $shippingType); + $cartInfo = $cartGroup['cartInfo']; + $priceGroup = $cartGroup['priceGroup']; + $cartIds = []; + $totalNum = 0; + $gainIntegral = 0; + foreach ($cartInfo as $cart) { + $cartIds[] = $cart['id']; + $totalNum += $cart['cart_num']; + $cartInfoGainIntegral = isset($cart['productInfo']['give_integral']) ? bcmul((string)$cart['cart_num'], (string)$cart['productInfo']['give_integral'], 0) : 0; + $gainIntegral = bcadd((string)$gainIntegral, (string)$cartInfoGainIntegral, 0); + } + $deduction = $cartGroup['deduction']; + $other = $cartGroup['other']; + $promotions_give = [ + 'give_integral' => $other['give_integral'] ?? 0, + 'give_coupon' => $other['give_coupon'] ?? [], + 'give_product' => $other['give_product'] ?? [], + 'promotions' => $other['promotions'] ?? [] + ]; + $type = (int)$deduction['type'] ?? 0; + $activity_id = (int)$deduction['activity_id'] ?? 0; + $collateCodeId = (int)$deduction['collate_code_id'] ?? 0; + $product_type = (int)$deduction['product_type'] ?? 0; + /** @var UserCollageServices $collageServices */ + $collageServices = app()->make(UserCollageServices::class); + if (in_array($type, [1, 2, 3, 5])) { + $couponId = 0; + if ($type != 5) $useIntegral = false; + $systemPayType = PayServices::PAY_TYPE; + unset($systemPayType['offline']); + if ($from != 'pc' && !array_key_exists($payType, $systemPayType)) { + throw new ValidateException('营销商品不能使用线下支付!'); + } + } else if ($type == 8) { + $gainIntegral = 0; + } else if ($type == 9 || $type == 10) { + if ($collateCodeId != $collate_code_id) throw new ValidateException('拼单/桌码ID有误!'); + $status = $collageServices->value(['id' => $collate_code_id], 'status'); + if ($status >= 2) throw new ValidateException($type == 10 ? '桌码' : '拼单' . '已生成订单!'); + $activity_id = $collate_code_id; + } + //$shipping_type = 1 快递发货 $shipping_type = 2 门店自提 + if (!sys_config('store_func_status', 1) || !sys_config('store_self_mention', 1)) $shippingType = 1; + + $userAddress = $addressInfo['province'] . ' ' . $addressInfo['city'] . ' ' . $addressInfo['district'] . ' ' . $addressInfo['street'] . ' ' . $addressInfo['detail']; + $userLocation = $addressInfo['longitude'] . ' ' . $addressInfo['latitude']; + $orderInfo = [ + 'uid' => $uid, + 'type' => $type, + 'order_id' => $this->getNewOrderId(), + 'real_name' => $addressInfo['real_name'], + 'user_phone' => $addressInfo['phone'], + 'user_address' => $userAddress, + 'user_location' => $userLocation, + 'cart_id' => $cartIds, + 'total_num' => $totalNum, + 'total_price' => $priceGroup['sumPrice'] ?? $priceGroup['totalPrice'], + 'total_postage' => $priceData['total_postage'] ?? $priceGroup['storePostage'], + 'coupon_id' => $couponId, + 'coupon_price' => $priceData['coupon_price'], + 'first_order_price' => $priceData['first_order_price'], + 'promotions_price' => $priceData['promotions_price'], + 'pay_price' => $priceData['pay_price'], + 'pay_postage' => $priceData['pay_postage'], + 'deduction_price' => $priceData['deduction_price'], + 'paid' => 0, + 'pay_type' => $payType, + 'use_integral' => $priceData['usedIntegral'], + 'gain_integral' => $gainIntegral, + 'mark' => htmlspecialchars($mark), + 'product_type' => $product_type, + 'activity_id' => $activity_id, + 'pink_id' => $pinkId, + 'cost' => $priceGroup['costPrice'], + 'is_channel' => $isChannel, + 'add_time' => time(), + 'unique' => $key, + 'shipping_type' => $shippingType, + 'channel_type' => $userInfo['user_type'], + 'province' => '', + 'spread_uid' => 0, + 'spread_two_uid' => 0, + 'custom_form' => json_encode($customForm), + 'promotions_give' => json_encode($promotions_give), + 'give_integral' => $promotions_give['give_integral'] ?? 0, + 'give_coupon' => implode(',', $promotions_give['give_coupon'] ?? []), + 'store_id' => $storeId + ]; + if ($userInfo['user_type'] == 'wechat' || $userInfo['user_type'] == 'routine') { + /** @var WechatUserServices $wechatServices */ + $wechatServices = app()->make(WechatUserServices::class); + $orderInfo['province'] = $wechatServices->value(['uid' => $uid, 'user_type' => $userInfo['user_type']], 'province') ?: ''; + } + if ($shippingType == 2) { + $orderInfo['verify_code'] = $this->getStoreCode(); + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $orderInfo['store_id'] = $storeServices->getStoreDisposeCache($storeId, 'id'); + if (!$orderInfo['store_id']) { + throw new ValidateException('暂无门店无法选择门店自提'); + } + } + + $priceData['coupon_id'] = $couponId; + $order = $this->transaction(function () use ($cartIds, $couponId, $orderInfo, $cartInfo, $key, $userInfo, $useIntegral, $priceData, $type, $activity_id, $uid, $addressId, $promotions_give, $storeId) { + //创建订单 + $order = $this->dao->save($orderInfo); + if ($couponId) { + /** @var StoreCouponUserServices $couponServices */ + $couponServices = app()->make(StoreCouponUserServices::class); + $couponServices->useCoupon($couponId, (int)$userInfo['uid'], $cartInfo, [], $storeId); + } + //抵扣积分 + $this->deductIntegral($userInfo, $useIntegral, $priceData, (int)$userInfo['uid'], $key); + //扣库存 + $this->decGoodsStock($cartInfo, $type, $activity_id, $orderInfo['store_id'] ?? 0); + //保存购物车商品信息 + /** @var StoreOrderCartInfoServices $cartServices */ + $cartServices = app()->make(StoreOrderCartInfoServices::class); + $cartServices->setCartInfo($order['id'], $cartInfo, (int)$uid, $promotions_give['promotions'] ?? []); + + return $order; + }); + + if (in_array($type, [9, 10]) && $collate_code_id > 0 && $order) { + //关联订单和拼单、桌码 + $collageServices->update($collate_code_id, ['oid' => $order['id'], 'status' => 2]); + + /** @var UserCollagePartakeServices $partakeService */ + $partakeService = app()->make(UserCollagePartakeServices::class); + $partakeService->update(['collate_code_id' => $collate_code_id, 'is_settle' => 0], ['status' => 0]); + } + + //保存购物车商品信息 +// StoreCartJob::dispatch([$order['id'], $cartInfo, $uid, $promotions_give['promotions'] ?? []]); + //扣除优惠活动赠品限量 + StorePromotionsJob::dispatchDo('changeGiveLimit', [$promotions_give]); + //订单创建事件 + event('order.create', [$order, $userInfo, compact('cartInfo', 'priceData', 'addressId', 'cartIds', 'news'), compact('type', 'activity_id'), $invoice_id]); + return $order; + } + + + /** + * 抵扣积分 + * @param array $userInfo + * @param bool $useIntegral + * @param array $priceData + * @param int $uid + * @param string $key + */ + public function deductIntegral(array $userInfo, bool $useIntegral, array $priceData, int $uid, string $key) + { + $res2 = true; + if (sys_config('integral_ratio_status', 1) && $userInfo && $useIntegral && $userInfo['integral'] > 0) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$priceData['SurplusIntegral']) { + $integral = 0; + } else { + $integral = bcsub((string)$userInfo['integral'], (string)$priceData['usedIntegral']); + } + $res2 = false !== $userServices->update($uid, ['integral' => $integral]); + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $res3 = $userBillServices->income('deduction', $uid, [ + 'number' => (int)$priceData['usedIntegral'], + 'deductionPrice' => $priceData['deduction_price'] + ], $integral, $key); + + $res2 = $res2 && false != $res3; + } + if (!$res2) { + throw new ValidateException('使用积分抵扣失败!'); + } + } + + /** + * 扣库存 + * @param array $cartInfo + * @param int $type + * @param int $activity_id + * @param int $store_id + */ + public function decGoodsStock(array $cartInfo, int $type, int $activity_id, int $store_id = 0) + { + $res5 = true; + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + /** @var StoreSeckillServices $seckillServices */ + $seckillServices = app()->make(StoreSeckillServices::class); + /** @var StoreCombinationServices $pinkServices */ + $pinkServices = app()->make(StoreCombinationServices::class); + /** @var StoreBargainServices $bargainServices */ + $bargainServices = app()->make(StoreBargainServices::class); + /** @var StoreDiscountsServices $discountServices */ + $discountServices = app()->make(StoreDiscountsServices::class); + /** @var StoreNewcomerServices $storeNewcomerServices */ + $storeNewcomerServices = app()->make(StoreNewcomerServices::class); + try { + foreach ($cartInfo as $cart) { + $unique = isset($cart['productInfo']['attrInfo']) ? $cart['productInfo']['attrInfo']['unique'] : ''; + $cart_num = (int)$cart['cart_num']; + //减库存加销量 + switch ($type) { + case 0://普通 + case 6://预售 + case 8://抽奖 + case 9://拼单 + case 10://桌码 + $res5 = $res5 && $services->decProductStock($cart_num, (int)$cart['productInfo']['id'], $unique, $store_id); + break; + case 1://秒杀 + $res5 = $res5 && $seckillServices->decSeckillStock($cart_num, $activity_id, $unique, $store_id); + break; + case 2://砍价 + $res5 = $res5 && $bargainServices->decBargainStock($cart_num, $activity_id, $unique, $store_id); + break; + case 3://拼团 + $res5 = $res5 && $pinkServices->decCombinationStock($cart_num, $activity_id, $unique, $store_id); + break; + case 5://套餐 + $res5 = $res5 && $discountServices->decDiscountStock($cart_num, $activity_id, (int)($cart['discount_product_id'] ?? 0), (int)($cart['product_id'] ?? 0), $unique, $store_id); + break; + case 7://新人专享 + $res5 = $res5 && $storeNewcomerServices->decNewcomerStock($cart_num, $activity_id, $unique, $store_id); + break; + default: + $res5 = $res5 && $services->decProductStock($cart_num, (int)$cart['productInfo']['id'], $unique, $store_id); + break; + } + } + if ($type == 5 && $activity_id) { + //改变套餐限量 + $res5 = $res5 && $discountServices->changeDiscountLimit($activity_id); + } + if (!$res5) { + throw new ValidateException('库存不足!'); + } + } catch (\Throwable $e) { + throw new ValidateException('库存不足!'); + } + } + + /** + * 订单创建后的后置事件 + * @param UserAddressServices $addressServices + * @param $order + * @param array $group + */ + public function orderCreateAfter($order, array $group) + { + /** @var UserAddressServices $addressServices */ + $addressServices = app()->make(UserAddressServices::class); + //设置用户默认地址 + if ($order['uid'] && isset($group['addressId']) && $group['addressId'] && !$addressServices->be(['is_default' => 1, 'uid' => $order['uid']])) { + $addressServices->setDefaultAddress($group['addressId'], $order['uid']); + } + //删除购物车 + if (isset($group['news']) && $group['news']) { + array_map(function ($key) { + CacheService::redisHandler()->delete($key); + }, $group['cartIds']); + } else { + if (!isset($group['delCart']) || (isset($group['delCart']) && $group['delCart'] !== false)) { + /** @var StoreCartServices $cartServices */ + $cartServices = app()->make(StoreCartServices::class); + $cartServices->deleteCartStatus($group['cartIds'] ?? []); + } + } + } + + /** + * 计算订单每个商品真实付款价格 + * @param array $orderInfo + * @param array $cartInfo + * @param array $priceData + * @param $addressId + * @param int $uid + * @param $userInfo + * @return array + */ + public function computeOrderProductTruePrice($orderInfo, array $cartInfo, array $priceData, $addressId, int $uid, $userInfo) + { + //统一放入默认数据 + foreach ($cartInfo as &$cart) { + $cart['use_integral'] = 0; + $cart['integral_price'] = 0.00; + if (!isset($cart['coupon_price'])) { + $cart['coupon_price'] = 0.00; + } + $cart['first_order_price'] = 0.00; + $cart['one_brokerage'] = 0.00; + $cart['two_brokerage'] = 0.00; + } + try { + $promotionsGice = isset($orderInfo['promotions_give']) ? (is_string($orderInfo['promotions_give']) ? json_decode($orderInfo['promotions_give'], true) : $orderInfo['promotions_give']) : []; + $promotions = []; + if (isset($promotionsGice['promotions']) && $promotionsGice['promotions']) { + $promotions = $promotionsGice['promotions']; + } + //$cartInfo = $this->computeOrderProductCoupon($cartInfo, $priceData, $promotions); + $cartInfo = $this->computeOrderProductIntegral($cartInfo, $priceData); +// $cartInfo = $this->computeOrderProductPostage($cartInfo, $priceData); + $cartInfo = $this->computeOrderProductFirstDiscount($cartInfo, $priceData); + } catch (\Throwable $e) { + Log::error('订单商品结算失败,File:' . $e->getFile() . ',Line:' . $e->getLine() . ',Message:' . $e->getMessage()); + throw new ValidateException('订单商品结算失败'); + } + //truePice实际支付单价(存在) + //几件商品总体优惠 以及积分抵扣金额 + foreach ($cartInfo as &$cart) { + $coupon_price = $cart['coupon_price'] ?? 0; + $integral_price = $cart['integral_price'] ?? 0; + $first_order_price = $cart['first_order_price'] ?? 0; + $cart['sum_true_price'] = bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2); + if ($coupon_price) { + $cart['sum_true_price'] = bcsub((string)$cart['sum_true_price'], (string)$coupon_price, 2); + $uni_coupon_price = (string)bcdiv((string)$coupon_price, (string)$cart['cart_num'], 4); + $cart['truePrice'] = $cart['truePrice'] > $uni_coupon_price ? bcsub((string)$cart['truePrice'], $uni_coupon_price, 2) : 0; + } + if ($integral_price) { + $cart['sum_true_price'] = bcsub((string)$cart['sum_true_price'], (string)$integral_price, 2); + $uni_integral_price = (string)bcdiv((string)$integral_price, (string)$cart['cart_num'], 4); + $cart['truePrice'] = $cart['truePrice'] > $uni_integral_price ? bcsub((string)$cart['truePrice'], $uni_integral_price, 2) : 0; + } + if ($first_order_price) { + $cart['sum_true_price'] = bcsub((string)$cart['sum_true_price'], (string)$first_order_price, 2); + $uni_first_order_price = (string)bcdiv((string)$first_order_price, (string)$cart['cart_num'], 4); + $cart['truePrice'] = $cart['truePrice'] > $uni_first_order_price ? bcsub((string)$cart['truePrice'], $uni_first_order_price, 2) : 0; + } + } + //计算佣金:1售价2实际支付金额 + [$cartInfo, $spread_ids] = $this->computeOrderProductBrokerage($uid, $cartInfo, $userInfo); + return [$cartInfo, $spread_ids]; + } + + /** + * 计算每个商品实际支付运费 + * @param array $cartInfo + * @param array $priceData + * @return array + */ + public function computeOrderProductPostage(array $cartInfo, array $priceData, $addressId) + { + $storePostage = $priceData['pay_postage'] ?? 0; + if ($storePostage) { + /** @var UserAddressServices $addressServices */ + $addressServices = app()->make(UserAddressServices::class); + $addr = $addressServices->getAdderssCache($addressId); + if ($addr) { + //按照运费模板计算每个运费模板下商品的件数/重量/体积以及总金额 按照首重倒序排列 + $cityId = $addr['city_id'] ?? 0; + $ids = []; + if ($cityId) { + /** @var CityAreaServices $cityAreaServices */ + $cityAreaServices = app()->make(CityAreaServices::class); + $ids = $cityAreaServices->getRelationCityIds($cityId); + } + $cityIds = array_merge([0], $ids); + + $tempIds[] = 1; + foreach ($cartInfo as $key_c => $item_c) { + $tempIds[] = $item_c['productInfo']['temp_id']; + } + $tempIds = array_unique($tempIds); + /** @var ShippingTemplatesServices $shippServices */ + $shippServices = app()->make(ShippingTemplatesServices::class); + $temp = $shippServices->getShippingColumn(['id' => $tempIds], 'type,appoint', 'id'); + /** @var ShippingTemplatesRegionServices $regionServices */ + $regionServices = app()->make(ShippingTemplatesRegionServices::class); + $regions = $regionServices->getTempRegionList($tempIds, $cityIds, 'temp_id,first,first_price,continue,continue_price', 'temp_id'); + $temp_num = []; + foreach ($cartInfo as $cart) { + $tempId = $cart['productInfo']['temp_id'] ?? 1; + $type = isset($temp[$tempId]['type']) ? $temp[$tempId]['type'] : $temp[1]['type']; + if ($type == 1) { + $num = $cart['cart_num']; + } elseif ($type == 2) { + $num = $cart['cart_num'] * $cart['productInfo']['attrInfo']['weight']; + } else { + $num = $cart['cart_num'] * $cart['productInfo']['attrInfo']['volume']; + } + $region = isset($regions[$tempId]) ? $regions[$tempId] : $regions[1]; + if (!isset($temp_num[$tempId])) { + $temp_num[$tempId] = [ + 'cart_id' => [$cart['id']], + 'number' => $num, + 'type' => $type, + 'price' => bcmul($cart['cart_num'], $cart['truePrice'], 2), + 'first' => $region['first'], + 'first_price' => $region['first_price'], + 'continue' => $region['continue'], + 'continue_price' => $region['continue_price'], + 'temp_id' => $tempId + ]; + } else { + $temp_num[$tempId]['cart_id'][] = $cart['id']; + $temp_num[$tempId]['number'] += $num; + $temp_num[$tempId]['price'] += bcmul($cart['cart_num'], $cart['truePrice'], 2); + } + } + $cartInfo = array_combine(array_column($cartInfo, 'id'), $cartInfo); + /** @var ShippingTemplatesFreeServices $freeServices */ + $freeServices = app()->make(ShippingTemplatesFreeServices::class); + $freeList = $freeServices->isFreeList($tempIds, $cityIds, 0, 'temp_id,number,price', 'temp_id'); + if ($freeList) { + foreach ($temp_num as $k => $v) { + if (isset($temp[$v['temp_id']]['appoint']) && $temp[$v['temp_id']]['appoint'] && isset($freeList[$v['temp_id']])) { + $free = $freeList[$v['temp_id']]; + $condition = $free['number'] <= $v['number']; + if ($free['price'] <= $v['price'] && $condition) { + //免运费 + foreach ($v['cart_id'] as $c_id) { + if (isset($cartInfo[$c_id])) $cartInfo[$c_id]['postage_price'] = 0.00; + } + } + } + } + } + $count = 0; + $compute_price = 0.00; + $total_price = 0; + $postage_price = 0.00; + foreach ($cartInfo as $cart) { + if (isset($cart['postage_price'])) {//免运费 + continue; + } + if (isset($cart['is_gift']) && $cart['is_gift'] == 1) { + continue; + } + $total_price = bcadd((string)$total_price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 4), 2); + $count++; + } + foreach ($cartInfo as &$cart) { + if (isset($cart['postage_price'])) {//免运费 + continue; + } + if (isset($cart['is_gift']) && $cart['is_gift'] == 1) { + continue; + } + if ($count > 1) { + $postage_price = bcmul((string)bcdiv((string)bcmul((string)$cart['cart_num'], (string)$cart['truePrice'], 4), (string)$total_price, 4), (string)$storePostage, 2); + $compute_price = bcadd((string)$compute_price, (string)$postage_price, 2); + } else { + $postage_price = bcsub((string)$storePostage, $compute_price, 2); + } + $cart['postage_price'] = $postage_price; + $count--; + } + $cartInfo = array_merge($cartInfo); + } + } + return $cartInfo; + } + + /** + * 计算订单商品积分实际抵扣金额 + * @param array $cartInfo + * @param array $priceData + * @return array + */ + public function computeOrderProductIntegral(array $cartInfo, array $priceData) + { + $usedIntegral = $priceData['usedIntegral'] ?? 0; + $deduction_price = $priceData['deduction_price'] ?? 0; + if ($deduction_price) { + $count = 0; + $total_price = 0.00; + $compute_price = 0.00; + $integral_price = 0.00; + $use_integral = 0; + $compute_integral = 0; + foreach ($cartInfo as $cart) { + if (isset($cart['is_gift']) && $cart['is_gift'] == 1) { + continue; + } + $total_price = bcadd((string)$total_price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 4), 2); + $count++; + } + if ($total_price == $deduction_price) { + $ratio = 1; + } else { + $ratio = bcdiv((string)$deduction_price, (string)$total_price, 4); + } + foreach ($cartInfo as &$cart) { + if (isset($cart['is_gift']) && $cart['is_gift'] == 1) { + continue; + } + if ($count > 1) { + + $integral_price = bcmul((string)bcmul((string)$cart['cart_num'], (string)$cart['truePrice'], 4), (string)$ratio, 2); + $compute_price = bcadd((string)$compute_price, (string)$integral_price, 2); + $use_integral = bcmul((string)bcdiv((string)bcmul((string)$cart['cart_num'], (string)$cart['truePrice'], 4), (string)$total_price, 4), (string)$usedIntegral, 0); + $compute_integral = bcadd((string)$compute_integral, $use_integral, 0); + } else { + $integral_price = bcsub((string)$deduction_price, $compute_price, 2); + $use_integral = bcsub((string)$usedIntegral, $compute_integral, 0); + } + $count--; + $cart['integral_price'] = $integral_price; + $cart['use_integral'] = $use_integral; + } + } + return $cartInfo; + } + + /** + * 计算订单商品优惠券实际抵扣金额 + * @param array $cartInfo + * @param array $priceData + * @return array + */ + public function computeOrderProductCoupon(array $cartInfo, array $priceData, array $promotions = [], int $store_id = 0) + { + if ($priceData['coupon_id'] && $priceData['coupon_price'] ?? 0) { + $count = 0; + $total_price = 0.00; + $compute_price = 0.00; + $coupon_price = 0.00; + /** @var StoreCouponUserServices $couponServices */ + $couponServices = app()->make(StoreCouponUserServices::class); + $couponInfo = $couponServices->getOne(['id' => $priceData['coupon_id']], '*', ['issue']); + if ($couponInfo) { + //验证是否适用门店 + if ($store_id && isset($couponInfo['applicable_type']) && isset($couponInfo['applicable_store_id'])) { + $applicable_store_id = is_array($couponInfo['applicable_store_id']) ? $couponInfo['applicable_store_id'] : explode(',', $couponInfo['applicable_store_id']); + //活动不适用该门店 + if ($couponInfo['applicable_type'] == 0 || ($couponInfo['applicable_type'] == 2 && !in_array($store_id, $applicable_store_id))) { + return [[], 0]; + } + } + $promotionsList = []; + if ($promotions) { + $promotionsList = array_combine(array_column($promotions, 'id'), $promotions); + } + $isOverlay = function ($cart) use ($promotionsList) { + $productInfo = $cart['productInfo'] ?? []; + if (!$productInfo) { + return false; + } + //门店独立商品 不使用优惠券 + $isBranchProduct = isset($productInfo['type']) && isset($productInfo['pid']) && $productInfo['type'] == 1 && !$productInfo['pid']; + if ($isBranchProduct) { + return false; + } + if (isset($cart['promotions_id']) && $cart['promotions_id']) { + foreach ($cart['promotions_id'] as $key => $promotions_id) { + $promotions = $promotionsList[$promotions_id] ?? []; + if ($promotions && $promotions['promotions_type'] != 4) { + $overlay = is_string($promotions['overlay']) ? explode(',', $promotions['overlay']) : $promotions['overlay']; + if (!in_array(5, $overlay)) { + return false; + } + } + } + } + return true; + }; + $type = $couponInfo['applicable_type'] ?? 0; + $counpon_id = $couponInfo['id']; + switch ($type) { + case 0: + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart) || (isset($cart['is_gift']) && $cart['is_gift'] == 1)) continue; + $total_price = bcadd((string)$total_price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 4), 2); + $count++; + } + foreach ($cartInfo as &$cart) { + $cart['coupon_id'] = 0; + $cart['coupon_price'] = 0; + if (!$isOverlay($cart) || (isset($cart['is_gift']) && $cart['is_gift'] == 1)) continue; + if ($count > 1) { + $coupon_price = bcmul((string)bcdiv((string)bcmul((string)$cart['cart_num'], (string)$cart['truePrice'], 4), (string)$total_price, 4), (string)$priceData['coupon_price'], 2); + $compute_price = bcadd((string)$compute_price, (string)$coupon_price, 2); + } else { + $coupon_price = bcsub((string)$priceData['coupon_price'], $compute_price, 2); + } + $cart['coupon_price'] = $coupon_price; + $cart['coupon_id'] = $counpon_id; + $count--; + } + break; + case 1://品类券 + /** @var StoreProductCategoryServices $storeCategoryServices */ + $storeCategoryServices = app()->make(StoreProductCategoryServices::class); + $cateGorys = $storeCategoryServices->getAllById((int)$couponInfo['category_id']); + if ($cateGorys) { + $cateIds = array_column($cateGorys, 'id'); + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart) || (isset($cart['is_gift']) && $cart['is_gift'] == 1)) continue; + if (isset($cart['productInfo']['cate_id']) && array_intersect(explode(',', $cart['productInfo']['cate_id']), $cateIds)) { + $total_price = bcadd((string)$total_price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 4), 2); + $count++; + } + } + foreach ($cartInfo as &$cart) { + $cart['coupon_id'] = 0; + $cart['coupon_price'] = 0; + if (!$isOverlay($cart) || (isset($cart['is_gift']) && $cart['is_gift'] == 1)) continue; + if (isset($cart['productInfo']['cate_id']) && array_intersect(explode(',', $cart['productInfo']['cate_id']), $cateIds)) { + if ($count > 1) { + $coupon_price = bcmul((string)bcdiv((string)bcmul((string)$cart['cart_num'], (string)$cart['truePrice'], 4), (string)$total_price, 4), (string)$priceData['coupon_price'], 2); + $compute_price = bcadd((string)$compute_price, (string)$coupon_price, 2); + } else { + $coupon_price = bcsub((string)$priceData['coupon_price'], $compute_price, 2); + } + $cart['coupon_id'] = $counpon_id; + $cart['coupon_price'] = $coupon_price; + $count--; + } + } + } + break; + case 2://商品劵 + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart) || (isset($cart['is_gift']) && $cart['is_gift'] == 1)) continue; + $product_id = isset($cart['productInfo']['pid']) && $cart['productInfo']['pid'] ? $cart['productInfo']['pid'] : ($cart['product_id'] ?? 0); + if ($product_id && in_array($product_id, explode(',', $couponInfo['product_id']))) { + $total_price = bcadd((string)$total_price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 4), 2); + $count++; + } + + } + foreach ($cartInfo as &$cart) { + $cart['coupon_id'] = 0; + $cart['coupon_price'] = 0; + if (!$isOverlay($cart) || (isset($cart['is_gift']) && $cart['is_gift'] == 1)) continue; + $product_id = isset($cart['productInfo']['pid']) && $cart['productInfo']['pid'] ? $cart['productInfo']['pid'] : ($cart['product_id'] ?? 0); + if ($product_id && in_array($product_id, explode(',', $couponInfo['product_id']))) { + if ($count > 1) { + $coupon_price = bcmul((string)bcdiv((string)bcmul((string)$cart['cart_num'], (string)$cart['truePrice'], 4), (string)$total_price, 4), (string)$priceData['coupon_price'], 2); + $compute_price = bcadd((string)$compute_price, (string)$coupon_price, 2); + } else { + $coupon_price = bcsub((string)$priceData['coupon_price'], $compute_price, 2); + } + $cart['coupon_id'] = $counpon_id; + $cart['coupon_price'] = $coupon_price; + $count--; + } + } + break; + case 3://品牌券 + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $brands = $storeBrandServices->getAllById((int)$couponInfo['brand_id']); + if ($brands) { + $brandIds = array_column($brands, 'id'); + foreach ($cartInfo as $cart) { + if (!$isOverlay($cart) || (isset($cart['is_gift']) && $cart['is_gift'] == 1)) continue; + if (isset($cart['productInfo']['brand_id']) && in_array($cart['productInfo']['brand_id'], $brandIds)) { + $total_price = bcadd((string)$total_price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 4), 2); + $count++; + } + } + foreach ($cartInfo as &$cart) { + $cart['coupon_id'] = 0; + $cart['coupon_price'] = 0; + if (!$isOverlay($cart) || (isset($cart['is_gift']) && $cart['is_gift'] == 1)) continue; + if (isset($cart['productInfo']['brand_id']) && in_array($cart['productInfo']['brand_id'], $brandIds)) { + if ($count > 1) { + $coupon_price = bcmul((string)bcdiv((string)bcmul((string)$cart['cart_num'], (string)$cart['truePrice'], 4), (string)$total_price, 4), (string)$priceData['coupon_price'], 2); + $compute_price = bcadd((string)$compute_price, (string)$coupon_price, 2); + } else { + $coupon_price = bcsub((string)$priceData['coupon_price'], $compute_price, 2); + } + $cart['coupon_id'] = $counpon_id; + $cart['coupon_price'] = $coupon_price; + $count--; + } + } + } + break; + } + } + } + return $cartInfo; + } + + /** + * 计算实际佣金 + * @param int $uid + * @param array $cartInfo + * @param $userInfo + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function computeOrderProductBrokerage(int $uid, array $cartInfo, $userInfo) + { + //获取后台一级返佣比例 + $storeBrokerageRatio = sys_config('store_brokerage_ratio'); + //获取二级返佣比例 + $storeBrokerageTwo = sys_config('store_brokerage_two'); + //佣金计算方式 + $brokerageComputeType = sys_config('brokerage_compute_type', 1); + /** @var AgentLevelServices $agentLevelServices */ + $agentLevelServices = app()->make(AgentLevelServices::class); + [$one_brokerage_up, $two_brokerage_up, $spread_uid, $spread_two_uid] = $agentLevelServices->getAgentLevelBrokerage($uid, $userInfo); + // 二级分销开关 + if (sys_config('brokerage_level', 2) == 1) { + $storeBrokerageTwo = $spread_two_uid = 0; + } + foreach ($cartInfo as &$cart) { + $oneBrokerage = '0';//一级返佣金额 + $twoBrokerage = '0';//二级返佣金额 + $cartNum = (string)$cart['cart_num'] ?? '0'; + if (isset($cart['productInfo']) && isset($cart['is_gift']) && $cart['is_gift'] == 0) { + $productInfo = $cart['productInfo']; + //指定返佣金额 + if (isset($productInfo['is_sub']) && $productInfo['is_sub'] == 1) { + $oneBrokerage = bcmul((string)($productInfo['attrInfo']['brokerage'] ?? '0'), $cartNum, 2); + $twoBrokerage = bcmul((string)($productInfo['attrInfo']['brokerage_two'] ?? '0'), $cartNum, 2); + } else {//比例返佣 + $price = 0; + switch ($brokerageComputeType) { + case 1://售价 + if (isset($productInfo['attrInfo'])) { + $price = bcmul((string)($productInfo['attrInfo']['price'] ?? '0'), $cartNum, 4); + } else { + $price = bcmul((string)($productInfo['price'] ?? '0'), $cartNum, 4); + } + break; + case 2://实付金额 + $price = bcmul((string)($cart['truePrice'] ?? 0), $cartNum, 4); + break; + case 3://商品利润 + $price = bcmul(bcsub((string)($cart['truePrice'] ?? 0), (string)($cart['costPrice'] ?? 0), 2), $cartNum, 4); + break; + } + if ($price > 0) { + //一级返佣比例 小于等于零时直接返回 不返佣 + if ($storeBrokerageRatio > 0) { + //计算获取一级返佣比例 + $brokerageRatio = bcdiv($storeBrokerageRatio, 100, 4); + $oneBrokerage = bcmul((string)$price, (string)$brokerageRatio, 2); + } + //二级返佣比例小于等于0 直接返回 + if ($storeBrokerageTwo > 0) { + //计算获取二级返佣比例 + $brokerageTwo = bcdiv($storeBrokerageTwo, 100, 4); + $twoBrokerage = bcmul((string)$price, (string)$brokerageTwo, 2); + } + } + } + } + //分销等级上浮佣金 + if ($one_brokerage_up) $oneBrokerage = bcadd((string)$oneBrokerage, (string)bcmul((string)$oneBrokerage, (string)bcdiv((string)$one_brokerage_up, '100', 2), 4), 2); + if ($two_brokerage_up) $twoBrokerage = bcadd((string)$twoBrokerage, (string)bcmul((string)$twoBrokerage, (string)bcdiv((string)$two_brokerage_up, '100', 2), 4), 2); + + $cart['one_brokerage'] = $oneBrokerage; + $cart['two_brokerage'] = $twoBrokerage; + } + return [$cartInfo, [$spread_uid, $spread_two_uid]]; + } + + /** + * 计算订单商品信任首单优惠实际抵扣金额 + * @param array $cartInfo + * @param array $priceData + * @return array + */ + public function computeOrderProductFirstDiscount(array $cartInfo, array $priceData) + { + $first_order_price = $priceData['first_order_price'] ?? 0; + if ($first_order_price) { + $count = 0; + $total_price = 0.00; + $compute_price = 0.00; + $discount_price = 0.00; + foreach ($cartInfo as $cart) { + if (isset($cart['is_gift']) && $cart['is_gift'] == 1) { + continue; + } + $total_price = bcadd((string)$total_price, (string)bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 4), 2); + $count++; + } + if ($total_price == $first_order_price) { + $ratio = 1; + } else { + $ratio = bcdiv((string)$first_order_price, (string)$total_price, 4); + } + foreach ($cartInfo as &$cart) { + if (isset($cart['is_gift']) && $cart['is_gift'] == 1) { + continue; + } + if ($count > 1) { + $discount_price = bcmul((string)bcmul((string)$cart['cart_num'], (string)$cart['truePrice'], 4), (string)$ratio, 2); + $compute_price = bcadd((string)$compute_price, (string)$discount_price, 2); + } else { + $discount_price = bcsub((string)$first_order_price, $compute_price, 2); + } + $count--; + $cart['first_order_price'] = $discount_price; + } + } + return $cartInfo; + } +} diff --git a/app/services/order/StoreOrderDeliveryServices.php b/app/services/order/StoreOrderDeliveryServices.php new file mode 100644 index 0000000..8c4f96d --- /dev/null +++ b/app/services/order/StoreOrderDeliveryServices.php @@ -0,0 +1,600 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order; + + +use app\services\BaseServices; +use app\dao\order\StoreOrderDao; +use app\services\store\finance\StoreFinanceFlowServices; +use app\services\serve\ServeServices; +use think\exception\ValidateException; +use crmeb\services\FormBuilder as Form; +use app\services\other\ExpressServices; +use crmeb\traits\OptionTrait; + +/** + * 订单发货 + * Class StoreOrderDeliveryServices + * @package app\services\order + * @mixin StoreOrderDao + */ +class StoreOrderDeliveryServices extends BaseServices +{ + use OptionTrait; + /** + * 构造方法 + * StoreOrderDeliveryServices constructor. + * @param StoreOrderDao $dao + */ + public function __construct(StoreOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * 订单整体发货 + * @param int $id + * @param array $data + * @param int $staff_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function delivery(int $id, array $data, int $staff_id = 0) + { + $orderInfo = $this->dao->get($id, ['*'], ['pink']); + if (!$orderInfo) { + throw new ValidateException('订单未能查到,不能发货!'); + } + if ($orderInfo['paid'] != 1) { + throw new ValidateException('订单未支付'); + } + if ($orderInfo->is_del) { + throw new ValidateException('订单已删除,不能发货!'); + } + if ($orderInfo->status == 1) { + throw new ValidateException('订单已发货请勿重复操作!'); + } + if ($orderInfo->shipping_type == 2) { + throw new ValidateException('核销订单不能发货!'); + } + if (isset($orderInfo['pinkStatus']) && $orderInfo['pinkStatus'] != 2) { + throw new ValidateException('拼团未完成暂不能发货!'); + } + $store_id = $this->getItem('store_id', 0); + $supplier_id = $this->getItem('supplier_id', 0); + + //拆分完整主订单查询未发货子订单 + if ($orderInfo['pid'] == -1) { + $orderInfo = $this->dao->get(['pid' => $id, 'status' => 0, 'supplier_id' => $supplier_id, 'store_id' => $store_id, 'refund_type' => [0, 3]], ['*'], ['pink']); + if (!$orderInfo) { + throw new ValidateException('订单未能查到,请在详情发货列表确认后发货'); + } + $id = (int)$orderInfo['id']; + } + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + if ($storeOrderRefundServices->count(['store_order_id' => $id, 'refund_type' => [1, 2, 4, 5, 6], 'is_cancel' => 0, 'is_del' => 0])) { + throw new ValidateException('订单有售后申请请先处理'); + } + //预售订单 验证预售活动是否结束 + if ($orderInfo['type'] == 6) { + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfo = $storeOrderCartInfoServices->getOrderCartInfo($id); + $time = time(); + foreach ($cartInfo as $cart) { + if (isset($cart['cart_info']['productInfo']['presale_end_time']) && $cart['cart_info']['productInfo']['presale_end_time'] > $time) { + throw new ValidateException('预售活动暂未结束,请稍后发货'); + } + } + } + + $dump = $this->doDelivery($id, $orderInfo, $data); + if ($staff_id) { + $this->dao->update($id, ['staff_id' => $staff_id]); + //流水关联店员 + /** @var StoreFinanceFlowServices $storeFinanceFlow */ + $storeFinanceFlow = app()->make(StoreFinanceFlowServices::class); + $storeFinanceFlow->setStaff($orderInfo['order_id'], $staff_id); + } + return compact('orderInfo', 'dump'); + } + + /** + * 订单拆单发货 + * @param int $id + * @param array $data + * @param int $staff_id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function splitDelivery(int $id, array $data, int $staff_id = 0) + { + $orderInfo = $this->dao->get($id, ['*'], ['pink']); + if (!$orderInfo) { + throw new ValidateException('订单未能查到,不能发货!'); + } + if ($orderInfo['paid'] != 1) { + throw new ValidateException('订单未支付'); + } + if ($orderInfo->is_del) { + throw new ValidateException('订单已删除,不能发货!'); + } + if ($orderInfo->shipping_type == 2) { + throw new ValidateException('核销订单不能发货!'); + } + if (isset($orderInfo['pinkStatus']) && $orderInfo['pinkStatus'] != 2) { + throw new ValidateException('拼团未完成暂不能发货!'); + } + $store_id = $this->getItem('store_id', 0); + $supplier_id = $this->getItem('supplier_id', 0); + + //拆分完整主订单查询未发货子订单 + if ($orderInfo['pid'] == -1) { + $orderInfo = $this->dao->get(['pid' => $id, 'status' => 0, 'supplier_id' => $supplier_id, 'store_id' => $store_id, 'refund_type' => [0, 3]], ['*'], ['pink']); + if (!$orderInfo) { + throw new ValidateException('订单未能查到,不能发货!'); + } + $id = (int)$orderInfo['id']; + } + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + if ($storeOrderRefundServices->count(['store_order_id' => $id, 'refund_type' => [1, 2, 4, 5, 6], 'is_cancel' => 0, 'is_del' => 0])) { + throw new ValidateException('订单有售后申请请先处理'); + } + //预售订单 验证预售活动是否结束 + if ($orderInfo['type'] == 6) { + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfo = $storeOrderCartInfoServices->getOrderCartInfo($id); + $time = time(); + foreach ($cartInfo as $cart) { + if (isset($cart['cart_info']['productInfo']['presale_end_time']) && $cart['cart_info']['productInfo']['presale_end_time'] > $time) { + throw new ValidateException('预售活动暂未结束,请稍后发货'); + } + } + } + + $cart_ids = $data['cart_ids']; + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + //检测选择商品是否还可拆分 + $storeOrderCartInfoServices->checkCartIdsIsSplit($id, $cart_ids); + unset($data['cart_ids']); + return $this->transaction(function () use ($id, $cart_ids, $orderInfo, $data, $storeOrderCartInfoServices, $staff_id) { + /** @var StoreOrderSplitServices $storeOrderSplitServices */ + $storeOrderSplitServices = app()->make(StoreOrderSplitServices::class); + //拆单 + $splitResult = $storeOrderSplitServices->equalSplit($id, $cart_ids, $orderInfo, 0, false, $data['erp_id'] ?? 0); + + if ($splitResult) {//拆分发货 + [$orderInfo, $otherOrder] = $splitResult; + + //拆分订单执行发货 + $this->doDelivery((int)$orderInfo->id, $orderInfo, $data); + //检测原订单商品是否 全部拆分发货完成 改原订单状态 + $status_data = ['oid' => $id, 'change_time' => time()]; + if (!$storeOrderCartInfoServices->getSplitCartList($id)) {//发货完成 + $status_data['change_type'] = 'delivery_split'; + $status_data['change_message'] = '已拆分发货'; + } else { + $status_data['change_type'] = 'delivery_part_split'; + $status_data['change_message'] = '已拆分部分发货'; + } + /** @var StoreOrderStatusServices $services */ + $services = app()->make(StoreOrderStatusServices::class); + + //记录原订单状态 + $services->save($status_data); + if ($staff_id) { + $this->dao->update($orderInfo['id'], ['staff_id' => $staff_id]); + //流水关联店员 + /** @var StoreFinanceFlowServices $storeFinanceFlow */ + $storeFinanceFlow = app()->make(StoreFinanceFlowServices::class); + $storeFinanceFlow->setStaff($orderInfo['order_id'], $staff_id); + } + } else {//整体发货 + $this->delivery($id, $data, $staff_id); + } + return $splitResult; + }); +// return true; + } + + /** + * 具体执行发货 + * @param int $id + * @param $orderInfo + * @param array $data + * @return bool + */ + public function doDelivery(int $id, $orderInfo, array $data) + { + $type = (int)$data['type']; + unset($data['type']); + //获取购物车内的商品标题 + /** @var StoreOrderCartInfoServices $orderInfoServices */ + $orderInfoServices = app()->make(StoreOrderCartInfoServices::class); + $storeName = $orderInfoServices->getCarIdByProductTitle((int)$orderInfo['id']); + //修改返回数据 + $res = []; + switch ($type) { + case 1://快递发货 + $res = $this->orderDeliverGoods($id, $data, $orderInfo, $storeName); + break; + case 2://配送 + $this->orderDelivery($id, $data, $orderInfo); + break; + case 3://虚拟发货 + $this->orderVirtualDelivery($id, $data); + break; + case 4://门店收银订单自动发货 + $this->dao->update($orderInfo['id'], ['delivery_type' => 'cashier']); + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $statusService->save([ + 'oid' => $orderInfo['id'], + 'change_type' => 'delivery_cashier', + 'change_message' => '门店收银台自提', + 'change_time' => time() + ]); + break; + default: + throw new ValidateException('暂时不支持其他发货类型'); + } + //跟新完成后重新赋值订单信息 + $orderInfo = $this->dao->get($id, ['*'], ['pink']); + event('order.delivery', [$orderInfo, $storeName, $data, $type]); + return $res; + } + + + /** + * 订单快递发货 + * @param int $id + * @param array $data + */ + public function orderDeliverGoods(int $id, array $data, $orderInfo, $storeTitle) + { + if (!$data['delivery_name']) { + throw new ValidateException('请选择快递公司'); + } + //修改 + $dump = []; + $data['delivery_type'] = 'express'; + if ($data['express_record_type'] == 2) {//电子面单 + if (!sys_config('config_export_open', 0)) { + throw new ValidateException('系统通知:电子面单已关闭,请选择其他发货方式!'); + } + if (!$data['delivery_code']) { + throw new ValidateException('快递公司编缺失'); + } + if (!$data['express_temp_id']) { + throw new ValidateException('请选择电子面单模板'); + } + if (!$data['to_name']) { + throw new ValidateException('请填写寄件人姓名'); + } + if (!$data['to_tel']) { + throw new ValidateException('请填写寄件人电话'); + } + if (!$data['to_addr']) { + throw new ValidateException('请填写寄件人地址'); + } + $expData['com'] = $data['delivery_code']; + $expData['to_name'] = $orderInfo['real_name']; + $expData['to_tel'] = $orderInfo['user_phone']; + $expData['to_addr'] = $orderInfo['user_address']; + $expData['from_name'] = $data['to_name']; + $expData['from_tel'] = $data['to_tel']; + $expData['from_addr'] = $data['to_addr']; + $expData['siid'] = sys_config('config_export_siid'); + $expData['temp_id'] = $data['express_temp_id']; + $expData['count'] = $orderInfo['total_num']; + $expData['weight'] = $this->getOrderSumWeight($id); + /** @var StoreOrderCartInfoServices $orderInfoServices */ + $orderInfoServices = app()->make(StoreOrderCartInfoServices::class); + $expData['cargo'] = $orderInfoServices->getCarIdByProductTitle((int)$orderInfo['id'], true); + $expData['order_id'] = $orderInfo['order_id']; + /** @var ServeServices $expressService */ + $expressService = app()->make(ServeServices::class); + $dump = $expressService->express()->dump($expData); + $data['express_dump'] = json_encode([ + 'com' => $expData['com'], + 'from_name' => $expData['from_name'], + 'from_tel' => $expData['from_tel'], + 'from_addr' => $expData['from_addr'], + 'temp_id' => $expData['temp_id'], + 'cargo' => $expData['cargo'], + ]); + $data['delivery_id'] = $dump['kuaidinum']; + //修改快递面单图片写入数据库 + if (!empty($dump['label'])) { + $data['kuaidi_label'] = $dump['label']; + } + } else { + if (!$data['delivery_id']) { + throw new ValidateException('请输入快递单号'); + } + } + $data['status'] = 1; + unset($data['delivery_remark']); + /** @var StoreOrderStatusServices $services */ + $services = app()->make(StoreOrderStatusServices::class); + $this->transaction(function () use ($id, $data, $services) { + $res = $this->dao->update($id, $data); + $res = $res && $services->save([ + 'oid' => $id, + 'change_time' => time(), + 'change_type' => 'delivery_goods', + 'change_message' => '已发货 快递公司:' . $data['delivery_name'] . ' 快递单号:' . $data['delivery_id'] + ]); + if (!$res) { + throw new ValidateException('发货失败:数据保存不成功'); + } + }); + return $dump; + } + + + /** + * 订单配送 + * @param int $id + * @param array $data + * @return bool + */ + public function orderDelivery(int $id, array $data, $orderInfo = []) + { + $data['delivery_name'] = $data['sh_delivery_name']; + $data['delivery_id'] = $data['sh_delivery_id']; + $data['delivery_uid'] = $data['sh_delivery_uid']; + $delivery_type = $data['delivery_type'] ?? 1; + $delivery_type = $delivery_type ? $delivery_type : 1; + $station_type = $data['station_type'] ?? 1; + unset($data['delivery_type'], $data['station_type']); + switch ($delivery_type) { + case 1://自己配送 + if (!$data['delivery_name']) { + throw new ValidateException('请输入送货人姓名'); + } + if (!$data['delivery_id']) { + throw new ValidateException('请输入送货人电话号码'); + } + if (!$data['delivery_uid']) { + throw new ValidateException('请输入送货人信息'); + } + if (!check_phone($data['delivery_id'])) { + throw new ValidateException('请输入正确的送货人电话号码'); + } + //获取核销码 + /** @var StoreOrderCreateServices $storeOrderCreateService */ + $storeOrderCreateService = app()->make(StoreOrderCreateServices::class); + $data['verify_code'] = $storeOrderCreateService->getStoreCode(); + $data['delivery_type'] = 'send'; + break; + case 2://第三方配送 + if (!isset($data['cargo_weight']) || !$data['cargo_weight']) { + throw new ValidateException('请填写配送商品重量'); + } + $data['delivery_type'] = 'city_delivery'; + /** @var StoreDeliveryOrderServices $storeDeliverOrderServices */ + $storeDeliverOrderServices = app()->make(StoreDeliveryOrderServices::class); + $storeDeliverOrderServices->create($id, $data, $station_type, $orderInfo); + break; + } + $data['status'] = 1; + + unset($data['sh_delivery_name'], $data['sh_delivery_id'], $data['sh_delivery_uid'], $data['delivery_remark']); + + /** @var StoreOrderStatusServices $services */ + $services = app()->make(StoreOrderStatusServices::class); + $this->transaction(function () use ($id, $data, $services, $delivery_type, $station_type) { + //修改订单发货信息 + $this->dao->update($id, $data); + if ($delivery_type == 1) { + $message = '已配送 发货人:' . $data['delivery_name'] . ' 发货人电话:' . $data['delivery_id']; + } else { + $message = '已'. ($station_type == 1 ? '达达' : 'UU跑腿') .'同城配送'; + } + //记录订单状态 + $services->save([ + 'oid' => $id, + 'change_type' => $delivery_type == 1 ? 'delivery' : 'city_delivery', + 'change_time' => time(), + 'change_message' => $message + ]); + }); + return true; + } + + /** + * 虚拟发货 + * @param int $id + * @param array $data + */ + public function orderVirtualDelivery(int $id, array $data) + { + $data['delivery_type'] = 'fictitious'; + $data['status'] = 1; + unset($data['sh_delivery_name'], $data['sh_delivery_id'], $data['delivery_name'], $data['delivery_id'], $data['delivery_remark']); + //保存信息 + /** @var StoreOrderStatusServices $services */ + $services = app()->make(StoreOrderStatusServices::class); + return $this->transaction(function () use ($id, $data, $services) { + $res1 = $this->dao->update($id, $data); + $res2 = $services->save([ + 'oid' => $id, + 'change_type' => 'delivery_fictitious', + 'change_message' => '已虚拟发货', + 'change_time' => time() + ]); + return $res1 && $res2; + }); + } + + /** + * 获取修改配送信息表单结构 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function distributionForm(int $id) + { + if (!$orderInfo = $this->dao->get($id)) + throw new ValidateException('订单不存在'); + + $f[] = Form::input('order_id', '订单号', $orderInfo->getData('order_id'))->disabled(1); + + switch ($orderInfo['delivery_type']) { + case 'send': + $f[] = Form::input('delivery_name', '送货人姓名', $orderInfo->getData('delivery_name'))->required('请输入送货人姓名'); + $f[] = Form::input('delivery_id', '送货人电话', $orderInfo->getData('delivery_id'))->required('请输入送货人电话'); + break; + case 'express': + /** @var ExpressServices $expressServices */ + $expressServices = app()->make(ExpressServices::class); + $f[] = Form::select('delivery_name', '快递公司', (string)$orderInfo->getData('delivery_name'))->setOptions(array_map(function ($item) { + $item['value'] = $item['label']; + return $item; + }, $expressServices->expressSelectForm(['is_show' => 1])))->filterable(true)->required('请选择快递公司'); + $f[] = Form::input('delivery_id', '快递单号', $orderInfo->getData('delivery_id'))->required('请填写快递单号'); + break; + } + return create_form('配送信息', $f, $this->url('/order/distribution/' . $id), 'PUT'); + } + + /** + * 修改配送信息 + * @param int $id 订单id + * @return mixed + */ + public function updateDistribution(int $id, array $data) + { + $order = $this->dao->get($id); + if (!$order) { + throw new ValidateException('数据不存在!'); + } + switch ($order['delivery_type']) { + case 'send': + if (!$data['delivery_name']) { + throw new ValidateException('请输入送货人姓名'); + } + if (!$data['delivery_id']) { + throw new ValidateException('请输入送货人电话号码'); + } + if (!check_phone($data['delivery_id'])) { + throw new ValidateException('请输入正确的送货人电话号码'); + } + break; + case 'express': + if (!$data['delivery_name']) { + throw new ValidateException('请选择快递公司'); + } + if (!$data['delivery_id']) { + throw new ValidateException('请输入快递单号'); + } + break; + default: + throw new ValidateException('未发货,请先发货再修改配送信息'); + break; + } + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $statusService->save([ + 'oid' => $id, + 'change_type' => 'distribution', + 'change_message' => '修改发货信息为' . $data['delivery_name'] . '号' . $data['delivery_id'], + 'change_time' => time() + ]); + return $this->dao->update($id, $data); + } + + /** + * 订单发货后打印电子面单 + * @param $orderId + * @return bool|mixed + */ + public function orderDump($orderId) + { + if (!$orderId) throw new ValidateException('订单号缺失'); + /** @var StoreOrderServices $orderService */ + $orderService = app()->make(StoreOrderServices::class); + $orderInfo = $orderService->getOne(['id' => $orderId]); + if (!$orderInfo) throw new ValidateException('订单不存在'); + if (in_array($orderInfo->shipping_type, [2, 4])) throw new ValidateException('订单无法打印'); + if (!$orderInfo->express_dump) throw new ValidateException('请先发货'); + if (!sys_config('config_export_open', 0)) { + throw new ValidateException('请先在系统设置中打开单子面单打印开关'); + } + $dumpInfo = json_decode($orderInfo->express_dump, true); + /** @var ServeServices $expressService */ + $expressService = app()->make(ServeServices::class); + $expData['com'] = $dumpInfo['com']; + $expData['to_name'] = $orderInfo->real_name; + $expData['to_tel'] = $orderInfo->user_phone; + $expData['to_addr'] = $orderInfo->user_address; + $expData['from_name'] = $dumpInfo['from_name']; + $expData['from_tel'] = $dumpInfo['from_tel']; + $expData['from_addr'] = $dumpInfo['from_addr']; + $expData['siid'] = sys_config('config_export_siid'); + $expData['temp_id'] = $dumpInfo['temp_id']; + $expData['cargo'] = $dumpInfo['cargo']; + $expData['count'] = $orderInfo->total_num; + $expData['weight'] = $this->getOrderSumWeight((int)$orderId); + $expData['order_id'] = $orderInfo->order_id; + try { + $dump = $expressService->express()->dump($expData); + $data['express_dump'] = json_encode([ + 'com' => $expData['com'], + 'from_name' => $expData['from_name'], + 'from_tel' => $expData['from_tel'], + 'from_addr' => $expData['from_addr'], + 'temp_id' => $expData['temp_id'], + 'cargo' => $expData['cargo'], + ]); + $data['delivery_id'] = $dump['kuaidinum'] ?? ''; + //修改快递面单图片写入数据库 + if (!empty($dump['label'])) { + $data['kuaidi_label'] = $dump['label']; + } + $orderService->update($orderId, $data); + } catch (\Throwable $e) { + $dump = []; + } + return $dump; + } + + /** + * 返回订单商品总重量 + * @param int $id + * @return int|string + */ + public function getOrderSumWeight(int $id, $default = false) + { + /** @var StoreOrderCartInfoServices $services */ + $services = app()->make(StoreOrderCartInfoServices::class); + $orderGoodInfo = $services->getOrderCartInfo((int)$id); + $weight = 0; + foreach ($orderGoodInfo as $cartInfo) { + $cart = $cartInfo['cart_info'] ?? []; + if ($cart) { + $weight = bcadd((string)$weight, (string)bcmul((string)$cart['cart_num'] ?? '0', (string)$cart['productInfo']['attrInfo']['weight'] ?? '0', 4), 2); + } + } + return $weight ? $weight : ($default === false ? 0 : $default); + } + +} diff --git a/app/services/order/StoreOrderRefundServices.php b/app/services/order/StoreOrderRefundServices.php new file mode 100644 index 0000000..27226f3 --- /dev/null +++ b/app/services/order/StoreOrderRefundServices.php @@ -0,0 +1,1341 @@ + +// +---------------------------------------------------------------------- +namespace app\services\order; + +use app\dao\order\StoreOrderRefundDao; +use app\services\activity\discounts\StoreDiscountsServices; +use app\services\activity\bargain\StoreBargainServices; +use app\services\activity\combination\StoreCombinationServices; +use app\services\activity\combination\StorePinkServices; +use app\services\activity\newcomer\StoreNewcomerServices; +use app\services\activity\seckill\StoreSeckillServices; +use app\services\BaseServices; +use app\services\activity\coupon\StoreCouponUserServices; +use app\services\other\ExpressServices; +use app\services\pay\PayServices; +use app\services\product\product\StoreProductServices; +use app\services\store\SystemStoreServices; +use app\services\supplier\SystemSupplierServices; +use app\services\user\UserBillServices; +use app\services\user\UserBrokerageServices; +use app\services\user\UserMoneyServices; +use app\services\user\UserServices; +use app\services\wechat\WechatUserServices; +use crmeb\services\AliPayService; +use crmeb\services\CacheService; +use crmeb\services\FormBuilder as Form; +use crmeb\services\wechat\Payment; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; + +/** + * 订单退款 + * Class StoreOrderRefundServices + * @package app\services\order + * @mixin StoreOrderRefundDao + */ +class StoreOrderRefundServices extends BaseServices +{ + use ServicesTrait; + + /** + * 订单services + * @var StoreOrderServices + */ + protected $storeOrderServices; + + /** + * 构造方法 + * StoreOrderRefundServices constructor. + * @param StoreOrderRefundDao $dao + * @param StoreOrderServices $storeOrderServices + */ + public function __construct(StoreOrderRefundDao $dao, StoreOrderServices $storeOrderServices) + { + $this->dao = $dao; + $this->storeOrderServices = $storeOrderServices; + } + + /** + * 退款订单列表 + * @param array $where + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function refundList(array $where, array $with = ['user']) + { + $where['is_cancel'] = 0; + $where['store_id'] = isset($where['store_id']) ? $where['store_id'] : 0; + if (isset($where['time']) && $where['time'] != '') { + $where['time'] = is_string($where['time']) ? explode('-', $where['time']) : $where['time']; + } + [$page, $limit] = $this->getPageValue(); + $with = array_merge($with, ['order' => function ($query) { + $query->field('id,shipping_type')->bind(['shipping_type']); + }]); + $list = $this->dao->getRefundList($where, '*', $with, $page, $limit); + $count = $this->dao->count($where); + if ($list) { + foreach ($list as &$item) { + $item['refund'] = []; + $item['is_all_refund'] = 1; + $item['paid'] = 1; + $item['add_time'] = isset($item['add_time']) ? date('Y-m-d H:i', (int)$item['add_time']) : ''; + $item['cartInfo'] = $item['cart_info']; + if (in_array($item['refund_type'], [1, 2, 4, 5])) { + $item['refund_status'] = 1; + } elseif ($item['refund_type'] == 6) { + $item['refund_status'] = 2; + } elseif ($item['refund_type'] == 3) { + $item['refund_status'] = 3; + } + foreach ($item['cart_info'] as $items) { + $item['_info'][]['cart_info'] = $items; + } + $item['total_num'] = $item['refund_num']; + $item['pay_price'] = $item['refund_price']; + $item['pay_postage'] = 0; + if (isset($item['shipping_type']) && !in_array($item['shipping_type'], [2, 4])) { + $item['pay_postage'] = floatval($this->getOrderSumPrice($item['cart_info'], 'postage_price', false)); + } + $item['status_name'] = [ + 'pic' => [], + 'status_name' => '' + ]; + unset($item['cart_info']); + if (in_array($item['refund_type'], [1, 2, 4, 5])) { + $_type = -1; + $_title = '申请退款中'; + if ($item['refund_type'] == 1) { + $item['status_name']['status_name'] = '仅退款'; + } elseif ($item['refund_type'] == 2) { + $item['status_name']['status_name'] = '退货退款'; + } elseif ($item['refund_type'] == 4) { + $item['status_name']['status_name'] = '等待用户退货'; + } elseif ($item['refund_type'] == 5) { + $item['status_name']['status_name'] = '商家待收货'; + } + } elseif ($item['refund_type'] == 3) { + $_type = -3; + $_title = '拒绝退款'; + $item['status_name']['status_name'] = '拒绝退款'; + } else { + $_type = -2; + $_title = '已退款'; + $item['status_name']['status_name'] = '已退款'; + } + $item['_status'] = [ + '_type' => $_type, + '_title' => $_title, + ]; + } + } + $data['list'] = $list; + $data['count'] = $count; + + $supplierId = $where['supplier_id'] ?? 0; + if ($supplierId) { + $del_where = ['supplier_id' => $supplierId, 'is_cancel' => 0]; + } else { + $del_where = ['store_id' => $where['store_id'], 'is_cancel' => 0]; + } + $data['num'] = [ + 0 => ['name' => '全部', 'num' => $this->dao->count($del_where)], + 1 => ['name' => '仅退款', 'num' => $this->dao->count($del_where + ['refund_type' => 1])], + 2 => ['name' => '退货退款', 'num' => $this->dao->count($del_where + ['refund_type' => 2])], + 3 => ['name' => '拒绝退款', 'num' => $this->dao->count($del_where + ['refund_type' => 3])], + 4 => ['name' => '商品待退货', 'num' => $this->dao->count($del_where + ['refund_type' => 4])], + 5 => ['name' => '退货待收货', 'num' => $this->dao->count($del_where + ['refund_type' => 5])], + 6 => ['name' => '已退款', 'num' => $this->dao->count($del_where + ['refund_type' => 6])] + ]; + return $data; + } + + /** + * 前端订单列表 + * @param array $where + * @param array|string[] $field + * @param array $with + * @return mixed + */ + public function getRefundOrderList(array $where, string $field = '*', array $with = []) + { + [$page, $limit] = $this->getPageValue(); + $where['is_cancel'] = 0; + $where['is_del'] = 0; + $data = $this->dao->getRefundList($where, $field, $with, $page, $limit); + foreach ($data as &$item) { + $item['add_time'] = isset($item['add_time']) ? date('Y-m-d H:i', (int)$item['add_time']) : ''; + $item['cartInfo'] = $item['cart_info']; + unset($item['cart_info']); + } + return $data; + } + + /** + * 订单申请退款 + * @param int $id + * @param int $uid + * @param array $order + * @param array $cart_ids + * @param int $refundType + * @param float $refundPrice + * @param array $refundData + * @return mixed + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function applyRefund(int $id, int $uid, $order = [], array $cart_ids = [], int $refundType = 0, float $refundPrice = 0.00, array $refundData = [], int $origin = 0, bool $isSync = true) + { + if (!$order) { + $order = $this->storeOrderServices->get($id); + } + if (!$order) { + throw new ValidateException('支付订单不存在!'); + } + if (!sys_config('erp_open')) { + $is_now = $this->dao->getCount([ + ['store_order_id', '=', $id], + ['refund_type', 'in', [1, 2, 4, 5]], + ['is_cancel', '=', 0], + ['is_del', '=', 0] + ]); + if ($is_now) throw new ValidateException('退款处理中,请联系商家'); + } + + $refund_num = $order['total_num']; + $refund_price = $order['pay_price']; + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + //退部分 + $cartInfo = []; + $cartInfos = $storeOrderCartInfoServices->getCartColunm(['oid' => $id], 'id,cart_id,product_type,is_support_refund,cart_num,refund_num,cart_info'); + if ($cart_ids) { + $cartInfo = array_combine(array_column($cartInfos, 'cart_id'), $cartInfos); + $refund_num = 0; + foreach ($cart_ids as $cart) { + if (!isset($cartInfo[$cart['cart_id']])) throw new ValidateException('该订单中商品不存在,请重新选择!'); + if (!$cartInfo[$cart['cart_id']]['is_support_refund'] && $origin == 0) { + throw new ValidateException('该订单中有商品不支持退款,请联系管理员'); + } + if ($cart['cart_num'] + $cartInfo[$cart['cart_id']]['refund_num'] > $cartInfo[$cart['cart_id']]['cart_num']) { + throw new ValidateException('超出订单中商品数量,请重新选择!'); + } + $refund_num = bcadd((string)$refund_num, (string)$cart['cart_num'], 0); + } + //总共申请多少件 + $total_num = array_sum(array_column($cart_ids, 'cart_num')); + if ($total_num < $order['total_num']) { + /** @var StoreOrderSplitServices $storeOrderSpliteServices */ + $storeOrderSpliteServices = app()->make(StoreOrderSplitServices::class); + $cartInfos = $storeOrderSpliteServices->getSplitOrderCartInfo($id, $cart_ids, $order); + $total_price = $pay_postage = 0; + foreach ($cartInfos as $cart) { + $_info = is_string($cart['cart_info']) ? json_decode($cart['cart_info'], true) : $cart['cart_info']; + $total_price = bcadd((string)$total_price, bcmul((string)($_info['truePrice'] ?? 0), (string)$cart['cart_num'], 4), 2); + if (!in_array($order['shipping_type'], [2, 4])) { + $pay_postage = bcadd((string)$pay_postage, (string)($_info['postage_price'] ?? 0), 2); + } + } + //实际退款金额 + $refund_pay_price = bcadd((string)$total_price, (string)$pay_postage, 2); + + if (isset($order['change_price']) && (float)$order['change_price']) {//有改价 且是拆分 + //订单原实际支付金额 + $order_pay_price = bcadd((string)$order['change_price'], (string)$order['pay_price'], 2); + $refund_price = bcmul((string)bcdiv((string)$order['pay_price'], (string)$order_pay_price, 4), (string)$refund_pay_price, 2); + } else { + $refund_price = $refund_pay_price; + } + } + } else {//整单退款 + foreach ($cartInfos as $cart) { + if (!$cart['is_support_refund']) { + throw new ValidateException('该订单中有商品不支持退款,请联系管理员'); + } + if ($cart['refund_num'] > 0) { + throw new ValidateException('超出订单中商品数量,请重新选择!'); + } + } + } + foreach ($cartInfos as &$cart) { + $cart['cart_info'] = is_string($cart['cart_info']) ? json_decode($cart['cart_info'], true) : $cart['cart_info']; + } + $refundData['uid'] = $uid; + $refundData['store_id'] = $order['store_id']; + $refundData['supplier_id'] = $order['supplier_id']; + $refundData['store_order_id'] = $id; + $refundData['refund_num'] = $refund_num; + $refundData['refund_type'] = $refundType; + $refundData['refund_price'] = $refund_price; + $refundData['order_id'] = app()->make(StoreOrderCreateServices::class)->getNewOrderId(''); + $refundData['add_time'] = time(); + $refundData['cart_info'] = json_encode(array_column($cartInfos, 'cart_info')); + $refundId = $this->transaction(function () use ($id, $order, $cart_ids, $refundData, $storeOrderCartInfoServices, $cartInfo, $cartInfos) { + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $res1 = false !== $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'apply_refund', + 'change_message' => '用户申请退款,原因:' . $refundData['refund_reason'], + 'change_time' => time() + ]); + $res2 = true; + //添加退款数据 + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + $res3 = $storeOrderRefundServices->save($refundData); + if (!$res3) { + throw new ValidateException('添加退款申请失败'); + } + $res4 = true; + if ($cart_ids) { + //修改订单商品退款信息 + foreach ($cart_ids as $cart) { + $res4 = $res4 && $storeOrderCartInfoServices->update(['oid' => $id, 'cart_id' => $cart['cart_id']], ['refund_num' => (($cartInfo[$cart['cart_id']]['refund_num'] ?? 0) + $cart['cart_num'])]); + } + } else {//整单退款 + //修改原订单状态 +// $res2 = false !== $this->storeOrderServices->update(['id' => $order['id']], ['refund_status' => 1]); + foreach ($cartInfos as $cart) { + $res4 = $res4 && $storeOrderCartInfoServices->update(['oid' => $id, 'cart_id' => $cart['cart_id']], ['refund_num' => $cart['cart_num']]); + } + } + if ($res1 && $res2 && $res3 && $res4) { + return (int)$res3->id; + } else { + return false; + } + }); + $storeOrderCartInfoServices->clearOrderCartInfo($order['id']); + //申请退款事件 + event('order.applyRefund', [$order, $refundId, $isSync]); + return $refundId; + } + + /**后台操作退款 + * @param int $id + * @param $order + * @param array $cart_ids + * @param int $refundType + * @param float $refundPrice + * @param array $refundData + * @param int $origin + * @param bool $isSync + * @return mixed + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function splitApplyRefund(int $id, $order = [], array $cart_ids = [], int $refundType = 0, float $refundPrice = 0.00, array $refundData = [], int $origin = 0, bool $isSync = true) + { + if (!$order) { + $order = $this->storeOrderServices->get($id); + } + if (!$order) { + throw new ValidateException('支付订单不存在!'); + } + if (!sys_config('erp_open')) { + $is_now = $this->dao->getCount([ + ['store_order_id', '=', $id], + ['refund_type', 'in', [1, 2, 4, 5]], + ['is_cancel', '=', 0], + ['is_del', '=', 0] + ]); + if ($is_now) throw new ValidateException('退款处理中,请联系商家'); + } + + $refund_num = $order['total_num']; + $refund_price = $order['pay_price']; + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + //退部分 + $cartInfo = []; + $cartInfos = $storeOrderCartInfoServices->getCartColunm(['oid' => $id], 'id,cart_id,product_type,is_support_refund,cart_num,refund_num,cart_info'); + if ($cart_ids) { + $cartInfo = array_combine(array_column($cartInfos, 'cart_id'), $cartInfos); + $refund_num = 0; + foreach ($cart_ids as $cart) { + if (!isset($cartInfo[$cart['cart_id']])) throw new ValidateException('该订单中商品不存在,请重新选择!'); + if ($cart['cart_num'] + $cartInfo[$cart['cart_id']]['refund_num'] > $cartInfo[$cart['cart_id']]['cart_num']) { + throw new ValidateException('超出订单中商品数量,请重新选择!'); + } + $refund_num = bcadd((string)$refund_num, (string)$cart['cart_num'], 0); + } + //总共申请多少件 + $total_num = array_sum(array_column($cart_ids, 'cart_num')); + if ($total_num < $order['total_num']) { + /** @var StoreOrderSplitServices $storeOrderSpliteServices */ + $storeOrderSpliteServices = app()->make(StoreOrderSplitServices::class); + $cartInfos = $storeOrderSpliteServices->getSplitOrderCartInfo($id, $cart_ids, $order); + $total_price = $pay_postage = 0; + foreach ($cartInfos as $cart) { + $_info = is_string($cart['cart_info']) ? json_decode($cart['cart_info'], true) : $cart['cart_info']; + $total_price = bcadd((string)$total_price, bcmul((string)($_info['truePrice'] ?? 0), (string)$cart['cart_num'], 4), 2); + if (!in_array($order['shipping_type'], [2, 4])) { + $pay_postage = bcadd((string)$pay_postage, (string)($_info['postage_price'] ?? 0), 2); + } + } + //实际退款金额 + $refund_pay_price = bcadd((string)$total_price, (string)$pay_postage, 2); + + if (isset($order['change_price']) && (float)$order['change_price']) {//有改价 且是拆分 + //订单原实际支付金额 + $order_pay_price = bcadd((string)$order['change_price'], (string)$order['pay_price'], 2); + $refund_price = bcmul((string)bcdiv((string)$order['pay_price'], (string)$order_pay_price, 4), (string)$refund_pay_price, 2); + } else { + $refund_price = $refund_pay_price; + } + } + } else {//整单退款 + foreach ($cartInfos as $cart) { + if ($cart['refund_num'] > 0) { + throw new ValidateException('超出订单中商品数量,请重新选择!'); + } + } + } + foreach ($cartInfos as &$cart) { + $cart['cart_info'] = is_string($cart['cart_info']) ? json_decode($cart['cart_info'], true) : $cart['cart_info']; + } + $refundData['uid'] = $order['uid']; + $refundData['store_id'] = $order['store_id']; + $refundData['supplier_id'] = $order['supplier_id']; + $refundData['store_order_id'] = $id; + $refundData['refund_num'] = $refund_num; + $refundData['refund_type'] = $refundType; + $refundData['refund_price'] = $refund_price; + $refundData['order_id'] = app()->make(StoreOrderCreateServices::class)->getNewOrderId(''); + $refundData['add_time'] = time(); + $refundData['cart_info'] = json_encode(array_column($cartInfos, 'cart_info')); + $refundId = $this->transaction(function () use ($id, $order, $cart_ids, $refundData, $storeOrderCartInfoServices, $cartInfo, $cartInfos) { + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $res1 = false !== $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'apply_refund', + 'change_message' => '管理员操作退款', + 'change_time' => time() + ]); + $res2 = true; + //添加退款数据 + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + $res3 = $storeOrderRefundServices->save($refundData); + if (!$res3) { + throw new ValidateException('添加退款申请失败'); + } + $res4 = true; + if ($cart_ids) { + //修改订单商品退款信息 + foreach ($cart_ids as $cart) { + $res4 = $res4 && $storeOrderCartInfoServices->update(['oid' => $id, 'cart_id' => $cart['cart_id']], ['refund_num' => (($cartInfo[$cart['cart_id']]['refund_num'] ?? 0) + $cart['cart_num'])]); + } + } else {//整单退款 + //修改原订单状态 + foreach ($cartInfos as $cart) { + $res4 = $res4 && $storeOrderCartInfoServices->update(['oid' => $id, 'cart_id' => $cart['cart_id']], ['refund_num' => $cart['cart_num']]); + } + } + if ($res1 && $res2 && $res3 && $res4) { + return (int)$res3->id; + } else { + return false; + } + }); + $storeOrderCartInfoServices->clearOrderCartInfo($order['id']); + + return $refundId; + } + + /** + * 拒绝退款 + * @param int $id + * @param array $data + * @param array $orderRefundInfo + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function refuseRefund(int $id, array $data, $orderRefundInfo = []) + { + if (!$orderRefundInfo) { + $orderRefundInfo = $this->dao->get(['id' => $id, 'is_cancel' => 0]); + } + if (!$orderRefundInfo) { + throw new ValidateException('售后订单不存在'); + } + $this->transaction(function () use ($id, $orderRefundInfo, $data) { + //处理售后订单 + if (isset($data['refund_price'])) unset($data['refund_price']); + $this->dao->update($id, $data); + //处理订单 + $oid = (int)$orderRefundInfo['store_order_id']; + $this->storeOrderServices->update($oid, ['refund_status' => 0, 'refund_type' => 3]); + //处理订单商品cart_info + $this->cancelOrderRefundCartInfo($id, $oid, $orderRefundInfo); + //记录 + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $statusService->save([ + 'oid' => $id, + 'change_type' => 'refund_n', + 'change_message' => '不退款原因:' . ($data['refund_reason'] ?? $data['refuse_reason'] ?? ''), + 'change_time' => time() + ]); + }); + $orderInfo = $this->storeOrderServices->get((int)$orderRefundInfo['store_order_id']); + //订单拒绝退款事件 + event('order.refuseRefund', [$orderInfo]); + return true; + } + + /** + * 取消申请、后台拒绝处理cart_info refund_num数据 + * @param int $id + * @param int $oid + * @param array $orderRefundInfo + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function cancelOrderRefundCartInfo(int $id, int $oid, $orderRefundInfo = []) + { + if (!$orderRefundInfo) { + $orderRefundInfo = $this->dao->get(['id' => $id, 'is_cancel' => 0]); + } + if (!$orderRefundInfo) { + throw new ValidateException('售后订单不存在'); + } + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $orderInfo = $storeOrderServices->get($oid, ['id', 'type', 'pink_id', 'activity_id']); + if (!$orderInfo) { + throw new ValidateException('订单不存在'); + } + //拼团订单处理拼团 + if ($orderInfo['type'] == 3 && $orderInfo['pink_id']) { + /** @var StorePinkServices $pinkServices */ + $pinkServices = app()->make(StorePinkServices::class); + $addTime = $pinkServices->value(['id' => $orderInfo['pink_id']], 'add_time'); + /** @var StoreCombinationServices $services */ + $services = app()->make(StoreCombinationServices::class); + $product = $services->getOne(['id' => $orderInfo['activity_id']], 'effective_time,title,people'); + $pinkServices->update(['id' => $orderInfo['pink_id']], ['status' => 1, 'stop_time' => $addTime + $product['effective_time'] * 3600]); + } + $cart_ids = array_column($orderRefundInfo['cart_info'], 'id'); + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfos = $storeOrderCartInfoServices->getColumn([['oid', '=', $oid], ['cart_id', 'in', $cart_ids]], 'cart_id,refund_num', 'cart_id'); + foreach ($orderRefundInfo['cart_info'] as $cart) { + $cart_refund_num = $cartInfos[$cart['id']]['refund_num'] ?? 0; + if ($cart['cart_num'] >= $cart_refund_num) { + $refund_num = 0; + } else { + $refund_num = bcsub((string)$cart_refund_num, (string)$cart['cart_num'], 0); + } + $storeOrderCartInfoServices->update(['oid' => $oid, 'cart_id' => $cart['id']], ['refund_num' => $refund_num]); + } + $storeOrderCartInfoServices->clearOrderCartInfo($oid); + return true; + } + + /** + * 商家同意退货退款,等待客户退货 + * @param int $id + * @return bool + */ + public function agreeRefundProdcut(int $id) + { + $res = $this->dao->update(['id' => $id], ['refund_type' => 4]); + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $statusService->save([ + 'oid' => $id, + 'change_type' => 'refund_express', + 'change_message' => '等待用户退货', + 'change_time' => time() + ]); + if ($res) return true; + throw new ValidateException('操作失败'); + } + + /** + * 同意退款:拆分退款单、退积分、佣金等 + * @param int $id + * @param array $refundData + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function agreeRefund(int $id, array $refundData) + { + $order = $this->transaction(function () use ($id, $refundData) { + //退款拆分 + $order = $this->agreeSplitRefundOrder($id); + //回退积分和优惠卷 + if (!$this->integralAndCouponBack($order)) { + throw new ValidateException('回退积分和优惠卷失败'); + } + //退拼团 + if ($order['pid'] == 0 && $order['type'] == 3) { + /** @var StorePinkServices $pinkServices */ + $pinkServices = app()->make(StorePinkServices::class); + if (!$pinkServices->setRefundPink($order)) { + throw new ValidateException('拼团修改失败!'); + } + } + //退佣金 + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + if (!$userBrokerageServices->orderRefundBrokerageBack($order)) { + throw new ValidateException('回退佣金失败'); + } + //回退库存 + if ($order['status'] == 0) { + /** @var StoreOrderStatusServices $services */ + $services = app()->make(StoreOrderStatusServices::class); + if (!$services->count(['oid' => $order['id'], 'change_type' => 'refund_price'])) { + $this->regressionStock($order); + } + } + //退金额 + if ($refundData['refund_price'] > 0) { + if (!isset($refundData['refund_id']) || !$refundData['refund_id']) { + mt_srand(); + $refundData['refund_id'] = $order['order_id'] . rand(100, 999); + } + if ($order['pid'] > 0) {//子订单 + $refundOrder = $this->storeOrderServices->get((int)$order['pid']); + $refundData['pay_price'] = $refundOrder['pay_price']; + } else { + $refundOrder = $order; + } + switch ($refundOrder['pay_type']) { + case PayServices::WEIXIN_PAY: + $no = $refundOrder['order_id']; + if ($refundOrder['trade_no'] && $refundOrder['trade_no'] != $refundOrder['order_id']) { + $no = $refundOrder['trade_no']; + $refundData['type'] = 'trade_no'; + } + if ($refundOrder['is_channel'] == 1) { + //小程序退款 + //判断是不是小程序支付 TODO 之后可根据订单判断 + $pay_routine_open = (bool)sys_config('pay_routine_open', 0); + if ($pay_routine_open) { + $refundData['refund_no'] = $refundOrder['order_id']; // 退款订单号 + /** @var WechatUserServices $wechatUserServices */ + $wechatUserServices = app()->make(WechatUserServices::class); + $refundData['open_id'] = $wechatUserServices->value(['uid' => (int)$order['uid']], 'openid'); + //判断订单是不是重新支付订单 + if (in_array(substr($refundOrder['unique'], 0, 2), ['wx', 'cp', 'hy', 'cz'])) { + $refundData['routine_order_id'] = $refundOrder['unique']; + } else { + $refundData['routine_order_id'] = $refundOrder['order_id']; + } + $refundData['pay_routine_open'] = true; + } + Payment::instance()->setAccessEnd(Payment::MINI)->payOrderRefund($no, $refundData);//小程序 + } else { + //微信公众号退款 + Payment::instance()->setAccessEnd(Payment::WEB)->payOrderRefund($no, $refundData);//公众号 + } + break; + case PayServices::YUE_PAY: + //余额退款 + if (!$this->yueRefund($refundOrder, $refundData)) { + throw new ValidateException('余额退款失败'); + } + break; + case PayServices::ALIAPY_PAY: + mt_srand(); + $refund_id = $refundData['refund_id'] ?? $refundOrder['order_id'] . rand(100, 999); + //支付宝退款 + AliPayService::instance()->refund(strpos($refundOrder['trade_no'], '_') !== false ? $refundOrder['trade_no'] : $refundOrder['order_id'], floatval($refundData['refund_price']), $refund_id); + break; + } + } + //订单记录 + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'refund_price', + 'change_message' => '退款给用户:' . $refundData['refund_price'] . '元', + 'change_time' => time() + ]); + return $order; + }); + //订单同意退款事件 + event('order.refund', [$refundData, $order, 'order_refund']); + return true; + } + + /** + * 处理退款 拆分订单 + * @param int $id + * @param array $orderRefundInfo + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function agreeSplitRefundOrder(int $id, $orderRefundInfo = []) + { + if (!$orderRefundInfo) { + $orderRefundInfo = $this->dao->get($id); + } + if (!$orderRefundInfo) { + throw new ValidateException('数据不存在'); + } + $cart_ids = []; + if ($orderRefundInfo['cart_info']) { + foreach ($orderRefundInfo['cart_info'] as $cart) { + $cart_ids[] = [ + 'cart_id' => $cart['id'], + 'cart_num' => $cart['cart_num'], + ]; + } + } + return $this->transaction(function () use ($orderRefundInfo, $cart_ids) { + /** @var StoreOrderSplitServices $storeOrderSplitServices */ + $storeOrderSplitServices = app()->make(StoreOrderSplitServices::class); + $oid = (int)$orderRefundInfo['store_order_id']; + $splitResult = $storeOrderSplitServices->equalSplit($oid, $cart_ids, [], 0, true); + $orderInfo = []; + if ($splitResult) {//拆分发货 + [$orderInfo, $otherOrder] = $splitResult; + } + if ($orderInfo) { + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + //原订单退款状态清空 + $storeOrderServices->update($oid, ['refund_status' => 0, 'refund_type' => 0]); + //修改新生成拆分退款订单状态 + $storeOrderServices->update($orderInfo['id'], ['refund_status' => 2, 'refund_type' => 6]); + //修改售后订单 关联退款订单 + $this->dao->update($orderRefundInfo['id'], ['store_order_id' => $orderInfo['id']]); + if ($oid != $otherOrder['id']) {//拆分生成新订单了 + //修改原订单还在申请的退款单 + $this->dao->update(['store_order_id' => $oid], ['store_order_id' => $otherOrder['id']]); + } + $orderInfo = $storeOrderServices->get($orderInfo['id']); + } else {//整单退款 + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $storeOrderServices->update($oid, ['refund_status' => 2, 'refund_type' => 6]); + //修改订单商品申请退款数量 + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCreateServices::class); + $storeOrderCartInfoServices->update(['oid' => $oid], ['refund_num' => 0]); + $orderInfo = $storeOrderServices->get($oid); + } + return $orderInfo; + }); + } + + /** + * 订单退款表单 + * @param int $id + * @param string $type + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function refundOrderForm(int $id, string $type = 'refund') + { + if ($type == 'refund') {//售后订单 + $orderRefund = $this->dao->get($id); + if (!$orderRefund) { + throw new ValidateException('未查到订单'); + } + $order = $this->storeOrderServices->get((int)$orderRefund['store_order_id']); + if (!$order) { + throw new ValidateException('未查到订单'); + } + if (!$order['paid']) { + throw new ValidateException('未支付无法退款'); + } + if ($orderRefund['refund_price'] > 0 && in_array($orderRefund['refund_type'], [1, 5])) { + if ($orderRefund['refund_price'] <= $orderRefund['refunded_price']) { + throw new ValidateException('订单已退款'); + } + } + $f[] = Form::input('order_id', '退款单号', $orderRefund->getData('order_id'))->disabled(true); + $f[] = Form::number('refund_price', '退款金额', (float)bcsub((string)$orderRefund->getData('refund_price'), (string)$orderRefund->getData('refunded_price'), 2))->min(0)->required('请输入退款金额'); + return create_form('退款处理', $f, $this->url('/refund/refund/' . $id), 'PUT'); + } else {//订单主动退款 + $order = $this->storeOrderServices->get((int)$id); + if (!$order) { + throw new ValidateException('未查到订单'); + } + if (!$order['paid']) { + throw new ValidateException('未支付无法退款'); + } + if ($order['pay_price'] > 0 && in_array($order['refund_status'], [0, 1])) { + if ($order['pay_price'] <= $order['refund_price']) { + throw new ValidateException('订单已退款'); + } + } + if ($order['pid'] >= 0) {//未拆分主订单、已拆分子订单 + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + if ($storeOrderRefundServices->count(['store_order_id' => $id, 'refund_type' => [1, 2, 4, 5, 6], 'is_cancel' => 0, 'is_del' => 0])) { + throw new ValidateException('请到售后订单列表处理'); + } + } else {//已拆分发货 + throw new ValidateException('主订单已拆分发货,暂不支持整单主动退款'); + } + $f[] = Form::input('order_id', '退款单号', $order->getData('order_id'))->disabled(true); + $f[] = Form::number('refund_price', '退款金额', (float)bcsub((string)$order->getData('pay_price'), (string)$order->getData('refund_price'), 2))->min(0)->required('请输入退款金额'); + return create_form('退款处理', $f, $this->url('/order/refund/' . $id), 'PUT'); + } + } + + /** + * 余额退款 + * @param $order + * @param array $refundData + * @return bool + */ + public function yueRefund($order, array $refundData) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$userServices->getUserInfo($order['uid'])) { + return false; + } + $usermoney = $userServices->value(['uid' => $order['uid']], 'now_money'); + $res = $userServices->bcInc($order['uid'], 'now_money', $refundData['refund_price'], 'uid'); + /** @var UserMoneyServices $userMoneyServices */ + $userMoneyServices = app()->make(UserMoneyServices::class); + return $res && $userMoneyServices->income('pay_product_refund', $order['uid'], $refundData['refund_price'], bcadd((string)$usermoney, (string)$refundData['refund_price'], 2), $order['id']); + } + + /** + * 回退积分和优惠卷 + * @param $order + * @return bool + */ + public function integralAndCouponBack($order) + { + $res = true; + //回退优惠卷 拆分子订单不退优惠券 + if (!$order['pid'] && $order['coupon_id'] && $order['coupon_price']) { + /** @var StoreCouponUserServices $coumonUserServices */ + $coumonUserServices = app()->make(StoreCouponUserServices::class); + $res = $res && $coumonUserServices->recoverCoupon((int)$order['coupon_id']); + } + //回退积分 + [$order, $changeIntegral] = $this->regressionIntegral($order); + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'integral_back', + 'change_message' => '商品退积分:' . $changeIntegral, + 'change_time' => time() + ]); + return $res && $order->save(); + } + + /** + * 回退使用积分和赠送积分 + * @param $order + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function regressionIntegral($order) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->get($order['uid'], ['integral']); + if (!$userInfo) { + $order->back_integral = $order->use_integral; + return [$order, 0]; + } + $integral = $userInfo['integral']; + if ($order['status'] == -2 || $order['is_del']) { + return [$order, 0]; + } + $res1 = $res2 = $res3 = $res4 = true; + //订单赠送积分 + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $where = [ + 'uid' => $order['uid'], + 'category' => 'integral', + 'type' => 'gain', + 'link_id' => $order['id'] + ]; + $give_integral = $userBillServices->sum($where, 'number'); + if ((int)$order['refund_status'] != 2 && $order['back_integral'] >= $order['use_integral']) { + return [$order, 0]; + } + //子订单退款 再次查询主订单 + if (!$give_integral && $order['pid']) { + $where['link_id'] = $order['pid']; + $give_integral = $userBillServices->sum($where, 'number'); + if ($give_integral) { + $p_order = $this->storeOrderServices->get($order['pid']); + $give_integral = bcmul((string)$give_integral, (string)bcdiv((string)$order['pay_price'], (string)$p_order['pay_price'], 4), 0); + } + } + if ($give_integral) { + //判断订单是否已经回退积分 + $count = $userBillServices->count(['category' => 'integral', 'type' => 'deduction', 'link_id' => $order['id']]); + if (!$count) { + if ($integral > $give_integral) { + $integral = bcsub((string)$integral, (string)$give_integral); + } else { + $integral = 0; + } + //记录赠送积分收回 + $res1 = $userBillServices->income('integral_refund', $order['uid'], (int)$give_integral, (int)$integral, $order['id']); + } + } + //返还下单使用积分 + $use_integral = $order['use_integral']; + if ($use_integral > 0) { + $integral = bcadd((string)$integral, (string)$use_integral); + //记录下单使用积分还回 + $res2 = $userBillServices->income('pay_product_integral_back', $order['uid'], (int)$use_integral, (int)$integral, $order['id']); + } + $res3 = $userServices->update($order['uid'], ['integral' => $integral]); + if (!($res1 && $res2 && $res3)) { + throw new ValidateException('回退积分增加失败'); + } + if ($use_integral > $give_integral) { + $order->back_integral = bcsub($use_integral, $give_integral, 2); + } + return [$order, bcsub((string)$integral, (string)$userInfo['integral'], 0)]; + } + + /** + * 回退库存 + * @param $order + * @return bool + */ + public function regressionStock($order) + { + if ($order['status'] == -2 || $order['is_del']) return true; + $res5 = true; + /** @var StoreOrderCartInfoServices $cartServices */ + $cartServices = app()->make(StoreOrderCartInfoServices::class); + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + /** @var StoreSeckillServices $seckillServices */ + $seckillServices = app()->make(StoreSeckillServices::class); + /** @var StoreCombinationServices $pinkServices */ + $pinkServices = app()->make(StoreCombinationServices::class); + /** @var StoreBargainServices $bargainServices */ + $bargainServices = app()->make(StoreBargainServices::class); + /** @var StoreDiscountsServices $discountServices */ + $discountServices = app()->make(StoreDiscountsServices::class); + /** @var StoreNewcomerServices $storeNewcomerServices */ + $storeNewcomerServices = app()->make(StoreNewcomerServices::class); + $activity_id = (int)$order['activity_id']; + $store_id = (int)$order['store_id'] ?? 0; + $cartInfo = $cartServices->getCartInfoList(['cart_id' => $order['cart_id']], ['cart_info']); + foreach ($cartInfo as $cart) { + $cart['cart_info'] = is_array($cart['cart_info']) ? $cart['cart_info'] : json_decode($cart['cart_info'], true); + //增库存减销量 + $unique = isset($cart['cart_info']['productInfo']['attrInfo']) ? $cart['cart_info']['productInfo']['attrInfo']['unique'] : ''; + $cart_num = (int)$cart['cart_info']['cart_num']; + $product_id = (int)$cart['cart_info']['productInfo']['id']; + switch ($order['type']) { + case 0://普通 + case 6://预售 + case 8://抽奖 + case 9://拼单 + case 10://桌码 + $res5 = $res5 && $services->incProductStock($cart_num, $product_id, $unique, $store_id); + break; + case 1://秒杀 + $res5 = $res5 && $seckillServices->incSeckillStock($cart_num, $activity_id, $unique, $store_id); + break; + case 2://砍价 + $res5 = $res5 && $bargainServices->incBargainStock($cart_num, $activity_id, $unique, $store_id); + break; + case 3://拼团 + $res5 = $res5 && $pinkServices->incCombinationStock($cart_num, $activity_id, $unique, $store_id); + break; + case 5://套餐 + CacheService::setStock(md5($activity_id), 1, 5, false); + $res5 = $res5 && $discountServices->incDiscountStock($cart_num, $activity_id, (int)($cart['cart_info']['discount_product_id'] ?? 0), (int)($cart['cart_info']['product_id'] ?? 0), $unique, $store_id); + break; + case 7://新人专享 + $res5 = $res5 && $storeNewcomerServices->incNewcomerStock($cart_num, $activity_id, $unique, $store_id); + break; + default: + $res5 = $res5 && $services->incProductStock($cart_num, $product_id, $unique, $store_id); + break; + } + if (in_array($order['type'], [1, 2, 3])) CacheService::setStock($unique, $cart_num, (int)$order['type'], false); + } + if ($order['type'] == 5) { + //改变套餐限量 + $res5 = $res5 && $discountServices->changeDiscountLimit($activity_id, false); + } + $this->regressionRedisStock($order); + return $res5; + } + + /** + * 回退redis占用库存 + * @param $order + * @return bool + */ + public function regressionRedisStock($order) + { + if ($order['status'] == -2 || $order['is_del']) return true; + $type = $order['type'] ?? 0; + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfo = $storeOrderCartInfoServices->getOrderCartInfo((int)$order['id']); + //回退套餐限量库 + if ($type == 5 && $order['activity_id']) CacheService::setStock(md5($order['activity_id']), 1, 5, false); + foreach ($cartInfo as $item) {//回退redis占用 + if (!isset($item['product_attr_unique']) || !$item['product_attr_unique']) continue; + $type = $item['type']; + if (in_array($type, [1, 2, 3])) CacheService::setStock($item['product_attr_unique'], (int)$item['cart_num'], $type, false); + } + return true; + } + + /** + * 同意退款退款失败写入订单记录 + * @param int $id + * @param $refund_price + */ + public function storeProductOrderRefundYFasle(int $id, $refund_price) + { + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $statusService->save([ + 'oid' => $id, + 'change_type' => 'refund_price', + 'change_message' => '退款给用户:' . $refund_price . '元失败', + 'change_time' => time() + ]); + } + + /** + * 不退款表单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function noRefundForm(int $id) + { + $orderRefund = $this->dao->get($id); + if (!$orderRefund) { + throw new ValidateException('未查到订单'); + } + $order = $this->storeOrderServices->get((int)$orderRefund['store_order_id']); + if (!$order) { + throw new ValidateException('未查到订单'); + } + $f[] = Form::input('order_id', '不退款单号', $order->getData('order_id'))->disabled(true); + $f[] = Form::input('refund_reason', '不退款原因')->type('textarea')->required('请填写不退款原因'); + return create_form('不退款原因', $f, $this->url('order/no_refund/' . $id), 'PUT'); + } + + /** + * 退积分表单创建 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function refundIntegralForm(int $id) + { + if (!$orderInfo = $this->storeOrderServices->get($id)) + throw new ValidateException('订单不存在'); + if ($orderInfo->use_integral < 0 || $orderInfo->use_integral == $orderInfo->back_integral) + throw new ValidateException('积分已退或者积分为零无法再退'); + if (!$orderInfo->paid) + throw new ValidateException('未支付无法退积分'); + $f[] = Form::input('order_id', '退款单号', $orderInfo->getData('order_id'))->disabled(1); + $f[] = Form::number('use_integral', '使用的积分', (float)$orderInfo->getData('use_integral'))->min(0)->disabled(1); + $f[] = Form::number('use_integrals', '已退积分', (float)$orderInfo->getData('back_integral'))->min(0)->disabled(1); + $f[] = Form::number('back_integral', '可退积分', (float)bcsub($orderInfo->getData('use_integral'), $orderInfo->getData('back_integral')))->min(0)->precision(0)->required('请输入可退积分'); + return create_form('退积分', $f, $this->url('/order/refund_integral/' . $id), 'PUT'); + } + + /** + * 单独退积分处理 + * @param $orderInfo + * @param $back_integral + */ + public function refundIntegral($orderInfo, $back_integral) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $integral = $userServices->value(['uid' => $orderInfo['uid']], 'integral'); + return $this->transaction(function () use ($userServices, $orderInfo, $back_integral, $integral) { + $res1 = $userServices->bcInc($orderInfo['uid'], 'integral', $back_integral, 'uid'); + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $res2 = $userBillServices->income('pay_product_integral_back', $orderInfo['uid'], $back_integral, $integral + $back_integral, $orderInfo['id']); + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $res3 = $statusService->save([ + 'oid' => $orderInfo['id'], + 'change_type' => 'integral_back', + 'change_message' => '商品退积分:' . $back_integral, + 'change_time' => time() + ]); + $res4 = $orderInfo->save(); + $res = $res1 && $res2 && $res3 && $res4; + if (!$res) { + throw new ValidateException('订单退积分失败'); + } + return true; + }); + } + + /** + * 写入退款快递单号 + * @param $order + * @param $express + * @return bool + */ + public function editRefundExpress($data) + { + $id = (int)$data['id']; + $refundOrder = $this->dao->get($id); + if (!$refundOrder) { + throw new ValidateException('退款订单不存在'); + } + $this->transaction(function () use ($id, $refundOrder, $data) { + $data['refund_type'] = 5; + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $res1 = false !== $statusService->save([ + 'oid' => $refundOrder['store_order_id'], + 'change_type' => 'refund_express', + 'change_message' => '用户已退货,快递单号:' . $data['refund_express'], + 'change_time' => time() + ]); + $res2 = false !== $this->dao->update(['id' => $id], $data); + $res = $res1 && $res2; + if (!$res) + throw new ValidateException('提交失败!'); + }); + return true; + } + + /** + * 退款订单详情 + * @param $uni + * @param array $field + * @param array $with + * @return mixed + */ + public function refundDetail($uni, array $field = ['*'], array $with = ['invoice', 'virtual']) + { + if (!strlen(trim($uni))) throw new ValidateException('参数错误'); + $order = $this->dao->get(['id|order_id' => $uni], ['*']); + if (!$order) throw new ValidateException('订单不存在'); + $order = $order->toArray(); + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $orderInfo = $orderServices->get($order['store_order_id'], $field, $with); + $orderInfo = $orderInfo->toArray(); + $orderInfo = $orderServices->tidyOrder($orderInfo, true, true); + + + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserWithTrashedInfo($order['uid']); + $order['mapKey'] = sys_config('tengxun_map_key'); + $order['yue_pay_status'] = (int)sys_config('balance_func_status') && (int)sys_config('yue_pay_status') == 1 ? (int)1 : (int)2;//余额支付 1 开启 2 关闭 + $order['pay_weixin_open'] = (int)sys_config('pay_weixin_open') ?? 0;//微信支付 1 开启 0 关闭 + $order['ali_pay_status'] = (bool)sys_config('ali_pay_status');//支付包支付 1 开启 0 关闭 + $orderData = $order; + $orderData['refunded_price'] = floatval($orderData['refunded_price']) ?: $orderData['refund_price']; + $orderData['store_order_sn'] = $orderInfo['order_id']; + $orderData['product_type'] = $orderInfo['product_type']; + $orderData['supplier_id'] = $orderInfo['supplier_id'] ?? 0; + $orderData['supplierInfo'] = $orderInfo['supplierInfo'] ?? null; + $orderData['cartInfo'] = $orderData['cart_info']; + $orderData['invoice'] = $orderInfo['invoice']; + $orderData['virtual'] = $orderInfo['virtual']; + $orderData['virtual_info'] = $orderInfo['virtual_info']; + $orderData['custom_form'] = is_string($orderInfo['custom_form']) ? json_decode($orderInfo['custom_form'], true) : $orderInfo['custom_form']; + $orderData['first_order_price'] = $orderInfo['first_order_price']; + $cateData = []; + if (isset($orderData['cartInfo']) && $orderData['cartInfo']) { + $productId = array_column($orderData['cartInfo'], 'product_id'); + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $cateData = $productServices->productIdByProductCateName($productId); + } + //核算优惠金额 + $vipTruePrice = 0; + $total_price = 0; + $promotionsPrice = 0; + foreach ($orderData['cartInfo'] ?? [] as $key => &$cart) { + if (!isset($cart['sum_true_price'])) $cart['sum_true_price'] = bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2); + $cart['vip_sum_truePrice'] = bcmul($cart['vip_truePrice'], $cart['cart_num'] ? $cart['cart_num'] : 1, 2); + $vipTruePrice = bcadd((string)$vipTruePrice, (string)$cart['vip_sum_truePrice'], 2); + if (isset($order['split']) && $order['split']) { + $orderData['cartInfo'][$key]['cart_num'] = $cart['surplus_num']; + if (!$cart['surplus_num']) unset($orderData['cartInfo'][$key]); + } + $total_price = bcadd($total_price, bcmul((string)$cart['sum_price'], (string)$cart['cart_num'], 2), 2); + $orderData['cartInfo'][$key]['class_name'] = $cateData[$cart['product_id']] ?? ''; + $promotionsPrice = bcadd($promotionsPrice, bcmul((string)($cart['promotions_true_price'] ?? 0), (string)$cart['cart_num'], 2), 2); + } + //优惠活动优惠详情 + /** @var StoreOrderPromotionsServices $storeOrderPromotiosServices */ + $storeOrderPromotiosServices = app()->make(StoreOrderPromotionsServices::class); + if ($orderData['refund_type'] == 6) { + $orderData['promotions_detail'] = $storeOrderPromotiosServices->getOrderPromotionsDetail((int)$orderData['store_order_id']); + } else { + $orderData['promotions_detail'] = $storeOrderPromotiosServices->applyRefundOrderPromotions((int)$orderData['store_order_id'], $orderData['cartInfo']); + } + if (!$orderData['promotions_detail'] && $promotionsPrice) { + $orderData['promotions_detail'][] = [ + 'name' => '优惠活动', + 'title' => '优惠活动', + 'promotions_price' => $promotionsPrice, + ]; + } + $orderData['use_integral'] = $this->getOrderSumPrice($orderData['cartInfo'], 'use_integral', false); + $orderData['integral_price'] = $this->getOrderSumPrice($orderData['cartInfo'], 'integral_price', false); + $orderData['coupon_id'] = $orderInfo['coupon_id']; + $orderData['coupon_price'] = $this->getOrderSumPrice($orderData['cartInfo'], 'coupon_price', false); + $orderData['deduction_price'] = $this->getOrderSumPrice($orderData['cartInfo'], 'integral_price', false); + $orderData['vip_true_price'] = $vipTruePrice; + $orderData['postage_price'] = 0; + $orderData['pay_postage'] = 0; + if (!in_array($orderInfo['shipping_type'], [2, 4])) { + $orderData['pay_postage'] = $this->getOrderSumPrice($orderData['cart_info'], 'postage_price', false); + } + $orderData['member_price'] = 0; + $orderData['routine_contact_type'] = sys_config('routine_contact_type', 0); + switch ($orderInfo['pay_type']) { + case PayServices::WEIXIN_PAY: + $pay_type_name = '微信支付'; + break; + case PayServices::YUE_PAY: + $pay_type_name = '余额支付'; + break; + case PayServices::OFFLINE_PAY: + $pay_type_name = '线下支付'; + break; + case PayServices::ALIAPY_PAY: + $pay_type_name = '支付宝支付'; + break; + case PayServices::CASH_PAY: + $pay_type_name = '现金支付'; + break; + default: + $pay_type_name = '其他支付'; + break; + } + $orderData['_add_time'] = date('Y-m-d H:i:s', $orderData['add_time']); + $orderData['add_time_y'] = date('Y-m-d', $orderData['add_time']); + $orderData['add_time_h'] = date('H:i:s', $orderData['add_time']); + if (in_array($orderData['refund_type'], [1, 2, 4, 5])) { + $_type = -1; + $_msg = '商家审核中,请耐心等待'; + $_title = '申请退款中'; + } elseif ($orderData['refund_type'] == 3) { + $_type = -3; + $_title = '拒绝退款'; + $_msg = '商家拒绝退款,请联系商家'; + } else { + $_type = -2; + $_title = '已退款'; + $_msg = '已为您退款,感谢您的支持'; + } + if ($orderData['store_id']) { + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeInfo = $storeServices->get($orderData['store_id']); + $refund_name = $storeInfo['name']; + $refund_phone = $storeInfo['phone']; + $refund_address = $storeInfo['detailed_address']; + } elseif ($orderData['supplier_id']) { + /** @var SystemSupplierServices $supplierServices */ + $supplierServices = app()->make(SystemSupplierServices::class); + $supplierIno = $supplierServices->get($orderData['supplier_id']); + $refund_name = $supplierIno['supplier_name']; + $refund_phone = $supplierIno['phone']; + $refund_address = $supplierIno['detailed_address']; + } else { + $refund_name = sys_config('refund_name', ''); + $refund_phone = sys_config('refund_phone', ''); + $refund_address = sys_config('refund_address', ''); + } + $orderData['_status'] = [ + '_type' => $_type, + '_title' => $_title, + '_msg' => $_msg ?? '', + '_payType' => $pay_type_name, + 'refund_name' => $refund_name, + 'refund_phone' => $refund_phone, + 'refund_address' => $refund_address, + ]; + $orderData['shipping_type'] = $orderInfo['shipping_type']; + $orderData['real_name'] = $orderInfo['real_name']; + $orderData['user_phone'] = $orderInfo['user_phone']; + $orderData['user_address'] = $orderInfo['user_address']; + $orderData['_pay_time'] = $orderInfo['pay_time'] ? date('Y-m-d H:i:s', $orderInfo['pay_time']) : ''; + $orderData['_add_time'] = $orderInfo['add_time'] ? date('Y-m-d H:i:s', $orderInfo['add_time']) : ''; + $orderData['_refund_time'] = $orderData['add_time'] ? date('Y-m-d H:i:s', $orderData['add_time']) : ''; + $orderData['nickname'] = $userInfo['nickname'] ?? ''; + $orderData['total_num'] = $orderData['refund_num']; + $orderData['pay_price'] = $orderData['refund_price']; + $orderData['refund_status'] = in_array($orderData['refund_type'], [1, 2, 4, 5]) ? 1 : 2; + $orderData['total_price'] = floatval(bcsub((string)$total_price, (string)$vipTruePrice, 2)); + $orderData['paid'] = 1; + $orderData['mark'] = $orderInfo['mark']; + $orderData['express_list'] = $orderData['refund_type'] == 4 ? app()->make(ExpressServices::class)->expressList(['is_show' => 1]) : []; + $orderData['spread_uid'] = $orderInfo['spread_uid'] ?? 0; + $orderData['orderStatus'] = $orderInfo['_status']; + return $orderData; + } + + /** + * 获取某个字段总金额 + * @param $cartInfo + * @param string $key + * @param bool $is_unit + * @return int|string + */ + public function getOrderSumPrice($cartInfo, $key = 'truePrice', $is_unit = true) + { + $SumPrice = 0; + foreach ($cartInfo as $cart) { + if (isset($cart['cart_info'])) $cart = $cart['cart_info']; + if ($is_unit) { + $SumPrice = bcadd($SumPrice, bcmul($cart['cart_num'] ?? 1, $cart[$key] ?? 0, 2), 2); + } else { + $SumPrice = bcadd($SumPrice, $cart[$key] ?? 0, 2); + } + } + return $SumPrice; + } +} diff --git a/app/services/order/StoreOrderServices.php b/app/services/order/StoreOrderServices.php new file mode 100644 index 0000000..3fad992 --- /dev/null +++ b/app/services/order/StoreOrderServices.php @@ -0,0 +1,2748 @@ + +// +---------------------------------------------------------------------- +namespace app\services\order; + +use app\common\dao\store\order\StoreOrderDao; +use app\jobs\BatchHandleJob; +use app\jobs\order\AutoOrderUnpaidCancelJob; +use app\jobs\order\SpliteStoreOrderJob; +use app\services\activity\combination\StorePinkServices; +use app\services\activity\coupon\StoreCouponIssueServices; +use app\services\BaseServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\other\queue\QueueAuxiliaryServices; +use app\services\other\queue\QueueServices; +use app\services\pay\PayServices; +use app\services\product\product\StoreProductLogServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\store\finance\StoreFinanceFlowServices; +use app\services\store\SystemStoreServices; +use app\services\system\config\ConfigServices; +use app\services\system\form\SystemFormServices; +use app\services\user\UserInvoiceServices; +use app\services\user\UserServices; +use app\services\product\product\StoreProductReplyServices; +use app\services\user\UserAddressServices; +use app\services\user\level\UserLevelServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use crmeb\services\FileService; +use crmeb\services\FormBuilder as Form; +use crmeb\services\printer\Printer; +use crmeb\services\SystemConfigService; +use crmeb\traits\ServicesTrait; +use crmeb\utils\Arr; +use think\exception\ValidateException; +use think\facade\Log; + +/** + * Class StoreOrderServices + * @package app\services\order + * @mixin StoreOrderDao + */ +class StoreOrderServices extends BaseServices +{ + use ServicesTrait; + + /** + * 订单类型 + * @var string[] + */ + protected $type = [ + 0 => '普通', + 1 => '秒杀', + 2 => '砍价', + 3 => '拼团', + 4 => '积分', + 5 => '套餐', + 6 => '预售', + 7 => '新人礼', + 8 => '抽奖', + 9 => '拼单', + 10 => '桌码' + ]; + + /** + * 发货类型 + * @var string[] + */ + public $deliveryType = ['send' => '商家配送', 'express' => '快递配送', 'fictitious' => '虚拟发货', 'delivery_part_split' => '拆分部分发货', 'delivery_split' => '拆分发货完成']; + + /** + * StoreOrderProductServices constructor. + * @param StoreOrderDao $dao + */ + public function __construct(StoreOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * 从缓存中获取购买商品个数 + * @param int $uid + * @param int $type + * @param int $id + * @return int + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/3 + */ + public function getBuyCountCache(int $uid, int $type, int $id) + { + $key = md5($uid . $type . $id); + $res = $this->dao->cacheInfoById($key); + if (null !== $res) { + $num = $this->dao->getBuyCount($uid, $type, $id); + $this->dao->cacheUpdate(['type' => $type, 'uid' => $uid, 'product_id' => $id, 'totalNum' => $num ?: 0], $key); + } else { + $num = $res['totalNum'] ?? 0; + } + return (int)$num; + } + + /** + * 获取门店订单统计 + * @param int $storeId + * @return array + */ + public function getStoreOrderHeader(int $storeId) + { + return [ + 'cashier' => $this->dao->count(['pid' => 0, 'type' => 6, 'is_system_del' => 0, 'store_id' => $storeId]), + 'delivery' => $this->dao->count(['pid' => 0, 'type' => 7, 'is_system_del' => 0, 'store_id' => $storeId]), + 'writeoff' => $this->dao->count(['pid' => 0, 'type' => 5, 'is_system_del' => 0, 'store_id' => $storeId]), + ]; + } + + /** + * 获取列表 + * @param array $where + * @param array $field + * @param array $with + * @param bool $abridge + * @param string $order + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderList(array $where, array $field = ['*'], array $with = [], bool $abridge = false, string $order = 'add_time DESC,id DESC') + { + [$page, $limit] = $this->getPageValue(); + $data = $this->dao->getOrderList($where, $field, $page, $limit, $with, $order); + $count = $this->dao->count($where); + $stat = []; + $batch_url = "file/upload/1"; + if ($data) { + $data = $this->tidyOrderList($data, true, $abridge); + foreach ($data as &$item) { + $refund_num = array_sum(array_column($item['refund'], 'refund_num')); + $cart_num = 0; + foreach ($item['_info'] as &$items) { + if (isset($items['cart_info']['is_gift']) && $items['cart_info']['is_gift']) continue; + $cart_num += $items['cart_info']['cart_num']; + $cart_ids = []; + $cart_ids[] = ['cart_id' => $items['cart_info']['id'], 'cart_num' => $items['cart_info']['cart_num']]; + /** @var StoreOrderSplitServices $storeOrderSpliteServices */ + $storeOrderSpliteServices = app()->make(StoreOrderSplitServices::class); + $cartInfos = $storeOrderSpliteServices->getSplitOrderCartInfo($item['id'], $cart_ids, $item); + $total_price = $pay_postage = 0; + foreach ($cartInfos as $cart) { + $_info = is_string($cart['cart_info']) ? json_decode($cart['cart_info'], true) : $cart['cart_info']; + $total_price = bcadd((string)$total_price, bcmul((string)($_info['truePrice'] ?? 0), (string)$cart['cart_num'], 4), 2); + if (!in_array($item['shipping_type'], [2, 4])) { + $pay_postage = bcadd((string)$pay_postage, (string)($_info['postage_price'] ?? 0), 2); + } + } + //实际退款金额 + $refund_pay_price = bcadd((string)$total_price, (string)$pay_postage, 2); + + if (isset($item['change_price']) && (float)$item['change_price']) {//有改价 且是拆分 + //订单原实际支付金额 + $order_pay_price = bcadd((string)$item['change_price'], (string)$item['pay_price'], 2); + $refund_price = bcmul((string)bcdiv((string)$item['pay_price'], (string)$order_pay_price, 4), (string)$refund_pay_price, 2); + } else { + $refund_price = $refund_pay_price; + } + $items['cart_info']['refund_price'] = $refund_price; + } + $item['is_all_refund'] = $refund_num == $cart_num; + } + } + return compact('data', 'count', 'stat', 'batch_url'); + } + + /**获取单个订单信息 + * @param string $key + * @param int $uid + * @param array $with + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOneOrderList(string $key, int $uid, array $with = []) + { + return $this->dao->getUserOrderDetail($key, $uid, array_merge(['user', 'spread', 'refund'], $with)); + } + + /** + * 获取列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSplitOrderList(array $where, array $field = ['*'], array $with = []) + { + $data = $this->dao->getOrderList($where, $field, 0, 0, $with); + if ($data) { + $data = $this->tidyOrderList($data); + /** @var StoreOrderStatusServices $statusServices */ + $statusServices = app()->make(StoreOrderStatusServices::class); + foreach ($data as &$item) { + $log = $statusServices->getColumn(['oid' => $item['id']], 'change_time', 'change_type'); + if (isset($log['delivery'])) { + $delivery = date('Y-m-d H:i:s', $log['delivery']); + } elseif (isset($log['delivery_goods'])) { + $delivery = date('Y-m-d H:i:s', $log['delivery_goods']); + } elseif (isset($log['delivery_fictitious'])) { + $delivery = date('Y-m-d H:i:s', $log['delivery_fictitious']); + } else { + $delivery = ''; + } + $item['delivery_time'] = $delivery; + } + } + return $data; + } + + /** + * 前端订单列表 + * @param array $where + * @param array|string[] $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderApiList(array $where, array $field = ['*'], array $with = []) + { + [$page, $limit] = $this->getPageValue(); + if (isset($where['status']) && $where['status'] === '') { + $data = $this->dao->getOrderList($where, $field, $page, $limit, $with, 'id DESC'); + } else { + $data = $this->dao->getOrderList($where, $field, $page, $limit, $with); + } + foreach ($data as &$item) { + $item = $this->tidyOrder($item, true); + $cart_num = 0; + foreach ($item['cartInfo'] ?: [] as $key => $product) { + if (isset($item['_status']['_type']) && $item['_status']['_type'] == 3) { + $item['cartInfo'][$key]['add_time'] = isset($product['add_time']) ? date('Y-m-d H:i', (int)$product['add_time']) : '时间错误'; + } + $item['cartInfo'][$key]['productInfo']['price'] = $product['truePrice'] ?? 0; + + if (isset($product['is_gift']) && $product['is_gift']) continue; + $cart_num += $product['cart_num']; + } + if (count($item['refund'])) { + $refund_num = array_sum(array_column($item['refund'], 'refund_num')); + $item['is_all_refund'] = $refund_num == $cart_num; + } else { + $item['is_all_refund'] = false; + } + } + return $data; + } + + /** + * 获取订单数量 + * @param int $uid + * @param int $store_id + * @param int $plat_type + * @return array + */ + public function getOrderData(int $uid = 0, int $store_id = -1, int $plat_type = -1) + { + $where = ['pid' => 0, 'uid' => $uid, 'is_del' => 0, 'is_system_del' => 0, 'plat_type' => $plat_type]; + $countWhere = []; + if ($store_id != -1) { + $where['store_id'] = $store_id; + $countWhere['store_id'] = $store_id; + } + $data['order_count'] = (string)$this->dao->count($where); + $where = $where + ['paid' => 1]; + $data['sum_price'] = (string)$this->dao->sum($where, 'pay_price', true); +// $countWhere = $store_id != -1 ? ['pid' => 0, 'store_id' => $store_id] : ['pid' => 0]; + if ($uid) { + $countWhere['uid'] = $uid; + } + if ($plat_type != -1) { + $countWhere['plat_type'] = $plat_type; + } + $pid_where = ['pid' => 0]; + $not_pid_where = ['not_pid' => 1]; + $data['unpaid_count'] = (string)$this->dao->count(['status' => 0] + $countWhere + $pid_where); + $data['unshipped_count'] = (string)$this->dao->count(['status' => 1] + $countWhere + $pid_where); + $data['received_count'] = (string)$this->dao->count(['status' => 2] + $countWhere + $pid_where); + $data['evaluated_count'] = (string)$this->dao->count(['status' => 3] + $countWhere + $pid_where); + $data['unwritoff_count'] = (string)$this->dao->count(['status' => 5] + $countWhere); + $data['complete_count'] = (string)$this->dao->count(['status' => 4] + $countWhere + $pid_where); + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + $refund_where = ['is_cancel' => 0]; + if ($uid) $refund_where['uid'] = $uid; + if ($store_id != -1) $refund_where['store_id'] = $store_id; + $data['refunding_count'] = (string)$storeOrderRefundServices->count($refund_where + ['refund_type' => [1, 2, 4, 5]]); + $data['refunded_count'] = (string)$storeOrderRefundServices->count($refund_where + ['refund_type' => [3, 6]]); + $data['refund_count'] = (string)bcadd($data['refunding_count'], $data['refunded_count'], 0); + $data['yue_pay_status'] = (int)sys_config('balance_func_status') && (int)sys_config('yue_pay_status') == 1 ? (int)1 : (int)2;//余额支付 1 开启 2 关闭 + $data['pay_weixin_open'] = (int)sys_config('pay_weixin_open') ?? 0;//微信支付 1 开启 0 关闭 + $data['ali_pay_status'] = (bool)sys_config('ali_pay_status');//支付包支付 1 开启 0 关闭 + return $data; + } + + /** + * 订单详情数据格式化 + * @param $order + * @param bool $detail 是否需要订单商品详情 + * @param bool $isPic 是否需要订单状态图片 + * @return mixed + */ + public function tidyOrder($order, bool $detail = false, bool $isPic = false) + { + if ($detail == true && isset($order['id'])) { + /** @var StoreOrderCartInfoServices $cartServices */ + $cartServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfos = $cartServices->getCartColunm(['oid' => $order['id']], 'cart_num,is_writeoff,surplus_num,cart_info,refund_num,product_type,is_support_refund,is_gift,promotions_id,type,relation_id,write_times,write_surplus_times,write_start,write_end', 'unique'); + $info = []; + /** @var StoreProductReplyServices $replyServices */ + $replyServices = app()->make(StoreProductReplyServices::class); + foreach ($cartInfos as $k => $cartInfo) { + $cart = json_decode($cartInfo['cart_info'], true); + $cart['cart_num'] = $cartInfo['cart_num']; + $cart['is_writeoff'] = $cartInfo['is_writeoff']; + $cart['surplus_num'] = $cartInfo['write_surplus_times']; + $cart['refund_num'] = $cartInfo['refund_num']; + $cart['write_times'] = $cartInfo['write_times']; + $cart['write_surplus_times'] = $cartInfo['write_surplus_times']; + $cart['write_start'] = $cartInfo['write_start']; + $cart['write_end'] = $cartInfo['write_end']; + $cart['write_off'] = max(bcsub((string)$cart['write_times'], (string)$cart['write_surplus_times'], 0), 0); + $cart['product_type'] = $cartInfo['product_type']; + $cart['supplier_id'] = $cart['store_id'] = 0; + if ($cartInfo['type'] == 1) { + $cart['store_id'] = $cartInfo['relation_id'] ?? 0; + } elseif ($cartInfo['type'] == 2) { + $cart['supplier_id'] = $cartInfo['relation_id'] ?? 0; + } + $cart['is_support_refund'] = $cartInfo['is_support_refund']; + $cart['is_gift'] = $cartInfo['is_gift']; + $cart['promotions_id'] = $cartInfo['promotions_id']; + $cart['unique'] = $k; + //新增是否评价字段 + $cart['is_reply'] = $replyServices->count(['unique' => $k]); + if (isset($cart['productInfo']['attrInfo'])) { + $cart['productInfo']['attrInfo'] = get_thumb_water($cart['productInfo']['attrInfo']); + } + $cart['productInfo'] = get_thumb_water($cart['productInfo']); + //一种商品买多件 计算总优惠 + $cart['vip_sum_truePrice'] = bcmul($cart['vip_truePrice'], $cart['cart_num'] ? $cart['cart_num'] : 1, 2); + $cart['is_valid'] = 1; + array_push($info, $cart); + unset($cart); + } + $order['cartInfo'] = $info; + } + /** @var StoreOrderStatusServices $statusServices */ + $statusServices = app()->make(StoreOrderStatusServices::class); + $status = []; + $storeInfo = []; + if ($order['store_id']) { + $storeServices = app()->make(SystemStoreServices::class); + $storeInfo = $storeServices->get((int)$order['store_id']); + } + //系统预设取消订单时间段 + $keyValue = ['order_cancel_time', 'order_activity_time', 'order_bargain_time', 'order_seckill_time', 'order_pink_time']; + //获取配置 + $systemValue = SystemConfigService::more($keyValue); + //格式化数据 + $systemValue = Arr::setValeTime($keyValue, is_array($systemValue) ? $systemValue : []); + switch ($order['type'] ?? 0) { + case 1://秒杀 + $secs = $systemValue['order_seckill_time'] ? $systemValue['order_seckill_time'] : $systemValue['order_activity_time']; + break; + case 2://砍价 + $secs = $systemValue['order_bargain_time'] ? $systemValue['order_bargain_time'] : $systemValue['order_activity_time']; + break; + case 3://拼团 + $secs = $systemValue['order_pink_time'] ? $systemValue['order_pink_time'] : $systemValue['order_activity_time']; + break; + default: + $secs = $systemValue['order_cancel_time']; + break; + } + $order['stop_time'] = $secs * 3600 + $order['add_time']; + + if (!$order['paid'] && $order['pay_type'] == 'offline' && !$order['status'] >= 2) { + $status['_type'] = 9; + $status['_title'] = '线下付款,未支付'; + $status['_msg'] = '等待商家处理,请耐心等待'; + $status['_class'] = 'nobuy'; + } else if (!$order['paid']) { + $status['_type'] = 0; + $status['_title'] = '未支付'; + $status['_msg'] = '请在' . date('m-d H:i:s', $order['stop_time']) . '前完成支付!'; + $status['_class'] = 'nobuy'; + } else if ($order['refund_status'] == 2) { + $status['_type'] = -2; + $status['_title'] = '已退款'; + $status['_msg'] = '已为您退款,感谢您的支持'; + $status['_class'] = 'state-sqtk'; + } else if ($order['status'] == 4) { + if ($order['delivery_type'] == 'send') {// 送货 + $status['_type'] = 2; + $status['_title'] = '待收货'; + $status['_msg'] = date('m月d日H时i分', $statusServices->value(['oid' => $order['id'], 'change_type' => 'delivery'], 'change_time')) . '服务商已送货'; + $status['_class'] = 'state-ysh'; + } elseif ($order['delivery_type'] == 'express') {// 发货 + $status['_type'] = 2; + $status['_title'] = '待收货'; + $status['_msg'] = date('m月d日H时i分', $statusServices->value(['oid' => $order['id'], 'change_type' => 'delivery_goods'], 'change_time')) . '服务商已发货'; + $status['_class'] = 'state-ysh'; + } elseif ($order['delivery_type'] == 'split') {//拆分发货 + $status['_type'] = 2; + $status['_title'] = '待收货'; + $status['_msg'] = date('m月d日H时i分', $statusServices->value(['oid' => $order['id'], 'change_type' => 'delivery_part_split'], 'change_time')) . '服务商已拆分多个包裹发货'; + $status['_class'] = 'state-ysh'; + } else { + $status['_type'] = 2; + $status['_title'] = '待收货'; + $status['_msg'] = date('m月d日H时i分', $statusServices->value(['oid' => $order['id'], 'change_type' => 'delivery_fictitious'], 'change_time')) . '服务商已虚拟发货'; + $status['_class'] = 'state-ysh'; + } + } elseif ($order['status'] == 5) { + if ($order['shipping_type'] == 2) { + $status['_type'] = 5; + $status['_title'] = '部分核销'; + $status['_msg'] = '部分核销,请继续进行核销'; + $status['_class'] = 'state-nfh'; + } else { + $status['_type'] = 2; + $status['_title'] = '待收货'; + $status['_msg'] = '部分核销收货,请继续进行核销'; + $status['_class'] = 'state-ysh'; + } + } else if ($order['refund_status'] == 1) { + if (in_array($order['refund_type'], [0, 1, 2])) { + $status['_type'] = -1; + $status['_title'] = '申请退款中'; + $status['_msg'] = '商家审核中,请耐心等待'; + $status['_class'] = 'state-sqtk'; + } elseif ($order['refund_type'] == 4) { + $status['_type'] = -1; + $status['_title'] = '申请退款中'; + $status['_msg'] = '商家同意退款,请填写退货订单号'; + $status['_class'] = 'state-sqtk'; + if ($order['shipping_type'] == 1 || !$storeInfo) {//平台 + $status['refund_name'] = sys_config('refund_name', ''); + $status['refund_phone'] = sys_config('refund_phone', ''); + $status['refund_address'] = sys_config('refund_address', ''); + } else { + $status['refund_name'] = $storeInfo['name']; + $status['refund_phone'] = $storeInfo['phone']; + $status['refund_address'] = $storeInfo['detailed_address']; + } + } elseif ($order['refund_type'] == 5) { + $status['_type'] = -1; + $status['_title'] = '申请退款中'; + $status['_msg'] = '等待商家收货'; + $status['_class'] = 'state-sqtk'; + if ($order['shipping_type'] == 1 || !$storeInfo) {//平台 + $status['refund_name'] = sys_config('refund_name', ''); + $status['refund_phone'] = sys_config('refund_phone', ''); + $status['refund_address'] = sys_config('refund_address', ''); + } else { + $status['refund_name'] = $storeInfo['name']; + $status['refund_phone'] = $storeInfo['phone']; + $status['refund_address'] = $storeInfo['detailed_address']; + } + } + } else if ($order['refund_status'] == 3) { + $status['_type'] = -1; + $status['_title'] = '部分退款(子订单)'; + $status['_msg'] = '拆分发货,部分退款'; + $status['_class'] = 'state-sqtk'; + } else if ($order['refund_status'] == 4) { + $status['_type'] = -1; + $status['_title'] = '子订单已全部申请退款中'; + $status['_msg'] = '拆分发货,全部退款'; + $status['_class'] = 'state-sqtk'; + } else if (!$order['status']) { + if ($order['pink_id']) { + /** @var StorePinkServices $pinkServices */ + $pinkServices = app()->make(StorePinkServices::class); + if ($pinkServices->getCount(['id' => $order['pink_id'], 'status' => 1])) { + $status['_type'] = 1; + $status['_title'] = '拼团中'; + $status['_msg'] = '等待其他人参加拼团'; + $status['_class'] = 'state-nfh'; + } else if (in_array($order['shipping_type'], [1, 3])) { + $status['_type'] = 1; + $status['_title'] = '未发货'; + $status['_msg'] = '商家未发货,请耐心等待'; + $status['_class'] = 'state-nfh'; + } else { + $status['_type'] = 5; + $status['_title'] = '待核销'; + $status['_msg'] = '待核销,请到核销点进行核销'; + $status['_class'] = 'state-nfh'; + } + } else { + if (in_array($order['shipping_type'], [1, 3])) { + $status['_type'] = 1; + $status['_title'] = '未发货'; + $status['_msg'] = '商家未发货,请耐心等待'; + $status['_class'] = 'state-nfh'; + } else { + $status['_type'] = 5; + $status['_title'] = '待核销'; + $status['_msg'] = '待核销,请到核销点进行核销'; + $status['_class'] = 'state-nfh'; + } + } + } else if ($order['status'] == 1) { + if ($order['delivery_type'] == 'send') {// 配送 + $status['_type'] = 2; + $status['_title'] = '待收货'; + $status['_msg'] = date('m月d日H时i分', $statusServices->value(['oid' => $order['id'], 'change_type' => 'delivery'], 'change_time')) . '服务商已发货'; + $status['_class'] = 'state-ysh'; + } elseif ($order['delivery_type'] == 'express') {// 发货 + $status['_type'] = 2; + $status['_title'] = '待收货'; + $status['_msg'] = date('m月d日H时i分', $statusServices->value(['oid' => $order['id'], 'change_type' => 'delivery_goods'], 'change_time')) . '服务商已发货'; + $status['_class'] = 'state-ysh'; + } elseif ($order['delivery_type'] == 'split') {//拆分发货 + $status['_type'] = 2; + $status['_title'] = '待收货'; + $status['_msg'] = date('m月d日H时i分', $statusServices->value(['oid' => $order['id'], 'change_type' => 'delivery_split'], 'change_time')) . '服务商已拆分多个包裹发货'; + $status['_class'] = 'state-ysh'; + } else { + $status['_type'] = 2; + $status['_title'] = '待收货'; + $status['_msg'] = date('m月d日H时i分', $statusServices->value(['oid' => $order['id'], 'change_type' => 'delivery_fictitious'], 'change_time')) . '服务商已虚拟发货'; + $status['_class'] = 'state-ysh'; + } + } else if ($order['status'] == 2) { + $status['_type'] = 3; + $status['_title'] = '待评价'; + $status['_msg'] = '已收货,快去评价一下吧'; + $status['_class'] = 'state-ypj'; + } else if ($order['status'] == 3) { + $status['_type'] = 4; + $status['_title'] = '交易完成'; + $status['_msg'] = '交易完成,感谢您的支持'; + $status['_class'] = 'state-ytk'; + } + if (isset($order['pay_type'])) + $status['_payType'] = ($status['_type'] ?? 0) == 0 ? '' : PayServices::PAY_TYPE[$order['pay_type']] ?? '其他方式'; + if (isset($order['delivery_type'])) + $status['_deliveryType'] = isset($this->deliveryType[$order['delivery_type']]) ? $this->deliveryType[$order['delivery_type']] : '其他方式'; + $order['_status'] = $status; + $order['_pay_time'] = isset($order['pay_time']) && $order['pay_time'] != null ? date('Y-m-d H:i:s', $order['pay_time']) : ''; + $order['_add_time'] = isset($order['add_time']) ? (strstr((string)$order['add_time'], '-') === false ? date('Y-m-d H:i:s', $order['add_time']) : $order['add_time']) : ''; + + $order['status_pic'] = ''; + //获取商品状态图片 + if ($isPic) { + try { + $order_details_images = sys_data('order_details_images') ?: []; + $order_details_images = array_combine(array_column($order_details_images, 'order_status'), $order_details_images); + $_type = $order['_status']['_type'] == 5 ? 2 : $order['_status']['_type']; + $order['status_pic'] = $order_details_images[$_type]['pic'] ?? ''; + } catch (\Throwable $e) { + } + } + $order['offlinePayStatus'] = (int)sys_config('offline_pay_status') ?? 2; + //自购返佣 + if ($order['uid'] == $order['spread_uid']) { + $order['spread_nickname'] = isset($order['spread_nickname']) ? $order['spread_nickname'] . '(自购)' : ''; + } + $order['longitude'] = $order['latitude'] = ''; + //处理地址定位 + if (isset($order['user_location']) && $order['user_location']) { + [$longitude, $latitude] = explode(' ', $order['user_location']); + $order['longitude'] = $longitude; + $order['latitude'] = $latitude; + } + return $order; + } + + /** + * 整理订单类型 + * @param $order + * @param bool $abridge + * @return string[] + */ + public function tidyOrderType($order, bool $abridge = false) + { + $pink_name = $color = ''; + if ($order && isset($order['type'])) { + switch ($order['type']) { + case 0://普通订单 + if ($order['shipping_type'] == 1) { + $pink_name = $abridge ? '普通' : '[普通订单]'; + $color = '#895612'; + } else if ($order['shipping_type'] == 2) { + $pink_name = $abridge ? '核销' : '[核销订单]'; + $color = '#8956E8'; + } else if ($order['shipping_type'] == 3) { + $pink_name = $abridge ? '分配' : '[分配订单]'; + $color = '#FFA21B'; + } else if ($order['shipping_type'] == 4) { + $pink_name = $abridge ? '收银' : '[收银订单]'; + $color = '#2EC479'; + } + break; + case 1://秒杀 + $pink_name = $abridge ? '秒杀' : '[秒杀订单]'; + $color = '#32c5e9'; + break; + case 2://砍价 + $pink_name = $abridge ? '砍价' : '[砍价订单]'; + $color = '#12c5e9'; + break; + case 3://拼团 + if (isset($order['pinkStatus'])) { + switch ($order['pinkStatus']) { + case 1: + $pink_name = $abridge ? '拼团' : '[拼团订单]正在进行中'; + $color = '#f00'; + break; + case 2: + $pink_name = $abridge ? '拼团' : '[拼团订单]已完成'; + $color = '#00f'; + break; + case 3: + $pink_name = $abridge ? '拼团' : '[拼团订单]未完成'; + $color = '#f0f'; + break; + default: + $pink_name = $abridge ? '拼团' : '[拼团订单]历史订单'; + $color = '#457856'; + break; + } + } else { + $pink_name = $abridge ? '拼团' : '[拼团订单]历史订单'; + $color = '#457856'; + } + break; + case 4://积分 + $pink_name = $abridge ? '积分' : '[积分订单]'; + $color = '#12c5e9'; + break; + case 5://套餐 + $pink_name = $abridge ? '优惠' : '[优惠套餐]'; + $color = '#12c5e9'; + break; + case 6://预售 + $pink_name = $abridge ? '预售' : '[预售订单]'; + $color = '#12c5e9'; + break; + case 7://新人礼 + $pink_name = $abridge ? '新人礼' : '[新人专享]'; + $color = '#12c5e9'; + break; + case 8://抽奖 + $pink_name = $abridge ? '抽奖' : '[抽奖订单]'; + $color = '#12c5e9'; + break; + case 9://拼单 + $pink_name = $abridge ? '拼单' : '[拼单订单]'; + $color = '#12c5e9'; + break; + case 10://桌码 + $pink_name = $abridge ? '桌码' : '[桌码订单]'; + $color = '#F5222D'; + break; + } + } + return [$pink_name, $color]; + } + + /** + * 数据转换 + * @param array $data + * @param bool $is_cart_info + * @return array|null + */ + public function tidyOrderList(array $data, bool $is_cart_info = true, bool $abridge = false) + { + if (!$data) { + return $data; + } + /** @var StoreOrderCartInfoServices $services */ + $services = app()->make(StoreOrderCartInfoServices::class); + foreach ($data as &$item) { + if ($is_cart_info) $item['_info'] = $services->getOrderCartInfo((int)$item['id']); + $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']); + $item['_refund_time'] = isset($item['refund_reason_time']) && $item['refund_reason_time'] ? date('Y-m-d H:i:s', $item['refund_reason_time']) : ''; + $item['_pay_time'] = isset($item['pay_time']) && $item['pay_time'] ? date('Y-m-d H:i:s', $item['pay_time']) : ''; + [$pink_name, $color] = $this->tidyOrderType($item, $abridge); + $item['pink_name'] = $pink_name; + $item['color'] = $color; + if ($item['paid'] == 1) { + switch ($item['pay_type']) { + case PayServices::WEIXIN_PAY: + $item['pay_type_name'] = '微信支付'; + break; + case PayServices::YUE_PAY: + $item['pay_type_name'] = '余额支付'; + break; + case PayServices::OFFLINE_PAY: + $item['pay_type_name'] = '线下支付'; + break; + case PayServices::ALIAPY_PAY: + $item['pay_type_name'] = '支付宝支付'; + break; + case PayServices::CASH_PAY: + $item['pay_type_name'] = '现金支付'; + break; + default: + $item['pay_type_name'] = '其他支付'; + break; + } + } else { + switch ($item['pay_type']) { + default: + $item['pay_type_name'] = '未支付'; + break; + case 'offline': + $item['pay_type_name'] = '线下支付'; + $item['pay_type_info'] = 1; + break; + } + } + $status_name = ['status_name' => '', 'pics' => []]; + if ($item['is_del'] || $item['is_system_del']) { + $status_name['status_name'] = '已删除'; + $item['_status'] = -1; + } else if ($item['paid'] == 0 && $item['status'] == 0) { + $status_name['status_name'] = '未支付'; + $item['_status'] = 1;//未支付 + } else if ($item['paid'] == 1 && $item['status'] == 4 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $status_name['status_name'] = '部分发货'; + $item['_status'] = 8;//已支付 部分发货 + } else if ($item['paid'] == 1 && $item['refund_status'] == 2) { + $status_name['status_name'] = '已退款'; + $item['_status'] = 7;//已支付 已退款 + } else if ($item['paid'] == 1 && $item['status'] == 5 && $item['refund_status'] == 0) { + $status_name['status_name'] = $item['shipping_type'] == 2 ? '部分核销' : '部分收货'; + $item['_status'] = 12;//已支付 部分核销 + } else if ($item['paid'] == 1 && $item['refund_status'] == 1) { + $item['_status'] = 3;//已支付 申请退款中 + $refundReasonTime = $item['refund_reason_time'] ? date('Y-m-d H:i', $item['refund_reason_time']) : ''; + $refundReasonWapImg = json_decode($item['refund_reason_wap_img'], true); + $refundReasonWapImg = $refundReasonWapImg ? $refundReasonWapImg : []; + $img = []; + if (count($refundReasonWapImg)) { + foreach ($refundReasonWapImg as $itemImg) { + if (strlen(trim($itemImg))) + $img[] = $itemImg; + } + } + $status_name['status_name'] = <<申请退款
+退款原因:{$item['refund_reason_wap']}
+备注说明:{$item['refund_reason_wap_explain']}
+退款时间:{$refundReasonTime}
+退款凭证: +HTML; + $status_name['pics'] = $img; + } else if ($item['paid'] == 1 && $item['refund_status'] == 4) { + $item['_status'] = 10;//拆单发货 已全部申请退款 + $status_name['status_name'] = '退款中'; + } else if ($item['paid'] == 1 && $item['status'] == 0 && in_array($item['shipping_type'], [1, 3, 4]) && $item['refund_status'] == 0) { + $status_name['status_name'] = '未发货'; + $item['_status'] = 2;//已支付 未发货 + } else if ($item['paid'] == 1 && in_array($item['status'], [0, 1]) && $item['shipping_type'] == 2 && $item['refund_status'] == 0) { + $status_name['status_name'] = '未核销'; + $item['_status'] = 11;//已支付 待核销 + } else if ($item['paid'] == 1 && in_array($item['status'], [1, 5]) && in_array($item['shipping_type'], [1, 3, 4]) && $item['refund_status'] == 0) { + $status_name['status_name'] = '待收货'; + $item['_status'] = 4;//已支付 待收货 + } else if ($item['paid'] == 1 && $item['status'] == 2 && $item['refund_status'] == 0) { + $status_name['status_name'] = '待评价'; + $item['_status'] = 5;//已支付 待评价 + } else if ($item['paid'] == 1 && $item['status'] == 3 && $item['refund_status'] == 0) { + $status_name['status_name'] = '已完成'; + $item['_status'] = 6;//已支付 已完成 + } else if ($item['paid'] == 1 && $item['refund_status'] == 3) { + $item['_status'] = 9;//拆单发货 部分申请退款 + $status_name['status_name'] = '部分退款'; + } + $item['status_name'] = $status_name; + if ($item['store_id'] == 0 && $item['clerk_id'] == 0 && !isset($item['clerk_name'])) { + $item['clerk_name'] = '总平台'; + } + //根据核销员更改store_name + if ($item['clerk_id'] && isset($item['staff_store_id']) && $item['staff_store_id']) { + /** @var SystemStoreServices $store */ + $store = app()->make(SystemStoreServices::class); + $storeOne = $store->value(['id' => $item['staff_store_id']], 'name'); + if ($storeOne) $item['store_name'] = $storeOne; + } + //自购返佣 + if ($item['uid'] == $item['spread_uid']) { + $item['spread_nickname'] = isset($item['spread_nickname']) ? $item['spread_nickname'] . '(自购)' : ''; + } + } + return $data; + } + + /** + * 处理订单金额 + * @param $where + * @return array + */ + public function getOrderPrice($where) + { +// $where['pid'] = 0;//子订单不统计 + $whereData = []; + $price['today_count_sum'] = 0; //今日订单总数 + $price['count_sum'] = 0; //订单总数 + $price['pay_price'] = 0;//支付金额 + $price['today_pay_price'] = 0;//今日支付金额 + if ($where['status'] == '') { + $whereData['paid'] = 1; + $whereData['refund_status'] = [0, 3]; + } + $not_pid = $where; + unset($not_pid['pid']); + $not_pid['not_pid'] = 1; + $sumNumber = $this->dao->search($where + $whereData)->field([ + 'count(id) as count_sum', + ])->find(); + $price['count_sum'] = $sumNumber && $sumNumber['count_sum'] ? $sumNumber['count_sum'] : 0; + $sumNumber = $this->dao->search($whereData + $where)->field([ + 'sum(pay_price) as sum_pay_price', + ])->find(); + $price['pay_price'] = $sumNumber && $sumNumber['sum_pay_price'] ? $sumNumber['sum_pay_price'] : 0; + $where['time'] = 'today'; + $not_pid['time'] = 'today'; + $sumNumber = $this->dao->search($where + $whereData)->field([ + 'count(id) as today_count_sum', + ])->find(); + $price['today_count_sum'] = $sumNumber && $sumNumber['today_count_sum'] ? $sumNumber['today_count_sum'] : 0; + $sumNumber = $this->dao->search($whereData + $where + ['paid' => 1])->field([ + 'sum(pay_price) as today_pay_price', + ])->find(); + $price['today_pay_price'] = $sumNumber && $sumNumber['today_pay_price'] ? $sumNumber['today_pay_price'] : 0; + return $price; + } + + /** + * 获取订单列表页面统计数据 + * @param $where + * @return array + */ + public function getBadge($where) + { + $price = $this->getOrderPrice($where); + return [ + [ + 'name' => '订单数量', + 'field' => '件', + 'count' => $price['count_sum'], + 'className' => 'md-basket', + 'col' => 6 + ], + [ + 'name' => '订单金额', + 'field' => '元', + 'count' => $price['pay_price'], + 'className' => 'md-pricetags', + 'col' => 6 + ], + [ + 'name' => '今日订单数量', + 'field' => '件', + 'count' => $price['today_count_sum'], + 'className' => 'ios-chatbubbles', + 'col' => 6 + ], + [ + 'name' => '今日支付金额', + 'field' => '元', + 'count' => $price['today_pay_price'], + 'className' => 'ios-cash', + 'col' => 6 + ], + ]; + } + + /** + * + * @param array $where + * @return mixed + */ + public function orderStoreCount(array $where) + { + $defaultWhere = ['time' => $where['time'], 'store_id' => $where['store_id'] ?? 0, 'is_system_del' => 0]; + if (isset($where['store_id'])) { + $defaultWhere['store_id'] = $where['store_id']; + } + //全部订单 + $data['all'] = (string)$this->dao->count(['pid' => 0] + $defaultWhere); + //普通订单 + $data['general'] = (string)$this->dao->count(['pid' => 0, 'type' => 0] + $defaultWhere); + //拼团订单 + $data['pink'] = (string)$this->dao->count(['pid' => 0, 'type' => 3] + $defaultWhere); + //秒杀订单 + $data['seckill'] = (string)$this->dao->count(['pid' => 0, 'type' => 1] + $defaultWhere); + //砍价订单 + $data['bargain'] = (string)$this->dao->count(['pid' => 0, 'type' => 2] + $defaultWhere); + //收银订单 + $data['cashier'] = (string)$this->dao->count(['pid' => 0, 'type' => 6] + $defaultWhere); + $data['write_off'] = (string)$this->dao->count(['pid' => 0, 'type' => 5] + $defaultWhere); + $data['delivery'] = (string)$this->dao->count(['pid' => 0, 'type' => 7] + $defaultWhere); + //预售订单 + $data['presale'] = (string)$this->dao->count(['pid' => 0, 'type' => 8] + $defaultWhere); + + switch ($where['type']) { + case 0: + $data['statusAll'] = $data['general']; + break; + case 1: + $data['statusAll'] = $data['seckill']; + break; + case 2: + $data['statusAll'] = $data['bargain']; + break; + case 3: + $data['statusAll'] = $data['pink']; + break; + case 4: + break; + case 5: + $data['statusAll'] = $data['write_off']; + break; + case 6: + $data['statusAll'] = $data['cashier']; + break; + case 7: + $data['statusAll'] = $data['delivery']; + break; + case 8: + $data['statusAll'] = $data['presale']; + break; + default: + $data['statusAll'] = $data['all']; + } + $count_where = ['pid' => 0, 'type' => $where['type']] + $defaultWhere; + //未支付 + $data['unpaid'] = (string)$this->dao->count($count_where + ['status' => 0]); + //未发货 + $data['unshipped'] = (string)$this->dao->count($count_where + ['status' => 1]); + //部分发货 + $data['partshipped'] = (string)$this->dao->count($count_where + ['status' => 7]); + //待收货 + $data['untake'] = (string)$this->dao->count($count_where + ['status' => 2]); + //待核销 + $data['write_off'] = (string)$this->dao->count($count_where + ['status' => 5]); + //已核销 + $data['write_offed'] = (string)$this->dao->count($count_where + ['status' => 6]); + //待评价 + $data['unevaluate'] = (string)$this->dao->count($count_where + ['status' => 3]); + //交易完成 + $data['complete'] = (string)$this->dao->count($count_where + ['status' => 4]); + //退款中 +// $data['refunding'] = (string)$this->dao->count(['status' => -1, 'time' => $where['time'], 'is_system_del' => 0, 'type' => $where['type']]); + //已退款 +// $data['refund'] = (string)$this->dao->count(['status' => -2, 'time' => $where['time'], 'is_system_del' => 0, 'type' => $where['type']]); + //删除订单 + $data['del'] = (string)$this->dao->count($count_where + ['status' => -4]); + return $data; + } + + /** + * + * @param array $where + * @return mixed + */ + public function orderCount(array $where) + { + $default_where = [ + 'pid' => [0, -1], + 'time' => $where['time'], + 'status' => $where['status'], + 'pay_type' => $where['pay_type'], + 'field_key' => $where['field_key'], + 'real_name' => $where['real_name'], + 'is_system_del' => 0 + ]; + $count_where = ['type' => $where['type'] ?? 0, 'store_id' => $where['store_id'] ?? 0, 'supplier_id' => $where['supplier_id'] ?? 0]; +// if ($count_where['store_id'] || $count_where['supplier_id'] || (isset($where['plat_type']) && in_array($where['plat_type'], [1, 2]))) { +// $default_where['pid'] = 0; +// } + //全部订单 + $data['all'] = (string)$this->dao->count($default_where + $count_where); + //普通订单 + $data['general'] = (string)$this->dao->count(['type' => 0] + $default_where + $count_where); + //平台订单 + $data['plat'] = (string)$this->dao->count(['plat_type' => 0, 'pid' => 0] + $default_where + $count_where); + //门店订单 + $data['store'] = (string)$this->dao->count(['plat_type' => 1, 'pid' => 0] + $default_where + $count_where); + //供应商订单 + $data['supplier'] = (string)$this->dao->count(['plat_type' => 2, 'pid' => 0] + $default_where + $count_where); + //拼团订单 + $data['pink'] = (string)$this->dao->count(['type' => 3] + $default_where); + //秒杀订单 + $data['seckill'] = (string)$this->dao->count(['type' => 1] + $default_where); + //砍价订单 + $data['bargain'] = (string)$this->dao->count(['type' => 2] + $default_where); + //预售订单 + $data['presale'] = (string)$this->dao->count(['type' => 8] + $default_where); + + $data['statusAll'] = $data['all']; + if (trim($where['type'], ' ') !== '') { + switch ($where['type']) { + case 0: + $data['statusAll'] = $data['general']; + break; + case 1: + $data['statusAll'] = $data['seckill']; + break; + case 2: + $data['statusAll'] = $data['bargain']; + break; + case 3: + $data['statusAll'] = $data['pink']; + break; + case 4: + break; + case 8: + $data['statusAll'] = $data['presale']; + break; + default: + $data['statusAll'] = $data['all']; + } + } + $count_where = ['type' => $where['type']] + $default_where; + //未支付 + $data['unpaid'] = (string)$this->dao->count($count_where + ['status' => 0]); + //未发货 + $data['unshipped'] = (string)$this->dao->count($count_where + ['status' => 1, 'shipping_type' => 1]); + //部分发货 + $data['partshipped'] = (string)$this->dao->count($count_where + ['status' => 7, 'shipping_type' => 1]); + //待收货 + $data['untake'] = (string)$this->dao->count($count_where + ['status' => 2, 'shipping_type' => 1]); + //待核销 + $data['write_off'] = (string)$this->dao->count($count_where + ['status' => 5]); + //已核销 + $data['write_offed'] = (string)$this->dao->count($count_where + ['status' => 6]); + //待评价 + $data['unevaluate'] = (string)$this->dao->count($count_where + ['status' => 3]); + //交易完成 + $data['complete'] = (string)$this->dao->count($count_where + ['status' => 4]); + //退款中 +// $data['refunding'] = (string)$this->dao->count(['status' => -1, 'time' => $where['time'], 'is_system_del' => 0, 'type' => $where['type']]); + //已退款 +// $data['refund'] = (string)$this->dao->count(['status' => -2, 'time' => $where['time'], 'is_system_del' => 0, 'type' => $where['type']]); + //删除订单 + $data['del'] = (string)$this->dao->count($count_where + ['status' => -4]); + return $data; + } + + /** + * 创建修改订单表单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function updateForm(int $id) + { + $product = $this->dao->get($id); + if (!$product) { + throw new ValidateException('Data does not exist!'); + } + $f = []; + $f[] = Form::input('order_id', '订单编号', $product->getData('order_id'))->disabled(true); + $f[] = Form::number('total_price', '商品总价', (float)$product->getData('total_price'))->min(0)->disabled(true); + $f[] = Form::number('total_postage', '原始邮费', (float)$product->getData('total_postage'))->min(0)->disabled(true); + $f[] = Form::number('pay_postage', '实际支付邮费', (float)$product->getData('pay_postage') ?: 0)->disabled(true); + $f[] = Form::number('pay_price', '实际支付金额', (float)$product->getData('pay_price'))->min(0); + $f[] = Form::number('gain_integral', '赠送积分', (float)$product->getData('gain_integral') ?: 0)->precision(0); + return create_form('修改订单', $f, $this->url('/order/update/' . $id), 'PUT'); + } + + /** + * 修改订单 + * @param int $id + * @param array $data + * @return mixed + * @throws \Exception + */ + public function updateOrder(int $id, array $data) + { + $order = $this->dao->getOne(['id' => $id, 'is_del' => 0]); + if (!$order) { + throw new ValidateException('订单不存在或已删除'); + } + if ($order['paid']) { + throw new ValidateException('订单已支付'); + } + //限制改价金额两位小数 + $data['pay_price'] = sprintf("%.2f", $data['pay_price']); + $pay_price = $order['pay_price']; + if ($order['change_price']) {//已经改过一次价 + $pay_price = bcadd((string)$pay_price, (string)$order['change_price'], 2); + } + //记录改价优惠金额 + $data['change_price'] = (float)bcsub((string)$pay_price, (string)($data['pay_price'] ?? 0), 2); + + /** @var StoreOrderStatusServices $services */ + $services = app()->make(StoreOrderStatusServices::class); + $this->transaction(function () use ($id, $order, $data, $services) { + $res = $this->dao->update($id, $data); + $res = $res && $services->save([ + 'oid' => $id, + 'change_type' => 'order_edit', + 'change_time' => time(), + 'change_message' => '商品总价为:' . $order['pay_price'] . ' 修改实际支付金额为:' . $data['pay_price'] + ]); + if ($res) { + return true; + } else { + throw new ValidateException('Modification failed'); + } + }); + //新订单号 + // $order['order_id'] = $data['order_id']; + //改价提醒 + event('order.price', [$order, $data['pay_price']]); + return true; + } + + /** + * 订单图表 + * @param $cycle + * @return array + */ + public function orderCharts($cycle) + { + $datalist = []; + $where = []; + $series1 = ['normal' => ['color' => [ + 'x' => 0, 'y' => 0, 'x2' => 0, 'y2' => 1, + 'colorStops' => [ + [ + 'offset' => 0, + 'color' => '#69cdff' + ], + [ + 'offset' => 0.5, + 'color' => '#3eb3f7' + ], + [ + 'offset' => 1, + 'color' => '#1495eb' + ] + ] + ]] + ]; + $series2 = ['normal' => ['color' => [ + 'x' => 0, 'y' => 0, 'x2' => 0, 'y2' => 1, + 'colorStops' => [ + [ + 'offset' => 0, + 'color' => '#6fdeab' + ], + [ + 'offset' => 0.5, + 'color' => '#44d693' + ], + [ + 'offset' => 1, + 'color' => '#2cc981' + ] + ] + ]] + ]; + $chartdata = []; + $data = [];//临时 + $chartdata['yAxis']['maxnum'] = 0;//最大值数量 + $chartdata['yAxis']['maxprice'] = 0;//最大值金额 + switch ($cycle) { + case 'thirtyday': + //上期 + $datebefor = date('Y-m-d 00:00:00', strtotime('-59 day')); + $dateafter = date('Y-m-d 23:59:59', strtotime('-29 day')); + //当前 + $now_datebefor = date('Y-m-d 00:00:00', strtotime('-29 day')); + $now_dateafter = date('Y-m-d 23:59:59'); + for ($i = -29; $i <= 0; $i++) { + $datalist[date('Y-m-d', strtotime($i . ' day'))] = date('Y-m-d', strtotime($i . ' day')); + } + $order_list = $this->dao->orderAddTimeList($where, [$now_datebefor, $now_dateafter], 'day'); + if (empty($order_list)) return ['yAxis' => [], 'legend' => [], 'xAxis' => [], 'serise' => [], 'pre_cycle' => [], 'cycle' => []]; + $order_list = array_combine(array_column($order_list, 'day'), $order_list); + $cycle_list = []; + foreach ($datalist as $dk => $dd) { + if (isset($order_list[$dk]) && !empty($order_list[$dd])) { + $cycle_list[$dd] = $order_list[$dd]; + } else { + $cycle_list[$dd] = ['count' => 0, 'day' => $dd, 'price' => '']; + } + } + foreach ($cycle_list as $k => $v) { + $data['day'][] = $v['day']; + $data['count'][] = $v['count']; + $data['price'][] = round($v['price'], 2); + if ($chartdata['yAxis']['maxnum'] < $v['count']) + $chartdata['yAxis']['maxnum'] = $v['count'];//日最大订单数 + if ($chartdata['yAxis']['maxprice'] < $v['price']) + $chartdata['yAxis']['maxprice'] = $v['price'];//日最大金额 + } + $chartdata['legend'] = ['订单金额', '订单数'];//分类 + $chartdata['xAxis'] = $data['day'];//X轴值 + $chartdata['series'][] = ['name' => $chartdata['legend'][0], 'type' => 'bar', 'itemStyle' => $series1, 'data' => $data['price']];//分类1值 + $chartdata['series'][] = ['name' => $chartdata['legend'][1], 'type' => 'line', 'itemStyle' => $series2, 'data' => $data['count'], 'yAxisIndex' => 1];//分类2值 + break; + case 'week': + $weekarray = [['周日'], ['周一'], ['周二'], ['周三'], ['周四'], ['周五'], ['周六']]; + $datebefor = date('Y-m-d 00:00:00', strtotime('-1 week Monday')); + $dateafter = date('Y-m-d 23:59:59', strtotime('-1 week Sunday')); + $order_list = $this->dao->orderAddTimeList($where, [$datebefor, $dateafter], 'week'); + //数据查询重新处理 + $new_order_list = array_combine(array_column($order_list, 'day'), $order_list); + $now_datebefor = date('Y-m-d 00:00:00', (time() - ((date('w') == 0 ? 7 : date('w')) - 1) * 24 * 3600)); + $now_dateafter = date('Y-m-d 23:59:59', strtotime("+1 day")); + $now_order_list = $this->dao->orderAddTimeList($where, [$now_datebefor, $now_dateafter], 'week'); + //数据查询重新处理 key 变为当前值 + $new_now_order_list = array_combine(array_column($now_order_list, 'day'), $now_order_list); + foreach ($weekarray as $dk => $dd) { + if (isset($new_order_list[$dk]) && !empty($new_order_list[$dk])) { + $weekarray[$dk]['pre'] = $new_order_list[$dk]; + } else { + $weekarray[$dk]['pre'] = ['count' => 0, 'day' => $weekarray[$dk][0], 'price' => '0']; + } + if (isset($new_now_order_list[$dk]) && !empty($new_now_order_list[$dk])) { + $weekarray[$dk]['now'] = $new_now_order_list[$dk]; + } else { + $weekarray[$dk]['now'] = ['count' => 0, 'day' => $weekarray[$dk][0], 'price' => '0']; + } + } + foreach ($weekarray as $k => $v) { + $data['day'][] = $v[0]; + $data['pre']['count'][] = $v['pre']['count']; + $data['pre']['price'][] = round($v['pre']['price'], 2); + $data['now']['count'][] = $v['now']['count']; + $data['now']['price'][] = round($v['now']['price'], 2); + if ($chartdata['yAxis']['maxnum'] < $v['pre']['count'] || $chartdata['yAxis']['maxnum'] < $v['now']['count']) { + $chartdata['yAxis']['maxnum'] = $v['pre']['count'] > $v['now']['count'] ? $v['pre']['count'] : $v['now']['count'];//日最大订单数 + } + if ($chartdata['yAxis']['maxprice'] < $v['pre']['price'] || $chartdata['yAxis']['maxprice'] < $v['now']['price']) { + $chartdata['yAxis']['maxprice'] = $v['pre']['price'] > $v['now']['price'] ? $v['pre']['price'] : $v['now']['price'];//日最大金额 + } + } + $chartdata['legend'] = ['上周金额', '本周金额', '上周订单数', '本周订单数'];//分类 + $chartdata['xAxis'] = $data['day'];//X轴值 + $chartdata['series'][] = ['name' => $chartdata['legend'][0], 'type' => 'bar', 'itemStyle' => $series1, 'data' => $data['pre']['price']];//分类1值 + $chartdata['series'][] = ['name' => $chartdata['legend'][1], 'type' => 'bar', 'itemStyle' => $series1, 'data' => $data['now']['price']];//分类1值 + $chartdata['series'][] = ['name' => $chartdata['legend'][2], 'type' => 'line', 'itemStyle' => $series2, 'data' => $data['pre']['count'], 'yAxisIndex' => 1];//分类2值 + $chartdata['series'][] = ['name' => $chartdata['legend'][3], 'type' => 'line', 'itemStyle' => $series2, 'data' => $data['now']['count'], 'yAxisIndex' => 1];//分类2值 + break; + case 'month': + $weekarray = ['01' => ['1'], '02' => ['2'], '03' => ['3'], '04' => ['4'], '05' => ['5'], '06' => ['6'], '07' => ['7'], '08' => ['8'], '09' => ['9'], '10' => ['10'], '11' => ['11'], '12' => ['12'], '13' => ['13'], '14' => ['14'], '15' => ['15'], '16' => ['16'], '17' => ['17'], '18' => ['18'], '19' => ['19'], '20' => ['20'], '21' => ['21'], '22' => ['22'], '23' => ['23'], '24' => ['24'], '25' => ['25'], '26' => ['26'], '27' => ['27'], '28' => ['28'], '29' => ['29'], '30' => ['30'], '31' => ['31']]; + $datebefor = date('Y-m-01 00:00:00', strtotime('-1 month')); + $dateafter = date('Y-m-d 23:59:59', strtotime(date('Y-m-01'))); + $order_list = $this->dao->orderAddTimeList($where, [$datebefor, $dateafter], "month"); + //数据查询重新处理 + $new_order_list = array_combine(array_column($order_list, 'day'), $order_list); + $now_datebefor = date('Y-m-01 00:00:00'); + $now_dateafter = date('Y-m-d', strtotime("+1 day")); + $now_order_list = $this->dao->orderAddTimeList($where, [$now_datebefor, $now_dateafter], "month"); + //数据查询重新处理 key 变为当前值 + $new_now_order_list = array_combine(array_column($now_order_list, 'day'), $now_order_list); + foreach ($weekarray as $dk => $dd) { + if (isset($new_order_list[$dk]) && !empty($new_order_list[$dk])) { + $weekarray[$dk]['pre'] = $new_order_list[$dk]; + } else { + $weekarray[$dk]['pre'] = ['count' => 0, 'day' => $weekarray[$dk][0], 'price' => '0']; + } + if (isset($new_now_order_list[$dk]) && !empty($new_now_order_list[$dk])) { + $weekarray[$dk]['now'] = $new_now_order_list[$dk]; + } else { + $weekarray[$dk]['now'] = ['count' => 0, 'day' => $weekarray[$dk][0], 'price' => '0']; + } + } + foreach ($weekarray as $k => $v) { + $data['day'][] = $v[0]; + $data['pre']['count'][] = $v['pre']['count']; + $data['pre']['price'][] = round($v['pre']['price'], 2); + $data['now']['count'][] = $v['now']['count']; + $data['now']['price'][] = round($v['now']['price'], 2); + if ($chartdata['yAxis']['maxnum'] < $v['pre']['count'] || $chartdata['yAxis']['maxnum'] < $v['now']['count']) { + $chartdata['yAxis']['maxnum'] = $v['pre']['count'] > $v['now']['count'] ? $v['pre']['count'] : $v['now']['count'];//日最大订单数 + } + if ($chartdata['yAxis']['maxprice'] < $v['pre']['price'] || $chartdata['yAxis']['maxprice'] < $v['now']['price']) { + $chartdata['yAxis']['maxprice'] = $v['pre']['price'] > $v['now']['price'] ? $v['pre']['price'] : $v['now']['price'];//日最大金额 + } + } + $chartdata['legend'] = ['上月金额', '本月金额', '上月订单数', '本月订单数'];//分类 + $chartdata['xAxis'] = $data['day'];//X轴值 + $chartdata['series'][] = ['name' => $chartdata['legend'][0], 'type' => 'bar', 'itemStyle' => $series1, 'data' => $data['pre']['price']];//分类1值 + $chartdata['series'][] = ['name' => $chartdata['legend'][1], 'type' => 'bar', 'itemStyle' => $series1, 'data' => $data['now']['price']];//分类1值 + $chartdata['series'][] = ['name' => $chartdata['legend'][2], 'type' => 'line', 'itemStyle' => $series2, 'data' => $data['pre']['count'], 'yAxisIndex' => 1];//分类2值 + $chartdata['series'][] = ['name' => $chartdata['legend'][3], 'type' => 'line', 'itemStyle' => $series2, 'data' => $data['now']['count'], 'yAxisIndex' => 1];//分类2值 + break; + case 'year': + $weekarray = ['01' => ['一月'], '02' => ['二月'], '03' => ['三月'], '04' => ['四月'], '05' => ['五月'], '06' => ['六月'], '07' => ['七月'], '08' => ['八月'], '09' => ['九月'], '10' => ['十月'], '11' => ['十一月'], '12' => ['十二月']]; + $datebefor = date('Y-01-01 00:00:00', strtotime('-1 year')); + $dateafter = date('Y-12-31 23:59:59', strtotime('-1 year')); + $order_list = $this->dao->orderAddTimeList($where, [$datebefor, $dateafter], 'year'); + //数据查询重新处理 + $new_order_list = array_combine(array_column($order_list, 'day'), $order_list); + $now_datebefor = date('Y-01-01 00:00:00'); + $now_dateafter = date('Y-12-31 23:59:59'); + $now_order_list = $this->dao->orderAddTimeList($where, [$now_datebefor, $now_dateafter], 'year'); + //数据查询重新处理 key 变为当前值 + $new_now_order_list = array_combine(array_column($now_order_list, 'day'), $now_order_list); + $y = date('Y'); + foreach ($weekarray as $dk => $dd) { + $order_dk = $y . '-' . $dk; + if (isset($new_order_list[$order_dk]) && !empty($new_order_list[$order_dk])) { + $weekarray[$dk]['pre'] = $new_order_list[$order_dk]; + } else { + $weekarray[$dk]['pre'] = ['count' => 0, 'day' => $weekarray[$dk][0], 'price' => '0']; + } + if (isset($new_now_order_list[$order_dk]) && !empty($new_now_order_list[$order_dk])) { + $weekarray[$dk]['now'] = $new_now_order_list[$order_dk]; + } else { + $weekarray[$dk]['now'] = ['count' => 0, 'day' => $weekarray[$dk][0], 'price' => '0']; + } + } + foreach ($weekarray as $k => $v) { + $data['day'][] = $v[0]; + $data['pre']['count'][] = $v['pre']['count']; + $data['pre']['price'][] = round($v['pre']['price'], 2); + $data['now']['count'][] = $v['now']['count']; + $data['now']['price'][] = round($v['now']['price'], 2); + if ($chartdata['yAxis']['maxnum'] < $v['pre']['count'] || $chartdata['yAxis']['maxnum'] < $v['now']['count']) { + $chartdata['yAxis']['maxnum'] = $v['pre']['count'] > $v['now']['count'] ? $v['pre']['count'] : $v['now']['count'];//日最大订单数 + } + if ($chartdata['yAxis']['maxprice'] < $v['pre']['price'] || $chartdata['yAxis']['maxprice'] < $v['now']['price']) { + $chartdata['yAxis']['maxprice'] = $v['pre']['price'] > $v['now']['price'] ? $v['pre']['price'] : $v['now']['price'];//日最大金额 + } + } + $chartdata['legend'] = ['去年金额', '今年金额', '去年订单数', '今年订单数'];//分类 + $chartdata['xAxis'] = $data['day'];//X轴值 + $chartdata['series'][] = ['name' => $chartdata['legend'][0], 'type' => 'bar', 'itemStyle' => $series1, 'data' => $data['pre']['price']];//分类1值 + $chartdata['series'][] = ['name' => $chartdata['legend'][1], 'type' => 'bar', 'itemStyle' => $series1, 'data' => $data['now']['price']];//分类1值 + $chartdata['series'][] = ['name' => $chartdata['legend'][2], 'type' => 'line', 'itemStyle' => $series2, 'data' => $data['pre']['count'], 'yAxisIndex' => 1];//分类2值 + $chartdata['series'][] = ['name' => $chartdata['legend'][3], 'type' => 'line', 'itemStyle' => $series2, 'data' => $data['now']['count'], 'yAxisIndex' => 1];//分类2值 + break; + default: + break; + } + //统计总数上期 + $pre_total = $this->dao->preTotalFind($where, [$datebefor, $dateafter]); + if ($pre_total) { + $chartdata['pre_cycle']['count'] = [ + 'data' => $pre_total['count'] ?: 0 + ]; + $chartdata['pre_cycle']['price'] = [ + 'data' => $pre_total['price'] ?: 0 + ]; + } + //统计总数 + $total = $this->dao->preTotalFind($where, [$now_datebefor, $now_dateafter]); + if ($total) { + $cha_count = intval($pre_total['count']) - intval($total['count']); + $pre_total['count'] = $pre_total['count'] == 0 ? 1 : $pre_total['count']; + $chartdata['cycle']['count'] = [ + 'data' => $total['count'] ?: 0, + 'percent' => round((abs($cha_count) / intval($pre_total['count']) * 100), 2), + 'is_plus' => $cha_count > 0 ? -1 : ($cha_count == 0 ? 0 : 1) + ]; + $cha_price = round($pre_total['price'], 2) - round($total['price'], 2); + $pre_total['price'] = $pre_total['price'] == 0 ? 1 : $pre_total['price']; + $chartdata['cycle']['price'] = [ + 'data' => $total['price'] ?: 0, + 'percent' => round(abs($cha_price) / $pre_total['price'] * 100, 2), + 'is_plus' => $cha_price > 0 ? -1 : ($cha_price == 0 ? 0 : 1) + ]; + } + return $chartdata; + } + + /** + * 获取订单数量 + * @param int $store_id + * @param int $type + * @param string $field + * @return int + */ + public function storeOrderCount(int $store_id = 0, int $type = -1, string $field = 'store_id') + { + return $this->dao->storeOrderCount($store_id, $type, $field); + } + + /** + * 首页头部统计 + * @return array + */ + public function homeStatics() + { + /** @var UserServices $uSercice */ + $uSercice = app()->make(UserServices::class); + /** @var StoreProductLogServices $productLogServices */ + $productLogServices = app()->make(StoreProductLogServices::class); + // 销售额 + //今日销售额 + $today_sales = $this->dao->totalSales('today'); + //昨日销售额 + $yesterday_sales = $this->dao->totalSales('yesterday'); + //日同比 + $sales_today_ratio = $this->countRate($today_sales, $yesterday_sales); + //周销售额 +// //本周 +// $this_week_sales = $this->dao->totalSales('week'); +// //上周 +// $last_week_sales = $this->dao->totalSales('last week'); +// //周同比 +// $sales_week_ratio = $this->countRate($this_week_sales, $last_week_sales); + //总销售额 + $total_sales = $this->dao->totalSales('month'); + $sales = [ + 'today' => $today_sales, + 'yesterday' => $yesterday_sales, + 'today_ratio' => $sales_today_ratio, +// 'week' => $this_week_sales, +// 'last_week' => $last_week_sales, +// 'week_ratio' => $sales_week_ratio, + 'total' => $total_sales . '元', + 'date' => '今日' + ]; + //用户访问量 + //今日访问量 + $today_visits = $productLogServices->count(['time' => 'today', 'type' => 'visit']); + //昨日访问量 + $yesterday_visits = $productLogServices->count(['time' => 'yesterday', 'type' => 'visit']); + //日同比 + $visits_today_ratio = $this->countRate($today_visits, $yesterday_visits); +// //本周访问量 +// $this_week_visits = $productLogServices->count(['time' => 'week', 'type' => 'visit']); +// //上周访问量 +// $last_week_visits = $productLogServices->count(['time' => 'last week', 'type' => 'visit']); +// //周同比 +// $visits_week_ratio = $this->countRate($this_week_visits, $last_week_visits); + //总访问量 + $total_visits = $productLogServices->count(['time' => 'month', 'type' => 'visit']); + $visits = [ + 'today' => $today_visits, + 'yesterday' => $yesterday_visits, + 'today_ratio' => $visits_today_ratio, +// 'week' => $this_week_visits, +// 'last_week' => $last_week_visits, +// 'week_ratio' => $visits_week_ratio, + 'total' => $total_visits . 'Pv', + 'date' => '今日' + ]; + // 订单量 + //今日订单量 + $today_order = $this->dao->totalOrderCount('today'); + //昨日订单量 + $yesterday_order = $this->dao->totalOrderCount('yesterday'); + //订单日同比 + $order_today_ratio = $this->countRate($today_order, $yesterday_order); +// //本周订单量 +// $this_week_order = $this->dao->totalOrderCount('week'); +// //上周订单量 +// $last_week_order = $this->dao->totalOrderCount('last week'); +// //订单周同比 +// $order_week_ratio = $this->countRate($this_week_order, $last_week_order); + //总订单量 + $total_order = $this->dao->totalOrderCount('month'); + $order = [ + 'today' => $today_order, + 'yesterday' => $yesterday_order, + 'today_ratio' => $order_today_ratio, +// 'week' => $this_week_order, +// 'last_week' => $last_week_order, +// 'week_ratio' => $order_week_ratio, + 'total' => $total_order . '单', + 'date' => '今日' + ]; + // 用户 + //今日新增用户 + $today_user = $uSercice->totalUserCount('today'); + //昨日新增用户 + $yesterday_user = $uSercice->totalUserCount('yesterday'); + //新增用户日同比 + $user_today_ratio = $this->countRate($today_user, $yesterday_user); +// //本周新增用户 +// $this_week_user = $uSercice->totalUserCount('week'); +// //上周新增用户 +// $last_week_user = $uSercice->totalUserCount('last week'); +// //新增用户周同比 +// $user_week_ratio = $this->countRate($this_week_user, $last_week_user); + //本月新增用户 + $total_user = $uSercice->totalUserCount('month'); + $user = [ + 'today' => $today_user, + 'yesterday' => $yesterday_user, + 'today_ratio' => $user_today_ratio, +// 'week' => $this_week_user, +// 'last_week' => $last_week_user, +// 'week_ratio' => $user_week_ratio, + 'total' => $total_user . '人', + 'date' => '今日' + ]; + $info = array_values(compact('sales', 'visits', 'order', 'user')); + $info[0]['title'] = '销售额'; + $info[1]['title'] = '用户访问量'; + $info[2]['title'] = '订单量'; + $info[3]['title'] = '新增用户'; + $info[0]['total_name'] = '本月销售额'; + $info[1]['total_name'] = '本月访问量'; + $info[2]['total_name'] = '本月订单量'; + $info[3]['total_name'] = '本月新增用户'; + return $info; + } + + /** + * 打印订单 + * @param int $id + * @param bool $isTable + * @return bool + */ + public function orderPrint(int $id, int $type = -1, int $relation_id = -1, bool $isTable = false) + { + $order = $this->dao->get($id); + if (!$order) { + throw new ValidateException('订单信息不存在!'); + } + $order = $order->toArray(); + /** @var StoreOrderCartInfoServices $cartServices */ + $cartServices = app()->make(StoreOrderCartInfoServices::class); + $product = $cartServices->getCartInfoPrintProduct((int)$order['id']); + if (!$product) { + throw new ValidateException('订单商品获取失败,无法打印!'); + } + if ($type == -1 && $relation_id == -1) {//取订单属于那一端 + $type = $relation_id = 0; + if (isset($order['store_id']) && $order['store_id']) { + $store_id = (int)$order['store_id']; + if ($isTable) { + if (isset($order['type']) && $order['type'] == 10) { + $print = store_config($store_id, 'store_printing_timing'); + if(!$print || !is_array($print) || !in_array(2, $print)){ + return true; + } + } + } + $type = 1; + $relation_id = $store_id; + } elseif (isset($order['supplier_id']) && $order['supplier_id']) { + $supplier_id = (int)$order['supplier_id']; + $type = 2; + $relation_id = $supplier_id; + } + } + + /** @var ConfigServices $configServices */ + $configServices = app()->make(ConfigServices::class); + [$switch, $name, $configData] = $configServices->getPrintingConfig($type, $relation_id); + if (!$switch) { + throw new ValidateException('请先开启小票打印'); + } + foreach ($configData as $value) { + if (!$value) { + throw new ValidateException('请先配置小票打印开发者'); + } + } + $printer = new Printer($name, $configData); + $printer->setPrinterContent([ + 'name' => sys_config('site_name'), + 'orderInfo' => is_object($order) ? $order->toArray() : $order, + 'product' => $product + ])->startPrinter(); + return true; + } + + /** + * 获取订单确认数据 + * @param array $user + * @param $cartId + * @param bool $new + * @param int $addressId + * @param int $shipping_type + * @param int $store_id + * @param int $coupon_id + * @return array + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderConfirmData(array $user, $cartId, bool $new, int $addressId, int $shipping_type = 1, int $store_id = 0, int $coupon_id = 0) + { + $addr = $data = []; + $uid = (int)$user['uid']; + /** @var UserAddressServices $addressServices */ + $addressServices = app()->make(UserAddressServices::class); + if ($addressId) { + $addr = $addressServices->getAdderssCache($addressId); + } + //没传地址id或地址已删除未找到 ||获取默认地址 + if (!$addr) { + $addr = $addressServices->getUserDefaultAddressCache($uid); + } + $data['upgrade_addr'] = 0; + if ($addr) { + $addr = is_object($addr) ? $addr->toArray() : $addr; + if (isset($addr['upgrade']) && $addr['upgrade'] == 0) { + $data['upgrade_addr'] = 1; + } + } else { + $addr = []; + } + if ($store_id) { + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeServices->getStoreInfo($store_id); + } + /** @var StoreCartServices $cartServices */ + $cartServices = app()->make(StoreCartServices::class); + //获取购物车信息 + $cartGroup = $cartServices->getUserProductCartListV1($uid, $cartId, $new, $addr, $shipping_type, $store_id, $coupon_id); + $storeFreePostage = floatval(sys_config('store_free_postage')) ?: 0;//满额包邮金额 + $data['storeFreePostage'] = $storeFreePostage; + $validCartInfo = $cartGroup['valid']; + $giveCartList = $cartGroup['giveCartList'] ?? []; + /** @var StoreOrderComputedServices $computedServices */ + $computedServices = app()->make(StoreOrderComputedServices::class); + $priceGroup = $computedServices->getOrderPriceGroup($validCartInfo, $addr, $user, $storeFreePostage); + $priceGroup['couponPrice'] = $cartGroup['couponPrice'] ?? 0; + $priceGroup['firstOrderPrice'] = $cartGroup['firstOrderPrice'] ?? 0; + $validCartInfo = array_merge($priceGroup['cartInfo'] ?? $validCartInfo, $giveCartList); + $other = [ + 'offlinePostage' => sys_config('offline_postage'), + 'integralRatio' => sys_config('integral_ratio'), + 'give_integral' => $cartGroup['giveIntegral'] ?? 0, + 'give_coupon' => $cartGroup['giveCoupon'] ?? [], + 'give_product' => $cartGroup['giveProduct'], + 'promotions' => $cartGroup['promotions'] + ]; + $deduction = $cartGroup['deduction']; + $data['product_type'] = $deduction['product_type'] ?? 0; + $data['valid_count'] = count($validCartInfo); + $data['addressInfo'] = $addr; + $data['type'] = $deduction['type'] ?? 0; + $data['activity_id'] = $deduction['activity_id'] ?? 0; + $data['seckill_id'] = $deduction['type'] == 1 ? $deduction['activity_id'] : 0; + $data['bargain_id'] = $deduction['type'] == 2 ? $deduction['activity_id'] : 0; + $data['combination_id'] = $deduction['type'] == 3 ? $deduction['activity_id'] : 0; + $data['luck_record_id'] = $deduction['type'] == 8 ? $deduction['activity_id'] : 0; + $data['collate_code_id'] = $deduction['type'] == 9 || $deduction['type'] == 10 ? $deduction['collate_code_id'] : 0; + $data['discount_id'] = $deduction['type'] == 5 ? $deduction['activity_id'] : 0; + $data['newcomer_id'] = $deduction['type'] == 7 ? $deduction['activity_id'] : 0; + $data['deduction'] = in_array($deduction['product_type'], [1, 2]) || $deduction['activity_id'] > 0; + $data['cartInfo'] = array_merge($cartGroup['cartInfo'], $giveCartList); + // $data['giveCartInfo'] = $giveCartList; + $data['custom_form'] = []; + if (isset($cartGroup['cartInfo'][0]['productInfo']['system_form_id']) && $cartGroup['cartInfo'][0]['productInfo']['system_form_id']) { + /** @var SystemFormServices $systemFormServices */ + $systemFormServices = app()->make(SystemFormServices::class); + $formInfo = $systemFormServices->value(['id' => $cartGroup['cartInfo'][0]['productInfo']['system_form_id']], 'value'); + if ($formInfo) { + $data['custom_form'] = is_string($formInfo) ? json_decode($formInfo, true) : $formInfo; + } + } + $data['give_integral'] = $other['give_integral']; + $data['give_coupon'] = []; + if ($other['give_coupon']) { + /** @var StoreCouponIssueServices $couponIssueService */ + $couponIssueService = app()->make(StoreCouponIssueServices::class); + $data['give_coupon'] = $couponIssueService->getColumn([['id', 'IN', $other['give_coupon']]], 'id,coupon_title'); + } + $data['priceGroup'] = $priceGroup; + $data['orderKey'] = $this->cacheOrderInfo($uid, $validCartInfo, $priceGroup, $other, $addr, $cartGroup['invalid'] ?? [], $deduction); + $data['offlinePostage'] = $other['offlinePostage']; + if (isset($user['pwd'])) unset($user['pwd']); + $user['vip'] = isset($priceGroup['vipPrice']) && $priceGroup['vipPrice'] > 0; + $user['vip_id'] = 0; + $user['discount'] = 0; + //用户等级是否开启 + if (sys_config('member_func_status', 1)) { + /** @var UserLevelServices $levelServices */ + $levelServices = app()->make(UserLevelServices::class); + $userLevel = $levelServices->getUerLevelInfoByUid($uid); + if ($user['vip'] || $userLevel) { + $user['vip'] = true; + $user['vip_id'] = $userLevel['id'] ?? 0; + $user['discount'] = $userLevel['discount'] ?? 0; + } + } + $user['record_pone'] = !isset($user['record_pone']) || !$user['record_pone'] ? '' : $user['record_pone']; + $data['userInfo'] = $user; + $data['integralRatio'] = $other['integralRatio']; + $data['offline_pay_status'] = (int)sys_config('offline_pay_status') ?? (int)2; + $data['yue_pay_status'] = (int)sys_config('balance_func_status') && (int)sys_config('yue_pay_status') == 1 ? (int)1 : (int)2;//余额支付 1 开启 2 关闭 + $data['pay_weixin_open'] = (int)sys_config('pay_weixin_open') ?? 0;//微信支付 1 开启 0 关闭 + $data['store_func_status'] = (int)(sys_config('store_func_status', 1));//门店是否开启 + $data['store_self_mention'] = false;//门店自提 + $data['store_delivery_status'] = false;//门店配送 + if ($data['store_func_status']) { + //门店自提是否开启 + /** @var SystemStoreServices $systemStoreServices */ + $systemStoreServices = app()->make(SystemStoreServices::class); + $data['store_self_mention'] = sys_config('store_self_mention') && $systemStoreServices->count(['type' => 0, 'is_store' => 1]); + $data['store_delivery_status'] = !!$systemStoreServices->count(['type' => 0]); + } + $data['store_func_status'] = $data['store_func_status'] && ($data['store_self_mention'] || $data['store_delivery_status']); + $data['ali_pay_status'] = (bool)sys_config('ali_pay_status');//支付包支付 1 开启 0 关闭 + $data['system_store'] = [];//门店信息 + /** @var UserInvoiceServices $userInvoice */ + $userInvoice = app()->make(UserInvoiceServices::class); + $invoice_func = $userInvoice->invoiceFuncStatus(); + $data['invoice_func'] = $invoice_func['invoice_func']; + $data['special_invoice'] = $invoice_func['special_invoice']; + $data['integral_ratio_status'] = (int)sys_config('integral_ratio_status', 1); + return $data; + } + + /** + * 缓存订单信息 + * @param int $uid + * @param array $cartInfo + * @param array $priceGroup + * @param array $other + * @param array $addr + * @param array $invalidCartInfo + * @param array $deduction + * @param int $cacheTime + * @return string + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function cacheOrderInfo(int $uid, array $cartInfo, array $priceGroup, array $other = [], array $addr = [], array $invalidCartInfo = [], array $deduction = [], int $cacheTime = 600) + { + /** @var StoreOrderCreateServices $storeOrderCreateService */ + $storeOrderCreateService = app()->make(StoreOrderCreateServices::class); + $key = md5($storeOrderCreateService->getNewOrderId((string)$uid) . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8)); + CacheService::redisHandler()->set('user_order_' . $uid . $key, compact('cartInfo', 'priceGroup', 'other', 'addr', 'invalidCartInfo', 'deduction'), $cacheTime); + return $key; + } + + /** + * 获取订单缓存信息 + * @param int $uid + * @param string $key + * @return |null + */ + public function getCacheOrderInfo(int $uid, string $key) + { + $cacheName = 'user_order_' . $uid . $key; + if (!CacheService::redisHandler()->has($cacheName)) return null; + return CacheService::redisHandler()->get($cacheName); + } + + /** + * 获取用户购买活动产品的次数 + * @param $uid + * @param $seckill_id + * @return int + */ + public function activityProductCount(array $where) + { + return $this->dao->count($where); + } + + /** + * 获取拼团的订单id + * @param int $pid + * @param int $uid + * @return mixed + */ + public function getStoreIdPink(int $pid, int $uid) + { + return $this->dao->value(['uid' => $uid, 'pink_id' => $pid, 'is_del' => 0], 'order_id'); + } + + /** + * 判断当前订单中是否有拼团 + * @param int $pid + * @param int $uid + * @return int + */ + public function getIsOrderPink($pid = 0, $uid = 0) + { + return $this->dao->count(['uid' => $uid, 'pink_id' => $pid, 'refund_status' => 0, 'is_del' => 0]); + } + + /** + * 判断支付方式是否开启 + * @param $payType + * @return bool + */ + public function checkPaytype(string $payType) + { + $res = false; + switch ($payType) { + case PayServices::WEIXIN_PAY: + $res = (bool)sys_config('pay_weixin_open'); + break; + case PayServices::YUE_PAY: + $res = sys_config('balance_func_status') && sys_config('yue_pay_status') == 1; + break; + case 'offline': + $res = sys_config('offline_pay_status') == 1; + break; + case PayServices::ALIAPY_PAY: + $res = sys_config('ali_pay_status') == 1; + break; + } + return $res; + } + + /** + * 修改支付方式为线下支付 + * @param string $orderId + * @return bool + */ + public function setOrderTypePayOffline(string $orderId) + { + return $this->dao->update($orderId, ['pay_type' => 'offline'], 'order_id'); + } + + /** + * 删除订单 + * @param $uni + * @param $uid + * @return bool + */ + public function removeOrder(string $uni, int $uid) + { + $order = $this->getUserOrderDetail($uni, $uid); + if (!$order) { + throw new ValidateException('订单不存在!'); + } + $order = $this->tidyOrder($order); + if ($order['_status']['_type'] != 0 && $order['_status']['_type'] != -2 && $order['_status']['_type'] != 4) + throw new ValidateException('该订单无法删除!'); + $order->is_del = 1; + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $res = $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'remove_order', + 'change_message' => '删除订单', + 'change_time' => time() + ]); + if ($order->save() && $res) { + //未支付和已退款的状态下才可以退积分退库存退优惠券 + if ($order['_status']['_type'] == 0 || $order['_status']['_type'] == -2) { + /** @var StoreOrderRefundServices $refundServices */ + $refundServices = app()->make(StoreOrderRefundServices::class); + $this->transaction(function () use ($order, $refundServices) { + //回退积分和优惠卷 + $res = $refundServices->integralAndCouponBack($order); + //回退库存 + $res = $res && $refundServices->regressionStock($order); + if (!$res) { + throw new ValidateException('取消订单失败!'); + } + }); + } + return true; + } else + throw new ValidateException('订单删除失败!'); + } + + /** + * 取消订单 + * @param $order_id + * @param int $uid + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function cancelOrder($order_id, int $uid) + { + $order = $this->dao->getOne(['order_id' => $order_id, 'uid' => $uid, 'is_del' => 0]); + if (!$order) { + throw new ValidateException('没有查到此订单'); + } + if ($order->paid) { + throw new ValidateException('订单已经支付无法取消'); + } + /** @var StoreOrderRefundServices $refundServices */ + $refundServices = app()->make(StoreOrderRefundServices::class); + $this->transaction(function () use ($refundServices, $order) { + //回退积分和优惠卷 + $res = $refundServices->integralAndCouponBack($order); + //回退库存和销量 + $res = $res && $refundServices->regressionStock($order); + $order->is_del = 1; + if (!($res && $order->save())) { + throw new ValidateException('取消订单失败'); + } + }); + //订单取消事件 + event('order.cancel', [$order]); + return true; + } + + /** + * 判断订单完成 + * @param StoreProductReplyServices $replyServices + * @param array $uniqueList + * @param $oid + * @return mixed + */ + public function checkOrderOver($replyServices, array $uniqueList, $oid) + { + //订单商品全部评价完成 + $replyServices->count(['unique' => $uniqueList, 'oid' => $oid]); + if ($replyServices->count(['unique' => $uniqueList, 'oid' => $oid]) == count($uniqueList)) { + $res = $this->dao->update($oid, ['status' => '3']); + if (!$res) throw new ValidateException('评价后置操作失败!'); + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $statusService->save([ + 'oid' => $oid, + 'change_type' => 'check_order_over', + 'change_message' => '用户评价', + 'change_time' => time() + ]); + } + } + + /** + * 某个用户订单 + * @param int $uid + * @param UserServices $userServices + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserOrderList(int $uid) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserWithTrashedInfo($uid); + if (!$user) { + throw new ValidateException('数据不存在'); + } + [$page, $limit] = $this->getPageValue(); + $where = ['uid' => $uid, 'pid' => [0, -1], 'paid' => 1, 'is_del' => 0, 'is_system_del' => 0]; + $list = $this->dao->getStairOrderList($where, 'order_id,real_name,total_num,total_price,pay_price,FROM_UNIXTIME(pay_time,"%Y-%m-%d") as pay_time,paid,pay_type,type,activity_id,activity_append', $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取推广订单列表 + * @param int $uid + * @param $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserStairOrderList(int $uid, $where) + { + $where_data = []; + if (isset($where['type'])) { + switch ((int)$where['type']) { + case 1: + $where_data['spread_uid'] = $uid; + break; + case 2: + $where_data['spread_two_uid'] = $uid; + break; + default: + $where_data['spread_or_uid'] = $uid; + break; + } + } + if (isset($where['data']) && $where['data']) { + $where_data['time'] = $where['data']; + } + if (isset($where['order_id']) && $where['order_id']) { + $where_data['order_id'] = $where['order_id']; + } + if (isset($where['nickname']) && $where['nickname']) { + $where_data['real_name'] = $where['nickname']; + } + //推广订单只显示支付过并且未退款的订单 + $where_data['pid'] = 0; + $where_data['paid'] = 1; + $where_data['refund_status'] = 0; + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getStairOrderList($where_data, '*', $page, $limit); + $count = $this->dao->count($where_data); + return compact('list', 'count'); + } + + /** + * 订单导出 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExportList(array $where, array $with = [], int $limit = 0) + { + if ($limit) { + [$page] = $this->getPageValue(); + } else { + [$page, $limit] = $this->getPageValue(); + } + $list = $this->dao->search($where)->with($with)->page($page, $limit)->order('id asc')->select()->toArray(); + if ($list) { + /** @var $userServices */ + $userServices = app()->make(UserServices::class); + $userSex = $userServices->getColumn([['uid', 'IN', array_unique(array_column($list, 'uid'))]], 'uid,sex', 'uid'); + foreach ($list as &$item) { + /** @var StoreOrderCartInfoServices $orderCart */ + $orderCart = app()->make(StoreOrderCartInfoServices::class); + $_info = $orderCart->getCartColunm(['oid' => $item['id']], 'cart_info', 'unique'); + foreach ($_info as $k => $v) { + $cart_info = is_string($v) ? json_decode($v, true) : $v; + if (!isset($cart_info['productInfo'])) $cart_info['productInfo'] = []; + $_info[$k] = $cart_info; + unset($cart_info); + } + $item['_info'] = $_info; + $item['sex'] = $userSex[$item['uid']]['sex'] ?? ''; + [$pink_name, $color] = $this->tidyOrderType($item); + $item['pink_name'] = $pink_name; + $item['color'] = $color; + } + } + return $list; + } + + /** + * 自动取消订单 + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function runOrderUnpaidCancel(int $page = 0, int $limit = 0) + { + $list = $this->dao->getOrderUnPaid(0, $page, $limit)->field(['*'])->select(); + if (!$list) { + return true; + } + //系统预设取消订单时间段 + $keyValue = ['order_cancel_time', 'order_activity_time', 'order_bargain_time', 'order_seckill_time', 'order_pink_time']; + //获取配置 + $systemValue = SystemConfigService::more($keyValue); + //格式化数据 + $systemValue = Arr::setValeTime($keyValue, is_array($systemValue) ? $systemValue : []); + $secsArr = []; + //秒杀 + $secsArr[1] = $systemValue['order_seckill_time'] ? $systemValue['order_seckill_time'] : $systemValue['order_activity_time']; + //砍价 + $secsArr[2] = $systemValue['order_bargain_time'] ? $systemValue['order_bargain_time'] : $systemValue['order_activity_time']; + //拼团 + $secsArr[3] = $systemValue['order_pink_time'] ? $systemValue['order_pink_time'] : $systemValue['order_activity_time']; + //默认 + $secsArr[0] = $systemValue['order_cancel_time']; + /** @var StoreOrderRefundServices $refundServices */ + $refundServices = app()->make(StoreOrderRefundServices::class); + + foreach ($list as $order) { + $type = $order['type']; + $secs = $secsArr[$type] ?? $secsArr[0]; + if ($secs == 0) continue; + if (($order['add_time'] + bcmul($secs, '3600', 0)) < time()) { + try { + $this->transaction(function () use ($order, $refundServices) { + //回退积分和优惠卷 + $res = $refundServices->integralAndCouponBack($order); + //回退库存和销量 + $res = $res && $refundServices->regressionStock($order); + //修改订单状态 + $res = $res && $this->dao->update($order['id'], ['is_del' => 1, 'mark' => '订单未支付已超过系统预设时间']); + if (!$res) { + throw new ValidateException('订单号' . $order['order_id'] . '自动取消订单失败'); + } + return true; + }); + //订单取消事件 + event('order.cancel', [$order]); + } catch (\Throwable $e) { + Log::error('自动取消订单失败,失败原因:' . $e->getMessage(), $e->getTrace()); + } + } + } + return true; + } + + /** + * 批量加入对接 + * @param int $count + * @param int $limit + */ + public function batchJoinJob(int $count, int $limit) + { + $pages = ceil($limit / $count); + for ($i = 1; $i <= $pages; $i++) { + AutoOrderUnpaidCancelJob::dispatch([$i, $limit]); + } + return true; + } + + /** + * 自动取消订单 + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function orderUnpaidCancel() + { + $count = $this->dao->getOrderUnPaid()->count(); + $maxLimit = 100; + if ($count > $maxLimit) { + return $this->batchJoinJob($count, $maxLimit); + } + return $this->runOrderUnpaidCancel(); + } + + /** + * 根据时间获取当天或昨天订单营业额 + * @param array $where + * @return float|int + */ + public function getOrderMoneyByWhere(array $where, string $sum_field, string $selectType, string $group = "") + { + + switch ($selectType) { + case "sum" : + return $this->dao->getDayTotalMoney($where, $sum_field); + case "group" : + return $this->dao->getDayGroupMoney($where, $sum_field, $group); + } + } + + /** + * 统计时间段订单数 + * @param array $where + * @param string $sum_field + */ + public function getOrderCountByWhere(array $where) + { + return $this->dao->getDayOrderCount($where); + } + + /** + * 分组统计时间段订单数 + * @param $where + * @return mixed + */ + public function getOrderGroupCountByWhere($where) + { + return $this->dao->getOrderGroupCount($where); + } + + /** + * 时间段支付订单人数 + * @param $where + * @return mixed + */ + public function getPayOrderPeopleByWhere($where) + { + return $this->dao->getPayOrderPeople($where); + } + + /** + * 时间段分组统计支付订单人数 + * @param $where + * @return mixed + */ + public function getPayOrderGroupPeopleByWhere($where) + { + return $this->dao->getPayOrderGroupPeople($where); + } + + /** + * 批量更新数据 + * @param array $ids + * @param array $data + * @param string|null $key + * @return BaseModel + */ + public function orderDel(array $ids, $redisKey, $queueId) + { + /** @var QueueServices $queueService */ + $queueService = app()->make(QueueServices::class); + $res = $this->dao->batchUpdateOrder($ids, ['is_system_del' => 1]); + if ($res) { + $queueService->doSuccessSremRedis($ids, $redisKey, $queueId['type']); + } else { + $queueService->addQueueFail($queueId['id'], $redisKey); + throw new AdminException('删除失败'); + } + } + + /**获取发货excel文件数据 + * @param string $file + * @param $row + * @return array + * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception + */ + public function readExpreExcel(string $file, $row = 2) + { + if (!$file) throw new AdminException('请上传发货数据表'); + /** @var FileService $readExcelService */ + $readExcelService = app()->make(FileService::class); + $exprData = $readExcelService->readExcel($file, $row); + if (!$exprData) throw new AdminException('发货数据为空'); + return $exprData; + } + + /** + * 队列发货 + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function adminQueueOrderDo(array $data, bool $is_again = false) + { + if (!$data) return false; + /** @var QueueServices $queueService */ + $queueService = app()->make(QueueServices::class); + /** @var QueueAuxiliaryServices $auxiliaryService */ + $auxiliaryService = app()->make(QueueAuxiliaryServices::class); + $queueWhere['type'] = $data['queueType']; + $queueWhere['status'] = 0; + if (isset($data['queueId']) && $data['queueId']) $queueWhere['id'] = $data['queueId']; + $queueInfo = $queueService->getQueueOne($queueWhere); + $ids = $auxiliaryService->getCacheOidList($queueInfo['id'], $data['cacheType']); + $data['ids'] = array_column($ids, 'relation_id'); + $data['queueId'] = $queueInfo['id']; + //if ($queueInfo['status'] == 2) throw new ValidateException('任务已完成'); + //把队列需要执行的入参数据存起来,以便队列执行失败后接着执行,同时队列状态改为正在执行状态。 + $queueService->setQueueDoing($data, $queueInfo['id'], $is_again); + $oids = $auxiliaryService->getOrderExpreList(['binding_id' => $queueInfo['id'], 'type' => $data['cacheType'], 'status' => [0, 2]]); + $oids = $oids ? array_column($oids, 'relation_id') : []; + // $chunkPids = array_chunk($oids, 1000, true); + $data['queueId'] = $queueInfo['id']; + foreach ($oids as $v) { + //加入队列 + BatchHandleJob::dispatch([$v, $data['queueType'], $data]); + } + return true; + } + + /** + * 对外接口获取订单状态 + * @param int $oid + */ + public function outGetStatus(string $oid) + { + $order = $this->dao->getOne(['order_id' => $oid]); + if (!$order['paid'] && $order['pay_type'] == 'offline' && !$order['status'] >= 2) { + $status_name = '线下支付'; + } else if (!$order['paid']) { + $status_name = '未支付'; + } else if ($order['status'] == 0 && $order['refund_status'] == 0) { + $status_name = '待发货'; + } else if ($order['refund_status'] == 1) { + $status_name = '申请退款中'; + } else if ($order['refund_status'] == 2) { + $status_name = '已退款'; + } else if ($order['refund_status'] == 3) { + $status_name = '部分退款(子订单)'; + } else if ($order['refund_status'] == 4) { + $status_name = '子订单已全部申请退款中'; + } else if (!$order['status']) { + $status_name = '未发货'; + } else if ($order['status'] == 1) { + $status_name = '待收货'; + } else if ($order['status'] == 2) { + $status_name = '待评价'; + } else if ($order['status'] == 3) { + $status_name = '交易完成'; + } + $data = []; + $data['status_name'] = $status_name; + $data['status'] = $order['status']; + $data['paid'] = $order['paid']; + $data['pay_type'] = $order['pay_type']; + $data['refund_status'] = $order['refund_status']; + return $data; + } + + /** + * 对外接口根据订单id查询收货方式 + * @param string $oid + * @return array + */ + public function outGetShippingType(string $oid) + { + $shipping_type = $this->dao->value(['order_id' => $oid], 'shipping_type'); + $shipping_type = $shipping_type == 1 ? '商家配送' : '到店自提'; + return compact('shipping_type'); + } + + /** + * 对外接口根据订单id查询配送信息 + * @param string $oid + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function OutDeliveryType(string $oid) + { + $info = $this->dao->getOne(['order_id' => $oid], 'order_id,delivery_type,delivery_name,delivery_id'); + return $info ? $info->toArray() : []; + } + + /** + * 对外接口获取运费 + * @param int $cartId + * @param int $uid + * @param int $addressId + * @return array + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function outGetPostage($cartId, int $uid, int $addressId, int $couponId = 0) + { + $addr = []; + /** @var UserAddressServices $addressServices */ + $addressServices = app()->make(UserAddressServices::class); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->get($uid); + if ($addressId) { + $addr = $addressServices->getAdderssCache($addressId); + } + //没传地址id或地址已删除未找到 ||获取默认地址 + if (!$addr) { + $addr = $addressServices->getUserDefaultAddressCache($uid); + } + + /** @var StoreCartServices $cartServices */ + $cartServices = app()->make(StoreCartServices::class); + $cartGroup = $cartServices->getUserProductCartListV1($uid, $cartId, true, $addr); + $storeFreePostage = floatval(sys_config('store_free_postage')) ?: 0;//满额包邮金额 + $validCartInfo = $cartGroup['valid']; + /** @var StoreOrderComputedServices $computedServices */ + $computedServices = app()->make(StoreOrderComputedServices::class); + $priceGroup = $computedServices->getOrderPriceGroup($validCartInfo, $addr, $user, $storeFreePostage); + $postage = $priceGroup['storePostage'] ?? 0; + return compact('postage'); + } + + /** + * 获取配送员订单统计列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDeliveryStatistics(array $where) + { + [$page, $limit] = $this->getPageValue(); + $where['is_del'] = 0; + $where['paid'] = 1; + $where['is_system_del'] = 0; + $where['delivery_type'] = 'send'; + $where['refund_status'] = [0, 3]; + $list = $this->dao->getList((array)$where, ['*'], (int)$page, (int)$limit, ['user']); + if ($list) { + $list = $this->tidyOrderList($list, false); + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 配送员订单统计 + * @param $store_id + * @param $delivery_uid + * @param $time + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStatisticsHeader($store_id, $delivery_uid, $time) + { + $where['is_del'] = 0; + $where['paid'] = 1; + $where['is_system_del'] = 0; + $where['delivery_type'] = 'send'; + $where['store_id'] = $store_id; + $where['refund_status'] = [0, 3]; + if ($delivery_uid) { + $where['delivery_uid'] = $delivery_uid; + } + [$start, $end, $timeType, $xAxis] = $time; + $order = $this->dao->orderAddTimeList($where, [$start, $end], $timeType, false); + $price = array_column($order, 'price', 'day'); + $count = array_column($order, 'count', 'day'); + $data = $series = []; + foreach ($xAxis as $key) { + $data['配送订单金额'][] = isset($price[$key]) ? floatval($price[$key]) : 0; + $data['配送单数'][] = isset($count[$key]) ? floatval($count[$key]) : 0; + } + foreach ($data as $key => $item) { + $series[] = [ + 'name' => $key, + 'data' => $item, + 'type' => 'line', + 'smooth' => 'true', + 'yAxisIndex' => 1, + ]; + } + return compact('xAxis', 'series'); + } + + /** + * 门店线上支付订单详情 + * @param int $store + * @param int $uid + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function payCashierOrder(int $store, int $uid) + { + $order = $this->dao->payCashierOrder($store, $uid); + if (!$order) throw new ValidateException('订单不存在'); + $order = $order->toArray(); + $order = $this->tidyOrder($order, true); + $order['yue_pay_status'] = (int)sys_config('balance_func_status') && (int)sys_config('yue_pay_status') == 1 ? (int)1 : (int)2;//余额支付 1 开启 2 关闭 + $order['pay_weixin_open'] = (int)sys_config('pay_weixin_open') ?? 0;//微信支付 1 开启 0 关闭 + $order['ali_pay_status'] = (bool)sys_config('ali_pay_status');//支付包支付 1 开启 0 关闭 + return $order; + } + + /** + * 订单分配|重新分配给你门店 + * @param int $id + * @param int $store_id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function shareOrder(int $id, int $store_id) + { + $orderInfo = $this->dao->get((int)$id); + if (!$orderInfo) { + throw new ValidateException('订单不存在'); + } + //卡密商品 + if ($orderInfo['product_type'] == 1) { + throw new ValidateException('订单中卡密商品门店暂不支持'); + } + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeInfo = $storeServices->getStoreInfo($store_id); + if ($orderInfo['status'] != 0) { + throw new ValidateException('订单已发货'); + } + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cart_info = $storeOrderCartInfoServices->getSplitCartList($id, 'cart_info'); + if (!$cart_info) { + throw new ValidateException('订单已发货'); + } + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + if ($storeOrderRefundServices->count(['store_order_id' => $id, 'refund_type' => [1, 2, 4, 5, 6], 'is_cancel' => 0, 'is_del' => 0])) { + throw new ValidateException('订单有售后申请请先处理'); + } + $platProductIds = []; + $platStoreProductIds = []; + $storeProductIds = []; + foreach ($cart_info as $cart) { + $productInfo = $cart['productInfo'] ?? []; + if (isset($productInfo['store_delivery']) && !$productInfo['store_delivery']) {//有商品不支持门店配送 + return [[], $cart_info]; + } + switch ($productInfo['type'] ?? 0) { + case 0://平台 + case 2://供应商 + $platProductIds[] = $cart['product_id']; + break; + case 1://门店 + if ($productInfo['pid']) {//门店自有商品 + $storeProductIds[] = $cart['product_id']; + } else { + $platStoreProductIds[] = $cart['product_id']; + } + break; + } + } + if ($storeProductIds && $store_id != $orderInfo['store_id']) { + throw new ValidateException('该门店商品未上架或未设置库存'); + } + /** @var StoreBranchProductServices $branchProductServics */ + $branchProductServics = app()->make(StoreBranchProductServices::class); + //转换成平台商品 + if ($platStoreProductIds) { + $ids = $branchProductServics->getStoreProductIds($platStoreProductIds); + $platProductIds = array_merge($platProductIds, $ids); + } + $productCount = count($platProductIds); + //商品没下架 && 库存足够 + if ($productCount != $branchProductServics->count(['pid' => $platProductIds, 'is_show' => 1, 'is_del' => 0, 'type' => 1, 'relation_id' => $store_id])) { + throw new ValidateException('该门店商品未上架或未设置库存'); + } + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + foreach ($cart_info as $cart) { + if (isset($cart['productInfo']['store_delivery']) && !$cart['productInfo']['store_delivery']) {//有商品不支持门店配送 + throw new ValidateException('有商品不支持门店配送'); + } + switch ($cart['type']) { + case 0: + case 6: + case 8: + $suk = $skuValueServices->value(['unique' => $cart['product_attr_unique'], 'product_id' => $cart['product_id'], 'type' => 0], 'suk'); + break; + case 1: + case 2: + case 3: + case 5: + case 7: + $suk = $skuValueServices->value(['unique' => $cart['product_attr_unique'], 'product_id' => $cart['activity_id'], 'type' => $cart['type']], 'suk'); + break; + } + $branchProductInfo = $branchProductServics->isValidStoreProduct((int)$cart['product_id'], $store_id); + if (!$branchProductInfo) { + throw new ValidateException('该门店商品库存不足'); + } + $attrValue = $skuValueServices->get(['suk' => $suk, 'product_id' => $branchProductInfo['id'], 'type' => 0]); + if (!$attrValue) { + throw new ValidateException('该门店商品库存不足'); + } + } + $res = $this->transaction(function () use ($id, $store_id, $orderInfo, $storeInfo, $cart_info, $branchProductServics) { + + if ($orderInfo['store_id'] > 0) {//重新分配门店 + //返还原来门店库存 + $res = $branchProductServics->regressionBranchProductStock($orderInfo, $cart_info, -1, 0); + } else { + //返还平台库存 + $res = $branchProductServics->regressionBranchProductStock($orderInfo, $cart_info, 0, -1); + } + //扣门店库存 + $res = $branchProductServics->regressionBranchProductStock($orderInfo, $cart_info, -1, 1, $store_id); + $res = $res && $this->dao->update($id, ['store_id' => $storeInfo['id'], 'shipping_type' => $orderInfo['shipping_type'] == 1 ? 3 : $orderInfo['shipping_type']]); + return $res; + }); + $orderInfo['store_id'] = $storeInfo['id']; + //删除之前的账单记录 + /** @var StoreFinanceFlowServices $storeFinanceFlowServices */ + $storeFinanceFlowServices = app()->make(StoreFinanceFlowServices::class); + $storeFinanceFlowServices->update(['link_id' => $orderInfo['order_id']], ['is_del' => 1]); + //分配后置方法 + SpliteStoreOrderJob::dispatchDo('splitAfter', [$orderInfo, true]); + return $res; + } + + /** + * 获取退货商品列表 + * @param array $cart_ids + * @param int $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function refundCartInfoList(array $cart_ids = [], int $id = 0) + { + $orderInfo = $this->dao->get($id); + if (!$orderInfo) { + throw new ValidateException('订单不存在'); + } + $orderInfo = $this->tidyOrder($orderInfo, true); + $cartInfo = $orderInfo['cartInfo'] ?? []; + $data = []; + if ($cart_ids) { + foreach ($cart_ids as $cart) { + if (!isset($cart['cart_id']) || !$cart['cart_id']) { + throw new ValidateException('请重新选择退款商品,或件数'); + } + } + $cart_ids = array_combine(array_column($cart_ids, 'cart_id'), $cart_ids); + $i = 0; + foreach ($cartInfo as $item) { + if (isset($cart_ids[$item['id']])) { + $data['cartInfo'][$i] = $item; + if (isset($cart_ids[$item['id']]['cart_num'])) $data['cartInfo'][$i]['cart_num'] = $cart_ids[$item['id']]['cart_num']; + $i++; + } + } + } + $data['_status'] = $orderInfo['_status'] ?? []; + $data['cartInfo'] = $data['cartInfo'] ?? $cartInfo; + return $data; + } + + /** + * 拆单的发货订单数量 + * @param int $id + * @param $order + * @return int + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDeliverNum(int $id, $order): int + { + if (!$order) { + $order = $this->get($id); + } + $ids = $id; + $pid = (int)$order['pid']; + if ($pid > 0) { + $ids = $this->Value([['pid', '=', $pid], ['status', '=', 1]], 'GROUP_CONCAT(id)'); + if (!empty($ids)) { + $ids = array_map('intval', array_filter(explode(',', $ids))); + } + } + return $this->getCount(['id' => $ids, 'status' => 1]); + } + + + /** + * 获取确认订单页面是否展示快递配送和到店自提 + * @param $uid + * @param $cartIds + * @param $new + * @return array + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function checkShipping($uid, $cartIds, $new) + { + if ($new) { + $cartIds = explode(',', $cartIds); + $cartInfo = []; + $redis = CacheService::redisHandler(); + foreach ($cartIds as $key) { + $info = $redis->get($key); + if ($info) { + $cartInfo[] = $info; + } + } + } else { + /** @var StoreCartServices $cartServices */ + $cartServices = app()->make(StoreCartServices::class); + $cartInfo = $cartServices->getCartList(['uid' => $uid, 'status' => 1, 'id' => $cartIds], 0, 0, ['productInfo', 'attrInfo']); + } + if (!$cartInfo) { + throw new ValidateException('获取购物车信息失败'); + } + $arr = []; + $store_id = []; + //delivery_type :1、快递,2、到店自提,3、门店配送 + foreach ($cartInfo as $item) { + $delivery_type = is_string($item['productInfo']['delivery_type']) ? explode(',', $item['productInfo']['delivery_type']) : $item['productInfo']['delivery_type']; + if (in_array(1, $delivery_type)) {//支持平台配送 验证平台该商品 + $productInfo = $item['productInfo'] ?? []; + if ($productInfo && isset($productInfo['type']) && $productInfo['type'] == 1 && isset($productInfo['pid']) && $productInfo['pid']) { + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $platInfo = $productServices->getCacheProductInfo((int)$productInfo['pid']); + if (!$platInfo || $platInfo['stock'] <= 0) { + unset($delivery_type[array_search('1', $delivery_type)]); + } + } + } + $arr = array_merge($arr, $delivery_type); + if (isset($item['productInfo']['type']) && isset($item['productInfo']['relation_id']) && $item['productInfo']['type'] == 1 && $item['productInfo']['relation_id']) { + $store_id[] = $item['productInfo']['relation_id']; + } + } + $count = count($arr); + if (!$count) { + $arr = [1, 2, 3]; + } + // 门店总开关 + if (!sys_config('store_func_status', 1)) { + if (in_array(2, $arr)) unset($arr[array_search(2, $arr)]); + if (in_array(3, $arr)) unset($arr[array_search(3, $arr)]); + } elseif (sys_config('store_self_mention', 1)) { + //判断有没有满足自提的店铺 + $where['id'] = $store_id; + $where['is_store'] = 1; + $where['is_show'] = 1; + $where['is_del'] = 0; + /** @var SystemStoreServices $SystemStoreServe */ + $SystemStoreServe = app()->make(SystemStoreServices::class); + $store_list = $SystemStoreServe->count($where); + if (!$store_list) { + if (in_array(2, $arr)) unset($arr[array_search(2, $arr)]); + } + } else { + if (in_array(2, $arr)) unset($arr[array_search(2, $arr)]); + } + $arr = array_merge(array_unique($arr)); + return ['type' => $arr]; + } + + /** + * @param int $pid + * @param int $order_id + * @return bool + * @throws \think\db\exception\DbException + */ + public function checkSubOrderNotSend(int $pid, int $order_id) + { + $order_count = $this->dao->getSubOrderNotSend($pid, $order_id); + if ($order_count > 0) { + return false; + } else { + return true; + } + } + + /** + * @param int $pid + * @param int $order_id + * @return bool + * @throws \think\db\exception\DbException + */ + public function checkSubOrderNotTake(int $pid, int $order_id) + { + $order_count = $this->dao->getSubOrderNotTake($pid, $order_id); + if ($order_count > 0) { + return false; + } else { + return true; + } + } + +} diff --git a/app/services/order/StoreOrderStatusServices.php b/app/services/order/StoreOrderStatusServices.php new file mode 100644 index 0000000..a271c6f --- /dev/null +++ b/app/services/order/StoreOrderStatusServices.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order; + + +use app\dao\order\StoreOrderStatusDao; +use app\services\BaseServices; +use crmeb\traits\ServicesTrait; + +/** + * 订单状态 + * Class StoreOrderStatusServices + * @package app\services\order + * @mixin StoreOrderStatusDao + */ +class StoreOrderStatusServices extends BaseServices +{ + use ServicesTrait; + + /** + * 构造方法 + * StoreOrderStatusServices constructor. + * @param StoreOrderStatusDao $dao + */ + public function __construct(StoreOrderStatusDao $dao) + { + $this->dao = $dao; + } + + /** + * 订单状态分页 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStatusList(array $where) + { + $list = $this->dao->getStatusList($where); + foreach ($list as &$item) { + if (is_int($item['change_time'])) $item['change_time'] = date('Y-m-d H:i:s', $item['change_time']); + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + +} diff --git a/app/services/order/StoreOrderSuccessServices.php b/app/services/order/StoreOrderSuccessServices.php new file mode 100644 index 0000000..551bc70 --- /dev/null +++ b/app/services/order/StoreOrderSuccessServices.php @@ -0,0 +1,106 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order; + + +use app\common\dao\store\order\StoreOrderDao; +use app\services\activity\lottery\LuckLotteryServices; +use app\services\BaseServices; +use app\services\pay\PayServices; +use app\services\user\UserServices; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; + +/** + * Class StoreOrderSuccessServices + * @package app\services\order + * @mixin StoreOrderDao + */ +class StoreOrderSuccessServices extends BaseServices +{ + use ServicesTrait; + + /** + * + * StoreOrderSuccessServices constructor. + * @param StoreOrderDao $dao + */ + public function __construct(StoreOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * 0元支付 + * @param array $orderInfo + * @param int $uid + * @return bool + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function zeroYuanPayment(array $orderInfo, int $uid, string $payType = PayServices::YUE_PAY) + { + $id = $orderInfo['id'] ?? 0; + if (!$orderInfo || !$id) { + throw new ValidateException('订单不存在!'); + } + //更新订单信息 + $orderInfo = $this->dao->get($id); + if (!$orderInfo) { + throw new ValidateException('订单不存在'); + } + $orderInfo = $orderInfo->toArray(); + if ($orderInfo['paid']) { + throw new ValidateException('该订单已支付!'); + } + return $this->paySuccess($orderInfo, $payType);//余额支付成功 + } + + /** + * 支付成功 + * @param array $orderInfo + * @param string $paytype + * @return bool + */ + public function paySuccess(array $orderInfo, string $paytype = PayServices::WEIXIN_PAY, array $other = []) + { + $updata = ['paid' => 1, 'pay_type' => $paytype, 'pay_time' => time()]; + if ($other && isset($other['trade_no'])) { + $updata['trade_no'] = $other['trade_no']; + } + $res1 = $this->dao->update($orderInfo['id'], $updata); + $orderInfo['trade_no'] = $other['trade_no'] ?? ''; + $orderInfo['pay_time'] = time(); + $orderInfo['pay_type'] = $paytype; + //缓存抽奖次数 除过线下支付 + if (isset($orderInfo['pay_type']) && $orderInfo['pay_type'] != 'offline') { + /** @var LuckLotteryServices $luckLotteryServices */ + $luckLotteryServices = app()->make(LuckLotteryServices::class); + $luckLotteryServices->setCacheLotteryNum((int)$orderInfo['uid'], 'order'); + } + //门店 +// if ($orderInfo['shipping_type'] == 4) { +// //订单发货 +// OrderDeliveryJob::dispatch([$orderInfo, [], 4]); +// //订单收货 +// OrderTakeJob::dispatch([$orderInfo]); +// } + //订单支付成功事件 + $userInfo = app()->make(UserServices::class)->get($orderInfo['uid']); + event('order.pay', [$orderInfo, $userInfo]); + $res = $res1; + return false !== $res; + } + +} diff --git a/app/services/order/StoreOrderTakeServices.php b/app/services/order/StoreOrderTakeServices.php new file mode 100644 index 0000000..99f2a47 --- /dev/null +++ b/app/services/order/StoreOrderTakeServices.php @@ -0,0 +1,552 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order; + + +use app\dao\order\StoreOrderDao; +use app\jobs\order\AutoTakeOrderJob; +use app\jobs\notice\SmsAdminJob; +use app\services\BaseServices; +use app\services\message\service\StoreServiceServices; +use app\services\message\sms\SmsSendServices; +use app\services\user\member\MemberCardServices; +use app\services\user\UserBillServices; +use app\services\user\UserBrokerageServices; +use app\services\user\level\UserLevelServices; +use app\services\user\UserServices; +use think\exception\ValidateException; +use think\facade\Log; + +/** + * 订单收货 + * Class StoreOrderTakeServices + * @package app\services\order + * @mixin StoreOrderDao + */ +class StoreOrderTakeServices extends BaseServices +{ + /** + * 构造方法 + * StoreOrderTakeServices constructor. + * @param StoreOrderDao $dao + */ + public function __construct(StoreOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * 小程序订单服务收货 + * @param $order_id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function miniOrderTakeOrder($order_id) + { + $orderArr = explode('_', $order_id); + if (count($orderArr) == 2) { + $order_id = $orderArr[1] ?? $order_id; + } + //查找订单信息 + $order = $this->dao->getOne(['order_id' => $order_id]); + if (!$order) { + return true; + } + if ($order['pid'] == -1) { // 有子订单 + // 查找待收货的子订单 + $son_order_list = $this->dao->getSubOrderNotSendList((int)$order['id']); + foreach ($son_order_list as $son_order) { + $this->takeOrder($son_order['order_id'], $son_order['uid']); + } + } else { + $this->takeOrder($order_id, $order['uid']); + } + + return true; + } + + /** + * 用户订单收货 + * @param $uni + * @param $uid + * @return bool + */ + public function takeOrder(string $uni, int $uid) + { + $order = $this->dao->getUserOrderDetail($uni, $uid); + if (!$order) { + throw new ValidateException('订单不存在!'); + } + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $order = $orderServices->tidyOrder($order); + if ($order['_status']['_type'] != 2) { + throw new ValidateException('订单状态错误!'); + } + //存在拆分发货 需要分开收货 + if ($this->dao->count(['pid' => $order['id']])) { + throw new ValidateException('拆分发货,请去订单详情中包裹确认收货'); + } + if ($order['type'] != 8) { + $order->status = 2; + } else { + $order->status = 3; + } + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $res = $order->save() && $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'user_take_delivery', + 'change_message' => '用户已收货', + 'change_time' => time() + ]); + $res = $res && $this->storeProductOrderUserTakeDelivery($order); + if (!$res) { + throw new ValidateException('收货失败'); + } + //核销订单 修改订单商品核销状态 + if ($order['shipping_type'] == 2 || (in_array($order['shipping_type'], [1, 3]) && $order['delivery_type'] == 'send')) { + //修改原来订单商品信息 + $cartData['is_writeoff'] = 1; + $cartData['write_surplus_times'] = 0; + /** @var StoreOrderCartInfoServices $cartInfoServices */ + $cartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfoServices->update(['oid' => $order['id']], $cartData); + } + return $order; + } + + /** + * 订单确认收货 + * @param $order + * @return bool + */ + public function storeProductOrderUserTakeDelivery($order, bool $isTran = true) + { + $res = true; + //获取购物车内的商品标题 + /** @var StoreOrderCartInfoServices $orderInfoServices */ + $orderInfoServices = app()->make(StoreOrderCartInfoServices::class); + $storeName = $orderInfoServices->getCarIdByProductTitle((int)$order['id']); + $storeTitle = substrUTf8($storeName, 20, 'UTF-8', ''); + if ($order['uid']) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->get((int)$order['uid']); + $res = $this->transaction(function () use ($order, $userInfo, $storeTitle) { + //赠送积分 + $res1 = $this->gainUserIntegral($order, $userInfo, $storeTitle); + //返佣 + $res2 = $this->backOrderBrokerage($order, $userInfo); + //经验 + $res3 = $this->gainUserExp($order, $userInfo); + if (!($res1 && $res2 && $res3)) { + throw new ValidateException('收货失败!'); + } + return true; + }, $isTran); + } + if ($res) { + //订单收货事件 + event('order.take', [$order, $storeTitle]); + return true; + } else { + return false; + } + } + + /** + * 赠送积分 + * @param $order + * @param $userInfo + * @param $storeTitle + * @return bool + */ + public function gainUserIntegral($order, $userInfo, $storeTitle) + { + $res2 = true; + if (!$userInfo) { + return true; + } + // 营销产品送积分 + if (!isset($order['type']) || in_array($order['type'], [1, 2, 3, 5, 8])) { + return true; + } + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $balance = $userInfo['integral']; + if ($order['gain_integral'] > 0) { + $balance = bcadd((string)$balance, (string)$order['gain_integral']); + $res2 = false != $userBillServices->income('pay_give_integral', $order['uid'], (int)$order['gain_integral'], (int)$balance, $order['id']); + } + $order_integral = 0; + $res3 = true; + $order_give_integral = sys_config('order_give_integral'); + if ($order['pay_price'] && $order_give_integral) { + //会员消费返积分翻倍 + if ($userInfo['is_money_level'] > 0) { + //看是否开启消费返积分翻倍奖励 + /** @var MemberCardServices $memberCardService */ + $memberCardService = app()->make(MemberCardServices::class); + $integral_rule_number = $memberCardService->isOpenMemberCardCache('integral'); + if ($integral_rule_number) { + $order_integral = bcmul((string)$order['pay_price'], (string)$integral_rule_number, 2); + } + } + $order_integral = bcmul((string)$order_give_integral, (string)($order_integral ? $order_integral : $order['pay_price']), 0); + + $balance = bcadd((string)$balance, (string)$order_integral); + $res3 = false != $userBillServices->income('order_give_integral', $order['uid'], (int)$order_integral, (int)$balance, $order['id']); + } + $give_integral = $order_integral + $order['gain_integral']; + if ($give_integral > 0) { + $integral = $userInfo['integral'] + $give_integral; + $userInfo->integral = $integral; + $res1 = false != $userInfo->save(); + $res = $res1 && $res2 && $res3; + //发送消息 + event('notice.notice', [['order' => $order, 'storeTitle' => $storeTitle, 'give_integral' => $give_integral, 'integral' => $integral], 'integral_accout']); + return $res; + } + return true; + } + + /** + * 一级返佣 + * @param $orderInfo + * @param $userInfo + * @return bool + */ + public function backOrderBrokerage($orderInfo, $userInfo) + { + // 当前订单|用户不存在 直接返回 + if (!$orderInfo || !$userInfo) { + return true; + } + //商城分销功能是否开启 0关闭1开启 + if (!sys_config('brokerage_func_status')) return true; + + // 营销产品不返佣金 + if (!isset($orderInfo['type']) || in_array($orderInfo['type'], [1, 2, 3, 5, 8])) { + return true; + } + //绑定失效 + if (isset($orderInfo['spread_uid']) && $orderInfo['spread_uid'] == -1) { + return true; + } + //是否开启自购返佣 + $isSelfBrokerage = sys_config('is_self_brokerage', 0); + if (!isset($orderInfo['spread_uid']) || !$orderInfo['spread_uid']) {//兼容之前订单表没有spread_uid情况 + //没开启自购返佣 没有上级 或者 当用用户上级时自己 直接返回 + if (!$isSelfBrokerage && (!$userInfo['spread_uid'] || $userInfo['spread_uid'] == $orderInfo['uid'])) { + return true; + } + $one_spread_uid = $isSelfBrokerage ? $userInfo['uid'] : $userInfo['spread_uid']; + } else { + $one_spread_uid = $orderInfo['spread_uid']; + } + $one_spread_uid = (int)$one_spread_uid; + //冻结时间 + $broken_time = intval(sys_config('extract_time')); + $frozen_time = time() + $broken_time * 86400; + + //订单中取出 + $brokeragePrice = $orderInfo['one_brokerage'] ?? 0; + // 一级返佣金额小于等于0 + if ($brokeragePrice <= 0) {//直接二级返佣 + return $this->backOrderBrokerageTwo($orderInfo, $userInfo, $isSelfBrokerage, $frozen_time); + } + // 获取上级推广员信息 + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $spreadPrice = $userServices->value(['uid' => $one_spread_uid], 'brokerage_price'); + // 上级推广员返佣之后的金额 + $balance = bcadd($spreadPrice, $brokeragePrice, 2); + // 添加佣金记录 + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + //自购返佣 || 上级 + $type = $one_spread_uid == $orderInfo['uid'] ? 'get_self_brokerage' : 'get_brokerage'; + $res1 = $userBrokerageServices->income($type, $one_spread_uid, [ + 'nickname' => $userInfo['nickname'], + 'pay_price' => floatval($orderInfo['pay_price']), + 'number' => floatval($brokeragePrice), + 'frozen_time' => $frozen_time + ], $balance, $orderInfo['id']); + // 添加用户佣金 + $res2 = $userServices->bcInc($one_spread_uid, 'brokerage_price', $brokeragePrice, 'uid'); + //给上级发送获得佣金的模板消息 + $this->sendBackOrderBrokerage($orderInfo, $one_spread_uid, $brokeragePrice); + // 一级返佣成功 跳转二级返佣 + $res = $res1 && $res2 && $this->backOrderBrokerageTwo($orderInfo, $userInfo, $isSelfBrokerage, $frozen_time); + return $res; + } + + + /** + * 二级推广返佣 + * @param $orderInfo + * @param $userInfo + * @param int $isSelfbrokerage + * @param int $frozenTime + * @return bool + */ + public function backOrderBrokerageTwo($orderInfo, $userInfo, $isSelfbrokerage = 0, $frozenTime = 0) + { + //绑定失效 + if (isset($orderInfo['spread_two_uid']) && $orderInfo['spread_two_uid'] == -1) { + return true; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (isset($orderInfo['spread_two_uid']) && $orderInfo['spread_two_uid']) { + $spread_two_uid = $orderInfo['spread_two_uid']; + } else { + // 获取上推广人 + $userInfoTwo = $userServices->get((int)$userInfo['spread_uid']); + // 订单|上级推广人不存在 直接返回 + if (!$orderInfo || !$userInfoTwo) { + return true; + } + //没开启自购返佣 或者 上推广人没有上级 或者 当用用户上上级时自己 直接返回 + if (!$isSelfbrokerage && (!$userInfoTwo['spread_uid'] || $userInfoTwo['spread_uid'] == $orderInfo['uid'])) { + return true; + } + $spread_two_uid = $isSelfbrokerage ? $userInfoTwo['uid'] : $userInfoTwo['spread_uid']; + } + $spread_two_uid = (int)$spread_two_uid; + + //订单中取出 + $brokeragePrice = $orderInfo['two_brokerage'] ?? 0; + // 返佣金额小于等于0 直接返回不返佣金 + if ($brokeragePrice <= 0) { + return true; + } + // 获取上上级推广员信息 + $spreadPrice = $userServices->value(['uid' => $spread_two_uid], 'brokerage_price'); + // 获取上上级推广员返佣之后余额 + $balance = bcadd($spreadPrice, $brokeragePrice, 2); + // 添加佣金记录 + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $res1 = $userBrokerageServices->income('get_two_brokerage', $spread_two_uid, [ + 'nickname' => $userInfo['nickname'], + 'pay_price' => floatval($orderInfo['pay_price']), + 'number' => floatval($brokeragePrice), + 'frozen_time' => $frozenTime + ], $balance, $orderInfo['id']); + // 添加用户佣金 + $res2 = $userServices->bcInc($spread_two_uid, 'brokerage_price', $brokeragePrice, 'uid'); + //给上级发送获得佣金的模板消息 + $this->sendBackOrderBrokerage($orderInfo, $spread_two_uid, $brokeragePrice); + return $res1 && $res2; + } + + /** + * 佣金到账发送模板消息 + * @param $orderInfo + * @param $spread_uid + * @param $brokeragePrice + */ + public function sendBackOrderBrokerage($orderInfo, $spread_uid, $brokeragePrice, string $type = 'order') + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($spread_uid, 'phone,user_type'); + if ($type == 'order') { + /** @var StoreOrderCartInfoServices $storeOrderCartInfoService */ + $storeOrderCartInfoService = app()->make(StoreOrderCartInfoServices::class); + $cartInfo = $storeOrderCartInfoService->getOrderCartInfo($orderInfo['id']); + if ($cartInfo) { + $cartInfo = array_column($cartInfo, 'cart_info'); + $goodsPrice = 0; + $goodsName = ""; + foreach ($cartInfo as $k => $v) { + $goodsName .= $v['productInfo']['store_name']; + $price = $v['productInfo']['attrInfo']['price'] ?? $v['productInfo']['price'] ?? 0; + $goodsPrice = bcadd((string)$goodsPrice, (string)$price, 2); + } + } + } else { + $goodsName = '推广用户获取佣金'; + $goodsPrice = $brokeragePrice; + } + //提醒推送 + event('notice.notice', [['spread_uid' => $spread_uid, 'phone' => $user['phone'], 'userType' => $user['user_type'], 'brokeragePrice' => $brokeragePrice, 'goodsName' => $goodsName, 'goodsPrice' => $goodsPrice, 'add_time' => $orderInfo['add_time'] ?? time()], 'order_brokerage']); + return true; + } + + + /** + * 发送短信 + * @param $order + * @param $storeTitle + */ + public function smsSend($order, $storeTitle) + { + /** @var SmsSendServices $smsServices */ + $smsServices = app()->make(SmsSendServices::class); + $switch = (bool)sys_config('confirm_take_over_switch'); + //模板变量 + $store_name = $storeTitle; + $order_id = $order['order_id']; + $smsServices->send($switch, $order['user_phone'], compact('store_name', 'order_id'), 'TAKE_DELIVERY_CODE'); + } + + /** + * 发送确认收货管理员短信 + * @param $order + */ + public function smsSendTake($order) + { + $switch = (bool)sys_config('admin_confirm_take_over_switch'); + /** @var StoreServiceServices $services */ + $services = app()->make(StoreServiceServices::class); + $adminList = $services->getStoreServiceOrderNotice(); + SmsAdminJob::dispatchDo('sendAdminConfirmTakeOver', [$switch, $adminList, $order]); + return true; + } + + /** + * 赠送经验 + * @param $order + * @param $userInfo + * @return bool + */ + public function gainUserExp($order, $userInfo) + { + if (!$userInfo) { + return true; + } + //用户等级是否开启 + if (!sys_config('member_func_status', 1)) { + return true; + } + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $order_exp = 0; + $res3 = true; + $order_give_exp = sys_config('order_give_exp'); + $balance = $userInfo['exp']; + if ($order['pay_price'] && $order_give_exp) { + $order_exp = bcmul($order_give_exp, (string)$order['pay_price'], 2); + $balance = bcadd((string)$balance, (string)$order_exp, 2); + $res3 = false != $userBillServices->income('order_give_exp', $order['uid'], $order_exp, $balance, $order['id']); + } + $res = true; + if ($order_exp > 0) { + $userInfo->exp = $balance; + $res1 = false != $userInfo->save(); + $res = $res1 && $res3; + } + /** @var UserLevelServices $levelServices */ + $levelServices = app()->make(UserLevelServices::class); + $levelServices->detection((int)$order['uid']); + return $res; + } + + /** + * 加入队列 + * @param array $where + * @param int $count + * @param int $maxLimit + * @return bool + */ + public function batchJoinJobs(array $where, int $count, int $maxLimit) + { + $page = ceil($count / $maxLimit); + for ($i = 1; $i <= $page; $i++) { + AutoTakeOrderJob::dispatch([$where, $i, $maxLimit]); + } + return true; + } + + /** + * 执行自动收货 + * @param array $where + * @param int $page + * @param int $maxLimit + * @return bool + */ + public function runAutoTakeOrder(array $where, int $page = 0, int $maxLimit = 0) + { + /** @var StoreOrderStoreOrderStatusServices $service */ + $service = app()->make(StoreOrderStoreOrderStatusServices::class); + $orderList = $service->getOrderIds($where, $page, $maxLimit); + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + foreach ($orderList as $order) { + if ($order['status'] == 2) { + continue; + } + if ($order['paid'] == 1 && $order['status'] == 1) { + $data['status'] = 2; + } else if ($order['pay_type'] == 'offline') { + $data['status'] = 2; + } else { + continue; + } + if ($storeOrderRefundServices->count(['store_order_id' => $order['id'], 'refund_type' => [0, 1, 2, 4, 5], 'is_cancel' => 0, 'is_del' => 1])) { + continue; + } + try { + /** @var StoreOrderStatusServices $statusService */ + $statusService = app()->make(StoreOrderStatusServices::class); + $res = $this->dao->update($order['id'], $data) && $statusService->save([ + 'oid' => $order['id'], + 'change_type' => 'take_delivery', + 'change_message' => '已收货[自动收货]', + 'change_time' => time() + ]); + $res = $res && $this->storeProductOrderUserTakeDelivery($order); + if (!$res) { + throw new ValidateException('订单号' . $order['order_id'] . '自动收货失败'); + } + } catch (\Throwable $e) { + Log::error('自动收货失败,失败原因:' . $e->getMessage()); + } + } + return true; + } + + /** + * 自动收货 + * @return bool + */ + public function autoTakeOrder() + { + //7天前时间戳 + $systemDeliveryTime = (int)sys_config('system_delivery_time', 0); + //0为取消自动收货功能 + if ($systemDeliveryTime == 0) { + return true; + } + $sevenDay = strtotime(date('Y-m-d H:i:s', strtotime('-' . $systemDeliveryTime . ' day'))); + /** @var StoreOrderStoreOrderStatusServices $service */ + $service = app()->make(StoreOrderStoreOrderStatusServices::class); + $where = [ + 'change_time' => $sevenDay, + 'is_del' => 0, + 'paid' => 1, + 'status' => 1, + 'change_type' => ['delivery_goods', 'delivery_fictitious', 'delivery', 'city_delivery'] + ]; + $maxLimit = 20; + $count = $service->getOrderCount($where); + if ($count > $maxLimit) { + return $this->batchJoinJobs($where, $count, $maxLimit); + } + return $this->runAutoTakeOrder($where); + } +} diff --git a/app/services/order/store/BranchOrderServices.php b/app/services/order/store/BranchOrderServices.php new file mode 100644 index 0000000..b6828db --- /dev/null +++ b/app/services/order/store/BranchOrderServices.php @@ -0,0 +1,334 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order\store; + + +use app\common\dao\store\order\StoreOrderDao; +use app\services\BaseServices; +use app\services\order\OtherOrderServices; +use app\services\order\StoreOrderRefundServices; +use app\services\order\StoreOrderServices; +use app\services\store\StoreUserServices; +use app\services\user\UserCardServices; +use app\services\user\UserRechargeServices; +use think\exception\ValidateException; + +/** + * Class StoreOrderWapServices + * @package app\services\order + * @mixin StoreOrderDao + */ +class BranchOrderServices extends BaseServices +{ + /** + * StoreOrderWapServices constructor. + * @param StoreOrderDao $dao + */ + public function __construct(StoreOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取订单数量 + * @param int $store_id + * @param int $staff_id + * @return array + */ + public function getOrderData(int $store_id, int $staff_id = 0) + { + $where = ['pid' => 0, 'store_id' => $store_id, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0]; + $data['order_count'] = (string)$this->dao->count($where); + $where = $where + ['paid' => 1]; + $data['sum_price'] = (string)$this->dao->sum($where, 'pay_price', true); + + $countWhere = ['store_id' => $store_id]; + if ($staff_id) { + $countWhere['staff_id'] = $staff_id; + } + $pid_where = ['pid' => 0]; + $not_pid_where = ['not_pid' => 1]; + $data['unpaid_count'] = (string)$this->dao->count(['status' => 0] + $countWhere + $pid_where); + $data['unshipped_count'] = (string)$this->dao->count(['status' => 1] + $countWhere + $pid_where); + $data['unwriteoff_count'] = (string)$this->dao->count(['status' => 5] + $countWhere + $pid_where); + $data['received_count'] = (string)$this->dao->count(['status' => 2] + $countWhere + $pid_where); + $data['evaluated_count'] = (string)$this->dao->count(['status' => 3] + $countWhere + $pid_where); + $data['complete_count'] = (string)$this->dao->count(['status' => 4] + $countWhere + $pid_where); + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + $refund_where = ['store_id' => $store_id, 'is_cancel' => 0]; + $data['refunding_count'] = (string)$storeOrderRefundServices->count($refund_where + ['refund_type' => [1, 2, 4, 5]]); + $data['refunded_count'] = (string)$storeOrderRefundServices->count($refund_where + ['refund_type' => [3, 6]]); + $data['refund_count'] = (string)$storeOrderRefundServices->count($refund_where); + return $data; + } + + /** + * 订单统计详情列表 + * @param int $store_id + * @param int $staff_id + * @param int $type + * @param array $time + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function time(int $store_id, int $staff_id = 0, int $type = 1, array $time = []) + { + if (!$time) { + return [[], []]; + } + [$start, $stop, $front, $front_stop] = $time; + $order_where = ['pid' => 0, 'is_del' => 0, 'paid' => 1, 'store_id' => $store_id]; + if ($type != 3) { + $order_where['refund_status'] = [0, 3]; + } + if ($staff_id) $order_where['staff_id'] = $staff_id; + switch ($type) { + case 1://配送 + case 2://配送 + $order_where['type'] = 7; + break; + case 3://退款 + $order_where['status'] = -3; + break; + case 4://收银订单 + $order_where['type'] = 6; + break; + case 5://核销 + $order_where['type'] = 5; + break; + } + if ($type == 2) {//数量 + $frontPrice = $this->dao->count($order_where + ['time' => [$front, $front_stop]]); + $nowPrice = $this->dao->count($order_where + ['time' => [$start, $stop]]); + } else {//金额 + $frontPrice = $this->dao->sum($order_where + ['time' => [$front, $front_stop]], 'pay_price', true); + $nowPrice = $this->dao->sum($order_where + ['time' => [$start, $stop]], 'pay_price', true); + } + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getOrderList($order_where + ['time' => [$start, $stop]], ['id', 'order_id', 'uid', 'spread_uid', 'pay_price', 'add_time'], $page, $limit); + foreach ($list as &$item) { + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + } + return [[$nowPrice, $frontPrice], $list]; + } + + /** + * 订单每月统计数据(按天分组) + * @param array $where + * @param array|string[] $field + * @return array + */ + public function getOrderDataPriceCount(array $where, array $field = ['sum(pay_price) as price', 'count(id) as count', 'FROM_UNIXTIME(add_time, \'%m-%d\') as time']) + { + [$page, $limit] = $this->getPageValue(); + $order_where = ['is_del' => 0, 'is_system_del' => 0, 'paid' => 1, 'refund_status' => [0, 3]]; + $where = array_merge($where, $order_where); + return $this->dao->getOrderDataPriceCount($where, $field, $page, $limit); + } + + /** + * 获取订单列表 + * @param array $where + * @param array $with + * @param false $is_count + * @return array|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreOrderList(array $where, array $field = ['*'], array $with = [], $is_count = false) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getOrderList($where, $field, $page, $limit, $with, 'id desc'); + if ($is_count) { + $count = $this->dao->count($where); + return compact('list', 'count'); + } + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $list = $orderServices->tidyOrderList($list); + foreach ($list as &$item) { + $refund_num = array_sum(array_column($item['refund'], 'refund_num')); + $cart_num = 0; + foreach ($item['_info'] as $items) { + if (isset($items['cart_info']['is_gift']) && $items['cart_info']['is_gift']) continue; + $cart_num += $items['cart_info']['cart_num']; + } + $item['is_all_refund'] = $refund_num == $cart_num; + } + return $list; + } + + /** + * 取消订单 + * @param $id + * @param int $store_id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function cancelOrder($id, int $store_id = 0) + { + $where = ['id' => $id, 'is_del' => 0, 'store_id' => $store_id]; + + $order = $this->dao->getOne($where); + if (!$order) { + throw new ValidateException('没有查到此订单'); + } + if ($order->paid) { + throw new ValidateException('订单已经支付无法取消'); + } + /** @var StoreOrderRefundServices $refundServices */ + $refundServices = app()->make(StoreOrderRefundServices::class); + $this->transaction(function () use ($refundServices, $order) { + //回退积分和优惠卷 + $res = $refundServices->integralAndCouponBack($order); + //回退库存和销量 + $res = $res && $refundServices->regressionStock($order); + $order->is_del = 1; + if (!($res && $order->save())) { + throw new ValidateException('取消订单失败'); + } + }); + return true; + } + + /** + * 门店首页头部统计 + * @param int $store_id + * @param $time + * @return array + */ + public function homeStatics(int $store_id, $time) + { + $data = []; + $where = ['time' => $time]; + if ($store_id) $where['store_id'] = $store_id; + + $order_where = ['paid' => 1, 'pid' => 0, 'is_system_del' => 0, 'refund_status' => [0, 3]]; + //门店营收 + $data['store_income'] = $this->dao->sum($order_where + $where, 'pay_price', true); + //消耗余额 + $data['store_use_yue'] = $this->dao->sum(['pay_type' => 'yue'] + $order_where + $where, 'pay_price', true); + //收银订单 + $data['cashier_order_price'] = $this->dao->sum(['type' => 6] + $order_where + $where, 'pay_price', true); + //分配订单 + $data['store_order_price'] = $this->dao->sum(['type' => 7] + $order_where + $where, 'pay_price', true); + //核销订单 + $data['store_writeoff_order_price'] = $this->dao->sum(['shipping_type' => 2] + $order_where + $where, 'pay_price', true); + /** @var StoreUserServices $storeUserServices */ + $storeUserServices = app()->make(StoreUserServices::class); + $data['store_user_count'] = $storeUserServices->count($where); + //门店成交用户数 + $data['store_pay_user_count'] = count(array_unique($this->dao->getColumn($order_where + $where, 'uid', '', true))); + /** @var OtherOrderServices $vipOrderServices */ + $vipOrderServices = app()->make(OtherOrderServices::class); + $data['vip_price'] = $vipOrderServices->sum(['paid' => 1, 'type' => [0, 1, 2, 4]] + $where, 'pay_price', true); + /** @var UserRechargeServices $userRecharge */ + $userRecharge = app()->make(UserRechargeServices::class); + $data['recharge_price'] = $userRecharge->sum(['paid' => 1] + $where, 'price', true); + /** @var UserCardServices $userCard */ + $userCard = app()->make(UserCardServices::class); + $data['card_count'] = $userCard->count($where + ['is_submit' => 1]); + return $data; + } + + /** + * 门店首页运营统计 + * @param int $store_id + * @param array $time + * @return array + */ + public function operateChart(int $store_id, array $time) + { + [$start, $end, $timeType, $timeKey] = $time; + $where = []; + if ($store_id == -1) { + $where[] = ['store_id', '>', 0]; + } else { + $where['store_id'] = $store_id; + } + $order = $this->dao->orderAddTimeList($where, [$start, $end], $timeType); + /** @var StoreUserServices $storeUserServices */ + $storeUserServices = app()->make(StoreUserServices::class); + $storeUser = $storeUserServices->userTimeList($where, [$start, $end], $timeType); + + $order = array_column($order, 'price', 'day'); + $storeUser = array_column($storeUser, 'count', 'day'); + + $data = $series = []; + $xAxis = []; + foreach ($timeKey as $key) { + $data['门店收款'][] = isset($order[$key]) ? floatval($order[$key]) : 0; + $data['新增用户数'][] = isset($storeUser[$key]) ? floatval($storeUser[$key]) : 0; + $xAxis[] = date('m-d', strtotime($key)); + } + foreach ($data as $key => $item) { + $series[] = [ + 'name' => $key, + 'data' => $item, + 'type' => 'line', + 'smooth' => 'true', + 'yAxisIndex' => 1, + ]; + } + return compact('xAxis', 'series'); + + } + + /** + * 首页交易统计 + * @param int $store_id + * @param array $time + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function orderChart(int $store_id, array $time) + { + $chartdata = []; + $where = ['time' => $time, 'pid' => 0]; + if ($store_id) $where['store_id'] = $store_id; + + $order_where = ['paid' => 1, 'pid' => 0, 'is_system_del' => 0, 'refund_status' => [0, 3]]; + + $list = $this->dao->getOrderList($where + $order_where, ['id', 'order_id', 'uid', 'pay_price', 'pay_time'], 0, 10); + $chartdata['order_list'] = $list; + + $chartdata['bing_xdata'] = ['收银订单', '充值订单', '分配订单', '核销订单', '付费会员订单']; + $color = ['#2EC479', '#7F7AE5', '#FFA21B', '#46A3FF', '#FF6046']; + //收银订单 + $pay[] = $this->dao->sum(['type' => 6] + $order_where + $where, 'pay_price', true); + /** @var UserRechargeServices $userRecharge */ + $userRecharge = app()->make(UserRechargeServices::class); + $pay[] = $userRecharge->sum(['paid' => 1] + $where, 'price', true); + //分配订单 + $pay[] = $this->dao->sum(['type' => 7] + $order_where + $where, 'pay_price', true); + //核销订单 + $pay[] = $this->dao->sum(['type' => 5] + $order_where + $where, 'pay_price', true); + + /** @var OtherOrderServices $vipOrderServices */ + $vipOrderServices = app()->make(OtherOrderServices::class); + $pay[] = $vipOrderServices->sum(['paid' => 1, 'type' => [0, 1, 2, 4]] + $where, 'pay_price', true); + foreach ($pay as $key => $item) { + $bing_data[] = ['name' => $chartdata['bing_xdata'][$key], 'value' => $pay[$key], 'itemStyle' => ['color' => $color[$key]]]; + } + $chartdata['bing_data'] = $bing_data; + return $chartdata; + } + +} diff --git a/app/services/order/store/WriteOffOrderServices.php b/app/services/order/store/WriteOffOrderServices.php new file mode 100644 index 0000000..32cbb51 --- /dev/null +++ b/app/services/order/store/WriteOffOrderServices.php @@ -0,0 +1,503 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\order\store; + + +use app\common\dao\store\order\StoreOrderDao; +use app\services\order\StoreOrderCartInfoServices; +use app\services\order\StoreOrderCreateServices; +use app\services\order\StoreOrderRefundServices; +use app\services\order\StoreOrderTakeServices; +use app\services\activity\integral\StoreIntegralOrderServices; +use app\services\activity\integral\StoreIntegralOrderStatusServices; +use app\services\activity\combination\StorePinkServices; +use app\services\BaseServices; +use app\services\store\SystemStoreStaffServices; +use app\services\store\DeliveryServiceServices; +use app\services\user\UserServices; +use crmeb\services\FormBuilder as Form; +use think\exception\ValidateException; + +/** + * 核销订单 + * Class StoreOrderWriteOffServices + * @package app\sservices\order + * @mixin StoreOrderDao + */ +class WriteOffOrderServices extends BaseServices +{ + + /** + * 构造方法 + * StoreOrderWriteOffServices constructor. + * @param StoreOrderDao $dao + */ + public function __construct(StoreOrderDao $dao) + { + $this->dao = $dao; + } + + /** + * 验证核销订单权限 + * @param int $uid + * @param array $orderInfo + * @param int $auth 0:管理员 1门店 2配送员 + * @param int $staff_id + * @return bool + */ + public function checkAuth(int $uid, array $orderInfo, int $auth = 1, int $staff_id = 0) + { + if (($auth > 0 && !$uid && !$staff_id) || !$orderInfo) { + throw new ValidateException('订单不存在'); + } + $store_id = $orderInfo['store_id'] ?? 0; + $info = $this->checkUserAuth($uid, $auth, $store_id, $staff_id); + $isAuth = true; + switch ($auth) { + case 0://管理员 + break; + case 1://门店 + if ($orderInfo['shipping_type'] == 2 && $info && isset($info['store_id']) && $info['store_id'] == $store_id) { + $isAuth = true; + } else { + $isAuth = false; + } + break; + case 2://配送员 + if (in_array($orderInfo['shipping_type'], [1, 3]) && $info && $orderInfo['delivery_type'] == 'send' && $orderInfo['delivery_uid'] == $uid) { + $isAuth = true; + } else { + $isAuth = false; + } + break; + } + if (!$isAuth) { + throw new ValidateException('您无权限核销此订单,请联系管理员'); + } + return true; + } + + /** + * 验证核销权限 + * @param int $uid + * @param int $auth + * @param int $store_id + * @param int $staff_id + * @return array|\think\Model + */ + public function checkUserAuth(int $uid, int $auth = 1, int $store_id = 0, int $staff_id = 0) + { + if ($auth > 0 && !$uid && !$staff_id) { + throw new ValidateException('用户不存在'); + } + $isAuth = true; + $info = []; + switch ($auth) { + case 0://管理员 + break; + case 1://门店 + //验证店员 + /** @var SystemStoreStaffServices $storeStaffServices */ + $storeStaffServices = app()->make(SystemStoreStaffServices::class); + try { + if ($staff_id) { + $info = $storeStaffServices->getStaffInfo($staff_id); + } else { + $info = $storeStaffServices->getStaffInfoByUid($uid, $store_id); + } + } catch (\Throwable $e) { + + } + if ($info && $info['verify_status'] == 1) { + $isAuth = true; + } + break; + case 2://配送员 + /** @var DeliveryServiceServices $deliverServiceServices */ + $deliverServiceServices = app()->make(DeliveryServiceServices::class); + try { + $info = $deliverServiceServices->getDeliveryInfoByUid($uid, 1, (int)$store_id); + } catch (\Throwable $e) { + + } + + if ($info) { + $isAuth = true; + } + break; + } + if (!$isAuth) { + throw new ValidateException('您无权限核销,请联系管理员'); + } + return $info; + } + + /** + * 用户码获取待核销订单列表 + * @param int $uid + * @param string $code + * @param int $auth + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function userUnWriteoffOrder(int $uid, string $code, int $auth = 1) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getOne(['bar_code' => $code]); + if (!$userInfo) { + throw new ValidateException('该用户不存在'); + } + $info = $this->checkUserAuth($uid, $auth); + $unWriteoffOrder = []; + if ($info && isset($info['store_id'])) { + if ($auth == 1) {//店员 + $where = ['store_id' => $info['store_id']]; + } else {//配送员 + $where = ['delivery_uid' => $info['uid']]; + } + $unWriteoffOrder = $this->dao->getUnWirteOffList(['uid' => $userInfo['uid']] + $where, ['id']); + } + $data = []; + if ($unWriteoffOrder) { + foreach ($unWriteoffOrder as $item) { + try { + $orderInfo = $this->writeoffOrderInfo($uid, '', $auth, $item['id']); + } catch (\Throwable $e) {//无权限或其他异常不返回订单信息 + $orderInfo = []; + } + if ($orderInfo) $data[] = $orderInfo; + } + } + return $data; + } + + /** + * 获取核销订单信息 + * @param int $uid + * @param string $code + * @param int $auth + * @param int $oid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function writeoffOrderInfo(int $uid, string $code = '', int $auth = 1, int $oid = 0, int $staff_id = 0) + { + if ($oid) { + //订单 + $orderInfo = $this->dao->getOne(['id' => $oid, 'is_del' => 0], '*', ['user', 'pink']); + $order_type = 'order'; + if (!$orderInfo) { + //积分兑换订单 + /** @var StoreIntegralOrderServices $storeIntegralOrderServices */ + $storeIntegralOrderServices = app()->make(StoreIntegralOrderServices::class); + $orderInfo = $storeIntegralOrderServices->getOne(['id' => $oid]); + $order_type = 'integral'; + } + } else { + //订单 + $orderInfo = $this->dao->getOne(['verify_code' => $code, 'is_del' => 0], '*', ['user', 'pink']); + $order_type = 'order'; + if (!$orderInfo) { + //积分兑换订单 + /** @var StoreIntegralOrderServices $storeIntegralOrderServices */ + $storeIntegralOrderServices = app()->make(StoreIntegralOrderServices::class); + $orderInfo = $storeIntegralOrderServices->getOne(['verify_code' => $code]); + $order_type = 'integral'; + } + } + + if (!$orderInfo) { + throw new ValidateException('Write off order does not exist'); + } + if ($order_type == 'order' && !$orderInfo['paid']) { + throw new ValidateException('订单还未完成支付'); + } + if ($order_type == 'order' && $orderInfo['refund_status'] != 0) { + throw new ValidateException('该订单状态暂不支持核销'); + } + + $orderInfo['order_type'] = $order_type; + $orderInfo = $orderInfo->toArray(); + //验证权限 + $this->checkAuth($uid, $orderInfo, $auth, $staff_id); + if ($order_type == 'order') { + /** @var StoreOrderCartInfoServices $cartServices */ + $cartServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfo = $cartServices->getCartInfoList(['oid' => $orderInfo['id']], ['id', 'oid', 'write_times', 'write_surplus_times', 'write_start', 'write_end']); + $orderInfo['write_off'] = $orderInfo['write_times'] = 0; + $orderInfo['write_day'] = ''; + $cart = $cartInfo[0] ?? []; + if ($orderInfo['product_type'] == 4 && $cart) {//次卡商品 + $orderInfo['write_off'] = max(bcsub((string)$cart['write_times'], (string)$cart['write_surplus_times'], 0), 0); + $orderInfo['write_times'] = $cart['write_times'] ?? 0; + $start = $cart['write_start'] ?? 0; + $end = $cart['write_end'] ?? 0; + if (!$start && !$end) { + $orderInfo['write_day'] = '不限时'; + } else { + $orderInfo['write_day'] = ($start ? date('Y-m-d', $start) : '') . '/' . ($end ? date('Y-m-d', $end) : ''); + } + } + } + return $orderInfo; + } + + /** + * 获取订单商品信息 + * @param int $uid + * @param int $id + * @param int $auth + * @param int $staff_id + * @param bool $isCasher + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderCartInfo(int $uid, int $id, int $auth = 1, int $staff_id = 0, bool $isCasher = false) + { + if ($isCasher) {//获取订单信息 暂时不验证权限 + $orderInfo = $this->dao->getOne(['id' => $id], '*', ['user', 'pink']); + if (!$orderInfo) { + throw new ValidateException('Write off order does not exist'); + } + $orderInfo = $orderInfo->toArray(); + } else { + $orderInfo = $this->writeoffOrderInfo($uid, '', $auth, $id, $staff_id); + } + $writeoff_count = 0; + /** @var StoreOrderCartInfoServices $cartInfoServices */ + $cartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfo = $cartInfoServices->getCartColunm(['oid' => $orderInfo['id']], 'id,cart_id,cart_num,surplus_num,is_writeoff,cart_info,product_type,is_support_refund,is_gift,write_times,write_surplus_times'); + foreach ($cartInfo as &$item) { + $_info = is_string($item['cart_info']) ? json_decode($item['cart_info'], true) : $item['cart_info']; + if (!isset($_info['productInfo'])) $_info['productInfo'] = []; + //缩略图处理 + if (isset($_info['productInfo']['attrInfo'])) { + $_info['productInfo']['attrInfo'] = get_thumb_water($_info['productInfo']['attrInfo']); + } + $_info['productInfo'] = get_thumb_water($_info['productInfo']); + $item['cart_info'] = $_info; + if ($item['write_times'] > $item['write_surplus_times']) { + $writeoff_count = bcadd((string)$writeoff_count, (string)bcsub((string)$item['write_times'], (string)$item['write_surplus_times'])); + } + $item['surplus_num'] = $item['write_surplus_times']; + unset($_info); + } + $orderInfo['cart_count'] = count($cartInfo); + $orderInfo['writeoff_count'] = $writeoff_count; + $orderInfo['cart_info'] = $cartInfo; + return $orderInfo; + } + + /** + * 核销订单 + * @param int $uid + * @param array $orderInfo + * @param array $cartIds + * @param int $auth + * @return array|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function writeoffOrder(int $uid, array $orderInfo, array $cartIds = [], int $auth = 1, int $staff_id = 0) + { + if (!$orderInfo) { + throw new ValidateException('订单不存在'); + } + //默认正常订单 + $orderInfo['order_type'] = $orderInfo['order_type'] ?? 'order'; + $time = time(); + if ($orderInfo['order_type'] == 'order') { + //验证核销权限 + $this->checkAuth($uid, $orderInfo, $auth, $staff_id); + + if (!$orderInfo['verify_code'] || ($orderInfo['shipping_type'] != 2 && $orderInfo['delivery_type'] != 'send')) { + throw new ValidateException('此订单不能被核销'); + } + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + if ($storeOrderRefundServices->count(['store_order_id' => $orderInfo['id'], 'refund_type' => [1, 2, 4, 5, 6], 'is_cancel' => 0, 'is_del' => 0])) { + throw new ValidateException('订单有售后申请请先处理'); + } + if (isset($orderInfo['pinkStatus']) && $orderInfo['pinkStatus'] != 2) { + throw new ValidateException('拼团未完成暂不能核销!'); + } + /** @var StoreOrderCartInfoServices $cartInfoServices */ + $cartInfoServices = app()->make(StoreOrderCartInfoServices::class); + if ($orderInfo['status'] >= 2 && !$cartInfoServices->count(['oid' => $orderInfo['id'], 'is_writeoff' => 0])) { + throw new ValidateException('订单已核销'); + } + $store_id = $orderInfo['store_id']; + if ($orderInfo['type'] == 3 && $orderInfo['activity_id'] && $orderInfo['pink_id']) { + /** @var StorePinkServices $services */ + $services = app()->make(StorePinkServices::class); + $res = $services->getCount([['id', '=', $orderInfo['pink_id']], ['status', '<>', 2]]); + if ($res) throw new ValidateException('Failed to write off the group order'); + } + + $cartInfo = []; + if ($cartIds) {//商城存在部分核销 + $ids = array_unique(array_column($cartIds, 'cart_id')); + //订单下原商品信息 + $cartInfo = $cartInfoServices->getCartColunm(['oid' => $orderInfo['id'], 'cart_id' => $ids, 'is_writeoff' => 0], 'id,cart_id,cart_num,surplus_num,product_id,write_times,write_surplus_times,write_start,write_end', 'cart_id'); + if (count($ids) != count($cartInfo)) { + throw new ValidateException('订单中有商品已核销'); + } + foreach ($cartIds as $cart) { + $info = $cartInfo[$cart['cart_id']] ?? []; + if (!$info) { + throw new ValidateException('核销商品不存在'); + } + if ($cart['cart_num'] > $info['write_surplus_times']) { + throw new ValidateException('核销数量超出剩余总核销次数'); + } + } + } else {//整单核销 + $cartInfo = $cartInfoServices->getCartColunm(['oid' => $orderInfo['id'], 'is_writeoff' => 0], 'id,cart_id,cart_num,surplus_num,product_id,write_times,write_surplus_times,write_start,write_end', 'cart_id'); + } + foreach ($cartInfo as $info) { + if ($info['write_start'] && $time < $info['write_start']) { + throw new ValidateException('还未到指定核销的开始时间,无法核销'); + } + if ($info['write_end'] && $time > $info['write_end']) { + throw new ValidateException('已经超过指定核销的结束时间,无法核销'); + } + } + + $data = ['clerk_id' => $uid]; + $cartData = ['writeoff_time' => $time]; + if ($auth == 1) {//店员 + /** @var SystemStoreStaffServices $storeStaffServices */ + $storeStaffServices = app()->make(SystemStoreStaffServices::class); + if ($uid) {//商城前端 + $staffInfo = $storeStaffServices->getStaffInfoByUid($uid, $store_id); + } else {//门店后台 + $staffInfo = $storeStaffServices->getStaffInfo($staff_id); + if ($store_id != $staffInfo['store_id']) { + throw new ValidateException('订单不存在'); + } + if ($staffInfo['verify_status'] != 1) { + throw new ValidateException('您暂无核销权限'); + } + $data['clerk_id'] = $staffInfo['uid']; + } + $data['staff_id'] = $staffInfo['id'] ?? 0; + $cartData['staff_id'] = $staffInfo['id'] ?? 0; + } else if ($auth == 2) {//配送员 + /** @var DeliveryServiceServices $deliverServiceServices */ + $deliverServiceServices = app()->make(DeliveryServiceServices::class); + $deliveryInfo = $deliverServiceServices->getDeliveryInfoByUid($uid, 1, (int)$store_id); + $cartData['delivery_id'] = $deliveryInfo['id'] ?? 0; + } + $data = $this->transaction(function () use ($orderInfo, $staff_id, $data, $cartIds, $cartInfoServices, $cartData, $auth, $cartInfo) { + if ($cartIds) {//选择商品、件数核销 + foreach ($cartIds as $cart) { + $write_surplus_num = $cartInfo[$cart['cart_id']]['write_surplus_times'] ?? 0; + if (!isset($cartInfo[$cart['cart_id']]) || !$write_surplus_num) continue; + if ($cart['cart_num'] >= $write_surplus_num) {//拆分完成 + $cartData['write_surplus_times'] = 0; + $cartData['is_writeoff'] = 1; + } else {//拆分部分数量 + $cartData['write_surplus_times'] = bcsub((string)$write_surplus_num, $cart['cart_num'], 0); + $cartData['is_writeoff'] = 0; + } + //修改原来订单商品信息 + $cartInfoServices->update(['oid' => $orderInfo['id'], 'cart_id' => $cart['cart_id']], $cartData); + } + } else {//整单核销 + //修改原来订单商品信息 + $cartData['is_writeoff'] = 1; + $cartData['write_surplus_times'] = 0; + $cartInfoServices->update(['oid' => $orderInfo['id']], $cartData); + } + if (!$cartInfoServices->count(['oid' => (int)$orderInfo['id'], 'is_writeoff' => 0])) {//全部核销 + if ($orderInfo['type'] == 8) { + $data['status'] = 3; + } else { + $data['status'] = 2; + } + /** @var StoreOrderTakeServices $storeOrdeTask */ + $storeOrdeTask = app()->make(StoreOrderTakeServices::class); + $re = $storeOrdeTask->storeProductOrderUserTakeDelivery($orderInfo); + if (!$re) { + throw new ValidateException('Write off failure'); + } + } else {//部分核销 + /** @var StoreOrderCreateServices $storeOrderCreateServices */ + $storeOrderCreateServices = app()->make(StoreOrderCreateServices::class); + $data['verify_code'] = $storeOrderCreateServices->getStoreCode(); + $data['status'] = 5; + } + if (!$this->dao->update($orderInfo['id'], $data)) { + throw new ValidateException('Write off failure'); + } + return $data; + }); + event('order.writeoff', [$orderInfo, $auth, $data, $cartIds, $cartInfo]); + } else {//积分订单 + if ($orderInfo['status'] == 3) { + throw new ValidateException('订单已核销'); + } + $data = ['status' => 3]; + /** @var StoreIntegralOrderServices $storeIntegralOrderServices */ + $storeIntegralOrderServices = app()->make(StoreIntegralOrderServices::class); + if (!$storeIntegralOrderServices->update($orderInfo['id'], $data)) { + throw new ValidateException('Write off failure'); + } + //增加收货订单状态 + /** @var StoreIntegralOrderStatusServices $statusService */ + $statusService = app()->make(StoreIntegralOrderStatusServices::class); + $statusService->save([ + 'oid' => $orderInfo['id'], + 'change_type' => 'take_delivery', + 'change_message' => '已收货', + 'change_time' => time() + ]); + } + return $orderInfo; + } + + /** + * 次卡商品核销表单 + * @param int $id + * @param int $staffId + * @param int $cart_num + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function writeOrderFrom(int $id, int $staffId, int $cart_num = 1) + { + $orderInfo = $this->getOrderCartInfo(0, (int)$id, 1, (int)$staffId); + $cartInfo = $orderInfo['cart_info'] ?? []; + if (!$cartInfo) { + throw new ValidateException('核销订单商品信息不存在'); + } + if ($orderInfo['product_type'] != 4) { + throw new ValidateException('订单商品不支持此类型核销'); + } + $name = ($cartInfo[0]['write_surplus_times'] ?? 0) . '/'. ($cartInfo[0]['write_times'] ?? 0); + $f[] = Form::hidden('cart_id', $cartInfo[0]['cart_id'] ?? 0); + $f[] = Form::input('name', '核销数', $name)->disabled(true); + $f[] = Form::number('cart_num', '本次核销数量', min(max($cart_num, 1), $cartInfo[0]['write_surplus_times'] ?? 0))->min(1)->max($cartInfo[0]['write_surplus_times'] ?? 1); + return create_form('次卡核销', $f, $this->url('/order/write/form/' . $id), 'POST'); + } + +} diff --git a/app/services/other/CacheServices.php b/app/services/other/CacheServices.php new file mode 100644 index 0000000..d6c907a --- /dev/null +++ b/app/services/other/CacheServices.php @@ -0,0 +1,102 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\other; + + +use app\dao\other\CacheDao; +use app\services\BaseServices; + +/** + * Class CacheServices + * @package app\services\other + * @mixin CacheDao + */ +class CacheServices extends BaseServices +{ + + public function __construct(CacheDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取数据缓存 + * @param string $key + * @param string|callable|int|array $default 默认值不存在则写入 + * @param int $expire + * @return mixed|null + */ + public function getDbCache(string $key, $default, int $expire = 0) + { + $this->dao->delectDeOverdueDbCache(); + $result = $this->dao->value(['key' => $key], 'result'); + if ($result) { + return json_decode($result, true); + } else { + if ($default instanceof \Closure) { + // 获取缓存数据 + $value = $default(); + if ($value) { + $this->setDbCache($key, $value, $expire); + return $value; + } + } else { + $this->setDbCache($key, $default, $expire); + return $default; + } + return null; + } + + } + + /** + * 设置数据缓存存在则更新,没有则写入 + * @param string $key + * @param $result + * @param $expire + * @return \crmeb\basic\BaseModel|mixed|\think\Model + * @throws \Exception + */ + public function setDbCache(string $key, $result, $expire = 0) + { + $this->dao->delectDeOverdueDbCache(); + $addTime = $expire ? time() + $expire : 0; + if ($this->dao->count(['key' => $key])) { + return $this->dao->update($key, [ + 'result' => json_encode($result), + 'expire_time' => $addTime, + 'add_time' => time() + ], 'key'); + } else { + return $this->dao->save([ + 'key' => $key, + 'result' => json_encode($result), + 'expire_time' => $addTime, + 'add_time' => time() + ]); + } + } + + + /** + * 删除某个缓存 + * @param string $key + */ + public function delectDbCache(string $key = '') + { + if ($key) + return $this->dao->delete($key, 'key'); + else + return false; + } + +} diff --git a/app/services/other/CategoryServices.php b/app/services/other/CategoryServices.php new file mode 100644 index 0000000..ababc28 --- /dev/null +++ b/app/services/other/CategoryServices.php @@ -0,0 +1,71 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\other; + + +use app\dao\other\CategoryDao; +use app\services\BaseServices; +use crmeb\traits\ServicesTrait; + +/** + * Class CategoryServices + * @package app\services\other + * @mixin CategoryDao + */ +class CategoryServices extends BaseServices +{ + + use ServicesTrait; + + protected $cacheName = 'crmeb_cate'; + + /** + * CategoryServices constructor. + * @param CategoryDao $dao + */ + public function __construct(CategoryDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取分类列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCateList(array $where = [], array $field = ['*'], array $with = []) + { + [$page, $limit] = $this->getPageValue(); + $data = $this->dao->getCateList($where, $page, $limit, $field, $with); + $count = $this->dao->count($where); + return compact('data', 'count'); + } + + /**桌码管理 + * @param array $where + * @param array $field + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTableCodeCateList(array $where = [], array $field = ['*'], array $with = []) + { + $data = $this->dao->getCateList($where, 0, 0, $field, $with); + return $data; + } + +} diff --git a/app/services/other/CityAreaServices.php b/app/services/other/CityAreaServices.php new file mode 100644 index 0000000..f1f3b0e --- /dev/null +++ b/app/services/other/CityAreaServices.php @@ -0,0 +1,187 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\other; + + +use app\dao\other\CityAreaDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use crmeb\services\FormBuilder as Form; + +/** + * 城市数据(街道) + * Class CityAreaServices + * @package app\services\other + * @mixin CityAreaDao + */ +class CityAreaServices extends BaseServices +{ + + /** + * 城市类型 + * @var string[] + */ + public $type = [ + '1' => 'province', + '2' => 'city', + '3' => 'area', + '4' => 'street' + ]; + + /** + * CityAreaServices constructor. + * @param CityAreaDao $dao + */ + public function __construct(CityAreaDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取某一个城市id相关上级所有ids + * @param int $id + * @return array|int[] + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRelationCityIds(int $id) + { + $cityInfo = $this->dao->get($id); + $ids = []; + if ($cityInfo) { + $ids = explode('/', trim($cityInfo['path'], '/')); + } + return array_merge([$id], $ids); + } + + /** + * @param int $id + * @param int $expire + * @return bool|mixed|null + */ + public function getRelationCityIdsCache(int $id, int $expire = 1800) + { + return CacheService::redisHandler('apiCity')->remember('city_ids_' . $id, function () use ($id) { + $cityInfo = $this->dao->get($id); + $ids = []; + if ($cityInfo) { + $ids = explode('/', trim($cityInfo['path'], '/')); + } + return array_merge([$id], $ids); + }, $expire); + } + + + /** + * 获取城市数据 + * @param int $pid + * @param int $type 1:省市 2:省市区 0、3:省市区街道 + * @return false|mixed|string|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCityTreeList(int $pid = 0, int $type = 0) + { + $cityList = $this->dao->cacheStrRemember('pid_' . $pid, function () use ($pid) { + $parent_name = '中国'; + if ($pid) { + $city = $this->dao->get($pid); + $parent_name = $city ? $city['name'] : ''; + } + $cityList = $this->dao->getCityList(['parent_id' => $pid], 'id as value,id,name as label,parent_id as pid,level', ['children']); + foreach ($cityList as &$item) { + $item['parent_name'] = $parent_name; + if (isset($item['children']) && $item['children']) { + $item['children'] = []; + $item['loading'] = false; + $item['_loading'] = false; + } else { + unset($item['children']); + } + } + return $cityList; + }); + if ($cityList) { + switch ($type) { + case 0: + case 3: + break; + case 1://控制children 前端不能请求下一级数据 + foreach ($cityList as &$item) { + if ($item['level'] == 2) { + unset($item['children'], $item['loading'], $item['_loading']); + } + } + break; + case 2: + foreach ($cityList as &$item) { + if ($item['level'] == 3) { + unset($item['children'], $item['loading'], $item['_loading']); + } + } + break; + } + } + + return $cityList; + } + + /** + * 添加城市数据表单 + * @param int $parentId + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createCityForm(int $parentId) + { + $info = []; + if ($parentId) { + $info = $this->dao->get($parentId); + } + $field[] = Form::hidden('level', $info['level'] ?? 0); + $field[] = Form::hidden('parent_id', $info['id'] ?? 0); + $field[] = Form::input('parent_name', '父类名称', $info['name'] ?? '中国')->disabled(true); + $field[] = Form::input('name', '名称')->required('请填写城市名称'); + return create_form('添加城市', $field, $this->url('/setting/city/save')); + } + + /** + * 添加城市数据创建 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function updateCityForm(int $id) + { + $info = $this->dao->get($id); + if (!$info) { + throw new AdminException('需改的数据不存在'); + } + if ($info['parent_id']) { + $city = $this->dao->get($info['parent_id']); + $info['parent_name'] = $city['name']; + } + $info = $info->toArray(); + $field[] = Form::hidden('id', $info['id']); + $field[] = Form::hidden('level', $info['level']); + $field[] = Form::hidden('parent_id', $info['parent_id']); + $field[] = Form::input('parent_name', '父类名称', $info['parent_name'] ?? '中国')->disabled(true); + $field[] = Form::input('name', '名称', $info['name'])->required('请填写城市名称'); + return create_form('修改城市', $field, $this->url('/setting/city/save')); + } +} diff --git a/app/services/other/ExpressServices.php b/app/services/other/ExpressServices.php new file mode 100644 index 0000000..bebb4b0 --- /dev/null +++ b/app/services/other/ExpressServices.php @@ -0,0 +1,297 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\other; + + +use app\dao\other\ExpressDao; +use app\services\BaseServices; +use app\services\serve\ServeServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use crmeb\services\ExpressService; +use crmeb\services\FormBuilder as Form; + +/** + * 物流数据 + * Class ExpressServices + * @package app\services\other + * @mixin ExpressDao + */ +class ExpressServices extends BaseServices +{ + + public $_cacheKey = "plat_express_list"; + + //物流查询物流公司code + public $express_code = [ + 'yunda' => 'yunda', + 'yundakuaiyun' => 'yunda56', + 'ems' => 'EMS', + 'youzhengguonei' => 'chinapost', + 'huitongkuaidi' => 'HTKY', + 'baishiwuliu' => 'BSKY', + 'shentong' => 'STO', + 'jd' => 'JD', + 'zhongtong' => 'ZTO', + 'zhongtongkuaiyun' => 'ZTO56', + ]; + + /** + * 构造方法 + * ExpressServices constructor. + * @param ExpressDao $dao + */ + public function __construct(ExpressDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取物流信息 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExpressList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getExpressList($where, '*', $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + public function apiExpressList() + { + return $this->dao->getExpressList([], '*', 0, 0); + } + + /** + * 物流表单 + * @param array $formData + * @return mixed + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createExpressForm(array $formData = []) + { + if (isset($formData['partner_id']) && $formData['partner_id'] == 1) $field[] = Form::input('account', '月结账号', $formData['account'] ?? ''); + if (isset($formData['partner_key']) && $formData['partner_key'] == 1) $field[] = Form::input('key', '月结密码', $formData['key'] ?? ''); + if (isset($formData['net']) && $formData['net'] == 1) $field[] = Form::input('net_name', '取件网点', $formData['net_name'] ?? ''); + if (isset($formData['check_man']) && $formData['check_man'] == 1) $field[] = Form::input('courier_name', '承载快递员名', $formData['courier_name'] ?? '')->required(); + if (isset($formData['partner_name']) && $formData['partner_name'] == 1) $field[] = Form::input('customer_name', '客户账户名称', $formData['customer_name'] ?? '')->required(); + if (isset($formData['is_code']) && $formData['is_code'] == 1) $field[] = Form::input('code_name', '电子面单承载编号', $formData['code_name'] ?? '')->required(); + $field[] = Form::number('sort', '排序', (int)($formData['sort'] ?? 0))->min(0); + $field[] = Form::radio('is_show', '是否启用', $formData['is_show'] ?? 1)->options([['value' => 0, 'label' => '隐藏'], ['value' => 1, 'label' => '启用']]); + return $field; + } + + /** + * 创建物流信息表单获取 + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createForm() + { + return create_form('添加物流公司', $this->createExpressForm(), $this->url('/freight/express')); + } + + /** + * 修改物流信息表单获取 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function updateForm(int $id) + { + $express = $this->dao->get($id); + if (!$express) { + throw new AdminException('查询数据失败,无法修改'); + } + return create_form('编辑物流公司', $this->createExpressForm($express->toArray()), $this->url('/freight/express/' . $id), 'PUT'); + } + + /** + * 平台获取快递 + * @return array|mixed + */ + public function getPlatExpress() + { + /** @var ServeServices $expressService */ + $expressService = app()->make(ServeServices::class); + /** @var CacheService $cacheService */ + $cacheService = app()->make(CacheService::class); + $data = []; + if ($list = $cacheService::get($this->_cacheKey)) { + $data = json_decode($list, true); + } else { + $list = $expressService->express()->express(0, 0, 1000); + if (isset($list['data'])) { + $cacheService->set($this->_cacheKey, json_encode($list['data']), 3600); + $data = $list['data']; + } + } + return $data; + } + + /** + * 获取物流信息组合成新的数组返回 + * @param array $where + * @return array + */ + public function express(array $where = [], string $k = 'id') + { + $list = $this->expressList($where); + $data = []; + if ($list) { + foreach ($list as $k => $v) { + $data[$k]['id'] = $v['id']; + $data[$k]['value'] = $v['name']; + $data[$k]['code'] = $v['code']; + } + } + return $data; + } + + /** + * 获取物流信息组合成新的数组返回 + * @param array $where + * @return array + */ + public function expressSelectForm(array $where = []) + { + $list = $this->expressList(); + //$list = $this->dao->getExpress($where, 'name', 'id'); + $data = []; + foreach ($list as $key => $value) { + $data[] = ['label' => $value['name'], 'value' => $value['id']]; + } + return $data; + } + + public function expressList($where = []) + { + if (empty($where)) $where = ['is_show' => 1]; + return $this->dao->getExpressList($where, 'id,name,code,partner_id,partner_key,net,account,key,net_name', 0, 0); + } + + /** + * 物流公司查询 + * @param string $cacheName + * @param string $expressNum + * @param string|null $com + * @return array + */ + public function query(string $cacheName, string $expressNum, string $com = null) + { + $resultData = CacheService::get($cacheName, null); + if ($resultData === null || !is_array($resultData)) { + $data = []; + switch ((int)sys_config('logistics_type')) { + case 1://一号通 + /** @var ServeServices $services */ + $services = app()->make(ServeServices::class); + $result = $services->express()->query($expressNum, $com); + if (isset($result['ischeck']) && $result['ischeck'] == 1) { + $cacheTime = 0; + } else { + $cacheTime = 1800; + } + foreach (isset($result['content']) ? $result['content'] : [] as $item) { + $data[] = ['time' => $item['time'], 'status' => $item['status']]; + } + break; + case 2://阿里云 + $result = ExpressService::query($expressNum); + if (is_array($result) && + isset($result['result']) && + isset($result['result']['deliverystatus']) && + $result['result']['deliverystatus'] >= 3) + $cacheTime = 0; + else + $cacheTime = 1800; + $data = $result['result']['list'] ?? []; + break; + } + CacheService::set($cacheName, $data, $cacheTime); + return $data; + } + + return $resultData; + } + + /** + * 同步物流公司 + * @return bool + */ + public function syncExpress() + { + if (CacheService::get('sync_express')) { + return true; + } + $expressList = $this->getPlatExpress(); + $data = $data_all = []; + $selfExpress = $this->dao->getExpress([], 'id,code,status,account,key,net_name,courier_name,customer_name,code_name', 'id'); + $codes = []; + if ($selfExpress) { + $codes = array_column($selfExpress, 'code'); + $selfExpress = array_combine($codes, $selfExpress); + } + foreach ($expressList as $express) { + $data = []; + $data['partner_id'] = $express['partner_id'] ?? 0; + $data['partner_key'] = $express['partner_key'] ?? 0; + $data['net'] = $express['net'] ?? 0; + $data['check_man'] = $express['check_man'] ?? 0; + $data['partner_name'] = $express['partner_name'] ?? 0; + $data['is_code'] = $express['is_code'] ?? 0; + if (!in_array($express['code'], $codes)) { + $data['name'] = $express['name'] ?? ''; + $data['code'] = $express['code'] ?? ''; + $data['is_show'] = 1; + $data['status'] = 0; + if ($express['partner_id'] == 0 && $express['partner_key'] == 0 && $express['net'] == 0 && $express['check_man'] == 0 && $express['partner_name'] == 0 && $express['is_code'] == 0) { + $data['status'] = 1; + } + $data_all[] = $data; + } else { + if (isset($selfExpress[$express['code']]['id']) && $selfExpress[$express['code']]['id']) { + if (($express['partner_id'] == 1 && !$selfExpress[$express['code']]['account']) || ($express['partner_key'] == 1 && !$selfExpress[$express['code']]['key']) || ($express['net'] == 1 && !$selfExpress[$express['code']]['net_name']) || ($express['check_man'] == 1 && !$selfExpress[$express['code']]['courier_name']) || ($express['partner_name'] == 1 && !$selfExpress[$express['code']]['customer_name']) || ($express['is_code'] == 1 && !$selfExpress[$express['code']]['code_name'])) { + $data['status'] = 0; + } else { + $data['status'] = 1; + } + $this->dao->update($selfExpress[$express['code']]['id'], $data, 'id'); + } + } + } + if ($data_all) { + $this->dao->saveAll($data_all); + } + CacheService::set('sync_express', 1, 3600); + return true; + } + + /** + * 查询单个快递公司 + * @param array $where + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOneByWhere(array $where) + { + return $this->dao->getOne($where); + } + +} diff --git a/app/services/other/QrcodeServices.php b/app/services/other/QrcodeServices.php new file mode 100644 index 0000000..7ef1ab8 --- /dev/null +++ b/app/services/other/QrcodeServices.php @@ -0,0 +1,378 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\other; + +use app\services\BaseServices; +use app\dao\other\QrcodeDao; +use app\services\system\attachment\SystemAttachmentServices; +use crmeb\exceptions\AdminException; +use crmeb\services\UploadService; +use crmeb\services\UtilService; +use crmeb\services\wechat\MiniProgram; +use crmeb\services\wechat\OfficialAccount; +use crmeb\traits\ServicesTrait; +use GuzzleHttp\Psr7\Utils; +use think\exception\ValidateException; + +/** + * + * Class QrcodeServices + * @package app\services\other + * @mixin QrcodeDao + */ +class QrcodeServices extends BaseServices +{ + + use ServicesTrait; + + /** + * QrcodeServices constructor. + * @param QrcodeDao $dao + */ + public function __construct(QrcodeDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取临时二维码 + * @param $type + * @param $id + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTemporaryQrcode($type, $id) + { + $where['third_id'] = $id; + $where['third_type'] = $type; + $res = $this->dao->getOne($where); + if (!$res) { + $this->createTemporaryQrcode($id, $type); + $res = $this->getTemporaryQrcode($type, $id); + } else if (empty($res['expire_seconds']) || $res['expire_seconds'] < time()) { + $this->createTemporaryQrcode($id, $type, $res['id']); + $res = $this->getTemporaryQrcode($type, $id); + } + if (!$res['ticket']) throw new AdminException('临时二维码获取错误'); + return $res; + } + + /** + * 临时二维码生成 + * @param $id + * @param $type + * @param string $qrcode_id + */ + public function createTemporaryQrcode($id, $type, $qrcode_id = '') + { + $qrcode = OfficialAccount::qrcodeService(); + $data = $qrcode->temporary($id, 30 * 24 * 3600); + $data['qrcode_url'] = $data['url']; + $data['expire_seconds'] = $data['expire_seconds'] + time(); + $data['url'] = $qrcode->url($data['ticket']); + $data['status'] = 1; + $data['third_id'] = $id; + $data['third_type'] = $type; + if ($qrcode_id) { + $this->dao->update($qrcode_id, $data); + } else { + $data['add_time'] = time(); + $this->dao->save($data); + } + } + + /** + * 获取永久二维码 + * @param $type + * @param $id + * @return array|mixed|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getForeverQrcode($type, $id) + { + $where['third_id'] = $id; + $where['third_type'] = $type; + $res = $this->dao->getOne($where); + if (!$res) { + $this->createForeverQrcode($id, $type); + $res = $this->getForeverQrcode($type, $id); + } + if (!$res['ticket']) throw new AdminException('永久二维码获取错误'); + return $res; + } + + /** + * 永久二维码生成 + * @param $id + * @param $type + */ + public function createForeverQrcode($id, $type) + { + $qrcode = OfficialAccount::qrcodeService(); + $data = $qrcode->forever($id); + $data['qrcode_url'] = $data['url']; + $data['url'] = $qrcode->url($data['ticket']); + $data['expire_seconds'] = 0; + $data['status'] = 1; + $data['third_id'] = $id; + $data['third_type'] = $type; + $data['add_time'] = time(); + $this->dao->save($data); + } + + /** + * 获取二维码完整路径,不存在则自动生成 + * @param string $name + * @param string $link + * @param bool $force + * @return bool|mixed|string + */ + public function getWechatQrcodePath(string $name, string $link, bool $force = false, bool $isSaveAttach = true) + { + /** @var SystemAttachmentServices $systemAttachmentService */ + $systemAttachmentService = app()->make(SystemAttachmentServices::class); + try { + if (!$isSaveAttach) { + $imageInfo = ""; + } else { + $imageInfo = $systemAttachmentService->getOne(['name' => $name]); + } + $siteUrl = sys_config('site_url'); + if (!$imageInfo) { + $codeUrl = UtilService::setHttpType($siteUrl . $link, request()->isSsl() ? 0 : 1);//二维码链接 + $imageInfo = UtilService::getQRCodePath($codeUrl, $name); + if (is_string($imageInfo) && $force) + return false; + if (is_array($imageInfo)) { + if ($isSaveAttach) { + $systemAttachmentService->save([ + 'name' => $imageInfo['name'], + 'att_dir' => $imageInfo['dir'], + 'satt_dir' => $imageInfo['thumb_path'], + 'att_size' => $imageInfo['size'], + 'att_type' => $imageInfo['type'], + 'image_type' => $imageInfo['image_type'], + 'module_type' => 2, + 'time' => time(), + 'pid' => 1, + 'type' => 1 + ]); + } + $url = $imageInfo['dir']; + } else { + $url = ''; + $imageInfo = ['image_type' => 0]; + } + } else $url = $imageInfo['att_dir']; + if ($imageInfo['image_type'] == 1 && $url) $url = $siteUrl . $url; + return $url; + } catch (\Throwable $e) { + if ($force) + return false; + else + return ''; + } + } + + /** + * 获取小程序分享二维码 + * @param int $id + * @param int $uid + * @param int $type 1 = 拼团,2 = 秒杀 + * @return bool|string + */ + public function getRoutineQrcodePath(int $id, int $uid, int $type, array $parame = [], bool $isSaveAttach = true) + { + /** @var SystemAttachmentServices $systemAttachmentService */ + $systemAttachmentService = app()->make(SystemAttachmentServices::class); + $page = ''; + $namePath = ''; + $data = 'id=' . $id . '&spid=' . $uid; + switch ($type) { + case 0: + $page = 'pages/goods_details/index'; + $namePath = $id . '_' . $uid . '_' . $parame['is_promoter'] . '_product.jpg'; + break; + case 1: + $page = 'pages/activity/goods_combination_details/index'; + $namePath = 'combination_' . $id . '_' . $uid . '.jpg'; + break; + case 2: + $page = 'pages/activity/goods_seckill_details/index'; + $namePath = 'seckill_' . $id . '_' . $uid . '.jpg'; + if (isset($parame['stop_time']) && $parame['stop_time']) { + $data .= '&time=' . $parame['stop_time']; + $namePath = $parame['stop_time'] . $namePath; + } + break; + case 3: + $page = 'pages/annex/offline_pay/index'; + $namePath = 'routine_offline_scan.jpg'; + break; + case 4: + $page = 'pages/annex/vip_active/index'; + $namePath = 'routine_member_card.jpg'; + break; + case 5: + $page = 'pages/annex/vip_paid/index'; + $namePath = 'routine_pay_vip_code.jpg'; + break; + case 6: + $page = 'pages/annex/special/index'; + $namePath = $id . 'routine_annex_index_code.jpg'; + break; + case 7: + $page = 'pages/goods/order_pay/index'; + $namePath = 'routine_cashier_pay' . $id . '.jpg'; + $data = 'store_id=' . $id; + break; + case 8: + $page = 'pages/index/index'; + $namePath = $id . 'routine_index_code.jpg'; + break; + case 9: //桌码二维码 + $page = 'pages/store/table_code/index'; + $namePath = 'routine_table_code_' . $parame['store_id'] . '_' . $parame['table_number'] . '_' . $id . '.jpg'; + $data = 'qrcode_id=' . $id. '&store_id=' . $parame['store_id']; + break; + case 10: //门店二维码 + $page = 'pages/store_cate/store_cate'; + $namePath = 'routine_store_cate_id_' . $id . '.jpg'; + $data = 'id=' . $id; + break; + } + if (!$page || !$namePath) { + return false; + } + try { + $to = 'routine/product'; + if (!$isSaveAttach) { + $imageInfo = ""; + } else { + $imageInfo = $systemAttachmentService->getOne(['name' => $to . '/' . $namePath]); + } + $siteUrl = sys_config('site_url'); + if (!$imageInfo) { + $res = MiniProgram::appCodeUnlimit($data, $page, 280); + if (!$res) return false; + $uploadType = (int)sys_config('upload_type', 1); + $upload = UploadService::init($uploadType); + $res = (string)Utils::streamFor($res); + $res = $upload->to($to)->validate()->stream($res, $namePath); + if ($res === false) { + return false; + } + $imageInfo = $upload->getUploadInfo(); + $imageInfo['image_type'] = $uploadType; + if ($imageInfo['image_type'] == 1) $remoteImage = UtilService::remoteImage($siteUrl . $imageInfo['dir']); + else $remoteImage = UtilService::remoteImage($imageInfo['dir']); + if (!$remoteImage['status']) return false; + if ($isSaveAttach) { + $systemAttachmentService->save([ + 'name' => $imageInfo['name'], + 'att_dir' => $imageInfo['dir'], + 'satt_dir' => $imageInfo['thumb_path'], + 'att_size' => $imageInfo['size'], + 'att_type' => $imageInfo['type'], + 'image_type' => $imageInfo['image_type'], + 'module_type' => 2, + 'time' => time(), + 'pid' => 1, + 'type' => 2 + ]); + } + $url = $imageInfo['dir']; + } else $url = $imageInfo['att_dir']; + if ($imageInfo['image_type'] == 1) $url = $siteUrl . $url; + return $url; + } catch (\Throwable $e) { + return false; + } + } + + /** + * 添加二维码 存在直接获取 + * @param int $thirdId + * @param string $thirdType + * @param string $page + * @param string $qrCodeLink + * @return array|false|object|\PDOStatement|string|\think\Model + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function qrCodeForever($thirdId = 0, $thirdType = 'spread', $page = '', $qrCodeLink = '') + { + $qrcode = $this->dao->getOne(['third_id' => $thirdId, 'third_type' => $thirdType]); + if ($qrcode) { + return $qrcode; + } + return $this->setQrcodeForever($thirdId, $thirdType, $page, $qrCodeLink); + } + + /** + * 检测是否存在 + * @param int $thirdId + * @param string $thirdType + * @return int + */ + public function qrCodeExist($thirdId = 0, $thirdType = 'spread') + { + return !!$this->dao->getCount(['third_id' => $thirdId, 'third_type' => $thirdType]); + } + + /** + * 添加二维码记录 + * @param int $thirdId + * @param string $thirdType + * @param string $page + * @param string $qrCodeLink + * @return object + */ + public function setQrcodeForever($thirdId = 0, $thirdType = 'spread', $page = '', $qrCodeLink = '') + { + $data['third_type'] = $thirdType; + $data['third_id'] = $thirdId; + $data['status'] = 1; + $data['add_time'] = time(); + $data['page'] = $page; + $data['url_time'] = ''; + $data['qrcode_url'] = $qrCodeLink; + if (!$re = $this->dao->save($data)) { + throw new ValidateException('生成失败'); + } + return $re; + } + + /** + * 修改二维码地址 + * @param int $id + * @param array $data + * @return bool + */ + public function setQrcodeFind($id = 0, $data = array()) + { + if (!$id) return false; + if (!$this->dao->get((int)$id)) { + throw new ValidateException('数据不存在'); + } + if (!$re = $this->dao->update($id, $data, 'id')) { + throw new ValidateException('修改数据失败'); + } + return $re; + } +} diff --git a/app/services/other/SystemCityServices.php b/app/services/other/SystemCityServices.php new file mode 100644 index 0000000..3ca9823 --- /dev/null +++ b/app/services/other/SystemCityServices.php @@ -0,0 +1,151 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\other; + + +use app\dao\other\SystemCityDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use crmeb\services\FormBuilder as Form; + +/** + * 城市数据 + * Class SystemCityServices + * @package app\services\other + * @mixin SystemCityDao + */ +class SystemCityServices extends BaseServices +{ + /** + * 城市数据 + * @var string + */ + public $tree_city_key = 'tree_city_list'; + + /** + * 构造方法 + * SystemCityServices constructor. + * @param SystemCityDao $dao + */ + public function __construct(SystemCityDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取城市数据 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCityList(array $where) + { + return CacheService::get($this->tree_city_key, function () { + return $this->getSonCityList(); + }, 86400); + } + + /** + * tree形城市列表 + * @param int $pid + * @param string $parent_name + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSonCityList($pid = 0, $parent_name = '中国') + { + $list = $this->dao->getCityList(['parent_id' => $pid], 'id,city_id,level,name'); + $arr = []; + if ($list) { + foreach ($list as $item) { + $item['parent_id'] = $parent_name; + $item['children'] = $this->getSonCityList($item['city_id'], $item['name']); + $arr [] = $item; + } + } + return $arr; + } + + /** + * 添加城市数据表单 + * @param int $parentId + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createCityForm(int $parentId) + { + if ($parentId) { + $info = $this->dao->getOne(['city_id' => $parentId], 'level,city_id,name'); + } else { + $info = ["level" => 0, "city_id" => 0, "name" => '中国']; + } + $field[] = Form::hidden('level', $info['level']); + $field[] = Form::hidden('parent_id', $info['city_id']); + $field[] = Form::input('parent_name', '上级名称', $info['name'])->readonly(true); + $field[] = Form::input('name', '名称')->required('请填写城市名称'); + return create_form('添加城市', $field, $this->url('/setting/city/save')); + } + + /** + * 添加城市数据创建 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function updateCityForm(int $id) + { + $info = $this->dao->get($id); + if (!$info) { + throw new AdminException('需改的数据不存在'); + } + $info = $info->toArray(); + $info['parent_name'] = $this->dao->value(['city_id' => $info['parent_id']], 'name') ?: '中国'; + $field[] = Form::hidden('id', $info['id']); + $field[] = Form::hidden('level', $info['level']); + $field[] = Form::hidden('parent_id', $info['parent_id']); + $field[] = Form::input('parent_name', '上级名称', $info['parent_name'])->readonly(true); + $field[] = Form::input('name', '名称', $info['name'])->required('请填写城市名称'); + $field[] = Form::input('merger_name', '合并名称', $info['merger_name'])->placeholder('格式:陕西,西安,雁塔')->required('请填写合并名称'); + return create_form('修改城市', $field, $this->url('/setting/city/save')); + } + + /** + * 获取城市数据 + * @return mixed + */ + public function cityList() + { + return CacheService::get('CITY_LIST', function () { + $allCity = $this->dao->getCityList([], 'city_id as v,name as n,parent_id'); + return sort_city_tier($allCity, 0); + }, 0); + } + + /** + * @return bool|mixed|null + */ + public function getCityTreeList() + { + return CacheService::get('CITY_TREE_LIST', function () { + return get_tree_children($this->dao->getCityList(['is_show' => 1], 'city_id as value,name as label,parent_id as pid'), 'children', 'value'); + }); + } + +} diff --git a/app/services/other/export/ExportServices.php b/app/services/other/export/ExportServices.php new file mode 100644 index 0000000..d0d6d23 --- /dev/null +++ b/app/services/other/export/ExportServices.php @@ -0,0 +1,1416 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\other\export; + +use app\services\BaseServices; +use app\jobs\system\ExportExcelJob; +use app\services\pay\PayServices; +use crmeb\services\SpreadsheetExcelService; + +/** + * 导出 + * Class ExportServices + * @package app\services\other\export + */ +class ExportServices extends BaseServices +{ + /** + * 不分页拆分处理最大条数 + * @var int + */ + public $maxLimit = 1000; + /** + * 分页导出每页条数 + * @var int + */ + public $limit = 1000; + + /** + * 真实请求导出 + * @param $header excel表头 + * @param $title 标题 + * @param array $export 填充数据 + * @param string $filename 保存文件名称 + * @param string $suffix 保存文件后缀 + * @param bool $is_save true|false 是否保存到本地 + * @return mixed + */ + public function export(array $header, array $title_arr, array $export = [], string $filename = '', string $suffix = 'xlsx', bool $is_save = true) + { + $path = []; + $exportNum = count($export); + $limit = $this->maxLimit; + if ($exportNum < $limit) { + $title = isset($title_arr[0]) && !empty($title_arr[0]) ? $title_arr[0] : '导出数据'; + $name = isset($title_arr[1]) && !empty($title_arr[1]) ? $title_arr[1] : '导出数据'; + $info = isset($title_arr[2]) && !empty($title_arr[2]) ? $title_arr[2] : date('Y-m-d H:i:s', time()); + $filePath = SpreadsheetExcelService::instance()->setExcelHeader($header) + ->setExcelTile($title, $name, $info) + ->setExcelContent($export) + ->excelSave($filename, $suffix, $is_save); + $path[] = sys_config('site_url') . $filePath; + } else { + $data = []; + $i = $j = 0; + $basePath = sys_config('site_url') . '/phpExcel/'; + foreach ($export as $item) { + $data[] = $item; + $i++; + if ($limit <= 1 || $i == $exportNum) { + if ($j > 0) { + $filename .= '_' . $j; + $header = []; + $title_arr = []; + } + //加入队列 + ExportExcelJob::dispatch([$data, $filename, $header, $title_arr, $suffix, $is_save]); + $path[] = $basePath . $filename . '.' . $suffix; + $data = []; + $limit = $this->limit + 1; + $j++; + } + $limit--; + } + } + return $path; + } + + /** + * 用户资金导出 + * @param array $data + * @param int $type 1:直接返回数据前端生成excel 2:后台生成excel + * @return array|mixed + */ + public function userFinance($data = [], $type = 1) + { + $header = ['会员ID', '昵称', '金额', '类型', '备注', '创建时间']; + $title = ['资金监控', '资金监控', date('Y-m-d H:i:s', time())]; + $filename = '资金监控_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $value) { + $one_data = [ + 'uid' => $value['uid'], + 'nickname' => $value['nickname'], + 'pm' => $value['pm'] == 0 ? '-' . $value['number'] : $value['number'], + 'title' => $value['title'], + 'mark' => $value['mark'], + 'add_time' => $value['add_time'], + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 用户佣金导出 + * @param $data 导出数据 + */ + public function userCommission($data = [], $type = 1) + { + $header = ['昵称/姓名', '总佣金金额', '账户余额', '账户佣金', '提现到账佣金', '时间']; + $title = ['佣金记录', '佣金记录' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '佣金记录_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $value) { + $one_data = [ + 'nickname' => $value['nickname'], + 'sum_number' => $value['sum_number'], + 'now_money' => $value['now_money'], + 'brokerage_price' => $value['brokerage_price'], + 'extract_price' => $value['extract_price'], + 'time' => $value['time'] + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 用户积分导出 + * @param $data 导出数据 + */ + public function userPoint($data = [], $type = 1) + { + $header = ['编号', '标题', '变动后积分', '积分变动', '备注', '用户微信昵称', '添加时间']; + $title = ['积分日志', '积分日志' . time(), '生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '积分日志_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $key => $item) { + $one_data = [ + 'id' => $item['id'], + 'title' => $item['title'], + 'balance' => $item['balance'], + 'number' => $item['number'], + 'mark' => $item['mark'], + 'nickname' => $item['nickname'], + 'add_time' => $item['add_time'], + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 用户充值导出 + * @param $data 导出数据 + */ + public function userRecharge($data = [], $type = 1) + { + $header = ['昵称/姓名', '充值金额', '是否支付', '充值类型', '支付时间', '是否退款', '添加时间']; + $title = ['充值记录', '充值记录' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '充值记录_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $item) { + switch ($item['recharge_type']) { + case 'routine': + $item['_recharge_type'] = '小程序充值'; + break; + case 'weixin': + $item['_recharge_type'] = '公众号充值'; + break; + case 'balance': + $item['_recharge_type'] = '佣金转入'; + break; + case 'store': + $item['_recharge_type'] = '门店余额充值'; + break; + default: + $item['_recharge_type'] = '其他充值'; + break; + } + $item['_pay_time'] = $item['pay_time'] ? date('Y-m-d H:i:s', $item['pay_time']) : '暂无'; + $item['_add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : '暂无'; + $item['paid_type'] = $item['paid'] ? '已支付' : '未支付'; + + $one_data = [ + 'nickname' => $item['nickname'], + 'price' => $item['price'], + 'paid_type' => $item['paid_type'], + '_recharge_type' => $item['_recharge_type'], + '_pay_time' => $item['_pay_time'], + 'paid' => $item['paid'] == 1 && $item['refund_price'] == $item['price'] ? '已退款' : '未退款', + '_add_time' => $item['_add_time'] + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 用户推广导出 + * @param $data 导出数据 + */ + public function userAgent($data = [], $type = 1) + { + $header = ['用户编号', '昵称', '电话号码', '推广用户数量', '订单数量', '推广订单金额', '佣金金额', '已提现金额', '提现次数', '未提现金额', '上级推广人']; + $title = ['推广用户', '推广用户导出' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '推广用户_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $index => $item) { + $one_data = [ + 'uid' => $item['uid'], + 'nickname' => $item['nickname'], + 'phone' => $item['phone'], + 'spread_count' => $item['spread_count'], + 'order_count' => $item['order_count'], + 'order_price' => $item['order_price'], + 'brokerage_money' => $item['brokerage_money'], + 'extract_count_price' => $item['extract_count_price'], + 'extract_count_num' => $item['extract_count_num'], + 'brokerage_price' => $item['brokerage_price'], + 'spread_name' => $item['spread_name'], + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 微信用户导出 + * @param $data 导出数据 + */ + public function wechatUser($data = [], $type = 1) + { + $header = ['名称', '性别', '地区', '是否关注公众号']; + $title = ['微信用户导出', '微信用户导出' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '微信用户导出_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $index => $item) { + $one_data = [ + 'nickname' => $item['nickname'], + 'sex' => $item['sex'], + 'address' => $item['country'] . $item['province'] . $item['city'], + 'subscribe' => $item['subscribe'] == 1 ? '关注' : '未关注', + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 订单资金导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function orderFinance($data = [], $type = 1) + { + $header = ['时间', '营业额(元)', '支出(元)', '成本', '优惠', '积分抵扣', '盈利(元)']; + $title = ['财务统计', '财务统计', date('Y-m-d H:i:s', time())]; + $filename = '财务统计_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $info) { + $time = $info['pay_time']; + $price = $info['total_price'] + $info['pay_postage']; + $zhichu = $info['coupon_price'] + $info['deduction_price'] + $info['cost']; + $profit = ($info['total_price'] + $info['pay_postage']) - ($info['coupon_price'] + $info['deduction_price'] + $info['cost']); + $deduction = $info['deduction_price'];//积分抵扣 + $coupon = $info['coupon_price'];//优惠 + $cost = $info['cost'];//成本 + $one_data = compact('time', 'price', 'zhichu', 'cost', 'coupon', 'deduction', 'profit'); + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 砍价活动导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function storeBargain($data = [], $type = 1) + { + $header = ['砍价活动名称', '砍价活动简介', '砍价金额', '砍价最低价', + '用户每次砍价的次数', '砍价状态', '砍价开启时间', '砍价结束时间', '销量', '库存', '返多少积分', '添加时间']; + $title = ['砍价商品导出', '商品信息' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '砍价商品导出_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $index => $item) { + $one_data = [ + 'title' => $item['title'], + 'info' => $item['info'], + 'price' => '¥' . $item['price'], + 'bargain_max_price' => '¥' . $item['min_price'], + 'bargain_num' => $item['bargain_num'], + 'status' => $item['status'] ? '开启' : '关闭', + 'start_time' => empty($item['start_time']) ? '' : date('Y-m-d H:i:s', (int)$item['start_time']), + 'stop_time' => empty($item['stop_time']) ? '' : date('Y-m-d H:i:s', (int)$item['stop_time']), + 'sales' => $item['sales'], + 'stock' => $item['stock'], + 'give_integral' => $item['give_integral'], + 'add_time' => empty($item['add_time']) ? '' : $item['add_time'], + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 拼团导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function storeCombination($data = [], $type = 1) + { + $header = ['编号', '拼团名称', '原价', '拼团价', '库存', '拼团人数', '参与人数', '成团数量', '销量', '商品状态', '结束时间']; + $title = ['拼团商品导出', '商品信息' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '拼团商品导出_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $item) { + $one_data = [ + 'id' => $item['id'], + 'title' => $item['title'], + 'ot_price' => $item['ot_price'], + 'price' => $item['price'], + 'stock' => $item['stock'], + 'people' => $item['count_people'], + 'count_people_all' => $item['count_people_all'], + 'count_people_pink' => $item['count_people_pink'], + 'sales' => $item['sales'] ?? 0, + 'is_show' => $item['is_show'] ? '开启' : '关闭', + 'stop_time' => empty($item['stop_time']) ? '' : date('Y/m/d H:i:s', (int)$item['stop_time']) + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 秒杀活动导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function storeSeckill($data = [], $type = 1) + { + $header = ['编号', '活动标题', '活动简介', '原价', '秒杀价', '限量', '限量剩余','秒杀状态', '结束时间', '状态']; + $title = ['秒杀商品导出', ' ', ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '秒杀商品导出_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $item) { + if ($item['status']) { + if ($item['start_time'] > time()) + $item['start_name'] = '活动未开始'; + else if ($item['stop_time'] < time()) + $item['start_name'] = '活动已结束'; + else if ($item['stop_time'] > time() && $item['start_time'] < time()) + $item['start_name'] = '正在进行中'; + } else { + $item['start_name'] = '活动已结束'; + } + $one_data = [ + 'id' => $item['id'], + 'title' => $item['title'], + 'info' => $item['info'], + 'ot_price' => $item['ot_price'], + 'price' => $item['price'], + 'quota_show' => $item['quota_show'], + 'quota' => $item['quota'], + 'start_name' => $item['start_name'], + 'stop_time' => $item['stop_time'] ? date('Y-m-d H:i:s', $item['stop_time']) : '/', + 'status' => $item['status'] ? '开启' : '关闭', + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 导出商品卡号、卡密模版 + * @param int $type + * @return array|mixed + */ + public function storeProductCardTemplate($type = 1) + { + $header = ['卡号', '卡密']; + $title = ['商品卡密模版', '商品密' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '商品卡密模版_' . date('YmdHis', time()); + + if ($type == 1) { + $export = []; + $filekey = ['card_no', 'card_pwd']; + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, [], $filename); + } + } + + /** + * 商品导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function storeProduct($data = [], $type = 1) + { + $header = ['商品名称', '商品简介', '商品分类', '价格', '库存', '销量', '浏览量']; + $title = ['商品导出', '商品信息' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '商品导出_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $index => $item) { + $one_data = [ + 'store_name' => $item['store_name'], + 'store_info' => $item['store_info'], + 'cate_name' => $item['cate_name'], + 'price' => '¥' . $item['price'], + 'stock' => $item['stock'], + 'sales' => $item['sales'], + 'visitor' => $item['visitor'], + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 商铺订单导出 + * @param array $data + * @param string $type + * @param int $export_type + * @return array|mixed + */ + public function storeOrder($data = [], $type = "", $export_type = 1) + { + if (!$type) { + $header = ['订单ID', '订单编号', '性别', '电话', '收货人姓名', '收货人电话', '收货地址', '商品信息', '商品总数', + '总价格', '实际支付', '邮费', '会员优惠金额', '优惠卷金额', '积分抵扣金额', '支付状态', '支付时间', '订单状态', '下单时间', '用户备注']; + $title = ['订单导出', '订单信息' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '订单导出_' . date('YmdHis', time()); + } else { + $header = ['订单ID', '订单编号', '物流公司', '物流编码', '物流单号', '发货地址', '收货人姓名', '收货人电话', '订单实付金额', '商品数量*售价', '商品ID', '商品名称', '商品规格', '商家备注', '订单成交时间']; + $title = ['发货单导出', '订单信息' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '发货单导出_' . date('YmdHis', time()); + } + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $item) { + if (!$type) { + if ($item['paid'] == 1) { + switch ($item['pay_type']) { + case PayServices::WEIXIN_PAY: + $item['pay_type_name'] = '微信支付'; + break; + case PayServices::YUE_PAY: + $item['pay_type_name'] = '余额支付'; + break; + case PayServices::OFFLINE_PAY: + $item['pay_type_name'] = '线下支付'; + break; + case PayServices::ALIAPY_PAY: + $item['pay_type_name'] = '支付宝支付'; + break; + case PayServices::CASH_PAY: + $item['pay_type_name'] = '现金支付'; + break; + default: + $item['pay_type_name'] = '其他支付'; + break; + } + } else { + switch ($item['pay_type']) { + default: + $item['pay_type_name'] = '未支付'; + break; + case 'offline': + $item['pay_type_name'] = '线下支付'; + $item['pay_type_info'] = 1; + break; + } + } + + if ($item['paid'] == 0 && $item['status'] == 0) { + $item['status_name'] = '未支付'; + } else if ($item['paid'] == 1 && $item['status'] == 4 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status_name'] = '部分发货'; + } else if ($item['paid'] == 1 && $item['status'] == 5 && $item['shipping_type'] == 2 && $item['refund_status'] == 0) { + $item['status_name'] = '部分核销'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 1) { + $item['status_name'] = '申请退款'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 2) { + $item['status_name'] = '已退款'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 4) { + $item['status_name'] = '退款中'; + } else if ($item['paid'] == 1 && $item['status'] == 0 && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status_name'] = '未发货'; + } else if ($item['paid'] == 1 && in_array($item['status'], [0, 1]) && $item['shipping_type'] == 2 && $item['refund_status'] == 0) { + $item['status_name'] = '未核销'; + } else if ($item['paid'] == 1 && in_array($item['status'], [1, 5]) && in_array($item['shipping_type'], [1, 3]) && $item['refund_status'] == 0) { + $item['status_name'] = '待收货'; + } else if ($item['paid'] == 1 && $item['status'] == 2 && $item['refund_status'] == 0) { + $item['status_name'] = '待评价'; + } else if ($item['paid'] == 1 && $item['status'] == 3 && $item['refund_status'] == 0) { + $item['status_name'] = '已完成'; + } else if ($item['paid'] == 1 && $item['refund_status'] == 3) { + $item['status_name'] = '部分退款'; + } + $goodsName = []; + $vip_sum_price = 0; + foreach ($item['_info'] as $k => $v) { + $suk = ''; + if (isset($v['productInfo']['attrInfo'])) { + if (isset($v['productInfo']['attrInfo']['suk'])) { + $suk = '(' . $v['productInfo']['attrInfo']['suk'] . ')'; + } + } + if (isset($v['productInfo']['store_name'])) { + $goodsName[] = implode(' ', + [ + $v['productInfo']['store_name'], + $suk, + "[{$v['cart_num']} * {$v['truePrice']}]", + ]); + } + $vip_sum_price = bcadd((string)$vip_sum_price, bcmul($v['vip_truePrice'], $v['cart_num'] ? $v['cart_num'] : 1, 4), 2); + } + if ($item['sex'] == 1) $sex_name = '男'; + else if ($item['sex'] == 2) $sex_name = '女'; + else $sex_name = '未知'; + + $one_data = [ + 'id' => $item['id'], + 'order_id' => $item['order_id'], + 'sex' => $sex_name, + 'phone' => $item['user_phone'], + 'real_name' => $item['real_name'], + 'user_phone' => $item['user_phone'], + 'user_address' => $item['user_address'], + 'goods_name' => $goodsName ? implode("\n", $goodsName) : '', + 'total_num' => $item['total_num'], + 'total_price' => $item['total_price'], + 'pay_price' => $item['pay_price'], + 'pay_postage' => $item['pay_postage'], + 'vip_sum_price' => $vip_sum_price, + 'coupon_price' => $item['coupon_price'], + 'deduction_price' => $item['deduction_price'] ?? 0, + 'pay_type_name' => $item['pay_type_name'], + 'pay_time' => $item['pay_time'] > 0 ? date('Y/m-d H:i', (int)$item['pay_time']) : '暂无', + 'status_name' => $item['status_name'] ?? '未知状态', + 'add_time' => empty($item['add_time']) ? 0 : date('Y-m-d H:i:s', (int)$item['add_time']), + 'mark' => $item['mark'] + ]; + } else { + if (isset($item['pinkStatus']) && $item['pinkStatus'] != 2) { + continue; + } + if (isset($item['refund']) && $item['refund']) { + continue; + } + $goodsName = []; + $g = 0; + foreach ($item['_info'] as $k => $v) { + $goodsName['cart_num'][$g] = $v['cart_num'] . ' * ' . ($v['productInfo']['attrInfo']['price'] ?? $v['productInfo']['price'] ?? 0.00); + $goodsName['product_id'][$g] = $v['product_id']; + $suk = $barCode = $code = ''; + if (!empty($v['productInfo']['attrInfo']['bar_code'])) { + $barCode = $v['productInfo']['attrInfo']['bar_code']; + } + if (!empty($v['productInfo']['attrInfo']['code'])) { + $code = $v['productInfo']['attrInfo']['code']; + } + if (isset($v['productInfo']['attrInfo'])) { + if (isset($v['productInfo']['attrInfo']['suk'])) { + $suk = '(' . $v['productInfo']['attrInfo']['suk'] . '|条码:' . $barCode . '|编码:' . $code . ')'; + } + } + $name = []; + if (isset($v['productInfo']['store_name'])) { + $name[] = implode(' ', + [ + $v['productInfo']['store_name'], + $suk, + "[{$v['cart_num']} * {$v['truePrice']}]", + ]); + } + $goodsName['goods_name'][$g] = implode(' ', $name); + $goodsName['attr'][$g] = $v['productInfo']['attrInfo']['suk'] ?? ''; + $g++; + } + $one_data = [ + 'id' => $item['id'], + 'order_id' => $item['order_id'], + 'a' => "", + 'b' => "", + 'c' => "", + 'user_address' => $item['user_address'], + 'real_name' => $item['real_name'], + 'user_phone' => $item['user_phone'], + 'pay_price' => $item['pay_price'], + 'cart_num' => isset($goodsName['cart_num']) ? implode("\n", $goodsName['cart_num']) : '', + 'product_id' => isset($goodsName['product_id']) ? implode("\n", $goodsName['product_id']) : '', + 'goods_name' => isset($goodsName['goods_name']) ? implode("\n", $goodsName['goods_name']) : '', + 'attr' => isset($goodsName['attr']) ? implode("\n", $goodsName['attr']) : '', + 'remark' => $item['remark'], + 'pay_time' => $item['pay_time'] ? date('Y-m-d H:i:s', (int)$item['pay_time']) : '暂无', + ]; + } + if ($export_type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($export_type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * @param string $str + * @return false|string|string[]|null + */ + public function strToUtf8($str = '') + { + + $current_encode = mb_detect_encoding($str, array("ASCII", "GB2312", "GBK", 'BIG5', 'UTF-8')); + + $encoded_str = mb_convert_encoding($str, 'UTF-8', $current_encode); + + return $encoded_str; + + } + + /** + * 商铺自提点导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function storeMerchant($data = [], $type = 1) + { + $header = ['提货点名称', '提货点', '地址', '营业时间', '状态']; + $title = ['提货点导出', '提货点信息' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '提货点导出_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $index => $item) { + $one_data = [ + 'name' => $item['name'], + 'phone' => $item['phone'], + 'address' => $item['address'] . '' . $item['detailed_address'], + 'day_time' => $item['day_time'], + 'is_show' => $item['is_show'] ? '开启' : '关闭' + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 会员卡导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function memberCard($data = [], $type = 1) + { + $header = ['会员卡号', '密码', '领取人', '领取人手机号', '领取时间', '是否使用']; + $title = ['会员卡导出', '会员卡导出' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = $data['title'] ? ("卡密会员_" . trim(str_replace(["\r\n", "\r", "\\", "\n", "/", "<", ">", "=", " "], '', $data['title']))) : ""; + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data['data'] as $index => $item) { + $one_data = [ + 'card_number' => $item['card_number'], + 'card_password' => $item['card_password'], + 'user_name' => $item['user_name'], + 'user_phone' => $item['user_phone'], + 'use_time' => $item['use_time'], + 'use_uid' => $item['use_uid'] ? '已领取' : '未领取' + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 批量任务发货记录导出 + * @param array $data + * @param $queueType + * @param int $type + * @return array|mixed + */ + public function batchOrderDelivery($data = [], $queueType, $type = 1) + { + if (in_array($queueType, [7, 8])) { + $header = ['订单ID', '物流公司', '物流单号', '处理状态', '异常原因']; + } + if ($queueType == 9) { + $header = ['订单ID', '配送员姓名', '配送员电话', '处理状态', '异常原因']; + } + if ($queueType == 10) { + $header = ['订单ID', '虚拟发货内容', '处理状态', '异常原因']; + } + $title = ['发货记录导出', '发货记录导出' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '批量任务发货记录_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $index => $item) { + if (!$item) { + continue; + } + if (in_array($queueType, [7, 8, 9])) { + $one_data = [ + 'order_id' => $item['order_id'] ?? '', + 'delivery_name' => $item['delivery_name'] ?? '', + 'delivery_id' => $item['delivery_id'] ?? '', + 'status_cn' => $item['status_cn'] ?? '', + 'error' => $item['error'] ?? '', + ]; + } else { + $one_data = [ + 'order_id' => $item['order_id'] ?? '', + 'fictitious_content' => $item['fictitious_content'] ?? '', + 'status_cn' => $item['status_cn'] ?? '', + 'error' => $item['error'] ?? '', + ]; + } + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 物流公司对照表 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function expressList($data = [], $type = 1) + { + $header = ['物流公司名称', '物流公司编码']; + $title = ['物流公司对照表导出', '物流公司对照表导出' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '物流公司对照表_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $index => $item) { + $one_data = [ + 'name' => $item['name'], + 'code' => $item['code'], + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 交易统计 + * @param array $data + * @param string $tradeTitle + * @param int $type + * @return array|mixed + */ + public function tradeData($data = [], $tradeTitle = "交易统计", $type = 1) + { + $header = ['时间']; + $title = [$tradeTitle, $tradeTitle, ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = $tradeTitle . '_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $headerArray = array_column($data['series'], 'name'); + $header = array_merge($header, $headerArray); + $export = []; + foreach ($data['series'] as $index => $item) { + foreach ($data['x'] as $k => $v) { + $export[$v]['time'] = $v; + $export[$v][] = $item['value'][$k]; + } + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + + /** + * 商品统计 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function productTrade($data = [], $type = 1) + { + $header = ['日期/时间', '商品浏览量', '商品访客数', '加购件数', '下单件数', '支付件数', '支付金额', '成本金额', '退款金额', '退款件数', '访客-支付转化率']; + $title = ['商品统计', '商品统计' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '商品统计_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $value) { + $one_data = [ + 'time' => $value['time'], + 'browse' => $value['browse'], + 'user' => $value['user'], + 'cart' => $value['cart'], + 'order' => $value['order'], + 'payNum' => $value['payNum'], + 'pay' => $value['pay'], + 'cost' => $value['cost'], + 'refund' => $value['refund'], + 'refundNum' => $value['refundNum'], + 'changes' => $value['changes'] . '%' + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 用户统计 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function userTrade($data = [], $type = 1) + { + $header = ['日期/时间', '访客数', '浏览量', '新增用户数', '成交用户数', '访客-支付转化率', '付费会员数', '充值用户数', '客单价']; + $title = ['用户统计', '用户统计' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '用户统计_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $value) { + $one_data = [ + 'time' => $value['time'], + 'user' => $value['user'], + 'browse' => $value['browse'], + 'new' => $value['new'], + 'paid' => $value['paid'], + 'changes' => $value['changes'] . '%', + 'vip' => $value['vip'], + 'recharge' => $value['recharge'], + 'payPrice' => $value['payPrice'], + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + + /** + * 导出积分兑换订单 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function storeIntegralOrder($data = [], $type = 1) + { + $header = ['订单号', '电话', '收货人姓名', '收货人电话', '收货地址', '商品信息', '订单状态', '下单时间', '用户备注']; + $title = ['积分兑换订单导出', '订单信息' . time(), ' 生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '积分兑换订单导出_' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $item) { + $one_data = [ + 'order_id' => $item['order_id'], + 'phone' => $item['user_phone'], + 'real_name' => $item['real_name'], + 'user_phone' => $item['user_phone'], + 'user_address' => $item['user_address'], + 'goods_name' => $item['store_name'], + 'status_name' => $item['status_name'] ?? '未知状态', + 'add_time' => $item['add_time'], + 'mark' => $item['mark'] + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 门店账单导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function financeRecord($data = [], $name = '账单导出', $type = 1) + { + $header = ['交易单号', '关联订单', '交易时间', '交易金额', '支出收入', '交易人', '关联店员', '交易类型', '支付方式']; + $title = [$name, $name . time(), '生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = $name . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $key => $item) { + $one_data = [ + 'order_id' => $item['order_id'], + 'link_id' => $item['link_id'], + 'trade_time' => $item['trade_time'], + 'number' => $item['number'], + 'pm' => $item['pm'] == 1 ? '收入' : '支出', + 'user_nickname' => $item['user_nickname'], + 'staff_name' => $item['staff_name'] ?? '', + 'type_name' => $item['type_name'], + 'pay_type_name' => $item['pay_type_name'], + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 供应商账单导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function SupplierFinanceRecord($data = [], $name = '账单导出', $type = 1) + { + $header = ['交易单号', '关联订单', '交易时间', '交易金额', '支出收入', '交易人', '交易类型', '支付方式']; + $title = [$name, $name . time(), '生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = $name . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $key => $item) { + $one_data = [ + 'order_id' => $item['order_id'], + 'link_id' => $item['link_id'], + 'trade_time' => $item['trade_time'], + 'number' => $item['number'], + 'pm' => $item['pm'] == 1 ? '收入' : '支出', + 'user_nickname' => $item['user_nickname'], + 'type_name' => $item['type_name'], + 'pay_type_name' => $item['pay_type_name'], + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * @param array $data + * @param int $type + * @return array|mixed + */ + public function vipOrder(array $data, int $type = 1) + { + $header = ['订单号', '用户名', '手机号', '会员类型', '有效期限', '支付金额', '支付方式', '购买时间', '到期时间']; + $title = ['会员订单', '会员订单' . time(), '生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '会员订单' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $key => $item) { + $one_data = [ + 'order_id' => $item['order_id'], + 'nickname' => $item['user']['nickname'] ?? '', + 'phone' => $item['user']['phone'] ?? '', + 'member_type' => $item['member_type'], + 'vip_day' => $item['vip_day'], + 'pay_price' => $item['pay_price'], + 'pay_type' => $item['pay_type'], + 'pay_time' => $item['pay_time'], + 'overdue_time' => $item['overdue_time'] + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 发票导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function invoiceRecord(array $data, int $type = 1) + { + $header = ['订单号', '订单金额', '发票类型', '发票抬头类型', '发票抬头名称', '下单时间', '开票状态', '订单状态']; + $title = ['发票导出', '发票导出' . time(), '生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '发票导出' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $key => $item) { + $one_data = [ + 'order_id' => $item['order_id'], + 'pay_price' => $item['pay_price'], + 'type' => $item['type'] == 1 ? '电子普通发票' : '纸质专用发票', + 'header_type' => $item['header_type'] == 1 ? '个人' : '企业', + 'name' => $item['name'], + 'add_time' => $item['add_time'], + 'is_invoice' => $item['is_invoice'] == 1 ? '已开票' : '未开票' + ]; + if ($item['refund_status'] > 0) { + if ($item['refund_status'] == 1) { + $one_data['status'] = '退款中'; + } else { + $one_data['status'] = '已退款'; + } + } else { + if ($item['status'] == 0) { + $one_data['status'] = '未发货'; + } elseif ($item['status'] == 1) { + $one_data['status'] = '待收货'; + } elseif ($item['status'] == 2) { + $one_data['status'] = '待评价'; + } elseif ($item['status'] == 3) { + $one_data['status'] = '已完成'; + } + } + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } + + /** + * 系统表单收集数据导出 + * @param array $data + * @param int $type + * @return array|mixed + */ + public function systemFormData(array $data, int $type = 1) + { + $header = ['模版名称', '用户UID', '用户昵称', '手机号', '模版内容', '创建时间']; + $title = ['系统表单收集数据导出', '表单收集数据导出' . time(), '生成时间:' . date('Y-m-d H:i:s', time())]; + $filename = '系统表单收集数据导出' . date('YmdHis', time()); + $export = []; + $filekey = []; + if (!empty($data)) { + $i = 0; + foreach ($data as $key => $item) { + $one_data = [ + 'system_form_name' => $item['system_form_name'] ?? '', + 'uid' => $item['uid'] ?? 0, + 'nickname' => $item['nickname'] ?? '', + 'phone' => $item['phone'] ?? '', + 'form_data' => is_string($item['value']) ? json_decode($item['value']) : $item['value'], + 'add_time' => $item['add_time'], + ]; + if ($type == 1) { + $export[] = $one_data; + if ($i == 0) { + $filekey = array_keys($one_data); + } + } else { + $export[] = array_values($one_data); + } + $i++; + } + } + if ($type == 1) { + return compact('header', 'filekey', 'export', 'filename'); + } else { + return $this->export($header, $title, $export, $filename); + } + } +} diff --git a/app/services/other/queue/QueueAuxiliaryServices.php b/app/services/other/queue/QueueAuxiliaryServices.php new file mode 100644 index 0000000..455e611 --- /dev/null +++ b/app/services/other/queue/QueueAuxiliaryServices.php @@ -0,0 +1,226 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\other\queue; + + +use app\dao\other\queue\QueueAuxiliaryDao; +use app\services\BaseServices; +use app\services\order\StoreOrderServices; +use think\exception\ValidateException; + +/** + * 队列辅助 + * Class QueueAuxiliaryServices + * @package app\services\other\queue + * @mixin QueueAuxiliaryDao + */ +class QueueAuxiliaryServices extends BaseServices +{ + public static $_status = [ + 0 => "未执行", + 1 => "成功", + 2 => "失败", + 3 => '删除' + ]; + + /** + * QueueAuxiliaryServices constructor. + * @param QueueAuxiliaryDao $dao + */ + public function __construct(QueueAuxiliaryDao $dao) + { + $this->dao = $dao; + } + + /** + * 添加队列数据 + * @param $queueId + * @param $ids + * @param $data + * @param $type + * @param $rediskey + * @return bool + */ + public function saveQueueOrderData($queueId, $ids, $data, $type, $rediskey) + { + if (!$ids) { + throw new ValidateException('缺少数据'); + } + $data_all = []; + $save = ['binding_id' => $queueId, 'type' => $rediskey, 'status' => 0, 'add_time' => time()]; + if ($type == 7) {//批量发货读取数据表格 + foreach ($data as $k => $v) { + if ($v[0] && $v[1] && $v[2] && $v[3] && $v[4] && in_array($v[0], $ids)) { + $save['relation_id'] = $v[0]; + $save['other'] = json_encode(['id' => $v[0], 'order_id' => $v[1], 'delivery_name' => $v[2], 'delivery_code' => $v[3], 'delivery_id' => $v[4], 'delivery_status' => 0, 'wx_message' => 0, 'phone_message' => 0, 'error_info' => ""]); + $data_all[] = $save; + } + } + } else { + foreach ($ids as $k => $v) { + $save['relation_id'] = $v; + $save['other'] = json_encode(['id' => $v, 'delivery_status' => 0, 'wx_message' => 0, 'phone_message' => 0, 'error_info' => ""]); + $data_all[] = $save; + } + } + if ($data_all) { + if (!$this->dao->saveAll($data_all)) { + throw new ValidateException('添加队列数据失败'); + } + } + return true; + } + + /** + * 获取发货缓存数据列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderExpreList(array $where, int $limit = 0) + { + if (!$where) throw new ValidateException("获取发货缓存数据条件缺失"); + if ($limit) { + [$page] = $this->getPageValue(); + } else { + [$page, $limit] = $this->getPageValue(); + } + return $this->dao->getOrderExpreList($where, $page, $limit); + } + + /** + * 修改订单缓存数据 + * @param array $where + * @param array $data + * @return mixed + */ + public function updateOrderStatus(array $where, array $data) + { + if (!$where) throw new ValidateException("数据条件缺失"); + return $this->dao->update($where, $data); + } + + /** + * 根据条件统计缓存数据 + * @param array $where + * @return int + */ + public function getCountOrder(array $where) + { + return $this->dao->count($where); + } + + /** + * 查看订单缓存数据的各种状态 + * @param $orderId + * @param $queueId + * @return bool|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderOtherSataus($orderId, $queueId, $cacheType) + { + if (!$orderId || !$queueId) return false; + $where['relation_id'] = $orderId; + $where['binding_id'] = $queueId; + $where['type'] = $cacheType; + $where['status'] = 1; + $getOne = $this->dao->getOrderCacheOne($where); + if ($getOne) return true; + return false; + } + + /** + * 获取发货记录 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function deliveryLogList(array $where = []) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->deliveryLogList($where, $page, $limit); + $list = $this->doBatchDeliveryData($list); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 下载发货记录 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExportData(array $where, int $limit = 0) + { + if (!$where) throw new ValidateException("数据条件缺失"); + $list = $this->getOrderExpreList($where, $limit); + $list = $this->doBatchDeliveryData($list); + return $list; + } + + /** + * 批量发货记录数据 + * @param $list + * @return mixed + */ + public function doBatchDeliveryData($list) + { + if ($list) { + /** @var StoreOrderServices $storeOrderService */ + $storeOrderService = app()->make(StoreOrderServices::class); + /** @var QueueServices $queueService */ + $queueService = app()->make(QueueServices::class); + $type = array_flip($queueService->queue_redis_key); + + foreach ($list as &$v) { + $v['add_time'] = date('Y-m-d H:i:s', $v['add_time']); + $v['status_cn'] = self::$_status[$v['status']]; + $v['error'] = $v['status'] == 1 ? '无' : '队列异常'; + $orderInfo = $storeOrderService->getOne(['id' => $v['relation_id']]); + if (!$orderInfo) { + continue; + } + $v['order_id'] = $orderInfo['order_id']; + if (in_array($type[$v['type']], [7, 8, 9])) { + $v['delivery_name'] = $orderInfo ? $orderInfo['delivery_name'] : ""; + $v['delivery_id'] = $orderInfo ? $orderInfo['delivery_id'] : ""; + } + if ($type[$v['type']] == 10) { + $v['fictitious_content'] = $orderInfo ? $orderInfo['fictitious_content'] : ""; + } + } + } + return $list; + } + + /** + * 获取某个队列的数据缓存 + * @param $bindingId + * @param $type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCacheOidList($bindingId, $type) + { + return $this->dao->getCacheOidList($bindingId, $type); + } +} diff --git a/app/services/other/queue/QueueServices.php b/app/services/other/queue/QueueServices.php new file mode 100644 index 0000000..e421b61 --- /dev/null +++ b/app/services/other/queue/QueueServices.php @@ -0,0 +1,888 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\other\queue; + + +use app\dao\other\queue\QueueDao; +use app\jobs\BatchHandleJob; +use app\services\BaseServices; +use app\services\activity\coupon\StoreCouponIssueServices; +use app\services\order\StoreCartServices; +use app\services\order\StoreOrderDeliveryServices; +use app\services\order\StoreOrderServices; +use app\services\product\category\StoreProductCategoryServices; +use app\services\product\product\StoreProductRelationServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductRuleServices; +use app\services\user\group\UserGroupServices; +use app\services\user\label\UserLabelRelationServices; +use app\services\user\label\UserLabelServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use think\exception\ValidateException; +use think\facade\Log; + + +/** + * 队列 + * Class QueueServices + * @package app\services\other\queue + * @mixin QueueDao + */ +class QueueServices extends BaseServices +{ + /** + * 任务类型名称 + * @var string[] + */ + public $queue_type_name = [ + 1 => "批量发放用户优惠券", + 2 => "批量设置用户分组", + 3 => "批量设置用户标签", + 4 => "批量下架商品", + 5 => "批量删除商品规格", + 6 => "批量删除订单", + 7 => "批量手动发货", + 8 => "批量打印电子面单", + 9 => "批量配送", + 10 => "批量虚拟发货", + ]; + + /** + * 任务redis缓存key + * @var string[] + */ + public $queue_redis_key = [ + 1 => "DrivingSendCoupon-ADMIN", + 2 => "DrivingUserGroup-ADMIN", + 3 => "DrivingUserLabel-ADMIN", + 4 => "DrivingProductUnshow-ADMIN", + 5 => "DrivingProductRule-ADMIN", + 6 => "DrivingOrderDel-ADMIN", + 7 => 3, + 8 => 4, + 9 => 5, + 10 => 6, + ]; + + /** + * 状态 + * @var string[] + */ + protected $status_name = [ + 0 => '未处理', + 1 => '正在处理', + 2 => '完成', + 3 => '失败' + ]; + + public function __construct(QueueDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取任务列表 + * @param array $where + */ + public function getList(array $where = []) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit); + if ($list) { + foreach ($list as &$v) { + $v['finish_time'] = $v['finish_time'] ? date('Y-m-d H:i:s', $v['finish_time']) : ""; + $v['first_time'] = $v['first_time'] ? date('Y-m-d H:i:s', $v['first_time']) : ""; + $v['again_time'] = $v['again_time'] ? date('Y-m-d H:i:s', $v['again_time']) : ""; + $v['status_cn'] = $this->status_name[$v['status']] ?? ''; + $v['is_show_log'] = false; + if (in_array($v['type'], [7, 8, 9, 10])) { + $v['is_show_log'] = true; + $v['is_error_button'] = $v['status'] == 2; + } + $v['type_cn'] = $this->queue_type_name[$v['type']] ?? ''; + $v['cache_type'] = $this->queue_redis_key[$v['type']] ?? 0; + $v['success_num'] = bcsub($v['total_num'], $v['surplus_num'], 0); + //是否显示停止按钮 + $v['is_stop_button'] = $v['status'] == 1; + $v['add_time'] = date('Y-m-d H:i:s', $v['add_time']); + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 将要执行的任务数据存入表中 + * @param array $where + * @param string $field + * @param array $data + * @param int $type + * @return mixed + */ + public function setQueueData(array $where = [], $field = "*", array $data = [], int $type = 1, $other = false) + { + if (!$type) throw new ValidateException('缺少执行任务类型'); + $queue_redis_keys = $this->queue_redis_key; + $redisKey = $queue_redis_keys[$type] ?? ''; + $queue_type_name = $this->queue_type_name; + $queueName = $queue_type_name[$type] ?? ''; + if (!$redisKey || !$queueName) { + throw new ValidateException('缺少队列缓存KEY,或者不存在此类型队列'); + } + //检查同类型其他任务 + $this->checkTypeQueue($redisKey); + + $source = 'admin'; + if (in_array($type, [1, 2, 3, 4, 5, 6])) { + $queueDataNum = $this->setRedisData($redisKey, $type, $data, $where, $field); + if (!$queueDataNum) { + throw new ValidateException('需要执行的批量数据为空'); + } + if (!$id = $this->dao->addQueueList($queueName, $queueDataNum, $type, $redisKey, $source)) { + throw new ValidateException('添加队列失败'); + } + } else { + if ($type == 7) { + $ids = array_column($data, 0); + } else { + $ids = $data; + } + /** @var StoreOrderServices $orderService */ + $orderService = app()->make(StoreOrderServices::class); + $oids = $orderService->getOrderDumpData($where, $ids, $field); + $order_ids = []; + if ($oids) { + //过滤拼团未完成订单 + foreach ($oids as $order) { + if (isset($order['pinkStatus']) && $order['pinkStatus'] != 2) { + continue; + } + $order_ids[] = $order['id']; + } + } + if (!$order_ids) { + throw new ValidateException('暂无需要发货订单'); + } + if (!$id = $this->dao->addQueueList($queueName, count($ids), $type, $redisKey, $source)) { + throw new ValidateException('添加队列失败'); + } + /** @var QueueAuxiliaryServices $auxiliaryService */ + $auxiliaryService = app()->make(QueueAuxiliaryServices::class); + $auxiliaryService->saveQueueOrderData($id, $order_ids, $data, $type, $redisKey); + } + + return $id; + } + + /** + * 队列数据放入redis集合 + * @param $redisKey + * @param $type + * @param array $dataIds + * @param array $where + * @param string $filed + * @return int + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setRedisData($redisKey, $type, array $dataIds, array $where, $filed = "*") + { + if (!$redisKey || !$type) return 0; + /** @var CacheService $redis */ + $redis = app()->make(CacheService::class); + if ($dataIds) { + foreach ($dataIds as $v) { + $redis->sAdd($redisKey, $v); + } + } else { + if ($where) { + foreach ($where as $k => $v) { + if (!$v) unset($where[$k]); + } + } + switch ($type) { + case 1://批量发放优惠券 + case 2://批量设置用户分组 + case 3://批量设置用户标签 + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $dataInfo = $userService->getUserInfoList($where, $filed); + if ($dataInfo) { + foreach ($dataInfo as $k => $v) { + $redis->sAdd($redisKey, $v['uid']); + } + } + break; + case 4://批量上下架商品 + $cateIds = []; + if (isset($where['cate_id']) && $where['cate_id']) { + /** @var StoreProductCategoryServices $storeCategory */ + $storeCategory = app()->make(StoreProductCategoryServices::class); + $cateIds = $storeCategory->getColumn(['pid' => $where['cate_id']], 'id'); + } + if ($cateIds) { + $cateIds[] = $where['cate_id']; + $where['cate_id'] = $cateIds; + } + /** @var StoreProductServices $productService */ + $productService = app()->make(StoreProductServices::class); + $dataInfo = $productService->getProductListByWhere($where, $filed); + if ($dataInfo) { + foreach ($dataInfo as $k => $v) { + $redis->sAdd($redisKey, $v['id']); + } + } + break; + case 5://批量删除商品规格 + /** @var StoreProductRuleServices $productRuleService */ + $productRuleService = app()->make(StoreProductRuleServices::class); + $dataInfo = $productRuleService->getProductRuleList($where, $filed); + if ($dataInfo) { + foreach ($dataInfo as $k => $v) { + $redis->sAdd($redisKey, $v['id']); + } + } + break; + case 6://批量删除用户已删除订单 + /** @var StoreOrderServices $orderService */ + $orderService = app()->make(StoreOrderServices::class); + $dataInfo = $orderService->getOrderListByWhere($where, $filed); + if ($dataInfo) { + foreach ($dataInfo as $k => $v) { + $redis->sAdd($redisKey, $v['id']); + } + } + break; + default: + return 0; + break; + } + + } + return $redis->sCard($redisKey); + } + + /** + * 获取队列redis中存的数据集合 + * @param string $redisKey + * @param array $queueInfo + * @return array + */ + public function getQueueRedisdata($queueInfo, string $redisKey = '') + { + if (!$queueInfo) return [$redisKey, []]; + if (!$redisKey) { + $redisKey = $queueInfo['execute_key'] ?? ''; + } + if (!$redisKey) { + return [$redisKey, []]; + } + /** @var CacheService $redis */ + $redis = app()->make(CacheService::class); + return [$redisKey, $redis->sMembers($redisKey)]; + } + + /** + * 批量发送优惠券 + * @param $coupon + * @param $type + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function sendCoupon($coupon, $type) + { + if (!$type || !$coupon) return false; + $queueInfo = $this->dao->getQueueOne(['type' => $type, 'status' => 0]); + if (!$queueInfo) { + return false; + } + //把队列需要执行的入参数据存起来,以便队列执行失败后接着执行,同时队列状态改为正在执行状态。 + $this->dao->setQueueDoing($coupon, $queueInfo['id']); + + [$redisKey, $uids] = $this->getQueueRedisdata($queueInfo); + if ($uids) { + $chunkUids = array_chunk($uids, 100, true); + /** @var StoreCouponIssueServices $issueService */ + $issueService = app()->make(StoreCouponIssueServices::class); + foreach ($chunkUids as $v) { + $issueService->setCoupon($coupon, $v, $redisKey, $queueInfo); + } + } + //发完后将队列置为完成 + $this->setQueueSuccess($queueInfo['id'], $queueInfo['type']); + return true; + } + + /** + * 批量设置用户分组 + * @param $groupId + * @param $type + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setUserGroup($groupId, $type) + { + if (!$groupId || !$type) return false; + $queueInfo = $this->dao->getQueueOne(['type' => $type, 'status' => 0]); + if (!$queueInfo) { + return false; + } + /** @var UserGroupServices $userGroup */ + $userGroup = app()->make(UserGroupServices::class); + if (!$userGroup->getGroup($groupId)) { + return false; + } + //把队列需要执行的入参数据存起来,以便队列执行失败后接着执行,同时队列状态改为正在执行状态。 + $this->dao->setQueueDoing($groupId, $queueInfo['id']); + + [$redisKey, $uids] = $this->getQueueRedisdata($queueInfo); + if ($uids) { + $chunkUids = array_chunk($uids, 1000, true); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + foreach ($chunkUids as $v) { + //执行分组 + if (!$userServices->setUserGroup($v, $groupId)) { + $this->setQueueFail($queueInfo['id'], $redisKey); + } else { + $this->doSuccessSremRedis($v, $redisKey, $type); + } + } + } + //发完后将队列置为完成 + $this->setQueueSuccess($queueInfo['id'], $queueInfo['type']); + return true; + } + + /** + * 批量设置用户标签 + * @param $labelId + * @param $type + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setUserLabel($labelId, $type, $other = []) + { + if (!$labelId || !$type) return false; + $queueInfo = $this->dao->getQueueOne(['type' => $type, 'status' => 0]); + if (!$queueInfo) { + return false; + } + /** @var UserLabelServices $userLabelServices */ + $userLabelServices = app()->make(UserLabelServices::class); + $count = $userLabelServices->getCount([['id', 'IN', $labelId]]); + if ($count != count($labelId)) { + return false; + } + //把队列需要执行的入参数据存起来,以便队列执行失败后接着执行,同时队列状态改为正在执行状态。 + $this->dao->setQueueDoing($labelId, $queueInfo['id']); + + [$redisKey, $uids] = $this->getQueueRedisdata($queueInfo); + if ($uids) { + $chunkUids = array_chunk($uids, 1000, true); + /** @var UserLabelRelationServices $services */ + $services = app()->make(UserLabelRelationServices::class); + $store_id = (int)$other['store_id'] ?? 0; + foreach ($chunkUids as $v) { + if (!$services->setUserLable($v, $labelId, $store_id ? 1 : 0, $store_id)) { + $this->setQueueFail($queueInfo['id'], $redisKey); + } else { + $this->doSuccessSremRedis($v, $redisKey, $type); + } + } + } + //发完后将队列置为完成 + $this->setQueueSuccess($queueInfo['id'], $queueInfo['type']); + return true; + } + + /** + * 商品批量上下架 + * @param string $upORdown + * @param $type + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setProductShow($upORdown = "up", $type = 1) + { + if (!$type) return false; + $queueInfo = $this->dao->getQueueOne(['type' => $type, 'status' => 0]); + if (!$queueInfo) { + return false; + } + //把队列需要执行的入参数据存起来,以便队列执行失败后接着执行,同时队列状态改为正在执行状态。 + $this->dao->setQueueDoing($upORdown, $queueInfo['id']); + + [$redisKey, $pids] = $this->getQueueRedisdata($queueInfo); + if ($pids) { + $chunkPids = array_chunk($pids, 1000, true); + $isShow = 0; + if ($upORdown == 'up') $isShow = 1; + /** @var StoreProductServices $storeproductServices */ + $storeproductServices = app()->make(StoreProductServices::class); + /** @var StoreProductRelationServices $storeProductRelationServices */ + $storeProductRelationServices = app()->make(StoreProductRelationServices::class); + /** @var StoreCartServices $cartService */ + $cartService = app()->make(StoreCartServices::class); + $update = ['is_show' => $isShow]; + if ($isShow) {//手动上架 清空定时下架状态 + $update['auto_off_time'] = 0; + } + + foreach ($chunkPids as $v) { + //商品 + $res = $storeproductServices->batchUpdate($v, $update); + //门店商品 + $storeproductServices->batchUpdateAppendWhere($v, $update, ['type' => 1], 'pid'); + + if ($isShow == 0) { + $storeProductRelationServices->setShow($v, (int)$isShow); + //购物车 + $cartService->batchUpdate($v, ['status' => 1], 'product_id'); + } + //下架检测是否有参与活动商品 + try { + $is_activity = $storeproductServices->checkActivity($v); + } catch (\Throwable $e) { + $is_activity = false; + } + if ($isShow == 0 || $is_activity) { + //改变购物车中状态 + $storeProductRelationServices->setShow($v, (int)$isShow); + } + if (!$res) { + $this->setQueueFail($queueInfo['id'], $redisKey); + } else { + $this->doSuccessSremRedis($v, $redisKey, $type); + } + } + } + //发完后将队列置为完成 + $this->setQueueSuccess($queueInfo['id'], $queueInfo['type']); + return true; + } + + /** + * 批量队列删除商品规格 + * @param $type + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function delProductRule($type) + { + if (!$type) return false; + $queueInfo = $this->dao->getQueueOne(['type' => $type, 'status' => 0]); + if (!$queueInfo) { + return false; + } + //把队列需要执行的入参数据存起来,以便队列执行失败后接着执行,同时队列状态改为正在执行状态。 + $this->dao->setQueueDoing('', $queueInfo['id']); + + [$redisKey, $pids] = $this->getQueueRedisdata($queueInfo); + if ($pids) { + $chunkPids = array_chunk($pids, 1000, true); + /** @var StoreProductRuleServices $storeProductRuleservices */ + $storeProductRuleservices = app()->make(StoreProductRuleServices::class); + foreach ($chunkPids as $v) { + $res = $storeProductRuleservices->del(implode(',', $v)); + if ($res) { + $this->doSuccessSremRedis($v, $redisKey, $queueInfo['type']); + } else { + $this->addQueueFail($queueInfo['id'], $redisKey); + } + } + } + //发完后将队列置为完成 + $this->setQueueSuccess($queueInfo['id'], $queueInfo['type']); + return true; + } + + /** + * 批量队列删除订单 + * @param $type + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function delOrder($type) + { + if (!$type) return false; + $queueInfo = $this->dao->getQueueOne(['type' => $type, 'status' => 0]); + if (!$queueInfo) { + return false; + } + //把队列需要执行的入参数据存起来,以便队列执行失败后接着执行,同时队列状态改为正在执行状态。 + $this->dao->setQueueDoing('', $queueInfo['id']); + + [$redisKey, $pids] = $this->getQueueRedisdata($queueInfo); + if ($pids) { + $chunkPids = array_chunk($pids, 1000, true); + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + foreach ($chunkPids as $v) { + $res = $storeOrderServices->batchUpdateOrder($v, ['is_system_del' => 1]); + if ($res) { + $this->doSuccessSremRedis($v, $redisKey, $type); + } else { + $this->setQueueFail($queueInfo['id'], $redisKey); + } + } + } + //发完后将队列置为完成 + $this->setQueueSuccess($queueInfo['id'], $queueInfo['type']); + return true; + } + + /** + * 队列批量发货 + * @param $oid + * @param array $deliveryData + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function orderDelivery($oid, array $deliveryData) + { + if (!$oid) return false; + if (!isset($deliveryData['queueType'])) { + return false; + } + /** @var QueueAuxiliaryServices $auxiliaryService */ + $auxiliaryService = app()->make(QueueAuxiliaryServices::class); + //看是否能查到任务数据 + $auxiliaryInfo = $auxiliaryService->getOrderCacheOne(['binding_id' => $deliveryData['queueId'], 'relation_id' => $oid, 'type' => $deliveryData['cacheType']]); + if (!$auxiliaryInfo || !$auxiliaryInfo['other']) { + return false; + } + $deliveryInfo = json_decode($auxiliaryInfo['other'], true); + if ($deliveryData['queueType'] == 7) { + if (!$deliveryInfo['delivery_name'] || !$deliveryInfo['delivery_code'] || !$deliveryInfo['delivery_id']) { + return false; + } + $deliveryData['express_record_type'] = 1; + $deliveryData['delivery_name'] = $deliveryInfo['delivery_name']; + $deliveryData['delivery_id'] = $deliveryInfo['delivery_id']; + $deliveryData['delivery_code'] = $deliveryInfo['delivery_code']; + } + + try { + /** @var StoreOrderDeliveryServices $storeOrderDelivery */ + $storeOrderDelivery = app()->make(StoreOrderDeliveryServices::class); + //发货 + $storeOrderDelivery->delivery($oid, $deliveryData); + } catch (\Throwable $e) { + Log::error('队列发货失败发货,order_id:' . $oid . ',原因:' . $e->getMessage()); + } + + //更改队列子集数据 + $this->doSuccessSremRedis(['order_id' => $oid], $deliveryData['queueId'], $deliveryData['queueType'], ['phone_message' => 1, 'status' => 1]); + //队列置为完成 + return $this->setQueueSuccess($deliveryData['queueId'], $deliveryData['queueType']); + } + + /** + * 添加任务前校验同类型任务状态 + * @param $type + * @param array $queueInfo + * @param false $is_again + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkTypeQueue($type, array $queueInfo = [], bool $is_again = false) + { + if (!$type) return false; + if (!$queueInfo) { + $queueInfo = $this->dao->getQueueOne(['type' => $type, 'status' => [0, 1]]); + } + if (!$queueInfo) { + return false; + } + $num = 0; + if (in_array($type, [7, 8, 9, 10])) { + /** @var QueueAuxiliaryServices $auxiliaryService */ + $auxiliaryService = app()->make(QueueAuxiliaryServices::class); + $num = $auxiliaryService->count(['binding_id' => $queueInfo['id'], 'status' => 0]); + } else { + if ($queueInfo['execute_key']) { + /** @var CacheService $redis */ + $redis = app()->make(CacheService::class); + $num = $redis->sCard($queueInfo['execute_key']); + } + } + if ($num) { + if (!$is_again) { + if ($queueInfo['status'] == 0) { + throw new AdminException('上次批量任务尚未执行,请前往任务列表手动执行'); + } + if ($queueInfo['status'] == 1) { + throw new AdminException('有正在执行中的任务,请耐心等待,若长时间无反应,前往任务列表修复异常数据,再手动执行'); + } + } + } else { + $this->delWrongQueue(0, $type); + return false; + } + return true; + } + + /** + * 修复异常任务 + * @param $queueInfo + * @return bool|mixed + */ + public function repairWrongQueue($queueInfo) + { + if (!$queueInfo) throw new AdminException('任务不存在'); + try { + switch ($queueInfo['type']) { + case 1://批量发放优惠券 + case 2://批量设置用户分组 + case 3://批量设置用户标签 + case 4://批量上下架商品 + case 5://批量删除商品规格 + case 6://批量删除用户已删除订单 + if (!$queueInfo['execute_key']) { + throw new AdminException('缓存key缺失,请清除数据'); + } + /** @var CacheService $redis */ + $redis = app()->make(CacheService::class); + $cacheNum = $redis->sCard($queueInfo['execute_key']); + if ($cacheNum != $queueInfo['surplus_num']) { + return $this->dao->update(['id' => $queueInfo['id']], ['surplus_num' => $cacheNum]); + } + break; + case 7://手动发货 + case 8://电子面单发货 + case 9://批量配送 + case 10://批量虚拟发货 + /** @var QueueAuxiliaryServices $auxiliaryService */ + $auxiliaryService = app()->make(QueueAuxiliaryServices::class); + $cacheType = $this->queue_redis_key[$queueInfo['type']] ?? ''; + + $cacheFailAndNoNum = $auxiliaryService->getCountOrder(['binding_id' => $queueInfo['id'], 'type' => $cacheType, 'status' => [0, 2]]); + $cacheTotalNum = $auxiliaryService->getCountOrder(['binding_id' => $queueInfo['id'], 'type' => $cacheType, 'status' => [0, 1, 2]]); + //如果任务已经执行完毕,但是记录却存在未执行数据,要进行修复,让其重新执行 + if ($cacheFailAndNoNum && $queueInfo['status'] == 2) return $this->dao->update(['id' => $queueInfo['id']], ['status' => 3, 'surplus_num' => $cacheFailAndNoNum, 'total_num' => $cacheTotalNum]); + //如果执行失败,记录全部执行成功,那么进行修复 + if (!$cacheFailAndNoNum && $queueInfo['status'] != 2) return $this->dao->update(['id' => $queueInfo['id']], ['status' => 2, 'surplus_num' => 0, 'total_num' => $cacheTotalNum]); + } + return true; + } catch (\Exception $e) { + throw new AdminException($e->getMessage()); + } + } + + /** + * 队列再次执行 + * @param $queueId + * @param $type + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function againDoQueue($queueId, $type) + { + $queueInfo = $this->getQueueOne(['id' => $queueId, 'type' => $type]); + if (!$queueInfo) { + throw new AdminException('队列任务不存在'); + } + if (!$queueInfo['queue_in_value']) { + throw new AdminException('队列关键数据缺失,请清除此任务及异常数据'); + } + if ($queueInfo['status'] == 2) { + throw new AdminException('队列已完成'); + } + if ($queueInfo['status'] == 3) { + throw new AdminException('队列异常,请清除队列重新加入'); + } + if ($queueInfo['status'] == 4) { + throw new AdminException('队列已删除'); + } + //检测当前队列 + if (!$this->checkTypeQueue($type, $queueInfo, true)) { + throw new AdminException('任务已清除,无需再次执行'); + } + //先进行数据修复 + $this->repairWrongQueue($queueInfo); + + if (in_array($type, [7, 8, 9, 10])) { + $queueInValue = json_decode($queueInfo['queue_in_value'], true); + /** @var StoreOrderServices $storeOrderService */ + $storeOrderService = app()->make(StoreOrderServices::class); + $storeOrderService->adminQueueOrderDo($queueInValue, true); + } else { + $queueInValue = $queueInfo['queue_in_value']; + if ($type == 1) { + $queueInValue = json_decode($queueInfo['queue_in_value'], true); + } + //加入队列 + BatchHandleJob::dispatch([$queueInValue, $type]); + } + + return true; + } + + /** + * 任务执行失败,修改队列状态 + * @param $queueId + * @param string $redisKey + * @return mixed + */ + public function setQueueFail($queueId, $redisKey = '') + { + if ($redisKey) { + /** @var CacheService $cacheService */ + $cacheService = app()->make(CacheService::class); + $surplusNum = $cacheService->sCard($redisKey); + } else { + /** @var QueueAuxiliaryServices $auxiliaryService */ + $auxiliaryService = app()->make(QueueAuxiliaryServices::class); + $surplusNum = $auxiliaryService->count(['binding_id' => $queueId, 'status' => 0]); + } + return $this->dao->update(['id' => $queueId], ['status' => 3, 'surplus_num' => $surplusNum]); + } + + /** + * 将执行成功数据移除redis集合 + * @param array $data + * @param $redisKey + * @return bool + */ + public function doSuccessSremRedis(array $data, $redisKey, $type, array $otherData = []) + { + if (!$data || !$redisKey || !$type) return true; + if (in_array($type, [7, 8, 9, 10])) { + $where['relation_id'] = $data['order_id']; + $where['binding_id'] = $redisKey; + /** @var QueueAuxiliaryServices $auxiliaryService */ + $auxiliaryService = app()->make(QueueAuxiliaryServices::class); + $getOne = $auxiliaryService->getOrderCacheOne($where); + if (!$getOne) return false; + $other = json_decode($getOne['other'], true); + if (isset($otherData['delivery_status'])) $other['delivery_status'] = $otherData['delivery_status']; + if (isset($otherData['wx_message'])) $other['wx_message'] = $otherData['wx_message']; + if (isset($otherData['phone_message'])) $other['phone_message'] = $otherData['phone_message']; + if (isset($otherData['error_info'])) $other['error_info'] = $otherData['error_info']; + $updateData['status'] = isset($otherData['status']) ? $otherData['status'] : 0; + $updateData['other'] = json_encode($other); + $updateData['update_time'] = time(); + $auxiliaryService->updateOrderStatus($where, $updateData); + } else {//在redis缓存集合的从集合删除 + /** @var CacheService $redis */ + $redis = app()->make(CacheService::class); + foreach ($data as $k => $v) { + $redis->sRem($redisKey, $v); + } + } + return true; + } + + /** + * 任务执行成功 + * @param $queueId + * @param $type + * @return bool|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setQueueSuccess($queueId, $type) + { + if (!$queueId || !$type) return false; + $queueInfo = $this->dao->get($queueId); + if (!$queueInfo) return false; + $res = true; + if (in_array($type, [7, 8, 9, 10])) { + $res = false; + if ($queueInfo['surplus_num'] > 0) { + $this->dao->bcDec($queueId, 'surplus_num', 1); + } + //看是否全部执行成功 + $queueInfo = $this->dao->get($queueId); + if ($queueInfo['surplus_num'] == 0) { + $res = true; + } + } + if ($res) { + $update = [ + 'status' => 2, + 'finish_time' => time(), + 'surplus_num' => 0 + ]; + return $this->dao->update(['id' => $queueId], $update); + } + return true; + + } + + /** + * 清除异常队列 + * @param $queueId + * @param $type + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function delWrongQueue($queueId, $type, $is_del = true) + { + if (!$type) return false; + if ($queueId) { + $queueInfo = $this->dao->getQueueOne(['id' => $queueId, 'type' => $type]); + } else { + $queueInfo = $this->dao->getQueueOne(['type' => $type]); + } + if (!$queueInfo) { + return true; + } + try { + $data = ['status' => 3]; + if ($is_del) { + if (in_array($type, [7, 8, 9, 10])) { + /** @var QueueAuxiliaryServices $auxiliaryService */ + $auxiliaryService = app()->make(QueueAuxiliaryServices::class); + $auxiliaryService->batchUpdate(['binding_id' => $queueInfo['id']], ['status' => 3]); + } else { + if ($queueInfo['execute_key']) { + /** @var CacheService $redis */ + $redis = app()->make(CacheService::class); + $redis->del($queueInfo['execute_key']); + } + } + $data = ['is_del' => 1, 'status' => 4]; + } + $this->dao->update(['id' => $queueInfo['id']], $data); + } catch (\Throwable $e) { + Log::error('清除异常队列失败,原因' . $e->getMessage()); + } + return true; + + } +} diff --git a/app/services/pay/OrderPayServices.php b/app/services/pay/OrderPayServices.php new file mode 100644 index 0000000..42d9285 --- /dev/null +++ b/app/services/pay/OrderPayServices.php @@ -0,0 +1,225 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\pay; + + +use app\services\order\StoreOrderCartInfoServices; +use app\services\wechat\WechatUserServices; +use think\exception\ValidateException; + +/** + * 订单发起支付 + * Class OrderPayServices + * @package app\services\pay + */ +class OrderPayServices +{ + /** + * 支付 + * @var PayServices + */ + protected $payServices; + + public function __construct(PayServices $services) + { + $this->payServices = $services; + } + + /** + * 订单发起支付 + * @param array $orderInfo + * @return array|string + */ + public function orderPay(array $orderInfo, string $payType) + { + if ($orderInfo['paid']) { + throw new ValidateException('订单已支付!'); + } + if ($orderInfo['pay_price'] <= 0) { + throw new ValidateException('该支付无需支付!'); + } + $openid = ''; + if (!in_array($payType, ['weixinh5', 'pc', 'store']) && !request()->isApp()) { + if ($payType === 'weixin') { + $userType = 'wechat'; + } else { + $userType = $payType; + } + /** @var WechatUserServices $services */ + $services = app()->make(WechatUserServices::class); + $openid = $services->uidToOpenid($orderInfo['uid'], $userType); + if (!$openid) { + throw new ValidateException('获取用户openid失败,无法支付'); + } + } + $site_name = sys_config('site_name'); + if (isset($orderInfo['member_type'])) { + $body = substrUTf8($site_name . '--' . $orderInfo['member_type'], 30); + $successAction = "member"; + } else { + /** @var StoreOrderCartInfoServices $orderInfoServices */ + $orderInfoServices = app()->make(StoreOrderCartInfoServices::class); + $body = $orderInfoServices->getCarIdByProductTitle((int)$orderInfo['id']); + $body = substrUTf8($site_name . '--' . $body, 30); + $successAction = "product"; + } + + if (!$body) { + throw new ValidateException('支付参数缺少:请前往后台设置->系统设置-> 填写 网站名称'); + } + return $this->payServices->pay($payType, $openid, $orderInfo['order_id'], $orderInfo['pay_price'], $successAction, $body); + } + + /** + * 支付宝支付 + * @param array $orderInfo + * @param string $quitUrl + * @return array|string + */ + public function alipayOrder(array $orderInfo, string $quitUrl, bool $isCode = false) + { + if ($orderInfo['paid']) { + throw new ValidateException('订单已支付!'); + } + if ($orderInfo['pay_price'] <= 0) { + throw new ValidateException('该支付无需支付!'); + } + $site_name = sys_config('site_name'); + if (isset($orderInfo['member_type'])) { + $body = substrUTf8($site_name . '--' . $orderInfo['member_type'], 30); + $successAction = "member"; + } else { + /** @var StoreOrderCartInfoServices $orderInfoServices */ + $orderInfoServices = app()->make(StoreOrderCartInfoServices::class); + $body = $orderInfoServices->getCarIdByProductTitle((int)$orderInfo['id']); + $body = substrUTf8($site_name . '--' . $body, 30); + $successAction = "product"; + } + + if (!$body) { + throw new ValidateException('支付参数缺少:请前往后台设置->系统设置-> 填写 网站名称'); + } + return $this->payServices->pay('alipay', $quitUrl, $orderInfo['order_id'], $orderInfo['pay_price'], $successAction, $body, $isCode); + } + + /** + * 积分商品订单发起支付 + * @param array $orderInfo + * @return array|string + */ + public function orderIntegralPay(array $orderInfo, string $payType) + { + if ($orderInfo['paid']) { + throw new ValidateException('订单已支付!'); + } + if ($orderInfo['total_price'] <= 0) { + throw new ValidateException('该支付无需支付!'); + } + $openid = ''; + if (!in_array($payType, ['weixinh5', 'pc', 'store']) && !request()->isApp()) { + if ($payType === 'weixin') { + $userType = 'wechat'; + } else { + $userType = $payType; + } + /** @var WechatUserServices $services */ + $services = app()->make(WechatUserServices::class); + $openid = $services->uidToOpenid($orderInfo['uid'], $userType); + if (!$openid) { + throw new ValidateException('获取用户openid失败,无法支付'); + } + } + $site_name = sys_config('site_name'); + $body = $orderInfo['store_name']; + $body = substrUTf8($site_name . '--' . $body, 30); + $successAction = "integral"; + + if (!$body) { + throw new ValidateException('支付参数缺少:请前往后台设置->系统设置-> 填写 网站名称'); + } + return $this->payServices->pay($payType, $openid, $orderInfo['order_id'], $orderInfo['total_price'], $successAction, $body); + } + + /** + * 积分商品支付宝支付 + * @param array $orderInfo + * @param string $quitUrl + * @return array|string + */ + public function alipayIntegralOrder(array $orderInfo, string $quitUrl, bool $isCode = false) + { + if ($orderInfo['paid']) { + throw new ValidateException('订单已支付!'); + } + if ($orderInfo['total_price'] <= 0) { + throw new ValidateException('该支付无需支付!'); + } + $site_name = sys_config('site_name'); + $body = $orderInfo['store_name']; + $body = substrUTf8($site_name . '--' . $body, 30); + $successAction = "integral"; + + if (!$body) { + throw new ValidateException('支付参数缺少:请前往后台设置->系统设置-> 填写 网站名称'); + } + return $this->payServices->pay('alipay', $quitUrl, $orderInfo['order_id'], $orderInfo['total_price'], $successAction, $body, $isCode); + } + + /**收银台会员充值 + * @param array $orderInfo + * @param string $payType + * @param string $authCode + * @return array|string + */ + public function otherRecharge(array $orderInfo, string $payType, string $authCode = '') + { + if (!$orderInfo) { + throw new ValidateException('订单失效或者不存在'); + } + if ($orderInfo['paid'] == 1) { + throw new ValidateException('订单已支付'); + } + $openid = ''; + //没有付款码,不是微信H5支付,门店支付,PC支付,不再APP端,需要判断用户openid + if (!$authCode && !in_array($payType, ['weixinh5', 'store', 'pc', 'alipay']) && !request()->isApp()) { + $userType = ''; + switch ($payType) { + case 'weixin': + case 'weixinh5': + $userType = 'wechat'; + break; + case 'routine': + $userType = 'routine'; + break; + } + if (!$userType) { + throw new ValidateException('不支持该类型方式'); + } + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + $openid = $wechatUser->uidToOpenid((int)$orderInfo['uid'], $userType); + if (!$openid) { + throw new ValidateException('获取用户openid失败,无法支付'); + } + } + $site_name = sys_config('site_name'); + if (isset($orderInfo['member_type'])) { + $body = substrUTf8($site_name . '--' . $orderInfo['member_type'], 30); + $successAction = "member_recharge"; + } + + if (!$body) { + throw new ValidateException('支付参数缺少:请前往后台设置->系统设置-> 填写 网站名称'); + } + return $this->payServices->setAuthCode($authCode)->pay($payType, $openid, $orderInfo['order_id'], $orderInfo['pay_price'], $successAction, $body); + } +} diff --git a/app/services/pay/PayServices.php b/app/services/pay/PayServices.php new file mode 100644 index 0000000..54ccd30 --- /dev/null +++ b/app/services/pay/PayServices.php @@ -0,0 +1,151 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\pay; + +use crmeb\services\AliPayService; +use crmeb\services\wechat\Payment; +use think\exception\ValidateException; + +/** + * 支付统一入口 + * Class PayServices + * @package app\services\pay + */ +class PayServices +{ + /** + * 微信支付类型 + */ + const WEIXIN_PAY = 'weixin'; + + /** + * 余额支付 + */ + const YUE_PAY = 'yue'; + + /** + * 线下支付 + */ + const OFFLINE_PAY = 'offline'; + + /** + * 支付宝 + */ + const ALIAPY_PAY = 'alipay'; + + /** + * 现金支付 + */ + const CASH_PAY = 'cash'; + + /** + * 支付方式 + * @var string[] + */ + const PAY_TYPE = [ + PayServices::WEIXIN_PAY => '微信支付', + PayServices::YUE_PAY => '余额支付', + PayServices::OFFLINE_PAY => '线下支付', + PayServices::ALIAPY_PAY => '支付宝', + PayServices::CASH_PAY => '现金支付', + ]; + + /** + * 二维码条码值 + * @var string + */ + protected $authCode; + + /** + * 设置二维码条码值 + * @param string $authCode + * @return $this + */ + public function setAuthCode(string $authCode) + { + $this->authCode = $authCode; + return $this; + } + + /** + * 发起支付 + * @param string $payType + * @param string $openid + * @param string $orderId + * @param string $price + * @param string $successAction + * @param string $body + * @return array|string + */ + public function pay(string $payType, string $openid, string $orderId, string $price, string $successAction, string $body, bool $isCode = false) + { + try { + switch ($payType) { + case 'routine': + //微信支付,从APP端请求过来 + if (request()->isApp()) { + return Payment::appPay($openid, $orderId, $price, $successAction, $body); + } else { + //判断有没有打开小程序支付 + if (sys_config('pay_routine_open', 0)) { + return Payment::miniPay($openid, $orderId, $price, $successAction, $body); + } else { + //开启了v3支付 + if (Payment::instance()->isV3PAy) { + return Payment::instance()->application()->v3pay->miniprogPay($openid, $orderId, $price, $body, $successAction); + } + return Payment::jsPay($openid, $orderId, $price, $successAction, $body); + } + } + case 'weixinh5': + ////开启了v3支付 + if (Payment::instance()->isV3PAy) { + return Payment::instance()->application()->v3pay->h5Pay($orderId, $price, $body, $successAction); + } + //旧版v2支付 + return Payment::paymentOrder(null, $orderId, $price, $successAction, $body, '', 'MWEB'); + case self::WEIXIN_PAY: + //微信支付,付款码支付,付款码支付使用v2支付接口 + if ($this->authCode) { + return Payment::microPay($this->authCode, $orderId, $price, $successAction, $body); + } else { + //微信支付,从APP端请求过来 + if (request()->isApp()) { + return Payment::appPay($openid, $orderId, $price, $successAction, $body); + } else { + //开启了v3支付 + if (Payment::instance()->isV3PAy) { + return Payment::instance()->application()->v3pay->jsapiPay($openid, $orderId, $price, $body, $successAction); + } + //使用v2旧版支付接口 + return Payment::jsPay($openid, $orderId, $price, $successAction, $body); + } + } + case self::ALIAPY_PAY: + if ($this->authCode) { + return AliPayService::instance()->microPay($this->authCode, $body, $orderId, $price, $successAction); + } else { + return AliPayService::instance()->create($body, $orderId, $price, $successAction, $openid, $openid, $isCode); + } + case 'pc': + case 'store': + //方法内部已经做了区分v2和v3 + return Payment::nativePay($openid, $orderId, $price, $successAction, $body); + default: + throw new ValidateException('支付方式不存在'); + } + } catch (\Throwable $e) { + throw new ValidateException($e->getMessage()); + } + } +} diff --git a/app/services/pay/RechargeServices.php b/app/services/pay/RechargeServices.php new file mode 100644 index 0000000..5e06e3b --- /dev/null +++ b/app/services/pay/RechargeServices.php @@ -0,0 +1,80 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\pay; + + +use app\services\user\UserRechargeServices; +use app\services\wechat\WechatUserServices; +use think\exception\ValidateException; + +/** + * + * Class RechargeServices + * @package app\services\pay + */ +class RechargeServices +{ + protected $pay; + + /** + * RechargeServices constructor. + * @param PayServices $pay + */ + public function __construct(PayServices $pay) + { + $this->pay = $pay; + } + + /** + * @param int $recharge_id + * @param string $authCode + * @return array|string + */ + public function recharge(int $recharge_id, string $authCode = '') + { + /** @var UserRechargeServices $rechargeServices */ + $rechargeServices = app()->make(UserRechargeServices::class); + $recharge = $rechargeServices->getRecharge($recharge_id); + if (!$recharge) { + throw new ValidateException('订单失效或者不存在'); + } + if ($recharge['paid'] == 1) { + throw new ValidateException('订单已支付'); + } + $openid = ''; + //没有付款码,不是微信H5支付,门店支付,PC支付,不再APP端,需要判断用户openid + if (!$authCode && !in_array($recharge['recharge_type'], ['weixinh5', 'store', 'pc', 'alipay']) && !request()->isApp()) { + $userType = ''; + switch ($recharge['recharge_type']) { + case 'weixin': + case 'weixinh5': + $userType = 'wechat'; + break; + case 'routine': + $userType = 'routine'; + break; + } + if (!$userType) { + throw new ValidateException('不支持该类型方式'); + } + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + $openid = $wechatUser->uidToOpenid((int)$recharge['uid'], $userType); + if (!$openid) { + throw new ValidateException('获取用户openid失败,无法支付'); + } + } + return $this->pay->setAuthCode($authCode)->pay($recharge['recharge_type'], $openid, $recharge['order_id'], $recharge['price'], 'user_recharge', '用户充值'); + } + +} diff --git a/app/services/product/branch/StoreBranchProductAttrValueServices.php b/app/services/product/branch/StoreBranchProductAttrValueServices.php new file mode 100644 index 0000000..9b8c313 --- /dev/null +++ b/app/services/product/branch/StoreBranchProductAttrValueServices.php @@ -0,0 +1,132 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\branch; + + +use app\dao\product\sku\StoreProductAttrValueDao; +use app\services\BaseServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\product\product\StoreProductStockRecordServices; +// use app\webscoket\SocketPush; +use crmeb\exceptions\AdminException; +use crmeb\traits\ServicesTrait; + +/** + * Class StoreBranchProductAttrValueServices + * @package app\services\product\branch + * @mixin StoreProductAttrValueDao + */ +class StoreBranchProductAttrValueServices extends BaseServices +{ + + use ServicesTrait; + + /** + * StoreBranchProductAttrValueServices constructor. + * @param StoreProductAttrValueDao $dao + */ + public function __construct(StoreProductAttrValueDao $dao) + { + $this->dao = $dao; + } + + /** + * @param string $unique + * @param int $storeId + * @return int|mixed + */ + public function uniqueByStock(string $unique, int $storeId) + { + if (!$unique) return 0; + return $this->dao->uniqueByStock($unique, $storeId); + } + + /** + * 更新 + * @param int $id + * @param array $data + * @param int $store_id + */ + public function updataAll(int $id, array $data, int $store_id) + { + /** @var StoreBranchProductServices $productServices */ + $productServices = app()->make(StoreBranchProductServices::class); + $where = []; + $where['product_id'] = $id; + $where['store_id'] = $store_id; + $where['type'] = 0; + + $this->transaction(function () use ($id, $store_id, $where, $data, $productServices) { + $attrArr = []; + $stock = 0; + $this->dao->delete($where); + foreach ($data['attrs'] as $key => $item) { + $attrArr[$key]['product_id'] = $id; + $attrArr[$key]['store_id'] = $store_id; + $attrArr[$key]['unique'] = $item['unique'] ?? ''; + $attrArr[$key]['stock'] = intval($item['stock']) ?? 0; + $attrArr[$key]['bar_code'] = $item['bar_code'] ?? 0; + $attrArr[$key]['type'] = 0; + $stock += (int)($item['stock'] ?? 0); + } + $res1 = $this->dao->saveAll($attrArr); + $productServices->saveStoreProduct($id, $store_id, $stock, $data); + $unique = array_column($data['attrs'], 'unique'); + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $storeProductAttrValueServices->updateSumStock($unique); + //记录入出库 + /** @var StoreProductStockRecordServices $storeProductStockRecordServces */ + $storeProductStockRecordServces = app()->make(StoreProductStockRecordServices::class); + $storeProductStockRecordServces->saveRecord($id, $attrArr, 0, $store_id); + if (!$res1) { + throw new AdminException('添加失败!'); + } + }); + } + + /** + * 获取某个门店商品下的库存 + * @param int $storeId + * @param int $productId + * @return array + */ + public function getProductAttrUnique(int $storeId, int $productId) + { + return $this->dao->getColumn(['store_id' => $storeId, 'product_id' => $productId], 'stock', 'unique'); + } + + /** + * 获取某个sku下的商品库存总和 + * @param string $unique + * @return array + */ + public function getProductAttrValueStockSum(array $unique) + { + return $this->dao->getColumn([['unique', 'in', $unique]], 'sum(stock) as sum_stock', 'unique'); + } + + /** + * 获取门店商品规格信息 + * @param int $id + * @param int $type + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreProductAttr(int $id, int $type = 0) + { + return $this->dao->getProductAttrValue(['product_id' => $id, 'type' => $type]); + } + +} diff --git a/app/services/product/branch/StoreBranchProductServices.php b/app/services/product/branch/StoreBranchProductServices.php new file mode 100644 index 0000000..37ad1d7 --- /dev/null +++ b/app/services/product/branch/StoreBranchProductServices.php @@ -0,0 +1,937 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\branch; + + +use app\dao\product\product\StoreProductDao; +use app\jobs\product\ProductSyncStoreJob; +use app\services\activity\seckill\StoreSeckillServices; +use app\services\BaseServices; +use app\services\order\StoreCartServices; +use app\services\product\brand\StoreBrandServices; +use app\services\product\product\StoreDescriptionServices; +use app\services\product\product\StoreProductRelationServices; +use app\services\product\product\StoreProductReplyCommentServices; +use app\services\product\product\StoreProductReplyServices; +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrResultServices; +use app\services\product\sku\StoreProductAttrServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\activity\coupon\StoreCouponIssueServices; +use app\services\product\category\StoreProductCategoryServices; +use app\services\store\SystemStoreServices; +use app\services\system\attachment\SystemAttachmentServices; +use app\services\user\UserServices; +// use app\webscoket\SocketPush; +use crmeb\exceptions\AdminException; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; + +/** + * Class StoreBranchProductServices + * @package app\services\product\branch + * @mixin StoreProductDao + */ +class StoreBranchProductServices extends BaseServices +{ + + use ServicesTrait; + + /** + * StoreBranchProductServices constructor. + * @param StoreProductDao $dao + */ + public function __construct(StoreProductDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取平台商品ID + * @param int $product_id + * @param $productInfo + * @return int + */ + public function getStoreProductId(int $product_id, $productInfo = []) + { + $id = 0; + if (!$product_id) { + return $id; + } + if (!$productInfo) { + $productInfo = $this->dao->get($product_id, ['id', 'pid']); + } + if ($productInfo) { + //门店平台共享商品 + if ($productInfo['pid']) { + $id = (int)$productInfo['pid']; + } else { + $id = $productInfo['id']; + } + } + return $id; + } + + /** + * 平台商品ID:获取在门店该商品详情 + * @param int $uid + * @param int $id + * @param int $store_id + * @return array|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreProductInfo(int $uid, int $id, int $store_id) + { + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + try { + $productInfo = $productServices->getCacheProductInfo($id); + } catch (\Throwable $e) { + $productInfo = []; + } + if (!$productInfo) { + return []; + } + if ($productInfo['type'] != 1 && $store_id) {//查询该门店商品 + $info = $this->dao->get(['is_del' => 0, 'is_show' => 1, 'is_verify' => 1, 'pid' => $id, 'type' => 1, 'relation_id' => $store_id], ['id']); + if ($info) { + $id = (int)$info['id']; + } + } + return $productServices->productDetail($uid, $id); + } + + /** + * 批量获取平台商品ID + * @param array $product_ids + * @return array + */ + public function getStoreProductIds(array $product_ids) + { + $result = []; + if (!$product_ids) { + return $result; + } + $productInfos = $this->dao->getColumn([['id', 'IN', $product_ids]], 'id,pid', 'id'); + if ($productInfos) { + foreach ($productInfos as $key => $productInfo) { + //门店平台共享商品 + if ($productInfo['pid']) { + $id = (int)$productInfo['pid']; + } else { + $id = $productInfo['id']; + } + $result[$key] = $id; + } + } + return $result; + } + + /** + * 根据商品ID,获取适用门店ids + * @param int $product_id + * @param int $type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getApplicableStoreIds(int $product_id, int $type = 0) + { + $ids = []; + $productInfo = []; + switch ($type) { + case 0://商品 + $productInfo = $this->dao->getOne(['id' => $product_id], 'id,type,relation_id,applicable_type,applicable_store_id'); + break; + case 1://秒杀商品 + /** @var StoreSeckillServices $seckillServices */ + $seckillServices = app()->make(StoreSeckillServices::class); + $productInfo = $seckillServices->getOne(['id' => $product_id], 'id,applicable_type,applicable_store_id'); + break; + } + $productInfo['applicable_type'] = 1; + if ($productInfo) { + switch ($productInfo['applicable_type']) { + case 0://仅平台 + break; + case 1://所有门店 查询有商品的门店 + if ($type == 0) { + if (!$productInfo['type']) {//平台商品 + $ids = $this->dao->getColumn(['pid' => $product_id, 'is_show' => 1, 'is_del' => 0, 'type' => 1], 'relation_id'); + } else if ($productInfo['type'] == 1) {//门店商品 + $ids = [$productInfo['relation_id']]; + } + } + break; + case 2://部分门店 + $ids = is_array($productInfo['applicable_store_id']) ? $productInfo['applicable_store_id'] : explode(',', $productInfo['applicable_store_id']); + break; + default: + break; + } + } + return [$ids, $productInfo['applicable_type']]; + } + + /** + * 收银台获取门店商品 + * @param array $where + * @param int $uid + * @param int $staff_id + * @param int $tourist_uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCashierProductListV2(array $where, int $store_id, int $uid = 0, int $staff_id = 0, int $tourist_uid = 0) + { + $where['is_del'] = 0; + $where['is_show'] = 1; + $where['is_verify'] = 1; + $where['type'] = 1; + $where['relation_id'] = $store_id; + + [$page, $limit] = $this->getPageValue(); + $where['is_vip_product'] = 0; + if ($uid) { + /** @var UserServices $user */ + $user = app()->make(UserServices::class); + $userInfo = $user->getUserCacheInfo($uid); + $is_vip = $userInfo['is_money_level'] ?? 0; + $where['is_vip_product'] = $is_vip ? -1 : 0; + } + //门店不展示卡密商品 + $where['product_type'] = [0, 2, 4]; + + $list = $this->dao->getSearchList($where, $page, $limit, ['*'], 'sort desc,sales desc,id desc', []); + $count = 0; + if ($list) { + $productIds = array_column($list, 'id'); + if ($uid || $tourist_uid) { + if ($uid) { + $tourist_uid = 0; + } + /** @var StoreCartServices $cartServices */ + $cartServices = app()->make(StoreCartServices::class); + $cartNumList = $cartServices->productIdByCartNum($productIds, $uid, $staff_id, $tourist_uid, $store_id); + $data = []; + foreach ($cartNumList as $item) { + $data[$item['product_id']][] = $item['cart_num']; + } + $newNumList = []; + foreach ($data as $key => $item) { + $newNumList[$key] = array_sum($item); + } + $cartNumList = $newNumList; + } else { + $cartNumList = []; + } + $product = ['image' => '', 'id' => 0, 'store_name' => '', 'spec_type' => 0, 'store_info' => '', 'keyword' => '', 'price' => 0, 'stock' => 0, 'sales' => 0]; + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $list = $storeProductServices->getProduceOtherList($list, $uid, true); + $list = $storeProductServices->getProductPromotions($list); + /** @var StoreCouponIssueServices $couponServices */ + $couponServices = app()->make(StoreCouponIssueServices::class); + /** @var StoreProductCategoryServices $storeCategoryService */ + $storeCategoryService = app()->make(StoreProductCategoryServices::class); + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $brands = $storeBrandServices->getColumn([], 'id,pid', 'id'); + foreach ($list as &$item) { + $product = array_merge($product, array_intersect_key($item, $product)); + $item['product'] = $product; + $item['product_id'] = $item['id']; + $item['cart_num'] = $cartNumList[$item['id']] ?? 0; + $item['branch_stock'] = $item['stock']; + + $cateId = $item['cate_id']; + $cateId = explode(',', $cateId); + $cateId = array_merge($cateId, $storeCategoryService->cateIdByPid($cateId)); + $cateId = array_diff($cateId, [0]); + $brandId = []; + if ($item['brand_id']) { + $brandId = $brands[$item['brand_id']] ?? []; + } + //平台商品 + $coupons = []; + if ($item['pid'] > 0) $coupons = $couponServices->getIssueCouponListNew($uid, ['product_id' => $item['id'], 'cate_id' => $cateId, 'brand_id' => $brandId], 'id,coupon_title,coupon_price,use_min_price', 0, 1, 'coupon_price desc,sort desc,id desc'); + $item['coupon'] = $coupons[0] ?? []; + } + } + $count = $this->dao->getCount($where); + $code = $where['store_name'] ?? ''; + $attrValue = $userInfo = null; + if ($code) { + /** @var StoreProductAttrValueServices $attrService */ + $attrService = app()->make(StoreProductAttrValueServices::class); + $attrValueArr = $attrService->getColumn(['bar_code' => $code], '*', 'product_id'); + if ($attrValueArr) { + $product_ids = array_unique(array_column($attrValueArr, 'product_id')); + $product = $this->dao->get(['id' => $product_ids, 'type' => 1, 'relation_id' => $store_id, 'is_del' => 0, 'is_show' => 1, 'is_verify' => 1], ['id', 'type', 'relation_id']); + if ($product) { + $attrValue = $attrValueArr[$product['id']] ?? []; + } + } + if (!$attrValue) { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $userInfo = $userService->get(['bar_code' => $code]); + if ($userInfo) { + $userInfo = $userInfo->toArray(); + $list = []; + $count = 0; + } + } + } + return compact('list', 'count', 'attrValue', 'userInfo'); + } + + /** + * 获取商品详情 + * @param int $storeId + * @param int $id + * @param int $uid + * @param int $touristUid + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductDetail(int $storeId, int $id, int $uid, int $touristUid) + { + /** @var StoreProductServices $productService */ + $productService = app()->make(StoreProductServices::class); + $storeInfo = $productService->getOne(['id' => $id, 'is_show' => 1, 'is_del' => 0], '*'); + if (!$storeInfo) { + throw new ValidateException('商品不存在'); + } else { + $storeInfo = $storeInfo->toArray(); + } + $siteUrl = sys_config('site_url'); + $storeInfo['image'] = set_file_url($storeInfo['image'], $siteUrl); + $storeInfo['image_base'] = set_file_url($storeInfo['image'], $siteUrl); + $storeInfo['fsales'] = $storeInfo['ficti'] + $storeInfo['sales']; + + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + $storeProductAttrServices->setItem('touristUid', $touristUid); + [$productAttr, $productValue] = $storeProductAttrServices->getProductAttrDetail($id, $uid, 1, 0, 0, $storeInfo); + $storeProductAttrServices->reset(); + + if (!$storeInfo['spec_type']) { + $productAttr = []; + $productValue = []; + } + $data['productAttr'] = $productAttr; + $data['productValue'] = $productValue; + $data['storeInfo'] = $storeInfo; + return $data; + } + + /** + * 保存或者修改门店数据 + * @param int $id + * @param int $storeId + * @param int $stock + * @param array $data + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveStoreProduct(int $id, int $storeId, int $stock, array $data = []) + { + /** @var StoreProductServices $service */ + $service = app()->make(StoreProductServices::class); + $productData = $service->get($id, ['store_name', 'image', 'sort', 'store_info', 'keyword', 'bar_code', 'cate_id', 'is_show']); + if (!$productData) { + throw new ValidateException('商品不穿在'); + } + $productData = $productData->toArray(); + $productInfo = $this->dao->get(['product_id' => $id, 'store_id' => $storeId]); + if ($productInfo) { + $productInfo->label_id = isset($data['label_id']) ? implode(',', $data['label_id']) : ''; + $productInfo->is_show = $data['is_show'] ?? 1; + $productInfo->stock = $stock; + $productInfo->image = $productData['image']; + $productInfo->sort = $productData['sort']; + $productInfo->store_name = $productData['store_name']; + $productInfo->store_info = $productData['store_info']; + $productInfo->keyword = $productData['keyword']; + $productInfo->bar_code = $productData['bar_code']; + $productInfo->cate_id = $productData['cate_id']; + $productInfo->save(); + } else { + $product = []; + $product['product_id'] = $id; + $product['label_id'] = isset($data['label_id']) ? implode(',', $data['label_id']) : ''; + $product['is_show'] = $data['is_show'] ?? 1; + $product['store_id'] = $storeId; + $product['stock'] = $stock; + $product['image'] = $productData['image']; + $product['sort'] = $productData['sort']; + $product['store_name'] = $productData['store_name']; + $product['store_info'] = $productData['store_info']; + $product['keyword'] = $productData['keyword']; + $product['bar_code'] = $productData['bar_code']; + $product['cate_id'] = $productData['cate_id']; + $product['add_time'] = time(); + $this->dao->save($product); + } + return true; + } + + /** + * 平台商品在门店是否存在 + * @param int $productId + * @param int $storeId + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function isValidStoreProduct(int $productId, int $storeId) + { + $info = $this->dao->getOne(['id' => $productId, 'type' => 1, 'relation_id' => $storeId, 'is_del' => 0, 'is_show' => 1]); + if ($info) { + return $info; + } + return $this->dao->getOne(['pid' => $productId, 'type' => 1, 'relation_id' => $storeId, 'is_del' => 0, 'is_show' => 1]); + } + + /** + * 获取商品库存 + * @param int $productId + * @param string $uniqueId + * @return int|mixed + */ + public function getProductStock(int $productId, int $storeId, string $uniqueId = '') + { + /** @var StoreProductAttrValueServices $StoreProductAttrValue */ + $StoreProductAttrValue = app()->make(StoreProductAttrValueServices::class); + return $uniqueId == '' ? + $this->dao->value(['product_id' => $productId], 'stock') ?: 0 + : $StoreProductAttrValue->uniqueByStock($uniqueId); + } + + /** + * 回退|扣除,门店、平台原商品库存 + * @param $order + * @param array $cartInfo + * @param int $platDec + * @param int $storeDec + * @return bool + */ + public function regressionBranchProductStock($order, $cartInfo = [], int $platDec = 0, int $storeDec = 0, int $store_id = 0) + { + if (!$order || !$cartInfo) return true; + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + $activity_id = (int)$order['activity_id']; + $store_id = $store_id ? $store_id : ((int)$order['store_id'] ?? 0); + $res = true; + + /** @var StoreBranchProductServices $branchServices */ + $branchServices = app()->make(StoreBranchProductServices::class); + try { + foreach ($cartInfo as $cart) { + $productInfo = $cart['productInfo'] ?? []; + if (!$productInfo) { + continue; + } + $type = $productInfo['type'] ?? 0; + //增库存减销量 + $unique = isset($cart['productInfo']['attrInfo']) ? $cart['productInfo']['attrInfo']['unique'] : ''; + $cart_num = (int)$cart['cart_num']; + $product_id = (int)$cart['product_id']; + //原商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $product_id, 'type' => 0], 'suk'); + if ($type == 1) {//门店 + $product_id = $productInfo['pid'] ?? $productInfo['id']; + } + //查出门店该商品ID,unique + if ($store_id) { + $storeProduct = $branchServices->isValidStoreProduct((int)$product_id, $store_id); + if (!$storeProduct) { + return false; + } + $product_id = $storeProduct['id']; + } + switch ($order['type']) { + case 0://普通 + case 6://预售 + $productUnique = $unique; + if ($store_id) { + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + } + break; + case 1://秒杀 + case 2://砍价 + case 3://拼团 + case 5://套餐 + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $activity_id, 'type' => $order['type']], 'suk'); + $productUnique = $skuValueServices->value(['suk' => $suk, 'product_id' => $product_id, 'type' => 0], 'unique'); + break; + default: + $productUnique = $unique; + break; + } + switch ($platDec) { + case -1://不执行 + break; + case 0://减销量、加库存 + $res = $res && $services->incProductStock($cart_num, $product_id, $productUnique); + break; + case 1://增加销量、减库存 + $res = $res && $services->decProductStock($cart_num, $product_id, $productUnique); + break; + } + switch ($storeDec) { + case -1://不执行 + break; + case 0://减销量、加库存 + $res = $res && $this->updataDecStock($cart_num, $product_id, $store_id, $productUnique, false); + break; + case 1://增加销量、减库存 + $res = $res && $this->updataDecStock($cart_num, $product_id, $store_id, $productUnique); + break; + } + } + } catch (\Throwable $e) { + throw new ValidateException('库存不足!'); + } + return $res; + } + + /** + * 加库存,减销量 + * @param $num + * @param $productId + * @param string $unique + * @return bool + */ + public function incProductStock(array $cartInfo, int $storeId) + { + $res = true; + foreach ($cartInfo as $cart) { + $unique = isset($cart['productInfo']['attrInfo']) ? $cart['productInfo']['attrInfo']['unique'] : ''; + $res = $res && $this->updataDecStock((int)$cart['cart_num'], (int)$cart['productInfo']['id'], $storeId, $unique, false); + } + return $res; + } + + /** + * 修改库存 + * @param array $cartInfo + * @param int $storeId + * @param bool $dec + * @return bool + */ + public function decProductStock(array $cartInfo, int $storeId, bool $dec = true) + { + $res = true; + foreach ($cartInfo as $cart) { + $unique = isset($cart['productInfo']['attrInfo']) ? $cart['productInfo']['attrInfo']['unique'] : ''; + $res = $res && $this->updataDecStock((int)$cart['cart_num'], (int)$cart['productInfo']['id'], $storeId, $unique, $dec); + } + return $res; + } + + + /** + * 修改库存 + * @param int $num + * @param int $productId + * @param int $storeId + * @param $unique + * @param bool $dec + * @return bool + */ + public function updataDecStock(int $num, int $productId, int $storeId, $unique, bool $dec = true) + { + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + //原商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $productId, 'type' => 0], 'suk'); + //查询门店商品 + $info = $this->isValidStoreProduct($productId, $storeId); + $res = true; + $storeProductId = $info['id'] ?? 0; + if ($productId && $storeProductId != $productId) { + $productId = $storeProductId; + //门店商品sku + $unique = $skuValueServices->value(['suk' => $suk, 'product_id' => $productId, 'type' => 0], 'unique'); + } + if ($dec) { + if ($unique) { + $res = $res && $skuValueServices->decProductAttrStock($productId, $unique, $num, 0); + } + $res = $res && $this->dao->decStockIncSales(['id' => $productId], $num); + } else { + if ($unique) { + $res = $res && $skuValueServices->incProductAttrStock($productId, $unique, $num, 0); + } + $res = $res && $this->dao->incStockDecSales(['id' => $productId], $num); + } + return $res; + } + + /** + * 上下架 + * @param int $store_id + * @param int $id + * @param int $is_show + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setShow(int $store_id, int $id, int $is_show) + { + $info = $this->dao->get($id); + if (!$info) { + throw new AdminException('操作失败!'); + } + //平台统一商品 + if ($info['pid']) { + $productInfo = $this->dao->get($info['pid']); + if ($is_show && !$productInfo['is_show']) { + throw new AdminException('平台该商品暂未上架!'); + } + } + /** @var StoreCartServices $cartService */ + $cartService = app()->make(StoreCartServices::class); + $cartService->batchUpdate([$id], ['status' => $is_show], 'product_id'); + $update = ['is_show' => $is_show]; + if ($is_show) {//手动上架 清空定时下架状态 + if ($info['is_verify'] != 1) {//验证商品是否审核通过 + throw new AdminException('该商品暂未审核通过'); + } + $update['auto_off_time'] = 0; + } + $res = $this->update($info['id'], $update); + /** @var StoreProductRelationServices $storeProductRelationServices */ + $storeProductRelationServices = app()->make(StoreProductRelationServices::class); + $storeProductRelationServices->setShow([$id], (int)$is_show); + + if (!$res) throw new AdminException('操作失败!'); + } + + /** + * 门店同步库存 + * @param $ids + * @param $storeId + * @return mixed + */ + public function synchStocks($ids, $storeId) + { + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + /** @var StoreProductAttrValueServices $attrValueServices */ + $attrValueServices = app()->make(StoreProductAttrValueServices::class); + /** @var StoreBranchProductAttrValueServices $services */ + $branchAttrValueServices = app()->make(StoreBranchProductAttrValueServices::class); + $productAllData = $productServices->getColumn([['id', 'in', $ids]], 'id,image,store_name,store_info,keyword,bar_code,cate_id,stock,label_id', 'id'); + $productBranchData = $this->dao->getColumn([['product_id', 'in', $ids], ['store_id', '=', $storeId]], 'product_id'); + $allData = $attrValueServices->getColumn([['product_id', 'in', $ids], ['type', '=', 0]], 'product_id,unique,stock,bar_code', 'unique'); + $branchData = $branchAttrValueServices->getColumn([['product_id', 'in', $ids], ['store_id', '=', $storeId]], 'unique'); + return $this->transaction(function () use ($allData, $branchData, $productAllData, $productBranchData, $storeId, $branchAttrValueServices) { + $data = []; + $res = true; + $datas = []; + foreach ($productAllData as $keys => $items) { + if (in_array($keys, $productBranchData)) { + $res = $res && $this->dao->update(['product_id' => $keys, 'store_id' => $storeId], [ + 'stock' => $items['stock'], + 'image' => $items['image'], + 'store_name' => $items['store_name'], + 'store_info' => $items['store_info'], + 'keyword' => $items['keyword'], + 'bar_code' => $items['bar_code'], + 'cate_id' => $items['cate_id'], + 'label_id' => $items['label_id'], + ]); + } else { + $datas[] = [ + 'product_id' => $items['id'], + 'image' => $items['image'], + 'store_name' => $items['store_name'], + 'store_info' => $items['store_info'], + 'keyword' => $items['keyword'], + 'bar_code' => $items['bar_code'], + 'cate_id' => $items['cate_id'], + 'label_id' => $items['label_id'], + 'store_id' => $storeId, + 'stock' => $items['stock'], + 'add_time' => time() + ]; + } + } + if ($datas) { + $res = $res && $this->dao->saveAll($datas); + } + foreach ($allData as $key => $item) { + if (in_array($key, $branchData)) { + $res = $res && $branchAttrValueServices->update(['unique' => $key, 'store_id' => $storeId], ['stock' => $item['stock']]); + } else { + $data[] = [ + 'product_id' => $item['product_id'], + 'store_id' => $storeId, + 'unique' => $item['unique'], + 'stock' => $item['stock'], + 'bar_code' => $item['bar_code'] + ]; + } + } + if ($data) { + $res = $res && $branchAttrValueServices->saveAll($data); + } + if (!$res) throw new ValidateException('同步库存失败!'); + return $res; + }); + } + + + /** + * 同步一个商品到多个门店 + * @param int $product_id + * @param array $store_ids + * @return bool + */ + public function syncProductToStores(int $product_id, int $applicable_type, array $store_ids = []) + { + if (!$product_id) { + return true; + } + $where = ['is_del' => 0]; + //不传门店ID,默认同步至所有未删除门店 + if ($store_ids) { + $where['id'] = $store_ids; + } + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $stores = $storeServices->getList($where, ['id']); + if (!$stores) { + return true; + } + $ids = array_column($stores, 'id'); + + //查询目前商品已经同步的门店 + $alreadyIds = $this->dao->getColumn(['type' => 1, 'pid' => $product_id], 'relation_id'); + switch ($applicable_type) { + case 0://仅平台 + $ids = []; + $this->dao->update(['type' => 1, 'pid' => $product_id], ['is_verify' => -1]); + break; + case 1://全部门店 + break; + case 2://部分门店 + $delIds = array_merge(array_diff($alreadyIds, $ids)); + if ($delIds) $this->dao->update(['type' => 1, 'pid' => $product_id, 'relation_id' => $delIds], ['is_verify' => -1]); + break; + } + foreach ($ids as $store_id) { + ProductSyncStoreJob::dispatchDo('syncProduct', [$product_id, $store_id]); + } + return true; + } + + /** + * 同步门店商品 + * @param int $product_id + * @param int $store_id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function syncProduct(int $product_id, int $store_id) + { + if (!$product_id || !$store_id) { + return true; + } + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + //同步正常普通商品、次卡商品 + $productInfo = $productServices->get(['type' => 0, 'product_type' => [0, 4], 'id' => $product_id]); + if (!$productInfo) { + return true; + } + $productInfo = $productInfo->toArray(); + $productInfo['pid'] = $productInfo['id']; + $productInfo['slider_image'] = json_encode($productInfo['slider_image']); + $productInfo['custom_form'] = json_encode($productInfo['custom_form']); + $productInfo['specs'] = is_array($productInfo['specs']) ? json_encode($productInfo['specs']) : $productInfo['specs']; + unset($productInfo['id'], $productInfo['sales']); + + //关联补充信息 + $relationData = []; + $relationData['cate_id'] = ($productInfo['cate_id'] ?? []) && is_string($productInfo['cate_id']) ? explode(',', $productInfo['cate_id']) : ($productInfo['cate_id'] ?? []); + $relationData['brand_id'] = ($productInfo['brand_id'] ?? []) && is_string($productInfo['brand_id']) ? explode(',', $productInfo['brand_id']) : ($productInfo['brand_id'] ?? []); + $relationData['store_label_id'] = ($productInfo['store_label_id'] ?? []) && is_string($productInfo['store_label_id']) ? explode(',', $productInfo['store_label_id']) : ($productInfo['store_label_id'] ?? []); + $relationData['label_id'] = ($productInfo['label_id'] ?? []) && is_string($productInfo['label_id']) ? explode(',', $productInfo['label_id']) : ($productInfo['label_id'] ?? []); + $relationData['ensure_id'] = ($productInfo['ensure_id'] ?? []) && is_string($productInfo['ensure_id']) ? explode(',', $productInfo['ensure_id']) : ($productInfo['ensure_id'] ?? []); + $relationData['specs_id'] = ($productInfo['specs_id'] ?? []) && is_string($productInfo['specs_id']) ? explode(',', $productInfo['specs_id']) : ($productInfo['specs_id'] ?? []); + $relationData['coupon_ids'] = ($productInfo['coupon_ids'] ?? []) && is_string($productInfo['coupon_ids']) ? explode(',', $productInfo['coupon_ids']) : ($productInfo['coupon_ids'] ?? []); + + $where = ['product_id' => $product_id, 'type' => 0]; + /** @var StoreProductAttrServices $productAttrServices */ + $productAttrServices = app()->make(StoreProductAttrServices::class); + $attrInfo = $productAttrServices->getProductAttr($where); + /** @var StoreProductAttrResultServices $productAttrResultServices */ + $productAttrResultServices = app()->make(StoreProductAttrResultServices::class); + $attrResult = $productAttrResultServices->getResult($where); + /** @var StoreProductAttrValueServices $productAttrValueServices */ + $productAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $attrValue = $productAttrValueServices->getList($where); + /** @var StoreDescriptionServices $productDescriptionServices */ + $productDescriptionServices = app()->make(StoreDescriptionServices::class); + $description = $productDescriptionServices->getDescription($where); + $description = $description ?: ''; + + $branchProductInfo = $this->dao->get(['pid' => $product_id, 'type' => 1, 'relation_id' => $store_id]); + //存在修改 + [$id, $is_new] = $productServices->transaction(function () use ( + $branchProductInfo, $productInfo, $store_id, $attrInfo, $attrResult, $attrValue, $description, + $productServices, $productAttrServices, $productAttrResultServices, $productAttrValueServices, $productDescriptionServices + ) { + $productInfo['type'] = 1; + $productInfo['is_verify'] = 1; + $productInfo['relation_id'] = $store_id; + if ($branchProductInfo) { + $id = $branchProductInfo['id']; + $productInfo['is_verify'] = 1; + unset($productInfo['stock'], $productInfo['is_show']); + $res = $this->dao->update($id, $productInfo); + if (!$res) throw new ValidateException('商品添加失败'); + + $updateSuks = array_column($attrValue, 'suk'); + $oldSuks = []; + $oldAttrValue = $productAttrValueServices->getSkuArray(['product_id' => $id, 'type' => 0], '*', 'suk'); + if ($oldAttrValue) $oldSuks = array_column($oldAttrValue, 'suk'); + $delSuks = array_merge(array_diff($oldSuks, $updateSuks)); + $dataAll = []; + $res1 = $res2 = $res3 = true; + foreach ($attrValue as $item) { + unset($item['id'], $item['stock'], $item['sales']); + $item['product_id'] = $id; + if ($oldSuks && in_array($item['suk'], $oldSuks) && isset($oldAttrValue[$item['suk']])) { + $attrId = $oldAttrValue[$item['suk']]['id']; + unset($item['suk'], $item['unique']); + $res1 = $res1 && $productAttrValueServices->update($attrId, $item); + } else { + $item['unique'] = $productAttrServices->createAttrUnique($id, $item['suk']); + $dataAll[] = $item; + } + } + if ($delSuks) { + $res2 = $productAttrValueServices->del($id, 0, $delSuks); + } + if ($dataAll) { + $res3 = $productAttrValueServices->saveAll($dataAll); + } + if (!$res1 || !$res2 || !$res3) { + throw new AdminException('商品规格信息保存失败'); + } + $is_new = 0; + } else {// 新增 保留平台库存到门店 + $res = $this->dao->save($productInfo); + if (!$res) throw new ValidateException('商品添加失败'); + $id = (int)$res->id; + if ($attrValue) { + foreach ($attrValue as &$value) { + unset($value['id'], $value['sales']); + $value['product_id'] = $id; + $value['unique'] = $productAttrServices->createAttrUnique($id, $value['suk']); + } + $productAttrValueServices->saveAll($attrValue); + } + $is_new = 1; + } + if ($attrInfo) { + foreach ($attrInfo as &$attr) { + unset($attr['id']); + $attr['product_id'] = $id; + } + $productAttrServices->setAttr($attrInfo, $id, 0); + } + if ($attrResult) $productAttrResultServices->setResult($attrResult, $id, 0); + $productDescriptionServices->saveDescription($id, $description, 0); + return [$id, $is_new]; + }); + //商品创建事件 + event('product.create', [$id, $productInfo, [], $is_new, [], $description, 1, $relationData]); + + $this->dao->cacheTag()->clear(); + $productAttrServices->cacheTag()->clear(); + return true; + } + + /** + * 删除门店、供应商同步删除商品 + * @param array $where + * @param int $type + * @param int $relation_id + * @return bool + */ + public function deleteProducts(array $where = [], int $type = 0, int $relation_id = 0) + { + $where['type'] = $type; + $where['relation_id'] = $relation_id; + $productIds = $this->dao->getColumn($where, 'id'); + if ($productIds) { + /** @var StoreProductAttrServices $productAttrServices */ + $productAttrServices = app()->make(StoreProductAttrServices::class); + /** @var StoreProductAttrResultServices $productAttrResultServices */ + $productAttrResultServices = app()->make(StoreProductAttrResultServices::class); + /** @var StoreProductAttrValueServices $productAttrValueServices */ + $productAttrValueServices = app()->make(StoreProductAttrValueServices::class); + /** @var StoreDescriptionServices $productDescriptionServices */ + $productDescriptionServices = app()->make(StoreDescriptionServices::class); + /** @var StoreProductRelationServices $productRelationServices */ + $productRelationServices = app()->make(StoreProductRelationServices::class); + /** @var StoreProductReplyServices $productReplyServices */ + $productReplyServices = app()->make(StoreProductReplyServices::class); + /** @var StoreProductReplyCommentServices $productReplyCommentServices */ + $productReplyCommentServices = app()->make(StoreProductReplyCommentServices::class); + $idsArr = array_chunk($productIds, 100); + foreach ($idsArr as $ids) { + $productAttrServices->delete(['product_id' => $ids, 'type' => 0]); + $productAttrResultServices->delete(['product_id' => $ids, 'type' => 0]); + $productAttrValueServices->delete(['product_id' => $ids, 'type' => 0]); + $productDescriptionServices->delete(['product_id' => $ids, 'type' => 0]); + $productRelationServices->delete(['product_id' => $ids]); + $this->dao->delete(['id' => $ids]); + + $replyIds = $productReplyServices->getColumn([['product_id', 'IN', $ids]], 'id'); + $replyIdsArr = array_chunk($replyIds, 100); + foreach ($replyIdsArr as $rids) { + $productReplyCommentServices->delete(['reply_id' => $rids]); + $productReplyServices->delete(['id' => $rids]); + } + + event('product.delete', [$ids]); + } + + $this->dao->cacheTag()->clear(); + $productAttrServices->cacheTag()->clear(); + } + return true; + } + +} diff --git a/app/services/product/brand/StoreBrandServices.php b/app/services/product/brand/StoreBrandServices.php new file mode 100644 index 0000000..31f2ad7 --- /dev/null +++ b/app/services/product/brand/StoreBrandServices.php @@ -0,0 +1,281 @@ + +// +---------------------------------------------------------------------- +namespace app\services\product\brand; + +use app\common\dao\store\StoreBrandDao; +use app\jobs\product\ProductCategoryBrandJob; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; + +/** + * 商品品牌 + * Class StoreBrandServices + * @package app\services\product\brand + * @mixin StoreBrandDao + */ +class StoreBrandServices extends BaseServices +{ + public function __construct(StoreBrandDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取缓存 + * @param int $id + * @return array|false|mixed|string|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/3 + */ + public function getCacheBrandInfo(int $id) + { + $storeBrandInfo = []; + if ($id) { + $storeBrandInfo = $this->dao->cacheRemember($id, function () use ($id) { + $storeBrandInfo = $this->dao->get(['id' => $id, 'is_show' => 1, 'is_del' => 0]); + if ($storeBrandInfo) { + $storeBrandInfo = $storeBrandInfo->toArray(); + } + return $storeBrandInfo ?: []; + }); + } + return $storeBrandInfo; + } + + /** + * 获取品牌列表 + * @param $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTreeList($where) + { + $list = $this->dao->getList($where, ['product']); + if (!empty($list) && $where['brand_name'] !== '') { + $fid = []; + foreach ($list as $item) { + $fid = array_merge($fid, explode(',', $item['fid'])); + } + $pids = array_unique(array_filter($fid)); + $parentList = $this->dao->getList(['id' => $pids], ['product']); + $list = array_merge($list, $parentList); + foreach ($list as $key => $item) { + $arr = $list[$key]; + unset($list[$key]); + if (!in_array($arr, $list)) { + $list[] = $arr; + } + } + } + foreach ($list as &$item) { + $item['brand_num'] = $item['product'][0]['brand_num'] ?? 0; + $item['fid'] = $item['fid'] ? array_map('intval', explode(',', $item['fid'])) : []; + $item['type'] = count($item['fid']) < 2 ? 1 : 0; + //添加子品牌fid + if ($item['type'] == 1) { + $item['fid_son'] = $item['fid']; + array_push($item['fid_son'], $item['id']); + } + unset($item['product']); + } + $list = get_tree_children($list); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取品牌列表 + * @param $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList($where) + { + $list = $this->dao->getList($where, ['product', 'children']); + $count = $this->dao->count($where); + if ($list) { + foreach ($list as &$item) { + $item['brand_num'] = $item['product'][0]['brand_num'] ?? 0; + $item['fid'] = $item['fid'] ? array_map('intval', explode(',', $item['fid'])) : []; + $item['type'] = count($item['fid']) < 2 ? 1 : 0; + //添加子品牌fid + if ($item['type'] == 1) { + $item['fid_son'] = $item['fid']; + array_push($item['fid_son'], $item['id']); + } + if (isset($item['children']) && $item['children']) { + $item['children'] = []; + $item['loading'] = false; + $item['_loading'] = false; + } else { + unset($item['children']); + } + unset($item['product']); + } + } + return compact('list', 'count'); + } + + /** + * 获取品牌cascader + * @param string $show + * @param int $type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function cascaderList($type = 1) + { + $where = []; + if ($type == 1) { + $top = true; + } else { + $top = false; + } + $menus = []; + $where['is_del'] = 0; + $where['is_show'] = 1; + $list = get_tree_children($this->dao->getList($where, [], ['id as value', 'brand_name as label', 'pid']), 'children', 'value'); + if ($top) { + $menus = [['value' => 0, 'label' => '顶级品牌']]; + foreach ($list as &$item) { + if (isset($item['children']) && $item['children']) { + foreach ($item['children'] as &$val) { + if (isset($val['children']) && $val['children']) { + unset($val['children']); + } + } + } + } + } + $menus = array_merge($menus, $list); + return $menus; + } + + /** + * 设置品牌状态 + * @param $id + * @param $is_show + */ + public function setShow(int $id, int $is_show) + { + $res = $this->dao->update($id, ['is_show' => $is_show]); +// $res = $res && $this->dao->update($id, ['is_show' => $is_show], 'pid'); + if (!$res) { + throw new AdminException('设置失败'); + } + + //设置缓存 + if (!$is_show) { + $this->cacheDelById($id); + return; + } + $branInfo = $this->dao->cacheInfoById($id); + if ($branInfo) { + $branInfo['is_show'] = 1; + } else { + $branInfo = $this->dao->get($id); + if (!$branInfo) { + return; + } + $branInfo = $branInfo->toArray(); + } + $this->dao->cacheUpdate($branInfo); + //修改关联 + ProductCategoryBrandJob::dispatchDo('setShow', [$id, 'brand_id', 'status', $is_show]); + return true; + } + + /** + * 保存新增数据 + * @param $data + */ + public function createData($data) + { + $data['pid'] = end($data['fid']); + if ($this->dao->getOne(['brand_name' => $data['brand_name'], 'pid' => $data['pid']])) { + throw new AdminException('该品牌已经存在'); + } + $data['fid'] = implode(',', $data['fid']); + $data['add_time'] = time(); + $res = $this->dao->save($data); + if (!$res) throw new AdminException('添加失败'); + //更新缓存 + if ($data['is_show']) { + $data['id'] = $res->id; + $this->cacheUpdate($data); + } + } + + /** + * 保存修改数据 + * @param $id + * @param $data + */ + public function editData($id, $data) + { + $cate = $this->dao->getOne(['id' => $id]); + if (!$cate) { + throw new AdminException('该品牌不存在'); + } + $data['pid'] = end($data['fid']) ?? 0; + if ($data['pid']) { + $pcate = $this->dao->getOne(['id' => $data['pid']]); + if (!$pcate) { + throw new AdminException('上级品牌不存在'); + } + if ($pcate['pid'] == $id) { + throw new AdminException('上级品牌不能是当前品牌的下级'); + } + } + + $data['fid'] = implode(',', $data['fid']); + $cate = $this->dao->getOne(['pid' => $data['pid'], 'brand_name' => $data['brand_name']]); + if ($cate && $cate['id'] != $id) { + throw new AdminException('该品牌已经存在'); + } + $res = $this->dao->update($id, $data); + if (!$res) throw new AdminException('修改失败'); + //更新缓存 + if ($data['is_show']) { + $data['id'] = $res->id; + $this->cacheUpdate($data); + } + } + + /** + * 删除数据 + * @param int $id + */ + public function del(int $id) + { + if ($this->dao->count(['pid' => $id])) { + throw new AdminException('请先删除子品牌!'); + } + $res = $this->dao->delete($id); + if (!$res) throw new AdminException('删除失败'); + //更新缓存 + $this->cacheDelById($id); + + //修改关联 + ProductCategoryBrandJob::dispatchDo('setShow', [$id, 'brand_id', 'is_del', 1]); + return true; + } +} diff --git a/app/services/product/category/StoreProductCategoryServices.php b/app/services/product/category/StoreProductCategoryServices.php new file mode 100644 index 0000000..821c582 --- /dev/null +++ b/app/services/product/category/StoreProductCategoryServices.php @@ -0,0 +1,380 @@ + +// +---------------------------------------------------------------------- +namespace app\services\product\category; + +use app\dao\product\category\StoreProductCategoryDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use crmeb\utils\Arr; +use think\facade\Route as Url; + + +/** + * Class StoreProductCategoryServices + * @package app\services\product\category + * @mixin StoreProductCategoryDao + */ +class StoreProductCategoryServices extends BaseServices +{ + public function __construct(StoreProductCategoryDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取分类列表 + * @param $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTreeList($where) + { + $list = $this->dao->getTierList($where); + if (!empty($list) && ($where['cate_name'] !== '' || $where['pid'] !== '')) { + $pids = Arr::getUniqueKey($list, 'pid'); + $parentList = $this->dao->getTierList(['id' => $pids]); + $list = array_merge($list, $parentList); + foreach ($list as $key => $item) { + $arr = $list[$key]; + unset($list[$key]); + if (!in_array($arr, $list)) { + $list[] = $arr; + } + } + } + $list = get_tree_children($list); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取分类列表 + * @param $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList($where) + { + $list = $this->dao->getList($where); + $count = $this->dao->count($where); + if ($list) { + foreach ($list as $key => &$item) { + if (isset($item['children']) && $item['children']) { + $item['children'] = []; + $item['loading'] = false; + $item['_loading'] = false; + } else { + unset($item['children']); + } + } + } + return compact('list', 'count'); + } + + /** + * 商品分类搜索下拉 + * @param string $show + * @param string $type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTierList($show = '', $type = 0) + { + $where = []; + if ($show !== '') $where['is_show'] = $show; + if (!$type) $where['pid'] = 0; + return sort_list_tier($this->dao->getTierList($where)); + } + + /** + * 获取分类cascader + * @param int $type + * @param int $relation_id + * @param bool $isPid + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function cascaderList(int $type = 0, int $relation_id = 0, bool $isPid = false) + { + $where = ['is_show' => 1, 'type' => $type, 'relation_id' => $relation_id]; + if ($isPid) $where['pid'] = 0; + $data = get_tree_children($this->dao->getTierList($where, ['id as value', 'cate_name as label', 'pid']), 'children', 'value'); + return $data; + } + + /** + * 设置分类状态 + * @param $id + * @param $is_show + */ + public function setShow(int $id, int $is_show) + { + $res = $this->dao->update($id, ['is_show' => $is_show]); + $res = $res && $this->dao->update($id, ['is_show' => $is_show], 'pid'); + if (!$res) { + throw new AdminException('设置失败'); + } else { + $this->cacheTag()->clear(); + $this->cacheTag()->set('category_version', uniqid()); + $this->getCategory(); + } + } + + /** + * 创建新增表单 + * @param int $type + * @return mixed + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createForm(int $type = 0) + { + return create_form('添加分类', $this->form([], $type), Url::buildUrl('/product/category'), 'POST'); + } + + /** + * 创建编辑表单 + * @param int $id + * @param int $type + * @return mixed + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function editForm(int $id, int $type = 0) + { + $info = $this->dao->get($id); + return create_form('编辑分类', $this->form($info, $type), $this->url('/product/category/' . $id), 'PUT'); + } + + /** + * 生成表单参数 + * @param array $info + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function form($info = [], int $type = 0) + { + if (isset($info['pid'])) { + if ($info['pid']) { + $f[] = Form::select('pid', '父级', (int)($info['pid'] ?? ''))->setOptions($this->menus())->filterable(1); + } else { + $f[] = Form::select('pid', '父级', (int)($info['pid'] ?? ''))->setOptions($this->menus())->filterable(1)->disabled(true); + } + } else { + $f[] = Form::select('pid', '父级', (int)($info['pid'] ?? ''))->setOptions($this->menus())->filterable(1); + } + $url = $type ? config('admin.store_prefix') . '/widget.images/index' : config('admin.admin_prefix') . '/widget.images/index'; + + $f[] = Form::input('cate_name', '分类名称', $info['cate_name'] ?? '')->maxlength(30)->required(); + $f[] = Form::frameImage('pic', '移动端图(180*180)', Url::buildUrl($url, array('fodder' => 'pic')), $info['pic'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + $f[] = Form::frameImage('big_pic', 'PC端大图(468*340)', Url::buildUrl($url, array('fodder' => 'big_pic')), $info['big_pic'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + $f[] = Form::number('sort', '排序', (int)($info['sort'] ?? 0))->min(0)->min(0); + $f[] = Form::radio('is_show', '状态', $info['is_show'] ?? 1)->options([['label' => '显示', 'value' => 1], ['label' => '隐藏', 'value' => 0]]); + return $f; + } + + /** + * 获取一级分类组合数据 + * @return array[] + */ + public function menus() + { + $list = $this->dao->getMenus(['pid' => 0]); + $menus = [['value' => 0, 'label' => '顶级菜单']]; + foreach ($list as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['cate_name']]; + } + return $menus; + } + + /** + * 保存新增数据 + * @param $data + */ + public function createData($data) + { + if ($this->dao->getOne(['pid' => $data['pid'] ?? 0, 'cate_name' => $data['cate_name']])) { + throw new AdminException('该分类已经存在'); + } + $data['add_time'] = time(); + $res = $this->dao->save($data); + if (!$res) throw new AdminException('添加失败'); + $this->cacheTag()->clear(); + $this->cacheTag()->set('category_version', uniqid()); + $this->getCategory(); + } + + /** + * 保存修改数据 + * @param $id + * @param $data + */ + public function editData($id, $data) + { + $cate = $this->dao->getOne(['id' => $id]); + if (!$cate) { + throw new AdminException('该分类不存在'); + } + if ($data['pid']) { + $cate = $this->dao->getOne(['pid' => $data['pid'], 'cate_name' => $data['cate_name']]); + if ($cate && $cate['id'] != $id) { + throw new AdminException('该分类已经存在'); + } + } + $res = $this->dao->update($id, $data); + if (!$res) throw new AdminException('修改失败'); + $this->cacheTag()->clear(); + $this->cacheTag()->set('category_version', uniqid()); + $this->getCategory(); + } + + /** + * 删除数据 + * @param int $id + */ + public function del(int $id) + { + if ($this->dao->count(['pid' => $id])) { + throw new AdminException('请先删除子分类!'); + } + $res = $this->dao->delete($id); + if (!$res) throw new AdminException('删除失败'); + $this->cacheTag()->clear(); + $this->cacheTag()->set('category_version', uniqid()); + $this->getCategory(); + } + + /** + * 获取分类版本 + * @return mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/11 + */ + public function getCategoryVersion() + { + return $this->dao->cacheTag()->remember('category_version', function () { + return uniqid(); + }); + } + + /** + * 获取指定id下的分类,一=以数组形式返回 + * @param string $cateIds + * @return array + */ + public function getCateArray(string $cateIds) + { + return $this->dao->getCateArray($cateIds); + } + + /** + * 前台分类列表 + * @return bool|mixed|null + */ + public function getCategory(array $where = []) + { + [$page, $limit] = $this->getPageValue(); + if ($limit) { + return $this->dao->cacheTag()->remember(md5(json_encode($where + ['limit' => $limit])), function () use ($where, $limit) { + return $this->dao->getALlByIndex($where, 'id,cate_name,pid,pic', $limit); + }); + } else { + return $this->dao->cacheTag()->remember('CATEGORY_All', function () { + return $this->dao->getCategory(); + }); + } + } + + /** + * 获取分类列表 + * @param $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOutList() + { + $list = $this->dao->getTierList(); + if (!empty($list)) { + $pids = Arr::getUniqueKey($list, 'pid'); + $parentList = $this->dao->getTierList(['id' => $pids]); + $list = array_merge($list, $parentList); + foreach ($list as $key => $item) { + $arr = $list[$key]; + unset($list[$key]); + if (!in_array($arr, $list)) { + $list[] = $arr; + } + } + } + $list = get_tree_children($list); + return $list; + } + + /** + * 获取一级分类 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOneCategory(array $where = []) + { + return $this->dao->getTierList($where + ['pid' => 0, 'is_show' => 1]); + } + + /** + * 根据IDS获取分类名称 + * @param array $cateList + * @param array $cate_ids + * @return array + */ + public function getCateName(array $cate_ids, array $cateList = []) + { + $cate_name = []; + if ($cate_ids) { + if (!$cateList) { + $cateName = $this->dao->getCateParentAndChildName(implode(',', array_unique($cate_ids))); + } else { + $cateName = array_filter($cateList, function ($val) use ($cate_ids) { + if (in_array($val['id'], $cate_ids)) { + return $val; + } + }); + } + foreach ($cateName as $k => $v) { + $str = ''; + if (isset($v['one']) && $v['one']) { + $str = $v['one'] . '/'; + } + if (isset($v['two']) && $v['two']) { + $str .= $v['two']; + } + if ($str) $cate_name[] = $str; + } + } + return $cate_name; + } + +} diff --git a/app/services/product/ensure/StoreProductEnsureServices.php b/app/services/product/ensure/StoreProductEnsureServices.php new file mode 100644 index 0000000..4cea5a5 --- /dev/null +++ b/app/services/product/ensure/StoreProductEnsureServices.php @@ -0,0 +1,154 @@ + +// +---------------------------------------------------------------------- +namespace app\services\product\ensure; + +use app\dao\product\ensure\StoreProductEnsureDao; +use app\services\BaseServices; +use app\services\product\product\StoreProductServices; +use crmeb\services\FormBuilder as Form; +use FormBuilder\Factory\Iview; +use think\facade\Route as Url; + +/** + * 商品保障服务 + * Class StoreProductEnsureServices + * @package app\services\product\ensure + * @mixin StoreProductEnsureDao + */ +class StoreProductEnsureServices extends BaseServices +{ + + /** + * StoreProductEnsureServices constructor. + * @param StoreProductEnsureDao $dao + */ + public function __construct(StoreProductEnsureDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取所有保障服务 + * @param int $type + * @param int $relation_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAllEnsure(int $type = 0, int $relation_id = 0) + { +// $where['relation_id'] = $relation_id; +// $where['type'] = $type; + $where['status'] = 1; + return $this->dao->getList($where, 'id,name'); + } + + /** + * 获取缓存内的数据 + * @param array $ids + * @param array|null $field + * @return array|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/14 + */ + public function getEnsurCache(array $ids, array $field = null) + { + if (app()->config->get('cache.is_data')) { + $list = $this->dao->cacheInfoByIds($ids); + } else { + $list = null; + } + + if (!$list) { + $list = $this->dao->getList(['ids' => $ids, 'status' => 1]); + foreach ($list as $item) { + $this->dao->cacheUpdate($item); + } + } + + if ($field && $list) { + $newList = []; + foreach ($list as $item) { + $data = []; + foreach ($field as $k) { + $data[$k] = $item[$k] ?? null; + } + $newList[] = $data; + } + $list = $newList; + } + + return $list; + } + + /** + * 获取保障服务列表(带标签) + * @param array $where + * @return array + */ + public function getEnsureList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit); + if ($list) { + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + foreach ($list as &$item) { + $item['product_count'] = $storeProductServices->getUseEnsureCount((int)$item['id']); + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + + /** + * 创建新增表单 + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createForm() + { + return create_form('添加保障服务', $this->form(), Url::buildUrl('/product/ensure'), 'POST'); + } + + /** + * 创建编辑表单 + * @param $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function editForm(int $id) + { + $info = $this->dao->get($id); + return create_form('编辑保障服务', $this->form($info), $this->url('/product/ensure/' . $id), 'PUT'); + } + + /** + * 生成表单参数 + * @param array $info + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function form($info = []) + { + $f[] = Form::input('name', '保障服务条款', $info['name'] ?? '')->maxlength(100)->required(); + $f[] = Form::textarea('desc', '内容描述', $info['desc'] ?? '')->required(); + $f[] = Form::frameImage('image', '图标(建议尺寸:100px*100px)', Url::buildUrl(config('admin.admin_prefix') . '/widget.images/index', array('fodder' => 'image')), $info['image'] ?? '')->icon('ios-add')->width('960px')->appendValidate(Iview::validateStr()->message('请选择图标(建议尺寸:100px*100px)')->required())->height('505px')->modal(['footer-hide' => true]); + $f[] = Form::number('sort', '排序', (int)($info['sort'] ?? 0))->min(0)->min(0); + return $f; + } +} diff --git a/app/services/product/label/StoreProductLabelAuxiliaryServices.php b/app/services/product/label/StoreProductLabelAuxiliaryServices.php new file mode 100644 index 0000000..c61fc56 --- /dev/null +++ b/app/services/product/label/StoreProductLabelAuxiliaryServices.php @@ -0,0 +1,57 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\label; + + +use app\dao\product\label\StoreProductLabelAuxiliaryDao; +use app\services\BaseServices; + +/** + * 标签辅助表 + * Class StoreProductLabelAuxiliaryServices + * @package app\services\product\label + * @mixin StoreProductLabelAuxiliaryDao + */ +class StoreProductLabelAuxiliaryServices extends BaseServices +{ + + /** + * StoreProductLabelAuxiliaryServices constructor. + * @param StoreProductLabelAuxiliaryDao $dao + */ + public function __construct(StoreProductLabelAuxiliaryDao $dao) + { + $this->dao = $dao; + } + + /** + * 关联标签 + * @param int $productId + * @param array $labelIds + * @return bool + */ + public function saveLabelRelation(int $productId, array $labelIds) + { + $data = []; + foreach ($labelIds as $labelId) { + $data[] = [ + 'product_id' => $productId, + 'label_id' => $labelId + ]; + } + $this->dao->delete(['product_id' => $productId]); + if ($labelIds) { + $this->dao->saveAll($data); + } + return true; + } +} diff --git a/app/services/product/label/StoreProductLabelServices.php b/app/services/product/label/StoreProductLabelServices.php new file mode 100644 index 0000000..98ef1f1 --- /dev/null +++ b/app/services/product/label/StoreProductLabelServices.php @@ -0,0 +1,156 @@ + +// +---------------------------------------------------------------------- +namespace app\services\product\label; + + +use app\dao\product\label\StoreProductLabelDao; +use app\services\BaseServices; +use crmeb\services\FormBuilder; +use think\facade\Route as Url; + +/** + * 商品标签 + * Class StoreProductLabelServices + * @package app\services\product\label + * @mixin StoreProductLabelDao + */ +class StoreProductLabelServices extends BaseServices +{ + /** + * StoreProductLabelServices constructor. + * @param StoreProductLabelDao $dao + */ + public function __construct(StoreProductLabelDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取所有商品标签 + * @param int $type + * @param int $relation_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAllLabel(int $type = 0, int $relation_id = 0) + { + $where['relation_id'] = $relation_id; + $where['type'] = $type; + return $this->dao->getList($where, 'id,label_name'); + } + + /** + * 获取商品标签 树形结构 + * @param int $type + * @param int $relation_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductLabelTreeList(int $type = 0, int $relation_id = 0) + { + /** @var StoreProductLabelCateServices $productLabelCateServices */ + $productLabelCateServices = app()->make(StoreProductLabelCateServices::class); + $cate = $productLabelCateServices->getAllProductLabelCate($type, $relation_id); + $data = []; + $label = []; + $where = []; + if ($cate) { + foreach ($cate as $value) { + $data[] = [ + 'id' => $value['id'] ?? 0, + 'value' => $value['id'] ?? 0, + 'label_cate' => 0, + 'label_name' => $value['name'] ?? '', + 'label' => $value['name'] ?? '', + 'relation_id' => $value['relation_id'] ?? 0, + 'type' => $value['type'] ?? 0, + ]; + } + $label = $this->dao->getList($where); + if ($label) { + foreach ($label as &$item) { + $item['label'] = $item['label_name']; + $item['value'] = $item['id']; + } + } + } + return $this->get_tree_children($data, $label); + } + + /** + * @param array $ids + * @return array|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/1 + */ + public function getLabelCache(array $ids, array $field = null) + { + if (app()->config->get('cache.is_data')) { + $list = $this->dao->cacheInfoByIds($ids); + } else { + $list = null; + } + + if (!$list) { + $list = $this->dao->getList(['ids' => $ids]); + foreach ($list as $item) { + $this->dao->cacheUpdate($item); + } + } + + if ($field && $list) { + $newList = []; + foreach ($list as $item) { + $data = []; + foreach ($field as $k) { + $data[$k] = $item[$k] ?? null; + } + $newList[] = $data; + } + $list = $newList; + } + + return $list; + } + + /** + * 获取商品标签表单 + * @param int $type + * @param int $relation_id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLabelForm(int $type = 0, int $relation_id = 0) + { + /** @var StoreProductLabelCateServices $service */ + $service = app()->make(StoreProductLabelCateServices::class); + $options = $service->getAllProductLabelCate($type, $relation_id); + $data = []; + foreach ($options as $option) { + $data[] = ['label' => $option['name'], 'value' => $option['id']]; + } + $rule = [ + FormBuilder::select('label_cate', '标签分组')->options($data), + FormBuilder::input('label_name', '标签名称')->maxlength(20), + ]; + return create_form('添加商品标签', $rule, Url::buildUrl('/product/label/0'), 'POST'); + } +} diff --git a/app/services/product/product/StoreDescriptionServices.php b/app/services/product/product/StoreDescriptionServices.php new file mode 100644 index 0000000..476c8d4 --- /dev/null +++ b/app/services/product/product/StoreDescriptionServices.php @@ -0,0 +1,68 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\product; + + +use app\dao\product\product\StoreDescriptionDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; + +/** + * Class StoreDescriptionService + * @package app\services\product\product + * @mixin StoreDescriptionDao + */ +class StoreDescriptionServices extends BaseServices +{ + /** + * StoreDescriptionServices constructor. + * @param StoreDescriptionDao $dao + */ + public function __construct(StoreDescriptionDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取商品详情 + * @param array $where + * @return string + */ + public function getDescription(array $where) + { + $info = $this->dao->getDescription($where); + if ($info) return htmlspecialchars_decode($info->description); + return ''; + } + + /** + * 保存商品详情 + * @param int $id + * @param string $description + * @param int $type + * @return bool + */ + public function saveDescription(int $id, string $description, int $type = 0) + { + $description = htmlspecialchars($description); + $data = ['product_id' => $id, 'type' => $type]; + $info = $this->dao->count($data); + if ($info) { + $res = $this->dao->update($data, ['description' => $description]); + } else { + $data['description'] = $description; + $res = $this->dao->save($data); + } + if (!$res) throw new AdminException('商品详情保存失败!'); + } + +} diff --git a/app/services/product/product/StoreProductCategoryBrandServices.php b/app/services/product/product/StoreProductCategoryBrandServices.php new file mode 100644 index 0000000..8c48e71 --- /dev/null +++ b/app/services/product/product/StoreProductCategoryBrandServices.php @@ -0,0 +1,94 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\product; + + +use app\dao\product\product\StoreProductCategoryBrandDao; +use app\dao\product\product\StoreProductRelationDao; +use app\services\BaseServices; +use app\services\product\brand\StoreBrandServices; + +/** + * 商品关联关系 + * Class StoreProductCategoryBrandServices + * @package app\services\product\product + * @mixin StoreProductRelationDao + */ +class StoreProductCategoryBrandServices extends BaseServices +{ + + + /** + * @param StoreProductCategoryBrandDao $dao + */ + public function __construct(StoreProductCategoryBrandDao $dao) + { + $this->dao = $dao; + } + + /** + * 保存商品关联关系 + * @param int $id + * @param array $cate_id + * @param array $brand_id + * @param int $status + * @return bool + */ + public function saveRelation(int $id, array $cate_id, array $brand_id, int $status = 1) + { + $cateData = []; + if ($cate_id && $brand_id) { + $time = time(); + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $brands = $storeBrandServices->getColumn([['id', 'in', $brand_id]], 'id,brand_name', 'id'); + foreach ($cate_id as $cid) { + foreach ($brand_id as $bid) { + if ($brands[$bid] ?? []) + $cateData[] = ['product_id' => $id, 'cate_id' => $cid, 'brand_id' => $bid, 'brand_name' => $brands[$bid]['brand_name'] ?? '', 'status' => $status, 'add_time' => $time]; + } + } + } + $this->change($id, $cateData); + return true; + } + + /** + * 商品添加商品关联 + * @param int $id + * @param array $cateData + * @param int $type + * @return bool + */ + public function change(int $id, array $cateData) + { + $this->dao->delete(['product_id' => $id]); + if ($cateData) $this->dao->saveAll($cateData); + return true; + } + + /** + * 批量设置关联状态 + * @param array $ids + * @param int $is_show + * @param int $type + * @return bool + */ + public function setShow(array $ids, int $is_show = 1, int $type = 1) + { + $this->dao->setShow($ids, $is_show, $type); + return true; + } + + + +} diff --git a/app/services/product/product/StoreProductLogServices.php b/app/services/product/product/StoreProductLogServices.php new file mode 100644 index 0000000..36fc787 --- /dev/null +++ b/app/services/product/product/StoreProductLogServices.php @@ -0,0 +1,180 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\product; + + +use app\dao\product\product\StoreProductLogDao; +use app\services\order\StoreOrderCartInfoServices; +use app\services\BaseServices; +use app\services\product\branch\StoreBranchProductServices; +use think\exception\ValidateException; + +/** + * 商品访问记录日志 + * Class StoreProductLogServices + * @package app\services\product\product + * @mixin StoreProductLogDao + */ +class StoreProductLogServices extends BaseServices +{ + /** + * StoreProductLogServices constructor. + * @param StoreProductLogDao $dao + */ + public function __construct(StoreProductLogDao $dao) + { + $this->dao = $dao; + } + + /** + * 创建各种访问日志 + * @param string $type + * @param array $data + * @return bool + */ + public function createLog(string $type, array $data) + { + if (!in_array($type, ['order', 'pay', 'refund']) && (!isset($data['product_id']) || !$data['product_id'])) { + throw new ValidateException('缺少商品ID'); + } + $log_data = $log_data_all = []; + $log_data['type'] = $type; + $log_data['uid'] = $data['uid'] ?? 0; + $log_data['add_time'] = time(); + switch ($type) { + case 'visit'://访问 + case 'cart'://加入购物车 + case 'collect'://收藏 + $key = $type . '_num'; + $log_data[$key] = isset($data[$key]) && $data[$key] ? $data[$key] : 1; + $log_data['product_id'] = 0; + if (isset($data['product_id']) && $data['product_id']){ + if (is_array($data['product_id'])) { + foreach ($data['product_id'] as $key => $product_id) { + $log_data['product_id'] = $product_id; + $log_data_all[] = $log_data; + } + } else { + $log_data['product_id'] = $data['product_id']; + $log_data_all[] = $log_data; + } + } + break; + case 'order'://下单 + if (!isset($data['order_id']) || !$data['order_id']) { + throw new ValidateException('缺少订单ID'); + } + /** @var StoreOrderCartInfoServices $cartInfoServices */ + $cartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfo = $cartInfoServices->getOrderCartInfo((int)$data['order_id']); + foreach ($cartInfo as $value) { + $product = $value['cart_info']; + $log_data['product_id'] = $product['product_id'] ?? 0; + $log_data['order_num'] = $product['cart_num'] ?? 1; + $log_data_all[] = $log_data; + } + break; + case 'pay'://支付 + if (!isset($data['order_id']) || !$data['order_id']) { + throw new ValidateException('缺少订单ID'); + } + /** @var StoreOrderCartInfoServices $cartInfoServices */ + $cartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfo = $cartInfoServices->getOrderCartInfo((int)$data['order_id']); + foreach ($cartInfo as $value) { + $product = $value['cart_info']; + $log_data['product_id'] = $product['product_id'] ?? 0; + $log_data['pay_num'] = $product['cart_num'] ?? 0; + $log_data['cost_price'] = $product['costPrice'] ?? 0; + $log_data['pay_price'] = $product['truePrice'] ?? 0; + $log_data['pay_uid'] = $data['uid'] ?? 0; + $log_data_all[] = $log_data; + } + break; + case 'refund'://退款 + if (!isset($data['order_id']) || !$data['order_id']) { + throw new ValidateException('缺少订单ID'); + } + /** @var StoreOrderCartInfoServices $cartInfoServices */ + $cartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cartInfo = $cartInfoServices->getOrderCartInfo((int)$data['order_id']); + foreach ($cartInfo as $value) { + $product = $value['cart_info']; + $log_data['product_id'] = $product['product_id'] ?? 0; + $log_data['order_num'] = $product['cart_num'] ?? 1; + $log_data_all[] = $log_data; + } + break; + default: + throw new ValidateException('暂不支持该类型记录'); + break; + } + $res = true; + if ($log_data_all) { + /** @var StoreBranchProductServices $productServices */ + $productServices = app()->make(StoreBranchProductServices::class); + foreach ($log_data_all as &$item) {//平台共享到门店商品 记录平台商品ID + $item['product_id'] = $productServices->getStoreProductId((int)$item['product_id']); + } + $res = $this->dao->saveAll($log_data_all); + } + if (!$res) { + throw new ValidateException('添加商品记录失败'); + } + return true; + } + + /** + * 查找购买商品排行 + * @param $where + * @return mixed + */ + public function getRanking(array $where) + { + $list = $this->dao->getRanking($where); + foreach ($list as $key => &$item) { + if (!$item['store_name'] || !$item['image']) { + unset($list[$key]); + } + if ($item['profit'] == null) $item['profit'] = 0; + if ($item['changes'] == null) $item['changes'] = 0; + if ($item['repeats'] == null) { + $item['repeats'] = 0; + } else { + $item['repeats'] = bcdiv($this->dao->getRepeats($where, $item['product_id']), $item['repeats'], 2); + } + } + return array_merge($list); + } + + /** + * 浏览商品列表 + * @param array $where + * @param string $group + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where, string $group = '', string $field = '*') + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $field, $page, $limit, $group); + if ($group) { + $count = $this->dao->getDistinctCount($where, $group, true); + } else { + $count = $this->dao->count($where); + } + return compact('list', 'count'); + } +} diff --git a/app/services/product/product/StoreProductRelationServices.php b/app/services/product/product/StoreProductRelationServices.php new file mode 100644 index 0000000..9a6abb6 --- /dev/null +++ b/app/services/product/product/StoreProductRelationServices.php @@ -0,0 +1,194 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\product; + + +use app\dao\product\product\StoreProductRelationDao; +use app\services\BaseServices; +use app\services\product\category\StoreProductCategoryServices; + +/** + * 商品关联关系 + * Class StoreProductRelationServices + * @package app\services\product\product + * @mixin StoreProductRelationDao + */ +class StoreProductRelationServices extends BaseServices +{ + + /** + * 1:分类2:品牌3:商品标签4:用户标签5:保障服务6:商品参数 + * @var string[] + */ + protected $type = [ + 1 => '商品分类', + 2 => '商品品牌', + 3 => '商品标签', + 4 => '用户标签', + 5 => '保障服务', + 6 => '商品参数' + ]; + + /** + * 缓存key + * @var string[] + */ + protected $typeKey = [ + 1 => 'cate', + 2 => 'brand', + 3 => 'product_label', + 4 => 'user_label', + 5 => 'ensure', + 6 => 'specs' + ]; + + /** + * @param StoreProductRelationDao $dao + */ + public function __construct(StoreProductRelationDao $dao) + { + $this->dao = $dao; + } + + /** + * 保存商品关联关系 + * @param int $id + * @param array $relation_id + * @param int $type + * @param int $is_show + * @return bool + */ + public function saveRelation(int $id, array $relation_id, int $type = 1, int $is_show = 1) + { + $cateData = []; + if ($relation_id) { + $time = time(); + if ($type == 1) {//分类 + /** @var StoreProductCategoryServices $storeCategoryServices */ + $storeCategoryServices = app()->make(StoreProductCategoryServices::class); + $cateGory = $storeCategoryServices->getColumn([['id', 'IN', $relation_id]], 'id,pid', 'id'); + foreach ($relation_id as $cid) { + if ($cid && isset($cateGory[$cid]['pid'])) { + $cateData[] = ['type' => $type, 'product_id' => $id, 'relation_id' => $cid, 'relation_pid' => $cateGory[$cid]['pid'], 'status' => $is_show, 'add_time' => $time]; + } + } + } else { + foreach ($relation_id as $cid) { + $cateData[] = ['type' => $type, 'product_id' => $id, 'relation_id' => $cid, 'status' => 1, 'add_time' => $time]; + } + } + } + $this->change($id, $cateData, $type); + return true; + } + + /** + * 商品添加商品关联 + * @param int $id + * @param array $cateData + * @param int $type + * @return bool + */ + public function change(int $id, array $cateData, int $type = 1) + { + $this->dao->delete(['product_id' => $id, 'type' => $type]); + if ($cateData) $this->dao->saveAll($cateData); + $this->setProductRelationCache($id, $cateData, $type); + return true; + } + + /** + * 批量设置关联状态 + * @param array $ids + * @param int $is_show + * @param int $type + * @return bool + */ + public function setShow(array $ids, int $is_show = 1, int $type = 1) + { + $this->dao->setShow($ids, $is_show, $type); + return true; + } + + /** + * 设置商品关联缓存 + * @param int $product_id + * @param array $data + * @param int $type + * @return array + */ + public function setProductRelationCache(int $product_id, array $data, int $type = 1) + { + $key ='cache_product_relation_' . ($this->typeKey[$type] ?? '') . '_' . $product_id; + if ($type == 1) { + $cacheData = $data; + } else { + $cacheData = array_column($data, 'relation_id'); + } + $this->dao->cacheHander()->delete($key); + $this->dao->cacheTag()->set($key, $cacheData); + return $cacheData; + } + + /** + * 更新缓存 + * @param int $product_id + * @param int $type + * @param string $key + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateProductRelationCache(int $product_id, int $type = 1) + { + $data = $this->dao->getList(['product_id' => $product_id, 'type' => $type]); + $cacheData = []; + if ($data) { + $cacheData = $this->setProductRelationCache($product_id, $data, $type); + } + return $cacheData; + } + + /** + * 获取商品关联缓存 + * @param int $product_id + * @param array $type + * @param bool $isCache + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductRelationCache(int $product_id, array $type = [], bool $isCache = false) + { + if (!$type) { + $typeArr = [1, 2, 3, 4, 5, 6]; + } else { + $typeArr = $type; + } + $data = []; + $typeKey = $this->typeKey; + $key = 'cache_product_relation_'; + foreach ($typeArr as $value) { + $key .= ($typeKey[$value] ?? '') . '_' . $product_id; + $relation = $this->dao->cacheHander()->get($key); + if (!$relation || $isCache) { + $relation = $this->updateProductRelationCache($product_id, (int)$value, $key); + } + $data[$value] = $relation; + } + return $data; + } + + +} diff --git a/app/services/product/product/StoreProductReplyCommentServices.php b/app/services/product/product/StoreProductReplyCommentServices.php new file mode 100644 index 0000000..1f87221 --- /dev/null +++ b/app/services/product/product/StoreProductReplyCommentServices.php @@ -0,0 +1,188 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\product; + + +use app\dao\product\product\StoreProductReplyCommentDao; +use app\services\BaseServices; +use app\services\user\UserRelationServices; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; + +/** + * Class StoreProductReplyCommentServices + * @package app\services\product\product + * @mixin StoreProductReplyCommentDao + */ +class StoreProductReplyCommentServices extends BaseServices +{ + use ServicesTrait; + + /** + * StoreProductReplyCommentServices constructor. + * @param StoreProductReplyCommentDao $dao + */ + public function __construct(StoreProductReplyCommentDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取评论回复列表 + * @param int $replyId + * @param string $time + * @param int $uid + * @param bool $notUid + * @return array + */ + public function getReplCommenList(int $replyId, string $time = '', int $uid = 0, bool $notUid = true) + { + [$page, $limit] = $this->getPageValue(); + $where = ['reply_id' => $replyId, 'pid' => 0, 'notUid' => 0, 'create_time' => $time]; + if (false === $notUid) { + unset($where['notUid']); + } + $list = $this->dao->getDataList($where, ['*'], $page, $limit, ['praise' => 'desc', 'create_time' => 'desc'], [ + 'user' => function ($query) { + $query->field('uid,avatar,nickname,is_money_level'); + }, + 'children' => function ($query) use ($uid) { + $query->with([ + 'user' => function ($query) { + $query->field('uid,avatar,nickname,is_money_level'); + }, + 'productRelation' => function ($query) use ($uid) { + $query->where('uid', $uid)->where('type', UserRelationServices::TYPE_LIKE) + ->where('category', UserRelationServices::CATEGORY_COMMENT) + ->field(['uid', 'relation_id']); + } + ]); + }, + 'productRelation' => function ($query) use ($uid) { + $query->where('uid', $uid)->where('type', UserRelationServices::TYPE_LIKE) + ->where('category', UserRelationServices::CATEGORY_COMMENT) + ->field(['uid', 'relation_id']); + } + ]); + $count = $this->dao->count($where); + $siteLogoSquare = sys_config('site_logo_square'); + $siteName = sys_config('site_name'); + foreach ($list as &$item) { + if (!isset($item['user']) && $item['uid'] === 0) { + $item['user'] = ['nickname' => $siteName, 'avatar' => $siteLogoSquare]; + } + if (isset($item['children']) && !isset($item['children']['user']) && $item['children']['uid'] === 0) { + $item['children']['user'] = ['nickname' => $siteName, 'avatar' => $siteLogoSquare]; + } + if (isset($item['user']['nickname'])) { + $item['user']['nickname'] = anonymity($item['user']['nickname']); + } + if ($uid) { + $item['is_praise'] = !empty($item['productRelation']); + if (isset($item['children'])) { + $item['children']['is_praise'] = !empty($item['children']['productRelation']); + } + } else { + $item['is_praise'] = false; + if (isset($item['children'])) { + $item['children']['is_praise'] = false; + } + } + } + return compact('list', 'count'); + } + + /** + * 保存回复 + * @param int $uid + * @param int $replyId + * @param string $content + * @return \crmeb\basic\BaseModel|\think\Model + */ + public function saveComment(int $uid, int $replyId, string $content) + { + return $this->dao->save(['uid' => $uid, 'reply_id' => $replyId, 'content' => $content, 'create_time' => time()]); + } + + /** + * 点赞回复 + * @param int $id + * @param int $uid + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function commentPraise(int $id, int $uid) + { + $commentInfo = $this->dao->get($id, ['id', 'praise']); + if (!$commentInfo) { + throw new ValidateException('回复不存在'); + } + $commentInfo->praise++; + /** @var UserRelationServices $service */ + $service = app()->make(UserRelationServices::class); + $res = $service->getUserCount($uid, $id, UserRelationServices::TYPE_LIKE, UserRelationServices::CATEGORY_COMMENT); + if ($res) { + return true; + } + $this->transaction(function () use ($id, $uid, $service, $commentInfo) { + $res = $service->save([ + 'uid' => $uid, + 'relation_id' => $id, + 'type' => UserRelationServices::TYPE_LIKE, + 'category' => UserRelationServices::CATEGORY_COMMENT, + 'add_time' => time() + ]); + $res = $res && $commentInfo->save(); + if (!$res) { + throw new ValidateException('点赞失败'); + } + }); + event('product.reply.update', [$uid]); + return true; + } + + /** + * 取消回复点赞 + * @param int $id + * @param int $uid + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function unCommentPraise(int $id, int $uid) + { + $commentInfo = $this->dao->get($id, ['id', 'praise']); + if (!$commentInfo) { + throw new ValidateException('回复不存在'); + } + $commentInfo->praise--; + /** @var UserRelationServices $service */ + $service = app()->make(UserRelationServices::class); + $this->transaction(function () use ($id, $uid, $service, $commentInfo) { + $res = $service->delete([ + 'uid' => $uid, + 'relation_id' => $id, + 'type' => UserRelationServices::TYPE_LIKE, + 'category' => UserRelationServices::CATEGORY_COMMENT + ]); + $res = $res && $commentInfo->save(); + if (!$res) { + throw new ValidateException('点赞失败'); + } + }); + event('product.reply.update', [$uid]); + return true; + } +} diff --git a/app/services/product/product/StoreProductReplyServices.php b/app/services/product/product/StoreProductReplyServices.php new file mode 100644 index 0000000..1415e0d --- /dev/null +++ b/app/services/product/product/StoreProductReplyServices.php @@ -0,0 +1,585 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\product; + + +use app\dao\product\product\StoreProductReplyDao; +use app\services\BaseServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\store\SystemStoreServices; +use app\services\supplier\SystemSupplierServices; +use app\services\user\UserRelationServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use crmeb\traits\ServicesTrait; +use think\db\exception\DataNotFoundException; +use think\db\exception\DbException; +use think\db\exception\ModelNotFoundException; +use think\exception\ValidateException; +use think\facade\Route as Url; + +/** + * Class StoreProductReplyService + * @package app\services\product\product + * @mixin StoreProductReplyDao + */ +class StoreProductReplyServices extends BaseServices +{ + use ServicesTrait; + + /** + * StoreProductReplyServices constructor. + * @param StoreProductReplyDao $dao + */ + public function __construct(StoreProductReplyDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取评论列表 + * @param array $where + * @return array + */ + public function sysPage(array $where) + { + //平台共享到门店商品处理 + if (isset($where['product_id']) && $where['product_id']) { + $productId = $this->checkReplyProductId($where['product_id']); + $where['product_id'] = $productId; + } + /** @var StoreProductReplyStoreProductServices $storeProductReplyStoreProductServices */ + $storeProductReplyStoreProductServices = app()->make(StoreProductReplyStoreProductServices::class); + $data = $storeProductReplyStoreProductServices->getProductReplyList($where, [ + 'replyComment' => function ($query) use ($where) { + if ($where['type'] ?? 0) { + $query->where('uid', 0)->where('pid', 0)->where('type', $where['type'] ?? 0)->where('relation_id', $where['relation_id'] ?? 0); + } else { + $query->where('uid', 0)->where('pid', 0); + } + } + ]); + foreach ($data['list'] as &$item) { + $item['time'] = time_tran(strtotime($item['add_time'])); + $item['create_time'] = $item['add_time']; + $item['score'] = ($item['product_score'] + $item['service_score']) / 2; + } + return $data; + } + + /** + * 创建自评表单 + * @param int $product_id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createForm(int $product_id) + { + if ($product_id == 0) { + $field[] = Form::frameImage('image', '商品', Url::buildUrl(config('admin.admin_prefix') . '/store.StoreProduct/index', array('fodder' => 'image')))->icon('ios-add')->width('960px')->height('560px')->modal(['footer-hide' => true])->Props(['srcKey' => 'image']); + } else { + $field[] = Form::hidden('product_id', $product_id); + } + $field[] = Form::frameImage('avatar', '用户头像', Url::buildUrl(config('admin.admin_prefix') . '/widget.images/index', array('fodder' => 'avatar')))->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + $field[] = Form::input('nickname', '用户名称')->col(24); + $field[] = Form::input('comment', '评价文字')->type('textarea'); + $field[] = Form::rate('product_score', '商品分数', 0)->allowHalf(false); + $field[] = Form::rate('service_score', '服务分数', 0)->allowHalf(false); + $field[] = Form::frameImages('pics', '评价图片', Url::buildUrl(config('admin.admin_prefix') . '/widget.images/index', array('fodder' => 'pics', 'type' => 'many', 'maxLength' => 8)))->maxLength(8)->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true])->modal(['footer-hide' => true])->props(['closeBtn' => false, 'okBtn' => false]); + $field[] = Form::dateTime('add_time', '评论时间', '')->placeholder('请选择评论时间(不选择默认当前添加时间)'); + return create_form('添加自评', $field, Url::buildUrl('/product/reply/save_fictitious_reply'), 'POST'); + } + + /** + * 添加自评 + * @param array $data + * @param int $type + * @param int $relation_id + * @return void + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function saveReply(array $data, int $type = 0, int $relation_id = 0) + { + $time = time(); + $data['uid'] = 0; + $data['oid'] = 0; + $data['type'] = $type; + $data['relation_id'] = $relation_id; + $data['unique'] = uniqid(); + $data['reply_type'] = 'product'; + $data['add_time'] = empty($data['add_time']) ? $time : strtotime($data['add_time']); + $data['pics'] = json_encode($data['pics']); + if (isset($data['sku_unique']) && $data['sku_unique']) { + /** @var StoreProductAttrValueServices $productAttrValueServices */ + $productAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $attrDetail = $productAttrValueServices->getone(['product_id' => $data['product_id'], 'unique' => $data['sku_unique']]); + $data['sku'] = $attrDetail ? $attrDetail['suk'] : ''; + } + unset($data['image']); + if ($data['add_time'] > $time) { + throw new AdminException('评论时间应小于当前时间'); + } + $res = $this->dao->save($data); + if (!$res) throw new AdminException('添加自评失败'); + $this->dao->cacheTag()->clear(); + } + + /** + * 回复评论 + * @param int $replyId + * @param string $content + * @param int $type + * @param int $relation_id + * @return void + */ + public function setReply(int $replyId, string $content, int $type = 0, int $relation_id = 0) + { + if ($content == '') throw new AdminException('请输入回复内容'); + $reply = $this->dao->get($replyId); + if (!$reply) { + throw new AdminException('评论不存在或已删除'); + } + $save['content'] = $content; + $save['create_time'] = time(); + $save['type'] = $type; + $save['relation_id'] = $relation_id; + $save['reply_id'] = $replyId; + /** @var StoreProductReplyCommentServices $service */ + $service = app()->make(StoreProductReplyCommentServices::class); + $where = ['reply_id' => $replyId, 'uid' => 0, 'type' => $type, 'relation_id' => $relation_id, 'pid' => 0]; + if ($service->count($where)) { + $res = $service->update($where, ['content' => $content, 'update_time' => time()]); + } else { + $res = $service->save($save); + } + if (!$res) throw new AdminException('回复失败,请稍后再试'); + if ($type == $reply['type']) {//回复端 与商品端一致 修改回复状态 + $this->dao->update($replyId, ['is_reply' => 1]); + } + $this->dao->cacheTag()->clear(); + } + + /** + * 删除 + * @param int $id + */ + public function del(int $id) + { + $res = $this->dao->update($id, ['is_del' => 1]); + if (!$res) throw new AdminException('删除失败'); + $this->dao->cacheTag()->clear(); + } + + /** + * @param int $productId + * @param int $limit + * @return mixed + * @throws \Throwable + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/3 + */ + public function getRecProductReplyCache(int $productId, int $limit = 1) + { + return $this->dao->cacheTag()->remember('id:' . $productId . ':limit:' . $limit, function () use ($productId, $limit) { + return $this->getRecProductReply($productId, $limit); + }, 3600); + } + + /** + * 获取最近的几条评论 + * @param int $productId + * @param int $limit + * @return array + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function getRecProductReply(int $productId, int $limit = 1) + { + $productId = $this->checkReplyProductId($productId); + $page = $limit ? 1 : 0; + $list = $this->dao->getProductReply($productId, '*', $page, $limit); + if ($list) { + foreach ($list as &$item) { + $item['suk'] = $item['sku']; + $item['nickname'] = anonymity($item['nickname']); + $item['merchant_reply_time'] = date('Y-m-d H:i', $item['merchant_reply_time']); + $item['add_time'] = time_tran($item['add_time']); + $item['star'] = bcadd($item['product_score'], $item['service_score'], 2); + $item['star'] = bcdiv($item['star'], '2', 0); + $item['comment'] = $item['comment'] ?: '此用户没有填写评价'; + $item['pics'] = $item['pics'] ? (is_string($item['pics']) ? json_decode($item['pics'], true) : $item['pics']) : []; + } + } + return $list; + } + + /** + * 获取好评率 + * @param int $id + * @return int|string + */ + public function getProductReplyChance(int $id) + { + $id = $this->checkReplyProductId($id); + + $replyCount = $this->dao->replyCount($id); + if ($replyCount) { + $goodReply = $this->dao->replyCount($id, 1); + if ($goodReply) { + $replyCount = bcdiv((string)$goodReply, (string)$replyCount, 2); + $replyCount = bcmul((string)$replyCount, '100', 0); + } else { + $replyCount = 0; + } + } else { + $replyCount = 100; + } + return $replyCount; + } + + /** + * 获取评论数据 评论总数 好评总数 好评率 + * @param int $id + * @return array + */ + public function getProductReplyData(int $id) + { + $goodReply = 0; + $id = $this->checkReplyProductId($id); + return $this->dao->cacheTag()->remember('ReplyData_' . $id, function () use ($id) { + $goodReply = $replyChance = 0; + $replyCount = $this->dao->replyCount($id); + if ($replyCount) { + $goodReply = $this->dao->replyCount($id, 1); + if ($goodReply) { + $replyChance = bcmul((string)bcdiv((string)$goodReply, (string)$replyCount, 2), '100', 0); + } else { + $replyChance = 0; + } + } else { + $replyChance = 100; + } + return [$replyCount, $goodReply, $replyChance]; + }, 3600); + } + + /** + * 商品评论数量 + * @param int $type + * @param int $relation_id + * @return int + */ + public function replyCount(int $type = 0, int $relation_id = 0) + { + return $this->dao->count(['is_reply' => 0, 'is_del' => 0, 'type' => $type, 'relation_id' => $relation_id]); + } + + /** + * 获取商品评论数量 + * @param int $id + * @return mixed + */ + public function productReplyCount(int $id) + { + $id = $this->checkReplyProductId($id); + + $data['sum_count'] = $this->dao->replyCount($id); + $data['good_count'] = $this->dao->replyCount($id, 1); + $data['in_count'] = $this->dao->replyCount($id, 2); + $data['poor_count'] = $this->dao->replyCount($id, 3); + if ($data['sum_count'] != 0) { + $data['reply_chance'] = bcdiv($data['good_count'], $data['sum_count'], 2); + $data['reply_star'] = bcdiv(($this->dao->sum(['product_id' => $id, 'is_del' => 0], 'product_score') + $this->dao->sum(['product_id' => $id, 'is_del' => 0], 'service_score')), $data['sum_count'] * 2, 0); + } else { + $data['reply_chance'] = 100; + $data['reply_star'] = 5; + } +// $data['reply_star'] = bcmul($data['reply_chance'], 5, 0); + $data['reply_chance'] = $data['sum_count'] == 0 ? 100 : bcmul($data['reply_chance'], 100, 0); + return $data; + } + + /** + * 获取商品评论列表 + * @param int $id + * @param int $type + * @return array + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function getProductReplyList(int $id, int $type) + { + $id = $this->checkReplyProductId($id); + if (!$id) { + return []; + } + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->replyList($id, $type, $page, $limit); + foreach ($list as &$item) { + $item['suk'] = $item['sku']; + $item['nickname'] = anonymity($item['nickname']); + $item['merchant_reply_time'] = date('Y-m-d H:i', $item['merchant_reply_time']); + $item['add_time'] = date('Y-m-d H:i', $item['add_time']); + $item['star'] = bcadd($item['product_score'], $item['service_score'], 2); + $item['star'] = bcdiv($item['star'], 2, 0); + $item['comment'] = $item['comment'] ?: '此用户没有填写评价'; + $item['pics'] = is_string($item['pics']) ? json_decode($item['pics'], true) : $item['pics']; + } + return $list; + } + + /** + * 评价点赞 + * @param int $id + * @param int $uid + * @return bool + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function replyPraise(int $id, int $uid) + { + $relyInfo = $this->dao->get($id, ['id', 'praise']); + if (!$relyInfo) { + throw new ValidateException('点赞的评价不存在'); + } + /** @var UserRelationServices $service */ + $service = app()->make(UserRelationServices::class); + if ($service->getUserCount($uid, $id, UserRelationServices::TYPE_COLLECT, UserRelationServices::CATEGORY_REPLY)) { + return true; + } + $relyInfo->praise++; + $this->transaction(function () use ($uid, $relyInfo, $service, $id) { + $res = $service->save([ + 'uid' => $uid, + 'relation_id' => $id, + 'type' => UserRelationServices::TYPE_LIKE, + 'category' => UserRelationServices::CATEGORY_REPLY, + 'add_time' => time() + ]); + $res = $res && $relyInfo->save(); + if (!$res) { + throw new ValidateException('点赞失败'); + } + }); + event('product.reply.update', [$uid]); + $this->dao->cacheTag()->clear(); + return true; + } + + /** + * 取消点赞 + * @param int $id + * @param int $uid + * @return bool + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function unReplyPraise(int $id, int $uid) + { + $relyInfo = $this->dao->get($id, ['id', 'praise']); + if (!$relyInfo) { + throw new ValidateException('点赞的评价不存在'); + } + /** @var UserRelationServices $service */ + $service = app()->make(UserRelationServices::class); + $relyInfo->praise--; + $this->transaction(function () use ($uid, $relyInfo, $service, $id) { + $res = $service->delete([ + 'uid' => $uid, + 'relation_id' => $id, + 'type' => UserRelationServices::TYPE_LIKE, + 'category' => UserRelationServices::CATEGORY_REPLY + ]); + $res = $res && $relyInfo->save(); + if (!$res) { + throw new ValidateException('取消点赞失败'); + } + }); + event('product.reply.update', [$uid]); + $this->dao->cacheTag()->clear(); + return true; + } + + /** + * 获取评论详情 + * @param int $id + * @param int $uid + * @return mixed + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function getReplyInfo(int $id, int $uid) + { + $replyInfo = $this->dao->get($id, ['*']); + if (!$replyInfo) { + throw new ValidateException('查看的评论不存在'); + } + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $productInfo = $productServices->get($replyInfo->product_id, ['image', 'store_name', 'id']); + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $userInfo = $userService->get($replyInfo->uid, ['nickname', 'uid', 'avatar', 'is_money_level']); + $userInfo = $userInfo ? $userInfo->toArray() : []; + $userInfo['nickname'] = anonymity($userInfo['nickname'] ?? ''); + $replyInfo->nickname = anonymity($replyInfo['nickname'] ?? ''); + $data['reply'] = $replyInfo->toArray(); + $data['reply']['add_time'] = $data['reply']['add_time'] ? date('Y-m-d H:i:s', $data['reply']['add_time']) : ''; + $data['reply']['suk'] = $replyInfo['sku'] ?? ''; + /** @var StoreProductReplyCommentServices $commentService */ + $commentService = app()->make(StoreProductReplyCommentServices::class); + $data['reply']['comment_sum'] = $commentService->count(['reply_id' => $id, 'pid' => 0]); + $data['product'] = $productInfo ? $productInfo->toArray() : []; + $data['user'] = $userInfo; + $data['star'] = bcdiv(bcadd($data['reply']['product_score'], $data['reply']['service_score'], 2), 2, 0); + /** @var UserRelationServices $make */ + $make = app()->make(UserRelationServices::class); + $data['is_praise'] = !!$make->getUserCount($uid, $id, UserRelationServices::TYPE_LIKE, UserRelationServices::CATEGORY_REPLY); + //记录浏览量 + $replyInfo->views_num++; + $replyInfo->save(); + return $data; + } + + /** + * 新版本获取商品评价 + * @param int $id + * @param int $type + * @return array + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function getNewProductReplyList(int $id, int $type, int $uid) + { + $id = $this->checkReplyProductId($id); + if (!$id) { + return []; + } + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->replyList($id, $type, $page, $limit, ['userInfo', + 'replyComment' => function ($query) { + $query->with([ + 'user' => function ($query) { + $query->field('uid,avatar,nickname'); + } + ])->where('pid', 0)->field(['uid', 'reply_id', 'content']) + ->order('praise desc,create_time desc'); + }, + 'productRelation' => function ($query) use ($uid) { + $query->where('uid', $uid)->where('type', UserRelationServices::TYPE_LIKE)->where('category', UserRelationServices::CATEGORY_REPLY)->field(['uid', 'relation_id']); + } + ]); + if ($list) { + $siteLogoSquare = sys_config('site_logo_square'); + $siteName = sys_config('site_name'); + $supplierIds = $storeIds = []; + foreach ($list as $value) { + switch ($value['type']) { + case 0: + break; + case 1://门店 + $storeIds[] = $value['relation_id']; + break; + case 2://供应商 + $supplierIds[] = $value['relation_id']; + break; + } + } + $supplierIds = array_unique($supplierIds); + $storeIds = array_unique($storeIds); + $supplierList = $storeList = []; + if ($supplierIds) { + /** @var SystemSupplierServices $supplierServices */ + $supplierServices = app()->make(SystemSupplierServices::class); + $supplierList = $supplierServices->getColumn([['id', 'in', $supplierIds], ['is_del', '=', 0]], 'id,supplier_name,avatar', 'id'); + } + if ($storeIds) { + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeList = $storeServices->getColumn([['id', 'in', $storeIds], ['is_del', '=', 0]], 'id,name,image', 'id'); + } + $replyId = array_column($list, 'id'); + /** @var StoreProductReplyCommentServices $make */ + $make = app()->make(StoreProductReplyCommentServices::class); + $replySum = $make->getReplyCommentCountList($replyId); + foreach ($list as &$item) { + $item['suk'] = $item['sku']; + $item['nickname'] = anonymity($item['nickname']); + $item['merchant_reply_time'] = date('Y-m-d H:i', $item['merchant_reply_time']); + $item['add_time'] = date('Y-m-d H:i', $item['add_time']); + $item['star'] = bcadd($item['product_score'], $item['service_score'], 2); + $item['star'] = bcdiv($item['star'], 2, 0); + $item['comment'] = $item['comment'] ?: '此用户没有填写评价'; + $item['pics'] = $item['pics'] ? (is_string($item['pics']) ? json_decode($item['pics'], true) : $item['pics']) : []; + + if (isset($item['replyComment']['user']['nickname'])) { + $item['replyComment']['user']['nickname'] = anonymity($item['replyComment']['user']['nickname']); + } else if (isset($item['replyComment']) && !$item['replyComment']['user'] && $item['replyComment']['uid'] === 0) { + $user = []; + switch ($item['replyComment']['type'] ?? 0) { + case 0: + $user = ['nickname' => $siteName, 'avatar' => $siteLogoSquare]; + break; + case 1://门店 + $user = ['nickname' => $storeList[$item['relation_id']]['name'] ?? '', 'avatar' => $storeList[$item['relation_id']]['image'] ?? '']; + + break; + case 2://供应商 + $user = ['nickname' => $supplierList[$item['relation_id']]['supplier_name'] ?? '', 'avatar' => $supplierList[$item['relation_id']]['avatar'] ?? '']; + break; + default: + $user = ['nickname' => $siteName, 'avatar' => $siteLogoSquare]; + break; + } + $item['replyComment']['user'] = $user; + } + if ($uid) { + $item['is_praise'] = !empty($item['productRelation']); + } else { + $item['is_praise'] = false; + } + if (isset($item['replyComment'])) { + foreach ($replySum as $value) { + if ($item['id'] === $value['reply_id']) { + $item['replyComment']['sum'] = $value['sum']; + } + } + } + } + } + + return $list; + } + + /** + * 检测获取评论商品ID(门店商品,处理为平台商品ID) + * @param int $id + * @return int + */ + public function checkReplyProductId(int $id) + { + /** @var StoreBranchProductServices $productServices */ + $productServices = app()->make(StoreBranchProductServices::class); + return $productServices->getStoreProductId($id); + } +} diff --git a/app/services/product/product/StoreProductServices.php b/app/services/product/product/StoreProductServices.php new file mode 100644 index 0000000..6cf8820 --- /dev/null +++ b/app/services/product/product/StoreProductServices.php @@ -0,0 +1,2648 @@ + +// +---------------------------------------------------------------------- +namespace app\services\product\product; + +use app\dao\product\product\StoreProductDao; +use app\jobs\product\ProductStockTips; +use app\Request; +use app\services\activity\discounts\StoreDiscountsProductsServices; +use app\services\activity\bargain\StoreBargainServices; +use app\services\activity\combination\StoreCombinationServices; +use app\services\activity\promotions\StorePromotionsServices; +use app\services\activity\seckill\StoreSeckillServices; +use app\services\activity\seckill\StoreSeckillTimeServices; +use app\services\BaseServices; +use app\services\activity\coupon\StoreCouponIssueServices; +use app\services\diy\DiyServices; +use app\services\order\StoreCartServices; +use app\services\product\category\StoreProductCategoryServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\brand\StoreBrandServices; +use app\services\other\queue\QueueServices; +use app\services\product\ensure\StoreProductEnsureServices; +use app\services\product\label\StoreProductLabelServices; +use app\services\product\sku\StoreProductAttrResultServices; +use app\services\product\sku\StoreProductAttrServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\product\sku\StoreProductRuleServices; +use app\services\product\shipping\ShippingTemplatesServices; +use app\services\product\sku\StoreProductVirtualServices; +use app\services\product\specs\StoreProductSpecsServices; +use app\services\store\SystemStoreServices; +use app\services\supplier\SystemSupplierServices; +use app\services\system\form\SystemFormServices; +use app\services\user\label\UserLabelServices; +use app\services\user\level\SystemUserLevelServices; +use app\services\user\member\MemberCardServices; +use app\services\activity\collage\UserCollagePartakeServices; +use app\services\user\UserRelationServices; +use app\services\user\UserSearchServices; +use app\services\user\UserServices; +use app\jobs\product\ProductLogJob; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use crmeb\services\SystemConfigService; +use crmeb\traits\ServicesTrait; +use crmeb\traits\OptionTrait; +use think\exception\ValidateException; +use think\facade\Config; +use think\facade\Route as Url; + +/** + * Class StoreProductService + * @package app\services\product\product + * @mixin StoreProductDao + */ +class StoreProductServices extends BaseServices +{ + use OptionTrait, ServicesTrait; + + /** + * StoreProductServices constructor. + * @param StoreProductDao $dao + */ + public function __construct(StoreProductDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取顶部标签 + * @param int $store_id + * @param array $where + * @return array[] + */ + public function getHeader(int $store_id = 0, array $where = []) + { + if ($store_id || (isset($where['store_id']) && $where['store_id'])) { + $where['type'] = 1; + $where['relation_id'] = $store_id ?: $where['store_id']; + unset($where['store_id']); + } elseif (isset($where['supplier_id']) && $where['supplier_id']) { + $where['type'] = 2; + $where['relation_id'] = $where['supplier_id']; + unset($where['supplier_id']); + } else { + $where['pid'] = 0; + } + //出售中的商品 + $onsale = $this->dao->getCount(['status' => 1] + $where); + //已经售馨商品 + $outofstock = $this->dao->getCount(['status' => 4] + $where); + //警戒库存商品 + $store_stock = sys_config('store_stock', 0); + $policeforce = $this->dao->getCount(['status' => 5, 'store_stock' => $store_stock > 0 ? $store_stock : 2] + $where); + //仓库中的商品 + $forsale = $this->dao->getCount(['status' => 2] + $where); + //回收站商品 + $delete = $this->dao->getCount(['status' => 6] + $where); + //待审核商品 + $unVerify = $this->dao->getCount(['status' => 0] + $where); + //审核未通过商品 + $refuseVerify = $this->dao->getCount(['status' => -1] + $where); + //强制下架商品 + $removeVerify = $this->dao->getCount(['status' => -2] + $where); + + return [ + ['type' => 1, 'name' => '销售中', 'count' => $onsale], + ['type' => 2, 'name' => '仓库中', 'count' => $forsale], + ['type' => 4, 'name' => '已售罄', 'count' => $outofstock], + ['type' => 5, 'name' => '库存预警', 'count' => $policeforce], + ['type' => 6, 'name' => '回收站', 'count' => $delete], + ['type' => 0, 'name' => '待审核', 'count' => $unVerify], + ['type' => -1, 'name' => '审核未通过', 'count' => $refuseVerify], + ['type' => -2, 'name' => '强制下架', 'count' => $removeVerify] + ]; + } + + /** + * 获取列表 + * @param $where + * @return array + */ + public function getList(array $where) + { + $store_stock = sys_config('store_stock', 0); + $where['store_stock'] = $store_stock > 0 ? $store_stock : 2; + [$page, $limit] = $this->getPageValue(); + $order_string = ''; + $order_arr = ['asc', 'desc']; + if (isset($where['sales']) && in_array($where['sales'], $order_arr)) { + $order_string = 'sales ' . $where['sales']; + } + //门店不展示卡密商品 + $count = $this->dao->getCount($where); + //页面搜索,第二页之后没有结果,强制返回第一页数据 + if ($count <= $limit && $page !== 1) { + $page = 1; + } + $list = $this->dao->getList($where, $page, $limit, $order_string); + if ($list) { + $cateIds = implode(',', array_column($list, 'cate_id')); + /** @var StoreProductCategoryServices $categoryService */ + $categoryService = app()->make(StoreProductCategoryServices::class); + $cateList = $categoryService->getCateParentAndChildName($cateIds); + $supplierIds = $storeIds = []; + foreach ($list as $value) { + switch ($value['type']) { + case 0: + break; + case 1://门店 + $storeIds[] = $value['relation_id']; + break; + case 2://供应商 + $supplierIds[] = $value['relation_id']; + break; + } + } + $supplierIds = array_unique($supplierIds); + $storeIds = array_unique($storeIds); + $supplierList = $storeList = []; + if ($supplierIds) { + /** @var SystemSupplierServices $supplierServices */ + $supplierServices = app()->make(SystemSupplierServices::class); + $supplierList = $supplierServices->getColumn([['id', 'in', $supplierIds], ['is_del', '=', 0]], 'id,supplier_name', 'id'); + } + if ($storeIds) { + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeList = $storeServices->getColumn([['id', 'in', $storeIds], ['is_del', '=', 0]], 'id,name', 'id'); + } + + foreach ($list as &$item) { + $item['branch_sales'] = $item['sales'] ?? 0; + $item['branch_stock'] = $item['stock'] ?? 0; + $item['is_show'] = $item['branch_is_show'] ?? $item['is_show']; + $item['cate_name'] = ''; + if (isset($item['cate_id']) && $item['cate_id']) { + $cate_name = $categoryService->getCateName(explode(',', $item['cate_id']), $cateList); + if ($cate_name) { + $item['cate_name'] = is_array($cate_name) ? implode(',', $cate_name) : ''; + } + } + + $item['stock_attr'] = $item['stock'] > 0;//库存 + $item['plate_name'] = '平台'; + switch ($item['type']) { + case 0: + $item['plate_name'] = '平台'; + break; + case 1://门店 + $item['plate_name'] = '门店:' . ($storeList[$item['relation_id']]['name'] ?? ''); + break; + case 2://供应商 + $item['plate_name'] = '供应商:' . ($supplierList[$item['relation_id']]['supplier_name'] ?? ''); + break; + } + } + } + + return compact('list', 'count'); + } + + /** + * 设置商品上下架 + * @param $ids + * @param $is_show + */ + public function setShow(array $ids, int $is_show) + { + if ($is_show == 0) { + //下架检测是否有参与活动商品 + $this->checkActivity($ids); + } else { + $count = $this->dao->getCount(['ids' => $ids, 'is_del' => 1]); + if ($count) throw new AdminException('回收站商品无法直接上架,请先恢复商品'); + } + /** @var StoreCartServices $cartService */ + $cartService = app()->make(StoreCartServices::class); + $cartService->batchUpdate($ids, ['status' => $is_show], 'product_id'); + $update = ['is_show' => $is_show]; + if ($is_show) {//手动上架 清空定时下架状态 + $update['auto_off_time'] = 0; + } + $this->dao->batchUpdate($ids, $update); + /** @var StoreProductRelationServices $storeProductRelationServices */ + $storeProductRelationServices = app()->make(StoreProductRelationServices::class); + $storeProductRelationServices->setShow($ids, (int)$is_show); + //门店商品处理 + /** @var StoreBranchProductServices $storeBranchProductServices */ + $storeBranchProductServices = app()->make(StoreBranchProductServices::class); + $storeBranchProductServices->update(['pid' => $ids, 'type' => 1], ['is_show' => $is_show ? 1 : 0]); + + event('product.status', [$ids, $is_show]); + + $this->dao->cacheTag()->clear(); + + return true; + } + + /** + * 队列批量上下架 + * @param array $ids + * @param int $is_show + * @param $redisKey + * @param $queueId + */ + public function setBatchShow(array $ids, int $is_show, $redisKey, $queueId) + { + $res = $this->dao->batchUpdate($ids, ['is_show' => $is_show]); + /** @var StoreProductRelationServices $storeProductRelationServices */ + $storeProductRelationServices = app()->make(StoreProductRelationServices::class); + /** @var QueueServices $queueService */ + $queueService = app()->make(QueueServices::class); + $re = true; + if ($is_show == 0) { + try { + //下架检测是否有参与活动商品 + $re = $this->checkActivity($ids); + //改变购物车中状态 + $storeProductRelationServices->setShow($ids, (int)$is_show); + /** @var StoreCartServices $cartService */ + $cartService = app()->make(StoreCartServices::class); + $cartService->changeStatus($ids, 1); + } catch (\Throwable $e) { + $re = false; + } + } + if ($re) $storeProductRelationServices->setShow($ids, (int)$is_show); + $queueService->doSuccessSremRedis($ids, $redisKey, $queueId['type']); + if (!$res) { + $queueService->addQueueFail($queueId['id'], $redisKey); + } + return true; + } + + /** + * 商品审核表单 + * @param int $id + * @param int $is_verify + * @return mixed + */ + public function verifyForm(int $id, int $is_verify = 1) + { + $f = []; + if ($is_verify == 1) { + $f[] = Form::radio('is_verify', '审核状态', 1)->options([['value' => 1, 'label' => '通过'], ['value' => -1, 'label' => '拒绝']])->appendControl(-1, [ + Form::textarea('refusal', '拒绝原因')->required('请输入拒绝原因')]); + } else { + $f[] = Form::hidden('is_verify', '-2'); + $f[] = Form::textarea('refusal', '下架原因')->required('请输入下架原因'); + } + return create_form($is_verify == 1 ? '商品审核' : '强制下架', $f, Url::buildUrl('/product/product/set_verify/' . $id), 'post'); + } + + /** + * 商品审核 + * @param int $id + * @param int $is_verify + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function verify(int $id, array $data) + { + $info = $this->dao->get($id); + if (!$info) { + throw new ValidateException('商品不存在'); + } + $is_verify = $data['is_verify'] ?? 1; + /** @var StoreCartServices $cartService */ + $cartService = app()->make(StoreCartServices::class); + if (in_array($is_verify, [-1, -2])) { + if ($is_verify == -1) { + $data['is_show'] = 0; + } + $cartService->update(['product_id' => $id, 'status' => 1], ['status' => 0]); + } elseif ($is_verify == 1) { + if ($info['is_show']) $cartService->update(['product_id' => $id, 'status' => 0], ['status' => 1]); + } + $this->dao->update($id, $data); + return true; + } + + /** + * 获取规格模板 + * @param int $type + * @param int $relation_id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRule(int $type = 0, int $relation_id = 0) + { + /** @var StoreProductRuleServices $storeProductRuleServices */ + $storeProductRuleServices = app()->make(StoreProductRuleServices::class); + $list = $storeProductRuleServices->getList(['type' => $type, 'relation_id' => $relation_id])['list'] ?? []; + foreach ($list as &$item) { + $item['rule_value'] = json_decode($item['rule_value'], true); + } + return $list; + } + + /** + * 获取商品详情 + * @param int $id + * @return array|\think\Model|null + */ + public function getInfo(int $id) + { + /** @var StoreProductCategoryServices $storeCatecoryService */ + $storeCatecoryService = app()->make(StoreProductCategoryServices::class); + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + /** @var StoreProductAttrResultServices $storeProductAttrResultServices */ + $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class); + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + /** @var StoreCouponIssueServices $storeCouponIssueServices */ + $storeCouponIssueServices = app()->make(StoreCouponIssueServices::class); + /** @var UserLabelServices $userLabelServices */ + $userLabelServices = app()->make(UserLabelServices::class); + $productInfo = $this->dao->getInfo($id); + if ($productInfo) $productInfo = $productInfo->toArray(); + else throw new ValidateException('商品不存在'); + $data['tempList'] = $this->getTemp((int)$productInfo['type'], (int)$productInfo['relation_id']); + $data['cateList'] = $storeCatecoryService->cascaderList(); + $couponIds = $productInfo['coupons'] ? array_column($productInfo['coupons'], 'issue_coupon_id') : []; + $is_sub = $recommend = []; + if ($productInfo['is_sub'] == 1) array_push($is_sub, 1); + if ($productInfo['is_vip'] == 1) array_push($is_sub, 0); + $productInfo['is_sub'] = $is_sub; + $productInfo['recommend'] = $recommend; + $productInfo['price'] = floatval($productInfo['price']); + $productInfo['postage'] = floatval($productInfo['postage']); + $productInfo['ot_price'] = floatval($productInfo['ot_price']); + $productInfo['vip_price'] = floatval($productInfo['vip_price']); + $productInfo['is_limit'] = boolval($productInfo['is_limit'] ?? 0); + $productInfo['cost'] = floatval($productInfo['cost']); + $productInfo['brand_id'] = $productInfo['brand_com'] ? array_map('intval', explode(',', $productInfo['brand_com'])) : []; + $productInfo['video_open'] = (bool)$productInfo['video_open']; + if ($productInfo['video_link'] && strpos($productInfo['video_link'], 'http') === false) { + $productInfo['video_link'] = sys_config('site_url') . $productInfo['video_link']; + } + $productInfo['coupons'] = $storeCouponIssueServices->productCouponList([['id', 'in', $couponIds]], 'title,id'); + $productInfo['cate_id'] = is_array($productInfo['cate_id']) ? $productInfo['cate_id'] : explode(',', $productInfo['cate_id']); + if ($productInfo['label_id']) { + $label_id = is_array($productInfo['label_id']) ? $productInfo['label_id'] : explode(',', $productInfo['label_id']); + $productInfo['label_id'] = $userLabelServices->getLabelList(['ids' => $label_id], ['id', 'label_name']); + } else { + $productInfo['label_id'] = []; + } + if ($productInfo['store_label_id']) { + /** @var StoreProductLabelServices $storeProductLabelServices */ + $storeProductLabelServices = app()->make(StoreProductLabelServices::class); + $productInfo['store_label_id'] = $storeProductLabelServices->getColumn([['id', 'in', $productInfo['store_label_id']]], 'id,label_name'); + } else { + $productInfo['store_label_id'] = []; + } + $productInfo['give_integral'] = floatval($productInfo['give_integral']); + $productInfo['presale_time'] = $productInfo['presale_start_time'] == 0 ? [] : [date('Y-m-d H:i:s', $productInfo['presale_start_time']), date('Y-m-d H:i:s', $productInfo['presale_end_time'])]; + $productInfo['auto_on_time'] = $productInfo['is_show'] ? '' : ($productInfo['auto_on_time'] ? date('Y-m-d H:i:s', $productInfo['auto_on_time']) : ''); + $productInfo['auto_off_time'] = !$productInfo['is_show'] ? '' : ($productInfo['auto_off_time'] ? date('Y-m-d H:i:s', $productInfo['auto_off_time']) : ''); + $productInfo['description'] = $storeDescriptionServices->getDescription(['product_id' => $id, 'type' => 0]); + //系统表单 + $productInfo['custom_form'] = $productInfo['custom_form_info'] = []; + if ($productInfo['system_form_id']) { + /** @var SystemFormServices $systemFormServices */ + $systemFormServices = app()->make(SystemFormServices::class); + $systemForm = $systemFormServices->value(['id' => $productInfo['system_form_id']], 'value'); + if ($systemForm) { + $productInfo['custom_form'] = is_string($systemForm) ? json_decode($systemForm, true) : $systemForm; + } + $productInfo['custom_form_info'] = $systemFormServices->handleForm($productInfo['custom_form']); + } + //无属性添加默认属性 + if (!$storeProductAttrResultServices->getResult(['product_id' => $id, 'type' => 0])) { + $attr = [ + [ + 'value' => '规格', + 'detailValue' => '', + 'attrHidden' => '', + 'detail' => ['默认'] + ] + ]; + $detail[0] = [ + 'value1' => '默认', + 'detail' => ['规格' => '默认'], + 'pic' => $productInfo['image'], + 'price' => $productInfo['price'], + 'settle_price' => $productInfo['settle_price'] ?? 0, + 'cost' => $productInfo['cost'], + 'ot_price' => $productInfo['ot_price'], + 'stock' => $productInfo['stock'], + 'bar_code' => '', + 'weight' => 0, + 'volume' => 0, + 'brokerage' => 0, + 'brokerage_two' => 0, + 'code' => 0, + ]; + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + $skuList = $storeProductAttrServices->validateProductAttr($attr, $detail, $id); + $storeProductAttrServices->saveProductAttr($skuList, $id, 0); + $this->dao->update($id, ['spec_type' => 0]); + } + if ($productInfo['spec_type'] == 1) { + $result = $storeProductAttrResultServices->getResult(['product_id' => $id, 'type' => 0]); + foreach ($result['value'] as $k => $v) { + $num = 1; + foreach ($v['detail'] as $dv) { + $result['value'][$k]['value' . $num] = $dv; + $num++; + } + } + $productInfo['items'] = $result['attr']; + $productInfo['attrs'] = $result['value']; + $productInfo['attr'] = ['pic' => '', 'vip_price' => 0, 'price' => 0, 'settle_price' => 0, 'cost' => 0, 'ot_price' => 0, 'stock' => 0, 'bar_code' => '', 'weight' => 0, 'volume' => 0, 'brokerage' => 0, 'brokerage_two' => 0, 'code' => '']; + } else { + /** @var StoreProductVirtualServices $virtualService */ + $virtualService = app()->make(StoreProductVirtualServices::class); + $result = $storeProductAttrValueServices->getOne(['product_id' => $id, 'type' => 0]); + $productInfo['items'] = []; + $productInfo['attrs'] = []; + $productInfo['attr'] = [ + 'pic' => $result['image'] ?? '', + 'vip_price' => isset($result['vip_price']) ? floatval($result['vip_price']) : 0, + 'price' => isset($result['price']) ? floatval($result['price']) : 0, + 'settle_price' => isset($result['settle_price']) ? floatval($result['settle_price']) : 0, + 'cost' => isset($result['cost']) ? floatval($result['cost']) : 0, + 'ot_price' => isset($result['ot_price']) ? floatval($result['ot_price']) : 0, + 'stock' => isset($result['stock']) ? floatval($result['stock']) : 0, + 'bar_code' => isset($result['bar_code']) ? $result['bar_code'] : '', + 'code' => isset($result['code']) ? $result['code'] : '', + 'virtual_list' => $virtualService->getArr(isset($result['unique']), $id), + 'weight' => isset($result['weight']) ? floatval($result['weight']) : 0, + 'volume' => isset($result['volume']) ? floatval($result['volume']) : 0, + 'brokerage' => isset($result['brokerage']) ? floatval($result['brokerage']) : 0, + 'brokerage_two' => isset($result['brokerage_two']) ? floatval($result['brokerage_two']) : 0, + 'disk_info' => $result['disk_info'] ?? [], + 'write_times' => intval($result['write_times'] ?? 1),//核销次数 + 'write_valid' => intval($result['write_valid'] ?? 1),//核销时效类型 + 'days' => intval($result['write_days'] ?? $result['days'] ?? 0),//购买后:N天有效 + 'section_time' => [($result['write_start'] ?? '') ? date('Y-m-d', $result['write_start'] ?? '') : '', ($result['write_end'] ?? '') ? date('Y-m-d', $result['write_end'] ?? '') : ''],//[核销开始时间,核销结束时间] + ]; + } + if ($productInfo['activity']) { + $activity = explode(',', $productInfo['activity']); + foreach ($activity as $k => $v) { + if ($v == 1) { + $activity[$k] = '秒杀'; + } elseif ($v == 2) { + $activity[$k] = '砍价'; + } elseif ($v == 3) { + $activity[$k] = '拼团'; + } elseif ($v == 0) { + $activity[$k] = '默认'; + } + } + $productInfo['activity'] = $activity; + } else { + $productInfo['activity'] = ['默认', '秒杀', '砍价', '拼团']; + } + //推荐产品 + $recommend_list = []; + if ($productInfo['recommend_list'] != '') { + $productInfo['recommend_list'] = explode(',', $productInfo['recommend_list']); + if (count($productInfo['recommend_list'])) { + $images = $this->getColumn([['id', 'in', $productInfo['recommend_list']]], 'image', 'id'); + foreach ($productInfo['recommend_list'] as $item) { + $recommend_list[] = [ + 'product_id' => $item, + 'image' => $images[$item] + ]; + } + } + } + $productInfo['recommend_list'] = $recommend_list; + //适用门店 + $productInfo['stores'] = []; + if (isset($productInfo['applicable_type']) && ($productInfo['applicable_type'] == 1 || ($productInfo['applicable_type'] == 2 && isset($productInfo['applicable_store_id']) && $productInfo['applicable_store_id']))) {//查询门店信息 + $where = ['is_del' => 0]; + if ($productInfo['applicable_type'] == 2) { + $store_ids = is_array($productInfo['applicable_store_id']) ? $productInfo['applicable_store_id'] : explode(',', $productInfo['applicable_store_id']); + $where['id'] = $store_ids; + } + $field = ['id', 'cate_id', 'name', 'phone', 'address', 'detailed_address', 'image', 'is_show', 'day_time', 'day_start', 'day_end']; + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeData = $storeServices->getStoreList($where, $field, '', '', 0, ['categoryName']); + $productInfo['stores'] = $storeData['list'] ?? []; + } + $data['productInfo'] = $productInfo; + return $data; + } + + /** + * 获取运费模板列表 + * @param int $type + * @param int $relation_id + * @return array + */ + public function getTemp(int $type = 0, int $relation_id = 0) + { + /** @var ShippingTemplatesServices $shippingTemplatesServices */ + $shippingTemplatesServices = app()->make(ShippingTemplatesServices::class); + return $shippingTemplatesServices->getSelectList(['type' => $type, 'relation_id' => $relation_id]); + } + + /** + * 获取商品规格 + * @param array $data + * @param int $id + * @param int $type + * @param int $plat_type + * @param int $relation_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAttr(array $data, int $id, int $type, int $plat_type = 0, int $relation_id = 0) + { + + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $productInfo = []; + if ($id) { + $productInfo = $this->dao->get($id); + if (!$productInfo) { + throw new ValidateException('商品不存在'); + } + } + /** @var StoreProductVirtualServices $virtualService */ + $virtualService = app()->make(StoreProductVirtualServices::class); + $attr = $data['attrs']; + $product_type = $productInfo['product_type'] ?? $data['product_type'] ?? 0; //商品类型 + $value = attr_format($attr)[1]; + $valueNew = []; + $count = 0; + foreach ($value as $key => $item) { + $detail = $item['detail']; + foreach ($detail as $v => $d) { + $detail[$v] = trim($d); + } +// sort($item['detail'], SORT_STRING); + $suk = implode(',', $detail); + $types = 1; + if ($id) { + $sukValue = $storeProductAttrValueServices->getSkuArray(['product_id' => $id, 'type' => 0, 'suk' => $suk], 'unique,bar_code,code,cost,price,settle_price,ot_price,stock,image as pic,weight,volume,brokerage,brokerage_two,vip_price,disk_info', 'suk'); + if (!$sukValue) { + if ($type == 0) $types = 0; //编辑商品时,将没有规格的数据不生成默认值 + $sukValue[$suk]['pic'] = ''; + $sukValue[$suk]['price'] = 0; + $sukValue[$suk]['settle_price'] = 0; + $sukValue[$suk]['cost'] = 0; + $sukValue[$suk]['ot_price'] = 0; + $sukValue[$suk]['stock'] = 0; + $sukValue[$suk]['bar_code'] = ''; + $sukValue[$suk]['code'] = ''; + $sukValue[$suk]['weight'] = 0; + $sukValue[$suk]['volume'] = 0; + $sukValue[$suk]['brokerage'] = 0; + $sukValue[$suk]['brokerage_two'] = 0; + switch ($product_type) { + case 1://卡密 + $sukValue[$suk]['virtual_list'] = []; + break; + case 2://优惠券 + $sukValue[$suk]['coupon_id'] = 0; + break; + case 3://虚拟商品 + break; + case 4://次卡商品 + $sukValue[$suk]['write_times'] = 1;//核销次数 + $sukValue[$suk]['write_valid'] = 1;//核销时效类型 + $sukValue[$suk]['days'] = 0;//购买后:N天有效 + $sukValue[$suk]['section_time'] = [];//[核销开始时间,核销结束时间] + break; + } + } + } else { + $sukValue[$suk]['pic'] = ''; + $sukValue[$suk]['price'] = 0; + $sukValue[$suk]['settle_price'] = 0; + $sukValue[$suk]['cost'] = 0; + $sukValue[$suk]['ot_price'] = 0; + $sukValue[$suk]['stock'] = 0; + $sukValue[$suk]['bar_code'] = ''; + $sukValue[$suk]['code'] = ''; + $sukValue[$suk]['weight'] = 0; + $sukValue[$suk]['volume'] = 0; + $sukValue[$suk]['brokerage'] = 0; + $sukValue[$suk]['brokerage_two'] = 0; + switch ($product_type) { + case 1://卡密 + $sukValue[$suk]['virtual_list'] = []; + break; + case 2://优惠券 + $sukValue[$suk]['coupon_id'] = 0; + break; + case 3://虚拟商品 + break; + case 4://次卡商品 + $sukValue[$suk]['write_times'] = 1;//核销次数 + $sukValue[$suk]['write_valid'] = 1;//核销时效类型 + $sukValue[$suk]['days'] = 0;//购买后:N天有效 + $sukValue[$suk]['section_time'] = [];//[核销开始时间,核销结束时间] + break; + } + } + if ($types) { //编辑商品时,将没有规格的数据不生成默认值 + foreach (array_keys($detail) as $k => $title) { + $header[$k]['title'] = $title; + $header[$k]['align'] = 'center'; + $header[$k]['minWidth'] = 130; + } + $values = ''; + foreach (array_values($detail) as $k => $v) { + $valueNew[$count]['value' . ($k + 1)] = $v; + $header[$k]['slot'] = 'value' . ($k + 1); + $values .= $v . ','; + } + $valueNew[$count]['values'] = substr($values, 0, strlen($values) - 1); + $valueNew[$count]['detail'] = $detail; + $valueNew[$count]['pic'] = $sukValue[$suk]['pic'] ?? ''; + $valueNew[$count]['price'] = $sukValue[$suk]['price'] ? floatval($sukValue[$suk]['price']) : 0; + $valueNew[$count]['settle_price'] = $sukValue[$suk]['settle_price'] ? floatval($sukValue[$suk]['settle_price']) : 0; + $valueNew[$count]['cost'] = $sukValue[$suk]['cost'] ? floatval($sukValue[$suk]['cost']) : 0; + $valueNew[$count]['ot_price'] = isset($sukValue[$suk]['ot_price']) ? floatval($sukValue[$suk]['ot_price']) : 0; + $valueNew[$count]['vip_price'] = isset($sukValue[$suk]['vip_price']) ? floatval($sukValue[$suk]['vip_price']) : 0; + $valueNew[$count]['stock'] = $sukValue[$suk]['stock'] ? intval($sukValue[$suk]['stock']) : 0; + $valueNew[$count]['bar_code'] = $sukValue[$suk]['bar_code'] ?? ''; + $valueNew[$count]['code'] = $sukValue[$suk]['code'] ?? ''; + $valueNew[$count]['weight'] = floatval($sukValue[$suk]['weight']) ?? 0; + $valueNew[$count]['volume'] = floatval($sukValue[$suk]['volume']) ?? 0; + $valueNew[$count]['brokerage'] = floatval($sukValue[$suk]['brokerage']) ?? 0; + $valueNew[$count]['brokerage_two'] = floatval($sukValue[$suk]['brokerage_two']) ?? 0; + switch ($product_type) { + case 1://卡密 + if (!$type) { + $valueNew[$count]['virtual_list'] = isset($sukValue[$suk]['unique']) && $sukValue[$suk]['unique'] ? $virtualService->getArr($sukValue[$suk]['unique'], $id) : []; + $valueNew[$count]['disk_info'] = $sukValue[$suk]['disk_info'] ?? ''; + } + break; + case 2://优惠券 + break; + case 3://虚拟商品 + break; + case 4://次卡商品 + $valueNew[$count]['write_times'] = intval($sukValue[$suk]['write_times'] ?? 1);//核销次数 + $valueNew[$count]['write_valid'] = intval($sukValue[$suk]['write_valid'] ?? 1);//核销时效类型 + $valueNew[$count]['days'] = intval($sukValue[$suk]['write_days'] ?? $sukValue[$suk]['days'] ?? 0);//购买后:N天有效 + $start = $sukValue[$suk]['write_start'] ?? ''; + $end = $sukValue[$suk]['write_end'] ?? ''; + $valueNew[$count]['section_time'] = [$start ? date('Y-m-d', $start) : '', $end ? date('Y-m-d', $end) : ''];//[核销开始时间,核销结束时间] + break; + } + $count++; + } + } + $header[] = ['title' => '图片', 'slot' => 'pic', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '售价', 'slot' => 'price', 'align' => 'center', 'minWidth' => 120]; + if ($plat_type == 2) {//供应商 + $header[] = ['title' => '结算价', 'slot' => 'settle_price', 'align' => 'center', 'minWidth' => 120]; + } else {//供应商不展示成本价 + $header[] = ['title' => '成本价', 'slot' => 'cost', 'align' => 'center', 'minWidth' => 140]; + } + + $header[] = ['title' => '原价', 'slot' => 'ot_price', 'align' => 'center', 'minWidth' => 140]; +// $header[] = ['title' => '会员价', 'slot' => 'vip_price', 'align' => 'center', 'minWidth' => 140]; + $header[] = ['title' => '库存', 'slot' => 'stock', 'align' => 'center', 'minWidth' => 140]; + $header[] = ['title' => '商品条形码', 'slot' => 'bar_code', 'align' => 'center', 'minWidth' => 140]; + $header[] = ['title' => '商品编码', 'slot' => 'code', 'align' => 'center', 'minWidth' => 140]; + switch ($product_type) { + case 0: + $header[] = ['title' => '重量(KG)', 'slot' => 'weight', 'align' => 'center', 'minWidth' => 140]; + $header[] = ['title' => '体积(m³)', 'slot' => 'volume', 'align' => 'center', 'minWidth' => 140]; + break; + case 1://卡密 + $header[] = ['title' => '卡密商品', 'slot' => 'fictitious', 'align' => 'center', 'minWidth' => 140]; + break; + case 2://优惠券 + break; + case 3://虚拟商品 + break; + case 4://次卡商品 + break; + default: + break; + } + $header[] = ['title' => '操作', 'slot' => 'action', 'align' => 'center', 'minWidth' => 70]; + return ['attr' => $attr, 'value' => $valueNew, 'header' => $header, 'product_type' => $product_type]; + } + + /** + * SPU + * @return string + */ + public function createSpu() + { + mt_srand(); + return substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8) . str_pad((string)mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); + } + + /** + * 新增编辑商品 + * @param int $id + * @param array $data + * @param int $type + * @param int $relation_id + * @return void + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function save(int $id, array $data, int $type = 0, int $relation_id = 0) + { + if (count($data['cate_id']) < 1) throw new AdminException('请选择商品分类'); + if (!$data['store_name']) throw new AdminException('请输入商品名称'); + if (count($data['slider_image']) < 1) throw new AdminException('请上传商品轮播图'); + if ($data['product_type'] == 0 && isset($data['delivery_type']) && count($data['delivery_type']) < 1) throw new AdminException('请选择商品配送方式'); + if (in_array($data['product_type'], [1, 2, 3, 4])) { + $data['freight'] = 2; + $data['temp_id'] = 0; + $data['postage'] = 0; + } else { + if ($data['freight'] == 1) { + $data['temp_id'] = 0; + $data['postage'] = 0; + } elseif ($data['freight'] == 2) { + $data['temp_id'] = 0; + } elseif ($data['freight'] == 3) { + $data['postage'] = 0; + } + if ($data['freight'] == 2 && !$data['postage']) { + throw new AdminException('请设置运费金额'); + } + if ($data['freight'] == 3 && !$data['temp_id']) { + throw new AdminException('请选择运费模版'); + } + } + // 开启ERP后商品编码验证 + $isOpen = sys_config('erp_open'); + if ($isOpen && $data['product_type'] == 0 && empty($data['code'])) { + throw new AdminException('请输入商品编码'); + } + $detail = $data['attrs']; + $attr = $data['items']; + $coupon_ids = $data['coupon_ids']; + + //关联补充信息 + $relationData = []; + $relationData['cate_id'] = $data['cate_id'] ?? []; + $relationData['brand_id'] = $data['brand_id'] ?? []; + $relationData['store_label_id'] = $data['store_label_id'] ?? []; + $relationData['label_id'] = $data['label_id'] ?? []; + $relationData['ensure_id'] = $data['ensure_id'] ?? []; + $relationData['specs_id'] = $data['specs_id'] ?? []; + $relationData['coupon_ids'] = $data['coupon_ids'] ?? []; + + $description = $data['description']; + $data['type'] = $type; + $data['relation_id'] = $relation_id; + $supplier_id = $data['supplier_id'] ?? 0; + if ($supplier_id) { + $data['type'] = 2; + $data['relation_id'] = $supplier_id; + } + if ($data['type'] == 0) {//平台商品不需要审核 + $data['is_verify'] = 1; + } + $is_copy = $data['is_copy']; + unset($data['supplier_id'], $data['is_copy']); + //视频 + if ($data['video_link'] && strpos($data['video_link'], 'http') === false) { + $data['video_link'] = sys_config('site_url') . $data['video_link']; + } + //品牌 + $data['brand_com'] = $data['brand_id'] ? implode(',', $data['brand_id']) : ''; + $data['brand_id'] = $data['brand_id'] ? end($data['brand_id']) : 0; + $data['is_vip'] = in_array(0, $data['is_sub']) ? 1 : 0; + $data['is_sub'] = in_array(1, $data['is_sub']) ? 1 : 0; + $data['product_type'] = intval($data['product_type']); + $data['is_vip_product'] = intval($data['is_vip_product']); + $data['is_presale_product'] = intval($data['is_presale_product']); + $data['presale_start_time'] = $data['is_presale_product'] ? strtotime($data['presale_time'][0]) : 0; + $data['presale_end_time'] = $data['is_presale_product'] ? strtotime($data['presale_time'][1]) : 0; + if ($data['presale_start_time'] && $data['presale_start_time'] < time()) { + throw new AdminException('预售开始时间不能小于当前时间'); + } + if ($data['presale_end_time'] && $data['presale_end_time'] < time()) { + throw new AdminException('预售结束时间不能小于当前时间'); + } + $data['auto_on_time'] = $data['auto_on_time'] ? strtotime($data['auto_on_time']) : 0; + $data['auto_off_time'] = $data['auto_off_time'] ? strtotime($data['auto_off_time']) : 0; + if ($data['auto_on_time']) { + $data['is_show'] = 0; + } + $data['is_limit'] = intval($data['is_limit']); + if (!$data['is_limit']) { + $data['limit_type'] = 0; + $data['limit_num'] = 0; + } else { + if (!in_array($data['limit_type'], [1, 2])) throw new AdminException('请选择限购类型'); + if ($data['limit_num'] <= 0) throw new AdminException('限购数量不能小于1'); + } + $data['custom_form'] = json_encode($data['custom_form']); + $storeLabelId = $data['store_label_id']; + if ($data['store_label_id']) { + $data['store_label_id'] = is_array($data['store_label_id']) ? implode(',', $data['store_label_id']) : $data['store_label_id']; + } else { + $data['store_label_id'] = ''; + } + if ($data['ensure_id']) { + $data['ensure_id'] = is_array($data['ensure_id']) ? implode(',', $data['ensure_id']) : $data['ensure_id']; + } else { + $data['ensure_id'] = ''; + } + if (!$data['specs_id']) { + $data['specs'] = ''; + } + if ($data['specs']) { + $specs = []; + if (is_array($data['specs'])) { + /** @var StoreProductSpecsServices $storeProductSpecsServices */ + $storeProductSpecsServices = app()->make(StoreProductSpecsServices::class); + foreach ($data['specs'] as $item) { + $specs[] = $storeProductSpecsServices->checkSpecsData($item); + } + $data['specs'] = json_encode($specs); + } + } else { + $data['specs'] = ''; + } + if ($data['spec_type'] == 0) { + $attr = [ + [ + 'value' => '规格', + 'detailValue' => '', + 'attrHidden' => '', + 'detail' => ['默认'] + ] + ]; + $detail[0]['value1'] = '默认'; + $detail[0]['detail'] = ['规格' => '默认']; + } + foreach ($detail as &$item) { + if ($isOpen && $data['product_type'] == 0 && (!isset($item['code']) || !$item['code'])) { + throw new AdminException('请输入【' . ($item['values'] ?? '默认') . '】商品编码'); + } + if ($type == 2 && !$item['settle_price']) { + throw new AdminException('请输入结算价'); + } + $item['product_type'] = $data['product_type']; + if ($data['is_sub'] == 0) { + $item['brokerage'] = 0; + $item['brokerage_two'] = 0; + } + if (($item['brokerage'] + $item['brokerage_two']) > $item['price']) { + throw new AdminException('一二级返佣相加不能大于商品售价'); + } + //验证次卡商品数据 + if ($data['product_type'] == 4) { + if (!isset($item['write_times']) || !$item['write_times']) { + throw new AdminException('请输入核销次数'); + } + if (!isset($item['write_valid'])) { + throw new AdminException('请选择核销时效类型'); + } + switch ($item['write_valid']) { + case 1://永久 + $item['days'] = 0; + $item['section_time'] = [0, 0]; + break; + case 2://购买后n天 + $item['section_time'] = [0, 0]; + if (!isset($item['days']) || !$item['days']) { + throw new AdminException('填写时效天数'); + } + break; + case 3://固定时间 + $item['days'] = 0; + if (!isset($item['section_time']) || !$item['section_time'] || !is_array($item['section_time']) || count($item['section_time']) != 2) { + throw new AdminException('请选择固定有效时间段或时间格式错误'); + } + [$start, $end] = $item['section_time']; + $data['start_time'] = $start ? strtotime($start) : 0; + $data['end_time'] = $end ? strtotime($end) : 0; + if ($data['start_time'] && $data['end_time'] && $data['end_time'] <= $data['start_time']) { + throw new AdminException('请重新选择:结束时间必须大于开始时间'); + } + break; + default: + throw new AdminException('请选择核销时效类型'); + break; + } + } + } + foreach ($data['activity'] as $k => $v) { + if ($v == '秒杀') { + $data['activity'][$k] = 1; + } elseif ($v == '砍价') { + $data['activity'][$k] = 2; + } elseif ($v == '拼团') { + $data['activity'][$k] = 3; + } else { + $data['activity'][$k] = 0; + } + } + $data['activity'] = implode(',', $data['activity']); + $data['recommend_list'] = count($data['recommend_list']) ? implode(',', $data['recommend_list']) : ''; + $data['price'] = min(array_column($detail, 'price')); + $data['ot_price'] = min(array_column($detail, 'ot_price')); + $data['cost'] = min(array_column($detail, 'cost')); + if (!$data['cost']) { + $data['cost'] = 0; + } + $data['cate_id'] = implode(',', $data['cate_id']); + $data['label_id'] = implode(',', $data['label_id']); + $data['image'] = $data['slider_image'][0];//封面图 + $slider_image = $data['slider_image']; + $data['slider_image'] = json_encode($data['slider_image']); + $data['stock'] = array_sum(array_column($detail, 'stock')); + //是否售罄 + $data['is_sold'] = $data['stock'] ? 0 : 1; + + unset($data['description'], $data['coupon_ids'], $data['items'], $data['attrs'], $data['recommend']); + /** @var StoreDescriptionServices $storeDescriptionServices */ + $storeDescriptionServices = app()->make(StoreDescriptionServices::class); + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + /** @var StoreProductCouponServices $storeProductCouponServices */ + $storeProductCouponServices = app()->make(StoreProductCouponServices::class); + /** @var StoreDiscountsProductsServices $storeDiscountProduct */ + $storeDiscountProduct = app()->make(StoreDiscountsProductsServices::class); + /** @var StoreProductVirtualServices $productVirtual */ + $productVirtual = app()->make(StoreProductVirtualServices::class); + //同一链接不多次保存 + if (!$id && $data['soure_link']) { + $productInfo = $this->dao->getOne(['soure_link' => $data['soure_link'], 'is_del' => 0], 'id'); + if ($productInfo) $id = (int)$productInfo['id']; + } + [$skuList, $id, $is_new, $data] = $this->transaction(function () use ($id, $data, $description, $storeDescriptionServices, $storeProductAttrServices, $storeProductCouponServices, $detail, $attr, $coupon_ids, $storeDiscountProduct, $productVirtual, $slider_image) { + + if ($id) { + //上下架处理 + $this->setShow([$id], $data['is_show']); + $oldInfo = $this->get($id)->toArray(); + if ($oldInfo['product_type'] != $data['product_type']) { + throw new AdminException('商品类型不能切换!'); + } + //修改不改变商品来源 + if ($oldInfo['type']) { + $data['type'] = $oldInfo['type']; + $data['relation_id'] = $oldInfo['relation_id']; + } + unset($data['sales']); + $res = $this->dao->update($id, $data); + if (!$res) throw new AdminException('修改失败'); + // 修改优惠套餐商品的运费模版id + $storeDiscountProduct->update(['product_id' => $id], ['temp_id' => $data['temp_id']]); + if (!empty($coupon_ids)) { + $storeProductCouponServices->setCoupon($id, $coupon_ids); + } else { + $storeProductCouponServices->delete(['product_id' => $id]); + } + if ($oldInfo['type'] == 1 && !$oldInfo['pid'] && !$data['is_verify']) { + /** @var StoreCartServices $cartService */ + $cartService = app()->make(StoreCartServices::class); + $cartService->batchUpdate([$id], ['status' => 0], 'product_id'); + } + $is_new = 1; + } else { + $data['add_time'] = time(); + $data['code_path'] = ''; + $data['spu'] = $this->createSpu(); + $res = $this->dao->save($data); + if (!$res) throw new AdminException('添加失败'); + $id = (int)$res->id; + if (!empty($coupon_ids)) $storeProductCouponServices->setCoupon($id, $coupon_ids); + $is_new = 0; + } + //商品详情 + $storeDescriptionServices->saveDescription($id, $description); + + $skuList = $storeProductAttrServices->validateProductAttr($attr, $detail, $id, 0, (int)$data['is_vip']); + foreach ($skuList['valueGroup'] as &$item) { + if (!isset($item['sum_stock']) || !$item['sum_stock']) $item['sum_stock'] = $item['stock'] ?? 0; + } + + $proudctVipPrice = 0; + $detailTemp = array_column($skuList['valueGroup'], 'vip_price'); + if ($detailTemp) { + $proudctVipPrice = min($detailTemp); + } + $this->dao->update($id, ['vip_price' => $proudctVipPrice]); + + $valueGroup = $storeProductAttrServices->saveProductAttr($skuList, $id); + if (!$valueGroup) throw new AdminException('添加失败!'); + if ($data['product_type'] == 1) { + $productVirtual->saveProductVirtual($id, $valueGroup); + } + return [$skuList, $id, $is_new, $data]; + }); + event('product.create', [$id, $data, $skuList, $is_new, $slider_image, $description, $is_copy, $relationData]); + } + + /** + * 放入回收站 + * @param int $id + * @return string + */ + public function del(int $id) + { + if (!$id) throw new AdminException('参数不正确'); + $productInfo = $this->dao->get($id); + if (!$productInfo) throw new AdminException('商品数据不存在'); + $msg = ''; + $data = $update = []; + if ($productInfo['is_del'] == 1) { + $data['is_del'] = 0; + $update = ['is_del' => 0]; + $msg = '成功恢复商品'; + } else { + $data['is_del'] = 1; + $data['is_show'] = 0; + $update = ['is_del' => 1]; + $msg = '成功移到回收站'; + } + $this->transaction(function () use ($id, $data, $productInfo, $update) { + //门店商品处理 + switch ($productInfo['type']) { + case 0://平台商品 + $this->dao->update(['pid' => $id], $update); + break; + case 1://门店商品 + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeInfo = $storeServices->getStoreInfo((int)$productInfo['relation_id']); + $data['is_verify'] = 0; + //门店开启免审 + if (isset($storeInfo['product_verify_status']) && $storeInfo['product_verify_status']) { + $data['is_verify'] = 1; + } + break; + case 2://供应商 + $data['is_verify'] = 0; + break; + } + $res = $this->dao->update($id, $data); + if (!$res) throw new AdminException($productInfo['is_del'] == 1 ? '恢复失败,请稍候再试!' : '删除失败,请稍候再试!'); + + }); + return $msg; + } + + /** + * 获取选择的商品列表 + * @param array $where + * @param bool $isStock + * @param int $limit + * @return array + */ + public function searchList(array $where, bool $isStock = false, int $limit = 0) + { + $store_stock = sys_config('store_stock'); + $where['store_stock'] = $store_stock > 0 ? $store_stock : 2; + $data = $this->getProductList($where, $isStock, $limit, ['attrValue', 'descriptions']); + if ($data['list']) { + $cateIds = implode(',', array_column($data['list'], 'cate_id')); + /** @var StoreProductCategoryServices $categoryService */ + $categoryService = app()->make(StoreProductCategoryServices::class); + $cateList = $categoryService->getCateParentAndChildName($cateIds); + /** @var StoreProductLabelServices $storeProductLabelServices */ + $storeProductLabelServices = app()->make(StoreProductLabelServices::class); + foreach ($data['list'] as &$item) { + $item['cate_name'] = ''; + if (isset($item['cate_id']) && $item['cate_id']) { + $cate_ids = explode(',', $item['cate_id']); + $cate_name = $categoryService->getCateName($cate_ids, $cateList); + if ($cate_name) { + $item['cate_name'] = is_array($cate_name) ? implode(',', $cate_name) : ''; + } + } + $item['give_integral'] = floatval($item['give_integral']); + $item['price'] = floatval($item['price']); + $item['vip_price'] = floatval($item['vip_price']); + $item['ot_price'] = floatval($item['ot_price']); + $item['postage'] = floatval($item['postage']); + $item['cost'] = floatval($item['cost']); + $item['delivery_type'] = is_string($item['delivery_type']) ? explode(',', $item['delivery_type']) : $item['delivery_type']; + $item['store_label'] = ''; + if ($item['store_label_id']) { + $storeLabelList = $storeProductLabelServices->getColumn([['relation_id', '=', 0], ['type', '=', 0], ['id', 'IN', $item['store_label_id']]], 'id,label_name'); + $item['store_label'] = $storeLabelList ? implode(',', array_column($storeLabelList, 'label_name')) : ''; + } + } + } + return $data; + } + + /** + * 后台获取商品列表展示 + * @param array $where + * @param bool $isStock + * @param int $limit + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductList(array $where, bool $isStock = true, int $limit = 0, array $with = ['attrValue']) + { + $field = ['*']; + if ($isStock) { + $prefix = Config::get('database.connections.' . Config::get('database.default') . '.prefix'); + $field = [ + '*', + '(SELECT count(*) FROM `' . $prefix . 'user_relation` WHERE `relation_id` = `' . $prefix . 'store_product`.`id` AND `type` = \'collect\') as collect', + '(SELECT count(*) FROM `' . $prefix . 'user_relation` WHERE `relation_id` = `' . $prefix . 'store_product`.`id` AND `type` = \'like\') as likes', + '(SELECT SUM(stock) FROM `' . $prefix . 'store_product_attr_value` WHERE `product_id` = `' . $prefix . 'store_product`.`id` AND `type` = 0) as stock', +// '(SELECT SUM(sales) FROM `' . $prefix . 'store_product_attr_value` WHERE `product_id` = `' . $prefix . 'store_product`.`id` AND `type` = 0) as sales', + '(SELECT count(*) FROM `' . $prefix . 'store_visit` WHERE `product_id` = `' . $prefix . 'store_product`.`id` AND `product_type` = \'product\') as visitor', + ]; + } + if ($limit) { + [$page] = $this->getPageValue(); + } else { + [$page, $limit] = $this->getPageValue(); + } + $list = $this->dao->getSearchList($where, $page, $limit, $field, '', $with); + $count = $this->dao->getCount($where); + return compact('count', 'list'); + } + + /** + * 获取商品规格 + * @param int $id + * @param int $type + * @return array + */ + public function getProductRules(int $id, int $type = 0) + { + $productInfo = $this->dao->get($id); + if (!$productInfo) { + throw new ValidateException('商品不存在'); + } + $product_type = $productInfo['product_type'] ?? $data['product_type'] ?? 0; //商品类型 + /** @var StoreProductAttrServices $storeProductAttrService */ + $storeProductAttrService = app()->make(StoreProductAttrServices::class); + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $productAttr = $storeProductAttrService->getProductAttr(['product_id' => $id, 'type' => 0]); + if (!$productAttr) return []; + $attr = []; + foreach ($productAttr as $key => $value) { + $attr[$key]['value'] = $value['attr_name']; + $attr[$key]['detailValue'] = ''; + $attr[$key]['attrHidden'] = true; + $attr[$key]['detail'] = $value['attr_values']; + } + $value = attr_format($attr)[1]; + $valueNew = []; + $count = 0; +// $sukValue = $storeProductAttrValueServices->getSkuArray(['product_id' => $id, 'type' => $type]); + $sukValue = $sukDefaultValue = $storeProductAttrValueServices->getSkuArray(['product_id' => $id, 'type' => 0]); + foreach ($value as $key => $item) { + $detail = $item['detail']; +// sort($item['detail'], SORT_STRING); + $suk = implode(',', $item['detail']); + if (!isset($sukDefaultValue[$suk])) continue; +// if (!isset($sukValue[$suk])) { +// $sukValue[$suk] = $sukDefaultValue[$suk]; +// } + foreach (array_keys($detail) as $k => $title) { + $header[$k]['title'] = $title; + $header[$k]['align'] = 'center'; + $header[$k]['minWidth'] = 80; + } + $valueNew[$count]['value'] = ''; + foreach (array_values($detail) as $k => $v) { + $valueNew[$count]['value' . ($k + 1)] = $v; + $header[$k]['key'] = 'value' . ($k + 1); + $valueNew[$count]['value'] .= $valueNew[$count]['value'] == '' ? $v : ',' . $v; + } + if ($type == 4) { + $valueNew[$count]['integral'] = $sukValue[$suk]['integral'] ?? 0; + $valueNew[$count]['product_id'] = $sukValue[$suk]['product_id']; + } + $valueNew[$count]['detail'] = $detail; + $valueNew[$count]['pic'] = $sukValue[$suk]['pic']; + $valueNew[$count]['price'] = floatval($sukValue[$suk]['price']); + if ($type == 2) $valueNew[$count]['min_price'] = 0; + if ($type == 3) $valueNew[$count]['r_price'] = floatval($sukValue[$suk]['price']); + if ($type == 0) $valueNew[$count]['p_price'] = floatval($sukValue[$suk]['price']); + $valueNew[$count]['cost'] = floatval($sukValue[$suk]['cost']); + $valueNew[$count]['ot_price'] = floatval($sukValue[$suk]['ot_price']); + $valueNew[$count]['stock'] = intval($sukValue[$suk]['stock']); + $valueNew[$count]['quota'] = intval($sukValue[$suk]['quota']); + $valueNew[$count]['bar_code'] = $sukValue[$suk]['bar_code'] ?? ''; + $valueNew[$count]['code'] = $sukValue[$suk]['code'] ?? ''; + $valueNew[$count]['weight'] = $sukValue[$suk]['weight'] ? floatval($sukValue[$suk]['weight']) : 0; + $valueNew[$count]['volume'] = $sukValue[$suk]['volume'] ? floatval($sukValue[$suk]['volume']) : 0; + $valueNew[$count]['brokerage'] = $sukValue[$suk]['brokerage'] ? floatval($sukValue[$suk]['brokerage']) : 0; + $valueNew[$count]['brokerage_two'] = $sukValue[$suk]['brokerage_two'] ? floatval($sukValue[$suk]['brokerage_two']) : 0; + $count++; + } + $header[] = ['title' => '图片', 'slot' => 'pic', 'align' => 'center', 'minWidth' => 120]; + if ($type == 1) { + $header[] = ['title' => '秒杀价', 'key' => 'price', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '成本价', 'key' => 'cost', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '原价', 'key' => 'ot_price', 'align' => 'center', 'minWidth' => 80]; + } elseif ($type == 2) { + $header[] = ['title' => '砍价起始金额', 'slot' => 'price', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '砍价最低价', 'slot' => 'min_price', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '成本价', 'key' => 'cost', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '原价', 'key' => 'ot_price', 'align' => 'center', 'minWidth' => 80]; + } elseif ($type == 3) { + $header[] = ['title' => '拼团价', 'key' => 'price', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '成本价', 'key' => 'cost', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '日常售价', 'key' => 'r_price', 'align' => 'center', 'minWidth' => 80]; + } elseif ($type == 4) { + $header[] = ['title' => '兑换积分', 'key' => 'integral', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '金额', 'key' => 'price', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + } else { + $header[] = ['title' => '成本价', 'key' => 'cost', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '原价', 'key' => 'ot_price', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '售价', 'key' => 'p_price', 'align' => 'center', 'minWidth' => 80]; + } + $header[] = ['title' => '库存', 'key' => 'stock', 'align' => 'center', 'minWidth' => 80]; + if ($type == 2) { + $header[] = ['title' => '限量', 'slot' => 'quota', 'align' => 'center', 'minWidth' => 80]; + } else if ($type == 4) { + $header[] = ['title' => '兑换次数', 'key' => 'quota', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + } else { + $header[] = ['title' => '限量', 'key' => 'quota', 'type' => 1, 'align' => 'center', 'minWidth' => 80]; + } + $header[] = ['title' => '重量(KG)', 'key' => 'weight', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '体积(m³)', 'key' => 'volume', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '商品条形码', 'key' => 'bar_code', 'align' => 'center', 'minWidth' => 80]; + $header[] = ['title' => '商品编码', 'key' => 'code', 'align' => 'center', 'minWidth' => 80]; + return ['items' => $attr, 'attrs' => $valueNew, 'header' => $header, 'product_type' => $product_type]; + } + + /** + * 检查商品是否有活动 + * @param $id + * @return bool + */ + public function checkActivity($id = 0) + { + if ($id) { + /** @var StoreSeckillServices $storeSeckillService */ + $storeSeckillService = app()->make(StoreSeckillServices::class); + /** @var StoreBargainServices $storeBargainService */ + $storeBargainService = app()->make(StoreBargainServices::class); + /** @var StoreCombinationServices $storeCombinationService */ + $storeCombinationService = app()->make(StoreCombinationServices::class); + $res1 = $storeSeckillService->count(['product_id' => $id, 'is_del' => 0, 'status' => 1, 'seckill_time' => 1]); + $res2 = $storeBargainService->count(['product_id' => $id, 'is_del' => 0, 'status' => 1, 'bargain_time' => 1]); + $res3 = $storeCombinationService->count(['product_id' => $id, 'is_del' => 0, 'is_show' => 1, 'pinkIngTime' => 1]); + if ($res1 || $res2 || $res3) throw new AdminException('商品有活动开启,无法进行此操作'); + } + return true; + } + + /** + * 获取临时缓存商品数据 + * @param int $id + * @return false|mixed|string|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/3 + */ + public function getCacheProductInfo(int $id) + { + $storeInfo = $this->dao->cacheTag()->remember((string)$id, function () use ($id) { + $storeInfo = $this->dao->getOne(['id' => $id], '*', ['descriptions']); + if (!$storeInfo) { + throw new ValidateException('商品不存在'); + } else { + $storeInfo = $storeInfo->toArray(); + } + return $storeInfo; + }, 600); + + return $storeInfo; + } + + /** + * 保存 + * @param array $data + * @return mixed + */ + public function create(array $data) + { + return $this->dao->save($data); + } + + /** + * 前台获取商品列表 + * @param array $where + * @param int $uid + * @param int $promotions_type + * @return array|array[] + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGoodsList(array $where, int $uid, int $promotions_type = 0) + { + $where['is_verify'] = 1; + $where['is_show'] = 1; + $where['is_del'] = 0; + $collate_code_id = 0; + if (isset($where['collate_code_id'])) { + $collate_code_id = $where['collate_code_id']; + } + if (isset($where['store_name']) && $where['store_name']) { + /** @var UserSearchServices $userSearchServices */ + $userSearchServices = app()->make(UserSearchServices::class); + $searchIds = $userSearchServices->vicSearch($uid, $where['store_name'], $where); + if ($searchIds) {//之前查询结果记录 + $where['ids'] = $searchIds; + unset($where['store_name']); + } + } + //优惠活动凑单 + if (isset($where['promotions_id']) && $where['promotions_id']) { + /** @var StorePromotionsServices $storePromotionsServices */ + $storePromotionsServices = app()->make(StorePromotionsServices::class); + $promotionsWhere = $storePromotionsServices->collectOrderProduct((int)$where['promotions_id']); + $where = array_merge($where, $promotionsWhere); + } + unset($where['promotions_id']); + if (isset($where['productId']) && $where['productId'] !== '') { + $where['ids'] = explode(',', $where['productId']); + $where['ids'] = array_unique(array_map('intval', $where['ids'])); + unset($where['productId']); + } + $where['star'] = 1; + $where['is_vip_product'] = 0; + $discount = 100; + $level_name = ''; + if (!$promotions_type && $uid) { + /** @var UserServices $user */ + $user = app()->make(UserServices::class); + $userInfo = $user->getUserCacheInfo($uid); + $is_vip = $userInfo['is_money_level'] ?? 0; + $where['is_vip_product'] = $is_vip ? -1 : 0; + //用户等级是否开启 + /** @var SystemUserLevelServices $systemLevel */ + $systemLevel = app()->make(SystemUserLevelServices::class); + $levelInfo = $systemLevel->getLevelCache((int)($userInfo['level'] ?? 0)); + if (sys_config('member_func_status', 1) && $levelInfo) { + $discount = $levelInfo['discount'] ?? 100; + } + $level_name = $levelInfo['name'] ?? ''; + } + + [$page, $limit] = $this->getPageValue(); + $field = ['id,relation_id,type,pid,delivery_type,product_type,store_name,cate_id,image,IFNULL(sales, 0) + IFNULL(ficti, 0) as sales,price,stock,activity,ot_price,spec_type,recommend_image,unit_name,is_vip,vip_price,is_presale_product,is_vip_product,system_form_id,is_presale_product,presale_start_time,presale_end_time,is_limit,limit_num,video_open,video_link']; + $list = $this->dao->getSearchList($where, $page, $limit, $field, '', ['couponId']); + if ($list) { + /** @var MemberCardServices $memberCardService */ + $memberCardService = app()->make(MemberCardServices::class); + $vipStatus = $memberCardService->isOpenMemberCardCache('vip_price') && sys_config('svip_price_status', 1); + /** @var SystemFormServices $systemFormServices */ + $systemFormServices = app()->make(SystemFormServices::class); + $systemForms = $systemFormServices->getColumn([['id', 'in', array_unique(array_column($list, 'system_form_id'))], ['is_del', '=', 0]], 'id,value', 'id'); + foreach ($list as &$item) { + $minData = $this->getMinPrice($uid, $item, $discount); + $item['price_type'] = $minData['price_type'] ?? ''; + $item['level_name'] = $level_name; + $item['vip_price'] = $minData['vip_price'] ?? 0; + if ($item['price_type'] == 'member' && (!$item['is_vip'] || !$vipStatus)) { + $item['vip_price'] = 0; + } + $custom_form = $systemForms[$item['system_form_id']]['value'] ?? []; + $item['custom_form'] = is_string($custom_form) ? json_decode($custom_form, true) : $custom_form; + $item['cart_button'] = $item['product_type'] > 0 || $item['is_presale_product'] || $item['system_form_id'] ? 0 : 1; + + if (isset($item['star']) && count($item['star'])) { + $item['star'] = bcdiv((string)array_sum(array_column($item['star'], 'product_score')), (string)count($item['star']), 1); + } else { + $item['star'] = config('admin.product_default_star'); + } + $item['presale_pay_status'] = $this->checkPresaleProductPay((int)$item['id'], $item); + if (!$item['video_open']) { + $item['video_link'] = ''; + } + } + $list = $this->getActivityList($list); + $list = $this->getProduceOtherList($list, $uid, isset($where['status']) && !!$where['status'], $collate_code_id); + $list = $this->getProductPromotions($list, $promotions_type ? [$promotions_type] : []); + } + return $list; + } + + /** + * 搜索获取商品品牌列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getBrandList(array $where) + { + $where['status'] = 1; + /** @var StoreProductCategoryBrandServices $productCategoryBrandServices */ + $productCategoryBrandServices = app()->make(StoreProductCategoryBrandServices::class); + return $productCategoryBrandServices->getList($where, 'distinct(`brand_id`) as id, brand_name'); + } + + /** + * 获取商品所在优惠活动 + * @param array $list + * @param array $promotions_type + * @return array|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductPromotions(array $list, array $promotions_type = []) + { + if (!$list) { + return $list; + } + $productIds = array_column($list, 'id'); + /** @var StorePromotionsServices $storePromotionsServices */ + $storePromotionsServices = app()->make(StorePromotionsServices::class); + $with = ['products' => function ($query) { + $query->field('promotions_id,product_id,is_all,unique'); + }]; + $field = 'id,promotions_type,name,desc,image,promotions_type,title,product_id,product_partake_type,discount,discount_type,start_time,stop_time,applicable_type,applicable_store_id'; + [$promotionsArr, $productDetails, $promotionsDetail] = $storePromotionsServices->getProductsPromotionsDetail($productIds, $field, $with, $promotions_type); + $promotionsArr = array_combine(array_column($promotionsArr, 'id'), $promotionsArr); + foreach ($list as &$item) { + $item['product_id'] = $item['id']; + $item['promotions'] = $item['activity_frame'] = $item['activity_background'] = []; + if ($item['type'] == 1) {//门店商品 + if (isset($item['pid']) && $item['pid']) {//平台共享到门店商品 + $id = $item['pid']; + } else {//门店独立商品 + continue; + } + } else { + $id = $item['id']; + } + $item['promotions'] = $item['activity_frame'] = $item['activity_background'] = []; + $promotionsIds = $productDetails[$id] ?? []; + if ($promotionsIds) { + foreach ($promotionsIds as $id) { + $promotions = $promotionsArr[$id] ?? []; + switch ($promotions['promotions_type']) { + case 1: + case 2: + case 3: + case 4: + if (!$promotions_type) {//无指定优惠类型 + if ($item['promotions']) { + if (($promotions['promotions_type'] ?? 0) <= ($item['promotions']['promotions_type'] ?? 0)) { + if (($promotions['promotions_type']['discount'] ?? 0) > ($item['promotions']['discount'] ?? 0)) { + $item['promotions'] = $promotions; + } + } else { + break; + } + } else { + $item['promotions'] = $promotions; + } + } else { + if (!$item['promotions']) { + $item['promotions'] = $promotions; + } else {//同类活动展示最新的一个 + break; + } + break; + } + break; + case 5://边框 + if (!$item['activity_frame']) { + $item['activity_frame'] = [ + 'id' => $promotions['id'], + 'name' => $promotions['name'], + 'image' => $promotions['image'], + ]; + } else {//同类活动展示最新的一个 + break; + } + break; + case 6://背景 + if (!$item['activity_background']) { + $item['activity_background'] = [ + 'id' => $promotions['id'], + 'name' => $promotions['name'], + 'image' => $promotions['image'], + ]; + } else {//同类活动展示最新的一个 + break; + } + break; + default: + break; + } + } + } + } + return $list; + } + + /** + * 获取某些模板所需得购物车数量 + * @param array $list + * @param int $uid + * @param bool $type + * @return array + */ + public function getProduceOtherList(array $list, int $uid, bool $type = true, int $collate_code_id = 0) + { + if (!$type || !$list) { + return $list; + } + $productIds = array_column($list, 'id'); + if ($productIds) { + /** @var StoreProductAttrValueServices $services */ + $services = app()->make(StoreProductAttrValueServices::class); + $attList = $services->getSkuArray([ + 'product_id' => $productIds, + 'type' => 0 + ], 'count(*)', 'product_id'); + $store_id = (int)$this->getItem('store_id', 0); + $staff_id = (int)$this->getItem('staff_id', 0); + $tourist_uid = (int)$this->getItem('tourist_uid', 0); + if ($uid || $tourist_uid) { + if ($collate_code_id) { + /** @var UserCollagePartakeServices $cartServices */ + $cartServices = app()->make(UserCollagePartakeServices::class); + $cartNumList = $cartServices->productIdByCartNum($productIds, $uid, $store_id, $collate_code_id); + } else { + /** @var StoreCartServices $cartServices */ + $cartServices = app()->make(StoreCartServices::class); + $cartNumList = $cartServices->productIdByCartNum($productIds, $uid, $staff_id, $tourist_uid, $store_id); + } + $data = []; + foreach ($cartNumList as $item) { + $data[$item['product_id']][] = $item['cart_num']; + } + $newNumList = []; + foreach ($data as $key => $item) { + $newNumList[$key] = array_sum($item); + } + $cartNumList = $newNumList; + } else { + $cartNumList = []; + } + foreach ($list as &$item) { + if (isset($item['spec_type']) && $item['spec_type']) { + $item['is_att'] = isset($attList[$item['id']]) && $attList[$item['id']]; + } else { + $item['is_att'] = false; + } + $item['cart_num'] = $cartNumList[$item['id']] ?? 0; + } + } + return $list; + } + + /** + * 获取商品活动标签 + * @param array $list + * @param bool $status + * @return array|array[]|mixed + */ + public function getActivityList(array $list, bool $status = true) + { + if (!$list) return []; + if (!$status) {//一维数组 + $list = [$list]; + } + $productIds = []; + //处理平台共享到门店、门店独立商品活动 + foreach ($list as $product) { + if (isset($product['type']) && isset($product['pid'])) { + if ($product['type'] == 1) {//门店商品 + if ($product['pid']) {//平台共享到门店商品 + $productIds[] = $product['pid']; + } else {//门店独立商品 + + } + } else { + $productIds[] = $product['id']; + } + } + } + if ($productIds) { + /** @var StoreSeckillServices $storeSeckillService */ + $storeSeckillService = app()->make(StoreSeckillServices::class); + /** @var StoreBargainServices $storeBargainServices */ + $storeBargainServices = app()->make(StoreBargainServices::class); + /** @var StoreCombinationServices $storeCombinationServices */ + $storeCombinationServices = app()->make(StoreCombinationServices::class); + $seckillIdsList = $storeSeckillService->getSeckillIdsArrayCache($productIds); + $pinkIdsList = $storeCombinationServices->getPinkIdsArrayCache($productIds); + $bargrainIdsList = $storeBargainServices->getBargainIdsArrayCache($productIds); + foreach ($list as &$item) { + if ($item['type'] == 1) {//门店商品 + if ($item['pid']) {//平台共享到门店商品 + $id = $item['pid']; + } else {//门店独立商品 + continue; + } + } else { + $id = $item['id']; + } + $seckillId = $seckillIdsList && is_array($seckillIdsList) ? array_filter($seckillIdsList, function ($val) use ($item, $id) { + if ($val['product_id'] === $id) { + return $val; + } + }) : []; + $item['activity'] = $this->activity($item['activity'], + $item['id'], + $pinkIdsList[$id] ?? 0, + $seckillId, + $bargrainIdsList[$id] ?? 0, + $status); + if (isset($item['couponId'])) { + $item['checkCoupon'] = (bool)count($item['couponId']); + unset($item['couponId']); + } else { + $item['checkCoupon'] = false; + } + } + } + if ($status) { + return $list; + } else { + return $list[0]['activity']; + } + } + + /** + * 获取商品在此时段活动优先类型 + * @param string $activity + * @param int $id + * @param int $combinationId + * @param array $seckillId + * @param int $bargainId + * @param bool $status + * @return array + */ + public function activity(string $activity, int $id, int $combinationId, array $seckillId, int $bargainId, bool $status = true) + { + if (!$activity) { + $activity = '0,1,2,3';//如果老商品没有活动顺序,默认活动顺序,秒杀-砍价-拼团 + } + $activity = explode(',', $activity); + if ($activity[0] == 0 && $status) return []; + $activityId = []; + $time = 0; + if ($seckillId) { + /** @var StoreSeckillTimeServices $storeSeckillTimeServices */ + $storeSeckillTimeServices = app()->make(StoreSeckillTimeServices::class); + $timeList = $storeSeckillTimeServices->time_list(); + if ($timeList) { + $timeList = array_combine(array_column($timeList, 'id'), $timeList); + $today = date('Y-m-d'); + $currentHour = date('Hi'); + foreach ($seckillId as $v) { + $time_ids = is_string($v['time_id']) ? explode(',', $v['time_id']) : $v['time_id']; + if ($time_ids) { + foreach ($time_ids as $time_id) { + $timeInfo = $timeList[$time_id] ?? []; + if ($timeInfo) { + $start = str_replace(':', '', $timeInfo['start_time']); + $end = str_replace(':', '', $timeInfo['end_time']); + if ($currentHour >= $start && $currentHour < $end) { + $activityId[1] = $v['id']; + $time = strtotime($today . ' ' . $timeInfo['end_time']); + break; + } + } + } + } + } + } + } + if ($bargainId) $activityId[2] = $bargainId; + if ($combinationId) $activityId[3] = $combinationId; + $data = []; + foreach ($activity as $k => $v) { + if (array_key_exists($v, $activityId)) { + if ($status) { + $data['type'] = $v; + $data['id'] = $activityId[$v]; + if ($v == 1) $data['time'] = $time; + break; + } else { + if ($v != 0) { + $arr['type'] = $v; + $arr['id'] = $activityId[$v]; + if ($v == 1) $arr['time'] = $time; + $data[] = $arr; + } + } + } + } + return $data; + } + + /** + * 获取热门商品 + * @param array $where + * @param string $order + * @return array|array[] + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProducts(array $where, string $order = '', int $num = 0, array $with = ['couponId', 'descriptions']) + { + [$page, $limit] = $this->getPageValue(); + if ($num) { + $page = 1; + $limit = $num; + } + $list = $this->dao->getSearchList($where, $page, $limit, ['id,pid,type,store_name,cate_id,image,IFNULL(sales, 0) + IFNULL(ficti, 0) as sales,price,stock,activity,unit_name'], $order, $with); + $list = $this->getActivityList($list); + $list = $this->getProductPromotions($list); + return $list; + } + + /** + * 检测预售商品是否可以购买 + * @param int $id + * @param array $productInfo + * @return int + */ + public function checkPresaleProductPay(int $id, array $productInfo = []) + { + if (!$id) return 0; + if (!$productInfo) { + $productInfo = $this->getCacheProductInfo($id); + if (!$productInfo) { + return 0; + } + } + if (!isset($productInfo['is_presale_product']) || !isset($productInfo['presale_start_time']) || !isset($productInfo['presale_end_time'])) { + return 0; + } + if ($productInfo['is_presale_product']) { + if ($productInfo['presale_start_time'] > time()) { + return 1; + } elseif ($productInfo['presale_start_time'] <= time() && $productInfo['presale_start_time'] > time()) { + return 2; + } elseif ($productInfo['presale_end_time'] < time()) { + return 3; + } else { + return 0; + } + } else { + return 0; + } + } + + /** + * 获取商品详情 + * @param int $uid + * @param int $id + * @param int $type + * @param int $promotions_type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @throws \throwable + */ + public function productDetail(int $uid, int $id, int $type = 0, int $promotions_type = 0) + { + $data['uid'] = $uid; + $storeInfo = $this->getCacheProductInfo($id); + if (!$storeInfo) { + throw new ValidateException('商品不存在'); + } + $storeInfo['description'] = $storeInfo['description'] ?: ''; + /** @var DiyServices $diyServices */ + $diyServices = app()->make(DiyServices::class); + $infoDiy = $diyServices->getProductDetailDiy(); + //diy控制参数 + if (!isset($infoDiy['is_specs']) || !$infoDiy['is_specs']) { + $storeInfo['specs'] = []; + } + $storeInfo['brand_name'] = $this->productIdByBrandName((int)$storeInfo['id'], $storeInfo); + $storeInfo['store_label'] = $storeInfo['ensure'] = []; + if ($storeInfo['store_label_id']) { + /** @var StoreProductLabelServices $storeProductLabelServices */ + $storeProductLabelServices = app()->make(StoreProductLabelServices::class); + $storeInfo['store_label'] = $storeProductLabelServices->getLabelCache($storeInfo['store_label_id'], ['id', 'label_name']); + } + if ($storeInfo['ensure_id'] && isset($infoDiy['is_ensure']) && $infoDiy['is_ensure']) { + /** @var StoreProductEnsureServices $storeProductEnsureServices */ + $storeProductEnsureServices = app()->make(StoreProductEnsureServices::class); + $storeInfo['ensure'] = $storeProductEnsureServices->getEnsurCache($storeInfo['ensure_id'], ['id', 'name', 'image', 'desc']); + } + + $discount = isset($storeInfo['promotions'][0]['promotions_type']) && $storeInfo['promotions'][0]['promotions_type'] == 1 ? $storeInfo['promotions'][0]['discount'] : -1; + + $configData = SystemConfigService::more(['site_url', 'tengxun_map_key', 'store_self_mention', 'routine_contact_type', 'site_name', 'share_qrcode', 'store_func_status', 'product_poster_title']); + $siteUrl = $configData['site_url'] ?? ''; + if ($storeInfo['video_open']) { + if ($storeInfo['video_link'] && strpos($storeInfo['video_link'], 'http') === false) { + $storeInfo['video_link'] = $siteUrl . $storeInfo['video_link']; + } + } else { + $storeInfo['video_link'] = ''; + } + + $storeInfo['image'] = set_file_url($storeInfo['image'], $siteUrl); + $storeInfo['image_base'] = set_file_url($storeInfo['image'], $siteUrl); + $storeInfo['fsales'] = $storeInfo['ficti'] + $storeInfo['sales']; +// /** @var QrcodeServices $qrcodeService */ +// $qrcodeService = app()->make(QrcodeServices::class); +// $storeInfo['code_base'] = $qrcodeService->getWechatQrcodePath($id . '_product_detail_wap.jpg', '/pages/goods_details/index?id=' . $id); + /** @var UserRelationServices $userRelationServices */ + $userRelationServices = app()->make(UserRelationServices::class); + $storeInfo['userCollect'] = $userRelationServices->isProductRelationCache(['uid' => $uid, 'relation_id' => $id, 'type' => 'collect', 'category' => UserRelationServices::CATEGORY_PRODUCT]); + $storeInfo['userLike'] = 0; + + //预售相关 + $storeInfo['presale_pay_status'] = $this->checkPresaleProductPay($id, $storeInfo); + + $storeInfo['presale_start_time'] = $storeInfo['presale_start_time'] ? date('Y-m-d H:i', $storeInfo['presale_start_time']) : ''; + $storeInfo['presale_end_time'] = $storeInfo['presale_end_time'] ? date('Y-m-d H:i', $storeInfo['presale_end_time']) : ''; + //系统表单 + $storeInfo['custom_form'] = []; + if ($storeInfo['system_form_id']) { + /** @var SystemFormServices $systemFormServices */ + $systemFormServices = app()->make(SystemFormServices::class); + $systemForm = $systemFormServices->value(['id' => $storeInfo['system_form_id']], 'value'); + if ($systemForm) { + $storeInfo['custom_form'] = is_string($systemForm) ? json_decode($systemForm, true) : $systemForm; + } + } + //有自定义表单或预售或虚拟不展示加入购物车按钮 + $storeInfo['cart_button'] = $storeInfo['custom_form'] || $storeInfo['is_presale_product'] || $storeInfo['product_type'] > 0 ? 0 : 1; + + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + [$productAttr, $productValue] = $storeProductAttrServices->getProductAttrDetailCache($id, $uid, $type, 0, 0, $storeInfo, $discount); + //无属性添加默认属性 + if (empty($productValue)) { + $attr = [ + [ + 'value' => '规格', + 'detailValue' => '', + 'attrHidden' => '', + 'detail' => ['默认'] + ] + ]; + $detail[0] = [ + 'value1' => '默认', + 'detail' => ['规格' => '默认'], + 'pic' => $storeInfo['image'], + 'price' => $storeInfo['price'], + 'cost' => $storeInfo['cost'], + 'ot_price' => $storeInfo['ot_price'], + 'stock' => $storeInfo['stock'], + 'bar_code' => '', + 'code' => '', + 'weight' => 0, + 'volume' => 0, + 'brokerage' => 0, + 'brokerage_two' => 0, + ]; + $skuList = $storeProductAttrServices->validateProductAttr($attr, $detail, $id); + $storeProductAttrServices->saveProductAttr($skuList, $id, 0); + } + $attrValue = $productValue; + if (!$storeInfo['spec_type']) { + $productAttr = []; + $productValue = []; + } + $data['productAttr'] = $productAttr; + $data['productValue'] = $productValue; + $storeInfo['small_image'] = get_thumb_water($storeInfo['image']); + + /** + * 判断配送方式 + */ + $storeInfo['delivery_type'] = $this->getDeliveryType((int)$storeInfo['id'], (int)$storeInfo['type'], (int)$storeInfo['relation_id'], $storeInfo['delivery_type']); + + $data['storeInfo'] = $storeInfo; + + /** @var MemberCardServices $memberCardService */ + $memberCardService = app()->make(MemberCardServices::class); + $vipStatus = $memberCardService->isOpenMemberCardCache('vip_price') && sys_config('svip_price_status', 1); + $price_count = count($infoDiy['price_type']); + if ($price_count >= 1) { + //两个都选 取最低的 + $minPrice = $this->getMinPrice($uid, $data['storeInfo'], null, count($infoDiy['price_type']) == 2); + $price_count = count($infoDiy['price_type']); + if ($price_count == 1) {// + if (in_array(1, $infoDiy['price_type'])) {//svip + $minPrice['price_type'] = 'member'; + } else {//用户等级 + $minPrice['price_type'] = 'level'; + $minPrice['vip_price'] = $minPrice['level_price']; + } + } + } else {//一个都不展示 + $minPrice = ['vip_price' => 0, 'price_type' => '', 'level_name' => '']; + } + + $data['storeInfo'] = array_merge($data['storeInfo'], $minPrice); + if ($data['storeInfo']['price_type'] == 'member' && (!$data['storeInfo']['is_vip'] || !$vipStatus)) { + $data['storeInfo']['vip_price'] = 0; + } + $data['priceName'] = 0; + if ($uid) { + $data['priceName'] = $this->getPacketPrice($storeInfo, $attrValue, $uid); + } + $data['reply'] = []; + $data['replyChance'] = $data['replyCount'] = 0; + if (isset($infoDiy['is_reply']) && $infoDiy['is_reply']) { + /** @var StoreProductReplyServices $storeProductReplyService */ + $storeProductReplyService = app()->make(StoreProductReplyServices::class); + $reply = $storeProductReplyService->getRecProductReplyCache($id, (int)($infoDiy['reply_num'] ?? 1)); + $data['reply'] = $reply ? get_thumb_water($reply, 'small', ['pics']) : []; + [$replyCount, $goodReply, $replyChance] = $storeProductReplyService->getProductReplyData($id); + $data['replyChance'] = $replyChance; + $data['replyCount'] = $replyCount; + } + $data['mer_id'] = 0; + $data['mapKey'] = $configData['tengxun_map_key'] ?? ''; + $data['store_func_status'] = (int)($configData['store_func_status'] ?? 1);//门店是否开启 + $data['store_self_mention'] = $data['store_func_status'] ? (int)($configData['store_self_mention'] ?? 1) : 0;//门店自提是否开启 + $data['routine_contact_type'] = $configData['routine_contact_type'] ?? 0; + $data['site_name'] = $configData['site_name'] ?? ''; + $data['share_qrcode'] = $configData['share_qrcode'] ?? 0; + $data['product_poster_title'] = $configData['product_poster_title'] ?? ''; + $data['is_store_buy'] = 0; + $count = 0; + //平台商品 && 支持门店(配送|自提)&& 适用门店(全部|部分) + if ($storeInfo['type'] == 0 && array_intersect([2, 3], $storeInfo['delivery_type']) && in_array($storeInfo['applicable_type'], [1, 2])) { + $where = ['is_del' => 0, 'is_show' => 1, 'is_verify' => 1, 'type' => 1]; + if ($storeInfo['applicable_type'] == 2) { + $applicable_store_id = is_string($storeInfo['applicable_store_id']) ? explode(',', $storeInfo['applicable_store_id']) : $storeInfo['applicable_store_id']; + if ($applicable_store_id) {//门店商品正常 + $where['relation_id'] = $applicable_store_id; + $count = $this->dao->count($where); + } + } else { + $count = $this->dao->count($where); + } + } + if (!$storeInfo['stock'] && $count) {//平台无库存,支持门店购买 + $data['is_store_buy'] = 1; + } + //浏览记录 + ProductLogJob::dispatch(['visit', ['uid' => $uid, 'id' => $id, 'product_id' => $id], 'product']); + return $data; + } + + /** + * 是否开启vip + * @param bool $vip + * @return bool + */ + public function vipIsOpen(bool $vip = false, $vipStatus = -1) + { + if (!$vip) { + return false; + } + $member_status = sys_config('member_card_status', 1); + if (!$member_status) { + return false; + } + if ($vipStatus == -1) { + /** @var MemberCardServices $memberCardService */ + $memberCardService = app()->make(MemberCardServices::class); + $vipStatus = $memberCardService->isOpenMemberCardCache('vip_price', false, $member_status); + } + return $vipStatus && $member_status && $vip && sys_config('svip_price_status', 1); + } + + /** + * 获取商品分销佣金最低和最高 + * @param $storeInfo + * @param $productValue + * @param int $uid + * @return int|string + */ + public function getPacketPrice($storeInfo, $productValue, int $uid) + { + if (!count($productValue)) { + return 0; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$userServices->checkUserPromoter($uid)) { + return 0; + } + if (isset($storeInfo['is_sub']) && $storeInfo['is_sub'] == 1) { + $maxPrice = (float)max(array_column($productValue, 'brokerage')); + $minPrice = (float)min(array_column($productValue, 'brokerage')); + } else { + $maxPrice = max(array_column($productValue, 'price')); + $minPrice = min(array_column($productValue, 'price')); + $store_brokerage_ratio = sys_config('store_brokerage_ratio'); + $store_brokerage_ratio = (string)bcdiv((string)$store_brokerage_ratio, '100', 2); + $maxPrice = (float)bcmul($store_brokerage_ratio, (string)$maxPrice, 2); + $minPrice = (float)bcmul($store_brokerage_ratio, (string)$minPrice, 2); + //大于1 取整(两位小数前端展示超出) + $maxPrice = $maxPrice > 1 ? floor($maxPrice) : $maxPrice; + $minPrice = $minPrice > 1 ? floor($minPrice) : $minPrice; + } + if ($minPrice == 0 && $maxPrice == 0) { + $priceName = 0; + } else if ($minPrice == 0 && $maxPrice) + $priceName = $maxPrice; + else if ($maxPrice == 0 && $minPrice) + $priceName = $minPrice; + else if ($maxPrice == $minPrice && $minPrice) + $priceName = $maxPrice; + else + $priceName = $minPrice . '~' . $maxPrice; + return strlen(trim($priceName)) ? $priceName : 0; + } + + /** + * 获取商品用户等级、svip最低价格,优惠类型 + * @param int $uid + * @param $productInfo + * @param $discount + * @param bool $is_min + * @return array + */ + public function getMinPrice(int $uid, $productInfo, $discount = null, $is_min = true) + { + $level_name = ''; + $vip_price = 0; + $price_type = ''; + $level_price = 0; + if ($productInfo && !($productInfo['type'] == 1 && $productInfo['pid'] == 0)) { + if (is_null($discount)) { + $discount = 100; + if ($uid) { + /** @var UserServices $user */ + $user = app()->make(UserServices::class); + $userInfo = $user->getUserCacheInfo($uid); + //用户等级是否开启 + /** @var SystemUserLevelServices $systemLevel */ + $systemLevel = app()->make(SystemUserLevelServices::class); + $levelInfo = $systemLevel->getLevelCache((int)($userInfo['level'] ?? 0)); + if (sys_config('member_func_status', 1) && $levelInfo) { + $discount = $levelInfo['discount'] ?? 100; + } + $level_name = $levelInfo['name'] ?? ''; + } + } + if ($discount >= 0 && $discount < 100) {//等级价格 + $level_price = (float)bcmul((string)bcdiv((string)$discount, '100', 2), (string)$productInfo['price'], 2); + } else { + $level_price = $productInfo['price']; + } + if ($productInfo['is_vip']) {//svip价格 + $vip_price = $productInfo['vip_price']; + } + if (($discount != 100 || $productInfo['is_vip']) && $is_min) {//需要对比价格 + if ($discount != 100 && $productInfo['is_vip']) { + if ($level_price < $productInfo['vip_price']) { + $price_type = 'level'; + $vip_price = $level_price; + } else { + $price_type = 'member'; + $vip_price = $productInfo['vip_price']; + } + } else if ($discount != 100 && !$productInfo['is_vip']) { + $price_type = 'level'; + $vip_price = $level_price; + } else if ($discount == 100 && $productInfo['is_vip']) { + $price_type = 'member'; + $vip_price = $productInfo['vip_price']; + } + } + } + return compact('level_name', 'vip_price', 'price_type', 'level_price'); + } + + /** + * 计算商品优惠后金额、优惠价格 + * @param $price + * @param int $uid + * @param $userInfo + * @param $vipStatus + * @param int $discount + * @param float $vipPrice + * @param int $is_vip + * @param false $is_show + * @return array [优惠后的总金额,优惠金额] + */ + public function setLevelPrice($price, int $uid, $userInfo, $vipStatus, $discount = 0, $vipPrice = 0.00, $is_vip = 0, $is_show = false) + { + if (!(float)$price) return [(float)$price, (float)$price, '']; + if (!$vipStatus) $is_vip = 0; + //已登录 + if ($uid) { + if (!$userInfo) { + /** @var UserServices $user */ + $user = app()->make(UserServices::class); + $userInfo = $user->getUserCacheInfo($uid); + } + if ($discount === 0) { + /** @var SystemUserLevelServices $systemLevel */ + $systemLevel = app()->make(SystemUserLevelServices::class); + $discount = $systemLevel->getDiscount($uid, (int)$userInfo['level']); + } + } else { + //没登录 + $discount = 100; + } + $discount = bcdiv((string)$discount, '100', 2); + //执行减去会员优惠金额 + [$truePrice, $vip_truePrice, $type] = $this->isPayLevelPrice($uid, $userInfo, $vipStatus, $price, $discount, $vipPrice, $is_vip, $is_show); + //返回优惠后的总金额 + $truePrice = $truePrice < 0.01 ? 0.01 : $truePrice; + //优惠的金额 + $vip_truePrice = $vip_truePrice == $price ? bcsub((string)$vip_truePrice, '0.01', 2) : $vip_truePrice; + return [(float)$truePrice, (float)$vip_truePrice, $type]; + } + + /** + * 获取会员价格(付费会员价格和购买商品会员价格) + * @param int $uid + * @param $userInfo + * @param $vipStatus + * @param $price + * @param string $discount + * @param float $payVipPrice + * @param int $is_vip + * @param false $is_show + * @return array + */ + public function isPayLevelPrice(int $uid, $userInfo, $vipStatus, $price, string $discount, $payVipPrice = 0.00, $is_vip = 0, $is_show = false) + { + //is_vip == 0表示会员价格不启用,展示为零 + if ($is_vip == 0) $payVipPrice = 0; + if (!$userInfo && $uid) { + //检测用户是否是付费会员 + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $userInfo = $userService->getUserCacheInfo($uid); + } + $noPayVipPrice = ($discount && $discount != 0.00) ? bcmul((string)$discount, (string)$price, 2) : $price; + if ($payVipPrice < $noPayVipPrice && $payVipPrice > 0) { + $vipPrice = $payVipPrice; + $type = 'member'; + } else { + $vipPrice = $noPayVipPrice; + $type = 'level'; + } + //如果$isSingle==true 返回优惠后的总金额,否则返回优惠的金额 + if ($vipStatus && $is_vip == 1) { + //$is_show == false 是计算支付价格,true是展示 + if (!$is_show) { + return [$vipPrice, bcsub((string)$price, (string)$vipPrice, 2), $type]; + } else { + if ($userInfo && isset($userInfo['is_money_level']) && $userInfo['is_money_level'] > 0) { + return [$vipPrice, bcsub((string)$price, (string)$vipPrice, 2), $type]; + } else { + $type = 'level'; + return [$noPayVipPrice, bcsub((string)$price, (string)$noPayVipPrice, 2), $type]; + } + } + } else { + $type = 'level'; + return [(float)$noPayVipPrice, (float)bcsub((string)$price, (string)$noPayVipPrice, 2), $type]; + } + } + + /** + * 商品列表 + * @param array $where + * @param $limit + * @param $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductLimit(array $where, $limit, $field) + { + return $this->dao->getProductLimit($where, $limit, $field); + } + + /** + * 通过条件获取商品列表 + * @param $where + * @param $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductListByWhere($where, $field) + { + return $this->dao->getProductListByWhere($where, $field); + } + + /** + * 根据指定id获取商品列表 + * @param array $ids + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductColumn(array $ids, string $field = '') + { + $productData = []; + $productInfoField = 'id,image,price,ot_price,vip_price,postage,give_integral,sales,stock,store_name,unit_name,is_show,is_del,is_postage,cost,is_sub,temp_id'; + if (!empty($ids)) { + $productAll = $this->dao->idByProductList($ids, $field ?: $productInfoField); + if (!empty($productAll)) + $productData = array_combine(array_column($productAll, 'id'), $productAll); + } + return $productData; + } + + /** + * 商品是否存在 + * @param int $productId + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function isValidProduct(int $productId) + { + return $this->dao->getOne(['id' => $productId, 'is_del' => 0, 'is_show' => 1, 'is_verify' => 1]); + } + + /** + * 获取商品库存 + * @param int $productId + * @param string $uniqueId + * @return int|mixed + */ + public function getProductStock(int $productId, string $uniqueId = '') + { + /** @var StoreProductAttrValueServices $StoreProductAttrValue */ + $StoreProductAttrValue = app()->make(StoreProductAttrValueServices::class); + return $uniqueId == '' ? + $this->dao->value(['id' => $productId], 'stock') ?: 0 + : $StoreProductAttrValue->uniqueByStock($uniqueId); + } + + /** + * 下单、退款商品、规格库存变化清空缓存 + * @return void + */ + public function clearProductCache() + { + $this->dao->cacheTag()->clear(); + /** @var StoreProductAttrServices $storeProductAttrServices */ + $storeProductAttrServices = app()->make(StoreProductAttrServices::class); + $storeProductAttrServices->cacheTag()->clear(); + } + + /** + * 减库存,加销量 + * @param int $num + * @param int $productId + * @param string $unique + * @param int $store_id + * @return bool + */ + public function decProductStock(int $num, int $productId, string $unique = '', int $store_id = 0) + { + $res = true; + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + if ($store_id) { + /** @var StoreBranchProductServices $branchProductServices */ + $branchProductServices = app()->make(StoreBranchProductServices::class); + //查询门店商品 + $info = $branchProductServices->isValidStoreProduct($productId, $store_id); + $storeProductId = $info['id'] ?? 0; + if ($productId && $storeProductId != $productId) { + //原商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $productId, 'type' => 0], 'suk'); + //门店商品ID + $productId = $storeProductId; + //门店商品sku unique + $unique = $skuValueServices->value(['suk' => $suk, 'product_id' => $productId, 'type' => 0], 'unique'); + } + } + if ($unique) { + $res = $res && $skuValueServices->decProductAttrStock($productId, $unique, $num); + } + $res = $res && $this->dao->decStockIncSales(['id' => $productId], $num); + if ($res) { + $this->workSendStock($productId); + } + $this->clearProductCache(); + return $res; + } + + /** + * 减销量,加库存 + * @param int $num + * @param int $productId + * @param string $unique + * @param int $store_id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function incProductStock(int $num, int $productId, string $unique = '', int $store_id = 0) + { + $res = true; + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + if ($store_id) { + /** @var StoreBranchProductServices $branchProductServices */ + $branchProductServices = app()->make(StoreBranchProductServices::class); + //查询门店商品 + $info = $branchProductServices->isValidStoreProduct($productId, $store_id); + $storeProductId = $info['id'] ?? 0; + if ($productId && $storeProductId != $productId) { + //原商品sku + $suk = $skuValueServices->value(['unique' => $unique, 'product_id' => $productId, 'type' => 0], 'suk'); + //门店商品ID + $productId = $storeProductId; + //门店商品sku unique + $unique = $skuValueServices->value(['suk' => $suk, 'product_id' => $productId, 'type' => 0], 'unique'); + } + } + if ($unique) { + $res = $res && $skuValueServices->incProductAttrStock($productId, $unique, $num); + } + $res = $res && $this->dao->incStockDecSales(['id' => $productId], $num); + $this->clearProductCache(); + return $res; + } + + /** + * 库存预警发送消息 + * @param int $productId + */ + public function workSendStock(int $productId) + { + ProductStockTips::dispatch([$productId]); + } + + /** + * 获取推荐商品 + * @param int $uid + * @param array $where + * @param int $num + * @param string $type + * @return array|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @throws \throwable + */ + public function getRecommendProduct(int $uid, array $where = [], int $num = 0, string $type = 'mid') + { + [$page, $limit] = $this->getPageValue(); + $where['is_vip_product'] = 0; + $where['is_verify'] = 1; + $where['pid'] = 0; + $where['is_show'] = 1; + $where['is_del'] = 0; + if ($uid) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $is_vip = $userServices->value(['uid' => $uid], 'is_money_level'); + $where['is_vip_product'] = $is_vip ? -1 : 0; + } + $field = ['id', 'type', 'pid', 'image', 'store_name', 'store_info', 'cate_id', 'price', 'ot_price', 'IFNULL(sales,0) + IFNULL(ficti,0) as sales', 'unit_name', 'sort', 'activity', 'stock', 'vip_price', 'is_vip', 'video_link']; + $list = $this->dao->getRecommendProduct($where, $field, $num, $page, $limit, ['couponId']); + if ($list) { + $list = get_thumb_water($list, $type); + $list = $this->getActivityList($list); + $list = $this->getProductPromotions($list); + /** @var MemberCardServices $memberCardService */ + $memberCardService = app()->make(MemberCardServices::class); + $vipStatus = $memberCardService->isOpenMemberCardCache('vip_price', false) && sys_config('svip_price_status', 1);; + foreach ($list as &$item) { + if (!($vipStatus && $item['is_vip'])) { + $item['vip_price'] = 0; + } + } + } + return $list; + } + + /** + * 商品名称 图片 + * @param array $productIds + * @return array + */ + public function getProductArray(array $where, string $field, string $key) + { + return $this->dao->getColumn($where, $field, $key); + } + + /** + * 获取商品详情 + * @param int $productId + * @param string $field + * @param array $with + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductInfo(int $productId, string $field = '*', array $with = []) + { + return $this->dao->getOne(['is_del' => 0, 'is_show' => 1, 'id' => $productId], $field, $with); + } + + /** 生成商品复制口令关键字 + * @param int $productId + * @return string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductWords(int $productId) + { + $productInfo = $this->dao->getOne(['is_del' => 0, 'is_show' => 1, 'id' => $productId]); + $keyWords = ""; + if ($productInfo) { + $oneKey = "crmeb-fu致文本 Http:/ZБ"; + $twoKey = "Б轉移至☞" . sys_config('site_name') . "☜"; + $threeKey = "【" . $productInfo['store_name'] . "】"; + $mainKey = base64_encode($productId); + $keyWords = $oneKey . $mainKey . $twoKey . $threeKey; + } + return $keyWords; + } + + /** + * 通过商品id获取商品分类 + * @param array $productId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function productIdByProductCateName(array $productId) + { + $data = $this->dao->productIdByCateId($productId); + $cateData = []; + foreach ($data as $item) { + $cateData[$item['id']] = implode(',', array_map(function ($i) { + return $i['cate_name']; + }, $item['cateName'])); + } + return $cateData; + } + + /** + * 根据商品id获取品牌名称 + * @param $productId + * @return mixed + */ + public function productIdByBrandName($productId, $productInfo = []) + { + if ($productInfo) { + $brand_id = $productInfo['brand_id'] ?? []; + } else { + $storeInfo = $this->getCacheProductInfo($productId); + $brand_id = $storeInfo['brand_id'] ?? []; + } + + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $storeBrandInfo = $storeBrandServices->getCacheBrandInfo($brand_id); + + return $storeBrandInfo['brand_name'] ?? ''; + } + + /** + * 自动上下架 + * @return bool + */ + public function autoUpperShelves() + { + $this->dao->overUpperShelves(1); + $this->dao->overUpperShelves(0); + return true; + } + + /** + * 获取预售列表 + * @param int $uid + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getPresaleList(int $uid, array $where) + { + [$page, $limit] = $this->getPageValue(); + $data = $this->dao->getPresaleList($where, $page, $limit); + if ($data['list']) { + /** @var StoreProductCategoryServices $storeCategoryService */ + $storeCategoryService = app()->make(StoreProductCategoryServices::class); + /** @var StoreCouponIssueServices $couponServices */ + $couponServices = app()->make(StoreCouponIssueServices::class); + /** @var StoreBrandServices $storeBrandServices */ + $storeBrandServices = app()->make(StoreBrandServices::class); + $brands = $storeBrandServices->getColumn([], 'id,pid', 'id'); + /** @var SystemFormServices $systemFormServices */ + $systemFormServices = app()->make(SystemFormServices::class); + $systemForms = $systemFormServices->getColumn([['id', 'in', array_unique(array_column($data['list'], 'system_form_id'))], ['is_del', '=', 0]], 'id,value', 'id'); + foreach ($data['list'] as &$item) { + $item['sales'] = $item['sales'] + $item['ficti']; + $custom_form = $systemForms[$item['system_form_id']]['value'] ?? []; + $item['custom_form'] = is_string($custom_form) ? json_decode($custom_form, true) : $custom_form; + + $cateId = $item['cate_id']; + $cateId = explode(',', $cateId); + $cateId = array_merge($cateId, $storeCategoryService->cateIdByPid($cateId)); + $cateId = array_diff($cateId, [0]); + $brandId = []; + if ($item['brand_id']) { + $brandId = $brands[$item['brand_id']] ?? []; + } + $counpons = $couponServices->getIssueCouponListNew($uid, ['product_id' => $item['id'], 'cate_id' => $cateId, 'brand_id' => $brandId], 'id,coupon_title,coupon_price,use_min_price', 0, 1, 'coupon_price desc,sort desc,id desc'); + $item['coupon'] = $counpons[0] ?? []; + } + } + return $data; + } + + /** + * 判断配送方式 + * @param int $product_id + * @param int $type 商品类型 0平台 1门店 2供应商 + * @param int $relation_id 门店id + * @param array $delivery_type 配送方式 + * @return array + * + * @date 2022/09/09 + * @author yyw + */ + public function getDeliveryType(int $product_id, int $type, int $relation_id, array $delivery_type) + { + //门店总开关 + if (!sys_config('store_func_status', 1)) { + if (in_array('2', $delivery_type)) unset($delivery_type[array_search('2', $delivery_type)]); + if (in_array('3', $delivery_type)) unset($delivery_type[array_search('3', $delivery_type)]); + } else { + //获取总平台自提配置设置 + $store_self_mention = (bool)sys_config('store_self_mention'); + $store_mention = true; + //获取门店自提配置 + if ($type === 1 && $relation_id) { + /** @var SystemStoreServices $storeServices */ + $storeServices = app()->make(SystemStoreServices::class); + $storeInfo = $storeServices->cacheRemember($relation_id, function () use ($storeServices, $relation_id) { + $storeInfo = $storeServices->get(['id' => $relation_id, 'is_show' => 1, 'is_del' => 0]); + return $storeInfo ? $storeInfo->toArray() : null; + }); + $store_mention = ($storeInfo['is_store'] ?? 0) === 1; + if (in_array(1, $delivery_type)) {//门店商品 支持平台配送 验证平台该商品 + $info = $this->getCacheProductInfo($product_id); + if ($info && $info['type'] == 1) { + $platInfo = $this->getCacheProductInfo((int)$info['pid']); + if (!$platInfo || $platInfo['stock'] <= 0) { + unset($delivery_type[array_search('1', $delivery_type)]); + } + } + } + } + //判断当前商品配送方式 + if (!$store_self_mention || !$store_mention || !(in_array('2', $delivery_type))) { + if (in_array('2', $delivery_type)) unset($delivery_type[array_search('2', $delivery_type)]); + } + } +// Log::error(['$type'=>$type,'$relation_id'=>$relation_id,'$delivery_type'=>$delivery_type,'$store_self_mention'=>$store_self_mention,'$store_mention'=>$store_mention,'$delivery_type'=>$delivery_type]); + return array_merge($delivery_type); + } +} diff --git a/app/services/product/product/StoreProductStockRecordServices.php b/app/services/product/product/StoreProductStockRecordServices.php new file mode 100644 index 0000000..8ab32e0 --- /dev/null +++ b/app/services/product/product/StoreProductStockRecordServices.php @@ -0,0 +1,84 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\product; + + +use app\dao\product\product\StoreProductStockRecordDao; +use app\services\BaseServices; +use app\services\product\sku\StoreProductAttrValueServices; + +/** + * 商品库存记录 + * Class StoreProductStockRecordServices + * @package app\services\product\product + * @mixin StoreProductStockRecordDao + */ +class StoreProductStockRecordServices extends BaseServices +{ + /** + * StoreProductStockRecordServices constructor. + * @param StoreProductStockRecordDao $dao + */ + public function __construct(StoreProductStockRecordDao $dao) + { + $this->dao = $dao; + } + + + /** + * 保存入库、出库记录 + * @param int $id + * @param array $attrs + * @param int $type + * @param int $store_id + * @return bool + */ + public function saveRecord(int $id, array $attrs, int $type = 0, int $store_id = 0) + { + if (!$attrs) { + return false; + } + try { + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + //原来规格数据 + $attrValues = $storeProductAttrValueServices->getProductAttrValue(['product_id' => $id, 'type' => $type]); + if ($attrValues) $attrValues = array_combine(array_column($attrValues, 'unique'), $attrValues); + $time = time(); + $dataAll = $data = []; + foreach ($attrs as $attr) { + $data = [ + 'store_id' => $store_id, + 'product_id' => $id, + 'unique' => $attr['unique'], + 'cost_price' => $attrValues[$attr['unique']]['cost'] ?? 0, + 'add_time' => $time, + ]; + if (!isset($attrValues[$attr['unique']]) || $attr['stock'] > $attrValues[$attr['unique']]['stock']) { + $data['pm'] = 1; + $data['number'] = !isset($attrValues[$attr['unique']]) ? $attr['stock'] : bcsub((string)$attr['stock'], (string)$attrValues[$attr['unique']]['stock'], 2); + } else { + $data['pm'] = 0; + $data['number'] = !isset($attrValues[$attr['unique']]) ? $attr['stock'] : bcsub((string)$attrValues[$attr['unique']]['stock'], (string)$attr['stock'], 2); + } + if ($data['number']) $dataAll[] = $data; + } + if ($dataAll) { + $this->dao->saveAll($dataAll); + } + } catch (\Throwable $e) { + + } + return true; + } + +} diff --git a/app/services/product/product/StoreVisitServices.php b/app/services/product/product/StoreVisitServices.php new file mode 100644 index 0000000..4bda583 --- /dev/null +++ b/app/services/product/product/StoreVisitServices.php @@ -0,0 +1,78 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\product; + + +use app\dao\product\product\StoreVisitDao; +use app\services\BaseServices; + +/** + * Class StoreVisitService + * @package app\services\product\product + * @mixin StoreVisitDao + */ +class StoreVisitServices extends BaseServices +{ + public function __construct(StoreVisitDao $dao) + { + $this->dao = $dao; + } + + /** + * 设置浏览信息 + * @param $uid + * @param array $productIds + * @param int $cate + * @param string $type + * @param string $content + * @param int $min + */ + public function setView($uid, $productIds = [], $product_type = 'product', $cate = 0, $type = '', $content = '', $min = 20) + { + if (!$productIds) { + return true; + } + if (!is_array($productIds)) { + $productIds = [$productIds]; + } + $views = $this->dao->getColumn(['uid' => $uid, 'product_id' => $productIds, 'product_type' => $product_type], 'count,add_time,id', 'product_id'); + $cate = explode(',', $cate)[0]; + $dataAll = []; + $time = time(); + foreach ($productIds as $key => $product_id) { + if (isset($views[$product_id]) && $type != 'search') { + $view = $views[$product_id] ?? []; + if ($view && ($view['add_time'] + $min) < $time) { + $this->dao->update($view['id'], ['count' => $view['count'] + 1, 'add_time' => time()]); + } + } else { + $data = [ + 'add_time' => $time, + 'count' => 1, + 'product_id' => $product_id, + 'product_type' => $product_type, + 'cate_id' => $cate, + 'type' => $type, + 'uid' => $uid, + 'content' => $content + ]; + $dataAll[] = $data; + } + } + if ($dataAll) { + if (!$this->dao->saveAll($dataAll)) { + throw new ValidateException('添加失败'); + } + } + return true; + } +} diff --git a/app/services/product/shipping/ShippingTemplatesFreeServices.php b/app/services/product/shipping/ShippingTemplatesFreeServices.php new file mode 100644 index 0000000..cf7acdf --- /dev/null +++ b/app/services/product/shipping/ShippingTemplatesFreeServices.php @@ -0,0 +1,219 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\shipping; + + +use app\dao\product\shipping\ShippingTemplatesFreeDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use crmeb\utils\Arr; + +/** + * 包邮 + * Class ShippingTemplatesFreeServices + * @package app\services\product\shipping + * @mixin ShippingTemplatesFreeDao + */ +class ShippingTemplatesFreeServices extends BaseServices +{ + /** + * 构造方法 + * ShippingTemplatesFreeServices constructor. + * @param ShippingTemplatesFreeDao $dao + */ + public function __construct(ShippingTemplatesFreeDao $dao) + { + $this->dao = $dao; + } + + /** + * @param array $tempIds + * @param array $cityIds + * @param int $expire + * @return bool|mixed|null + */ + public function isFreeListCache(array $tempIds, array $cityIds, int $expire = 60) + { + return CacheService::redisHandler('apiShipping')->remember(md5('isFreeList' . json_encode([$tempIds, $cityIds])), function () use ($tempIds, $cityIds) { + return $this->dao->isFreeList($tempIds, $cityIds, 0, 'temp_id,number,price', 'temp_id'); + }, $expire); + } + + /** + * 添加包邮信息 + * @param array $appointInfo + * @param int $group + * @param int $tempId + * @return bool + * @throws \Exception + */ + public function saveFreeV1(array $appointInfo, int $group = 0, int $tempId = 0) + { + $res = true; + if ($tempId) { + if ($this->dao->count(['temp_id' => $tempId])) { + $res = $this->dao->delete($tempId, 'temp_id'); + } + } + $placeList = []; + mt_srand(); + foreach ($appointInfo as $item) { + $uniqid = uniqid('adminapi') . rand(1000, 9999); + foreach ($item['city_ids'] as $cityId) { + $placeList [] = [ + 'temp_id' => $tempId, + 'city_id' => $cityId[count($cityId) - 1], + 'value' => json_encode($cityId), + 'number' => $item['number'] ?? 0, + 'price' => $item['price'] ?? 0, + 'group' => $group, + 'uniqid' => $uniqid, + ]; + } + } + if (count($placeList)) { + return $res && $this->dao->saveAll($placeList); + } else { + return $res; + } + } + + /** + * 添加包邮信息 + * @param array $appointInfo + * @param int $group + * @param int $tempId + * @return bool + * @throws \Exception + */ + public function saveFree(array $appointInfo, int $group = 0, int $tempId = 0) + { + $res = true; + if ($tempId) { + if ($this->dao->count(['temp_id' => $tempId])) { + $res = $this->dao->delete($tempId, 'temp_id'); + } + } + $placeList = []; + mt_srand(); + foreach ($appointInfo as $item) { + if (isset($item['place']) && is_array($item['place'])) { + $uniqid = uniqid('adminapi') . rand(1000, 9999); + foreach ($item['place'] as $value) { + if (isset($value['children']) && is_array($value['children'])) { + foreach ($value['children'] as $vv) { + if (!isset($vv['city_id'])) { + throw new AdminException('缺少城市id无法保存'); + } + $placeList [] = [ + 'temp_id' => $tempId, + 'province_id' => $value['city_id'] ?? 0, + 'city_id' => $vv['city_id'] ?? 0, + 'number' => $item['a_num'] ?? 0, + 'price' => $item['a_price'] ?? 0, + 'group' => $group, + 'uniqid' => $uniqid, + ]; + } + } + } + } + } + if (count($placeList)) { + return $res && $this->dao->saveAll($placeList); + } else { + return $res; + } + } + + /** + * @param int $tempId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFreeListV1(int $tempId) + { + $freeList = $this->dao->getShippingList(['temp_id' => $tempId]); + return Arr::formatShipping($freeList); + } + + /** + * 获得指定包邮城市地址 + * @param int $tempId + * @return array + */ + public function getFreeList(int $tempId) + { + $freeIdList = $this->dao->getShippingGroupArray(['temp_id' => $tempId], 'uniqid', 'uniqid', ''); + $freeData = []; + $infos = $this->dao->getShippingArray(['uniqid' => $freeIdList, 'temp_id' => $tempId], '*', 'uniqid'); + foreach ($freeIdList as $uniqid) { + $info = $infos[$uniqid]; + $freeData[] = [ + 'place' => $this->getFreeTemp($uniqid, $info['province_id']), + 'a_num' => $info['number'] ? floatval($info['number']) : 0, + 'a_price' => $info['price'] ? floatval($info['price']) : 0, + ]; + } + foreach ($freeData as &$item) { + $item['placeName'] = implode(';', array_column($item['place'], 'name')); + } + return $freeData; + } + + /** + * 获取包邮的省份 + * @param string $uniqid + * @param int $provinceId + * @return array + */ + public function getFreeTemp(string $uniqid, int $provinceId) + { + /** @var ShippingTemplatesFreeCityServices $service */ + $service = app()->make(ShippingTemplatesFreeCityServices::class); + $infoList = $service->getUniqidList(['uniqid' => $uniqid]); + $childrenData = []; + foreach ($infoList as $item) { + $childrenData[] = [ + 'city_id' => $item['province_id'], + 'name' => $item['name'] ?? '全国', + 'children' => $this->getCityTemp($uniqid, $provinceId) + ]; + } + return $childrenData; + } + + /** + * 获取市区数据 + * @param string $uniqid + * @param int $provinceId + * @return array + */ + public function getCityTemp(string $uniqid, int $provinceId) + { + /** @var ShippingTemplatesFreeCityServices $service */ + $service = app()->make(ShippingTemplatesFreeCityServices::class); + $infoList = $service->getUniqidList(['uniqid' => $uniqid, 'province_id' => $provinceId], false); + $childrenData = []; + foreach ($infoList as $item) { + $childrenData[] = [ + 'city_id' => $item['city_id'], + 'name' => $item['name'] ?? '全国', + ]; + } + return $childrenData; + } + +} diff --git a/app/services/product/shipping/ShippingTemplatesNoDeliveryServices.php b/app/services/product/shipping/ShippingTemplatesNoDeliveryServices.php new file mode 100644 index 0000000..abf980c --- /dev/null +++ b/app/services/product/shipping/ShippingTemplatesNoDeliveryServices.php @@ -0,0 +1,194 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\shipping; + + +use app\dao\product\shipping\ShippingTemplatesNoDeliveryDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; +use crmeb\utils\Arr; + +/** + * 不送达 + * Class ShippingTemplatesNoDeliveryServices + * @package app\services\product\shipping + * @mixin ShippingTemplatesNoDeliveryDao + */ +class ShippingTemplatesNoDeliveryServices extends BaseServices +{ + /** + * 构造方法 + * ShippingTemplatesNoDeliveryServices constructor. + * @param ShippingTemplatesNoDeliveryDao $dao + */ + public function __construct(ShippingTemplatesNoDeliveryDao $dao) + { + $this->dao = $dao; + } + + + /** + * 添加不送达信息 + * @param array $noDeliveryInfo + * @param int $tempId + * @return bool|mixed + */ + public function saveNoDeliveryV1(array $noDeliveryInfo, int $tempId = 0) + { + $res = true; + if ($tempId) { + if ($this->dao->count(['temp_id' => $tempId])) { + $res = $this->dao->delete($tempId, 'temp_id'); + } + } + $placeList = []; + mt_srand(); + foreach ($noDeliveryInfo as $item) { + $uniqid = uniqid('adminapi') . rand(1000, 9999); + foreach ($item['city_ids'] as $cityId) { + $placeList [] = [ + 'temp_id' => $tempId, + 'city_id' => $cityId[count($cityId) - 1], + 'value' => json_encode($cityId), + 'uniqid' => $uniqid, + ]; + } + } + if (count($placeList)) { + return $res && $this->dao->saveAll($placeList); + } else { + return $res; + } + } + + /** + * 添加不送达信息 + * @param array $noDeliveryInfo + * @param int $tempId + * @return bool|mixed + */ + public function saveNoDelivery(array $noDeliveryInfo, int $tempId = 0) + { + $res = true; + if ($tempId) { + if ($this->dao->count(['temp_id' => $tempId])) { + $res = $this->dao->delete($tempId, 'temp_id'); + } + } + $placeList = []; + mt_srand(); + foreach ($noDeliveryInfo as $item) { + if (isset($item['place']) && is_array($item['place'])) { + $uniqid = uniqid('adminapi') . rand(1000, 9999); + foreach ($item['place'] as $value) { + if (isset($value['children']) && is_array($value['children'])) { + foreach ($value['children'] as $vv) { + if (!isset($vv['city_id'])) { + throw new AdminException('缺少城市id无法保存'); + } + $placeList [] = [ + 'temp_id' => $tempId, + 'province_id' => $value['city_id'] ?? 0, + 'city_id' => $vv['city_id'] ?? 0, + 'uniqid' => $uniqid, + ]; + } + } + } + } + } + if (count($placeList)) { + return $res && $this->dao->saveAll($placeList); + } else { + return $res; + } + } + + /** + * @param int $tempId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getNoDeliveryListV1(int $tempId) + { + $freeList = $this->dao->getShippingList(['temp_id' => $tempId]); + return Arr::formatShipping($freeList); + } + + /** + * 获得指定包邮城市地址 + * @param int $tempId + * @return array + */ + public function getNoDeliveryList(int $tempId) + { + $freeIdList = $this->dao->getShippingGroupArray(['temp_id' => $tempId], 'uniqid', 'uniqid', ''); + $freeData = []; + $infos = $this->dao->getShippingArray(['uniqid' => $freeIdList, 'temp_id' => $tempId], '*', 'uniqid'); + foreach ($freeIdList as $uniqid) { + $info = $infos[$uniqid]; + $freeData[] = [ + 'place' => $this->getNoDeliveryTemp($uniqid, $info['province_id']), + ]; + } + foreach ($freeData as &$item) { + $item['placeName'] = implode(';', array_column($item['place'], 'name')); + } + return $freeData; + } + + /** + * 获取不送达的省份 + * @param string $uniqid + * @param int $provinceId + * @return array + */ + public function getNoDeliveryTemp(string $uniqid, int $provinceId) + { + /** @var ShippingTemplatesNoDeliveryCityServices $service */ + $service = app()->make(ShippingTemplatesNoDeliveryCityServices::class); + $infoList = $service->getUniqidList(['uniqid' => $uniqid]); + $childrenData = []; + foreach ($infoList as $item) { + $childrenData[] = [ + 'city_id' => $item['province_id'], + 'name' => $item['name'] ?? '全国', + 'children' => $this->getCityTemp($uniqid, $provinceId) + ]; + } + return $childrenData; + } + + /** + * 获取市区数据 + * @param string $uniqid + * @param int $provinceId + * @return array + */ + public function getCityTemp(string $uniqid, int $provinceId) + { + /** @var ShippingTemplatesNoDeliveryCityServices $service */ + $service = app()->make(ShippingTemplatesNoDeliveryCityServices::class); + $infoList = $service->getUniqidList(['uniqid' => $uniqid, 'province_id' => $provinceId], false); + $childrenData = []; + foreach ($infoList as $item) { + $childrenData[] = [ + 'city_id' => $item['city_id'], + 'name' => $item['name'] ?? '全国', + ]; + } + return $childrenData; + } + +} diff --git a/app/services/product/shipping/ShippingTemplatesRegionServices.php b/app/services/product/shipping/ShippingTemplatesRegionServices.php new file mode 100644 index 0000000..24aae06 --- /dev/null +++ b/app/services/product/shipping/ShippingTemplatesRegionServices.php @@ -0,0 +1,256 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\shipping; + + +use app\dao\product\shipping\ShippingTemplatesRegionDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use crmeb\utils\Arr; + +/** + * 指定邮费 + * Class ShippingTemplatesRegionServices + * @package app\services\product\shipping + * @mixin ShippingTemplatesRegionDao + */ +class ShippingTemplatesRegionServices extends BaseServices +{ + /** + * 构造方法 + * ShippingTemplatesRegionServices constructor. + * @param ShippingTemplatesRegionDao $dao + */ + public function __construct(ShippingTemplatesRegionDao $dao) + { + $this->dao = $dao; + } + + /** + * @param array $tempIds + * @param array $cityIds + * @param int $expire + * @return bool|mixed|null + */ + public function getTempRegionListCache(array $tempIds, array $cityIds, int $expire = 60) + { + return CacheService::redisHandler('apiShipping')->remember(md5('RegionList' . json_encode([$tempIds, $cityIds])), function () use ($tempIds, $cityIds) { + return $this->dao->getTempRegionList($tempIds, $cityIds, 'temp_id,first,first_price,continue,continue_price', 'temp_id'); + }, $expire); + } + + /** + * 添加运费信息 + * @param array $regionInfo + * @param int $group + * @param int $tempId + * @return bool + * @throws \Exception + */ + public function saveRegionV1(array $regionInfo, int $group = 0, $tempId = 0) + { + $res = true; + if ($tempId) { + if ($this->dao->count(['temp_id' => $tempId])) { + $res = $this->dao->delete($tempId, 'temp_id'); + } + } + $regionList = []; + mt_srand(); + foreach ($regionInfo as $item) { + $uniqid = uniqid('adminapi') . rand(1000, 9999); + if (isset($item['city_ids']) && $item['city_ids']) { + foreach ($item['city_ids'] as $value) { + $regionList[] = [ + 'temp_id' => $tempId, + 'city_id' => $value ? $value[count($value) - 1] : 0, + 'value' => json_encode($value), + 'first' => $item['first'] ?? 0, + 'first_price' => $item['first_price'] ?? 0, + 'continue' => $item['continue'] ?? 0, + 'continue_price' => $item['continue_price'] ?? 0, + 'group' => $group, + 'uniqid' => $uniqid, + ]; + } + } + } + return $res && $this->dao->saveAll($regionList); + } + + /** + * 添加运费信息 + * @param array $regionInfo + * @param int $group + * @param int $tempId + * @return bool + * @throws \Exception + */ + public function saveRegion(array $regionInfo, int $group = 0, $tempId = 0) + { + $res = true; + if ($tempId) { + if ($this->dao->count(['temp_id' => $tempId])) { + $res = $this->dao->delete($tempId, 'temp_id'); + } + } + $regionList = []; + mt_srand(); + foreach ($regionInfo as $item) { + if (isset($item['region']) && is_array($item['region'])) { + $uniqid = uniqid('adminapi') . rand(1000, 9999); + foreach ($item['region'] as $value) { + if (isset($value['children']) && is_array($value['children'])) { + foreach ($value['children'] as $vv) { + if (!isset($vv['city_id'])) { + throw new AdminException('缺少城市id无法保存'); + } + $regionList[] = [ + 'temp_id' => $tempId, + 'province_id' => $value['city_id'] ?? 0, + 'city_id' => $vv['city_id'] ?? 0, + 'first' => $item['first'] ?? 0, + 'first_price' => $item['price'] ?? 0, + 'continue' => $item['continue'] ?? 0, + 'continue_price' => $item['continue_price'] ?? 0, + 'group' => $group, + 'uniqid' => $uniqid, + ]; + } + } else { + $regionList[0] = [ + 'temp_id' => $tempId, + 'province_id' => 0, + 'city_id' => 0, + 'first' => $item['first'] ?? 0, + 'first_price' => $item['price'] ?? 0, + 'continue' => $item['continue'] ?? 0, + 'continue_price' => $item['continue_price'] ?? 0, + 'group' => $group, + 'uniqid' => $uniqid, + ]; + } + } + } + } + return $res && $this->dao->saveAll($regionList); + } + + /** + * @param int $tempId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRegionListV1(int $tempId) + { + $freeList = $this->dao->getShippingList(['temp_id' => $tempId]); + return Arr::formatShipping($freeList); + } + + /** + * 获取某个运费模板下的城市数据 + * @param int $tempId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getRegionList(int $tempId) + { + $regionList = $this->dao->getShippingGroupArray(['temp_id' => $tempId], 'uniqid', 'uniqid', ''); + $regionData = []; + $infos = $this->dao->getShippingArray(['uniqid' => $regionList, 'temp_id' => $tempId], '*', 'uniqid'); + foreach ($regionList as $uniqid) { + $info = $infos[$uniqid]; + if ($info['province_id'] == 0) { + $regionData[] = [ + 'region' => [ + 'city_id' => 0, + 'name' => '默认全国', + ], + 'regionName' => '默认全国', + 'first' => $info['first'] ? floatval($info['first']) : 0, + 'price' => $info['first_price'] ? floatval($info['first_price']) : 0, + 'continue' => $info['continue'] ? floatval($info['continue']) : 0, + 'continue_price' => $info['continue_price'] ? floatval($info['continue_price']) : 0, + 'uniqid' => $info['uniqid'], + ]; + } else { + $regionData[] = [ + 'region' => $this->getRegionTemp($uniqid, $info['province_id']), + 'regionName' => '', + 'first' => $info['first'] ? floatval($info['first']) : 0, + 'price' => $info['first_price'] ? floatval($info['first_price']) : 0, + 'continue' => $info['continue'] ? floatval($info['continue']) : 0, + 'continue_price' => $info['continue_price'] ? floatval($info['continue_price']) : 0, + 'uniqid' => $info['uniqid'], + ]; + } + } + + foreach ($regionData as &$item) { + if (!$item['regionName']) { + $item['regionName'] = implode(';', array_map(function ($val) { + return $val['name']; + }, $item['region'])); + } + } + + return $regionData; + } + + /** + * 获取省份下运费模板 + * @param string $uniqid + * @param int $provinceId + * @return array + */ + public function getRegionTemp(string $uniqid, int $provinceId) + { + /** @var ShippingTemplatesRegionCityServices $services */ + $services = app()->make(ShippingTemplatesRegionCityServices::class); + $infoList = $services->getUniqidList(['uniqid' => $uniqid]); + $childrenData = []; + foreach ($infoList as $item) { + $childrenData[] = [ + 'city_id' => $item['province_id'], + 'name' => $item['name'] ?? '全国', + 'children' => $this->getCityTemp($uniqid, $item['province_id']) + ]; + } + return $childrenData; + } + + /** + * 获取市区下的数据 + * @param string $uniqid + * @param int $provinceId + * @return array + */ + public function getCityTemp(string $uniqid, int $provinceId) + { + /** @var ShippingTemplatesRegionCityServices $services */ + $services = app()->make(ShippingTemplatesRegionCityServices::class); + $infoList = $services->getUniqidList(['uniqid' => $uniqid, 'province_id' => $provinceId], false); + $childrenData = []; + foreach ($infoList as $item) { + $childrenData[] = [ + 'city_id' => $item['city_id'], + 'name' => $item['name'] ?? '全国', + ]; + } + return $childrenData; + } +} diff --git a/app/services/product/shipping/ShippingTemplatesServices.php b/app/services/product/shipping/ShippingTemplatesServices.php new file mode 100644 index 0000000..0245569 --- /dev/null +++ b/app/services/product/shipping/ShippingTemplatesServices.php @@ -0,0 +1,228 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\product\shipping; + +use app\services\BaseServices; +use app\dao\product\shipping\ShippingTemplatesDao; +use app\services\product\product\StoreProductServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; + +/** + * 运费模板 + * Class ShippingTemplatesServices + * @package app\services\product\shipping + * @mixin ShippingTemplatesDao + */ +class ShippingTemplatesServices extends BaseServices +{ + + /** + * 计费类型 + * @var string[] + */ + protected $group = [ + 1 => '按件数', + 2 => '按重量', + 3 => '按体积' + ]; + + /** + * 构造方法 + * ShippingTemplatesServices constructor. + * @param ShippingTemplatesDao $dao + */ + public function __construct(ShippingTemplatesDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取运费模板列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getShippingList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $data = $this->dao->getShippingList($where, $page, $limit); + if ($data) { + $groupName = $this->group; + foreach ($data as &$item) { + $item['type'] = $groupName[$item['group']] ?? ''; + } + } + $count = $this->dao->count($where); + return compact('data', 'count'); + } + + /** + * @param array $where + * @param $field + * @param string|null $key + * @param int $exprie + * @return bool|mixed|null + */ + public function getShippingColumnCache(array $where, $field, string $key = null, int $exprie = 60) + { + return CacheService::redisHandler('apiShipping')->remember(md5('Shipping_column' . json_encode($where)), function () use ($where, $field, $key) { + return $this->dao->getShippingColumn($where, $field, $key); + }, $exprie); + } + + /** + * 获取需要修改的运费模板 + * @param int $id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getShippingV1(int $id) + { + $templates = $this->dao->get($id); + if (!$templates) { + throw new AdminException('修改的模板不存在'); + } + /** @var ShippingTemplatesFreeServices $freeServices */ + $freeServices = app()->make(ShippingTemplatesFreeServices::class); + /** @var ShippingTemplatesRegionServices $regionServices */ + $regionServices = app()->make(ShippingTemplatesRegionServices::class); + /** @var ShippingTemplatesNoDeliveryServices $noDeliveryServices */ + $noDeliveryServices = app()->make(ShippingTemplatesNoDeliveryServices::class); + $data['appointList'] = $freeServices->getFreeListV1($id); + $data['templateList'] = $regionServices->getRegionList($id); + $data['noDeliveryList'] = $noDeliveryServices->getNoDeliveryList($id); + if (!isset($data['templateList'][0]['region'])) { + $data['templateList'][0]['region'] = ['city_id' => 0, 'name' => '默认全国']; + } + $data['formData'] = [ + 'name' => $templates->name, + 'type' => $templates->getData('type'), + 'appoint_check' => intval($templates->getData('appoint')), + 'no_delivery_check' => intval($templates->getData('no_delivery')), + 'sort' => intval($templates->getData('sort')), + ]; + return $data; + } + + /** + * 获取需要修改的运费模板 + * @param int $id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getShipping(int $id) + { + $templates = $this->dao->get($id); + if (!$templates) { + throw new AdminException('修改的模板不存在'); + } + /** @var ShippingTemplatesFreeServices $freeServices */ + $freeServices = app()->make(ShippingTemplatesFreeServices::class); + /** @var ShippingTemplatesRegionServices $regionServices */ + $regionServices = app()->make(ShippingTemplatesRegionServices::class); + /** @var ShippingTemplatesNoDeliveryServices $noDeliveryServices */ + $noDeliveryServices = app()->make(ShippingTemplatesNoDeliveryServices::class); + $data['appointList'] = $freeServices->getFreeListV1($id); + $data['templateList'] = $regionServices->getRegionListV1($id); + $data['noDeliveryList'] = $noDeliveryServices->getNoDeliveryListV1($id); + if (!isset($data['templateList'][0]['city_ids'])) { + $data['templateList'][0] = ['city_ids' => [[0]], 'regionName' => '默认全国']; + } + $data['formData'] = [ + 'name' => $templates->name, + 'type' => $templates->getData('group'), + 'appoint_check' => intval($templates->getData('appoint')), + 'no_delivery_check' => intval($templates->getData('no_delivery')), + 'sort' => intval($templates->getData('sort')), + ]; + return $data; + } + + /** + * 保存或者修改运费模板 + * @param int $id + * @param array $temp + * @param array $data + * @return mixed + */ + public function save(int $id, array $temp, array $data) + { + + /** @var ShippingTemplatesRegionServices $regionServices */ + $regionServices = app()->make(ShippingTemplatesRegionServices::class); + $this->transaction(function () use ($regionServices, $data, $id, $temp) { + + if ($id) { + $res = $this->dao->update($id, $temp); + } else { + $res = $this->dao->save($temp); + } + if (!$res) { + throw new AdminException('保存失败'); + } + if (!$id) $id = $res->id; + + //设置区域配送 + if (!$regionServices->saveRegionV1($data['region_info'], (int)$data['group'], (int)$id)) { + throw new AdminException('指定区域邮费添加失败!'); + } + //设置指定包邮 + if ($data['appoint']) { + /** @var ShippingTemplatesFreeServices $freeServices */ + $freeServices = app()->make(ShippingTemplatesFreeServices::class); + if (!$freeServices->saveFreeV1($data['appoint_info'], (int)$data['group'], (int)$id)) { + throw new AdminException('指定包邮添加失败!'); + } + } + //设置不送达 + if ($data['no_delivery']) { + /** @var ShippingTemplatesNoDeliveryServices $noDeliveryServices */ + $noDeliveryServices = app()->make(ShippingTemplatesNoDeliveryServices::class); + if (!$noDeliveryServices->saveNoDeliveryV1($data['no_delivery_info'], (int)$id)) { + throw new AdminException('指定不送达添加失败!'); + } + } + }); + return true; + } + + /** + * 删除运费模板 + * @param int $id + */ + public function detete(int $id) + { + $this->dao->delete($id); + /** @var ShippingTemplatesFreeServices $freeServices */ + $freeServices = app()->make(ShippingTemplatesFreeServices::class); + /** @var ShippingTemplatesRegionServices $regionServices */ + $regionServices = app()->make(ShippingTemplatesRegionServices::class); + /** @var ShippingTemplatesNoDeliveryServices $noDeliveryServices */ + $noDeliveryServices = app()->make(ShippingTemplatesNoDeliveryServices::class); + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + $this->transaction(function () use ($id, $freeServices, $regionServices, $noDeliveryServices, $storeProductServices) { + $freeServices->delete($id, 'temp_id'); + $regionServices->delete($id, 'temp_id'); + $noDeliveryServices->delete($id, 'temp_id'); + $storeProductServices->update(['temp_id' => $id], ['temp_id' => 1]); + }); + } +} diff --git a/app/services/product/sku/StoreProductAttrResultServices.php b/app/services/product/sku/StoreProductAttrResultServices.php new file mode 100644 index 0000000..aa0902b --- /dev/null +++ b/app/services/product/sku/StoreProductAttrResultServices.php @@ -0,0 +1,77 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\sku; + + +use app\dao\product\sku\StoreProductAttrResultDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; + +/** + * Class StoreProductAttrResultService + * @package app\services\product\sku + * @mixin StoreProductAttrResultDao + */ +class StoreProductAttrResultServices extends BaseServices +{ + /** + * StoreProductAttrResultServices constructor. + * @param StoreProductAttrResultDao $dao + */ + public function __construct(StoreProductAttrResultDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取属性规格 + * @param array $where + * @return mixed + */ + public function getResult(array $where) + { + return json_decode($this->dao->value($where, 'result'), true); + } + + /** + * 删除属性 + * @param int $id + * @param int $type + * @return bool + */ + public function del(int $id, int $type) + { + return $this->dao->del($id, $type); + } + + /** + * 设置属性 + * @param array $data + * @param int $id + * @param int $type + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setResult(array $data, int $id, int $type) + { + $result = $this->dao->get(['product_id' => $id, 'type' => $type]); + if ($result) { + $res = $this->dao->update($result['id'], ['result' => json_encode($data), 'change_time' => time()]); + } else { + $res = $this->dao->save(['product_id' => $id, 'result' => json_encode($data), 'change_time' => time(), 'type' => $type]); + } + if (!$res) throw new AdminException('规格保存失败'); + return true; + } +} diff --git a/app/services/product/sku/StoreProductAttrServices.php b/app/services/product/sku/StoreProductAttrServices.php new file mode 100644 index 0000000..eeeb57c --- /dev/null +++ b/app/services/product/sku/StoreProductAttrServices.php @@ -0,0 +1,473 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\sku; + + +use app\dao\product\sku\StoreProductAttrDao; +use app\services\BaseServices; +use app\services\order\StoreCartServices; +use app\services\product\product\StoreProductServices; +use crmeb\exceptions\AdminException; +use crmeb\traits\OptionTrait; + +/** + * Class StoreProductAttrService + * @package app\services\product\sku + * @mixin StoreProductAttrDao + */ +class StoreProductAttrServices extends BaseServices +{ + + use OptionTrait; + + /** + * StoreProductAttrServices constructor. + * @param StoreProductAttrDao $dao + */ + public function __construct(StoreProductAttrDao $dao) + { + $this->dao = $dao; + } + + /** + * 生成规格唯一值 + * @param int $id + * @param string $sku + * @return false|string + */ + public function createAttrUnique(int $id, string $sku) + { + return substr(md5($id . $sku . uniqid(true)), 12, 8); + } + + /** + * 根据根据前端规格数据获取sku + * @param array $value + * @param string + */ + public function getSku(array $value) + { + $sku = ''; + if ($value) { + $detail = []; + $count = count($value['detail'] ?? []); + for ($i=1; $i<=$count ; $i++) { + $detail[] = trim($value['value' . $i]); + } + $sku = implode(',', $detail); + } + return $sku; + } + + /** + * 添加商品属性数据判断 + * @param array $attrList + * @param array $valueList + * @param int $productId + * @param int $type + * @param int $is_vip + * @param int $validate + * @return array + */ + public function validateProductAttr(array $attrList, array $valueList, int $productId, int $type = 0, int $is_vip = 0, int $validate = 1) + { + $result = ['attr' => $attrList, 'value' => $valueList]; + $attrValueList = []; + $attrNameList = []; + foreach ($attrList as $index => $attr) { + if (!isset($attr['value'])) { + throw new AdminException('请输入规则名称!'); + } + $attr['value'] = trim($attr['value']); + if (!isset($attr['value'])) { + throw new AdminException('请输入规则名称!!'); + } + if (!isset($attr['detail']) || !count($attr['detail'])) { + throw new AdminException('请输入属性名称!'); + } + foreach ($attr['detail'] as $k => $attrValue) { + $attrValue = trim($attrValue); + if (empty($attrValue)) { + throw new AdminException('请输入正确的属性'); + } + $attr['detail'][$k] = $attrValue; + $attrValueList[] = $attrValue; + $attr['detail'][$k] = $attrValue; + } + $attrNameList[] = $attr['value']; + $attrList[$index] = $attr; + } + $attrCount = count($attrList); + foreach ($valueList as $index => $value) { + if (!isset($value['detail']) || count($value['detail']) != $attrCount) { + throw new AdminException('请填写正确的商品信息'); + } + if (!isset($value['price']) || !is_numeric($value['price']) || floatval($value['price']) != $value['price']) { + throw new AdminException('请填写正确的商品价格'); + } + if ($type == 4) { + if (!isset($value['integral']) || !is_numeric($value['integral']) || floatval($value['integral']) != $value['integral']) { + throw new AdminException('请填写正确的商品兑换积分'); + } + if (isset($value['price']) && $value['price'] <= 0 && isset($value['integral']) && $value['integral'] <= 0) { + throw new AdminException('积分商品兑换积分和价格不能同时为空'); + } + } + if (!isset($value['stock']) || !is_numeric($value['stock']) || intval($value['stock']) != $value['stock']) { + throw new AdminException('请填写正确的商品库存'); + } + //供应商结算价 不用成本价 + if (!($value['settle_price'] ?? 0) && (!isset($value['cost']) || !is_numeric($value['cost']) || floatval($value['cost']) != $value['cost'])) { + throw new AdminException('请填写正确的商品成本价格'); + } + if ($validate && (!isset($value['pic']) || empty($value['pic']))) { + throw new AdminException('请上传商品规格图片'); + } + if ($is_vip && (!isset($value['vip_price']) || !$value['vip_price'])) { + throw new AdminException('会员价格不能为0'); + } + foreach ($value['detail'] as $attrName => $attrValue) { + //如果attrName 存在空格 则这个规格key 会出现两次 + unset($valueList[$index]['detail'][$attrName]); + $attrName = trim($attrName); + $attrValue = trim($attrValue); + if (!in_array($attrName, $attrNameList, true)) { + throw new AdminException($attrName . '规则不存在'); + } + if (!in_array($attrValue, $attrValueList, true)) { + throw new AdminException($attrName . '属性不存在'); + } + if (empty($attrName)) { + throw new AdminException('请输入正确的属性'); + } + $valueList[$index]['detail'][$attrName] = $attrValue; + } + } + $attrGroup = []; + $valueGroup = []; + foreach ($attrList as $k => $value) { + $attrGroup[] = [ + 'product_id' => $productId, + 'attr_name' => $value['value'], + 'attr_values' => $value['detail'], + 'type' => $type + ]; + } + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + $skuArray = $storeProductAttrValueServices->getSkuArray(['product_id' => $productId, 'type' => $type], 'unique', 'suk'); + foreach ($valueList as $k => $value) { +// sort($value['detail'], SORT_STRING); + $sku = implode(',', $value['detail']); + $valueGroup[$sku] = [ + 'product_id' => $productId, + 'suk' => $this->getSku($value), + 'price' => $value['price'], + 'integral' => isset($value['integral']) ? $value['integral'] : 0, + 'settle_price' => $value['settle_price'] ?? 0.00,//供应商结算价 + 'cost' => ($value['settle_price'] ?? 0.00) ?: $value['cost'],//供应端:去除成本价 + 'ot_price' => $value['ot_price'], + 'stock' => $value['stock'], + 'unique' => $skuArray[$sku] ?? $this->createAttrUnique($productId, $sku), + 'image' => $value['pic'], + 'bar_code' => $value['bar_code'] ?? '', + 'weight' => $value['weight'] ?? 0, + 'volume' => $value['volume'] ?? 0, + 'brokerage' => $value['brokerage'] ?? 0, + 'brokerage_two' => $value['brokerage_two'] ?? 0, + 'type' => $type, + 'quota' => $value['quota'] ?? 0, + 'quota_show' => $value['quota'] ?? 0, + 'vip_price' => $value['vip_price'] ?? 0, + 'code' => $value['code'] ?? '', + 'product_type' => $value['product_type'] ?? 0, + 'virtual_list' => $value['virtual_list'] ?? [], + 'disk_info' => $value['disk_info'] ?? '', + 'write_times' => $value['write_times'] ?? 1, + 'write_valid' => $value['write_valid'] ?? 1, + 'write_days' => $value['write_days'] ?? $value['days'] ?? 0, + 'write_start' => ($value['section_time'][0] ?? '') ? strtotime($value['section_time'][0] ?? '') : 0, + 'write_end' => ($value['section_time'][1] ?? '') ? strtotime($value['section_time'][1] ?? '') : 0, + ]; + } + if (!count($attrGroup) || !count($valueGroup)) { + throw new AdminException('请设置至少一个属性!'); + } + return compact('result', 'attrGroup', 'valueGroup'); + } + + /** + * 保存商品规格 + * @param array $data + * @param int $id + * @param int $type + * @return bool|mixed|\think\Collection + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveProductAttr(array $data, int $id, int $type = 0) + { + $this->setAttr($data['attrGroup'], $id, $type); + /** @var StoreProductAttrResultServices $storeProductAttrResultServices */ + $storeProductAttrResultServices = app()->make(StoreProductAttrResultServices::class); + $storeProductAttrResultServices->setResult($data['result'], $id, $type); + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + + $valueGroup = $data['valueGroup'] ?? []; + $updateSuks = array_column($valueGroup, 'suk'); + $oldSuks = []; + $oldAttrValue = $storeProductAttrValueServices->getSkuArray(['product_id' => $id, 'type' => $type], '*', 'suk'); + if ($oldAttrValue) $oldSuks = array_column($oldAttrValue, 'suk'); + $delSuks = array_merge(array_diff($oldSuks, $updateSuks)); + $dataAll = []; + $res1 = $res2 = $res3 = true; + foreach ($valueGroup as $item) { + if ($oldSuks && in_array($item['suk'], $oldSuks) && isset($oldAttrValue[$item['suk']])) { + $attrId = $oldAttrValue[$item['suk']]['id']; + unset($item['suk'], $item['unique']); + $item['virtual_list'] =json_encode($item['virtual_list']); + $res1 = $res1 && $storeProductAttrValueServices->update($attrId, $item); + } else { + $dataAll[] = $item; + } + } + if ($delSuks) { + $res2 = $storeProductAttrValueServices->del($id, $type, $delSuks); + } + if ($dataAll) { + $res3 = $storeProductAttrValueServices->saveAll($dataAll); + } + if ($res1 && $res2 && $res3) { +// $unique = array_column($valueGroup, 'unique'); +// $storeProductAttrValueServices->updateSumStock($unique ?? []); + return $valueGroup; + } else { + throw new AdminException('商品规格信息保存失败'); + } + + } + + + /** + * 获取商品规格 + * @param array $where + * @return array + */ + public function getProductAttr(array $where) + { + return $this->dao->getProductAttr($where); + } + + /** + * 获取商品规格详情 + * @param int $id + * @param int $uid + * @param int $cartNum //是否查询购物车数量 + * @param int $type //活动类型 attr_value表 + * @param int $productId + * @param array $productInfo + * @param int $discount //限时折扣 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductAttrDetailCache(int $id, int $uid, int $cartNum = 0, int $type = 0, int $productId = 0, array $productInfo = [], int $discount = -1) + { + $attrDetail = $this->dao->cacheTag()->remember('attr_' . $id . '_' . $type . '_' . $productId, function () use ($productId, $id, $type) { + $attrDetail = $this->dao->getProductAttr(['product_id' => $id, 'type' => $type]); + if (!$attrDetail && $type > 0 && $productId) {//活动商品未获取到规格信息 + $attrDetail = $this->dao->getProductAttr(['product_id' => $productId, 'type' => 0]); + } + return $attrDetail; + }, 600); + + /** @var StoreProductAttrValueServices $storeProductAttrValueService */ + $storeProductAttrValueService = app()->make(StoreProductAttrValueServices::class); + $_values = $this->dao->cacheTag()->remember('attr_value_' . $id . '_' . $type, function () use ($storeProductAttrValueService, $id, $type) { + return $storeProductAttrValueService->getProductAttrValue(['product_id' => $id, 'type' => $type]); + }, 600); + + if ($productId == 0) { + $productId = $id; + } + + /** @var StoreProductServices $storeProductService */ + $storeProductService = app()->make(StoreProductServices::class); + $vip_price = true; + + if (!$storeProductService->vipIsOpen(!!($productInfo['is_vip'] ?? 0))) $vip_price = false; + + $cartNumList = []; + $activityAttr = []; + if ($cartNum) { + /** @var StoreCartServices $storeCartService */ + $storeCartService = app()->make(StoreCartServices::class); + $unique = array_column($_values, 'unique'); + $cartNumList = $storeCartService->cacheTag('Cart_Nums_' . $uid)->remember(md5(json_encode($unique)), + function () use ($storeCartService, $unique, $id, $uid) { + return $storeCartService->getUserCartNums($unique, $id, $uid); + } + , 600); + } + + $values = []; + $field = $type ? 'stock,price' : 'stock'; + $storeProducts = $this->dao->cacheTag()->remember('attr_sku_' . $productId . '_' . $type, function () use ($storeProductAttrValueService, $productId, $field) { + return $storeProductAttrValueService->getSkuArray(['product_id' => $productId, 'type' => 0], $field, 'suk'); + }); + foreach ($_values as $value) { + if ($cartNum) { + $value['cart_num'] = $cartNumList[$value['unique']] ?? 0; + } + if (!$vip_price) $value['vip_price'] = 0; + $value['product_stock'] = $storeProducts[$value['suk']]['stock'] ?? 0; + if ($discount != -1) $value['price'] = bcmul((string)$value['price'], (string)bcdiv((string)$discount, '100', 2), 2); + if ($type) { + $value['product_price'] = $storeProducts[$value['suk']]['price'] ?? 0; + $attrs = explode(',', $value['suk']); + $count = count($attrs); + for ($i = 0; $i < $count; $i++) { + $activityAttr[$i][] = $attrs[$i]; + } + } + $values[$value['suk']] = $value; + } + foreach ($attrDetail as $k => $v) { + $attr = $v['attr_values']; + //活动商品只展示参与活动sku + if ($type && $activityAttr && $a = array_merge(array_intersect($v['attr_values'], $activityAttr[$k]))) { + $attrDetail[$k]['attr_values'] = $a; + $attr = $a; + } + foreach ($attr as $kk => $vv) { + $attrDetail[$k]['attr_value'][$kk]['attr'] = $vv; + $attrDetail[$k]['attr_value'][$kk]['check'] = false; + } + } + return [$attrDetail, $values]; + } + + /** + * 获取商品规格详情 + * @param int $id + * @param int $uid + * @param int $cartNum //是否查询购物车数量 + * @param int $type //活动类型 attr_value表 + * @param int $productId + * @param array $productInfo + * @param int $discount //限时折扣 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductAttrDetail(int $id, int $uid, int $cartNum = 0, int $type = 0, int $productId = 0, array $productInfo = [], int $discount = -1) + { + $attrDetail = $this->dao->getProductAttr(['product_id' => $id, 'type' => $type]); + if (!$attrDetail && $type > 0 && $productId) {//活动商品未获取到规格信息 + $attrDetail = $this->dao->getProductAttr(['product_id' => $productId, 'type' => 0]); + } + /** @var StoreProductAttrValueServices $storeProductAttrValueService */ + $storeProductAttrValueService = app()->make(StoreProductAttrValueServices::class); + $_values = $storeProductAttrValueService->getProductAttrValue(['product_id' => $id, 'type' => $type]); + if ($productId == 0) $productId = $id; + /** @var StoreProductServices $storeProductService */ + $storeProductService = app()->make(StoreProductServices::class); + if (!$productInfo) { + $productInfo = $storeProductService->get($productId, ['is_vip']); + } + $vip_price = true; + if (!$storeProductService->vipIsOpen(!!$productInfo['is_vip'])) $vip_price = false; + + $cartNumList = []; + $activityAttr = []; + if ($cartNum) { + /** @var StoreCartServices $storeCartService */ + $storeCartService = app()->make(StoreCartServices::class); + //真实用户 + if ($uid) { + $cartNumList = $storeCartService->getUserCartNums(array_column($_values, 'unique'), $id, $uid); + } else { + //虚拟用户 + $touristUid = $this->getItem('touristUid'); + if ($touristUid) { + $cartNumList = $storeCartService->getUserCartNums(array_column($_values, 'unique'), $id, $touristUid, 'tourist_uid'); + } + } + } + $values = []; + $field = $type ? 'stock,price' : 'stock'; + $storeProducts = $storeProductAttrValueService->getSkuArray(['product_id' => $productId, 'type' => 0], $field, 'suk'); + foreach ($_values as $value) { + if ($cartNum) { + $value['cart_num'] = $uid || $touristUid ? ($cartNumList[$value['unique']] ?? 0) : 0; + } + if (!$vip_price) $value['vip_price'] = 0; + $value['product_stock'] = $storeProducts[$value['suk']]['stock'] ?? 0; + if ($discount != -1) $value['price'] = bcmul((string)$value['price'], (string)bcdiv((string)$discount, '100', 2), 2); + if ($type) { + $value['product_price'] = $storeProducts[$value['suk']]['price'] ?? 0; + $attrs = explode(',', $value['suk']); + $count = count($attrs); + for ($i = 0; $i < $count; $i++) { + $activityAttr[$i][] = $attrs[$i]; + } + } + $values[$value['suk']] = $value; + } + foreach ($attrDetail as $k => $v) { + $attr = $v['attr_values']; + //活动商品只展示参与活动sku + if ($type && $activityAttr && $a = array_merge(array_intersect($v['attr_values'], $activityAttr[$k]))) { + $attrDetail[$k]['attr_values'] = $a; + $attr = $a; + } + foreach ($attr as $kk => $vv) { + $attrDetail[$k]['attr_value'][$kk]['attr'] = $vv; + $attrDetail[$k]['attr_value'][$kk]['check'] = false; + } + } + return [$attrDetail, $values]; + } + + /** + * 删除一条数据 + * @param int $id + * @param int $type + */ + public function del(int $id, int $type) + { + $this->dao->del($id, $type); + } + + + /** + * 设置规格 + * @param array $data + * @param int $id + * @param int $type + * @return bool + * @throws \Exception + */ + public function setAttr(array $data, int $id, int $type) + { + if ($data) { + $this->dao->del($id, $type); + $res = $this->dao->saveAll($data); + if (!$res) throw new AdminException('规格保存失败'); + } + return true; + } +} diff --git a/app/services/product/sku/StoreProductAttrValueServices.php b/app/services/product/sku/StoreProductAttrValueServices.php new file mode 100644 index 0000000..6afc029 --- /dev/null +++ b/app/services/product/sku/StoreProductAttrValueServices.php @@ -0,0 +1,409 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\sku; + + +use app\dao\product\sku\StoreProductAttrValueDao; +use app\jobs\product\ProductStockTips; +use app\jobs\product\ProductStockValueTips; +use app\services\activity\bargain\StoreBargainServices; +use app\services\activity\combination\StoreCombinationServices; +use app\services\activity\discounts\StoreDiscountsServices; +use app\services\activity\integral\StoreIntegralServices; +use app\services\activity\seckill\StoreSeckillServices; +use app\services\BaseServices; +use app\services\product\branch\StoreBranchProductAttrValueServices; +use app\services\product\product\StoreProductStockRecordServices; +// use app\webscoket\SocketPush; +use crmeb\exceptions\AdminException; +use app\services\product\product\StoreProductServices; +use crmeb\services\CacheService; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; + +/** + * Class StoreProductAttrValueService + * @package app\services\product\sku + * @mixin StoreProductAttrValueDao + */ +class StoreProductAttrValueServices extends BaseServices +{ + + use ServicesTrait; + + /** + * StoreProductAttrValueServices constructor. + * @param StoreProductAttrValueDao $dao + */ + public function __construct(StoreProductAttrValueDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取单规格规格 + * @param array $where + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOne(array $where) + { + return $this->dao->getOne($where); + } + + /** + * 根据活动商品unique查看原商品unique + * @param string $unique + * @param int $activity_id + * @param int $type + * @param array|string[] $field + * @return array|mixed|string|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUniqueByActivityUnique(string $unique, int $activity_id, int $type = 1, array $field = ['unique']) + { + if ($type == 0) return $unique; + $attrValue = $this->dao->get(['unique' => $unique, 'product_id' => $activity_id, 'type' => $type], ['id', 'suk', 'product_id']); + if (!$attrValue) { + return ''; + } + switch ($type) { + case 1://秒杀 + /** @var StoreSeckillServices $activityServices */ + $activityServices = app()->make(StoreSeckillServices::class); + break; + case 2://砍价 + /** @var StoreBargainServices $activityServices */ + $activityServices = app()->make(StoreBargainServices::class); + break; + case 3://拼团 + /** @var StoreCombinationServices $activityServices */ + $activityServices = app()->make(StoreCombinationServices::class); + break; + case 4://积分 + /** @var StoreIntegralServices $activityServices */ + $activityServices = app()->make(StoreIntegralServices::class); + break; + case 5://套餐 + /** @var StoreDiscountsServices $activityServices */ + $activityServices = app()->make(StoreDiscountsServices::class); + break; + default: + /** @var StoreProductServices $activityServices */ + $activityServices = app()->make(StoreProductServices::class); + break; + + } + $product_id = $activityServices->value(['id' => $activity_id], 'product_id'); + if (!$product_id) { + return ''; + } + if (count($field) == 1) { + return $this->dao->value(['suk' => $attrValue['suk'], 'product_id' => $product_id, 'type' => 0], $field[0] ?? 'unique'); + } else { + return $this->dao->get(['suk' => $attrValue['suk'], 'product_id' => $product_id, 'type' => 0], $field); + } + + } + + /** + * 删除一条数据 + * @param int $id + * @param int $type + * @param array $suk + * @return bool + */ + public function del(int $id, int $type, array $suk = []) + { + return $this->dao->del($id, $type, $suk); + } + + /** + * 批量保存 + * @param array $data + */ + public function saveAll(array $data) + { + $res = $this->dao->saveAll($data); + if (!$res) throw new AdminException('规格保存失败'); + return $res; + } + + /** + * 获取sku + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getSkuArray(array $where, string $field = 'unique,bar_code,cost,price,ot_price,stock,image as pic,weight,volume,brokerage,brokerage_two,quota,product_id,code', string $key = 'suk') + { + return $this->dao->getColumn($where, $field, $key); + } + + /** + * 交易排行榜 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function purchaseRanking() + { + $dlist = $this->dao->attrValue(); + /** @var StoreProductServices $proServices */ + $proServices = app()->make(StoreProductServices::class); + $slist = $proServices->getProductLimit(['is_del' => 0], $limit = 20, 'id as product_id,store_name,sales * price as val'); + $data = array_merge($dlist, $slist); + $last_names = array_column($data, 'val'); + array_multisort($last_names, SORT_DESC, $data); + $list = array_splice($data, 0, 20); + return $list; + } + + /** + * 获取商品的属性数量 + * @param $product_id + * @param $unique + * @param $type + * @return int + */ + public function getAttrvalueCount($product_id, $unique, $type) + { + return $this->dao->count(['product_id' => $product_id, 'unique' => $unique, 'type' => $type]); + } + + /** + * 获取唯一值下的库存 + * @param string $unique + * @return int + */ + public function uniqueByStock(string $unique) + { + if (!$unique) return 0; + return $this->dao->uniqueByStock($unique); + } + + /** + * 减销量,加库存 + * @param $productId + * @param $unique + * @param $num + * @param int $type + * @return mixed + */ + public function decProductAttrStock($productId, $unique, $num, $type = 0) + { + $res = $this->dao->decStockIncSales([ + 'product_id' => $productId, + 'unique' => $unique, + 'type' => $type + ], $num); + if ($res) { + $this->workSendStock($productId, $unique, $type); + } + return $res; + } + + /** + * 减少销量增加库存 + * @param $productId + * @param $unique + * @param $num + * @return bool + */ + public function incProductAttrStock(int $productId, string $unique, int $num, int $type = 0) + { + return $this->dao->incStockDecSales(['unique' => $unique, 'product_id' => $productId, 'type' => $type], $num); + } + + /** + * 库存预警消息提醒 + * @param int $productId + * @param string $unique + * @param int $type + */ + public function workSendStock(int $productId, string $unique, int $type) + { + ProductStockValueTips::dispatch([$productId, $unique, $type]); + } + + /** + * 获取秒杀库存 + * @param int $productId + * @param string $unique + * @param bool $isNew + * @return array|mixed|\think\Model|null + * @throws \Psr\SimpleCache\InvalidArgumentException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSeckillAttrStock(int $productId, string $unique, bool $isNew = false) + { + $key = md5('seclkill_attr_stock_' . $productId . '_' . $unique); + $stock = CacheService::redisHandler()->get($key); + if (!$stock || $isNew) { + $stock = $this->dao->getOne(['product_id' => $productId, 'unique' => $unique, 'type' => 1], 'suk,quota'); + if ($stock) { + CacheService::redisHandler()->set($key, $stock, 60); + } + } + return $stock; + } + + /** + * @param $product_id + * @param string $suk + * @param string $unique + * @param bool $is_new + * @return int|mixed + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getProductAttrStock(int $productId, string $suk = '', string $unique = '', $isNew = false) + { + if (!$suk && !$unique) return 0; + $key = md5('product_attr_stock_' . $productId . '_' . $suk . '_' . $unique); + $stock = CacheService::redisHandler()->get($key); + if (!$stock || $isNew) { + $where = ['product_id' => $productId, 'type' => 0]; + if ($suk) { + $where['suk'] = $suk; + } + if ($unique) { + $where['unique'] = $unique; + } + $stock = $this->dao->value($where, 'stock'); + CacheService::redisHandler()->set($key, $stock, 60); + } + return $stock; + } + + /** + * 根据商品id获取对应规格库存 + * @param int $productId + * @param int $type + * @return float + */ + public function pidBuStock(int $productId, int $type = 0) + { + return $this->dao->pidBuStock($productId, $type); + } + + /** + * 更新sum_stock + * @param array $uniques + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateSumStock(array $uniques) + { + /** @var StoreBranchProductAttrValueServices $storeValueService */ + $storeValueService = app()->make(StoreBranchProductAttrValueServices::class); + $stockSumData = $storeValueService->getProductAttrValueStockSum($uniques ?? []); + $this->dao->getList(['unique' => $uniques])->map(function ($item) use ($stockSumData) { + if (isset($stockSumData[$item->unique])) { + $data['sum_stock'] = $item->stock + $stockSumData[$item->unique]; + } else { + $data['sum_stock'] = $item->stock; + } + $this->dao->update(['product_id' => $item['product_id'], 'unique' => $item['unique'], 'type' => $item['type']], $data); + }); + } + + /** + * 批量快速修改商品规格库存 + * @param int $id + * @param array $data + * @return int|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveProductAttrsStock(int $id, array $data) + { + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $product = $productServices->get($id); + if (!$product) { + throw new ValidateException('商品不存在'); + } + $attrs = $this->dao->getProductAttrValue(['product_id' => $id, 'type' => 0]); + if ($attrs) $attrs = array_combine(array_column($attrs, 'unique'), $attrs); + $dataAll = $update = []; + $stock = 0; + $time = time(); + foreach ($data as $attr) { + if (!isset($attrs[$attr['unique']])) continue; + if ($attr['pm']) { + $stock = bcadd((string)$stock, (string)$attr['stock'], 0); + $update['stock'] = bcadd((string)$attrs[$attr['unique']]['stock'], (string)$attr['stock'], 0); + $update['sum_stock'] = bcadd((string)$attrs[$attr['unique']]['sum_stock'], (string)$attr['stock'], 0); + } else { + $stock = bcsub((string)$stock, (string)$attr['stock'], 0); + $update['stock'] = bcsub((string)$attrs[$attr['unique']]['stock'], (string)$attr['stock'], 0); + $update['sum_stock'] = bcsub((string)$attrs[$attr['unique']]['sum_stock'], (string)$attr['stock'], 0); + } + $update['stock'] = $update['stock'] > 0 ? $update['stock'] : 0; + $this->dao->update(['id' => $attrs[$attr['unique']]['id']], $update); + + $dataAll[] = [ + 'product_id' => $id, + 'unique' => $attr['unique'], + 'cost_price' => $attrs[$attr['unique']]['cost'] ?? 0, + 'number' => $attr['stock'], + 'pm' => $attr['pm'] ? 1 : 0, + 'add_time' => $time, + ]; + } + $product_stock = $stock ? bcadd((string)$product['stock'], (string)$stock, 0) : bcsub((string)$product['stock'], (string)$stock, 0); + $product_stock = $product_stock > 0 ? $product_stock : 0; + //修改商品库存 + $productServices->update($id, ['stock' => $product_stock]); + //检测库存警戒和检测是否售罄 + ProductStockTips::dispatch([$id, 0]); + //添加库存记录$product_stock + if ($dataAll) { + /** @var StoreProductStockRecordServices $storeProductStockRecordServces */ + $storeProductStockRecordServces = app()->make(StoreProductStockRecordServices::class); + $storeProductStockRecordServces->saveAll($dataAll); + } + + //清除缓存 + $productServices->cacheTag()->clear(); + /** @var StoreProductAttrServices $attrService */ + $attrService = app()->make(StoreProductAttrServices::class); + $attrService->cacheTag()->clear(); + + return $product_stock; + } + + /** + * 查询库存预警产品ids + * @param array $where + * @return array + */ + public function getGroupId(array $where) + { + $res1 = []; + $res2 = $this->dao->getGroupData('product_id', 'product_id', $where); + foreach ($res2 as $id) { + $res1[] = $id['product_id']; + } + return $res1; + } + +} diff --git a/app/services/product/sku/StoreProductRuleServices.php b/app/services/product/sku/StoreProductRuleServices.php new file mode 100644 index 0000000..2090ac0 --- /dev/null +++ b/app/services/product/sku/StoreProductRuleServices.php @@ -0,0 +1,131 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\product\sku; + + +use app\dao\product\sku\StoreProductRuleDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; + +/** + * Class StoreProductRuleService + * @package app\services\product\sku + * @mixin StoreProductRuleDao + */ +class StoreProductRuleServices extends BaseServices +{ + public function __construct(StoreProductRuleDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取商品规格列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where = []) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit); + foreach ($list as &$item) { + $attr_name = $attr_value = []; + if ($item['rule_value']) { + $specs = json_decode($item['rule_value'], true); + if ($specs) { + foreach ($specs as $key => $value) { + $attr_name[] = $value['value']; + $attr_value[] = implode(',', $value['detail']); + } + } else { + $attr_name[] = ''; + $attr_value[] = ''; + } + $item['attr_name'] = implode(',', $attr_name); + $item['attr_value'] = $attr_value; + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 保存数据 + * @param int $id + * @param array $data + * @param int $type + * @param int $relation_id + * @return void + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function save(int $id, array $data, int $type = 0, int $relation_id = 0) + { + $data['type'] = $type; + $data['relation_id'] = $relation_id; + $data['rule_value'] = json_encode($data['spec']); + unset($data['spec']); + $rule = $this->dao->getOne(['rule_name' => $data['rule_name'], 'type' => $type, 'relation_id' => $relation_id]); + if ($id) { + if ($rule && $rule['id'] != $id) { + throw new AdminException('分类名称已存在'); + } + $res = $this->dao->update($id, $data); + } else { + if ($rule) { + throw new AdminException('分类名称已存在'); + } + $res = $this->dao->save($data); + } + if (!$res) throw new AdminException('保存失败'); + } + + /** + * 获取一条数据 + * @param int $id + * @return array + */ + public function getInfo(int $id) + { + $info = $this->dao->get($id); + $info['spec'] = json_decode($info['rule_value'], true); + return compact('info'); + } + + /** + * 删除数据 + * @param string $ids + */ + public function del(string $ids) + { + if ($ids == '') throw new AdminException('请至少选择一条数据'); + $this->dao->del($ids); + } + + /** + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getProductRuleList(array $where, $field = "*") + { + return $this->dao->getProductRuleList($where, $field); + } + +} diff --git a/app/services/product/sku/StoreProductVirtualServices.php b/app/services/product/sku/StoreProductVirtualServices.php new file mode 100644 index 0000000..9827df9 --- /dev/null +++ b/app/services/product/sku/StoreProductVirtualServices.php @@ -0,0 +1,82 @@ +dao = $dao; + } + + /** + * 规格中获取卡密列表 + * @param $unique + * @param $product_id + * @return array + */ + public function getArr($unique, $product_id) + { + $res = $this->dao->getColumn(['attr_unique' => $unique, 'product_id' => $product_id], 'card_no,card_pwd'); + $data = []; + foreach ($res as $item) { + $data[] = ['key' => $item['card_no'], 'value' => $item['card_pwd']]; + } + return $data; + } + + /** + * 获取订单发送卡密列表 + * @param array $where + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrderCardList(array $where, int $limit = 1) + { + return $this->dao->getList($where, '*', 0, $limit); + } + + /** + * 保存商品规格(虚拟卡密信息) + * @param int $id + * @param array $valueGroup + * @param int $store_id + * @return bool + */ + public function saveProductVirtual(int $id, array $valueGroup, int $store_id = 0) + { + foreach ($valueGroup as &$item) { + if (isset($item['product_type']) && $item['product_type'] == 1 && isset($item['virtual_list']) && count($item['virtual_list'])) { + $this->dao->delete(['store_id' => $store_id, 'product_id' => $id, 'attr_unique' => $item['unique'], 'uid' => 0]); + $data = []; + foreach ($item['virtual_list'] as &$items) { + if (!$this->dao->count(['product_id' => $id, 'store_id' => $store_id, 'card_no' => $items['key'], 'card_pwd' => $items['value']])) { + $data = [ + 'product_id' => $id, + 'attr_unique' => $item['unique'], + 'card_no' => $items['key'], + 'card_pwd' => $items['value'], + 'card_unique' => md5($item['unique'] . ',' . $items['key'] . ',' . $items['value']) + ]; + $this->dao->save($data); + } + } + } + } + return true; + } +} diff --git a/app/services/product/specs/StoreProductSpecsServices.php b/app/services/product/specs/StoreProductSpecsServices.php new file mode 100644 index 0000000..7bf56f0 --- /dev/null +++ b/app/services/product/specs/StoreProductSpecsServices.php @@ -0,0 +1,140 @@ + +// +---------------------------------------------------------------------- +namespace app\services\product\specs; + + +use app\dao\product\specs\StoreProductSpecsDao; +use app\services\BaseServices; +use think\exception\ValidateException; + + +/** + * 商品参数 + * Class StoreProductSpecsServices + * @package app\services\product\ensure + * @mixin StoreProductSpecsDao + */ +class StoreProductSpecsServices extends BaseServices +{ + + /** + * 商品参数字段 + * @var array + */ + protected $specs = [ + 'id' => 0, + 'temp_id' => 0, + 'name' => '', + 'value' => '', + 'sort' => 0 + ]; + + /** + * StoreProductSpecsServices constructor. + * @param StoreProductSpecsDao $dao + */ + public function __construct(StoreProductSpecsDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取参数模版列表(带参数) + * @param array $where + * @return array + */ + public function getSpecsTemplateList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 检测 + * @param array $data + * @return array + */ + public function checkSpecsData(array $data) + { + $data = array_merge($this->specs, array_intersect_key($data, $this->specs)); + if (!isset($data['name']) || !$data['name']) { + throw new ValidateException('请填写参数名称'); + } + if (!isset($data['value']) || !$data['value']) { + throw new ValidateException('请填写参数值'); + } + return $data; + } + + /** + * 修改参数模版(商品参数) + * @param int $id + * @param array $specsArr + * @param int $type + * @param int $relation_id + * @return bool + */ + public function updateData(int $id, array $specsArr, int $type = 0, int $relation_id = 0) + { + $this->dao->delete(['temp_id' => $id]); + $insert = []; + $time = time(); + foreach ($specsArr as $specs) { + $specs = $this->checkSpecsData($specs); + $specs['type'] = $type; + $specs['relation_id'] = $relation_id; + $specs['temp_id'] = $id; + if (isset($specs['id'])) { + unset($specs['id']); + } + $specs['add_time'] = $time; + $insert[] = $specs; + + } + if ($insert) { + if (!$this->dao->saveAll($insert)) { + throw new ValidateException('新增商品参数失败'); + } + } + return true; + } + + /** + * 保存参数模版(商品参数) + * @param int $id + * @param array $specsArr + * @param int $type + * @param int $relation_id + * @return bool + */ + public function saveData(int $id, array $specsArr, int $type = 0, int $relation_id = 0) + { + if (!$specsArr) return true; + $dataAll = []; + $time = time(); + foreach ($specsArr as $specs) { + $specs = $this->checkSpecsData($specs); + $specs['type'] = $type; + $specs['relation_id'] = $relation_id; + $specs['temp_id'] = $id; + $specs['add_time'] = $time; + $dataAll[] = $specs; + } + if ($dataAll) { + $this->dao->saveAll($dataAll); + } + return true; + } + + +} diff --git a/app/services/serve/ServeServices.php b/app/services/serve/ServeServices.php new file mode 100644 index 0000000..f3988f1 --- /dev/null +++ b/app/services/serve/ServeServices.php @@ -0,0 +1,143 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\serve; + + +use app\services\BaseServices; +use crmeb\services\express\Express; +use crmeb\services\FormBuilder; +use crmeb\services\product\Product; +use crmeb\services\serve\Serve; +use crmeb\services\sms\Sms; + +/** + * 平台服务入口 + * Class ServeServices + * @package app\services\serve + */ +class ServeServices extends BaseServices +{ + + /** + * @var FormBuilder + */ + protected $builder; + + /** + * SmsTemplateApplyServices constructor. + * @param FormBuilder $builder + */ + public function __construct(FormBuilder $builder) + { + $this->builder = $builder; + } + + /** + * 获取配置 + * @return array + */ + public function getConfig(array $config = []) + { + return array_merge([ + 'account' => sys_config('sms_account'), + 'secret' => sys_config('sms_token') + ], $config); + } + + /** + * 短信 + * @return Sms + */ + public function sms(array $config = []) + { + return app()->make(Sms::class, [$this->getConfig($config)]); + } + + /** + * 复制商品 + * @return Product + */ + public function copy(array $config = []) + { + return app()->make(Product::class, [$this->getConfig($config)]); + } + + /** + * 电子面单 + * @return Express + */ + public function express(array $config = []) + { + return app()->make(Express::class, [$this->getConfig($config)]); + } + + /** + * 用户 + * @return Serve + */ + public function user(array $config = []) + { + return app()->make(Serve::class, [$this->getConfig($config)]); + } + + /** + * 获取短信模板 + * @param int $page + * @param int $limit + * @param int $type + * @return array + */ + public function getSmsTempsList(int $page, int $limit, int $type) + { + $list = $this->sms()->temps($page, $limit, $type); + foreach ($list['data'] as &$item) { + $item['templateid'] = $item['temp_id']; + switch ((int)$item['temp_type']) { + case 1: + $item['type'] = '验证码'; + break; + case 2: + $item['type'] = '通知'; + break; + case 30: + $item['type'] = '营销短信'; + break; + } + } + return $list; + } + + /** + * 创建短信模板表单 + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createSmsTemplateForm() + { + $field = [ + $this->builder->input('title', '模板名称')->placeholder('模板名称,如:订单支付成功'), + $this->builder->input('content', '模板内容')->type('textarea')->placeholder('模板内容,如:您购买的商品已支付成功,支付金额{$pay_price}元,订单号{$order_id},感谢您的光临!(注:模板内容不用添加短信签名,可在一号通主页单独修改签名)')->rows(3), + $this->builder->radio('type', '模板类型', 1)->options([['label' => '验证码', 'value' => 1], ['label' => '通知', 'value' => 2], ['label' => '推广', 'value' => 3]]) + ]; + return $field; + } + + /** + * 获取短信申请模板 + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function getSmsTemplateForm() + { + return create_form('申请短信模板', $this->createSmsTemplateForm(), $this->url('/notify/sms/temp'), 'POST'); + } +} diff --git a/app/services/statistic/TradeStatisticServices.php b/app/services/statistic/TradeStatisticServices.php new file mode 100644 index 0000000..db966e9 --- /dev/null +++ b/app/services/statistic/TradeStatisticServices.php @@ -0,0 +1,852 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\statistic; + +use app\services\BaseServices; +use app\services\order\StoreOrderRefundServices; +use app\services\other\export\ExportServices; +use app\services\order\OtherOrderServices; +use app\services\order\StoreOrderServices; +use app\services\user\UserExtractServices; +use app\services\user\UserMoneyServices; +use app\services\user\UserRechargeServices; + +/** + * Class TradeStatisticServices + * @package app\services\statistic + */ +class TradeStatisticServices extends BaseServices +{ + public $_day = ['00时', '01时', '02时', '03时', '04时', '05时', '06时', '07时', '08时', '09时', '10时', '11时', '12时', '13时', '14时', '15时', '16时', '17时', '18时', '19时', '20时', '21时', '22时', '23时', '24时']; + + /** + * 基本概况 + * @param $where + * @return mixed + */ + public function getTopLeftTrade($where) + { + //总交易额 + $selectType = "sum"; + $tradeTotalMoney = $this->tradeTotalMoney($where, $selectType); + //交易曲线 + $selectType = "group"; + $hourTotalMoney = $this->tradeGroupMoney($where, $selectType); + return ['total_money' => $tradeTotalMoney, 'curve' => $hourTotalMoney]; + } + + public function getTopRightOneTrade() + { + /** @var StoreOrderServices $orderService */ + $orderService = app()->make(StoreOrderServices::class); + /** day订单数 */ + //今日订单数 + $orderCountWhere['paid'] = 1; + $orderCountWhere['pid'] = 0; + $orderCountWhere['timeKey'] = $this->TimeConvert("today"); + $todayOrderCount = $orderService->getOrderCountByWhere($orderCountWhere); + + //今日订单数曲线 + $todayHourOrderCount = $orderService->getOrderGroupCountByWhere($orderCountWhere); + $todayHourOrderCount = $this->trendYdata((array)$todayHourOrderCount, $orderCountWhere['timeKey']); + //昨日订单数 + $yestodayWhere['paid'] = 1; + $yestodayWhere['pid'] = 0; + $yestodayWhere['timeKey'] = $this->TimeConvert("yestoday"); + $yesTodayOrderCount = $orderService->getOrderCountByWhere($yestodayWhere); + //昨日订单曲线 + // $yestodayHourOrderCount = $orderService->getOrderGroupCountByWhere($yestodayWhere); + // $yestodayHourOrderCount = $this->trendYdata((array)$yestodayHourOrderCount, 'day'); + //订单数环比增长率 + $orderCountDayChain = $this->countRate($todayOrderCount, $yesTodayOrderCount); + $data[] = [ + 'name' => "今日订单数", + 'now_value' => $todayOrderCount, + 'last_value' => $yesTodayOrderCount, + 'rate' => $orderCountDayChain, + 'curve' => $todayHourOrderCount + ]; + /** day支付人数 */ + //今日支付人数 + $orderPeopleWhere['timeKey'] = $this->TimeConvert("today"); + $orderPeopleWhere['paid'] = 1; + $orderPeopleWhere['pid'] = 0; + $todayPayOrderPeople = count($orderService->getPayOrderPeopleByWhere($orderPeopleWhere)); + //今日支付人数曲线 + $todayHourOrderPeople = $orderService->getPayOrderGroupPeopleByWhere($orderPeopleWhere); + $todayHourOrderPeople = $this->trendYdata((array)$todayHourOrderPeople, $orderPeopleWhere['timeKey']); + //昨日支付人数 + $yestodayOrderPeopleWhere['timeKey'] = $this->TimeConvert("yestoday"); + $yestodayOrderPeopleWhere['paid'] = 1; + $yestodayPayOrderPeople = count($orderService->getPayOrderPeopleByWhere($yestodayOrderPeopleWhere)); + //昨日支付曲线 + // $yestodayHourOrderPeople = $orderService->getPayOrderGroupPeopleByWhere($yestodayOrderPeopleWhere); + // $yestodayHourOrderPeople = $this->trendYdata((array)$yestodayHourOrderPeople, 'day'); + //订单支付人数环比 + $orderPeopleDayChain = $this->countRate($todayPayOrderPeople, $yestodayPayOrderPeople); + $data[] = [ + 'name' => "今日支付人数", + 'now_value' => $todayPayOrderPeople, + 'last_value' => $yestodayPayOrderPeople, + 'rate' => $orderPeopleDayChain, + 'curve' => $todayHourOrderPeople + ]; + $new_data = []; + foreach ($data as $k => $v) { + $new_data['x'] = $v['curve']['x']; + $new_data['series'][$k]['name'] = $v['name']; + $new_data['series'][$k]['now_money'] = $v['now_value']; + $new_data['series'][$k]['last_money'] = $v['last_value']; + $new_data['series'][$k]['rate'] = $v['rate']; + $new_data['series'][$k]['value'] = array_values($v['curve']['y']); + + } + + return $new_data; + } + + public function getTopRightTwoTrade() + { + /** @var StoreOrderServices $orderService */ + $orderService = app()->make(StoreOrderServices::class); + /** month订单数 */ + $monthOrderCountWhere['paid'] = 1; + $monthOrderCountWhere['pid'] = 0; + $monthOrderCountWhere['timeKey'] = $this->TimeConvert("month"); + $monthOrderCount = $orderService->getOrderCountByWhere($monthOrderCountWhere); + //本月订单数曲线 + $monthCurveOrderCount = $orderService->getOrderGroupCountByWhere($monthOrderCountWhere); + $monthCurveOrderCount = $this->trendYdata((array)$monthCurveOrderCount, $monthOrderCountWhere['timeKey']); + //上月订单数 + $lastOrderCountWhere['timeKey'] = $this->TimeConvert("last_month"); + $lastOrderCountWhere['is_del'] = 0; + $lastOrderCountWhere['paid'] = 1; + $lastOrderCountWhere['pid'] = 0; + $lastOrderCount = $orderService->getOrderCountByWhere($lastOrderCountWhere); + //上月订单曲线 + // $lastCurveOrderCount = $orderService->getOrderGroupCountByWhere($lastOrderCountWhere); + // $lastCurveOrderCount = $this->trendYdata((array)$lastCurveOrderCount, 'month'); + //订单数环比增长率 + // $orderCountMonthChain = (($monthOrderCount - $lastOrderCount) / $lastOrderCount) * 100; + $orderCountMonthChain = $this->countRate($monthOrderCount, $lastOrderCount); + $data[] = [ + 'name' => "本月订单数", + 'now_value' => $monthOrderCount, + 'last_value' => $lastOrderCount, + 'rate' => $orderCountMonthChain, + 'curve' => $monthCurveOrderCount + ]; + /** month下单人数 */ + //本月支付人数 + $monthOrderPeopleWhere['timeKey'] = $this->TimeConvert("month");; + $monthOrderPeopleWhere['paid'] = 1; + $monthPayOrderPeople = count($orderService->getPayOrderPeopleByWhere($monthOrderPeopleWhere)); + //本月支付人数曲线 + $monthCurveOrderPeople = $orderService->getPayOrderGroupPeopleByWhere($monthOrderPeopleWhere); + $monthCurveOrderPeople = $this->trendYdata((array)$monthCurveOrderPeople, $monthOrderPeopleWhere['timeKey']); + //上月支付人数 + $lastOrderPeopleWhere['timeKey'] = $this->TimeConvert("last_month"); + $lastOrderPeopleWhere['paid'] = 1; + $lastPayOrderPeople = count($orderService->getPayOrderPeopleByWhere($lastOrderPeopleWhere)); + //上月支付曲线 + // $lastCurveOrderPeople = $orderService->getPayOrderGroupPeopleByWhere($lastOrderPeopleWhere); + // $lastCurveOrderPeople = $this->trendYdata((array)$lastCurveOrderPeople, 'month'); + //订单支付人数环比 + $orderPeopleDayChain = $this->countRate($monthPayOrderPeople, $lastPayOrderPeople); + $data[] = [ + 'name' => "本月支付人数", + 'now_value' => $monthPayOrderPeople, + 'last_value' => $lastPayOrderPeople, + 'rate' => $orderPeopleDayChain, + 'curve' => $monthCurveOrderPeople + ]; + $new_data = []; + foreach ($data as $k => $v) { + $new_data[$k]['name'] = $v['name']; + $new_data[$k]['now_money'] = $v['now_value']; + $new_data[$k]['last_money'] = $v['last_value']; + $new_data[$k]['rate'] = $v['rate']; + $new_data[$k]['value'] = $v['curve']['y']; + } + + return $new_data; + } + + /** + * 交易总额 + * @param $where + * @param $selectType + * @return array|float|int|mixed + */ + public function tradeTotalMoney($where, $selectType, $isNum = false) + { + /** 收入营业额 */ + //商品订单收入 + $inOrderMoney = $this->getOrderTotalMoney($where, $selectType, "", $isNum); + //用户充值收入 + $inRechargeMoneyHome = $this->getRechargeTotalMoney($where, $selectType, "", $isNum); + $inrechgeMoneyAdmin = $this->getBillYeTotalMoney($where, $selectType, '', $isNum); + $inRechargeMoney = bcadd($inRechargeMoneyHome, $inrechgeMoneyAdmin, 2); + //购买会员收入 + $inMemberMoney = $this->getMemberTotalMoney($where, $selectType, "", $isNum); + //线下收款收入 + $inOfflineMoney = $this->getOfflineTotalMoney($where, $selectType, "", $isNum); + //总交易额 + $inTotalMoney = bcadd(bcadd($inOrderMoney, $inRechargeMoney, 2), bcadd($inMemberMoney, $inOfflineMoney, 2), 2);/* - $outExtractUserMoney*/ + return $inTotalMoney; + } + + /** + * 交易额曲线图 + * @param $where + * @param $selectType + * @return array + */ + public function tradeGroupMoney($where, $selectType) + { + + //商品订单收入 + $orderGroup = "add_time"; + $OrderMoney = $this->getOrderTotalMoney($where, $selectType, $orderGroup); + //用户充值收入 + $rechargeGroup = "add_time"; + $RechargeMoneyHome = $this->getRechargeTotalMoney($where, $selectType, $rechargeGroup); + $RechargeMoneyAdmin = $this->getBillYeTotalMoney($where, $selectType, $rechargeGroup); + $RechargeMoney = $this->totalArrData([$RechargeMoneyHome, $RechargeMoneyAdmin]); + //购买会员收入 + $memberGroup = "add_time"; + $MemberMoney = $this->getMemberTotalMoney($where, $selectType, $memberGroup); + //线下收款收入 + $offlineGroup = "add_time"; + $OfflineMoney = $this->getOfflineTotalMoney($where, $selectType, $offlineGroup); + return $this->totalArrData([$OrderMoney, $RechargeMoney, $MemberMoney, $OfflineMoney]); + } + + /** + * 底部数据 + * @param $where + * @return array + * @throws \Exception + */ + public function getBottomTrade($where) + { + + if (!$where['data']) { + $where['time'] = ['start_time' => date('Y-m-d 00:00:00', time()), "end_time" => date('Y-m-d 23:59:59', time())]; + } else { + $time = explode("-", $where['data']); + $where['time'] = ['start_time' => date('Y-m-d 00:00:00', strtotime($time[0])), "end_time" => date('Y-m-d 23:59:59', strtotime($time[1]))]; + } + unset($where['data']); + /** @var ExportServices $exportService */ + $exportService = app()->make(ExportServices::class); + $chainTime = $this->chainTime($where['time']); + $isNum = false; + if ($chainTime == "other") $isNum = true; + $dateWhere['time'] = $isNum ? $where['time'] : $chainTime; + $topData = array(); + $Chain = array(); + + /** 商品支付金额 */ + $OrderMoney = $this->getOrderTotalMoney($where, "sum"); + $lastOrderMoney = $this->getOrderTotalMoney($dateWhere, "sum", "", $isNum); + $OrderCurve = $this->getOrderTotalMoney($where, "group", "add_time"); + $OrderChain = $this->countRate($OrderMoney, $lastOrderMoney); + $topData[2] = [ + 'title' => '商品支付金额', + 'desc' => '选定条件下,用户购买商品的实际支付金额,包括微信支付、余额支付、支付宝支付、线下支付金额(拼团商品在成团之后计入,线下支付订单在后台确认支付后计入)', + 'total_money' => $OrderMoney, + 'rate' => $OrderChain, + 'value' => $OrderCurve['y'], + 'type' => 1, + 'sign' => 'goods', + ]; + + $Chain['goods'] = $OrderCurve; + + /** 购买会员金额 */ + $memberMoney = $this->getMemberTotalMoney($where, 'sum'); + $lastMemberMoney = $this->getMemberTotalMoney($dateWhere, 'sum', "", $isNum); + $memberCurve = $this->getMemberTotalMoney($where, 'group', "pay_time"); + $MemberChain = $this->countRate($memberMoney, $lastMemberMoney); + $topData[3] = [ + 'title' => '购买会员金额', + 'desc' => '选定条件下,用户成功购买付费会员的金额', + 'total_money' => $memberMoney, + 'rate' => $MemberChain, + 'value' => $memberCurve['y'], + 'type' => 1, + 'sign' => 'member', + ]; + $Chain['member'] = $memberCurve; + + /** 充值金额 */ + $rechgeMoneyHome = $this->getRechargeTotalMoney($where, 'sum'); + $rechgeMoneyAdmin = $this->getBillYeTotalMoney($where, 'sum'); + $rechgeMoneyTotal = bcadd($rechgeMoneyHome, $rechgeMoneyAdmin, 2); + $lastRechgeMoneyHome = $this->getRechargeTotalMoney($dateWhere, 'sum', "", $isNum); + $lastRechgeMoneyAdmin = $this->getBillYeTotalMoney($dateWhere, 'sum', "", $isNum); + $lastRechgeMoneyTotal = bcadd($lastRechgeMoneyHome, $lastRechgeMoneyAdmin, 2); + $RechgeHomeCurve = $this->getRechargeTotalMoney($where, 'group', "pay_time"); + $RechgeAdminCurve = $this->getBillYeTotalMoney($where, 'group', "add_time"); + $RechgeTotalCurve = $this->totalArrData([$RechgeHomeCurve, $RechgeAdminCurve]); + $RechgeChain = $this->countRate($rechgeMoneyTotal, $lastRechgeMoneyTotal); + $topData[4] = [ + 'title' => '充值金额', + 'desc' => '选定条件下,用户成功充值的金额', + 'total_money' => $rechgeMoneyTotal, + 'rate' => $RechgeChain, + 'value' => $RechgeTotalCurve['y'], + 'type' => 1, + 'sign' => 'rechge', + ]; + $Chain['rechage'] = $RechgeTotalCurve; + + /** 线下收银 */ + $offlineMoney = $this->getOfflineTotalMoney($where, 'sum'); + $lastOfflineMoney = $this->getOfflineTotalMoney($dateWhere, 'sum', "", $isNum); + $offlineCurve = $this->getOfflineTotalMoney($where, 'group', "pay_time"); + $offlineChain = $this->countRate($offlineMoney, $lastOfflineMoney); + $topData[5] = [ + 'title' => '线下收银金额', + 'desc' => '选定条件下,用户在线下扫码支付的金额', + 'total_money' => $offlineMoney, + 'rate' => $offlineChain, + 'value' => $offlineCurve['y'], + 'type' => 0, + 'sign' => 'offline', + ]; + $Chain['offline'] = $offlineCurve; + + /** 支出*/ + //余额支付商品 + $outYeOrderMoney = $this->getOrderTotalMoney(['pay_type' => "yue", 'time' => $where['time']], 'sum'); + $lastOutYeOrderMoney = $this->getOrderTotalMoney(['pay_type' => "yue", 'time' => $dateWhere['time']], 'sum', "", $isNum); + $outYeOrderCurve = $this->getOrderTotalMoney(['pay_type' => "yue", 'time' => $where['time']], 'group', 'pay_time'); + $outYeOrderChain = $this->countRate($outYeOrderMoney, $lastOutYeOrderMoney); + //余额购买会员 + $outYeMemberMoney = $this->getMemberTotalMoney(['pay_type' => "yue", 'time' => $where['time']], 'sum'); + $lastOutYeMemberMoney = $this->getMemberTotalMoney(['pay_type' => "yue", 'time' => $dateWhere['time']], 'sum', "", $isNum); + $outYeMemberCurve = $this->getMemberTotalMoney(['pay_type' => "yue", 'time' => $where['time']], 'group', "pay_time"); + $outYeMemberChain = $this->countRate($outYeMemberMoney, $lastOutYeMemberMoney); + //余额支付 + $outYeMoney = bcadd($outYeOrderMoney, $outYeMemberMoney, 2); + $lastOutYeMoney = bcadd($lastOutYeOrderMoney, $lastOutYeMemberMoney, 2); + $outYeCurve = $this->totalArrData([$outYeOrderCurve, $outYeMemberCurve]); + $outYeChain = $this->countRate($outYeOrderChain, $outYeMemberChain); + $topData[7] = [ + 'title' => '余额支付金额', + 'desc' => '用户下单时使用余额实际支付的金额', + 'total_money' => $outYeMoney, + 'rate' => $outYeChain, + 'value' => $outYeCurve['y'], + 'type' => 0, + 'sign' => 'yue', + ]; + $Chain['out_ye'] = $outYeCurve; + + + //支付佣金金额 + $outExtractMoney = $this->getExtractTotalMoney($where, 'sum'); + $lastOutExtractMoney = $this->getExtractTotalMoney($dateWhere, 'sum', "", $isNum); + $OutExtractCurve = $this->getExtractTotalMoney($where, 'group', "add_time"); + $OutExtractChain = $this->countRate($outExtractMoney, $lastOutExtractMoney); + $topData[8] = [ + 'title' => '支付佣金金额', + 'desc' => '后台给推广员支付的推广佣金,以实际支付为准', + 'total_money' => $outExtractMoney, + 'rate' => $OutExtractChain, + 'value' => $OutExtractCurve['y'], + 'type' => 0, + 'sign' => 'yong', + ]; + $Chain['extract'] = $OutExtractCurve; + + //商品退款金额 + $outOrderRefund = $this->getOrderRefundTotalMoney(['refund_type' => 6, 'time' => $where['time']], 'sum'); + $lastOutOrderRefund = $this->getOrderRefundTotalMoney(['refund_type' => 6, 'time' => $dateWhere['time']], 'sum', "", $isNum); + $outOrderRefundCurve = $this->getOrderRefundTotalMoney(['refund_type' => 6, 'time' => $where['time']], 'group', 'add_time'); + $orderRefundChain = $this->countRate($outOrderRefund, $lastOutOrderRefund); + $topData[9] = [ + 'title' => '商品退款金额', + 'desc' => '用户成功退款的商品金额', + 'total_money' => $outOrderRefund, + 'rate' => $orderRefundChain, + 'value' => $outOrderRefundCurve['y'], + 'type' => 0, + 'sign' => 'refund', + ]; + $Chain['refund'] = $outOrderRefundCurve; + + //支出金额 + $outTotalMoney = bcadd($outYeMoney, $outExtractMoney, 2); + $lastOutTotalMoney = bcadd($lastOutYeMoney, $lastOutExtractMoney, 2); + $outTotalCurve = $this->totalArrData([$outYeCurve, $OutExtractCurve]); + $outTotalChain = $this->countRate($outTotalMoney, $lastOutTotalMoney); + $topData[6] = [ + 'title' => '支出金额', + 'desc' => '余额支付金额、支付佣金金额', + 'total_money' => $outTotalMoney, + 'rate' => $outTotalChain, + 'value' => $outTotalCurve['y'], + 'type' => 1, + 'sign' => 'out', + ]; + $Chain['out'] = $outTotalCurve; + + /** 交易毛利金额*/ + $jiaoyiMoney = $this->tradeTotalMoney($where, "sum"); + + $jiaoyiMoney = bcsub($jiaoyiMoney, $outTotalMoney, 2); + $lastJiaoyiMoney = $this->tradeTotalMoney($dateWhere, "sum", $isNum); + $lastJiaoyiMoney = bcsub($lastJiaoyiMoney, $lastOutTotalMoney, 2); + $jiaoyiCurve = $this->tradeGroupMoney($where, "group"); + $jiaoyiCurve = $this->subdutionArrData($jiaoyiCurve, $outTotalCurve); + $jiaoyiChain = $this->countRate($jiaoyiMoney, $lastJiaoyiMoney); + $topData[1] = [ + 'title' => '交易毛利金额', + 'desc' => '交易毛利金额 = 营业额 - 支出金额', + 'total_money' => $jiaoyiMoney, + 'rate' => $jiaoyiChain, + 'value' => $jiaoyiCurve['y'], + 'type' => 1, + 'sign' => 'jiaoyi', + ]; + $Chain['jiaoyi'] = $jiaoyiCurve; + + /** @var 营业额 $inTotalMoney */ + $inTotalMoney = $this->tradeTotalMoney($where, "sum"); + $lastInTotalMoney = $this->tradeTotalMoney($dateWhere, "sum", $isNum); + $inTotalCurve = $this->tradeGroupMoney($where, "group"); + $inTotalChain = $this->countRate($inTotalMoney, $lastInTotalMoney); + $topData[0] = [ + 'title' => '营业额', + 'desc' => '商品支付金额、充值金额、购买付费会员金额、线下收银金额', + 'total_money' => $inTotalMoney, + 'rate' => $inTotalChain, + 'value' => $inTotalCurve['y'], + 'type' => 1, + 'sign' => 'in', + ]; + ksort($topData); + $data = []; + foreach ($topData as $k => $v) { + $data['x'] = $Chain['out']['x']; + $data['series'][$k]['name'] = $v['title']; + $data['series'][$k]['desc'] = $v['desc']; + $data['series'][$k]['money'] = $v['total_money']; + $data['series'][$k]['type'] = $v['type']; + $data['series'][$k]['rate'] = $v['rate']; + $data['series'][$k]['value'] = array_values($v['value']); + } + $export = $exportService->tradeData($data, '交易统计', 2); + $data['export'] = $export[0]; + return $data; + } + + /** + * 多个数组相加 + * @param array $arr + * @return array|false + */ + public function totalArrData(array $arr) + { + if (!$arr || !is_array($arr)) return false; + $item = array(); + $y = array_column($arr, "y"); + $x = array_column($arr, "x")[0]; + foreach ($y as $key => $value) { + foreach ($value as $k => $v) { + if (isset($item[$k])) { + $item[$k] = bcadd($item[$k], $v, 2); + } else { + $item[$k] = $v; + } + } + } + return ['x' => $x, 'y' => $item]; + } + + /** + * 数组相减 + * @param array $arr1 + * @param array $arr2 + * @return array + */ + public function subdutionArrData(array $arr1, array $arr2) + { + $item = array(); + foreach ($arr1['y'] as $key => $value) { + $item['y'][$key] = bcsub($value, $arr2['y'][$key], 2); + } + $item['x'] = $arr1['x']; + return $item; + } + + /** + * 搜索时间转换 + * @param $timeKey + * @param false $isNum + * @return array + * @throws \Exception + */ + public function TimeConvert($timeKey, $isNum = false) + { + switch ($timeKey) { + case "today" : + $data['start_time'] = date('Y-m-d 00:00:00', time()); + $data['end_time'] = date('Y-m-d 23:59:59', time()); + $data['days'] = 1; + break; + case "yestoday" : + $data['start_time'] = date('Y-m-d 00:00:00', strtotime('-1 day')); + $data['end_time'] = date('Y-m-d 23:59:59', strtotime('-1 day')); + $data['days'] = 1; + break; + case "last_month" : + $data['start_time'] = date('Y-m-01 00:00:00', strtotime('-1 month')); + $data['end_time'] = date('Y-m-t 23:59:59', strtotime('-1 month')); + $data['days'] = 30; + break; + case "month" : + $data['start_time'] = $month_start_time = date('Y-m-01 00:00:00', strtotime(date("Y-m-d"))); + $data['end_time'] = date('Y-m-d 23:59:59', strtotime("$month_start_time +1 month -1 day")); + $data['days'] = 30; + break; + case "year" : + $data['start_time'] = date('Y-01-01 00:00:00', time()); + $data['end_time'] = date('Y-12-t 23:59:59', time()); + $data['days'] = 365; + break; + case "last_year" : + $data['start_time'] = date('Y-01-01 00:00:00', strtotime('-1 year')); + $data['end_time'] = date('Y-12-t 23:59:59', strtotime('-1 year')); + $data['days'] = 365; + break; + case 30 : + case 15 : + case 7 : + if (!$isNum) { + $data['start_time'] = date("Y-m-d 00:00:00", strtotime("-$timeKey day")); + $data['end_time'] = date('Y-m-d 23:59:59', time()); + $data['days'] = $timeKey; + } else { + $day = $timeKey * 2; + $data['start_time'] = date("Y-m-d 00:00:00", strtotime("-$day day")); + $data['end_time'] = date("Y-m-d 23:59:59", strtotime("-$timeKey day")); + $data['days'] = $timeKey; + } + break; + default: + $datetime_start = new \DateTime($timeKey['start_time']); + $datetime_end = new \DateTime($timeKey['end_time']); + $days = $datetime_start->diff($datetime_end)->days; + $days = $days > 0 ? $days : 1; + if (!$isNum) { + $data['start_time'] = $timeKey['start_time']; + $data['end_time'] = $timeKey['end_time']; + $data['days'] = $days; + } else { + $data['start_time'] = date("Y-m-d 00:00:00", strtotime("-$days day")); + $data['end_time'] = $timeKey['start_time']; + $data['days'] = $days; + } + + } + + return $data; + } + + /** + * 获取订单退款 + * @param $where + * @param string $selectType + * @param string $group + * @param bool $isNum + * @return array|float|int + * @throws \Exception + */ + public function getOrderRefundTotalMoney($where, string $selectType, string $group = '', bool $isNum = false) + { + $orderSumField = isset($where['refund_type']) ? "refunded_price" : "refund_price"; + $whereOrderMoner['refund_type'] = isset($where['refund_type']) ? $where['refund_type'] : 6; + $whereOrderMoner['is_cancel'] = 0; + $whereOrderMoner['timeKey'] = $this->TimeConvert($where['time'], $isNum); + + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + $totalMoney = $storeOrderRefundServices->getOrderRefundMoneyByWhere($whereOrderMoner, $orderSumField, $selectType, $group); + if ($group) { + $totalMoney = $this->trendYdata((array)$totalMoney, $whereOrderMoner['timeKey']); + } + return $totalMoney; + } + + /** + * 获取商品营收 + * @param $where + * @param string $selectType + * @param string $group + * @param bool $isNum + * @return array|float|int + * @throws \Exception + */ + public function getOrderTotalMoney($where, string $selectType, string $group = "", bool $isNum = false) + { + /** 普通商品订单支付金额 */ + /** @var StoreOrderServices $storeOrderService */ + $storeOrderService = app()->make(StoreOrderServices::class); + $orderSumField = isset($where['refund_status']) ? "refund_price" : "pay_price"; + $whereOrderMoner['refund_status'] = isset($where['refund_status']) ? $where['refund_status'] : [0, 3]; + $whereOrderMoner['paid'] = 1; + $whereOrderMoner['pid'] = 0; + + if (isset($where['pay_type'])) { + $whereOrderMoner['pay_type'] = $where['pay_type']; + } else { + // $whereOrderMoner['pay_type_no'] = 2; + } + $whereOrderMoner['timeKey'] = $this->TimeConvert($where['time'], $isNum); + $totalMoney = $storeOrderService->getOrderMoneyByWhere($whereOrderMoner, $orderSumField, $selectType, $group); + + if ($group) { + $totalMoney = $this->trendYdata((array)$totalMoney, $whereOrderMoner['timeKey']); + } + return $totalMoney; + } + + /** + * 支付佣金 + * @param $where + * @param string $selectType + * @param string $group + * @param bool $isNum + * @return array|float|mixed + * @throws \Exception + */ + public function getExtractTotalMoney($where, string $selectType, string $group = "", bool $isNum = false) + { + /** 普通商品订单支付金额 */ + /** @var UserExtractServices $extractService */ + $extractService = app()->make(UserExtractServices::class); + $orderSumField = "extract_price"; + $whereData['status'] = 1; + $whereData['timeKey'] = $this->TimeConvert($where['time'], $isNum); + $totalMoney = $extractService->getOutMoneyByWhere($whereData, $orderSumField, $selectType, $group); + if ($group) { + $totalMoney = $this->trendYdata((array)$totalMoney, $whereData['timeKey']); + } + return $totalMoney; + } + + /** + * 获取用户充值营收 + * @param array $where + * @param string $selectType + * @param string $group + * @param bool $isNum + * @return array|float|int + * @throws \Exception + */ + public function getRechargeTotalMoney(array $where, string $selectType, string $group = "", bool $isNum = false) + { + /** 用户充值金额 */ + /** @var UserRechargeServices $userRechageService */ + $userRechageService = app()->make(UserRechargeServices::class); + $rechargeSumField = "price"; + $whereInRecharge['paid'] = 1; + $whereInRecharge['refund_price'] = '0.00'; + $whereInRecharge['timeKey'] = $this->TimeConvert($where['time'], $isNum); + $whereInRecharge['store_id'] = 0; + $totalMoney = $userRechageService->getRechargeMoneyByWhere($whereInRecharge, $rechargeSumField, $selectType, $group); + if ($group) { + $totalMoney = $this->trendYdata((array)$totalMoney, $whereInRecharge['timeKey']); + } + return $totalMoney; + } + + /** + * 后台手动充值 + * @param array $where + * @param string $selectType + * @param string $group + * @param bool $isNum + * @return array|float|int + * @throws \Exception + */ + public function getBillYeTotalMoney(array $where, string $selectType, string $group = "", bool $isNum = false) + { + /** 后台用户充值金额 */ + $rechargeSumField = "number"; + $whereInRecharge['pm'] = 1; + $whereInRecharge['type'] = 'system_add'; + $whereInRecharge['timeKey'] = $this->TimeConvert($where['time'], $isNum); + $whereInRecharge['store_id'] = 0; + /** @var UserMoneyServices $userMoneyServices */ + $userMoneyServices = app()->make(UserMoneyServices::class); + $totalMoney = $userMoneyServices->getRechargeMoneyByWhere($whereInRecharge, $rechargeSumField, $selectType, $group); + if ($group) { + $totalMoney = $this->trendYdata((array)$totalMoney, $whereInRecharge['timeKey']); + } + return $totalMoney; + } + + /** + * 购买会员总额 + * @param array $where + * @param string $selectType + * @param string $group + * @param bool $isNum + * @return array|mixed + * @throws \Exception + */ + public function getMemberTotalMoney(array $where, string $selectType, string $group = "", bool $isNum = false) + { + + /** 购买会员 */ + /** @var OtherOrderServices $otherOrderService */ + $otherOrderService = app()->make(OtherOrderServices::class); + $memberSumField = "pay_price"; + $whereInMember['type'] = 1; + $whereInMember['paid'] = 1; + $whereInMember['store_id'] = 0; + if (isset($where['pay_type'])) { + $whereInMember['pay_type'] = $where['pay_type']; + } else { + //$whereInMember['pay_type_no'] = 'yue'; + } + $whereInMember['timeKey'] = $this->TimeConvert($where['time'], $isNum); + $totalMoney = $otherOrderService->getMemberMoneyByWhere($whereInMember, $memberSumField, $selectType, $group); + if ($group) { + $totalMoney = $this->trendYdata((array)$totalMoney, $whereInMember['timeKey']); + } + return $totalMoney; + + } + + /** + * 线下付款总额 + * @param array $where + * @param string $selectType + * @param string $group + * @param bool $isNum + * @return array|mixed + * @throws \Exception + */ + public function getOfflineTotalMoney(array $where, string $selectType, string $group = "", bool $isNum = false) + { + /** 线下付款总额 */ + /** @var OtherOrderServices $otherOrderService */ + $otherOrderService = app()->make(OtherOrderServices::class); + $offlineSumField = "pay_price"; + $whereOffline['type'] = 3; + $whereOffline['paid'] = 1; + $whereOffline['store_id'] = 0; + // $whereOffline['pay_type_no'] = 'yue'; + $whereOffline['timeKey'] = $this->TimeConvert($where['time'], $isNum); + $totalMoney = $otherOrderService->getMemberMoneyByWhere($whereOffline, $offlineSumField, $selectType, $group); + if ($group) { + $totalMoney = $this->trendYdata((array)$totalMoney, $whereOffline['timeKey']); + } + return $totalMoney; + } + + /** + * 处理Y坐标数据 + * @param array $data + * @param array $timeKey + * @return array + * @throws \Exception + */ + public function trendYdata(array $data, array $timeKey) + { + $hourMoney = array(); + $timeData = array(); + //获取日期之间的天数 + $getDayRange = function ($date, $timeKey) { + $datearr = []; + $stime = strtotime($timeKey['start_time']); + $etime = strtotime($timeKey['end_time']); + while ($stime <= $etime) { + $datearr['x'][] = date($date, $stime); + $datearr['y'][] = date($date, $stime); + $stime = $stime + 86400; + } + return $datearr; + }; + //获取日期之间的月份 + $getMonthRange = function ($date, $timeKey) { + $datearr = []; + $stime = date('Y-m-d', strtotime($timeKey['start_time'])); + $etime = date('Y-m-d', strtotime($timeKey['end_time'])); + $start = new \DateTime($stime); + $end = new \DateTime($etime); + $interval = \DateInterval::createFromDateString('1 month'); + $period = new \DatePeriod($start, $interval, $end); + foreach ($period as $dt) { + $datearr['x'][] = $dt->format($date); + $datearr['y'][] = $dt->format($date); + } + return $datearr; + }; + if ($timeKey['days'] == 1) { + for ($i = 0; $i <= 24; $i++) { + $timeData['x'][] = (string)($i < 10 ? ('0' . $i) : $i); + $timeData['y'][] = $i < 10 ? ('0' . $i) : $i; + //$timeData['y'][] = $i < 10 ? ('0' . $i . ":00") : $i . ":00"; + //$timeData['x'][] = $i < 10 ? ('0' . $i . ":00") : $i . ":00"; + } + } elseif ($timeKey['days'] == 30) { + $timeData = $getDayRange('Y-m-d', $timeKey); + } elseif ($timeKey['days'] == 365) { + $timeData = $getMonthRange('Y-m', $timeKey); + } elseif ($timeKey['days'] > 1 && $timeKey['days'] < 30) { + $timeData = $getDayRange('Y-m-d', $timeKey); + } elseif ($timeKey['days'] > 30 && $timeKey['days'] < 365) { + $timeData = $getMonthRange('Y-m', $timeKey); + } + if ($data) { + $hourMoney = array_column($data, 'number', 'time'); + } + $y = array(); + foreach ($timeData['y'] as $k => $v) { + if (array_key_exists($v, $hourMoney)) { + $y[$v] = $hourMoney[$v]; + } else { + $y[$v] = 0; + } + } + return ['x' => $timeData['x'], 'y' => $y]; + } + + + /** + * 获取环比时间类型 + * @param $timeKey + * @return string + */ + public function chainTime($timeKey) + { + switch ($timeKey) { + case "today" : + return "yestoday"; + case "month" : + return "last_month"; + case "year" : + return "last_year"; + default : + return "other"; + } + + } + + +} diff --git a/app/services/store/DeliveryServiceServices.php b/app/services/store/DeliveryServiceServices.php new file mode 100644 index 0000000..61888b8 --- /dev/null +++ b/app/services/store/DeliveryServiceServices.php @@ -0,0 +1,337 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\store; + + +use app\dao\store\DeliveryServiceDao; +use app\services\BaseServices; +use app\services\order\store\BranchOrderServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder; +use think\exception\ValidateException; + + +/** + * 配送 + * Class DeliveryServiceServices + * @package app\services\store + * @mixin DeliveryServiceDao + */ +class DeliveryServiceServices extends BaseServices +{ + /** + * 创建form表单 + * @var Form + */ + protected $builder; + + /** + * 构造方法 + * DeliveryServiceServices constructor. + * @param DeliveryServiceDao $dao + * @param FormBuilder $builder + */ + public function __construct(DeliveryServiceDao $dao, FormBuilder $builder) + { + $this->dao = $dao; + $this->builder = $builder; + } + + /** + * 获取配送员详情 + * @param int $id + * @param string $field + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDeliveryInfo(int $id, string $field = '*') + { + $info = $this->dao->getOne(['id' => $id, 'is_del' => 0], $field); + if (!$info) { + throw new ValidateException('配送员不存在'); + } + return $info; + } + + /** + * 更具uid获取配送员信息 + * @param int $uid + * @param int $type + * @param int $relation_id + * @param string $field + * @return array|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDeliveryInfoByUid(int $uid, int $type = 0, int $relation_id = 0, string $field = '*') + { + $where = ['uid' => $uid, 'is_del' => 0, 'status' => 1, 'type' => $type, 'relation_id' => $relation_id]; + $info = $this->dao->getOne($where, $field); + if (!$info) { + throw new ValidateException('配送员不存在'); + } + return $info; + } + + /** + * 获取配送员所在门店id + * @param int $uid + * @param int $type + * @param string $field + * @param string $key + * @return array + */ + public function getDeliveryStoreIds(int $uid, int $type = 0, string $field = '*', string $key = '') + { + return $this->dao->getColumn(['uid' => $uid, 'type' => $type, 'is_del' => 0, 'status' => 1], $field, $key); + } + + /** + * 获取单个配送员统计信息 + * @param int $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function deliveryDetail(int $id) + { + $deliveryInfo = $this->getDeliveryInfo($id); + if (!$deliveryInfo) { + throw new AdminException('配送员不存在'); + } + $where = ['store_id' => $deliveryInfo['store_id'], 'delivery_uid' => $deliveryInfo['uid']]; + /** @var BranchOrderServices $orderServices */ + $orderServices = app()->make(BranchOrderServices::class); + $order_where = ['paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0]; + + $field = ['id', 'type', 'order_id', 'real_name', 'total_num', 'total_price', 'pay_price', 'FROM_UNIXTIME(pay_time,"%Y-%m-%d") as pay_time', 'paid', 'pay_type', 'activity_id', 'pink_id']; + $list = $orderServices->getStoreOrderList($where + $order_where, $field, [], true); + $data = ['id' => $id]; + $data['info'] = $deliveryInfo; + $data['list'] = $list; + $data['data'] = [ + [ + 'title' => '待配送订单数', + 'value' => $orderServices->count($where + $order_where + ['status' => 2]), + 'key' => '单', + ], + [ + 'title' => '已配送订单数', + 'value' => $orderServices->count($where + $order_where + ['status' => 3]), + 'key' => '单', + ], + [ + 'title' => '待配送订单金额', + 'value' => $orderServices->sum($where + $order_where + ['status' => 2], 'pay_price', true), + 'key' => '元', + ], + [ + 'title' => '已配送订单', + 'value' => $orderServices->sum($where + $order_where + ['status' => 3], 'pay_price', true), + 'key' => '元', + ] + ]; + return $data; + } + + /** + * 获取门店配送员订单统计 + * @param int $uid + * @param int $store_id + * @param string $time + * @return array + */ + public function getStoreData(int $uid, int $store_id, string $time = 'today') + { + $this->getDeliveryInfoByUid($uid, $store_id ? 1: 0, $store_id); + $data = []; + $where = ['delivery_uid' => $uid, 'time' => $time, 'paid' => 1, 'is_del' => 0, 'is_system_del' => 0, 'refund_status' => [0, 3]]; + if ($store_id) { + $where['store_id'] = $store_id; + } + /** @var BranchOrderServices $order */ + $order = app()->make(BranchOrderServices::class); + $where['status'] = 2; + $data['unsend'] = $order->count($where); + $where['status'] = 9; + $data['send'] = $order->count($where); + $data['send_price'] = $order->sum($where, 'pay_price', true); + return $data; + } + + /** + * 获取配送员列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getServiceList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getServiceList($where, $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取配送员列表 + * @param int $type + * @param int $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDeliveryList(int $type = 0, int $relation_id = 0) + { + [$page, $limit] = $this->getPageValue(); + $where = ['status' => 1, 'is_del' => 0, 'type' => $type, 'relation_id' => $relation_id]; + $list = $this->dao->getServiceList($where, $page, $limit); + foreach ($list as &$item) { + if (!$item['nickname']) $item['nickname'] = $item['wx_name']; + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 创建配送员表单 + * @param array $formData + * @return mixed + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createServiceForm(array $formData = []) + { + if ($formData) { + $field[] = $this->builder->frameImage('avatar', '配送员头像', $this->url(config('admin.admin_prefix') . '/widget.images/index', ['fodder' => 'avatar'], true), $formData['avatar'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + } else { + $field[] = $this->builder->frameImage('image', '商城用户', $this->url(config('admin.admin_prefix') . '/system.user/list', ['fodder' => 'image'], true))->icon('ios-add')->width('960px')->height('550px')->modal(['footer-hide' => true])->Props(['srcKey' => 'image']); + $field[] = $this->builder->hidden('uid', 0); + $field[] = $this->builder->hidden('avatar', ''); + } + $field[] = $this->builder->input('nickname', '配送员名称', $formData['nickname'] ?? '')->required('请填写配送员名称')->col(24); + $field[] = $this->builder->input('phone', '手机号码', $formData['phone'] ?? '')->required('请填写电话')->col(24)->maxlength(11); + $field[] = $this->builder->radio('status', '配送员状态', $formData['status'] ?? 1)->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]); + return $field; + } + + /** + * 创建配送员获取表单 + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function create() + { + return create_form('添加配送员', $this->createServiceForm(), $this->url('/order/delivery/save'), 'POST'); + } + + /** + * 编辑获取表单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function edit(int $id) + { + $serviceInfo = $this->dao->get($id); + if (!$serviceInfo) { + throw new AdminException('数据不存在!'); + } + return create_form('编辑配送员', $this->createServiceForm($serviceInfo->toArray()), $this->url('/order/delivery/update/' . $id), 'PUT'); + } + + /** + * 获取某人的聊天记录用户列表 + * @param int $uid + * @return array|array[] + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getChatUser(int $uid) + { + /** @var StoreServiceLogServices $serviceLog */ + $serviceLog = app()->make(StoreServiceLogServices::class); + /** @var UserServices $serviceUser */ + $serviceUser = app()->make(UserServices::class); + $uids = $serviceLog->getChatUserIds($uid); + if (!$uids) { + return []; + } + return $serviceUser->getUserList(['uid' => $uids], 'nickname,uid,avatar as headimgurl'); + } + + /** + * 检查用户是否是配送员 + * @param int $uid + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkoutIsService(int $uid) + { + return (bool)$this->dao->count(['uid' => $uid, 'status' => 1, 'is_del' => 0]); + } + + /** + * 添加门店配送员 + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createStoreDeliveryForm() + { + $field[] = $this->builder->frameImage('image', '商城用户', $this->url(config('admin.store_prefix') . '/system.User/list', ['fodder' => 'image'], true))->icon('ios-add')->width('960px')->height('430px')->modal(['footer-hide' => true])->Props(['srcKey' => 'image']); + $field[] = $this->builder->hidden('uid', 0); + $field[] = $this->builder->hidden('avatar', ''); + $field[] = $this->builder->input('nickname', '配送员名称')->required('请填写配送员名称')->col(24); + $field[] = $this->builder->input('phone', '手机号码')->required('请填写电话')->col(24)->maxlength(11); + $field[] = $this->builder->radio('status', '配送员状态', 1)->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]); + return create_form('添加门店配送员', $field, $this->url('/staff/delivery')); + } + + /** + * 编辑门店配送员 + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function updateStoreDeliveryForm($id, $formData) + { + $field[] = $this->builder->input('nickname', '配送员名称', $formData['nickname'] ?? '')->required('请填写配送员名称')->col(24); + $field[] = $this->builder->frameImage('avatar', '配送员头像', $this->url(config('admin.store_prefix') . '/widget.images/index', ['fodder' => 'avatar'], true), $formData['avatar'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + $field[] = $this->builder->input('phone', '手机号码', $formData['phone'] ?? '')->required('请填写电话')->col(24)->maxlength(11); + $field[] = $this->builder->radio('status', '配送员状态', $formData['status'] ?? 1)->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]); + return create_form('编辑门店配送员', $field, $this->url('/staff/delivery/' . $id), 'put'); + } + + /** + * 获取配送员select(Uid) + * @param array $where + * @return mixed + */ + public function getSelectList($where = []) + { + $list = $this->dao->getSelectList($where); + $menus = []; + foreach ($list as $menu) { + $menus[] = ['value' => $menu['uid'], 'label' => $menu['nickname']]; + } + return $menus; + } +} diff --git a/app/services/store/StoreConfigServices.php b/app/services/store/StoreConfigServices.php new file mode 100644 index 0000000..0237e0f --- /dev/null +++ b/app/services/store/StoreConfigServices.php @@ -0,0 +1,258 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\store; + + +use app\dao\store\StoreConfigDao; +use app\services\BaseServices; +use app\services\system\config\SystemConfigServices; +use crmeb\form\Build; +use crmeb\services\SystemConfigService; +use think\exception\ValidateException; + +/** + * Class StoreConfigServices + * @package app\services\store + * @mixin StoreConfigDao + */ +class StoreConfigServices extends BaseServices +{ + + /** + * 表单数据切割符号 + * @var string + */ + protected $cuttingStr = '=>'; + + //打印机配置 + const PRINTER_KEY = [ + 'store_printing_timing', 'store_terminal_number', 'store_printing_client_id', + 'store_printing_api_key', 'store_develop_id', 'store_pay_success_printing_switch', + 'store_print_type', 'store_fey_user', 'store_fey_ukey', 'store_fey_sn', + ]; + //快递发货配置 + const EXPRESS_KEY = [ + 'store_config_export_id', 'store_config_export_temp_id', 'store_config_export_to_name', + 'store_config_export_to_tel', 'store_config_export_to_address', 'store_config_export_siid', 'store_config_export_open' + ]; + + //桌码配置 + const TABLE_CODE = [ + 'store_code_switch', 'store_checkout_method', 'store_number_diners_window' + ]; + + const CONFIG_TYPE = [ + 'store_printing_deploy' => self::PRINTER_KEY, + 'store_electronic_sheet' => self::EXPRESS_KEY, + 'store_table_code' => self::TABLE_CODE + ]; + + /** + * StoreConfigServices constructor. + * @param StoreConfigDao $dao + */ + public function __construct(StoreConfigDao $dao) + { + $this->dao = $dao; + } + + /** + * 保存或者更新门店配置 + * @param array $data + * @param int $storeId + */ + public function saveConfig(array $data, int $type = 1, int $relation_id = 0) + { + $config = []; + foreach ($data as $key => $value) { + if ($this->dao->count(['key_name' => $key, 'type' => $type, 'relation_id' => $relation_id])) { + $this->dao->update(['key_name' => $key, 'type' => $type, 'relation_id' => $relation_id], ['value' => json_encode($value)]); + } else { + $config[] = [ + 'key_name' => $key, + 'type' => $type, + 'relation_id' => $relation_id, + 'value' => json_encode($value) + ]; + } + } + if ($config) { + $this->dao->saveAll($config); + } + } + + /** + * 获取配置 + * @param string $key + * @param int $type + * @param int $relation_id + * @param $default + * @return mixed|null + */ + public function getConfig(string $key, int $type = 0, int $relation_id = 0, $default = null) + { + $value = $this->dao->value(['key_name' => $key, 'type' => $type, 'relation_id' => $relation_id], 'value'); + return is_null($value) ? $default : json_decode($value, true); + } + + /** + * 获取门店配置 + * @param int $storeId + * @param array $keys + */ + public function getConfigAll(array $keys, int $type = 0, int $relation_id = 0) + { + $confing = $this->dao->searchs($keys, $type, $relation_id)->column('value', 'key_name'); + return array_map(function ($item) { + return json_decode($item, true); + }, $confing); + } + + public function getOptions(string $parameter) + { + $parameter = explode("\n", $parameter); + $options = []; + foreach ($parameter as $v) { + if (strstr($v, $this->cuttingStr) !== false) { + $pdata = explode($this->cuttingStr, $v); + $options[] = ['label' => $pdata[1], 'value' => (int)$pdata[0]]; + } + } + return $options; + } + + /** + * @param array $configName + * @param int $type + * @param int $relation_id + * @param int $group + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getConfigAllField(array $configName = [], int $type = 0, int $relation_id = 0, int $group = 0) + { + /** @var SystemConfigServices $systemConfigServices */ + $systemConfigServices = app()->make(SystemConfigServices::class); + $list = $systemConfigServices->getConfigAllListByWhere(['menu_name' => $configName], $type, $relation_id, ['menu_name', 'info', 'type', 'value', 'desc', 'parameter']); + if ($list) { + foreach ($list as &$item) { + if ($relation_id != 0) { + $item['value'] = $item['store_value'] ?? ''; + } + $item['value'] = json_decode($item['value'], true); + } + $list = array_combine(array_column($list, 'menu_name'), $list); + } + + $value = []; + foreach ($configName as $key) { + if ($group) { + $value[$key] = $list[$key]['value'] ?? ''; + } else { + $value[$key] = $list[$key] ?? ['info' => '', 'type' => 'text', 'value' => '', 'desc' => '', 'parameter' => '']; + } + } + return $value; + } + + /** + * 获取表单 + * @param string $name + * @param int $type + * @param int $relation_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFormBuildRule(string $name, int $type = 0, int $relation_id = 0) + { + switch ($name) { + case 'third'://第三方 + $data = $this->thirdPartyFormBuild($type, $relation_id); + break; + default: + throw new ValidateException('类型错误'); + } + return $data; + } + + /** + * 第三方配置 + * @return array + */ + public function thirdPartyFormBuild(int $type = 0, int $relation_id = 0) + { + $build = new Build(); + $build->url('system/config'); + if ($type == 1) { + $data = $this->getConfigAllField([ + 'store_pay_success_printing_switch', 'store_develop_id', 'store_printing_api_key', 'store_printing_client_id', 'store_terminal_number', + 'store_print_type', 'store_fey_user', 'store_fey_ukey', 'store_fey_sn', + 'store_printing_timing', + 'store_config_export_open', 'store_config_export_siid', 'store_config_export_to_name', 'store_config_export_to_tel', 'store_config_export_to_address', + ], $type, $relation_id); + + $build->rule([ + Build::tabs()->option('小票打印配置', [ + Build::switch('store_pay_success_printing_switch', $data['store_pay_success_printing_switch']['info'], (int)($data['store_pay_success_printing_switch']['value'] ?? 0))->control(1, [ + Build::radio('store_printing_timing', $data['store_printing_timing']['info'], $data['store_printing_timing']['value'])->options($this->getOptions($data['store_printing_timing']['parameter']))->info($data['store_printing_timing']['desc']), + Build::radio('store_print_type', $data['store_print_type']['info'], in_array($data['store_print_type']['value'], [1, 2]) ? $data['store_print_type']['value'] : 1)->control(1, [ + Build::input('store_develop_id', $data['store_develop_id']['info'], $data['store_develop_id']['value'])->info($data['store_develop_id']['desc']), + Build::input('store_printing_api_key', $data['store_printing_api_key']['info'], $data['store_printing_api_key']['value'])->info($data['store_printing_api_key']['desc']), + Build::input('store_printing_client_id', $data['store_printing_client_id']['info'], $data['store_printing_client_id']['value'])->info($data['store_printing_client_id']['desc']), + Build::input('store_terminal_number', $data['store_terminal_number']['info'], $data['store_terminal_number']['value'])->info($data['store_terminal_number']['desc']), + ])->control(2, [ + Build::input('store_fey_user', $data['store_fey_user']['info'], $data['store_fey_user']['value'])->info($data['store_fey_user']['desc']), + Build::input('store_fey_ukey', $data['store_fey_ukey']['info'], $data['store_fey_ukey']['value'])->info($data['store_fey_ukey']['desc']), + Build::input('store_fey_sn', $data['store_fey_sn']['info'], $data['store_fey_sn']['value'])->info($data['store_fey_sn']['desc']) + ])->options($this->getOptions($data['store_print_type']['parameter']))->info($data['store_print_type']['desc']) + ])->trueValue('打开', 1)->falseValue('关闭', 0), + ])->option('电子面单', [ + Build::radio('store_config_export_open', $data['store_config_export_open']['info'], (int)($data['store_config_export_open']['value'] ?? 0))->control(1, [ + Build::input('store_config_export_to_name', $data['store_config_export_to_name']['info'], $data['store_config_export_to_name']['value'])->info($data['store_config_export_to_name']['desc']), + Build::input('store_config_export_to_tel', $data['store_config_export_to_tel']['info'], $data['store_config_export_to_tel']['value'])->info($data['store_config_export_to_tel']['desc']), + Build::input('store_config_export_to_address', $data['store_config_export_to_address']['info'], $data['store_config_export_to_address']['value'])->info($data['store_config_export_to_address']['desc']), + Build::input('store_config_export_siid', $data['store_config_export_siid']['info'], $data['store_config_export_siid']['value'])->info($data['store_config_export_siid']['desc']), + ])->options($this->getOptions($data['store_config_export_open']['parameter']))->info($data['store_config_export_open']['desc']) + ]) + ]); + } elseif ($type = 2) { + $data = $this->getConfigAllField([ + 'store_pay_success_printing_switch', 'store_develop_id', 'store_printing_api_key', 'store_printing_client_id', 'store_terminal_number', + 'store_print_type', 'store_fey_user', 'store_fey_ukey', 'store_fey_sn', + ], $type, $relation_id); + + $build->rule([ + Build::tabs()->option('小票打印配置', [ + Build::switch('store_pay_success_printing_switch', $data['store_pay_success_printing_switch']['info'], (int)($data['store_pay_success_printing_switch']['value'] ?? 0))->control(1, [ + Build::radio('store_print_type', $data['store_print_type']['info'], in_array($data['store_print_type']['value'], [1, 2]) ? $data['store_print_type']['value'] : 1)->control(1, [ + Build::input('store_develop_id', $data['store_develop_id']['info'], $data['store_develop_id']['value'])->info($data['store_develop_id']['desc']), + Build::input('store_printing_api_key', $data['store_printing_api_key']['info'], $data['store_printing_api_key']['value'])->info($data['store_printing_api_key']['desc']), + Build::input('store_printing_client_id', $data['store_printing_client_id']['info'], $data['store_printing_client_id']['value'])->info($data['store_printing_client_id']['desc']), + Build::input('store_terminal_number', $data['store_terminal_number']['info'], $data['store_terminal_number']['value'])->info($data['store_terminal_number']['desc']), + ])->control(2, [ + Build::input('store_fey_user', $data['store_fey_user']['info'], $data['store_fey_user']['value'])->info($data['store_fey_user']['desc']), + Build::input('store_fey_ukey', $data['store_fey_ukey']['info'], $data['store_fey_ukey']['value'])->info($data['store_fey_ukey']['desc']), + Build::input('store_fey_sn', $data['store_fey_sn']['info'], $data['store_fey_sn']['value'])->info($data['store_fey_sn']['desc']) + ])->options($this->getOptions($data['store_print_type']['parameter']))->info($data['store_print_type']['desc']) + ])->trueValue('打开', 1)->falseValue('关闭', 0), + ]) + ]); + } + return $build->toArray(); + } + + +} diff --git a/app/services/store/StoreUserServices.php b/app/services/store/StoreUserServices.php new file mode 100644 index 0000000..e9449ea --- /dev/null +++ b/app/services/store/StoreUserServices.php @@ -0,0 +1,123 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\store; + + +use app\dao\store\StoreUserDao; +use app\services\BaseServices; +use app\services\user\level\SystemUserLevelServices; +use app\services\user\level\UserLevelServices; +use app\services\user\UserServices; +use think\exception\ValidateException; + +/** + * 门店用户 + * Class StoreUser + * @package app\services\store + * @mixin StoreUserDao + */ +class StoreUserServices extends BaseServices +{ + /** + * 构造方法 + * StoreUser constructor. + * @param StoreUserDao $dao + */ + public function __construct(StoreUserDao $dao) + { + $this->dao = $dao; + } + + /** + * @param array $where + * @param int $store_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function index(array $where, int $store_id) + { + $where['is_filter_del'] = 1; + $where['store_id'] = $store_id; + /** @var UserStoreUserServices $userStoreUserServices */ + $userStoreUserServices = app()->make(UserStoreUserServices::class); + $fields = 'u.*'; + [$list, $count] = $userStoreUserServices->getWhereUserList($where, $fields); + if ($list) { + $uids = array_column($list, 'uid'); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userlabel = $userServices->getUserLablel($uids, 1, $store_id); + $levelName = app()->make(SystemUserLevelServices::class)->getUsersLevel(array_unique(array_column($list, 'level'))); + $userLevel = app()->make(UserLevelServices::class)->getUsersLevelInfo($uids); + $spread_names = $userServices->getColumn([['uid', 'in', $uids]], 'nickname', 'uid'); + /** @var SystemStoreStaffServices $staffServices */ + $staffServices = app()->make(SystemStoreStaffServices::class); + $staffNames = $staffServices->getColumn([['store_id', '=', $store_id], ['uid', 'in', array_column($list, 'uid')], ['is_del', '=', 0]], 'uid,staff_name', 'uid'); + foreach ($list as &$item) { + $item['status'] = ($item['status'] == 1) ? '正常' : '禁止'; + $item['birthday'] = $item['birthday'] ? date('Y-m-d', (int)$item['birthday']) : ''; + $item['spread_uid_nickname'] = $item['spread_uid'] ? ($spread_names[$item['spread_uid']] ?? '') . '/' . $item['spread_uid'] : '无'; + $item['staff_name'] = $staffNames[$item['uid']]['staff_name'] ?? ''; + //用户类型 + if ($item['user_type'] == 'routine') { + $item['user_type'] = '小程序'; + } else if ($item['user_type'] == 'wechat') { + $item['user_type'] = '公众号'; + } else if ($item['user_type'] == 'h5') { + $item['user_type'] = 'H5'; + } else if ($item['user_type'] == 'pc') { + $item['user_type'] = 'PC'; + } else if ($item['user_type'] == 'app') { + $item['user_type'] = 'APP'; + } else $item['user_type'] = '其他'; + //等级名称 + $item['level'] = $levelName[$item['level']] ?? '无'; + //用户等级 + $item['vip_name'] = false; + $levelinfo = $userLevel[$item['uid']] ?? null; + if ($levelinfo) { + if ($levelinfo && ($levelinfo['is_forever'] || time() < $levelinfo['valid_time'])) { + $item['vip_name'] = $item['level'] != '无' ? $item['level'] : false; + } + } + $item['labels'] = $userlabel[$item['uid']] ?? ''; + $item['isMember'] = $item['is_money_level'] > 0 ? 1 : 0; + } + } + + return compact('count', 'list'); + } + + + /** + * 同步写入门店用户 + * @param int $uid + * @param int $store_id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setStoreUser(int $uid, int $store_id) + { + $storeUser = $this->dao->getOne(['uid' => $uid, 'store_id' => $store_id]); + if (!$storeUser) { + if (!$this->dao->save(['uid' => $uid, 'store_id' => $store_id, 'add_time' => time()])) { + throw new ValidateException('新增门店用户失败'); + } + } + return true; + } + +} diff --git a/app/services/store/SystemStoreServices.php b/app/services/store/SystemStoreServices.php new file mode 100644 index 0000000..1bf9ba4 --- /dev/null +++ b/app/services/store/SystemStoreServices.php @@ -0,0 +1,740 @@ + +// +---------------------------------------------------------------------- +namespace app\services\store; + +use app\dao\store\SystemStoreDao; +use app\services\BaseServices; +use app\services\order\store\BranchOrderServices; +use app\services\order\StoreDeliveryOrderServices; +use app\services\order\StoreOrderCartInfoServices; +use app\services\order\StoreOrderServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\product\sku\StoreProductAttrValueServices; +use app\services\store\finance\StoreFinanceFlowServices; +use app\services\system\SystemRoleServices; +use crmeb\exceptions\AdminException; +use crmeb\services\erp\Erp; +use crmeb\services\FormBuilder; +use think\exception\ValidateException; +use think\facade\Cache; +use think\facade\Log; + + +/** + * 门店 + * Class SystemStoreServices + * @package app\services\system\store + * @mixin SystemStoreDao + */ +class SystemStoreServices extends BaseServices +{ + /** + * 创建form表单 + * @var Form + */ + protected $builder; + + /** + * 构造方法 + * SystemStoreServices constructor. + * @param SystemStoreDao $dao + * @param FormBuilder $builder + */ + public function __construct(SystemStoreDao $dao, FormBuilder $builder) + { + $this->dao = $dao; + $this->builder = $builder; + } + + /** + * 获取单个门店信息 + * @param int $id + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreInfo(int $id) + { + $storeInfo = $this->dao->getOne(['id' => $id, 'is_del' => 0]); + if (!$storeInfo) { + throw new ValidateException('获取门店信息失败'); + } + $storeInfo['day_time'] = $storeInfo['day_time'] ? explode('-', $storeInfo['day_time']) : []; + return $storeInfo->toArray(); + } + + /** + * 附近门店 + * @param array $where + * @param string $latitude + * @param string $longitude + * @param string $ip + * @param int $limit + * @return array|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getNearbyStore(array $where, string $latitude, string $longitude, string $ip = '', int $limit = 0) + { + $where = array_merge($where, ['type' => 0]); + if ($limit) { + $page = 1; + $field = ['*']; + } else { + [$page, $limit] = $this->getPageValue(); + $field = ['id', 'name', 'phone', 'image', 'latitude', 'longitude', 'address', 'detailed_address', 'is_show', 'day_time', 'day_start', 'day_end', 'valid_range']; + } + //默认附近门店 + $store_type = $where['store_type'] ?? 1; + $uid = $where['uid'] ?? 0; + unset($where['store_type'], $where['uid']); + if ($store_type != 1) {//常用门店 + if ($uid) { + /** @var StoreUserServices $storeUserServices */ + $storeUserServices = app()->make(StoreUserServices::class); + $ids = $storeUserServices->getColumn(['uid' => $uid], 'store_id'); + if (!$ids) { + return []; + } + $where['ids'] = $ids; + } else {//没登录,无常用门店 + return []; + } + } + $storeList = []; + + if (isset($where['id']) && $where['id']) { + $storeList = $this->dao->getStoreList($where, $field, $page, $limit, [], $latitude, $longitude, $latitude && $longitude ? 1 : 0); + } elseif ($latitude && $longitude) { + $storeList = $this->dao->getStoreList($where, $field, $page, $limit, [], $latitude, $longitude, 1); + } elseif ((isset($where['province']) && $where['province']) || (isset($where['city']) && $where['city']) || (isset($where['area']) && $where['area'])) { + $storeList = $this->dao->getStoreList($where, $field, $page, $limit, [], $latitude, $longitude, $latitude && $longitude ? 1 : 0); + } elseif ((isset($where['province']) && $where['province']) && (isset($where['city']) && $where['city']) && (isset($where['area']) && $where['area']) && !$latitude && !$longitude) { + $storeList = $this->dao->getStoreList($where, $field, $page, $limit); + } elseif ($ip) { + $strField = $limit == 1 ? '*' : implode(',', $field); + $addressArr = $this->addressHandle($this->convertIp($ip)); + $city = $addressArr['city'] ?? ''; + if ($city) { + $storeList = $this->dao->getStoreByAddressInfo($city, $where, $strField, $page, $limit); + } + $province = $addressArr['province'] ?? ''; + if (!$storeList && $province) { + $storeList = $this->dao->getStoreByAddressInfo($province, $where, $strField, $page, $limit); + } + } + //上面条件都没获取到门店 + if (!$storeList) { + $storeList = $this->dao->getStoreList($where, $field, $page, $limit); + } + if ($storeList) { + foreach ($storeList as &$item) { + $item['range'] = 0; + if (isset($item['distance'])) { + $item['range'] = bcdiv($item['distance'], '1000', 1); + } else { + $item['range'] = 0; + } + if (isset($item['is_show']) && $item['is_show'] == 1) { + $item['status_name'] = '营业中'; + } else { + $item['status_name'] = '已停业'; + } + if ($item['day_start'] && $item['day_end']) { + $start = date('H:i', strtotime($item['day_start'])); + $end = date('H:i', strtotime($item['day_end'])); + $item['day_time'] = implode(' - ', [$start, $end]); + } + } + } + return $limit == 1 ? ($storeList[0] ?? []) : $storeList; + } + + /** + * 获取门店 + * @param array $where + * @param array $field + * @param string $latitude + * @param string $longitude + * @param int $product_id + * @param array $with + * @param int $type 0:普通1:秒杀 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreList(array $where, array $field = ['*'], string $latitude = '', string $longitude = '', int $product_id = 0, array $with = [], int $type = 0, $store_id = 0) + { + [$page, $limit] = $this->getPageValue(); + $order = 0; + if (isset($where['order_id']) && $where['order_id']) { + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $user_location = $storeOrderServices->value(['id' => $where['order_id']], 'user_location'); + [$longitude, $latitude] = explode(' ', $user_location); + } + if ($longitude && $latitude) { + $order = 1; + } + $list = []; + $count = 0; + //该商品上架的门店 + if ($product_id) { + /** @var StoreBranchProductServices $productServices */ + $productServices = app()->make(StoreBranchProductServices::class); + [$ids, $applicableType] = $productServices->getApplicableStoreIds($product_id, $type); + if ($ids) { + if ($store_id && !in_array($store_id, $ids)) { + $store_id = 0; + } + $where['ids'] = $ids; + } else { + if ($applicableType != 1) {//不是所有门店 + return compact('list', 'count'); + } + } + } + $oid = (int)($where['order_id'] ?? 0); + unset($where['order_id']); + $storeList = $this->dao->getStoreList($where, $field, $page, $limit, $with, $latitude, $longitude, $order); + $storeIds = []; + if ($oid) { + [$storeIds, $cartInfo] = $this->checkOrderProductShare($oid, $storeList, 2); + } + + $prefix = config('admin.store_prefix'); + foreach ($storeList as &$item) { + if (isset($item['distance'])) { + $item['range'] = bcdiv($item['distance'], '1000', 1); + } else { + $item['range'] = 0; + } + if ($item['is_show'] == 1) { + $item['status_name'] = '营业中'; + } else { + $item['status_name'] = '已停业'; + } + $item['prefix'] = $prefix; + if ($oid) { + if (in_array($item['id'], $storeIds)) { + $list[] = $item; + } + } else { + $list[] = $item; + } + } + if (count($list) && $store_id) { + $arr = []; + foreach ($list as $key => $value) { + if ($value['id'] == $store_id) { + $arr = $value; + unset($list[$key]); + } + } + array_unshift($list, $arr); + } + if ($oid) { + $count = count($list); + } else { + $count = $this->dao->count($where); + } + return compact('list', 'count'); + } + + /** + * 获取提货点头部统计信息 + * @return mixed + */ + public function getStoreData() + { + $data['show'] = [ + 'name' => '显示中的提货点', + 'num' => $this->dao->count(['type' => 0]), + ]; + $data['hide'] = [ + 'name' => '隐藏中的提货点', + 'num' => $this->dao->count(['type' => 1]), + ]; + $data['recycle'] = [ + 'name' => '回收站的提货点', + 'num' => $this->dao->count(['type' => 2]) + ]; + return $data; + } + + /** + * 门店重置账号密码表单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function storeAdminAccountForm(int $id) + { + $storeInfo = $this->getStoreInfo($id); + /** @var SystemStoreStaffServices $staffServices */ + $staffServices = app()->make(SystemStoreStaffServices::class); + $staffInfo = $staffServices->getOne(['store_id' => $storeInfo['id'], 'level' => 0, 'is_admin' => 1, 'is_manager' => 1, 'is_del' => 0]); + $field[] = $this->builder->hidden('staff_id', $staffInfo['id'] ?? 0); + $field[] = $this->builder->input('phone', '管理员手机号', $staffInfo['phone'] ?? '')->col(24)->required('请输入手机号')->info('重置手机号:会同步清除之前绑定的商城用户信息,需要在门店后台重新编辑绑定'); + $field[] = $this->builder->input('account', '管理员账号', $staffInfo['account'] ?? '')->col(24)->required('请输入账号'); + $field[] = $this->builder->input('password', '登录密码')->type('password')->col(24)->required('请输入密码'); + $field[] = $this->builder->input('true_password', '确认密码')->type('password')->col(24)->required('请再次确认密码'); + return create_form('门店重置账号密码', $field, $this->url('/store/store/reset_admin/' . $id)); + } + + /** + * 门店重置账号、密码、管理员手机号 + * @param int $id + * @param array $data + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function resetAdmin(int $id, array $data) + { + $storeInfo = $this->getStoreInfo((int)$id); + if (!$storeInfo) { + throw new ValidateException('门店数据不存在'); + } + /** @var SystemStoreStaffServices $staffServices */ + $staffServices = app()->make(SystemStoreStaffServices::class); + $staffInfo = $staffServices->get((int)$data['staff_id']); + if (!$staffInfo) { + throw new ValidateException('门店管理员数据不存在'); + } + $staff_data = []; + if ($data['phone'] != $storeInfo['phone']) {//重置手机号 清空之前绑定 + $staff_data['uid'] = 0; + } + $staff_data['account'] = $data['account']; + $staff_data['store_id'] = $storeInfo['id']; + $staff_data['level'] = 0; + $staff_data['is_admin'] = 1; + $staff_data['verify_status'] = 1; + $staff_data['is_manager'] = 1; + $staff_data['is_cashier'] = 1; + $staff_data['add_time'] = time(); + $staff_data['pwd'] = $this->passwordHash($data['password']); + $staffInfo = $staffServices->getOne(['account' => $data['account'], 'is_del' => 0]); + if ($data['staff_id'] && $staffServices->getCount(['id' => $data['staff_id'], 'is_del' => 0])) { + if ($staffInfo && $staffInfo['id'] != $data['staff_id']) { + throw new ValidateException('该账号已存在'); + } + if (!$staffServices->update($data['staff_id'], $staff_data)) { + throw new ValidateException('创建门店管理员失败!'); + } + } else { + if ($staffInfo) { + throw new ValidateException('该账号已存在'); + } + if (!$staffServices->save($staff_data)) { + throw new ValidateException('创建门店管理员失败!'); + } + } + return true; + } + + /** + * 获取erp门店列表 + * @return array|mixed + * @throws \Exception + */ + public function erpShopList() + { + [$page, $limit] = $this->getPageValue(); + if (!sys_config('erp_open')) { + return []; + } + try { + /** @var Erp $erpService */ + $erpService = app()->make(Erp::class); + $res = Cache::tag('erp_shop')->remember('list_' . $page . '_' . $limit, function () use ($page, $limit, $erpService) { + return $erpService->serviceDriver('Comment')->getShopList($page, $limit); + }, 60); + } catch (\Throwable $e) { + Log::error([ + 'message' => '读取ERP门店信息失败', + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + + return $res['data']['datas'] ?? []; + } + + /** + * 保存或修改门店 + * @param int $id + * @param array $data + * @param array $staff_data + * @return mixed + */ + public function saveStore(int $id, array $data, array $staff_data = []) + { + return $this->transaction(function () use ($id, $data, $staff_data) { + if ($id) { + $is_new = 0; + if ($this->dao->update($id, $data)) { + return [$id, $is_new]; + } else { + throw new AdminException('修改失败或者您没有修改什么!'); + } + } else { + $is_new = 1; + $data['add_time'] = time(); + if ($res = $this->dao->save($data)) { + if ($staff_data) { + $staffServices = app()->make(SystemStoreStaffServices::class); + if ($staffServices->count(['phone' => $staff_data['phone'], 'is_del' => 0])) { + throw new AdminException('该手机号已经存在'); + } + if ($staffServices->count(['account' => $staff_data['account'], 'is_del' => 0])) { + throw new AdminException('管理员账号已存在'); + } + $staff_data['level'] = 0; + $staff_data['store_id'] = $res->id; + $staff_data['is_admin'] = 1; + $staff_data['is_store'] = 1; + $staff_data['verify_status'] = 1; + $staff_data['is_manager'] = 1; + $staff_data['is_cashier'] = 1; + $staff_data['add_time'] = time(); + $staff_data['pwd'] = $this->passwordHash($staff_data['pwd']); + if (!$staffServices->save($staff_data)) { + throw new AdminException('创建门店管理员失败!'); + } + $data = [ + ['role_name' => '店员', 'type' => 1, 'relation_id' => $res->id, 'status' => 1, 'level' => 1, 'rules' => '1048,1049,1097,1098,1099,1100,1050,1051,1101,1102,1103,1273,1274,1275,1276,1081,1104,1105,1106,1052,1054,1086,1129,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1130,1166,1167,1168,1169,1131,1170,1171,1172,1173,1174,1175,1176,1242,1088,1122,1123,1124,1125,1126,1127,1164,1165,1053,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1280'], + ['role_name' => '管理员', 'type' => 1, 'relation_id' => $res->id, 'status' => 1, 'level' => 1, 'rules' => '1048,1049,1097,1098,1099,1100,1050,1051,1101,1102,1103,1273,1274,1275,1276,1081,1104,1105,1106,1052,1054,1086,1129,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1130,1166,1167,1168,1169,1131,1170,1171,1172,1173,1174,1175,1176,1242,1088,1122,1123,1124,1125,1126,1127,1164,1165,1053,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1280,1055,1056,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1277,1057,1193,1194,1195,1196,1197,1249,1250,1251,1252,1253,1254,1058,1059,1060,1198,1199,1200,1243,1255,1256,1257,1258,1259,1260,1061,1201,1241,1062,1063,1215,1218,1219,1220,1244,1261,1262,1263,1264,1265,1064,1216,1217,1202,1065,1066,1203,1214,1067,1204,1212,1213,1235,1068,1205,1206,1069,1207,1208,1070,1089,1071,1209,1210,1211,1072,1073,1082,1083,1084,1085,1228,1229,1230,1231,1232,1233,1234,1236,1245,1246,1247,1248,1221,1222,1223,1224,1225,1226,1227,1266,1267,1268,1269,1270,1271,1272,1237,1238,1239,1240'] + ]; + /** @var SystemRoleServices $systemRoleServices */ + $systemRoleServices = app()->make(SystemRoleServices::class); + $systemRoleServices->saveAll($data); + } + return [(int)$res->id, $is_new]; + } else { + throw new AdminException('保存失败!'); + } + } + }); + } + + /** + * 获取门店缓存 + * @param int $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/21 + */ + public function getStoreDisposeCache(int $id, string $felid = '') + { + $storeInfo = $this->dao->cacheRemember($id, function () use ($id) { + $storeInfo = $this->dao->get($id); + return $storeInfo ? $storeInfo->toArray() : null; + }); + + if ($felid) { + return $storeInfo[$felid] ?? null; + } + + if ($storeInfo) { + $storeInfo['latlng'] = $storeInfo['latitude'] . ',' . $storeInfo['longitude']; + $storeInfo['dataVal'] = $storeInfo['valid_time'] ? explode(' - ', $storeInfo['valid_time']) : []; + $storeInfo['timeVal'] = $storeInfo['day_time'] ? explode(' - ', $storeInfo['day_time']) : []; + $storeInfo['address2'] = $storeInfo['address'] ? explode(',', $storeInfo['address']) : []; + return $storeInfo; + } + return []; + } + + /** + * 后台获取提货点详情 + * @param int $id + * @param string $felid + * @return array|false|mixed|string|string[]|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreDispose(int $id, string $felid = '') + { + if ($felid) { + return $this->dao->value(['id' => $id], $felid); + } else { + $storeInfo = $this->dao->get($id); + if ($storeInfo) { + $storeInfo['latlng'] = $storeInfo['latitude'] . ',' . $storeInfo['longitude']; + $storeInfo['dataVal'] = $storeInfo['valid_time'] ? explode(' - ', $storeInfo['valid_time']) : []; + $storeInfo['timeVal'] = $storeInfo['day_time'] ? explode(' - ', $storeInfo['day_time']) : []; + $storeInfo['address2'] = $storeInfo['address'] ? explode(',', $storeInfo['address']) : []; + return $storeInfo; + } + return false; + } + } + + /** + * 获取门店不分页 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStore() + { + return $this->dao->getStore(['type' => 0]); + } + + /** + * 获得导出店员列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExportData(array $where) + { + return $this->dao->getStoreList($where, ['*']); + } + + /** + * 平台门店运营统计 + * @param array $time + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function storeChart(array $time) + { + $list = $this->dao->getStoreList(['is_del' => 0, 'is_show' => 1], ['id', 'name', 'image']); + /** @var StoreUserServices $storeUserServices */ + $storeUserServices = app()->make(StoreUserServices::class); + /** @var BranchOrderServices $orderServices */ + $orderServices = app()->make(BranchOrderServices::class); + /** @var StoreFinanceFlowServices $storeFinancFlowServices */ + $storeFinancFlowServices = app()->make(StoreFinanceFlowServices::class); + $where = ['time' => $time]; + $order_where = ['paid' => 1, 'pid' => 0, 'is_del' => 0, 'is_system_del' => 0, 'refund_status' => [0, 3]]; + foreach ($list as &$item) { + $store_where = ['store_id' => $item['id']]; + $item['store_price'] = (float)bcsub((string)$storeFinancFlowServices->sum($where + $store_where + ['trade_type' => 1, 'no_type' => 1, 'pm' => 1, 'is_del' => 0], 'number', true), (string)$storeFinancFlowServices->sum($where + $store_where + ['trade_type' => 1, 'no_type' => 1, 'pm' => 0, 'is_del' => 0], 'number', true), 2); + $item['store_product_count'] = $orderServices->sum($where + $store_where + $order_where, 'total_num', true); + $item['store_order_price'] = $orderServices->sum($where + $store_where + $order_where, 'pay_price', true); + $item['store_user_count'] = $storeUserServices->count($where + $store_where); + } + return $list; + } + + /** + * 检测订单商品门店是否支持配送 + * @param int $oid + * @param array $storeList + * @param int $getType + * @return array + */ + public function checkOrderProductShare(int $oid, array $storeList, int $getType = 1) + { + if (!$oid || !$storeList) { + return [[], []]; + } + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $orderInfo = $storeOrderServices->get($oid, ['id', 'order_id', 'uid', 'store_id', 'supplier_id']); + /** @var StoreOrderCartInfoServices $storeOrderCartInfoServices */ + $storeOrderCartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $cart_info = $storeOrderCartInfoServices->getSplitCartList($oid, 'cart_info'); + if (!$cart_info) { + return [[], []]; + } + + /** @var StoreProductAttrValueServices $skuValueServices */ + $skuValueServices = app()->make(StoreProductAttrValueServices::class); + $platProductIds = []; + $platStoreProductIds = []; + $storeProductIds = []; + foreach ($cart_info as $cart) { + $productInfo = $cart['productInfo'] ?? []; + if (isset($productInfo['store_delivery']) && !$productInfo['store_delivery']) {//有商品不支持门店配送 + return [[], $cart_info]; + } + switch ($productInfo['type'] ?? 0) { + case 0://平台 + case 2://供应商 + $platProductIds[] = $cart['product_id']; + break; + case 1://门店 + if ($productInfo['pid']) {//门店自有商品 + $storeProductIds[] = $cart['product_id']; + } else { + $platStoreProductIds[] = $cart['product_id']; + } + break; + + } + } + /** @var StoreBranchProductServices $branchProductServics */ + $branchProductServics = app()->make(StoreBranchProductServices::class); + //转换成平台商品 + if ($platStoreProductIds) { + $ids = $branchProductServics->getStoreProductIds($platStoreProductIds); + $platProductIds = array_merge($platProductIds, $ids); + } + $productCount = count($platProductIds); + $result = []; + foreach ($storeList as $store) { + if ($storeProductIds && $store['id'] != $orderInfo['store_id']) { + continue; + } + $is_show = $productCount == $branchProductServics->count(['pid' => $platProductIds, 'is_show' => 1, 'is_del' => 0, 'type' => 1, 'relation_id' => $store['id']]);//商品没下架 && 库存足够 + if (!$is_show) { + continue; + } + $stock = true; + foreach ($cart_info as $cart) { + $productInfo = $cart['productInfo'] ?? []; + if (!$productInfo) { + $stock = false; + break; + } + $applicable_store_ids = []; + //验证商品适用门店 + if (isset($productInfo['applicable_store_id'])) $applicable_store_ids = is_array($productInfo['applicable_store_id']) ? $productInfo['applicable_store_id'] : explode(',', $productInfo['applicable_store_id']); + if (!isset($productInfo['applicable_type']) || $productInfo['applicable_type'] == 0 || ($productInfo['applicable_type'] == 2 && !in_array($store['id'], $applicable_store_ids))) { + $stock = false; + break; + } + switch ($cart['type']) { + case 0: + case 6: + case 8: + case 9: + case 10: + $suk = $skuValueServices->value(['unique' => $cart['product_attr_unique'], 'product_id' => $cart['product_id'], 'type' => 0], 'suk'); + break; + case 1: + case 2: + case 3: + case 5: + case 7: + $suk = $skuValueServices->value(['unique' => $cart['product_attr_unique'], 'product_id' => $cart['activity_id'], 'type' => $cart['type']], 'suk'); + break; + } + $branchProductInfo = $branchProductServics->isValidStoreProduct((int)$cart['product_id'], $store['id']); + if (!$branchProductInfo) { + $stock = false; + break; + } + $attrValue = $skuValueServices->get(['suk' => $suk, 'product_id' => $branchProductInfo['id'], 'type' => 0]); + if (!$attrValue) { + $stock = false; + break; + } + if ($cart['cart_num'] > $attrValue['stock']) { + $stock = false; + break; + } + } + if ($stock) { + if ($getType == 1) {//只取一条门店数据 + $result[] = $store['id']; + break; + } else { + $result[] = $store['id']; + } + } + } + return [$result, $cart_info]; + } + + /** + * 计算距离 + * @param string $user_lat + * @param string $user_lng + * @param string $store_lat + * @param $store_lng + * @return string + */ + public function distance(string $user_lat, string $user_lng, string $store_lat, $store_lng) + { + try { + return (round(6378137 * 2 * asin(sqrt(pow(sin((($store_lat * pi()) / 180 - ($user_lat * pi()) / 180) / 2), 2) + cos(($user_lat * pi()) / 180) * cos(($store_lat * pi()) / 180) * pow(sin((($store_lng * pi()) / 180 - ($user_lng * pi()) / 180) / 2), 2))))); + } catch (\Throwable $e) { + return '0'; + } + } + + /** + * 验证门店配送范围 + * @param int $store_id + * @param array $addressInfo + * @param string $latitude + * @param string $longitude + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkStoreDeliveryScope(int $store_id, array $addressInfo = [], string $latitude = '', string $longitude = '') + { + //没登录 || 无添加地址 默认不验证 + if (!$store_id || (!$addressInfo && (!$latitude || !$longitude))) { + return true; + } + //门店是否开启 + if (!sys_config('store_func_status', 1)) { + return false; + } + $store_delivery_scope = (int)sys_config('store_delivery_scope', 0); + if (!$store_delivery_scope) {//配送范围不验证 + return true; + } + $storeInfo = $this->getStoreDisposeCache($store_id); + if (!$storeInfo) { + return false; + } + if ($addressInfo) { + $user_lat = $addressInfo['latitude'] ?? ''; + $user_lng = $addressInfo['longitude'] ?? ''; + if (!$user_lat || !$user_lng) { + try { + $user_address = $addressInfo['province'] . $addressInfo['city'] . $addressInfo['district'] . $addressInfo['street'] . $addressInfo['detail']; + /** @var StoreDeliveryOrderServices $deliveryServices */ + $deliveryServices = app()->make(StoreDeliveryOrderServices::class); + $addres = $deliveryServices->lbs_address($addressInfo['city'], $user_address); + $user_lat = $addres['location']['lat'] ?? ''; + $user_lng = $addres['location']['lng'] ?? ''; + } catch (\Exception $e) { + return false; + } + } + } else { + $user_lat = $latitude; + $user_lng = $longitude; + } + + $distance = $this->distance($user_lat, $user_lng, $storeInfo['latitude'], $storeInfo['longitude']); + + return $distance <= $storeInfo['valid_range']; + } +} diff --git a/app/services/store/SystemStoreStaffServices.php b/app/services/store/SystemStoreStaffServices.php new file mode 100644 index 0000000..dec302e --- /dev/null +++ b/app/services/store/SystemStoreStaffServices.php @@ -0,0 +1,763 @@ + +// +---------------------------------------------------------------------- +namespace app\services\store; + +use app\dao\store\SystemStoreStaffDao; +use app\services\BaseServices; +use app\services\order\OtherOrderServices; +use app\services\order\store\BranchOrderServices; +use app\services\system\SystemRoleServices; +use app\services\user\UserCardServices; +use app\services\user\UserRechargeServices; +use app\services\user\UserSpreadServices; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder; +use think\exception\ValidateException; + +/** + * 门店店员 + * Class SystemStoreStaffServices + * @package app\services\system\store + * @mixin SystemStoreStaffDao + */ +class SystemStoreStaffServices extends BaseServices +{ + /** + * @var FormBuilder + */ + protected $builder; + + /** + * 构造方法 + * SystemStoreStaffServices constructor. + * @param SystemStoreStaffDao $dao + */ + public function __construct(SystemStoreStaffDao $dao, FormBuilder $builder) + { + $this->dao = $dao; + $this->builder = $builder; + } + + /** + * 获取低于等级的店员名称和id + * @param string $field + * @param int $level + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOrdAdmin(string $field = 'real_name,id', int $storeId = 0, int $level = 0) + { + return $this->dao->getWhere()->when('store_id', $storeId)->where('level', '>=', $level)->field($field)->select()->toArray(); + } + + /** + * 获取门店客服列表 + * @param int $store_id + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCustomerList(int $store_id, string $field = '*') + { + return $this->dao->getWhere()->where('store_id', $store_id)->where('status', 1)->where('is_del', 0)->where('is_customer', 1)->field($field)->select()->toArray(); + } + + /** + * 获取店员详情 + * @param int $id + * @param string $field + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStaffInfo(int $id, string $field = '*') + { + $info = $this->dao->getOne(['id' => $id, 'is_del' => 0], $field); + if (!$info) { + throw new ValidateException('店员不存在'); + } + return $info; + } + + /** + * 根据uid获取门店店员信息 + * @param int $uid + * @param int $store_id + * @param string $field + * @return array|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStaffInfoByUid(int $uid, int $store_id = 0, string $field = '*') + { + $where = ['uid' => $uid, 'is_del' => 0, 'status' => 1]; + if ($store_id) $where['store_id'] = $store_id; + $info = $this->dao->getOne($where, $field); + if (!$info) { + throw new ValidateException('店员不存在'); + } + return $info; + } + + /** + * 获取门店|店员统计 + * @param int $store_id + * @param int $staff_id + * @param string $time + * @return array + */ + public function getStoreData(int $uid, int $store_id, int $staff_id = 0, string $time = 'today') + { + $where = ['store_id' => $store_id, 'time' => $time]; + if ($staff_id) { + $where['staff_id'] = $staff_id; + } + $data = []; + $order_where = ['pid' => 0, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0]; + /** @var BranchOrderServices $orderServices */ + $orderServices = app()->make(BranchOrderServices::class); + $data['send_price'] = $orderServices->sum($where + $order_where + ['type' => 7], 'pay_price', true); + $data['send_count'] = $orderServices->count($where + $order_where + ['type' => 7]); + $data['refund_price'] = $orderServices->sum($where + ['status' => -3], 'pay_price', true); + $data['refund_count'] = $orderServices->count($where + ['status' => -3]); + $data['cashier_price'] = $orderServices->sum($where + $order_where + ['type' => 6], 'pay_price', true); + $data['writeoff_price'] = $orderServices->sum($where + $order_where + ['type' => 5], 'pay_price', true); + /** @var OtherOrderServices $otherOrder */ + $otherOrder = app()->make(OtherOrderServices::class); + $data['svip_price'] = $otherOrder->sum($where + ['paid' => 1, 'type' => [0, 1, 2, 4]], 'pay_price', true); + /** @var UserRechargeServices $userRecharge */ + $userRecharge = app()->make(UserRechargeServices::class); + $data['recharge_price'] = $userRecharge->getWhereSumField($where + ['paid' => 1], 'price'); + /** @var UserSpreadServices $userSpread */ + $userSpread = app()->make(UserSpreadServices::class); + $data['spread_count'] = $userSpread->count($where + ['timeKey' => 'spread_time']); + /** @var UserCardServices $userCard */ + $userCard = app()->make(UserCardServices::class); + $data['card_count'] = $userCard->count($where + ['is_submit' => 1]); + return $data; + } + + /** + * 判断是否是有权限核销的店员 + * @param $uid + * @return bool + */ + public function verifyStatus($uid) + { + return (bool)$this->dao->getOne(['uid' => $uid, 'status' => 1, 'is_del' => 0, 'verify_status' => 1]); + } + + /** + * 获取店员列表 + * @param array $where + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreStaffList(array $where, array $with = []) + { +// $with = array_merge($with, [ +// 'workMember' => function ($query) { +// $query->field(['uid', 'name', 'position', 'qr_code', 'external_position']); +// } +// ]); + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getStoreStaffList($where, '*', $page, $limit, $with); + if ($list) { + /** @var SystemRoleServices $service */ + $service = app()->make(SystemRoleServices::class); + $allRole = $service->getRoleArray(['type' => 1, 'store_id' => $where['store_id'], 'status' => 1]); + foreach ($list as &$item) { + $item['workMember'] = []; + if ($item['level']) { + if ($item['roles']) { + $roles = []; + foreach ($item['roles'] as $id) { + if (isset($allRole[$id])) $roles[] = $allRole[$id]; + } + if ($roles) { + $item['roles'] = implode(',', $roles); + } else { + $item['roles'] = ''; + } + } else { + $item['roles'] = ''; + } + } else { + $item['roles'] = '超级管理员'; + } + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 不查询总数 + * @param array $where + * @param array $with + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreStaff(array $where, array $with = []) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getStoreStaffList($where, '*', $page, $limit, $with); + foreach ($list as $key => $item) { + unset($list[$key]['pwd']); + } + return $list; + } + + /** + * 店员详情 + * @param int $id + * @return array|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function read(int $id) + { + $staffInfo = $this->getStaffInfo($id); + $info = [ + 'id' => $id, + 'headerList' => $this->getHeaderList($id, $staffInfo), + 'ps_info' => $staffInfo + ]; + return $info; + } + + /** + * 获取单个店员统计信息 + * @param $id 用户id + * @return mixed + */ + public function staffDetail(int $id, string $type) + { + $staffInfo = $this->getStaffInfo($id); + if (!$staffInfo) { + throw new AdminException('店员不存在'); + } + $where = ['store_id' => $staffInfo['store_id'], 'staff_id' => $staffInfo['id']]; + $data = []; + switch ($type) { + case 'cashier_order': + /** @var BranchOrderServices $orderServices */ + $orderServices = app()->make(BranchOrderServices::class); + $where = array_merge($where, ['pid' => 0, 'type' => 6, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0]); + $field = ['uid', 'order_id', 'real_name', 'total_num', 'total_price', 'pay_price', 'FROM_UNIXTIME(pay_time,"%Y-%m-%d") as pay_time', 'paid', 'pay_type', 'type', 'activity_id', 'pink_id']; + $data = $orderServices->getStoreOrderList($where, $field, [], true); + break; + case 'self_order': + /** @var BranchOrderServices $orderServices */ + $orderServices = app()->make(BranchOrderServices::class); + $where = array_merge($where, ['pid' => 0, 'type' => 7, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0]); + $field = ['uid', 'order_id', 'real_name', 'total_num', 'total_price', 'pay_price', 'FROM_UNIXTIME(pay_time,"%Y-%m-%d") as pay_time', 'paid', 'pay_type', 'type', 'activity_id', 'pink_id']; + $data = $orderServices->getStoreOrderList($where, $field, [], true); + break; + case 'writeoff_order': + /** @var BranchOrderServices $orderServices */ + $orderServices = app()->make(BranchOrderServices::class); + $where = array_merge($where, ['pid' => 0, 'type' => 5, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0]); + $field = ['uid', 'order_id', 'real_name', 'total_num', 'total_price', 'pay_price', 'FROM_UNIXTIME(pay_time,"%Y-%m-%d") as pay_time', 'paid', 'pay_type', 'type', 'activity_id', 'pink_id']; + $data = $orderServices->getStoreOrderList($where, $field, [], true); + break; + case 'recharge': + /** @var UserRechargeServices $userRechargeServices */ + $userRechargeServices = app()->make(UserRechargeServices::class); + $data = $userRechargeServices->getRechargeList($where + ['paid' => 1]); + break; + case 'spread': + /** @var UserSpreadServices $userSpreadServices */ + $userSpreadServices = app()->make(UserSpreadServices::class); + $data = $userSpreadServices->getSpreadList($where); + break; + case 'card': + /** @var UserCardServices $userCardServices */ + $userCardServices = app()->make(UserCardServices::class); + $data = $userCardServices->getCardList($where + ['is_submit' => 1]); + break; + case 'svip': + /** @var OtherOrderServices $otherOrderServices */ + $otherOrderServices = app()->make(OtherOrderServices::class); + $data = $otherOrderServices->getMemberRecord($where); + break; + default: + throw new AdminException('type参数错误'); + } + return $data; + } + + /** + * 店员详情头部信息 + * @param int $id + * @param array $staffInfo + * @return array[] + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getHeaderList(int $id, $staffInfo = []) + { + if (!$staffInfo) { + $staffInfo = $this->dao->get($id); + } + $where = ['store_id' => $staffInfo['store_id'], 'staff_id' => $staffInfo['id']]; + /** @var BranchOrderServices $orderServices */ + $orderServices = app()->make(BranchOrderServices::class); + $cashier_order = $orderServices->sum($where + ['pid' => 0, 'type' => 6, 'paid' => 1, 'refund_status' => 0, 'is_del' => 0, 'is_system_del' => 0], 'pay_price', true); + $writeoff_order = $orderServices->sum($where + ['pid' => 0, 'type' => 5, 'paid' => 1, 'refund_status' => 0, 'is_del' => 0, 'is_system_del' => 0], 'pay_price', true); + $self_order = $orderServices->sum($where + ['pid' => 0, 'type' => 7, 'paid' => 1, 'refund_status' => 0, 'is_del' => 0, 'is_system_del' => 0], 'pay_price', true); + /** @var UserRechargeServices $userRechargeServices */ + $userRechargeServices = app()->make(UserRechargeServices::class); + $recharge = $userRechargeServices->sum($where + ['paid' => 1], 'price', true); + /** @var UserSpreadServices $userSpreadServices */ + $userSpreadServices = app()->make(UserSpreadServices::class); + $spread = $userSpreadServices->count($where); + /** @var UserCardServices $userCardServices */ + $userCardServices = app()->make(UserCardServices::class); + $card = $userCardServices->count($where + ['is_submit' => 1]); + /** @var OtherOrderServices $otherOrderServices */ + $otherOrderServices = app()->make(OtherOrderServices::class); + $svip = $otherOrderServices->sum($where, 'pay_price', true); + return [ + [ + 'title' => '收银订单', + 'value' => $cashier_order, + 'key' => '元', + ], + [ + 'title' => '核销订单', + 'value' => $writeoff_order, + 'key' => '元', + ], + [ + 'title' => '配送订单', + 'value' => $self_order, + 'key' => '元', + ], + [ + 'title' => '充值订单', + 'value' => $recharge, + 'key' => '元', + ], + [ + 'title' => '付费会员', + 'value' => $svip, + 'key' => '元', + ], + [ + 'title' => '推广用户数', + 'value' => $spread, + 'key' => '人', + ], + [ + 'title' => '激活会员卡', + 'value' => $card, + 'key' => '张', + ] + ]; + } + + /** + * 获取select选择框中的门店列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreSelectFormData() + { + /** @var SystemStoreServices $service */ + $service = app()->make(SystemStoreServices::class); + $menus = []; + foreach ($service->getStore() as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['name']]; + } + return $menus; + } + + /** + * 获取核销员表单 + * @param array $formData + * @return mixed + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createStaffForm(array $formData = []) + { + if ($formData) { + $field[] = $this->builder->frameImage('image', '更换头像', $this->url(config('admin.admin_prefix') . '/widget.images/index', array('fodder' => 'image'), true), $formData['avatar'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + } else { + $field[] = $this->builder->frameImage('image', '商城用户', $this->url(config('admin.admin_prefix') . '/system.User/list', ['fodder' => 'image'], true))->icon('ios-add')->width('960px')->height('550px')->modal(['footer-hide' => true])->Props(['srcKey' => 'image']); + } + $field[] = $this->builder->hidden('uid', $formData['uid'] ?? 0); + $field[] = $this->builder->hidden('avatar', $formData['avatar'] ?? ''); + $field[] = $this->builder->select('store_id', '所属提货点', ($formData['store_id'] ?? 0))->setOptions($this->getStoreSelectFormData())->filterable(true); + $field[] = $this->builder->input('staff_name', '核销员名称', $formData['staff_name'] ?? '')->col(24)->required(); + $field[] = $this->builder->input('phone', '手机号码', $formData['phone'] ?? '')->col(24)->required(); + $field[] = $this->builder->radio('verify_status', '核销开关', $formData['verify_status'] ?? 1)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('status', '状态', $formData['status'] ?? 1)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + return $field; + } + + /** + * 添加核销员表单 + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createForm() + { + return create_form('添加核销员', $this->createStaffForm(), $this->url('/merchant/store_staff/save/0')); + } + + /** + * 编辑核销员form表单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateForm(int $id) + { + $storeStaff = $this->dao->get($id); + if (!$storeStaff) { + throw new AdminException('没有查到信息,无法修改'); + } + return create_form('修改核销员', $this->createStaffForm($storeStaff->toArray()), $this->url('/merchant/store_staff/save/' . $id)); + } + + /** + * 获取门店店员 + * @param $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreAdminList($where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getStoreAdminList($where, $page, $limit); + /** @var SystemRoleServices $service */ + $service = app()->make(SystemRoleServices::class); + $allRole = $service->getRoleArray(['type' => 1, 'store_id' => $where['store_id']]); + foreach ($list as &$item) { + if ($item['roles']) { + $roles = []; + foreach ($item['roles'] as $id) { + if (isset($allRole[$id])) $roles[] = $allRole[$id]; + } + if ($roles) { + $item['roles'] = implode(',', $roles); + } else { + $item['roles'] = ''; + } + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 添加门店管理员 + * @param int $store_id + * @param $level + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createStoreAdminForm(int $store_id, $level) + { + $field[] = $this->builder->input('staff_name', '管理员名称')->col(24)->required(); + $field[] = $this->builder->frameImage('avatar', '管理员头像', $this->url(config('admin.admin_prefix') . '/widget.images/index', ['fodder' => 'avatar'], true))->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + $field[] = $this->builder->input('account', '管理员账号')->maxlength(35)->required('请填写管理员账号'); + $field[] = $this->builder->input('phone', '手机号码')->col(24)->required(); + $field[] = $this->builder->input('pwd', '管理员密码')->type('password')->required('请填写管理员密码'); + $field[] = $this->builder->input('conf_pwd', '确认密码')->type('password')->required('请输入确认密码'); + /** @var SystemRoleServices $service */ + $service = app()->make(SystemRoleServices::class); + $options = $service->getRoleFormSelect($level, 1, $store_id); + $roles = []; + $field[] = $this->builder->select('roles', '管理员身份', $roles)->setOptions(FormBuilder::setOptions($options))->multiple(true)->required('请选择管理员身份'); + $field[] = $this->builder->radio('status', '状态', 1)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + return create_form('添加门店管理员', $field, $this->url('/system/admin')); + } + + /** + * 修改门店管理员 + * @param $id + * @param $level + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateStoreAdminForm($id, $level) + { + $adminInfo = $this->dao->get($id); + if (!$adminInfo) { + throw new AdminException('门店管理员不存在!'); + } + if ($adminInfo->is_del) { + throw new AdminException('门店管理员已经删除'); + } + $adminInfo = $adminInfo->toArray(); + $field[] = $this->builder->input('staff_name', '门店管理员名称', $adminInfo['staff_name'])->col(24)->required('请填写门店管理员名称'); + $field[] = $this->builder->frameImage('avatar', '管理员头像', $this->url(config('admin.store_prefix') . '/widget.images/index', ['fodder' => 'avatar'], true), $adminInfo['avatar'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + $field[] = $this->builder->input('account', '门店管理员账号', $adminInfo['account'])->maxlength(35)->required('请填写门店管理员账号'); + $field[] = $this->builder->input('phone', '手机号码', $adminInfo['phone'])->col(24)->required(); + $field[] = $this->builder->input('pwd', '门店管理员密码')->placeholder('不更改密码请留空')->type('password'); + $field[] = $this->builder->input('conf_pwd', '确认密码')->placeholder('不更改密码请留空')->type('password'); + /** @var SystemRoleServices $service */ + $service = app()->make(SystemRoleServices::class); + $options = $service->getRoleFormSelect($level, 1, (int)$adminInfo['store_id']); + $roles = []; + if ($adminInfo && isset($adminInfo['roles']) && $adminInfo['roles']) { + foreach ($adminInfo['roles'] as $role) { + $roles[] = (int)$role; + } + } + $field[] = $this->builder->select('roles', '管理员身份', $roles)->setOptions(FormBuilder::setOptions($options))->multiple(true)->required('请选择门店管理员身份'); + $field[] = $this->builder->radio('status', '状态', (int)$adminInfo['status'])->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + return create_form('修改门店管理员', $field, $this->url('/system/admin/' . $id), 'put'); + } + + /** + * 添加门店店员 + * @param int $store_id + * @param $level + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createStoreStaffForm(int $store_id, $level) + { + $field[] = $this->builder->input('staff_name', '店员名称')->col(24)->required('请输入门店店员名称'); + $field[] = $this->builder->frameImage('image', '商城用户', $this->url(config('admin.store_prefix') . '/system.User/list', ['fodder' => 'image'], true))->icon('ios-add')->width('960px')->height('450px')->modal(['footer-hide' => true])->Props(['srcKey' => 'image']); + $field[] = $this->builder->hidden('uid', 0); + $field[] = $this->builder->hidden('avatar', ''); + $field[] = $this->builder->input('account', '店员账号')->maxlength(35)->required('请填写门店店员账号'); + $field[] = $this->builder->input('pwd', '店员密码')->type('password')->required('请填写门店店员密码'); + $field[] = $this->builder->input('conf_pwd', '确认密码')->type('password')->required('请输入确认密码'); + $field[] = $this->builder->input('phone', '手机号码')->col(24)->required('请输入手机号'); + /** @var SystemRoleServices $service */ + $service = app()->make(SystemRoleServices::class); + $options = $service->getRoleFormSelect($level, 1, $store_id); + $roles = []; + $field[] = $this->builder->select('roles', '店员身份', $roles)->setOptions(FormBuilder::setOptions($options))->multiple(true)->required('请选择店员身份'); + $field[] = $this->builder->radio('is_manager', '是否是店长', 0)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('order_status', '订单管理', 1)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('verify_status', '核销开关', 1)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('is_cashier', '是否是收银员', 1)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('is_customer', '是否是客服', 1)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']])->appendControl(1, [ + $this->builder->input('customer_phone', '客服手机号码')->col(24), + $this->builder->frameImage('customer_url', '客服二维码', $this->url(config('admin.store_prefix') . '/widget.images/index', ['fodder' => 'customer_url'], true))->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]) + ]); + $field[] = $this->builder->radio('notify', '通知开关', 0)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('status', '状态', 1)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + return create_form('添加门店店员', $field, $this->url('/staff/staff')); + } + + /** + * 编辑门店店员 + * @param $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateStoreStaffForm($id, $level) + { + $staffInfo = $this->dao->get($id); + if (!$staffInfo) { + throw new AdminException('门店店员不存在!'); + } + if ($staffInfo->is_del) { + throw new AdminException('门店店员已经删除'); + } + $field[] = $this->builder->input('staff_name', '店员名称', $staffInfo['staff_name'])->col(24)->required('请填写门店店员名称'); + if ($staffInfo['uid']) { + $field[] = $this->builder->frameImage('avatar', '店员头像', $this->url(config('admin.store_prefix') . '/widget.images/index', ['fodder' => 'avatar'], true), $staffInfo['avatar'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + } else {//没绑定过商城用户 + $field[] = $this->builder->frameImage('image', '商城用户', $this->url(config('admin.store_prefix') . '/system.User/list', ['fodder' => 'image'], true))->icon('ios-add')->width('960px')->height('450px')->modal(['footer-hide' => true])->Props(['srcKey' => 'image']); + $field[] = $this->builder->hidden('uid', 0); + $field[] = $this->builder->hidden('avatar', ''); + } + $field[] = $this->builder->input('account', '店员账号', $staffInfo['account'])->maxlength(35)->required('请填写门店店员账号'); + $field[] = $this->builder->input('pwd', '店员密码')->placeholder('不更改密码请留空')->type('password'); + $field[] = $this->builder->input('conf_pwd', '确认密码')->placeholder('不更改密码请留空')->type('password'); + $field[] = $this->builder->input('phone', '手机号码', $staffInfo['phone'])->col(24)->required('请输入手机号'); + /** @var SystemRoleServices $service */ + $service = app()->make(SystemRoleServices::class); + $options = $service->getRoleFormSelect($level, 1, (int)$staffInfo['store_id']); + $roles = []; + if ($staffInfo && isset($staffInfo['roles']) && $staffInfo['roles']) { + foreach ($staffInfo['roles'] as $role) { + $roles[] = (int)$role; + } + } + $field[] = $this->builder->select('roles', '店员身份', $roles)->setOptions(FormBuilder::setOptions($options))->multiple(true)->required('请选择店员身份'); + $field[] = $this->builder->radio('is_manager', '是否是店长', (int)$staffInfo['is_manager'])->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('order_status', '订单管理', (int)$staffInfo['order_status'])->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('verify_status', '核销开关', (int)$staffInfo['verify_status'])->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('is_cashier', '是否是收银员', (int)$staffInfo['is_cashier'])->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('is_customer', '是否是客服', (int)$staffInfo['is_customer'])->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']])->appendControl(1, [ + $this->builder->input('customer_phone', '客服手机号码', $staffInfo['customer_phone'] ?? '')->col(24), + $this->builder->frameImage('customer_url', '客服二维码', $this->url(config('admin.store_prefix') . '/widget.images/index', ['fodder' => 'customer_url'], true), $staffInfo['customer_url'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]) + ]); + $field[] = $this->builder->radio('notify', '通知开关', (int)$staffInfo['notify'])->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + $field[] = $this->builder->radio('status', '状态', (int)$staffInfo['status'])->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + return create_form('编辑门店店员', $field, $this->url('/staff/staff/' . $id), 'put'); + } + + /** + * 获取店员select + * @param array $where + * @return mixed + */ + public function getSelectList($where = []) + { + $list = $this->dao->getSelectList($where); + $menus = []; + foreach ($list as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['staff_name'] ?? '']; + } + return $menus; + } + + /** + * 首页店员统计 + * @param int $store_id + * @param array $time + * @return array + */ + public function staffChart(int $store_id, array $time) + { + $list = $this->dao->getStoreStaffList(['store_id' => $store_id, 'is_del' => 0], 'id,uid,avatar,staff_name'); + if ($list) { + /** @var UserSpreadServices $userSpreadServices */ + $userSpreadServices = app()->make(UserSpreadServices::class); + /** @var BranchOrderServices $orderServices */ + $orderServices = app()->make(BranchOrderServices::class); + /** @var OtherOrderServices $otherOrderServices */ + $otherOrderServices = app()->make(OtherOrderServices::class); + $where = ['store_id' => $store_id, 'time' => $time]; + $order_where = ['paid' => 1, 'pid' => 0, 'is_del' => 0, 'is_system_del' => 0, 'refund_status' => [0, 3]]; + $staffIds = array_unique(array_column($list, 'id')); + $otherStaff = $otherOrderServices->preStaffTotal($where + ['staff_id' => $staffIds, 'paid' => 1, 'type' => [0, 1, 2, 4]], 'distinct(`uid`)'); + $otherStaff = array_combine(array_column($otherStaff, 'staff_id'), $otherStaff); + foreach ($list as &$item) { + $staff_where = ['staff_id' => $item['id']]; + $spread_uid = $userSpreadServices->getColumn($where + ['timeKey' => 'spread_time'] + $staff_where, 'uid', '', true); + $item['spread_count'] = count($spread_uid); + $item['speread_order_price'] = 0; + if ($spread_uid) { + $item['speread_order_price'] = $orderServices->sum($where + $order_where + ['uid' => $spread_uid], 'pay_price', true); + } + $item['vip_count'] = $otherStaff[$item['id']]['count'] ?? 0; + $item['vip_price'] = $otherStaff[$item['id']]['price'] ?? 0; + unset($spread); + } + } + return $list; + } + + /** + * 修改当前店员信息 + * @param int $id + * @param array $data + * @return bool + */ + public function updateStaffPwd(int $id, array $data) + { + $staffInfo = $this->dao->get($id); + if (!$staffInfo) + throw new AdminException('店员信息未查到'); + if ($staffInfo->is_del) { + throw new AdminException('店员已经删除'); + } + if ($data['real_name']) { + $staffInfo->staff_name = $data['real_name']; + } + if ($data['avatar']) { + $staffInfo->avatar = $data['avatar']; + } + if ($data['pwd']) { + if (!password_verify($data['pwd'], $staffInfo['pwd'])) + throw new AdminException('原始密码错误'); + if (!$data['new_pwd']) + throw new AdminException('请输入新密码'); + if (!$data['conf_pwd']) + throw new AdminException('请输入确认密码'); + if ($data['new_pwd'] != $data['conf_pwd']) + throw new AdminException('两次输入的密码不一致'); + $staffInfo->pwd = $this->passwordHash($data['new_pwd']); + } + if ($staffInfo->save()) + return true; + else + return false; + } + + /** + * 获取门店接收通知店员 + * @param int $store_id + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getNotifyStoreStaffList(int $store_id, string $field = '*') + { + $where = [ + 'store_id' => $store_id, + 'status' => 1, + 'is_del' => 0, + 'notify' => 1 + ]; + $list = $this->dao->getStoreStaffList($where, $field); + return $list; + } + + /** + * 获取所有门店有用的手机号 + * @return array + */ + public function getPhoneAll() + { + $phone = array_merge(array_unique($this->dao->getColumn([ + ['is_del', '=', 0], + ['status', '=', 1], + ], 'phone'))); + + return $phone ?: [0]; + } +} diff --git a/app/services/store/finance/StoreFinanceFlowServices.php b/app/services/store/finance/StoreFinanceFlowServices.php new file mode 100644 index 0000000..e94d085 --- /dev/null +++ b/app/services/store/finance/StoreFinanceFlowServices.php @@ -0,0 +1,505 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\store\finance; + + +use app\dao\store\finance\StoreFinanceFlowDao; +use app\dao\store\StoreUserDao; +use app\services\BaseServices; +use app\services\order\StoreOrderCreateServices; +use app\services\order\StoreOrderServices; +use app\services\pay\PayServices; +use app\services\store\SystemStoreStaffServices; + +/** + * 门店流水 + * Class StoreExtractServices + * @package app\services\store\finance + * @mixin StoreFinanceFlowDao + */ +class StoreFinanceFlowServices extends BaseServices +{ + /** + * 支付类型 + * @var string[] + */ + public $pay_type = ['weixin' => '微信支付', 'yue' => '余额支付', 'offline' => '线下支付', 'alipay' => '支付宝支付', 'cash' => '现金支付', 'automatic' => '自动转账', 'store' => '微信支付']; + + /** + * 交易类型 + * @var string[] + */ + public $type = [ + 1 => '支付订单', + 2 => '支付订单', + 3 => '订单手续费', + 4 => '退款订单', + 5 => '充值返点', + 6 => '付费会员返点', + 7 => '充值订单', + 8 => '付费订单', + 9 => '收银订单', + 10 => '核销订单', + 11 => '分配订单', + 12 => '配送订单', + 13 => '同城配送订单', + ]; + + /** + * 构造方法 + * StoreUser constructor. + * @param StoreUserDao $dao + */ + public function __construct(StoreFinanceFlowDao $dao) + { + $this->dao = $dao; + } + + /** + * 显示资源列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit, ['user', 'systemStoreStaff', 'systemStore' => function ($query) { + $query->field('id,name')->bind(['store_name' => 'name']); + }]); + foreach ($list as &$item) { + $item['type_name'] = isset($this->type[$item['type']]) ? $this->type[$item['type']] : '其他类型'; + $item['pay_type_name'] = isset($this->pay_type[$item['pay_type']]) ? $this->pay_type[$item['pay_type']] : '其他方式'; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + $item['trade_time'] = $item['trade_time'] ? date('Y-m-d H:i:s', $item['trade_time']) : $item['add_time']; + $item['user_nickname'] = $item['user_nickname'] ?: '游客'; + } + $count = $this->dao->getCount($where); + return compact('list', 'count'); + } + + /** + * 门店账单 + * @param $where + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFundRecord($where) + { + [$page, $limit] = $this->getPageValue(); + $where['is_del'] = 0; + $data = $this->dao->getFundRecord($where, $page, $limit); + $i = 1; + foreach ($data['list'] as &$item) { + $item['id'] = $i; + $i++; + $item['entry_num'] = bcsub($item['income_num'], $item['exp_num'], 2); + switch ($where['timeType']) { + case "day" : + $item['title'] = "日账单"; + $item['add_time'] = date('Y-m-d', $item['add_time']); + break; + case "week" : + $item['title'] = "周账单"; + $item['add_time'] = '第' . $item['day'] . '周(' . date('m', $item['add_time']) . '月)'; + break; + case "month" : + $item['title'] = "月账单"; + $item['add_time'] = date('Y-m', $item['add_time']); + break; + } + } + return $data; + } + + /** + * 店员交易统计头部数据 + * @param $where + * @return mixed + */ + public function getStatisticsHeader($where) + { + $data = []; + $data['legend'] = '业绩统计'; + $color = ['#2EC479', '#7F7AE5', '#FFA21B', '#46A3FF', '#FF6046', '#5cadff', '#b37feb', '#19be6b', '#ff9900']; + $data['series'] = []; + $list = $this->dao->getStatisticsHeader($where, 'staff_id', 'pay_price'); + if ($list) { + $lists = []; + $i = 0; + foreach ($list as $item) { + $data['series'][$i] = $item['total_number'] ?? 0; + $data['xAxis'][$i] = $item['staff_name'] ?? ''; + if ($i < 5) { + $lists[$i] = $item; + } else { + $lists[5]['staff_name'] = '其他'; + $lists[5]['total_number'] = (float)bcadd((string)($lists[5]['total_number'] ?? 0), (string)$item['total_number'], 2); + } + $i++; + } + foreach ($lists as $key => &$item) { + $data['bing_data'][$key]['itemStyle']['color'] = $color[$key]; + $data['bing_data'][$key]['name'] = $item['staff_name'] ?? ''; + $data['bing_data'][$key]['value'] = $item['total_number'] ?? 0; + $data['bing_xdata'][$key] = $item['staff_name'] ?? ''; + } + $data['yAxis']['maxnum'] = $data['series'] ? max($data['series']) : 0; + } else { + /** @var SystemStoreStaffServices $systemStoreStaffServices */ + $systemStoreStaffServices = app()->make(SystemStoreStaffServices::class); + $storeList = $systemStoreStaffServices->getSelectList(['store_id' => $where['store_id'], 'is_del' => 0, 'status' => 1]); + foreach ($storeList as $store) { + $data['series'][] = 0; + $data['xAxis'][] = $store['label'] ?? ''; + } + } + + return $data; + } + + /** + * 获取一段时间订单统计数量、金额 + * @param $where + * @param $time + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTypeHeader($where, $time) + { + [$start, $end, $timeType, $xAxis] = $time; + $order = $this->dao->orderAddTimeList($where, [$start, $end], $timeType, '*', 'pay_price'); + $price = array_column($order, 'price', 'day'); + $count = array_column($order, 'count', 'day'); + $datas = $series = []; + foreach ($xAxis as $i => $key) { + $datas['销售业绩金额'][] = isset($price[$key]) ? floatval($price[$key]) : 0; + $datas['销售业绩单数'][] = isset($count[$key]) ? floatval($count[$key]) : 0; + $arr = explode('-', $key); + if (count($arr) >= 3) { + $xAxis[$i] = $arr[1] . '-' . $arr[2]; + } + } + + foreach ($datas as $key => $item) { + $series[] = [ + 'name' => $key, + 'data' => $item, + 'type' => 'line', + 'smooth' => 'true', + 'yAxisIndex' => 1, + ]; + } + $data['order']['xAxis'] = $xAxis; + $data['order']['series'] = $series; + + $color = ['#2EC479', '#7F7AE5', '#FFA21B', '#46A3FF', '#FF6046', '#5cadff', '#b37feb', '#19be6b', '#ff9900']; + $data['bing']['series'] = []; + $list = $this->dao->getStatisticsHeader($where, 'type', 'pay_price'); + foreach ($list as $key => &$item) { + $item['type_name'] = isset($this->type[$item['type']]) ? $this->type[$item['type']] : '其他类型'; + $data['bing']['bing_data'][$key]['itemStyle']['color'] = $color[$key]; + $data['bing']['bing_data'][$key]['name'] = $item['type_name']; + $data['bing']['bing_data'][$key]['value'] = $item['total_number']; + $data['bing']['bing_xdata'][$key] = $item['type_name']; + $data['bing']['series'][$key] = $item['total_number']; + $data['bing']['xAxis'][$key] = $item['type_name']; + } + $data['bing']['yAxis']['maxnum'] = $data['bing']['series'] ? max($data['bing']['series']) : 0; + return $data; + } + + /** + * 获取百分比 + * @param $num + * @return string|null + */ + public function getPercent($num) + { + return bcdiv($num, '100', 4); + } + + /** + * 写入流水账单 + * @param $order + * @param int $type + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setFinance($order, $type = 1, $price = 0) + { + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + switch ($type) { + case 1 ://商品订单 + if ($order['store_id'] > 0) { + //门店订单 + //2.1修改门店流水按照下单支付金额计算, + if ($order['type'] == 8) { + $order['pay_price'] = $order['total_price']; + } + $total_price = $order['pay_price']; + //商品总价+支付邮费 +// $total_price = bcadd($total_price, $order['pay_postage'], 2); + $append = [ + 'pay_price' => $total_price, + 'total_price' => $total_price, + 'rate' => 1 + ]; + //支付订单 + $this->savaData($order, $order['pay_price'], 1, 1, 1); + //现金支付增加 + if ($order['pay_type'] == PayServices::CASH_PAY) { + //交易订单记录 + $this->savaData($order, $total_price, 0, 2, 1, $append); + } + //门店订单 + $this->savaData($order, $total_price, 1, 2, 1, $append); + if ($order['shipping_type'] == 1) {//配送订单 + //分配订单费率 + $rate = sys_config('store_self_order_rate'); + $type = 12; + } elseif ($order['shipping_type'] == 2) { + //核销订单费率 + $rate = sys_config('store_writeoff_order_rate'); + $type = 10; + } else if ($order['shipping_type'] == 4) { + //收银订单费率 + $rate = sys_config('store_cashier_order_rate'); + $type = 9; + } else { + //分配订单费率 + $rate = sys_config('store_self_order_rate'); + $type = 11; + } + $total_price = bcmul($total_price, $this->getPercent($rate), 2); + $append['rate'] = $rate; + //交易订单记录 + $this->savaData($order, $total_price, 1, $type, 2, $append); + $this->savaData($order, $total_price, 0, 3, 1, $append); + } else { + $orderList = $storeOrderServices->getSonOrder($order['id'], '*', 1); + if ($orderList) { + foreach ($orderList as $order) { + $total_price = $order['pay_price']; + //商品总价+支付邮费 +// $total_price = bcadd($total_price, $order['pay_postage']); + $append = [ + 'pay_price' => $total_price, + 'total_price' => $total_price, + 'rate' => 1 + ]; + //支付订单 + $this->savaData($order, $order['pay_price'], 1, 1, 1); + //门店订单 + $this->savaData($order, $total_price, 1, 2, 1, $append); + if ($order['shipping_type'] == 1) {//配送订单 + //分配订单费率 + $rate = sys_config('store_self_order_rate'); + $type = 12; + } elseif ($order['shipping_type'] == 2) { + //核销订单费率 + $rate = sys_config('store_writeoff_order_rate'); + $type = 10; + } else if ($order['shipping_type'] == 4) { + //收银订单费率 + $rate = sys_config('store_cashier_order_rate'); + $type = 9; + } else { + //分配订单费率 + $rate = sys_config('store_self_order_rate'); + $type = 11; + } + $total_price = bcmul($total_price, $this->getPercent($rate), 2); + $append['rate'] = $rate; + //交易订单记录 + $this->savaData($order, $total_price, 1, $type, 2); + $this->savaData($order, $total_price, 0, 3, 1, $append); + } + } + } + break; + case 2://充值订单 + //充值订单返点 + $store_recharge_order_rate = sys_config('store_recharge_order_rate'); + $order['pay_type'] = $order['recharge_type']; + $append = [ + 'pay_price' => $order['price'], + 'total_price' => $order['price'], + 'rate' => $store_recharge_order_rate + ]; + //订单账单 + $this->savaData($order, $order['price'], 1, 7, 2, $append); + + //收银台充值线下付款记录一条负记录 + if ($order['recharge_type'] = PayServices::OFFLINE_PAY) { + $this->savaData($order, $order['price'], 0, 7, 1, $append); + } + + //返点 + $pay_price = bcmul($order['price'], $this->getPercent($store_recharge_order_rate), 2); + $this->savaData($order, $pay_price, 1, 5, 1, $append); + break; + case 3://付费会员订单 + //购买付费会员返点 + $store_svip_order_rate = sys_config('store_svip_order_rate'); + $append = [ + 'pay_price' => $order['pay_price'], + 'total_price' => $order['pay_price'], + 'rate' => $store_svip_order_rate + ]; + //订单账单 + $this->savaData($order, $order['pay_price'], 1, 8, 2, $append); + + //收银台充值线下付款记录一条负记录 + if ($order['pay_type'] = PayServices::OFFLINE_PAY) { + $this->savaData($order, $order['pay_price'], 0, 8, 1, $append); + } + + //返点 + $pay_price = bcmul($order['pay_price'], $this->getPercent($store_svip_order_rate), 2); + $this->savaData($order, $pay_price, 1, 6, 1, $append); + break; + case 4://退款 + //取下单流水记录费率 + $rate = $this->dao->value(['link_id' => $order['order_id'], 'type' => 3, 'trade_type' => 1], 'rate'); + if (!$rate) { + //获取失败,如果是子订单;在查询主订单 + if (isset($order['pid']) && $order['pid']) { + $order_id = $storeOrderServices->value(['id' => $order['pid']], 'order_id'); + if ($order_id) $rate = $this->dao->value(['link_id' => $order_id, 'type' => 3, 'trade_type' => 1], 'rate'); + } + } + if (!$rate) {//未获取到,下单保存费率;获取系统配置 + if ($order['shipping_type'] == 2) { + //核销订单费率 + $rate = sys_config('store_writeoff_order_rate'); + } else if ($order['shipping_type'] == 4) { + //收银订单费率 + $rate = sys_config('store_cashier_order_rate'); + } else { + //分配订单费率 + $rate = sys_config('store_self_order_rate'); + } + } + $total_price = bcmul($price, $this->getPercent($rate), 2); + $append['rate'] = $rate; + + //退款 + $this->savaData($order, $price, 0, 4, 1, $append); + $this->savaData($order, $total_price, 1, 3, 1, $append); + break; + case 5://充值退款 + //取充值流水记录费率 + $rate = $this->dao->value(['link_id' => $order['order_id'], 'type' => 5, 'trade_type' => 1], 'rate'); + if (!$rate) {//获取失败,取系统配置 + $rate = sys_config('store_recharge_order_rate'); + } + $order['pay_type'] = $order['recharge_type']; + $append = [ + 'pay_price' => $order['price'], + 'total_price' => $order['price'], + 'rate' => $rate + ]; + //订单账单 + $this->savaData($order, $price, 0, 4, 2, $append); + //返点扣除 + $pay_price = bcmul($price, $this->getPercent($rate), 2); + $this->savaData($order, $pay_price, 0, 5, 1, $append); + break; + case 6://配送订单 + $append = ['pay_price' => $order['cargo_price'],]; + $this->savaData($order, $price, 0, 13, 1, $append); + break; + case 7://取消配送订单 + $append = ['pay_price' => $order['cargo_price'],]; + $this->savaData($order, $price, 1, 13, 1, $append); + break; + } + } + + /** + * 写入数据 + * @param $order + * @param $number + * @param $pm + * @param $type + * @param $trade_type + * @param array $append + * @throws \Exception + */ + public function savaData($order, $number, $pm, $type, $trade_type, array $append = []) + { + /** @var StoreOrderCreateServices $storeOrderCreateServices */ + $storeOrderCreateServices = app()->make(StoreOrderCreateServices::class); + $order_id = $storeOrderCreateServices->getNewOrderId('ls'); + $data = [ + 'store_id' => $order['store_id'] ?? $order['relation_id'] ?? 0, + 'uid' => $order['uid'] ?? 0, + 'staff_id' => $order['staff_id'] ?? 0, + 'order_id' => $order_id, + 'link_id' => $order['order_id'] ?? '', + 'pay_type' => $order['pay_type'] ?? '', + 'trade_time' => $order['pay_time'] ?? $order['add_time'] ?? '', + 'pm' => $pm, + 'number' => $trade_type == 1 ? ($number ?: 0) : 0, + 'type' => $type, + 'trade_type' => $trade_type, + 'add_time' => time() + ]; + $data = array_merge($data, $append); + $this->dao->save($data); + } + + /** + * 关联门店店员 + * @param $link_id + * @param int $staff_id + * @return mixed + */ + public function setStaff($link_id, int $staff_id) + { + return $this->dao->update(['link_id' => $link_id], ['staff_id' => $staff_id]); + } + + /** + * 可提现金额 + * @param array $where + * @return int|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSumFinance(array $where, array $whereData) + { + $field = 'sum(if(pm = 1,number,0)) as income_num,sum(if(pm = 0,number,0)) as exp_num'; + $data = $this->dao->getList($whereData, $field); + if (!$data) return 0; + $income_num = isset($data[0]['income_num']) ? $data[0]['income_num'] : 0; + $exp_num = isset($data[0]['exp_num']) ? $data[0]['exp_num'] : 0; + $number = bcsub($income_num, $exp_num, 2); + //已提现金额 + /** @var StoreExtractServices $storeExtractServices */ + $storeExtractServices = app()->make(StoreExtractServices::class); + $where['not_status'] = -1; + $extract_price = $storeExtractServices->dao->getExtractMoneyByWhere($where, 'extract_price'); + $price_not = bcsub((string)$number, (string)$extract_price, 2); + return $price_not; + } +} diff --git a/app/services/supplier/LoginServices.php b/app/services/supplier/LoginServices.php new file mode 100644 index 0000000..c5c68db --- /dev/null +++ b/app/services/supplier/LoginServices.php @@ -0,0 +1,249 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\supplier; + + +use app\Request; +use app\services\BaseServices; +use app\dao\supplier\SystemSupplierDao; +use app\services\system\SystemMenusServices; +use app\services\system\SystemRoleServices; +use crmeb\exceptions\AdminException; +use app\dao\system\admin\SystemAdminDao; +use crmeb\exceptions\AuthException; +use crmeb\services\CacheService; +use crmeb\traits\ServicesTrait; +use crmeb\utils\ApiErrorCode; +use crmeb\utils\JwtAuth; +use Firebase\JWT\ExpiredException; +use think\exception\ValidateException; +use think\facade\Cache; + + +/** + * + * Class LoginServices + * @package app\services\supplier + * @mixin SystemSupplierDao + */ +class LoginServices extends BaseServices +{ + + use ServicesTrait; + + protected $adminDao; + + /** + * 权限缓存前缀 + */ + const SUPPLIER_RULES_LEVEL = 'store_supplier_rules_level_'; + + /** + * LoginServices constructor. + * @param SystemSupplierDao $dao + */ + public function __construct(SystemSupplierDao $dao, SystemAdminDao $adminDao) + { + $this->dao = $dao; + $this->adminDao = $adminDao; + } + + /** + * 获取登陆前的login等信息 + * @return array + */ + public function getLoginInfo() + { + return [ + 'slide' => sys_data('admin_login_slide') ?? [], + 'logo_square' => sys_config('site_logo_square'),//透明 + 'logo_rectangle' => sys_config('site_logo'),//方形 + 'login_logo' => sys_config('login_logo'),//登陆 + 'site_name' => sys_config('site_name'), + 'site_url' => sys_config('site_url'), + 'upload_file_size_max' => config('upload.filesize'),//文件上传大小kb + ]; + } + + /** + * H5账号登陆 + * @param Request $request + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function login($account, $password, $type) + { + $supplierInfo = $this->adminDao->getOne(['admin_type' => 4, 'account' => $account, 'is_del' => 0], '*', ['supplier']); + $key = 'supplier_login_captcha_' . $account; + if (!$supplierInfo) { + Cache::inc($key); + throw new AdminException('账号不存在!'); + } + if ($password) {//平台还可以登录 + if (!$supplierInfo->status || !$supplierInfo->is_show) { + Cache::inc($key); + throw new AdminException('您已被禁止登录!'); + } + + if (!password_verify($password, $supplierInfo->pwd)) { + Cache::inc($key); + throw new AdminException('账号或密码错误,请重新输入'); + } + } + + $supplierInfo->last_time = time(); + $supplierInfo->last_ip = app('request')->ip(); + $supplierInfo->login_count++; + $supplierInfo->save(); + + $tokenInfo = $this->createToken($supplierInfo['id'], $type, $supplierInfo->pwd); + /** @var SystemMenusServices $services */ + $services = app()->make(SystemMenusServices::class); + [$menus, $uniqueAuth] = $services->getMenusList($supplierInfo->roles, 0, 4); + + return [ + 'token' => $tokenInfo['token'], + 'expires_time' => $tokenInfo['params']['exp'], + 'menus' => $menus, + 'unique_auth' => $uniqueAuth, + 'user_info' => [ + 'id' => $supplierInfo->getData('id'), + 'account' => $supplierInfo->getData('account'), + 'avatar' => $supplierInfo->getData('head_pic'), + ], + 'logo' => sys_config('site_logo'), + 'logo_square' => sys_config('site_logo_square'), + 'version' => get_crmeb_version(), + 'newOrderAudioLink' => get_file_link(sys_config('new_order_audio_link', '')), + 'prefix' => config('admin.supplier_prefix') + ]; + } + + /** + * 重置密码 + * @param $account + * @param $password + */ + public function reset($account, $password) + { + $user = $this->dao->getOne(['account|phone' => $account]); + if (!$user) { + throw new ValidateException('用户不存在'); + } + if (!$this->dao->update($user['id'], ['pwd' => md5((string)$password)])) { + throw new ValidateException('修改密码失败'); + } + return true; + } + + + /** + * 获取Admin授权信息 + * @param string $token + * @return array + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function parseToken(string $token): array + { + /** @var CacheService $cacheService */ + $cacheService = app()->make(CacheService::class); + + if (!$token || $token === 'undefined') { + throw new AuthException(ApiErrorCode::ERR_LOGIN); + } + //检测token是否过期 + $md5Token = md5($token); + if (!$cacheService->hasToken($md5Token) || !($cacheToken = $cacheService->getTokenBucket($md5Token))) { + throw new AuthException(ApiErrorCode::ERR_LOGIN); + } + //是否超出有效次数 + if (isset($cacheToken['invalidNum']) && $cacheToken['invalidNum'] >= 3) { + if (!request()->isCli()) { + $cacheService->clearToken($md5Token); + } + throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID); + } + + /** @var JwtAuth $jwtAuth */ + $jwtAuth = app()->make(JwtAuth::class); + //设置解析token + [$id, $type, $auth] = $jwtAuth->parseToken($token); + //验证token + try { + $jwtAuth->verifyToken(); + $cacheService->setTokenBucket($md5Token, $cacheToken, $cacheToken['exp']); + } catch (ExpiredException $e) { + $cacheToken['invalidNum'] = isset($cacheToken['invalidNum']) ? $cacheToken['invalidNum']++ : 1; + $cacheService->setTokenBucket($md5Token, $cacheToken, $cacheToken['exp']); + } catch (\Throwable $e) { + if (!request()->isCli()) { + $cacheService->clearToken($md5Token); + } + throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID); + } + //获取管理员信息 + $adminInfo = $this->adminDao->getOne(['id' => $id, 'is_del' => 0, 'status' => 1]); + if(!$adminInfo){ + throw new AuthException(ApiErrorCode::ERR_ADMINID_VOID); + } + if ($auth !== md5($adminInfo->pwd)) { + throw new AuthException(ApiErrorCode::ERR_LOGIN_INVALID); + } + $supplierInfo = $this->dao->getOne(['id' =>(int)$adminInfo->relation_id, 'is_del' => 0], '*', ['admin']); + if (!$supplierInfo || !$supplierInfo->account || $supplierInfo->admin_is_del) { + if (!request()->isCli()) { + $cacheService->clearToken($md5Token); + } + throw new AuthException(ApiErrorCode::ERR_LOGIN_STATUS); + } + + $supplierInfo->type = $type; + return $supplierInfo->hidden(['pwd', 'is_del', 'status'])->toArray(); + } + + /** + * 后台验证权限 + * @param Request $request + */ + public function verifiAuth(Request $request) + { + // TODO: 供应商不做验证 + return true; + $rule = str_replace('supplierapi/', '', trim(strtolower($request->rule()->getRule()))); + if (in_array($rule, ['supplier/logout', 'menuslist'])) { + return true; + } + $method = trim(strtolower($request->method())); + /** @var SystemRoleServices $roleServices */ + $roleServices = app()->make(SystemRoleServices::class); + $auth = $roleServices->getAllRoles(2, 4, self::SUPPLIER_RULES_LEVEL); + //验证访问接口是否存在 + if ($auth && !in_array($method . '@@' . $rule, array_map(function ($item) { + return trim(strtolower($item['methods'])). '@@'. trim(strtolower(str_replace(' ', '', $item['api_url']))); + }, $auth))) { + return true; + } + $auth = $roleServices->getRolesByAuth($request->supplierInfo()['roles'], 2, 4, self::SUPPLIER_RULES_LEVEL); + //验证访问接口是否有权限 + if ($auth && empty(array_filter($auth, function ($item) use ($rule, $method) { + if (trim(strtolower($item['api_url'])) === $rule && $method === trim(strtolower($item['methods']))) + return true; + }))) { + throw new AuthException(ApiErrorCode::ERR_AUTH); + } + } + + +} diff --git a/app/services/supplier/SupplierTicketPrintServices.php b/app/services/supplier/SupplierTicketPrintServices.php new file mode 100644 index 0000000..a9ea8de --- /dev/null +++ b/app/services/supplier/SupplierTicketPrintServices.php @@ -0,0 +1,87 @@ + +// +---------------------------------------------------------------------- +namespace app\services\supplier; + +use app\dao\supplier\SupplierTicketPrintDao; +use app\services\BaseServices; +use think\exception\ValidateException; + +/** + * 小票打印 + * Class SupplierTicketPrintServices + * @package app\services\supplier + * @mixin SupplierTicketPrintDao + */ +class SupplierTicketPrintServices extends BaseServices +{ + + /** + * 构造方法 + * SupplierTicketPrintServices constructor. + * @param SupplierTicketPrintDao $dao + */ + public function __construct(SupplierTicketPrintDao $dao) + { + $this->dao = $dao; + } + + + /** + * 获取打印配置 + * @param int $supplierId + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getTicketInfo(int $supplierId, string $field = '*') + { + $info = $this->dao->getOne(['supplier_id' => $supplierId], $field); + if ($info) { + $data = $info->toArray(); + } else { + $data = [ + 'id' => 0, + 'supplier_id' => $supplierId, + 'develop_id' => 0, + 'api_key' => '', + 'client_id' => '', + 'terminal_number' => '', + 'status' => 0, + ]; + } + return $data; + } + + /** + * 更新打印配置 + * @param int $supplierId + * @param $data + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function savePrintData(int $supplierId, $data) + { + $info = $this->dao->getOne(['supplier_id' => $supplierId], 'id, supplier_id'); + if ($info) { + $res = $this->dao->update($info['id'], $data); + } else { + $data['supplier_id'] = $supplierId; + $res = $this->dao->save($data); + } + + if (!$res) throw new ValidateException('保存失败!'); + return true; + } +} diff --git a/app/services/supplier/SystemSupplierServices.php b/app/services/supplier/SystemSupplierServices.php new file mode 100644 index 0000000..5232111 --- /dev/null +++ b/app/services/supplier/SystemSupplierServices.php @@ -0,0 +1,324 @@ + +// +---------------------------------------------------------------------- +namespace app\services\supplier; + +use app\dao\supplier\SystemSupplierDao; +use app\dao\system\admin\SystemAdminDao; +use app\services\BaseServices; +use app\services\order\StoreOrderServices; +use app\services\order\StoreOrderRefundServices; +use app\services\product\branch\StoreBranchProductServices; +use app\services\system\attachment\SystemAttachmentServices; +use app\services\system\SystemUserApplyServices; +use crmeb\exceptions\AdminException; +use think\exception\ValidateException; + +/** + * 供应商 + * Class SystemSupplierServices + * @package app\services\supplier + * @mixin SystemSupplierDao + */ +class SystemSupplierServices extends BaseServices +{ + + protected $adminDao = null; + + /** + * 构造方法 + * SystemSupplierServices constructor. + * @param SystemSupplierDao $dao + * @param SystemAdminDao $adminDao + */ + public function __construct(SystemSupplierDao $dao, SystemAdminDao $adminDao) + { + $this->dao = $dao; + $this->adminDao = $adminDao; + } + + /** + * 获取供应商 + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\DataNotFoundException + */ + public function getSupplierInfo(int $id, string $field = '*', array $with = []) + { + $info = $this->dao->getOne(['id' => $id, 'is_del' => 0], $field, $with); + if (!$info) { + throw new ValidateException('供应商不存在'); + } + return $info; + } + + /** + * 供应商列表 + * @param array $where + * @param array $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSupplierList(array $where, array $field = ['*']) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getSupplierList($where, $field, $page, $limit); + if ($list) { + $prefix = config('admin.supplier_prefix'); + foreach ($list as &$item) { + if (isset($item['add_time']) && $item['add_time']) $item['_add_time'] = date('Y-m-d H:i:s', $item['add_time']); + $item['prefix'] = $prefix; + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 保存供应商 + * @param array $data + * @return mixed + */ + public function create(array $data) + { + if ($this->adminDao->count(['account' => $data['account'], 'admin_type' => 4, 'is_del' => 0])) { + throw new AdminException('管理员账号已存在'); + } + return $this->transaction(function () use ($data) { + $adminData = [ + 'pwd' => $this->passwordHash($data['pwd']), + 'admin_type' => 4, + 'account' => $data['account'], + 'roles' => '', + 'real_name' => $data['name'], + 'phone' => $data['phone'], + 'add_time' => time(), + 'level' => 0 + ]; + unset($data['pwd'], $data['conf_pwd'], $data['account']); + + // 创建管理员 + $res = $this->adminDao->save($adminData); + if (!$res) throw new AdminException('管理员添加失败'); + $data['admin_id'] = (int)$res->id; + $data['add_time'] = time(); + + // 创建供应商 + $relation_id = $this->dao->save($data)->id; + if (!$relation_id) throw new AdminException('供应商添加失败'); + + $this->adminDao->update($res->id, ['relation_id' => $relation_id]); + return $relation_id; + }); + } + + /** + * 修改管理员 + * @param array $data + * @return mixed + */ + public function save(int $id, array $data) + { + if (!$supplierInfo = $this->dao->get($id)) { + throw new AdminException('供应商不存在,无法修改'); + } + if ($supplierInfo->is_del) { + throw new AdminException('供应商已经删除'); + } + if (!$adminInfo = $this->adminDao->get($supplierInfo['admin_id'])) { + throw new AdminException('管理员不存在,无法修改'); + } + if ($adminInfo->is_del) { + throw new AdminException('管理员已经删除'); + } + //修改账号 + if (isset($data['account']) && $data['account'] != $adminInfo->account && $this->adminDao->isAccountUsable($data['account'], $supplierInfo['admin_id'], 4)) { + throw new AdminException('管理员账号已存在'); + } + return $this->transaction(function () use ($id, $data, $adminInfo, $supplierInfo) { + + $adminData = [ + 'pwd' => $this->passwordHash($data['pwd']), + 'real_name' => $data['name'] ?? $adminInfo->real_name, + 'phone' => $data['phone'] ?? $adminInfo->phone, + 'account' => $data['account'] ?? $adminInfo->account + ]; + // 修改管理员 + $res = $this->adminDao->update($adminInfo['id'], $adminData); + if (!$res) throw new AdminException('管理员修改失败'); + + // 修改供应商 + unset($data['pwd'], $data['conf_pwd'], $data['account']); + $this->dao->update($id, $data); + $res1 = $supplierInfo->save(); + if (!$res1) throw new AdminException('供应商修改失败'); + return true; + }); + } + + /** + * @param int $id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function delete(int $id) + { + if (!$supplierInfo = $this->dao->get($id)) { + throw new AdminException('供应商不存在,无法修改'); + } + if ($supplierInfo->is_del) { + throw new AdminException('供应商已经删除'); + } + if (!$adminInfo = $this->adminDao->get($supplierInfo['admin_id'])) { + throw new AdminException('管理员不存在,无法删除'); + } + if ($adminInfo->is_del) { + throw new AdminException('管理员已经删除'); + } + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + $orderCount = $storeOrderServices->count(['supplier_id' => $id, 'status' => 0]); + if (!$orderCount) { + $orderCount = $storeOrderServices->count(['supplier_id' => $id, 'status' => 1]); + if (!$orderCount) { + $orderCount = $storeOrderServices->count(['supplier_id' => $id, 'status' => 5]); + } + } + if ($orderCount) { + return $this->fail('删除失败,该供应商还有待处理订单'); + } + return $this->transaction(function () use ($id, $supplierInfo, $adminInfo) { + $adminInfo->status = 0; + $adminInfo->is_del = 1; + // 修改管理员 + $res = $adminInfo->save(); + if (!$res) throw new AdminException('管理员删除失败'); + + $supplierInfo->is_show = 0; + $supplierInfo->is_del = 1; + // 修改供应商 + $res1 = $supplierInfo->save(); + if (!$res1) throw new AdminException('供应商删除失败'); + + /** @var StoreBranchProductServices $storeBranchProducesServices */ + $storeBranchProducesServices = app()->make(StoreBranchProductServices::class); + //删除供应商商品 + $storeBranchProducesServices->deleteProducts([], 2, $id); + /** @var SystemAttachmentServices $attach */ + $attach = app()->make(SystemAttachmentServices::class); + //删除附件 + $attach->delAttachment([], 4, $id); + return true; + }); + } + + + /** + * 平台供应商运营统计 + * @param int $supplierId + * @param array $time + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function supplierChart(int $supplierId, array $time) + { + $list = $this->dao->getSupplierList(['is_del' => 0, 'is_show' => 1], ['id', 'supplier_name']); + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + /** @var StoreOrderRefundServices $orderRefundServices */ + $orderRefundServices = app()->make(StoreOrderRefundServices::class); + $where = ['time' => $time]; + + $order_where = ['paid' => 1, 'pid' => 0, 'is_del' => 0, 'is_system_del' => 0, 'refund_status' => [0, 3]]; + $refund_where = ['refund_type' => 6]; + foreach ($list as &$item) { + $supplier_where = ['supplier_id' => $item['id']]; + $item['order_price'] = $orderServices->sum($where + $supplier_where + $order_where, 'pay_price', true); + $item['order_count'] = $orderServices->count($where + $supplier_where + $order_where); + $item['refund_order_price'] = $orderRefundServices->sum($where + $supplier_where + $refund_where, 'refunded_price', true); + $item['refund_order_count'] = $orderRefundServices->count($where + $supplier_where + $refund_where); + } + return $list; + } + + /** + * 供应商选择列表 + * @param array $where + * @param array $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSupplierSearch(array $where, array $field = ['*']) + { + return $this->dao->getSupplierList($where, $field, 0, 0); + } + + /** + * 供应商入住审核通过创建数据 + * @param int $applyId + * @param array $info + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function verifyAgreeCreate(int $applyId, array $info = []) + { + if (!$applyId) { + throw new ValidateException('缺少申请ID'); + } + /** @var SystemUserApplyServices $applyServices */ + $applyServices = app()->make(SystemUserApplyServices::class); + if (!$info) { + $info = $applyServices->get($applyId); + if (!$info) { + throw new ValidateException('申请数据不存在'); + } + $info = $info->toArray(); + } + $data = [ + 'supplier_name' => $info['system_name'], + 'account' => $this->getAccount($info['phone']), + 'phone' => $info['phone'], + 'name' => $info['name'], + 'pwd' => substr($info['phone'], -6) + ]; + $supplier_id = $this->create($data); + return $this->dao->get($supplier_id)->toArray(); + } + + /** + * 获取同意申请 创建账号 + * @param string $phone + * @return string + */ + public function getAccount(string $phone) + { + $account = ''; + if ($phone) { + //当前手机号当作账号是否存在 + $adminDCount = $this->adminDao->count(['account' => $phone, 'admin_type' => 4, 'is_del' => 0]); + $account = $phone; + if ($adminDCount) { + $account = $account . '_' . $adminDCount; + } + } + return $account; + } + +} diff --git a/app/services/supplier/finance/SupplierExtractServices.php b/app/services/supplier/finance/SupplierExtractServices.php new file mode 100644 index 0000000..3331e7a --- /dev/null +++ b/app/services/supplier/finance/SupplierExtractServices.php @@ -0,0 +1,238 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\supplier\finance; + +use app\services\supplier\finance\SupplierFlowingWaterServices; +use app\dao\supplier\finance\SupplierExtractDao; +use app\dao\store\StoreUserDao; +use app\services\BaseServices; +use app\services\store\SystemStoreServices; +use app\services\store\SystemStoreStaffServices; +use app\services\supplier\SystemSupplierServices; +use app\services\system\admin\SystemAdminServices; +use crmeb\services\FormBuilder as Form; +use think\exception\ValidateException; +use think\facade\Route as Url; + +/** + * 门店提现 + * Class StoreExtractServices + * @package app\services\store\finance + * @mixin SupplierExtractDao + */ +class SupplierExtractServices extends BaseServices +{ + /** + * 构造方法 + * StoreUser constructor. + * @param StoreUserDao $dao + */ + public function __construct(SupplierExtractDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取一条提现记录 + * @param int $id + * @param array $field + * @return array|\think\Model|null + */ + public function getExtract(int $id, array $field = []) + { + return $this->dao->get($id, $field); + } + + /** + * 显示资源列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function index(array $where, array $whereData = []) + { + $list = $this->getStoreExtractList($where); + //待审核金额 + $where['status'] = 0; + $extract_statistics['price'] = $this->dao->getExtractMoneyByWhere($where, 'extract_price'); + + //待转账金额 + $where['status'] = 1; + $where['pay_status'] = 0; + $extract_statistics['unPayPrice'] = $this->dao->getExtractMoneyByWhere($where, 'extract_price'); + //累计提现 + $where['status'] = 1; + $where['pay_status'] = 1; + $extract_statistics['paidPrice'] = $this->dao->getExtractMoneyByWhere($where, 'extract_price'); + $extract_statistics['price_count'] = 0; + //未提现金额 + /** @var SupplierFlowingWaterServices $financeFlowServices */ + $financeFlowServices = app()->make(SupplierFlowingWaterServices::class); + $price_not = $financeFlowServices->getSumFinance(['supplier_id' => isset($where['supplier_id']) && $where['supplier_id'] ? $where['supplier_id'] : 0], $whereData); + $extract_statistics['price_not'] = max($price_not, 0); + + $extract_statistics['extract_min_price'] = sys_config('supplier_extract_min_price'); + $extract_statistics['extract_max_price'] = sys_config('supplier_extract_max_price'); + return compact('extract_statistics', 'list'); + } + + + /** + * 获取提现列表 + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getStoreExtractList(array $where, string $field = '*') + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getExtractList($where, $field, ['supplier'], $page, $limit); + if ($list) { + /** @var SystemAdminServices $adminServices */ + $adminServices = app()->make(SystemAdminServices::class); + $adminIds = array_unique(array_column($list, 'admin_id')); + $adminInfos = []; + + if ($adminIds) $adminInfos = $adminServices->getColumn([['id', 'in', $adminIds]], 'id,real_name', 'id'); + foreach ($list as &$item) { + $item['add_time'] = $item['add_time'] ? date("Y-m-d H:i:s", $item['add_time']) : ''; + $item['admin_name'] = $item['admin_id'] ? ($adminInfos[$item['admin_id']]['real_name'] ?? '') : ''; + } + } + + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 提现申请 + * @param int $supplierId + * @param array $data + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function cash(int $supplierId, array $data) + { + /** @var SystemSupplierServices $systemSupplier */ + $systemSupplier = app()->make(SystemSupplierServices::class); + $supplierInfo = $systemSupplier->getSupplierInfo($supplierId); + + $insertData = []; + switch ($data['extract_type']) { + case 'bank': + if (!$supplierInfo['bank_code'] || !$supplierInfo['bank_address']) { + throw new ValidateException('请先设置提现银行与开户行信息'); + } + $insertData['bank_code'] = $supplierInfo['bank_code']; + $insertData['bank_address'] = $supplierInfo['bank_address']; + break; + case 'alipay': + if (!$supplierInfo['alipay_account'] || !$supplierInfo['alipay_qrcode_url']) { + throw new ValidateException('请先设置提现支付宝信息'); + } + $insertData['alipay_account'] = $supplierInfo['alipay_account']; + $insertData['qrcode_url'] = $supplierInfo['alipay_qrcode_url']; + break; + case 'weixin': + if (!$supplierInfo['wechat'] || !$supplierInfo['wechat_qrcode_url']) { + throw new ValidateException('请先设置提现微信信息'); + } + $insertData['wechat'] = $supplierInfo['wechat']; + $insertData['qrcode_url'] = $supplierInfo['wechat_qrcode_url']; + break; + default: + throw new ValidateException('暂不支持该类型提现'); + break; + } + $insertData['supplier_id'] = $supplierInfo['id']; + $insertData['extract_type'] = $data['extract_type']; + $insertData['extract_price'] = $data['money']; + $insertData['add_time'] = time(); + $insertData['supplier_mark'] = $data['mark']; + $insertData['status'] = 0; + if (!$this->dao->save($insertData)) { + return false; + } + return true; + } + + + /** + * 拒绝 + * @param $id + * @return mixed + */ + public function refuse(int $id, string $message, int $adminId) + { + $extract = $this->getExtract($id); + if (!$extract) { + throw new ValidateException('操作记录不存在!'); + } + if ($extract->status == 1) { + throw new ValidateException('已经提现,错误操作'); + } + if ($extract->status == -1) { + throw new ValidateException('您的提现申请已被拒绝,请勿重复操作!'); + } + if ($this->dao->update($id, ['fail_time' => time(), 'fail_msg' => $message, 'status' => -1, 'admin_id' => $adminId])) { + return true; + } else { + throw new ValidateException('操作失败!'); + } + } + + /** + * 通过 + * @param $id + * @return mixed + */ + public function adopt(int $id, int $adminId) + { + $extract = $this->getExtract($id); + if (!$extract) { + throw new ValidateException('操作记录不存!'); + } + if ($extract->status == 1) { + throw new ValidateException('您已提现,请勿重复提现!'); + } + if ($extract->status == -1) { + throw new ValidateException('您的提现申请已被拒绝!'); + } + if ($this->dao->update($id, ['status' => 1, 'admin_id' => $adminId])) { + return true; + } else { + throw new ValidateException('操作失败!'); + } + } + + /** + * 转账页面 + * @param int $id + * @return string + */ + public function add_transfer(int $id) + { + $field = array(); + $title = '转账信息'; + $field[] = Form::input('voucher_title', '转账说明', '')->maxlength(30)->required(); + $field[] = Form::frameImage('voucher_image', '转账凭证', Url::buildUrl(config('admin.admin_prefix') . '/widget.images/index', array('fodder' => 'voucher_image')), '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + return create_form($title, $field, Url::buildUrl('/supplier/extract/save_transfer/' . $id), 'POST'); + } +} diff --git a/app/services/supplier/finance/SupplierFlowingWaterServices.php b/app/services/supplier/finance/SupplierFlowingWaterServices.php new file mode 100644 index 0000000..e22359a --- /dev/null +++ b/app/services/supplier/finance/SupplierFlowingWaterServices.php @@ -0,0 +1,246 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\supplier\finance; + + +use app\dao\supplier\finance\SupplierFlowingWaterDao; +use app\services\supplier\finance\SupplierTransactionsServices; +use app\services\BaseServices; +use app\services\order\StoreOrderCreateServices; +use app\services\order\StoreOrderCartInfoServices; +use app\services\order\StoreOrderRefundServices; +use app\services\order\StoreOrderServices; +use app\services\pay\PayServices; + +/** + * 供应商流水 + * Class SupplierFlowingWaterServices + * @package app\services\supplier\finance + * @mixin SupplierFlowingWaterDao + */ +class SupplierFlowingWaterServices extends BaseServices +{ + /** + * 支付类型 + * @var string[] + */ + public $pay_type = ['weixin' => '微信支付', 'yue' => '余额支付', 'offline' => '线下支付', 'alipay' => '支付宝支付', 'cash' => '现金支付', 'automatic' => '自动转账', 'store' => '微信支付']; + + /** + * 交易类型 + * @var string[] + */ + public $type = [ + 1 => '支付订单', + 2 => '退款订单' + ]; + + /** + * 构造方法 + * @param SupplierFlowingWaterDao $dao + */ + public function __construct(SupplierFlowingWaterDao $dao) + { + $this->dao = $dao; + } + + /** + * 显示资源列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit, ['user', 'supplier' => function ($query) { + $query->field('id,supplier_name')->bind(['supplier_name']); + }]); + foreach ($list as &$item) { + $item['type_name'] = isset($this->type[$item['type']]) ? $this->type[$item['type']] : '其他类型'; + $item['pay_type_name'] = isset($this->pay_type[$item['pay_type']]) ? $this->pay_type[$item['pay_type']] : '其他方式'; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + $item['trade_time'] = $item['trade_time'] ? date('Y-m-d H:i:s', $item['trade_time']) : $item['add_time']; + $item['user_nickname'] = $item['user_nickname'] ?: '游客'; + } + $count = $this->dao->getCount($where); + return compact('list', 'count'); + } + + /** + * 供应商账单 + * @param $where + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFundRecord($where) + { + [$page, $limit] = $this->getPageValue(); + $where['is_del'] = 0; + $data = $this->dao->getFundRecord($where, $page, $limit); + $i = 1; + foreach ($data['list'] as &$item) { + $item['id'] = $i; + $i++; + $item['entry_num'] = bcsub($item['income_num'], $item['exp_num'], 2); + switch ($where['timeType']) { + case "day" : + $item['title'] = "日账单"; + $item['add_time'] = date('Y-m-d', $item['add_time']); + break; + case "week" : + $item['title'] = "周账单"; + $item['add_time'] = '第' . $item['day'] . '周(' . date('m', $item['add_time']) . '月)'; + break; + case "month" : + $item['title'] = "月账单"; + $item['add_time'] = date('Y-m', $item['add_time']); + break; + } + } + return $data; + } + + /** + * 获取百分比 + * @param $num + * @return string|null + */ + public function getPercent($num) + { + return bcdiv($num, '100', 4); + } + + /**写入流水账单 + * @param $oid + * @param $type + * @return bool|void + * @throws \Exception + */ + public function setSupplierFinance($oid, $type = 1) + { + /** @var SupplierTransactionsServices $transactionsServices */ + $transactionsServices = app()->make(SupplierTransactionsServices::class); + /** @var StoreOrderCartInfoServices $cartInfoServices */ + $cartInfoServices = app()->make(StoreOrderCartInfoServices::class); + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + /** @var StoreOrderRefundServices $storeOrderRefundServices */ + $storeOrderRefundServices = app()->make(StoreOrderRefundServices::class); + $order = $storeOrderServices->get($oid); + if (!$order) { + return true; + } + if ($order['supplier_id'] <= 0) return true; + $data = $cartInfoServices->getOrderCartInfoSettlePrice($order['id']); + $pay_postage = 0; + if (isset($order['shipping_type']) && !in_array($order['shipping_type'], [2, 4])) { + $pay_postage = floatval($storeOrderRefundServices->getOrderSumPrice($data['info'], 'postage_price', false)); + } + if ($order['type'] == 8) { + $order['pay_price'] = $order['total_price']; + } + $append = [ + 'pay_price' => $order['pay_price'], + 'pay_postage' => $pay_postage, + 'total_price' => $order['total_price'], + ]; + switch ($type) { + case 1 ://支付 + $number = bcadd((string)$data['settlePrice'], $pay_postage, 2); + //支付订单 + $this->savaData($order, $number, 1, 1, $append); + + //交易订单记录 + $transactionsServices->savaData($order, 1, 1, $append); + break; + case 2://退款 + $number = bcadd((string)$data['refundSettlePrice'], $pay_postage, 2); + $this->savaData($order, $number, 0, 2, $append); + + //交易订单记录 + $transactionsServices->savaData($order, 0, 2, $append); + break; + } + } + + /** + * 写入数据 + * @param $order + * @param $number + * @param $pm + * @param $type + * @param $trade_type + * @param array $append + * @throws \Exception + */ + public function savaData($order, $number, $pm, $type, array $append = []) + { + /** @var StoreOrderCreateServices $storeOrderCreateServices */ + $storeOrderCreateServices = app()->make(StoreOrderCreateServices::class); + $order_id = $storeOrderCreateServices->getNewOrderId('ls'); + $data = [ + 'supplier_id' => $order['supplier_id'] ?? 0, + 'uid' => $order['uid'] ?? 0, + 'order_id' => $order_id, + 'link_id' => $order['order_id'] ?? '', + 'pay_type' => $order['pay_type'] ?? '', + 'trade_time' => $order['pay_time'] ?? $order['add_time'] ?? '', + 'pm' => $pm, + 'number' => $number ?: 0, + 'type' => $type, + 'add_time' => time() + ]; + $data = array_merge($data, $append); + $this->dao->save($data); + } + + /** + * 关联门店店员 + * @param $link_id + * @param int $staff_id + * @return mixed + */ + public function setStaff($link_id, int $staff_id) + { + return $this->dao->update(['link_id' => $link_id], ['staff_id' => $staff_id]); + } + + /** + * 可提现金额 + * @param array $where + * @return int|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSumFinance(array $where, array $whereData) + { + $field = 'sum(if(pm = 1,number,0)) as income_num,sum(if(pm = 0,number,0)) as exp_num'; + $data = $this->dao->getList($whereData, $field); + if (!$data) return 0; + $income_num = isset($data[0]['income_num']) ? $data[0]['income_num'] : 0; + $exp_num = isset($data[0]['exp_num']) ? $data[0]['exp_num'] : 0; + $number = bcsub($income_num, $exp_num, 2); + //已提现金额 + /** @var SupplierExtractServices $extractServices */ + $extractServices = app()->make(SupplierExtractServices::class); + $where['not_status'] = -1; + $extract_price = $extractServices->dao->getExtractMoneyByWhere($where, 'extract_price'); + $price_not = bcsub((string)$number, (string)$extract_price, 2); + return $price_not; + } +} diff --git a/app/services/supplier/finance/SupplierTransactionsServices.php b/app/services/supplier/finance/SupplierTransactionsServices.php new file mode 100644 index 0000000..16f24ec --- /dev/null +++ b/app/services/supplier/finance/SupplierTransactionsServices.php @@ -0,0 +1,95 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\supplier\finance; + + +use app\dao\supplier\finance\SupplierTransactionsDao; +use app\dao\store\StoreUserDao; +use app\services\BaseServices; +use app\services\order\StoreOrderCreateServices; +use app\services\order\StoreOrderServices; +use app\services\pay\PayServices; + +/** + * 门店流水 + * Class StoreExtractServices + * @package app\services\store\finance + * @mixin SupplierTransactionsDao + */ +class SupplierTransactionsServices extends BaseServices +{ + /** + * 支付类型 + * @var string[] + */ + public $pay_type = ['weixin' => '微信支付', 'yue' => '余额支付', 'offline' => '线下支付', 'alipay' => '支付宝支付', 'cash' => '现金支付', 'automatic' => '自动转账', 'store' => '微信支付']; + + /** + * 交易类型 + * @var string[] + */ + public $type = [ + 1 => '支付订单', + 2 => '支付订单', + 3 => '订单手续费', + 4 => '退款订单', + 5 => '充值返点', + 6 => '付费会员返点', + 7 => '充值订单', + 8 => '付费订单', + 9 => '收银订单', + 10 => '核销订单', + 11 => '分配订单', + 12 => '配送订单', + 13 => '同城配送订单', + ]; + + /** + * 构造方法 + * StoreUser constructor. + * @param StoreUserDao $dao + */ + public function __construct(SupplierTransactionsDao $dao) + { + $this->dao = $dao; + } + /** + * 写入数据 + * @param $order + * @param $number + * @param $pm + * @param $type + * @param $trade_type + * @param array $append + * @throws \Exception + */ + public function savaData($order, $pm, $type, array $append = []) + { + /** @var StoreOrderCreateServices $storeOrderCreateServices */ + $storeOrderCreateServices = app()->make(StoreOrderCreateServices::class); + $order_id = $storeOrderCreateServices->getNewOrderId('ls'); + $data = [ + 'supplier_id' => $order['supplier_id'] ?? 0, + 'uid' => $order['uid'] ?? 0, + 'order_id' => $order_id, + 'link_id' => $order['order_id'] ?? '', + 'pay_type' => $order['pay_type'] ?? '', + 'trade_time' => $order['pay_time'] ?? $order['add_time'] ?? '', + 'pm' => $pm, + 'type' => $type, + 'add_time' => time() + ]; + $data = array_merge($data, $append); + $this->dao->save($data); + } +} diff --git a/app/services/system/CapitalFlowServices.php b/app/services/system/CapitalFlowServices.php new file mode 100644 index 0000000..9d92192 --- /dev/null +++ b/app/services/system/CapitalFlowServices.php @@ -0,0 +1,193 @@ + '商城购物', + 2 => '商城购物退款', + 3 => '用户充值', + 4 => '用户充值退款', + 5 => '抽奖中奖', + 6 => '佣金提现', + 7 => '购买会员', + 8 => '线下支付', + ]; + + /** + * @param CapitalFlowDao $dao + */ + public function __construct(CapitalFlowDao $dao) + { + $this->dao = $dao; + } + + /** + * 添加资金流水 + * @param $orderInfo + * @param string $type + */ + public function setFlow($data, $type = '') + { + $data['flow_id'] = 'ZJ' . date('Ymdhis', time()) . rand('1000', '9999'); + switch ($type) { + case 'order': + $data['trading_type'] = 1; + break; + case 'refund': + $data['price'] = bcmul('-1', $data['price'], 2); + $data['trading_type'] = 2; + break; + case 'recharge': + $data['trading_type'] = 3; + break; + case 'refund_recharge': + $data['price'] = bcmul('-1', $data['price'], 2); + $data['trading_type'] = 4; + break; + case 'luck': + $data['price'] = bcmul('-1', $data['price'], 2); + $data['trading_type'] = 5; + break; + case 'extract': + $data['price'] = bcmul('-1', $data['price'], 2); + $data['trading_type'] = 6; + break; + case 'pay_member': + $data['trading_type'] = 7; + break; + case 'offline_scan': + $data['trading_type'] = 8; + break; + default: + break; + } + $data['add_time'] = time(); + $this->dao->save($data); + } + + /** + * 获取资金流水 + * @param $where + * @return array + */ + public function getFlowList($where) + { + $export = $where['export'] ?? 0; + unset($where['export']); + [$page, $limit] = $this->getPageValue(); + $status = ['未知', '支付订单', '订单退款', '充值订单', '充值退款', '抽奖红包', '佣金提现', '购买会员', '线下收银']; + $pay_type = ['weixin' => '微信支付', 'routine' => '小程序', 'alipay' => '支付宝', 'offline' => '线下支付', 'bank' => '银行']; + $list = $this->dao->getList($where, '*', $page, $limit); + if ($list) { + $typeName = $this->typeName; + foreach ($list as &$item) { + $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']); + $item['trading_type'] = $typeName[$item['trading_type']] ?? '未知'; + $item['pay_type'] = $pay_type[$item['pay_type']] ?? ''; + } + } + + $count = $this->dao->count($where); + if ($export) { + $fileKey = ['flow_id', 'order_id', 'nickname', 'phone', 'price', 'trading_type', 'pay_type', 'add_time', 'mark']; + $header = ['交易单号', '关联订单', '用户', '电话', '金额', '订单类型', '支付类型', '交易时间', '备注']; + $fileName = '账单导出' . date('YmdHis') . rand(1000, 9999); + return compact('list', 'fileKey', 'header', 'fileName'); + } else { + return compact('list', 'count', 'status'); + } + } + + /** + * 添加备注 + * @param $id + * @param $data + * @return bool + */ + public function setMark($id, $data) + { + $res = $this->dao->update($id, $data); + if ($res) { + return true; + } else { + throw new AdminException('备注失败'); + } + } + + /** + * 获取账单记录 + * @param $where + * @return array + */ + public function getFlowRecord($where) + { + [$page, $limit] = $this->getPageValue(); + $data = $this->dao->getRecordList($where, $page, $limit); + $i = 1; + foreach ($data['list'] as &$item) { + $item['id'] = $i; + $i++; + $item['entry_price'] = bcadd($item['income_price'], $item['exp_price'], 2); + switch ($where['type']) { + case "day" : + $item['title'] = "日账单"; + $item['add_time'] = date('Y-m-d', $item['add_time']); + break; + case "week" : + $item['title'] = "周账单"; + $item['add_time'] = '第' . $item['day'] . '周(' . date('m', $item['add_time']) . '月)'; + break; + case "month" : + $item['title'] = "月账单"; + $item['add_time'] = date('Y-m', $item['add_time']); + break; + } + } + return $data; + } + + /** + * 用户资金记录 + * @param int $uid + * @param array $data + * @return array + */ + public function userCapitalList(int $uid, array $data) + { + $where = []; + $where['uid'] = $uid; + $where['trading_type'] = [1, 7]; + if ((isset($data['start']) && $data['start']) || (isset($data['stop']) && $data['stop'])) { + $where['time'] = [$data['start'], $data['stop']]; + } + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit); + $times = []; + if ($list) { + $typeName = $this->typeName; + foreach ($list as &$item) { + $item['time_key'] = $item['add_time'] ? date('Y-m', (int)$item['add_time']) : ''; + $item['day'] = $item['add_time'] ? date('Y-m-d', (int)$item['add_time']) : ''; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i', (int)$item['add_time']) : ''; + $item['type'] = $item['trading_type']; + $item['type_name'] = $typeName[$item['type'] ?? ''] ?? '未知类型'; + $item['title'] = $item['type_name']; + } + $times = array_merge(array_unique(array_column($list, 'time_key'))); + } + return ['list' => $list, 'time' => $times]; + } +} diff --git a/app/services/system/SystemMenusServices.php b/app/services/system/SystemMenusServices.php new file mode 100644 index 0000000..1c825a8 --- /dev/null +++ b/app/services/system/SystemMenusServices.php @@ -0,0 +1,298 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\system; + +use app\common\dao\system\menu\MenuDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use crmeb\utils\Arr; + +/** + * 权限菜单 + * Class SystemMenusServices + * @package app\services\system + * @mixin MenuDao + */ +class SystemMenusServices extends BaseServices +{ + + /** + * @var string[] + */ + protected $type = [ + 1 => 'admin',//平台 + 2 => 'store',//门店 + 3 => 'cashier',//收银台 + 4 => 'supplier',//供应商 + ]; + + /** + * 初始化 + * SystemMenusServices constructor. + * @param MenuDao $dao + */ + public function __construct(MenuDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取菜单没有被修改器修改的数据 + * @param $menusList + * @return array + */ + public function getMenusData($menusList, int $type = 1) + { + $data = []; + foreach ($menusList as $item) { +// $item['expand'] = true; + $item['selected'] = false; + $item['title'] = $item['menu_name']; + $item['menu_path'] = preg_replace('/^\/' . ($this->type[$type] ?? 'admin') . '/', '', $item['menu_path']); + $data[] = $item->getData(); + } + return $data; + } + + /** + * 获取后台权限菜单和权限 + * @param $rouleId + * @param int $level + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMenusList($rouleId, int $level, int $type = 1) + { + $rulesStr = ''; + if ($level) {//超级管理员查询所有菜单 + /** @var SystemRoleServices $systemRoleServices */ + $systemRoleServices = app()->make(SystemRoleServices::class); + $rules = $systemRoleServices->getRoleArray(['status' => 1, 'id' => $rouleId], $type == 3 ? 'cashier_rules' : 'rules'); + $rulesStr = Arr::unique($rules); + } + $menusList = $this->dao->getMenusRoule(['type' => $type, 'route' => $level ? $rulesStr : '']); + $unique = $this->dao->getMenusUnique(['type' => $type, 'unique' => $level ? $rulesStr : '']); + return [Arr::getMenuIviewList($this->getMenusData($menusList, $type)), $unique]; + } + + /** + * 获取后台菜单树型结构列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where) + { + $menusList = $this->dao->getMenusList($where); + $menusList = $this->getMenusData($menusList); + return get_tree_children($menusList); + } + + /** + * 获取form表单所需要的所要的菜单列表 + * @return array[] + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + protected function getFormSelectMenus() + { + $menuList = $this->dao->getMenusRoule(['is_del' => 0], ['id', 'pid', 'menu_name']); + $list = get_tree_children($this->getMenusData($menuList), '0', 'pid', 'id'); + $menus = [['value' => 0, 'label' => '顶级按钮']]; + foreach ($list as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['html'] . $menu['menu_name']]; + } + return $menus; + } + + /** + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + protected function getFormCascaderMenus(int $value = 0, int $type = 1) + { + $menuList = $this->dao->getMenusRoule(['is_del' => 0, 'type' => $type], ['id as value', 'pid', 'menu_name as label']); + $menuList = $this->getMenusData($menuList); + if ($value) { + $data = get_tree_value($menuList, $value); + } else { + $data = []; + } + return [get_tree_children($menuList, 'children', 'value'), array_reverse($data)]; + } + + /** + * 创建权限规格生表单 + * @param array $formData + * @return mixed + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createMenusForm(array $formData = [], int $type = 1) + { + $field[] = Form::hidden('type', $type); + $field[] = Form::input('menu_name', '按钮名称', $formData['menu_name'] ?? '')->required('按钮名称必填'); + // $field[] = Form::select('pid', '父级id', $formData['pid'] ?? 0)->setOptions($this->getFormSelectMenus())->filterable(1); + $field[] = Form::input('menu_path', '路由名称', $formData['menu_path'] ?? '')->placeholder('请输入前台跳转路由地址')->required('请填写前台路由地址'); + $field[] = Form::input('unique_auth', '权限标识', $formData['unique_auth'] ?? '')->placeholder('不填写则后台自动生成'); + $params = $formData['params'] ?? ''; + $field[] = Form::input('params', '参数', is_array($params) ? '' : $params)->placeholder('举例:a/123/b/234'); + $field[] = Form::frameInput('icon', '图标', $this->url(config('admin.admin_prefix') . '/widget.widgets/icon', ['fodder' => 'icon']), $formData['icon'] ?? '')->icon('md-add')->height('500px'); + $field[] = Form::number('sort', '排序', (int)($formData['sort'] ?? 0))->min(0); + $field[] = Form::radio('auth_type', '类型', $formData['auth_type'] ?? 1)->options([['value' => 2, 'label' => '接口'], ['value' => 1, 'label' => '菜单(菜单只显示三级)']]); + $field[] = Form::radio('is_show', '状态', $formData['is_show'] ?? 1)->options([['value' => 0, 'label' => '关闭'], ['value' => 1, 'label' => '开启']]); + $field[] = Form::radio('is_show_path', '是否为前端隐藏菜单', $formData['is_show_path'] ?? 0)->options([['value' => 1, 'label' => '是'], ['value' => 0, 'label' => '否']]); + [$menuList, $data] = $this->getFormCascaderMenus((int)($formData['pid'] ?? 0), $type); + $field[] = Form::cascader('menu_list', '父级id', $data)->data($menuList)->filterable(true); + return $field; + } + + /** + * 新增权限表单 + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createMenus(int $type = 1) + { + return create_form('添加权限', $this->createMenusForm([], $type), $this->url('/setting/save')); + } + + /** + * 修改权限菜单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateMenus(int $id) + { + $menusInfo = $this->dao->get($id); + if (!$menusInfo) { + throw new AdminException('数据不存在'); + } + $menusInfo = $menusInfo->getData(); + return create_form('修改权限', $this->createMenusForm($menusInfo, $menusInfo['type'] ?? 1), $this->url('/setting/update/' . $id), 'PUT'); + } + + /** + * 获取一条数据 + * @param int $id + * @return mixed + */ + public function find(int $id) + { + $menusInfo = $this->dao->get($id); + if (!$menusInfo) { + throw new AdminException('数据不存在'); + } + $menu = $menusInfo->getData(); + $menu['pid'] = (int)$menu['pid']; + $menu['auth_type'] = (int)$menu['auth_type']; + $menu['is_header'] = (int)$menu['is_header']; + $menu['is_show'] = (int)$menu['is_show']; + $menu['is_show_path'] = (int)$menu['is_show_path']; + if (!$menu['path']) { + [$menuList, $data] = $this->getFormCascaderMenus($menu['pid']); + $menu['path'] = $data; + } else { + $menu['path'] = explode('/', $menu['path']); + if (is_array($menu['path'])) { + $menu['path'] = array_map(function ($item) { + return (int)$item; + }, $menu['path']); + } + } + return $menu; + } + + /** + * 删除菜单 + * @param int $id + * @return mixed + */ + public function delete(int $id) + { + if ($this->dao->count(['pid' => $id])) { + throw new AdminException('请先删除改菜单下的子菜单'); + } + return $this->dao->delete($id); + } + + /** + * 获取添加身份规格 + * @param $roles + * @param int $type + * @param int $is_show + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMenus($roles, int $type = 1, int $is_show = 1): array + { + $field = ['menu_name', 'pid', 'id']; + $where = ['is_del' => 0, 'type' => $type]; + if ($is_show) $where['is_show'] = 1; + if (!$roles) { + $menus = $this->dao->getMenusRoule($where, $field); + } else { + /** @var SystemRoleServices $service */ + $service = app()->make(SystemRoleServices::class); + //拼接有长度限制 +// $ids = $service->value([['id', 'in', $roles]], 'GROUP_CONCAT(rules) as ids'); + $roles = is_string($roles) ? explode(',', $roles) : $roles; + $ids = $service->getRoleIds($roles); + $menus = $this->dao->getMenusRoule(['rule' => $ids] + $where, $field); + } + return $this->tidyMenuTier(false, $menus); + } + + /** + * 组合菜单数据 + * @param bool $adminFilter + * @param $menusList + * @param int $pid + * @param array $navList + * @return array + */ + public function tidyMenuTier(bool $adminFilter = false, $menusList, int $pid = 0, array $navList = []): array + { + foreach ($menusList as $k => $menu) { + $menu = $menu->getData(); + $menu['title'] = $menu['menu_name']; + unset($menu['menu_name']); + if ($menu['pid'] == $pid) { + unset($menusList[$k]); + $menu['children'] = $this->tidyMenuTier($adminFilter, $menusList, $menu['id']); + if ($pid == 0 && !count($menu['children'])) continue; + if ($menu['children']) $menu['expand'] = true; + $navList[] = $menu; + } + } + return $navList; + } +} diff --git a/app/services/system/SystemRoleServices.php b/app/services/system/SystemRoleServices.php new file mode 100644 index 0000000..0a0240b --- /dev/null +++ b/app/services/system/SystemRoleServices.php @@ -0,0 +1,182 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\system; + + +use app\Request; +use app\services\BaseServices; +use app\dao\system\SystemRoleDao; +use app\services\store\SystemStoreStaffServices; +use crmeb\exceptions\AuthException; +use crmeb\utils\ApiErrorCode; +use crmeb\services\CacheService; + + +/** + * Class SystemRoleServices + * @package app\services\system + * @mixin SystemRoleDao + */ +class SystemRoleServices extends BaseServices +{ + + /** + * 当前管理员权限缓存前缀 + */ + const ADMIN_RULES_LEVEL = 'Admin_rules_level_'; + + /** + * SystemRoleServices constructor. + * @param SystemRoleDao $dao + */ + public function __construct(SystemRoleDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取权限 + * @return mixed + */ + public function getRoleArray(array $where = [], string $field = '', string $key = '') + { + return $this->dao->getRoule($where, $field, $key); + } + + /** + * 获取表单所需的权限名称列表 + * @param int $level + * @param int $type + * @param int $relation_id + * @return array + */ + public function getRoleFormSelect(int $level, int $type = 0, int $relation_id = 0) + { + $list = $this->getRoleArray(['level' => $level, 'type' => $type, 'relation_id' => $relation_id, 'status' => 1]); + $options = []; + foreach ($list as $id => $roleName) { + $options[] = ['label' => $roleName, 'value' => $id]; + } + return $options; + } + + /** + * 身份管理列表 + * @param array $where + * @return array + */ + public function getRoleList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getRouleList($where, $page, $limit); + $count = $this->dao->count($where); + /** @var SystemMenusServices $service */ + $service = app()->make(SystemMenusServices::class); + foreach ($list as &$item) { + $item['rules'] = implode(',', array_merge($service->column(['id' => $item['rules']], 'menu_name', 'id'))); + } + return compact('count', 'list'); + } + + /** + * 后台验证权限 + * @param Request $request + */ + public function verifiAuth(Request $request) + { + $rule = str_replace('adminapi/', '', trim(strtolower($request->rule()->getRule()))); + if (in_array($rule, ['setting/admin/logout', 'menuslist'])) { + return true; + } + $method = trim(strtolower($request->method())); + $auth = $this->getAllRoles(2); + //验证访问接口是否存在 + if (!in_array($method . '@@' . $rule, array_map(function ($item) { + return trim(strtolower($item['methods'])). '@@'. trim(strtolower(str_replace(' ', '', $item['api_url']))); + }, $auth))) { + return true; + } + $auth = $this->getRolesByAuth($request->adminInfo()['roles'], 2); + //验证访问接口是否有权限 + if ($auth && empty(array_filter($auth, function ($item) use ($rule, $method) { + if (trim(strtolower($item['api_url'])) === $rule && $method === trim(strtolower($item['methods']))) + return true; + }))) { + throw new AuthException(ApiErrorCode::ERR_AUTH); + } + } + + /** + * 获取所有权限 + * @param int $auth_type + * @param int $type + * @param string $cachePrefix + * @return array|bool|mixed|null + */ + public function getAllRoles(int $auth_type = 1, int $type = 1, string $cachePrefix = self::ADMIN_RULES_LEVEL) + { + $cacheName = md5($cachePrefix . '_' . $auth_type . '_' . $type . '_ALl' ); + return CacheService::redisHandler('system_menus')->remember($cacheName, function () use ($auth_type, $type) { + /** @var SystemMenusServices $menusService */ + $menusService = app()->make(SystemMenusServices::class); + return $menusService->getColumn([['auth_type', '=', $auth_type], ['type', '=', $type]], 'api_url,methods'); + }); + } + + /** + * 获取指定权限 + * @param array $roles + * @param int $auth_type + * @param int $type + * @param string $cachePrefix + * @return array|bool|mixed|null + */ + public function getRolesByAuth(array $roles, int $auth_type = 1, int $type = 1, string $cachePrefix = self::ADMIN_RULES_LEVEL) + { + if (empty($roles)) return []; + $cacheName = md5($cachePrefix . '_' . $auth_type . '_' . $type . '_' . implode('_', $roles)); + return CacheService::redisHandler('system_menus')->remember($cacheName, function () use ($roles, $auth_type, $type) { + /** @var SystemMenusServices $menusService */ + $menusService = app()->make(SystemMenusServices::class); + return $menusService->getColumn([['id', 'IN', $this->getRoleIds($roles, $type == 3 ? 'cashier_rules' : 'rules')], ['auth_type', '=', $auth_type], ['type', '=', $type]], 'api_url,methods'); + }); + } + + /** + * 获取权限id + * @param array $roles + * @return array + */ + public function getRoleIds(array $roles, string $field = 'rules', string $key = 'id') + { + $rules = $this->dao->getColumn([['id', 'IN', $roles], ['status', '=', '1']], $field, $key); + return $rules ? array_unique(explode(',', implode(',', $rules))) : []; + } + + /** + * 门店角色状态更改改变角色下店员、管理员状态 + * @param int $store_id + * @param int $role_id + * @param $status + * @return mixed + */ + public function setStaffStatus(int $store_id, int $role_id, $status) + { + /** @var SystemStoreStaffServices $storeStaffServices */ + $storeStaffServices = app()->make(SystemStoreStaffServices::class); + if ($status) { + return $storeStaffServices->update(['store_id' => $store_id, 'roles' => $role_id, 'is_del' => 0, 'status' => 0], ['status' => 1]); + } else { + return $storeStaffServices->update(['store_id' => $store_id, 'roles' => $role_id, 'status' => 1], ['status' => 0]); + } + } +} diff --git a/app/services/system/SystemUserApplyServices.php b/app/services/system/SystemUserApplyServices.php new file mode 100644 index 0000000..fea0f31 --- /dev/null +++ b/app/services/system/SystemUserApplyServices.php @@ -0,0 +1,202 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\system; + + +use app\dao\system\SystemUserApplyDao; +use app\Request; +use app\services\BaseServices; +use app\dao\system\SystemRoleDao; +use app\services\supplier\SystemSupplierServices; +use crmeb\services\FormBuilder as Form; +use think\exception\ValidateException; +use think\facade\Route as Url; + + +/** + * Class SystemUserApplyServices + * @package app\services\system + * @mixin SystemRoleDao + */ +class SystemUserApplyServices extends BaseServices +{ + + /** + * @var string[] + */ + protected $status_name = [ + 0 => '未处理', + 1 => '已审核', + 2 => '未通过', + ]; + + /** + * SystemUserApplyServices constructor. + * @param SystemUserApplyDao $dao + */ + public function __construct(SystemUserApplyDao $dao) + { + $this->dao = $dao; + } + + + /** + * 所有申请记录列表 + * @param array $where + * @param string $field + * @return array + */ + public function getApplyList(array $where, string $field = '*') + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $field, ['user'], $page, $limit); + foreach ($list as &$item) { + $item['nickname'] = $item['user']['nickname'] ?? ''; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', (int)$item['add_time']) : ''; + $item['status_name'] = $this->status_name[$item['status']] ?? '未处理'; + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 用户申请记录 + * @param int $uid + * @param int $type + * @return array + */ + public function getUserApply(int $uid, int $type = 2) + { + [$page, $limit] = $this->getPageValue(); + $where = ['uid' => $uid, 'type' => $type, 'is_del' => 0]; + $list = $this->dao->getList($where, '*', ['user'], $page, $limit); + foreach ($list as &$item) { + $item['nickname'] = $item['user']['nickname'] ?? ''; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', (int)$item['add_time']) : ''; + } + return $list; + } + + /** + * 添加申请 + * @param int $id + * @param int $uid + * @param $data + * @param int $type + * @return int|mixed + */ + public function saveApply(int $id, int $uid, $data, int $type = 2) + { + $data['uid'] = $uid; + $data['add_time'] = time(); + $data['type'] = $type; + if ($id) { + $data['status'] = 0; + $data['status_time'] = 0; + $data['fail_msg'] = ''; + $this->dao->update($id, $data); + } else { + $res = $this->dao->save($data); + if (!$res) { + throw new ValidateException('保存失败,请稍后再试'); + } + $id = $res->id; + } + return $id; + + } + + /** + * 审核表单 + * @param int $id + * @return mixed + */ + public function verifyForm(int $id) + { + $info = $this->dao->get($id); + if (!$info) { + throw new ValidateException('申请记录不存在'); + } + $f = []; + $f[] = Form::radio('status', '审核状态', 1)->options([['value' => 1, 'label' => '通过'], ['value' => 2, 'label' => '拒绝']])->appendControl(2, [ + Form::textarea('fail_msg', '拒绝原因')->required('请输入拒绝原因') + ]); + return create_form('供应商申请审核', $f, Url::buildUrl('/supplier/apply/verify/' . $id), 'post'); + } + + /** + * 备注表单 + * @param int $id + * @return mixed + */ + public function markForm(int $id) + { + $info = $this->dao->get($id); + if (!$info) { + throw new ValidateException('申请记录不存在'); + } + $info = $info->toArray(); + $f = []; + $f[] = Form::textarea('mark', '备注', $info['mark'])->required('请输入拒绝原因'); + return create_form('供应商申请备注', $f, Url::buildUrl('/supplier/apply/mark/' . $id), 'post'); + } + + /** + * 审核申请记录 + * @param int $id + * @param array $data + * @param int $type + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function verifyApply(int $id, array $data, int $type = 2) + { + $info = $this->dao->get($id); + if (!$info) { + throw new ValidateException('申请记录不存在'); + } + $info = $info->toArray(); + if ($info['status'] != 0) { + throw new ValidateException('请不要重复审核'); + } + if (!isset($data['status']) || !in_array($data['status'], [1, 2])) { + throw new ValidateException('审核状态异常,请稍后重试'); + } + $res = $this->transaction(function () use ($id, $info, $data, $type) { + $result = []; + if ($data['status'] == 1) {//通过 + switch ($type) { + case 2://供应商 + /** @var SystemSupplierServices $systemSupplierServices */ + $systemSupplierServices = app()->make(SystemSupplierServices::class); + $result = $systemSupplierServices->verifyAgreeCreate($id, $info); + break; + } + } + $data['relation_id'] = $result['id'] ?? 0; + $data['status_time'] = time(); + $res = $this->dao->update($id, $data); + if (!$res) { + throw new ValidateException('审核保存失败'); + } + return $info; + }); + event('supplier.verify', [$res, $data['status']]); + return $res; + } + + + + +} diff --git a/app/services/system/admin/SystemAdminServices.php b/app/services/system/admin/SystemAdminServices.php new file mode 100644 index 0000000..b43aaac --- /dev/null +++ b/app/services/system/admin/SystemAdminServices.php @@ -0,0 +1,444 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\system\admin; + +use app\services\BaseServices; +use app\services\order\StoreOrderServices; +use app\services\product\product\StoreProductReplyServices; +use app\services\product\product\StoreProductServices; +use app\services\user\UserExtractServices; +use crmeb\exceptions\AdminException; +use app\common\dao\system\admin\AdminDao; +use app\services\system\SystemMenusServices; +use crmeb\services\CacheService; +use crmeb\services\FormBuilder; +use app\services\system\SystemRoleServices; +use crmeb\services\SystemConfigService; +use think\facade\Cache; + +/** + * 管理员service + * Class SystemAdminServices + * @package app\services\system\admin + * @mixin AdminDao + */ +class SystemAdminServices extends BaseServices +{ + + /** + * form表单创建 + * @var FormBuilder + */ + protected $builder; + + /** + * SystemAdminServices constructor. + * @param AdminDao $dao + */ + public function __construct(AdminDao $dao, FormBuilder $builder) + { + $this->dao = $dao; + $this->builder = $builder; + } + + /** + * 管理员登陆 + * @param string $account + * @param string $password + * @param bool $is_mobile + * @param int $adminType + * @return array|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function verifyLogin(string $account, string $password, bool $is_mobile = false, int $adminType = 1) + { + $key = 'login_captcha_' . $account; + if ($is_mobile) { + $adminInfo = $this->dao->phoneByAdmin($account, $adminType); + } else { + $adminInfo = $this->dao->accountByAdmin($account, $adminType); + } + if (!$adminInfo) { + Cache::inc($key); + throw new AdminException('管理员不存在!'); + } + if (!$adminInfo->status) { + Cache::inc($key); + throw new AdminException('您已被禁止登录!'); + } + if (!$is_mobile && !password_verify($password, $adminInfo->pwd)) { + Cache::inc($key); + throw new AdminException('账号或密码错误,请重新输入'); + } + $adminInfo->last_time = time(); + $adminInfo->last_ip = app('request')->ip(); + $adminInfo->login_count++; + $adminInfo->save(); + + return $adminInfo; + } + + /** + * 后台登陆获取菜单获取token + * @param string $account + * @param string $password + * @param string $type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function login(string $account, string $password, string $type, bool $is_mobile = false) + { + $adminInfo = $this->verifyLogin($account, $password, $is_mobile, 1); + $tokenInfo = $this->createToken($adminInfo->id, $type, $adminInfo['pwd']); + /** @var SystemMenusServices $services */ + $services = app()->make(SystemMenusServices::class); + [$menus, $uniqueAuth] = $services->getMenusList($adminInfo->roles, (int)$adminInfo['level']); + $data = SystemConfigService::more(['site_logo', 'site_logo_square', 'new_order_audio_link']); + return [ + 'token' => $tokenInfo['token'], + 'expires_time' => $tokenInfo['params']['exp'], + 'menus' => $menus, + 'unique_auth' => $uniqueAuth, + 'user_info' => [ + 'id' => $adminInfo['id'], + 'account' => $adminInfo['account'], + 'head_pic' => $adminInfo['head_pic'], + ], + 'logo' => $data['site_logo'], + 'logo_square' => $data['site_logo_square'], + 'version' => get_crmeb_version(), + 'newOrderAudioLink' => get_file_link($data['new_order_audio_link']), + 'prefix' => config('admin.admin_prefix') + ]; + } + + /** + * 获取登陆前的login等信息 + * @return array + */ + public function getLoginInfo() + { + $data = SystemConfigService::more(['admin_login_slide', 'site_logo_square', 'site_logo', 'login_logo']); + return [ + 'slide' => sys_config('admin_login_slide') ?? [], + 'logo_square' => $data['site_logo_square'] ?? '',//透明 + 'logo_rectangle' => $data['site_logo'] ?? '',//方形 + 'login_logo' => $data['login_logo'] ?? '',//登陆 + 'version' => get_crmeb_version(), + 'upload_file_size_max' => config('upload.filesize'),//文件上传大小kb + ]; + } + + /** + * 管理员列表 + * @param array $where + * @return array + */ + public function getAdminList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit); + $count = $this->dao->count($where); + + /** @var SystemRoleServices $service */ + $service = app()->make(SystemRoleServices::class); + $allRole = $service->getRoleArray(['type' => 0]); + foreach ($list as &$item) { + if ($item['roles']) { + $roles = []; + foreach ($item['roles'] as $id) { + if (isset($allRole[$id])) $roles[] = $allRole[$id]; + } + if ($roles) { + $item['roles'] = implode(',', $roles); + } else { + $item['roles'] = ''; + } + } + $item['_add_time'] = date('Y-m-d H:i:s', $item['add_time']); + $item['_last_time'] = $item['last_time'] ? date('Y-m-d H:i:s', $item['last_time']) : ''; + } + return compact('list', 'count'); + } + + /** + * 创建管理员表单 + * @param int $level + * @param array $formData + * @return mixed + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createAdminForm(int $level, array $formData = []) + { + if (!$level) { + $f[] = $this->builder->frameImage('head_pic', '头像', $this->url(config('admin.supplier_prefix') . '/widget.images/index', ['fodder' => 'head_pic'], true), $formData['head_pic'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true]); + } + + $f[] = $this->builder->input('account', '管理员账号', $formData['account'] ?? '')->required('请填写管理员账号'); + if ($formData) { + $f[] = $this->builder->input('pwd', '管理员密码')->type('password')->placeholder('不修改密码请留空'); + $f[] = $this->builder->input('conf_pwd', '确认密码')->type('password')->placeholder('不修改密码请留空'); + } else { + $f[] = $this->builder->input('pwd', '管理员密码')->type('password')->required('请填写管理员密码'); + $f[] = $this->builder->input('conf_pwd', '确认密码')->type('password')->required('请输入确认密码'); + } + + $f[] = $this->builder->input('real_name', '管理员姓名', $formData['real_name'] ?? '')->required('请输入管理员姓名'); + $f[] = $this->builder->input('phone', '管理员电话', $formData['phone'] ?? '')->required('请输入管理员电话'); + + /** @var SystemRoleServices $service */ + $service = app()->make(SystemRoleServices::class); + $options = $service->getRoleFormSelect($level); + $roles = []; + if ($formData && ($formData['roles'] ?? [])) { + foreach ($formData['roles'] as $role) { + $roles[] = (int)$role; + } + } + if ($level) { + $f[] = $this->builder->select('roles', '管理员身份', $roles)->setOptions(FormBuilder::setOptions($options))->multiple(true)->required('请选择管理员身份'); + } + $f[] = $this->builder->radio('status', '状态', $formData['status'] ?? 1)->options([['label' => '开启', 'value' => 1], ['label' => '关闭', 'value' => 0]]); + return $f; + } + + /** + * 添加管理员form表单获取 + * @param int $level + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createForm(int $level, string $url = '/setting/admin') + { + return create_form('管理员添加', $this->createAdminForm($level), $this->url($url)); + } + + /** + * 创建管理员 + * @param array $data + * @return bool + */ + public function create(array $data) + { + if ($data['conf_pwd'] != $data['pwd']) { + throw new AdminException('两次输入的密码不相同'); + } + unset($data['conf_pwd']); + + if ($this->dao->count(['account' => $data['account'], 'admin_type' => $data['admin_type'] ?? 1, 'is_del' => 0])) { + throw new AdminException('管理员账号已存在'); + } + if ($this->dao->count(['phone' => $data['phone'], 'admin_type' => $data['admin_type'] ?? 1, 'is_del' => 0])) { + throw new AdminException('管理员电话已存在'); + } + + $data['pwd'] = $this->passwordHash($data['pwd']); + $data['add_time'] = time(); + $data['roles'] = implode(',', $data['roles']); + + return $this->transaction(function () use ($data) { + if ($this->dao->save($data)) { + \crmeb\services\CacheService::clear(); + return true; + } else { + throw new AdminException('添加失败'); + } + }); + } + + /** + * 修改管理员表单 + * @param int $level + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function updateForm(int $level, int $id, string $url = '/setting/admin/') + { + $adminInfo = $this->dao->get($id); + if (!$adminInfo) { + throw new AdminException('管理员不存在!'); + } + if ($adminInfo->is_del) { + throw new AdminException('管理员已经删除'); + } + return create_form('管理员修改', $this->createAdminForm($level, $adminInfo->toArray()), $this->url($url . $id), 'PUT'); + } + + /** + * 修改管理员 + * @param int $id + * @param array $data + * @return bool + */ + public function save(int $id, array $data) + { + if (!$adminInfo = $this->dao->get($id)) { + throw new AdminException('管理员不存在,无法修改'); + } + if ($adminInfo->is_del) { + throw new AdminException('管理员已经删除'); + } + //修改密码 + if ($data['pwd']) { + + if (!$data['conf_pwd']) { + throw new AdminException('请输入确认密码'); + } + + if ($data['conf_pwd'] != $data['pwd']) { + throw new AdminException('上次输入的密码不相同'); + } + $adminInfo->pwd = $this->passwordHash($data['pwd']); + } + //修改账号 + if (isset($data['account']) && $data['account'] != $adminInfo->account && $this->dao->isAccountUsable($data['account'], $id)) { + throw new AdminException('管理员账号已存在'); + } + if (isset($data['phone']) && $data['phone'] != $adminInfo->phone && $this->dao->count(['phone' => $data['phone'], 'admin_type' => 1, 'is_del' => 0])) { + throw new AdminException('管理员电话已存在'); + } + if (isset($data['roles'])) { + $adminInfo->roles = implode(',', $data['roles']); + } + $adminInfo->real_name = $data['real_name'] ?? $adminInfo->real_name; + $adminInfo->phone = $data['phone'] ?? $adminInfo->phone; + $adminInfo->account = $data['account'] ?? $adminInfo->account; + $adminInfo->head_pic = $data['head_pic'] ?? $adminInfo->head_pic; + $adminInfo->status = $data['status']; + if ($adminInfo->save()) { + \crmeb\services\CacheService::clear(); + return true; + } else { + return false; + } + } + + /** + * 修改当前管理员信息 + * @param int $id + * @param array $data + * @return bool + */ + public function updateAdmin(int $id, array $data) + { + $adminInfo = $this->dao->get($id); + if (!$adminInfo) + throw new AdminException('管理员信息未查到'); + if ($adminInfo->is_del) { + throw new AdminException('管理员已经删除'); + } + if ($data['head_pic'] != '') { + $adminInfo->head_pic = $data['head_pic']; + } elseif ($data['real_name'] != '') { + $adminInfo->real_name = $data['real_name']; + } elseif ($data['pwd'] != '') { + if (!password_verify($data['pwd'], $adminInfo['pwd'])) + throw new AdminException('原始密码错误'); + if (!$data['new_pwd']) + throw new AdminException('请输入新密码'); + if (!$data['conf_pwd']) + throw new AdminException('请输入确认密码'); + if ($data['new_pwd'] != $data['conf_pwd']) + throw new AdminException('两次输入的密码不一致'); + $adminInfo->pwd = $this->passwordHash($data['new_pwd']); + } elseif ($data['phone'] != '') { + $verifyCode = CacheService::get('code_' . $data['phone']); + if (!$verifyCode) + throw new AdminException('请先获取验证码'); + $verifyCode = substr($verifyCode, 0, 6); + if ($verifyCode != $data['code']) { + CacheService::delete('code_' . $data['phone']); + throw new AdminException('验证码错误'); + } + $adminInfo->phone = $data['phone']; + } + if ($adminInfo->save()) { + CacheService::delete('code_' . $data['phone']); + return true; + } else { + return false; + } + } + + + /** + * 后台订单下单,评论,支付成功,后台消息提醒 + */ + public function adminNewPush() + { + try { + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $data['ordernum'] = $orderServices->count(['is_del' => 0, 'status' => 1, 'shipping_type' => 1]); + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $data['inventory'] = $productServices->count(['type' => 5]); + /** @var StoreProductReplyServices $replyServices */ + $replyServices = app()->make(StoreProductReplyServices::class); + $data['commentnum'] = $replyServices->count(['is_reply' => 0]); + /** @var UserExtractServices $extractServices */ + $extractServices = app()->make(UserExtractServices::class); + $data['reflectnum'] = $extractServices->getCount(['status' => 0]);//提现 + $data['msgcount'] = intval($data['ordernum']) + intval($data['inventory']) + intval($data['commentnum']) + intval($data['reflectnum']); + // SocketPush::admin()->type('ADMIN_NEW_PUSH')->data($data)->push(); + } catch (\Exception $e) { + } + } + + /** + * 短信修改密码 + * @param $phone + * @param $newPwd + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function resetPwd($phone, $newPwd) + { + $adminInfo = $this->dao->phoneByAdmin($phone); + if ($adminInfo) { + $adminInfo->pwd = $this->passwordHash($newPwd); + $adminInfo->save(); + return true; + } else { + throw new AdminException('管理员不存在,请检查手机号码'); + } + + } + + /** + * 获取供应商接收通知管理员 + * @param int $supplier_id + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getNotifySupplierList(int $supplier_id, string $field = '*') + { + $where = [ + 'relation_id' => $supplier_id, + 'status' => 1, + 'is_del' => 0 + ]; + $list = $this->dao->getList($where, 0, 0, $field); + return $list; + } + +} diff --git a/app/services/system/attachment/SystemAttachmentCategoryServices.php b/app/services/system/attachment/SystemAttachmentCategoryServices.php new file mode 100644 index 0000000..48d95f4 --- /dev/null +++ b/app/services/system/attachment/SystemAttachmentCategoryServices.php @@ -0,0 +1,195 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\system\attachment; + +use app\services\BaseServices; +use app\common\dao\system\attachment\AttachmentCategoryDao; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use think\facade\Route as Url; + +/** + * + * Class SystemAttachmentCategoryServices + * @package app\services\attachment + * @mixin AttachmentCategoryDao + */ +class SystemAttachmentCategoryServices extends BaseServices +{ + + /** + * SystemAttachmentCategoryServices constructor. + * @param AttachmentCategoryDao $dao + */ + public function __construct(AttachmentCategoryDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取分类列表 + * @param array $where + * @return array + */ + public function getAll(array $where) + { + $list = $this->dao->getList($where); + foreach ($list as &$item) { + $item['title'] = $item['name']; + $item['children'] = []; + if ($where['name'] == '' && $this->dao->count(['pid' => $item['id'],'file_type'=>$where['file_type']])) $item['loading'] = false; + } + return compact('list'); + } + + /** + * 格式化列表 + * @param $menusList + * @param int $pid + * @param array $navList + * @return array + */ + public function tidyMenuTier($menusList, $pid = 0, $navList = []) + { + foreach ($menusList as $k => $menu) { + $menu['title'] = $menu['name']; + if ($menu['pid'] == $pid) { + unset($menusList[$k]); + $menu['children'] = $this->tidyMenuTier($menusList, $menu['id']); + if ($menu['children']) $menu['expand'] = true; + $navList[] = $menu; + } + } + return $navList; + } + + /** + * 创建新增表单 + * @param $pid + * @param int $type + * @param int $relationId + * @param int $file_type + * @return mixed + */ + public function createForm($pid, int $type = 1, int $relationId = 0, int $file_type = 1) + { + return create_form('添加分类', $this->form(['pid' => $pid], $type, $relationId, $file_type), Url::buildUrl('/file/category'), 'POST'); + } + + /** + * 创建编辑表单 + * @param int $id + * @param int $type + * @param int $relationId + * @param int $file_type + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function editForm(int $id, int $type = 1, int $relationId = 0, int $file_type = 1) + { + $info = $this->dao->get($id); + return create_form('编辑分类', $this->form($info, $type, $relationId, $file_type), Url::buildUrl('/file/category/' . $id), 'PUT'); + } + + /** + * 生成表单参数 + * @param $info + * @param int $type + * @param int $relationId + * @param int $file_type + * @return array + */ + public function form($info = [], int $type = 1, int $relationId = 0, int $file_type = 1) + { + return [ + Form::hidden('file_type', $file_type), + Form::select('pid', '上级分类', (int)($info['pid'] ?? ''))->setOptions($this->getCateList(['pid' => 0, 'type' => $type, 'relation_id' => $relationId]))->filterable(true), + Form::input('name', '分类名称', $info['name'] ?? '')->maxlength(20), + ]; + } + + /** + * 获取分类列表(添加修改) + * @param array $where + * @return mixed + */ + public function getCateList(array $where) + { + $list = $this->dao->getList($where); + $options = [['value' => 0, 'label' => '所有分类']]; + foreach ($list as $id => $cateName) { + $options[] = ['label' => $cateName['name'], 'value' => $cateName['id']]; + } + return $options; + } + + /** + * 保存新建的资源 + * @param array $data + */ + public function save(array $data) + { + if ($this->dao->getOne(['name' => $data['name'], 'relation_id' => $data['relation_id'] ?? 0])) { + throw new AdminException('该分类已经存在'); + } + $res = $this->dao->save($data); + if (!$res) throw new AdminException('新增失败!'); + return $res; + } + + /** + * 保存修改的资源 + * @param int $id + * @param array $data + */ + public function update(int $id, array $data) + { + $attachment = $this->dao->getOne(['name' => $data['name'], 'relation_id' => $data['relation_id'] ?? 0]); + if ($attachment && $attachment['id'] != $id) { + throw new AdminException('该分类已经存在'); + } + $res = $this->dao->update($id, $data); + if (!$res) throw new AdminException('编辑失败!'); + } + + /** + * 删除分类 + * @param int $id + */ + public function del(int $id) + { + $count = $this->dao->getCount(['pid' => $id]); + if ($count) { + throw new AdminException('请先删除下级分类!'); + } else { + $res = $this->dao->delete($id); + if (!$res) throw new AdminException('请先删除下级分类!'); + } + } + + + /** + * 获取一条数据 + * @param $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOne($where) + { + return $this->dao->getOne($where); + } +} diff --git a/app/services/system/attachment/SystemAttachmentServices.php b/app/services/system/attachment/SystemAttachmentServices.php new file mode 100644 index 0000000..605d2f0 --- /dev/null +++ b/app/services/system/attachment/SystemAttachmentServices.php @@ -0,0 +1,473 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\system\attachment; + +use app\services\BaseServices; +use app\dao\system\attachment\SystemAttachmentDao; +use crmeb\exceptions\AdminException; +use crmeb\exceptions\UploadException; +use crmeb\services\DownloadImageService; +use crmeb\services\UploadService; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; + +/** + * + * Class SystemAttachmentServices + * @package app\services\attachment + * @mixin SystemAttachmentDao + */ +class SystemAttachmentServices extends BaseServices +{ + use ServicesTrait; + + /** + * SystemAttachmentServices constructor. + * @param SystemAttachmentDao $dao + */ + public function __construct(SystemAttachmentDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取单个资源 + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getInfo(array $where, string $field = '*') + { + return $this->dao->getOne($where, $field); + } + + /** + * 获取图片列表 + * @param array $where + * @return array + */ + public function getImageList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit); + if ($list) { + $site_url = sys_config('site_url'); + foreach ($list as &$item) { + if ($item['file_type'] == 1) { + if ($site_url) { + $item['satt_dir'] = (strpos($item['satt_dir'], $site_url) !== false || strstr($item['satt_dir'], 'http') !== false) ? $item['satt_dir'] : $site_url . $item['satt_dir']; + $item['att_dir'] = (strpos($item['att_dir'], $site_url) !== false || strstr($item['att_dir'], 'http') !== false) ? $item['satt_dir'] : $site_url . $item['att_dir']; + } + } + $item['att_size'] = formatFileSize($item['att_size']); + $item['time'] = date('Y-m-d H:i:s',$item['time']); + } + $list = get_thumb_water($list, 'mid', ['satt_dir']); + } + $where['module_type'] = 1; + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 删除图片 + * @param string $ids + */ + public function del(string $ids) + { + $ids = explode(',', $ids); + if (empty($ids)) throw new AdminException('请选择要删除的图片'); + foreach ($ids as $v) { + $attinfo = $this->dao->get((int)$v); + if ($attinfo) { + try { + $upload = UploadService::init($attinfo['image_type']); + if ($attinfo['image_type'] == 1) { + if (strpos($attinfo['att_dir'], '/') == 0) { + $attinfo['att_dir'] = substr($attinfo['att_dir'], 1); + } + if ($attinfo['att_dir']) $upload->delete(public_path() . $attinfo['att_dir']); + } else { + if ($attinfo['name']) $upload->delete($attinfo['name']); + } + } catch (\Throwable $e) { + } + $this->dao->delete((int)$v); + } + } + } + + /** + * 图片上传 + * @param int $pid + * @param string $file + * @param int $upload_type + * @param int $type + * @param int $relation_id + * @param string $uploadToken + * @return mixed + */ + public function upload(int $pid, string $file, int $upload_type, int $type) + { + if ($upload_type == 0) { + $upload_type = (int)sys_config('upload_type', 1); + } + try { + $path = make_path('attach', 2, true); + $upload = UploadService::init($upload_type); + $res = $upload->to($path)->validate()->setAuthThumb(true)->move($file); + if ($res === false) { + throw new UploadException($upload->getError()); + } else { + $fileInfo = $upload->getUploadInfo(); + //保存附件记录 + if ($type == 0) $this->saveAttachment($fileInfo, $pid, $type, 0, 1, $upload_type); + return $res->filePath; + } + } catch (\Exception $e) { + throw new UploadException($e->getMessage()); + } + } + + /** + * @param array $data + * @return \crmeb\basic\BaseModel + */ + public function move(array $data) + { + $res = $this->dao->move($data); + if (!$res) throw new AdminException('移动失败或不能重复移动到同一分类下'); + } + + /** + * 添加信息 + * @param array $data + */ + public function save(array $data) + { + $this->dao->save($data); + } + + /**添加附件记录 + * @param $name + * @param $att_size + * @param $att_type + * @param $att_dir + * @param $satt_dir + * @param $pid + * @param $imageType + * @param $time + * @param $module_type + * @return bool + */ + public function attachmentAdd($name, $att_size, $att_type, $att_dir, $satt_dir = '', $pid = 0, $imageType = 1, $time = 0, $module_type = 1) + { + $data['name'] = $name; + $data['att_dir'] = $att_dir; + $data['satt_dir'] = $satt_dir; + $data['att_size'] = $att_size; + $data['att_type'] = $att_type; + $data['image_type'] = $imageType; + $data['module_type'] = $module_type; + $data['time'] = $time ? $time : time(); + $data['pid'] = $pid; + if (!$this->dao->save($data)) { + throw new ValidateException('添加附件失败'); + } + return true; + } + + /** + * 推广名片生成 + * @param $name + */ + public function getLikeNameList($name) + { + return $this->dao->getLikeNameList(['like_name' => $name], 0, 0); + } + + /** + * 清除昨日海报 + * @return bool + * @throws \Exception + */ + public function emptyYesterdayAttachment() + { + try { + $list = $this->dao->getYesterday(); + foreach ($list as $key => $item) { + $upload = UploadService::init((int)$item['image_type']); + if ($item['image_type'] == 1) { + $att_dir = $item['att_dir']; + if ($att_dir && strstr($att_dir, 'uploads') !== false) { + if (strstr($att_dir, 'http') === false) + $upload->delete($att_dir); + else { + $filedir = substr($att_dir, strpos($att_dir, 'uploads')); + if ($filedir) $upload->delete($filedir); + } + } + } else { + if ($item['name']) $upload->delete($item['name']); + } + } + $this->dao->delYesterday(); + return true; + } catch (\Exception $e) { + $this->dao->delYesterday(); + return true; + } + } + + + /** + * 门店图片上传 + * @param int $pid + * @param string $file + * @param int $relationId + * @param int $type + * @return mixed + */ + public function storeUpload(int $pid, string $file, int $relationId, int $type = 2, int $upload_type = 0, string $uploadToken = '') + { + try { + if ($type == 4) { + $upload_type = 0; + } else { + if ($upload_type == 0) { + $upload_type = (int)sys_config('upload_type', 1); + } + } + $path = make_path('attach/' . ($type == 4 ? 'supplier' : ($type == 2 ? 'store' : '')), 2, true); + $upload = UploadService::init($upload_type); + $res = $upload->to($path)->validate()->setAuthThumb(true)->move($file); + if ($res === false) { + throw new UploadException($upload->getError()); + } else { + $fileInfo = $upload->getUploadInfo(); + //保存附件记录 + $this->saveAttachment($fileInfo, $pid, $type, $relationId, 1, $upload_type, $uploadToken); + return $res->filePath; + } + } catch (\Exception $e) { + throw new UploadException($e->getMessage()); + } + } + + /** + * 视频分片上传 + * @param $data + * @param $file + * @param int $type + * @param int $relation_id + * @return mixed + */ + public function videoUpload($data, $file, int $type = 1, int $relation_id = 0) + { + $public_dir = app()->getRootPath() . 'public'; + $store_dir = ''; + switch ($type) { + case 2: + $store_dir = 'store' . DIRECTORY_SEPARATOR; + break; + case 4: + $store_dir = 'supplier' . DIRECTORY_SEPARATOR; + break; + } + $dir = '/uploads/video/' . $store_dir . date('Y') . DIRECTORY_SEPARATOR . date('m') . DIRECTORY_SEPARATOR . date('d'); + $all_dir = $public_dir . $dir; + if (!is_dir($all_dir)) mkdir($all_dir, 0777, true); + $filename = $all_dir . '/' . $data['filename'] . '__' . $data['chunkNumber']; + move_uploaded_file($file->getPathName(), $filename); + $res['code'] = 0; + $res['msg'] = 'error'; + $res['file_path'] = ''; + if ($data['chunkNumber'] == $data['totalChunks']) { + $blob = ''; + for ($i = 1; $i <= $data['totalChunks']; $i++) { + $blob .= file_get_contents($all_dir . '/' . $data['filename'] . '__' . $i); + } + file_put_contents($all_dir . '/' . $data['filename'], $blob); + for ($i = 1; $i <= $data['totalChunks']; $i++) { + @unlink($all_dir . '/' . $data['filename'] . '__' . $i); + } + if (file_exists($all_dir . '/' . $data['filename'])) { + $res['code'] = 2; + $res['msg'] = 'success'; + $res['file_path'] = $dir . '/' . $data['filename']; + } + } else { + if (file_exists($all_dir . '/' . $data['filename'] . '__' . $data['chunkNumber'])) { + $res['code'] = 1; + $res['msg'] = 'waiting'; + $res['file_path'] = ''; + } + } + $res['name'] = $res['dir'] = $res['file_path']; + if (strpos($res['file_path'], 'http') === false) { + $res['dir'] = $res['file_path'] = sys_config('site_url') . $res['file_path']; + } + if ($res['code'] == 2) { + $res['size'] = getFileHeaders($res['file_path'])['size'] ?? 0; + $this->saveAttachment($res, (int)($data['pid'] ?? 0), $type, $relation_id, 2); + } + return $res; + } + + /** + * 云端上传的视频保存记录 + * @param array $data + * @param int $type + * @param int $relation_id + * @param int $upload_type + * @return bool + */ + public function saveOssVideoAttachment(array $data, int $type = 1, int $relation_id = 0, int $upload_type = 1) + { + $fileInfo = []; + $fileInfo['name'] = $fileInfo['real_name'] = $data['path']; + $fileInfo['cover_image'] = $data['cover_image']; + if (!$fileInfo['cover_image'] && $upload_type != 1) {//云端视频 + $res = UploadService::init($upload_type)->videoCoverImage($data['path'], 'mid'); + $fileInfo['cover_image'] = $res['mid'] ?? ''; + } + $fileInfo['size'] = getFileHeaders($data['path'])['size'] ?? 0; + $this->saveAttachment($fileInfo, (int)$data['pid'], $type, $relation_id, 2, $upload_type); + return true; + } + + /** + * 保存附件信息 + * @param $fileInfo + * @param int $pid + * @param int $type + * @param int $relation_id + * @param int $file_type + * @param int $upload_type + * @return bool + */ + public function saveAttachment($fileInfo, int $pid = 0, int $type = 1, int $relation_id = 0, int $file_type = 1, int $upload_type = 1, string $uploadToken = '') + { + $fileType = pathinfo($fileInfo['name'], PATHINFO_EXTENSION); + if ($fileInfo && !in_array($fileType, ['xlsx', 'xls'])) { + $data['file_type'] = $file_type; + $data['type'] = $type == 0 ? 1 : $type; + $data['relation_id'] = $relation_id; + $data['name'] = $fileInfo['name']; + $data['real_name'] = $fileInfo['real_name'] ?? $fileInfo['name'] ?? ''; + $data['att_dir'] = $fileInfo['dir'] ?? $fileInfo['name'] ?? ''; + if ($data['att_dir'] && strpos($data['att_dir'], 'http') === false) { + $data['att_dir'] = sys_config('site_url') . $data['att_dir']; + } + $data['satt_dir'] = $fileInfo['thumb_path'] ?? $fileInfo['cover_image'] ?? ''; + if ($file_type == 2) { + if (!$data['satt_dir']) {//视频 默认封面 + $data['satt_dir'] = sys_config('site_url') . '/statics/images/video_default_cover.png'; + } + if ($data['real_name'] && strpos($data['real_name'], '/') !== false) { + $nameArr = explode('/', $data['real_name']); + $data['real_name'] = end($nameArr) ?? $data['real_name']; + } + } + + $data['att_size'] = $fileInfo['size'] ?? ''; + $data['att_type'] = $fileInfo['type'] ?? ''; + $data['image_type'] = $upload_type; + $data['module_type'] = 1; + $data['time'] = $fileInfo['time'] ?? time(); + $data['pid'] = $pid; + $data['scan_token'] = $uploadToken; + $this->dao->save($data); + } + return true; + } + + /** + * 网络图片上传 + * @param $data + * @return bool + * @throws \Exception + */ + public function onlineUpload(array $data, int $type = 1, int $relation_id = 0) + { + //生成附件目录 + if (make_path('attach', 3, true) === '') { + throw new AdminException('无法创建文件夹,请检查您的上传目录权限'); + } + + //上传图片 + /** @var SystemAttachmentServices $systemAttachmentService */ + $systemAttachmentService = app()->make(SystemAttachmentServices::class); + $siteUrl = sys_config('site_url'); + + foreach ($data['images'] as $image) { + try{ + $uploadValue = app()->make(DownloadImageService::class)->thumb(true)->downloadImage($image); + } catch (\Throwable $e) { + throw new AdminException('文件下载失败,请更换链接'); + } + + if (is_array($uploadValue)) { + //TODO 拼接图片地址 + if ($uploadValue['image_type'] == 1) { + $imagePath = $siteUrl . $uploadValue['path']; + } else { + $imagePath = $uploadValue['path']; + } + //写入数据库 + if (!$uploadValue['is_exists']) { + $systemAttachmentService->save([ + 'type' => $type == 0 ? 1 : $type, + 'relation_id' => $relation_id, + 'name' => $uploadValue['name'], + 'real_name' => $uploadValue['name'], + 'att_dir' => $imagePath, + 'satt_dir' => $imagePath, + 'att_size' => $uploadValue['size'], + 'att_type' => $uploadValue['mime'], + 'image_type' => $uploadValue['image_type'], + 'module_type' => 1, + 'time' => time(), + 'pid' => $data['pid'] + ]); + } + } + } + return true; + } + + /** + * 删除门店、供应商附件 + * @param array $where + * @param int $type + * @param int $relation_id + * @return bool + */ + public function delAttachment(array $where = [], int $type = 0, int $relation_id = 0) + { + $where['type'] = $type; + $where['relation_id'] = $relation_id; + //删除附件 + $attachments = $this->dao->getColumn($where, 'att_id'); + if ($attachments) { + $idsArr = array_chunk($attachments, 100); + foreach ($idsArr as $ids) { + $this->del(implode(',', $ids)); + } + } + return true; + } +} diff --git a/app/services/system/config/ConfigServices.php b/app/services/system/config/ConfigServices.php new file mode 100644 index 0000000..22626c0 --- /dev/null +++ b/app/services/system/config/ConfigServices.php @@ -0,0 +1,135 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\system\config; + + +use app\dao\store\StoreConfigDao; +use app\services\BaseServices; +use crmeb\services\SystemConfigService; + +/** + * Class ConfigServices + * @package app\services\system\config + * @mixin StoreConfigDao + */ +class ConfigServices extends BaseServices +{ + //打印机类型 + const PRINTER_NAME = [ + 1 => 'yi_lian_yun', + 2 => 'fei_e_yun', + ]; + + //平台打印机配置 + const PRINTER_KEY = [ + 'switch' => 'pay_success_printing_switch', + 'print_type' => 'print_type', + 'printing_client_id', 'printing_api_key', 'develop_id', 'terminal_number', + 'fey_user', 'fey_ukey', 'fey_sn', + ]; + //配置字段对应 + const PRINTER_CONFIG_KEY = [ + 'yi_lian_yun' => ['printing_client_id' => 'clientId', 'printing_api_key' => 'apiKey', 'develop_id' => 'partner', 'terminal_number' => 'terminal'], + 'fei_e_yun' => ['fey_user' => 'feyUser', 'fey_ukey' => 'feyUkey', 'fey_sn' => 'feySn'], + ]; + + //门店打印机配置 + const STORE_PRINTER_KEY = [ + 'switch' => 'store_pay_success_printing_switch', + 'print_type' => 'store_print_type', + 'store_printing_client_id', 'store_printing_api_key', 'store_develop_id', 'store_terminal_number', + 'store_fey_user', 'store_fey_ukey', 'store_fey_sn', + ]; + + //门店配置字段对应 + const STORE_PRINTER_CONFIG_KEY = [ + 'yi_lian_yun' => ['store_printing_client_id' => 'clientId', 'store_printing_api_key' => 'apiKey', 'store_develop_id' => 'partner', 'store_terminal_number' => 'terminal'], + 'fei_e_yun' => ['store_fey_user' => 'feyUser', 'store_fey_ukey' => 'feyUkey', 'store_fey_sn' => 'feySn'], + ]; + + //供应商打印机配置 + const SUPPLIER_PRINTER_KEY = [ + 'switch' => 'store_pay_success_printing_switch', + 'print_type' => 'store_print_type', + 'store_printing_client_id', 'store_printing_api_key', 'store_develop_id', 'store_terminal_number', + 'store_fey_user', 'store_fey_ukey', 'store_fey_sn', + ]; + + //供应商配置字段对应 + const SUPPLIER_PRINTER_CONFIG_KEY = [ + 'yi_lian_yun' => ['store_printing_client_id' => 'clientId', 'store_printing_api_key' => 'apiKey', 'store_develop_id' => 'partner', 'store_terminal_number' => 'terminal'], + 'fei_e_yun' => ['store_fey_user' => 'feyUser', 'store_fey_ukey' => 'feyUkey', 'store_fey_sn' => 'feySn'], + ]; + + const CONFIG_TYPE = [ + 'printing_deploy' => self::PRINTER_KEY, + 'store_printing_deploy' => self::STORE_PRINTER_KEY, + 'supplier_printing_deploy' => self::SUPPLIER_PRINTER_KEY + ]; + + /** + * StoreConfigServices constructor. + * @param StoreConfigDao $dao + */ + public function __construct(StoreConfigDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取小票打印配置 + * @param int $type + * @param int $relation_id + * @return array + */ + public function getPrintingConfig(int $type = 0, int $relation_id = 0) : array + { + /** @var SystemConfigService $configService */ + $configService = app('sysConfig'); + switch ($type) { + case 0://平台 + $keys = self::PRINTER_KEY; + $configKey = self::PRINTER_CONFIG_KEY; + break; + case 1://门店 + $keys = self::STORE_PRINTER_KEY; + $configKey = self::STORE_PRINTER_CONFIG_KEY; + $configService->setStore($relation_id); + break; + case 2://供应商 + $keys = self::SUPPLIER_PRINTER_KEY; + $configKey = self::SUPPLIER_PRINTER_CONFIG_KEY; + $configService->setSupplier($relation_id); + break; + default: + $keys = self::PRINTER_KEY; + $configKey = self::PRINTER_CONFIG_KEY; + break; + } + $key = array_values($keys); + $config = $configService->more($key); + $switch = $config[$keys['switch']] ?? 0; + $printType = $config[$keys['print_type']] ?? 1; + $name = self::PRINTER_NAME[$printType] ?? 'yi_lian_yun'; + $configKey = $configKey[$name]; + $configData = []; + foreach ($config as $key => $value) { + if (isset($configKey[$key])) { + $configData[$configKey[$key]] = $value; + } + } + return [$switch, $name, $configData]; + } + + + +} diff --git a/app/services/system/config/SystemConfigServices.php b/app/services/system/config/SystemConfigServices.php new file mode 100644 index 0000000..c5a767b --- /dev/null +++ b/app/services/system/config/SystemConfigServices.php @@ -0,0 +1,2487 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\system\config; + + +use app\dao\system\config\SystemConfigDao; +use app\jobs\agent\SystemJob; +use app\services\activity\coupon\StoreCouponIssueServices; +use app\services\activity\newcomer\StoreNewcomerServices; +use app\services\BaseServices; +use app\services\other\CacheServices; +use app\services\store\StoreConfigServices; +use app\services\user\UserIntegralServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\form\Build; +use crmeb\form\components\Alert; +use crmeb\form\components\InputNumber; +use crmeb\form\validate\StrRules; +use crmeb\services\FileService; +use crmeb\services\FormBuilder; +use crmeb\services\wechat\contract\ServeConfigInterface; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; +use think\facade\Log; + +/** + * 系统配置 + * Class SystemConfigServices + * @package app\services\system\config + * @mixin SystemConfigDao + */ +class SystemConfigServices extends BaseServices implements ServeConfigInterface +{ + use ServicesTrait; + + /** + * form表单句柄 + * @var FormBuilder + */ + protected $builder; + + /** + * 表单数据切割符号 + * @var string + */ + protected $cuttingStr = '=>'; + + /** + * 表单提交url + * @var string[] + */ + protected $postUrl = [ + 'setting' => [ + 'url' => '/setting/config/save_basics', + 'auth' => [], + ], + 'serve' => [ + 'url' => '/serve/sms_config/save_basics', + 'auth' => ['short_letter_switch'], + ], + 'freight' => [ + 'url' => '/freight/config/save_basics', + 'auth' => ['express'], + ], + 'agent' => [ + 'url' => '/agent/config/save_basics', + 'auth' => ['fenxiao'], + ], + 'marketing' => [ + 'url' => '/marketing/integral_config/save_basics', + 'auth' => ['point'], + ], + 'store' => [ + 'url' => '/store/finance/header_basics', + 'auth' => [], + ] + ]; + + /** + * 子集控制规则 + * @var array[] + */ + protected $relatedRule = [ + 'brokerage_func_status' => [ + 'son_type' => [ + 'store_brokerage_statu' => [ + 'son_type' => ['store_brokerage_price' => ''], + 'show_value' => 3 + ], + 'brokerage_bindind' => '', + 'store_brokerage_binding_status' => [ + 'son_type' => ['store_brokerage_binding_time' => ''], + 'show_value' => 2 + ], + 'spread_banner' => '', + ], + 'show_value' => 1 + ], + 'brokerage_user_status' => [ + 'son_type' => [ + 'uni_brokerage_price' => '', + 'day_brokerage_price_upper' => '', + ], + 'show_value' => 1 + ], + 'pay_success_printing_switch' => [ + 'son_type' => [ + 'develop_id' => '', + 'printing_api_key' => '', + 'printing_client_id' => '', + 'terminal_number' => '', + ], + 'show_value' => 1 + ], + 'wss_open' => [ + 'son_type' => [ + 'wss_local_cert' => '', + 'wss_local_pk' => '', + ], + 'show_value' => 1 + ], + 'invoice_func_status' => [ + 'son_type' => [ + 'special_invoice_status' => '', + ], + 'show_value' => 1 + ], + 'member_func_status' => [ + 'son_type' => [ + 'member_price_status' => '', + 'order_give_exp' => '', + 'sign_give_exp' => '', + 'invite_user_exp' => '' + ], + 'show_value' => 1 + ], + 'balance_func_status' => [ + 'son_type' => [ + 'recharge_attention' => '', + 'recharge_switch' => '', + 'store_user_min_recharge' => '', + ], + 'show_value' => 1 + ], + 'system_product_copy_type' => [ + 'son_type' => [ + 'copy_product_apikey' => '', + ], + 'show_value' => 2 + ], + 'logistics_type' => [ + 'son_type' => [ + 'system_express_app_code' => '', + ], + 'show_value' => 2 + ], + 'ali_pay_status' => [ + 'son_type' => [ + 'ali_pay_appid' => '', + 'alipay_merchant_private_key' => '', + 'alipay_public_key' => '', + ], + 'show_value' => 1 + ], + 'pay_weixin_open' => [ + 'son_type' => [ + 'pay_weixin_mchid' => '', + 'pay_weixin_key' => '', + 'pay_weixin_client_cert' => '', + 'pay_weixin_client_key' => '', + 'paydir' => '', + 'pay_routine_open' => '' + ], + 'show_value' => 1 + ], + 'pay_routine_open' => [ + 'son_type' => [ + 'pay_routine_mchid' => '' + ], + 'show_value' => 1 + ], + 'config_export_open' => [ + 'son_type' => [ + 'config_export_to_name' => '', + 'config_export_to_tel' => '', + 'config_export_to_address' => '', + 'config_export_siid' => '', + ], + 'show_value' => 1 + ], + 'image_watermark_status' => [ + 'son_type' => [ + 'watermark_type' => [ + 'son_type' => [ + 'watermark_image' => '', + 'watermark_opacity' => '', + 'watermark_rotate' => '', + ], + 'show_value' => 1 + ], + 'watermark_position' => '', + 'watermark_x' => '', + 'watermark_y' => '', + 'watermark_type@' => [ + 'son_type' => [ + 'watermark_text' => '', + 'watermark_text_size' => '', + 'watermark_text_color' => '', + 'watermark_text_angle' => '' + ], + 'show_value' => 2 + ], + ], + 'show_value' => 1 + ], + 'share_qrcode' => [ + 'son_type' => [ + 'spread_share_forever' => '', + ], + 'show_value' => 1 + ], + 'integral_effective_status' => [ + 'son_type' => [ + 'integral_effective_time' => [ + 'son_type' => [ + 'next_clear_month_time' => '', + ], + 'show_value' => 1 + ], + 'integral_effective_time@' => [ + 'son_type' => [ + 'next_clear_quarter_time' => '', + ], + 'show_value' => 2 + ], + 'integral_effective_time#' => [ + 'son_type' => [ + 'next_clear_year_time' => '', + ], + 'show_value' => 3 + ], + ], + 'show_value' => 1 + ], + 'user_extract_bank_status' => [ + 'son_type' => [ + 'user_extract_bank' => '', + ], + 'show_value' => 1 + ], + ]; + + /** + * SystemConfigServices constructor. + * @param SystemConfigDao $dao + */ + public function __construct(SystemConfigDao $dao, FormBuilder $builder) + { + $this->dao = $dao; + $this->builder = $builder; + } + + public function getSonConfig() + { + $sonConfig = []; + $rolateRule = $this->relatedRule; + if ($rolateRule) { + foreach ($rolateRule as $key => $value) { + $sonConfig = array_merge($sonConfig, array_keys($value['son_type'])); + foreach ($value['son_type'] as $k => $v) { + if (isset($v['son_type'])) { + $sonConfig = array_merge($sonConfig, array_keys($v['son_type'])); + } + } + } + } + return $sonConfig; + } + + /** + * 获取单个系统配置 + * @param string $configName + * @param int $type + * @param int $relation_id + * @param $default + * @return mixed|null + */ + public function getConfigValue(string $configName, int $type = 1, int $relation_id = 0, $default = null) + { + if ($relation_id) {//查门店、供应商 + /** @var StoreConfigServices $service */ + $service = app()->make(StoreConfigServices::class); + return $service->getConfig($configName, $type, $relation_id); + } else {//平台 + $value = $this->dao->getConfigValue($configName); + return is_null($value) ? $default : json_decode($value, true); + } + } + + /** + * 获取全部配置 + * @param array $configName + * @return array + */ + public function getConfigAll(array $configName = [], int $type = 1, int $relation_id = 0) + { + if ($relation_id) { + /** @var StoreConfigServices $service */ + $service = app()->make(StoreConfigServices::class); + return $service->getConfigAll($configName, $type, $relation_id); + } else { + return array_map(function ($item) { + return json_decode($item, true); + }, $this->dao->getConfigAll($configName)); + } + } + + /** + * 获取配置并分页 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getConfigList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getConfigList($where, $page, $limit); + $count = $this->dao->count($where); + foreach ($list as &$item) { + $item['value'] = $item['value'] ? json_decode($item['value'], true) ?: '' : ''; + if ($item['type'] == 'radio' || $item['type'] == 'checkbox') { + $item['value'] = $this->getRadioOrCheckboxValueInfo($item['menu_name'], $item['value']); + } + if ($item['type'] == 'upload' && !empty($item['value'])) { + if ($item['upload_type'] == 1 || $item['upload_type'] == 3) { + $item['value'] = [set_file_url($item['value'])]; + } elseif ($item['upload_type'] == 2) { + $item['value'] = set_file_url($item['value']); + } + foreach ($item['value'] as $key => $value) { + $tidy_srr[$key]['filepath'] = $value; + $tidy_srr[$key]['filename'] = basename($value); + } + $item['value'] = $tidy_srr; + } + } + return compact('count', 'list'); + } + + /** + * 获取单选按钮或者多选按钮的显示值 + * @param $menu_name + * @param $value + * @return string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getRadioOrCheckboxValueInfo(string $menu_name, $value): string + { + $option = []; + $config_one = $this->dao->getOne(['menu_name' => $menu_name]); + if (!$config_one) { + return ''; + } + $parameter = explode("\n", $config_one['parameter']); + foreach ($parameter as $k => $v) { + if (isset($v) && strlen($v) > 0) { + $data = explode('=>', $v); + $option[$data[0]] = $data[1]; + } + } + $str = ''; + if (is_array($value)) { + foreach ($value as $v) { + $str .= $option[$v] . ','; + } + } else { + $str .= !empty($value) ? $option[$value] ?? '' : $option[0] ?? ''; + } + return $str; + } + + /** + * 获取系统配置信息 + * @param int $tabId + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getReadList(int $tabId) + { + $info = $this->dao->getConfigTabAllList($tabId); + foreach ($info as $k => $v) { + if (!is_null(json_decode($v['value']))) + $info[$k]['value'] = json_decode($v['value'], true); + if ($v['type'] == 'upload' && !empty($v['value'])) { + if ($v['upload_type'] == 1 || $v['upload_type'] == 3) $info[$k]['value'] = explode(',', $v['value']); + } + } + return $info; + } + + /** + * 创建单行表单 + * @param string $type + * @param array $data + * @return array + */ + public function createTextForm(string $type, array $data) + { + $formbuider = []; + switch ($type) { + case 'input': + $data['value'] = isset($data['value']) ? json_decode($data['value'], true) : ''; + $formbuider[] = $this->builder->input($data['menu_name'], $data['info'], $data['value'])->info($data['desc'])->placeholder($data['desc'])->col(13); + break; + case 'number': + $data['value'] = isset($data['value']) ? json_decode($data['value'], true) : 0; + if ($data['menu_name'] == 'integral_max_num' || $data['menu_name'] == 'order_give_integral') { + $formbuider[] = $this->builder->number($data['menu_name'], $data['info'], (float)$data['value'])->info($data['desc'])->precision(0); + } else { + $formbuider[] = $this->builder->number($data['menu_name'], $data['info'], (float)$data['value'])->info($data['desc']); + } + break; + case 'dateTime': + $formbuider[] = $this->builder->dateTime($data['menu_name'], $data['info'], $data['value'])->info($data['desc']); + break; + case 'color': + $data['value'] = isset($data['value']) ? json_decode($data['value'], true) : ''; + $formbuider[] = $this->builder->color($data['menu_name'], $data['info'], $data['value'])->info($data['desc']); + break; + default: + $data['value'] = isset($data['value']) ? json_decode($data['value'], true) : ''; + $formbuider[] = $this->builder->input($data['menu_name'], $data['info'], $data['value'])->info($data['desc'])->placeholder($data['desc'])->col(13); + break; + } + return $formbuider; + } + + /** + * 创建多行文本框 + * @param array $data + * @return mixed + */ + public function createTextareaForm(array $data) + { + $data['value'] = json_decode($data['value'], true) ?: ''; + $formbuider[] = $this->builder->textarea($data['menu_name'], $data['info'], $data['value'])->placeholder($data['desc'])->info($data['desc'])->rows(6)->col(13); + return $formbuider; + } + + /** + * 创建当选表单 + * @param array $data + * @param false $control + * @param array $control_two + * @param array $controlle_three + * @return array + */ + public function createRadioForm(array $data, $control = false, $control_two = [], $controlle_three = []) + { + $formbuider = []; + $data['value'] = json_decode($data['value'], true) ?: '0'; + $parameter = explode("\n", $data['parameter']); + $options = []; + if ($parameter) { + foreach ($parameter as $v) { + if (strstr($v, $this->cuttingStr) !== false) { + $pdata = explode($this->cuttingStr, $v); + $options[] = ['label' => $pdata[1], 'value' => (int)$pdata[0]]; + } + } + $formbuider[] = $radio = $this->builder->radio($data['menu_name'], $data['info'], (int)$data['value'])->options($options)->info($data['desc'])->col(13); + if ($control) { + $radio->appendControl(isset($data['show_value']) ? $data['show_value'] : 1, is_array($control) ? $control : [$control]); + } + if ($control_two && isset($data['show_value2'])) { + $radio->appendControl(isset($data['show_value2']) ? $data['show_value2'] : 2, is_array($control_two) ? $control_two : [$control_two]); + } + if ($controlle_three && isset($data['show_value3'])) { + $radio->appendControl(isset($data['show_value3']) ? $data['show_value3'] : 3, is_array($controlle_three) ? $controlle_three : [$controlle_three]); + } + return $formbuider; + } + } + + /** + * 创建上传组件表单 + * @param int $type + * @param array $data + * @param int $admin_type + * @return array + */ + public function createUpoadForm(int $type, array $data, int $admin_type = 0) + { + $formbuider = []; + switch ($admin_type) { + case 0://平台 + $from = config('admin.admin_prefix'); + break; + case 1://门店 + $from = config('admin.store_prefix'); + break; + case 2://供应商 + $from = config('admin.supplier_prefix'); + break; + default: + $from = config('admin.admin_prefix'); + break; + } + switch ($type) { + case 1: + $data['value'] = json_decode($data['value'], true) ?: ''; + if (!$data['value']) $data['value'] = set_file_url($data['value']); + $formbuider[] = $this->builder->frameImage($data['menu_name'], $data['info'], $this->url($from . '/widget.images/index', ['fodder' => $data['menu_name']], true), $data['value']) + ->icon('ios-image')->width('960px')->height('505px')->modal(['footer-hide' => true])->info($data['desc'])->col(13); + break; + case 2: + $data['value'] = json_decode($data['value'], true) ?: []; + if (!$data['value']) $data['value'] = set_file_url($data['value']); + $formbuider[] = $this->builder->frameImages($data['menu_name'], $data['info'], $this->url($from . '/widget.images/index', ['fodder' => $data['menu_name'], 'type' => 'many', 'maxLength' => 5], true), $data['value']) + ->maxLength(5)->icon('ios-images')->width('960px')->modal(['footer-hide' => true])->height('505px') + ->info($data['desc'])->col(13); + break; + case 3: + $data['value'] = json_decode($data['value'], true) ?: ''; + if (!$data['value']) $data['value'] = set_file_url($data['value']); + $formbuider[] = $this->builder->uploadFile($data['menu_name'], $data['info'], $this->url('/adminapi/file/upload/1', ['type' => 1], false, false), $data['value']) + ->name('file')->info($data['desc'])->col(13)->headers([ + 'Authori-zation' => app()->request->header('Authori-zation'), + ]); + break; + } + return $formbuider; + } + + /** + * 创建单选框 + * @param array $data + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createCheckboxForm(array $data) + { + $formbuider = []; + $data['value'] = json_decode($data['value'], true) ?: []; + $parameter = explode("\n", $data['parameter']); + $options = []; + if ($parameter) { + foreach ($parameter as $v) { + if (strstr($v, $this->cuttingStr) !== false) { + $pdata = explode($this->cuttingStr, $v); + $options[] = ['label' => $pdata[1], 'value' => $pdata[0]]; + } + } + $formbuider[] = $this->builder->checkbox($data['menu_name'], $data['info'], $data['value'])->options($options)->info($data['desc'])->col(13); + } + return $formbuider; + } + + /** + * 创建选择框表单 + * @param array $data + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createSelectForm(array $data) + { + $formbuider = []; + $data['value'] = json_decode($data['value'], true) ?: []; + $parameter = explode("\n", $data['parameter']); + $options = []; + if ($parameter) { + foreach ($parameter as $v) { + if (strstr($v, $this->cuttingStr) !== false) { + $pdata = explode($this->cuttingStr, $v); + $options[] = ['label' => $pdata[1], 'value' => $pdata[0]]; + } + } + $formbuider[] = $this->builder->select($data['menu_name'], $data['info'], $data['value'])->options($options)->info($data['desc'])->col(13); + } + return $formbuider; + } + + public function bindBuilderData($data, $relatedRule) + { + if (!$data) return false; + $p_list = array(); + foreach ($relatedRule as $rk => $rv) { + $p_list[$rk] = $data[$rk]; + if (isset($rv['son_type']) && is_array($rv['son_type'])) { + foreach ($rv['son_type'] as $sk => $sv) { + if (is_array($sv) && isset($sv['son_type'])) { + foreach ($sv['son_type'] as $ssk => $ssv) { + $tmp = $data[$sk]; + $tmp['console'] = $data[$ssk]; + $p_list[$rk]['console'][] = $tmp; + } + } else { + $p_list[$rk]['console'][] = $data[$sk]; + } + } + } + + } + return array_values($p_list); + } + + /** + * 获取系统配置表单 + * @param int $id + * @param array $formData + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + + public function formTypeShine($data, $control = false, $controle_two = [], $controlle_three = []) + { + + switch ($data['type']) { + case 'text'://文本框 + return $this->createTextForm($data['input_type'], $data); + break; + case 'radio'://单选框 + return $this->createRadioForm($data, $control, $controle_two, $controlle_three); + break; + case 'textarea'://多行文本框 + return $this->createTextareaForm($data); + break; + case 'upload'://文件上传 + return $this->createUpoadForm((int)$data['upload_type'], $data); + break; + case 'checkbox'://多选框 + return $this->createCheckboxForm($data); + break; + case 'select'://多选框 + return $this->createSelectForm($data); + break; + } + } + + /** + * @param int $tabId + * @param array $formData + * @param array $relatedRule + * @return array|bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createConfigForm(int $tabId, array $relatedRule) + { + $list = $this->dao->getConfigTabAllList($tabId); + if (!$relatedRule) { + $formbuider = $this->createNoCrontrolForm($list); + } else { + $formbuider = $this->createBindCrontrolForm($list, $relatedRule); + } + return $formbuider; + } + + /** + * 创建 + * @param array $list + * @param int $type + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createForm(array $list, int $type = 0) + { + if (!$list) return []; + $list = array_combine(array_column($list, 'menu_name'), $list); + $formbuider = []; + $relateRule = $this->relatedRule; + $sonConfig = $this->getSonConfig(); + $next_clear_month_time = $next_clear_quarter_time = $next_clear_year_time = ''; + if (in_array('next_clear_month_time', $sonConfig)) { + /** @var UserIntegralServices $userIntergralServices */ + $userIntergralServices = app()->make(UserIntegralServices::class); + [$next_clear_month_time] = $userIntergralServices->getTime(1); + [$next_clear_quarter_time] = $userIntergralServices->getTime(2); + [$next_clear_year_time] = $userIntergralServices->getTime(3); + } + foreach ($list as $key => $data) { + if (in_array($key, $sonConfig)) { + continue; + } + switch ($data['type']) { + case 'text'://文本框 + $formbuider = array_merge($formbuider, $this->createTextForm($data['input_type'], $data)); + break; + case 'radio'://单选框 + $builder = []; + if (isset($relateRule[$key])) { + $role = $relateRule[$key]; + $data['show_value'] = $role['show_value']; + foreach ($role['son_type'] as $sk => $sv) { + if (isset($list[$sk])) { + $son_data = $list[$sk]; + $son_data['show_value'] = $role['show_value']; + $son_build = []; + if (isset($sv['son_type'])) { + foreach ($sv['son_type'] as $ssk => $ssv) { + $son_data['show_value'] = $sv['show_value']; + if ($ssk == 'next_clear_month_time') { + $son_build[] = $this->builder->input('next_clear_month_time', '最近清零时间', $next_clear_month_time ? date('Y-m-d', $next_clear_month_time) : '')->info('最近清零时间')->disabled(true)->col(13); + } else { + $son_build[] = $this->formTypeShine($list[$ssk])[0]; + unset($list[$ssk]); + } + } + } + $son_build_two = []; + if (isset($role['son_type'][$sk . '@'])) { + $son_type_two = $role['son_type'][$sk . '@']; + $son_data['show_value2'] = $son_type_two['show_value']; + if (isset($son_type_two['son_type'])) { + foreach ($son_type_two['son_type'] as $ssk => $ssv) { + if ($ssk == 'next_clear_quarter_time') { + $son_build_two[] = $this->builder->input('next_clear_quarter_time', '最近清零时间', $next_clear_quarter_time ? date('Y-m-d', $next_clear_quarter_time) : '')->info('最近清零时间')->disabled(true)->col(13); + } else { + $son_build_two[] = $this->formTypeShine($list[$ssk])[0]; + unset($list[$ssk]); + } + } + } + } + $son_build_three = []; + if (isset($role['son_type'][$sk . '#'])) { + $son_type_two = $role['son_type'][$sk . '#']; + $son_data['show_value3'] = $son_type_two['show_value']; + if (isset($son_type_two['son_type'])) { + foreach ($son_type_two['son_type'] as $ssk => $ssv) { + if ($ssk == 'next_clear_year_time') { + $son_build_three[] = $this->builder->input('next_clear_year_time', '最近清零时间', $next_clear_year_time ? date('Y-m-d', $next_clear_year_time) : '')->info('最近清零时间')->disabled(true)->col(13); + } else { + $son_build_three[] = $this->formTypeShine($list[$ssk])[0]; + unset($list[$ssk]); + } + } + } + } + $builder[] = $this->formTypeShine($son_data, $son_build, $son_build_two, $son_build_three)[0]; + unset($list[$sk]); + } + } + $data['show_value'] = $role['show_value']; + } + $formbuider = array_merge($formbuider, $this->createRadioForm($data, $builder)); + break; + case 'textarea'://多行文本框 + $formbuider = array_merge($formbuider, $this->createTextareaForm($data)); + break; + case 'upload'://文件上传 + $formbuider = array_merge($formbuider, $this->createUpoadForm((int)$data['upload_type'], $data, $type)); + break; + case 'checkbox'://多选框 + $formbuider = array_merge($formbuider, $this->createCheckboxForm($data)); + break; + case 'select'://多选框 + $formbuider = array_merge($formbuider, $this->createSelectForm($data)); + break; + } + } + return $formbuider; + } + + /**无组件绑定规则 + * @param array $list + * @return array|bool + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createNoCrontrolForm(array $list) + { + if (!$list) return false; + $formbuider = []; + foreach ($list as $key => $data) { + + switch ($data['type']) { + case 'text'://文本框 + $formbuider = array_merge($formbuider, $this->createTextForm($data['input_type'], $data)); + break; + case 'radio'://单选框 + $formbuider = array_merge($formbuider, $this->createRadioForm($data)); + break; + case 'textarea'://多行文本框 + $formbuider = array_merge($formbuider, $this->createTextareaForm($data)); + break; + case 'upload'://文件上传 + $formbuider = array_merge($formbuider, $this->createUpoadForm((int)$data['upload_type'], $data)); + break; + case 'checkbox'://多选框 + $formbuider = array_merge($formbuider, $this->createCheckboxForm($data)); + break; + case 'select'://多选框 + $formbuider = array_merge($formbuider, $this->createSelectForm($data)); + break; + } + } + return $formbuider; + } + + /** + * 有组件绑定规则 + * @param array $list + * @param array $relatedRule + * @return array|bool + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function createBindCrontrolForm(array $list, array $relatedRule) + { + if (!$list || !$relatedRule) return false; + $formbuider = []; + $new_data = array(); + foreach ($list as $dk => $dv) { + $new_data[$dv['menu_name']] = $dv; + } + foreach ($relatedRule as $rk => $rv) { + if (isset($rv['son_type'])) { + $data = $new_data[$rk]; + switch ($data['type']) { + case 'text'://文本框 + $formbuider = array_merge($formbuider, $this->createTextForm($data['input_type'], $data)); + break; + case 'radio'://单选框 + $son_builder = array(); + foreach ($rv['son_type'] as $sk => $sv) { + if (isset($sv['son_type'])) { + foreach ($sv['son_type'] as $ssk => $ssv) { + $son_data = $new_data[$sk]; + $son_data['show_value'] = $sv['show_value']; + $son_builder[] = $this->formTypeShine($son_data, $this->formTypeShine($new_data[$ssk])[0])[0]; + } + } else { + $son_data = $new_data[$sk]; + $son_data['show_value'] = $rv['show_value']; + $son_builder[] = $this->formTypeShine($son_data)[0]; + } + + } + $formbuider = array_merge($formbuider, $this->createRadioForm($data, $son_builder)); + break; + case 'textarea'://多行文本框 + $formbuider = array_merge($formbuider, $this->createTextareaForm($data)); + break; + case 'upload'://文件上传 + $formbuider = array_merge($formbuider, $this->createUpoadForm((int)$data['upload_type'], $data)); + break; + case 'checkbox'://多选框 + $formbuider = array_merge($formbuider, $this->createCheckboxForm($data)); + break; + case 'select'://多选框 + $formbuider = array_merge($formbuider, $this->createSelectForm($data)); + break; + } + } + } + return $formbuider; + } + + /** + * 系统配置form表单创建 + * @param int $tabId + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getConfigForm($url, int $tabId, int $type = 0, $relation_id = 0) + { + /** @var SystemConfigTabServices $service */ + $service = app()->make(SystemConfigTabServices::class); + $title = $service->value(['id' => $tabId], 'title'); + $list = $this->dao->getConfigTabAllList($tabId, 1, $type, $relation_id); + if ($relation_id != 0) { + foreach ($list as &$item) { + $item['value'] = $item['store_value'] ?? ''; + } + } + $formbuider = $this->createForm($list, $type); + $name = 'setting'; + if ($url) { + $name = explode('/', $url)[2] ?? $name; + } + $postUrl = $this->postUrl[$name]['url'] ?? '/setting/config/save_basics'; + $postUrl = $relation_id ? $url : $postUrl; + return create_form($title, $formbuider, $this->url($postUrl), 'POST'); + } + + /** + * 新增路由增加设置项验证 + * @param $url + * @param $post + * @return bool + */ + public function checkParam($url, $post) + { + $name = ''; + if ($url) { + $name = explode('/', $url)[2] ?? $name; + } + $auth = $this->postUrl[$name]['auth'] ?? false; + if ($auth === false) { + throw new ValidateException('请求不被允许'); + } + if ($auth) { + /** @var SystemConfigTabServices $systemConfigTabServices */ + $systemConfigTabServices = app()->make(SystemConfigTabServices::class); + foreach ($post as $key => $value) { + $tab_ids = $systemConfigTabServices->getColumn([['eng_title', 'IN', $auth]], 'id'); + if (!$tab_ids || !in_array($key, $this->dao->getColumn([['config_tab_id', 'IN', $tab_ids]], 'menu_name'))) { + throw new ValidateException('设置类目不被允许'); + } + } + } + return true; + } + + /** + * 修改配置获取form表单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function editConfigForm(int $id) + { + $menu = $this->dao->get($id)->getData(); + if (!$menu) { + throw new AdminException('修改数据不存在!'); + } + /** @var SystemConfigTabServices $service */ + $service = app()->make(SystemConfigTabServices::class); + $formbuider = []; + $formbuider[] = $this->builder->input('menu_name', '字段变量', $menu['menu_name'])->disabled(1); + $formbuider[] = $this->builder->hidden('type', $menu['type']); + $formbuider[] = $this->builder->cascader('config_tab_id', '分类', [$menu['config_tab_id']])->data($service->getSelectForm())->changeOnSelect(true); + $formbuider[] = $this->builder->input('info', '配置名称', $menu['info'])->autofocus(1); + $formbuider[] = $this->builder->input('desc', '配置简介', $menu['desc']); + switch ($menu['type']) { + case 'text': + $menu['value'] = json_decode($menu['value'], true); + $formbuider[] = $this->builder->select('input_type', '类型', $menu['input_type'])->setOptions([ + ['value' => 'input', 'label' => '文本框'] + , ['value' => 'dateTime', 'label' => '时间'] + , ['value' => 'color', 'label' => '颜色'] + , ['value' => 'number', 'label' => '数字'] + ]); + //输入框验证规则 + if (!$menu['is_store']) { + $formbuider[] = $this->builder->input('value', '默认值', $menu['value']); + } + if (!empty($menu['required'])) { + $formbuider[] = $this->builder->number('width', '文本框宽(%)', (int)$menu['width']); + $formbuider[] = $this->builder->input('required', '验证规则', $menu['required'])->placeholder('多个请用,隔开例如:required:true,url:true'); + } + break; + case 'textarea': + $menu['value'] = json_decode($menu['value'], true); + //多行文本 + if (!empty($menu['high'])) { + if (!$menu['is_store']) {//友情链接 数据是数组 + $formbuider[] = $this->builder->textarea('value', '默认值', is_array($menu['value']) ? json_encode($menu['value']) : $menu['value'])->rows(5); + } + $formbuider[] = $this->builder->number('width', '文本框宽(%)', (int)$menu['width']); + $formbuider[] = $this->builder->number('high', '多行文本框高(%)', (int)$menu['high']); + } else { + if (!$menu['is_store']) { + $formbuider[] = $this->builder->input('value', '默认值', $menu['value']); + } + } + break; + case 'radio': + $formbuider = array_merge($formbuider, $this->createRadioForm($menu)); + //单选和多选参数配置 + if (!empty($menu['parameter'])) { + $formbuider[] = $this->builder->textarea('parameter', '配置参数', $menu['parameter'])->placeholder("参数方式例如:\n1=>白色\n2=>红色\n3=>黑色"); + } + break; + case 'checkbox': + $formbuider = array_merge($formbuider, $this->createCheckboxForm($menu)); + //单选和多选参数配置 + if (!empty($menu['parameter'])) { + $formbuider[] = $this->builder->textarea('parameter', '配置参数', $menu['parameter'])->placeholder("参数方式例如:\n1=>白色\n2=>红色\n3=>黑色"); + } + break; + case 'upload': + $formbuider = array_merge($formbuider, $this->createUpoadForm(($menu['upload_type']), $menu)); + //上传类型选择 + if (!empty($menu['upload_type'])) { + $formbuider[] = $this->builder->radio('upload_type', '上传类型', $menu['upload_type'])->options([['value' => 1, 'label' => '单图'], ['value' => 2, 'label' => '多图'], ['value' => 3, 'label' => '文件']]); + } + break; + } + $formbuider[] = $this->builder->number('sort', '排序', (int)$menu['sort'])->min(0); + $formbuider[] = $this->builder->radio('status', '状态', $menu['status'])->options([['value' => 1, 'label' => '显示'], ['value' => 2, 'label' => '隐藏']]); + return create_form('编辑字段', $formbuider, $this->url('/setting/config/' . $id), 'PUT'); + } + + /** + * 字段状态 + * @return array + */ + public function formStatus(): array + { + return [['value' => 1, 'label' => '显示'], ['value' => 2, 'label' => '隐藏']]; + } + + /** + * 选择文文件类型 + * @return array + */ + public function uploadType(): array + { + return [ + ['value' => 1, 'label' => '单图'] + , ['value' => 2, 'label' => '多图'] + , ['value' => 3, 'label' => '文件'] + ]; + } + + /** + * 选择文本框类型 + * @return array + */ + public function textType(): array + { + return [ + ['value' => 'input', 'label' => '文本框'] + , ['value' => 'dateTime', 'label' => '时间'] + , ['value' => 'color', 'label' => '颜色'] + , ['value' => 'number', 'label' => '数字'] + ]; + } + + /** + * 获取创建配置规格表单 + * @param int $type + * @param int $tab_id + * @return array + */ + public function createFormRule(int $type, int $tab_id): array + { + /** @var SystemConfigTabServices $service */ + $service = app()->make(SystemConfigTabServices::class); + $formbuider = []; + $form_type = ''; + $info_type = []; + $parameter = []; + $formbuider[] = $this->builder->radio('is_store', '配置类型', 0)->options([ + ['value' => 0, 'label' => '总后台'], + ['value' => 1, 'label' => '门店后台'] + ]); + switch ($type) { + case 0://文本框 + $form_type = 'text'; + $info_type = $this->builder->select('input_type', '类型')->setOptions($this->textType()); + $parameter[] = $this->builder->input('value', '默认值'); + $parameter[] = $this->builder->number('width', '文本框宽(%)', 100); + $parameter[] = $this->builder->input('required', '验证规则')->placeholder('多个请用,隔开例如:required:true,url:true'); + break; + case 1://多行文本框 + $form_type = 'textarea'; + $parameter[] = $this->builder->textarea('value', '默认值'); + $parameter[] = $this->builder->number('width', '文本框宽(%)', 100); + $parameter[] = $this->builder->number('high', '多行文本框高(%)', 5); + break; + case 2://单选框 + $form_type = 'radio'; + $parameter[] = $this->builder->textarea('parameter', '配置参数')->placeholder("参数方式例如:\n1=>男\n2=>女\n3=>保密"); + $parameter[] = $this->builder->input('value', '默认值'); + break; + case 3://文件上传 + $form_type = 'upload'; + $parameter[] = $this->builder->radio('upload_type', '上传类型', 1)->options($this->uploadType()); + break; + case 4://多选框 + $form_type = 'checkbox'; + $parameter[] = $this->builder->textarea('parameter', '配置参数')->placeholder("参数方式例如:\n1=>白色\n2=>红色\n3=>黑色"); + break; + case 5://下拉框 + $form_type = 'select'; + $parameter[] = $this->builder->textarea('parameter', '配置参数')->placeholder("参数方式例如:\n1=>白色\n2=>红色\n3=>黑色"); + break; + } + if ($form_type) { + $formbuider[] = $this->builder->hidden('type', $form_type); + $formbuider[] = $this->builder->cascader('config_tab_id', '分类', [])->data($service->getSelectForm())->changeOnSelect(true); + if ($info_type) { + $formbuider[] = $info_type; + } + $formbuider[] = $this->builder->input('info', '配置名称')->autofocus(1); + $formbuider[] = $this->builder->input('menu_name', '字段变量')->placeholder('例如:site_url'); + $formbuider[] = $this->builder->input('desc', '配置简介'); + $formbuider = array_merge($formbuider, $parameter); + $formbuider[] = $this->builder->number('sort', '排序', 0)->min(0); + $formbuider[] = $this->builder->radio('status', '状态', 1)->options($this->formStatus()); + } + return create_form('添加字段', $formbuider, $this->url('/setting/config'), 'POST'); + } + + /** + * radio 和 checkbox规则的判断 + * @param $data + * @return bool + */ + public function valiDateRadioAndCheckbox($data) + { + $option = []; + $option_new = []; + $data['parameter'] = str_replace("\r\n", "\n", $data['parameter']);//防止不兼容 + $parameter = explode("\n", $data['parameter']); + if (count($parameter) < 2) { + throw new AdminException('请输入正确格式的配置参数'); + } + foreach ($parameter as $k => $v) { + if (isset($v) && !empty($v)) { + $option[$k] = explode('=>', $v); + } + } + if (count($option) < 2) { + throw new AdminException('请输入正确格式的配置参数'); + } + $bool = 1; + foreach ($option as $k => $v) { + $option_new[$k] = $option[$k][0]; + foreach ($v as $kk => $vv) { + $vv_num = strlen($vv); + if (!$vv_num) { + $bool = 0; + } + } + } + if (!$bool) { + throw new AdminException('请输入正确格式的配置参数'); + } + $num1 = count($option_new);//提取该数组的数目 + $arr2 = array_unique($option_new);//合并相同的元素 + $num2 = count($arr2);//提取合并后数组个数 + if ($num1 > $num2) { + throw new AdminException('请输入正确格式的配置参数'); + } + return true; + } + + /** + * 验证参数 + * @param $data + * @return bool + */ + public function valiDateValue($data) + { + if (!$data || !isset($data['required']) || !$data['required']) { + return true; + } + $valids = explode(',', $data['required']); + foreach ($valids as $valid) { + $valid = explode(':', $valid); + if (isset($valid[0]) && isset($valid[1])) { + $k = strtolower(trim($valid[0])); + $v = strtolower(trim($valid[1])); + switch ($k) { + case 'required': + if ($v == 'true' && $data['value'] === '') { + throw new ValidateException(($data['info'] ?? '') . '请输入默认值'); + } + break; + case 'url': + if ($v == 'true' && !check_link($data['value'])) { + throw new ValidateException(($data['info'] ?? '') . '请输入正确url'); + } + break; + } + } + } + } + + /** + * 保存平台电子面单打印信息 + * @param array $data + * @return bool + */ + public function saveExpressInfo(array $data) + { + if (!is_array($data) || !$data) return false; + // config_export_id 快递公司id + // config_export_temp_id 快递公司模板id + // config_export_com 快递公司编码 + // config_export_to_name 发货人姓名 + // config_export_to_tel 发货人电话 + // config_export_to_address 发货人详细地址 + // config_export_siid 电子面单打印机编号 + foreach ($data as $key => $value) { + $this->dao->update(['menu_name' => 'config_export_' . $key], ['value' => json_encode($value)]); + } + \crmeb\services\SystemConfigService::clear(); + return true; + } + + /** + * 获取分享海报 兼容方法 + */ + public function getSpreadBanner() + { + //配置 + $banner = sys_config('spread_banner', []); + if (!$banner) { + //组合数据 + $banner = sys_data('routine_spread_banner'); + if ($banner) { + $banner = array_column($banner, 'pic'); + $this->dao->update(['menu_name' => 'spread_banner'], ['value' => json_encode($banner)]); + \crmeb\services\SystemConfigService::clear(); + } + } + return $banner; + } + + /** + * 保存wss配置 + * @param int $wssOpen + * @param string $wssLocalpk + * @param string $wssLocalCert + */ + public function saveSslFilePath(int $wssOpen, string $wssLocalpk, string $wssLocalCert) + { + $wssFile = root_path() . '.wss'; + $content = <<getMessage()); + } + } + + /** + * 获取wss配置 + * @param string $key + * @return array|false|mixed + */ + public function getSslFilePath(string $key = '') + { + $wssFile = root_path() . '.wss'; + try { + $content = parse_ini_file($wssFile); + } catch (\Throwable $e) { + $content = []; + } + if (isset($content[$key])) { + return $content[$key]; + } else { + return $content; + } + } + + /** + * 检测缩略图水印配置是否更改 + * @param array $post + * @return bool + */ + public function checkThumbParam(array $post) + { + unset($post['upload_type'], $post['image_watermark_status']); + /** @var SystemConfigTabServices $systemConfigTabServices */ + $systemConfigTabServices = app()->make(SystemConfigTabServices::class); + //上传配置->基础配置 + $tab_id = $systemConfigTabServices->getColumn(['eng_title' => 'base_config'], 'id'); + if ($tab_id) { + $all = $this->dao->getColumn(['config_tab_id' => $tab_id], 'value', 'menu_name'); + if (array_intersect(array_keys($all), array_keys($post))) { + foreach ($post as $key => $item) { + //配置更改删除原来生成的缩略图 + if (isset($all[$key]) && $item != $all[$key]) { + try { + FileService::delDir(public_path('uploads/thumb_water')); + break; + } catch (\Throwable $e) { + + } + } + } + } + } + return true; + } + + /** + * 变更分销绑定关系模式 + * @param array $post + * @return bool + */ + public function checkBrokerageBinding(array $post) + { + try { + $config_data = $post['store_brokerage_binding_status']; + $config_one = $this->dao->getOne(['menu_name' => 'store_brokerage_binding_status']); + $config_old = json_decode($config_one['value'], true); + if ($config_old != 2 && $config_data == 2) { + //自动解绑上级绑定 + SystemJob::dispatch('resetSpreadTime'); + } + } catch (\Throwable $e) { + Log::error('变更分销绑定模式重置绑定时间失败,失败原因:' . $e->getMessage()); + return false; + } + return true; + } + + + /** + * 获取表单 + * @param string $type + * @return array + */ + public function getNewFormBuildRule(string $type) + { + switch ($type) { + case 'base'://商城基础设置 + $data = $this->shopBaseFormBuild(); + break; + case 'store'://门店设置 + $data = $this->storeFormBuild(); + break; + case 'trade'://交易设置 + $data = $this->shopTradeFormBuild(); + break; + case 'pay'://支付设置 + $data = $this->shopPayFormBuild(); + break; + case 'wechat'://微信设置 + $data = $this->wechatBaseFormBuild(); + break; + case 'routine'://小程序设置 + $data = $this->routineBaseFormBuild(); + break; + case 'pc'://pc + $data = $this->pcBaseFormBuild(); + break; + case 'app'://app + $data = $this->appBaseFormBuild(); + break; + case 'wxopen'://开放平台 + $data = $this->wxOpenBaseFormBuild(); + break; + case 'third'://第三方配置 + $data = $this->thirdPartyFormBuild(); + break; + case 'deliver'://发货设置 + $data = $this->deliverFormBuild(); + break; + case 'city_deliver'://同城配送 + $data = $this->cityDeliverFormBuild(); + break; + case 'recharge'://充值设置 + $data = $this->rechargeFormBuild(); + break; + case 'user'://用户设置 + $data = $this->userFormBuild(); + break; + case 'svip'://付费会员 + $data = $this->svipFormBuild(); + break; + case 'invoice'://发票 + $data = $this->invoiceFormBuild(); + break; + case 'vip'://会员等级 + $data = $this->vipFormBuild(); + break; + case 'kefu'://客服配置 + $data = $this->kefuFormBuild(); + break; + case 'integral'://积分设置 + $data = $this->integralFormBuild(); + break; + case 'distribution'://分销设置 + $data = $this->distributionFormBuild(); + break; + case 'work'://企业微信设置 + $data = $this->workFormBuild(); + break; + case 'finance'://门店财务设置 + $data = $this->financeFormBuild(); + break; + case 'bargain'://砍价设置 + $data = $this->bargainFormBuild(); + break; + case 'supplier_finance'://供应商财务设置 + $data = $this->supplierFinanceFormBuild(); + break; + default: + throw new ValidateException('类型错误'); + } + + return $data; + } + + /** + * 获取全部配置 + * @param array $configName + * @param int $storeId + * @param int $type 0 正常结构 1:只返回key value + * @return array + */ + public function getConfigAllField(array $configName = [], int $storeId = 0, int $type = 0) + { + $list = $this->dao->getConfigAllField($configName, $storeId, ['info', 'type', 'value', 'desc', 'parameter']); + foreach ($list as &$item) { + $item['value'] = json_decode($item['value'], true); + } + $value = []; + foreach ($configName as $key) { + if ($type) { + $value[$key] = $list[$key]['value'] ?? ''; + } else { + $value[$key] = $list[$key] ?? ['info' => '', 'type' => 'text', 'value' => '', 'desc' => '', 'parameter' => '']; + } + } + return $value; + } + + + public function getOptions(string $parameter) + { + $parameter = explode("\n", $parameter); + $options = []; + foreach ($parameter as $v) { + if (strstr($v, $this->cuttingStr) !== false) { + $pdata = explode($this->cuttingStr, $v); + $options[] = ['label' => $pdata[1], 'value' => (int)$pdata[0]]; + } + } + return $options; + } + + /** + * 分销设置 + * @return array + */ + public function distributionFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'brokerage_func_status', 'store_brokerage_statu', 'store_brokerage_price', 'brokerage_bindind', + 'store_brokerage_binding_status', 'store_brokerage_binding_time', 'spread_banner', 'store_brokerage_ratio', + 'store_brokerage_two', 'extract_time', 'is_self_brokerage', 'brokerage_user_status', 'uni_brokerage_price', + 'day_brokerage_price_upper', 'brokerage_type', 'user_extract_min_price', 'user_extract_bank_status', + 'user_extract_wechat_status', 'user_extract_alipay_status', 'user_extract_bank', + 'pay_weixin_client_cert', 'pay_weixin_client_key', 'withdraw_fee', 'brokerage_level', 'brokerage_compute_type' + ]); + + $build->rule([ + Build::tabs()->option('分销模式', [ + Build::switch('brokerage_func_status', $data['brokerage_func_status']['info'], (int)$data['brokerage_func_status']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->control(1, [ + Build::radio('brokerage_level', $data['brokerage_level']['info'], $data['brokerage_level']['value'])->info($data['brokerage_level']['desc'])->options($this->getOptions($data['brokerage_level']['parameter'])), + Build::radio('store_brokerage_statu', $data['store_brokerage_statu']['info'], $data['store_brokerage_statu']['value']) + ->info($data['store_brokerage_statu']['desc']) + ->options($this->getOptions($data['store_brokerage_statu']['parameter'])) + ->control(3, [ + Build::inputNum('store_brokerage_price', $data['store_brokerage_price']['info'], $data['store_brokerage_price']['value'])->info($data['store_brokerage_price']['desc']) + ]), + Build::radio('brokerage_bindind', $data['brokerage_bindind']['info'], $data['brokerage_bindind']['value'])->info($data['brokerage_bindind']['desc'])->options($this->getOptions($data['brokerage_bindind']['parameter'])), + Build::radio('store_brokerage_binding_status', $data['store_brokerage_binding_status']['info'], $data['store_brokerage_binding_status']['value']) + ->options($this->getOptions($data['store_brokerage_binding_status']['parameter'])) + ->control(2, [ + Build::inputNum('store_brokerage_binding_time', $data['store_brokerage_binding_time']['info'], $data['store_brokerage_binding_time']['value'])->info($data['store_brokerage_binding_time']['desc']), + ])->info($data['store_brokerage_binding_status']['desc']), + Build::uploadFrame('spread_banner', $data['spread_banner']['info'], $data['spread_banner']['value'])->maxNum(5)->info($data['spread_banner']['desc'])->url('/' . config('admin.admin_prefix') . '/widget.images/index.html') + ])->info($data['brokerage_func_status']['desc']), + ])->option('返佣设置', [ + Build::radio('brokerage_compute_type', $data['brokerage_compute_type']['info'], $data['brokerage_compute_type']['value'])->options($this->getOptions($data['brokerage_compute_type']['parameter']))->info($data['brokerage_compute_type']['desc']), + Build::inputNum('store_brokerage_ratio', $data['store_brokerage_ratio']['info'], $data['store_brokerage_ratio']['value'])->min(0)->info($data['store_brokerage_ratio']['desc']), + Build::inputNum('store_brokerage_two', $data['store_brokerage_two']['info'], $data['store_brokerage_two']['value'])->min(0)->info($data['store_brokerage_two']['desc']), + Build::inputNum('extract_time', $data['extract_time']['info'], $data['extract_time']['value'])->min(0)->info($data['extract_time']['desc']), + Build::switch('is_self_brokerage', $data['is_self_brokerage']['info'], (int)$data['is_self_brokerage']['value']) + ->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['is_self_brokerage']['desc']), + Build::switch('brokerage_user_status', $data['brokerage_user_status']['info'], (int)$data['brokerage_user_status']['value'])->control(1, [ + Build::inputNum('uni_brokerage_price', $data['uni_brokerage_price']['info'], $data['uni_brokerage_price']['value'])->min(0)->info($data['uni_brokerage_price']['desc']), + Build::inputNum('day_brokerage_price_upper', $data['day_brokerage_price_upper']['info'], $data['day_brokerage_price_upper']['value'])->min(-1)->info($data['day_brokerage_price_upper']['desc']), + ])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['brokerage_user_status']['desc']), + ])->option('提现设置', [ + Build::alert('微信提现到零钱为自动到账(需要开通微信:企业付款到零钱(商家转账到零钱),并确保配置微信支付证书正确,特别注意:需要配置场景、开启API发起转账),其他方式均需要手动转账', Alert::WARNING)->showIcon(true), + Build::radio('brokerage_type', $data['brokerage_type']['info'], $data['brokerage_type']['value'])->options($this->getOptions($data['brokerage_type']['parameter']))->control(1, [ + Build::uploadImage('pay_weixin_client_cert', $data['pay_weixin_client_cert']['info'], $data['pay_weixin_client_cert']['value']) + ->url('/file/upload/1?type=1')->format(config('upload.fileExt'))->headers(['Authori-zation' => app()->request->header('Authori-zation')]) + ->type('file')->icon('md-add')->info($data['pay_weixin_client_cert']['desc']), + Build::uploadImage('pay_weixin_client_key', $data['pay_weixin_client_key']['info'], $data['pay_weixin_client_key']['value']) + ->url('/file/upload/1?type=1')->format(config('upload.fileExt'))->headers(['Authori-zation' => app()->request->header('Authori-zation')]) + ->type('file')->icon('md-add')->info($data['pay_weixin_client_key']['desc']), + ])->info($data['brokerage_type']['desc']), + Build::inputNum('user_extract_min_price', $data['user_extract_min_price']['info'], $data['user_extract_min_price']['value'])->info($data['user_extract_min_price']['desc']), + Build::inputNum('withdraw_fee', $data['withdraw_fee']['info'], $data['withdraw_fee']['value'])->info($data['withdraw_fee']['desc']), + Build::switch('user_extract_bank_status', $data['user_extract_bank_status']['info'], (int)$data['user_extract_bank_status']['value'])->trueValue('开启', 1)->falseValue('关闭', 0)->control(1, [ + Build::input('user_extract_bank', $data['user_extract_bank']['info'], $data['user_extract_bank']['value'])->type('textarea')->rows(6)->info($data['user_extract_bank']['desc']) + ])->info($data['user_extract_bank_status']['desc']), + Build::switch('user_extract_wechat_status', $data['user_extract_wechat_status']['info'], (int)$data['user_extract_wechat_status']['value'])->info($data['user_extract_wechat_status']['desc'])->trueValue('开启', 1)->falseValue('关闭', 0), + Build::switch('user_extract_alipay_status', $data['user_extract_alipay_status']['info'], (int)$data['user_extract_alipay_status']['value'])->info($data['user_extract_alipay_status']['desc'])->trueValue('开启', 1)->falseValue('关闭', 0), + ]), + ]); + + return $build->toArray(); + } + + /** + * 积分设置 + * @return array + */ + public function integralFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'integral_ratio_status', 'integral_ratio', 'integral_max_type', 'integral_max_num', 'integral_max_rate', 'order_give_integral', 'integral_effective_status', + 'integral_effective_time', 'next_clear_month_time', 'next_clear_quarter_time', 'next_clear_year_time' + ]); + + /** @var UserIntegralServices $userIntergralServices */ + $userIntergralServices = app()->make(UserIntegralServices::class); + [$next_clear_month_time] = $userIntergralServices->getTime(1); + [$next_clear_quarter_time] = $userIntergralServices->getTime(2); + [$next_clear_year_time] = $userIntergralServices->getTime(3); + + $build->rule([ + Build::card('积分设置')->components([ + Build::switch('integral_ratio_status', $data['integral_ratio_status']['info'], (int)$data['integral_ratio_status']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->control(1, [ + Build::inputNum('integral_ratio', $data['integral_ratio']['info'], $data['integral_ratio']['value'])->info($data['integral_ratio']['desc'])->min(0), + Build::radio('integral_max_type', $data['integral_max_type']['info'], $data['integral_max_type']['value'])->control(1, [ + Build::inputNum('integral_max_num', $data['integral_max_num']['info'], $data['integral_max_num']['value'])->info($data['integral_max_num']['desc'])->min(0), + ])->control(2, [ + Build::inputNum('integral_max_rate', $data['integral_max_rate']['info'], $data['integral_max_rate']['value'])->info($data['integral_max_rate']['desc'])->min(0), + ])->options($this->getOptions($data['integral_max_type']['parameter'])) + ])->info($data['integral_ratio_status']['desc']), + Build::inputNum('order_give_integral', $data['order_give_integral']['info'], $data['order_give_integral']['value'])->info($data['order_give_integral']['desc'])->min(0), + Build::radio('integral_effective_status', $data['integral_effective_status']['info'], $data['integral_effective_status']['value']) + ->info($data['integral_effective_status']['desc'])->control(1, [ + Build::radio('integral_effective_time', $data['integral_effective_time']['info'], $data['integral_effective_time']['value']) + ->info($data['integral_effective_time']['desc'])->control(1, [ + Build::input('next_clear_month_time', '最近清零时间', $next_clear_month_time ? date('Y-m-d', $next_clear_month_time) : '')->disabled()->info('最近清零时间') + ])->control(2, [ + Build::input('next_clear_quarter_time', '最近清零时间', $next_clear_quarter_time ? date('Y-m-d', $next_clear_quarter_time) : '')->info('最近清零时间')->disabled() + ])->control(3, [ + Build::input('next_clear_year_time', '最近清零时间', $next_clear_year_time ? date('Y-m-d', $next_clear_year_time) : '')->info('最近清零时间')->disabled() + ])->options($this->getOptions($data['integral_effective_time']['parameter'])) + ])->options($this->getOptions($data['integral_effective_status']['parameter'])), + ]), + ]); + + return $build->toArray(); + } + + /** + * 客服配置 + * @return array + */ + public function kefuFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'customer_type', 'service_feedback', 'customer_phone', 'customer_url', +// 'tourist_avatar' + ]); + + $options = $this->getOptions($data['customer_type']['parameter']); + + $build->rule([ + Build::card('客服设置')->components([ + Build::radio('customer_type', $data['customer_type']['info'], $data['customer_type']['value']) + ->options($options)->control(0, [ +// Build::uploadFrame('tourist_avatar', $data['tourist_avatar']['info'], $data['tourist_avatar']['value'])->maxNum(5)->info($data['tourist_avatar']['desc'])->url('/' . config('admin.admin_prefix') . '/widget.images/index.html'), + Build::input('service_feedback', $data['service_feedback']['info'], $data['service_feedback']['value'])->type('textarea')->rows(5)->info($data['service_feedback']['desc']), + ])->control(1, [ + Build::input('customer_phone', $data['customer_phone']['info'], $data['customer_phone']['value'])->info($data['customer_phone']['desc'])->validate(StrRules::pattern(StrRules::PHONE_NUMBER)->message('请输入正确的手机号')), + ])->control(2, [ + Build::input('customer_url', $data['customer_url']['info'], $data['customer_url']['value'])->info($data['customer_url']['desc']), + ])->info($data['customer_type']['desc']), + ]), + ]); + + return $build->toArray(); + } + + /** + * 等级设置 + * @return array + */ + public function vipFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'member_func_status', 'member_price_status', 'order_give_exp', 'sign_give_exp', 'invite_user_exp' + ]); + + $build->rule([ + Build::card('等级设置')->components([ + Build::switch('member_func_status', $data['member_func_status']['info'], (int)$data['member_func_status']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->control(1, [ +// Build::switch('member_price_status', $data['member_price_status']['info'], (int)$data['member_price_status']['value']) +// ->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['member_price_status']['desc']), + Build::inputNum('order_give_exp', $data['order_give_exp']['info'], $data['order_give_exp']['value'])->info($data['order_give_exp']['desc'])->min(0), + Build::inputNum('sign_give_exp', $data['sign_give_exp']['info'], $data['sign_give_exp']['value'])->info($data['sign_give_exp']['desc'])->min(0), + Build::inputNum('invite_user_exp', $data['invite_user_exp']['info'], $data['invite_user_exp']['value'])->info($data['invite_user_exp']['desc'])->min(0), + ])->info($data['member_func_status']['desc']), + ]), + ]); + + return $build->toArray(); + } + + /** + * 发票 + * @return array + */ + public function invoiceFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'invoice_func_status', 'special_invoice_status' + ]); + + $build->rule([ + Build::card('发票设置')->components([ + Build::switch('invoice_func_status', $data['invoice_func_status']['info'], (int)$data['invoice_func_status']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['invoice_func_status']['desc']), + Build::switch('special_invoice_status', $data['special_invoice_status']['info'], (int)$data['special_invoice_status']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['special_invoice_status']['desc']), + ]), + ]); + + return $build->toArray(); + } + + /** + * 付费会员 + * @return array + */ + public function svipFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'member_card_status', 'svip_price_status' + ]); + + //缺少svip会员价格是否展示字段 + $build->rule([ + Build::card('会员设置')->components([ + Build::switch('member_card_status', $data['member_card_status']['info'], (int)$data['member_card_status']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['member_card_status']['desc'])->control(1, [ + Build::switch('svip_price_status', $data['svip_price_status']['info'], (int)$data['svip_price_status']['value'])->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['svip_price_status']['desc']) + ]) + ]), + ]); + + return $build->toArray(); + } + + /** + * 充值设置 + * @return array + */ + public function rechargeFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'balance_func_status', 'recharge_attention', 'recharge_switch', 'store_user_min_recharge' + ]); + + $build->rule([ + Build::card('充值设置')->components([ + Build::switch('balance_func_status', $data['balance_func_status']['info'], (int)$data['balance_func_status']['value'])->falseValue('关闭', 0)->trueValue('开启', 1)->control(1, [ + Build::input('recharge_attention', $data['recharge_attention']['info'], $data['recharge_attention']['value'])->rows(5)->type($data['recharge_attention']['type'])->info($data['recharge_attention']['desc']), + Build::switch('recharge_switch', $data['recharge_switch']['info'], (int)$data['recharge_switch']['value'])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['recharge_switch']['desc']), + Build::inputNum('store_user_min_recharge', $data['store_user_min_recharge']['info'], $data['store_user_min_recharge']['value'])->info($data['store_user_min_recharge']['desc'])->min(0.01)->max(999999999), + ])->info($data['balance_func_status']['desc']), + ]), + ]); + + return $build->toArray(); + } + + /** + * 用户设置 + * @return array + */ + public function userFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'h5_avatar', + 'store_user_mobile', 'register_integral_status', 'register_give_integral', 'register_money_status', 'register_give_money', 'register_coupon_status', 'register_give_coupon', 'first_order_status', 'first_order_discount', 'first_order_discount_limit', 'register_price_status', + 'member_func_status', 'member_price_status', 'order_give_exp', 'sign_give_exp', 'invite_user_exp', 'level_activate_status', 'level_activate_status', 'level_integral_status', 'level_give_integral', 'level_money_status', 'level_give_money', 'level_coupon_status', 'level_give_coupon', + 'member_card_status', 'svip_price_status' + ]); + + + $build->rule([ + Build::tabs()->option('基础信息', [ + Build::uploadFrame('h5_avatar', $data['h5_avatar']['info'], $data['h5_avatar']['value'])->url('/' . config('admin.admin_prefix') . '/widget.images/index.html')->info($data['h5_avatar']['desc']), + ])->option('登录注册', [ + Build::alert('多端(公众号、小程序)账号统一,可以开启强制手机号登录实现,也可以绑定微信开放平台实现:https://open.weixin.qq.com', Alert::WARNING)->showIcon(true), + Build::radio('store_user_mobile', $data['store_user_mobile']['info'], $data['store_user_mobile']['value'])->options($this->getOptions($data['store_user_mobile']['parameter']))->info($data['store_user_mobile']['desc']) + ])->option('等级会员', [ + Build::switch('member_func_status', $data['member_func_status']['info'], (int)$data['member_func_status']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->control(1, [ +// Build::switch('member_price_status', $data['member_price_status']['info'], (int)$data['member_price_status']['value']) +// ->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['member_price_status']['desc']), + Build::inputNum('order_give_exp', $data['order_give_exp']['info'], $data['order_give_exp']['value'])->info($data['order_give_exp']['desc'])->min(0), + Build::inputNum('sign_give_exp', $data['sign_give_exp']['info'], $data['sign_give_exp']['value'])->info($data['sign_give_exp']['desc'])->min(0), + Build::inputNum('invite_user_exp', $data['invite_user_exp']['info'], $data['invite_user_exp']['value'])->info($data['invite_user_exp']['desc'])->min(0), + ])->info($data['member_func_status']['desc']), + ])->option('付费会员', [ + Build::switch('member_card_status', $data['member_card_status']['info'], (int)$data['member_card_status']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['member_card_status']['desc'])->control(1, [ + Build::switch('svip_price_status', $data['svip_price_status']['info'], (int)$data['svip_price_status']['value'])->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['svip_price_status']['desc']) + ]) + ]) + ]); + + return $build->toArray(); + } + + /** + * 发货设置 + * @return array + */ + public function deliverFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'store_free_postage', 'offline_postage', +// 'store_self_mention' + ]); + + $build->rule([ + Build::card('发货设置')->components([ + Build::switch('whole_free_shipping', '全场包邮', (int)$data['store_free_postage']['value'] > 0 ? 1 : 0) + ->falseValue('关闭', 0)->trueValue('开启', 1)->control(1, [ + Build::inputNum('store_free_postage', $data['store_free_postage']['info'], $data['store_free_postage']['value'])->info($data['store_free_postage']['desc'])->min(0) + ])->info('开启全场包邮必须设置包邮金额大于0的金额才能开启'), + Build::switch('offline_postage', '线下支付是否包邮', (int)$data['offline_postage']['value'] > 0 ? 1 : 0) + ->falseValue('不包邮', 0)->trueValue('包邮', 1)->info($data['offline_postage']['desc'] ?? ''), +// Build::switch('store_self_mention', '是否开启到店自提', (int)$data['store_self_mention']['value'] > 0 ? 1 : 0) +// ->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['store_self_mention']['desc'] ?? ''), + ]), + + ]); + + return $build->toArray(); + } + + + /** + * 同城设置 + * @return array + */ + public function cityDeliverFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'city_delivery_status', 'self_delivery_status', 'dada_delivery_status', 'dada_app_key', 'dada_app_sercret', 'dada_source_id', 'uupt_appkey', 'uu_delivery_status', 'uupt_app_id', 'uupt_open_id' + ]); + + $build->rule([ + Build::card('同城配送')->components([ + Build::switch('city_delivery_status', $data['city_delivery_status']['info'], (int)$data['city_delivery_status']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->control(1, [ + Build::switch('self_delivery_status', $data['self_delivery_status']['info'], (int)$data['self_delivery_status']['value'])->info($data['self_delivery_status']['desc']), + Build::switch('dada_delivery_status', $data['dada_delivery_status']['info'], (int)$data['dada_delivery_status']['value'])->control(1, [ + Build::input('dada_app_key', $data['dada_app_key']['info'], $data['dada_app_key']['value'])->info($data['dada_app_key']['desc']), + Build::input('dada_app_sercret', $data['dada_app_sercret']['info'], $data['dada_app_sercret']['value'])->info($data['dada_app_sercret']['desc']), + Build::input('dada_source_id', $data['dada_source_id']['info'], $data['dada_source_id']['value'])->info($data['dada_source_id']['desc']), + ])->info($data['dada_delivery_status']['desc']), + Build::switch('uu_delivery_status', $data['uu_delivery_status']['info'], (int)$data['uu_delivery_status']['value'])->control(1, [ + Build::input('uupt_appkey', $data['uupt_appkey']['info'], $data['uupt_appkey']['value'])->info($data['uupt_appkey']['desc']), + Build::input('uupt_app_id', $data['uupt_app_id']['info'], $data['uupt_app_id']['value'])->info($data['uupt_app_id']['desc']), + Build::input('uupt_open_id', $data['uupt_open_id']['info'], $data['uupt_open_id']['value'])->info($data['uupt_open_id']['desc']), + ])->info($data['uu_delivery_status']['desc']), + ])->info($data['city_delivery_status']['desc']) + ]), + ]); + + return $build->toArray(); + } + + /** + * 第三方配置 + * @return array + */ + public function thirdPartyFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'pay_success_printing_switch', 'develop_id', 'printing_api_key', 'printing_client_id', 'terminal_number', + 'print_type', 'fey_user', 'fey_ukey', 'fey_sn', + 'system_product_copy_type', 'copy_product_apikey', 'logistics_type', + 'system_express_app_code', 'config_export_open', 'config_export_siid', 'config_export_to_name', 'config_export_to_tel', + 'tengxun_map_key', 'system_statistics', 'config_export_to_address', 'verify_expire_time', 'erp_open', + 'erp_type', 'jst_appkey', 'jst_appsecret', 'jst_default_shopid', 'jst_login_account', 'jst_login_password' + ]); + + $build->rule([ + Build::tabs()->option('小票打印配置', [ + Build::switch('pay_success_printing_switch', $data['pay_success_printing_switch']['info'], (int)$data['pay_success_printing_switch']['value'])->control(1, [ + Build::radio('print_type', $data['print_type']['info'], $data['print_type']['value'])->control(1, [ + Build::input('develop_id', $data['develop_id']['info'], $data['develop_id']['value'])->info($data['develop_id']['desc']), + Build::input('printing_api_key', $data['printing_api_key']['info'], $data['printing_api_key']['value'])->info($data['printing_api_key']['desc']), + Build::input('printing_client_id', $data['printing_client_id']['info'], $data['printing_client_id']['value'])->info($data['printing_client_id']['desc']), + Build::input('terminal_number', $data['terminal_number']['info'], $data['terminal_number']['value'])->info($data['terminal_number']['desc']), + ])->control(2, [ + Build::input('fey_user', $data['fey_user']['info'], $data['fey_user']['value'])->info($data['fey_user']['desc']), + Build::input('fey_ukey', $data['fey_ukey']['info'], $data['fey_ukey']['value'])->info($data['fey_ukey']['desc']), + Build::input('fey_sn', $data['fey_sn']['info'], $data['fey_sn']['value'])->info($data['fey_sn']['desc']) + ])->options($this->getOptions($data['print_type']['parameter']))->info($data['print_type']['desc']) + ])->trueValue('打开', 1)->falseValue('关闭', 0), + ])->option('采集商品配置', [ + Build::radio('system_product_copy_type', $data['system_product_copy_type']['info'], $data['system_product_copy_type']['value'])->control(2, [ + Build::input('copy_product_apikey', $data['copy_product_apikey']['info'], $data['copy_product_apikey']['value'])->info($data['copy_product_apikey']['desc']) + ])->options($this->getOptions($data['system_product_copy_type']['parameter']))->info($data['system_product_copy_type']['desc']) + ])->option('物流查询', [ + Build::radio('logistics_type', $data['logistics_type']['info'], $data['logistics_type']['value'])->control(2, [ + Build::input('system_express_app_code', $data['system_express_app_code']['info'], $data['system_express_app_code']['value'])->info($data['system_express_app_code']['desc']) + ])->options($this->getOptions($data['logistics_type']['parameter']))->info($data['logistics_type']['desc']) + ])->option('电子面单', [ + Build::radio('config_export_open', $data['config_export_open']['info'], $data['config_export_open']['value'])->control(1, [ + Build::input('config_export_to_name', $data['config_export_to_name']['info'], $data['config_export_to_name']['value'])->info($data['config_export_to_name']['desc']), + Build::input('config_export_to_tel', $data['config_export_to_tel']['info'], $data['config_export_to_tel']['value'])->info($data['config_export_to_tel']['desc']), + Build::input('config_export_to_address', $data['config_export_to_address']['info'], $data['config_export_to_address']['value'])->info($data['config_export_to_address']['desc']), + Build::input('config_export_siid', $data['config_export_siid']['info'], $data['config_export_siid']['value'])->info($data['config_export_siid']['desc']), + ])->options($this->getOptions($data['config_export_open']['parameter']))->info($data['config_export_open']['desc']) + ])->option('地图配置', [ + Build::input('tengxun_map_key', $data['tengxun_map_key']['info'], $data['tengxun_map_key']['value'])->info($data['tengxun_map_key']['desc']), + ])->option('短信', [ + Build::inputNum('verify_expire_time', $data['verify_expire_time']['info'], $data['verify_expire_time']['value'])->info($data['verify_expire_time']['desc'])->min(0), + ])->option('统计', [ + Build::input('system_statistics', $data['system_statistics']['info'], $data['system_statistics']['value'])->rows(7)->type('textarea')->info($data['system_statistics']['desc']), + ])->option('ERP配置', [ + Build::switch('erp_open', $data['erp_open']['info'], (int)$data['erp_open']['value'])->control(1, [ + Build::radio('erp_type', $data['erp_type']['info'], $data['erp_type']['value'])->control(1, [ + Build::input('jst_login_account', $data['jst_login_account']['info'], $data['jst_login_account']['value'])->info($data['jst_login_account']['desc']), + Build::input('jst_login_password', $data['jst_login_password']['info'], $data['jst_login_password']['value'])->info($data['jst_login_password']['desc']), + Build::input('jst_appkey', $data['jst_appkey']['info'], $data['jst_appkey']['value'])->info($data['jst_appkey']['desc']), + Build::input('jst_appsecret', $data['jst_appsecret']['info'], $data['jst_appsecret']['value'])->info($data['jst_appsecret']['desc']), + Build::input('jst_default_shopid', $data['jst_default_shopid']['info'], $data['jst_default_shopid']['value'])->info($data['jst_default_shopid']['desc']), + ])->options($this->getOptions($data['erp_type']['parameter']))->info($data['erp_type']['desc']) + ])->trueValue('打开', 1)->falseValue('关闭', 0), + ]), + ]); + + return $build->toArray(); + } + + /** + * 微信开放平台 + * @return array + */ + public function wxOpenBaseFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'wechat_open_app_id', 'wechat_open_app_secret' + ]); + + $build->rule([ + Build::card('微信开放平台')->components([ + Build::alert('小程序、公众号、PC端登录、APP微信登录或企业微信用户同步必须配置微信开放平台,申请微信开放平台地址:https://open.weixin.qq.com', 'warning')->showIcon(true), + Build::input('wechat_open_app_id', $data['wechat_open_app_id']['info'], $data['wechat_open_app_id']['value'])->info($data['wechat_open_app_id']['desc']), + Build::input('wechat_open_app_secret', $data['wechat_open_app_secret']['info'], $data['wechat_open_app_secret']['value'])->info($data['wechat_open_app_secret']['desc']), + ]), + ]); + + return $build->toArray(); + } + + /** + * PC设置 + * @return array + */ + public function pcBaseFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'pc_logo', 'bast_number', 'first_number', 'product_phone_buy_url', 'contact_number', + 'company_address', 'copyright', 'seo_title', 'site_keywords', 'site_description', 'record_No', + 'wechat_open_app_id', 'wechat_open_app_secret', 'links_open', 'links_list', 'filing_list' + ]); + $base[] = Build::uploadFrame('pc_logo', $data['pc_logo']['info'], $data['pc_logo']['value'])->info($data['pc_logo']['desc'])->url('/' . config('admin.admin_prefix') . '/widget.images/index.html'); + foreach (['contact_number', 'company_address', 'copyright', 'seo_title', 'site_keywords', 'site_description', 'record_No'] as $key) { + $base[] = Build::input($key, $data[$key]['info'], $data[$key]['value'])->info($data[$key]['desc'])->type($data[$key]['type']); + } + $open = []; + foreach (['wechat_open_app_id', 'wechat_open_app_secret'] as $key) { + $open[] = Build::input($key, $data[$key]['info'], $data[$key]['value'])->info($data[$key]['desc'])->type($data[$key]['type']); + } + $build->rule([ + Build::card('基础设置')->components($base), + Build::card('商品设置')->components([ + Build::inputNum('bast_number', $data['bast_number']['info'], $data['bast_number']['value'])->info($data['bast_number']['desc'])->min(0), + Build::inputNum('first_number', $data['first_number']['info'], $data['first_number']['value'])->info($data['first_number']['desc'])->min(0), + Build::radio('product_phone_buy_url', $data['product_phone_buy_url']['info'], $data['product_phone_buy_url']['value'])->info($data['product_phone_buy_url']['desc'])->options($this->getOptions($data['product_phone_buy_url']['parameter'])), + ]), + Build::card('微信开放平台(pc端用户扫码登录使用)')->components($open), + Build::card('友情链接')->components([ + Build::switch('links_open', $data['links_open']['info'], (int)$data['links_open']['value'])->info($data['links_open']['desc'])->control(1, [ + Build::diyTable('links_list', $data['links_list']['info'], is_array($data['links_list']['value']) ? $data['links_list']['value'] : [])->info($data['links_list']['desc']) + ->column('链接名称', 'name')->column('链接地址', 'url')->column('排序', 'sort', InputNumber::NAME, ['editable' => false]), + ])->trueValue('开启', 1)->falseValue('关闭', 0), + ]), + Build::card('底部(公安备案等自定义)')->components([ + Build::diyTable('filing_list', $data['filing_list']['info'], is_array($data['filing_list']['value']) ? $data['filing_list']['value'] : [])->info($data['filing_list']['desc']) + ->column('图标', 'icon', 'image')->column('名称', 'name')->column('链接地址', 'url')->column('排序', 'sort', InputNumber::NAME, ['editable' => false]) + ]), + ]); + + return $build->toArray(); + } + + /** + * PC设置 + * @return array + */ + public function appBaseFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'wechat_app_appid', 'wechat_app_appsecret' + ]); + $open = []; + foreach (['wechat_app_appid', 'wechat_app_appsecret'] as $key) { + $open[] = Build::input($key, $data[$key]['info'], $data[$key]['value'])->info($data[$key]['desc'])->type($data[$key]['type']); + } + $build->rule([ + Build::card('微信开放平台(微信登录、支付都需要开通此配置)')->components($open), + ]); + + return $build->toArray(); + } + + /** + * 微信基础配置 + * @return array + */ + public function wechatBaseFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'share_qrcode', 'spread_share_forever', 'wechat_qrcode', 'wechat_appid', + 'wechat_appsecret', 'wechat_encodingaeskey', 'wechat_token', 'api', 'wechat_encode', +// 'wechat_share_img', 'wechat_share_title', 'wechat_share_synopsis', + 'create_wechat_user' + ]); + + $build->rule([ + Build::card('公众号开发者信息')->components([ + Build::input('wechat_appid', $data['wechat_appid']['info'], $data['wechat_appid']['value'])->info($data['wechat_appid']['desc']), + Build::input('wechat_appsecret', $data['wechat_appsecret']['info'], $data['wechat_appsecret']['value'])->info($data['wechat_appsecret']['desc']), + ]), + Build::card('服务器配置')->components([ + Build::input('wechat_encodingaeskey', $data['wechat_encodingaeskey']['info'], $data['wechat_encodingaeskey']['value'])->info($data['wechat_encodingaeskey']['desc'])->randAESK(), + Build::input('wechat_token', $data['wechat_token']['info'], $data['wechat_token']['value'])->info($data['wechat_token']['desc'])->randToken(), + Build::input('api', $data['api']['info'], sys_config('site_url') . '/api/wechat/serve')->info($data['api']['desc'])->disabled()->copy(), + Build::radio('wechat_encode', $data['wechat_encode']['info'], $data['wechat_encode']['value'])->vertical(true)->info($data['wechat_encode']['desc'])->options($this->getOptions($data['wechat_encode']['parameter'])), + ]), + Build::card('微信公众号')->components([ + Build::radio('share_qrcode', $data['share_qrcode']['info'], $data['share_qrcode']['value'])->info($data['share_qrcode']['desc'])->options($this->getOptions($data['share_qrcode']['parameter'])), + Build::radio('spread_share_forever', $data['spread_share_forever']['info'], $data['spread_share_forever']['value'])->info($data['spread_share_forever']['desc'])->options($this->getOptions($data['spread_share_forever']['parameter'])), + Build::uploadFrame('wechat_qrcode', $data['wechat_qrcode']['info'], $data['wechat_qrcode']['value'])->info($data['wechat_qrcode']['desc'])->url('/' . config('admin.admin_prefix') . '/widget.images/index.html'), + Build::switch('create_wechat_user', $data['create_wechat_user']['info'], (int)$data['create_wechat_user']['value'])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['create_wechat_user']['desc']), + ]), +// Build::card('首页分享')->components([ +// Build::uploadFrame('wechat_share_img', $data['wechat_share_img']['info'], $data['wechat_share_img']['value'])->info($data['wechat_share_img']['desc'])->url('/' . config('admin.admin_prefix') . '/widget.images/index.html'), +// Build::input('wechat_share_title', $data['wechat_share_title']['info'], $data['wechat_share_title']['value'])->info($data['wechat_share_title']['desc']), +// Build::input('wechat_share_synopsis', $data['wechat_share_synopsis']['info'], $data['wechat_share_synopsis']['value'])->type('textarea')->info($data['wechat_share_synopsis']['desc']), +// ]) + ]); + + return $build->toArray(); + } + + /** + * 小程序基础配置 + * @return array + */ + public function routineBaseFormBuild() + { + $data = $this->getConfigAllField([ + 'routine_appId', 'routine_appsecret', 'routine_contact_type', 'routine_name', 'store_user_avatar', 'order_shipping_open', 'routine_auth_type', 'store_user_agreement' + ]); + return $data; + } + + + /** + * 支付 + * @return array + */ + public function shopPayFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'pay_weixin_open', 'pay_weixin_mchid', 'pay_weixin_key', 'paydir', 'yue_pay_status','is_cashier_yue_pay_verify', 'offline_pay_status', + 'offline_pay_status', 'ali_pay_status', 'ali_pay_appid', 'alipay_public_key', 'alipay_merchant_private_key', + 'pay_weixin_client_cert', 'pay_wechat_type', 'pay_weixin_serial_no', 'v3_pay_weixin_key', 'pay_weixin_client_key', 'pay_routine_open', 'pay_routine_mchid' + ]); + $site_url = sys_config('site_url', ''); + $build->rule([ + Build::tabs()->option('微信支付', [ + Build::alert('登录微信商户(地址:https://pay.weixin.qq.com,支付授权目录、回调链接:' . $site_url . '; http,https最好都配置),帮助文档地址:https://doc.crmeb.com/web/pro/crmebprov2/1203', Alert::WARNING)->showIcon(true), + Build::switch('pay_weixin_open', $data['pay_weixin_open']['info'], (int)$data['pay_weixin_open']['value'])->control(1, [ + Build::input('pay_weixin_mchid', $data['pay_weixin_mchid']['info'], $data['pay_weixin_mchid']['value'])->info($data['pay_weixin_mchid']['desc']), + Build::radio('pay_wechat_type', $data['pay_wechat_type']['info'], (int)$data['pay_wechat_type']['value'])->control(1, [ + Build::input('pay_weixin_serial_no', $data['pay_weixin_serial_no']['info'], $data['pay_weixin_serial_no']['value'])->info($data['pay_weixin_serial_no']['desc']), + Build::input('v3_pay_weixin_key', $data['v3_pay_weixin_key']['info'], $data['v3_pay_weixin_key']['value'])->info($data['v3_pay_weixin_key']['desc']), + ])->control(0, [ + Build::input('pay_weixin_key', $data['pay_weixin_key']['info'], $data['pay_weixin_key']['value'])->info($data['pay_weixin_key']['desc']), + ])->options($this->getOptions($data['pay_wechat_type']['parameter']))->info($data['pay_wechat_type']['desc']), + Build::uploadImage('pay_weixin_client_cert', $data['pay_weixin_client_cert']['info'], $data['pay_weixin_client_cert']['value']) + ->url('/file/upload/1?type=1')->format(config('upload.fileExt'))->headers(['Authori-zation' => app()->request->header('Authori-zation')]) + ->type('file')->icon('md-add')->info($data['pay_weixin_client_cert']['desc']), + Build::uploadImage('pay_weixin_client_key', $data['pay_weixin_client_key']['info'], $data['pay_weixin_client_key']['value']) + ->url('/file/upload/1?type=1')->format(config('upload.fileExt'))->headers(['Authori-zation' => app()->request->header('Authori-zation')]) + ->type('file')->icon('md-add')->info($data['pay_weixin_client_key']['desc']), + Build::switch('pay_routine_open', $data['pay_routine_open']['info'], (int)$data['pay_routine_open']['value'])->control(1, [ + Build::input('pay_routine_mchid', $data['pay_routine_mchid']['info'], $data['pay_routine_mchid']['value'])->info($data['pay_routine_mchid']['desc']) + ])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['pay_routine_open']['desc']) + + ])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['pay_weixin_open']['desc']), + ])->option('余额支付', [ + Build::switch('yue_pay_status', $data['yue_pay_status']['info'], (int)$data['yue_pay_status']['value'])->trueValue('开启', 1)->falseValue('关闭', 2)->info($data['yue_pay_status']['desc']), + Build::switch('is_cashier_yue_pay_verify', $data['is_cashier_yue_pay_verify']['info'], (int)$data['is_cashier_yue_pay_verify']['value'])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['is_cashier_yue_pay_verify']['desc']) + ])->option('线下支付', [ + Build::switch('offline_pay_status', $data['offline_pay_status']['info'], (int)$data['offline_pay_status']['value'])->trueValue('开启', 1)->falseValue('关闭', 2)->info($data['offline_pay_status']['desc']) + ])->option('支付宝支付', [ + Build::alert('登录支付宝商家(地址:https://b.alipay.com,需要配置ip白名单以及回调地址回调地址:' . $site_url . '),帮助文档地址:https://doc.crmeb.com/web/pro/crmebprov2/1204', Alert::WARNING)->showIcon(true), + Build::switch('ali_pay_status', $data['ali_pay_status']['info'], (int)$data['ali_pay_status']['value'])->control(1, [ + Build::input('ali_pay_appid', $data['ali_pay_appid']['info'], $data['ali_pay_appid']['value'])->info($data['ali_pay_appid']['desc']), + Build::input('alipay_public_key', $data['alipay_public_key']['info'], $data['alipay_public_key']['value'])->rows(5)->type('textarea')->info($data['alipay_public_key']['desc']), + Build::input('alipay_merchant_private_key', $data['alipay_merchant_private_key']['info'], $data['alipay_merchant_private_key']['value'])->rows(5)->type('textarea')->info($data['alipay_merchant_private_key']['desc']), + ])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['ali_pay_status']['desc']), + ]), + ]); + + return $build->toArray(); + } + + /** + * 砍价设置 + * @return array + */ + public function bargainFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'bargain_subscribe' + ]); + + $build->rule([ + Build::card('砍价设置')->components([ + Build::switch('bargain_subscribe', $data['bargain_subscribe']['info'], (int)$data['bargain_subscribe']['value'])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['bargain_subscribe']['desc']), + ]), + ]); + + return $build->toArray(); + } + + /** + * 交易设置 + * @return array + */ + public function shopTradeFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'store_stock', 'order_cancel_time', 'order_activity_time', + 'order_bargain_time', 'order_seckill_time', 'order_pink_time', + 'system_delivery_time', 'refund_name', 'refund_phone', 'refund_address', 'stor_reason','collate_not_operating_time','reminder_deadline_second_card_time', + 'bargain_subscribe', 'system_comment_time', 'rebate_points_orders_time', 'table_code_not_operating_time' + // 'store_cashier_order_rate', 'store_recharge_order_rate', 'store_self_order_rate', 'store_svip_order_rate', 'store_writeoff_order_rate' + ]); + $timeData[] = Build::alert('营销活动未支付时间如果设置为0将使用默认活动取消时间,优先使用单独活动配置', Alert::WARNING)->showIcon(true); + foreach (['order_cancel_time', 'order_activity_time', + 'order_bargain_time', 'order_seckill_time', 'order_pink_time', 'rebate_points_orders_time', 'table_code_not_operating_time', 'collate_not_operating_time', 'reminder_deadline_second_card_time'] as $item) { + $timeData[] = Build::inputNum($item, $data[$item]['info'], $data[$item]['value'])->info($data[$item]['desc'])->min(0); + } + $refund[] = Build::alert('售后处理默认退货地址(门店订单退货默认门店地址)', Alert::WARNING)->showIcon(true); + foreach (['refund_name', 'refund_phone', 'refund_address', 'stor_reason'] as $key) { + $rule = Build::input($key, $data[$key]['info'], $data[$key]['value'])->rows(5)->type($data[$key]['type'])->info($data[$key]['desc']); + if ('refund_phone' === $key) { + $rule->validate(StrRules::pattern(StrRules::PHONE_NUMBER)->message('请输入正确的手机号码')); + } + $refund[] = $rule; + } + // $store[] = Build::alert('需要和门店对账,请仔细配置(配置立即生效,不影响已成交订单)', Alert::WARNING)->showIcon(true); + // foreach (['store_cashier_order_rate', 'store_recharge_order_rate', 'store_self_order_rate', 'store_svip_order_rate', 'store_writeoff_order_rate'] as $key) { + // $store[] = Build::inputNum($key, $data[$key]['info'], $data[$key]['value'])->min(0)->info($data[$key]['desc']); + // } + $build->rule([ + Build::card('库存警戒')->components([ + Build::inputNum('store_stock', $data['store_stock']['info'], $data['store_stock']['value'])->info($data['store_stock']['desc'])->min(0), + ]), + Build::card('订单取消时间')->components($timeData), + Build::card('自动收货时间')->components([ + Build::alert('输入0为不设置自动收货', Alert::WARNING)->showIcon(true), + Build::inputNum('system_delivery_time', $data['system_delivery_time']['info'], $data['system_delivery_time']['value'])->info($data['system_delivery_time']['desc'])->min(0), + ]), + Build::card('自动默认好评时间')->components([ + Build::alert('输入0为不设置自动默认好评', Alert::WARNING)->showIcon(true), + Build::inputNum('system_comment_time', $data['system_comment_time']['info'], $data['system_comment_time']['value'])->info($data['system_comment_time']['desc'])->min(0), + ]), + Build::card('售后退款设置')->components($refund), + // Build::card('门店手续费设置')->components($store), + ]); + + return $build->toArray(); + } + + /** + * 商城首页 + * @return array + */ + public function shopBaseFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'station_open', 'site_name', 'site_url', 'wap_login_logo', 'site_phone', + 'admin_login_slide', 'site_logo', 'site_logo_square', 'login_logo', + 'navigation_open', 'cache_config', 'start_login_logo', +// 'store_func_status', + 'video_func_status', 'product_video_status', +// 'h5_avatar', 'store_user_mobile' + 'wechat_share_img', 'wechat_share_title', 'wechat_share_synopsis', 'product_poster_title', + ]); + + $system = []; + foreach (['site_name', 'site_url', 'site_phone', 'cache_config'] as $key) { + $system[] = Build::input($key, $data[$key]['info'], $data[$key]['value'])->maxlength($key === 'site_name' ? 20 : null)->info($data[$key]['desc'])->type($data[$key]['type']); + } + $setting = []; + foreach (['site_logo', 'site_logo_square', 'login_logo', 'admin_login_slide', 'start_login_logo'] as $key) { + $setting[] = Build::uploadFrame($key, $data[$key]['info'], $data[$key]['value'])->url('/' . config('admin.admin_prefix') . '/widget.images/index.html')->info($data[$key]['desc'])->maxNum($key === 'admin_login_slide' ? 5 : 1); + } + $build->rule([ + Build::tabs()->option('系统信息', [ + Build::switch('station_open', $data['station_open']['info'], (int)$data['station_open']['value'])->control(1, $system)->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['station_open']['desc']), + ])->option('后台设置', $setting) + ->option('移动端设置', [ + Build::uploadFrame('wap_login_logo', $data['wap_login_logo']['info'], $data['wap_login_logo']['value'])->url('/' . config('admin.admin_prefix') . '/widget.images/index.html')->info($data['wap_login_logo']['desc']), +// Build::uploadFrame('h5_avatar', $data['h5_avatar']['info'], $data['h5_avatar']['value'])->url('/' . config('admin.admin_prefix') . '/widget.images/index.html')->info($data['h5_avatar']['desc']), +// Build::radio('store_user_mobile', $data['store_user_mobile']['info'], $data['store_user_mobile']['value'])->options($this->getOptions($data['store_user_mobile']['parameter']))->info($data['store_user_mobile']['desc']), + Build::switch('navigation_open', $data['navigation_open']['info'], (int)$data['navigation_open']['value'])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['navigation_open']['desc']), +// Build::switch('store_func_status', $data['store_func_status']['info'], (int)$data['store_func_status']['value'])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['store_func_status']['desc']), + Build::switch('video_func_status', $data['video_func_status']['info'], (int)$data['video_func_status']['value'])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['video_func_status']['desc']), + Build::switch('product_video_status', $data['product_video_status']['info'], (int)$data['product_video_status']['value'])->trueValue('开启', 1)->falseValue('关闭', 0)->info($data['product_video_status']['desc']), + ]) + ->option('分享设置', [ + Build::uploadFrame('wechat_share_img', $data['wechat_share_img']['info'], $data['wechat_share_img']['value'])->info($data['wechat_share_img']['desc'])->url('/' . config('admin.admin_prefix') . '/widget.images/index.html'), + Build::input('wechat_share_title', $data['wechat_share_title']['info'], $data['wechat_share_title']['value'])->info($data['wechat_share_title']['desc']), + Build::input('wechat_share_synopsis', $data['wechat_share_synopsis']['info'], $data['wechat_share_synopsis']['value'])->type('textarea')->info($data['wechat_share_synopsis']['desc']), + Build::input('product_poster_title', $data['product_poster_title']['info'], $data['product_poster_title']['value'])->maxlength(25)->info($data['product_poster_title']['desc']), + ]), +// ->option('APP微信开放平台', [ +// Build::alert('小程序、公众号、PC端登录、APP微信登录或企业微信用户同步必须配置微信开放平台,申请微信开放平台地址:https://open.weixin.qq.com', 'warning')->showIcon(true), +// Build::input('wechat_app_appid', $data['wechat_app_appid']['info'], $data['wechat_app_appid']['value'])->info($data['wechat_app_appid']['desc']), +// Build::input('wechat_app_appsecret', $data['wechat_app_appsecret']['info'], $data['wechat_app_appsecret']['value'])->info($data['wechat_app_appsecret']['desc']), +// ]), + ]); + + return $build->toArray(); + } + + /** + * 门店设置 + * @return array + */ + public function storeFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'store_func_status', 'store_delivery_scope', 'store_splicing_switch', 'store_self_mention' + ]); + + $build->rule([ + Build::card('门店设置')->components([ + Build::switch('store_func_status', $data['store_func_status']['info'], (int)$data['store_func_status']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->control(1, [ + Build::switch('store_self_mention', $data['store_self_mention']['info'], (int)$data['store_self_mention']['value'] > 0 ? 1 : 0) + ->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['store_self_mention']['desc'] ?? ''), + Build::switch('store_delivery_scope', $data['store_delivery_scope']['info'], (int)$data['store_delivery_scope']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['store_delivery_scope']['desc']), + Build::switch('store_splicing_switch', $data['store_splicing_switch']['info'], (int)$data['store_splicing_switch']['value']) + ->falseValue('关闭', 0)->trueValue('开启', 1)->info($data['store_splicing_switch']['desc']), + ])->info($data['store_func_status']['desc']), + ]), + ]); + return $build->toArray(); + } + + /** + * 企业微信配置 + * @return array + */ + public function workFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'wechat_work_corpid', 'wechat_work_address_secret', 'wechat_work_token', 'wechat_work_aes_key', + 'wechat_work_user_secret', 'wechat_work_build_agent_id', 'wechat_work_build_secret', + ]); + + $build->rule([ + Build::card('企业微信基础配置')->components([ + Build::input('wechat_work_corpid', $data['wechat_work_corpid']['info'], $data['wechat_work_corpid']['value'])->info($data['wechat_work_corpid']['desc']), + ]), + Build::card('企业微信通讯录配置')->components([ + Build::alert('1.请先登录企业微信:https://work.weixin.qq.com 客户与上下游->客户联系->关联微信开发者ID。2.请必须绑定微信开放平台', Alert::WARNING)->closable(true), + Build::input('wechat_work_address_secret', $data['wechat_work_address_secret']['info'], $data['wechat_work_address_secret']['value'])->info($data['wechat_work_address_secret']['desc']), + Build::input('wechat_work_token', $data['wechat_work_token']['info'], $data['wechat_work_token']['value'])->randToken()->copy()->info($data['wechat_work_token']['desc']), + Build::input('wechat_work_aes_key', $data['wechat_work_aes_key']['info'], $data['wechat_work_aes_key']['value'])->randAESK()->copy()->info($data['wechat_work_aes_key']['desc']), + Build::input('work_address_url', '服务器地址', sys_config('site_url') . '/api/work/serve')->disabled()->copy(), + ]), + Build::card('企业微信客户设置')->components([ + Build::input('wechat_work_user_secret', $data['wechat_work_user_secret']['info'], $data['wechat_work_user_secret']['value'])->info($data['wechat_work_user_secret']['desc']), + ]), + Build::card('企业微信自建应用设置')->components([ + Build::input('wechat_work_build_agent_id', $data['wechat_work_build_agent_id']['info'], $data['wechat_work_build_agent_id']['value'])->info($data['wechat_work_build_agent_id']['desc']), + Build::input('wechat_work_build_secret', $data['wechat_work_build_secret']['info'], $data['wechat_work_build_secret']['value'])->info($data['wechat_work_build_secret']['desc']), + ]), + ]); + + return $build->toArray(); + } + + /** + * 门店财务设置 + * @return array + */ + public function financeFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ + 'store_cashier_order_rate', 'store_self_order_rate', 'store_writeoff_order_rate', 'store_recharge_order_rate', + 'store_svip_order_rate', 'store_extract_min_price', 'store_extract_max_price', + ]); + + $build->rule([ + Build::tabs()->option('手续费', [ + Build::inputNum('store_cashier_order_rate', $data['store_cashier_order_rate']['info'], $data['store_cashier_order_rate']['value']) + ->info($data['store_cashier_order_rate']['desc'])->min(0), + Build::inputNum('store_self_order_rate', $data['store_self_order_rate']['info'], $data['store_self_order_rate']['value']) + ->info($data['store_self_order_rate']['desc'])->min(0), + Build::inputNum('store_writeoff_order_rate', $data['store_writeoff_order_rate']['info'], $data['store_writeoff_order_rate']['value']) + ->info($data['store_writeoff_order_rate']['desc'])->min(0), + Build::inputNum('store_recharge_order_rate', $data['store_recharge_order_rate']['info'], $data['store_recharge_order_rate']['value']) + ->info($data['store_recharge_order_rate']['desc'])->min(0), + Build::inputNum('store_svip_order_rate', $data['store_svip_order_rate']['info'], $data['store_svip_order_rate']['value']) + ->info($data['store_svip_order_rate']['desc'])->min(0), + ])->option('提现设置', [ + Build::inputNum('store_extract_min_price', $data['store_extract_min_price']['info'], $data['store_extract_min_price']['value']) + ->info($data['store_extract_min_price']['desc'])->min(0), + Build::inputNum('store_extract_max_price', $data['store_extract_max_price']['info'], $data['store_extract_max_price']['value']) + ->info($data['store_extract_max_price']['desc'])->min(0), + ]) + ]); + + return $build->toArray(); + } + + /** + * 供应商财务设置 + * @return array + */ + public function supplierFinanceFormBuild() + { + $build = new Build(); + $build->url('setting/config/save_basics'); + + $data = $this->getConfigAllField([ 'supplier_extract_min_price', 'supplier_extract_max_price']); + + $build->rule([ + Build::tabs()->option('财务设置', [ + Build::inputNum('supplier_extract_min_price', $data['supplier_extract_min_price']['info'], $data['supplier_extract_min_price']['value']) + ->info($data['supplier_extract_min_price']['desc'])->min(0), + Build::inputNum('supplier_extract_max_price', $data['supplier_extract_max_price']['info'], $data['supplier_extract_max_price']['value']) + ->info($data['supplier_extract_max_price']['desc'])->min(0), + ]) + ]); + + return $build->toArray(); + } + + /** + * 获取缩略图配置 + * @return array + */ + public function getImageConfig() + { + return $this->getConfigAllField([ + 'image_watermark_status', 'thumb_big_width', 'thumb_big_height', 'thumb_mid_width', + 'thumb_mid_height', 'thumb_small_width', 'thumb_small_height', 'watermark_type', + 'watermark_text', 'watermark_text_angle', 'watermark_text_color', 'watermark_text_size', + 'watermark_position', 'watermark_image', 'watermark_opacity', 'watermark_rotate', + 'watermark_x', 'watermark_y', 'upload_type' + ]); + } + + /** + * 获取配置 + * @param string $key + * @param null $default + * @return mixed + */ + public function getConfig(string $key, $default = null) + { + return sys_config($key, $default); + } + + /** + * 获取用户基础配置 + * @param string $type + * @return mixed + */ + public function getUserConfig(string $type = 'basic') + { + switch ($type) { + case 'basic'://基础 + $data = $this->getConfigAllField(['h5_avatar', 'user_extend_info'], 0, 1); + if (!$data['user_extend_info']) {//没保存过,获取默认数据 + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $data['user_extend_info'] = $userServices->defaultExtendInfo; + } + break; + case 'register'://注册 + $data = $this->getConfigAllField(['store_user_mobile', 'routine_auth_type', 'store_user_agreement', 'newcomer_status', 'newcomer_limit_status', 'newcomer_limit_time', 'register_integral_status', 'register_give_integral', 'register_money_status', 'register_give_money', 'register_coupon_status', 'register_give_coupon', 'first_order_status', 'first_order_discount', 'first_order_discount_limit', 'register_price_status'], 0, 1); + /** @var StoreNewcomerServices $newcomerServices */ + $newcomerServices = app()->make(StoreNewcomerServices::class); + $data['product'] = $newcomerServices->getCustomerProduct(); + /** @var CacheServices $cache */ + $cache = app()->make(CacheServices::class); + $data['newcomer_agreement'] = $cache->getDbCache('newcomer_agreement', ''); + $ids = $data['register_give_coupon'] ?? []; + $data['register_give_coupon'] = []; + if ($data['register_coupon_status'] && $ids) { + /** @var StoreCouponIssueServices $couponServices */ + $couponServices = app()->make(StoreCouponIssueServices::class); + $coupon = $couponServices->getList(['id' => $ids]); + $data['register_give_coupon'] = $coupon; + } + $data['register_notice'] = '多端(公众号、小程序)账号统一,可以开启强制手机号登录实现,也可以绑定微信开放平台实现:https://open.weixin.qq.com'; + break; + case 'level'://等级 + $data = $this->getConfigAllField(['member_func_status', 'member_price_status', 'order_give_exp', 'sign_give_exp', 'invite_user_exp', 'level_activate_status', 'level_activate_status', 'level_integral_status', 'level_give_integral', 'level_money_status', 'level_give_money', 'level_coupon_status', 'level_give_coupon', 'level_extend_info'], 0, 1); + $ids = $data['level_give_coupon'] ?? []; + $data['level_give_coupon'] = []; + if ($data['level_coupon_status'] && $ids) { + /** @var StoreCouponIssueServices $couponServices */ + $couponServices = app()->make(StoreCouponIssueServices::class); + $coupon = $couponServices->getList(['id' => $ids]); + $data['level_give_coupon'] = $coupon; + } + break; + case 'svip'://付费会员 + $data = $this->getConfigAllField(['member_card_status', 'svip_price_status'], 0, 1); + break; + default: + throw new AdminException('类型错误'); + break; + } + return $data; + } + + /** + * 保存用户设置 + * @param string $type + * @param array $data + * @return bool + */ + public function saveUserConfig(string $type, array $data) + { + switch ($type) { + case 'basic'://基础 + break; + case 'register'://注册 + $products = $data['product'] ?? []; + //新人专享商品 + /** @var StoreNewcomerServices $newcomerServices */ + $newcomerServices = app()->make(StoreNewcomerServices::class); + $newcomerServices->saveNewcomer($products); + //新人专享规则说明 + /** @var CacheServices $cache */ + $cache = app()->make(CacheServices::class); + $content = $data['newcomer_agreement'] ?? ''; + $cache->setDbCache('newcomer_agreement', $content); + + unset($data['product'], $data['newcomer_agreement']); + break; + case 'level'://等级 + break; + case 'svip'://付费会员 + break; + } + foreach ($data as $k => $v) { + $config_one = $this->dao->getOne(['menu_name' => $k]); + if ($config_one) { + $config_one['value'] = $v; + $this->valiDateValue($config_one); + $this->dao->update($k, ['value' => json_encode($v)], 'menu_name'); + } + } + \crmeb\services\SystemConfigService::clear(); + return true; + } +} diff --git a/app/services/system/config/SystemGroupDataServices.php b/app/services/system/config/SystemGroupDataServices.php new file mode 100644 index 0000000..eab0af2 --- /dev/null +++ b/app/services/system/config/SystemGroupDataServices.php @@ -0,0 +1,379 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\system\config; + +use app\dao\system\config\SystemGroupDataDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use crmeb\traits\ServicesTrait; + +/** + * 组合数据数据集 + * Class SystemGroupDataServices + * @package app\services\system\config + * @mixin SystemGroupDataDao + */ +class SystemGroupDataServices extends BaseServices +{ + + use ServicesTrait; + + /** + * SystemGroupDataServices constructor. + * @param SystemGroupDataDao $dao + */ + public function __construct(SystemGroupDataDao $dao) + { + $this->dao = $dao; + } + + /** + * @param int $id + * @return mixed|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/3 + */ + public function cacheInfoValue(int $id) + { + $groupData = $this->cacheRemember($id, function () use ($id) { + $res = $this->dao->get($id); + return $res ? $res->toArray() : null; + }); + + return $groupData['value'] ?? ''; + } + + /** + * 获取某个配置下的数据从新组合成新得数据返回 + * @param string $configName + * @param int $limit + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getConfigNameValue(string $configName, int $limit = 0) + { + /** @var SystemGroupServices $systemGroupServices */ + $systemGroupServices = app()->make(SystemGroupServices::class); + $value = $this->dao->getGroupDate((int)$systemGroupServices->getConfigNameId($configName), $limit); + $data = []; + foreach ($value as $key => $item) { + $data[$key]["id"] = $item["id"]; + if (isset($item['status'])) $data[$key]["status"] = $item["status"]; + $fields = json_decode($item["value"], true) ?: []; + foreach ($fields as $index => $field) { + if ($field['type'] == 'upload') { + $data[$key][$index] = set_file_url($field['value']); + } else { + $data[$key][$index] = $field["value"]; + } + } + } + return $data; + } + + /** + * 获取组合数据列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGroupDataList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getGroupDataList($where, $page, $limit); + $count = $this->dao->count($where); + $type = ''; + $gid = (int)$where['gid']; + /** @var SystemGroupServices $services */ + $services = app()->make(SystemGroupServices::class); + $group = $services->getOne(['id' => $gid], 'id,config_name,fields'); + + $header = json_decode($group['fields'], true) ?? []; + $title = []; + $param = []; + foreach ($header as $item) { + if ($group['config_name'] == 'order_details_images' && $item['title'] == 'order_status') { + $status = str_replace("\r\n", "\n", $item["param"]);//防止不兼容 + $status = explode("\n", $status); + if (is_array($status) && !empty($status)) { + foreach ($status as $index => $v) { + $vl = explode('=>', $v); + if (isset($vl[0]) && isset($vl[1])) { + $param[$vl[0]] = $vl[1]; + } + } + } + } + if ($item['type'] == 'upload' || $item['type'] == 'uploads') { + $title[$item['title']] = []; + $type = $item['title']; + } else { + $title[$item['title']] = ''; + } + } + foreach ($list as $key => $value) { + $list[$key] = array_merge($value, $title); + $infos = json_decode($value['value'], true) ?: []; + foreach ($infos as $index => $info) { + if ($group['config_name'] == 'order_details_images' && $index == 'order_status') { + $list[$key][$index] = ($param[$info['value']] ?? '') . '/' . $info['value']; + } else { + if ($info['type'] == 'upload') { + $list[$key][$index] = [set_file_url($info['value'])]; + } elseif ($info['type'] == 'checkbox') { + $list[$key][$index] = implode(",", $info["value"]); + } else { + $list[$key][$index] = $info['value']; + } + } + } + unset($list[$key]['value']); + } + return compact('list', 'count', 'type', 'gid'); + } + + /** + * 根据gid判断出是否能再次添加组合数据 + * @param int $gid + * @param int $count + * @param string $key + * @return bool + */ + public function isGroupGidSave(int $gid, int $count, string $key): bool + { + /** @var SystemGroupServices $services */ + $services = app()->make(SystemGroupServices::class); + $configName = $services->value(['id' => $gid], 'config_name'); + if ($configName == $key) { + return $this->dao->count(['gid' => $gid]) >= $count; + } else { + return false; + } + } + + /** + * 创建表单 + * @param int $gid + * @param array $groupData + * @return mixed + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createGroupForm(int $gid, array $groupData = []) + { + $groupDataValue = isset($groupData["value"]) ? json_decode($groupData["value"], true) : []; + /** @var SystemGroupServices $services */ + $services = app()->make(SystemGroupServices::class); + $configName = $services->value(['id' => $gid], 'config_name'); + $fields = $services->getValueFields($gid); + $f[] = Form::hidden('gid', $gid); + foreach ($fields as $key => $value) { + $info = []; + if (isset($value["param"])) { + $value["param"] = str_replace("\r\n", "\n", $value["param"]);//防止不兼容 + $params = explode("\n", $value["param"]); + if (is_array($params) && !empty($params)) { + foreach ($params as $index => $v) { + $vl = explode('=>', $v); + if (isset($vl[0]) && isset($vl[1])) { + $info[$index]["value"] = $vl[0]; + $info[$index]["label"] = $vl[1]; + } + } + } + } + $fvalue = isset($groupDataValue[$value['title']]['value']) ? $groupDataValue[$value['title']]['value'] : ''; + switch ($value["type"]) { + case 'input': + if (($gid == 55 && $value['title'] == 'sign_num') || in_array($configName, ['user_recharge_quota'])) { + if ('user_recharge_quota' === $configName) { + $f[] = Form::number($value["title"], $value["name"], $fvalue)->max(999999)->required(); + } else { + $f[] = Form::number($value["title"], $value["name"], (int)$fvalue)->precision(0); + } + } else { + $f[] = Form::input($value["title"], $value["name"], $fvalue); + } + break; + case 'textarea': + $f[] = Form::input($value["title"], $value["name"], $fvalue)->type('textarea')->placeholder($value['param']); + break; + case 'radio': + $f[] = Form::radio($value["title"], $value["name"], $fvalue ?: (int)$info[0]["value"] ?? '')->options($info); + break; + case 'checkbox': + $f[] = Form::checkbox($value["title"], $value["name"], $fvalue ?: $info[0] ?? '')->options($info); + break; + case 'select': + $f[] = Form::select($value["title"], $value["name"], $fvalue !== '' ? $fvalue : $info[0] ?? '')->options($info)->multiple(false); + break; + case 'upload': + if (!empty($fvalue)) { + $image = is_string($fvalue) ? $fvalue : $fvalue[0]; + } else { + $image = ''; + } + $f[] = Form::frameImage($value["title"], $value["name"], $this->url(config('admin.admin_prefix') . '/widget.images/index', ['fodder' => $value["title"], 'big' => 1], true), $image)->icon('ios-image')->width('960px')->height('505px')->modal(['footer-hide' => true]); + break; + case 'uploads': + if ($fvalue) { + if (is_string($fvalue)) $fvalue = [$fvalue]; + $images = !empty($fvalue) ? $fvalue : []; + } else { + $images = []; + } + $f[] = Form::frameImages($value["title"], $value["name"], $this->url(config('admin.admin_prefix') . '/widget.images/index', ['fodder' => $value["title"], 'big' => 1, 'type' => 'many', 'maxLength' => 5], true), $images)->maxLength(5)->icon('ios-images')->width('960px')->height('505px')->modal(['footer-hide' => true])->spin(0); + break; + default: + $f[] = Form::input($value["title"], $value["name"], $fvalue); + break; + + } + } + $f[] = Form::number('sort', '排序', (int)($groupData["sort"] ?? 1))->min(0); + $f[] = Form::radio('status', '状态', (int)($groupData["status"] ?? 1))->options([['value' => 1, 'label' => '显示'], ['value' => 0, 'label' => '隐藏']]); + return $f; + } + + /** + * 获取添加组合数据表单 + * @param int $gid + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function createForm(int $gid) + { + return create_form('添加数据', $this->createGroupForm($gid), $this->url('/setting/group_data')); + } + + /** + * 获取修改组合数据表单 + * @param int $gid + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function updateForm(int $gid, int $id) + { + $groupData = $this->dao->get($id); + if (!$groupData) { + throw new AdminException('修改失败未查到数据!'); + } + return create_form('编辑数据', $this->createGroupForm($gid, $groupData->toArray()), $this->url('/setting/group_data/' . $id), 'PUT'); + } + + /** + * 根据id获取当前记录中的数据 + * @param $id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getDateValue($id) + { + $value = $this->dao->get($id); + $data = []; + if ($value) { + $data["id"] = $value["id"]; + $data["status"] = $value["status"]; + $fields = json_decode($value["value"], true); + foreach ($fields as $index => $field) { + $data[$index] = $field["value"]; + } + } + return $data; + } + + /** + * 根据id获取数据 + * @param array $ids + * @param string $field + */ + public function getGroupDataColumn(array $ids) + { + $systemGroup = []; + if (!empty($ids)) { + $systemGroupData = $this->dao->idByGroupList($ids); + if (!empty($systemGroupData)) + $systemGroup = array_combine(array_column($systemGroupData, 'id'), $systemGroupData); + } + return $systemGroup; + } + + /** + * 添加数据配置 + * @param $data + */ + public function saveAll(array $data) + { + return $this->dao->saveAll($data); + } + + /** + * 根据gid删除数据 + * @param int $gid + * @return mixed + */ + public function delGroupDate(int $gid) + { + return $this->dao->delGroupDate($gid); + } + + public function saveAllData(array $params, string $config_name) + { + /** @var SystemGroupServices $systemGroupServices */ + $systemGroupServices = app()->make(SystemGroupServices::class); + $gid = $systemGroupServices->value(['config_name' => $config_name], 'id'); + $group = $systemGroupServices->getOne(['id' => $gid], 'id,config_name,fields'); + $fields = json_decode($group['fields'], true) ?? []; + $this->transaction(function () use ($gid, $params, $fields) { + $this->delGroupDate($gid); + $data = []; + $sort = count($params); + foreach ($params as $k => $v) { + $value = []; + foreach ($v as $key => $param) { + foreach ($fields as $index => $field) { + if ($key == $field["title"]) { + if ($param == "" && $key != 'link') {//跳转链接可以为空 + throw new AdminException($field["name"] . "不能为空!"); + } else { + $value[$key]["type"] = $field["type"]; + $value[$key]["value"] = $param; + } + } + } + } + $data[$k] = [ + "gid" => $gid, + "add_time" => time(), + "value" => json_encode($value), + "sort" => $sort, + "status" => $v["status"] ?? 1 + ]; + $sort--; + } + $this->dao->saveAll($data); + }); + \crmeb\services\CacheService::clear(); + return true; + } +} diff --git a/app/services/system/config/SystemGroupServices.php b/app/services/system/config/SystemGroupServices.php new file mode 100644 index 0000000..fdeefc0 --- /dev/null +++ b/app/services/system/config/SystemGroupServices.php @@ -0,0 +1,95 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\system\config; + +use app\dao\system\config\SystemGroupDao; +use app\services\BaseServices; + +/** + * 组合数据 + * Class SystemGroupServices + * @package app\services\system\config + * @mixin SystemGroupDao + */ +class SystemGroupServices extends BaseServices +{ + + /** + * SystemGroupServices constructor. + * @param SystemGroupDao $dao + */ + public function __construct(SystemGroupDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取组合数据列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGroupList(array $where, array $field = ['*']) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getGroupList($where, $field, $page, $limit); + $count = $this->dao->count($where); + foreach ($list as $key => $value) { + if (isset($value['fields'])) { + $list[$key]['typelist'] = $value['fields']; + unset($list[$key]['fields']); + } + } + return compact('list', 'count'); + } + + /** + * 获取组合数据tab下的header头部 + * @param int $id + * @return array + */ + public function getGroupDataTabHeader(int $id) + { + $data = $this->getValueFields($id); + $header = []; + foreach ($data as $key => $item) { + if ($item['type'] == 'upload' || $item['type'] == 'uploads') { + $header[$key]['key'] = $item['title']; + $header[$key]['minWidth'] = 60; + $header[$key]['type'] = 'img'; + } elseif ($item['title'] == 'url' || $item['title'] == 'wap_url' || $item['title'] == 'link' || $item['title'] == 'wap_link') { + $header[$key]['key'] = $item['title']; + $header[$key]['minWidth'] = 200; + } else { + $header[$key]['key'] = $item['title']; + $header[$key]['minWidth'] = 100; + } + $header[$key]['title'] = $item['name']; + } + array_unshift($header, ['key' => 'id', 'title' => '编号', 'width' => 55]); + array_push($header, ['slot' => 'status', 'title' => '是否可用', 'minWidth' => 80], ['key' => 'sort', 'title' => '排序', 'minWidth' => 80], ['slot' => 'action', 'fixed' => 'right', 'title' => '操作', 'minWidth' => 120]); + return compact('header'); + } + + /** + * 获取组合数据fields字段 + * @param int $id + * @return array|mixed + */ + public function getValueFields(int $id) + { + return json_decode($this->dao->value(['id' => $id], 'fields'), true) ?: []; + } + +} diff --git a/app/services/system/form/SystemFormServices.php b/app/services/system/form/SystemFormServices.php new file mode 100644 index 0000000..bd73e55 --- /dev/null +++ b/app/services/system/form/SystemFormServices.php @@ -0,0 +1,104 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\system\form; + +use app\dao\system\form\SystemFormDao; +use app\services\BaseServices; +use app\dao\diy\DiyDao; + + +/** + * 系统表单 + * Class SystemFormServices + * @package app\services\diy + * @mixin DiyDao + */ +class SystemFormServices extends BaseServices +{ + + /** + * form类型 + * @var string[] + */ + protected $formType = [ + 'checkboxs' => '多选框', + 'citys' => '城市', + 'dates' => '日期', + 'dateranges' => '日期范围', + 'radios' => '单选框', + 'selects' => '下拉框', + 'texts' => '文本框', + 'times' => '时间', + 'timeranges' => '时间范围', + 'uploadPicture' => '图片' + ]; + + /** + * DiyServices constructor. + * @param SystemFormDao $dao + */ + public function __construct(SystemFormDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取系统表单 + * @param array $where + * @param array $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFormList(array $where = [], array $field = ['*']) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getFormList($where, $field, $page, $limit); + $count = $this->dao->count($where + ['is_del' => 0]); + return compact('list', 'count'); + } + + /** + * 处理表单数据 + * @param array $form + * @return array + */ + public function handleForm(array $form) + { + $info = []; + if ($form) { + $infoOne = []; + foreach ($form as $item) { + $infoOne['id'] = $item['id'] ?? ''; + $infoOne['type'] = $item['name'] ?? ''; + $infoOne['name'] = $this->formType[$infoOne['type']] ?? ''; + $infoOne['title'] = $item['titleConfig']['value'] ?? ''; + $infoOne['tip'] = $item['tipConfig']['value'] ?? ''; + $infoOne['list'] = []; + $infoOne['require'] = $item['titleShow']['val'] ?? false; + switch ($item['name']) { + case 'checkboxs': + case 'radios': + case 'selects': + $infoOne['list'] = $item['wordsConfig']['list'] ?? []; + break; + } + $infoOne['value'] = $item['value'] ?? ''; + $info[] = $infoOne; + } + } + return $info; + } + +} diff --git a/app/services/system/log/SystemLogServices.php b/app/services/system/log/SystemLogServices.php new file mode 100644 index 0000000..01fa418 --- /dev/null +++ b/app/services/system/log/SystemLogServices.php @@ -0,0 +1,94 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\system\log; + + +use app\common\dao\system\admin\LogDao; +use app\services\BaseServices; +use app\services\system\admin\SystemAdminServices; +use app\services\system\SystemMenusServices; + +/** + * 系统日志 + * Class SystemLogServices + * @package app\services\system\log + * @mixin LogDao + */ +class SystemLogServices extends BaseServices +{ + + /** + * 构造方法 + * SystemLogServices constructor. + * @param LogDao $dao + */ + public function __construct(LogDao $dao) + { + $this->dao = $dao; + } + + /** + * 记录访问日志 + * @param int $adminId + * @param string $adminName + * @param string $module + * @param string $rule + * @param string $ip + * @param string $type + * @return bool + */ + public function recordAdminLog(int $adminId, string $adminName, string $module, string $rule, string $ip, string $type) + { + /** @var SystemMenusServices $service */ + $service = app()->make(SystemMenusServices::class); + $data = [ + 'method' => $module, + 'add_time' => time(), + 'admin_name' => $adminName, + 'path' => $rule, + 'page' => $service->getVisitName($rule) ?: '未知', + 'ip' => $ip, + 'type' => $type + ]; + if ($type == 'store') { + $data['store_id'] = $adminId; + } else { + $data['admin_id'] = $adminId; + } + if ($this->dao->save($data)) { + return true; + } else { + return false; + } + } + + /** + * 获取系统日志列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLogList(array $where, int $level) + { + [$page, $limit] = $this->getPageValue(); + if (!$where['admin_id']) { + /** @var SystemAdminServices $service */ + $service = app()->make(SystemAdminServices::class); + $where['admin_id'] = $service->getAdminIds($level); + } + $list = $this->dao->getLogList($where, $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } +} diff --git a/app/services/user/LoginServices.php b/app/services/user/LoginServices.php new file mode 100644 index 0000000..ba4d94f --- /dev/null +++ b/app/services/user/LoginServices.php @@ -0,0 +1,395 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\jobs\notice\SmsJob; +use app\services\BaseServices; +use app\common\dao\user\UserDao; +use app\services\message\sms\SmsRecordServices; +use app\services\wechat\WechatUserServices; +use crmeb\services\CacheService; +use think\exception\ValidateException; +use think\facade\Config; + +/** + * + * Class LoginServices + * @package app\services\user + * @mixin UserDao + */ +class LoginServices extends BaseServices +{ + + /** + * LoginServices constructor. + * @param UserDao $dao + */ + public function __construct(UserDao $dao) + { + $this->dao = $dao; + } + + /** + * H5账号登陆 + * @param Request $request + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function login($account, $password, $spread_uid) + { + $user = $this->dao->getOne(['phone' => $account], 'uid,pwd,status'); + if ($user) { + if ($user->pwd !== md5((string)$password)) + throw new ValidateException('账号或密码错误'); + if ($user->pwd === md5('123456')) + throw new ValidateException('请修改您的初始密码,再尝试登录!'); + } else { + throw new ValidateException('账号或密码错误'); + } + if (!$user['status']) + throw new ValidateException('已被禁止,请联系管理员'); + + //更新用户信息 + $token = $this->createToken((int)$user['uid'], 'api', $user->pwd); + if ($token) { + // 用户登录成功事件 + $this->updateUserInfo(['spread_uid' => $spread_uid], $user); + return ['token' => $token['token'], 'expires_time' => $token['params']['exp']]; + } else + throw new ValidateException('登录失败'); + } + + /** + * 更新用户信息 + * @param $user + * @param $uid + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function updateUserInfo($user, $userInfo) + { + $data = []; + if (isset($userInfo['nickname']) && $userInfo['nickname']) { + $data['nickname'] = !isset($user['nickname']) || !$user['nickname'] ? $userInfo->nickname : $user['nickname']; + } + if (isset($userInfo['avatar']) && $userInfo['avatar']) { + $data['avatar'] = !isset($user['headimgurl']) || !$user['headimgurl'] ? $userInfo->avatar : $user['headimgurl']; + } + if (isset($userInfo['phone']) && $userInfo['phone']) { + $data['phone'] = !isset($user['phone']) || !$user['phone'] ? $userInfo->phone : $user['phone']; + } + $data['last_time'] = time(); + $data['last_ip'] = app()->request->ip(); + //永久绑定 + $store_brokergae_binding_status = sys_config('store_brokerage_binding_status', 1); + $spread_uid = isset($user['code']) && $user['code'] && $user['code'] != $userInfo->uid ? $user['code'] : ($user['spread_uid'] ?? 0); + if ($userInfo->spread_uid && $store_brokergae_binding_status == 1) { + $data['login_type'] = $user['login_type'] ?? $userInfo->login_type; + } else { + //绑定分销关系 = 所有用户 + if (sys_config('brokerage_bindind', 1) == 1) { + //分销绑定类型为时间段且过期 ||临时 + $store_brokerage_binding_time = sys_config('store_brokerage_binding_time', 30); + if (!$userInfo['spread_uid'] || $store_brokergae_binding_status == 3 || ($store_brokergae_binding_status == 2 && ($userInfo['spread_time'] + $store_brokerage_binding_time * 24 * 3600) < time())) { + $spreadUid = $spread_uid; + if ($spreadUid && $userInfo->uid == $this->dao->value(['uid' => $spreadUid], 'spread_uid')) { + $spreadUid = 0; + } + if ($spreadUid && $this->dao->get((int)$spreadUid)) { + $data['spread_uid'] = $spreadUid; + $data['spread_time'] = time(); + } + } + } + } + if (!$this->dao->update($userInfo['uid'], $data, 'uid')) { + throw new ValidateException('修改信息失败'); + } + if (isset($data['spread_uid']) && $data['spread_uid']) { + event('user.register', [$this->dao->get((int)$userInfo['uid']), false, $spread_uid]); + //推送消息 +// event('notice.notice', [['spreadUid' => $spreadUid, 'user_type' => $userInfo['user_type'], 'nickname' => $userInfo['nickname']], 'bind_spread_uid']); + } + + return true; + } + + /** + * 发送验证码 + * @param $phone + * @param $type + * @param $time + * @param $ip + * @return int + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function verify($phone, $type, $time, $ip) + { + if ($this->dao->getOne(['account' => $phone]) && $type == 'register') { + throw new ValidateException('手机号已注册'); + } + $default = Config::get('sms.default', 'yunxin'); + $defaultMaxPhoneCount = Config::get('sms.maxPhoneCount', 10); + $defaultMaxIpCount = Config::get('sms.maxIpCount', 50); + $maxPhoneCount = Config::get('sms.stores.' . $default . '.maxPhoneCount', $defaultMaxPhoneCount); + $maxIpCount = Config::get('sms.stores.' . $default . '.maxIpCount', $defaultMaxIpCount); + /** @var SmsRecordServices $smsRecord */ + $smsRecord = app()->make(SmsRecordServices::class); + if ($smsRecord->count(['phone' => $phone, 'add_ip' => $ip, 'time' => 'today']) >= $maxPhoneCount) { + throw new ValidateException('您今日发送得短信次数已经达到上限'); + } + if ($smsRecord->count(['add_ip' => $ip, 'time' => 'today']) >= $maxIpCount) { + throw new ValidateException('此IP今日发送次数已经达到上限'); + } + if (CacheService::get('code_' . $phone)) + throw new ValidateException($time . '分钟内有效'); + mt_srand(); + $code = rand(100000, 999999); + $data['code'] = $code; + $data['time'] = $time; + $res = SmsJob::dispatch([$phone, $data, 'VERIFICATION_CODE_TIME']); + if (!$res) + throw new ValidateException('短信平台验证码发送失败'); + return $code; + } + + /** + * H5用户注册 + * @param $account + * @param $password + * @param $spread_uid + * @return User|\think\Model + */ + public function register($account, $password, $spread_uid, $user_type = 'h5') + { + if ($this->dao->getOne(['phone' => $account])) { + throw new ValidateException('用户已存在,请去修改密码'); + } + $phone = $account; + $data['account'] = $account; + $data['pwd'] = md5((string)$password); + $data['phone'] = $phone; + if ($spread_uid && $this->dao->get((int)$spread_uid)) { + $data['spread_uid'] = $spread_uid; + $data['spread_time'] = time(); + } + $data['real_name'] = ''; + $data['birthday'] = 0; + $data['card_id'] = ''; + $data['mark'] = ''; + $data['addres'] = ''; + $data['user_type'] = $user_type; + $data['add_time'] = time(); + $data['add_ip'] = app('request')->ip(); + $data['last_time'] = time(); + $data['last_ip'] = app('request')->ip(); + $data['nickname'] = substr(md5($account . time()), 0, 12); + $data['avatar'] = $data['headimgurl'] = sys_config('h5_avatar'); + $data['city'] = ''; + $data['language'] = ''; + $data['province'] = ''; + $data['country'] = ''; + $data['status'] = 1; + if (!$re = $this->dao->save($data)) { + throw new ValidateException('注册失败'); + } else { + //用户注册成功事件 + event('user.register', [$this->dao->get((int)$re->uid), true, $spread_uid]); + //推送消息 +// event('notice.notice', [['spreadUid' => $spread, 'user_type' => $user_type, 'nickname' => $data['nickname']], 'bind_spread_uid']); + return $re; + } + } + + /** + * 重置密码 + * @param $account + * @param $password + */ + public function reset($account, $password) + { + $user = $this->dao->getOne(['phone' => $account]); + if (!$user) { + throw new ValidateException('用户不存在'); + } + if (!$this->dao->update($user['uid'], ['pwd' => md5((string)$password)], 'uid')) { + throw new ValidateException('修改密码失败'); + } + return true; + } + + /** + * 手机号登录 + * @param $phone + * @param $spread_uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function mobile($phone, $spread_uid, $user_type = 'h5') + { + //数据库查询 + $user = $this->dao->getOne(['phone' => $phone]); + if (!$user) { + $user = $this->register($phone, '123456', $spread_uid, $user_type); + if (!$user) { + throw new ValidateException('用户登录失败,无法生成新用户,请稍后再试!'); + } + } + + if (!$user->status) + throw new ValidateException('已被禁止,请联系管理员'); + + // 设置推广关系 + $this->updateUserInfo(['spread_uid' => $spread_uid], $user); + + $token = $this->createToken((int)$user['uid'], 'api', $user->pwd); + if ($token) { + return ['token' => $token['token'], 'expires_time' => $token['params']['exp']]; + } else { + throw new ValidateException('登录失败'); + } + } + + /** + * 切换登录 + * @param $user + * @param $from + */ + public function switchAccount($user, $from) + { + if ($from === 'h5') { + $where = [['phone', '=', $user['phone']], ['user_type', '<>', 'h5']]; + $login_type = 'wechat'; + } else { + //数据库查询 + $where = [['account|phone', '=', $user['phone']], ['user_type', '=', 'h5']]; + $login_type = 'h5'; + } + $switch_user = $this->dao->getOne($where); + if (!$switch_user) { + return app('json')->fail('用户不存在,无法切换'); + } + if (!$switch_user->status) { + return app('json')->fail('已被禁止,请联系管理员'); + } + $edit_data = ['login_type' => $login_type]; + if (!$this->dao->update($switch_user['uid'], $edit_data, 'uid')) { + throw new ValidateException('修改新用户登录类型出错'); + } + $token = $this->createToken((int)$switch_user['uid'], 'api', $switch_user['pwd']); + if ($token) { + return ['token' => $token['token'], 'expires_time' => $token['params']['exp']]; + } else { + throw new ValidateException('切换失败'); + } + } + + /** + * 绑定手机号(静默还没写入用户信息) + * @param $user + * @param $phone + * @param $step + * @return mixed + */ + public function bindind_phone($phone, $key = '') + { + if (!$key) { + throw new ValidateException('请刷新页面或者重新授权'); + } + [$openid, $wechatInfo, $spread_uid, $login_type, $userType] = $createData = CacheService::getTokenBucket($key); + if (!$createData) { + throw new ValidateException('请刷新页面或者重新授权'); + } + $wechatInfo['phone'] = $phone; + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + //更新用户信息 + $user = $wechatUser->wechatOauthAfter([$openid, $wechatInfo, $spread_uid, $login_type, $userType]); + $token = $this->createToken((int)$user['uid'], $userType, $user['pwd']); + if ($token) { + return [ + 'token' => $token['token'], + 'userInfo' => $user, + 'expires_time' => $token['params']['exp'], + ]; + } else + return app('json')->fail('获取用户访问token失败!'); + } + + /** + * 用户绑定手机号 + * @param $user + * @param $phone + * @param $step + * @return mixed + */ + public function userBindindPhone(int $uid, $phone, $step) + { + $userInfo = $this->dao->get($uid); + if (!$userInfo) { + throw new ValidateException('用户不存在'); + } + if ($this->dao->getOne([['phone', '=', $phone], ['user_type', '<>', 'h5']])) { + throw new ValidateException('此手机已经绑定,无法多次绑定!'); + } + if ($userInfo->phone) { + throw new ValidateException('您的账号已经绑定过手机号码!'); + } + $data = []; + if ($this->dao->getOne(['account' => $phone, 'phone' => $phone, 'user_type' => 'h5'])) { + if (!$step) return ['msg' => 'H5已有账号是否绑定此账号上', 'data' => ['is_bind' => 1]]; + } else { + $data['account'] = $phone; + } + $data['phone'] = $phone; + if ($this->dao->update($userInfo['uid'], $data, 'uid') || $userInfo->phone == $phone) + return ['msg' => '绑定成功', 'data' => []]; + else + throw new ValidateException('绑定失败'); + } + + /** + * 用户绑定手机号 + * @param $user + * @param $phone + * @param $step + * @return mixed + */ + public function updateBindindPhone(int $uid, $phone) + { + $userInfo = $this->dao->get($uid); + if (!$userInfo) { + throw new ValidateException('用户不存在'); + } + if ($userInfo->phone == $phone) { + throw new ValidateException('新手机号和原手机号相同,无需修改'); + } + if ($this->dao->getOne([['phone', '=', $phone]])) { + throw new ValidateException('此手机已经注册'); + } + $data = []; + $data['phone'] = $phone; + if ($this->dao->update($userInfo['uid'], $data, 'uid')) + return ['msg' => '修改成功', 'data' => []]; + else + throw new ValidateException('修改失败'); + } +} diff --git a/app/services/user/UserAddressServices.php b/app/services/user/UserAddressServices.php new file mode 100644 index 0000000..136cde5 --- /dev/null +++ b/app/services/user/UserAddressServices.php @@ -0,0 +1,298 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\validate\api\user\AddressValidate; +use app\services\BaseServices; +use app\dao\user\UserAddressDao; +use app\services\other\SystemCityServices; +use crmeb\exceptions\AdminException; +use think\db\exception\DataNotFoundException; +use think\db\exception\DbException; +use think\db\exception\ModelNotFoundException; +use think\Exception; +use think\exception\ValidateException; +use think\Model; + +/** + * + * Class UserAddressServices + * @package app\services\user + * @mixin UserAddressDao + */ +class UserAddressServices extends BaseServices +{ + + /** + * UserAddressServices constructor. + * @param UserAddressDao $dao + */ + public function __construct(UserAddressDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取单个地址 + * @param $id + * @param $field + * @return array + */ + public function getAddress($id, $field = []) + { + return $this->dao->get($id, $field); + } + + /** + * 获取用户缓存地址 + * @param int $uid + * @param int $addressId + * @return bool|mixed|null + */ + public function getAdderssCache(int $id, int $expire = 60) + { + return $this->dao->cacheTag()->remember('user_adderss_cache_' . $id, function () use ($id) { + $res = $this->dao->getOne(['id' => $id, 'is_del' => 0]); + if ($res) { + return $res->toArray(); + } else { + return []; + } + }, $expire); + } + + /** + * 获取所有地址 + * @param array $where + * @param string $field + * @return array + */ + public function getAddressList(array $where, string $field = '*'): array + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $field, $page, $limit); + $count = $this->getAddresCount($where); + return compact('list', 'count'); + } + + /** + * 获取某个用户的所有地址 + * @param int $uid + * @param string $field + * @return array + */ + public function getUserAddressList(int $uid, string $field = '*'): array + { + [$page, $limit] = $this->getPageValue(); + $where = ['uid' => $uid]; + $where['is_del'] = 0; + return $this->dao->getList($where, $field, $page, $limit); + } + + /** + * @param int $uid + * @param int $expire + * @return mixed + * @throws \throwable + */ + public function getUserDefaultAddressCache(int $uid, int $expire = 60) + { + return $this->dao->cacheTag()->remember('user_adderss_cache_user_' . $uid, function () use ($uid) { + $res = $this->dao->getOne(['uid' => $uid, 'is_default' => 1, 'is_del' => 0]); + if ($res) { + return $res->toArray(); + } else { + return []; + } + }, $expire); + } + + /** + * @param int $uid + * @param string $field + * @return array|Model|null + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function getUserDefaultAddress(int $uid, string $field = '*') + { + return $this->dao->getOne(['uid' => $uid, 'is_default' => 1, 'is_del' => 0], $field); + } + + /** + * 获取条数 + * @param array $where + * @return int + */ + public function getAddresCount(array $where): int + { + return $this->dao->count($where); + } + + /** + * 添加地址 + * @param array $data + * @return bool + */ + public function create(array $data) + { + if (!$this->dao->save($data)) + throw new AdminException('写入失败'); + return true; + } + + /** + * 修改地址 + * @param $id + * @param $data + * @return bool + */ + public function updateAddress(int $id, array $data) + { + if (!$this->dao->update($id, $data)) + throw new AdminException('修改失败'); + return true; + } + + /** + * 设置默认定制 + * @param int $uid + * @param int $id + * @return bool + */ + public function setDefault(int $uid, int $id) + { + if (!$this->dao->be($id)) { + throw new ValidateException('地址不存在'); + } + if (!$this->dao->update($uid, ['is_default' => 0], 'uid')) + throw new Exception('取消原来默认地址'); + if (!$this->dao->update($id, ['is_default' => 1])) + throw new Exception('设置默认地址失败'); + return true; + } + + /** + * 获取单个地址 + * @param int $id + * @return mixed + */ + public function address(int $id) + { + $addressInfo = $this->getAdderssCache($id); + if (!$addressInfo || (isset($addressInfo['is_del']) && $addressInfo['is_del'] == 1)) { + throw new ValidateException('数据不存在'); + } + return $addressInfo; + } + + /** + * 添加|修改地址 + * @param int $uid + * @param array $addressInfo + * @return mixed + */ + public function editAddress(int $uid, array $addressInfo) + { + if ($addressInfo['type'] == 1 && !$addressInfo['id']) { + $city = $addressInfo['address']['city']; + /** @var SystemCityServices $systemCity */ + $systemCity = app()->make(SystemCityServices::class); + $cityInfo = $systemCity->getOne([['name', '=', $city], ['parent_id', '<>', 0]]); + if ($cityInfo && $cityInfo['city_id']) { + $addressInfo['address']['city_id'] = $cityInfo['city_id']; + } else { + $cityInfo = $systemCity->getOne([['name', 'like', "%$city%"], ['parent_id', '<>', 0]]); + if (!$cityInfo) { + throw new ValidateException('收货地址格式错误!修改后请重新导入!'); + } + $addressInfo['address']['city_id'] = $cityInfo['city_id']; + } + } + if (!isset($addressInfo['address']['city_id']) || $addressInfo['address']['city_id'] == 0) throw new ValidateException('添加收货地址失败!'); + $addressInfo['province'] = $addressInfo['address']['province'] ?? ''; + $addressInfo['city'] = $addressInfo['address']['city'] ?? ''; + $addressInfo['city_id'] = $addressInfo['address']['city_id'] ?? 0; + $addressInfo['district'] = $addressInfo['address']['district'] ?? ''; + $addressInfo['street'] = $addressInfo['address']['street'] ?? ''; + $addressInfo['is_default'] = (int)$addressInfo['is_default'] == true ? 1 : 0; + $addressInfo['uid'] = $uid; + unset($addressInfo['address'], $addressInfo['type']); + //数据验证 + validate(AddressValidate::class)->check($addressInfo); + $address_check = []; + if ($addressInfo['id']) { + $address_check = $this->getAddress((int)$addressInfo['id']); + } + if ($address_check && $address_check['is_del'] == 0 && $address_check['uid'] = $uid) { + $id = (int)$addressInfo['id']; + unset($addressInfo['id']); + if (!$this->dao->update($id, $addressInfo, 'id')) { + throw new ValidateException('编辑收货地址失败'); + } + if ($addressInfo['is_default']) { + $this->setDefault($uid, $id); + } + + $this->dao->cacheTag()->clear(); + + return ['type' => 'edit', 'msg' => '编辑地址成功', 'data' => []]; + } else { + $addressInfo['add_time'] = time(); + if (!$address = $this->dao->save($addressInfo)) { + throw new ValidateException('添加收货地址失败'); + } + if ($addressInfo['is_default']) { + $this->setDefault($uid, (int)$address->id); + } + + $this->dao->cacheTag()->clear(); + + return ['type' => 'add', 'msg' => '添加地址成功', 'data' => ['id' => $address->id]]; + } + } + + /** + * 删除地址 + * @param int $uid + * @param int $id + * @return bool + */ + public function delAddress(int $uid, int $id) + { + $addressInfo = $this->getAddress($id); + if (!$addressInfo || $addressInfo['is_del'] == 1 || $addressInfo['uid'] != $uid) { + throw new ValidateException('数据不存在'); + } + if ($this->dao->update($id, ['is_del' => '1'], 'id')) { + $this->dao->cacheTag()->clear(); + return true; + } else + throw new ValidateException('删除地址失败!'); + } + + /** + * 设置默认用户地址 + * @param $id + * @param $uid + * @return bool + */ + public function setDefaultAddress(int $id, int $uid) + { + $res1 = $this->dao->update($uid, ['is_default' => 0], 'uid'); + $res2 = $this->dao->update(['id' => $id, 'uid' => $uid], ['is_default' => 1]); + $res = $res1 !== false && $res2 !== false; + return $res; + } +} diff --git a/app/services/user/UserBillServices.php b/app/services/user/UserBillServices.php new file mode 100644 index 0000000..d5335d5 --- /dev/null +++ b/app/services/user/UserBillServices.php @@ -0,0 +1,573 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\services\BaseServices; +use app\dao\user\UserBillDao; +use app\services\user\level\UserLevelServices; +use think\Exception; +use think\exception\ValidateException; +use crmeb\services\CacheService; +use think\facade\Log; + +/** + * + * Class UserBillServices + * @package app\services\user + * @mixin UserBillDao + */ +class UserBillServices extends BaseServices +{ + + /** + * 用户记录模板 + * @var array[] + */ + protected $incomeData = [ + 'pay_give_integral' => [ + 'title' => '购买商品赠送积分', + 'category' => 'integral', + 'type' => 'gain', + 'mark' => '购买商品赠送{%num%}积分', + 'status' => 1, + 'pm' => 1 + ], + 'order_give_integral' => [ + 'title' => '下单赠送积分', + 'category' => 'integral', + 'type' => 'gain', + 'mark' => '下单赠送{%num%}积分', + 'status' => 1, + 'pm' => 1 + ], + 'order_promotions_give_integral' => [ + 'title' => '下单优惠活动赠送积分', + 'category' => 'integral', + 'type' => 'gain', + 'mark' => '下单优惠活动赠送{%num%}积分', + 'status' => 1, + 'pm' => 1 + ], + 'order_give_exp' => [ + 'title' => '下单赠送经验', + 'category' => 'exp', + 'type' => 'gain', + 'mark' => '下单赠送{%num%}经验', + 'status' => 1, + 'pm' => 1 + ], + 'integral_refund' => [ + 'title' => '积分回退', + 'category' => 'integral', + 'type' => 'deduction', + 'mark' => '购买商品失败,回退{%num%}积分', + 'status' => 1, + 'pm' => 0 + ], + 'order_integral_refund' => [ + 'title' => '返还下单使用积分', + 'category' => 'integral', + 'type' => 'integral_refund', + 'mark' => '购买商品失败,回退{%num%}积分', + 'status' => 1, + 'pm' => 1 + ], + 'pay_product_integral_back' => [ + 'title' => '商品退积分', + 'category' => 'integral', + 'type' => 'pay_product_integral_back', + 'mark' => '订单返还{%num%}积分', + 'status' => 1, + 'pm' => 1 + ], + 'deduction' => [ + 'title' => '积分抵扣', + 'category' => 'integral', + 'type' => 'deduction', + 'mark' => '购买商品使用{%number%}积分抵扣{%deductionPrice%}元', + 'status' => 1, + 'pm' => 0 + ], + 'lottery_use_integral' => [ + 'title' => '参与抽奖使用积分', + 'category' => 'integral', + 'type' => 'lottery_use', + 'mark' => '参与抽奖使用{%num%}积分', + 'status' => 1, + 'pm' => 0 + ], + 'lottery_give_integral' => [ + 'title' => '抽奖中奖赠送积分', + 'category' => 'integral', + 'type' => 'lottery_add', + 'mark' => '抽奖中奖赠送{%num%}积分', + 'status' => 1, + 'pm' => 1 + ], + 'storeIntegral_use_integral' => [ + 'title' => '积分兑换商品', + 'category' => 'integral', + 'type' => 'storeIntegral_use', + 'mark' => '积分商城兑换商品使用{%num%}积分', + 'status' => 1, + 'pm' => 0 + ], + 'system_clear_integral' => [ + 'title' => '到期自动清除积分', + 'category' => 'integral', + 'type' => 'system_clear', + 'mark' => '到期自动清除{%num%}积分', + 'status' => 1, + 'pm' => 0 + ], + 'newcomer_give_integral' => [ + 'title' => '新人礼赠送积分', + 'category' => 'integral', + 'type' => 'newcomer_add', + 'mark' => '新人礼赠送{%num%}积分', + 'status' => 1, + 'pm' => 1 + ], + 'level_give_integral' => [ + 'title' => '会员卡激活赠送积分', + 'category' => 'integral', + 'type' => 'level_add', + 'mark' => '会员卡激活赠送{%num%}积分', + 'status' => 1, + 'pm' => 1 + ], + 'system_add_integral' => [ + 'title' => '系统增加积分', + 'category' => 'integral', + 'type' => 'system_add', + 'mark' => '系统增加了{%num%}积分', + 'status' => 1, + 'pm' => 1 + ], + 'system_sub_integral' => [ + 'title' => '系统减少积分', + 'category' => 'integral', + 'type' => 'system_sub', + 'mark' => '系统扣除了{%num%}积分', + 'status' => 1, + 'pm' => 0 + ], + ]; + + /** + * UserBillServices constructor. + * @param UserBillDao $dao + */ + public function __construct(UserBillDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取用户记录总和 + * @param $uid + * @param string $category + * @param array $type + * @return mixed + */ + public function getRecordCount(int $uid, $category = 'now_money', $type = [], $time = '', $pm = false) + { + + $where = []; + $where['uid'] = $uid; + $where['category'] = $category; + $where['status'] = 1; + + if (is_string($type) && strlen(trim($type))) { + $where['type'] = explode(',', $type); + } + if ($time) { + $where['time'] = $time; + } + + $where['pm'] = $pm ? 1 : 0; + + return $this->dao->getBillSumColumn($where); + } + + /** + * 获取积分列表 + * @param int $uid + * @param array $where_time + * @param string $field + * @return array + */ + public function getIntegralList(int $uid = 0, $where_time = [], string $field = '*') + { + [$page, $limit] = $this->getPageValue(); + $where = ['category' => 'integral']; + if ($uid) $where['uid'] = $uid; + if ($where_time) $where['add_time'] = $where_time; + $list = $this->dao->getList($where, $field, $page, $limit); + foreach ($list as &$item) { + $item['number'] = intval($item['number']); + $item['balance'] = intval($item['balance']); + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取签到列表 + * @param int $uid + * @param array $where_time + * @param string $field + * @return array + */ + public function getSignList(int $uid = 0, $where_time = [], string $field = '*') + { + [$page, $limit] = $this->getPageValue(); + $where = ['category' => 'integral', 'type' => 'sign']; + if ($uid) $where['uid'] = $uid; + if ($where_time) $where['add_time'] = $where_time; + $list = $this->dao->getList($where, $field, $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 经验总数 + * @param int $uid + * @param array $where_time + * @return float + */ + public function getExpSum(int $uid = 0, $where_time = []) + { + $where = ['category' => ['exp'], 'pm' => 1, 'status' => 1]; + if ($uid) $where['uid'] = $uid; + if ($where_time) $where['time'] = $where_time; + return $this->dao->getBillSum($where); + } + + /** + * 获取所有经验列表 + * @param int $uid + * @param array $where_time + * @param string $field + * @return array + */ + public function getExpList(int $uid = 0, $where_time = [], string $field = '*') + { + [$page, $limit] = $this->getPageValue(); + $where = ['category' => ['exp']]; + $where['status'] = 1; + if ($uid) $where['uid'] = $uid; + if ($where_time) $where['time'] = $where_time; + $list = $this->dao->getList($where, $field, $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 记录积分变化 + * @param int $uid + * @param string $type + * @param array $data + * @param int $pm + * @return bool + * @throws Exception + */ + public function incomeIntegral(int $uid, string $type, array $data, int $pm = 1) + { + $data['uid'] = $uid; + $data['category'] = 'integral'; + $data['type'] = $type; + $data['pm'] = $pm; + $data['status'] = 1; + $data['add_time'] = time(); + if (!$this->dao->save($data)) + throw new Exception('增加记录失败'); + return true; + } + + + /** + * 写入用户记录 + * @param string $type 写入类型 + * @param int $uid + * @param int|string|array $number + * @param int|string $balance + * @param int $link_id + * @return bool|mixed + */ + public function income(string $type, int $uid, $number, $balance, $link_id = 0) + { + $data = $this->incomeData[$type] ?? null; + if (!$data) { + return true; + } + $data['uid'] = $uid; + $data['balance'] = $balance ?? 0; + $data['link_id'] = $link_id; + if (is_array($number)) { + $key = array_keys($number); + $key = array_map(function ($item) { + return '{%' . $item . '%}'; + }, $key); + $value = array_values($number); + $data['number'] = $number['number'] ?? 0; + $data['mark'] = str_replace($key, $value, $data['mark']); + } else { + $data['number'] = $number; + $data['mark'] = str_replace(['{%num%}'], $number, $data['mark']); + } + $data['add_time'] = time(); + if ((float)$data['number']) { + return $this->dao->save($data); + } + return true; + } + + /** + * 邀请新用户增加经验 + * @param int $spreadUid + */ + public function inviteUserIncExp(int $spreadUid) + { + if (!$spreadUid) { + return false; + } + //用户等级是否开启 + if (!sys_config('member_func_status', 1)) { + return false; + } + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $spread_user = $userService->getUserInfo($spreadUid); + if (!$spread_user) { + return false; + } + $exp_num = sys_config('invite_user_exp', 0); + if ($exp_num) { + $userService->incField($spreadUid, 'exp', (int)$exp_num); + $data = []; + $data['uid'] = $spreadUid; + $data['number'] = $exp_num; + $data['category'] = 'exp'; + $data['type'] = 'invite_user'; + $data['title'] = $data['mark'] = '邀新奖励'; + $data['balance'] = (int)$spread_user['exp']; + $data['pm'] = 1; + $data['status'] = 1; + $this->dao->save($data); + } + //检测会员等级 + try { + /** @var UserLevelServices $levelServices */ + $levelServices = app()->make(UserLevelServices::class); + //检测会员升级 + $levelServices->detection($spreadUid); + } catch (\Throwable $e) { + Log::error('会员等级升级失败,失败原因:' . $e->getMessage()); + } + return true; + } + + /** + * 获取type + * @param array $where + * @param string $filed + */ + public function getBillType(array $where) + { + return $this->dao->getType($where); + } + + /** + * 资金类型 + */ + public function bill_type() + { + $where = []; + $where['not_type'] = ['gain', 'system_sub', 'deduction', 'sign']; + $where['not_category'] = ['exp', 'integral']; + return CacheService::get('user_type_list', function () use ($where) { + return ['list' => $this->dao->getType($where)]; + }, 600); + } + + /** + * 记录分享次数 + * @param int $uid 用户uid + * @param int $cd 冷却时间 + * @return Boolean + * */ + public function setUserShare(int $uid, $cd = 300) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('用户不存在!'); + } + $cachename = 'Share_' . $uid; + if (CacheService::get($cachename)) { + return false; + } + $data = ['title' => '用户分享记录', 'uid' => $uid, 'category' => 'share', 'type' => 'share', 'number' => 0, 'link_id' => 0, 'balance' => 0, 'mark' => date('Y-m-d H:i:s', time()) . ':用户分享']; + if (!$this->dao->save($data)) { + throw new ValidateException('记录分享记录失败'); + } + CacheService::set($cachename, 1, $cd); + return true; + } + + /** + * 获取积分列表 + * @param array $where + * @param string $field + * @param int $limit + * @return array + */ + public function getPointList(array $where, string $field = '*', int $limit = 0) + { + $where_data = []; + $where_data['category'] = 'integral'; + if (isset($where['uid']) && $where['uid'] != '') { + $where_data['uid'] = $where['uid']; + } + if ($where['start_time'] != '' && $where['end_time'] != '') { + $where_data['time'] = $where['start_time'] . ' - ' . $where['end_time']; + } + if (isset($where['type']) && $where['type'] != '') { + $where_data['type'] = $where['type']; + } + if (isset($where['nickname']) && $where['nickname'] != '') { + $where_data['like'] = $where['nickname']; + } + if (isset($where['excel']) && $where['excel'] != '') { + $where_data['excel'] = $where['excel']; + } else { + $where_data['excel'] = 0; + } + if ($limit) { + [$page] = $this->getPageValue(); + } else { + [$page, $limit] = $this->getPageValue(); + } + $list = $this->dao->getBillList($where_data, $field, $page, $limit); + foreach ($list as &$item) { + $item['nickname'] = $item['user']['nickname'] ?? ''; + $item['number'] = intval($item['number']); + $item['balance'] = intval($item['balance']); + unset($item['user']); + } + $count = $this->dao->count($where_data); + return compact('list', 'count'); + } + + /** + * 积分头部信息 + * @param array $where + * @return array[] + */ + public function getUserPointBadgelist(array $where) + { + $data = []; + $where_data = []; + $where_data['category'] = 'integral'; + if ($where['start_time'] != '' && $where['end_time'] != '') { + $where_data['time'] = $where['start_time'] . ' - ' . $where['end_time']; + } + if (isset($where['nickname']) && $where['nickname'] != '') { + $where_data['like'] = $where['nickname']; + } + $data['SumIntegral'] = intval($this->dao->getBillSumColumn($where_data + ['pm' => 1])); + $where_data['type'] = 'sign'; + $data['CountSign'] = $this->dao->getUserSignPoint($where_data); + $data['SumSign'] = intval($this->dao->getBillSumColumn($where_data)); + unset($where_data['type']); + $data['SumDeductionIntegral'] = intval($this->dao->getBillSumColumn($where_data + ['pm' => 0])); + return [ + [ + 'col' => 6, + 'count' => $data['SumIntegral'], + 'name' => '总积分(个)', + ], + [ + 'col' => 6, + 'count' => $data['CountSign'], + 'name' => '客户签到次数(次)', + ], + [ + 'col' => 6, + 'count' => $data['SumSign'], + 'name' => '签到送出积分(个)', + ], + [ + 'col' => 6, + 'count' => $data['SumDeductionIntegral'], + 'name' => '使用积分(个)', + ], + ]; + } + + /** + * @param $uid + * @param $type + * @return array + */ + public function getUserBillList(int $uid, int $type) + { + $where = []; + $where['uid'] = $uid; + $where['category'] = 'now_money'; + switch ((int)$type) { + case 0: + $where['type'] = ['recharge', 'pay_money', 'system_add', 'pay_product_refund', 'system_sub', 'pay_member', 'offline_scan', 'lottery_use', 'lottery_add']; + break; + case 1: + $where['type'] = ['pay_money', 'pay_member', 'offline_scan', 'user_recharge_refund', 'lottery_use']; + break; + case 2: + $where['type'] = ['recharge', 'system_add', 'lottery_add']; + break; + case 3: + $where['type'] = ['brokerage', 'brokerage_user']; + break; + case 4: + $where['type'] = ['extract']; + break; + } + $field = 'FROM_UNIXTIME(add_time,"%Y-%m") as time,group_concat(id SEPARATOR ",") ids'; + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getUserBillListByGroup($where, $field, 'time', $page, $limit); + $data = []; + if ($list) { + $listIds = array_column($list, 'ids'); + $ids = []; + foreach ($listIds as $id) { + $ids = array_merge($ids, explode(',', $id)); + } + $info = $this->dao->getColumn([['id', 'in', $ids]], 'FROM_UNIXTIME(add_time,"%Y-%m-%d %H:%i") as add_time,title,number,pm', 'id'); + foreach ($list as $item) { + $value['time'] = $item['time']; + $id = explode(',', $item['ids']); + array_multisort($id, SORT_DESC); + $value['list'] = []; + foreach ($id as $v) { + if (isset($info[$v])) { + $value['list'][] = $info[$v]; + } + } + array_push($data, $value); + } + } + return $data; + } +} diff --git a/app/services/user/UserBrokerageServices.php b/app/services/user/UserBrokerageServices.php new file mode 100644 index 0000000..f7a4a12 --- /dev/null +++ b/app/services/user/UserBrokerageServices.php @@ -0,0 +1,633 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\dao\user\UserBrokerageDao; +use app\services\BaseServices; +use app\services\order\StoreOrderCartInfoServices; +use app\services\order\StoreOrderServices; +use think\exception\ValidateException; +use crmeb\services\CacheService; + +/** + * 用户佣金 + * Class UserBrokerageServices + * @package app\services\user + * @mixin UserBrokerageDao + */ +class UserBrokerageServices extends BaseServices +{ + + /** + * 用户记录模板 + * @var array[] + */ + protected $incomeData = [ + 'get_self_brokerage' => [ + 'title' => '获得自购订单佣金', + 'type' => 'self_brokerage', + 'mark' => '您成功消费{%pay_price%}元,奖励自购佣金{%number%}', + 'status' => 1, + 'pm' => 1 + ], + 'get_brokerage' => [ + 'title' => '获得下级推广订单佣金', + 'type' => 'one_brokerage', + 'mark' => '{%nickname%}成功消费{%pay_price%}元,奖励推广佣金{%number%}', + 'status' => 1, + 'pm' => 1 + ], + 'get_two_brokerage' => [ + 'title' => '获得推广订单佣金', + 'type' => 'two_brokerage', + 'mark' => '二级推广人{%nickname%}成功消费{%pay_price%}元,奖励推广佣金{%number%}', + 'status' => 1, + 'pm' => 1 + ], + 'get_user_brokerage' => [ + 'title' => '获得推广用户佣金', + 'type' => 'brokerage_user', + 'mark' => '成功推广用户:{%nickname%},奖励推广佣金{%number%}', + 'status' => 1, + 'pm' => 1 + ], + 'extract' => [ + 'title' => '佣金提现', + 'type' => 'extract', + 'mark' => '{%mark%},佣金提现{%number%}元', + 'status' => 1, + 'pm' => 0 + ], + 'extract_fail' => [ + 'title' => '提现失败', + 'type' => 'extract_fail', + 'mark' => '提现失败,退回佣金{%num%}元', + 'status' => 1, + 'pm' => 1 + ], + 'brokerage_to_nowMoney' => [ + 'title' => '佣金提现到余额', + 'type' => 'extract_money', + 'mark' => '佣金提现到余额{%num%}元', + 'status' => 1, + 'pm' => 0 + ], + 'brokerage_refund' => [ + 'title' => '退款退佣金', + 'type' => 'refund', + 'mark' => '订单退款扣除佣金{%num%}元', + 'status' => 1, + 'pm' => 0 + ], + ]; + + /** + * UserBrokerageServices constructor. + * @param UserBrokerageDao $dao + */ + public function __construct(UserBrokerageDao $dao) + { + $this->dao = $dao; + } + + /** + * 计算佣金 + * @param array $where + * @param int $time + * @return mixed + */ + public function getUsersBokerageSum(array $where, $time = 0) + { + $where_data = [ + 'status' => 1, + 'pm' => $where['pm'] ?? '', + 'uid' => $where['uid'] ?? '', + 'time' => $where['time'] ?? 0, + 'type' => $where['type'] ?? '', + 'not_type' => $where['not_type'] ?? '' + ]; + if ($time) $where_data['time'] = $time; + return $this->dao->getBrokerageSumColumn($where_data); + } + + /** + * 某个用户佣金总和 + * @param int $uid + * @param array|string[] $type + * @param string $time + * @return float + */ + public function getUserBillBrokerageSum(int $uid, array $type = ['self_brokerage', 'one_brokerage', 'two_brokerage', 'brokerage_user'], $time = '') + { + $where = ['uid' => $uid]; + if ($type) $where['type'] = $type; + if ($time) $where['time'] = $time; + return $this->dao->getBrokerageSum($where); + } + + + /** + * 写入用户记录 + * @param string $type 写入类型 + * @param int $uid + * @param int|string|array $number + * @param int|string $balance + * @param int $link_id + * @return bool|mixed + */ + public function income(string $type, int $uid, $number, $balance, $link_id) + { + $data = $this->incomeData[$type] ?? null; + if (!$data) { + return true; + } + $data['uid'] = $uid; + $data['balance'] = $balance ?? 0; + $data['link_id'] = $link_id; + if (is_array($number)) { + $key = array_keys($number); + $key = array_map(function ($item) { + return '{%' . $item . '%}'; + }, $key); + $value = array_values($number); + $data['number'] = $number['number'] ?? 0; + $data['frozen_time'] = $number['frozen_time'] ?? 0; + $data['mark'] = str_replace($key, $value, $data['mark']); + } else { + $data['number'] = $number; + $data['mark'] = str_replace(['{%num%}'], $number, $data['mark']); + } + $data['add_time'] = time(); + if ((float)$data['number']) { + return $this->dao->save($data); + } + return true; + } + + + /** + * 资金类型 + */ + public function bill_type() + { + return CacheService::get('user_brokerage_type_list', function () { + return ['list' => $this->dao->getBrokerageType([])]; + }, 600); + } + + /** + * 获取资金列表 + * @param array $where + * @param string $field + * @param int $limit + * @return array + */ + public function getBrokerageList(array $where, string $field = '*', int $limit = 0) + { + $where_data = []; + if (isset($where['uid']) && $where['uid'] != '') { + $where_data['uid'] = $where['uid']; + } + if ($where['start_time'] != '' && $where['end_time'] != '') { + $where_data['time'] = str_replace('-', '/', $where['start_time']) . ' - ' . str_replace('-', '/', $where['end_time']); + } + if (isset($where['type']) && $where['type'] != '') { + $where_data['type'] = $where['type']; + } + if (isset($where['nickname']) && $where['nickname'] != '') { + $where_data['like'] = $where['nickname']; + } + if ($limit) { + [$page] = $this->getPageValue(); + } else { + [$page, $limit] = $this->getPageValue(); + } + $data = $this->dao->getBrokerageList($where_data, $field, $page, $limit); + foreach ($data as &$item) { + $item['nickname'] = $item['user']['nickname'] ?? ''; + $item['_add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + unset($item['user']); + } + $count = $this->dao->count($where_data); + return compact('data', 'count'); + } + + /** + * 获取佣金列表 + * @param array $where + * @param int $limit + * @return array + */ + public function getCommissionList(array $where, int $limit = 0) + { + $where_data = []; + $where_data['time'] = $where['time']; + if (isset($where['nickname']) && $where['nickname']) { + $where_data[] = ['u.account|u.nickname|u.uid|u.phone', 'LIKE', "%$where[nickname]%"]; + } + if (isset($where['price_max']) && isset($where['price_min'])) { + if ($where['price_max'] != '' && $where['price_min'] != '') { + $where_data[] = ['u.brokerage_price', 'between', [$where['price_min'], $where['price_max']]]; + } elseif ($where['price_min'] != '' && $where['price_max'] == '') { + $where_data[] = ['u.brokerage_price', '>=', $where['price_min']]; + } elseif ($where['price_min'] == '' && $where['price_max'] != '') { + $where_data[] = ['u.brokerage_price', '<=', $where['price_max']]; + } + } + $order_string = ''; + $order_arr = ['asc', 'desc']; + if (isset($where['sum_number']) && in_array($where['sum_number'], $order_arr)) { + $order_string .= ',income ' . $where['sum_number']; + } + if (isset($where['brokerage_price']) && in_array($where['brokerage_price'], $order_arr)) { + $order_string .= ',u.brokerage_price ' . $where['brokerage_price']; + } + if ($order_string) { + $order_string = trim($order_string, ','); + } + /** @var UserUserBrokerageServices $userUserBrokerage */ + $userUserBrokerage = app()->make(UserUserBrokerageServices::class); + [$count, $list] = $userUserBrokerage->getBrokerageList($where_data, 'b.type,b.pm,sum(IF(b.pm = 1, b.number, 0)) as income,sum(IF(b.pm = 0, b.number, 0)) as pay,u.nickname,u.phone,u.uid,u.now_money,u.brokerage_price,u.delete_time,b.add_time as time', $order_string, $limit); + $uids = array_unique(array_column($list, 'uid')); + /** @var UserExtractServices $userExtract */ + $userExtract = app()->make(UserExtractServices::class); + $extractSumList = $userExtract->getUsersSumList($uids); + foreach ($list as &$item) { +// $item['sum_number'] = $item['income'] > $item['pay'] ? bcsub($item['income'], $item['pay'], 2) : 0; + $item['nickname'] = $item['nickname'] . "|" . ($item['phone'] ? $item['phone'] . "|" : '') . $item['uid']; + $item['extract_price'] = $extractSumList[$item['uid']] ?? 0; + $item['sum_number'] = bcadd((string)$item['extract_price'], (string)$item['brokerage_price'], 2); + $item['time'] = $item['time'] ? date('Y-m-d H:i:s', $item['time']) : ''; + } + return compact('count', 'list'); + } + + /** + * 用户佣金详情 + * @param int $uid + * @return array + */ + public function user_info(int $uid) + { + /** @var UserServices $user */ + $user = app()->make(UserServices::class); + $user_info = $user->getUserWithTrashedInfo($uid, 'nickname,spread_uid,now_money,brokerage_price,add_time'); + if (!$user_info) { + throw new ValidateException('您查看的用户信息不存在!'); + } + $user_info = $user_info->toArray(); + $income = $this->getUserBillBrokerageSum($uid); + $expend = $this->getUserBillBrokerageSum($uid, ['refund']); + $number = (float)bcsub((string)$income, (string)$expend, 2); + $user_info['number'] = max($number, 0); + $user_info['add_time'] = date('Y-m-d H:i:s', $user_info['add_time']); + $user_info['spread_name'] = $user_info['spread_uid'] ? $user->getUserInfo((int)$user_info['spread_uid'], 'nickname', true)['nickname'] ?? '' : ''; + return compact('user_info'); + } + + + /** + * 退佣金 + * @param $order + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function orderRefundBrokerageBack($order) + { + $id = (int)$order['id']; + $where = [ + 'uid' => [$order['spread_uid'], $order['spread_two_uid']], + 'type' => ['self_brokerage', 'one_brokerage', 'two_brokerage'], + 'link_id' => $id, + 'pm' => 1 + ]; + $brokerageList = $this->dao->getUserBrokerageList($where); + //子订单 + if (!$brokerageList && $order['pid']) { + $where['link_id'] = $order['pid']; + $p_brokerageList = $this->dao->getUserBrokerageList($where); + //主订单已分佣 子订单按订单拆分后计算结果回退 + if ($p_brokerageList) { + $brokerageList = [ + ['uid' => $order['spread_uid'], 'number' => $order['one_brokerage']], + ['uid' => $order['spread_two_uid'], 'number' => $order['two_brokerage']], + ]; + } + } + $res = true; + if ($brokerageList) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $brokerages = $userServices->getColumn([['uid', 'in', array_column($brokerageList, 'uid')]], 'brokerage_price', 'uid'); + $brokerageData = []; + + foreach ($brokerageList as $item) { + if (!$item['uid'] || $item['uid'] <= 0) continue; + $usermoney = $brokerages[$item['uid']] ?? 0; + if ($item['number'] > $usermoney) { + $item['number'] = $usermoney; + } + if ($item['number'] <= 0) continue; + $res = $res && $userServices->bcDec($item['uid'], 'brokerage_price', (string)$item['number'], 'uid'); + $brokerageData[] = [ + 'title' => '退款退佣金', + 'uid' => $item['uid'], + 'pm' => 0, + 'add_time' => time(), + 'type' => 'refund', + 'number' => $item['number'], + 'link_id' => $id, + 'balance' => bcsub((string)$usermoney, (string)$item['number'], 2), + 'mark' => '订单退款扣除佣金' . floatval($item['number']) . '元' + ]; + } + if ($brokerageData) { + $res = $res && $this->dao->saveAll($brokerageData); + } + //修改佣金冻结时间 + $this->dao->update($where, ['frozen_time' => 0]); + } + return $res; + } + + /** + * 佣金排行 + * @param string $time + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function brokerageRankList(string $time = 'week') + { + $where = []; + if ($time) { + $where['time'] = $time; + } + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->brokerageRankList($where, $page, $limit); + foreach ($list as $key => &$item) { + if (!isset($item['user']) || !$item['user'] || $item['brokerage_price'] <= 0) { + unset($list[$key]); + continue; + } + $item['nickname'] = $item['user']['nickname'] ?? ''; + $item['avatar'] = $item['user']['avatar'] ?? ''; + if ($item['brokerage_price'] == '0.00' || $item['brokerage_price'] == 0 || !$item['brokerage_price']) { + unset($list[$key]); + } + unset($item['user']); + } + return array_merge($list); + } + + /** + * 获取用户排名 + * @param int $uid + * @param string $time + */ + public function getUserBrokerageRank(int $uid, string $time = 'week') + { + $where = []; + if ($time) { + $where['time'] = $time; + } + $list = $this->dao->brokerageRankList($where); + foreach ($list as $key => &$item) { + if (!isset($item['user']) || !$item['user'] || $item['brokerage_price'] <= 0) { + unset($list[$key]); + } + } + $position_tmp_one = array_column($list, 'uid'); + $position_tmp_two = array_column($list, 'brokerage_price', 'uid'); + if (!in_array($uid, $position_tmp_one)) { + $position = 0; + } else { + if ($position_tmp_two[$uid] == 0.00) { + $position = 0; + } else { + $position = array_search($uid, $position_tmp_one) + 1; + } + } + return $position; + } + + + /** + * 推广数据 昨天的佣金 累计提现金额 当前佣金 + * @param int $uid + * @return mixed + */ + public function commission(int $uid) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$userServices->userExist($uid)) { + throw new ValidateException('数据不存在'); + } + /** @var UserExtractServices $userExtract */ + $userExtract = app()->make(UserExtractServices::class); + $data = []; + $data['uid'] = $uid; + $data['pm'] = 1; + $data['commissionSum'] = $this->getUsersBokerageSum($data); + $data['pm'] = 0; + $data['commissionRefund'] = $this->getUsersBokerageSum($data); + $data['commissionCount'] = $data['commissionSum'] > $data['commissionRefund'] ? bcsub((string)$data['commissionSum'], (string)$data['commissionRefund'], 2) : 0.00; + $data['lastDayCount'] = $this->getUsersBokerageSum($data, 'yesterday');//昨天的佣金 + $data['extractCount'] = $userExtract->getUserExtract($uid);//累计提现金额 + + return $data; + } + + /** + * 前端佣金排行页面数据 + * @param int $uid + * @param $type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function brokerage_rank(int $uid, $type) + { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + if (!$userService->userExist($uid)) { + throw new ValidateException('数据不存在'); + } + return [ + 'rank' => $this->brokerageRankList($type), + 'position' => $this->getUserBrokerageRank($uid, $type) + ]; + } + + /** + * 推广 佣金/提现 总和 + * @param int $uid + * @param $type 3 佣金 4 提现 + * @return mixed + */ + public function spread_count(int $uid, $type) + { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + if (!$userService->userExist($uid)) { + throw new ValidateException('数据不存在'); + } + $count = 0; + if ($type == 3) { + $where = [ + 'uid' => $uid, + 'status' => 1, + 'pm' => 1 + ]; + $count1 = $this->dao->getBrokerageSumColumn($where); + $where['pm'] = 0; + $count2 = $this->dao->getBrokerageSumColumn($where); + $count = $count1 - $count2; + } else if ($type == 4) { + /** @var UserExtractServices $userExtract */ + $userExtract = app()->make(UserExtractServices::class); + $count = $userExtract->getUserExtract($uid);//累计提现 + } + return $count ? $count : 0; + } + + /** + * 推广订单 + * @param Request $request + * @return mixed + */ + public function spread_order(int $uid, array $data) + { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + if (!$userService->userExist($uid)) { + throw new ValidateException('数据不存在'); + } + $result = ['list' => [], 'time' => [], 'count' => 0]; + /** @var StoreOrderServices $storeOrderServices */ + $storeOrderServices = app()->make(StoreOrderServices::class); + [$page, $limit] = $this->getPageValue(); + $time_data = []; + $where = ['type' => 0, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0, 'spread_or_uid' => $uid]; + if ($data['start'] || $data['stop']) { + $where['time'] = [$data['start'], $data['stop']]; + } + $where['real_name'] = $data['keyword']; + $list = $storeOrderServices->getList($where, ['id,order_id,uid,add_time,spread_uid,status,spread_two_uid,one_brokerage,two_brokerage,pay_price,cart_id'], $page, $limit, ['brokerage' => function ($query) use ($uid) { + $query->where('uid', $uid); + }]); + $result['count'] = $storeOrderServices->count($where); + if ($list) { + /** @var StoreOrderCartInfoServices $cartInfoServices */ + $cartInfoServices = app()->make(StoreOrderCartInfoServices::class); + $uids = array_unique(array_column($list, 'uid')); + $userInfos = $userService->getColumn([['uid', 'in', $uids]], 'uid,avatar,nickname', 'uid'); + foreach ($list as &$item) { + $item['store_name'] = $cartInfoServices->getCarIdByProductTitle((int)$item['id']); + $item['avatar'] = $userInfos[$item['uid']]['avatar'] ?? ''; + $item['nickname'] = $userInfos[$item['uid']]['nickname'] ?? ''; + $item['number'] = $item['spread_uid'] == $uid ? $item['one_brokerage'] : $item['two_brokerage']; + $item['time_key'] = $item['add_time'] ? date('Y-m', $item['add_time']) : ''; + $item['type'] = in_array($item['status'], [2, 3]) ? 'brokerage' : 'number'; + $brokerage = $item['brokerage']; + $time = $brokerage['add_time'] ?? $item['add_time']; + $item['time'] = $time ? date('Y-m-d H:i', $time) : ''; + $item['is_frozen'] = $brokerage && $brokerage['frozen_time'] > time() ? 1 : 0; + unset($item['brokerage']); + } + $times = array_unique(array_column($list, 'time_key')); + $time_data = []; + $i = 0; + foreach ($times as $time) { + $time_data[$i]['time'] = $time; + $time_data[$i]['count'] = $storeOrderServices->getMonthCount($where, $time); + $time_data[$i]['sumPrice'] = $storeOrderServices->getMonthMoneyCount($where, $time, 'pay_price'); + $i++; + } + } + $result['list'] = $list; + $result['time'] = $time_data; + + $priceWhere = ['pid' => 0, 'type' => 0, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0]; + if ($data['start'] || $data['stop']) { + $priceWhere['time'] = [$data['start'], $data['stop']]; + } + //条件获取一级佣金总和 + $sum_price_one = $storeOrderServices->sum($priceWhere + ['spread_uid' => $uid], 'one_brokerage', true); + $sum_price_two = $storeOrderServices->sum($priceWhere + ['spread_two_uid' => $uid], 'two_brokerage', true); + $result['sum_brokerage'] = bcadd((string)$sum_price_one, (string)$sum_price_two, 2); + + + return $result; + } + + /** + * 用户佣金记录v2 + * @param int $uid + * @return array + */ + public function userBrokerageList(int $uid, $data = []) + { + $where = []; + $where['uid'] = $uid; + if ($data['start'] || $data['stop']) { + $where['time'] = [$data['start'], $data['stop']]; + } + $where['like'] = $data['keyword']; + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit); + $times = []; + if ($list) { + foreach ($list as &$item) { + $item['time_key'] = $item['add_time'] ? date('Y-m', (int)$item['add_time']) : ''; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i', (int)$item['add_time']) : ''; + } + $times = array_merge(array_unique(array_column($list, 'time_key'))); + } + $income = $this->dao->sum($where + ['pm' => 1, 'not_type' => ['extract_fail']], 'number', true); + $expend = $this->dao->sum($where + ['pm' => 0], 'number', true); + return ['list' => $list, 'time' => $times, 'income' => $income, 'expend' => $expend]; + } + + /** + * 用户提现记录v2 + * @param int $uid + * @param array $data + * @return array + */ + public function userExtractList(int $uid, array $data) + { + $where = []; + $where['uid'] = $uid; + $where['type'] = ['extract', 'extract_money', 'extract_fail']; + if ((isset($data['start']) && $data['start']) || (isset($data['stop']) && $data['stop'])) { + $where['time'] = [$data['start'], $data['stop']]; + } + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, '*', $page, $limit); + $times = []; + if ($list) { + foreach ($list as &$item) { + $item['time_key'] = $item['add_time'] ? date('Y-m', (int)$item['add_time']) : ''; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i', (int)$item['add_time']) : ''; + } + $times = array_merge(array_unique(array_column($list, 'time_key'))); + } + return ['list' => $list, 'time' => $times]; + } +} diff --git a/app/services/user/UserCardServices.php b/app/services/user/UserCardServices.php new file mode 100644 index 0000000..d5c35bd --- /dev/null +++ b/app/services/user/UserCardServices.php @@ -0,0 +1,225 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\services\BaseServices; +use app\dao\user\UserCardDao; +use app\services\store\SystemStoreStaffServices; +use app\services\wechat\WechatCardServices; +use app\services\wechat\WechatUserServices; +use crmeb\services\wechat\OfficialAccount; + +/** + * 用户领取卡券 + * Class UserCardServices + * @package app\services\user + * @mixin UserCardDao + */ +class UserCardServices extends BaseServices +{ + + /** + * 激活会员卡微信字段对应eb_user字段 + * @var string[] + */ + protected $userField = [ + 'USER_FORM_INFO_FLAG_MOBILE' => 'phone', + 'USER_FORM_INFO_FLAG_BIRTHDAY' => 'birthday' + ]; + + /** + * UserCardServices constructor. + * @param UserCardDao $dao + */ + public function __construct(UserCardDao $dao) + { + $this->dao = $dao; + } + + /** + * 用户领取微信会员卡事件 + * @param $message + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function userGetCard($message) + { + if (isset($message['CardId'])) { + $card_id = $message['CardId']; + $openid = $message['FromUserName']; + /** @var WechatCardServices $wechatCardServices */ + $wechatCardServices = app()->make(WechatCardServices::class); + $card = $wechatCardServices->getOne(['card_id' => $card_id]); + if ($card) { + $staffInfo = []; + $uid = (int)$message['OuterId']; + if ($uid) { + try { + /** @var SystemStoreStaffServices $staffServices */ + $staffServices = app()->make(SystemStoreStaffServices::class); + $staffInfo = $staffServices->getStaffInfoByUid($uid); + } catch (\Throwable $e) { + $staffInfo = []; + } + } + $data = [ + 'openid' => $openid, + 'card_id' => $card_id, + 'code' => $message['UserCardCode'], + 'wechat_card_id' => $card['id'], + 'staff_id' => $staffInfo['id'] ?? 0, + 'store_id' => $staffInfo['store_id'] ?? 0, + 'spread_uid' => $uid, + 'add_time' => time() + ]; + $userCard = $this->dao->getOne(['openid' => $openid, 'card_id' => $card_id, 'is_del' => 0]); + if ($userCard) { + $this->dao->update($userCard['id'], $data); + } else { + $this->dao->save($data); + } + } + } + return true; + } + + /** + * 激活会员事件 + * @param $message + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function userSubmitCard($message) + { + if (isset($message['CardId'])) { + $card_id = $message['CardId']; + $openid = $message['FromUserName']; + /** @var WechatCardServices $wechatCardServices */ + $wechatCardServices = app()->make(WechatCardServices::class); + $card = $wechatCardServices->getOne(['card_id' => $card_id]); + if ($card) { + $userCard = $this->dao->getOne(['openid' => $openid, 'card_id' => $card_id, 'is_del' => 0]); + if ($userCard && !$userCard['is_submit']) { + $this->transaction(function () use ($openid, $userCard, $card_id) { + + $data = $this->getWechatCardInfo($card_id, $userCard['code']); + /** @var WechatUserServices $wechatUserSerives */ + $wechatUserSerives = app()->make(WechatUserServices::class); + $userInfo = $wechatUserSerives->saveUser($openid, $userCard['spread_uid'], $data['phone']); + $this->dao->update($userCard['id'], ['uid' => $userInfo['uid'] ?? 0, 'is_submit' => 1, 'submit_time' => time()]); + }); + } + } + } + return true; + } + + /** + * 获取用户激活会员卡填写信息 + * @param string $card_id + * @param string $code + * @return array + */ + public function getWechatCardInfo(string $card_id, string $code) + { + //获取用户激活填写字段信息 + $cart = OfficialAccount::getMemberCardUser($card_id, $code); + $formList = $cart['user_info']['common_field_list'] ?? []; + $userField = $this->userField; + $fields = array_keys($userField); + $data = []; + foreach ($formList as $item) { + if (in_array($item['name'], $fields)) { + $data[$userField[$item['name']]] = $item['value']; + } + } + return $data; + } + + /** + * 用户删除微信会员卡事件 + * @param $message + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function userDelCard($message) + { + if (isset($message['CardId'])) { + $card_id = $message['CardId']; + $openid = $message['FromUserName']; + /** @var WechatCardServices $wechatCardServices */ + $wechatCardServices = app()->make(WechatCardServices::class); + $card = $wechatCardServices->getOne(['card_id' => $card_id]); + if ($card) { + $userCard = $this->dao->getOne(['openid' => $openid, 'card_id' => $card_id, 'is_del' => 0]); + if ($userCard) { + $this->dao->update($userCard['id'], ['is_del' => 1, 'del_time' => time()]); + } + } + } + return true; + } + + /** + * 门店推广统计详情列表 + * @param int $store_id + * @param int $staff_id + * @param array $time + * @return array|array[] + */ + public function time(int $store_id, int $staff_id, array $time = []) + { + if (!$time) { + return [[], []]; + } + [$start, $stop, $front, $front_stop] = $time; + $where = ['store_id' => $store_id, 'is_submit' => 1]; + if ($staff_id) { + $where['staff_id'] = $staff_id; + } + $frontPrice = $this->dao->count($where + ['time' => [$front, $front_stop]]); + $nowPrice = $this->dao->count($where + ['time' => [$start, $stop]]); + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where + ['time' => [$start, $stop]], '*', ['user'], $page, $limit); + foreach ($list as &$item) { + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + } + return [[$nowPrice, $frontPrice], $list]; + } + + /** + * 获取会员卡列表 + * @param int $uid + * @param UserServices $userServices + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getCardList(array $where, string $field = '*', array $with = ['user']) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $field, $with, $page, $limit); + $count = $this->dao->count($where); + foreach ($list as &$item) { + $item['submit_time'] = $item['submit_time'] ? date('Y-m-d H:i:s', $item['submit_time']) : ''; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + } + return compact('list', 'count'); + } +} diff --git a/app/services/user/UserExtractServices.php b/app/services/user/UserExtractServices.php new file mode 100644 index 0000000..22d3de2 --- /dev/null +++ b/app/services/user/UserExtractServices.php @@ -0,0 +1,483 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\jobs\system\CapitalFlowJob; +use app\services\BaseServices; +use app\dao\user\UserExtractDao; +use app\services\order\StoreOrderCreateServices; +use app\services\wechat\WechatUserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\wechat\Payment; +use crmeb\services\FormBuilder as Form; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; +use think\facade\Route as Url; + +/** + * + * Class UserExtractServices + * @package app\services\user + * @mixin UserExtractDao + */ +class UserExtractServices extends BaseServices +{ + + use ServicesTrait; + + /** + * UserExtractServices constructor. + * @param UserExtractDao $dao + */ + public function __construct(UserExtractDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取一条提现记录 + * @param int $id + * @param array $field + * @return array|\think\Model|null + */ + public function getExtract(int $id, array $field = []) + { + return $this->dao->get($id, $field); + } + + /** + * 获取某个用户提现总数 + * @param int $uid + * @return float + */ + public function getUserExtract(int $uid) + { + return $this->dao->getWhereSum(['uid' => $uid, 'status' => [0, 1]]); + } + + /** + * 获取某些用户的提现总数列表 + * @param array $uids + */ + public function getUsersSumList(array $uids) + { + return $this->dao->getWhereSumList(['uid' => $uids, 'status' => [0, 1]]); + } + + public function getCount(array $where = []) + { + return $this->dao->getCount($where); + } + + /** + * 获取提现列表 + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserExtractList(array $where, string $field = '*') + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getExtractList($where, $field, $page, $limit); + foreach ($list as &$item) { + $item['nickname'] = $item['user']['nickname'] ?? ''; + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取提现总数 + * @param array $where + */ + public function getExtractSum(array $where) + { + return $this->dao->getExtractMoneyByWhere($where, 'extract_price'); + } + + /** + * 拒绝提现申请 + * @param $id + * @param $fail_msg + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function changeFail(int $id, $userExtract, $message) + { + $fail_time = time(); + $extract_number = bcadd((string)$userExtract['extract_price'], (string)$userExtract['extract_fee'], 2); + $mark = '提现失败,退回佣金' . $extract_number . '元'; + $uid = $userExtract['uid']; + $status = -1; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('用户不存在'); + } + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $this->transaction(function () use ($user, $userBrokerageServices, $uid, $id, $extract_number, $message, $userServices, $status, $fail_time) { + $now_brokerage = bcadd((string)$user['brokerage_price'], (string)$extract_number, 2); + //增加佣金记录 + $userBrokerageServices->income('extract_fail', $uid, $extract_number, $now_brokerage, $id); + //修改用户佣金 + if (!$userServices->update($uid, ['brokerage_price' => $now_brokerage], 'uid')) + throw new AdminException('增加用户佣金失败'); + if (!$this->dao->update($id, ['fail_time' => $fail_time, 'fail_msg' => $message, 'status' => $status])) { + throw new AdminException('修改失败'); + } + }); + //消息推送 + event('notice.notice', [['uid' => $uid, 'userType' => strtolower($user['user_type']), 'extract_number' => $extract_number, 'nickname' => $user['nickname'], 'message' => $message], 'user_balance_change']); + return true; + } + + /** + * 通过提现申请 + * @param $id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function changeSuccess(int $id, $userExtract) + { + $extractNumber = $userExtract['extract_price']; + $this->transaction(function () use ($id, $userExtract, $extractNumber) { + if (!$this->dao->update($id, ['status' => 1])) { + throw new AdminException('修改失败'); + } + //配置开启自动到零钱 + if (sys_config('brokerage_type', 0)) { + /** @var WechatUserServices $wechatServices */ + $wechatServices = app()->make(WechatUserServices::class); + $openid = $wechatServices->getWechatOpenid((int)$userExtract['uid'], 'wechat'); + if ($openid) {//公众号用户 + $type = Payment::WEB; + } else {//小程序用户 + $openid = $wechatServices->getWechatOpenid((int)$userExtract['uid'], 'routine'); + $type = Payment::MINI; + } + //app微信用户 + if (!$openid) { + $openid = $wechatServices->getWechatOpenid((int)$userExtract['uid'], 'app'); + $type = Payment::APP; + } + if ($openid) { + /** @var StoreOrderCreateServices $services */ + $services = app()->make(StoreOrderCreateServices::class); + $wechat_order_id = $services->getNewOrderId(); + $res = Payment::merchantPay($openid, $wechat_order_id, $extractNumber, '提现佣金到零钱', $type); + if (!$res) { + throw new ValidateException('企业付款到零钱失败,请稍后再试'); + } + } else { + throw new ValidateException('该用户暂不支持企业付款到零钱,请手动转账'); + } + } + }); + + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userType = $userServices->value(['uid' => $userExtract['uid']], 'user_type'); + $nickname = $userServices->value(['uid' => $userExtract['uid']], 'nickname'); + $phone = $userServices->value(['uid' => $userExtract['uid']], 'phone'); + + switch ($userExtract['extract_type']) { + case 'bank': + $order_id = $userExtract['bank_code']; + break; + case 'weixin': + $order_id = $userExtract['wechat']; + break; + case 'alipay': + $order_id = $userExtract['alipay_code']; + break; + default: + $order_id = ''; + break; + } + //记录资金流水队列 + CapitalFlowJob::dispatch([['order_id' => $order_id, 'store_id' => 0, 'uid' => $userExtract['uid'], 'nickname' => $nickname, 'phone' => $phone, 'price' => $extractNumber, 'pay_type' => $userExtract['extract_type']], 'extract']); + + //消息推送 + event('notice.notice', [['uid' => $userExtract['uid'], 'userType' => strtolower($userType), 'extractNumber' => $extractNumber, 'nickname' => $nickname], 'user_extract']); + return true; + } + + /** + * 显示资源列表 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function index(array $where) + { + $list = $this->getUserExtractList($where); + //待提现金额 + $where['status'] = 0; + $extract_statistics['price'] = $this->getExtractSum($where); + //已提现金额 + $where['status'] = 1; + $extract_statistics['priced'] = $this->getExtractSum($where); + //佣金总金额 + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $extract_statistics['brokerage_count'] = $userBrokerageServices->getUsersBokerageSum(array_merge($where, ['pm' => 1, 'not_type' => ['extract_fail', 'refund']])); + //未提现金额 + $extract_statistics['brokerage_not'] = $extract_statistics['brokerage_count'] > $extract_statistics['priced'] ? bcsub((string)$extract_statistics['brokerage_count'], (string)$extract_statistics['priced'], 2) : 0.00; + return compact('extract_statistics', 'list'); + } + + /** + * 显示编辑资源表单页. + * + * @param int $id + * @return \think\Response + */ + public function edit(int $id) + { + $UserExtract = $this->getExtract($id); + if (!$UserExtract) { + throw new AdminException('数据不存在!'); + } + $f = array(); + $f[] = Form::input('real_name', '姓名', $UserExtract['real_name']); + $f[] = Form::number('extract_price', '提现金额', (float)$UserExtract['extract_price'])->precision(2)->disabled(true); + if ($UserExtract['extract_type'] == 'alipay') { + $f[] = Form::input('alipay_code', '支付宝账号', $UserExtract['alipay_code']); + } else if ($UserExtract['extract_type'] == 'weixin') { + $f[] = Form::input('wechat', '微信号', $UserExtract['wechat']); + } else { + $f[] = Form::input('bank_code', '银行卡号', $UserExtract['bank_code']); + $f[] = Form::input('bank_address', '开户行', $UserExtract['bank_address']); + } + $f[] = Form::input('mark', '备注', $UserExtract['mark'])->type('textarea'); + return create_form('编辑', $f, Url::buildUrl('/finance/extract/' . $id), 'PUT'); + } + + public function update(int $id, array $data) + { + if (!$this->dao->update($id, $data)) + throw new AdminException('修改失败'); + else + return true; + } + + /** + * 拒绝 + * @param $id + * @return mixed + */ + public function refuse(int $id, string $message) + { + $extract = $this->getExtract($id); + if (!$extract) { + throw new AdminException('操作记录不存在!'); + } + if ($extract->status == 1) { + throw new AdminException('已经提现,错误操作'); + } + if ($extract->status == -1) { + throw new AdminException('您的提现申请已被拒绝,请勿重复操作!'); + } + $res = $this->changeFail($id, $extract, $message); + if ($res) { + return true; + } else { + throw new AdminException('操作失败!'); + } + } + + /** + * 通过 + * @param $id + * @return mixed + */ + public function adopt(int $id) + { + $extract = $this->getExtract($id); + if (!$extract) { + throw new AdminException('操作记录不存!'); + } + if ($extract->status == 1) { + throw new AdminException('您已提现,请勿重复提现!'); + } + if ($extract->status == -1) { + throw new AdminException('您的提现申请已被拒绝!'); + } + if ($this->changeSuccess($id, $extract)) { + return true; + } else { + throw new AdminException('操作失败!'); + } + } + + /**待提现的数量 + * @return int + */ + public function userExtractCount() + { + return $this->dao->count(['status' => 0]); + } + + /** + * 银行卡提现 + * @param int $uid + * @return mixed + */ + public function bank(int $uid) + { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $user = $userService->getUserInfo($uid); + if (!$user) { + throw new ValidateException('数据不存在'); + } + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $data['broken_commission'] = $userBrokerageServices->getUserFrozenPrice($uid); + if ($data['broken_commission'] < 0) + $data['broken_commission'] = '0'; + $data['brokerage_price'] = $user['brokerage_price']; + //可提现佣金 + $data['commissionCount'] = bcsub((string)$data['brokerage_price'], (string)$data['broken_commission'], 2); + $extractBank = sys_config('user_extract_bank') ?? []; //提现银行 + $extractBank = str_replace("\r\n", "\n", $extractBank);//防止不兼容 + $data['extractBank'] = explode("\n", is_array($extractBank) ? (isset($extractBank[0]) ? $extractBank[0] : $extractBank) : $extractBank); + $data['minPrice'] = sys_config('user_extract_min_price');//提现最低金额 + $data['withdraw_fee'] = sys_config('withdraw_fee');//提现手续费 + $data['extract_wechat_type'] = sys_config('brokerage_type');//微信提现到账方式 + return $data; + } + + /** + * 提现申请 + * @param int $uid + * @param array $data + */ + public function cash(int $uid, array $data) + { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $user = $userService->getUserInfo($uid); + if (!$user) { + throw new ValidateException('数据不存在'); + } + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $data['broken_commission'] = $userBrokerageServices->getUserFrozenPrice($uid); + if ($data['broken_commission'] < 0) + $data['broken_commission'] = 0; + $data['brokerage_price'] = $user['brokerage_price']; + //可提现佣金 + $commissionCount = (float)bcsub((string)$data['brokerage_price'], (string)$data['broken_commission'], 2); + if ($data['money'] > $commissionCount) { + throw new ValidateException('可提现佣金不足'); + } + + $extractPrice = $user['brokerage_price']; + $userExtractMinPrice = sys_config('user_extract_min_price'); + if ($data['money'] < $userExtractMinPrice) { + throw new ValidateException('提现金额不能小于' . $userExtractMinPrice . '元'); + } + if ($extractPrice < 0) { + throw new ValidateException('提现佣金不足' . $data['money']); + } + if ($data['money'] > $extractPrice) { + throw new ValidateException('提现佣金不足' . $data['money']); + } + if ($data['money'] <= 0) { + throw new ValidateException('提现佣金大于0'); + } + $extract_fee = bcmul((string)$data['money'], bcdiv(sys_config('withdraw_fee'), '100', 2), 2); + $insertData = [ + 'uid' => $user['uid'], + 'extract_type' => $data['extract_type'], + 'extract_price' => bcsub((string)$data['money'], (string)$extract_fee, 2), + 'extract_fee' => $extract_fee, + 'add_time' => time(), + 'balance' => $user['brokerage_price'], + 'status' => 0 + ]; + if (isset($data['name']) && strlen(trim($data['name']))) $insertData['real_name'] = $data['name']; + else $insertData['real_name'] = $user['nickname']; + if (isset($data['cardnum'])) $insertData['bank_code'] = $data['cardnum']; + else $insertData['bank_code'] = ''; + if (isset($data['bankname'])) $insertData['bank_address'] = $data['bankname']; + else $insertData['bank_address'] = ''; + if (isset($data['weixin'])) $insertData['wechat'] = $data['weixin']; + else $insertData['wechat'] = $user['nickname']; + if ($data['extract_type'] == 'alipay') { + $insertData['alipay_code'] = $data['alipay_code']; + $insertData['qrcode_url'] = $data['qrcode_url']; + $mark = '使用支付宝'; + } else if ($data['extract_type'] == 'bank') { + $mark = '使用银联卡' . $insertData['bank_code']; + } else if ($data['extract_type'] == 'weixin') { + $insertData['qrcode_url'] = $data['qrcode_url']; + $mark = '使用微信提现'; + /** @var WechatUserServices $wechatServices */ + $wechatServices = app()->make(WechatUserServices::class); + $openid = $wechatServices->getWechatOpenid($uid, 'wechat'); + if (sys_config('brokerage_type', 0) && $openid) { + if ($insertData['extract_price'] < 1) { + throw new ValidateException('扣除手续费后,提现金额不足1元;而企业微信付款到零钱最低金额为1元'); + } + } + } + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $res1 = $this->transaction(function () use ($insertData, $data, $uid, $userService, $user, $userBrokerageServices, $mark) { + if (!$res1 = $this->dao->save($insertData)) { + throw new ValidateException('提现失败'); + } + //修改用户佣金 + $balance = bcsub((string)$user['brokerage_price'], (string)$data['money'], 2) ?? 0; + if (!$userService->update($uid, ['brokerage_price' => $balance], 'uid')) { + throw new ValidateException('修改用户信息失败'); + } + //保存佣金记录 + $userBrokerageServices->income('extract', $uid, ['mark' => $mark, 'number' => $data['money']], $balance, $res1['id']); + return $res1; + }); + //用户申请体现事件 + event('user.extract', [$user, $data, $res1]); + return true; + } + + /** + * @param array $where + * @param string $SumField + * @param string $selectType + * @param string $group + * @return float|mixed + */ + public function getOutMoneyByWhere(array $where, string $SumField, string $selectType, string $group = "") + { + switch ($selectType) { + case "sum" : + return $this->dao->getWhereSumField($where, $SumField); + case "group" : + return $this->dao->getGroupField($where, $SumField, $group); + } + } +} diff --git a/app/services/user/UserFriendsServices.php b/app/services/user/UserFriendsServices.php new file mode 100644 index 0000000..125d511 --- /dev/null +++ b/app/services/user/UserFriendsServices.php @@ -0,0 +1,109 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\user; + + +use app\dao\user\UserFriendsDao; +use app\services\BaseServices; +use app\services\user\level\SystemUserLevelServices; +use crmeb\traits\ServicesTrait; + +/** + * 获取好友列表 + * Class UserFriendsServices + * @package app\services\user + * @mixin UserFriendsDao + */ +class UserFriendsServices extends BaseServices +{ + + use ServicesTrait; + + public function __construct(UserFriendsDao $dao) + { + $this->dao = $dao; + } + + /** + * 保存好友关系 + * @param int $uid + * @param int $friends_uid + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveFriend(int $uid, int $friends_uid) + { + $data = [ + 'uid' => $uid, + 'friends_uid' => $friends_uid + ]; + $userFriend = $this->dao->get($data); + $res1 = true; + if (!$userFriend) { + $data['add_time'] = time(); + $res1 = $this->dao->save($data); + } + return $res1; + } + + /** + * 获取好友uids 我推广的 推广我的 + * @param int $uid + * @return array + */ + public function getFriendUids(int $uid) + { + $result = []; + if ($uid) { + $spread = $this->dao->getColumn(['uid' => $uid], 'friends_uid'); + $sup_spread = $this->dao->getColumn(['friends_uid' => $uid], 'uid'); + $result = array_unique(array_merge($spread, $sup_spread)); + } + return $result; + } + + /** + * 获取好友 + * @param int $id + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFriendList(int $uid, string $field = 'uid,nickname,level,add_time') + { + $uids = $this->getFriendUids($uid); + $list = []; + $count = 0; + if ($uids) { + [$page, $limit] = $this->getPageValue(); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $list = $userServices->getList(['uid' => $uids], $field, $page, $limit); + /** @var SystemUserLevelServices $systemLevelServices */ + $systemLevelServices = app()->make(SystemUserLevelServices::class); + $systemLevelList = $systemLevelServices->getWhereLevelList([], 'id,name'); + if ($systemLevelList) $systemLevelList = array_combine(array_column($systemLevelList, 'id'), $systemLevelList); + foreach ($list as &$item) { + $item['type'] = $systemLevelList[$item['level']]['name'] ?? '暂无'; + $item['add_time'] = $item['add_time'] && is_numeric($item['add_time']) ? date('Y-m-d H:i:s', $item['add_time']) : ''; + } + $count = $userServices->count(['uid' => $uids]); + } + + return compact('list', 'count'); + } + +} diff --git a/app/services/user/UserIntegralServices.php b/app/services/user/UserIntegralServices.php new file mode 100644 index 0000000..c13928e --- /dev/null +++ b/app/services/user/UserIntegralServices.php @@ -0,0 +1,244 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\user; + +use app\dao\user\UserBillDao; +use app\jobs\user\UserIntegralJob; +use app\services\BaseServices; + +/** + * 用户积分 + * Class UserIntegralServices + * @package app\services\user + * @mixin UserBillDao + */ +class UserIntegralServices extends BaseServices +{ + + /** + * UserIntegralServices constructor. + * @param UserBillDao $dao + */ + public function __construct(UserBillDao $dao) + { + $this->dao = $dao; + } + + /** + * 清空到期积分(分批加入队列) + * @return bool + */ + public function clearExpireIntegral() + { + //是否开启积分有效期 + if (!sys_config('integral_effective_status', 0)) { + return true; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $users = $userServices->getColumn([['integral', '>', 0]], 'uid'); + if ($users) { + //拆分大数组 + $uidsArr = array_chunk($users, 100); + foreach ($uidsArr as $uids) { + //加入同步|更新用户队列 + UserIntegralJob::dispatch([$uids]); + } + } + return true; + } + + /** + * 执行清空到期积分 + * @param array $uids + * @return bool + */ + public function doClearExpireIntegral(array $uids) + { + if (!$uids) return true; + //是否开启积分有效期 + if (!sys_config('integral_effective_status', 0)) { + return true; + } + [$clear_time, $start_time, $end_time] = $this->getTime(); + $start = date('Y年m月d日', $start_time); + $end = date('Y年m月d日', $end_time); + $where = ['category' => 'integral', 'pm' => 1, 'status' => 1]; + $where['add_time'] = [$start_time, $end_time]; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $users = $userServices->getColumn([['uid', 'in', $uids]], 'uid,integral', 'uid'); + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + //查询是否清除过 + $clear_uids = $userBillServices->getColumn([['category', '=', 'integral'], ['type', '=', 'system_clear'], ['add_time', '>=', $clear_time], ['add_time', '<=', $clear_time + 86400]], 'uid'); + foreach ($uids as $uid) { + $number = 0; + if (!isset($users[$uid]) || in_array($uid, $clear_uids)) continue; + $user = $users[$uid]; + $where['uid'] = $uid; + $where['pm'] = 1; + $userSumIntegralInc = $userBillServices->getBillSum($where); + $where['pm'] = 0; + $userSumIntegralDec = $userBillServices->getBillSum($where); + $userSumIntegral = $userSumIntegralInc > $userSumIntegralDec ? (int)bcsub((string)$userSumIntegralInc, (string)$userSumIntegralDec, 0) : 0; + if ($userSumIntegral) { + $user_data = []; + $number = $userSumIntegral; + if ($userSumIntegral >= $user['integral']) { + $number = $user['integral']; + $user_data['integral'] = 0; + } else { + $user_data['integral'] = (int)bcsub((string)$user['integral'], (string)$userSumIntegral, 0); + } + //记录清除积分 + $userBillServices->income('system_clear_integral', $uid, (int)$number, (int)$user_data['integral'], $uid); + $userServices->update($uid, $user_data); + } + } + return true; + } + + /** + * 获取用户清空到期积分 + * @param int $uid + * @param array $user + * @return array|int[] + */ + public function getUserClearIntegral(int $uid, $user = []) + { + if (!$uid) return [0, 0]; + //是否开启积分有效期 + if (!sys_config('integral_effective_status', 0)) { + return [0, 0]; + } + [$clear_time, $start_time, $end_time] = $this->getTime(); + + $where = ['category' => 'integral', 'pm' => 1, 'status' => 1]; + $where['add_time'] = [$start_time, $end_time]; + if (!$user) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->get($uid, 'uid,integral'); + } + + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $where['uid'] = $uid; + $userSumIntegralInc = $userBillServices->getBillSum($where); + $where['pm'] = 0; + $userSumIntegralDec = $userBillServices->getBillSum($where); + $userSumIntegral = $userSumIntegralInc > $userSumIntegralDec ? (int)bcsub((string)$userSumIntegralInc, (string)$userSumIntegralDec, 0) : 0; + if ($userSumIntegral) { + if ($userSumIntegral >= $user['integral']) { + $userSumIntegral = $user['integral']; + } + } + return [$userSumIntegral, $clear_time]; + } + + + /** + * 获取清空积分时间段 + * @param int $type + * @return int[] + */ + public function getTime(int $type = 0) + { + if (!$type) $type = (int)sys_config('integral_effective_time', 3); + switch ($type) { + case 1://月 + $start = date('Y-m-01 00:00:00', strtotime('-1 month')); + $end = date("Y-m-d 23:59:59", strtotime(-date('d') . 'day')); + $clear_end = date('Y-m-t', strtotime(date('Y-m-d'))); + break; + case 2://季度 + $season = ceil((date('n')) / 3) - 1;//上季度是第几季度 + $start = date('Y-m-d 00:00:00', mktime(0, 0, 0, $season * 3 - 3 + 1, 1, date('Y'))); + $end = date('Y-m-d 23:59:59', mktime(23, 59, 59, $season * 3, date('t', mktime(0, 0, 0, $season * 3, 1, date("Y"))), date('Y'))); + $clear_end = date('Y-m-t', mktime(0, 0, 0, ($season + 1) * 3, 1, date('Y'))); + break; + case 3://年 + default://默认年 + $start = date('Y-01-01 00:00:00', strtotime('-1 year')); + $end = date('Y-m-d 23:59:59', strtotime($start . "+12 month -1 day")); + $clear_end = date('Y-12-31'); + break; + } + return [strtotime($clear_end), strtotime($start), strtotime($end)]; + } + + /** + * 新人礼赠送积分 + * @param int $uid + * @return bool + */ + public function newcomerGiveIntegral(int $uid) + { + if (!sys_config('newcomer_status')) { + return false; + } + $status = sys_config('register_integral_status'); + if (!$status) {//未开启 + return true; + } + $integral = (int)sys_config('register_give_integral', []); + if (!$integral) { + return true; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + return true; + } + $balance = bcadd((string)$userInfo['integral'], (string)$integral, 2); + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $userBillServices->income('newcomer_give_integral', $uid, (int)$integral, (int)$balance); + $userServices->update($uid, ['integral' => $balance]); + return true; + } + + /** + * 会员卡激活赠送积分 + * @param int $uid + * @return bool + */ + public function levelGiveIntegral(int $uid) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + return true; + } + $status = sys_config('level_activate_status'); + if (!$status) {//是否需要激活 + return true; + } + $status = sys_config('level_integral_status'); + if (!$status) {//未开启 + return true; + } + $integral = (int)sys_config('level_give_integral', []); + if (!$integral) { + return true; + } + $balance = bcadd((string)$userInfo['integral'], (string)$integral, 2); + /** @var UserBillServices $userBillServices */ + $userBillServices = app()->make(UserBillServices::class); + $userBillServices->income('level_give_integral', $uid, (int)$integral, (int)$balance); + $userServices->update($uid, ['integral' => $balance]); + return true; + } +} diff --git a/app/services/user/UserInvoiceServices.php b/app/services/user/UserInvoiceServices.php new file mode 100644 index 0000000..78bc47b --- /dev/null +++ b/app/services/user/UserInvoiceServices.php @@ -0,0 +1,212 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + + +use app\dao\user\UserInvoiceDao; +use app\services\BaseServices; +use think\exception\ValidateException; + + +/** + * Class UserInvoiceServices + * @package app\services\user + * @mixin UserInvoiceDao + */ +class UserInvoiceServices extends BaseServices +{ + /** + * LiveAnchorServices constructor. + * @param UserInvoiceDao $dao + */ + public function __construct(UserInvoiceDao $dao) + { + $this->dao = $dao; + } + + /** + * 检测系统设置发票功能 + * @param bool $is_speclial + * @return bool|bool[] + */ + public function invoiceFuncStatus(bool $is_speclial = true) + { + $invoice = (bool)sys_config('invoice_func_status', 0); + if ($is_speclial) { + $specialInvoice = (bool)sys_config('special_invoice_status', 0); + return ['invoice_func' => $invoice, 'special_invoice' => $invoice && $specialInvoice]; + } + return $invoice; + } + + /** + * 获取单个发票信息 + * @param int $id + * @param int $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getInvoice(int $id, int $uid = 0) + { + $invoice = $this->dao->getOne(['id' => $id, 'is_del' => 0]); + if (!$invoice || ($uid && $invoice['uid'] != $uid)) { + return []; + } + return $invoice->toArray(); + } + + /** + * 检测该发票是否可用 + * @param int $id + * @param int $uid + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function checkInvoice(int $id, int $uid) + { + $invoice = $this->getInvoice($id, $uid); + if (!$invoice) { + throw new ValidateException('发票不存在或删除'); + } + $invoice_func = $this->invoiceFuncStatus(); + if (!$invoice_func['invoice_func']) { + throw new ValidateException('暂未开启开票,请联系管理员'); + } + //专用发票 + if ($invoice['type'] == 2) { + if (!$invoice_func['special_invoice']) { + throw new ValidateException('暂未开启专用发票,请联系管理员'); + } + } + return $invoice; + } + + + /** + * 获取某个用户发票列表 + * @param int $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserList(int $uid, $where) + { + [$page, $limit] = $this->getPageValue(); + $where['is_del'] = 0; + $where['uid'] = $uid; + return $this->dao->getList($where, '*', $page, $limit); + } + + /** + * 获取某个用户默认发票 + * @param int $uid + * @param string $field + * @return array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserDefaultInvoice(int $uid, int $type, string $field = '*') + { + return $this->dao->getOne(['uid' => $uid, 'is_default' => 1, 'is_del' => 0, 'type' => $type], $field); + } + + /** + * 添加|修改 + * @param int $uid + * @param array $data + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveInvoice(int $uid, array $data) + { + $id = (int)$data['id']; + $data['uid'] = $uid; + unset($data['id']); + $invoice = $this->dao->get(['uid' => $uid, 'name' => $data['name'], 'drawer_phone' => $data['drawer_phone'], 'is_del' => 0]); + if ($id) { + if ($invoice && $id != $invoice['id']) { + throw new ValidateException('该发票已经存在'); + } + if ($this->dao->update($id, $data, 'id')) { + if ($data['is_default']) { + $this->setDefaultInvoice($uid, $id); + } + return ['type' => 'edit', 'msg' => '修改发票成功', 'data' => []]; + } else { + throw new ValidateException('修改失败或者您没有修改什么'); + } + } else { + if ($invoice) { + throw new ValidateException('该发票已经存在'); + } + if ($add_invoice = $this->dao->save($data)) { + $id = (int)$add_invoice['id']; + if ($data['is_default']) { + $this->setDefaultInvoice($uid, $id); + } + return ['type' => 'add', 'msg' => '添加发票成功', 'data' => ['id' => $id]]; + } else { + throw new ValidateException('添加失败'); + } + } + } + + /** + * 设置默认发票 + * @param int $id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setDefaultInvoice(int $uid, int $id) + { + if (!$invoice = $this->getInvoice($id)) { + throw new ValidateException('发票不存在'); + } + if ($invoice['uid'] != $uid) { + throw new ValidateException('数据错误'); + } + if (!$this->dao->setDefault($uid, $id, $invoice['header_type'], $invoice['type'])) { + throw new ValidateException('设置默认发票失败'); + } + return true; + } + + /** + * 删除 + * @param $id + * @throws \Exception + */ + public function delInvoice(int $uid, int $id) + { + if ($invoice = $this->getInvoice($id)) { + if ($invoice['uid'] != $uid) { + throw new ValidateException('数据错误'); + } + if (!$this->dao->update($id, ['is_del' => 1])) { + throw new ValidateException('删除失败,请稍候再试!'); + } + } + return true; + } + +} diff --git a/app/services/user/UserMoneyServices.php b/app/services/user/UserMoneyServices.php new file mode 100644 index 0000000..47a7a2c --- /dev/null +++ b/app/services/user/UserMoneyServices.php @@ -0,0 +1,568 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\dao\user\UserMoneyDao; +use app\services\BaseServices; +use crmeb\services\CacheService; +use crmeb\exceptions\AdminException; + +/** + * 用户余额 + * Class UserMoneyServices + * @package app\services\user + * @mixin UserMoneyDao + */ +class UserMoneyServices extends BaseServices +{ + + + /** + * 用户记录模板 + * @var array[] + */ + protected $incomeData = [ + 'pay_product' => [ + 'title' => '余额支付购买商品', + 'type' => 'pay_product', + 'mark' => '余额支付{%num%}元购买商品', + 'status' => 1, + 'pm' => 0 + ], + 'pay_product_refund' => [ + 'title' => '商品退款', + 'type' => 'pay_product_refund', + 'mark' => '订单余额退款{%num%}元', + 'status' => 1, + 'pm' => 1 + ], + 'system_add' => [ + 'title' => '系统增加余额', + 'type' => 'system_add', + 'mark' => '系统增加{%num%}余额', + 'status' => 1, + 'pm' => 1 + ], + 'system_sub' => [ + 'title' => '系统减少余额', + 'type' => 'system_sub', + 'mark' => '系统扣除了{%num%}余额', + 'status' => 1, + 'pm' => 0 + ], + 'user_recharge' => [ + 'title' => '用户充值余额', + 'type' => 'recharge', + 'mark' => '成功充值余额{%price%}元,赠送{%give_price%}元', + 'status' => 1, + 'pm' => 1 + ], + 'user_recharge_refund' => [ + 'title' => '用户充值退款', + 'type' => 'recharge_refund', + 'mark' => '退款扣除用户余额{%num%}元', + 'status' => 1, + 'pm' => 0 + ], + 'brokerage_to_nowMoney' => [ + 'title' => '佣金提现到余额', + 'type' => 'extract', + 'mark' => '佣金提现到余额{%num%}元', + 'status' => 1, + 'pm' => 1 + ], + 'lottery_use_money' => [ + 'title' => '参与抽奖使用余额', + 'type' => 'lottery_use', + 'mark' => '参与抽奖使用{%num%}余额', + 'status' => 1, + 'pm' => 0 + ], + 'lottery_give_money' => [ + 'title' => '抽奖中奖赠送余额', + 'type' => 'lottery_add', + 'mark' => '抽奖中奖赠送{%num%}余额', + 'status' => 1, + 'pm' => 1 + ], + 'newcomer_give_money' => [ + 'title' => '新人礼赠送余额', + 'type' => 'newcomer_add', + 'mark' => '新人礼赠送{%num%}余额', + 'status' => 1, + 'pm' => 1 + ], + 'level_give_money' => [ + 'title' => '会员卡激活赠送余额', + 'type' => 'level_add', + 'mark' => '会员卡激活赠送{%num%}余额', + 'status' => 1, + 'pm' => 1 + ], + 'pay_integral_product' => [ + 'title' => '余额支付购买积分商品', + 'type' => 'pay_integral_product', + 'mark' => '余额支付{%num%}元购买积分商品', + 'status' => 1, + 'pm' => 0 + ], + ]; + + /** + * 类型名称 + * @var string[] + */ + protected $typeName = [ + 'pay_product' => '商城购物', + 'pay_product_refund' => '商城购物退款', + 'system_add' => '系统充值', + 'system_sub' => '系统扣除', + 'recharge' => '用户充值', + 'recharge_refund' => '用户充值退款', + 'extract' => '佣金提现充值', + 'lottery_use' => '抽奖使用', + 'lottery_add' => '抽奖中奖充值', + 'newcomer_add' => '新人礼赠送充值', + 'level_add' => '会员卡激活赠送充值' + ]; + + /** + * UserMoneyServices constructor. + * @param UserMoneyDao $dao + */ + public function __construct(UserMoneyDao $dao) + { + $this->dao = $dao; + } + + + /** + * 获取资金列表 + * @param array $where + * @param string $field + * @param int $limit + * @return array + */ + public function getMoneyList(array $where, string $field = '*', int $limit = 0) + { + $where_data = []; + if (isset($where['uid']) && $where['uid'] != '') { + $where_data['uid'] = $where['uid']; + } + if ($where['start_time'] != '' && $where['end_time'] != '') { + $where_data['time'] = str_replace('-', '/', $where['start_time']) . ' - ' . str_replace('-', '/', $where['end_time']); + } + if (isset($where['type']) && $where['type'] != '') { + $where_data['type'] = $where['type']; + } + if (isset($where['nickname']) && $where['nickname'] != '') { + $where_data['like'] = $where['nickname']; + } + if (isset($where['excel']) && $where['excel'] != '') { + $where_data['excel'] = $where['excel']; + } else { + $where_data['excel'] = 0; + } + if ($limit) { + [$page] = $this->getPageValue(); + } else { + [$page, $limit] = $this->getPageValue(); + } + $data = $this->dao->getList($where_data, $field, $page, $limit, [ + 'user' => function ($query) { + $query->field('uid,nickname'); + }]); + foreach ($data as &$item) { + $item['nickname'] = $item['user']['nickname'] ?? ''; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + unset($item['user']); + } + $count = $this->dao->count($where_data); + return compact('data', 'count'); + } + + /** + * 用户|所有资金变动列表 + * @param int $uid + * @param array $where_time + * @param string $field + * @return array + */ + public function getUserMoneyList(int $uid = 0, $where_time = [], string $field = '*') + { + [$page, $limit] = $this->getPageValue(); + $where = []; + if ($uid) $where['uid'] = $uid; + if ($where_time) $where['add_time'] = $where_time; + $list = $this->dao->getList($where, $field, $page, $limit); + $count = $this->dao->count($where); + foreach ($list as &$item) { + $value = array_filter($this->incomeData, function ($value) use ($item) { + if ($item['type'] == $value['type']) { + return $item['title']; + } + }); + $item['type_title'] = $value[$item['type']]['title'] ?? '未知类型'; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + } + return compact('list', 'count'); + } + + /** + * 获取用户的充值总数 + * @param int $uid + * @return float + */ + public function getRechargeSum(int $uid = 0, $time = []) + { + $where = ['uid' => $uid, 'pm' => 1, 'status' => 1, 'type' => ['system_add', 'recharge', 'extract', 'lottery_add', 'newcomer_add', 'level_add']]; + if ($time) $where['add_time'] = $time; + return $this->dao->sum($where, 'number', true); + } + + /** + * 用户|所有充值列表 + * @param int $uid + * @param string $field + * @return array + */ + public function getRechargeList(int $uid = 0, $where_time = [], string $field = '*') + { + [$page, $limit] = $this->getPageValue(); + $where = []; + if ($uid) $where['uid'] = $uid; + if ($where_time) $where['add_time'] = $where_time; + $list = $this->dao->getList($where, $field, $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 写入用户记录 + * @param string $type 写入类型 + * @param int $uid + * @param int|string|array $number + * @param int|string $balance + * @param int $link_id + * @return bool|mixed + */ + public function income(string $type, int $uid, $number, $balance, $link_id = 0) + { + $data = $this->incomeData[$type] ?? null; + if (!$data) { + return true; + } + $data['uid'] = $uid; + $data['balance'] = $balance ?? 0; + $data['link_id'] = $link_id; + if (is_array($number)) { + $key = array_keys($number); + $key = array_map(function ($item) { + return '{%' . $item . '%}'; + }, $key); + $value = array_values($number); + $data['number'] = $number['number'] ?? 0; + $data['mark'] = str_replace($key, $value, $data['mark']); + } else { + $data['number'] = $number; + $data['mark'] = str_replace(['{%num%}'], $number, $data['mark']); + } + $data['add_time'] = time(); + if ((float)$data['number']) { + return $this->dao->save($data); + } + return true; + } + + /** + * 资金类型 + * @return bool|mixed|null + */ + public function bill_type() + { + return CacheService::get('user_money_type_list', function () { + return ['list' => $this->dao->getMoneyType([])]; + }, 600); + } + + /** + * 用户余额记录列表 + * @param int $uid + * @param int $type + * @param array $data + * @return array + */ + public function userMoneyList(int $uid, int $type, array $data = []) + { + $where = []; + $where['uid'] = $uid; + switch ($type) { + case 1: + $where['pm'] = 0; + break; + case 2: + $where['pm'] = 1; + break; + case 0: + default: + break; + } + [$page, $limit] = $this->getPageValue(); + if ((isset($data['start']) && $data['start']) || (isset($data['stop']) && $data['stop'])) { + $where['time'] = [$data['start'], $data['stop']]; + } + $list = $this->dao->getList($where, '*', $page, $limit); + $count = $this->dao->count($where); + $times = []; + if ($list) { + $typeName = $this->typeName; + foreach ($list as &$item) { + $item['time_key'] = $item['time'] = $item['add_time'] ? date('Y-m', (int)$item['add_time']) : ''; + $item['day'] = $item['add_time'] ? date('Y-m-d', (int)$item['add_time']) : ''; + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i', (int)$item['add_time']) : ''; + $item['type_name'] = $typeName[$item['type'] ?? ''] ?? '未知类型'; + } + $times = array_merge(array_unique(array_column($list, 'time_key'))); + } + + return ['count' => $count, 'list' => $list, 'time' => $times]; + } + + /** + * 根据查询用户充值金额 + * @param array $where + * @param string $rechargeSumField + * @param string $selectType + * @param string $group + * @return float|mixed + */ + public function getRechargeMoneyByWhere(array $where, string $rechargeSumField, string $selectType, string $group = "") + { + switch ($selectType) { + case "sum" : + return $this->dao->getWhereSumField($where, $rechargeSumField); + case "group" : + return $this->dao->getGroupField($where, $rechargeSumField, $group); + } + } + + + /** + * 新人礼赠送余额 + * @param int $uid + * @return bool + */ + public function newcomerGiveMoney(int $uid) + { + if (!sys_config('newcomer_status')) { + return false; + } + $status = sys_config('register_money_status'); + if (!$status) {//未开启 + return true; + } + $money = (int)sys_config('register_give_money', []); + if (!$money) { + return true; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + return true; + } + $balance = bcadd((string)$userInfo['now_money'], (string)$money); + $this->income('newcomer_give_money', $uid, $money, $balance); + $userServices->update($uid, ['now_money' => $balance]); + return true; + } + + /** + * 会员卡激活赠送余额 + * @param int $uid + * @return bool + */ + public function levelGiveMoney(int $uid) + { + $status = sys_config('level_activate_status'); + if (!$status) {//是否需要激活 + return true; + } + $status = sys_config('level_money_status'); + if (!$status) {//未开启 + return true; + } + $money = (int)sys_config('level_give_money', []); + if (!$money) { + return true; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + if (!$userInfo) { + return true; + } + $balance = bcadd((string)$userInfo['now_money'], (string)$money); + $this->income('level_give_money', $uid, $money, $balance); + $userServices->update($uid, ['now_money' => $balance]); + return true; + } + + /** + * 余额统计基础 + * @param $where + * @return array + */ + public function getBasic($where) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $data['now_balance'] = $userServices->sum(['status' => 1], 'now_money', true); + $data['add_balance'] = $this->dao->sum(['pm' => 1], 'number', true); + $data['sub_balance'] = $this->dao->sum(['pm' => 0], 'number', true); + return $data; + } + + /** + * 余额趋势 + * @param $where + * @return array + */ + public function getTrend($where) + { + $time = explode('-', $where['time']); + if (count($time) != 2) throw new AdminException('参数错误'); + $dayCount = (strtotime($time[1]) - strtotime($time[0])) / 86400 + 1; + $data = []; + if ($dayCount == 1) { + $data = $this->trend($time, 0); + } elseif ($dayCount > 1 && $dayCount <= 31) { + $data = $this->trend($time, 1); + } elseif ($dayCount > 31 && $dayCount <= 92) { + $data = $this->trend($time, 3); + } elseif ($dayCount > 92) { + $data = $this->trend($time, 30); + } + return $data; + } + + /** + * 余额趋势 + * @param $time + * @param $num + * @param false $excel + * @return array + */ + public function trend($time, $num, $excel = false) + { + if ($num == 0) { + $xAxis = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']; + $timeType = '%H'; + } elseif ($num != 0) { + $dt_start = strtotime($time[0]); + $dt_end = strtotime($time[1]); + while ($dt_start <= $dt_end) { + if ($num == 30) { + $xAxis[] = date('Y-m', $dt_start); + $dt_start = strtotime("+1 month", $dt_start); + $timeType = '%Y-%m'; + } else { + $xAxis[] = date('m-d', $dt_start); + $dt_start = strtotime("+$num day", $dt_start); + $timeType = '%m-%d'; + } + } + } + $time[1] = date("Y-m-d", strtotime("+1 day", strtotime($time[1]))); + $point_add = array_column($this->dao->getBalanceTrend($time, $timeType, 'add_time', 'sum(number)', 'add'), 'num', 'days'); + $point_sub = array_column($this->dao->getBalanceTrend($time, $timeType, 'add_time', 'sum(number)', 'sub'), 'num', 'days'); + $data = $series = []; + foreach ($xAxis as $item) { + $data['余额积累'][] = isset($point_add[$item]) ? floatval($point_add[$item]) : 0; + $data['余额消耗'][] = isset($point_sub[$item]) ? floatval($point_sub[$item]) : 0; + } + foreach ($data as $key => $item) { + $series[] = [ + 'name' => $key, + 'data' => $item, + 'type' => 'line', + ]; + } + return compact('xAxis', 'series'); + } + + /** + * 余额来源 + * @param $where + * @return array + */ + public function getChannel($where) + { + $bing_xdata = ['系统增加', '用户充值', '佣金提现', '抽奖赠送', '商品退款']; + $color = ['#64a1f4', '#3edeb5', '#70869f', '#ffc653', '#fc7d6a']; + $data = ['system_add', 'recharge', 'extract', 'lottery_add', 'pay_product_refund']; + $bing_data = []; + foreach ($data as $key => $item) { + $bing_data[] = [ + 'name' => $bing_xdata[$key], + 'value' => $this->dao->sum(['pm' => 1, 'type' => $item, 'time' => $where['time']], 'number', true), + 'itemStyle' => ['color' => $color[$key]] + ]; + } + + $list = []; + $count = array_sum(array_column($bing_data, 'value')); + foreach ($bing_data as $key => $item) { + $list[] = [ + 'name' => $item['name'], + 'value' => $item['value'], + 'percent' => $count != 0 ? bcmul((string)bcdiv((string)$item['value'], (string)$count, 4), '100', 2) : 0, + ]; + } + array_multisort(array_column($list, 'value'), SORT_DESC, $list); + return compact('bing_xdata', 'bing_data', 'list'); + } + + /** + * 余额类型 + * @param $where + * @return array + */ + public function getType($where) + { + $bing_xdata = ['系统减少', '充值退款', '购买商品']; + $color = ['#64a1f4', '#3edeb5', '#70869f']; + $data = ['system_sub', 'recharge_refund', 'pay_product']; + $bing_data = []; + foreach ($data as $key => $item) { + $bing_data[] = [ + 'name' => $bing_xdata[$key], + 'value' => $this->dao->sum(['pm' => 0, 'type' => $item, 'time' => $where['time']], 'number', true), + 'itemStyle' => ['color' => $color[$key]] + ]; + } + + $list = []; + $count = array_sum(array_column($bing_data, 'value')); + foreach ($bing_data as $key => $item) { + $list[] = [ + 'name' => $item['name'], + 'value' => $item['value'], + 'percent' => $count != 0 ? bcmul((string)bcdiv((string)$item['value'], (string)$count, 4), '100', 2) : 0, + ]; + } + array_multisort(array_column($list, 'value'), SORT_DESC, $list); + return compact('bing_xdata', 'bing_data', 'list'); + } + +} diff --git a/app/services/user/UserRechargeServices.php b/app/services/user/UserRechargeServices.php new file mode 100644 index 0000000..d58ec9e --- /dev/null +++ b/app/services/user/UserRechargeServices.php @@ -0,0 +1,717 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\jobs\user\MicroPayOrderJob; +use app\services\BaseServices; +use app\common\dao\user\UserRechargeDao; +use app\services\pay\PayServices; +use app\services\pay\RechargeServices; +use app\services\store\finance\StoreFinanceFlowServices; +use app\services\system\config\SystemGroupDataServices; +use crmeb\exceptions\AdminException; +use crmeb\traits\ServicesTrait; +use crmeb\services\{AliPayService, FormBuilder as Form, wechat\Payment}; +use think\exception\ValidateException; +use think\facade\Route as Url; +use app\services\order\StoreOrderCreateServices; +use app\services\wechat\WechatUserServices; +// use app\webscoket\SocketPush; + +/** + * + * Class UserRechargeServices + * @package app\services\user + * @mixin UserRechargeDao + */ +class UserRechargeServices extends BaseServices +{ + + use ServicesTrait; + + /** + * UserRechargeServices constructor. + * @param UserRechargeDao $dao + */ + public function __construct(UserRechargeDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取单条数据 + * @param int $id + * @param array $field + */ + public function getRecharge(int $id, array $field = []) + { + return $this->dao->get($id, $field); + } + + /** + * @param int $storeId + * @return int + */ + public function getRechargeCount(int $storeId) + { + return $this->dao->count(['store_id' => $storeId, 'paid' => 1]); + } + + /** + * 获取统计数据 + * @param array $where + * @param string $field + * @return float + */ + public function getRechargeSum(array $where, string $field = '') + { + $whereData = []; + if (isset($where['data'])) { + $whereData['time'] = $where['data']; + } + if (isset($where['paid']) && $where['paid'] != '') { + $whereData['paid'] = $where['paid']; + } + if (isset($where['nickname']) && $where['nickname']) { + $whereData['like'] = $where['nickname']; + } + if (isset($where['recharge_type']) && $where['recharge_type']) { + $whereData['recharge_type'] = $where['recharge_type']; + } + if (isset($where['store_id'])) { + $whereData['store_id'] = $where['store_id']; + } + return $this->dao->getWhereSumField($whereData, $field); + } + + /** + * 获取充值列表 + * @param array $where + * @param string $field + * @param int $limit + * @return array + */ + public function getRechargeList(array $where, string $field = '*', int $limit = 0, array $with = []) + { + $whereData = $where; + if (isset($where['data'])) { + $whereData['time'] = $where['data']; + unset($whereData['data']); + } + if (isset($where['nickname']) && $where['nickname']) { + $whereData['like'] = $where['nickname']; + unset($whereData['nickname']); + } + if ($limit) { + [$page] = $this->getPageValue(); + } else { + [$page, $limit] = $this->getPageValue(); + } + $list = $this->dao->getList($whereData, $field, $page, $limit, $with); + $count = $this->dao->count($whereData); + + foreach ($list as &$item) { + switch ($item['recharge_type']) { + case 'routine': + $item['_recharge_type'] = '小程序充值'; + break; + case 'weixin': + $item['_recharge_type'] = '公众号充值'; + break; + case 'alipay': + $item['_recharge_type'] = '支付宝充值'; + break; + case 'balance': + $item['_recharge_type'] = '佣金转入'; + break; + case 'store': + $item['_recharge_type'] = '门店余额充值'; + break; + case 'offline' : + $item['_recharge_type'] = "线下支付"; + break; + case 'cash' : + $item['_recharge_type'] = "现金支付"; + break; + default: + $item['_recharge_type'] = '其他充值'; + break; + } + $item['_pay_time'] = $item['pay_time'] ? date('Y-m-d H:i:s', $item['pay_time']) : '暂无'; + $item['_add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : '暂无'; + $item['paid_type'] = $item['paid'] ? '已支付' : '未支付'; + unset($item['user']); + } + return compact('list', 'count'); + } + + /** + * 获取用户充值数据 + * @return array + */ + public function user_recharge(array $where) + { + $data = []; + $where['paid'] = 1; + $data['sumPrice'] = $this->getRechargeSum($where, 'price'); + $data['sumRefundPrice'] = $this->getRechargeSum($where, 'refund_price'); + $where['recharge_type'] = 'routine'; + $data['sumRoutinePrice'] = $this->getRechargeSum($where, 'price'); + $where['recharge_type'] = 'weixin'; + $data['sumWeixinPrice'] = $this->getRechargeSum($where, 'price'); + return [ + [ + 'name' => '充值总金额', + 'field' => '元', + 'count' => $data['sumPrice'], + 'className' => 'logo-yen', + 'col' => 6, + ], + [ + 'name' => '充值退款金额', + 'field' => '元', + 'count' => $data['sumRefundPrice'], + 'className' => 'logo-usd', + 'col' => 6, + ], + [ + 'name' => '小程序充值金额', + 'field' => '元', + 'count' => $data['sumRoutinePrice'], + 'className' => 'logo-bitcoin', + 'col' => 6, + ], + [ + 'name' => '公众号充值金额', + 'field' => '元', + 'count' => $data['sumWeixinPrice'], + 'className' => 'ios-bicycle', + 'col' => 6, + ], + ]; + } + + /** + * 退款表单 + * @param int $id + * @return mixed + */ + public function refund_edit(int $id) + { + $UserRecharge = $this->getRecharge($id); + if (!$UserRecharge) { + throw new AdminException('数据不存在!'); + } + if ($UserRecharge['paid'] != 1) { + throw new AdminException('订单未支付'); + } + if ($UserRecharge['price'] == $UserRecharge['refund_price']) { + throw new AdminException('已退完支付金额!不能再退款了'); + } + if ($UserRecharge['recharge_type'] == 'balance') { + throw new AdminException('佣金转入余额,不能退款'); + } + $f = array(); + $f[] = Form::input('order_id', '退款单号', $UserRecharge->getData('order_id'))->disabled(true); + $f[] = Form::radio('refund_price', '状态', 1)->options([['label' => '本金(扣赠送余额)', 'value' => 1], ['label' => '仅本金', 'value' => 0]]); +// $f[] = Form::number('refund_price', '退款金额', (float)$UserRecharge->getData('price'))->precision(2)->min(0)->max($UserRecharge->getData('price')); + if ($UserRecharge['store_id']) { + return create_form('退款', $f, Url::buildUrl('/order/recharge/' . $id), 'PUT'); + } else { + return create_form('退款', $f, Url::buildUrl('/finance/recharge/' . $id), 'PUT'); + } + } + + /** + * 充值退款操作 + * @param int $id + * @param $refund_price + * @return mixed + */ + public function refund_update(int $id, string $refund_price) + { + $UserRecharge = $this->getRecharge($id); + if (!$UserRecharge) { + throw new AdminException('数据不存在!'); + } + if ($UserRecharge['price'] == $UserRecharge['refund_price']) { + throw new AdminException('已退完支付金额!不能再退款了'); + } + if ($UserRecharge['recharge_type'] == 'balance') { + throw new AdminException('佣金转入余额,不能退款'); + } + $UserRecharge = $UserRecharge->toArray(); + +// $data['refund_price'] = bcadd($refund_price, $UserRecharge['refund_price'], 2); + $data['refund_price'] = $UserRecharge['price']; +// $bj = bccomp((string)$UserRecharge['price'], (string)$data['refund_price'], 2); +// if ($bj < 0) { +// throw new AdminException('退款金额大于支付金额,请修改退款金额'); +// } + $refund_data['pay_price'] = $UserRecharge['price']; + $refund_data['refund_price'] = $UserRecharge['price']; +// $refund_data['refund_account']='REFUND_SOURCE_RECHARGE_FUNDS'; + if ($refund_price == 1) { + $number = bcadd($UserRecharge['price'], $UserRecharge['give_price'], 2); + } else { + $number = $UserRecharge['price']; + } + + try { + $recharge_type = $UserRecharge['recharge_type']; + if ($recharge_type == 'alipay') { + mt_srand(); + $refund_id = $refundData['refund_id'] ?? $UserRecharge['order_id'] . rand(100, 999); + //支付宝退款 + AliPayService::instance()->refund($UserRecharge['order_id'], $refund_data['refund_price'], $refund_id); + } else if ($recharge_type == 'weixin') { + //判断是不是小程序支付 TODO 之后可根据订单判断 + $pay_routine_open = (bool)sys_config('pay_routine_open', 0); + if ($pay_routine_open) { + $refund_data['refund_no'] = $UserRecharge['order_id']; // 退款订单号 + /** @var WechatUserServices $wechatUserServices */ + $wechatUserServices = app()->make(WechatUserServices::class); + $refund_data['open_id'] = $wechatUserServices->value(['uid' => (int)$UserRecharge['uid']], 'openid'); + $refund_data['routine_order_id'] = $UserRecharge['order_id']; + $refund_data['pay_routine_open'] = true; + } + $transaction_id = $UserRecharge['trade_no']; + if (!$transaction_id) { + $refund_data['type'] = 'out_trade_no'; + $transaction_id = $UserRecharge['order_id']; + } else { + $refund_data['type'] = 'transaction_id'; + } + Payment::instance()->setAccessEnd(Payment::WEB)->payOrderRefund($transaction_id, $refund_data); + } else { + $transaction_id = $UserRecharge['trade_no']; + if (!$transaction_id) { + $refund_data['type'] = 'out_trade_no'; + $transaction_id = $UserRecharge['order_id']; + } else { + $refund_data['type'] = 'transaction_id'; + } + Payment::instance()->setAccessEnd(Payment::MINI)->payOrderRefund($transaction_id, $refund_data); + } + } catch (\Exception $e) { + throw new AdminException($e->getMessage()); + } + if (!$this->dao->update($id, $data)) { + throw new AdminException('修改提现数据失败'); + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo((int)$UserRecharge['uid']); + if ($userInfo['now_money'] > $number) { + $now_money = bcsub((string)$userInfo['now_money'], $number, 2); + } else { + $number = $userInfo['now_money']; + $now_money = 0; + } + //修改用户余额 + $userServices->update((int)$UserRecharge['uid'], ['now_money' => $now_money], 'uid'); + $UserRecharge['nickname'] = $userInfo['nickname']; + $UserRecharge['phone'] = $userInfo['phone']; + $UserRecharge['now_money'] = $userInfo['now_money']; + + /** @var UserMoneyServices $userMoneyServices */ + $userMoneyServices = app()->make(UserMoneyServices::class); + //保存余额记录 + $userMoneyServices->income('user_recharge_refund', $UserRecharge['uid'], $number, $now_money, $id); + + //充值退款事件 + event('user.rechargeRefund', [$UserRecharge, $data]); + return true; + } + + /** + * 删除 + * @param int $id + * @return bool + */ + public function delRecharge(int $id) + { + $rechargInfo = $this->getRecharge($id); + if (!$rechargInfo) throw new AdminException('订单未找到'); + if ($rechargInfo->paid) { + throw new AdminException('已支付的订单记录无法删除'); + } + if ($this->dao->delete($id)) + return true; + else + throw new AdminException('删除失败'); + } + + /** + * 生成充值订单号 + * @return bool|string + */ + public function getOrderId() + { + return 'wx' . date('YmdHis', time()) . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8); + } + + /** + * 导入佣金到余额 + * @param int $uid + * @param $price + * @return bool + */ + public function importNowMoney(int $uid, $price) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('数据不存在'); + } + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $broken_commission = $userBrokerageServices->getUserFrozenPrice($uid); + $commissionCount = bcsub((string)$user['brokerage_price'], (string)$broken_commission, 2); + if ($price > $commissionCount) { + throw new ValidateException('转入金额不能大于可提现佣金!'); + } + return $this->transaction(function () use ($uid, $user, $price, $userServices) { + $edit_data = []; + $edit_data['now_money'] = bcadd((string)$user['now_money'], (string)$price, 2); + $edit_data['brokerage_price'] = $user['brokerage_price'] > $price ? bcsub((string)$user['brokerage_price'], (string)$price, 2) : 0; + //修改用户佣金、余额信息 + $userServices->update($uid, $edit_data, 'uid'); + //写入充值记录 + $rechargeInfo = [ + 'uid' => $uid, + 'order_id' => $this->getOrderId(), + 'recharge_type' => 'balance', + 'price' => $price, + 'give_price' => 0, + 'paid' => 1, + 'pay_time' => time(), + 'add_time' => time() + ]; + //写入充值记录 + $re = $this->dao->save($rechargeInfo); + /** @var UserMoneyServices $userMoneyServices */ + $userMoneyServices = app()->make(UserMoneyServices::class); + //余额记录 + $userMoneyServices->income('brokerage_to_nowMoney', $uid, $price, $edit_data['now_money'], $re['id']); + $extractInfo = [ + 'uid' => $uid, + 'real_name' => $user['nickname'], + 'extract_type' => 'balance', + 'extract_price' => $price, + 'balance' => $edit_data['brokerage_price'], + 'add_time' => time(), + 'status' => 1 + ]; + /** @var UserExtractServices $userExtract */ + $userExtract = app()->make(UserExtractServices::class); + //写入提现记录 + $userExtract->save($extractInfo); + //佣金提现记录 + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $userBrokerageServices->income('brokerage_to_nowMoney', $uid, $price, $edit_data['brokerage_price'], $re['id']); + }); + } + + /** + * 申请充值 + * @param int $uid + * @param $price + * @param $recharId + * @param $type + * @param $from + * @param array $staffinfo + * @param string $authCode 扫码code + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + */ + public function recharge(int $uid, $price, $recharId, $type, $from, array $staffinfo = [], string $authCode = '') + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('数据不存在'); + } + $paid_price = 0; + if ($recharId) { + /** @var SystemGroupDataServices $systemGroupData */ + $systemGroupData = app()->make(SystemGroupDataServices::class); + $data = $systemGroupData->getDateValue($recharId); + if (!$data) { + return app('json')->fail('您选择的充值方式已下架!'); + } else { + $paid_price = $data['give_money'] ?? 0; + } + $price = $data['price']; + } + switch ((int)$type) { + case 0: //支付充值余额 + /** @var StoreOrderCreateServices $orderCreateServices */ + $orderCreateServices = app()->make(StoreOrderCreateServices::class); + $recharge_data = []; + $recharge_data['order_id'] = $orderCreateServices->getNewOrderId('cz'); + $recharge_data['uid'] = $uid; + $recharge_data['price'] = $price; + $recharge_data['recharge_type'] = $from; + $recharge_data['paid'] = 0; + $recharge_data['add_time'] = time(); + $recharge_data['give_price'] = $paid_price; + $recharge_data['channel_type'] = $user['user_type']; + if (!$rechargeOrder = $this->dao->save($recharge_data)) { + throw new ValidateException('充值订单生成失败'); + } + try { + /** @var RechargeServices $recharge */ + $recharge = app()->make(RechargeServices::class); + $order_info = $recharge->recharge((int)$rechargeOrder->id); + } catch (\Exception $e) { + throw new ValidateException($e->getMessage()); + } + return ['msg' => '', 'type' => $from, 'data' => $order_info]; + break; + case 1: //佣金转入余额 + $this->importNowMoney($uid, $price); + return ['msg' => '转入余额成功', 'type' => $from, 'data' => []]; + break; + case 2://门店充值-用户扫码付款 + case 3://门店充值-付款码付款 + if (!$staffinfo) { + throw new ValidateException('请稍后重试'); + } + $recharge_data = []; + $recharge_data['order_id'] = $this->getOrderId(); + $recharge_data['uid'] = $uid; + $recharge_data['store_id'] = $staffinfo['store_id']; + $recharge_data['staff_id'] = $staffinfo['id']; + $recharge_data['price'] = $price; + //自动判定支付方式 + if ($authCode) { + $recharge_data['auth_code'] = $authCode; + if (Payment::isWechatAuthCode($authCode)) { + $recharge_data['recharge_type'] = PayServices::WEIXIN_PAY; + } else if (AliPayService::isAliPayAuthCode($authCode)) { + $recharge_data['recharge_type'] = PayServices::ALIAPY_PAY; + } else { + throw new ValidateException('付款二维码错误'); + } + } else { + $recharge_data['recharge_type'] = $from; + } + $recharge_data['paid'] = 0; + $recharge_data['add_time'] = time(); + $recharge_data['give_price'] = $paid_price; + $recharge_data['channel_type'] = $user['user_type']; + if (!$rechargeOrder = $this->dao->save($recharge_data)) { + throw new ValidateException('充值订单生成失败'); + } + try { + /** @var RechargeServices $recharge */ + $recharge = app()->make(RechargeServices::class); + $order_info = $recharge->recharge((int)$rechargeOrder->id, $authCode); + if ($type === 3) { + if ($order_info['paid'] === 1) { + //修改支付状态 + $this->rechargeSuccess($recharge_data['order_id']); + return [ + 'msg' => $order_info['message'], + 'status' => 'SUCCESS', + 'type' => $from, + 'payInfo' => [], + 'data' => [ + 'jsConfig' => [], + 'order_id' => $recharge_data['order_id'] + ] + ]; + } else { + //发起支付但是还没有支付,需要在5秒后查询支付状态 + if ($recharge_data['recharge_type'] === PayServices::WEIXIN_PAY) { + if (isset($order_info['payInfo']['err_code']) && in_array($order_info['payInfo']['err_code'], ['AUTH_CODE_INVALID', 'NOTENOUGH'])) { + return ['status' => 'ERROR', 'msg' => '支付失败', 'payInfo' => $order_info]; + } + $secs = 5; + if (isset($order_info['payInfo']['err_code']) && $order_info['payInfo']['err_code'] === 'USERPAYING') { + $secs = 10; + } + MicroPayOrderJob::dispatchSece($secs, [$recharge_data['order_id']]); + } + return [ + 'msg' => $order_info['message'] ?? '等待支付中', + 'status' => 'PAY_ING', + 'type' => $from, + 'payInfo' => $order_info, + 'data' => [ + 'jsConfig' => [], + 'order_id' => $recharge_data['order_id'] + ] + ]; + } + } + } catch (\Exception $e) { + \think\facade\Log::error('充值失败,原因:' . $e->getMessage()); + throw new ValidateException('充值失败:' . $e->getMessage()); + } + return ['msg' => '', 'status' => 'PAY', 'type' => $from, 'data' => ['jsConfig' => $order_info, 'order_id' => $recharge_data['order_id']]]; + break; + case 4: //现金支付 + if (!$staffinfo) { + throw new ValidateException('请稍后重试'); + } + /** @var StoreOrderCreateServices $orderCreateServices */ + $orderCreateServices = app()->make(StoreOrderCreateServices::class); + $recharge_data = []; + $recharge_data['order_id'] = $orderCreateServices->getNewOrderId('cz'); + $recharge_data['uid'] = $uid; + $recharge_data['price'] = $price; + $recharge_data['store_id'] = $staffinfo['store_id']; + $recharge_data['staff_id'] = $staffinfo['id']; + $recharge_data['recharge_type'] = PayServices::CASH_PAY; + $recharge_data['paid'] = 0; + $recharge_data['add_time'] = time(); + $recharge_data['give_price'] = $paid_price; + $recharge_data['channel_type'] = $user['user_type']; + if (!$rechargeOrder = $this->dao->save($recharge_data)) { + throw new ValidateException('充值订单生成失败'); + } + try { + //修改支付状态 + $this->rechargeSuccess($recharge_data['order_id']); + } catch (\Exception $e) { + throw new ValidateException($e->getMessage()); + } + return ['msg' => '','status' => 'SUCCESS', 'type' => $from, 'data' => ['order_id' => $recharge_data['order_id']]]; + break; + default: + throw new ValidateException('缺少参数'); + break; + } + } + + /** + * 用户充值成功 + * @param $orderId + * @return bool + * @throws \think\Exception + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function rechargeSuccess($orderId, array $other = []) + { + $order = $this->dao->getOne(['order_id' => $orderId, 'paid' => 0]); + if (!$order) { + throw new ValidateException('订单失效或者不存在'); + } + $order = $order->toArray(); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo((int)$order['uid']); + if (!$user) { + throw new ValidateException('数据不存在'); + } + $price = bcadd((string)$order['price'], (string)$order['give_price'], 2); + if (!$this->dao->update($order['id'], ['paid' => 1, 'pay_time' => time(), 'trade_no' => $other['trade_no'] ?? ''], 'id')) { + throw new ValidateException('修改订单失败'); + } + $now_money = bcadd((string)$user['now_money'], (string)$price, 2); + /** @var UserMoneyServices $userMoneyServices */ + $userMoneyServices = app()->make(UserMoneyServices::class); + $userMoneyServices->income('user_recharge', $user['uid'], ['number' => $price, 'price' => $order['price'], 'give_price' => $order['give_price']], $now_money, $order['id']); + if (!$userServices->update((int)$order['uid'], ['now_money' => $now_money], 'uid')) { + throw new ValidateException('修改用户信息失败'); + } + + //记录流水账单 + if ($order['store_id'] > 0) { + $data = $order; + $data['pay_time'] = time(); + /** @var StoreFinanceFlowServices $storeFinanceFlowServices */ + $storeFinanceFlowServices = app()->make(StoreFinanceFlowServices::class); + $storeFinanceFlowServices->setFinance($data, 2); + } + $order['nickname'] = $user['nickname']; + $order['phone'] = $user['phone']; + + if ($order['staff_id']) { + //发送消息 + try { + // SocketPush::instance()->to($order['staff_id'])->setUserType('cashier')->type('changUser')->data(['uid' => $user['uid']])->push(); + } catch (\Throwable $e) { + } + } + + //用户充值成功事件 + event('user.recharge', [$order, $now_money]); + return true; + } + + /** + * 根据查询用户充值金额 + * @param array $where + * @return float|int + */ + public function getRechargeMoneyByWhere(array $where, string $rechargeSumField, string $selectType, string $group = "") + { + switch ($selectType) { + case "sum" : + return $this->dao->getWhereSumField($where, $rechargeSumField); + case "group" : + return $this->dao->getGroupField($where, $rechargeSumField, $group); + } + } + + /** + * 充值每日统计数据 + * @param int $store_id + * @param int $staff_id + * @param array $time + * @return array + */ + public function getDataPriceCount(int $store_id, int $staff_id = 0, $time = []) + { + [$page, $limit] = $this->getPageValue(); + $where = ['store_id' => $store_id, 'paid' => 1, 'time' => $time]; + if ($staff_id) { + $where['staff_id'] = $staff_id; + } + return $this->dao->getDataPriceCount($where, ['sum(price) as price', 'count(id) as count', 'FROM_UNIXTIME(add_time, \'%m-%d\') as time'], $page, $limit); + } + + /** + * 门店充值统计详情列表 + * @param int $store_id + * @param int $staff_id + * @param array $time + * @return array|array[] + */ + public function time(int $store_id, int $staff_id, array $time = []) + { + if (!$time) { + return [[], []]; + } + [$start, $stop, $front, $front_stop] = $time; + $where = ['store_id' => $store_id, 'paid' => 1]; + if ($staff_id) $where['staff_id'] = $staff_id; + $frontPrice = $this->dao->sum($where + ['time' => [$front, $front_stop]], 'price', true); + $nowPrice = $this->dao->sum($where + ['time' => [$start, $stop]], 'price', true); + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where + ['time' => [$start, $stop]], 'id,uid,order_id,price,add_time', $page, $limit); + foreach ($list as &$item) { + $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i:s', $item['add_time']) : ''; + } + return [[$nowPrice, $frontPrice], $list]; + } +} diff --git a/app/services/user/UserRelationServices.php b/app/services/user/UserRelationServices.php new file mode 100644 index 0000000..be4514b --- /dev/null +++ b/app/services/user/UserRelationServices.php @@ -0,0 +1,261 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\user; + + +use app\dao\user\UserRelationDao; +use app\jobs\product\ProductLogJob; +use app\services\BaseServices; +use app\services\product\product\StoreProductServices; +use crmeb\traits\ServicesTrait; +use think\db\exception\DataNotFoundException; +use think\db\exception\DbException; +use think\db\exception\ModelNotFoundException; +use think\exception\ValidateException; + +/** + * Class UserRelationServices + * @package app\services\user + * @mixin UserRelationDao + */ +class UserRelationServices extends BaseServices +{ + use ServicesTrait; + + const CATEGORY_PRODUCT = 'product';//商品 + const CATEGORY_REPLY = 'reply';//评价 + const CATEGORY_COMMENT = 'comment';//评价回复 + const CATEGORY_VIDEO = 'video';//短视频 + const CATEGORY_VIDEO_COMMENT = 'video_comment';//短视频评价 + + const TYPE_COLLECT = 'collect';//收藏 + const TYPE_LIKE = 'like';//点赞 + const TYPE_SHARE = 'share';//分享 + const TYPE_PLAY = 'play';//播放 + + const TYPE_NAME = [ + 'collect' => '收藏', + 'like' => '点赞', + 'share' => '分享', + 'play' => '播放', + ]; + + + /** + * UserRelationServices constructor. + * @param UserRelationDao $dao + */ + public function __construct(UserRelationDao $dao) + { + $this->dao = $dao; + } + + /** + * 用户是否点赞或收藏商品 + * @param array $where + * @return bool + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function isProductRelation(array $where) + { + $res = $this->dao->getOne($where); + if ($res) { + return true; + } else { + return false; + } + } + + /** + * @param array $where + * @return mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/17 + */ + public function isProductRelationCache(array $where) + { + return $this->cacheTag()->remember(md5(json_encode($where)), function () use ($where) { + return $this->isProductRelation($where); + }); + } + + /** + * 获取用户收藏数量 + * @param int $uid + * @param int $relationId + * @param string $type + * @param string $category + * @return int + */ + public function getUserCount(int $uid, int $relationId = 0, string $type = self::TYPE_COLLECT, string $category = self::CATEGORY_PRODUCT) + { + $where = ['uid' => $uid]; + if ($type) { + $where['type'] = $type; + } + if ($category) { + $where['category'] = $category; + } + if ($relationId) { + $where['relation_id'] = $relationId; + } + return $this->dao->count($where); + } + + /** + * @param int $uid + * @return mixed + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function getUserRelationList(int $uid, string $category = self::CATEGORY_PRODUCT, string $type = self::TYPE_COLLECT) + { + $where['uid'] = $uid; + $where['type'] = $type; + $where['category'] = $category; + [$page, $limit] = $this->getPageValue(); + $with = []; + switch ($category) { + case 'product': + $with = ['product']; + break; + case 'video': + $with = ['video']; + break; + } + //短视频未启用 + if ($category == 'video' && !sys_config('video_func_status', 1)) { + $list = []; + } else { + $list = $this->dao->getList($where, 'relation_id,category', $with, $page, $limit); + } + $result = []; + foreach ($list as $k => $item) { + switch ($category) { + case 'product': + if (isset($item['product']) && isset($item['product']['id'])) { + $product = $item['product']; + $data = [ + 'id' => $product['id'] ?? 0, + 'product_id' => $item['relation_id'], + 'type' => $product['type'] ?? 0, + 'pid' => $product['pid'] ?? 0, + 'relation_id' => $product['relation_id'] ?? 0, + 'store_name' => $product['store_name'] ?? 0, + 'price' => $product['price'] ?? 0, + 'ot_price' => $product['ot_price'] ?? 0, + 'sales' => $product['sales'] ?? 0, + 'image' => get_thumb_water($product['image'] ?? 0), + 'is_del' => $product['is_del'] ?? 0, + 'is_show' => $product['is_show'] ?? 0, + 'is_fail' => ($product['is_del'] ?? 1) && !($product['is_show'] ?? 0), + 'activity' => $product['activity'] ?? '' + ]; + $result[] = $data; + } + break; + case 'video': + if (isset($item['video']) && isset($item['video']['id'])) { + $video = $item['video']; + $data = [ + 'id' => $video['id'] ?? 0, + 'video_id' => $item['relation_id'], + 'image' => $video['image'] ?? '', + 'desc' => $video['desc'] ?? '', + 'video_url' => $video['video_url'] ?? '', + 'like_num' => $video['like_num'] ?? 0 + ]; + $result[] = $data; + } + break; + } + } + if ($result && $category == 'product') { + /** @var StoreProductServices $productServices */ + $productServices = app()->make(StoreProductServices::class); + $result = $productServices->getActivityList($result); + $result = $productServices->getProductPromotions($result); + } + return $result; + } + + /** + * 添加点赞 收藏 + * @param int $uid + * @param array $productIds + * @param string $relationType + * @param string $category + * @return bool + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function productRelation(int $uid, array $productIds, string $relationType, string $category = self::CATEGORY_PRODUCT) + { + $relationType = strtolower($relationType); + $category = strtolower($category); + $relationId = $this->dao->getColumn([['uid', '=', $uid], ['relation_id', 'IN', $productIds], ['type', '=', $relationType], ['category', '=', $category]], 'relation_id'); + $data = ['uid' => $uid, 'add_time' => time(), 'type' => $relationType, 'category' => $category]; + $dataAll = []; + foreach ($productIds as $key => $product_id) { + if (in_array($product_id, $relationId)) { + continue; + } + $data['relation_id'] = $product_id; + $dataAll[] = $data; + } + if ($dataAll) { + if (!$this->dao->saveAll($dataAll)) { + throw new ValidateException('添加失败'); + } + } + if ($category == 'product') { + //收藏记录 + ProductLogJob::dispatch(['collect', ['uid' => $uid, 'relation_id' => $productIds, 'product_id' => $productIds]]); + } + + $this->cacheTag()->clear(); + + return true; + } + + /** + * 取消 点赞 收藏 + * @param int $uid + * @param array $productId + * @param string $relationType + * @param string $category + * @return bool + * @throws \Exception + */ + public function unProductRelation(int $uid, array $productId, string $relationType = self::TYPE_COLLECT, string $category = self::CATEGORY_PRODUCT) + { + $relationType = strtolower($relationType); + $category = strtolower($category); + $storeProductRelation = $this->dao->delete(['uid' => $uid, 'relation_id' => $productId, 'type' => $relationType, 'category' => $category]); + if ($category == 'video') { + foreach ($productId as $id) { + $this->dao->bcDec($id, $relationType. '_num', 1); + } + } + if (!$storeProductRelation) throw new ValidateException('取消失败'); + + $this->cacheTag()->clear(); + + return true; + } + +} diff --git a/app/services/user/UserSearchServices.php b/app/services/user/UserSearchServices.php new file mode 100644 index 0000000..5236400 --- /dev/null +++ b/app/services/user/UserSearchServices.php @@ -0,0 +1,123 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\services\BaseServices; +use app\services\product\product\StoreProductServices; +use app\dao\user\UserSearchDao; + +/** + * + * Class UserLabelServices + * @package app\services\user + * @mixin UserSearchDao + */ +class UserSearchServices extends BaseServices +{ + + /** + * UserSearchServices constructor. + * @param UserSearchDao $dao + */ + public function __construct(UserSearchDao $dao) + { + $this->dao = $dao; + } + + /** + * 分词搜索得到ids + * @param int $uid + * @param string $keyword + * @param array $where + * @return array + */ + public function vicSearch(int $uid, string $keyword, array $where) + { + //分词 + try { + $vicWordArr = \crmeb\services\VicWordService::instance()->getWord($keyword); + } catch (\Throwable $e) { + $vicWordArr = []; + } + if ($vicWordArr) { + $vicword = $vicWordArr; + unset($where['store_name']); + $where['keyword'] = $vicWordArr; + } + $result = $this->dao->getKeywordResult(0, $keyword); + $ids = []; + if ($result && isset($result['result']) && $result['result']) {//之前查询结果记录 + $ids = $result['result']; + } else {//分词查询 + + } + //搜索没有记录 + if (!$ids && $where) { + //查出所有结果ids存搜索记录表 + /** @var StoreProductServices $services */ + $services = app()->make(StoreProductServices::class); + $idsArr = $services->getSearchList($where, 0, 0, ['id'], '', []); + if ($idsArr) { + $ids = array_column($idsArr, 'id'); + } + } + $vicword = is_string($vicword) ? [$vicword] : $vicword; + $this->saveUserSearch($uid, $keyword, $vicword, $ids); + return $ids; + } + + + /** + * 获取用户搜索关键词列表 + * @param int $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserList(int $uid) + { + if (!$uid) { + return []; + } + [$page, $limit] = $this->getPageValue(); + return $this->dao->getList(['uid' => $uid, 'is_del' => 0], 'add_time desc,num desc', $page, $limit); + } + + /** + * 用户增加搜索记录 + * @param int $uid + * @param string $key + * @param array $result + */ + public function saveUserSearch(int $uid, string $keyword, array $vicword, array $result) + { + $result = json_encode($result); + $vicword = json_encode($vicword, JSON_UNESCAPED_UNICODE); + $userkeyword = $this->dao->getKeywordResult($uid, $keyword, 0); + $data = []; + $data['result'] = $result; + $data['vicword'] = $vicword; + $data['add_time'] = time(); + if ($userkeyword) { + $data['num'] = $userkeyword['num'] + 1; + $this->dao->update(['id' => $userkeyword['id']], $data); + } else { + $data['uid'] = $uid; + $data['keyword'] = $keyword; + $this->dao->save($data); + } + return true; + } + +} diff --git a/app/services/user/UserServices.php b/app/services/user/UserServices.php new file mode 100644 index 0000000..f6a686a --- /dev/null +++ b/app/services/user/UserServices.php @@ -0,0 +1,2358 @@ + +// +---------------------------------------------------------------------- +//declare (strict_types=1); +namespace app\services\user; + +use app\jobs\user\UserFriendsJob; +use app\jobs\user\UserJob; +use app\jobs\user\UserSpreadJob; +use app\jobs\user\UserSvipJob; +use app\services\activity\bargain\StoreBargainServices; +use app\services\activity\combination\StoreCombinationServices; +use app\services\activity\newcomer\StoreNewcomerServices; +use app\services\activity\seckill\StoreSeckillServices; +use app\services\agent\AgentLevelServices; +use app\services\BaseServices; +use app\dao\user\UserDao; +use app\services\activity\coupon\StoreCouponUserServices; +use app\services\diy\DiyServices; +use app\services\message\service\StoreServiceRecordServices; +use app\services\message\service\StoreServiceServices; +use app\services\order\OtherOrderServices; +use app\services\order\StoreOrderCreateServices; +use app\services\order\StoreOrderServices; +use app\services\order\StoreOrderTakeServices; +use app\services\other\QrcodeServices; +use app\services\product\product\StoreProductLogServices; +use app\services\other\queue\QueueServices; +use app\services\message\SystemMessageServices; +use app\services\user\group\UserGroupServices; +use app\services\user\label\UserLabelServices; +use app\services\user\label\UserLabelRelationServices; +use app\services\user\level\SystemUserLevelServices; +use app\services\user\level\UserLevelServices; +use app\services\wechat\WechatUserServices; +use app\services\work\WorkClientServices; +use app\services\work\WorkMemberServices; +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use crmeb\services\FormBuilder as Form; +use crmeb\services\FormBuilder; +use crmeb\services\SystemConfigService; +use crmeb\services\wechat\OfficialAccount; +use think\Exception; +use think\exception\ValidateException; +use think\facade\Route as Url; + +/** + * + * Class UserServices + * @package app\services\user + * @mixin UserDao + */ +class UserServices extends BaseServices +{ + /** + * 性别 + * @var array + */ + public $sex = [ + '男' => 1, + '女' => 2, + '保密' => 0, + 0 => 1,//男 + 1 => 2,//女 + 2 => 0,//保密 + ]; + + /** + * 逆转数据 + * @var int[] + */ + public $rSex = [ + 0 => 2, + 1 => 0, + 2 => 1, + ]; + + /** + * 用户默认补充信息 + * @var array + */ + public $defaultExtendInfo = [ + ['info' => '姓名', 'tip' => '请填写真实姓名', 'format' => 'text', 'label' => '文本', 'param' => 'real_name', 'single' => '', 'singlearr' => [], 'required' => 0, 'use' => 0, 'user_show' => 0, 'sort' => 1], + ['info' => '性别', 'tip' => '请选择性别', 'format' => 'radio', 'label' => '单选项', 'param' => 'sex', 'single' => '', 'singlearr' => ['男', '女', '保密'], 'required' => 0, 'use' => 0, 'user_show' => 0, 'sort' => 2], + ['info' => '生日', 'tip' => '请选择出生日期', 'format' => 'date', 'label' => '日期', 'param' => 'birthday', 'single' => '', 'singlearr' => [], 'required' => 0, 'use' => 0, 'user_show' => 0, 'sort' => 3], + ['info' => '身份证', 'tip' => '请填写身份证', 'format' => 'id', 'label' => '身份证', 'param' => 'card_id', 'single' => '', 'singlearr' => [], 'required' => 0, 'use' => 0, 'user_show' => 0, 'sort' => 4], + ['info' => '地址', 'tip' => '请填写地址', 'format' => 'address', 'label' => '地址', 'param' => 'address', 'single' => '', 'singlearr' => [], 'required' => 0, 'use' => 0, 'user_show' => 0, 'sort' => 5], + ['info' => '备注', 'tip' => '请填写补充备注内容', 'format' => 'text', 'label' => '文本', 'param' => 'mark', 'single' => '', 'singlearr' => [], 'required' => 0, 'use' => 0, 'user_show' => 0, 'sort' => 6], + ]; + + /** + * UserServices constructor. + * @param UserDao $dao + */ + public function __construct(UserDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取用户信息 + * @param $id + * @param $field + */ + public function getUserInfo(int $uid, $field = '*') + { + if (is_string($field)) $field = explode(',', $field); + return $this->dao->get($uid, $field); + } + + /** + * 是否存在 + * @param int $uid + * @return bool + */ + public function userExist(int $uid) + { + return $this->dao->exist($uid); + } + + /** + * 获取用户缓存信息 + * @param int $uid + * @param string $field + * @param int $expire + * @return bool|mixed|null + */ + public function getUserCacheInfo(int $uid, int $expire = 60) + { + return $this->dao->cacheTag()->remember('user_info_' . $uid, function () use ($uid) { + return $this->dao->get($uid); + }, $expire); + } + + /** + * 获取用户列表 + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserList(array $where, string $field = '*'): array + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $field, $page, $limit); + $count = $this->count($where); + return compact('list', 'count'); + } + + /** + * 列表条数 + * @param array $where + * @return int + */ + public function getCount(array $where, bool $is_list = false) + { + return $this->dao->getCount($where, $is_list); + } + + /** + * 保存用户信息 + * @param $user + * @param int $spreadUid + * @param string $userType + * @return User|\think\Model + */ + public function setUserInfo($user, int $spreadUid = 0, string $userType = 'wechat') + { + mt_srand(); + $ip = app()->request->ip(); + $data = [ + 'account' => $user['account'] ?? 'wx' . rand(1, 9999) . time(), + 'pwd' => $user['pwd'] ?? md5('123456'), + 'nickname' => $user['nickname'] ?? '', + 'avatar' => !empty($user['headimgurl']) ? $user['headimgurl'] : sys_config('h5_avatar'), + 'phone' => $user['phone'] ?? '', + 'birthday' => $user['birthday'] ?? '', + 'add_time' => time(), + 'add_ip' => $ip, + 'last_time' => time(), + 'last_ip' => $ip, + 'user_type' => $userType + ]; + if ($spreadUid && $this->dao->get((int)$spreadUid)) { + $data['spread_uid'] = $spreadUid; + $data['spread_time'] = time(); + } + $res = $this->dao->save($data); + if (!$res) + throw new AdminException('保存用户信息失败'); + //用户注册成功事件 + $userInfo = $res->toArray(); + $userInfo['unionid'] = $user['unionid'] ?? ''; + event('user.register', [$userInfo, true, $spreadUid]); + return $res; + } + + /** + * 某些条件用户佣金总和 + * @param array $where + * @return mixed + */ + public function getSumBrokerage(array $where) + { + return $this->dao->getWhereSumField($where, 'brokerage_price'); + } + + /** + * 根据条件获取用户指定字段列表 + * @param array $where + * @param string $field + * @param string $key + * @return array + */ + public function getColumn(array $where, string $field = '*', string $key = '') + { + return $this->dao->getColumn($where, $field, $key); + } + + /** + * 获取分销用户 + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getAgentUserList(array $where = [], string $field = '*', $is_page = true) + { + $where_data['status'] = 1; + if (sys_config('store_brokerage_statu') != 2) $where_data['is_promoter'] = 1; + $where_data['spread_open'] = 1; + if (isset($where['nickname']) && $where['nickname'] !== '') { + $where_data['like'] = $where['nickname']; + } + if (isset($where['data']) && $where['data']) { + $where_data['time'] = $where['data']; + } + [$page, $limit] = $this->getPageValue($is_page); + $list = $this->dao->getAgentUserList($where_data, $field, $page, $limit); + $count = $this->dao->count($where_data); + return compact('count', 'list'); + } + + /** + * 获取分销员ids + * @param array $where + * @return array + */ + public function getAgentUserIds(array $where) + { + $where['status'] = 1; + if (sys_config('store_brokerage_statu') != 2) $where['is_promoter'] = 1; + $where['spread_open'] = 1; + if (isset($where['nickname']) && $where['nickname'] !== '') { + $where['like'] = $where['nickname']; + } + if (isset($where['data']) && $where['data']) { + $where['time'] = $where['data']; + } + return $this->dao->getAgentUserIds($where); + } + + /** + * 获取推广人列表 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSairList(array $where, string $field = '*') + { + $where_data = []; + if (isset($where['uid'])) { + if (isset($where['type'])) { + $type = (int)$where['type']; + $type = in_array($type, [1, 2]) ? $type : 0; + $uids = $this->getUserSpredadUids((int)$where['uid'], $type); + $where_data['uid'] = count($uids) > 0 ? $uids : 0; + } + if (isset($where['data']) && $where['data']) { + $where_data['time'] = $where['data']; + } + if (isset($where['nickname']) && $where['nickname']) { + $where_data['like'] = $where['nickname']; + } + $where_data['status'] = 1; + } + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getSairList($where_data, '*', $page, $limit); + $count = $this->dao->count($where_data); + return compact('list', 'count'); + } + + /** + * 获取推广人统计 + * @param array $where + * @param string $field + * @param int $page + * @param int $limit + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSairCount(array $where) + { + $where_data = []; + if (isset($where['uid'])) { + if (isset($where['type'])) { + $uids = $this->getColumn(['spread_uid' => $where['uid']], 'uid'); + switch ((int)$where['type']) { + case 1: + $where_data['uid'] = count($uids) > 0 ? $uids : 0; + break; + case 2: + if (count($uids)) + $spread_uid_two = $this->dao->getColumn([['spread_uid', 'IN', $uids]], 'uid'); + else + $spread_uid_two = []; + $where_data['uid'] = count($spread_uid_two) > 0 ? $spread_uid_two : 0; + break; + default: + if (count($uids)) { + if ($spread_uid_two = $this->dao->getColumn([['spread_uid', 'IN', $uids]], 'uid')) { + $uids = array_merge($uids, $spread_uid_two); + $uids = array_unique($uids); + $uids = array_merge($uids); + } + } + $where_data['uid'] = count($uids) > 0 ? $uids : 0; + break; + } + } + if (isset($where['data']) && $where['data']) { + $where_data['time'] = $where['data']; + } + if (isset($where['nickname']) && $where['nickname']) { + $where_data['like'] = $where['nickname']; + } + $where_data['status'] = 1; + } + return $this->dao->count($where_data); + } + + /** + * 写入用户信息 + * @param array $data + * @return bool + */ + public function create(array $data) + { + if (!$this->dao->save($data)) + throw new AdminException('写入失败'); + return true; + } + + /** + * 设置用户登录类型 + * @param int $uid + * @param string $type + * @return bool + * @throws Exception + */ + public function setLoginType(int $uid, string $type = 'h5') + { + if (!$this->dao->update($uid, ['login_type' => $type])) + throw new Exception('设置登录类型失败'); + return true; + } + + /** + * 设置用户分组 + * @param $uids + * @param int $group_id + */ + public function setUserGroup($uids, int $group_id) + { + return $this->dao->batchUpdate($uids, ['group_id' => $group_id], 'uid'); + } + + /** + * 获取用户标签 + * @param array $uids + * @param int $type + * @param int $relation_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserLablel(array $uids, int $type = 0, int $relation_id = 0) + { + /** @var UserLabelRelationServices $services */ + $services = app()->make(UserLabelRelationServices::class); + $userlabels = $services->getUserLabelList($uids, $type, $relation_id); + $data = []; + foreach ($uids as $uid) { + $labels = array_filter($userlabels, function ($item) use ($uid) { + if ($item['uid'] == $uid && $item['label_id'] && isset($item['label_name']) && $item['label_name']) { + return true; + } + }); + $data[$uid] = implode(',', array_column($labels, 'label_name')); + } + return $data; + } + + /** + * 显示资源列表头部 + * @return array[] + */ + public function typeHead() + { + //全部会员 + $all = $this->getCount([]); + /** @var UserWechatuserServices $userWechatUser */ + $userWechatUser = app()->make(UserWechatuserServices::class); + //小程序会员 + $routine = $userWechatUser->getCount(['w.user_type' => 'routine']); + //公众号会员 + $wechat = $userWechatUser->getCount(['w.user_type' => 'wechat']); + //H5会员 + $h5 = $userWechatUser->getCount(['w.openid' => '', 'u.user_type' => 'h5']); + //pc会员 + $pc = $userWechatUser->getCount(['w.openid' => '', 'u.user_type' => 'pc']); + //app会员 + $app = $userWechatUser->getCount(['w.user_type' => 'app']); + return [ + ['user_type' => '', 'name' => '全部会员', 'count' => $all], + ['user_type' => 'routine', 'name' => '小程序会员', 'count' => $routine], + ['user_type' => 'wechat', 'name' => '公众号会员', 'count' => $wechat], + ['user_type' => 'h5', 'name' => 'H5会员', 'count' => $h5], + ['user_type' => 'pc', 'name' => 'PC会员', 'count' => $pc], + ['user_type' => 'app', 'name' => 'APP会员', 'count' => $app], + ]; + } + + /** + * 会员列表 + * @param array $where + * @return array + */ + public function index(array $where) + { + /** @var UserWechatuserServices $userWechatUser */ + $userWechatUser = app()->make(UserWechatuserServices::class); + $fields = 'u.*,w.country,w.province,w.city,w.sex,w.unionid,w.openid,w.user_type as w_user_type,w.groupid,w.tagid_list,w.subscribe,w.subscribe_time'; + [$list, $count] = $userWechatUser->getWhereUserList($where, $fields); + if ($list) { + $uids = array_column($list, 'uid'); + $userlabel = $this->getUserLablel($uids); + $userGroup = app()->make(UserGroupServices::class)->getUsersGroupName(array_unique(array_column($list, 'group_id'))); + $userExtract = app()->make(UserExtractServices::class)->getUsersSumList($uids); + $levelName = app()->make(SystemUserLevelServices::class)->getUsersLevel(array_unique(array_column($list, 'level'))); + $userLevel = app()->make(UserLevelServices::class)->getUsersLevelInfo($uids); + $spreadUids = array_unique(array_column($list, 'spread_uid')); + $spread_names = $this->dao->getColumn([['uid', 'in', $spreadUids]], 'nickname', 'uid'); + /** @var WorkClientServices $workClientService */ + $workClientService = app()->make(WorkClientServices::class); + $clientData = $workClientService->getList(['uid' => $uids], ['id', 'uid', 'name', 'external_userid', 'corp_id', 'unionid'], false); + $clientlist = $clientData['list']; + //补充信息 + $extendInfo = SystemConfigService::get('user_extend_info', []); + $is_extend_info = false; + if ($extendInfo) { + foreach ($extendInfo as $item) { + //没选择使用 跳过 + if (isset($item['use']) && $item['use']) { + $is_extend_info = true; + break; + } + } + } + foreach ($list as &$item) { + if (empty($item['addres'])) { + if (!empty($item['country']) || !empty($item['province']) || !empty($item['city'])) { + $item['addres'] = $item['country'] . $item['province'] . $item['city']; + } + } + $item['status'] = ($item['status'] == 1) ? '正常' : '禁止'; + $item['birthday'] = $item['birthday'] ? date('Y-m-d', (int)$item['birthday']) : ''; + $item['extract_count_price'] = $userExtract[$item['uid']] ?? 0;//累计提现 + $item['spread_uid_nickname'] = $item['spread_uid'] ? (($spread_names[$item['spread_uid']] ?? '') . '/' . $item['spread_uid']) : '无'; + //用户类型 + if ($item['user_type'] == 'routine') { + $item['user_type'] = '小程序'; + } else if ($item['user_type'] == 'wechat') { + $item['user_type'] = '公众号'; + } else if ($item['user_type'] == 'h5') { + $item['user_type'] = 'H5'; + } else if ($item['user_type'] == 'pc') { + $item['user_type'] = 'PC'; + } else if ($item['user_type'] == 'app') { + $item['user_type'] = 'APP'; + } else $item['user_type'] = '其他'; + if ($item['sex'] == 1) { + $item['sex'] = '男'; + } else if ($item['sex'] == 2) { + $item['sex'] = '女'; + } else $item['sex'] = '保密'; + //等级名称 + $item['level'] = $levelName[$item['level']] ?? '无'; + //分组名称 + $item['group_id'] = $userGroup[$item['group_id']] ?? '无'; + //用户等级 + $item['vip_name'] = false; + $levelinfo = $userLevel[$item['uid']] ?? null; + if ($levelinfo) { + if ($levelinfo && ($levelinfo['is_forever'] || time() < $levelinfo['valid_time'])) { + $item['vip_name'] = $item['level'] != '无' ? $item['level'] : false; + } + } + $item['labels'] = $userlabel[$item['uid']] ?? ''; + $item['isMember'] = $item['is_money_level'] > 0 ? 1 : 0; + $item['follow_list'] = []; + if ($clientlist) { + foreach ($clientlist as $value) { + if (!empty($value['followOne']) && $value['uid'] == $item['uid']) { + $item['follow_list'][] = $value['followOne']; + } + } + } + $item['is_extend_info'] = $is_extend_info; + } + } + return compact('count', 'list'); + } + + /** + * 获取修改页面数据 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function edit(int $id) + { + $user = $this->getUserInfo($id); + if (!$user) + throw new AdminException('数据不存在'); + $f = array(); + $f[] = Form::input('uid', '用户编号', $user->getData('uid'))->disabled(true); + $f[] = Form::input('real_name', '真实姓名', $user->getData('real_name'))->col(12); + $f[] = Form::input('phone', '手机号码', $user->getData('phone'))->col(12); + $f[] = Form::date('birthday', '生日', $user->getData('birthday') ? date('Y-m-d', $user->getData('birthday')) : '')->col(12); + $f[] = Form::input('card_id', '身份证号', $user->getData('card_id'))->col(12); + $f[] = Form::input('addres', '用户地址', $user->getData('addres')); + $f[] = Form::textarea('mark', '用户备注', $user->getData('mark')); + $f[] = Form::input('pwd', '登录密码')->type('password')->col(12)->placeholder('不改密码请留空'); + $f[] = Form::input('true_pwd', '确认密码')->type('password')->col(12)->placeholder('不改密码请留空'); + //查询高于当前会员的所有会员等级 +// $grade = app()->make(UserLevelServices::class)->getUerLevelInfoByUid($id, 'grade'); + $systemLevelList = app()->make(SystemUserLevelServices::class)->getWhereLevelList([], 'id,name'); + $setOptionLevel = function () use ($systemLevelList) { + $menus = []; + foreach ($systemLevelList as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['name']]; + } + return $menus; + }; + $f[] = Form::select('level', '用户等级', (int)$user->getData('level'))->col(12)->setOptions(FormBuilder::setOptions($setOptionLevel))->filterable(true); + $systemGroupList = app()->make(UserGroupServices::class)->getGroupList(); + $setOptionGroup = function () use ($systemGroupList) { + $menus = []; + foreach ($systemGroupList as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['group_name']]; + } + return $menus; + }; + $f[] = Form::select('group_id', '用户分组', $user->getData('group_id'))->col(12)->setOptions(FormBuilder::setOptions($setOptionGroup))->filterable(true); + /** @var UserLabelServices $userlabelServices */ + $userlabelServices = app()->make(UserLabelServices::class); + $systemLabelList = $userlabelServices->getLabelList(['type' => 0]); + $labels = app()->make(UserLabelRelationServices::class)->getUserLabels($user['uid']); + $setOptionLabel = function () use ($systemLabelList) { + $menus = []; + foreach ($systemLabelList as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['label_name']]; + } + return $menus; + }; + $f[] = Form::select('label_id', '用户标签', $labels)->setOptions(FormBuilder::setOptions($setOptionLabel))->filterable(true)->multiple(true); + $f[] = Form::radio('spread_open', '推广资格', $user->getData('spread_open'))->info('禁用用户的推广资格后,在任何分销模式下该用户都无分销权限')->options([['value' => 1, 'label' => '启用'], ['value' => 0, 'label' => '禁用']]); + //分销模式 人人分销 + $storeBrokerageStatus = sys_config('store_brokerage_statu', 1); + if (in_array($storeBrokerageStatus, [1, 3])) { + $f[] = Form::radio('is_promoter', '推广员权限', $user->getData('is_promoter'))->info('指定分销模式下,开启或关闭用户的推广权限')->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + } + $f[] = Form::radio('status', '用户状态', $user->getData('status'))->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '锁定']]); + return create_form('编辑', $f, Url::buildUrl('/user/user/' . $id), 'PUT'); + } + + /** + * 添加用户表单 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function saveForm() + { + $f = array(); + $f[] = Form::input('real_name', '真实姓名', '')->col(12)->placeholder('请输入真实姓名'); + $f[] = Form::input('phone', '手机号码', '')->col(12)->placeholder('请输入手机号码')->required(); + $f[] = Form::date('birthday', '生日', '')->col(12)->placeholder('请选择生日'); + $f[] = Form::input('card_id', '身份证号', '')->col(12)->placeholder('请输入身份证号'); + $f[] = Form::input('addres', '用户地址', '')->placeholder('请输入用户地址'); + $f[] = Form::textarea('mark', '用户备注', '')->placeholder('请输入用户备注'); + $f[] = Form::input('pwd', '登录密码')->col(12)->type('password')->placeholder('请输入登录密码'); + $f[] = Form::input('true_pwd', '确认密码')->col(12)->type('password')->placeholder('请再次确认密码'); + /** @var SystemUserLevelServices $systemUserLevelServices */ + $systemUserLevelServices = app()->make(SystemUserLevelServices::class); + $systemLevelList = $systemUserLevelServices->getWhereLevelList([], 'id,name'); + $setOptionLevel = function () use ($systemLevelList) { + $menus = []; + foreach ($systemLevelList as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['name']]; + } + return $menus; + }; + $f[] = Form::select('level', '用户等级', '')->col(12)->setOptions(FormBuilder::setOptions($setOptionLevel))->filterable(true); + $systemGroupList = app()->make(UserGroupServices::class)->getGroupList(); + $setOptionGroup = function () use ($systemGroupList) { + $menus = []; + foreach ($systemGroupList as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['group_name']]; + } + return $menus; + }; + $f[] = Form::select('group_id', '用户分组', '')->col(12)->setOptions(FormBuilder::setOptions($setOptionGroup))->filterable(true); + /** @var UserLabelServices $userLabelServices */ + $userLabelServices = app()->make(UserLabelServices::class); + $systemLabelList = $userLabelServices->getLabelList(['type' => 0]); + $setOptionLabel = function () use ($systemLabelList) { + $menus = []; + foreach ($systemLabelList as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['label_name']]; + } + return $menus; + }; + $f[] = Form::select('label_id', '用户标签', '')->setOptions(FormBuilder::setOptions($setOptionLabel))->filterable(true)->multiple(true); + $f[] = Form::radio('spread_open', '推广资格', 1)->info('禁用用户的推广资格后,在任何分销模式下该用户都无分销权限')->options([['value' => 1, 'label' => '启用'], ['value' => 0, 'label' => '禁用']]); + //分销模式 人人分销 + $storeBrokerageStatus = sys_config('store_brokerage_statu', 1); + if (in_array($storeBrokerageStatus, [1, 3])) { + $f[] = Form::radio('is_promoter', '推广员权限', 0)->info('指定分销模式下,开启或关闭用户的推广权限')->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '关闭']]); + } + $f[] = Form::radio('status', '用户状态', 1)->options([['value' => 1, 'label' => '开启'], ['value' => 0, 'label' => '锁定']]); + return create_form('添加用户', $f, $this->url('/user/user'), 'POST'); + } + + /** + * 获取追加信息 + * @param int $uid + * @param $userInfo + * @return \FormBuilder\UI\Iview\Components\Group + */ + public function extendInfoForm(int $uid = 0) + { + $f = []; + if ($uid) { + $userInfo = $this->getUserInfo($uid); + } + $extendInfo = SystemConfigService::get('user_extend_info', []); + $f = []; + if ($extendInfo) { + $userExtendInfo = $userInfo['extend_info'] ?? []; + if ($userExtendInfo) $userExtendInfo = array_combine(array_column($userExtendInfo, 'info'), $userExtendInfo); + foreach ($extendInfo as $item) { + //没选择使用 跳过 + if (!isset($item['use']) || !$item['use']) continue; + $format = $item['format'] ?? ''; + $field = $item['param'] ?? $item['info'] ?? ''; + switch ($format) { + case 'num'://'数字' + $form = Form::number($field, $item['info'], $userExtendInfo[$item['info']]['value'] ?? 0); + break; + case 'date'://'日期' + $form = Form::date($field, $item['info'], $userExtendInfo[$item['info']]['value'] ?? ''); + break; + case 'radio'://'单选项' + $options = []; + if (isset($item['singlearr']) && $item['singlearr']) { + foreach ($item['singlearr'] as $key => $value) { + $options[] = ['value' => $key, 'label' => $value]; + } + } + $form = Form::radio($field, $item['info'], (int)($userExtendInfo[$item['info']]['value'] ?? 0))->options($options); + break; + case 'text'://'文本' + case 'id'://'身份证' + case 'mail'://'邮件' + case 'phone'://'手机号' + case 'address'://'地址' + $form = Form::input($field, $item['info'], $userExtendInfo[$item['info']]['value'] ?? ''); + break; + } + if ($item['required']) { + $f[] = $form->required($item['tip'] ?? ''); + } else { + $f[] = $form; + } + + } + } + if (!$f) { + throw new ValidateException('请先去用户设置:设置用户信息'); + } + return create_form('用户补充信息', $f, $this->url('/user/user/extend_info/' . $uid), 'POST'); + } + + /** + * 处理用户补充信息 + * @param array $inputExtendInfo + * @param bool $is_all + * @return array + */ + public function handelExtendInfo(array $inputExtendInfo, bool $is_all = false) + { + $extendInfo = SystemConfigService::get('user_extend_info', []); + if ($inputExtendInfo && $extendInfo) { + if ($is_all) {//移动端全数据 处理 + $inputExtendInfo = array_combine(array_column($inputExtendInfo, 'info'), $inputExtendInfo); + } else {//后台key=>value类型数据 + $inputExtendInfo = $inputExtendInfo[0] ?? $inputExtendInfo; + } + foreach ($extendInfo as &$item) { + $value = $is_all ? ($inputExtendInfo[$item['info'] ?? '']['value'] ?? '') : ($inputExtendInfo[$item['param'] ?? $item['info'] ?? ''] ?? ''); + if ($value) { + //验证手机号 + if ($item['format'] == 'phone') { + if (!check_phone($value)) throw new AdminException( '请填写正确的手机号'); + } + //验证邮箱 + if ($item['format'] == 'mail') { + if (!check_mail($value)) throw new AdminException( '请填写正确的邮箱'); + } + //验证身份证号 + if ($item['format'] == 'id') { + try { + if (!check_card($value)) throw new AdminException( '请填写正确的身份证号码'); + } catch (\Throwable $e) { +// throw new AdminException( '请填写正确的身份证号码'); + } + } + } + $item['value'] = $value; + } + } + return $extendInfo; + } + + /** + * 保存用户补充信息 + * @param int $uid + * @param array $extend_info 补充信息 + * @param array $update 原本需要修改字段 + * @param bool $is_all + * @return bool + */ + public function saveExtendForm(int $uid, array $extend_info, array $update = [], bool $is_all = false) + { + $userInfo = $this->getUserInfo($uid); + if (!$userInfo) { + throw new ValidateException('用户不存在'); + } + $extend_info = $this->handelExtendInfo($extend_info, $is_all) ?: []; + if ($extend_info) { + $default = $this->defaultExtendInfo; + $params = array_column($default, 'param'); + $sex = $this->sex; + $update['extend_info'] = $extend_info; + foreach ($extend_info as $info) { + if (isset($info['param']) && in_array($info['param'], $params) && isset($info['value'])) { + if ($info['param'] == 'sex') { + $update['sex'] = $sex[$info['value']] ?? 0; + } elseif ($info['param'] == 'birthday') { + $update['birthday'] = strtotime($info['value']); + } elseif($info['param'] == 'address') { + $update['addres'] = $info['value']; + } else { + $update[$info['param']] = $info['value']; + } + } + } + } + if ($update) { + $this->dao->update($uid, $update); + $this->dao->cacheTag()->clear(); + } + return true; + } + + /** + * 修改提交处理 + * @param $id + * @return mixed + */ + public function updateInfo(int $id, array $data) + { + $user = $this->getUserInfo($id); + if (!$user) { + throw new AdminException('数据不存在!'); + } + $res1 = false; + $res2 = false; + $edit = []; + if ($data['money_status'] && $data['money']) {//余额增加或者减少 + $data['money'] = sprintf("%.2f", $data['money']); + /** @var UserMoneyServices $userMoneyServices */ + $userMoneyServices = app()->make(UserMoneyServices::class); + if ($data['money_status'] == 1) {//增加 + $edit['now_money'] = bcadd($user['now_money'], $data['money'], 2); + $res1 = $userMoneyServices->income('system_add', $user['uid'], $data['money'], $edit['now_money'], $data['adminId'] ?? 0); + } else if ($data['money_status'] == 2) {//减少 + if ($user['now_money'] > $data['money']) { + $edit['now_money'] = bcsub($user['now_money'], $data['money'], 2); + } else { + $edit['now_money'] = 0; + $data['money'] = $user['now_money']; + } + $res1 = $userMoneyServices->income('system_sub', $user['uid'], $data['money'], $edit['now_money'], $data['adminId'] ?? 0); + } + } else { + $res1 = true; + } + if ($data['integration_status'] && $data['integration']) {//积分增加或者减少 + $integral_data = ['link_id' => $data['adminId'] ?? 0, 'number' => $data['integration'], 'balance' => $user['integral']]; + /** @var UserBillServices $userBill */ + $userBill = app()->make(UserBillServices::class); + $balance = $user['integral']; + if ($data['integration_status'] == 1) {//增加 + $balance = bcadd((string)$user['integral'], (string)$data['integration'], 0); + $res2 = $userBill->income('system_add_integral', $id, (int)$data['integration'], (int)$balance, $data['adminId'] ?? 0); + } else if ($data['integration_status'] == 2) {//减少 + $balance = bcsub((string)$user['integral'], (string)$data['integration'], 0); + $res2 = $userBill->income('system_sub_integral', $id, (int)$data['integration'], (int)$balance, $data['adminId'] ?? 0); + } + $edit['integral'] = $balance; + } else { + $res2 = true; + } + //修改基本信息 + if (!isset($data['is_other']) || !$data['is_other']) { + if ($data['phone']) { + $otherUser = $this->getOne(['phone' => $data['phone']], 'uid,phone'); + if ($otherUser && $otherUser['uid'] != $id) { + throw new AdminException('该手机号码已被注册'); + } + } + /** @var UserLabelRelationServices $userLabel */ + $userLabel = app()->make(UserLabelRelationServices::class); + if (is_string($data['label_id'])) { + $data['label_id'] = [$data['label_id']]; + } + $userLabel->setUserLable([$id], $data['label_id'], 0, 0, true); + if (isset($data['pwd']) && $data['pwd'] && $data['pwd'] != $user['pwd']) { + $edit['pwd'] = $data['pwd']; + } + if (isset($data['spread_open'])) { + $edit['spread_open'] = $data['spread_open']; + } + $edit['status'] = $data['status']; + $edit['real_name'] = $data['real_name']; + $edit['card_id'] = $data['card_id']; + $edit['birthday'] = strtotime($data['birthday']); + $edit['mark'] = $data['mark']; + $edit['is_promoter'] = $data['is_promoter']; + $edit['level'] = $data['level']; + $edit['phone'] = $data['phone']; + $edit['addres'] = $data['addres']; + $edit['group_id'] = $data['group_id']; + if ($data['spread_uid'] != -1) { + $edit['spread_uid'] = $data['spread_uid']; + $edit['spread_time'] = $data['spread_uid'] ? time() : 0; + } + $edit['sex'] = $data['sex']; + $edit['provincials'] = $data['provincials']; + $edit['province'] = $data['province']; + $edit['city'] = $data['city']; + $edit['area'] = $data['area']; + $edit['street'] = $data['street']; + if (isset($data['extend_info']) && $data['extend_info']) $edit['extend_info'] = $data['extend_info']; + if ($user['level'] != $data['level']) { + /** @var UserLevelServices $userLevelService */ + $userLevelService = app()->make(UserLevelServices::class); + $userLevelService->setUserLevel((int)$user['uid'], (int)$data['level']); + } + if ($data['spread_uid'] != $user['spread_uid']) { + //记录推广绑定关系 + UserSpreadJob::dispatch([$id, (int)$data['spread_uid'], 0, (int)$data['adminId']]); + //记录好友关系 + UserFriendsJob::dispatch([$id, (int)$data['spread_uid']]); + } + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + $wechatUser->update(['uid' => $id], ['sex' => $data['sex']]); + } + if ($edit) $res3 = $this->dao->update($id, $edit); + else $res3 = true; + if ($res1 && $res2 && $res3){ + $this->dao->cacheTag()->clear(); + return true; + } else throw new AdminException('修改失败'); + } + + /** + * 编辑其他 + * @param $id + * @return mixed + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function editOther($id) + { + $user = $this->getUserInfo($id); + if (!$user) { + throw new AdminException('数据不存在!'); + } + $f = array(); + $f[] = Form::radio('money_status', '修改余额', 1)->options([['value' => 1, 'label' => '增加'], ['value' => 2, 'label' => '减少']]); + $f[] = Form::number('money', '余额', 0)->max(999999)->min(0)->style(['width' => '100px']); + $f[] = Form::radio('integration_status', '修改积分', 1)->options([['value' => 1, 'label' => '增加'], ['value' => 2, 'label' => '减少']]); + $f[] = Form::number('integration', '积分', 0)->max(999999)->min(0)->precision(0)->style(['width' => '100px']); + return create_form('修改积分余额', $f, Url::buildUrl('/user/update_other/' . $id), 'PUT'); + } + + /** + * 设置会员分组 + * @param $id + * @return mixed + */ + public function setGroup($uids, $all, $where) + { + $userGroup = app()->make(UserGroupServices::class)->getGroupList(); + if (count($uids) == 1) { + $user = $this->getUserInfo($uids[0], ['group_id']); + $setOptionUserGroup = function () use ($userGroup) { + $menus = []; + foreach ($userGroup as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['group_name']]; + } + return $menus; + }; + $field[] = Form::select('group_id', '用户分组', $user->getData('group_id'))->setOptions(FormBuilder::setOptions($setOptionUserGroup))->filterable(true); + } else { + $setOptionUserGroup = function () use ($userGroup) { + $menus = []; + foreach ($userGroup as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['group_name']]; + } + return $menus; + }; + $field[] = Form::select('group_id', '用户分组')->setOptions(FormBuilder::setOptions($setOptionUserGroup))->filterable(true); + } + $field[] = Form::hidden('uids', implode(',', $uids)); + $field[] = Form::hidden('all', $all); + $field[] = Form::hidden('where', $where ? json_encode($where) : ""); + return create_form('设置用户分组', $field, Url::buildUrl('/user/save_set_group'), 'PUT'); + } + + /** + * 保存会员分组 + * @param $id + * @return mixed + */ + public function saveSetGroup($uids, int $group_id, $redisKey, $queueId) + { + /** @var UserGroupServices $userGroup */ + $userGroup = app()->make(UserGroupServices::class); + /** @var QueueServices $queueService */ + $queueService = app()->make(QueueServices::class); + if (!$userGroup->getGroup($group_id)) { + throw new AdminException('该分组不存在'); + } + if (!$this->setUserGroup($uids, $group_id)) { + $queueService->addQueueFail($queueId['id'], $redisKey); + throw new AdminException('设置分组失败或无改动'); + } else { + $queueService->doSuccessSremRedis($uids, $redisKey, $queueId['type']); + } + return true; + } + + /** + * 设置用户标签 + * @param $uids + * @param $all + * @param $where + * @param int $type + * @param int $store_id + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setLabel($uids, $all, $where, int $type = 0, int $relation_id = 0) + { + /** @var UserLabelServices $userlabelServices */ + $userlabelServices = app()->make(UserLabelServices::class); + $userLabel = $userlabelServices->getLabelList(['type' => $type, 'relation_id' => $relation_id]); + if (count($uids) == 1) { + /** @var UserLabelRelationServices $userLabeLRelation */ + $userLabeLRelation = app()->make(UserLabelRelationServices::class); + $lids = $userLabeLRelation->getUserLabels($uids[0], $type, $relation_id); + $setOptionUserLabel = function () use ($userLabel) { + $menus = []; + foreach ($userLabel as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['label_name']]; + } + return $menus; + }; + $field[] = Form::select('label_id', '用户标签', $lids)->setOptions(FormBuilder::setOptions($setOptionUserLabel))->filterable(true)->multiple(true); + } else { + $setOptionUserLabel = function () use ($userLabel) { + $menus = []; + foreach ($userLabel as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['label_name']]; + } + return $menus; + }; + $field[] = Form::select('label_id', '用户标签')->setOptions(FormBuilder::setOptions($setOptionUserLabel))->filterable(true)->multiple(true); + } + $field[] = Form::hidden('uids', implode(',', $uids)); + $field[] = Form::hidden('all', $all); + $field[] = Form::hidden('where', $where ? json_encode($where) : ""); + return create_form('设置用户标签', $field, Url::buildUrl('/user/save_set_label'), 'PUT'); + } + + /** + * 保存用户标签 + * @param $uids + * @param $label_id + * @return bool + */ + public function saveSetLabel($uids, $label_id) + { + /** @var UserLabelRelationServices $services */ + $services = app()->make(UserLabelRelationServices::class); + if ($label_id) { + /** @var UserLabelServices $userlabel */ + $userlabel = app()->make(UserLabelServices::class); + if (count($label_id) != $userlabel->getCount([['id', 'in', $label_id]])) { + throw new AdminException('用户标签不存在或被删除'); + } + if (!$services->setUserLable($uids, $label_id)) { + throw new AdminException('设置标签失败'); + } + } else {//没传入标签 默认清空 + if (!is_array($uids)) { + $uids = [$uids]; + } + foreach ($uids as $uid) { + $services->unUserLabel((int)$uid); + } + } + + return true; + } + + /** + * 批量队列设置标签 + * @param $uids + * @param $lable_id + * @param $redisKey + * @param $queueId + * @return bool + */ + public function saveBatchSetLabel($uids, $lable_id, $redisKey, $queueId) + { + /** @var QueueServices $queueService */ + $queueService = app()->make(QueueServices::class); + foreach ($lable_id as $id) { + if (!app()->make(UserLabelServices::class)->getLable((int)$id)) { + throw new AdminException('用户标签不存在或被删除'); + } + } + /** @var UserLabelRelationServices $services */ + $services = app()->make(UserLabelRelationServices::class); + if (!$services->setUserLable($uids, $lable_id)) { + $queueService->addQueueFail($queueId['id'], $redisKey); + throw new AdminException('设置标签失败'); + } else { + $queueService->doSuccessSremRedis($uids, $redisKey, $queueId['type']); + } + return true; + } + + /** + * 赠送会员等级 + * @param int $uid + * @return mixed + * */ + public function giveLevel($id) + { + if (!$this->userExist($id)) { + throw new AdminException('用户不存在'); + } + //查询高于当前会员的所有会员等级 + $grade = app()->make(UserLevelServices::class)->getUerLevelInfoByUid($id, 'grade'); + $systemLevelList = app()->make(SystemUserLevelServices::class)->getWhereLevelList(['grade', '>', $grade ?? 0], 'id,name'); + $setOptionlevel = function () use ($systemLevelList) { + $menus = []; + foreach ($systemLevelList as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['name']]; + } + return $menus; + }; + $field[] = Form::select('level_id', '用户等级')->setOptions(FormBuilder::setOptions($setOptionlevel))->filterable(true); + return create_form('赠送等级', $field, Url::buildUrl('/user/save_give_level/' . $id), 'PUT'); + } + + /** + * 执行赠送会员等级 + * @param int $uid + * @return mixed + * */ + public function saveGiveLevel(int $id, int $level_id) + { + if (!$this->userExist($id)) { + throw new AdminException('用户不存在'); + } + /** @var SystemUserLevelServices $systemLevelServices */ + $systemLevelServices = app()->make(SystemUserLevelServices::class); + /** @var UserLevelServices $userLevelServices */ + $userLevelServices = app()->make(UserLevelServices::class); + //查询当前选择的会员等级 + $systemLevel = $systemLevelServices->getLevel($level_id); + if (!$systemLevel) throw new AdminException('您选择赠送的用户等级不存在!'); + //检查是否拥有此会员等级 + $level = $userLevelServices->getWhereLevel(['uid' => $id, 'level_id' => $level_id], 'valid_time,is_forever'); + if ($level && $level['status'] == 1 && $level['is_del'] == 0) { + throw new AdminException('此用户已有该用户等级,无法再次赠送'); + } + //保存会员信息 + if (!$userLevelServices->setUserLevel($id, $level_id, $systemLevel)) { + throw new AdminException('赠送失败'); + } + return true; + } + + /** + * 赠送付费会员时长 + * @param int $uid + * @return mixed + * */ + public function giveLevelTime($id) + { + $userInfo = $this->getUserCacheInfo($id); + if (!$userInfo) { + throw new AdminException('用户不存在'); + } + $overdue_time = ''; + if ($userInfo['is_ever_level'] == 1) { + $overdue_time = '永久'; + } else { + if ($userInfo['is_money_level'] > 0 && $userInfo['overdue_time'] > 0) { + $overdue_time = date('Y-m-d H:i:s', $userInfo['overdue_time']); + } else { + $overdue_time = '已过期/暂未开通'; + } + } + $field[] = Form::input('overdue_time', '会员到期时间', $overdue_time)->disabled(true); + $field[] = Form::radio('days_status', '修改付费会员', 1)->options([['value' => 1, 'label' => '增加'], ['value' => 2, 'label' => '减少']]); + $field[] = Form::number('days', '调整时长(天)')->min(0)->max(999999)->precision(0)->placeholder('请输入'); + return create_form('赠送付费会员时长', $field, Url::buildUrl('/user/save_give_level_time/' . $id), 'PUT'); + } + + /** + * 执行赠送付费会员时长 + * @param int $id + * @param int $days + * @param int $days_status 1:增加 2:减少 + * @return bool + * @throws \Exception + */ + public function saveGiveLevelTime(int $id, int $days, int $days_status = 1) + { + $userInfo = $this->getUserInfo($id); + if (!$userInfo) { + throw new AdminException('用户不存在'); + } + if ($days <= 0) throw new AdminException('赠送天数不能小于1天'); + if ($userInfo['is_ever_level'] == 1) { + throw new AdminException('永久会员无需操作'); + } + $update = []; + $days_time = bcmul((string)$days, '86400'); + if ($userInfo['is_money_level'] == 0) { + $update['is_money_level'] = 3; + $time = time(); + } else { + $time = $userInfo['overdue_time']; + } + if ($days_status == 1) {//增加 + $time = (int)bcadd((string)$time, (string)$days_time); + } else {//减少 + if ($time <= $days_time) { + $time = 0; + } else { + $time = (int)bcsub((string)$time, (string)$days_time); + } + } + $update['overdue_time'] = $time; + if ($time < time()) {//已经过期 + $update['is_money_level'] = 0; + } + $this->dao->update($id, $update); + $userInfo->save(); + /** @var StoreOrderCreateServices $storeOrderCreateService */ + $storeOrderCreateService = app()->make(StoreOrderCreateServices::class); + $orderInfo = [ + 'uid' => $id, + 'order_id' => $storeOrderCreateService->getNewOrderId(), + 'type' => 4, + 'member_type' => 0, + 'pay_type' => 'admin', + 'paid' => 1, + 'pay_time' => time(), + 'is_free' => 1, + 'overdue_time' => $time, + 'vip_day' => $days_status == 1 ? $days : bcsub('0', $days), + 'add_time' => time() + ]; + /** @var OtherOrderServices $otherOrder */ + $otherOrder = app()->make(OtherOrderServices::class); + $otherOrder->save($orderInfo); + return true; + } + + /** + * 清除会员等级 + * @paran int $uid + * @paran boolean + * */ + public function cleanUpLevel($uid) + { + if (!$this->userExist($uid)) + throw new AdminException('用户不存在'); + /** @var UserLevelServices $services */ + $services = app()->make(UserLevelServices::class); + return $this->transaction(function () use ($uid, $services) { + $res = $services->delUserLevel($uid); + $res1 = $this->dao->update($uid, ['clean_time' => time(), 'level' => 0, 'exp' => 0], 'uid'); + if (!$res && !$res1) + throw new AdminException('清除失败'); + return true; + }); + } + + /** + * 获取用户详情里面的用户消费能力和用户余额积分等 + * @param $uid + * @return array[] + */ + public function getHeaderList(int $uid, $userInfo = []) + { + if (!$userInfo) { + $userInfo = $this->getUserInfo($uid); + } + /** @var StoreOrderServices $orderServices */ + $orderServices = app()->make(StoreOrderServices::class); + $where = ['pid' => 0, 'uid' => $uid, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0]; + return [ + [ + 'title' => '余额', + 'value' => $userInfo['now_money'] ?? 0, + 'key' => '元', + ], + [ + 'title' => '总计订单', + 'value' => $orderServices->count($where), + 'key' => '笔', + ], + [ + 'title' => '总消费金额', + 'value' => $orderServices->together($where, 'pay_price'), + 'key' => '元', + ], + [ + 'title' => '积分', + 'value' => $userInfo['integral'] ?? 0, + 'key' => '', + ], + [ + 'title' => '用户经验', + 'value' => $userInfo['exp'] ?? 0, + 'key' => '', + ], + [ + 'title' => '消费次数', + 'value' => $orderServices->count(['uid' => $uid, 'pid' => [0, -1], 'paid' => 1, 'is_del' => 0, 'is_system_del' => 0]), + 'key' => '', + ], +// [ +// 'title' => '本月订单', +// 'value' => $orderServices->count($where + ['time' => 'month']), +// 'key' => '笔', +// ], +// [ +// 'title' => '本月消费金额', +// 'value' => $orderServices->together($where + ['time' => 'month'], 'pay_price'), +// 'key' => '元', +// ] + ]; + } + + /** + * 用户详情 + * @param int $uid + * @return array + */ + public function read(int $uid) + { + $userInfo = $this->dao->getUserWithTrashedInfo($uid); + if (!$userInfo) { + throw new AdminException('数据不存在'); + } + $userInfo = $userInfo->toArray(); + $spread_uid_nickname = ''; + if ($userInfo['spread_uid']) { + $spread_uid_nickname = $this->dao->value(['uid' => $userInfo['spread_uid']], 'nickname'); + } + if ($userInfo['is_ever_level'] == 1) { + $userInfo['overdue_time'] = '永久'; + } else { + if ($userInfo['is_money_level'] > 0 && $userInfo['overdue_time'] > 0) { + $userInfo['overdue_time'] = date('Y-m-d H:i:s', $userInfo['overdue_time']); + } + } + $userInfo['spread_uid_nickname'] = $userInfo['spread_uid'] ? $spread_uid_nickname . '/' . $userInfo['spread_uid'] : ''; + $userInfo['_add_time'] = date('Y-m-d H:i:s', $userInfo['add_time']); + $userInfo['_last_time'] = date('Y-m-d H:i:s', $userInfo['last_time']); + $userInfo['birthday'] = $userInfo['birthday'] ? date('Y-m-d', $userInfo['birthday']) : ''; + /** @var UserLabelRelationServices $userLabelServices */ + $userLabelServices = app()->make(UserLabelRelationServices::class); + $label_list = $userLabelServices->getUserLabelList([$uid]); + $label_id = []; + $userInfo['group'] = app()->make(UserGroupServices::class)->getGroup($userInfo['group_id']); + $userInfo['label_list'] = ''; + if ($label_list) { + $userInfo['label_list'] = implode(',', array_column($label_list, 'label_name')); + foreach ($label_list as $item) { + $label_id[] = [ + 'id' => $item['label_id'], + 'label_name' => $item['label_name'] + ]; + } + } + $userInfo['vip_name'] = ''; + if ($userInfo['level']) { + /** @var SystemUserLevelServices $levelServices */ + $levelServices = app()->make(SystemUserLevelServices::class); + $levelInfo = $levelServices->getOne(['id' => $userInfo['level']], 'id,name'); + $userInfo['vip_name'] = $levelInfo['name'] ?? ''; + } + $userInfo['label_id'] = $label_id; + $workClientInfo = $workMemberInfo = null; + if ($userInfo['phone']) { + /** @var WorkMemberServices $workMemberService */ + $workMemberService = app()->make(WorkMemberServices::class); + $workMemberInfo = $workMemberService->get(['mobile' => $userInfo['phone']]); + if ($workMemberInfo) { + if (!$workMemberInfo->uid) { + $workMemberInfo->uid = $userInfo['uid']; + $workMemberInfo->save(); + } + $workMemberInfo = $workMemberInfo->toArray(); + } + } + if (!$workMemberInfo) { + /** @var WorkClientServices $workClientService */ + $workClientService = app()->make(WorkClientServices::class); + $workClientInfo = $workClientService->get(['uid' => $userInfo['uid']]); + if ($workClientInfo) { + $workClientInfo = $workClientInfo->toArray(); + } + } + $info = [ + 'uid' => $uid, + 'userinfo' => [], + 'headerList' => $this->getHeaderList($uid, $userInfo), + 'count' => [0, 0, 0], + 'ps_info' => $userInfo, + 'workClientInfo' => $workClientInfo, + 'workMemberInfo' => $workMemberInfo, + ]; + return $info; + } + + /** + * 获取单个用户信息 + * @param $id 用户id + * @return mixed + */ + public function oneUserInfo(int $id, string $type) + { + switch ($type) { + case 'spread': +// /** @var UserSpreadServices $services */ +// $services = app()->make(UserSpreadServices::class); + /** @var UserFriendsServices $services */ + $services = app()->make(UserFriendsServices::class); + return $services->getFriendList($id); + break; + case 'order': + /** @var StoreOrderServices $services */ + $services = app()->make(StoreOrderServices::class); + return $services->getUserOrderList($id); + break; + case 'integral': + /** @var UserBillServices $services */ + $services = app()->make(UserBillServices::class); + return $services->getIntegralList($id, [], 'title,number,balance,mark,add_time'); + break; + case 'sign': + /** @var UserBillServices $services */ + $services = app()->make(UserBillServices::class); + return $services->getSignList($id, [], 'title,number,mark,add_time'); + break; + case 'coupon': + /** @var StoreCouponUserServices $services */ + $services = app()->make(StoreCouponUserServices::class); + return $services->getUserCouponList($id, 0); + break; + case 'balance_change': + /** @var UserMoneyServices $services */ + $services = app()->make(UserMoneyServices::class); + return $services->getUserMoneyList($id, [], 'title,type,number,balance,mark,pm,status,add_time'); + break; + case 'visit': + /** @var StoreProductLogServices $services */ + $services = app()->make(StoreProductLogServices::class); + return $services->getList(['uid' => $id, 'type' => 'visit'], 'product_id', 'id,product_id,max(add_time) as add_time'); + break; + case 'spread_change': + /** @var UserSpreadServices $services */ + $services = app()->make(UserSpreadServices::class); + return $services->getSpreadList(['store_id' => 0, 'staff_id' => 0, 'uid' => $id], '*', ['spreadUser', 'admin'], false); + break; + default: + throw new AdminException('type参数错误'); + } + } + + /** + * 用户图表 + * @return array + */ + public function userChart() + { + [$starday, $yesterday, $timeType, $timeKey] = $this->timeHandle('thirtyday', true); + $user_list = $this->dao->userList($starday, $yesterday); + $chartdata = []; + $chartdata['legend'] = ['用户数'];//分类 + $chartdata['yAxis']['maxnum'] = 0;//最大值数量 + $chartdata['xAxis'] = $timeKey;//X轴值 + $chartdata['series'] = [];//分类1值 + if (!empty($user_list)) { + $user_list = array_combine(array_column($user_list, 'day'), $user_list); + $chartdata['yAxis']['maxnum'] = max(array_column($user_list, 'count')); + foreach ($timeKey as $day) { + if (isset($user_list[$day])) { + $chartdata['series'][] = $user_list[$day]['count'] ?? 0; + } else { + $chartdata['series'][] = 0; + } + } + } + $chartdata['bing_xdata'] = ['未消费用户', '消费一次用户', '留存客户', '回流客户']; + $color = ['#5cadff', '#b37feb', '#19be6b', '#ff9900']; + $pay[0] = $this->dao->count(['pay_count' => 0]); + $pay[1] = $this->dao->count(['pay_count' => 1]); + $pay[2] = $this->dao->userCount(1); + $pay[3] = $this->dao->userCount(2); + foreach ($pay as $key => $item) { + $bing_data[] = ['name' => $chartdata['bing_xdata'][$key], 'value' => $pay[$key], 'itemStyle' => ['color' => $color[$key]]]; + } + $chartdata['bing_data'] = $bing_data; + return $chartdata; + } + + /***********************************************/ + /************ 前端api services *****************/ + /***********************************************/ + /** + * 用户信息 + * @param $info + * @return mixed + */ + public function userInfo($info) + { + $uid = (int)$info['uid']; + $broken_time = intval(sys_config('extract_time')); + $search_time = time() - 86400 * $broken_time; + //改造时间 + $search_time = '1970/01/01' . ' - ' . date('Y/m/d H:i:s', $search_time); + //可提现佣金 + //返佣 + + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $brokerage_commission = (string)$userBrokerageServices->getUsersBokerageSum(['uid' => $uid, 'pm' => 1], $search_time); + //退款退的佣金 - + $refund_commission = (string)$userBrokerageServices->getUsersBokerageSum(['uid' => $uid, 'pm' => 0], $search_time); + $info['broken_commission'] = bcsub($brokerage_commission, $refund_commission, 2); + if ($info['broken_commission'] < 0) + $info['broken_commission'] = 0; + $info['commissionCount'] = bcsub($info['brokerage_price'], $info['broken_commission'], 2); + if ($info['commissionCount'] < 0) + $info['commissionCount'] = 0; + return $info; + } + + /** + * 个人中心 + * @param array $user + */ + public function personalHome(array $user, $tokenData) + { + $uid = (int)$user['uid']; + event('user.login', [$uid, app('request')->ip()]); + /** @var StoreCouponUserServices $storeCoupon */ + $storeCoupon = app()->make(StoreCouponUserServices::class); + /** @var UserMoneyServices $userMoneyServices */ + $userMoneyServices = app()->make(UserMoneyServices::class); + /** @var UserExtractServices $userExtract */ + $userExtract = app()->make(UserExtractServices::class); + /** @var StoreOrderServices $storeOrder */ + $storeOrder = app()->make(StoreOrderServices::class); + /** @var UserLevelServices $userLevel */ + $userLevel = app()->make(UserLevelServices::class); + /** @var StoreServiceServices $storeService */ + $storeService = app()->make(StoreServiceServices::class); + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + /** @var UserRelationServices $productRelation */ + $productRelation = app()->make(UserRelationServices::class); + /** @var SystemMessageServices $systemMessageServices */ + $systemMessageServices = app()->make(SystemMessageServices::class); + /** @var DiyServices $diyServices */ + $diyServices = app()->make(DiyServices::class); + /** @var AgentLevelServices $agentLevelServices */ + $agentLevelServices = app()->make(AgentLevelServices::class); + /** @var StoreProductLogServices $storeProductLogServices */ + $storeProductLogServices = app()->make(StoreProductLogServices::class); + //是否存在核销码 + if (!$user['bar_code']) { + $bar_code = $this->getBarCode(); + $this->dao->update($uid, ['bar_code' => $bar_code], 'uid'); + $user['bar_code'] = $bar_code; + } + //获取配置参数 + $configData = SystemConfigService::more([ + 'member_card_status', + 'brokerage_func_status', + 'store_brokerage_statu', + 'store_brokerage_price', + 'member_func_status', + 'recharge_switch', + 'extract_time', + 'balance_func_status', + 'invoice_func_status', + 'special_invoice_status', + 'user_extract_bank_status', + 'user_extract_wechat_status', + 'user_extract_alipay_status', + 'level_activate_status' + ]); + //看付费会员是否开启 + $user['is_open_member'] = $user['svip_open'] = !!($configData['member_card_status'] ?? 0); + $user['agent_level_name'] = ''; + //分销等级信息 + if ($user['agent_level']) { + $levelInfo = $agentLevelServices->getLevelInfo((int)$user['agent_level'], 'id,name'); + $user['agent_level_name'] = $levelInfo && $levelInfo['name'] ? $levelInfo['name'] : ''; + } + $wechatUserInfo = $wechatUser->getOne(['uid' => $uid, 'user_type' => $tokenData['type']]); + $user['is_complete'] = $wechatUserInfo['is_complete'] ?? 0; + $user['couponCount'] = $storeCoupon->getUserValidCouponCount((int)$uid); + $user['like'] = $productRelation->getUserCount($uid, 0, 'like'); + $collectCategory = sys_config('video_func_status', 1) ? '' : 'product'; + $user['collectCount'] = $productRelation->getUserCount($uid, 0, 'collect', $collectCategory); + $user['orderStatusNum'] = $storeOrder->getOrderData($uid); + $user['notice'] = 0; + $user['recharge'] = $userMoneyServices->getRechargeSum($uid);//累计充值 + $user['orderStatusSum'] = (float)$userMoneyServices->sum(['uid' => $uid, 'pm' => 0, 'status' => 1], 'number', true); + $user['extractTotalPrice'] = $userExtract->getExtractSum(['uid' => $uid, 'status' => 1]);//累计提现 + $user['extractPrice'] = $user['brokerage_price'];//可提现 + $user['statu'] = (int)($configData['store_brokerage_statu'] ?? 0); + $orderStatusSum = (float)$storeOrder->sum(['pid' => 0, 'paid' => 1, 'refund_status' => [0, 3], 'uid' => $user['uid'], 'is_del' => 0], 'pay_price', true);//累计有效消费 + $user['spread_status'] = ($configData['brokerage_func_status'] ?? 1) && $this->checkUserPromoter($user['uid'], $user, $orderStatusSum); + if (!$user['is_promoter'] && $user['spread_status']) { + $this->dao->update($uid, ['is_promoter' => 1], 'uid'); + $user['is_promoter'] = 1; + } + if ($user['statu'] == 3) { + $storeBrokeragePrice = $configData['store_brokerage_price'] ?? 0; + $user['promoter_price'] = bcsub((string)$storeBrokeragePrice, (string)$user['orderStatusSum'], 2); + } + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + $user['broken_commission'] = $userBrokerageServices->getUserFrozenPrice($uid); + if ($user['broken_commission'] < 0) + $user['broken_commission'] = 0; + $user['commissionCount'] = bcsub($user['brokerage_price'], $user['broken_commission'], 2); + if ($user['commissionCount'] < 0) + $user['commissionCount'] = 0; + //用户等级信息 + $userLevelInfo = $userLevel->homeGetUserLevel((int)$user['uid'], $user); + $user = array_merge($user, $userLevelInfo); + $user['yesterDay'] = $userBrokerageServices->getUsersBokerageSum(['uid' => $uid, 'pm' => 1], 'yesterday'); + $user['recharge_switch'] = (int)($configData['recharge_switch'] ?? 0);//充值开关 + $user['adminid'] = $storeService->checkoutIsService(['uid' => $uid, 'status' => 1, 'customer' => 1]); + $user['broken_day'] = (int)($configData['extract_time'] ?? 0);//佣金冻结时间 + $user['balance_func_status'] = (int)($configData['balance_func_status'] ?? 0); + $user['invioce_func'] = !!($configData['invoice_func_status'] ?? 0); + $user['special_invoice'] = $user['invioce_func'] && ($configData['special_invoice_status'] ?? 0); + $user['pay_vip_status'] = $user['is_ever_level'] || ($user['is_money_level'] && $user['overdue_time'] > time()); + $user['member_style'] = (int)$diyServices->getColorChange('member'); + if ($user['is_ever_level']) { + $user['vip_status'] = 1;//永久会员 + } else { + if (!$user['is_money_level'] && $user['overdue_time'] && $user['overdue_time'] < time()) { + $user['vip_status'] = -1;//开通过已过期 + } else if (!$user['overdue_time'] && !$user['is_money_level']) { + $user['vip_status'] = 2;//没有开通过 + } else if ($user['is_money_level'] && $user['overdue_time'] && $user['overdue_time'] > time()) { + $user['vip_status'] = 3;//开通了,没有到期 + } + } + /** @var StoreServiceRecordServices $servicesRecord */ + $servicesRecord = app()->make(StoreServiceRecordServices::class); + $service_num = $servicesRecord->sum(['user_id' => $uid], 'mssage_num'); + $message = $systemMessageServices->count(['uid' => $uid, 'look' => 0, 'is_del' => 0]); + $user['service_num'] = (int)bcadd((string)$service_num, (string)$message); + $user['is_agent_level'] = ($configData['brokerage_func_status'] ?? 1) && $agentLevelServices->count(['status' => 1, 'is_del' => 0]); + $user['visit_num'] = $storeProductLogServices->getDistinctCount(['uid' => $uid, 'type' => 'visit'], 'product_id'); + $user['user_extract_bank_status'] = (int)($configData['user_extract_bank_status'] ?? 1); + $user['user_extract_wechat_status'] = (int)($configData['user_extract_wechat_status'] ?? 1); + $user['user_extract_alipay_status'] = (int)($configData['user_extract_alipay_status'] ?? 1); + //是否享受新人专享 + /** @var StoreNewcomerServices $newcomerServices */ + $newcomerServices = app()->make(StoreNewcomerServices::class); + $user['newcomer_status'] = $newcomerServices->checkUserNewcomer($uid); + $user['level_activate_status'] = $configData['level_activate_status']; + $user['member_func_status'] = $configData['member_func_status']; + $extendInfo = SystemConfigService::get('user_extend_info', []); + $user['register_extend_info'] = []; + if (!$user['level_activate_status']) {//不需要激活,用户激活状态默认为1 + $user['level_status'] = 1; + } + if ($extendInfo) { + foreach ($extendInfo as $item) { + if (isset($item['use']) && $item['use'] && isset($item['user_show']) && $item['user_show']) $user['register_extend_info'][] = $item; + } + } + if (isset($user['extend_info']) && $user['extend_info']) { + $default = $this->defaultExtendInfo; + $params = array_column($default, 'param'); + $sex = $this->rSex; + foreach ($user['extend_info'] as &$info) { + if (isset($info['param']) && in_array($info['param'], $params)) { + if ($info['param'] == 'sex') { + $info['value'] = $sex[$user['sex']] ?? 0; + } elseif ($info['param'] == 'birthday') { + $info['value'] = ($user['birthday'] ?? '') ? date('Y-m-d', $user['birthday']) : ''; + } elseif($info['param'] == 'address') { + $info['value'] = $user['addres'] ?? ''; + } else { + $info['value'] = $user[$info['param']] ?? ''; + } + } + } + } + + $user['is_default_avatar'] = $user['avatar'] == sys_config('h5_avatar') ? 1 : 0; + return $user; + } + + /** + * 用户资金统计 + * @param int $uid ` + */ + public function balance(int $uid) + { + $userInfo = $this->getUserInfo($uid); + if (!$userInfo) { + throw new ValidateException('数据不存在'); + } + /** @var UserMoneyServices $userMoneyServices */ + $userMoneyServices = app()->make(UserMoneyServices::class); + /** @var StoreOrderServices $storeOrder */ + $storeOrder = app()->make(StoreOrderServices::class); + $user['now_money'] = $userInfo['now_money'];//当前总资金 + $user['recharge'] = $userMoneyServices->getRechargeSum($uid);//累计充值 + $user['orderStatusSum'] = $storeOrder->sum(['pid' => 0, 'uid' => $uid, 'paid' => 1, 'is_del' => 0, 'refund_status' => [0, 3]], 'pay_price', true);//累计消费 + return $user; + } + + /** + * 用户修改信息 + * @param Request $request + * @return mixed + */ + public function eidtNickname(int $uid, array $data) + { + if (!$this->userExist($uid)) { + throw new ValidateException('用户不存在'); + } + if (!$this->dao->update($uid, $data, 'uid')) { + throw new ValidateException('修改失败'); + } + return true; + } + + /** + * 获取推广人排行 + * @param $data 查询条件 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getRankList(array $data) + { + $startTime = $endTime = 0; + switch ($data['type']) { + case 'week': + $startTime = strtotime('this week Monday'); + break; + case 'month': + $startTime = strtotime('first day of ' . date('F Y')); + break; + } + $endTime = time(); + [$page, $limit] = $this->getPageValue(); + $field = 'spread_uid,count(uid) AS count,spread_time'; + /** @var UserSpreadServices $userSpreadServices */ + $userSpreadServices = app()->make(UserSpreadServices::class); + $list = $userSpreadServices->getAgentRankList([$startTime, $endTime], $field, $page, $limit); + $rank = 0; + foreach ($list as $key => $item) { + if ($item['spread_uid'] == $data['uid']) $rank = $key + 1; + } + $week = $userSpreadServices->count(['spread_uid' => $data['uid'], 'time' => [strtotime('this week Monday'), time()], 'timeKey' => 'spread_time']); + $month = $userSpreadServices->count(['spread_uid' => $data['uid'], 'time' => [strtotime('last month'), time()], 'timeKey' => 'spread_time']); + $start = date('Y-m-d H:i', $startTime); + $end = date('Y-m-d H:i', time()); + return compact('list', 'rank', 'week', 'month', 'start', 'end'); + } + + /** + * 静默绑定推广人 + * @param Request $request + * @return mixed + */ + public function spread(int $uid, int $spreadUid, $code) + { + $userInfo = $this->getUserInfo($uid); + if (!$userInfo) { + throw new ValidateException('数据不存在'); + } + if ($code && !$spreadUid) { + /** @var QrcodeServices $qrCode */ + $qrCode = app()->make(QrcodeServices::class); + if ($info = $qrCode->getOne(['id' => $code, 'status' => 1])) { + $spreadUid = $info['third_id']; + } + } + //记录好友关系 + if ($spreadUid && $uid && $spreadUid != $uid) { + /** @var UserFriendsServices $serviceFriend */ + $serviceFriend = app()->make(UserFriendsServices::class); + $serviceFriend->saveFriend($uid, $spreadUid); + } + $data = []; + //永久绑定 + $store_brokergae_binding_status = sys_config('store_brokerage_binding_status', 1); + $spread_uid = isset($user['code']) && $user['code'] && $user['code'] != $userInfo->uid ? $user['code'] : ($userInfo['spread_uid'] ?? 0); + if ($userInfo->spread_uid && $store_brokergae_binding_status == 1) { + $data['login_type'] = $user['login_type'] ?? $userInfo->login_type; + } else { + //绑定分销关系 = 所有用户 + if (sys_config('brokerage_bindind', 1) == 1) { + //分销绑定类型为时间段且过期 ||临时 + $store_brokerage_binding_time = sys_config('store_brokerage_binding_time', 30); + if (!$userInfo['spread_uid'] || $store_brokergae_binding_status == 3 || ($store_brokergae_binding_status == 2 && ($userInfo['spread_time'] + $store_brokerage_binding_time * 24 * 3600) < time())) { + $spreadUid = $spread_uid; + if ($spreadUid && $userInfo->uid == $this->dao->value(['uid' => $spreadUid], 'spread_uid')) { + $spreadUid = 0; + } + if ($spreadUid && $this->dao->get((int)$spreadUid)) { + $data['spread_uid'] = $spreadUid; + $data['spread_time'] = time(); + } + } + } + } + if ($data && !$this->dao->update($userInfo['uid'], $data, 'uid')) { + throw new ValidateException('修改信息失败'); + } + if (isset($data['spread_uid']) && $data['spread_uid']) { + /** @var UserBillServices $userBill */ + $userBill = app()->make(UserBillServices::class); + //邀请新用户增加经验 + $userBill->inviteUserIncExp((int)$spreadUid); + } + return true; + } + + /** + * 添加访问记录 + * @param Request $request + * @return mixed + */ + public function setVisit(array $data) + { + $userInfo = $this->getUserInfo($data['uid']); + if (!$userInfo) { + throw new ValidateException('数据不存在'); + } + if (isset($data['ip']) && $data['ip']) { + $addressArr = $this->addressHandle($this->convertIp($data['ip'])); + $data['province'] = $addressArr['province'] ?? ''; + } + $data['channel_type'] = $userInfo['user_type']; + $data['add_time'] = time(); + /** @var UserVisitServices $userVisit */ + $userVisit = app()->make(UserVisitServices::class); + if ($userVisit->save($data)) { + return true; + } else { + throw new ValidateException('添加访问记录失败'); + } + } + + /** + * 获取活动状态 + * @return mixed + */ + public function activity() + { + /** @var StoreBargainServices $storeBragain */ + $storeBragain = app()->make(StoreBargainServices::class); + /** @var StoreCombinationServices $storeCombinaion */ + $storeCombinaion = app()->make(StoreCombinationServices::class); + /** @var StoreSeckillServices $storeSeckill */ + $storeSeckill = app()->make(StoreSeckillServices::class); + $data['is_bargin'] = (bool)$storeBragain->validBargain(); + $data['is_pink'] = (bool)$storeCombinaion->validCombination(); + $data['is_seckill'] = (bool)$storeSeckill->getSeckillCount(); + return $data; + } + + /** + * 获取用户下级推广人 + * @param int $uid 当前用户 + * @param int $grade 等级 0 一级 1 二级 + * @param string $orderBy 排序 + * @param string $keyword + * @return array|bool + */ + public function getUserSpreadGrade(int $uid = 0, $grade = 0, $orderBy = '', $keyword = '', $time = []) + { + $user = $this->getUserInfo($uid); + if (!$user) { + throw new ValidateException('数据不存在'); + } + $spread_one_ids = $this->getUserSpredadUids($uid, 1); + $spread_two_ids = $this->getUserSpredadUids($uid, 2); + $data = [ + 'total' => count($spread_one_ids), + 'totalLevel' => count($spread_two_ids), + 'list' => [] + ]; + /** @var UserStoreOrderServices $userStoreOrder */ + $userStoreOrder = app()->make(UserStoreOrderServices::class); + $list = []; + $where = ['pid' => 0, 'type' => 0, 'paid' => 1, 'refund_status' => [0, 3], 'is_del' => 0, 'is_system_del' => 0]; + if ($grade == 0) { + if ($spread_one_ids) $list = $userStoreOrder->getUserSpreadCountList($spread_one_ids, $orderBy, $keyword, $time); + $where = $where + ['spread_uid' => $uid]; + } else { + if ($spread_two_ids) $list = $userStoreOrder->getUserSpreadCountList($spread_two_ids, $orderBy, $keyword, $time); + $where = $where + ['spread_two_uid' => $uid]; + } + foreach ($list as &$item) { + if (isset($item['spread_time']) && $item['spread_time']) { + $item['time'] = date('Y/m/d', $item['spread_time']); + } + } + $data['list'] = $list; + $data['brokerage_level'] = (int)sys_config('brokerage_level', 2); + $data['count'] = 0; + $data['price'] = 0; + if ($list) { + $uids = array_column($list, 'uid'); + $data['count'] = count($uids); + /** @var StoreOrderServices $storeOrder */ + $storeOrder = app()->make(StoreOrderServices::class); + $data['price'] = $storeOrder->sum($where, $grade == 0 ? 'one_brokerage' : 'two_brokerage'); + } + return $data; + } + + /** + * 获取推广人uids + * @param int $uid + * @param bool $one + * @return array + */ + public function getUserSpredadUids(int $uid, int $type = 0) + { + $uids = $this->dao->getColumn(['spread_uid' => $uid], 'uid'); + if ($type === 1) { + return $uids; + } + if ($uids) { + $uidsTwo = $this->dao->getColumn([['spread_uid', 'in', $uids]], 'uid'); + if ($type === 2) { + return $uidsTwo; + } + if ($uidsTwo) { + $uids = array_merge($uids, $uidsTwo); + } + } + return $uids; + } + + /** + * 检测用户是否是推广员 + * @param int $uid + * @param array $user + * @param float $sumPrice + * @return bool + */ + public function checkUserPromoter(int $uid, $user = [], float $sumPrice = 0.00) + { + if (!$uid) { + return false; + } + if (!$user) { + $user = $this->getUserInfo($uid); + } + if (!$user) { + return false; + } + //分销是否开启 + if (!sys_config('brokerage_func_status')) { + return false; + } + //用户分校推广资格是否开启4.0.32 + if (isset($user['spread_open']) && !$user['spread_open']) { + return false; + } + $store_brokerage_statu = sys_config('store_brokerage_statu'); + if ($user['is_promoter'] || $store_brokerage_statu == 2) { + return true; + } + if ($store_brokerage_statu == 3) { + if ($sumPrice) { + /** @var StoreOrderServices $storeOrder */ + $storeOrder = app()->make(StoreOrderServices::class); + $sumPrice = $storeOrder->sum(['pid' => 0, 'uid' => $uid, 'paid' => 1, 'is_del' => 0, 'refund_status' => [0, 3]], 'pay_price');//累计消费 + } + $store_brokerage_price = sys_config('store_brokerage_price'); + if ($sumPrice > $store_brokerage_price) { + return true; + } + } + return false; + } + + /** + * 同步微信粉丝用户(后台接口) + * @return bool + */ + public function syncWechatUsers() + { + $key = md5('sync_wechat_users'); + //一天点击一次 + if (CacheService::get($key)) { + return true; + } + $next_openid = null; + do { + $result = OfficialAccount::userService()->list($next_openid); + $userOpenids = $result['data']; + //拆分大数组 + $opemidArr = array_chunk($userOpenids, 100); + foreach ($opemidArr as $openids) { + //加入同步|更新用户队列 + UserJob::dispatch([$openids]); + } + $next_openid = $result['next_openid']; + } while ($next_openid != null); + CacheService::set($key, 1, 3600 * 24); + return true; + } + + /** + * 导入微信粉丝用户 + * @param array $openids + * @return bool + */ + public function importUser(array $noBeOpenids) + { + if (!$noBeOpenids) { + return true; + } + $dataAll = $data = []; + $time = time(); + foreach ($noBeOpenids as $openid) { + try { + $info = OfficialAccount::userService()->get($openid); + $info = is_object($info) ? $info->toArray() : $info; + } catch (\Throwable $e) { + $info = []; + } + if (!$info) continue; + if (($info['subscribe'] ?? 1) == 1) { + $data['nickname'] = $info['nickname'] ?? ''; + $data['headimgurl'] = $info['headimgurl'] ?? ''; + $userInfoData = $this->setUserInfo($data); + if (!$userInfoData) { + throw new AdminException('用户信息储存失败!'); + } + $data['uid'] = $userInfoData['uid']; + $data['subscribe'] = $info['subscribe']; + $data['unionid'] = $info['unionid'] ?? ''; + $data['openid'] = $info['openid'] ?? ''; + $data['sex'] = $info['sex'] ?? 0; + $data['language'] = $info['language'] ?? ''; + $data['city'] = $info['city'] ?? ''; + $data['province'] = $info['province'] ?? ''; + $data['country'] = $info['country'] ?? ''; + $data['subscribe_time'] = $info['subscribe_time'] ?? ''; + $data['groupid'] = $info['groupid'] ?? 0; + $data['remark'] = $info['remark'] ?? ''; + $data['tagid_list'] = isset($info['tagid_list']) && $info['tagid_list'] ? implode(',', $info['tagid_list']) : ''; + $data['add_time'] = $time; + $data['is_complete'] = 1; + $dataAll[] = $data; + } + } + if ($dataAll) { + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + if (!$wechatUser->saveAll($dataAll)) { + throw new ValidateException('保存用户信息失败'); + } + } + return true; + } + + /** + * 修改会员的时间及是否会员状态 + * @param int $vip_day 会员天数 + * @param array $user_id 用户id + * @param int $is_money_level 会员来源途径 + * @param bool $member_type 会员卡类型 + * @return mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setMemberOverdueTime($vip_day, int $user_id, int $is_money_level, $member_type = false) + { + if ($vip_day == 0) throw new ValidateException('天数不能为0'); + $user_info = $this->getUserInfo($user_id); + if (!$user_info) throw new ValidateException('用户数据不存在'); + if (!$member_type) $member_type = "month"; + if ($member_type == 'ever') { + $overdue_time = 0; + $is_ever_level = 1; + } else { + if ($user_info['is_money_level'] == 0) { + $overdue_time = bcadd(bcmul($vip_day, 86400, 0), time(), 0); + } else { + $overdue_time = bcadd(bcmul($vip_day, 86400, 0), $user_info['overdue_time'], 0); + } + $is_ever_level = 0; + } + $setData['overdue_time'] = $overdue_time; + $setData['is_ever_level'] = $is_ever_level; + $setData['is_money_level'] = $is_money_level ? $is_money_level : 0; + // if ($user_info['level'] == 0) $setData['level'] = 1; + return $this->dao->update(['uid' => $user_id], $setData); + } + + /** + * 清空到期svip(分批加入队列) + * @return bool + */ + public function offUserSvip() + { + $users = $this->dao->getColumn([['is_ever_level', '=', 0], ['is_money_level', '>', 0], ['overdue_time', '<', time()]], 'uid'); + if ($users) { + //拆分大数组 + $uidsArr = array_chunk($users, 100); + foreach ($uidsArr as $uids) { + //加入同步|更新用户队列 + UserSvipJob::dispatch([$uids]); + } + } + return true; + } + + /** + * 会员过期改变状态,变为普通会员 + * @param $uid + * @param null $userInfo + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function offMemberLevel($uid, $userInfo = null) + { + if (!$uid) return false; + $userInfo = $userInfo ?: $this->dao->get($uid); + if (!$userInfo) return false; + if ($userInfo['is_ever_level'] == 0 && $userInfo['is_money_level'] > 0 && $userInfo['overdue_time'] < time()) { + $this->dao->update(['uid' => $uid], ['is_money_level' => 0]); + return false; + } + return true; + } + + /** + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserInfoList(array $where, $field = "*") + { + return $this->dao->getUserInfoList($where, $field); + } + + /** + * 保存用户上级推广人 + * @param int $uid + * @param int $spread_uid + * @return bool + */ + public function saveUserSpreadUid(int $uid, int $spread_uid) + { + if (!$uid || !$spread_uid) { + return false; + } + if ($uid == $spread_uid) { + throw new ValidateException('上级推广人不能为自己'); + } + $userInfo = $this->getUserInfo($uid); + if (!$userInfo) { + throw new ValidateException('用户不存在'); + } + //上级已经是这个uid + if ($userInfo['spread_uid'] == $spread_uid) { + return true; + } + $spreadInfo = $this->getUserInfo($spread_uid); + if (!$spreadInfo) { + throw new ValidateException('上级用户不存在'); + } + if ($spreadInfo['spread_uid'] == $uid) { + throw new ValidateException('上级推广人不能为自己下级'); + } + $data = ['spread_uid' => $spread_uid, 'spread_time' => time()]; + $this->dao->update($uid, $data); + //记录推广绑定关系 + UserSpreadJob::dispatch([$uid, $spread_uid]); + //记录好友关系 + UserFriendsJob::dispatch([$uid, $spread_uid]); + return true; + } + + /** + * 增加推广用户佣金 + * @param int $uid + * @param int $spread_uid + * @param array $userInfo + * @param array $spread_user + * @return bool|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function addBrokeragePrice(int $uid, int $spread_uid, array $userInfo = [], array $spread_user = []) + { + if (!$uid || !$spread_uid) { + return false; + } + //商城分销功能是否开启 0关闭1开启 + if (!sys_config('brokerage_func_status')) return true; + //获取设置推广佣金单价 + $brokerage_price = sys_config('uni_brokerage_price', 0); + //推广佣金是否开启 + if (!sys_config('brokerage_user_status', 0)) { + return true; + } + //获取推广佣金当日限额 + $day_brokerage_price_upper = sys_config('day_brokerage_price_upper', 0); + if (!floatval($brokerage_price) || !floatval($day_brokerage_price_upper)) { + return true; + } + if (!$userInfo) { + $userInfo = $this->getUserInfo($uid); + } + if (!$userInfo) { + return false; + } + if (!$spread_user) { + $spread_user = $this->dao->getOne(['uid' => $spread_uid, 'status' => 1]); + } + if (!$spread_user) { + return false; + } + if (!$this->checkUserPromoter($spread_uid, $spread_user)) { + return false; + } + /** @var UserBrokerageServices $userBrokerageServices */ + $userBrokerageServices = app()->make(UserBrokerageServices::class); + // -1不限制 + if ($day_brokerage_price_upper != -1) { + if ($day_brokerage_price_upper <= 0) { + return true; + } else { + //获取上级用户今日获取推广用户佣金 + $spread_day_brokerage = $userBrokerageServices->getUserBillBrokerageSum($spread_uid, ['brokerage_user'], 'today'); + //超过上限 + if (($spread_day_brokerage + $brokerage_price) > $day_brokerage_price_upper) { + return true; + } + } + } + $spreadPrice = $spread_user['brokerage_price']; + // 上级推广员返佣之后的金额 + $balance = bcadd($spreadPrice, $brokerage_price, 2); + return $this->transaction(function () use ($uid, $spread_uid, $brokerage_price, $userInfo, $balance, $userBrokerageServices) { + // 添加返佣记录 + $res1 = $userBrokerageServices->income('get_user_brokerage', $spread_uid, [ + 'nickname' => $userInfo['nickname'], + 'number' => floatval($brokerage_price) + ], $balance, $uid); + // 添加用户余额 + $res2 = $this->dao->bcInc($spread_uid, 'brokerage_price', $brokerage_price, 'uid'); + //给上级发送获得佣金的模板消息 + /** @var StoreOrderTakeServices $storeOrderTakeServices */ + $storeOrderTakeServices = app()->make(StoreOrderTakeServices::class); + $storeOrderTakeServices->sendBackOrderBrokerage([], $spread_uid, $brokerage_price, 'user'); + return $res1 && $res2; + }); + } + + /** + * 获取上级uid + * @param int $uid + * @param array $userInfo + * @param bool $is_spread + * @return int|mixed + */ + public function getSpreadUid(int $uid, $userInfo = [], $is_spread = true) + { + if (!$uid) { + return 0; + } + //商城分销功能是否开启 0关闭1开启 + if (!sys_config('brokerage_func_status')) return -1; + if (!$userInfo) { + $userInfo = $this->getUserCacheInfo($uid); + } + if (!$userInfo) { + return 0; + } + //上级的上级不需要检测自购 + if ($is_spread) { + //开启自购 + $is_self_brokerage = sys_config('is_self_brokerage', 0); + if ($is_self_brokerage && $is_spread) { + return $uid; + } + } + //绑定类型 + $store_brokergae_binding_status = sys_config('store_brokerage_binding_status', 1); + if ($store_brokergae_binding_status == 1 || $store_brokergae_binding_status == 3) { + return $userInfo['spread_uid']; + } + //分销绑定类型为时间段且没过期 + $store_brokerage_binding_time = sys_config('store_brokerage_binding_time', 30); + if ($store_brokergae_binding_status == 2 && ($userInfo['spread_time'] + $store_brokerage_binding_time * 24 * 3600) > time()) { + return $userInfo['spread_uid']; + } + return -1; + } + + /** + * 用户付款code + * @param int $uid + * @return bool|mixed|null + */ + public function getRandCode(int $uid) + { + $key = 'user_rand_code' . $uid; + return CacheService::redisHandler()->remember($key, function () { + return substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 9), 1))), 0, 3) . str_pad((string)mt_rand(1, 999), 3, '0', STR_PAD_LEFT); + }, 600); + } + + /** + * 获取barcode + * @return bool|int|mixed|null + */ + public function getBarCode() + { + mt_srand(); + $code = substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 9), 1))), 0, 4) . str_pad((string)mt_rand(1, 99999), 5, '0', STR_PAD_LEFT); + if (!$this->dao->count(['bar_code' => $code])) { + return $code; + } else { + return $this->getBarCode(); + } + } + + /** + * 获取用户推广用户列表 + * @param $uid + * @param $type + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function agentUserList($uid, $type) + { + $where['spread_uid'] = $uid; + if ($type == 1) { + $where['pay_count'] = -1; + } + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, 'uid,nickname,avatar,FROM_UNIXTIME(spread_time, \'%Y.%m.%d %H:%m\') as spread_time', $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + } +} diff --git a/app/services/user/UserSignServices.php b/app/services/user/UserSignServices.php new file mode 100644 index 0000000..38ad680 --- /dev/null +++ b/app/services/user/UserSignServices.php @@ -0,0 +1,265 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\jobs\user\UserLevelJob; +use app\services\BaseServices; +use app\services\user\member\MemberCardServices; +use app\dao\user\UserSignDao; +use think\exception\ValidateException; + +/** + * + * Class UserSignServices + * @package app\services\user + * @mixin UserSignDao + */ +class UserSignServices extends BaseServices +{ + + /** + * UserSignServices constructor. + * @param UserSignDao $dao + */ + public function __construct(UserSignDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取用户是否签到 + * @param $uid + * @return bool + */ + public function getIsSign(int $uid, string $type = 'today') + { + return (bool)$this->dao->count(['uid' => $uid, 'time' => $type]); + } + + /** + * 获取用户累计签到次数 + * @Parma int $uid 用户id + * @return int + * */ + public function getSignSumDay(int $uid) + { + return $this->dao->count(['uid' => $uid]); + } + + /** + * 设置签到数据 + * @param int $uid 用户uid + * @param string $title 签到说明 + * @param int $number 签到获得积分 + * @param int $integral_balance + * @param int $exp_banlance + * @param int $exp_num + * @return bool + * @throws \think\Exception + */ + public function setSignData($uid, $title = '', $number = 0, $integral_balance = 0, $exp_banlance = 0, $exp_num = 0) + { + $data = []; + $data['uid'] = $uid; + $data['title'] = $title; + $data['number'] = $number; + $data['balance'] = $integral_balance; + $data['add_time'] = time(); + if (!$this->dao->save($data)) { + throw new ValidateException('添加签到数据失败'); + } + /** @var UserBillServices $userBill */ + $userBill = app()->make(UserBillServices::class); + $data['mark'] = $title; + $userBill->incomeIntegral($uid, 'sign', $data); + + if ($exp_num) { + $data['number'] = $exp_num; + $data['category'] = 'exp'; + $data['type'] = 'sign'; + $data['title'] = $data['mark'] = '签到奖励'; + $data['balance'] = $exp_banlance; + $data['pm'] = 1; + $data['status'] = 1; + if (!$userBill->save($data)) { + throw new ValidateException('赠送经验失败'); + } + //检测会员等级 + UserLevelJob::dispatch([$uid]); + } + + return true; + } + + /** + * 获取用户签到列表 + * @param int $uid + * @param string $field + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserSignList(int $uid, string $field = '*') + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList(['uid' => $uid], $field, $page, $limit); + foreach ($list as &$item) { + $item['add_time'] = $item['add_time'] ? date('Y-m-d', $item['add_time']) : ''; + } + return $list; + } + + /** + * 用户签到 + * @param $uid + * @return bool|int|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function sign(int $uid) + { + $sign_list = \crmeb\services\GroupDataService::getData('sign_day_num') ?: []; + if (!count($sign_list)) { + throw new ValidateException('请先配置签到天数'); + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('用户不存在'); + } + if ($this->getIsSign($uid, 'today')) { + throw new ValidateException('已经签到'); + } + //检测昨天是否签到 + if ($this->getIsSign($uid, 'yesterday')) { + if ($user->sign_num > (count($sign_list) - 1)) { + $user->sign_num = 0; + } + } else { + $user->sign_num = 0; + } + $integral_num = 0; + foreach ($sign_list as $key => $item) { + if ($key == $user->sign_num) { + $integral_num = $item['sign_num']; + break; + } + } + //会员签到积分会员奖励 + if ($user->is_money_level > 0) { + //看是否开启签到积分翻倍奖励 + /** @var MemberCardServices $memberCardService */ + $memberCardService = app()->make(MemberCardServices::class); + $sign_rule_number = $memberCardService->isOpenMemberCardCache('sign'); + if ($sign_rule_number) { + $integral_num = (int)$sign_rule_number * $integral_num; + } + } + $user_data = []; + $user_data['sign_num'] = bcadd((string)$user->sign_num, '1', 0); + if ($user_data['sign_num'] > 1) { + $title = '连续签到奖励'; + } else { + $title = '签到奖励'; + } + //用户等级是否开启 + $exp_num = 0; + if (sys_config('member_func_status', 1)) { + $exp_num = sys_config('sign_give_exp'); + } + //增加签到数据 + $this->transaction(function () use ($uid, $title, $integral_num, $user, $exp_num, $user_data, $userServices) { + $user_data['integral'] = $integral_balance = bcadd((string)$user['integral'], (string)$integral_num, 0); + $user_data['exp'] = $exp_balance = bcadd((string)$user['exp'], (string)$exp_num); + $this->setSignData($uid, $title, $integral_num, $integral_balance, $exp_balance, $exp_num); + if (!$userServices->update($user->uid, $user_data)) { + throw new ValidateException('修改用户信息失败'); + } + }); + return $integral_num; + } + + /** + * 签到用户信息 + * @param int $uid + * @param $sign + * @param $integral + * @param $all + * @return mixed + */ + public function signUser(int $uid, $sign, $integral, $all) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('数据不存在'); + } + //是否统计签到 + if ($sign || $all) { + $user['sum_sgin_day'] = $this->getSignSumDay($user['uid']); + $user['is_day_sgin'] = $this->getIsSign($user['uid']); + $user['is_YesterDay_sgin'] = $this->getIsSign($user['uid'], 'yesterday'); + if (!$user['is_day_sgin'] && !$user['is_YesterDay_sgin']) { + $user['sign_num'] = 0; + } + } + /** @var UserIntegralServices $userIntegralServices */ + $userIntegralServices = app()->make(UserIntegralServices::class); + [$clear_integral, $clear_time] = $userIntegralServices->getUserClearIntegral($uid, $user); + $user['clear_integral'] = $clear_integral; + $user['clear_time'] = $clear_time; + //是否统计积分使用情况 + if ($integral || $all) { + /** @var UserBillServices $userBill */ + $userBill = app()->make(UserBillServices::class); + $user['sum_integral'] = intval($userBill->getRecordCount($user['uid'], 'integral', '', '', true)); + $user['deduction_integral'] = intval($userBill->getRecordCount($user['uid'], 'integral') ?? 0); + $user['today_integral'] = intval($userBill->getRecordCount($user['uid'], 'integral', '', 'today', true)); + } + unset($user['pwd']); + if (!$user['is_promoter']) { + $user['is_promoter'] = (int)sys_config('store_brokerage_statu') == 2; + } + return $user->hidden(['account', 'real_name', 'birthday', 'card_id', 'mark', 'partner_id', 'group_id', 'add_time', 'add_ip', 'phone', 'last_time', 'last_ip', 'spread_uid', 'spread_time', 'user_type', 'status', 'level', 'clean_time', 'addres'])->toArray(); + } + + + /** + * 获取签到 + * @param $uid + * @return array + */ + public function getSignMonthList($uid) + { + [$page, $limit] = $this->getPageValue(); + $data = $this->dao->getListGroup(['uid' => $uid], 'FROM_UNIXTIME(add_time,"%Y-%m") as time,group_concat(id SEPARATOR ",") ids', $page, $limit, 'time'); + $list = []; + if ($data) { + $ids = array_unique(array_column($data, 'ids')); + $dataIdsList = $this->dao->getList(['id' => $ids], 'FROM_UNIXTIME(add_time,"%Y-%m-%d") as add_time,title,number,id,uid', 0, 0); + foreach ($data as $item) { + $value['month'] = $item['time']; + $value['list'] = array_merge(array_filter($dataIdsList, function ($val) use ($item) { + if (in_array($val['id'], explode(',', $item['ids']))) { + return $val; + } + })); + array_push($list, $value); + } + } + return $list; + } +} diff --git a/app/services/user/UserSpreadServices.php b/app/services/user/UserSpreadServices.php new file mode 100644 index 0000000..064f1d6 --- /dev/null +++ b/app/services/user/UserSpreadServices.php @@ -0,0 +1,229 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user; + +use app\services\BaseServices; +use app\dao\user\UserSpreadDao; +use app\services\order\store\BranchOrderServices; +use app\services\store\StoreUserServices; +use app\services\store\SystemStoreStaffServices; +use app\services\user\level\SystemUserLevelServices; + +/** + * Class UserSpreadServices + * @package app\services\user + * @mixin UserSpreadDao + */ +class UserSpreadServices extends BaseServices +{ + /** + * UserSpreadServices constructor. + * @param UserSpreadDao $dao + */ + public function __construct(UserSpreadDao $dao) + { + $this->dao = $dao; + } + + /** + * 记录推广关系 + * @param int $uid + * @param int $spread_uid + * @param int $spread_time + * @param int $admin_id + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setSpread(int $uid, int $spread_uid, int $spread_time = 0, int $admin_id = 0) + { + if (!$uid || !$spread_uid) return false; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$userServices->userExist($uid)) { + return false; + } + + if (!$userServices->userExist($spread_uid)) { + return false; + } + $data = ['uid' => $uid, 'spread_uid' => $spread_uid, 'spread_time' => $spread_time ?: time(), 'admin_id' => $admin_id]; + try { + /** @var SystemStoreStaffServices $storeStaffServices */ + $storeStaffServices = app()->make(SystemStoreStaffServices::class); + $staffInfo = $storeStaffServices->getStaffInfoByUid($spread_uid); + } catch (\Throwable $e) { + $staffInfo = []; + } + if ($staffInfo) { + $data['store_id'] = $staffInfo['store_id']; + $data['staff_id'] = $staffInfo['id']; + } + if ($this->dao->save($data)) { + if ($staffInfo) { + //记录门店用户 + /** @var StoreUserServices $storeUserServices */ + $storeUserServices = app()->make(StoreUserServices::class); + $storeUserServices->setStoreUser($uid, (int)$staffInfo['store_id']); + } + return true; + } else { + return false; + } + } + + /** + * 查询推广用户uids + * @param int $uid + * @param int $type 1:一级2:二级 0:所有 + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSpreadUids(int $uid, int $type = 0, array $where = []) + { + if (!$uid) return []; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$userServices->userExist($uid)) { + return []; + } + if ($where && isset($where['time'])) { + $where['timeKey'] = 'spread_time'; + } + $where['spread_uid'] = $uid; + $spread_one = $this->dao->getSpreadUids($where); + if ($type == 1) { + return $spread_one; + } + $where['spread_uid'] = $spread_one; + $spread_two = $this->dao->getSpreadUids($where); + if ($type == 2) { + return $spread_two; + } + return array_unique(array_merge($spread_one, $spread_two)); + } + + /** + * 门店推广统计详情列表 + * @param int $store_id + * @param int $staff_id + * @param array $time + * @return array|array[] + */ + public function time(int $store_id, int $staff_id, array $time = []) + { + if (!$time) { + return [[], []]; + } + [$start, $stop, $front, $front_stop] = $time; + $where = ['store_id' => $store_id]; + if ($staff_id) { + $where['staff_id'] = $staff_id; + } + $frontPrice = $this->dao->count($where + ['time' => [$front, $front_stop], 'timeKey' => 'spread_time']); + $nowPrice = $this->dao->count($where + ['time' => [$start, $stop], 'timeKey' => 'spread_time']); + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where + ['time' => [$start, $stop], 'timeKey' => 'spread_time'], '*', ['user'], $page, $limit); + /** @var BranchOrderServices $order */ + $order = app()->make(BranchOrderServices::class); + $order_where = ['time' => $time, 'is_del' => 0, 'is_system_del' => 0, 'paid' => 1, 'pid' => 0, 'refund_status' => [0, 3]]; + foreach ($list as &$item) { + $item['spread_time'] = $item['spread_time'] ? date('Y-m-d H:i:s', $item['spread_time']) : ''; + $orderCount = $order->column($order_where + ['uid' => $item['uid']], 'count(`id`) as count,sum(`pay_price`) as price'); + $item['order_count'] = $orderCount[0]['count'] ?? 0; + $item['order_price'] = $orderCount[0]['price'] ?? 0.00; + } + return [[$nowPrice, $frontPrice], $list]; + } + + /** + * 获取推广列表 + * @param int $uid + * @param UserServices $userServices + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSpreadList(array $where, string $field = '*', array $with = ['user'], bool $type = true) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $field, $with, $page, $limit); + $count = $this->dao->count($where); + /** @var BranchOrderServices $order */ + $order = app()->make(BranchOrderServices::class); + foreach ($list as &$item) { + $item['spread_time'] = $item['spread_time'] ? date('Y-m-d H:i:s', $item['spread_time']) : ''; + $item['type'] = $item['admin_id'] ? ('手动变更(' . ($item['real_name'] ?? '') . ')') : '自动变更'; + if ($type) { + $orderCount = $order->column(['uid' => $item['uid'], 'is_del' => 0, 'is_system_del' => 0, 'paid' => 1, 'pid' => 0, 'refund_status' => [0, 3]], 'count(`id`) as count,sum(`pay_price`) as price'); + $item['order_count'] = $orderCount[0]['count'] ?? 0; + $item['order_price'] = $orderCount[0]['price'] ?? 0.00; + } + } + return compact('list', 'count'); + } + + /** + * 获取好友uids 我推广的 推广我的 + * @param int $uid + * @return array + */ + public function getFriendUids(int $uid) + { + $result = []; + if ($uid) { + $spread = $this->dao->getColumn(['spread_uid' => $uid], 'uid'); + $sup_spread = $this->dao->getColumn(['uid' => $uid], 'spread_uid'); + $result = array_unique(array_merge($spread, $sup_spread)); + } + return $result; + } + + /** + * 获取好友 + * @param int $id + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getFriendList(int $uid, string $field = 'uid,nickname,level,add_time') + { + $uids = $this->getFriendUids($uid); + $list = []; + $count = 0; + if ($uids) { + [$page, $limit] = $this->getPageValue(); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $list = $userServices->getList(['uid' => $uids], $field, $page, $limit); + /** @var SystemUserLevelServices $systemLevelServices */ + $systemLevelServices = app()->make(SystemUserLevelServices::class); + $systemLevelList = $systemLevelServices->getWhereLevelList([], 'id,name'); + if ($systemLevelList) $systemLevelServices = array_combine(array_column($systemLevelList, 'id'), $systemLevelList); + foreach ($list as &$item) { + $item['type'] = $systemLevelServices[$item['level']]['name'] ?? '暂无'; + $item['add_time'] = $item['add_time'] && is_numeric($item['add_time']) ? date('Y-m-d H:i:s', $item['add_time']) : ''; + } + $count = $this->dao->count(['spread_uid' => $uid]); + } + + return compact('list', 'count'); + } +} diff --git a/app/services/user/group/UserGroupServices.php b/app/services/user/group/UserGroupServices.php new file mode 100644 index 0000000..0e1252f --- /dev/null +++ b/app/services/user/group/UserGroupServices.php @@ -0,0 +1,147 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user\group; + +use app\services\BaseServices; +use app\dao\user\group\UserGroupDao; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use crmeb\traits\ServicesTrait; +use think\facade\Route as Url; + +/** + * 用户分组 + * Class UserGroupServices + * @package app\services\user\group + * @mixin UserGroupDao + */ +class UserGroupServices extends BaseServices +{ + use ServicesTrait; + + /** + * UserGroupServices constructor. + * @param UserGroupDao $dao + */ + public function __construct(UserGroupDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取某一个分组 + * @param int $id + * @return array|\think\Model|null + */ + public function getGroup(int $id) + { + return $this->dao->get($id); + } + + /** + * 获取分组列表 + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGroupList($field = 'id,group_name', bool $is_page = false) + { + $page = $limit = 0; + if ($is_page) { + [$page, $limit] = $this->getPageValue(); + $count = $this->dao->count([]); + } + $list = $this->dao->getList([], $field, $page, $limit); + + return $is_page ? compact('list', 'count') : $list; + } + + /** + * 获取一些用户的分组名称 + * @param array $ids + */ + public function getUsersGroupName(array $ids) + { + return $this->dao->getColumn([['id', 'IN', $ids]], 'group_name', 'id'); + } + + /** + * 添加/修改分组页面 + * @param int $id + * @return string + */ + public function add(int $id) + { + $group = $this->getGroup($id); + $field = array(); + if (!$group) { + $title = '添加分组'; + $field[] = Form::input('group_name', '分组名称', '')->maxlength(20)->required(); + } else { + $title = '修改分组'; + $field[] = Form::hidden('id', $id); + $field[] = Form::input('group_name', '分组名称', $group->getData('group_name'))->maxlength(20)->required(); + } + return create_form($title, $field, Url::buildUrl('/user/user_group/save'), 'POST'); + } + + /** + * 添加|修改 + * @param int $id + * @param array $data + * @return mixed + */ + public function save(int $id, array $data) + { + $groupName = $this->dao->getOne(['group_name' => $data['group_name']]); + if ($id) { + if (!$this->getGroup($id)) { + throw new AdminException('数据不存在'); + } + if ($groupName && $id != $groupName['id']) { + throw new AdminException('该分组已经存在'); + } + if ($this->dao->update($id, $data)) { + return true; + } else { + throw new AdminException('修改失败或者您没有修改什么!'); + } + } else { + unset($data['id']); + if ($groupName) { + throw new AdminException('该分组已经存在'); + } + if ($this->dao->save($data)) { + return true; + } else { + throw new AdminException('添加失败!'); + } + } + } + + /** + * 删除 + * @param int $id + */ + public function delGroup(int $id) + { + if ($this->getGroup($id)) { + if (!$this->dao->delete($id)) { + throw new AdminException('删除失败,请稍候再试!'); + } + } + return '删除成功!'; + } +} diff --git a/app/services/user/label/UserLabelRelationServices.php b/app/services/user/label/UserLabelRelationServices.php new file mode 100644 index 0000000..b5f3566 --- /dev/null +++ b/app/services/user/label/UserLabelRelationServices.php @@ -0,0 +1,138 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user\label; + +use app\services\BaseServices; +use app\dao\user\label\UserLabelRelationDao; +use crmeb\exceptions\AdminException; + +/** + * 用户关联标签 + * Class UserLabelRelationServices + * @package app\services\user\label + * @mixin UserLabelRelationDao + */ +class UserLabelRelationServices extends BaseServices +{ + + /** + * UserLabelRelationServices constructor. + * @param UserLabelRelationDao $dao + */ + public function __construct(UserLabelRelationDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取某个用户标签ids + * @param int $uid + * @param int $store_id + * @return array + */ + public function getUserLabels(int $uid, int $type = 0, int $relation_id = 0) + { + return $this->dao->getColumn(['uid' => $uid, 'type' => $type, 'relation_id' => $relation_id], 'label_id', ''); + } + + /** + * 用户设置标签 + * @param $uids + * @param array $labels + * @param int $type + * @param int $relation_id + * @param bool $group + * @return bool + */ + public function setUserLable($uids, array $labels, int $type = 0, int $relation_id = 0, bool $group = false) + { + if (!$uids) { + return true; + } + if (!is_array($uids)) { + $uids = [$uids]; + } + //增加标签 + $data = []; + foreach ($uids as $uid) { + //用户已经存在的标签 + $user_label_ids = $this->dao->getColumn([ + ['uid', '=', $uid], + ['type', '=', $type], + ['relation_id', '=', $relation_id] + ], 'label_id'); + //删除未选中标签 + if ($group) { + $del_labels = array_diff($user_label_ids, $labels); + $this->dao->delete([ + ['uid', '=', $uid], + ['label_id', 'in', $del_labels], + ['type', '=', $type], + ['relation_id', '=', $relation_id] + ]); + } + $add_labels = array_diff($labels, $user_label_ids); + if ($add_labels) { + foreach ($add_labels as $label) { + $label = (int)$label; + if (!$label) continue; + $data[] = ['uid' => $uid, 'label_id' => $label, 'type' => $type, 'relation_id' => $relation_id]; + } + } + } + if ($data) { + if (!$this->dao->saveAll($data)) + throw new AdminException('设置标签失败'); + } + + return true; + } + + /** + * 取消用户标签 + * @param int $uid + * @param array $labels + * @param int $type + * @param int $relation_id + * @return bool + */ + public function unUserLabel(int $uid, array $labels = [], int $type = 0, int $relation_id = 0) + { + $where = [ + ['uid', '=', $uid], + ['type', '=', $type], + ['relation_id', '=', $relation_id] + ]; + //不传入 清空用户所有标签 + if (count($labels)) { + $where[] = ['label_id', 'in', $labels]; + } + $this->dao->delete($where); + return true; + } + + /** + * 获取用户标签 + * @param array $uids + * @param int $type + * @param int $relation_id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getUserLabelList(array $uids, int $type = 0, int $relation_id = 0) + { + return $this->dao->getLabelList($uids, $type, $relation_id); + } +} diff --git a/app/services/user/label/UserLabelServices.php b/app/services/user/label/UserLabelServices.php new file mode 100644 index 0000000..c4a5b24 --- /dev/null +++ b/app/services/user/label/UserLabelServices.php @@ -0,0 +1,356 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user\label; + +use app\jobs\user\UserLabelJob; +use app\services\BaseServices; +use app\common\dao\user\UserLabelDao; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use crmeb\services\wechat\Work; +use FormBuilder\Factory\Iview; +use think\exception\ValidateException; +use think\facade\Route as Url; + +/** + * 用户标签 + * Class UserLabelServices + * @package app\services\user\label + * @mixin UserLabelDao + */ +class UserLabelServices extends BaseServices +{ + + /** + * UserLabelServices constructor. + * @param UserLabelDao $dao + */ + public function __construct(UserLabelDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取某一本标签 + * @param $id + * @return array|\think\Model|null + */ + public function getLable($id) + { + return $this->dao->get($id); + } + + /** + * 获取所有用户标签 + * @param array $where + * @param array|string[] $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLabelList(array $where = [], array $field = ['*']) + { + return $this->dao->getList(0, 0, $where, $field); + } + + /** + * 获取列表 + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($page, $limit, $where); + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 添加修改标签表单 + * @param int $id + * @param int $type + * @param int $relation_id + * @param int $label_cate + * @return mixed + */ + public function add(int $id, int $type = 0, int $relation_id = 0, int $label_cate = 0) + { + $label = $this->getLable($id); + $field = array(); + /** @var UserLabelCateServices $service */ + $service = app()->make(UserLabelCateServices::class); + $options = []; + foreach ($service->getLabelCateAll($type, $relation_id) as $item) { + $options[] = ['value' => $item['id'], 'label' => $item['name']];; + } + if (!$label) { + $title = '添加标签'; + $field[] = Form::select('label_cate', '标签分类', $label_cate)->setOptions($options)->filterable(true)->appendValidate(Iview::validateInt()->message('请选择标签分类')->required()); + $field[] = Form::input('label_name', '标签名称', '')->maxlength(20)->required('请填写标签名称'); + } else { + $title = '修改标签'; + $field[] = Form::select('label_cate', '分类', (int)$label->getData('label_cate'))->setOptions($options)->filterable(true)->appendValidate(Iview::validateInt()->message('请选择标签分类')->required()); + $field[] = Form::hidden('id', $label->getData('id')); + $field[] = Form::input('label_name', '标签名称', $label->getData('label_name'))->maxlength(20)->required('请填写标签名称'); + } + return create_form($title, $field, Url::buildUrl('/user/user_label/save'), 'POST'); + } + + /** + * 保存标签表单数据 + * @param int $id + * @param array $data + * @return mixed + */ + public function save(int $id, array $data, int $type = 0, int $relation_id = 0) + { + if (!$data['label_cate']) { + throw new ValidateException('请选择标签分类'); + } + $data['type'] = $type; + $data['relation_id'] = $relation_id; + $levelName = $this->dao->getOne(['label_name' => $data['label_name'], 'type' => $type, 'relation_id' => $relation_id]); + if ($id) { + if (!$this->getLable($id)) { + throw new AdminException('数据不存在'); + } + if ($levelName && $id != $levelName['id']) { + throw new AdminException('该标签已经存在'); + } + if ($this->dao->update($id, $data)) { + return true; + } else { + throw new AdminException('修改失败或者您没有修改什么!'); + } + } else { + unset($data['id']); + if ($levelName) { + throw new AdminException('该标签已经存在'); + } + if ($this->dao->save($data)) { + return true; + } else { + throw new AdminException('添加失败!'); + } + } + } + + /** + * 删除 + * @param $id + * @throws \Exception + */ + public function delLabel(int $id) + { + if ($this->getLable($id)) { + if (!$this->dao->delete($id)) { + throw new AdminException('删除失败,请稍候再试!'); + } + } + return true; + } + + /** + * 同步标签 + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function authWorkClientLabel() + { + /** @var UserLabelCateServices $cateService */ + $cateService = app()->make(UserLabelCateServices::class); + $data = $cateService->getLabelList(['group' => 0, 'owner_id' => 0, 'type' => 0]); + if ($data['list']) { + foreach ($data['list'] as $item) { + UserLabelJob::dispatchDo('authLabel', [$item['id'], $item['name']]); + } + } + UserLabelJob::dispatchSece(count($data['list']) + 1, 'authWorkLabel'); + return true; + } + + /** + * 同步平台标签到企业微信客户 + * @param int $cateId + * @param string $groupName + * @return bool + */ + public function addCorpClientLabel(int $cateId, string $groupName) + { + try { + $list = $this->dao->getList(0, 0, ['not_tag_id' => 1, 'type' => 0, 'label_cate' => $cateId], ['label_name as name', 'id']); + if (!$list) { + return true; + } + $data = []; + foreach ($list as $item) { + $data[] = ['name' => $item['name']]; + } + + $res = Work::addCorpTag($groupName, $data); + /** @var UserLabelCateServices $categoryService */ + $categoryService = app()->make(UserLabelCateServices::class); + $categoryService->update($cateId, ['other' => $res['tag_group']['group_id']]); + foreach ($res['tag_group']['tag'] ?? [] as $item) { + foreach ($list as $value) { + if ($item['name'] == $value['name']) { + $this->dao->update($value['id'], ['tag_id' => $item['id']]); + } + } + } + + return true; + } catch (\Throwable $e) { + return false; + } + } + + /** + * 客户标签同步到平台 + * @param array $tagIds + * @param array $group + * @return bool + */ + public function authWorkLabel(array $tagIds = [], array $group = []) + { + $res = Work::getCorpTags($tagIds, $group); + $tagGroup = $res['tag_group'] ?? []; + $cateData = []; + $labelData = []; + $groupIds = []; + /** @var UserLabelCateServices $cateService */ + $cateService = app()->make(UserLabelCateServices::class); + $this->transaction(function () use ($tagGroup, $cateData, $cateService, $labelData, $groupIds) { + foreach ($tagGroup as $item) { + if ($id = $cateService->value(['other' => $item['group_id']], 'id')) { + $cateService->update(['id' => $id], ['name' => $item['group_name'], 'other' => $item['group_id'], 'sort' => $item['order']]); + } else { + $cateData[] = [ + 'name' => $item['group_name'], + 'sort' => $item['order'], + 'add_time' => $item['create_time'], + 'other' => $item['group_id'], + 'group' => 0 + ]; + } + $groupIds[] = $item['group_id']; + foreach ($item['tag'] as $tag) { + if ($labelId = $this->dao->value(['tag_id' => $tag['id']], 'id')) { + $this->dao->update($labelId, ['tag_id' => $tag['id']]); + } else { + $labelData[$item['group_id']][] = [ + 'label_name' => $tag['name'], + 'type' => 1, + 'tag_id' => $tag['id'], + ]; + } + } + } + if ($cateData) { + $cateService->saveAll($cateData); + } + $cateIds = $cateService->getColumn([ + ['other', 'in', $groupIds], + ['type', '=', 1], + ['owner_id', '=', 0], + ['group', '=', 0], + ], 'id', 'other'); + if ($labelData) { + $saveData = []; + foreach ($labelData as $groupId => $labels) { + $cateId = $cateIds[$groupId]; + foreach ($labels as $label) { + $label['label_cate'] = $cateId; + $saveData[] = $label; + } + } + $this->dao->saveAll($saveData); + } + }); + $cateService->deleteCateCache(); + return true; + } + + /** + * 获取同步企业微信的标签数据 + * @return array + */ + public function getWorkLabel() + { + /** @var UserLabelCateServices $cateService */ + $cateService = app()->make(UserLabelCateServices::class); + $list = $cateService->getLabelTree(['type' => 0, 'owner_id' => 0, 'group' => 0, 'other' => true], ['name', 'id', 'other as value'], [ + 'label' => function ($query) { + $query->where('tag_id', '<>', '')->where('type', 0)->where('relation_id', 0)->field(['id', 'label_cate', 'tag_id as value', 'label_name as label']); + } + ]); + foreach ($list as &$item) { + $label = $item['label']; + $item['children'] = $label; + unset($item['label']); + $item['label'] = $item['name']; + } + return $list; + } + + /** + * 企业微信创建客户标签事件 + * @param string $corpId + * @param string $strId + * @param string $type + * @return bool + */ + public function createUserLabel(string $corpId, string $strId, string $type) + { + return $this->authWorkLabel($type === 'tag' ? [$strId] : [], $type === 'tag_group' ? [$strId] : []); + } + + /** + * 企业微信更新客户标签事件 + * @param string $corpId + * @param string $strId + * @param string $type + * @return bool + */ + public function updateUserLabel(string $corpId, string $strId, string $type) + { + return $this->authWorkLabel($type === 'tag' ? [$strId] : [], $type === 'tag_group' ? [$strId] : []); + } + + /** + * 删除标签 + * @param string $corpId + * @param string $strId + * @param string $type + */ + public function deleteUserLabel(string $corpId, string $strId, string $type) + { + if ('tag' === $type) { + $this->dao->delete(['tag_id' => $strId]); + } else if ('tag_group' === $type) { + /** @var UserLabelCateServices $cateService */ + $cateService = app()->make(UserLabelCateServices::class); + $cateInfo = $cateService->get(['type' => 0, 'owner_id' => 0, 'group' => 0, 'other' => $strId]); + if ($cateInfo) { + $this->dao->delete(['label_cate' => $cateInfo->id, 'type' => 1]); + $cateInfo->delete(); + } + } + } + +} diff --git a/app/services/user/level/SystemUserLevelServices.php b/app/services/user/level/SystemUserLevelServices.php new file mode 100644 index 0000000..73b31f6 --- /dev/null +++ b/app/services/user/level/SystemUserLevelServices.php @@ -0,0 +1,193 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user\level; + +use app\services\BaseServices; +use app\dao\user\level\UserLevelDao; +use app\services\user\UserServices; + +/** + * 系统设置用户等级 + * Class SystemUserLevelServices + * @package app\services\user\level + * @mixin UserLevelDao + */ +class SystemUserLevelServices extends BaseServices +{ + + /** + * SystemUserLevelServices constructor. + * @param UserLevelDao $dao + */ + public function __construct(UserLevelDao $dao) + { + $this->dao = $dao; + } + + /** + * 单个等级 + * @param int $id + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLevel(int $id, string $field = '*') + { + return $this->dao->getOne(['id' => $id, 'is_del' => 0], $field); + } + + /** + * 获取会员等级信息缓存 + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/14 + * @param int $id + * @return array|false|mixed|string|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLevelCache(int $id) + { + $level = $this->dao->cacheRemember($id, function () use ($id) { + $level = $this->getLevel($id); + if ($level) { + return $level->toArray(); + } else { + return null; + } + }); + + return $level; + } + + /** + * 获取某条件等级 + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getWhereLevel(array $where, string $field = '*') + { + return $this->dao->getOne($where, $field); + } + + /** + * 获取所有等级列表 + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getLevelList(array $where, string $field = '*') + { + $where_data = []; + if (isset($where['is_show']) && $where['is_show'] !== '') $where_data[] = ['is_show', '=', $where['is_show']]; + if (isset($where['title']) && $where['title']) $where_data[] = ['name', 'LIKE', "%$where[title]%"]; + $where_data[] = ['is_del', '=', '0']; + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where_data, $field ?? '*', $page, $limit); + foreach ($list as &$item) { + $item['image'] = set_file_url($item['image']); + $item['icon'] = set_file_url($item['icon']); + } + $count = $this->dao->getCount($where_data); + return compact('list', 'count'); + } + + /** + * 获取条件的会员等级列表 + * @param array $where + * @param string $field + */ + public function getWhereLevelList(array $where, string $field = '*') + { + if ($where) { + $whereData = [['is_show', '=', 1], ['is_del', '=', 0], $where]; + } else { + $whereData = [['is_show', '=', 1], ['is_del', '=', 0]]; + } + return $this->dao->getList($whereData, $field ?? '*'); + } + + /** + * 获取一些用户等级名称 + * @param $ids + * @return array + */ + public function getUsersLevel($ids) + { + return $this->dao->getColumn([['id', 'IN', $ids]], 'name', 'id'); + } + + /** + * 获取会员等级列表 + * @param int $leval_id + * @return array + */ + public function getLevelListAndGrade(int $leval_id = 0, string $field = 'name,discount,image,icon,explain,id,grade,is_forever,valid_date,exp_num') + { + $list = $this->dao->getList(['is_del' => 0, 'is_show' => 1], $field); + if ($list) { + $listNew = array_combine(array_column($list, 'id'), $list); + $grade = $listNew[$leval_id]['grade'] ?? 0; + foreach ($list as &$item) { + if ($grade < $item['grade']) + $item['is_clear'] = true; + else + $item['is_clear'] = false; + $item['task_list'] = []; + } + } + return $list; + } + + /** + * 获取用户等级折扣 + * @param int $uid + * @param int $id + * @return null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getDiscount(int $uid, int $id) + { + $discount = null; + /** @var UserServicesr $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getOne(['uid' => $uid], 'uid,level,level_status'); + //需要会员是激活状态 + if ($id && $userInfo && $userInfo['level_status']) { + $level = $this->dao->getOne(['id' => $id, 'is_del' => 0, 'is_show' => 1], 'id,discount'); + if (!$level) {//用户存在等级ID 但是被删除或者下架重新检测升级 + try { + /** @var UserLevelServices $userLeveleServices */ + $userLeveleServices = app()->make(UserLevelServices::class); + $userLeveleServices->detection($uid); + } catch (\Throwable $e) { + + } + $level = $this->dao->getOne(['id' => $userInfo['level'], 'is_del' => 0, 'is_show' => 1], 'id,discount'); + } + $discount = $level['discount'] ?? null; + } + return $discount; + } +} diff --git a/app/services/user/level/UserLevelServices.php b/app/services/user/level/UserLevelServices.php new file mode 100644 index 0000000..01dd9d0 --- /dev/null +++ b/app/services/user/level/UserLevelServices.php @@ -0,0 +1,652 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\user\level; + +use app\services\activity\coupon\StoreCouponIssueServices; +use app\services\activity\coupon\StoreCouponUserServices; +use app\services\BaseServices; +use app\services\user\UserServices; +use app\services\user\UserBillServices; +use app\services\user\UserSignServices; +use app\dao\user\level\UserLevelDao; +use crmeb\exceptions\AdminException; +use crmeb\services\FormBuilder as Form; +use crmeb\services\SystemConfigService; +use FormBuilder\Factory\Iview; +use think\exception\ValidateException; +use think\facade\Route as Url; + +/** + * 用户等级 + * Class UserLevelServices + * @package app\services\user\level + * @mixin UserLevelDao + */ +class UserLevelServices extends BaseServices +{ + + /** + * UserLevelServices constructor. + * @param UserLevelDao $dao + */ + public function __construct(UserLevelDao $dao) + { + $this->dao = $dao; + } + + /** + * 某些条件获取单个 + * @param array $where + * @param string $field + * @return mixed + */ + public function getWhereLevel(array $where, string $field = '*') + { + return $this->getOne($where, $field); + } + + /** + * 获取一些用户等级信息 + * @param array $uids + * @param string $field + * @param string $key + * @return array + */ + public function getUsersLevelInfo(array $uids) + { + return $this->dao->getColumn([['uid', 'in', $uids]], 'level_id,is_forever,valid_time', 'uid'); + } + + /** + * 清除会员等级 + * @param $uids + * @return \crmeb\basic\BaseModel|mixed + */ + public function delUserLevel($uids) + { + $where = []; + if (is_array($uids)) { + $where[] = ['uid', 'IN', $uids]; + $re = $this->dao->batchUpdate($uids, ['is_del' => 1, 'status' => 0], 'uid'); + } else { + $where[] = ['uid', '=', $uids]; + $re = $this->dao->update($uids, ['is_del' => 1, 'status' => 0], 'uid'); + } + if (!$re) + throw new AdminException('修改会员信息失败'); + $where[] = ['category', 'IN', ['exp']]; + /** @var UserBillServices $userbillServices */ + $userbillServices = app()->make(UserBillServices::class); + $userbillServices->update($where, ['status' => -1]); + return true; + } + + /** + * 个人中心获取用户等级信息 + * @param int $uid + * @return array + */ + public function homeGetUserLevel(int $uid, $userInfo = []) + { + $data = ['vip' => false, 'vip_id' => 0, 'vip_icon' => '', 'vip_name' => '']; + //用户存在 + if ($uid && sys_config('member_func_status', 0)) { + if (!$userInfo) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + } + if ($userInfo) { + $levelInfo = $this->getUerLevelInfoByUid($uid); + if (!$levelInfo) {//不存在等级 展示最低等级 + /** @var SystemUserLevelServices $systemUserLevel */ + $systemUserLevel = app()->make(SystemUserLevelServices::class); + $alllevelInfo = $systemUserLevel->getList([['is_del', '=', 0], ['is_show', '=', 1]], 'id,name,icon,grade', 1, 1); + $levelInfo = $alllevelInfo[0] ?? []; + if ($levelInfo) { + $levelInfo['id'] = 0; + } + } + if ($levelInfo) { + $data['vip'] = true; + $data['vip_id'] = $levelInfo['id']; + $data['vip_icon'] = $levelInfo['icon']; + $data['vip_name'] = $levelInfo['name']; + } + } + } + return $data; + } + + /** + * 根据用户uid 获取会员详细信息 + * @param int $uid + * @param string $field + */ + public function getUerLevelInfoByUid(int $uid, string $field = '') + { + $userLevelInfo = $this->dao->getUserLevel($uid); + $data = []; + if ($userLevelInfo) { + $data = ['id' => $userLevelInfo['id'], 'level_id' => $userLevelInfo['level_id'], 'add_time' => $userLevelInfo['add_time']]; + $data['discount'] = $userLevelInfo['levelInfo']['discount'] ?? 0; + $data['name'] = $userLevelInfo['levelInfo']['name'] ?? ''; + $data['money'] = $userLevelInfo['levelInfo']['money'] ?? 0; + $data['icon'] = set_file_url($userLevelInfo['levelInfo']['icon'] ?? ''); + $data['image'] = set_file_url($userLevelInfo['levelInfo']['image'] ?? ''); + $data['is_pay'] = $userLevelInfo['levelInfo']['is_pay'] ?? 0; + $data['grade'] = $userLevelInfo['levelInfo']['grade'] ?? 0; + $data['exp_num'] = $userLevelInfo['levelInfo']['exp_num'] ?? 0; + } + if ($field) return $data[$field] ?? ''; + return $data; + } + + /** + * 设置会员等级 + * @param $uid 用户uid + * @param $level_id 等级id + * @return UserLevel|bool|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function setUserLevel(int $uid, int $level_id, $vipinfo = []) + { + /** @var SystemUserLevelServices $systemLevelServices */ + $systemLevelServices = app()->make(SystemUserLevelServices::class); + if (!$vipinfo) { + $vipinfo = $systemLevelServices->getLevel($level_id); + if (!$vipinfo) { + throw new AdminException('会员等级不存在'); + } + } + /** @var UserServices $user */ + $user = app()->make(UserServices::class); + $userinfo = $user->getUserInfo($uid); + //把之前等级作废 + $this->dao->update(['uid' => $uid], ['status' => 0, 'is_del' => 1]); + //检查是否购买过 + $uservipinfo = $this->getWhereLevel(['uid' => $uid, 'level_id' => $level_id]); + $data['mark'] = '尊敬的用户' . $userinfo['nickname'] . '在' . date('Y-m-d H:i:s', time()) . '成为了' . $vipinfo['name']; + $data['add_time'] = time(); + if ($uservipinfo) { + $data['status'] = 1; + $data['is_del'] = 0; + if (!$this->dao->update(['id' => $uservipinfo['id']], $data)) + throw new AdminException('修改会员信息失败'); + } else { + $data = array_merge($data, [ + 'is_forever' => $vipinfo->is_forever, + 'status' => 1, + 'is_del' => 0, + 'grade' => $vipinfo->grade, + 'uid' => $uid, + 'level_id' => $level_id, + 'discount' => $vipinfo->discount, + ]); + $data['valid_time'] = 0; + if (!$this->dao->save($data)) throw new AdminException('写入会员信息失败'); + } + if (!$user->update(['uid' => $uid], ['level' => $level_id, 'exp' => $vipinfo['exp_num']])) + throw new AdminException('修改用户会员等级失败'); + return true; + } + + /** + * 会员列表 + * @param $where + * @return mixed + */ + public function getSytemList($where) + { + /** @var SystemUserLevelServices $systemLevelServices */ + $systemLevelServices = app()->make(SystemUserLevelServices::class); + return $systemLevelServices->getLevelList($where); + } + + /** + * 获取添加修改需要表单数据 + * @param int $id + * @return array + * @throws \FormBuilder\Exception\FormBuilderException + */ + public function edit(int $id) + { + + if ($id) { + $vipinfo = app()->make(SystemUserLevelServices::class)->getlevel($id); + if (!$vipinfo) { + throw new AdminException('数据不存在'); + } + $field[] = Form::hidden('id', $id); + $msg = '编辑会员等级'; + } else { + $msg = '添加会员等级'; + } + $field[] = Form::input('name', '等级名称', $vipinfo['name'] ?? '')->col(24)->required('请填写等级名称'); +// $field[] = Form::number('valid_date', '有效时间(天)', $vipinfo['valid_date'] ?? 0)->min(0)->col(12); + $field[] = Form::number('grade', '等级', $vipinfo['grade'] ?? 0)->min(0)->precision(0)->col(8); + $field[] = Form::number('discount', '享受折扣', $vipinfo['discount'] ?? 100)->min(0)->max(100)->col(8)->placeholder('输入折扣数100,代表原价,90代表9折'); + $field[] = Form::number('exp_num', '解锁需经验值达到', $vipinfo['exp_num'] ?? 0)->min(0)->precision(0)->col(8); + $field[] = Form::frameImage('icon', '图标', Url::buildUrl(config('admin.admin_prefix') . '/widget.images/index', array('fodder' => 'icon')), $vipinfo['icon'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true])->appendValidate(Iview::validateStr()->required()->message('请选择图标')); + $field[] = Form::frameImage('image', '会员背景', Url::buildUrl(config('admin.admin_prefix') . '/widget.images/index', array('fodder' => 'image')), $vipinfo['image'] ?? '')->icon('ios-add')->width('960px')->height('505px')->modal(['footer-hide' => true])->appendValidate(Iview::validateStr()->required()->message('请选择背景')); + $field[] = Form::radio('is_show', '是否显示', $vipinfo['is_show'] ?? 0)->options([['label' => '显示', 'value' => 1], ['label' => '隐藏', 'value' => 0]])->col(24); + $field[] = Form::textarea('explain', '等级说明', $vipinfo['explain'] ?? ''); + return create_form($msg, $field, Url::buildUrl('/user/user_level'), 'POST'); + } + + /* + * 会员等级添加或者修改 + * @param $id 修改的等级id + * @return json + * */ + public function save(int $id, array $data) + { + /** @var SystemUserLevelServices $systemUserLevel */ + $systemUserLevel = app()->make(SystemUserLevelServices::class); + $levelOne = $systemUserLevel->getWhereLevel(['is_del' => 0, 'grade' => $data['grade']]); + $levelTwo = $systemUserLevel->getWhereLevel(['is_del' => 0, 'exp_num' => $data['exp_num']]); + $levelThree = $systemUserLevel->getWhereLevel(['is_del' => 0, 'name' => $data['name']]); + $levelPre = $systemUserLevel->getPreLevel($data['grade']); + $levelNext = $systemUserLevel->getNextLevel($data['grade']); + if ($levelPre && $data['exp_num'] <= $levelPre['exp_num']) { + throw new AdminException('会员经验必须大于上一等级设置的经验'); + } + if ($levelNext && $data['exp_num'] >= $levelNext['exp_num']) { + throw new AdminException('会员经验必须小于下一等级设置的经验'); + } + //修改 + if ($id) { + if (($levelOne && $levelOne['id'] != $id) || ($levelThree && $levelThree['id'] != $id)) { + throw new AdminException('已检测到您设置过的会员等级,此等级不可重复'); + } + if ($levelTwo && $levelTwo['id'] != $id) { + throw new AdminException('已检测到您设置过该会员经验值,经验值不可重复'); + } + if (!$systemUserLevel->update($id, $data)) { + throw new AdminException('修改失败'); + } + + $data['id'] = $id; + $systemUserLevel->dao->cacheUpdate($data); + + return '修改成功'; + } else { + if ($levelOne || $levelThree) { + throw new AdminException('已检测到您设置过的会员等级,此等级不可重复'); + } + if ($levelTwo) { + throw new AdminException('已检测到您设置过该会员经验值,经验值不可重复'); + } + //新增 + $data['add_time'] = time(); + $res = $systemUserLevel->save($data); + if (!$res) { + throw new AdminException('添加失败'); + } + + $data['id'] = $res->id; + $systemUserLevel->cacheUpdate($data); + + return '添加成功'; + } + } + + /** + * 假删除 + * @param int $id + * @return mixed + */ + public function delLevel(int $id) + { + /** @var SystemUserLevelServices $systemUserLevel */ + $systemUserLevel = app()->make(SystemUserLevelServices::class); + $level = $systemUserLevel->getWhereLevel(['id' => $id]); + if ($level && $level['is_del'] != 1) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if ($userServices->count(['level' => $level['id']])) { + throw new AdminException('存在用户已是该等级,无法删除'); + } + if (!$systemUserLevel->update($id, ['is_del' => 1])) + throw new AdminException('删除失败'); + if (!$this->dao->update(['level_id' => $id], ['is_del' => 1])) { + throw new AdminException('删除失败'); + } + } + $systemUserLevel->cacheDelById($id); + return '删除成功'; + } + + /** + * 设置是否显示 + * @param int $id + * @param $is_show + * @return mixed + */ + public function setShow(int $id, int $is_show) + { + /** @var SystemUserLevelServices $systemUserLevel */ + $systemUserLevel = app()->make(SystemUserLevelServices::class); + if (!$systemUserLevel->getWhereLevel(['id' => $id])) + throw new AdminException('数据不存在'); + if ($systemUserLevel->update($id, ['is_show' => $is_show])) { + $systemUserLevel->cacheSaveValue($id, 'is_show', $is_show); + return $is_show == 1 ? '显示成功' : '隐藏成功'; + } else { + throw new AdminException($is_show == 1 ? '显示失败' : '隐藏失败'); + } + } + + /** + * 快速修改 + * @param int $id + * @param $is_show + * @return mixed + */ + public function setValue(int $id, array $data) + { + /** @var SystemUserLevelServices $systemUserLevel */ + $systemUserLevel = app()->make(SystemUserLevelServices::class); + if (!$systemUserLevel->getWhereLevel(['id' => $id])) + throw new AdminException('数据不存在'); + if ($systemUserLevel->update($id, [$data['field'] => $data['value']])) { + $systemUserLevel->cacheSaveValue($id, $data['field'], $data['value']); + return true; + } else { + throw new AdminException('保存失败'); + } + } + + /** + * 检测用户会员升级 + * @param $uid + * @return bool + */ + public function detection(int $uid) + { + //商城会员是否开启 + if (!sys_config('member_func_status')) { + return true; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('没有此用户,无法检测升级会员'); + } + //没有激活暂不升级 + if (!$user['level_status']) { + return true; + } + /** @var SystemUserLevelServices $systemUserLevel */ + $systemUserLevel = app()->make(SystemUserLevelServices::class); + $userAllLevel = $systemUserLevel->getList([['is_del', '=', 0], ['is_show', '=', 1], ['exp_num', '<=', (float)$user['exp']]]); + if (!$userAllLevel) { + return true; + } + $data = []; + $data['add_time'] = time(); + $userLevel = $this->dao->getColumn(['uid' => $uid, 'status' => 1, 'is_del' => 0], 'level_id'); + foreach ($userAllLevel as $vipinfo) { + if (in_array($vipinfo['id'], $userLevel)) { + continue; + } + $data['mark'] = '尊敬的用户' . $user['nickname'] . '在' . date('Y-m-d H:i:s', time()) . '成为了' . $vipinfo['name']; + $uservip = $this->dao->getOne(['uid' => $uid, 'level_id' => $vipinfo['id']]); + if ($uservip) { + //降级在升级情况 + $data['status'] = 1; + $data['is_del'] = 0; + if (!$this->dao->update($uservip['id'], $data, 'id')) { + throw new ValidateException('检测升级失败'); + } + } else { + $data = array_merge($data, [ + 'is_forever' => $vipinfo['is_forever'], + 'status' => 1, + 'is_del' => 0, + 'grade' => $vipinfo['grade'], + 'uid' => $uid, + 'level_id' => $vipinfo['id'], + 'discount' => $vipinfo['discount'], + ]); + if (!$this->dao->save($data)) { + throw new ValidateException('检测升级失败'); + } + } + $data['add_time'] += 1; + } + if (!$userServices->update($uid, ['level' => end($userAllLevel)['id']], 'uid')) { + throw new ValidateException('检测升级失败'); + } + return true; + } + + /** + * 会员等级列表 + * @param int $uid + */ + public function grade(int $uid) + { + //商城会员是否开启 + if (!sys_config('member_func_status')) { + return []; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('没有此用户,无法检测升级会员'); + } + $userLevelInfo = $this->getUerLevelInfoByUid($uid); + if (empty($userLevelInfo)) { + $level_id = 0; + } else { + $level_id = $userLevelInfo['level_id']; + } + /** @var SystemUserLevelServices $systemUserLevel */ + $systemUserLevel = app()->make(SystemUserLevelServices::class); + return $systemUserLevel->getLevelListAndGrade($level_id); + } + + /** + * 获取会员信息 + * @param int $uid + * @return array[] + */ + public function getUserLevelInfo(int $uid) + { + $data = ['user' => [], 'level_info' => [], 'level_list' => [], 'task' => []]; + //商城会员是否开启 + if (!sys_config('member_func_status')) { + return $data; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('没有此会员'); + } + /** @var StoreCouponUserServices $storeCoupon */ + $storeCoupon = app()->make(StoreCouponUserServices::class); + $user['couponCount'] = $storeCoupon->getUserValidCouponCount((int)$uid); + try { + //检测升级 + $this->detection($uid); + } catch (\Throwable $e) { + + } + + $data['user'] = $user; + /** @var SystemUserLevelServices $systemUserLevel */ + $systemUserLevel = app()->make(SystemUserLevelServices::class); + $levelList = $systemUserLevel->getList(['is_del' => 0, 'is_show' => 1]); + $i = 0; + foreach ($levelList as &$level) { + $level['next_exp_num'] = $levelList[$i + 1]['exp_num'] ?? $level['exp_num']; + $level['image'] = set_file_url($level['image']); + $level['icon'] = set_file_url($level['icon']); + $i++; + } + $data['level_list'] = $levelList; + $levelInfo = $this->getUerLevelInfoByUid($uid); + if (!$levelInfo) {//不存在等级 展示最低等级 + /** @var SystemUserLevelServices $systemUserLevel */ + $systemUserLevel = app()->make(SystemUserLevelServices::class); + $alllevelInfo = $systemUserLevel->getList([['is_del', '=', 0], ['is_show', '=', 1]], 'id,name,icon,grade', 1, 1); + $levelInfo = $alllevelInfo[0] ?? []; + if ($levelInfo) { + $levelInfo['id'] = 0; + } + } + if ($levelInfo) { + $levelInfo['vip'] = true; + $levelInfo['vip_id'] = $levelInfo['id']; + $levelInfo['vip_icon'] = $levelInfo['icon']; + $levelInfo['vip_name'] = $levelInfo['name']; + } + $data['level_info'] = $levelInfo; + $i = 0; + foreach ($levelList as &$level) { + if ($level['grade'] < $levelInfo['grade']) { + $level['next_exp_num'] = $levelList[$i + 1]['exp_num'] ?? $level['exp_num']; + } else { + $level['next_exp_num'] = $level['exp_num']; + } + $level['image'] = set_file_url($level['image']); + $level['icon'] = set_file_url($level['icon']); + $i++; + } + $data['level_list'] = $levelList; + + $data['level_info']['exp'] = $user['exp'] ?? 0; + /** @var UserBillServices $userBillservices */ + $userBillservices = app()->make(UserBillServices::class); + $data['level_info']['today_exp'] = $userBillservices->getExpSum($uid, 'today'); + $task = []; + /** @var UserSignServices $userSignServices */ + $userSignServices = app()->make(UserSignServices::class); + $task['sign_count'] = $userSignServices->getSignSumDay($uid); + $config = SystemConfigService::more(['sign_give_exp', 'order_give_exp', 'invite_user_exp']); + $task['sign'] = $config['sign_give_exp'] ?? 0; + $task['order'] = $config['order_give_exp'] ?? 0; + $task['invite'] = $config['invite_user_exp'] ?? 0; + $data['task'] = $task; + return $data; + } + + /** + * 经验列表 + * @param int $uid + * @return array + */ + public function expList(int $uid) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('没有此用户'); + } + /** @var UserBillServices $userBill */ + $userBill = app()->make(UserBillServices::class); + $data = $userBill->getExpList($uid, [], 'id,title,number,pm,add_time'); + $list = $data['list'] ?? []; + return $list; + } + + /** + * 获取激活会员卡需要的信息 + * @return mixed + */ + public function getActivateInfo() + { + //商城会员是否开启 + if (!sys_config('member_func_status')) { + throw new ValidateException('会员卡功能暂未开启'); + } + //是否需要激活 + if (!sys_config('level_activate_status')) { + throw new ValidateException('会员卡功能暂不需要激活'); + } + return SystemConfigService::get('level_extend_info'); + } + + /** + * 激活会员卡 + * @param int $uid + * @param array $data + * @return array + */ + public function userActivatelevel(int $uid, array $data) + { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (!$user) { + throw new ValidateException('用户已注销,或不存在'); + } + if ($user['level_status']) { + throw new ValidateException('不需要重复激活'); + } + $extend_info = $userServices->handelExtendInfo($data, true) ?: []; + $update = ['level_status' => 1]; + if ($extend_info) { + $default = $userServices->defaultExtendInfo; + $params = array_column($default, 'param'); + $sex = $userServices->sex; + $update['level_extend_info'] = $extend_info; + foreach ($extend_info as $info) { + if (isset($info['param']) && in_array($info['param'], $params) && isset($info['value'])) { + if ($info['param'] == 'sex') { + $update['sex'] = $sex[$info['value']] ?? 0; + } elseif ($info['param'] == 'birthday') { + $update['birthday'] = strtotime($info['value']); + } else { + $update[$info['param']] = $info['value']; + } + } + } + } + $userServices->update($uid, $update); + $data = []; + //获取激活送好礼 + $data = SystemConfigService::more([ + 'level_integral_status', + 'level_give_integral', + 'level_money_status', + 'level_give_money', + 'level_coupon_status', + 'level_give_coupon', + ]); + $ids = $data['level_give_coupon'] ?? []; + $data['level_give_coupon'] = []; + if ($data['level_coupon_status'] && $ids) { + /** @var StoreCouponIssueServices $couponServices */ + $couponServices = app()->make(StoreCouponIssueServices::class); + $coupon = $couponServices->getList(['id' => $ids]); + $data['level_give_coupon'] = $coupon; + } + if (!$data['level_integral_status']) { + $data['level_give_integral'] = 0; + } + if (!$data['level_money_status']) { + $data['level_give_money'] = 0; + } + //激活会员卡事件 + event('user.activate.level', [$uid]); + return $data; + } +} diff --git a/app/services/user/member/MemberCardServices.php b/app/services/user/member/MemberCardServices.php new file mode 100644 index 0000000..2d7f23d --- /dev/null +++ b/app/services/user/member/MemberCardServices.php @@ -0,0 +1,455 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\user\member; + + +use app\dao\user\member\MemberCardDao; +use app\services\BaseServices; +use app\services\order\OtherOrderServices; +use app\services\order\StoreOrderCreateServices; +use crmeb\exceptions\AdminException; +use crmeb\services\SystemConfigService; +use think\exception\ValidateException; +use app\services\user\UserServices; +use think\Route; + +/** + * Class MemberCardServices + * @package app\services\user\member + * @mixin MemberCardDao + */ +class MemberCardServices extends BaseServices +{ + + /** + * 初始化,获得dao层句柄 + * MemberCardServices constructor. + * @param MemberCardDao $memberCardDao + */ + public static $_memberTypePrefix = ['month', 'quarter', 'year', 'ever', 'free']; + + public function __construct(MemberCardDao $memberCardDao) + { + $this->dao = $memberCardDao; + } + + public function getSearchList(array $where = []) + { + [$page, $limit] = $this->getPageValue(); + $where['batch_card_id'] = $where['card_batch_id']; + if ($where['is_use'] != "") { + if ($where['is_use'] == 0) { + $where['use_time'] = 0; + } else { + $where['use_time'] = 1; + } + } + unset($where['is_use']); + $list = $this->dao->getSearchList($where, $page, $limit); + if ($list) { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $userInfos = $userService->getColumn([['uid', 'in', array_unique(array_column($list, 'use_uid'))]], 'uid,real_name,nickname,phone', 'uid'); + foreach ($list as $k => $v) { + $list[$k]['username'] = $list[$k]['phone'] = ''; + $user_info = $userInfos[$v['use_uid']] ?? []; + if ($v['use_uid'] && $user_info) { + $list[$k]['username'] = $user_info['real_name'] ? $user_info['real_name'] : $user_info['nickname']; + $list[$k]['phone'] = $user_info ? $user_info['phone'] : ""; + } + $list[$k]['add_time'] = date('Y-m-d H:i:s', $v['add_time']); + $list[$k]['use_time'] = $v['use_time'] != 0 ? date('Y-m-d H:i:s', $v['use_time']) : "未使用"; + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + + } + + /** + * 生成免费会员卡 + * @param array $data + * @return bool + */ + public function addCard(array $data) + { + if (!isset($data['card_batch_id']) || !$data['card_batch_id'] || $data['card_batch_id'] == 0 || !isset($data['total_num']) || !$data['total_num'] || $data['total_num'] == 0) { + throw new AdminException("非法参数"); + } + try { + if (!isset($data['total_num'])) throw new AdminException("数据缺失"); + $num = $data['total_num']; + unset($data['total_num']); + for ($i = 0; $i < $num; $i++) { + $data['card_number'] = $this->makeRandomNumber("CR", $data['card_batch_id']); + $data['card_password'] = $this->makeRandomNumber(); + $data['status'] = 1; + $data['add_time'] = time(); + $res[] = $data; + } + //数据切片批量插入,提高性能。 + $chunk_inster_card = array_chunk($res, 100, true); + foreach ($chunk_inster_card as $v) { + $this->dao->saveAll($v); + } + return true; + } catch (\Exception $exception) { + throw new AdminException("生成卡失败"); + } + } + + /** + * 获取制卡卡号随机数 + * @param false $prefix + * @param false $random + * @return string + */ + public function makeRandomNumber($prefix = false, $random = false) + { + if (!$prefix) { + $prefix = ""; + } + if (!$random || !is_numeric($random)) { + mt_srand(); + $one_random = mt_rand(11111, 99999); + } else { + $one_random = sprintf("%05d", $random); + } + $date_random = date('ymd', time()); + $random_tmp = strlen($one_random); + mt_srand(); + $two_randow = str_pad(mt_rand(1, 99999), $random_tmp, '0', STR_PAD_LEFT); + if (!$random) { + return $two_randow; + } else { + return $prefix . $one_random . $date_random . $two_randow; + } + } + + /** + * 领取会员卡 + * @param array $data + * @param int $uid + */ + public function drawMemberCard(array $data, int $uid) + { + if (!$uid || !$data) throw new ValidateException('参数缺失!'); + $isOpenMember = $this->isOpenMemberCardCache(); + if (!$isOpenMember) throw new ValidateException('会员功能暂未开启!'); + if (!isset($data['member_card_code']) || !$data['member_card_code']) throw new ValidateException('请输入会员卡号!'); + if (!isset($data['member_card_code']) || !$data['member_card_pwd']) throw new ValidateException('请输入领取卡密!'); + $card_info = $this->dao->getOneByWhere(['card_number' => trim($data['member_card_code'])]); + if (!$card_info) throw new ValidateException('会员卡不存在!'); + /** @var MemberCardBatchServices $memberBatchServices */ + $memberBatchServices = app()->make(MemberCardBatchServices::class); + $batch_info = $memberBatchServices->getOne($card_info['card_batch_id']); + if (!$batch_info) throw new ValidateException('会员卡未激活,暂无法使用.'); + if ($batch_info->status != 1) throw new ValidateException('会员卡未激活,暂无法使用..'); + if ($card_info['status'] == 0) throw new ValidateException('会员卡暂未激活'); + if ($card_info['card_password'] != trim($data['member_card_pwd'])) throw new ValidateException('会员卡密码有误!'); + if ($card_info['use_uid'] && $card_info['use_time']) throw new ValidateException('会员卡已使用!'); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user_info = $userServices->getUserInfo($uid); + if (!$user_info) throw new ValidateException('用户不存在,请重新登录'); + if ($user_info->is_money_level > 0 && $user_info->is_ever_level == 1) throw new ValidateException('您已是永久会员,无需再领取,可以将此卡转送亲朋好友,一起享受优惠'); + + /** + * 批次卡具体使用期限,业务需要打开即可,勿删。 + */ + if ($card_info->status != 1) throw new ValidateException('会员卡未激活,暂无法使用...'); + $this->transaction(function () use ($card_info, $user_info, $batch_info, $memberBatchServices, $userServices, $data) { + $res1 = $this->dao->update($card_info->id, ['use_uid' => $user_info->uid, 'use_time' => time(), 'update_time' => time()], 'id'); + if ($res1) { + $res2 = $memberBatchServices->useCardSetInc($batch_info->id, 'use_num', 1); + if ($user_info->overdue_time > time()) { + $overdue_time = bcadd(bcmul($batch_info->use_day, 86400, 0), $user_info->overdue_time, 0); + } else { + $overdue_time = bcadd(bcmul($batch_info->use_day, 86400, 0), time(), 0); + } + $channel_type = $data['from']; + /** @var OtherOrderServices $OtherOrderServices */ + $OtherOrderServices = app()->make(OtherOrderServices::class); + $storeOrderCreateService = app()->make(StoreOrderCreateServices::class); + $record_data['uid'] = $user_info->uid; + $record_data['member_code'] = $card_info->card_number; + $record_data['use_day'] = $batch_info->use_day; + $record_data['overdue_time'] = $overdue_time; + $record_data['order_id'] = $storeOrderCreateService->getNewOrderId(); + $record_data['channel_type'] = $channel_type; + $record_data['member_type'] = "free"; + $record_data['vip_day'] = $batch_info->use_day; + $record_data['type'] = 2; + $record_data['paid'] = 1; + $record_data['pay_time'] = time(); + $res3 = $OtherOrderServices->addOtherOrderData($record_data, 2); + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $res4 = $userServices->setMemberOverdueTime($batch_info->use_day, $user_info->uid, 2, $record_data['member_type']); + $res5 = $res1 && $res2 && $res3 && $res4; + return $res5; + } + }); + + + } + + /** 验证是否存在此类型会员卡 + * @param string $member_type + * @return bool + */ + public function checkmemberType(string $member_type) + { + $member_type_arr = $this->getMemberTypeInfo(); + if (!array_key_exists($member_type, $member_type_arr)) throw new ValidateException('暂无此类型会员卡'); + return true; + } + + /** 获取会员权益和说明配置 + * @return array + */ + public function getMemberRightsInfo() + { + /** @var MemberRightServices $memberRightService */ + $memberRightService = app()->make(MemberRightServices::class); + $memberRight = $memberRightService->getSearchList(['status' => 1]); + if ($memberRight['list']) { + foreach ($memberRight['list'] as $k => &$v) { + $v['title'] = $v['show_title']; + $v['pic'] = $v['image']; + $v['right'] = $v['explain']; + $v['number'] = $v['number']; + } + } + + return ['member_right' => $memberRight['list']]; + } + + /** + * 获取会员卡配置 + * @return array + */ + public function getMemberTypeInfo() + { + /** @var SystemConfigService $systemConfigService */ + $systemConfigService = app()->make(SystemConfigService::class); + $data = []; + foreach (self::$_memberTypePrefix as $v) { + $data[$v] = $systemConfigService::more([$v . '_title', $v . '_vip_day', $v . '_pre_price', $v . '_price']); + } + return $data; + } + + /** + * 会员卡数据处理 + * @param int $uid + * @param bool $is_free + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function DoMemberType(int $uid = 0, bool $is_free = true) + { + $data = array(); + /** @var MemberShipServices $memberShipService */ + $memberShipService = app()->make(MemberShipServices::class); + if ($is_free) { + $where = [['is_del', '=', 0]]; + } else { + $where = [['is_del', '=', 0], ['type', '<>', 'free']]; + } + $userInfo = []; + if ($uid) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + } + $list = $memberShipService->getApiList($where); + foreach ($list as $v) { + $data[] = [ + 'id' => $v['id'], + 'title' => $v['title'], + 'type' => $v['type'], + 'vip_day' => $v['vip_day'], + 'pre_price' => $v['pre_price'], + 'price' => $v['price'], + 'overdue_time' => $memberShipService->getOverdueTime($uid, (int)$v['id'], $userInfo, $v) + ]; + } + return $data; + } + + /** + * 会员类型数据 + * @param int $uid + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMemberTypeValue(int $uid = 0) + { + $member_type = $this->DoMemberType($uid); + $new_member_data = []; + if ($member_type) { + foreach ($member_type as $k => $v) { + $new_member_data[$v['id']] = $v; + } + } + return $new_member_data; + } + + /** + * 导出会员卡 + * @param $where + * @return \think\Collection + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getExportData($where, int $limit = 0) + { + if ($limit) { + [$page] = $this->getPageValue(); + } else { + [$page, $limit] = $this->getPageValue(); + } + $data = $this->dao->getSearchList($where, $page, $limit); + if ($data) { + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $userInfos = $userService->getColumn([['uid', 'in', array_unique(array_column($data, 'use_uid'))]], 'uid,real_name,nickname,phone', 'uid'); + foreach ($data as $k => $v) { + $data[$k]['use_time'] = $v['use_time'] != 0 ? date('Y-m-d H:i:s', $v['use_time']) : ""; + $data[$k]['user_name'] = ''; + $data[$k]['user_phone'] = ''; + $userInfo = $userInfos[$v['use_uid']] ?? []; + if ($v['use_uid'] && $userInfo) { + $data[$k]['user_name'] = $userInfo['real_name'] ? $userInfo['real_name'] : $userInfo['nickname']; + $data[$k]['user_phone'] = $userInfo['phone']; + } + } + } + /** @var MemberCardBatchServices $batchService */ + $batchService = app()->make(MemberCardBatchServices::class); + $batchInfo = $batchService->getOne($where['batch_card_id']); + $dataArray['title'] = $batchInfo ? $batchInfo['title'] : ""; + $dataArray['data'] = $data; + return $dataArray; + } + + /** + * 获取会员记录 + * @param array $where + * @return array + */ + public function getSearchRecordList(array $where) + { + /** @var OtherOrderServices $otherOrderSevice */ + $otherOrderSevice = app()->make(OtherOrderServices::class); + return $otherOrderSevice->getMemberRecord($where); + } + + /** + * 看是会员权益时候开启|并返回数据 + * @param string $rightType + * @param bool $get_number + * @param int $member_status + * @return bool|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function isOpenMemberCard(string $rightType = '', bool $get_number = true, $member_status = -1) + { + if ($member_status == -1) { + $isOpen = sys_config('member_card_status', 1); + } else { + $isOpen = $member_status; + } + //如果传入权益类别,查看是否具有某权益 + if (!$rightType) { + if ($isOpen) return true; + return false; + } else { + if ($isOpen) { + /** @var MemberRightServices $memberRightService */ + $memberRightService = app()->make(MemberRightServices::class); + $memberRight = $memberRightService->getOne(['right_type' => $rightType], 'status,number'); + if ($memberRight && $memberRight['status']) { + if ($get_number) { + $number = $memberRight['number']; + if (!$number) return false; + return $number; + } + return true; + } + } + return false; + } + + } + + /** + * 看是会员权益时候开启|并返回数据 + * @param string $rightType + * @param bool $get_number + * @param int $member_status + * @param int $expire + * @return bool|mixed|null + * @throws \throwable + */ + public function isOpenMemberCardCache(string $rightType = '', bool $get_number = true, $member_status = -1, int $expire = 30) + { + if ($member_status == -1) { + $isOpen = sys_config('member_card_status', 1); + } else { + $isOpen = $member_status; + } + //如果传入权益类别,查看是否具有某权益 + if (!$rightType) { + if ($isOpen) return true; + return false; + } else { + if ($isOpen) { + /** @var MemberRightServices $memberRightService */ + $memberRightService = app()->make(MemberRightServices::class); + $memberRight = $memberRightService->getMemberRightCache($rightType); + if ($memberRight && $memberRight['status']) { + if ($get_number) { + $number = $memberRight['number']; + if (!$number) return false; + return $number; + } + return true; + } + return false; + } + return false; + } + + } + + /** + * 修改会员卡状态 + * @param $id + * @param $status + * @return bool + */ + public function setStatus($id, $status) + { + $res = $this->dao->update($id, ['status' => $status]); + if ($res) return true; + return false; + } +} diff --git a/app/services/user/member/MemberRightServices.php b/app/services/user/member/MemberRightServices.php new file mode 100644 index 0000000..df8da1c --- /dev/null +++ b/app/services/user/member/MemberRightServices.php @@ -0,0 +1,152 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\user\member; + +use app\dao\user\member\MemberRightDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; + +/** + * Class MemberRightServices + * @package app\services\user\member + * @mixin MemberRightDao + */ +class MemberRightServices extends BaseServices +{ + /** + * MemberRightServices constructor. + * @param MemberRightDao $memberRightDao + */ + public function __construct(MemberRightDao $memberRightDao) + { + $this->dao = $memberRightDao; + } + + /** + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getSearchList(array $where = []) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getSearchList($where, $page, $limit); + foreach ($list as &$item) { + $item['image'] = set_file_url($item['image']); + } + $count = $this->dao->count($where); + return compact('list', 'count'); + + } + + /** + * 编辑保存 + * @param int $id + * @param array $data + */ + public function save(int $id, array $data) + { + if (!$data['right_type']) throw new AdminException("会员权益类型缺失"); + if (!$id) throw new AdminException("id参数缺失"); + if (!$data['title'] || !$data['show_title']) throw new AdminException("请设置权益名称"); + if (!$data['image']) throw new AdminException("请上传会员权益图标"); + switch ($data['right_type']) { + case "integral": + if (!$data['number']) throw new AdminException("请设置返还积分倍数"); + if ($data['number'] < 0) throw new AdminException("返还积分倍数不能为负数"); + $save['number'] = abs($data['number']); + break; + case "express" : + if (!$data['number']) throw new AdminException("请设置运费折扣"); + if ($data['number'] < 0) throw new AdminException("运费折扣不能为负数"); + $save['number'] = abs($data['number']); + break; + case "sign" : + if (!$data['number']) throw new AdminException("请设置签到积分倍数"); + if ($data['number'] < 0) throw new AdminException("签到积分倍数不能为负数"); + $save['number'] = abs($data['number']); + break; + case "offline" : + if (!$data['number']) throw new AdminException("请设置线下付款折扣"); + if ($data['number'] < 0) throw new AdminException("线下付款不能为负数"); + $save['number'] = abs($data['number']); + } + $save['show_title'] = $data['show_title']; + $save['image'] = $data['image']; + $save['status'] = $data['status']; + $save['sort'] = $data['sort']; + $this->dao->update($id, $data); + $right = $this->dao->get($id); + if ($right) { + $this->dao->cacheUpdate($right->toArray()); + } + return true; + } + + /** + * 获取单条信息 + * @param array $where + * @param string $field + * @return array|false|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOne(array $where, string $field = '*') + { + if (!$where) return false; + return $this->dao->getOne($where, $field); + } + + /** + * 查看某权益是否开启 + * @param $rightType + * @return bool + */ + public function getMemberRightStatus($rightType) + { + if (!$rightType) return false; + $status = $this->dao->value(['right_type' => $rightType], 'status'); + if ($status) return true; + return false; + } + + /** + * 在缓存中查找权益等级 + * @param string $rightType + * @return false|mixed + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/10 + */ + public function getMemberRightCache(string $rightType) + { + $list = $this->dao->cacheList(); + if ($list === null || (is_array($list) && !$list)) { + $list = $this->dao->getSearchList(['status' => 1]); + $this->dao->cacheCreate($list); + } + + foreach ($list as $item) { + if ($rightType == $item['right_type']) { + return $item; + } + } + return false; + } + +} diff --git a/app/services/user/member/MemberShipServices.php b/app/services/user/member/MemberShipServices.php new file mode 100644 index 0000000..b7f9876 --- /dev/null +++ b/app/services/user/member/MemberShipServices.php @@ -0,0 +1,204 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\user\member; + +use app\dao\user\member\MemberShipDao; +use app\services\BaseServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use think\exception\ValidateException; + +/** + * Class MemberShipServices + * @package app\services\user\member + * @mixin MemberShipDao + */ +class MemberShipServices extends BaseServices +{ + + public function __construct(MemberShipDao $memberShipDao) + { + $this->dao = $memberShipDao; + } + + /** + * 获取单个付费会员信息 + * @param int $id + * @param string $field + * @return array|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getMemberInfo(int $id, string $field = '*') + { + $member = $this->dao->getOne(['id' => $id, 'is_del' => 0], $field); + if (!$member) { + throw new ValidateException('该会员类型不存在或已删除'); + } + return $member; + } + + /** + * 后台获取会员类型 + * @param array $where + * @return array + */ + public function getSearchList(array $where = []) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getSearchList($where, $page, $limit); + $count = $this->dao->count($where); + return compact('list', 'count'); + + } + + /** + * 获取会员卡api接口 + * @return mixed + */ + public function getApiList(array $where) + { + return $this->dao->getApiList($where); + } + + /** + * 卡类型编辑保存 + * @param int $id + * @param array $data + */ + public function save(int $id, array $data) + { + if (!$data['title']) throw new AdminException("请填写会员名称"); + if (!$data['type']) throw new AdminException("会员类型缺失"); + if ($data['type'] == "ever") { + $data['vip_day'] = -1; + } else { + if (!$data['vip_day']) throw new AdminException("请填写体验天数"); + if ($data['vip_day'] < 0) throw new AdminException("体验天数不能为负数"); + } + if ($data['type'] == "free") { +// $data['price'] = 0.00; + $data['pre_price'] = 0.00; + } else { + if ($data['pre_price'] == 0 || $data['price'] == 0) throw new AdminException("请填写价格"); + } + if ($data['pre_price'] < 0 || $data['price'] < 0) throw new AdminException("价格不能为负数"); + if ($data['pre_price'] > $data['price']) throw new AdminException("优惠价不能大于原价"); + if ($id) { + $data['id'] = $id; + $this->dao->cacheUpdate($data); + return $this->dao->update($id, $data); + } else { + $res = $this->dao->save($data); + $data['id'] = $res->id; + $this->dao->cacheUpdate($data); + return $res; + } + } + + /** + * 获取卡会员天数 + * @param array $where + * @return mixed + */ + public function getVipDay(array $where) + { + return $this->dao->value($where, 'vip_day'); + } + + /** + * 修改会员类型状态 + * @param $id + * @param $is_del + * @return bool + */ + public function setStatus($id, $is_del) + { + $res = $this->dao->update($id, ['is_del' => $is_del]); + if ($is_del) { + $this->cacheDelById($id); + } else { + $this->cacheSaveValue($id, 'is_del', 0); + } + if ($res) return true; + return false; + } + + /** + * 查询会员类型select + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getShipSelect() + { + $menus = []; + foreach ($this->dao->getSearchList(['is_del' => 0], 0, 0, ['id', 'title']) as $menu) { + $menus[] = ['value' => $menu['id'], 'label' => $menu['title']]; + } + $menus[] = ['value' => 'card' , 'label' => '卡密激活']; + return $menus; + } + + + /** + * 获取过期时间 + * @param int $uid + * @param int $id + * @param $userInfo + * @param $memberInfo + * @return false|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getOverdueTime(int $uid, int $id, $userInfo = [], $memberInfo = []) + { + if (!$id) { + throw new ValidateException('缺少会员ID'); + } + if (!$memberInfo) { + /** @var MemberShipServices $memberShipService */ + $memberShipService = app()->make(MemberShipServices::class); + $memberInfo = $memberShipService->get($id); + } + if (!$memberInfo) { + throw new ValidateException('会员卡类型不存在'); + } + if ($uid && !$userInfo) { + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $userInfo = $userServices->getUserInfo($uid); + } + $member_type = $memberInfo['type']; + $vip_day = $memberInfo['vip_day']; + if ($member_type == 'ever') { + $overdue_time = 0; + $is_ever_level = 1; + } else { + if (!$userInfo || $userInfo['is_money_level'] == 0) { + $overdue_time = bcadd(bcmul($vip_day, 86400, 0), time(), 0); + } else { + $overdue_time = bcadd(bcmul($vip_day, 86400, 0), $userInfo['overdue_time'], 0); + } + $is_ever_level = 0; + } + if ($is_ever_level == 1) { + $res = "永久会员"; + } else { + $res = date('Y-m-d', $overdue_time); + } + return $res; + } +} diff --git a/app/services/wechat/WechatCardServices.php b/app/services/wechat/WechatCardServices.php new file mode 100644 index 0000000..4bdcbe2 --- /dev/null +++ b/app/services/wechat/WechatCardServices.php @@ -0,0 +1,266 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\wechat; + + +use app\dao\wechat\WechatCardDao; +use app\services\BaseServices; +use crmeb\exceptions\AdminException; +use crmeb\services\DownloadImageService; +use crmeb\services\FormBuilder; +use crmeb\services\SystemConfigService; +use crmeb\services\wechat\OfficialAccount; +use think\facade\Log; + +/** + * 微信卡券 + * Class WechatCardServices + * @package app\services\wechat + * @mixin WechatCardDao + */ +class WechatCardServices extends BaseServices +{ + protected $builder; + + protected $code_type = [ + 'CODE_TYPE_NONE' => '文本', + 'CODE_TYPE_BARCODE' => '一维码', + 'CODE_TYPE_QRCODE' => '二维码', + 'CODE_TYPE_ONLY_QRCODE' => '仅显示二维码', + 'CODE_TYPE_ONLY_BARCODE' => '仅显示一维码', + 'CODE_TYPE_NONE' => '不显示任何码型' + ]; + + /** + * 构造方法 + * WechatCardServices constructor. + * @param WechatCardDao $dao + */ + public function __construct(WechatCardDao $dao, FormBuilder $formBuilder) + { + $this->dao = $dao; + $this->builder = $formBuilder; + } + + + public function getList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $count = $this->dao->count($where); + $list = $this->dao->getList($where, $page, $limit); + return compact('count', $list); + } + + /** + * 获取微信会员卡信息 + * @return array + */ + public function getInfo() + { + if (!sys_config('wechat_appid') || !sys_config('wechat_appsecret')) { + throw new AdminException('请先配置公众号的appid、appSecret等参数,且需要开通微信卡券功能'); + } + $data = []; + try { + $data['color'] = OfficialAccount::getCardColors()['colors'] ?? []; + } catch (\Throwable $e) { + $data['color'] = []; + } + if (!$data['color']) { + throw new AdminException('请先在公众号后台开通微信卡券功能'); + } + $info = $this->dao->getOne(['card_type' => 'member_card', 'status' => 1, 'is_del' => 0]); + $data['info'] = ['custom_cell' => []]; + $data['selet'] = 0; + if ($info) { + $info = $info->toArray(); + $info['custom_cell'] = $info['especial']['custom_cell'] ?? []; + $data['info'] = $info; + $data['selet'] = $info['background_pic_url'] ? 1 : 0; + + } + return $data; + } + + public function createForm($formData) + { + $f[] = $this->builder->input('brand_name', '商户名称', $formData['brand_name'] ?? '')->required('请填写名称'); + $f[] = $this->builder->input('title', '卡券名称', $formData['title'] ?? '')->required('请填写卡券名称'); + $f[] = $this->builder->input('service_phone', '电话', $formData['service_phone'] ?? '')->required('请填写电话'); + $f[] = $this->builder->select('code_type', 'CODE展示类型', $formData['code_type'])->setOptions(FormBuilder::setOptions($this->code_type))->multiple(true)->required('请选择CODE展示类型'); + $f[] = $this->builder->input('color', '卡券颜色', $formData['color'] ?? '')->required('请填写卡券颜色'); + $f[] = $this->builder->frameImage('logo_url', 'LOGO', $this->url(config('admin.admin_prefix') . '/widget.images/index', ['fodder' => 'logo_url'], true), $formData['logo_url'] ?? '')->info('建议300*300')->icon('ios-add')->width('960px')->height('550px')->modal(['footer-hide' => true]); + $f[] = $this->builder->frameImage('background_pic_url', '背景图', $this->url(config('admin.admin_prefix') . '/widget.images/index', ['fodder' => 'background_pic_url'], true), $formData['background_pic_url'] ?? '')->info('建议1000*600')->icon('ios-add')->width('960px')->height('550px')->modal(['footer-hide' => true]); + $f[] = $this->builder->input('notice', '提示', $formData['notice'] ?? '')->required('请填写提示信息'); + $f[] = $this->builder->input('description', '描述', $formData['description'] ?? '')->required('请填写描述'); + $f[] = $this->builder->textarea('prerogative', '卡券特权说明', $formData['notice'] ?? '')->info('会员卡特权说明,限制1024汉字')->required('请填写卡券特权说明'); + return create_form('微信卡券添加', $f, $this->url('/wechat/card/0'), 'post'); + } + + /** + * 添加|编辑微信卡券 + * @param int $id + * @param array $data + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function save(array $data) + { + $card = $this->dao->getOne(['card_type' => 'member_card', 'status' => 1, 'is_del' => 0]); + if ($card) { + [$cardInfo, $especial] = $this->wxCreate($data, $card['card_id']); + unset($data['custom_cell']); + $data['especial'] = $especial; + if (!$this->dao->update($card['id'], $data)) { + throw new AdminException('修改失败'); + } + } else { + [$cardInfo, $especial] = $this->wxCreate($data); + $this->activateUserForm($cardInfo['card_id']); + unset($data['custom_cell']); + $data['card_id'] = $cardInfo['card_id']; + $data['especial'] = $especial; + if (!$this->dao->save($data)) { + throw new AdminException('添加失败'); + } + } + return true; + } + + /** + * 微信卡券添加|编辑 + * @param array $card + * @param string $card_id + * @param string $card_type + * @return array + * @throws \EasyWeChat\Core\Exceptions\InvalidArgumentException + */ + public function wxCreate(array $card, string $card_id = '', string $card_type = 'member_card') + { + if (!sys_config('wechat_appid') || !sys_config('wechat_appsecret')) { + throw new AdminException('请先配置公众号的appid、appSecret等参数,且需要开通微信卡券功能'); + } + $baseUrl = sys_config('site_url'); + try { + $logo = root_path() . 'public' . app()->make(DownloadImageService::class)->downloadImage($card['logo_url'])['path']; + $background = $card['background_pic_url'] ? (root_path() . 'public' . app()->make(DownloadImageService::class)->downloadImage($card['background_pic_url'])['path']) : ''; + } catch (\Throwable $e) { + Log::error('添加会员卡券封面图出错误,原因:' . $e->getMessage()); + $logo = root_path() . 'public' . $card['logo_url']; + $background = root_path() . 'public' . $card['background_pic_url']; + } + $base_info = [ + 'logo_url' => OfficialAccount::temporaryUpload($logo)->media_id, + 'brand_name' => $card['brand_name'], + 'code_type' => 'CODE_TYPE_NONE', + 'title' => $card['title'], + 'color' => $card['color'] ?: 'Color010', + 'notice' => $card['notice'], + 'service_phone' => $card['service_phone'], + 'description' => $card['description'], + 'use_custom_code' => false, + 'can_share' => true, + 'can_give_friend' => false, + 'get_limit' => 1, + 'date_info' => ['type' => 'DATE_TYPE_PERMANENT'], + 'sku' => ['quantity' => 50000000], + 'promotion_url' => $baseUrl . '/pages/users/user_spread_user/index', + 'promotion_url_name' => '推荐给朋友', + 'promotion_url_sub_title' => '' + ]; + if ($card['center_title']) { + $base_info = array_merge($base_info, [ + 'center_title' => $card['center_title'], + 'center_sub_title ' => $card['center_sub_title'], + 'center_url' => $card['center_url'], + ]); + } + $integral = SystemConfigService::more(['integral_max_num', 'integral_ratio', 'order_give_integral']); + $especial = [ +// 'name' => $card['title'],//入口名称 +// 'tips' => $card['description'],//入口提示语 +// 'url' => $baseUrl,//入口跳转地址 + 'supply_bonus' => true,//显示积分 + 'bonus_rule' => [ + 'cost_money_unit' => 100,//消费金额。以分为单位 + 'increase_bonus' => $integral['order_give_integral'],//对应增加的积分 + 'max_increase_bonus' => $integral['integral_max_num'],//用户单次可获取的积分上限 + 'cost_bonus_unit' => 1,//每使用X积分 + 'reduce_money' => bcmul($integral['integral_ratio'], '100', 0),//抵扣xx元,(这里以分为单位) + ], + 'supply_balance' => false,//是否支持充值 +// 'activate_url' => $baseUrl, + 'bonus_url' => $baseUrl . '/pages/users/user_integral/index',//积分详情跳转链接 + 'prerogative' => $card['prerogative'], +// 'auto_activate' => false, + 'wx_activate' => true, + 'background_pic_url' => $background ? OfficialAccount::temporaryUpload($background)->media_id : '', + 'custom_field2' => [ + 'name_type' => 'FIELD_NAME_TYPE_TIMS',//消费次数 + 'url' => $baseUrl . '/pages/users/order_list/index' + ], + 'custom_field3' => [ + 'name_type' => 'FIELD_NAME_TYPE_COUPON',//优惠券 + 'url' => $baseUrl . '/pages/users/user_coupon/index' + ], + ]; + if ($card['custom_cell']) { + $cell_data = []; + $i = 1; + foreach ($card['custom_cell'] as $item) { + $cell_data['custom_cell' . $i] = $item; + $i++; + } + $especial = array_merge($especial, $cell_data); + } + @unlink($logo); + @unlink($background); + if ($card_id) { + unset($especial['bonus_rule']); + unset($base_info['brand_name'], $base_info['sku'], $base_info['use_custom_code']); + return [OfficialAccount::updateCard($card_id, $card_type, $base_info, $especial), array_merge($especial, ['custom_cell' => $card['custom_cell']])]; + } else { + return [OfficialAccount::createCard($card_type, $base_info, $especial), array_merge($especial, ['custom_cell' => $card['custom_cell']])]; + } + + } + + + /** + * 创建会员卡激活表单 + * @param string $cardId + * @return \EasyWeChat\Support\Collection + */ + public function activateUserForm(string $cardId) + { + $requireForm = [ + 'required_form' => [ + 'can_modify' => false, + 'common_field_id_list' => [ + 'USER_FORM_INFO_FLAG_MOBILE' + ] + ] + ]; + $optionFrom = [ + 'optional_form' => [ + 'can_modify' => false, + 'common_field_id_list' => [ + 'USER_FORM_INFO_FLAG_BIRTHDAY' + ] + ] + ]; + return OfficialAccount::cardActivateUserForm($cardId, $requireForm, $optionFrom); + } + +} diff --git a/app/services/wechat/WechatQrcodeServices.php b/app/services/wechat/WechatQrcodeServices.php new file mode 100644 index 0000000..457e6a1 --- /dev/null +++ b/app/services/wechat/WechatQrcodeServices.php @@ -0,0 +1,310 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\wechat; + +use app\common\dao\wechat\WechatQrcodeDao; +use app\services\BaseServices; +use app\services\other\QrcodeServices; +use crmeb\services\DownloadImageService; +use crmeb\services\UploadService; +use app\services\system\attachment\SystemAttachmentServices; +use app\services\user\label\UserLabelRelationServices; +use app\services\user\label\UserLabelServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\services\wechat\Messages; +use EasyWeChat\Kernel\Messages\Image; +use EasyWeChat\Kernel\Messages\News; +use EasyWeChat\Kernel\Messages\Text; +use EasyWeChat\Kernel\Messages\Voice; +use think\facade\Log; + +/** + * + * Class WechatQrcodeServices + * @package app\services\wechat + * @mixin WechatQrcodeDao + */ +class WechatQrcodeServices extends BaseServices +{ + + /** + * 构造方法 + * WechatReplyKeyServices constructor. + * @param WechatQrcodeDao $dao + */ + public function __construct(WechatQrcodeDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取渠道码列表 + * @param $where + * @return array + */ + public function qrcodeList($where) + { + /** @var UserLabelServices $userLabel */ + $userLabel = app()->make(UserLabelServices::class); + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getList($where, $page, $limit); + foreach ($list as &$item) { + $item['y_follow'] = $item['y_follow'] ?? 0; + $item['stop'] = $item['end_time'] ? $item['end_time'] > time() ? 1 : -1 : 0; + $item['label_name'] = $userLabel->getColumn([['id', 'in', $item['label_id']]], 'label_name'); + $item['end_time'] = date('Y-m-d H:i:s', $item['end_time']); + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 获取详情 + * @param $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function qrcodeInfo($id) + { + $info = $this->dao->get($id); + if ($info) { + $info = $info->toArray(); + } else { + throw new AdminException('数据不存在'); + } + /** @var UserServices $userService */ + $userService = app()->make(UserServices::class); + $info['label_id'] = explode(',', $info['label_id']); + foreach ($info['label_id'] as &$item) { + $item = (int)$item; + } + /** @var UserLabelServices $userLabelServices */ + $userLabelServices = app()->make(UserLabelServices::class); + $info['label_id'] = $userLabelServices->getLabelList(['ids' => $info['label_id']], ['id', 'label_name']); + $info['time'] = $info['continue_time']; + $info['content'] = json_decode($info['content'], true); + $info['data'] = json_decode($info['data'], true); + $info['avatar'] = $userService->value(['uid' => $info['uid']], 'avatar'); + return $info; + } + + /** + * 保存渠道码数据 + * @param $id + * @param $data + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveQrcode($id, $data) + { + $data['label_id'] = implode(',', $data['label_id']); + $data['add_time'] = time(); + $data['continue_time'] = $data['time']; + $data['end_time'] = $data['time'] ? $data['add_time'] + ($data['time'] * 86400) : 0; + /** @var WechatReplyServices $replyServices */ + $replyServices = app()->make(WechatReplyServices::class); + $type = $data['type']; + if ($data['type'] == 'url') $type = 'text'; + $content = $data['content']; + if ($data['type'] == 'news') $content = $data['content']['list'] ?? []; + $method = 'tidy' . ucfirst($type); + if ($type == 'image') { + /** @var DownloadImageService $download */ + $download = app()->make(DownloadImageService::class); + try { + $path = $download->thumb(true)->downloadImage($content['src'])['path']; + $content['src'] = $path; + $data['data'] = $replyServices->{$method}($content, 0); + @unlink(root_path() . 'public' . $path); + } catch (\Throwable $e) { + Log::error('获取图片错误,原因:' . $e->getMessage()); + throw new AdminException($e->getMessage()); + } + } else { + $data['data'] = $replyServices->{$method}($content, 0); + } + $data['content'] = json_encode($data['content']); + $data['data'] = json_encode($data['data']); + if ($id) { + $info = $this->dao->get($id); + if (!$info) throw new AdminException('数据不存在'); + if ($info['image'] == '') $data['image'] = $this->getChannelCode($id); + $info = $this->dao->update($id, $data); + if (!$info) throw new AdminException('保存失败'); + } else { + $info = $this->dao->save($data); + $image = $this->getChannelCode($info['id']); + $info = $this->dao->update($info['id'], ['image' => $image]); + if (!$info) throw new AdminException('保存失败'); + } + return true; + } + + /** + * 生成渠道码 + * @param int $id + * @return mixed|string + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getChannelCode($id = 0) + { + /** @var SystemAttachmentServices $systemAttachment */ + $systemAttachment = app()->make(SystemAttachmentServices::class); + $name = 'wechatqrcode_' . $id . '.jpg'; + $siteUrl = sys_config('site_url'); + $imageInfo = $systemAttachment->getInfo(['name' => $name]); + if (!$imageInfo) { + /** @var QrcodeServices $qrCode */ + $qrCode = app()->make(QrcodeServices::class); + //公众号 + $resCode = $qrCode->getForeverQrcode('wechatqrcode', $id); + if ($resCode) { + $res = ['res' => $resCode, 'id' => $resCode['id']]; + } else { + $res = false; + } + if (!$res) throw new AdminException('二维码生成失败'); + $imageInfo = $this->downloadImage($resCode['url'], $name); + $systemAttachment->attachmentAdd($name, $imageInfo['size'], $imageInfo['type'], $imageInfo['att_dir'], $imageInfo['att_dir'], 1, $imageInfo['image_type'], time(), 2); + } + return strpos($imageInfo['att_dir'], 'http') === false ? $siteUrl . $imageInfo['att_dir'] : $imageInfo['att_dir']; + } + + /**下载图片 + * @param $url + * @param $name + * @param $type + * @param $timeout + * @param $w + * @param $h + * @return array|string + */ + public function downloadImage($url = '', $name = '', $type = 0, $timeout = 30, $w = 0, $h = 0) + { + if (!strlen(trim($url))) return ''; + //TODO 获取远程文件所采用的方法 + if ($type) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //TODO 跳过证书检查 + if (stripos($url, "https://") !== FALSE) curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); //TODO 从证书中检查SSL加密算法是否存在 + curl_setopt($ch, CURLOPT_HTTPHEADER, array('user-agent:' . $_SERVER['HTTP_USER_AGENT'])); + if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off')) curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);//TODO 是否采集301、302之后的页面 + $content = curl_exec($ch); + curl_close($ch); + } else { + try { + ob_start(); + readfile($url); + $content = ob_get_contents(); + ob_end_clean(); + } catch (\Exception $e) { + return $e->getMessage(); + } + } + $size = strlen(trim($content)); + if (!$content || $size <= 2) return '图片流获取失败'; + $upload_type = sys_config('upload_type', 1); + $upload = UploadService::init(); + if ($upload->to('attach/spread/agent')->stream($content, $name) === false) { + return $upload->getError(); + } + $imageInfo = $upload->getUploadInfo(); + $data['att_dir'] = $imageInfo['dir']; + $data['name'] = $imageInfo['name']; + $data['size'] = $imageInfo['size']; + $data['type'] = $imageInfo['type']; + $data['image_type'] = $upload_type; + $data['is_exists'] = false; + return $data; + } + + /** + * 扫码完成后方法 + * @param $qrcodeInfo + * @param $userInfo + * @param $spreadInfo + * @param int $isFollow + * @return mixed + */ + public function wechatQrcodeRecord($qrcodeInfo, $userInfo, $spreadInfo, $isFollow = 0) + { + $response = $this->transaction(function () use ($qrcodeInfo, $userInfo, $spreadInfo, $isFollow) { + + //绑定用户标签 + /** @var UserLabelRelationServices $labelServices */ + $labelServices = app()->make(UserLabelRelationServices::class); + foreach ($qrcodeInfo['label_id'] as $item) { + $labelArr[] = [ + 'uid' => $userInfo['uid'], + 'label_id' => $item['id'] ?? $item + ]; + } + $labelServices->saveAll($labelArr); + + //增加二维码扫码数量 + $this->dao->upFollowAndScan($qrcodeInfo['id'], $isFollow); + + //写入扫码记录 + /** @var WechatQrcodeRecordServices $recordServices */ + $recordServices = app()->make(WechatQrcodeRecordServices::class); + $data['qid'] = $qrcodeInfo['id']; + $data['uid'] = $userInfo['uid']; + $data['is_follow'] = $isFollow; + $data['add_time'] = time(); + $recordServices->save($data); + + //回复信息内容 + return $this->replyDataByMessage($qrcodeInfo['type'], $qrcodeInfo['data']); + }); + return $response; + } + + /** + * 发送扫码之后的信息 + * @param $type + * @param $data + * @return array|Image|News|Text|Voice + */ + public function replyDataByMessage($type, $data) + { + if ($type == 'text') { + return Messages::textMessage($data['content']); + } else if ($type == 'image') { + return Messages::imageMessage($data['media_id']); + } else if ($type == 'news') { + $title = $data['title'] ?? ''; + $image = $data['image'] ?? ''; + $description = $data['synopsis'] ?? ''; + $url = $data['url'] ?? ''; + return Messages::newsMessage($title, $description, $url, $image); + } else if ($type == 'url') { + $title = $data['content']; + $image = sys_config('h5_avatar'); + $description = $data['content']; + $url = $data['content']; + return Messages::newsMessage($title, $description, $url, $image); + } else if ($type == 'voice') { + return Messages::voiceMessage($data['media_id']); + } + } +} diff --git a/app/services/wechat/WechatReplyServices.php b/app/services/wechat/WechatReplyServices.php new file mode 100644 index 0000000..3babc46 --- /dev/null +++ b/app/services/wechat/WechatReplyServices.php @@ -0,0 +1,352 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\wechat; + +use app\services\BaseServices; +use app\common\dao\wechat\WechatReplyDao; +use app\services\kefu\KefuServices; +use crmeb\exceptions\AdminException; +use crmeb\services\wechat\ErrorMessage; +use crmeb\services\wechat\Messages; +use crmeb\services\wechat\OfficialAccount; +use EasyWeChat\Kernel\Messages\Image; +use EasyWeChat\Kernel\Messages\News; +use EasyWeChat\Kernel\Messages\Text; +use EasyWeChat\Kernel\Messages\Transfer; +use EasyWeChat\Kernel\Messages\Voice; +use think\db\exception\DataNotFoundException; +use think\db\exception\DbException; +use think\db\exception\ModelNotFoundException; +use think\exception\ValidateException; +use think\Model; + +/** + * + * Class UserWechatuserServices + * @package app\services\user + * @mixin WechatReplyDao + */ +class WechatReplyServices extends BaseServices +{ + + /** + * UserWechatuserServices constructor. + * @param WechatReplyDao $dao + */ + public function __construct(WechatReplyDao $dao) + { + $this->dao = $dao; + } + + /** + * 消息类型 + * @return string[] + * @var string[] + */ + public function replyType() + { + return ['text', 'image', 'news', 'voice']; + } + + /** + * 自定义简单查询总数 + * @param array $where + * @return int + */ + public function getCount(array $where): int + { + return $this->dao->getCount($where); + } + + /** + * 复杂条件搜索列表 + * @param array $where + * @param string $field + * @return array + */ + public function getWhereUserList(array $where, string $field): array + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getListByModel($where, $field, $page, $limit); + $count = $this->dao->getCountByWhere($where); + return [$list, $count]; + } + + /** + * @param $key + * @return array|Model|null + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function getDataByKey(string $key) + { + /** @var WechatKeyServices $services */ + $services = app()->make(WechatKeyServices::class); + $data = $services->getOne(['keys' => $key]); + $resdata = $this->dao->getOne(['id' => $data['reply_id'] ?? '']); + $resdata['data'] = isset($resdata['data']) ? json_decode($resdata['data'], true) : []; + $resdata['key'] = $key; + return $resdata; + } + + /** + * @param $data + * @param $key + * @param $type + * @param int $status + * @return bool + */ + public function redact($data, $id, $key, $type, $status = 1) + { + $method = 'tidy' . ucfirst($type); + if ($id == 'undefined') { + $id = 0; + } + if ((!isset($data['content']) && !isset($data['src'])) || (isset($data['content']) && $data['content'] == '' && isset($data['src']) && $data['src'] == '')) $data = $data['list'][0] ?? []; + try { + $res = $this->{$method}($data, $id); + } catch (\Throwable $e) { + throw new AdminException($e->getMessage()); + } + if (!$res) return false; + $arr = []; + /** @var WechatKeyServices $keyServices */ + $keyServices = app()->make(WechatKeyServices::class); + $count = $this->dao->getCount(['id' => $id]); + if ($count) { + $keyServices->delete($id, 'reply_id'); + $insertData = explode(',', $key); + foreach ($insertData as $k => $v) { + $arr[$k]['keys'] = $v; + $arr[$k]['reply_id'] = $id; + } + $res = $this->dao->update($id, ['type' => $type, 'data' => json_encode($res), 'status' => $status], 'id'); + $res1 = $keyServices->saveAll($arr); + if (!$res || !$res1) { + throw new AdminException('保存失败!'); + } + } else { + $reply = $this->dao->save([ + 'type' => $type, + 'data' => json_encode($res), + 'status' => $status, + ]); + $insertData = explode(',', $key); + foreach ($insertData as $k => $v) { + $arr[$k]['keys'] = $v; + $arr[$k]['reply_id'] = $reply->id; + } + $res = $keyServices->saveAll($arr); + if (!$res) throw new AdminException('保存失败!'); + } + return true; + } + + /** + * 获取所有关键字 + * @param array $where + * @return array + */ + public function getKeyAll($where = array()) + { + /** @var WechatReplyKeyServices $replyKeyServices */ + $replyKeyServices = app()->make(WechatReplyKeyServices::class); + $data = $replyKeyServices->getReplyKeyAll($where); + /** @var WechatKeyServices $keyServices */ + $keyServices = app()->make(WechatKeyServices::class); + foreach ($data['list'] as &$item) { + if ($item['data']) $item['data'] = json_decode($item['data'], true); + switch ($item['type']) { + case 'text': + $item['typeName'] = '文字消息'; + break; + case 'image': + $item['typeName'] = '图片消息'; + break; + case 'news': + $item['typeName'] = '图文消息'; + break; + case 'voice': + $item['typeName'] = '声音消息'; + break; + } + $keys = $keyServices->getColumn(['reply_id' => $item['id']], 'keys'); + $item['key'] = implode(',', $keys); + } + return $data; + } + + /** + * 查询一条 + * @param int $id + * @return array|null|Model + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function getKeyInfo(int $id) + { + $resdata = $this->dao->getOne(['id' => $id]); + /** @var WechatKeyServices $keyServices */ + $keyServices = app()->make(WechatKeyServices::class); + $keys = $keyServices->getColumn(['reply_id' => $resdata['id']], 'keys'); + $resdata['data'] = $resdata['data'] ? json_decode($resdata['data'], true) : []; + $resdata['key'] = implode(',', $keys); + return $resdata; + } + + /** + * 整理文本输入的消息 + * @param $data + * @param $key + * @return array|bool + */ + public function tidyText($data, $id) + { + $res = []; + if (!isset($data['content']) || $data['content'] == '') { + throw new AdminException('请输入回复信息内容'); + } + $res['content'] = $data['content']; + return $res; + } + + /** + * 整理图片资源 + * @param $data + * @param $key + * @return array|bool|mixed + */ + public function tidyImage($data, $id) + { + if (!isset($data['src']) || $data['src'] == '') { + throw new AdminException('请上传回复的图片'); + } + $reply = $this->dao->get((int)$id); + if ($reply) $reply['data'] = json_decode($reply['data'], true); + if ($reply && isset($reply['data']['src']) && $reply['data']['src'] == $data['src']) { + $res = $reply['data']; + } else { + $res = []; + // 图片转media + $res['src'] = $data['src']; + try { + $material = OfficialAccount::temporaryUpload(url_to_path($data['src'])); + } catch (\Throwable $e) { + throw new ValidateException(ErrorMessage::getMessage($e->getMessage())); + } + $res['media_id'] = $material->media_id; + $dataEvent = ['type' => 'image', 'media_id' => $material->media_id, 'path' => $res['src'], 'url' => $material->url ?? '']; + /** @var WechatMediaServices $mateServices */ + $mateServices = app()->make(WechatMediaServices::class); + $mateServices->save($dataEvent); + } + return $res; + } + + /** + * 整理声音资源 + * @param $data + * @param $key + * @return array|bool|mixed + */ + public function tidyVoice($data, $id) + { + if (!isset($data['src']) || $data['src'] == '') { + throw new AdminException('请上传回复的声音'); + } + $reply = $this->dao->get((int)$id); + if ($reply) $reply['data'] = json_decode($reply['data'], true); + if ($reply && isset($reply['data']['src']) && $reply['data']['src'] == $data['src']) { + $res = $reply['data']; + } else { + $res = []; + // 声音转media + $res['src'] = $data['src']; + try { + $material = OfficialAccount::materialService()->uploadVoice(url_to_path($data['src'])); + } catch (\Throwable $e) { + throw new ValidateException(ErrorMessage::getMessage($e->getMessage())); + } + $res['media_id'] = $material['media_id']; + $dataEvent = ['media_id' => $material['media_id'], 'path' => $res['src'], 'type' => 'voice']; + /** @var WechatMediaServices $mateServices */ + $mateServices = app()->make(WechatMediaServices::class); + $mateServices->save($dataEvent); + } + return $res; + } + + /** + * 整理图文资源 + * @param $data + * @param $key + * @return bool + */ + public function tidyNews($data, $id = 0) + { +// $data = $data['list'][0]; + if (!count($data)) { + throw new AdminException('请选择图文消息'); + } + $siteUrl = sys_config('site_url'); + if (empty($data['url'])) $data['url'] = $siteUrl . '/pages/extension/news_details/index?id=' . $data['id']; + if (count($data['image_input'])) $data['image'] = $data['image_input'][0]; + return $data; + } + + /** + * 获取关键字 + * @param $key + * @param string $openId + * @return array|Image|News|Text|Transfer|Voice + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function reply($key, string $openId = '') + { + $res = $this->dao->getKey($key); + if (empty($res)) { + /** @var KefuServices $services */ + $services = app()->make(KefuServices::class); + $services->replyTransferService($key, $openId); + return Messages::transfer(); + } + return $this->replyDataByMessage($res->toArray()); + } + + /** + * 根据关键字内容返回对应的内容 + * @param array $res + * @return array|Image|News|Text|Voice + */ + public function replyDataByMessage(array $res) + { + $res['data'] = json_decode($res['data'], true); + if ($res['type'] == 'text') { + return Messages::textMessage($res['data']['content']); + } else if ($res['type'] == 'image') { + return Messages::imageMessage($res['data']['media_id']); + } else if ($res['type'] == 'news') { + $title = $res['data']['title'] ?? ''; + $image = $res['data']['image'] ?? ''; + $description = $res['data']['synopsis'] ?? ''; + $url = $res['data']['url'] ?? ''; + return Messages::newsMessage($title, $description, $url, $image); + } else if ($res['type'] == 'voice') { + return Messages::voiceMessage($res['data']['media_id']); + } + } +} diff --git a/app/services/wechat/WechatUserServices.php b/app/services/wechat/WechatUserServices.php new file mode 100644 index 0000000..681b1e6 --- /dev/null +++ b/app/services/wechat/WechatUserServices.php @@ -0,0 +1,538 @@ + +// +---------------------------------------------------------------------- +declare (strict_types=1); + +namespace app\services\wechat; + +use app\jobs\user\UserJob; +use app\services\BaseServices; +use app\common\dao\wechat\WechatUserDao; +use app\services\user\LoginServices; +use app\services\user\UserServices; +use crmeb\exceptions\AdminException; +use crmeb\exceptions\AuthException; +use crmeb\services\wechat\OfficialAccount; +use think\exception\ValidateException; + +/** + * + * Class WechatUserServices + * @package app\services\wechat + * @mixin WechatUserDao + */ +class WechatUserServices extends BaseServices +{ + + /** + * WechatUserServices constructor. + * @param WechatUserDao $dao + */ + public function __construct(WechatUserDao $dao) + { + $this->dao = $dao; + } + + + public function getColumnUser($user_ids, $column, $key, string $user_type = 'wechat') + { + return $this->dao->getColumn([['uid', 'IN', $user_ids], ['user_type', '=', $user_type]], $column, $key); + } + + /** + * 获取单个微信用户 + * @param array $where + * @param string $field + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getWechatUserInfo(array $where, $field = '*') + { + return $this->dao->getOne($where, $field); + } + + /** + * 用uid获得 微信openid + * @param $uid + * @return mixed + */ + public function uidToOpenid(int $uid, string $userType = 'wechat') + { + return $this->dao->value(['uid' => $uid, 'user_type' => $userType], 'openid'); + } + + + /** + * 用openid获得uid + * @param $openid + * @param string $openidType + * @return mixed + */ + public function openidTouid($openid, $openidType = 'openid') + { + $uid = $this->dao->value([[$openidType, '=', $openid], ['user_type', '<>', 'h5']], 'uid'); + if (!$uid) + throw new AdminException('对应的uid不存在'); + return $uid; + } + + /** + * 用户取消关注 + * @param $openid + * @return bool + */ + public function unSubscribe($openid) + { + if (!$this->dao->update($openid, ['subscribe' => 0, 'subscribe_time' => time()], 'openid')) + throw new AdminException('取消关注失败'); + return true; + } + + /** + * 更新微信用户信息 + * @param $message + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveUserV1($message) + { + $openid = $message->FromUserName; + if ($this->getWechatUserInfo(['openid' => $openid])) { + $this->updateWecahtUser($openid); + } else { + $this->setWecahtUser($openid); + } + return true; + } + + /** + * 用户存在就更新 不存在就添加 + * @param $openid + * @param int $spread_uid + * @param string $phone + * @return \app\services\user\User|array|\think\Model|null + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function saveUser($openid, $spread_uid = 0, $phone = '') + { + $is_new = false; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $wechatUser = $this->getWechatUserInfo(['openid' => $openid]); + if ($wechatUser && $wechatUser['uid']) { + $userInfo = $userServices->getUserInfo((int)$wechatUser['uid']); + //无关注只是授权生成用户 + if (!$wechatUser['subscribe_time'] && $wechatUser['uid']) { + $is_new = true; + $spread_uid = $userInfo['spread_uid'] ?? 0; + } + $this->updateWecahtUser($openid); + } else { + $userInfo = $this->setNewUser($openid, $spread_uid, ['phone' => $phone]); + $is_new = true; + } + if ($is_new) { + UserJob::dispatchDo('subscribeSpreadLottery', [(int)$userInfo['uid'], $openid, (int)$spread_uid]); + } + return $userInfo; + } + + /** + * 更新用户信息 + * @param $openid + * @param string $phone + * @return bool + */ + public function updateWecahtUser($openid) + { + try { + $userInfo = OfficialAccount::getUserInfo($openid); + } catch (\Throwable $e) { + $userInfo = []; + } + if (isset($userInfo['nickname']) && $userInfo['nickname']) { + $userInfo['nickname'] = filter_emoji($userInfo['nickname']); + } else { + if (isset($userInfo['nickname'])) unset($userInfo['nickname']); + } + if (isset($userInfo['tagid_list'])) { + $userInfo['tagid_list'] = implode(',', $userInfo['tagid_list']); + } + if ($userInfo && !$this->dao->update($openid, $userInfo, 'openid')) + throw new AdminException('更新失败'); + return true; + } + + /** + * 写入微信用户信息 + * @param $openid + * @param int $uid + * @return bool + */ + public function setWecahtUser($openid, int $uid = 0) + { + try { + $wechatInfo = OfficialAccount::getUserInfo($openid); + } catch (\Throwable $e) { + $wechatInfo = []; + } + if (!isset($wechatInfo['openid'])) + throw new ValidateException('请关注公众号!'); + if (isset($wechatInfo['nickname']) && $wechatInfo['nickname']) { + $wechatInfo['nickname'] = filter_emoji($wechatInfo['nickname']); + } else { + mt_srand(); + $wechatInfo['nickname'] = 'wx' . rand(100000, 999999); + } + if (isset($wechatInfo['tagid_list'])) { + $wechatInfo['tagid_list'] = implode(',', $wechatInfo['tagid_list']); + } + $wechatInfo['user_type'] = 'wechat'; + $wechatInfo['uid'] = $uid; + $wechatInfo['add_time'] = time(); + if ($wechatInfo && !$this->dao->save($wechatInfo)) { + throw new AdminException('用户储存失败!'); + } + return true; + } + + /** + * 添加新用户 + * @param $openid + * @param int $spread_uid + * @param array $append 追加字段 + * @return \app\services\user\User|\think\Model + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function setNewUser($openid, $spread_uid = 0, array $append = []) + { + + try { + $wechatInfo = OfficialAccount::getUserInfo($openid); + } catch (\Throwable $e) { + $wechatInfo = []; + } + if (!isset($wechatInfo['openid'])) + throw new ValidateException('请关注公众号!'); + + if (isset($wechatInfo['nickname']) && $wechatInfo['nickname']) { + $wechatInfo['nickname'] = $wechatInfo['nickname']; + $wechatInfo['is_complete'] = 1; + } else { + //昵称不存在的信息不完整 + $wechatInfo['is_complete'] = 0; + mt_srand(); + $wechatInfo['nickname'] = 'wx' . rand(100000, 999999); + } + if (isset($wechatInfo['tagid_list'])) { + $wechatInfo['tagid_list'] = implode(',', $wechatInfo['tagid_list']); + } + + $uid = 0; + $userType = 'wechat'; + $userInfo = []; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (isset($append['phone']) && $append['phone']) { + $userInfo = $userServices->getOne(['phone' => $append['phone']]); + $wechatInfo['phone'] = $append['phone']; + } + if (!$userInfo) { + if (isset($wechatInfo['unionid']) && $wechatInfo['unionid']) { + $uid = $this->dao->value(['unionid' => $wechatInfo['unionid']], 'uid'); + if ($uid) { + $userInfo = $userServices->getOne(['uid' => $uid]); + } + } else { + $userInfo = $this->getAuthUserInfo($openid, $userType); + } + } + if ($userInfo) { + $uid = (int)$userInfo['uid']; + if (isset($userInfo['status']) && !$userInfo['status']) + throw new ValidateException('您已被禁止登录,请联系管理员'); + } + $wechatInfo['user_type'] = $userType; + if ($userInfo) { + //更新用户表和wechat_user表 + //判断该类性用户在wechatUser中是否存在 + $wechatUser = $this->dao->getOne(['uid' => $uid, 'user_type' => $userType]); + if ($wechatUser) { + $wechatUser = $this->dao->getOne(['openid' => $openid]); + } + /** @var LoginServices $loginService */ + $loginService = app()->make(LoginServices::class); + $this->transaction(function () use ($openid, $loginService, $wechatInfo, $userInfo, $uid, $userType, $spread_uid, $wechatUser) { + $wechatInfo['code'] = $spread_uid; + $wechatInfo['uid'] = $uid; + $loginService->updateUserInfo($wechatInfo, $userInfo); + if ($wechatUser) { + //更换微信登录情况 +// if (isset($append['phone']) && $append['phone'] && $wechatUser['openid'] != $openid) { +// throw new ValidateException('该手机号已被注册'); +// } + if (!$this->dao->update($wechatUser['id'], $wechatInfo, 'id')) { + throw new ValidateException('更新数据失败'); + } + } else { + if (!$this->dao->save($wechatInfo)) { + throw new ValidateException('写入信息失败'); + } + } + }); + } else { + //user表没有用户,wechat_user表没有用户创建新用户 + //不存在则创建用户 + $userInfo = $this->transaction(function () use ($openid, $userServices, $wechatInfo, $spread_uid, $userType) { + $userInfo = $userServices->setUserInfo($wechatInfo, (int)$spread_uid, $userType); + if (!$userInfo) { + throw new AuthException('生成User用户失败!'); + } + $wechatInfo['uid'] = $userInfo->uid; + $wechatInfo['add_time'] = $userInfo->add_time; + $wechatUser = $this->dao->getOne(['openid' => $openid]); + if ($wechatUser) { + if (!$this->dao->update($wechatUser['id'], $wechatInfo, 'id')) { + throw new ValidateException('更新数据失败'); + } + } else { + if (!$this->dao->save($wechatInfo)) { + throw new AuthException('生成微信用户失败!'); + } + } + return $userInfo; + }); + } + return $userInfo; + } + + /** + * 授权后获取用户信息 + * @param $openid + * @param $user_type + */ + public function getAuthUserInfo($openid, $user_type) + { + $user = []; + //兼容老用户 + $uids = $this->dao->getColumn(['unionid|openid' => $openid, 'is_del' => 0], 'uid,user_type', 'user_type'); + if ($uids) { + $uid = $uids[$user_type]['uid'] ?? 0; + if (!$uid) { + $ids = array_column($uids, 'uid'); + $uid = $ids[0]; + } + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + $user = $userServices->getUserInfo($uid); + if (isset($user['status']) && !$user['status']) + throw new AuthException('您已被禁止登录,请联系管理员', 410020); + } + return $user; + } + + /** + * 更新微信用户信息 + * @param $event + * @return bool + */ + public function wechatUpdata($data) + { + [$uid, $userData] = $data; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if (!$userInfo = $userServices->getUserInfo($uid)) { + return false; + } + /** @var LoginServices $loginService */ + $loginService = app()->make(LoginServices::class); + $loginService->updateUserInfo($userData, $userInfo); + //更新用户信息 + /** @var WechatUserServices $wechatUser */ + $wechatUser = app()->make(WechatUserServices::class); + + $wechatUserInfo = []; + if (isset($userData['nickname']) && $userData['nickname']) $wechatUserInfo['nickname'] = filter_emoji($userData['nickname'] ?? '');//姓名 + if (isset($userData['headimgurl']) && $userData['headimgurl']) $wechatUserInfo['headimgurl'] = $userData['headimgurl'] ?? '';//头像 + if (isset($userData['sex']) && $userData['sex']) $wechatUserInfo['sex'] = $userData['gender'] ?? '';//性别 + if (isset($userData['language']) && $userData['language']) $wechatUserInfo['language'] = $userData['language'] ?? '';//语言 + if (isset($userData['city']) && $userData['city']) $wechatUserInfo['city'] = $userData['city'] ?? '';//城市 + if (isset($userData['province']) && $userData['province']) $wechatUserInfo['province'] = $userData['province'] ?? '';//省份 + if (isset($userData['country']) && $userData['country']) $wechatUserInfo['country'] = $userData['country'] ?? '';//国家 + if (!empty($wechatUserInfo['nickname']) || !empty($wechatUserInfo['headimgurl'])) { + $wechatUserInfo['is_complete'] = 1; + } else { + $wechatUserInfo['is_complete'] = 0; + } + if ($wechatUserInfo) { + if (isset($userData['openid']) && $userData['openid'] && false === $wechatUser->update(['uid' => $userInfo['uid'], 'openid' => $userData['openid']], $wechatUserInfo)) { + throw new ValidateException('更新失败'); + } + } + return true; + } + + /** + * 微信授权成功后 + * @param $event + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function wechatOauthAfter(array $data) + { + [$openid, $wechatInfo, $spread_uid, $login_type, $userType] = $data; + /** @var UserServices $userServices */ + $userServices = app()->make(UserServices::class); + if ($spread_uid && !$userServices->userExist((int)$spread_uid)) { + $spread_uid = 0; + } + //删除多余字段 + unset($wechatInfo['subscribe_scene'], $wechatInfo['qr_scene'], $wechatInfo['qr_scene_str']); + if ($login_type) { + $wechatInfo['login_type'] = $login_type; + } + if (!isset($wechatInfo['nickname']) || !$wechatInfo['nickname']) { + if (isset($wechatInfo['phone']) && $wechatInfo['phone']) { + $wechatInfo['nickname'] = substr_replace($wechatInfo['phone'], '****', 3, 4); + } else { + mt_srand(); + $wechatInfo['nickname'] = 'wx' . rand(100000, 999999); + } + } else { + $wechatInfo['is_complete'] = 1; + $wechatInfo['nickname'] = filter_emoji($wechatInfo['nickname']); + } + //统一用户处理:1:同一手机号用户 2:开放平台 3:openid + $userInfo = []; + if (isset($wechatInfo['phone']) && $wechatInfo['phone']) { + $userInfo = $userServices->getOne(['phone' => $wechatInfo['phone']]); + } + if (!$userInfo) { + if (isset($wechatInfo['unionid']) && $wechatInfo['unionid']) { + $uid = $this->dao->value(['unionid' => $wechatInfo['unionid'], 'is_del' => 0], 'uid'); + if ($uid) { + $userInfo = $userServices->getOne(['uid' => $uid]); + } + } else { + $userInfo = $this->getAuthUserInfo($openid, $userType); + } + } + $uid = (int)($userInfo['uid'] ?? 0); + $wechatInfo['user_type'] = $userType; + //user表存在和wechat_user表同时存在 + return $this->transaction(function () use ($openid, $uid, $userInfo, $wechatInfo, $userServices, $spread_uid, $userType) { + $wechatInfo['spread_uid'] = $spread_uid; + $wechatInfo['uid'] = $uid; + if ($userInfo) { + if (isset($userInfo['status']) && !$userInfo['status']) + throw new ValidateException('您已被禁止登录,请联系管理员'); + //更新用户表 + /** @var LoginServices $loginService */ + $loginService = app()->make(LoginServices::class); + $loginService->updateUserInfo($wechatInfo, $userInfo); + } else { + //新增用户表 + $userInfo = $userServices->setUserInfo($wechatInfo, (int)$spread_uid, $userType); + if (!$userInfo) { + throw new AuthException('生成User用户失败!'); + } + //用户绑定客户事件 + if (!empty($wechatInfo['unionid'])) { + event('user.client', [$userInfo['uid'], $wechatInfo['unionid']]); + } + //用户绑定成员事件 + if (!empty($userInfo['phone'])) { + event('user.work', [$userInfo['uid'], $userInfo['phone']]); + } + } + $uid = $userInfo['uid']; + $wechatInfo['uid'] = $userInfo->uid; + $wechatInfo['add_time'] = $userInfo->add_time; + + //判断该类性用户在wechatUser中是否存在 + $wechatUser = $this->dao->getOne(['uid' => $uid, 'user_type' => $userType, 'is_del' => 0]); + if (!$wechatUser) { + $wechatUser = $this->dao->getOne(['openid' => $openid, 'is_del' => 0]); + } + if ($wechatUser) { + //更换微信登录情况 +// if (isset($wechatInfo['phone']) && $wechatInfo['phone'] && $wechatUser['openid'] != $openid) { +// throw new ValidateException('该手机号已被注册'); +// } + if (!$this->dao->update($wechatUser['id'], $wechatInfo, 'id')) { + throw new ValidateException('更新数据失败'); + } + } else { + if (!$this->dao->save($wechatInfo)) { + throw new AuthException('生成微信用户失败!'); + } + } + return $userInfo; + }); + } + + /** + * 更新用户信息(同步) + * @param array $openids + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function syncWechatUser(array $openids) + { + if (!$openids) { + return []; + } + $wechatUser = $this->dao->getList([['openid', 'in', $openids]]); + $noBeOpenids = $openids; + if ($wechatUser) { + $beOpenids = array_column($wechatUser, 'openid'); + $noBeOpenids = array_diff($openids, $beOpenids); + if ($beOpenids) { + $data = []; + foreach ($beOpenids as $openid) { + try { + $info = OfficialAccount::getUserInfo($openid); + } catch (\Throwable $e) { + $info = []; + } + if (!$info) continue; + $data['subscribe'] = $info['subscribe'] ?? 1; + if (($info['subscribe'] ?? 1) == 1) { + $data['unionid'] = $info['unionid'] ?? ''; + $data['nickname'] = $info['nickname'] ?? ''; + $data['sex'] = $info['sex'] ?? 0; + $data['language'] = $info['language'] ?? ''; + $data['city'] = $info['city'] ?? ''; + $data['province'] = $info['province'] ?? ''; + $data['country'] = $info['country'] ?? ''; + $data['headimgurl'] = $info['headimgurl'] ?? ''; + $data['subscribe_time'] = $info['subscribe_time'] ?? ''; + $data['groupid'] = $info['groupid'] ?? 0; + $data['remark'] = $info['remark'] ?? ''; + $data['tagid_list'] = isset($info['tagid_list']) && $info['tagid_list'] ? implode(',', $info['tagid_list']) : ''; + } + $this->dao->update(['openid' => $info['openid']], $data); + } + } + } + return $noBeOpenids; + } +} diff --git a/app/services/work/WorkClientServices.php b/app/services/work/WorkClientServices.php new file mode 100644 index 0000000..974759e --- /dev/null +++ b/app/services/work/WorkClientServices.php @@ -0,0 +1,586 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\work; + + +use app\dao\work\WorkClientDao; +use app\jobs\work\WorkClientJob; +use app\services\BaseServices; +use app\services\user\label\UserLabelServices; +use app\services\user\UserServices; +use crmeb\services\wechat\config\WorkConfig; +use crmeb\services\wechat\WechatResponse; +use crmeb\services\wechat\Work; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; +use think\facade\Log; + +/** + * 企业微信客户 + * Class WorkClientServices + * @package app\services\work + * @mixin WorkClientDao + */ +class WorkClientServices extends BaseServices +{ + + use ServicesTrait; + + /** + * WorkClientServices constructor. + * @param WorkClientDao $dao + */ + public function __construct(WorkClientDao $dao) + { + $this->dao = $dao; + } + + /** + * @param array $where + * @param array $field + * @param bool $isPage + * @return array + */ + public function getList(array $where, array $field = ['*'], bool $isPage = true) + { + $page = $limit = 0; + if ($isPage) { + [$page, $limit] = $this->getPageValue(); + } + $list = $this->dao->getDataList($where, $field, $page, $limit, 'create_time', [ + 'followOne' => function ($query) { + $query->with([ + 'member' => function ($query) { + $query->field(['userid', 'id', 'name', 'main_department']) + ->with(['mastareDepartment']); + } + ])->field(['userid', 'client_id', 'state', 'id']); + }, + 'follow' => function ($query) { + $query->field(['id', 'client_id'])->with(['tags']); + }, + ]); + foreach ($list as &$item) { + if (!empty($item['follow'])) { + $tags = []; + foreach ($item['follow'] as $value) { + if (!empty($value['tags'])) { + $tags = array_merge($tags, $value['tags']); + } + } + $newTags = []; + foreach ($tags as $tag) { + if (!in_array($tag['tag_name'], array_column($newTags, 'tag_name'))) { + $newTags[] = $tag; + } + } + $item['followOne']['tags'] = $newTags; + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 自动同步客户 + * @param int $page + * @param string $cursor + * @return bool + */ + public function authGetExternalcontact(int $page = 1, string $cursor = '') + { + /** @var WorkConfig $config */ + $config = app()->make(WorkConfig::class); + $corpId = $config->get('corpId'); + if (!$corpId) { + return true; + } + /** @var WorkMemberServices $memberService */ + $memberService = app()->make(WorkMemberServices::class); + $menmberList = $memberService->getDataList(['corp_id' => $corpId], ['userid'], $page, 10); + //没有数据就返回成功 + if (!$menmberList) { + return true; + } + + $userids = array_column($menmberList, 'userid'); + $response = Work::getBatchClientList($userids, $cursor); + $externalContactList = $response['external_contact_list'] ?? []; + $external = []; + $followUser = [];//内部人员跟踪 + $externalUserids = [];//客户信息 + $this->transaction(function () use ($externalContactList, $corpId, $externalUserids, $followUser, $external) { + foreach ($externalContactList as $item) { + $externalContact = $item['external_contact']; + $unionid = $externalContact['unionid'] ?? ''; + if (isset($externalContact['unionid'])) { + unset($externalContact['unionid']); + } + $corpName = $corpFullName = $position = ''; + if (isset($externalContact['corp_name'])) { + $corpName = $externalContact['corp_name']; + unset($externalContact['corp_name']); + } + if (isset($externalContact['corp_full_name'])) { + $corpFullName = $externalContact['corp_full_name']; + unset($externalContact['corp_full_name']); + } + if (isset($externalContact['position'])) { + $position = $externalContact['position']; + unset($externalContact['position']); + } + + $externalContact['position'] = $position; + $externalContact['external_profile'] = json_encode($externalContact['external_profile'] ?? []); + + $followUserData = [ + 'userid' => $item['follow_info']['userid'], + 'remark' => $item['follow_info']['remark'] ?? '', + 'description' => $item['follow_info']['description'] ?? '', + 'createtime' => $item['follow_info']['createtime'] ?? '', + 'remark_corp_name' => $item['follow_info']['remark_corp_name'] ?? '', + 'remark_mobiles' => json_encode($item['follow_info']['remark_mobiles'] ?? ''), + 'add_way' => $item['follow_info']['add_way'] ?? '', + 'oper_userid' => $item['follow_info']['oper_userid'] ?? '', + 'create_time' => time(), + 'tags' => [], + ]; + + if (!empty($item['follow_info']['tag_id'])) { + $tagRes = Work::getCorpTags($item['follow_info']['tag_id']); + foreach ($tagRes['tag_group'] ?? [] as $group) { + foreach ($group['tag'] as $tag) { + $followUserData['tags'][] = [ + 'group_name' => $group['group_name'] ?? '', + 'tag_name' => $tag['name'] ?? '', + 'type' => $tag['type'] ?? 1, + 'tag_id' => $tag['id'], + 'create_time' => time() + ]; + } + } + } + + $followUser[$externalContact['external_userid']] = $followUserData; + $externalUserids[] = $externalContact['external_userid']; + $externalUserid = $externalContact['external_userid']; + $externalContact['corp_id'] = $corpId; + $externalContact['unionid'] = $unionid; + $externalContact['corp_name'] = $corpName; + $externalContact['corp_full_name'] = $corpFullName; + if ($this->dao->count(['external_userid' => $externalUserid, 'corp_id' => $corpId])) { + unset($externalContact['external_userid']); + $this->dao->update(['external_userid' => $externalUserid], $externalContact); + } else { + $externalContact['create_time'] = time(); + $externalContact['update_time'] = time(); + $external[] = $externalContact; + } + } + if ($external) { + $this->dao->saveAll($external); + } + $clientList = $this->dao->getColumn([['external_userid', 'in', $externalUserids], ['corp_id', '=', $corpId]], 'id', 'external_userid'); + /** @var WorkClientFollowServices $followService */ + $followService = app()->make(WorkClientFollowServices::class); + if ($followUser) { + /** @var WorkClientFollowTagsServices $tagService */ + $tagService = app()->make(WorkClientFollowTagsServices::class); + foreach ($followUser as $userid => $items) { + $items['client_id'] = $clientList[$userid]; + if (($id = $followService->value(['client_id' => $clientList[$userid], 'userid' => $items['userid']], 'id'))) { + $followService->update($id, [ + 'remark' => $items['remark'], + 'description' => $items['description'], + 'createtime' => $items['createtime'], + 'remark_corp_name' => $items['remark_corp_name'], + 'remark_mobiles' => $items['remark_mobiles'], + 'add_way' => $items['add_way'], + 'oper_userid' => $items['oper_userid'], + ]); + } else { + $res = $followService->save($items); + $id = $res->id; + } + if (!empty($items['tags'])) { + $tagService->delete(['follow_id' => $id]); + foreach ($items['tags'] as &$tag) { + $tag['follow_id'] = $id; + } + $tagService->saveAll($items['tags']); + } + } + } + }); + + if (isset($response['next_cursor']) && $response['next_cursor']) { + WorkClientJob::dispatchDo('authClient', [$page, $response['next_cursor'] ?? '']); + } else if (count($userids) >= 10 && empty($response['next_cursor'])) { + WorkClientJob::dispatchDo('authClient', [$page + 1, '']); + } + + return true; + } + + + public function saveClientTags(array $tagGroup) + { + + } + + /** + * 创建客户 + * @param array $payload + * @return mixed + */ + public function createClient(array $payload) + { + $corpId = $payload['ToUserName'];//企业id + $externalUserID = $payload['ExternalUserID'];//外部企业userid + $state = $payload['State'] ?? '';//扫码值 + $userId = $payload['UserID'];//成员userid + + //保存客户 + $clientId = $this->saveOrUpdateClient($corpId, $externalUserID, $userId); + + //发送欢迎语 + try { + event('work.welcome', [$payload['WelcomeCode'] ?? '', $state, $clientId, $userId]); + } catch (\Throwable $e) { + Log::error([ + 'message' => '发送欢迎语失败:' . $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + + //设置欢客户标签 + try { + event('work.label', [$state, $userId, $externalUserID]); + } catch (\Throwable $e) { + Log::error([ + 'message' => '设置欢客户标签失败:' . $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + + //关联客户与商城用户 + try { + event('work.user', [$clientId]); + } catch (\Throwable $e) { + Log::error([ + 'message' => '关联客户与商城用户失败:' . $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + + return $clientId; + } + + /** + * 更新客户信息 + * @param array $payload + * @return mixed + */ + public function updateClient(array $payload) + { + $corpId = $payload['ToUserName']; + $externalUserID = $payload['ExternalUserID']; + $userId = $payload['UserID'];//成员serid + + $clientId = $this->saveOrUpdateClient($corpId, $externalUserID, $userId); + + //关联客户与商城用户 + try { + event('work.user', [$clientId]); + } catch (\Throwable $e) { + Log::error([ + 'message' => '关联客户与商城用户失败:' . $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + + return $clientId; + } + + /** + * 企业成员删除客户 + * @param array $payload + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function deleteClient(array $payload) + { + $corpId = $payload['ToUserName']; + $externalUserID = $payload['ExternalUserID']; + $userId = $payload['UserID'];//成员serid + $clientInfo = $this->dao->get(['external_userid' => $externalUserID, 'corp_id' => $corpId], ['id']); + if ($clientInfo) { + $this->transaction(function () use ($clientInfo, $userId) { + $this->dao->destroy($clientInfo->id); + /** @var WorkClientFollowServices $followService */ + $followService = app()->make(WorkClientFollowServices::class); + $followService->update(['client_id' => $clientInfo->id, 'userid' => $userId], ['is_del_user' => 1]); + }); + } + return true; + } + + /** + * 客户删除企业微信成员 + * @param array $payload + * @return bool + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function deleteFollowClient(array $payload) + { + $corpId = $payload['ToUserName']; + $externalUserID = $payload['ExternalUserID']; + $userId = $payload['UserID'];//成员serid + $clientInfo = $this->dao->get(['external_userid' => $externalUserID, 'corp_id' => $corpId], ['id']); + /** @var WorkClientFollowServices $followService */ + $followService = app()->make(WorkClientFollowServices::class); + if ($clientInfo) { + $followService->update(['client_id' => $clientInfo->id, 'userid' => $userId], ['is_del_user' => 1]); + } + return true; + } + + /** + * 更新或者添加客户信息 + * @param string $corpId + * @param string $externalUserID + * @param string $userId + * @return mixed + */ + public function saveOrUpdateClient(string $corpId, string $externalUserID, string $userId) + { + $response = Work::getClientInfo($externalUserID); + $externalContact = $response['external_contact'] ?? []; + $followUser = $response['follow_user'] ?? []; + $res = true; + $externalContact['corp_id'] = $corpId; + $externalContact['external_profile'] = json_encode($externalContact['external_profile'] ?? []); + $clientId = $this->dao->value(['external_userid' => $externalContact['external_userid'], 'corp_id' => $corpId], 'id'); + + try { + $clientId = $this->transaction(function () use ($userId, $res, $clientId, $externalContact, $followUser) { + if ($clientId) { + $this->dao->update($clientId, $externalContact); + } else { + $res = $this->dao->save($externalContact); + $clientId = $res->id; + } + $userids = []; + $res1 = false; + foreach ($followUser as &$item) { + $item['create_time'] = time(); + if ($userId === $item['userid']) { + $res1 = true; + } + $userids[] = $item['userid']; + $item['client_id'] = $clientId; + if (isset($item['wechat_channels'])) { + unset($item['wechat_channels']); + } + } + if (!$res1 && $userId) { + $followUser[] = [ + 'client_id' => $clientId, + 'userid' => $userId, + 'createtime' => time(), + 'tags' => [] + ]; + } + //添加了此外部联系人的企业成员 + if ($followUser) { + /** @var WorkClientFollowServices $followService */ + $followService = app()->make(WorkClientFollowServices::class); + /** @var WorkClientFollowTagsServices $tagService */ + $tagService = app()->make(WorkClientFollowTagsServices::class); + foreach ($followUser as $item) { + if (($id = $followService->value(['client_id' => $clientId, 'userid' => $item['userid']], 'id'))) { + $followService->update($id, [ + 'remark' => $item['remark'], + 'description' => $item['description'], + 'remark_corp_name' => $item['remark_corp_name'] ?? '', + 'add_way' => $item['add_way'] ?? '', + 'oper_userid' => $item['oper_userid'] ?? '', + ]); + } else { + $res = $followService->save($item); + $id = $res->id; + } + $tagService->delete(['follow_id' => $id]); + if (!empty($item['tags'])) { + $tagsNews = []; + foreach ($item['tags'] as $tag) { + $tag['follow_id'] = $id; + $tagsNews[] = $tag; + } + $tagService->saveAll($tagsNews); + } + } + } + if (!$res) { + throw new ValidateException('保存失败'); + } + return $clientId; + }); + + } catch (\Throwable $e) { + Log::error([ + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + return $clientId; + } + + /** + * @param string $userid + * @param array $clientInfo + * @return array + */ + public function getClientInfo(string $userid, array $clientInfo) + { + $clientInfo['userInfo'] = []; + if ($clientInfo['uid']) { + /** @var UserServices $make */ + $make = app()->make(UserServices::class); + $userInfo = $make->get($clientInfo['uid'], ['*'], ['label', 'userGroup', 'spreadUser']); + if ($userInfo) { + $clientInfo['userInfo'] = $userInfo->toArray(); + $clientInfo['userInfo']['birthday'] = $clientInfo['userInfo']['birthday'] ? date('Y-m-d', $clientInfo['userInfo']['birthday']) : ''; + } + } + return $clientInfo; + } + + /** + * 异步批量设置标签 + * @param array $addTag + * @param array $removeTag + * @param array $userId + * @param array $where + * @param int $isAll + * @return bool + */ + public function synchBatchLabel(array $addTag, array $removeTag, array $userId, array $where, int $isAll = 0) + { + if ($isAll) { + $clientList = $this->dao->getDataList($where, ['external_userid', 'id', 'unionid', 'uid'], 0, 0, null, ['followOne']); + } else { + $clientList = $this->dao->getDataList(['external_userid' => $userId], ['external_userid', 'id', 'unionid', 'uid'], 0, 0, null, ['followOne']); + } + $batchClient = []; + foreach ($clientList as $item) { + if (!empty($item['followOne'])) { + $batchClient[] = [ + 'external_userid' => $item['external_userid'], + 'userid' => $item['followOne']['userid'], + 'add_tag' => $addTag, + 'remove_tag' => $removeTag, + ]; + } + } + if ($batchClient) { + foreach ($batchClient as $item) { + WorkClientJob::dispatchDo('setLabel', [$item]); + } + } + return true; + } + + /** + * 设置客户标签 + * @param array $markTag + * @return WechatResponse|false + */ + public function setClientMarkTag(array $markTag) + { + try { + $res = Work::markTags($markTag['userid'], $markTag['external_userid'], $markTag['add_tag'], $markTag['remove_tag']); + $res = new WechatResponse($res); + //同步标签后同步用户信息 + /** @var WorkConfig $config */ + $config = app()->make(WorkConfig::class); + $corpId = $config->get('corpId'); + WorkClientJob::dispatchSece(2, 'saveClientInfo', [$corpId, $markTag['external_userid'], $markTag['userid']]); + return $res; + } catch (\Throwable $e) { + Log::error([ + 'message' => '修改客户标签发生错误:' . $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + return false; + } + } + + /** + * 查找成员下附带的客户人数 + * @param array $where + * @return int + */ + public function getUserIdsByCount(array $where) + { + if ($where['is_all']) { + unset($where['time'], $where['label'], $where['notLabel']); + } + $where['timeKey'] = 'create_time'; + + if (!empty($where['label'])) { + /** @var UserLabelServices $service */ + $service = app()->make(UserLabelServices::class); + $tagId = $service->getColumn([ + ['id', 'in', $where['label']], + ], 'tag_id'); + $where['label'] = array_unique($tagId); + } + if (!empty($where['notLabel'])) { + /** @var UserLabelServices $service */ + $service = app()->make(UserLabelServices::class); + $tagId = $service->getColumn([ + ['id', 'in', $where['notLabel']], + ], 'tag_id'); + $where['notLabel'] = array_unique($tagId); + } + + return $this->dao->getClientCount($where); + } + + /** + * 解绑用户 + * @param int $uid + */ + public function unboundUser(int $uid) + { + try { + $this->dao->update(['uid' => $uid], ['uid' => 0]); + } catch (\Throwable $e) { + Log::error([ + 'message' => '解绑用户失败:' . $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + } +} diff --git a/app/services/work/WorkGroupChatAuthServices.php b/app/services/work/WorkGroupChatAuthServices.php new file mode 100644 index 0000000..93daf41 --- /dev/null +++ b/app/services/work/WorkGroupChatAuthServices.php @@ -0,0 +1,228 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\work; + + +use app\dao\work\WorkGroupChatAuthDao; +use app\services\BaseServices; +use app\services\user\label\UserLabelServices; +use crmeb\services\wechat\Work; +use crmeb\traits\service\ContactWayQrCode; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; + +/** + * 企业微信自动拉群 + * Class WorkGroupChatAuthServices + * @package app\services\work + * @mixin WorkGroupChatAuthDao + */ +class WorkGroupChatAuthServices extends BaseServices +{ + + use ServicesTrait, ContactWayQrCode; + + /** + * WorkGroupChatAuthServices constructor. + * @param WorkGroupChatAuthDao $dao + */ + public function __construct(WorkGroupChatAuthDao $dao) + { + $this->dao = $dao; + } + + + /** + * @param array $where + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getDataList($where, ['*'], $page, $limit, 'create_time'); + $chatIds = []; + $labels = []; + foreach ($list as $item) { + $chatIds = array_merge($chatIds, $item['chat_id']); + $labels = array_merge($labels, $item['label'] ?? []); + } + $chatIds = array_merge(array_unique(array_filter($chatIds))); + $labels = array_merge(array_unique(array_filter($labels))); + /** @var UserLabelServices $userLabelService */ + $userLabelService = app()->make(UserLabelServices::class); + $labelList = $userLabelService->getColumn([ + ['tag_id', 'in', $labels] + ], 'label_name', 'tag_id'); + /** @var WorkGroupChatServices $service */ + $service = app()->make(WorkGroupChatServices::class); + $chatList = $service->getColumn([ + ['chat_id', 'in', $chatIds] + ], 'name', 'chat_id'); + foreach ($list as &$item) { + $chatNewList = $labelNewList = []; + foreach ($chatList as $key => $val) { + if (in_array($key, $item['chat_id'])) { + $chatNewList[] = ['name' => $val, 'chat_id' => $key]; + } + } + if ($item['label']) { + foreach ($labelList as $k => $v) { + if (in_array($k, $item['label'])) { + $labelNewList[] = ['name' => $v, 'label_id' => $k]; + } + } + } + $item['chat_list'] = $chatNewList; + $item['label_list'] = $labelNewList; + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 保存或者修改自动拉群 + * @param array $data + * @param int $id + * @return mixed + */ + public function saveGroupChatAuth(array $data, int $id = 0) + { + return $this->transaction(function () use ($data, $id) { + if ($id) { + $authInfo = $this->dao->get(['id' => $id], ['config_id']); + $this->dao->update($id, $data); + $this->handleGroupChat($data, $id, $authInfo->config_id); + } else { + $res = $this->dao->save($data); + $id = $res->id; + $this->handleGroupChat($data, $id); + } + return $id; + }); + } + + /** + * 配置加入群聊并获取二维码 + * @param string $configId + * @param array $data + * @param int $id + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + protected function handleGroupChat(array $data, int $id, string $configId = '') + { + //设置进群配置 + if ($configId) { + $qrCode = Work::groupChat()->updateJoinWay($configId, $data['group_name'], $data['chat_id'], 'groupChat-' . $id, (int)$data['auth_group_chat'], (int)$data['group_num']); + } else { + $qrCode = Work::groupChat()->addJoinWay($data['group_name'], $data['chat_id'], 'groupChat-' . $id, (int)$data['auth_group_chat'], (int)$data['group_num']); + } + + if (0 !== $qrCode['errcode']) { + throw new ValidateException($qrCode['errmsg']); + } + + if (!$configId) { + $this->dao->update($id, ['config_id' => $qrCode['config_id']]); + $configId = $qrCode['config_id']; + } + + //获取群二维码 + $qrCodeInfo = Work::groupChat()->getJoinWay($configId); + if (0 !== $qrCodeInfo['errcode']) { + throw new ValidateException($qrCodeInfo['errmsg']); + } + + $this->dao->update($id, ['qr_code' => $qrCodeInfo['join_way']['qr_code']]); + } + + /** + * 删除客户进群配置 + * @param int $id + * @return bool + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function deleteGroupChatAuth(int $id) + { + $groupChatAuthInfo = $this->dao->get($id); + + if (!$groupChatAuthInfo) { + throw new ValidateException('删除的群聊配置不存在'); + } + + //删除入群配置 + if ($groupChatAuthInfo->config_id) { + $qrCode = Work::groupChat()->deleteJoinWay($groupChatAuthInfo->config_id); + if (0 !== $qrCode['errcode']) { + throw new ValidateException($qrCode['errmsg']); + } + } + + return $this->dao->destroy($id); + } + + /** + * 获取群配置 + * @param int $id + * @return array + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\DbException + * @throws \think\db\exception\ModelNotFoundException + */ + public function getGrouChatAuthInfo(int $id) + { + $groupChatAuthInfo = $this->dao->get($id); + if (!$groupChatAuthInfo) { + throw new ValidateException('删除的群聊配置不存在'); + } + $groupChatAuthInfo['labelList'] = $groupChatAuthInfo['chatList'] = []; + if ($groupChatAuthInfo->label) { + /** @var UserLabelServices $userLabelService */ + $userLabelService = app()->make(UserLabelServices::class); + $groupChatAuthInfo['labelList'] = $userLabelService->getColumn([ + ['tag_id', 'in', $groupChatAuthInfo->label] + ], 'label_name,tag_id'); + } + if ($groupChatAuthInfo->chat_id) { + /** @var WorkGroupChatServices $service */ + $service = app()->make(WorkGroupChatServices::class); + $groupChatAuthInfo['chatList'] = $service->getColumn([ + ['chat_id', 'in', $groupChatAuthInfo->chat_id] + ], 'name,chat_id'); + } + return $groupChatAuthInfo->toArray(); + } + + /** + * + * @param int $groupAuthId + * @param string $userid + * @param string $externalUserID + */ + public function clientAddLabel(int $groupAuthId, string $userid, string $externalUserID) + { + $label = $this->dao->value(['id' => $groupAuthId], 'label'); + + $resTage = Work::markTags($userid, $externalUserID, $label); + if (0 !== $resTage['errcode']) { + throw new ValidateException($resTage['errmsg']); + } + } + +} diff --git a/app/services/work/WorkMediaServices.php b/app/services/work/WorkMediaServices.php new file mode 100644 index 0000000..9a0a74c --- /dev/null +++ b/app/services/work/WorkMediaServices.php @@ -0,0 +1,207 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\work; + + +use app\dao\work\WorkMediaDao; +use app\services\BaseServices; +use crmeb\basic\BaseModel; +use crmeb\services\FileService; +use crmeb\services\wechat\Work; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use GuzzleHttp\Exception\GuzzleException; +use think\db\exception\DataNotFoundException; +use think\db\exception\DbException; +use think\db\exception\ModelNotFoundException; +use think\exception\ValidateException; +use think\Model; + +/** + * 企业微信素材 + * Class WorkMediaServices + * @package app\services\work + * @mixin WorkMediaDao + */ +class WorkMediaServices extends BaseServices +{ + + /** + * WorkMediaServices constructor. + * @param WorkMediaDao $dao + */ + public function __construct(WorkMediaDao $dao) + { + $this->dao = $dao; + } + + /** + * 获取附件资源 + * @param string $url + * @param string $type + * @param int $uploadType + * @return array|bool + * @throws DataNotFoundException + * @throws DbException + * @throws GuzzleException + * @throws InvalidConfigException + * @throws ModelNotFoundException + */ + public function getPathMediaInfo(string $url, string $type, int $uploadType = 0) + { + $pathInfo = parse_url($url); + $path = $pathInfo['path']; + $mediaInfo = []; + $md5Path = md5($path); + $info = $this->dao->get(['md5_path' => $md5Path, 'type' => $type, 'upload_type' => $uploadType], ['media_id', 'url', 'temporary']); + if ($info) { + if ($info->temporary && $info->media_id) { + $mediaInfo = $info->toArray(); + } + if (((int)$info->temporary) === 0) { + $mediaInfo = $info->toArray(); + } + } + + if (!$mediaInfo) { + $pathUrl = public_path() . $path; + if (is_file($pathUrl)) { + $uploadInfo = $this->mediaUpload($uploadType, $pathUrl, $type, $md5Path, $info); + $mediaInfo['media_id'] = $uploadInfo['media_id']; + } else { + //获取文件内容 + $stream = file_get_contents($url); + + //创建文件路径 + $dir = public_path() . 'uploads' . DS . 'temp'; + try { + FileService::mkDir($dir); + } catch (\Throwable $e) { + throw new ValidateException($e->getMessage()); + } + + //把文件流保存到本地 + $pathUrl = $dir . DS . basename($url); + file_put_contents($pathUrl, $stream); + + //上传到素材附件 + $uploadInfo = $this->mediaUpload($uploadType, $pathUrl, $type, $md5Path, $info); + unlink($pathUrl); + + $mediaInfo['media_id'] = $uploadInfo['media_id']; + + } + } + return $mediaInfo; + } + + /** + * 上传临时素材 + * @param int $uploadType + * @param string $pathUrl + * @param string $type + * @param string $md5Path + * @param $info + * @return BaseModel|Model + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function mediaUpload(int $uploadType, string $pathUrl, string $type, string $md5Path, $info) + { + if ($uploadType) { + $resMedia = Work::mediaUploadAttachment($pathUrl, $type); + } else { + $resMedia = Work::mediaUpload($pathUrl, $type); + } + if ($info) { + $info->media_id = $resMedia['media_id']; + $info->valid_time = (int)$resMedia['created_at'] + 60 * 60 * 24 * 3 - 60;//3天有效期 + $info->save(); + return $info; + } else { + return $this->dao->save([ + 'path' => $pathUrl, + 'md5_path' => $md5Path, + 'type' => $type, + 'upload_type' => $uploadType, + 'media_id' => $resMedia['media_id'], + 'valid_time' => (int)$resMedia['created_at'] + 60 * 60 * 24 * 3 - 60,//3天有效期 + 'create_time' => time(), + 'temporary' => 1 + ]); + } + } + + + /** + * 获取欢迎语 + * @param array $welcome + * @return array + * @throws DataNotFoundException + * @throws DbException + * @throws GuzzleException + * @throws InvalidConfigException + * @throws ModelNotFoundException + */ + public function resolvingWelcome(array $welcome, int $uploadType = 0) + { + //清除过期的附件 + $this->dao->deleteValidFile(); + + $attachments = []; + foreach ($welcome['attachments'] as $item) { + switch ($item['msgtype']) { + case 'image': + $mediaInfo = $this->getPathMediaInfo($item['image']['pic_url'], 'image', $uploadType); + if (!empty($mediaInfo['media_id'])) { + $item['image']['media_id'] = $mediaInfo['media_id']; + unset($item['image']['pic_url']); + $attachments[] = $item; + } + break; + case 'link': + + break; + case 'miniprogram': + $mediaInfo = $this->getPathMediaInfo($item['miniprogram']['pic_url'], 'image', $uploadType); + if (!empty($mediaInfo['media_id'])) { + $item['miniprogram']['pic_media_id'] = $mediaInfo['media_id']; + unset($item['miniprogram']['pic_url']); + $attachments[] = $item; + } + break; + case 'video': + $mediaInfo = $this->getPathMediaInfo($item['video']['url'], 'video', $uploadType); + if (!empty($mediaInfo['media_id'])) { + $item['video']['media_id'] = $mediaInfo['media_id']; + unset($item['video']['pic_url']); + $attachments[] = $item; + } + break; + case 'file': + $mediaInfo = $this->getPathMediaInfo($item['file']['url'], 'file', $uploadType); + if (!empty($mediaInfo['media_id'])) { + $item['file']['media_id'] = $mediaInfo['media_id']; + unset($item['file']['pic_url']); + $attachments[] = $item; + } + break; + } + } + + return [ + 'text' => [ + 'content' => $welcome['text']['content'], + ], + 'attachments' => $attachments + ]; + } +} diff --git a/app/services/work/WorkMemberServices.php b/app/services/work/WorkMemberServices.php new file mode 100644 index 0000000..0c1cbf7 --- /dev/null +++ b/app/services/work/WorkMemberServices.php @@ -0,0 +1,430 @@ + +// +---------------------------------------------------------------------- + +namespace app\services\work; + + +use app\dao\work\WorkMemberDao; +use app\jobs\work\WorkMemberJob; +use app\services\BaseServices; +use crmeb\services\wechat\config\WorkConfig; +use crmeb\services\wechat\Work; +use crmeb\traits\ServicesTrait; +use think\exception\ValidateException; +use think\facade\Log; +use think\helper\Str; + +/** + * 企业微信成员 + * Class WorkMemberServices + * @package app\services\work + * @mixin WorkMemberDao + */ +class WorkMemberServices extends BaseServices +{ + use ServicesTrait; + + const TABLE_FIELD = ['Name', 'MainDepartment', 'DirectLeader', + 'Mobile', 'Position', 'Gender', 'Email', 'BizMail', 'Status', 'Avatar', 'Alias', + 'Telephone', 'Address']; + + /** + * WorkMemberServices constructor. + * @param WorkMemberDao $dao + */ + public function __construct(WorkMemberDao $dao) + { + $this->dao = $dao; + } + + /** + * 员工列表 + * @param array $where + * @return array + */ + public function getList(array $where) + { + [$page, $limit] = $this->getPageValue(); + $list = $this->dao->getDataList($where, ['*'], $page, $limit, 'create_time', [ + 'departmentRelation', + 'clientFollow' => function ($query) { + $query->group('userid')->where('is_del_user', 0)->field(['count(*) as sum_follow', 'userid']); + }, + 'chat' => function ($query) { + $query->group('userid')->field(['count(*) as sum_chat', 'userid']); + }, + ]); + $departmentIds = []; + foreach ($list as &$item) { + if (!empty($item['departmentRelation'])) { + $item['departmentId'] = array_column($item['departmentRelation'], 'department'); + } else { + $item['departmentId'] = []; + } + $departmentIds = array_merge($departmentIds, $item['departmentId']); + } + $departmentIds = array_merge(array_unique(array_filter($departmentIds))); + if ($departmentIds) { + /** @var WorkDepartmentServices $services */ + $services = app()->make(WorkDepartmentServices::class); + $departmentList = $services->getColumn([ + ['department_id', 'in', $departmentIds], + ], 'name', 'department_id'); + foreach ($list as &$item) { + $department = []; + foreach ($departmentList as $k => $v) { + if (in_array($k, $item['departmentId'])) { + $department[] = ['name' => $v, 'department' => $k]; + } + } + $item['department_list'] = $department; + } + } + $count = $this->dao->count($where); + return compact('list', 'count'); + } + + /** + * 保存成员数据 + * @param array $members + * @return bool + */ + public function saveMember(array $members) + { + /** @var WorkConfig $config */ + $config = app()->make(WorkConfig::class); + $corpId = $config->get('corpId'); + if (!$corpId) { + return true; + } + /** @var WorkDepartmentServices $departmentService */ + $departmentService = app()->make(WorkDepartmentServices::class); + $defaultDepartment = $departmentService->value(['corp_id' => $corpId, 'parentid' => 0], 'department_id'); + + $this->transaction(function () use ($members, $corpId, $defaultDepartment) { + $data = []; + $relation = []; + $other = []; + $userids = array_column($members, 'userid'); + foreach ($members as $member) { + if (isset($member['english_name'])) { + unset($member['english_name']); + } + $address = $bizMail = ''; + if (isset($member['address'])) { + $address = $member['address']; + unset($member['address']); + } + if (isset($member['biz_mail'])) { + $bizMail = $member['biz_mail']; + unset($member['biz_mail']); + } + $member['address'] = $address; + $member['biz_mail'] = $bizMail; + if (isset($member['extattr']) && $member['extattr']) { + $other[$member['userid']] = [ + 'extattr' => json_encode($member['extattr'] ?? []), + 'external_profile' => json_encode($member['external_profile'] ?? []), + ]; + } + if (!empty($member['department'])) { + foreach ($member['department'] as $i => $department) { + $relation[$member['userid']][] = [ + 'department' => $member['department'][$i] ?? 0, + 'srot' => $member['order'][$i] ?? 0, + 'is_leader_in_dept' => $member['is_leader_in_dept'][$i] ?? 0 + ]; + } + } else { + //写入默认部门 + $relation[$member['userid']][] = ['department' => $defaultDepartment, 'srot' => 0, 'is_leader_in_dept' => 0]; + } + $externalPosition = ''; + if (isset($member['external_position'])) { + $externalPosition = $member['external_position']; + unset($member['external_position']); + } + $member['external_position'] = $externalPosition; + $member['direct_leader'] = json_encode($member['direct_leader']); + $member['is_leader'] = $member['isleader']; + $member['corp_id'] = $corpId; + if (isset($member['external_profile'])) { + unset($member['external_profile']); + } + unset($member['isleader'], $member['is_leader_in_dept'], $member['order'], $member['department'], $member['extattr']); + if ($this->dao->count(['userid' => $member['userid'], 'corp_id' => $corpId])) { + $this->dao->update(['userid' => $member['userid']], $member); + } else { + $member['create_time'] = time(); + $data[] = $member; + } + } + //写入成员数据 + if ($data) { + $this->dao->saveAll($data); + } + $userList = $this->dao->getColumn([['userid', 'in', $userids], ['corp_id', '=', $corpId]], 'id', 'userid'); + $userValueAll = array_values($userList); + //写入关联数据 + if (count($relation)) { + /** @var WorkMemberRelationServices $relationService */ + $relationService = app()->make(WorkMemberRelationServices::class); + $relationService->delete([['member_id', 'in', $userValueAll]]); + $saveRelation = []; + foreach ($relation as $userid => $item) { + $memberId = $userList[$userid]; + foreach ($item as $value) { + $saveRelation[] = [ + 'member_id' => $memberId, + 'create_time' => time(), + 'department' => $value['department'], + 'srot' => $value['srot'], + 'is_leader_in_dept' => $value['is_leader_in_dept'], + ]; + } + } + $relationService->saveAll($saveRelation); + } + //写入其他数据 + if (count($other)) { + /** @var WorkMemberOtherServices $otherService */ + $otherService = app()->make(WorkMemberOtherServices::class); + $otherService->delete([['member_id', 'in', $userValueAll]]); + foreach ($other as $userid => &$item) { + $memberId = $userList[$userid]; + $item['member_id'] = $memberId; + } + $otherService->saveAll($other); + } + }); + return true; + } + + /** + * 自动更新企业成员 + * @param int $departmentId + */ + public function authUpdataMember(int $departmentId) + { + $res = Work::getDetailedDepartmentUsers($departmentId); + $members = $res['userlist'] ?? []; + $maxCount = 500; + $sumCount = count($members); + if ($sumCount > $maxCount) { + $page = ceil($maxCount / $sumCount); + for ($i = 1; $i < $page; $i++) { + $res = collect($members)->slice($maxCount * $i, $maxCount)->toArray(); + WorkMemberJob::dispatchDo('save', [$res]); + } + } else { + $this->saveMember($members); + } + } + + /** + * 获取提交字段 + * @param array $payload + * @return array + */ + protected function getTableField(array $payload) + { + $data = []; + foreach (self::TABLE_FIELD as $key) { + $strKey = Str::snake($key); + if (isset($payload[$strKey])) { + $data[$strKey] = $payload[$strKey]; + } + } + return $data; + } + + /** + * 更新企业成员 + * @param array $payload + * @return mixed + */ + public function updateMember(array $payload) + { + $corpId = $payload['ToUserName'] ?? ''; + $userId = $payload['UserID'] ?? ''; + $updateData = $this->getTableField($payload); + if (!empty($payload['NewUserID'])) { + $updateData['userid'] = $payload['NewUserID']; + } + + $memberInfo = Work::getMemberInfo($userId); + if (0 !== $memberInfo['errcode']) { + throw new ValidateException($memberInfo['errmsg']); + } + $extattr = $memberInfo['extattr'] ?? []; + $externalProfile = $memberInfo['external_profile'] ?? []; + unset($memberInfo['errcode'], $memberInfo['errmsg'], $memberInfo['department'], + $memberInfo['order'], $memberInfo['is_leader_in_dept'], $memberInfo['extattr'], + $memberInfo['external_profile']); + $updateData = array_merge($updateData, $memberInfo); + + $memberId = $this->dao->value(['userid' => $userId], 'id'); + if ($memberId) { + if ($updateData) { + $dbCorpId = $this->dao->value(['userid' => $userId], 'corp_id'); + if (!$dbCorpId) { + $updateData['corp_id'] = $corpId; + } + $this->dao->update(['corp_id' => $corpId, 'userid' => $userId], $updateData); + } + } else { + if (!empty($payload['NewUserID'])) { + $updateData['userid'] = $payload['NewUserID']; + } + $res = $this->dao->save($updateData); + $memberId = $res->id; + } + /** @var WorkMemberRelationServices $relationServices */ + $relationServices = app()->make(WorkMemberRelationServices::class); + $relationServices->saveMemberDepartment($memberId, $payload['IsLeaderInDept'] ?? '', $payload['Department'] ?? ''); + + //写入其他数据 + if (!empty($extattr['attrs']) || !empty($externalProfile)) { + /** @var WorkMemberOtherServices $otherService */ + $otherService = app()->make(WorkMemberOtherServices::class); + $otherInfo = $otherService->get(['member_id' => $memberId]); + if ($otherInfo) { + $otherInfo->extattr = json_encode($extattr); + $otherInfo->external_profile = json_encode($externalProfile); + $otherInfo->save(); + } else { + $otherService->save([ + 'member_id' => $memberId, + 'extattr' => json_encode($extattr), + 'external_profile' => json_encode($externalProfile), + ]); + } + } + + return $memberId; + } + + /** + * 创建企业微信成员 + * @param array $payload + * @return mixed + */ + public function createMember(array $payload) + { + $corpId = $payload['ToUserName'] ?? ''; + if (!$corpId) { + /** @var WorkConfig $config */ + $config = app()->make(WorkConfig::class); + $corpId = $config->get('corpId'); + } + $userId = $payload['UserID'] ?? ''; + $data = $this->getTableField($payload); + $memberInfo = Work::getMemberInfo($userId); + if (0 !== $memberInfo['errcode']) { + throw new ValidateException($memberInfo['errmsg']); + } + $extattr = $memberInfo['extattr'] ?? []; + $externalProfile = $memberInfo['external_profile'] ?? []; + unset($memberInfo['errcode'], $memberInfo['errmsg'], $memberInfo['department'], + $memberInfo['order'], $memberInfo['is_leader_in_dept'], $memberInfo['extattr'], + $memberInfo['external_profile']); + $data = array_merge($data, $memberInfo); + $memberId = $this->dao->value(['userid' => $userId], 'id'); + if ($memberId) { + if ($data) { + $dbCorpId = $this->dao->value(['userid' => $userId], 'corp_id'); + if (!$dbCorpId) { + $data['corp_id'] = $corpId; + } + $this->dao->update(['userid' => $userId], $data); + } + } else { + $data['corp_id'] = $corpId; + $res = $this->dao->save($data); + $memberId = $res->id; + } + + //记录 + $isLeaderInDept = $payload['IsLeaderInDept'] ?? ''; + $department = $payload['Department'] ?? ''; + if (!$department && !$isLeaderInDept) { + //写入主部门 + /** @var WorkDepartmentServices $departmentService */ + $departmentService = app()->make(WorkDepartmentServices::class); + $id = $departmentService->value(['corp_id' => $corpId, 'parentid' => 0], 'department_id'); + if ($id) { + $department = (string)$id; + $isLeaderInDept = '0'; + } + } + /** @var WorkMemberRelationServices $relationServices */ + $relationServices = app()->make(WorkMemberRelationServices::class); + $relationServices->saveMemberDepartment($memberId, $isLeaderInDept, $department); + + //写入其他数据 + if (!empty($extattr['attrs']) || !empty($externalProfile)) { + /** @var WorkMemberOtherServices $otherService */ + $otherService = app()->make(WorkMemberOtherServices::class); + $otherInfo = $otherService->get(['member_id' => $memberId]); + if ($otherInfo) { + $otherInfo->extattr = json_encode($extattr); + $otherInfo->external_profile = json_encode($externalProfile); + $otherInfo->save(); + } else { + $otherService->save([ + 'member_id' => $memberId, + 'extattr' => json_encode($extattr), + 'external_profile' => json_encode($externalProfile), + ]); + } + } + + return $memberId; + } + + /** + * 删除企业微信成员 + * @param string $corpId + * @param string $userid + */ + public function deleteMember(string $corpId, string $userid) + { + $memberId = $this->dao->value(['corp_id' => $corpId, 'userid' => $userid], 'id'); + if ($memberId) { + $this->transaction(function () use ($memberId) { + /** @var WorkMemberRelationServices $relationServices */ + $relationServices = app()->make(WorkMemberRelationServices::class); + $relationServices->delete(['member_id' => $memberId]); + /** @var WorkMemberOtherServices $otherServices */ + $otherServices = app()->make(WorkMemberOtherServices::class); + $otherServices->delete(['member_id' => $memberId]); + $this->dao->delete($memberId); + }); + } + } + + /** + * 用户注销解绑用户 + * @param int $uid + */ + public function unboundUser(int $uid) + { + try { + $this->dao->update(['uid' => $uid], ['uid' => 0]); + } catch (\Throwable $e) { + Log::error([ + 'message' => '解绑用户失败:' . $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + } +} diff --git a/app/validate/api/user/AddressValidate.php b/app/validate/api/user/AddressValidate.php new file mode 100644 index 0000000..e676dea --- /dev/null +++ b/app/validate/api/user/AddressValidate.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- +namespace app\validate\api\user; + +use think\Validate; + +/** + * 用户地址验证类 + * Class AddressValidate + * @package app\validate\api\user + */ +class AddressValidate extends Validate +{ + //移动 + protected $regex = ['phone' => '/^1[3456789]\d{9}|([0-9]{3,4}-)?[0-9]{7,8}$/']; + + protected $rule = [ + 'real_name' => 'require|max:25', + 'phone' => 'require|regex:phone', + 'province' => 'require', + 'city' => 'require', + 'district' => 'require', + 'detail' => 'require', + ]; + + protected $message = [ + 'real_name.require' => '名称必须填写', + 'real_name.max' => '名称最多不能超过25个字符', + 'phone.require' => '手机号必须填写', + 'phone.regex' => '手机号格式错误', + 'province.require' => '省必须填写', + 'city.require' => '市名称必须填写', + 'district.require' => '区/县名称必须填写', + 'detail.require' => '详细地址必须填写', + ]; +} diff --git a/crmeb/basic/BaseAuth.php b/crmeb/basic/BaseAuth.php new file mode 100644 index 0000000..39e5648 Binary files /dev/null and b/crmeb/basic/BaseAuth.php differ diff --git a/crmeb/basic/BaseErp.php b/crmeb/basic/BaseErp.php new file mode 100644 index 0000000..8688fbd --- /dev/null +++ b/crmeb/basic/BaseErp.php @@ -0,0 +1,49 @@ +accessToken = $accessToken; + } + + /** + * 初始化 + * @param array $config + * @return mixed|void + */ + protected function initialize(array $config = []) + { +// parent::initialize($config); + } +} \ No newline at end of file diff --git a/crmeb/basic/BaseJobs.php b/crmeb/basic/BaseJobs.php new file mode 100644 index 0000000..80c31da --- /dev/null +++ b/crmeb/basic/BaseJobs.php @@ -0,0 +1,80 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\basic; + +use crmeb\interfaces\JobInterface; +use think\facade\Log; +use think\queue\Job; + +/** + * 消息队列基类 + * Class BaseJobs + * @package crmeb\basic + */ +class BaseJobs implements JobInterface +{ + + /** + * @param $name + * @param $arguments + */ + public function __call($name, $arguments) + { + $this->fire(...$arguments); + } + + /** + * 运行消息队列 + * @param Job $job + * @param $data + */ + public function fire(Job $job, $data): void + { + try { + $action = $data['do'] ?? 'doJob';//任务名 + $infoData = $data['data'] ?? [];//执行数据 + $errorCount = $data['errorCount'] ?? 0;//最大错误次数 + $this->runJob($action, $job, $infoData, $errorCount); + } catch (\Throwable $e) { + $job->delete(); + } + } + + /** + * 执行队列 + * @param string $action + * @param Job $job + * @param array $infoData + * @param int $errorCount + */ + protected function runJob(string $action, Job $job, array $infoData, int $errorCount = 3) + { + + $action = method_exists($this, $action) ? $action : 'handle'; + if (!method_exists($this, $action)) { + $job->delete(); + } + if ($this->{$action}(...$infoData)) { + //删除任务 + $job->delete(); + } else { + if ($job->attempts() >= $errorCount && $errorCount) { + //删除任务 + $job->delete(); + } else { + //从新放入队列 + $job->release(); + } + } + } + +} diff --git a/crmeb/exceptions/AdminException.php b/crmeb/exceptions/AdminException.php new file mode 100644 index 0000000..78b5138 --- /dev/null +++ b/crmeb/exceptions/AdminException.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\exceptions; + + +use Throwable; + +/** + * Class AuthException + * @package crmeb\exceptions + */ +class AdminException extends \RuntimeException +{ + public function __construct($message, $code = 0, Throwable $previous = null) + { + if(is_array($message)){ + $errInfo = $message; + $message = $errInfo[1] ?? '未知错误'; + if ($code === 0) { + $code = $errInfo[0] ?? 400; + } + } + + parent::__construct($message, $code, $previous); + } +} diff --git a/crmeb/exceptions/ErpException.php b/crmeb/exceptions/ErpException.php new file mode 100644 index 0000000..15f1df8 --- /dev/null +++ b/crmeb/exceptions/ErpException.php @@ -0,0 +1,16 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\exceptions; + +/** + * Class PayException + * @package crmeb\exceptions + */ +class PayException extends \RuntimeException +{ + public function __construct($message = "", $code = 0, Throwable $previous = null) + { + if (is_array($message)) { + $errInfo = $message; + $message = $errInfo[1] ?? '未知错误'; + if ($code === 0) { + $code = $errInfo[0] ?? 400; + } + } + + parent::__construct($message, $code, $previous); + } +} diff --git a/crmeb/form/BaseComponent.php b/crmeb/form/BaseComponent.php new file mode 100644 index 0000000..c8c5c9c --- /dev/null +++ b/crmeb/form/BaseComponent.php @@ -0,0 +1,121 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form; + +/** + * 基础组件集成 + * Class BaseComponent + * @package crmeb\form + */ +abstract class BaseComponent +{ + + /** + * @var bool + */ + protected $init = false; + + /** + * @var array + */ + protected $rule = []; + + /** + * 数据库验证 + * @var array + */ + protected $validate = []; + + /** + * @var CommonRule + */ + protected $validataRule; + + /** + * 是否实例化 + */ + protected function init() + { + if (!$this->init) { + $this->validataRule = new CommonRule; + $this->init = true; + } + } + + /** + * 多个验证规则 + * @param array $validate + * @return $this + */ + public function validates(array $validate) + { + $this->validate = $validate; + return $this; + } + + /** + * 单个验证规则 + * @param CommonRule $validate + * @return $this + */ + public function validate(CommonRule $validate) + { + $this->validate[] = $validate; + return $this; + } + + + /** + * 是否必填 + * @return $this + */ + public function required() + { + $this->init(); + $this->validataRule->required(); + return $this; + } + + /** + * 设置提示消息 + * @param string $message + * @return $this + */ + public function message(string $message) + { + $this->init(); + $this->validataRule->message($message); + return $this; + } + + /** + * 数据写入 + */ + protected function before() + { + if (!$this->validate && $this->validataRule instanceof CommonRule) { + if (!$this->validataRule->getMessage() && $this->rule['title']) { + $this->validataRule->message('请输入' . $this->rule['title']); + } + $this->validate[] = $this->validataRule->toArray(); + } + $validate = []; + foreach ($this->validate as $item) { + if ($item instanceof CommonRule) { + $validate[] = $item->toArray(); + } else { + $validate[] = $item; + } + } + $this->rule['validate'] = $validate; + } +} diff --git a/crmeb/form/Build.php b/crmeb/form/Build.php new file mode 100644 index 0000000..b028b97 --- /dev/null +++ b/crmeb/form/Build.php @@ -0,0 +1,261 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form; + + +use crmeb\form\components\Addres; +use crmeb\form\components\Alert; +use crmeb\form\components\Card; +use crmeb\form\components\DiyTable; +use crmeb\form\components\Input; +use crmeb\form\components\InputNumber; +use crmeb\form\components\Map; +use crmeb\form\components\Radio; +use crmeb\form\components\Select; +use crmeb\form\components\Switchs; +use crmeb\form\components\Tabs; +use crmeb\form\components\Time; +use crmeb\form\components\UploadFrame; +use crmeb\form\components\UploadImage; + +/** + * Class Build + * @package crmeb\form + * @method Input input(string $field, string $title, $value = null) + * @method Tabs tabs(array $options = []) + * @method Card card(string $title) + * @method InputNumber inputNum(string $field, string $title, $value = null) + * @method Select select(string $field, string $title, $value = null) + * @method UploadFrame uploadFrame(string $field, string $title, $value = null) + * @method UploadImage uploadImage(string $field, string $title, $value = null) + * @method Radio radio(string $field, string $title, $value = null) + * @method Switchs switch (string $field, string $title, $value = null) + * @method Alert alert (string $title, string $type = '', bool $closable = false, bool $showIcon = false) + * @method DiyTable diyTable(string $field, string $title, array $value = [], array $options = []) + * @method Map map(string $field, string $title, $value = null) + * @method Addres addres(string $field, string $title, $value = null) + * @method Time time(string $field, string $title, $value = null) + */ +class Build +{ + + /** + * 挂载组件 + * @var string[] + */ + protected static $components = [ + 'input' => Input::class, + 'tabs' => Tabs::class, + 'card' => Card::class, + 'inputNum' => InputNumber::class, + 'select' => Select::class, + 'uploadFrame' => UploadFrame::class, + 'uploadImage' => UploadImage::class, + 'radio' => Radio::class, + 'switch' => Switchs::class, + 'alert' => Alert::class, + 'diyTable' => DiyTable::class, + 'addres' => Addres::class, + 'map' => Map::class, + 'time' => Time::class, + ]; + + /** + * @var array + */ + protected $rule = []; + + /** + * 请求地址 + * @var + */ + protected $url; + + /** + * @var string + */ + protected $method = 'POST'; + + /** + * @var array + */ + protected $data = []; + + /** + * Build constructor. + * @param string|null $url + * @param array $rule + * @param string|null $method + * @param array $data + */ + public function __construct(string $url = null, array $rule = [], string $method = null, array $data = []) + { + $this->url = $url; + $this->rule = $rule; + $this->method = $method ?: 'POST'; + $this->data = $data; + } + + /** + * @param array $rule + * @return $this + */ + public function rule(array $rule = []) + { + $this->rule = $rule; + return $this; + } + + /** + * @param string $url + * @return $this + */ + public function url(string $url) + { + $this->url = $url; + return $this; + } + + /** + * @param string $method + * @return $this + */ + public function method(string $method) + { + $this->method = $method; + return $this; + } + + /** + * @param array $data + * @return Build + */ + public function data(array $data) + { + $this->data = $data; + return $this; + } + + /** + * 批量设置数据 + * @param $rule + * @return mixed + */ + public function setValue($rule) + { + if (!$this->data) { + return $rule; + } + foreach ($rule as &$value) { + if (isset($value['value']) && $value['value'] !== '' && isset($value['field'])) { + $value['value'] = $this->data[$value['field']]; + } + if (isset($value['options']) && $value['options']) { + foreach ($value['options'] as $i => $option) { + if (isset($option['componentsModel']) && $option['componentsModel']) { + $value['options'][$i] = $this->setValue($option['componentsModel']); + } + } + } + if (isset($value['control']) && $value['control']) { + foreach ($value['control'] as $ii => $control) { + if (isset($control['componentsModel']) && $control['componentsModel']) { + $value['control'][$ii] = $this->setValue($control['componentsModel']); + } + } + } + if (isset($value['componentsModel']) && $value['componentsModel']) { + $value['componentsModel'] = $this->setValue($value['componentsModel']); + } + } + return $rule; + } + + /** + * 提取验证值 + * @param $rule + * @return array + */ + protected function getValidate($rule) + { + $validate = []; + foreach ($rule as $value) { + if (isset($value['field']) && isset($value['validate']) && $value['validate']) { + $validate[$value['field']] = $value['validate']; + } + if (isset($value['options']) && $value['options']) { + foreach ($value['options'] as $option) { + if (isset($option['componentsModel']) && $option['componentsModel']) { + $validate = array_merge($validate, $this->getValidate($option['componentsModel'])); + } + } + } + if (isset($value['control']) && $value['control']) { + foreach ($value['control'] as $control) { + if (isset($control['componentsModel']) && $control['componentsModel']) { + $validate = array_merge($validate, $this->getValidate($control['componentsModel'])); + } + } + } + if (isset($value['componentsModel']) && $value['componentsModel']) { + $validate = array_merge($validate, $this->getValidate($value['componentsModel'])); + } + } + return $validate; + } + + /** + * @return array + */ + public function toArray() + { + $rule = []; + foreach ($this->rule as $item) { + if ($item instanceof BuildInterface) { + $rule[] = $item->toArray(); + } + } + $data = [ + 'rules' => $this->setValue($rule), + 'validate' => $this->getValidate($rule), + 'url' => $this->url, + 'method' => $this->method + ]; + $data['validate'] = $data['validate'] ?: (object)[]; + $this->url = null; + $this->rule = []; + $this->method = 'POST'; + return $data; + } + + /** + * @return false|string + */ + public function toString() + { + return json_encode($this->toArray()); + } + + /** + * @param $name + * @param $arguments + * @return mixed + */ + public static function __callStatic($name, $arguments) + { + $compKeys = array_keys(self::$components); + if (in_array($name, $compKeys)) { + return new self::$components[$name](...$arguments); + } + throw new BuildException('Method does not exist'); + } +} diff --git a/crmeb/form/BuildInterface.php b/crmeb/form/BuildInterface.php new file mode 100644 index 0000000..a95e787 --- /dev/null +++ b/crmeb/form/BuildInterface.php @@ -0,0 +1,24 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form; + + +/** + * Interface BuildInterface + * @package crmeb\form + */ +interface BuildInterface +{ + + public function toArray(): array; + +} diff --git a/crmeb/form/CommonRule.php b/crmeb/form/CommonRule.php new file mode 100644 index 0000000..bae4c2b --- /dev/null +++ b/crmeb/form/CommonRule.php @@ -0,0 +1,180 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form; + +/** + * Class CommonRule + * @package crmeb\form + */ +class CommonRule implements BuildInterface +{ + /** + * 验证类型 + */ + const VALIDATE_TYPE = ['string', 'number', 'boolean', 'method', 'regexp', 'integer', 'float', 'array', 'object', 'enum', 'date', 'url', 'hex', 'email']; + + /** + * @var string + */ + protected $type = ''; + + /** + * @var bool + */ + protected $required = false; + + /** + * @var string + */ + protected $pattern = ''; + + /** + * 枚举值 + * @var array + */ + protected $enum = []; + + /** + * 提示语 + * @var string + */ + protected $message = ''; + + /** + * 深度验证对象 + * @var array + */ + protected $fields = []; + + /** + * 验证触发方式 + * @var string + */ + protected $trigger = 'blur'; + + /** + * @param string $type + * @return $this + */ + public function type(string $type) + { + $this->type = in_array($type, self::VALIDATE_TYPE) ? $type : null; + if (!$this->type) { + throw new FormValidate('验证类型错误'); + } + return $this; + } + + /** + * 是否必填 + * @return $this + */ + public function required() + { + $this->required = true; + return $this; + } + + /** + * 设置错误提示 + * @param string $message + * @return $this + */ + public function message(string $message) + { + $this->message = $message; + return $this; + } + + /** + * 提示语 + * @return string + */ + public function getMessage() + { + return $this->message; + } + + /** + * 枚举数据 + * @param array $enum + * @return $this + */ + public function enum(array $enum) + { + $this->enum = $enum; + return $this; + } + + /** + * 正则表达式 + * @param string $pattern + * @return $this + */ + public function pattern(string $pattern) + { + $this->pattern = $pattern; + return $this; + } + + /** + * 验证规则 + * @param string|array|BuildInterface $field + * @param array $rule + * @return $this + */ + public function field($field, array $rule = []) + { + if (!in_array($this->type, ['array', 'object'])) { + throw new BuildException('无效规则,类型只能在array或者object情况下才可设置'); + } + if ($this->type === 'array') { + $rules = []; + if ($field instanceof BuildInterface) { + $rules = $field->toArray(); + } + $this->fields[] = $rules; + } else { + $rules = []; + foreach ($rule as $item) { + if ($item instanceof BuildInterface) { + $rules[] = $item->toArray(); + } + } + $this->fields[$field] = $rules; + } + return $this; + } + + /** + * 数据转换 + * @return array + */ + public function toArray(): array + { + $data = [ + 'required' => $this->required, + 'message' => $this->message, + 'trigger' => $this->trigger, + 'pattern' => $this->pattern, + 'enum' => $this->enum, + 'type' => $this->type + ]; + $res = []; + foreach ($data as $key => $value) { + if (is_bool($value) || $value) { + $res[$key] = $value; + } + } + return $res; + } +} diff --git a/crmeb/form/FormValidate.php b/crmeb/form/FormValidate.php new file mode 100644 index 0000000..d168557 --- /dev/null +++ b/crmeb/form/FormValidate.php @@ -0,0 +1,25 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form; + + +/** + * Class FormValidate + * @package crmeb\form + */ +class FormValidate extends \RuntimeException +{ + public function __construct($message = "", $code = 0, Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/crmeb/form/components/Addres.php b/crmeb/form/components/Addres.php new file mode 100644 index 0000000..2ba5f06 --- /dev/null +++ b/crmeb/form/components/Addres.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BaseComponent; +use crmeb\form\BuildInterface; + +/** + * 选择地址 + * Class Addres + * @package crmeb\form\components + */ +class Addres extends BaseComponent implements BuildInterface +{ + + const NAME = 'addres'; + + /** + * @var string[] + */ + protected $rule = [ + 'title' => '', + 'field' => '', + 'value' => '', + 'info' => '', + ]; + + /** + * Map constructor. + * @param string $field + * @param string $title + * @param array $value + */ + public function __construct(string $field, string $title, array $value = null) + { + $this->rule['field'] = $field; + $this->rule['title'] = $title; + $this->rule['value'] = empty($value) ? '' : $value; + } + + /** + * @param string $info + * @return $this + */ + public function info(string $info) + { + $this->rule['info'] = $info; + return $this; + } + + /** + * @return array|string[] + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $this->before(); + return $this->rule; + } +} diff --git a/crmeb/form/components/Alert.php b/crmeb/form/components/Alert.php new file mode 100644 index 0000000..e4ce587 --- /dev/null +++ b/crmeb/form/components/Alert.php @@ -0,0 +1,102 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BuildInterface; + +/** + * 警告框 + * Class Alert + * @package crmeb\form\components + */ +class Alert implements BuildInterface +{ + + //组件名 + const NAME = 'alert'; + //提示类型 + const INFO = 'info'; + //成功类型 + const SUCCESS = 'success'; + //警告类型 + const WARNING = 'warning'; + //错误类型 + const ERROR = 'error'; + //组件类型 + const TYPE = [self::INFO, self::SUCCESS, self::WARNING, self::ERROR]; + + /** + * 规则 + * @var array + */ + protected $rule = [ + 'title' => '', + 'type' => '', + 'closable' => false, + 'showIcon' => false + ]; + + /** + * Alert constructor. + * @param string $title + * @param string $type + * @param bool $closable + * @param bool $showIcon + */ + public function __construct(string $title, string $type = '', bool $closable = false, bool $showIcon = false) + { + $this->rule['type'] = in_array($type, self::TYPE) ? $type : ''; + $this->rule['title'] = $title; + $this->rule['closable'] = $closable; + $this->rule['showIcon'] = $showIcon; + } + + /** + * 设置类型 + * @param string $type + * @return $this + */ + public function type(string $type) + { + $this->rule['type'] = in_array($type, self::TYPE) ? $type : ''; + return $this; + } + + /** + * 是否可关闭 + * @param bool $closable + * @return $this + */ + public function closable(bool $closable = false) + { + $this->rule['closable'] = $closable; + return $this; + } + + /** + * 是否展示图标 + * @param bool $showIcon + * @return $this + */ + public function showIcon(bool $showIcon = false) + { + $this->rule['showIcon'] = $showIcon; + return $this; + } + + public function toArray(): array + { + $this->rule['name'] = self::NAME; + return $this->rule; + } +} diff --git a/crmeb/form/components/Card.php b/crmeb/form/components/Card.php new file mode 100644 index 0000000..354db2f --- /dev/null +++ b/crmeb/form/components/Card.php @@ -0,0 +1,74 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BuildInterface; + +/** + * 卡片组件 + * Class Card + * @package crmeb\form\components + */ +class Card implements BuildInterface +{ + + /** + * 组件名 + */ + const NAME = 'card'; + + /** + * 规则 + * @var array + */ + protected $rule = [ + 'componentsModel' => [], + 'title' => '', + ]; + + /** + * Card constructor. + * @param string $title + */ + public function __construct(string $title) + { + $this->rule['title'] = $title; + } + + /** + * 添加组件群 + * @param array $components + * @return $this + */ + public function components(array $components = []) + { + $this->rule['componentsModel'] = $components; + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $componentsModel = []; + foreach ($this->rule['componentsModel'] as $item) { + if ($item instanceof BuildInterface) { + $componentsModel[] = $item->toArray(); + } + } + $this->rule['componentsModel'] = $componentsModel; + return $this->rule; + } +} diff --git a/crmeb/form/components/DiyTable.php b/crmeb/form/components/DiyTable.php new file mode 100644 index 0000000..ba0adbe --- /dev/null +++ b/crmeb/form/components/DiyTable.php @@ -0,0 +1,91 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BaseComponent; +use crmeb\form\BuildInterface; + +/** + * 自定义表格 + * Class DiyTable + * @package crmeb\form\components + */ +class DiyTable extends BaseComponent implements BuildInterface +{ + /** + * 组件名称 + */ + const NAME = 'diyTable'; + + //内部表格自定义类型 + const TYPE = ['input', 'select', 'inputNumber', 'switch']; + + /** + * 规则 + * @var string[] + */ + protected $rule = [ + 'title' => '', + 'value' => [], + 'type' => '', + 'field' => '', + 'options' => [], + 'info' => '', + ]; + + /** + * DiyTable constructor. + * @param string $field + * @param string $title + * @param array $value + * @param array $options + */ + public function __construct(string $field, string $title, array $value = [], array $options = []) + { + $this->rule['title'] = $title; + $this->rule['field'] = $field; + $this->rule['options'] = $options; + $this->rule['value'] = $value; + } + + /** + * @param string $info + * @return $this + */ + public function info(string $info) + { + $this->rule['info'] = $info; + return $this; + } + + /** + * 设置列 + * @param string $name + * @param string $key + * @param string $type + * @param array $props + * @return $this + */ + public function column(string $name, string $key, string $type = 'input', array $props = []) + { + $this->rule['options'][] = ['name' => $name, 'key' => $key, 'type' => $type, 'props' => $props]; + return $this; + } + + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $this->before(); + return $this->rule; + } +} diff --git a/crmeb/form/components/Input.php b/crmeb/form/components/Input.php new file mode 100644 index 0000000..7deec8a --- /dev/null +++ b/crmeb/form/components/Input.php @@ -0,0 +1,132 @@ + '', + 'value' => '', + 'type' => '', + 'field' => '', + 'info' => '', + 'disabled' => false, + 'placeholder' => '', + 'suffix' => '', + 'prefix' => '', + 'rows' => 2, + 'copy' => false, + 'copyText' => '', + 'randToken' => 0, + 'maxlength' => null, + ]; + + /** + * Input constructor. + * @param string $field + * @param string $title + * @param null $value + */ + public function __construct(string $field, string $title, $value = null) + { + $this->rule['title'] = $title; + $this->rule['field'] = $field; + $this->rule['value'] = empty($value) ? '' : $value; + } + + /** + * 是否禁用 + * @param bool $disabled + * @return $this + */ + public function disabled(bool $disabled = true) + { + $this->rule['disabled'] = $disabled; + return $this; + } + + /** + * 随机token + * @return $this + */ + public function randToken() + { + $this->rule['randToken'] = 1; + return $this; + } + + /** + * 随机encodingAESKeyGen + * @return $this + */ + public function randAESK() + { + $this->rule['randToken'] = 2; + return $this; + } + + /** + * 复制按钮 + * @param string $copyText + * @return $this + */ + public function copy(string $copyText = '复制') + { + $this->rule['copy'] = true; + $this->rule['copyText'] = $copyText; + return $this; + } + + /** + * @return string[] + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $this->before(); + return $this->rule; + } + + /** + * @param $name + * @param $arguments + * @return $this + */ + public function __call($name, $arguments) + { + if (in_array($name, ['title', 'field', 'disabled', 'copyText'])) { + return $this; + } + $keys = array_keys($this->rule); + if (in_array($name, $keys)) { + $this->rule[$name] = $arguments[0] ?? null; + } + return $this; + } +} diff --git a/crmeb/form/components/InputNumber.php b/crmeb/form/components/InputNumber.php new file mode 100644 index 0000000..330ef50 --- /dev/null +++ b/crmeb/form/components/InputNumber.php @@ -0,0 +1,118 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BaseComponent; +use crmeb\form\BuildInterface; + +/** + * 数字输入框 + * Class InputNumber + * @package crmeb\form\components + */ +class InputNumber extends BaseComponent implements BuildInterface +{ + + /** + * 组件名 + */ + const NAME = 'inputNumber'; + + /** + * 规则 + * @var string[] + */ + protected $rule = [ + 'title' => '', + 'value' => '', + 'type' => '', + 'field' => '', + 'prefix' => '', + 'suffix' => '', + 'info' => '', + 'min' => null, + 'max' => 99999999 + ]; + + /** + * InputNumber constructor. + * @param string $field + * @param string $title + * @param null $value + */ + public function __construct(string $field, string $title, $value = null) + { + $this->rule['title'] = $title; + $this->rule['field'] = $field; + $this->rule['value'] = floatval($value); + } + + /** + * 提示语 + * @param string $info + * @return $this + */ + public function info(string $info) + { + $this->rule['info'] = $info; + return $this; + } + + /** + * 最小值 + * @param int $min + * @return $this + */ + public function min(int $min) + { + $this->rule['min'] = $min; + return $this; + } + + /** + * 最大值 + * @param int $max + * @return $this + */ + public function max(int $max) + { + $this->rule['max'] = $max; + return $this; + } + + /** + * @return array|string[] + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $this->before(); + return $this->rule; + } + + /** + * @param $name + * @param $arguments + * @return $this + */ + public function __call($name, $arguments) + { + if (in_array($name, ['title', 'field'])) { + return $this; + } + $keys = array_keys($this->rule); + if (in_array($name, $keys)) { + $this->rule[$name] = $arguments[0] ?? null; + } + } +} diff --git a/crmeb/form/components/Map.php b/crmeb/form/components/Map.php new file mode 100644 index 0000000..ab95603 --- /dev/null +++ b/crmeb/form/components/Map.php @@ -0,0 +1,70 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BaseComponent; +use crmeb\form\BuildInterface; + +/** + * 地图组件 + * Class Map + * @package crmeb\form\components + */ +class Map extends BaseComponent implements BuildInterface +{ + + const NAME = 'map'; + + /** + * @var string[] + */ + protected $rule = [ + 'title' => '', + 'field' => '', + 'value' => '', + 'info' => '', + ]; + + /** + * Map constructor. + * @param string $field + * @param string $title + * @param string $value + */ + public function __construct(string $field, string $title, string $value = null) + { + $this->rule['field'] = $field; + $this->rule['title'] = $title; + $this->rule['value'] = empty($value) ? '' : $value; + } + + /** + * @param string $info + * @return $this + */ + public function info(string $info) + { + $this->rule['info'] = $info; + return $this; + } + + /** + * @return array|string[] + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $this->before(); + return $this->rule; + } +} diff --git a/crmeb/form/components/Radio.php b/crmeb/form/components/Radio.php new file mode 100644 index 0000000..0e74375 --- /dev/null +++ b/crmeb/form/components/Radio.php @@ -0,0 +1,134 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BaseComponent; +use crmeb\form\BuildInterface; + +/** + * 单选框组件 + * Class Radio + * @package crmeb\form\components + */ +class Radio extends BaseComponent implements BuildInterface +{ + /** + * 组件名 + */ + const NAME = 'radio'; + + /** + * 组件规则 + * @var array + */ + protected $rule = [ + 'title' => '', + 'field' => '', + 'value' => 0, + 'info' => '', + 'vertical' => false, + 'control' => [], + 'options' => [], + ]; + + /** + * Radio constructor. + * @param string $title + * @param string $field + * @param null $value + */ + public function __construct(string $field, string $title, $value = null) + { + $this->rule['title'] = $title; + $this->rule['field'] = $field; + $this->rule['value'] = !is_null($value) ? $value : null; + } + + /** + * 多个组件联动 + * @param array $controls + * @return $this + */ + public function controls(array $controls = []) + { + $this->rule['control'] = $controls; + return $this; + } + + /** + * options数据 ['label'=>'确定','value'=>1] + * @param array $options + * @return $this + */ + public function options(array $options = []) + { + $this->rule['options'] = $options; + return $this; + } + + /** + * 组件联动 + * @param $value + * @param array $components + * @return $this + */ + public function control($value, array $components = []) + { + $this->rule['control'][] = ['value' => $value, 'componentsModel' => $components]; + return $this; + } + + /** + * 设置提示语 + * @param string $info + * @return $this + */ + public function info(string $info) + { + $this->rule['info'] = $info; + return $this; + } + + /** + * 是否垂直展示 + * @param bool $vertical + * @return $this + */ + public function vertical(bool $vertical) + { + $this->rule['vertical'] = $vertical; + return $this; + } + + /** + * 数据转换 + * @return array + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $control = []; + foreach ($this->rule['control'] as $item) { + $data = ['value' => $item['value'], 'componentsModel' => []]; + foreach ($item['componentsModel'] as $value) { + if ($value instanceof BuildInterface) { + $data['componentsModel'][] = $value->toArray(); + } + } + $control[] = $data; + } + $this->rule['control'] = $control; + $this->before(); + return $this->rule; + } +} diff --git a/crmeb/form/components/Select.php b/crmeb/form/components/Select.php new file mode 100644 index 0000000..015bae8 --- /dev/null +++ b/crmeb/form/components/Select.php @@ -0,0 +1,90 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BaseComponent; +use crmeb\form\BuildInterface; + +/** + * 多选组件 + * Class Select + * @package crmeb\form\components + * @method placeholder(string $placeholder) + * @method options(array $options = []) + * @method info(string $info) + */ +class Select extends BaseComponent implements BuildInterface +{ + + /** + * 组件名 + */ + const NAME = 'select'; + + /** + * 规则 + * @var string[] + */ + protected $rule = [ + 'title' => '', + 'value' => '', + 'type' => '', + 'field' => '', + 'info' => '', + 'placeholder' => '', + 'options' => [], + ]; + + /** + * Radio constructor. + * @param string $title + * @param string $field + * @param null $value + */ + public function __construct(string $field, string $title, $value = null) + { + $this->rule['title'] = $title; + $this->rule['field'] = $field; + $this->rule['value'] = !is_null($value) ? $value : null; + } + + /** + * 转换数据 + * @return array|string[] + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $this->before(); + return $this->rule; + } + + /** + * @param $name + * @param $arguments + * @return $this + */ + public function __call($name, $arguments) + { + if (in_array($name, ['title', 'field'])) { + return $this; + } + $keys = array_keys($this->rule); + if (in_array($name, $keys)) { + $this->rule[$name] = $arguments[0] ?? null; + } + return $this; + } + + +} diff --git a/crmeb/form/components/Switchs.php b/crmeb/form/components/Switchs.php new file mode 100644 index 0000000..2f25f46 --- /dev/null +++ b/crmeb/form/components/Switchs.php @@ -0,0 +1,135 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BaseComponent; +use crmeb\form\BuildInterface; + +/** + * 开关组件 + * Class Switchs + * @package crmeb\form\components + */ +class Switchs extends BaseComponent implements BuildInterface +{ + /** + * 组件名 + */ + const NAME = 'switch'; + + /** + * 规则 + * @var array + */ + protected $rule = [ + 'title' => '', + 'field' => '', + 'value' => '', + 'info' => '', + 'control' => [], + 'options' => [], + ]; + + /** + * Switchs constructor. + * @param string $field + * @param string $title + * @param int $value + */ + public function __construct(string $field, string $title, int $value = null) + { + $this->rule['title'] = $title; + $this->rule['field'] = $field; + $this->rule['value'] = !is_null($value) ? intval($value) : null; + } + + /** + * 多组件群添加 + * @param array $controls + * @return $this + */ + public function controls(array $controls = []) + { + $this->rule['control'] = $controls; + return $this; + } + + /** + * 组件联动添加 + * @param $value + * @param array $components + * @return $this + */ + public function control(int $value, array $components = []) + { + $this->rule['control'][] = ['value' => $value, 'componentsModel' => $components]; + return $this; + } + + /** + * 开启值和名称设置 + * @param string $label + * @param int $value + * @return $this + */ + public function trueValue(string $label, int $value) + { + $this->rule['options'][] = ['trueValue' => $value, 'label' => $label]; + return $this; + } + + /** + * 关闭值和名称设置 + * @param string $label + * @param int $value + * @return $this + */ + public function falseValue(string $label, int $value) + { + $this->rule['options'][] = ['falseValue' => $value, 'label' => $label]; + return $this; + } + + /** + * 设置提示信息 + * @param string $info + * @return $this + */ + public function info(string $info) + { + $this->rule['info'] = $info; + return $this; + } + + /** + * 数据转换 + * @return array + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $control = []; + foreach ($this->rule['control'] as $item) { + $data = ['value' => $item['value'], 'componentsModel' => []]; + foreach ($item['componentsModel'] as $value) { + if ($value instanceof BuildInterface) { + $data['componentsModel'][] = $value->toArray(); + } + } + $control[] = $data; + } + $this->rule['control'] = $control; + $this->before(); + return $this->rule; + } +} diff --git a/crmeb/form/components/Tabs.php b/crmeb/form/components/Tabs.php new file mode 100644 index 0000000..f20a972 --- /dev/null +++ b/crmeb/form/components/Tabs.php @@ -0,0 +1,76 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BuildInterface; + +/** + * Tabs 组件 + * Class Tabs + * @package crmeb\form\components + */ +class Tabs implements BuildInterface +{ + + //组件名 + const NAME = 'tabs'; + + /** + * 规则 + * @var array[] + */ + protected $rule = [ + 'options' => [] + ]; + + /** + * Tabs constructor. + * @param array $options + */ + public function __construct(array $options = []) + { + $this->rule['options'] = $options; + } + + /** + * 添加单个选项卡组件群 + * @param string $label + * @param array $components + * @return $this + */ + public function option(string $label, array $components = []) + { + $this->rule['options'][] = ['label' => $label, 'componentsModel' => $components]; + return $this; + } + + /** + * @return array|array[] + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $options = []; + foreach ($this->rule['options'] as $option) { + $data = ['label' => $option['label'], 'componentsModel' => []]; + foreach ($option['componentsModel'] as $item) { + if ($item instanceof BuildInterface) { + $data['componentsModel'][] = $item->toArray(); + } + } + $options[] = $data; + } + $this->rule['options'] = $options; + return $this->rule; + } +} diff --git a/crmeb/form/components/Time.php b/crmeb/form/components/Time.php new file mode 100644 index 0000000..8048d64 --- /dev/null +++ b/crmeb/form/components/Time.php @@ -0,0 +1,85 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + +use crmeb\form\BaseComponent; +use crmeb\form\BuildInterface; + +/** + * 时间组件 + * Class Time + * @package crmeb\form\components + * @method Time info(string $info) 设置info + * @method Time value($value) 设置value + * @method Time type(string $type) 设置type + * @method Time placeholder(string $placeholder) 设置placeholder + */ +class Time extends BaseComponent implements BuildInterface +{ + + const NAME = 'time'; + + /** + * @var string[] + */ + protected $rule = [ + 'title' => '', + 'value' => '', + 'field' => '', + 'info' => '', + 'placeholder' => '', + 'format' => 'HH:mm:ss', + 'type' => 'timerange' + ]; + + /** + * Time constructor. + * @param string $field + * @param string $title + * @param array|null $value + */ + public function __construct(string $field, string $title, array $value = null) + { + $this->rule['field'] = $field; + $this->rule['title'] = $title; + $this->rule['value'] = empty($value) ? '' : $value; + } + + + + /** + * @return array|string[] + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $this->before(); + return $this->rule; + } + + /** + * @param $name + * @param $arguments + * @return $this + */ + public function __call($name, $arguments) + { + if (in_array($name, ['title', 'field', 'disabled', 'copyText'])) { + return $this; + } + $keys = array_keys($this->rule); + if (in_array($name, $keys)) { + $this->rule[$name] = $arguments[0] ?? null; + } + return $this; + } +} diff --git a/crmeb/form/components/UploadFrame.php b/crmeb/form/components/UploadFrame.php new file mode 100644 index 0000000..18550eb --- /dev/null +++ b/crmeb/form/components/UploadFrame.php @@ -0,0 +1,140 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BaseComponent; +use crmeb\form\BuildInterface; + +/** + * 图片选择组件 + * Class UploadFrame + * @package crmeb\form\components + */ +class UploadFrame extends BaseComponent implements BuildInterface +{ + + /** + * 组件名 + */ + const NAME = 'uploadFrame'; + + /** + * 规则 + * @var array + */ + protected $rule = [ + 'upload' => [ + 'url' => '', + 'width' => '960px', + 'height' => '505px', + 'field' => 'att_dir', + 'maxNum' => 1, + ], + 'field' => '', + 'title' => '', + 'value' => '', + 'info' => '', + ]; + + /** + * UploadFrame constructor. + * @param string $field + * @param string $title + * @param null $value + */ + public function __construct(string $field, string $title, $value = null) + { + $this->rule['title'] = $title; + $this->rule['field'] = $field; + $this->rule['value'] = !is_null($value) ? $value : null; + } + + /** + * 设置iframe跳转地址 + * @param string $url + * @return $this + */ + public function url(string $url) + { + $this->rule['upload']['url'] = $url; + return $this; + } + + /** + * 设置iframe宽 + * @param string $width + * @return $this + */ + public function width(string $width = '960px') + { + $this->rule['upload']['width'] = $width; + return $this; + } + + /** + * 设置iframe高 + * @param string $height + * @return $this + */ + public function height(string $height = '505px') + { + $this->rule['upload']['height'] = $height; + return $this; + } + + /** + * 设置提取字段 + * @param string $field + * @return $this + */ + public function field(string $field) + { + $this->rule['upload']['field'] = $field; + return $this; + } + + /** + * 多图单图选择 + * @param int $maxNum + * @return $this + */ + public function maxNum(int $maxNum = 1) + { + $this->rule['upload']['maxNum'] = $maxNum; + return $this; + } + + /** + * 设置提示 + * @param string $info + * @return $this + */ + public function info(string $info) + { + $this->rule['info'] = $info; + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + if ($this->rule['upload']['maxNum'] > 1 && $this->rule['value'] && !is_array($this->rule['value'])) { + $this->rule['value'] = []; + } + $this->rule['name'] = self::NAME; + $this->before(); + return $this->rule; + } +} diff --git a/crmeb/form/components/UploadImage.php b/crmeb/form/components/UploadImage.php new file mode 100644 index 0000000..c9411e3 --- /dev/null +++ b/crmeb/form/components/UploadImage.php @@ -0,0 +1,156 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\components; + + +use crmeb\form\BaseComponent; +use crmeb\form\BuildInterface; + +/** + * 文件上传组件 + * Class UploadImage + * @package crmeb\form\components + */ +class UploadImage extends BaseComponent implements BuildInterface +{ + + // 组件名 + const NAME = 'uploadImage'; + //图片类型 + const IMAGE = 'image'; + //文件类型 + const FILE = 'file'; + //上传支持类型 + const TYPE = [self::IMAGE, self::FILE]; + + /** + * 规则 + * @var array + */ + protected $rule = [ + 'upload' => [ + 'url' => '', + 'size' => 2097152, + 'format' => [], + 'headers' => [], + 'maxNum' => 1, + ], + 'field' => '', + 'type' => '', + 'title' => '', + 'icon' => '', + 'info' => '', + 'value' => '' + ]; + + /** + * UploadImage constructor. + * @param string $field + * @param string $title + * @param null $value + */ + public function __construct(string $field, string $title, $value = null) + { + $this->rule['title'] = $title; + $this->rule['field'] = $field; + $this->rule['value'] = !is_null($value) ? $value : null; + } + + /** + * 上传地址 + * @param string $url + * @return $this + */ + public function url(string $url) + { + $this->rule['upload']['url'] = $url; + return $this; + } + + + /** + * 上传类型 + * @param string $type + * @return $this + */ + public function type(string $type) + { + $this->rule['type'] = in_array($type, self::TYPE) ? $type : ''; + return $this; + } + + /** + * 上传展示icon + * @param string $icon + * @return $this + */ + public function icon(string $icon) + { + $this->rule['icon'] = $icon; + return $this; + } + + + /** + * 上传文件headers + * @param array $headers + * @return $this + */ + public function headers(array $headers = []) + { + $this->rule['upload']['headers'] = (object)$headers; + return $this; + } + + /** + * 上传文件大小 + * @param string $size + * @return $this + */ + public function size(string $size) + { + $this->rule['upload']['size'] = $size; + return $this; + } + + /** + * 上传文件类型 + * @param array $format + * @return $this + */ + public function format(array $format = []) + { + $this->rule['upload']['format'] = $format; + return $this; + } + + /** + * 组件提示 + * @param string $info + * @return $this + */ + public function info(string $info) + { + $this->rule['info'] = $info; + return $this; + } + + /** + * @return array + */ + public function toArray(): array + { + $this->rule['name'] = self::NAME; + $this->before(); + return $this->rule; + } +} diff --git a/crmeb/form/validate/BaseRules.php b/crmeb/form/validate/BaseRules.php new file mode 100644 index 0000000..2929d77 --- /dev/null +++ b/crmeb/form/validate/BaseRules.php @@ -0,0 +1,72 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\validate; + + +use crmeb\form\CommonRule; + +/** + * Class BaseRules + * @package crmeb\form\validate + * @method CommonRule required() 是否必填 + * @method CommonRule message(string $message) 设置错误提示 + * @method CommonRule enum(array $enum) 枚举 + * @method CommonRule pattern(string $pattern) 正则表达式 + * @method CommonRule field($field, array $rule = []) 验证规则 + */ +abstract class BaseRules +{ + /** + * 是否初始化 + * @var bool + */ + protected static $init = false; + + /** + * @var CommonRule + */ + protected static $rule; + + /** + * @return mixed + */ + public static function getType(): string + { + return ''; + } + + /** + * 初始化 + */ + public static function init() + { + if (!self::$init) { + self::$rule = new CommonRule(); + self::$rule->type(static::getType()); + self::$init = true; + } + } + + /** + * @param $name + * @param $arguments + * @return mixed + */ + public static function __callStatic($name, $arguments) + { + self::init(); + if (method_exists(self::$rule, $name)) { + return self::$rule->{$name}(...$arguments); + } + throw new FormValidate(__CLASS__ . ' Method does not exist' . $name . '()'); + } +} diff --git a/crmeb/form/validate/StrRules.php b/crmeb/form/validate/StrRules.php new file mode 100644 index 0000000..a624cd6 --- /dev/null +++ b/crmeb/form/validate/StrRules.php @@ -0,0 +1,37 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\form\validate; + +use crmeb\form\FormValidate; + +/** + * Class StrRules + * @package crmeb\form\validate + */ +class StrRules extends BaseRules +{ + + /** + * 手机号正则 + */ + const PHONE_NUMBER = '/(^(\d{3,4}-)?[0-9]{7,8}$)|(^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])[0-9]{8}$)/'; + + /** + * 设置类型 + * @return string + */ + public static function getType(): string + { + return 'string'; + } + +} diff --git a/crmeb/services/FormBuilder.php b/crmeb/services/FormBuilder.php new file mode 100644 index 0000000..621c0d0 --- /dev/null +++ b/crmeb/services/FormBuilder.php @@ -0,0 +1,49 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services; + +use FormBuilder\Factory\Iview as Form; +use FormBuilder\UI\Iview\Components\InputNumber; + +/** + * Form Builder + * Class FormBuilder + * @package crmeb\services + */ +class FormBuilder extends Form +{ + + public static function setOptions($call) + { + if (is_array($call)) { + return $call; + } else { + return $call(); + } + + } + + public static function number($field,$title,$value = null){ + $number = new InputNumber($field, $title, $value); + if (!$number->getProp('max')) { + if ($field == 'sort') { + $number->max(99999); + if (!$number->getProp('min')) { + $number->min(0); + } + } else { + $number->max(9999999999); + } + } + return $number; + } +} diff --git a/crmeb/services/SystemConfigService.php b/crmeb/services/SystemConfigService.php new file mode 100644 index 0000000..3e748e1 --- /dev/null +++ b/crmeb/services/SystemConfigService.php @@ -0,0 +1,146 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services; + +use app\services\system\config\SystemConfigServices; +use crmeb\utils\Arr; +use think\facade\Config; +use think\facade\Db; + +/** 获取系统配置服务类 + * Class SystemConfigService + * @package service + */ +class SystemConfigService +{ + /** + * 缓存前缀字符 + */ + const CACHE_SYSTEM = 'system_config'; + /** + * 过期时间 + */ + const EXPIRE_TIME = 30 * 24 * 3600; + + /** + * @var int + */ + protected static $relationId = 0; + + /** + * @var int + */ + protected static $type = 0; + + /** + * 获取配置缓存前缀 + * @return string + */ + public static function getTag() + { + return Config::get('cache.stores.redis.tag_prefix') . 'cahce_' . self::CACHE_SYSTEM; + } + + /** + * @param int $storeId + */ + public static function setStore(int $storeId) + { + self::$relationId = $storeId; + self::$type = 1; + } + + /** + * @param int $supplier + */ + public static function setSupplier(int $supplier) + { + self::$relationId = $supplier; + self::$type = 2; + } + + /** + * 获取单个配置效率更高 + * @param $key + * @param string $default + * @param bool $isCaChe 是否获取缓存配置 + * @return bool|mixed|string + */ + public static function get(string $key, $default = '', bool $isCaChe = false) + { + $cacheName = self::CACHE_SYSTEM . ':' . $key . (self::$type ? '_' . self::$type : '') . (self::$relationId ? '_' . self::$relationId : ''); + $type = self::$type; + $relationId = self::$relationId; + $callable = function () use ($key, $type, $relationId) { + event('get.config'); + /** @var SystemConfigServices $service */ + $service = app()->make(SystemConfigServices::class); + return $service->getConfigValue($key, $type, $relationId); + }; + + try { + if ($isCaChe) { + return $callable(); + } + $value = CacheService::redisHandler(self::getTag())->remember($cacheName, $callable, self::EXPIRE_TIME); + self::$relationId = 0; + self::$type = 0; + return $value; + } catch (\Throwable $e) { + return $default; + } + } + + /** + * 获取多个配置 + * @param array $keys 示例 [['appid','1'],'appkey'] + * @param bool $isCaChe 是否获取缓存配置 + * @return array + */ + public static function more(array $keys, bool $isCaChe = false) + { + $cacheName = self::CACHE_SYSTEM . ':' . md5(implode(',', $keys) . (self::$type ? '_' . self::$type : '') . (self::$relationId ? '_' . self::$relationId : '')); + $type = self::$type; + $relationId = self::$relationId; + $callable = function () use ($keys, $type, $relationId) { + /** @var SystemConfigServices $service */ + $service = app()->make(SystemConfigServices::class); + return Arr::getDefaultValue($keys, $service->getConfigAll($keys, $type, $relationId)); + }; + try { + if ($isCaChe) + return $callable(); + + $value = CacheService::redisHandler(self::getTag())->remember($cacheName, $callable, self::EXPIRE_TIME); + self::$relationId = 0; + self::$type = 0; + return $value; + } catch (\Throwable $e) { + return Arr::getDefaultValue($keys); + } + } + + /** + * 清空配置缓存 + * @return bool|void + */ + public static function clear() + { + try { + return CacheService::redisHandler(self::getTag())->clear(); + } catch (\Throwable $e) { + \think\facade\Log::error('清空配置缓存失败:原因:' . $e->getMessage()); + return false; + } + } + +} diff --git a/crmeb/services/UtilService.php b/crmeb/services/UtilService.php new file mode 100644 index 0000000..df0133c --- /dev/null +++ b/crmeb/services/UtilService.php @@ -0,0 +1,124 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services; + +use think\facade\Config; +use Endroid\QrCode\Color\Color; +use Endroid\QrCode\Encoding\Encoding; +use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow; +use Endroid\QrCode\QrCode; +use Endroid\QrCode\Label\Label; +use Endroid\QrCode\Logo\Logo; +use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin; +use Endroid\QrCode\Writer\PngWriter; + +/** + * Class UtilService + * @package crmeb\services + */ +class UtilService +{ + /** + * 获取小程序二维码是否生成 + * @param $url + * @return array + */ + public static function remoteImage($url) + { + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + $result = curl_exec($curl); + $result = json_decode($result, true); + if (is_array($result)) return ['status' => false, 'msg' => $result['errcode'] . '---' . $result['errmsg']]; + return ['status' => true]; + } + + /** + * 修改 https 和 http 移动到common + * @param $url $url 域名 + * @param int $type 0 返回https 1 返回 http + * @return string + */ + public static function setHttpType($url, $type = 0) + { + $domainTop = substr($url, 0, 5); + if ($type) { + if ($domainTop == 'https') $url = 'http' . substr($url, 5, strlen($url)); + } else { + if ($domainTop != 'https') $url = 'https:' . substr($url, 5, strlen($url)); + } + return $url; + } + + + /** + * 获取二维码 + * @param $url + * @param $name + * @return array|bool|string + */ + public static function getQRCodePath($url, $name) + { + if (!strlen(trim($url)) || !strlen(trim($name))) return false; + try { + $uploadType = sys_config('upload_type'); + // 没有选择默认使用本地上传 + if (!$uploadType) $uploadType = 1; + $uploadType = (int)$uploadType; + $siteUrl = sys_config('site_url'); + if (!$siteUrl) return '请前往后台设置->系统设置->网站域名 填写您的域名格式为:http://域名'; + $info = []; + $outfiles = Config::get('qrcode.cache_dir'); + $root_outfile = root_path('public/' . $outfiles); + if (!is_dir($root_outfile)) + mkdir($root_outfile, 0777, true); + + // Create QR code + $writer = new PngWriter(); + $qrCode = QrCode::create($url) + ->setEncoding(new Encoding('UTF-8')) + ->setErrorCorrectionLevel(new ErrorCorrectionLevelLow()) + ->setSize(300) + ->setMargin(10) + ->setRoundBlockSizeMode(new RoundBlockSizeModeMargin()) + ->setForegroundColor(new Color(0, 0, 0)) + ->setBackgroundColor(new Color(255, 255, 255)); + $writer->write($qrCode)->saveToFile($root_outfile . $name); + + if ($uploadType === 1) { + $info["code"] = 200; + $info["name"] = $name; + $info["dir"] = '/' . $outfiles . '/' . $name; + $info["time"] = time(); + $info['size'] = 0; + $info['type'] = 'image/png'; + $info["image_type"] = 1; + $info['thumb_path'] = '/' . $outfiles . '/' . $name; + return $info; + } else { + $upload = UploadService::init($uploadType); + $content = file_get_contents($root_outfile . $name); + + $res = $upload->to($outfiles)->validate()->stream($content, $name); + if ($res === false) { + return $upload->getError(); + } + $info = $upload->getUploadInfo(); + $info['image_type'] = $uploadType; + return $info; + } + } catch (\Exception $e) { + return $e->getMessage(); + } + } +} diff --git a/crmeb/services/erp/AccessToken.php b/crmeb/services/erp/AccessToken.php new file mode 100644 index 0000000..c6d54c0 --- /dev/null +++ b/crmeb/services/erp/AccessToken.php @@ -0,0 +1,293 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\erp; + +use crmeb\services\HttpService; +use crmeb\services\CacheService; +use think\facade\Config; +use think\facade\Log; +use think\helper\Str; +use Psr\SimpleCache\CacheInterface; + +/** + * Class AccessTokenServeService + * @package crmeb\services + */ +class AccessToken extends HttpService +{ + /** + * 配置 + * @var string + */ + protected $account; + + /** + * @var string + */ + protected $secret; + + /** + * @var string + */ + protected $apiUrl; + + /** + * 授权登录账号 + * @var string + */ + protected $authAccount; + + /** + * 授权登录密码 + * @var string + */ + protected $authPassword; + + /** + * @var CacheInterface + */ + protected $cache; + + /** + * @var string + */ + protected $accessToken; + + /** + * 驱动类型 + * @var string + */ + protected $name; + + /** + * 配置文件名 + * @var string + */ + protected $configFile; + + /** + * 缓存token + * @var string + */ + protected $cacheTokenPrefix = "_crmeb_erp"; + + /** + * 刷新token + * @var string + */ + protected $cacheRefreshTokenPrefix = "_crmeb_erp_re"; + + /** + * AccessTokenServeService constructor. + * AccessToken constructor. + * @param string $name + * @param string $configFile + * @param array $config + * @param CacheInterface|null $cache + */ + public function __construct(string $name, string $configFile, array $config, ?CacheInterface $cache = null) + { + if (!$cache) { + /** @var CacheService $cache */ + $cache = app()->make(CacheService::class); + } + $this->account = isset($config["app_key"]) ? $config["app_key"] : config($configFile . '.stores.' . $name . '.app_key', ''); + $this->secret = isset($config["secret"]) ? $config["secret"] : config($configFile . '.stores.' . $name . '.secret', ''); + $this->authAccount = $config['login_account'] ?? config($configFile . '.stores.' . $name . '.login_account', ''); + $this->authPassword = $config['login_password'] ?? config($configFile . '.stores.' . $name . '.login_password', ''); + $this->cache = $cache; + $this->name = $name; + $this->configFile = $configFile; + $this->apiUrl = Config::get($configFile . '.stores.' . $name . '.url', ''); + $this->apiUrl = 'https://openapi.jushuitan.com'; + } + + /** + * 获取配置 + * @return array + */ + public function getConfig(): array + { + return [ + 'account' => $this->account, + 'secret' => $this->secret + ]; + } + + /** + * 获取请求链接 + * @param string $url + * @return string + */ + public function getApiUrl(string $url = ''): string + { + return $url ? $this->apiUrl . $url : $this->apiUrl; + } + + /** + * 获取appKey + * @return string + */ + public function getAccount(): string + { + return $this->account; + } + + /** + * @return mixed|string + */ + public function getAuthAccount() + { + return $this->authAccount; + } + + /** + * @return mixed|string + */ + public function getAuthPassword() + { + return $this->authPassword; + } + + /** + * 获取appSecret + * @return string + */ + public function getSecret(): string + { + return $this->secret; + } + + /** + * 获取token + * @return string + * @throws \Exception + */ + public function getAccessToken(): ?string + { + if (isset($this->accessToken)) { + return $this->accessToken; + } + + /** + * @see getJushuitanAccessToken + */ + $action = 'get' . Str::studly($this->name) . 'AccessToken'; + if (method_exists($this, $action)) { + return $this->{$action}(); + } else { + throw new \RuntimeException(__CLASS__ . '->' . $action . '(),Method not worn in'); + } + } + + /** + * 获取聚水潭token + * @return mixed|null|string + * @throws \Exception + */ + protected function getJushuitanAccessToken(): ?string + { + //读缓存 + $cacheKey = md5($this->account . '_' . $this->secret . $this->cacheTokenPrefix); + $this->accessToken = $this->cache->get($cacheKey); + + //需要前端异步授权 + if (empty($this->accessToken)) { + throw new \RuntimeException("请跳转授权", 610); + } + + return $this->accessToken; + } + + /** + * 设置AccessToken缓存 + * @param string $accessToken + * @param int $expiresIn + * @return bool + */ + public function setAccessToken(string $accessToken, int $expiresIn): bool + { + if (empty($accessToken) || !is_numeric($expiresIn) || $expiresIn <= 0) { + return false; + } + //写缓存 + $cacheKey = md5($this->account . '_' . $this->secret . $this->cacheTokenPrefix); + $this->cache->redisHandler()->tag('erp_shop')->set($cacheKey, $accessToken, $expiresIn); + + $this->accessToken = $accessToken; + + return true; + } + + /** + * 设置AccessToken提前过期缓存 + * @param string $accessToken + * @param int $expiresIn + * @return bool + */ + public function setTokenExpire(string $accessToken, int $expiresIn): bool + { + if (empty($accessToken) || !is_numeric($expiresIn) || $expiresIn <= 0) { + return false; + } + //写缓存 + $cacheKey = md5($this->account . '_' . $this->secret . '_epr_expire'); + $this->cache->redisHandler()->tag('erp_shop')->set($cacheKey, $accessToken, $expiresIn); + + $this->accessToken = $accessToken; + + return true; + } + + /** + * 获取提前过期缓存 + * @return bool|mixed|null + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getTokenExpire() + { + $cacheKey = md5($this->account . '_' . $this->secret . '_epr_expire'); + return $this->cache->get($cacheKey); + } + + /** + * 设置refreshToken缓存 + * @param string refreshToken + * @return bool + */ + public function setRefreshToken(string $refreshToken): bool + { + if (empty($refreshToken)) { + return false; + } + //写缓存 + $cacheKey = md5($this->account . '_' . $this->secret . $this->cacheRefreshTokenPrefix); + $this->cache->redisHandler()->tag('erp_shop')->set($cacheKey, $refreshToken); + + return true; + } + + /** + * 获取refreshToken缓存 + * @param string refreshToken + * @return string + */ + public function getRefreshToken(): string + { + //读缓存 + $cacheKey = md5($this->account . '_' . $this->secret . $this->cacheRefreshTokenPrefix); + + return $this->cache->get($cacheKey); + } + + +} diff --git a/crmeb/services/erp/Erp.php b/crmeb/services/erp/Erp.php new file mode 100644 index 0000000..85148fb --- /dev/null +++ b/crmeb/services/erp/Erp.php @@ -0,0 +1,75 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\erp; + +use crmeb\basic\BaseManager; +use crmeb\services\erp\storage\Jushuitan; +use think\Container; +use think\facade\Config; + + +/** + * Class Erp + * @package crmeb\services\erp + * @mixin Jushuitan + */ +class Erp extends BaseManager +{ + + /** + * 空间名 + * @var string + */ + protected $namespace = '\\crmeb\\services\\erp\\storage\\'; + + protected $type = [ + 'nothing', + 'jushuitan', + ]; + + /** + * 默认驱动 + * @return mixed + */ + protected function getDefaultDriver() + { + $this->config = [ + 'app_key' => sys_config('jst_appkey'), + 'secret' => sys_config('jst_appsecret'), + 'login_account' => sys_config('jst_login_account'), + 'login_password' => sys_config('jst_login_password'), + ]; + return $this->type[sys_config('erp_type')]; + } + + /** + * 获取类的实例 + * @param $class + * @return mixed|void + */ + protected function invokeClass($class) + { + if (!class_exists($class)) { + throw new \RuntimeException('class not exists: ' . $class); + } + $this->getConfigFile(); + + if (!$this->config) { + $this->config = Config::get($this->configFile . '.stores.' . $this->name, []); + } + $handleAccessToken = new AccessToken($this->name, $this->configFile, $this->config); + $handle = Container::getInstance()->invokeClass($class, [$this->name, $handleAccessToken, $this->configFile]); + $this->config = []; + + return $handle; + } +} diff --git a/crmeb/services/erp/storage/Jushuitan.php b/crmeb/services/erp/storage/Jushuitan.php new file mode 100644 index 0000000..de6f8b7 --- /dev/null +++ b/crmeb/services/erp/storage/Jushuitan.php @@ -0,0 +1,460 @@ + +// +---------------------------------------------------------------------- +namespace crmeb\services\erp\storage; + +use crmeb\basic\BaseErp; +use crmeb\exceptions\AdminException; +use crmeb\exceptions\ErpException; +use crmeb\services\erp\storage\jushuitan\Comment; +use crmeb\services\erp\storage\jushuitan\Order; +use crmeb\services\erp\storage\jushuitan\Product; +use crmeb\services\erp\storage\jushuitan\Stock; +use EasyWeChat\Kernel\Support\Str; +use think\Collection; +use think\facade\Cache; +use think\Response; + +/** + * Class Jushuitan + * @package crmeb\services\erp\storage + */ +class Jushuitan extends BaseErp +{ + + //==================商家授权================== + + /** + * 获取授权参数 + * @param string $state 透传数据 + * @return array + */ + public function getAuthParams($state = ""): array + { + $params = []; + + //开发者应用Key + $params["app_key"] = $this->accessToken->getAccount(); + + //当前请求的时间戳【单位是秒】 + $params["timestamp"] = time(); + + //透传数据 非必填 + $params["state"] = $state; + + //交互数据的编码【utf-8】目前只能传utf-8,不能不传! + $params["charset"] = "utf-8"; + + //签名 + $params["sign"] = $this->sign($params); + + //授权跳转地址 + $params["url"] = "https://openweb.jushuitan.com/auth"; + + return $params; + } + + /** + * 获取AccessToken 用于验证授权回调是否成功 + * @return string + * @throws \Exception + */ + public function getAccessToken(): string + { + return $this->accessToken->getAccessToken(); + } + + /** + * 设置AccessToken + * @param $at + * @return string + */ + public function setAccessToken($at): string + { + return $this->accessToken->setAccessToken($at, 999999); + } + + /** + * 平台授权回调 + * @return Response + */ + public function authCallback(): Response + { + $params = request()->get(); + //验证必要参数 返回失败 + if (!isset($params["app_key"]) || !isset($params["code"]) || !isset($params["sign"])) { + return response(["code" => 504]); + } + $appKey = $params["app_key"]; + $code = $params["code"]; //授权码,有效期为15分钟 + $sign = $params["sign"]; + $state = isset($params["state"]) ? $params["state"] : ""; //透传数据 + + //appKey是否匹配 不匹配返回成功-抛弃消息 + if ($appKey !== $this->accessToken->getAccount()) { + return response(["code" => 0, "msg" => "appKey不匹配"]); + } + + //sign验证失败 返回失败 + if ($sign !== $this->sign($params)) { + return response(["code" => 505, "msg" => "签名错误"]); + } + + //code换access_token + $request = $this->code2accessToken($code); + + //缓存token + $this->accessToken->setAccessToken($request["access_token"], $request["expires_in"]); + //token提前过期时间 + $this->accessToken->setTokenExpire($request["access_token"], $request["expires_in"] - (48 * 60 * 60)); + //缓存刷新token + $this->accessToken->setRefreshToken($request["refresh_token"]); + + return response(["code" => 0]); + } + + /** + * @return bool|mixed|null + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getTokenExpire() + { + return $this->accessToken->getTokenExpire(); + } + + /** + * 授权临时code换access_token + * @param $code + * @return array + */ + public function code2accessToken($code): array + { + $url = $this->accessToken->getApiUrl("/openWeb/auth/accessToken"); + + //请求参数 + $params = []; + + //开发者应用Key + $params["app_key"] = $this->accessToken->getAccount(); + + //当前请求的时间戳【单位是秒】 + $params["timestamp"] = time(); + + //固定值:authorization_code + $params["grant_type"] = "authorization_code"; + + //交互数据的编码【utf-8】目前只能传utf-8,不能不传! + $params["charset"] = "utf-8"; + + //授权码 + $params["code"] = $code; + + //签名 + $params["sign"] = $this->sign($params); + + try { + $request = $this->accessToken::postRequest($url, $params); + } catch (\Exception $e) { + throw new AdminException($e->getMessage()); + } + //处理平台响应异常 + $this->checkRequestError($request); + + return $request["data"]; + } + + /** + * @param $content + * @return Collection + */ + protected function json($content) + { + if (false === $content) { + return collect(); + } + $data = json_decode($content, true); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new ErpException(sprintf('Failed to parse JSON: %s', json_last_error_msg())); + } + + return collect($data); + } + + /** + * 通过接口方式授权登录聚水潭 + * @param string $account + * @param string $password + * @return bool + */ + public function authLogin(string $account = null, string $password = null) + { + if (Cache::has('erp_login_count') && Cache::get('erp_login_count') > 10) { + return false; + } + + $authParams = $this->getAuthParams(); + + $loginInfo = $this->accessToken->postRequest('https://api.jushuitan.com/erp/webapi/UserApi/WebLogin/Passport', json_encode([ + 'ipAddress' => '', + 'uid' => '', + 'data' => [ + 'account' => $account ?: $this->accessToken->getAuthAccount(), + 'j_d_3' => '', + 'password' => $password ?: $this->accessToken->getAuthPassword(), + 'v_d_144' => '' + ], + ]), ['Content-Type:application/json; charset=utf-8']); + + $loginInfo = $this->json($loginInfo); + + if ($loginInfo['code'] === null || $loginInfo['code'] !== 0) { + + if ($loginInfo['code'] === 10001) { + $erpLoginCount = 0; + if (Cache::has('erp_login_count')) { + $erpLoginCount = Cache::get('erp_login_count', 0); + $erpLoginCount++; + } + Cache::set('erp_login_count', $erpLoginCount, 60); + } + throw new ErpException($loginInfo['msg'] ?? '登录失败'); + } + + $cookie = $loginInfo['cookie']; + $cookieData = []; + foreach ($cookie as $k => $v) { + $cookieData[] = $k . '=' . $v; + } + + $res = $this->accessToken->postRequest('https://api.jushuitan.com/openWeb/auth/oauthAction', json_encode([ + 'uid' => '', + 'data' => [ + 'app_key' => $authParams['app_key'], + 'charset' => $authParams['charset'], + 'sign' => $authParams['sign'], + 'state' => '', + 'timestamp' => $authParams['timestamp'], + ], + ]), [ + 'Content-Type:application/json', + 'Cookie:' . implode(';', $cookieData), + ]); + + $res = $this->json($res); + + if (!$res) { + throw new ErpException('请求失败'); + } + if ($res['code'] != 0) { + throw new ErpException($res['msg'] ?? '授权失败'); + } + + return true; + } + + /** + * 刷新access_token + * @return bool + */ + public function refreshToken(): bool + { + $refreshToken = $this->accessToken->getRefreshToken(); + if (empty($refreshToken)) { + throw new AdminException("请跳转授权手动授权", 610); + } + + $url = $this->accessToken->getApiUrl("/openWeb/auth/refreshToken"); + + //请求参数 + $params = []; + + //开发者应用Key + $params["app_key"] = $this->accessToken->getAccount(); + + //当前请求的时间戳【单位是秒】 + $params["timestamp"] = time(); + + //固定值:refresh_token + $params["grant_type"] = "refresh_token"; + + //交互数据的编码【utf-8】目前只能传utf-8,不能不传! + $params["charset"] = "utf-8"; + + //更新令牌 + $params["refresh_token"] = $refreshToken; + + //固定值:all + $params["scope"] = "all"; + + //签名 + $params["sign"] = $this->sign($params); + + try { + $request = $this->accessToken::postRequest($url, $params); + } catch (\Exception $e) { + throw new AdminException($e->getMessage()); + } + + //处理平台响应异常 + $this->checkRequestError($request); + + //缓存token + $this->accessToken->setAccessToken($request["access_token"], $request["expires_in"]); + //token提前过期时间 + $this->accessToken->setTokenExpire($request["access_token"], $request["expires_in"] - (48 * 60 * 60)); + //缓存刷新token + $this->accessToken->setRefreshToken($request["refresh_token"]); + + return true; + } + + + //==================内部方法================== + + /** + * 发送post请求并处理异常 + * @param $url + * @param $params + * @return mixed + */ + public function postRequest($url, $params) + { + //请求平台接口 + $request = $this->accessToken->postRequest($url, $params); + $this->checkRequestError($request); + + return $request; + } + + /** + * 检测平台响应异常 并将响应转换为数组 + * @param $request + */ + protected function checkRequestError(&$request) + { + if ($request === false || empty($request)) { + throw new AdminException('平台请求失败,请稍后重试'); + } + $request = is_string($request) ? json_decode($request, true) : $request; + if (empty($request) || !isset($request['code'])) { + throw new AdminException('平台请求失败,请稍后重试!'); + } + if (intval($request['code']) === 100) { + throw new AdminException("请重新授权", 610); + } + if ($request['code'] != 0) { + throw new AdminException(isset($request['msg']) ? '平台错误:' . $request['msg'] : '平台错误:发生异常,请稍后重试'); + } + } + + /** + * 拼装请求参数 + * @param array $biz + * @return array + * @throws \Exception + * @throws \Exception + */ + public function getParams(array $biz = []): array + { + //请求参数 + $params = []; + + $accessToken = null; + try { + $accessToken = $this->accessToken->getAccessToken(); + } catch (\Throwable $e) { + } + + //刷新token + if (!$accessToken) { + $this->refreshToken(); + $accessToken = $this->accessToken->getAccessToken(); + } + + if (!$accessToken) { + throw new ErpException('缺少access_token,请手动登录聚水潭开放平台进行授权登录'); + } + + //商户授权token值 + $params["access_token"] = $accessToken; + + //开发者应用Key + $params["app_key"] = $this->accessToken->getAccount(); + + //当前请求的时间戳【单位是秒】 + $params["timestamp"] = time(); + + //接口版本,当前版本为【2】,目前只能传2,不能不传! + $params["version"] = "2"; + + //交互数据的编码【utf-8】目前只能传utf-8,不能不传! + $params["charset"] = "utf-8"; + + //业务请求参数,格式为jsonString + if (empty($biz)) { + $biz = new \ArrayObject(); + } + $params["biz"] = json_encode($biz, JSON_UNESCAPED_UNICODE); + + //签名 + $params["sign"] = $this->sign($params); + + return $params; + } + + /** + * 计算签名 + * @param array $params + * @return string + */ + protected function sign(array $params): string + { + if (empty($params)) { + return ""; + } + + //1.将请求参数中除 sign 外的多个键值对,根据键按照字典序排序 + ksort($params); + + //按照 "key1value1key2value2..." 的格式拼成一个字符串。 + $str = ""; + foreach ($params as $k => $v) { + if ($k == null || $k == "" || $k == "sign" || $v == "") { + continue; + } + if (is_array($v) || is_object($v)) { + $v = json_encode($v, JSON_UNESCAPED_UNICODE); + } + $str .= $k . $v; + } + + //2.将 app_secret 拼接在 1 中排序后的字符串前面得到待签名字符串 + $str = $this->accessToken->getSecret() . $str; + + //3.使用 MD5 算法加密待加密字符串并转为小写 + return bin2hex(md5($str, true)); + } + + /** + * @param string $type + * @return Stock|Order|Product|Comment + */ + public function serviceDriver(string $type = '') + { + $namespace = '\\crmeb\\services\\erp\\storage\\jushuitan\\'; + $class = strpos($type, '\\') ? $type : $namespace . Str::studly($type); + if (!class_exists($class)) { + throw new \RuntimeException('class not exists: ' . $class); + } + + return \think\Container::getInstance()->invokeClass($class, [$this->accessToken, $this]); + } +} diff --git a/crmeb/services/erp/storage/jushuitan/Comment.php b/crmeb/services/erp/storage/jushuitan/Comment.php new file mode 100644 index 0000000..691d9c0 --- /dev/null +++ b/crmeb/services/erp/storage/jushuitan/Comment.php @@ -0,0 +1,51 @@ +accessToken = $accessToken; + $this->jushuitan = $jushuitan; + } + + /** + * 获取商铺列表 + * @param int $page + * @param int $limit + * @return mixed + * @throws \Exception + */ + public function getShopList(int $page = 1, int $limit = 10) + { + $api = $this->accessToken->getApiUrl('/open/shops/query'); + $biz['items'] = ['page_index' => $page, 'page_size' => $limit]; + $params = $this->jushuitan->getParams($biz); + return $this->jushuitan->postRequest($api, $params); + } +} diff --git a/crmeb/services/erp/storage/jushuitan/Order.php b/crmeb/services/erp/storage/jushuitan/Order.php new file mode 100644 index 0000000..5d5e4b7 --- /dev/null +++ b/crmeb/services/erp/storage/jushuitan/Order.php @@ -0,0 +1,151 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\erp\storage\jushuitan; + +use crmeb\exceptions\AdminException; +use crmeb\services\erp\AccessToken; +use crmeb\services\erp\storage\Jushuitan; +use Exception; + +class Order +{ + /** + * token句柄 + * @var AccessToken + */ + protected $accessToken; + + /*** @var Jushuitan */ + protected $jushuitan; + + /** + * @param Jushuitan $jushuitan + */ + public function __construct(AccessToken $accessToken, Jushuitan $jushuitan) + { + $this->accessToken = $accessToken; + $this->jushuitan = $jushuitan; + } + + /** + * 订单上传(推荐) + * @param array $data + * @return array + * @throws Exception + */ + public function ordersUpload(array $data): array + { + $url = $this->accessToken->getApiUrl("/open/jushuitan/orders/upload"); + + //拼装请求参数 + $params = $this->jushuitan->getParams($data); + + //请求平台接口 + $request = $this->jushuitan->postRequest($url, $params); + return $request["data"]; + } + + /** + * 订单查询 + * @param array $data + * @return array + * @throws Exception + */ + public function ordersSingleQuery(array $data): array + { + $url = $this->accessToken->getApiUrl("/open/orders/single/query"); + + //业务参数 + $biz = []; + + if (isset($data["page_index"])) { + //int 第几页,从第一页开始,默认1 + $biz["page_index"] = intval($data["page_index"]); + } + if (isset($data["page_size"])) { + //int 每页多少条;默认30条,最大50条 + $biz["page_size"] = intval($data["page_size"]); + } + if (isset($data["modified_begin"])) { + if (empty($data["modified_end"])) { + throw new AdminException("起始和结束时间必须同时存在"); + } + //string日志起始时间,起始时间和 结束时间必须同时存在,时间间隔不能超过七天 + $biz["modified_begin"] = strval($data["modified_begin"]); + $biz["modified_end"] = strval($data["modified_end"]); + } + ///list 线上单号号,最大限制20条 + if (isset($data["so_ids"])) { + $biz["so_ids"] = $data["so_ids"]; + } elseif (empty($biz["modified_begin"]) || empty($biz["modified_end"])) { + //线上单号与修改时间不能同时为空 + throw new AdminException("线上单号,与修改时间不能同时为空"); + } + //店铺编号 + if (!empty($data["shop_id"])) { + $biz["shop_id"] = $data["shop_id"]; + } + //shop_id为0且is_offline_shop为true查询线下店铺单据 非必填 bool + if (isset($data["is_offline_shop"]) && !is_null($data["is_offline_shop"])) { + $biz["is_offline_shop"] = $data["is_offline_shop"]; + } + + if (!empty($data["status"])) { + $biz["status"] = $data["status"]; + } + + //拼装请求参数 + $params = $this->jushuitan->getParams($biz); + + //请求平台接口 + $request = $this->jushuitan->postRequest($url, $params); + return $request["data"]; + } + + /** + * 订单取消-按内部单号取消 + * @param array $data + * @return bool + * @throws Exception + */ + public function orderByOIdCancel(array $data): bool + { + $url = $this->accessToken->getApiUrl("/open/jushuitan/orderbyoid/cancel"); + + //拼装请求参数 + $params = $this->jushuitan->getParams($data); + + //请求平台接口 + $this->jushuitan->postRequest($url, $params); + + return true; + } + + /** + * 实际收货上传 + * @param array $list + * @return array + * @throws Exception + */ + public function afterSaleUpload(array $list): array + { + $url = $this->accessToken->getApiUrl("/open/aftersale/upload"); + + //拼装请求参数 + $params = $this->jushuitan->getParams($list); + + //请求平台接口 + $request = $this->jushuitan->postRequest($url, $params); + return $request["data"]; + } + +} \ No newline at end of file diff --git a/crmeb/services/erp/storage/jushuitan/Product.php b/crmeb/services/erp/storage/jushuitan/Product.php new file mode 100644 index 0000000..378aaf7 --- /dev/null +++ b/crmeb/services/erp/storage/jushuitan/Product.php @@ -0,0 +1,127 @@ +accessToken = $accessToken; + $this->jushuitan = $jushuitan; + } + + /** + * 上传商品 + * @param $data + * @return mixed + * @throws \Exception + */ + public function updateProduct($data) + { + $url = $this->accessToken->getApiUrl("/open/jushuitan/itemsku/upload"); + + //业务参数 + $biz['items'] = $data; + + //拼装请求参数 + $params = $this->getParams($biz); + + //请求平台接口 + $request = $this->postRequest($url, $params); + + if ($request['code'] == 0) { + return true; + } else { + return false; + } + } + + /** + * 上传店铺商品 + * @param $data + * @return mixed + * @throws \Exception + */ + public function updateShopProduct($data) + { + $url = $this->accessToken->getApiUrl("/open/jushuitan/skumap/upload"); + + //业务参数 + $biz['items'] = $data; + + //拼装请求参数 + $params = $this->getParams($biz); + + //请求平台接口 + $request = $this->postRequest($url, $params); + + if ($request['code'] == 0) { + return true; + } else { + return false; + } + } + + + /** + * 同步商品 + * @param $spuArr + * @return mixed + * @throws \Exception + */ + public function syncProduct($spuArr) + { + $url = $this->accessToken->getApiUrl("/open/mall/item/query"); + + //业务参数 + $biz['i_ids'] = $spuArr; + + //拼装请求参数 + $params = $this->getParams($biz); + + //请求平台接口 + $request = $this->postRequest($url, $params); + + //获取ERP商品信息 + return $request["data"]; + } + + /** + * 库存查询 + * @param string $codeStr + * @return mixed + * @throws \Exception + */ + public function syncStock(string $codeStr) + { + $url = $this->accessToken->getApiUrl("/open/inventory/query"); + + //业务参数 + $biz = []; + + $biz["sku_ids"] = $codeStr; + + //拼装请求参数 + $params = $this->getParams($biz); + + //请求平台接口 + $request = $this->postRequest($url, $params); + return $request["data"]; + } +} \ No newline at end of file diff --git a/crmeb/services/erp/storage/jushuitan/Stock.php b/crmeb/services/erp/storage/jushuitan/Stock.php new file mode 100644 index 0000000..12c34c3 --- /dev/null +++ b/crmeb/services/erp/storage/jushuitan/Stock.php @@ -0,0 +1,151 @@ + +// +---------------------------------------------------------------------- +namespace crmeb\services\erp\storage\jushuitan; + +use app\services\product\product\StoreProductServices; +use app\services\product\sku\StoreProductAttrValueServices; +use crmeb\exceptions\AdminException; +use crmeb\services\erp\AccessToken; +use crmeb\services\erp\storage\Jushuitan; + +class Stock +{ + /** + * token句柄 + * @var AccessToken + */ + protected $accessToken; + + /*** @var Jushuitan */ + protected $jushuitan; + + /** + * @param AccessToken $accessToken + * @param Jushuitan $jushuitan + */ + public function __construct(AccessToken $accessToken, Jushuitan $jushuitan) + { + $this->accessToken = $accessToken; + $this->jushuitan = $jushuitan; + } + + /** + * 同步商品库存 + * @param array $ids + * @return void + * @throws \Exception + */ + public function syncStock(string $ids = '') + { + /** @var StoreProductServices $storeProductServices */ + $storeProductServices = app()->make(StoreProductServices::class); + + /** @var StoreProductAttrValueServices $storeProductAttrValueServices */ + $storeProductAttrValueServices = app()->make(StoreProductAttrValueServices::class); + + //查询ids下的所有规格对应的sku + $ids = array_unique(array_map('intval', explode(',', $ids))); + $list = $storeProductAttrValueServices->getSkuArray(['product_id' => $ids, 'type' => 0], 'code', 'id'); + + $values = array_filter(array_values($list)); + if (empty($values)) { + throw new AdminException('没有符合同步库存的商品'); + } + + $skuData = $skuMap = []; + + $basic = 20; // 单次查询数量最多20 + $num = count($values); + $rate = ceil($num / $basic); + for ($i = 0; $i < $rate; $i++) { + $code = array_slice($values, $i * $basic, $basic); + $skuMap = $skuMap + $this->getSkuStockByCode($code); + } + + // 拼装规格数据 + if (!empty($skuMap)) { + foreach ($skuMap as $key => $item) { + if ($id = array_search($key, $list)) { + $skuData[] = ['id' => $id, 'stock' => $item, 'sum_stock' => $item]; + } + } + } + + // 同步库存 TODO:待添加至队列 + $storeProductServices->transaction(function () use ($ids, $skuData, $skuMap, $storeProductAttrValueServices, $storeProductServices) { + // 同步规格库存 + $storeProductAttrValueServices->saveAll($skuData); + // 同步商品库存 + $productData = $storeProductAttrValueServices->getProductStockByValues($ids); + $storeProductServices->saveAll($productData); + + //同步门店库存 + foreach ($skuMap as $item) { + + } + }); + + return true; + } + + /** + * 库存查询 + * @param array $data + * @return array + * @throws \Exception + */ + public function inventoryQuery(string $codeStr): array + { + $url = $this->accessToken->getApiUrl("/open/inventory/query"); + + //业务参数 + $biz = []; + + $biz["sku_ids"] = $codeStr; + + //拼装请求参数 + $params = $this->getParams($biz); + + //请求平台接口 + $request = $this->postRequest($url, $params); + return $request["data"]; + } + + /** + * 获取erp库存 + * @param array $code + * @return array + * @throws \Exception + */ + public function getSkuStockByCode(array $code = []): array + { + $skuMap = []; + $codeStr = implode(',', $code); + $result = $this->inventoryQuery($codeStr); + if (!empty($result['inventorys'])) { + foreach ($result['inventorys'] as $inventory) { + $skuMap[$inventory['sku_id']] = $inventory['qty']; + } + } + return $skuMap; + } + + /** + * @param $name + * @param $arguments + * @return mixed + */ + public function __call($name, $arguments) + { + return call_user_func_array([$this->jushuitan, $name], $arguments); + } + +} \ No newline at end of file diff --git a/crmeb/services/wechat/BaseApplication.php b/crmeb/services/wechat/BaseApplication.php new file mode 100644 index 0000000..e3c6323 --- /dev/null +++ b/crmeb/services/wechat/BaseApplication.php @@ -0,0 +1,154 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + + +use crmeb\services\wechat\contract\BaseApplicationInterface; +use think\facade\Log; + +/** + * Class BaseApplication + * @package crmeb\services\wechat + */ +abstract class BaseApplication implements BaseApplicationInterface +{ + + //app端 + const APP = 'app'; + //h5端、公众端 + const WEB = 'web'; + //小程序端 + const MINI = 'mini'; + //开发平台 + const OPEN = 'open'; + //pc端 + const PC = 'pc'; + + /** + * 访问端 + * @var string + */ + protected $accessEnd; + + /** + * @var array + */ + protected static $property = []; + + /** + * @var string + */ + protected $pushMessageHandler; + + /** + * Debug + * @var bool + */ + protected $debug = true; + + /** + * 设置消息处理类 + * @param string $handler + * @return $this + */ + public function setPushMessageHandler(string $handler) + { + $this->pushMessageHandler = $handler; + return $this; + } + + /** + * 设置访问端 + * @param string $accessEnd + * @return $this + */ + public function setAccessEnd(string $accessEnd) + { + if (in_array($accessEnd, [self::APP, self::WEB, self::MINI])) { + $this->accessEnd = $accessEnd; + } + return $this; + } + + /** + * 自动获取访问端 + * @param \think\Request $request + * @return string + */ + public function getAuthAccessEnd(\think\Request $request) + { + if (!$this->accessEnd) { + try { + if ($request->isApp()) { + $this->accessEnd = self::APP; + } else if ($request->isPc()) { + $this->accessEnd = self::PC; + } else if ($request->isWechat() || $request->isH5()) { + $this->accessEnd = self::WEB; + } else if ($request->isRoutine()) { + $this->accessEnd = self::MINI; + } else { + $this->accessEnd = self::WEB; + } + } catch (\Throwable $e) { + $this->accessEnd = self::WEB; + } + } + return $this->accessEnd; + } + + /** + * 记录错误日志 + * @param \Throwable $e + */ + protected static function error(\Throwable $e) + { + static::instance()->debug && Log::error([ + 'error' => $e->getMessage(), + 'line' => $e->getLine(), + 'file' => $e->getFile() + ]); + } + + /** + * 请求日志 + * @param string $message + * @param $request + * @param $response + */ + protected static function logger(string $message, $request, $response) + { + $debug = static::instance()->debug; + + if ($debug) { + Log::info([ + 'message' => $message, + 'request' => json_encode($request), + 'response' => json_encode($response) + ]); + } + } + + /** + * @param $name + * @param $arguments + * @return mixed + */ + public static function __callStatic($name, $arguments) + { + if (in_array($name, array_keys(static::$property))) { + $name = static::$property[$name]; + return static::instance()->application()->{$name}; + } + throw new WechatException('方法不存在'); + } +} diff --git a/crmeb/services/wechat/DefaultConfig.php b/crmeb/services/wechat/DefaultConfig.php new file mode 100644 index 0000000..0780aff --- /dev/null +++ b/crmeb/services/wechat/DefaultConfig.php @@ -0,0 +1,123 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + +use think\facade\Config; + +/** + * 默认配置 + * Class DefaultConfig + * @package crmeb\services\wechat + */ +class DefaultConfig +{ + //小程序appid + const MINI_APPID = 'mini.appid'; + //公众号appid + const OFFICIAL_APPID = 'official.appid'; + //开放平台appid + const APP_APPID = 'app.appid'; + //开放平台网页端appid + const WEB_APPID = 'web.appid'; + //企业微信id + const WORK_CORP_ID = 'work.corp_id'; + //商户id + const PAY_MCHID = 'pay.mchid'; + //系统配置域名地址,携带,格式:http://www.a.com + const COMMENT_URL = 'comment.url'; + + /** + * + */ + const WECHAT_CONFIG = [ + //请求响应日志 + 'logger' => true, + //公用 + 'comment' => [ + 'url' => 'site_url', + ], + //小程序配置 + 'mini' => [ + 'appid' => 'routine_appId', + 'secret' => 'routine_appsecret', + 'notifyUrl' => '/api/pay/notify/routine',//必须携带斜杠开头 + ], + //公众号配置 + 'official' => [ + 'appid' => 'wechat_appid', + 'secret' => 'wechat_appsecret', + 'token' => 'wechat_token', + 'key' => 'wechat_encodingaeskey', + 'encode' => 'wechat_encode', + ], + //开放平台APP + 'app' => [ + 'appid' => 'wechat_app_appid', + 'secret' => 'wechat_app_appsecret', + 'token' => 'wechat_openapp_app_token', + 'key' => 'wechat_openapp_app_aes_key', + 'notifyUrl' => '/api/pay/notify/app',//必须携带斜杠开头 + ], + //开放平台网页应用 + 'web' => [ + 'appid' => 'wechat_open_app_id', + 'secret' => 'wechat_open_app_secret', + 'token' => 'wechat_open_app_token', + 'key' => 'wechat_open_app_aes_key', + ], + //企业微信 + 'work' => [ + 'corp_id' => 'wechat_work_corpid', + 'token' => 'wechat_work_token', + 'key' => 'wechat_work_aes_key', + ], + //支付 + 'pay' => [ + 'mchid' => 'pay_weixin_mchid',//商户号 + 'key' => 'pay_weixin_key',//支付key + 'client_cert' => 'pay_weixin_client_cert',//证书 + 'client_key' => 'pay_weixin_client_key',//证书 + 'notifyUrl' => '/api/pay/notify/wechat',//支付回调,必须携带斜杠开头 + 'refundUrl' => '/api/pay/refund/wechat',//退款回到,必须携带斜杠开头 + ] + ]; + + /** + * 获取配置,如果配置为数组则使用value的值,如果没有值返回key + * @param string $key + * @return array|mixed|string[]|null + */ + public static function value(string $key) + { + $config = []; + if (Config::has('wechat')) { + $config = Config::get('wechat', []); + } + $config = array_merge(self::WECHAT_CONFIG, $config); + + $key = explode('.', $key); + $value = null; + foreach ($key as $k) { + if ($value) { + $value = $value[$k] ?? null; + } else { + $value = $config[$k] ?? null; + } + } + + if (is_array($value)) { + $value = !empty($value['value']) ? $value['value'] : $value['key']; + } + + return $value; + } +} diff --git a/crmeb/services/wechat/ErrorMessage.php b/crmeb/services/wechat/ErrorMessage.php new file mode 100644 index 0000000..3651d0c --- /dev/null +++ b/crmeb/services/wechat/ErrorMessage.php @@ -0,0 +1,142 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + + +use crmeb\utils\ApiErrorCode; + +/** + * 错误消息处理 + * Class ErrorMessage + * @package crmeb\services\wechat + */ +class ErrorMessage +{ + + const MSG_CODE = [ + '1' => '未创建直播间', + '1003' => '商品id不存在', + '47001' => '入参格式不符合规范', + '200002' => '入参错误', + '300001' => '禁止创建/更新商品 或 禁止编辑&更新房间', + '300002' => '名称长度不符合规则', + '300006' => '图片上传失败', + '300022' => '此房间号不存在', + '300023' => '房间状态 拦截', + '300024' => '商品不存在', + '300025' => '商品审核未通过', + '300026' => '房间商品数量已经满额', + '300027' => '导入商品失败', + '300028' => '房间名称违规', + '300029' => '主播昵称违规', + '300030' => '主播微信号不合法', + '300031' => '直播间封面图不合规', + '300032' => '直播间分享图违规', + '300033' => '添加商品超过直播间上限', + '300034' => '主播微信昵称长度不符合要求', + '300035' => '主播微信号不存在', + '300036' => '主播微信号未实名认证', + '300037' => '购物直播频道封面图不合规', + '300038' => '未在小程序管理后台配置客服', + '9410000' => '直播间列表为空', + '9410001' => '获取房间失败', + '9410002' => '获取商品失败', + '9410003' => '获取回放失败', + '300001' => '禁止创建/更新商品(如:商品创建功能被封禁)', + '300002' => '名称长度不符合规则', + '300003' => '价格输入不合规', + '300004' => '商品名称存在违规违法内容', + '300005' => '商品图片存在违规违法内容', + '300007' => '线上小程序版本不存在该链接', + '300008' => '添加商品失败', + '300009' => '商品审核撤回失败', + '300010' => '商品审核状态不对', + '300011' => '操作非法', + '300012' => '没有提审额度', + '300013' => '提审失败', + '300014' => '审核中,无法删除', + '300017' => '商品未提审', + '300018' => '图片尺寸不符合要求', + '300021' => '商品添加成功,审核失败', + '40001' => 'AppSecret错误或者AppSecret不属于这个小程序,请确认AppSecret 的正确性', + '40002' => '请确保grant_type字段值为client_credential', + '40013' => '不合法的AppID,请检查AppID的正确性,避免异常字符,注意大小写', + '40125' => '小程序配置无效,请检查配置', + '40164' => 'IP地址不在白名单中,请检查设置', + '41002' => '缺少appid参数', + '41004' => '缺少secret参数', + '43104' => 'appid与openid不匹配', + '48001' => '微信接口暂无权限,请先去获取', + '-1' => '系统错误', + ]; + + const WORK_ERROR_MESSAGE = [ + 40098 => '成员尚未实名认证', + 40068 => '不合法的标签/标签组ID' + ]; + + /** + * 处理返回错误信息友好提示 + * @param string $message + * @return array|mixed|string + */ + public static function getMessage(string $message) + { + if (strstr($message, 'Request AccessToken fail') !== false || strstr($message, 'Request access_token fail') !== false) { + $message = str_replace('Request AccessToken fail. response:', '', $message); + $message = str_replace('Request access_token fail:', '', $message); + $message = trim($message); + $message = json_decode($message, true) ?: []; + $errcode = $message['errcode'] ?? false; + if ($errcode) { + $message = ApiErrorCode::ERROR_WECHAT_MESSAGE[$errcode] ?? $message; + } + } + return $message; + } + + /** + * 解析错误 + * @param \Throwable $e + * @return array|mixed|string + */ + public static function getValidMessgae(\Throwable $e) + { + $message = ''; + if (!isset(self::MSG_CODE[$e->getCode()]) && (strstr($e->getMessage(), 'Request AccessToken fail') !== false || strstr($e->getMessage(), 'Request access_token fail') !== false)) { + $message = str_replace('Request AccessToken fail. response:', '', $e->getMessage()); + $message = str_replace('Request access_token fail:', '', $message); + $message = trim($message); + $message = json_decode($message, true) ?: []; + $errcode = $message['errcode'] ?? false; + if ($errcode) { + $message = self::MSG_CODE[$errcode] ?? $message; + } + } + return $message ? $message : self::MSG_CODE[$e->getCode()] ?? $e->getMessage(); + } + + /** + * 获取企业微信错误提示 + * @param int $errcode + * @param string|null $message + * @return string|null + */ + public static function getWorkMessage(int $errcode, string $message = null) + { + if (isset(self::WORK_ERROR_MESSAGE[$errcode])) { + return self::WORK_ERROR_MESSAGE[$errcode]; + } else { + return $message; + } + } +} diff --git a/crmeb/services/wechat/Factory.php b/crmeb/services/wechat/Factory.php new file mode 100644 index 0000000..7d0b69d --- /dev/null +++ b/crmeb/services/wechat/Factory.php @@ -0,0 +1,41 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + +use EasyWeChat\Kernel\Messages\Text; +use EasyWeChat\Kernel\Messages\Image; +use EasyWeChat\Kernel\Messages\TextCard; +use EasyWeChat\Kernel\Messages\Transfer; +use EasyWeChat\Kernel\Messages\Video; +use EasyWeChat\Kernel\Messages\Voice; +use EasyWeChat\Kernel\Messages\News; +use EasyWeChat\Kernel\Messages\Article; +use EasyWeChat\Kernel\Messages\Media; +use EasyWeChat\Kernel\Messages\NewsItem; + +/** + * 消息回复模板 + * Class Messages + * @package crmeb\services\wechat + */ +class Messages +{ + /** + * 回复文本消息 + * @param string $content 文本内容 + * @return Text + */ + public static function textMessage(string $content = '') + { + $content = str_replace("\\n", "\n", $content); //返回数据处理换行符 + $content = str_replace("\\r", "\r", $content); //返回数据处理换行符 + return new Text($content); + } + + /** + * 回复图片消息 + * @param string $mediaId 媒体资源 ID + * @return Image + */ + public static function imageMessage(string $mediaId) + { + return new Image($mediaId); + } + + /** + * 回复视频消息 + * @param string $mediaId 媒体资源 ID + * @param string $title 标题 + * @param string $description 描述 + * @param null $thumb_media_id 封面资源 ID + * @return Video + */ + public static function videoMessage(string $mediaId, $title = '', $description = '...', $thumb_media_id = null) + { + return new Video($mediaId, compact('title', 'description', 'thumb_media_id')); + } + + /** + * 回复声音消息 + * @param string $mediaId 媒体资源 ID + * @return Voice + */ + public static function voiceMessage(string $mediaId) + { + return new Voice($mediaId); + } + + /** + * 回复图文消息 + * @param $title + * @param string $description + * @param string $url + * @param string $image + * @return array|News + */ + public static function newsMessage($title, $description = '...', $url = '', $image = '') + { + if (is_array($title)) { + $items = [ + new NewsItem([ + 'title' => $title['title'], + 'description' => $title['description'], + 'url' => $title['url'], + 'image' => $title['image'] + ]) + ]; + } else { + $items = [ + new NewsItem([ + 'title' => $title, + 'description' => $description, + 'url' => $url, + 'image' => $image + ]) + ]; + } + return new News($items); + } + + /** + * 创建新闻消息类型 + * @param $title + * @param string $description + * @param string $url + * @param string $image + * @return News + */ + public static function newMessage($title, string $description = '...', string $url = '', string $image = '') + { + if (is_array($title)) { + $items = [ + new NewsItem([ + 'title' => $title['title'], + 'description' => $title['description'], + 'url' => $title['url'], + 'image' => $title['image'] + ]) + ]; + } else { + $items = [ + new NewsItem([ + 'title' => $title, + 'description' => $description, + 'url' => $url, + 'image' => $image + ]) + ]; + } + return new News($items); + } + + /** + * 回复文章消息 + * @param string|array $title 标题 + * @param string $thumb_media_id 图文消息的封面图片素材id(必须是永久 media_ID) + * @param string $source_url 图文消息的原文地址,即点击“阅读原文”后的URL + * @param string $content 图文消息的具体内容,支持HTML标签,必须少于2万字符,小于1M,且此处会去除JS + * @param string $author 作者 + * @param string $digest 图文消息的摘要,仅有单图文消息才有摘要,多图文此处为空 + * @param int $show_cover_pic 是否显示封面,0为false,即不显示,1为true,即显示 + * @param int $need_open_comment 是否打开评论,0不打开,1打开 + * @param int $only_fans_can_comment 是否粉丝才可评论,0所有人可评论,1粉丝才可评论 + * @return Article + */ + public static function articleMessage($title, $thumb_media_id, $source_url, $content = '', $author = '', $digest = '', $show_cover_pic = 0, $need_open_comment = 0, $only_fans_can_comment = 1) + { + $data = is_array($title) ? $title : compact('title', 'thumb_media_id', 'source_url', 'content', 'author', 'digest', 'show_cover_pic', 'need_open_comment', 'only_fans_can_comment'); + return new Article($data); + } + + /** + * 回复素材消息 + * @param string $mediaId + * @param null $type mpnews、 mpvideo、voice、image + * @return Media + */ + public static function materialMessage(string $mediaId, $type = null) + { + return new Media($mediaId, $type); + } + + /** + * 多客服消息转发 + * @param string|null $account + * @return Transfer + */ + public static function transfer(string $account = null) + { + return new Transfer($account); + } + + /** + * 消息模板 + * @param string $title + * @param string $description + * @param string $url + * @return TextCard + */ + public static function TextCardMessage(string $title, string $description, string $url) + { + return new TextCard([ + 'title' => $title, + 'description' => $description, + 'url' => $url + ]); + } +} diff --git a/crmeb/services/wechat/MiniPayment/Application.php b/crmeb/services/wechat/MiniPayment/Application.php new file mode 100644 index 0000000..4ab38e1 --- /dev/null +++ b/crmeb/services/wechat/MiniPayment/Application.php @@ -0,0 +1,30 @@ +providers = array_merge($this->mini_providers,$this->providers); + parent::__construct($config, $prepends, $id); + + } +} \ No newline at end of file diff --git a/crmeb/services/wechat/MiniPayment/Payment/ServiceProvider.php b/crmeb/services/wechat/MiniPayment/Payment/ServiceProvider.php new file mode 100644 index 0000000..541b5de --- /dev/null +++ b/crmeb/services/wechat/MiniPayment/Payment/ServiceProvider.php @@ -0,0 +1,33 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace crmeb\services\wechat\MiniPayment\Payment; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author mingyoung + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['orders'] = function ($app) { + return new WeChatClient($app); + }; + } +} diff --git a/crmeb/services/wechat/MiniPayment/Payment/WeChatClient.php b/crmeb/services/wechat/MiniPayment/Payment/WeChatClient.php new file mode 100644 index 0000000..3cd0c7c --- /dev/null +++ b/crmeb/services/wechat/MiniPayment/Payment/WeChatClient.php @@ -0,0 +1,77 @@ +'支付者的openid', + * 'out_trade_no'=>'商家合单支付总交易单号', + * 'total_fee'=>'支付金额', + * 'wx_out_trade_no'=>'商家交易单号', + * 'body'=>'商品描述', + * 'attach'=>'支付类型', //product 产品 member 会员 + * ] + * @param $isContract + * @return mixed + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function createorder(array $params) + { + $data = [ + 'openid'=>$params['openid'], // 支付者的openid + 'combine_trade_no'=>$params['out_trade_no'], // 商家合单支付总交易单号 + 'expire_time'=>time()+$this->expire_time, + 'sub_orders'=>[ + [ + 'mchid'=>$this->app['config']['mch_id'], + 'amount'=>(int)$params['total_fee'], + 'trade_no'=>$params['out_trade_no'], + 'description'=>$params['body'], + ] + ], + ]; + return $this->httpPostJson(self::API_SET_CREATE_ORDER, $data); + } + + /** + * 退款 + * @param array $params[ + * 'openid'=>'退款者的openid', + * 'trade_no'=>'商家交易单号', + * 'transaction_id'=>'支付单号', + * 'refund_no'=>'商家退款单号', + * 'total_amount'=>'订单总金额', + * 'refund_amount'=>'退款金额', //product 产品 member 会员 + * ] + * @return mixed + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function refundorder(array $params) + { + $data = [ + 'openid'=>$params['openid'], + 'mchid'=>$this->app['config']['mch_id'], + 'trade_no'=>$params['trade_no'], + 'transaction_id'=>$params['transaction_id'], + 'refund_no'=>$params['refund_no'], + 'total_amount'=>(int)$params['total_amount'], + 'refund_amount'=>(int)$params['refund_amount'], + ]; + return $this->httpPostJson(self::API_SET_REFUND_ORDER, $data); + } +} \ No newline at end of file diff --git a/crmeb/services/wechat/MiniProgram.php b/crmeb/services/wechat/MiniProgram.php new file mode 100644 index 0000000..2e078e4 --- /dev/null +++ b/crmeb/services/wechat/MiniProgram.php @@ -0,0 +1,647 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + + +use crmeb\services\wechat\config\MiniProgramConfig; +use crmeb\services\wechat\live\LiveClient; +use crmeb\services\wechat\orderShipping\OrderClient; +use EasyWeChat\Factory; +use EasyWeChat\Kernel\Exceptions\DecryptException; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\Kernel\Support\Collection; +use EasyWeChat\MiniProgram\Application; +use GuzzleHttp\Exception\GuzzleException; +use Psr\Http\Message\ResponseInterface; +use Symfony\Component\HttpFoundation\Request; +use think\facade\Cache; +use think\Response; +use Yurun\Util\Swoole\Guzzle\SwooleHandler; +use crmeb\services\wechat\live\ServiceProvider as LiveServiceProvider; +use Symfony\Component\Cache\Adapter\RedisAdapter; +use crmeb\services\wechat\orderShipping\ServiceProvider as OrderShippingServiceProvider; + +/** + * 小程序服务 + * Class MiniProgram + * @package crmeb\services\wechat + * @method \EasyWeChat\OfficialAccount\CustomerService\Client staffService() 客服 + * @method \EasyWeChat\BasicService\Media\Client mediaService() 临时素材 + * @method \EasyWeChat\MiniProgram\Encryptor encryptor() 解密 + * @method \EasyWeChat\MiniProgram\AppCode\Client qrcodeService() 小程序码 + * @method \EasyWeChat\MiniProgram\SubscribeMessage\Client subscribenoticeService() 订阅消息 + * @method LiveClient liveService() 直播 + * @method OrderClient orderShippingService() 订单管理 + */ +class MiniProgram extends BaseApplication +{ + + /** + * @var MiniProgramConfig + */ + protected $config; + + /** + * @var Application + */ + protected $application; + + /** + * @var string[] + */ + protected static $property = [ + 'mediaService' => 'media', + 'staffService' => 'customer_service', + 'encryptor' => 'encryptor', + 'qrcodeService' => 'app_code', + 'subscribenoticeService' => 'subscribe_message', + 'liveService' => 'live', + 'orderShippingService' => 'orderShipping', + ]; + + /** + * MiniProgram constructor. + */ + public function __construct() + { + $this->config = app(MiniProgramConfig::class); + $this->debug = DefaultConfig::value('logger'); + } + + /** + * 初始化 + * @return Application + */ + public function application() + { + if (!$this->application) { + $this->application = Factory::miniProgram($this->config->all()); + $request = request(); + $this->application['guzzle_handler'] = SwooleHandler::class; + $this->application->rebind('request', new Request($request->get(), $request->post(), [], [], [], $request->server(), $request->getContent())); + $this->application->register(new LiveServiceProvider()); + $this->application->register(new OrderShippingServiceProvider()); + $this->application->rebind('cache', new RedisAdapter(Cache::store('redis')->handler())); + } + return $this->application; + } + + public static function serve(): Response + { + $make = self::instance(); + $make->application()->server->push($make->pushMessageHandler); + $response = $make->application()->server->serve(); + return response($response->getContent()); + } + + /** + * @return MiniProgram + */ + public static function instance() + { + return app()->make(self::class); + } + + /** + * 获得用户信息 根据code 获取session_key + * @param string $code + * @return array|Collection|object|ResponseInterface|string + */ + public static function getUserInfo(string $code) + { + try { + $response = self::instance()->application()->auth->session($code); + + self::logger('获得用户信息 根据code 获取session_key', compact('code'), $response); + + return $response; + } catch (\Throwable $e) { + throw new WechatException($e->getMessage()); + } + } + + /** + * 解密数据 + * @param string $sessionKey + * @param string $iv + * @param string $encryptData + * @return array + * @throws DecryptException + */ + public static function decryptData(string $sessionKey, string $iv, string $encryptData) + { + $response = self::encryptor()->decryptData($sessionKey, $iv, $encryptData); + + self::logger('解密数据', compact('sessionKey', 'iv', 'encryptData'), $response); + + return $response; + } + + /** + * 获取小程序码:适用于需要的码数量极多,或仅临时使用的业务场景 + * @param string $scene + * @param string $path + * @param int $width + * @return array|Collection|object|ResponseInterface|string + */ + public static function appCodeUnlimit(string $scene, string $path = '', int $width = 0) + { + $optional = [ + 'page' => $path, + 'width' => $width + ]; + if (!$optional['page']) { + unset($optional['page']); + } + if (!$optional['width']) { + unset($optional['width']); + } + $response = self::qrcodeService()->getUnlimit($scene, $optional); + + self::logger('获取小程序码', compact('scene', 'optional'), $response); + + return $response; + } + + /** + * 发送订阅消息 + * @param string $touser + * @param string $templateId + * @param array $data + * @param string $link + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws GuzzleException + */ + public static function sendSubscribeTemlate(string $touser, string $templateId, array $data, string $link = '') + { + $response = self::subscribenoticeService()->send([ + 'template_id' => $templateId, + 'touser' => $touser, + 'page' => $link, + 'data' => $data + ]); + + self::logger('发送订阅消息', compact('templateId', 'touser', 'link', 'data'), $response); + + return $response; + } + + /** + * 添加订阅消息模版 + * @param string $tid + * @param array $kidList + * @param string $sceneDesc + * @return mixed + */ + public static function addSubscribeTemplate(string $tid, array $kidList, string $sceneDesc = '') + { + try { + $res = self::subscribenoticeService()->addTemplate($tid, $kidList, $sceneDesc); + + self::logger('添加订阅消息模版', compact('tid', 'kidList', 'sceneDesc'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['priTmplId'])) { + return $res['priTmplId']; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException($e); + } + } + + /** + * 删除订阅消息 + * @param string $templateId + * @return array|Collection|object|ResponseInterface|string + */ + public static function delSubscribeTemplate(string $templateId) + { + try { + $response = self::subscribenoticeService()->deleteTemplate($templateId); + + self::logger('删除订阅消息', compact('templateId'), $response); + + return $response; + } catch (\Throwable $e) { + throw new WechatException($e->getMessage()); + } + } + + /** + * 获取模版标题的关键词列表 + * @param string $tid + * @return mixed + */ + public static function getSubscribeTemplateKeyWords(string $tid) + { + try { + $res = self::subscribenoticeService()->getTemplateKeywords($tid); + + self::logger('获取模版标题的关键词列表', compact('tid'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['data'])) { + return $res['data']; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException($e); + } + } + + /** + * 获取直播列表 + * @param int $page + * @param int $limit + * @return array + */ + public static function getLiveInfo(int $page = 1, int $limit = 10) + { + try { + $res = self::liveService()->getRooms($page, $limit); + + self::logger('获取直播列表', compact('page', 'limit'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['room_info']) && $res['room_info']) { + return $res['room_info']; + } else { + return []; + } + } catch (\Throwable $e) { + return []; + } + } + + /** + * 获取直播回放 + * @param int $room_id + * @param int $page + * @param int $limit + * @return mixed + */ + public static function getLivePlayback(int $room_id, int $page = 1, int $limit = 10) + { + try { + $res = self::liveService()->getPlaybacks($room_id, $page, $limit); + + self::logger('获取直播回放', compact('room_id', 'page', 'limit'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['live_replay'])) { + return $res['live_replay']; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 创建直播间 + * @param array $data + * @return mixed + */ + public static function createLiveRoom(array $data) + { + try { + $res = self::liveService()->createRoom($data); + + self::logger('创建直播间', compact('data'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['roomId'])) { + unset($res['errcode']); + return $res; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 直播间添加商品 + * @param int $roomId + * @param $ids + * @return bool + */ + public static function roomAddGoods(int $roomId, $ids) + { + try { + $res = self::liveService()->roomAddGoods($roomId, $ids); + + self::logger('直播间添加商品', compact('roomId', 'ids'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0) { + return true; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 获取商品列表 + * @param int $status + * @param int $page + * @param int $limit + * @return mixed + */ + public static function getGoodsList(int $status = 2, int $page = 1, int $limit = 10) + { + try { + $res = self::liveService()->getGoodsList($status, $page, $limit); + + self::logger('获取商品列表', compact('status', 'page', 'limit'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['goods'])) { + return $res['goods']; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 获取商品详情 + * @param $goods_ids + * @return mixed + */ + public static function getGooodsInfo($goods_ids) + { + try { + $res = self::liveService()->getGooodsInfo($goods_ids); + + self::logger('获取商品详情', compact('goods_ids'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['goods'])) { + return $res['goods']; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 添加商品 + * @param string $coverImgUrl + * @param string $name + * @param int $priceType + * @param string $url + * @param $price + * @param string $price2 + * @return mixed + */ + public static function addGoods(string $coverImgUrl, string $name, int $priceType, string $url, $price, $price2 = '') + { + try { + $res = self::liveService()->addGoods($coverImgUrl, $name, $priceType, $url, $price, $price2); + + self::logger('添加商品', compact('coverImgUrl', 'name', 'priceType', 'url', 'price', 'price2'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['goodsId'])) { + unset($res['errcode']); + return $res; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 商品撤回审核 + * @param int $goodsId + * @param $auditId + * @return bool + */ + public static function resetauditGoods(int $goodsId, $auditId) + { + try { + $res = self::liveService()->resetauditGoods($goodsId, $auditId); + + self::logger('商品撤回审核', compact('goodsId', 'auditId'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0) { + return true; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 商品重新提交审核 + * @param int $goodsId + * @return mixed + */ + public static function auditGoods(int $goodsId) + { + try { + $res = self::liveService()->auditGoods($goodsId); + + self::logger('商品重新提交审核', compact('goodsId'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['auditId'])) { + return $res['auditId']; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 删除商品 + * @param int $goodsId + * @return bool + */ + public static function deleteGoods(int $goodsId) + { + try { + $res = self::liveService()->deleteGoods($goodsId); + + self::logger('删除商品', compact('goodsId'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0) { + return true; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 更新商品 + * @param int $goodsId + * @param string $coverImgUrl + * @param string $name + * @param int $priceType + * @param string $url + * @param $price + * @param string $price2 + * @return bool + */ + public static function updateGoods(int $goodsId, string $coverImgUrl, string $name, int $priceType, string $url, $price, $price2 = '') + { + try { + $res = self::liveService()->updateGoods($goodsId, $coverImgUrl, $name, $priceType, $url, $price, $price2); + + self::logger('更新商品', compact('goodsId', 'coverImgUrl', 'name', 'priceType', 'url', 'price', 'price2'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0) { + return true; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 获取成员列表 + * @param int $role + * @param int $page + * @param int $limit + * @param string $keyword + * @return mixed + */ + public static function getRoleList($role = 2, int $page = 0, int $limit = 30, $keyword = '') + { + try { + $res = self::liveService()->getRoleList($role, $page, $limit, $keyword); + + self::logger('获取成员列表', compact('role', 'page', 'limit', 'keyword'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['list'])) { + return $res['list']; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getValidMessgae($e)); + } + } + + /** + * 小程序临时素材上传 + * @param string $path + * @param string $type + * @return WechatResponse + * @throws GuzzleException + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public static function temporaryUpload(string $path, string $type = 'image') + { + $response = self::mediaService()->upload($type, $path); + + self::logger('小程序-临时素材上传', compact('path', 'type'), $response); + + return new WechatResponse($response); + } + + /** + * 上传订单 + * @param string $out_trade_no 订单号(商城订单好) + * @param int $logistics_type 物流模式,发货方式枚举值:1、实体物流配送采用快递公司进行实体物流配送形式 2、同城配送 3、虚拟商品,虚拟商品,例如话费充值,点卡等,无实体配送形式 4、用户自提 + * @param array $shipping_list 物流信息列表,发货物流单列表,支持统一发货(单个物流单)和分拆发货(多个物流单)两种模式,多重性: [1, 10] + * @param string $payer_openid 支付者,支付者信息 + * @param int $delivery_mode 发货模式,发货模式枚举值:1、UNIFIED_DELIVERY(统一发货)2、SPLIT_DELIVERY(分拆发货) 示例值: UNIFIED_DELIVERY + * @param bool $is_all_delivered 分拆发货模式时必填,用于标识分拆发货模式下是否已全部发货完成,只有全部发货完成的情况下才会向用户推送发货完成通知。示例值: true/false + * @return array + * + * @throws HttpException + * + */ + public static function shippingByTradeNo(string $out_trade_no, int $logistics_type, array $shipping_list, string $payer_openid, string $path, int $delivery_mode = 1, bool $is_all_delivered = true) + { + return self::orderShippingService()->shippingByTradeNo($out_trade_no, $logistics_type, $shipping_list, $payer_openid, $path, $delivery_mode, $is_all_delivered); + } + + /** + * 合单 + * @param string $out_trade_no + * @param int $logistics_type + * @param array $sub_orders + * @param string $payer_openid + * @param int $delivery_mode + * @param bool $is_all_delivered + * @return array + * @throws HttpException + * + * + */ + public static function combinedShippingByTradeNo(string $out_trade_no, int $logistics_type, array $sub_orders, string $payer_openid, int $delivery_mode = 2, bool $is_all_delivered = false) + { + return self::orderShippingService()->combinedShippingByTradeNo($out_trade_no, $logistics_type, $sub_orders, $payer_openid, $delivery_mode, $is_all_delivered); + } + + /** + * 签收通知 + * @param string $merchant_trade_no + * @param string $received_time + * @return array + * + * + */ + public static function notifyConfirmByTradeNo(string $merchant_trade_no, string $received_time) + { + return self::orderShippingService()->notifyConfirmByTradeNo($merchant_trade_no, $received_time); + } + + /** + * 判断是否开通 + * @return bool + * @throws HttpException + * + * @date 2023/05/17 + * @author yyw + */ + public static function isManaged() + { + return self::orderShippingService()->checkManaged(); + } + + + /** + * 设置小修跳转路径 + * @param $path + * @return array + * @throws HttpException + * + * + */ + public static function setMesJumpPathAndCheck($path) + { + return self::orderShippingService()->setMesJumpPathAndCheck($path); + } + + +} diff --git a/crmeb/services/wechat/OfficialAccount.php b/crmeb/services/wechat/OfficialAccount.php new file mode 100644 index 0000000..28e58af --- /dev/null +++ b/crmeb/services/wechat/OfficialAccount.php @@ -0,0 +1,652 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + +use crmeb\services\wechat\config\OfficialAccountConfig; +use crmeb\services\wechat\config\OpenAppConfig; +use crmeb\services\wechat\config\OpenWebConfig; +use EasyWeChat\BasicService\Url\Client; +use EasyWeChat\Factory; +use EasyWeChat\Kernel\Exceptions\BadRequestException; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\Kernel\Support\Collection; +use EasyWeChat\OfficialAccount\Application; +use EasyWeChat\OfficialAccount\Card\Card; +use EasyWeChat\OfficialAccount\User\TagClient; +use EasyWeChat\OfficialAccount\User\UserClient; +use GuzzleHttp\Exception\GuzzleException; +use Overtrue\Socialite\Providers\WeChat; +use Psr\Http\Message\ResponseInterface; +use think\facade\Cache; +use think\Response; +use Yurun\Util\Swoole\Guzzle\SwooleHandler; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Cache\Adapter\RedisAdapter; + +/** + * 公众号服务 + * Class OfficialAccount + * @package crmeb\services\wechat + * @method \EasyWeChat\OfficialAccount\Material\Client materialService() 永久素材 + * @method \EasyWeChat\BasicService\Media\Client mediaService() 临时素材 + * @method \EasyWeChat\BasicService\QrCode\Client qrcodeService() 微信二维码生成接口 + * @method UserClient userService() 用户接口 + * @method \EasyWeChat\OfficialAccount\CustomerService\Client staffService() 客服管理 + * @method \EasyWeChat\OfficialAccount\Menu\Client menuService() 微信公众号菜单接口 + * @method Client urlService() 短链接生成接口 + * @method WeChat oauthService() 用户授权 + * @method \EasyWeChat\OfficialAccount\TemplateMessage\Client templateService() 模板消息 + * @method Card cardServices() 卡券接口 + * @method TagClient userTagService() 用户标签 + */ +class OfficialAccount extends BaseApplication +{ + + /** + * 配置 + * @var OfficialAccountConfig + */ + protected $config; + + /** + * @var array + */ + protected $application; + + /** + * @var string[] + */ + protected static $property = [ + 'materialService' => 'material', + 'mediaService' => 'media', + 'qrcodeService' => 'qrcode', + 'userService' => 'user', + 'staffService' => 'customer_service', + 'menuService' => 'menu', + 'urlService' => 'url', + 'oauthService' => 'oauth', + 'templateService' => 'template_message', + 'cardServices' => 'card', + 'userTagService' => 'user_tag' + ]; + + /** + * OfficialAccount constructor. + */ + public function __construct() + { + /** @var OfficialAccountConfig config */ + $this->config = app(OfficialAccountConfig::class); + $this->debug = DefaultConfig::value('logger'); + } + + /** + * 初始化 + * @return Application + */ + public function application() + { + $request = request(); + switch ($accessEnd = $this->getAuthAccessEnd($request)) { + case self::APP: + /** @var OpenAppConfig $meke */ + $meke = app()->make(OpenAppConfig::class); + $config = $meke->all(); + break; + case self::PC: + /** @var OpenWebConfig $meke */ + $meke = app()->make(OpenWebConfig::class); + $config = $meke->all(); + break; + default: + $config = $this->config->all(); + break; + } + if (!isset($this->application[$accessEnd])) { + $this->application[$accessEnd] = Factory::officialAccount($config); + $this->application[$accessEnd]['guzzle_handler'] = SwooleHandler::class; + $this->application[$accessEnd]->rebind('request', new Request($request->get(), $request->post(), [], [], [], $request->server(), $request->getContent())); + $this->application[$accessEnd]->rebind('cache', new RedisAdapter(Cache::store('redis')->handler())); + } + + return $this->application[$accessEnd]; + } + + /** + * 服务端 + * @return Response + * @throws BadRequestException + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws \ReflectionException + */ + public static function serve(): Response + { + $make = self::instance(); + $make->application()->server->push($make->pushMessageHandler); + $response = $make->application()->server->serve(); + return response($response->getContent()); + } + + /** + * @return OfficialAccount + */ + public static function instance() + { + return app()->make(self::class); + } + + /** + * 获取js的SDK + * @param string $url + * @return string + * @throws GuzzleException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public static function jsSdk($url = '') + { + $apiList = ['openAddress', 'updateTimelineShareData', 'updateAppMessageShareData', 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareWeibo', 'onMenuShareQZone', 'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'playVoice', 'pauseVoice', 'stopVoice', 'onVoicePlayEnd', 'uploadVoice', 'downloadVoice', 'chooseImage', 'previewImage', 'uploadImage', 'downloadImage', 'translateVoice', 'getNetworkType', 'openLocation', 'getLocation', 'hideOptionMenu', 'showOptionMenu', 'hideMenuItems', 'showMenuItems', 'hideAllNonBaseMenuItem', 'showAllNonBaseMenuItem', 'closeWindow', 'scanQRCode', 'chooseWXPay', 'openProductSpecificView', 'addCard', 'chooseCard', 'openCard']; + $jsService = self::instance()->application()->jssdk; + if ($url) $jsService->setUrl($url); + try { + return $jsService->buildConfig($apiList, false, true); + } catch (\Exception $e) { + self::error($e); + return '{}'; + } + } + + /** + * 获取微信用户信息 + * @param $openid + * @return array|Collection|mixed|object|ResponseInterface|string + */ + public static function getUserInfo($openid) + { + $userService = self::userService(); + $userInfo = []; + try { + if (is_array($openid)) { + $res = $userService->select($openid); + if (isset($res['user_info_list'])) { + $userInfo = $res['user_info_list']; + } else { + throw new WechatException($res['errmsg'] ?? '获取微信粉丝信息失败'); + } + } else { + $userInfo = $userService->get($openid); + $userInfo = is_object($userInfo) ? $userInfo->toArray() : $userInfo; + } + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + + self::logger('获取微信用户信息', compact('openid'), $userInfo); + + return $userInfo; + } + + /** + * 获取会员卡列表 + * @param int $offset + * @param int $count + * @param string $statusList + * @return mixed + * @throws GuzzleException + */ + public static function getCardList($offset = 0, $count = 10, $statusList = 'CARD_STATUS_VERIFY_OK') + { + try { + $res = self::cardServices()->list($offset, $count, $statusList); + + self::logger('获取会员卡列表', compact('offset', 'count', 'statusList'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['card_id_list'])) { + return $res['card_id_list']; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 获取卡券颜色 + * @return mixed + */ + public static function getCardColors() + { + try { + + $response = self::cardServices()->colors(); + + self::logger('获取卡券颜色', [], $response); + + return $response; + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 创建卡券 + * @param string $cardType + * @param array $baseInfo + * @param array $especial + * @param array $advancedInfo + * @return mixed + * @throws GuzzleException + */ + public static function createCard(string $cardType, array $baseInfo, array $especial = [], array $advancedInfo = []) + { + try { + $res = self::cardServices()->create($cardType, array_merge(['base_info' => $baseInfo, 'advanced_info' => $advancedInfo], $especial)); + + self::logger('创建卡券', compact('cardType', 'baseInfo', 'especial', 'advancedInfo'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['card_id'])) { + return $res; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 获取卡券信息 + * @param $cardId + * @return mixed + * @throws GuzzleException + */ + public static function getCard($cardId) + { + try { + + $res = self::cardServices()->get($cardId); + + self::logger('获取卡券信息', compact('cardId'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0) { + return $res; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 修改卡券 + * @param string $cardId + * @param string $type + * @param array $baseInfo + * @param array $especial + * @return mixed + * @throws GuzzleException + */ + public static function updateCard(string $cardId, string $type, array $baseInfo = [], array $especial = []) + { + try { + $res = self::cardServices()->update($cardId, $type, array_merge(['base_info' => $baseInfo], $especial)); + + self::logger('修改卡券', compact('cardId', 'type', 'baseInfo', 'especial'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0) { + return $res; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 获取领卡券二维码 + * @param string $card_id 卡券ID + * @param string $outer_id 生成二维码标识参数 + * @param string $code 自动移code + * @param int $expire_time + * @return mixed + * @throws GuzzleException + */ + public static function getCardQRCode(string $card_id, string $outer_id, string $code = '', int $expire_time = 1800) + { + $data = [ + 'action_name' => 'QR_CARD', + 'expire_seconds' => $expire_time, + 'action_info' => [ + 'card' => [ + 'card_id' => $card_id, + 'is_unique_code' => false, + 'outer_id' => $outer_id + ] + ] + ]; + if ($code) $data['action_info']['card']['code'] = $code; + try { + $res = self::cardServices()->createQrCode($data); + + self::logger('获取领卡券二维码', compact('data'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['url'])) { + return $res; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 设置会员卡激活字段 + * @param string $cardId + * @param array $requiredForm + * @param array $optionalForm + * @return mixed + * @throws GuzzleException + */ + public static function cardActivateUserForm(string $cardId, array $requiredForm = [], array $optionalForm = []) + { + try { + $res = self::cardServices()->member_card->setActivationForm($cardId, array_merge($requiredForm, $optionalForm)); + + self::logger('设置会员卡激活字段', compact('cardId', 'requiredForm', 'optionalForm'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0) { + return $res; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 会员卡激活 + * @param string $card_id + * @param string $code + * @param string $membership_number + * @return mixed + * @throws GuzzleException + */ + public static function cardActivate(string $card_id, string $code, $membership_number = '') + { + $info = [ + 'membership_number' => $membership_number ? $membership_number : $code, //会员卡编号,由开发者填入,作为序列号显示在用户的卡包里。可与Code码保持等值。 + 'code' => $code, //创建会员卡时获取的初始code。 + 'activate_begin_time' => '', //激活后的有效起始时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式 + 'activate_end_time' => '', //激活后的有效截至时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式。 + 'init_bonus' => '0', //初始积分,不填为0。 + 'init_balance' => '0', //初始余额,不填为0。 + ]; + try { + $res = self::cardServices()->member_card->activate($info); + + self::logger('会员卡激活', compact('info'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['url'])) { + return $res; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 获取会员信息 + * @param string $cardId + * @param string $code + * @return mixed + * @throws GuzzleException + */ + public static function getMemberCardUser(string $cardId, string $code) + { + try { + $res = self::cardServices()->member_card->getUser($cardId, $code); + + self::logger('获取会员信息', compact('cardId', 'code'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['user_info'])) { + return $res; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 更新会员信息 + * @param array $data + * @return mixed + * @throws GuzzleException + */ + public static function updateMemberCardUser(array $data) + { + try { + $res = self::cardServices()->member_card->updateUser($data); + + self::logger('更新会员信息', compact('data'), $res); + + if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['user_info'])) { + return $res; + } else { + throw new WechatException($res['errmsg']); + } + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 设置模版消息行业 + * @param int $industryOne + * @param int $industryTwo + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidConfigException + */ + public static function setIndustry(int $industryOne, int $industryTwo) + { + $response = self::templateService()->setIndustry($industryOne, $industryTwo); + + self::logger('设置模版消息行业', compact('industryOne', 'industryTwo'), $response); + + return $response; + } + + /** + * 获得添加模版ID + * @param $templateIdShort + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + */ + public static function addTemplateId($templateIdShort, $keywordList) + { + try { + $response = self::templateService()->addTemplate($templateIdShort, $keywordList); + + self::logger('获得添加模版ID', compact('templateIdShort'), $response); + + return $response; + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 获取模板列表 + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + */ + public static function getPrivateTemplates() + { + try { + $response = self::templateService()->getPrivateTemplates(); + + self::logger('获取模板列表', [], $response); + + return $response; + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 根据模版ID删除模版 + * @param string $templateId + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + */ + public static function deleleTemplate(string $templateId) + { + try { + return self::templateService()->deletePrivateTemplate($templateId); + } catch (\Exception $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 获取行业 + * @return array|Collection|object|ResponseInterface|string + */ + public static function getIndustry() + { + try { + $response = self::templateService()->getIndustry(); + + self::logger('获取行业', [], $response); + + return $response; + } catch (\Throwable $e) { + throw new WechatException(ErrorMessage::getMessage($e->getMessage())); + } + } + + /** + * 发送模板消息 + * @param string $openid + * @param string $templateId + * @param array $data + * @param string|null $url + * @param string|null $defaultColor + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public static function sendTemplate(string $openid, string $templateId, array $data, string $url = null, string $defaultColor = null) + { + $response = self::templateService()->send([ + 'touser' => $openid, + 'template_id' => $templateId, + 'data' => $data, + 'url' => $url + ]); + + self::logger('发送模板消息', compact('openid', 'templateId', 'data', 'url'), $response); + + return $response; + } + + /** + * 静默授权-使用code获取用户授权信息 + * @param string|null $code + * @return array + */ + public static function tokenFromCode(string $code = null) + { + $code = $code ?: request()->param('code'); + if (!$code) { + throw new WechatException('无效CODE'); + } + + try { + $response = self::oauthService()->setGuzzleOptions(['verify' => false])->tokenFromCode($code); + + self::logger('静默授权-使用code获取用户授权信息', compact('code'), $response); + + return $response; + } catch (\Throwable $e) { + throw new WechatException('授权失败' . $e->getMessage() . 'line' . $e->getLine()); + } + } + + /** + * 使用code获取用户授权信息 + * @param string|null $code + * @return array + */ + public static function userFromCode(string $code = null) + { + $code = $code ?: request()->param('code'); + if (!$code) { + throw new WechatException('无效CODE'); + } + try { + $response = self::oauthService()->setGuzzleOptions(['verify' => false])->userFromCode($code); + + self::logger('使用code获取用户授权信息', compact('code'), $response); + + return $response->getRaw(); + } catch (\Throwable $e) { + throw new WechatException('授权失败' . $e->getMessage() . 'line' . $e->getLine()); + } + } + + /** + * 永久素材上传 + * @param string $path + * @return WechatResponse + * @throws GuzzleException + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public static function uploadImage(string $path) + { + $response = self::materialService()->uploadImage($path); + + self::logger('素材管理-上传附件', compact('path'), $response); + + return new WechatResponse($response); + } + + /** + * 临时素材上传 + * @param string $path + * @param string $type + * @return WechatResponse + * @throws GuzzleException + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public static function temporaryUpload(string $path, string $type = 'image') + { + $response = self::mediaService()->upload($type, $path); + + self::logger('临时素材上传', compact('path', 'type'), $response); + + return new WechatResponse($response); + } +} diff --git a/crmeb/services/wechat/OpenPlatform.php b/crmeb/services/wechat/OpenPlatform.php new file mode 100644 index 0000000..50c83d7 --- /dev/null +++ b/crmeb/services/wechat/OpenPlatform.php @@ -0,0 +1,94 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + + +use crmeb\services\wechat\config\OpenWebConfig; +use EasyWeChat\Factory; +use EasyWeChat\Kernel\Exceptions\BadRequestException; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\OpenPlatform\Application; +use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\HttpFoundation\Request; +use think\facade\Cache; +use think\Response; +use Yurun\Util\Swoole\Guzzle\SwooleHandler; + +/** + * 开放平台 + * Class OpenPlatform + * @package crmeb\services\wechat + */ +class OpenPlatform extends BaseApplication +{ + + /** + * @var OpenWebConfig + */ + protected $config; + + /** + * @var Application + */ + protected $application; + + /** + * OpenPlatform constructor. + */ + public function __construct() + { + /** @var OpenWebConfig config */ + $this->config = app()->make(OpenWebConfig::class); + $this->debug = DefaultConfig::value('logger'); + } + + /** + * @return OpenPlatform + */ + public static function instance() + { + return app()->make(static::class); + } + + /** + * @return Application + */ + public function application() + { + if (!$this->application) { + $this->application = Factory::openPlatform($this->config->all()); + $request = request(); + $this->application['guzzle_handler'] = SwooleHandler::class; + $this->application->rebind('request', new Request($request->get(), $request->post(), [], [], [], $request->server(), $request->getContent())); + $this->application->rebind('cache', new RedisAdapter(Cache::store('redis')->handler())); + } + return $this->application; + } + + /** + * 服务端 + * @return Response + * @throws BadRequestException + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws \ReflectionException + */ + public static function serve(): Response + { + $make = self::instance(); + $make->application()->server->push($make->pushMessageHandler); + $response = $make->application()->server->serve(); + return response($response->getContent()); + } + +} diff --git a/crmeb/services/wechat/Payment.php b/crmeb/services/wechat/Payment.php new file mode 100644 index 0000000..b96634d --- /dev/null +++ b/crmeb/services/wechat/Payment.php @@ -0,0 +1,742 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + + +use crmeb\exceptions\PayException; +use crmeb\services\wechat\config\MiniProgramConfig; +use crmeb\services\wechat\config\OpenAppConfig; +use crmeb\services\wechat\config\OpenWebConfig; +use crmeb\services\wechat\config\PaymentConfig; +use crmeb\services\wechat\config\V3PaymentConfig; +use crmeb\services\wechat\v3pay\ServiceProvider; +use EasyWeChat\Factory; +use EasyWeChat\Kernel\Exceptions\Exception; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\Kernel\Support\Collection; +use EasyWeChat\Payment\Application; +use GuzzleHttp\Exception\GuzzleException; +use Psr\Http\Message\ResponseInterface; +use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\HttpFoundation\Request; +use think\facade\Cache; +use think\facade\Event; +use think\Response; +use Yurun\Util\Swoole\Guzzle\SwooleHandler; +use crmeb\services\wechat\Factory as miniFactory; + + +/** + * 微信支付 + * Class Payment + * @package crmeb\services\wechat + */ +class Payment extends BaseApplication +{ + + /** + * @var PaymentConfig + */ + protected $config; + + /** + * @var + */ + protected $v3Config; + + /** + * 是否v3支付 + * @var bool + */ + public $isV3PAy = true; + + /** + * @var array + */ + protected $application = []; + + /** + * Payment constructor. + * @param PaymentConfig $config + */ + public function __construct(PaymentConfig $config, V3PaymentConfig $v3Config) + { + $this->config = $config; + $this->v3Config = $v3Config; + $this->isV3PAy = $this->v3Config->get('isV3PAy'); + $this->debug = DefaultConfig::value('logger'); + } + + /** + * @return Payment + */ + public static function instance() + { + return app()->make(static::class); + } + + /** + * @return Application|mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/10/11 + */ + public function application() + { + $request = request(); + $config = $this->config->all(); + switch ($accessEnd = $this->getAuthAccessEnd($request)) { + case self::APP: + /** @var OpenAppConfig $make */ + $make = app()->make(OpenAppConfig::class); + $config['app_id'] = $make->get('appId'); + $config['notify_url'] = trim($make->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('app.notifyUrl'); + break; + case self::PC: + /** @var OpenWebConfig $make */ + $make = app()->make(OpenWebConfig::class); + $config['app_id'] = $make->get('appId'); + break; + case self::MINI: + /** @var MiniProgramConfig $make */ + $make = app()->make(MiniProgramConfig::class); + $config['app_id'] = $make->get('appId'); + $config['notify_url'] = trim($make->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('mini.notifyUrl'); + break; + } + + //v3支付配置 + $config['v3_payment'] = $this->v3Config->all(); + + if (!isset($this->application[$accessEnd])) { + $this->application[$accessEnd] = Factory::payment($config); + $this->application[$accessEnd]['guzzle_handler'] = SwooleHandler::class; + $this->application[$accessEnd]->rebind('request', new Request($request->get(), $request->post(), [], [], [], $request->server(), $request->getContent())); + $this->application[$accessEnd]->register(new ServiceProvider()); + $this->application[$accessEnd]->rebind('cache', new RedisAdapter(Cache::store('redis')->handler())); + } + return $this->application[$accessEnd]; + } + + /** + * @return \crmeb\services\wechat\MiniPayment\Application + */ + public function miniApplication($isMerchantPay = false) + { + $request = request(); + $accessEnd = $this->getAuthAccessEnd($request); + if (!$isMerchantPay && $accessEnd !== 'mini') { + throw new PayException('支付方式错误,请刷新后重试!'); + } + $config = $this->config->all(); + /** @var MiniProgramConfig $make */ + $make = app()->make(MiniProgramConfig::class); + $config['app_id'] = $make->get('appId'); + $config['secret'] = $make->get('secret'); + $config['mch_id'] = $this->config->get('routineMchId'); + if (!isset($this->application[$accessEnd])) { + $this->application[$accessEnd] = miniFactory::MiniPayment($config); + $this->application[$accessEnd]['guzzle_handler'] = SwooleHandler::class; + $this->application[$accessEnd]->rebind('request', new Request($request->get(), $request->post(), [], [], [], $request->server(), $request->getContent())); + } + return $this->application[$accessEnd]; + } + + /** + * 付款码支付 + * @param string $authCode + * @param string $outTradeNo + * @param string $totalFee + * @param string $attach + * @param string $body + * @param string $detail + * @return array + * @throws InvalidConfigException + * @throws InvalidArgumentException + * @throws GuzzleException + */ + public static function microPay(string $authCode, string $outTradeNo, string $totalFee, string $attach, string $body, string $detail = '') + { + $application = self::instance()->application(); + $totalFee = bcmul($totalFee, 100, 0); + $response = $application->pay([ + 'auth_code' => $authCode, + 'out_trade_no' => $outTradeNo, + 'total_fee' => (int)$totalFee, + 'attach' => $attach, + 'body' => $body, + 'detail' => $detail + ]); + + self::logger('付款码支付', compact('authCode', 'outTradeNo', 'totalFee', 'attach', 'body', 'detail'), $response); + + //下单成功 + if ($response['return_code'] === 'SUCCESS') { + //扫码付款直接支付成功 + if ($response['result_code'] === 'SUCCESS' && $response['trade_type'] === 'MICROPAY') { + return [ + 'paid' => 1, + 'message' => '支付成功', + 'payInfo' => $response, + ]; + } else { + return [ + 'paid' => 0, + 'message' => $response['err_code_des'], + 'payInfo' => $response + ]; + } + } else { + throw new PayException($response['return_msg']); + } + } + + /** + * 撤销订单 + * @param string $outTradeNo + * @return bool + * @throws InvalidConfigException + */ + public static function reverseOrder(string $outTradeNo) + { + $response = self::instance()->application()->reverse->byOutTradeNumber($outTradeNo); + + self::logger('撤销订单', compact('outTradeNo'), $response); + + if ($response['return_code'] === 'SUCCESS') { + return true; + } else { + throw new PayException($response['return_msg']); + } + } + + /** + * 查询订单支付状态 + * @param string $outTradeNo + * @return array + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public static function queryOrder(string $outTradeNo) + { + $response = self::instance()->application()->order->queryByOutTradeNumber($outTradeNo); + + self::logger('查询订单支付状态', compact('outTradeNo'), $response); + + if ($response['return_code'] === 'SUCCESS') { + if ($response['result_code'] === 'SUCCESS') { + return [ + 'paid' => 1, + 'out_trade_no' => $outTradeNo, + 'payInfo' => $response + ]; + } else { + return [ + 'paid' => 0, + 'out_trade_no' => $outTradeNo, + 'payInfo' => $response + ]; + } + } else { + throw new PayException($response['return_msg']); + } + } + + /** + * 企业付款到零钱 + * @param string $openid openid + * @param string $orderId 订单号 + * @param string $amount 金额 + * @param string $desc 说明 + * @param string $type 类型 + * @return bool + * @throws GuzzleException + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public static function merchantPay(string $openid, string $orderId, string $amount, string $desc, string $type = 'wechat') + { + $application = self::instance()->setAccessEnd($type)->application(); + $config = $application->getConfig(); + if (!isset($config['cert_path'])) { + throw new PayException('企业微信支付到零钱需要支付证书,检测到您没有上传!'); + } + if (!$config['cert_path']) { + throw new PayException('企业微信支付到零钱需要支付证书,检测到您没有上传!'); + } + + if (self::instance()->isV3PAy) { + //v3支付使用发起商家转账API + $res = $application->v3pay->setType($type)->batches( + $orderId, + $amount, + $desc, + $desc, + [ + [ + 'out_detail_no' => $orderId, + 'transfer_amount' => $amount, + 'transfer_remark' => $desc, + 'openid' => $openid + ] + ] + ); + + return $res; + + } else { + $merchantPayData = [ + 'partner_trade_no' => $orderId, //随机字符串作为订单号,跟红包和支付一个概念。 + 'openid' => $openid, //收款人的openid + 'check_name' => 'NO_CHECK', //文档中有三种校验实名的方法 NO_CHECK OPTION_CHECK FORCE_CHECK + 'amount' => (int)bcmul($amount, '100', 0), //单位为分 + 'desc' => $desc, + 'spbill_create_ip' => request()->ip(), //发起交易的IP地址 + ]; + $result = $application->transfer->toBalance($merchantPayData); + + self::logger('企业付款到零钱', compact('merchantPayData'), $result); + + if ($result['return_code'] == 'SUCCESS' && $result['result_code'] != 'FAIL') { + return true; + } else { + throw new PayException(($result['return_msg'] ?? '支付失败') . ':' . ($result['err_code_des'] ?? '发起企业支付到零钱失败')); + } + } + + } + + /** + * 生成支付订单对象 + * @param $openid + * @param $out_trade_no + * @param $total_fee + * @param $attach + * @param $body + * @param string $detail + * @param $trade_type + * @param array $options + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + * @throws InvalidArgumentException + * @throws GuzzleException + */ + public static function paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', array $options = []) + { + $total_fee = bcmul($total_fee, 100, 0); + $order = array_merge(compact('out_trade_no', 'total_fee', 'attach', 'body', 'detail', 'trade_type'), $options); + if (!is_null($openid)) $order['openid'] = $openid; + if ($order['detail'] == '') unset($order['detail']); + $order['spbill_create_ip'] = request()->ip(); + $result = self::instance()->application()->order->unify($order); + + self::logger('生成支付订单对象', compact('order'), $result); + + if ($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS') { + return $result; + } else { + if ($result['return_code'] == 'FAIL') { + throw new PayException('微信支付错误返回:' . $result['return_msg']); + } else if (isset($result['err_code'])) { + throw new PayException('微信支付错误返回:' . $result['err_code_des']); + } else { + throw new PayException('没有获取微信支付的预支付ID,请重新发起支付!'); + } + } + } + + /** + * 生成支付订单对象(小程序商户号支付时) + * @param $openid + * @param $out_trade_no + * @param $total_fee + * @param $attach + * @param $body + * @param string $detail + * @param $trade_type + * @param array $options + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + * @throws InvalidArgumentException + * @throws GuzzleException + */ + public static function paymentMiniOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', array $options = []) + { + $total_fee = bcmul($total_fee, 100, 0); + $order = array_merge(compact('out_trade_no', 'total_fee', 'attach', 'body', 'detail', 'trade_type'), $options); + if (!is_null($openid)) $order['openid'] = $openid; + if ($order['detail'] == '') unset($order['detail']); + $order['spbill_create_ip'] = request()->ip(); + $result = self::instance()->miniApplication()->orders->createorder($order); + self::logger('生成支付订单对象', compact('order'), $result); + if ($result['errcode'] == '0') { + return $result; + } else { + throw new PayException('微信支付错误返回:' . $result['errmsg']); + } + } + + + /** + * 获得jsSdk支付参数 + * @param $openid + * @param $out_trade_no + * @param $total_fee + * @param $attach + * @param $body + * @param string $detail + * @param string $trade_type + * @param array $options + * @return array + */ + public static function jsPay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', $options = []) + { + $paymentPrepare = self::paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options); + $config = self::instance()->application()->jssdk->bridgeConfig($paymentPrepare['prepay_id'], false); + $config['timestamp'] = $config['timeStamp']; + unset($config['timeStamp']); + return $config; + } + + /** + * 获得jsSdk支付参数(小程序商户号支付时) + * @param $openid + * @param $out_trade_no + * @param $total_fee + * @param $attach + * @param $body + * @param string $detail + * @param string $trade_type + * @param array $options + * @return array + */ + public static function miniPay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', $options = []) + { + $paymentPrepare = self::paymentMiniOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options); + $paymentPrepare['payment_params']['timestamp'] = $paymentPrepare['payment_params']['timeStamp']; + return $paymentPrepare['payment_params'] ?? []; + } + + /** + * 获得APP付参数 + * @param $openid + * @param $out_trade_no + * @param $total_fee + * @param $attach + * @param $body + * @param string $detail + * @param string $trade_type + * @param array $options + * @return array|string + */ + public static function appPay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'APP', $options = []) + { + if (self::instance()->isV3PAy) { + return self::instance()->application()->v3pay->appPay($out_trade_no, $total_fee, $body, $attach); + } else { + $paymentPrepare = self::paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options); + return self::instance()->application()->jssdk->appConfig($paymentPrepare['prepay_id']); + } + } + + /** + * 获得native支付参数 + * @param $openid + * @param $out_trade_no + * @param $total_fee + * @param $attach + * @param $body + * @param string $detail + * @param string $trade_type + * @param array $options + * @return array|string + */ + public static function nativePay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'NATIVE', $options = []) + { + $instance = self::instance(); + + if ($instance->isV3PAy) { + $data = $instance->application()->v3pay->nativePay($out_trade_no, $total_fee, $body, $attach); + $res['code_url'] = $data['code_url']; + $res['invalid'] = time() + 60; + $res['logo'] = []; + return $res; + } + + $data = $instance->setAccessEnd(self::WEB)->paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options); + if ($data) { + $res['code_url'] = $data['code_url']; + $res['invalid'] = time() + 60; + $res['logo'] = []; + } else $res = []; + return $res; + } + + /** + * 使用商户订单号退款 + * @param $orderNo + * @param $refundNo + * @param $totalFee + * @param null $refundFee + * @param null $opUserId + * @param string $refundReason + * @param string $type + * @param string $refundAccount + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function refund($orderNo, $refundNo, $totalFee, $refundFee = null, $opUserId = null, string $refundReason = '', string $type = 'out_trade_no', string $refundAccount = 'REFUND_SOURCE_UNSETTLED_FUNDS') + { + $totalFee = floatval($totalFee); + $refundFee = floatval($refundFee); + if ($type == 'out_trade_no') { + $result = $this->application()->refund->byOutTradeNumber($orderNo, $refundNo, $totalFee, $refundFee, [ + 'refund_account' => $refundAccount, + 'notify_url' => self::instance()->config->get('refundUrl'), + 'refund_desc' => $refundReason + ]); + } else { + $result = $this->application()->refund->byTransactionId($orderNo, $refundNo, $totalFee, $refundFee, [ + 'refund_account' => $refundAccount, + 'notify_url' => self::instance()->config->get('refundUrl'), + 'refund_desc' => $refundReason + ]); + } + + self::logger('使用商户订单号退款', compact('orderNo', 'refundNo', 'totalFee', 'refundFee', 'opUserId', 'refundReason', 'type', 'refundAccount'), $result); + + return $result; + } + + /** + * 小程序商户退款 + * @param $orderNo //微信支付单号 + * @param $refundNo //微信退款单号 + * @param $totalFee + * @param null $refundFee + * @param null $opUserId + * @param string $refundReason + * @param string $type + * @param string $refundAccount + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + */ + public function miniRefund($orderNo, $refundNo, $totalFee, $refundFee = null, array $opt = []) + { + $totalFee = floatval($totalFee); + $refundFee = floatval($refundFee); + + $order = [ + 'openid' => $opt['open_id'], + 'trade_no' => $opt['routine_order_id'], + 'transaction_id' => $orderNo, + 'refund_no' => $refundNo, + 'total_amount' => $totalFee, + 'refund_amount' => $refundFee, + ]; + $result = $this->miniApplication()->orders->refundorder($order); + + self::logger('使用商户订单号退款', compact('orderNo', 'refundNo', 'totalFee', 'refundFee', 'opt'), $result); + + return $result; + } + + /** + * 退款 + * @param $orderNo + * @param array $opt + * @return bool + */ + public function payOrderRefund($orderNo, array $opt) + { + if (isset($opt['pay_routine_open']) && $opt['pay_routine_open']) { + return $this->payMiniOrderRefund($orderNo, $opt); + } + if (!isset($opt['pay_price'])) { + throw new PayException('缺少pay_price'); + } + $certPath = $this->config->get('certPath'); + if (!$certPath) { + throw new PayException('请上传支付证书cert'); + } + $keyPath = $this->config->get('keyPath'); + if (!$keyPath) { + throw new PayException('请上传支付证书key'); + } + if (!is_file($certPath)) { + throw new PayException('支付证书cert不存在'); + } + if (!is_file($keyPath)) { + throw new PayException('支付证书key不存在'); + } + + if ($this->isV3PAy) { + return $this->application()->v3pay->refund($orderNo, $opt); + } + + $totalFee = floatval(bcmul($opt['pay_price'], 100, 0)); + $refundFee = isset($opt['refund_price']) ? floatval(bcmul($opt['refund_price'], 100, 0)) : null; + $refundReason = $opt['desc'] ?? ''; + $refundNo = $opt['refund_id'] ?? $orderNo; + $opUserId = $opt['op_user_id'] ?? null; + $type = $opt['type'] ?? 'out_trade_no'; + /*仅针对老资金流商户使用 + REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款) + REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款*/ + $refundAccount = $opt['refund_account'] ?? 'REFUND_SOURCE_UNSETTLED_FUNDS'; + try { + $res = $this->refund($orderNo, $refundNo, $totalFee, $refundFee, $opUserId, $refundReason, $type, $refundAccount); + + if ($res['return_code'] == 'FAIL') { + throw new PayException('退款失败:' . $res['return_msg']); + } + if (isset($res['err_code'])) { + throw new PayException('退款失败:' . $res['err_code_des']); + } + } catch (\Exception $e) { + + self::error($e); + + throw new PayException($e->getMessage()); + } + return true; + } + + /** + * 小程序商户退款 + * @param $orderNo + * @param array $opt + * @return bool + */ + public function payMiniOrderRefund($orderNo, array $opt) + { + if (!isset($opt['pay_price'])) { + throw new PayException('缺少pay_price'); + } + if (!isset($opt['routine_order_id'])) { + throw new PayException('缺少订单单号'); + } + $totalFee = floatval(bcmul($opt['pay_price'], 100, 0)); + $refundFee = isset($opt['refund_price']) ? floatval(bcmul($opt['refund_price'], 100, 0)) : null; + $refundNo = $opt['refund_no']; + try { + $result = $this->miniRefund($orderNo, $refundNo, $totalFee, $refundFee, $opt); + if ($result['errcode'] == '0') { + return true; + } else { + throw new PayException('退款失败:' . $result['errmsg']); + } + } catch (\Exception $e) { + + self::error($e); + + throw new PayException($e->getMessage()); + } + } + + /** + * 微信支付成功回调接口 + * @return Response + * @throws Exception + */ + public function handleNotify() + { + if ($this->isV3PAy) { + $response = $this->application()->v3pay->handleNotify(function ($notify, $success) { + + self::logger('微信支付成功回调接口', [], $notify); + + if (isset($notify['out_trade_no']) && $success) { + $res = Event::until('pay.notify', [$notify]); + if ($res) { + return $res; + } else { + return false; + } + } + + }); + } else { + $response = $this->application()->handlePaidNotify(function ($notify, $fail) { + + self::logger('微信支付成功回调接口', [], $notify); + + if (isset($notify['out_trade_no'])) { + $res = Event::until('pay.notify', [$notify]); + if ($res) { + return $res; + } else { + return $fail('支付通知失败'); + } + } + }); + } + + return response($response->getContent()); + } + + /** + * 扫码支付通知 + * @return Response + * @throws Exception + */ + public static function handleScannedNotify() + { + $make = self::instance(); + $response = $make->application()->handleScannedNotify(function ($message, $fail, $alert) use ($make) { + + self::logger('扫码支付通知', [], $message); + + $res = Event::until('pay.scan.notify', [$message]); + if ($res) { + return $res; + } else { + return $fail('扫码通知支付失败'); + } + }); + + return response($response->getContent()); + } + + /** + * 退款结果通知 + * @return Response + * @throws Exception + */ + public function handleRefundedNotify() + { + $response = $this->application()->handleRefundedNotify(function ($message, $reqInfo, $fail) { + + self::logger('退款结果通知', [], compact('message', 'reqInfo')); + + $res = Event::until('pay.refunded.notify', [$message, $reqInfo]); + if ($res) { + return $res; + } else { + return $fail('扫码通知支付失败'); + } + }); + + return response($response->getContent()); + } + + /** + * 是否时微信付款二维码值 + * @param string $authCode + * @return bool + */ + public static function isWechatAuthCode(string $authCode) + { + return preg_match('/^[0-9]{18}$/', $authCode) && in_array(substr($authCode, 0, 2), ['10', '11', '12', '13', '14', '15']); + } +} diff --git a/crmeb/services/wechat/WechatException.php b/crmeb/services/wechat/WechatException.php new file mode 100644 index 0000000..d0cff7e --- /dev/null +++ b/crmeb/services/wechat/WechatException.php @@ -0,0 +1,35 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + + +use Throwable; + +/** + * 微信错误统一处理 + * Class WechatException + * @package crmeb\services\wechat + */ +class WechatException extends \RuntimeException +{ + + /** + * WechatException constructor. + * @param string $message + * @param int $code + * @param Throwable|null $previous + */ + public function __construct($message = "", $code = 0, Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/crmeb/services/wechat/WechatResponse.php b/crmeb/services/wechat/WechatResponse.php new file mode 100644 index 0000000..8ea8b69 --- /dev/null +++ b/crmeb/services/wechat/WechatResponse.php @@ -0,0 +1,124 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + +use EasyWeChat\Kernel\Support\Collection; + +/** + * 微信错误 + * Class WechatResponse + * @package crmeb\services\wechat + */ +class WechatResponse extends Collection +{ + + /** + * @var \Throwable + */ + protected $e; + + /** + * @var string + */ + protected $response; + + /** + * 是否抛出默认错误 + * @var bool + */ + protected $error = true; + + /** + * WechatResponse constructor. + * @param array $items + */ + public function __construct(array $items = []) + { + parent::__construct($items); + + $this->wechatError(); + } + + /** + * 错误统一处理 + */ + public function wechatError() + { + if (!$this->error) { + return; + } + if (isset($this->items['errcode']) && 0 !== $this->items['errcode']) { + throw new WechatException( + ErrorMessage::getWorkMessage( + $this->items['errcode'] ?? 0, + $this->items['errmsg'] ?? null + ) + ); + } + } + + /** + * @param bool $boole + * @return $this + */ + public function serError(bool $boole) + { + $this->error = $boole; + return $this; + } + + /** + * 正确处理 + * @param callable $then + * @param bool $error + * @return $this + */ + public function then(callable $then, bool $error = null) + { + $error = $error ?: $this->error; + if (0 !== $this->items['errcode'] && $error) { + throw new WechatException($this->items['errmsg']); + } + try { + $this->response = $then(new static($this->items)); + } catch (\Throwable $e) { + $this->e = $e; + } + return $this; + } + + /** + * 异常处理 + * @param callable $catch + * @return $this + */ + public function catch(callable $catch) + { + + if (!$this->e) { + $this->e = new WechatException('success'); + } + + $catch($this->e, $this->items); + + return $this; + } + + /** + * 获取返回值 + * @return string + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/crmeb/services/wechat/Work.php b/crmeb/services/wechat/Work.php new file mode 100644 index 0000000..2dd4af9 --- /dev/null +++ b/crmeb/services/wechat/Work.php @@ -0,0 +1,908 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat; + + +use crmeb\services\wechat\config\WorkConfig; +use crmeb\services\wechat\groupChat\Client; +use crmeb\services\wechat\groupChat\ServiceProvider; +use crmeb\services\wechat\department\ServiceProvider as DepartmentServiceProvider; +use EasyWeChat\Factory; +use EasyWeChat\Kernel\Exceptions\BadRequestException; +use EasyWeChat\Kernel\Exceptions\InvalidArgumentException; +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use GuzzleHttp\Exception\GuzzleException; +use Symfony\Component\HttpFoundation\Request; +use think\facade\Cache; +use think\Response; +use Yurun\Util\Swoole\Guzzle\SwooleHandler; +use EasyWeChat\Work\Application; +use EasyWeChat\Work\ExternalContact\MessageTemplateClient; +use Symfony\Component\Cache\Adapter\RedisAdapter; + +/** + * 企业微信 + * Class Work + * @package crmeb\services\wechat + * @method Client groupChat() 加入群聊配置 + * @method MessageTemplateClient groupChatWelcome() 入群欢迎语素材 + */ +class Work extends BaseApplication +{ + /** + * @var WorkConfig + */ + protected $config; + + /** + * @var Application[] + */ + protected $application = []; + + /** + * @var string + */ + protected $configHandler; + + /** + * @var string[] + */ + protected static $property = [ + 'groupChat' => 'external_contact', + 'groupChatWelcome' => 'external_contact_message_template' + ]; + + /** + * Work constructor. + */ + public function __construct() + { + /** @var WorkConfig config */ + $this->config = app()->make(WorkConfig::class); + $this->debug = DefaultConfig::value('logger'); + } + + /** + * 设置获取配置 + * @param string $handler + * @return $this + */ + public function setConfigHandler(string $handler) + { + $this->configHandler = $handler; + return $this; + } + + /** + * @return Work + */ + public static function instance() + { + return app()->make(static::class); + } + + /** + * 获取实例化句柄 + * @param string $type + * @return Application + */ + public function application(string $type = WorkConfig::TYPE_USER) + { + $config = $this->config->all(); + $config = array_merge($config, $this->config->setHandler($this->configHandler)->getAppConfig($type)); + if (!isset($this->application[$type])) { + $this->application[$type] = Factory::work($config); + $this->application[$type]['guzzle_handler'] = SwooleHandler::class; + $request = request(); + $this->application[$type]->rebind('request', new Request($request->get(), $request->post(), [], [], [], $request->server(), $request->getContent())); + $this->application[$type]->register(new ServiceProvider()); + $this->application[$type]->register(new DepartmentServiceProvider()); + $this->application[$type]->rebind('cache', new RedisAdapter(Cache::store('redis')->handler())); + } + return $this->application[$type]; + } + + /** + * 服务端 + * @return Response + * @throws BadRequestException + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws \ReflectionException + */ + public static function serve(): Response + { + $make = self::instance(); + $make->application()->server->push($make->pushMessageHandler); + $response = $make->application()->server->serve(); + return response($response->getContent()); + } + + /** + * 获取用户授权信息 + * @param string $code + * @return array + */ + public static function getAuthUserInfo(string $code = '') + { + $code = $code ?: request()->param('code'); + if (!$code) { + throw new WechatException('缺少CODE'); + } + try { + $userInfo = self::instance()->application(WorkConfig::TYPE_USER_APP)->oauth->detailed()->userFromCode($code); + + self::logger('获取用户授权信息', compact('code'), $userInfo); + + } catch (\Throwable $e) { + throw new WechatException($e->getMessage()); + } + return $userInfo->getRaw(); + } + + /** + * 创建联系我二维码 + * @param int $channelCodeId + * @param array $users + * @param bool $skipVerify + * @return array + * @throws InvalidConfigException + * @throws GuzzleException + */ + public static function createQrCode(int $channelCodeId, array $users, bool $skipVerify = true) + { + $config = [ + 'skip_verify' => $skipVerify, + 'state' => 'channelCode-' . $channelCodeId, + 'user' => $users, + ]; + + $response = self::instance()->application()->contact_way->create(2, 2, $config); + + self::logger('创建联系我二维码', $config, $response); + + return $response; + } + + /** + * 更新联系我二维码 + * @param int $channelCodeId + * @param array $users + * @param string $wxConfigId + * @param bool $skipVerify + * @return array + * @throws InvalidConfigException + * @throws GuzzleException + */ + public static function updateQrCode(int $channelCodeId, array $users, string $wxConfigId, bool $skipVerify = true) + { + $config = [ + 'skip_verify' => $skipVerify, + 'state' => 'channelCode-' . (string)$channelCodeId, + 'user' => $users, + ]; + + $response = self::instance()->application()->contact_way->update($wxConfigId, $config); + + self::logger('更新联系我二维码', compact('config', 'wxConfigId'), $response); + + return $response; + } + + /** + * 删除联系我二维码 + * @param string $wxConfigId + * @return array + * @throws InvalidConfigException + * @throws GuzzleException + */ + public static function deleteQrCode(string $wxConfigId) + { + $response = self::instance()->application()->contact_way->delete($wxConfigId); + + self::logger('删除联系我二维码', compact('wxConfigId'), $response); + + return $response; + } + + + /** + * 添加企业群发消息模板 + * @param array $msg + * @return WechatResponse + */ + public static function addMsgTemplate(array $msg) + { + $response = self::instance()->application()->external_contact_message->submit($msg); + + self::logger('添加企业群发消息模板', compact('msg'), $response); + + return new WechatResponse($response); + } + + /** + * 获取群发成员发送任务列表 + * @param string $msgId + * @param int|null $limit + * @param string|null $cursor + * @return WechatResponse + * @throws GuzzleException + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public static function getGroupmsgTask(string $msgId, ?int $limit = null, ?string $cursor = null) + { + $response = self::instance()->application()->external_contact_message->getGroupmsgTask($msgId, $limit, $cursor); + + self::logger('获取群发成员发送任务列表', compact('msgId', 'limit', 'cursor'), $response); + + return new WechatResponse($response); + } + + /** + * 获取企业群发成员执行结果 + * @param string $msgId + * @param string $userid + * @param int|null $limit + * @param string|null $cursor + * @return WechatResponse + * @throws GuzzleException + * @throws InvalidArgumentException + * @throws InvalidConfigException + */ + public static function getGroupmsgSendResult(string $msgId, string $userid, ?int $limit = null, ?string $cursor = null) + { + $response = self::instance()->application()->external_contact_message->getGroupmsgSendResult($msgId, $userid, $limit, $cursor); + + self::logger('获取企业群发成员执行结果', compact('msgId', 'userid', 'limit', 'cursor'), $response); + + return new WechatResponse($response); + } + + /** + * 创建发送朋友圈任务 + * @param array $param + * @return WechatResponse + */ + public static function addMomentTask(array $param) + { + $response = self::instance()->application()->external_contact_moment->createTask($param); + + self::logger('创建发送朋友圈任务', compact('param'), $response); + + return new WechatResponse($response); + } + + /** + * 获取发送朋友圈任务创建结果 + * @param string $jobId + * @return WechatResponse + */ + public static function getMomentTask(string $jobId) + { + $response = self::instance()->application()->external_contact_moment->getTask($jobId); + + self::logger('获取发送朋友圈任务创建结果', compact('jobId'), $response); + + return new WechatResponse($response); + } + + + /** + * 获取客户朋友圈企业发表的列表 + * @param string $momentId + * @param string $cursor + * @param int $limit + * @return WechatResponse + */ + public static function getMomentTaskInfo(string $momentId, string $cursor = '', int $limit = 500) + { + $response = self::instance()->application()->external_contact_moment->getTasks($momentId, $cursor, $limit); + + self::logger('获取客户朋友圈企业发表的列表', compact('momentId', 'cursor', 'limit'), $response); + + return new WechatResponse($response); + } + + /** + * 获取客户朋友圈发表时选择的可见范围 + * @param string $momentId + * @param string $userId + * @param string $cursor + * @param int $limit + * @return WechatResponse + */ + public static function getMomentCustomerList(string $momentId, string $userId, string $cursor, int $limit = 500) + { + $response = self::instance()->application()->external_contact_moment->getCustomers($momentId, $userId, $cursor, $limit); + + self::logger('获取客户朋友圈发表时选择的可见范围', compact('momentId', 'cursor', 'userId', 'limit'), $response); + + return new WechatResponse($response); + } + + /** + * 发送应用消息 + * @param array $message + * @return WechatResponse + * @throws GuzzleException + * @throws InvalidConfigException + */ + public static function sendMessage(array $message) + { + $instance = self::instance(); + + if (empty($message['agentid'])) { + $config = $instance->config->getAppConfig(WorkConfig::TYPE_USER_APP); + + if (empty($config['agent_id'])) { + throw new WechatException('请先配置agent_id'); + } + + $message['agentid'] = $config['agent_id']; + } + + $response = $instance->application(WorkConfig::TYPE_USER_APP)->message->send($message); + + self::logger('发送应用消息', compact('message'), $response); + + return new WechatResponse($response); + } + + /** + * 获取部门列表 + * @return mixed + */ + public static function getDepartment() + { + try { + + $response = self::instance()->application(WorkConfig::TYPE_USER_APP)->department->list(); + + self::logger('获取部门列表', [], $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 获取子部门ID列表 + * @return array|mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/10/9 + */ + public static function simpleList() + { + try { + + $response = self::instance()->application(WorkConfig::TYPE_USER_APP)->department->simpleList(); + + self::logger('获取子部门ID列表', [], $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 获取成员ID列表 + * @return array|mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/10/9 + */ + public static function getUserListIds(int $limit, string $cursor = '') + { + try { + + $response = self::instance()->application(WorkConfig::TYPE_USER_APP)->department->getUserListIds($limit, $cursor); + + self::logger('获取成员ID列表', [], $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 获取部门详细信息 + * @param int $id + * @return array|mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/10/10 + */ + public static function getDepartmentInfo(int $id) + { + try { + + $response = self::instance()->application(WorkConfig::TYPE_USER_APP)->department->get($id); + + self::logger('获取部门详细信息', [], $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 获取部门成员详细信息 + * @param int $departmentId + * @return array + */ + public static function getDetailedDepartmentUsers(int $departmentId) + { + try { + + $response = self::instance()->application(WorkConfig::TYPE_USER_APP)->user->getDetailedDepartmentUsers($departmentId, true); + + self::logger('获取部门成员详细信息', compact('departmentId'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 获取通讯录成员详情 + * @param string $userId + * @return array + */ + public static function getMemberInfo(string $userId) + { + try { + + $response = self::instance()->application(WorkConfig::TYPE_USER_APP)->user->get($userId); + + self::logger('获取通讯录成员详情', compact('userId'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * userid转openid + * @param string $userId + * @return mixed|null + */ + public static function useridByOpenid(string $userId) + { + try { + + $response = self::instance()->application(WorkConfig::TYPE_USER_APP)->user->userIdToOpenid($userId); + + self::logger('userid转openid', compact('userId'), $response); + + return $response['openid'] ?? null; + } catch (\Throwable $e) { + + self::error($e); + + return null; + } + } + + /** + * 获取某个成员下的客户信息 + * @param string $externalUserID + * @return array + */ + public static function getClientInfo(string $externalUserID) + { + try { + + $response = self::instance()->application()->external_contact->get($externalUserID); + + self::logger('获取某个成员下的客户信息', compact('externalUserID'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 批量获取客户详情 + * @param array $userids + * @param string $cursor + * @param int $limit + * @return array|mixed|null + */ + public static function getBatchClientList(array $userids, string $cursor = '', int $limit = 100) + { + if ($limit > 100) { + $limit = 100; + } + try { + + $response = self::instance()->application()->external_contact->batchGet($userids, $cursor, $limit); + + self::logger('批量获取客户详情', compact('userids', 'cursor', 'limit'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 获取客户标签 + * @param array $tagIds + * @param array $groupIds + * @return array + */ + public static function getCorpTags(array $tagIds = [], array $groupIds = []) + { + try { + + $response = self::instance()->application()->external_contact->getCorpTags($tagIds, $groupIds); + + self::logger('获取客户标签', compact('tagIds', 'groupIds'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 添加客户标签 + * @param string $groupName + * @param array $tag + * @return array + */ + public static function addCorpTag(string $groupName, array $tag = []) + { + $params = [ + "group_name" => $groupName, + "tag" => $tag + ]; + try { + + $response = self::instance()->application()->external_contact->addCorpTag($params); + + self::logger('添加客户标签', compact('groupName', 'tag'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 编辑客户标签 + * @param string $id + * @param string $name + * @param int $order + * @return array + */ + public static function updateCorpTag(string $id, string $name, int $order = 1) + { + try { + + $response = self::instance()->application()->external_contact->updateCorpTag($id, $name, $order); + + self::logger('编辑客户标签', compact('id', 'name', 'order'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 删除客户标签 + * @param array $tagId + * @param array $groupId + * @return array + */ + public static function deleteCorpTag(array $tagId, array $groupId) + { + try { + + $response = self::instance()->application()->external_contact->deleteCorpTag($tagId, $groupId); + + self::logger('删除客户标签', compact('tagId', 'groupId'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 编辑客户标签 + * @param string $userid + * @param string $externalUserid + * @param array $addTag + * @param array $removeTag + * @return array + */ + public static function markTags(string $userid, string $externalUserid, array $addTag = [], array $removeTag = []) + { + $params = [ + "userid" => $userid, + "external_userid" => $externalUserid, + "add_tag" => $addTag, + "remove_tag" => $removeTag + ]; + try { + + $response = self::instance()->application()->external_contact->markTags($params); + + self::logger('编辑客户标签', compact('params'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 获取客户群列表 + * @param array $useridList + * @param string $offset + * @return array + */ + public static function getGroupChats(array $useridList = [], int $limit = 100, string $offset = null) + { + $params = [ + "status_filter" => 0, + "owner_filter" => [ + "userid_list" => $useridList, + ], + "limit" => $limit + ]; + + if ($offset) { + $params['cursor'] = $offset; + } + + try { + + $response = self::instance()->application()->external_contact->getGroupChats($params); + + self::logger('获取客户群列表', compact('params'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 获取群详情 + * @param string $chatId + * @return array + */ + public static function getGroupChat(string $chatId) + { + try { + + $response = self::instance()->application()->external_contact->getGroupChat($chatId); + + self::logger('获取群详情', compact('chatId'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 获取群聊数据统计 + * @param int $dayBeginTime + * @param int $dayEndTime + * @param array $userIds + * @return array + */ + public static function groupChatStatisticGroupByDay(int $dayBeginTime, int $dayEndTime, array $userIds) + { + try { + + $response = self::instance()->application()->external_contact_statistics->groupChatStatisticGroupByDay($dayBeginTime, $dayEndTime, $userIds); + + self::logger('获取群聊数据统计', compact('dayBeginTime', 'dayEndTime', 'userIds'), $response); + + return $response; + } catch (\Throwable $e) { + + self::error($e); + + return []; + } + } + + /** + * 发送欢迎语 + * @param string $welcomeCode + * @param array $message + * @return array|WechatResponse + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws GuzzleException + */ + public static function sendWelcome(string $welcomeCode, array $message) + { + $response = self::instance()->application()->external_contact_message->sendWelcome($welcomeCode, $message); + + self::logger('发送欢迎语', compact('welcomeCode', 'message'), $response); + + return new WechatResponse($response); + } + + /** + * 上传临时素材 + * @param string $path + * @param string $type + * @return WechatResponse + * @throws InvalidConfigException + * @throws GuzzleException + */ + public static function mediaUpload(string $path, string $type = 'image') + { + $response = self::instance()->application()->media->upload($type, $path); + + self::logger('上传临时素材', compact('type', 'path'), $response); + + return new WechatResponse($response); + } + + /** + * 上传附件资源 + * @param string $path + * @param string $mediaType + * @param string $attachmentType + * @return WechatResponse + * @throws GuzzleException + * @throws InvalidConfigException + */ + public static function mediaUploadAttachment(string $path, string $mediaType = 'image', string $attachmentType = '1') + { + if (in_array($mediaType, ['video', 'file', 'voice'])) { + + $url = 'https://qyapi.weixin.qq.com/cgi-bin/media/upload_attachment'; + $url .= '?access_token=' . self::instance()->application()->external_contact->getAccessToken()->getToken()['access_token']; + $url .= '&media_type=' . $mediaType . '&attachment_type=' . $attachmentType; + + + $pathAtt = explode('/', $path); + $filename = $pathAtt[count($pathAtt) - 1]; + $file = new \think\File($path); + $request = new \Yurun\Util\HttpRequest(); + $request->header('Content-Type', 'multipart/form-data'); + $fileuploade = new \Yurun\Util\YurunHttp\Http\Psr7\UploadedFile($filename, $file->getMime(), $path); + $res = $request->requestBody([$filename => $fileuploade])->post($url); + $response = json_decode($res->body(), true); + } else { + $response = self::instance()->application()->external_contact->uploadAttachment($path, $mediaType, $attachmentType); + } + + self::logger('上传附件资源', compact('path', 'mediaType', 'attachmentType'), $response); + + return new WechatResponse($response); + } + + /** + * 获取临时素材 + * @param string $mediaId + * @return array + * @throws InvalidConfigException + * @throws GuzzleException + */ + public static function getMedia(string $mediaId) + { + $response = self::instance()->application()->media->get($mediaId); + + self::logger('获取临时素材', compact('mediaId'), $response); + + return new WechatResponse($response); + } + + /** + * 获取jsSDK权限配置 + * @return array|object + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public static function getJsSDK(string $url = '') + { + try { + $jsSDK = self::instance()->application(WorkConfig::TYPE_USER_APP)->jssdk; + if ($url) { + $jsSDK->setUrl($url); + } + return $jsSDK->buildConfig(['getCurExternalContact', 'getCurExternalChat', 'getContext', 'chooseImage'], false, false, false); + } catch (\Throwable $e) { + return (object)[]; + } + } + + /** + * 获取应用配置信息 + * @param string|null $url + * @return array|string + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public static function getAgentConfig(string $url = null) + { + + $instance = self::instance(); + $jsSDK = $instance->application(WorkConfig::TYPE_USER_APP)->jssdk; + + $config = $instance->config->getAppConfig(WorkConfig::TYPE_USER_APP); + + if (empty($config['agent_id'])) { + throw new WechatException('请先配置agent_id'); + } + + try { + return $jsSDK->getAgentConfigArray(['getCurExternalContact', 'getCurExternalChat', 'getContext', 'chooseImage'], $config['agent_id'], false, false, [], $url); + } catch (\Throwable $e) { + return (object)[]; + } + + } +} diff --git a/crmeb/services/wechat/config/HttpCommonConfig.php b/crmeb/services/wechat/config/HttpCommonConfig.php new file mode 100644 index 0000000..759a802 --- /dev/null +++ b/crmeb/services/wechat/config/HttpCommonConfig.php @@ -0,0 +1,97 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\config; + +use crmeb\services\wechat\contract\ConfigHandlerInterface; +use crmeb\services\wechat\contract\ServeConfigInterface; +use crmeb\services\wechat\DefaultConfig; + +/** + * Http请求配置 + * Class HttpCommonConfig + * @package crmeb\services\wechat\config + */ +class HttpCommonConfig implements ConfigHandlerInterface +{ + /** + * @var bool[] + */ + protected $config = [ + 'verify' => false, + ]; + + /** + * @var string + */ + protected $serve; + + /** + * @param string $serve + * @return $this + */ + public function setServe(string $serve) + { + $this->serve = $serve; + return $this; + } + + /** + * 获取服务端实例 + * @return ServeConfigInterface + */ + public function getServe() + { + return app()->make($this->serve); + } + + /** + * 直接获取配置 + * @param string $key + * @param null $default + * @return mixed + */ + public function getConfig(string $key, $default = null) + { + return $this->getServe()->getConfig(DefaultConfig::value($key), $default); + } + + /** + * @param string $key + * @param $value + * @return $this|mixed + */ + public function set(string $key, $value) + { + $this->config[$key] = $value; + return $this; + } + + /** + * @param string|null $key + * @return bool|bool[]|mixed + */ + public function get(string $key = null) + { + if ($key) { + return $this->config[$key]; + } + return $this->config; + } + + /** + * @return array|bool[] + */ + public function all(): array + { + return $this->config; + } +} diff --git a/crmeb/services/wechat/config/LogCommonConfig.php b/crmeb/services/wechat/config/LogCommonConfig.php new file mode 100644 index 0000000..7cbbb19 --- /dev/null +++ b/crmeb/services/wechat/config/LogCommonConfig.php @@ -0,0 +1,77 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\config; + + +use crmeb\services\wechat\contract\ConfigHandlerInterface; + +/** + * 日志 + * Class LogCommonConfig + * @package crmeb\services\wechat\config + */ +class LogCommonConfig implements ConfigHandlerInterface +{ + + /** + * 日志配置 + * @var array + */ + protected $log = []; + + /** + * LogCommonConfig constructor. + */ + public function __construct() + { + $default = env('APP_DEBUG', false) ? 'prod' : 'dev'; + $this->log = [ + 'default' => $default, // 默认使用的 channel,生产环境可以改为下面的 prod + 'channels' => [ + // 测试环境 + 'dev' => [ + 'driver' => 'single', + 'path' => runtime_path() . 'easywechat.log', + 'level' => 'debug', + ], + // 生产环境 + 'prod' => [ + 'driver' => 'daily', + 'path' => runtime_path() . 'easywechat.log', + 'level' => 'info', + ], + ], + ]; + } + + public function set(string $key, $value) + { + + } + + /** + * @param string|null $key + * @return array|mixed + */ + public function get(string $key = null) + { + return $this->log; + } + + /** + * @return array + */ + public function all(): array + { + return $this->log; + } +} diff --git a/crmeb/services/wechat/config/MicroMerchantConfig.php b/crmeb/services/wechat/config/MicroMerchantConfig.php new file mode 100644 index 0000000..89fd12f --- /dev/null +++ b/crmeb/services/wechat/config/MicroMerchantConfig.php @@ -0,0 +1,22 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\config; + +/** + * 小微商户 + * Class MicroMerchantConfig + * @package crmeb\services\wechat\config + */ +class MicroMerchantConfig +{ + +} diff --git a/crmeb/services/wechat/config/MiniProgramConfig.php b/crmeb/services/wechat/config/MiniProgramConfig.php new file mode 100644 index 0000000..f79af70 --- /dev/null +++ b/crmeb/services/wechat/config/MiniProgramConfig.php @@ -0,0 +1,156 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\config; + + +use crmeb\services\wechat\contract\ConfigHandlerInterface; +use crmeb\services\wechat\DefaultConfig; + +/** + * 小程序配置 + * Class MiniProgramConfig + * @package crmeb\services\wechat\config + */ +class MiniProgramConfig implements ConfigHandlerInterface +{ + + /** + * APPid + * @var string + */ + protected $appId; + + /** + * APPsecret + * @var string + */ + protected $secret; + + /** + * Token + * @var string + */ + protected $token; + + /** + * EncodingAESKey + * @var string + */ + protected $aesKey; + + /** + * @var string + */ + protected $responseType = 'array'; + + /** + * 日志记录 + * @var LogCommonConfig + */ + protected $logConfig; + + /** + * http配置 + * @var HttpCommonConfig + */ + protected $httpConfig; + + /** + * 是否初始化过 + * @var bool + */ + protected $init = false; + + /** + * MiniProgramConfig constructor. + * @param LogCommonConfig $config + * @param HttpCommonConfig $commonConfig + */ + public function __construct(LogCommonConfig $config, HttpCommonConfig $commonConfig) + { + $this->logConfig = $config; + $this->httpConfig = $commonConfig; + } + + /** + * 初始化 + */ + protected function init() + { + if ($this->init) { + return; + } + $this->init = true; + $this->appId = $this->appId ?: $this->httpConfig->getConfig(DefaultConfig::MINI_APPID, ''); + $this->secret = $this->secret ?: $this->httpConfig->getConfig('mini.secret', ''); + $this->token = $this->token ?: $this->httpConfig->getConfig('mini.token', ''); + $this->aesKey = $this->aesKey ?: $this->httpConfig->getConfig('mini.key', ''); + } + + + /** + * 获取配置 + * @param string $key + * @param null $default + * @return mixed + */ + public function getConfig(string $key, $default = null) + { + return $this->httpConfig->getConfig($key, $default); + } + + /** + * 设置 + * @param string $key + * @param $value + * @return $this|mixed + */ + public function set(string $key, $value) + { + $this->{$key} = $value; + return $this; + } + + /** + * @param string|null $key + * @return array|mixed + */ + public function get(string $key = null) + { + $this->init(); + if ('log' === $key) { + return $this->logConfig->all(); + } + if ('http' === $key) { + return $this->httpConfig->all(); + } + return $this->{$key}; + } + + /** + * 全部 + * @return array + */ + public function all(): array + { + $this->init(); + return [ + 'app_id' => $this->appId, + 'secret' => $this->secret, + 'token' => $this->token, + 'aes_key' => $this->aesKey, + 'response_type' => $this->responseType, + 'log' => $this->logConfig->all(), + 'http' => $this->httpConfig->all() + ]; + } +} diff --git a/crmeb/services/wechat/config/OfficialAccountConfig.php b/crmeb/services/wechat/config/OfficialAccountConfig.php new file mode 100644 index 0000000..4c8c980 --- /dev/null +++ b/crmeb/services/wechat/config/OfficialAccountConfig.php @@ -0,0 +1,147 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\config; + + +use crmeb\services\wechat\contract\ConfigHandlerInterface; + +/** + * 公众号配置 + * Class OfficialAccountConfig + * @package crmeb\services\wechat\config + */ +class OfficialAccountConfig implements ConfigHandlerInterface +{ + + /** + * AppID + * @var string + */ + protected $appId; + + /** + * AppSecret + * @var string + */ + protected $secret; + + /** + * Token + * @var string + */ + protected $token; + + /** + * EncodingAESKey + * @var string + */ + protected $aesKey; + + /** + * 指定 API 调用返回结果的类型 + * @var string + */ + protected $responseType = 'array'; + + /** + * @var LogCommonConfig + */ + protected $logConfig; + + /** + * @var HttpCommonConfig + */ + protected $httpConfig; + + /** + * @var bool + */ + protected $init = false; + + /** + * OfficialAccountConfig constructor. + * @param LogCommonConfig $config + * @param HttpCommonConfig $commonConfig + */ + public function __construct(LogCommonConfig $config, HttpCommonConfig $commonConfig) + { + $this->logConfig = $config; + $this->httpConfig = $commonConfig; + } + + /** + * 初始化 + */ + protected function init() + { + if ($this->init) { + return; + } + $this->init = true; + $this->appId = $this->appId ?: $this->httpConfig->getConfig('official.appid', ''); + $this->secret = $this->secret ?: $this->httpConfig->getConfig('official.secret', ''); + $this->token = $this->token ?: $this->httpConfig->getConfig('official.token', ''); + $this->aesKey = $this->aesKey ?: ($this->httpConfig->getConfig('official.encode', -1) > 0 ? $this->httpConfig->getConfig('official.key', '') : ''); + } + + /** + * 设置 + * @param string $key + * @param $value + * @return $this|mixed + */ + public function set(string $key, $value) + { + $this->{$key} = $value; + return $this; + } + + /** + * 获取配置 + * @param string|null $key + * @return array|mixed|null + */ + public function get(string $key = null) + { + $this->init(); + if ($key) { + if (isset($this->{$key})) { + return $this->{$key}; + } + if ('log' === $key) { + return $this->logConfig->all(); + } + if ('http' === $key) { + return $this->httpConfig->all(); + } + } + return null; + } + + /** + * 获取所有配置 + * @return array + */ + public function all(): array + { + $this->init(); + return [ + 'app_id' => $this->appId, + 'secret' => $this->secret, + 'token' => $this->token, + 'aes_key' => $this->aesKey, + 'response_type' => $this->responseType, + 'log' => $this->logConfig->all(), + 'http' => $this->httpConfig->all() + ]; + } +} diff --git a/crmeb/services/wechat/config/OpenAppConfig.php b/crmeb/services/wechat/config/OpenAppConfig.php new file mode 100644 index 0000000..7d6048d --- /dev/null +++ b/crmeb/services/wechat/config/OpenAppConfig.php @@ -0,0 +1,36 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\config; + +/** + * 开放平台APP配置 + * Class OpenAppConfig + * @package crmeb\services\wechat\config + */ +class OpenAppConfig extends OpenWebConfig +{ + + /** + * OpenAppConfig constructor. + */ + public function init() + { + if ($this->init) { + return; + } + $this->init = true; + $this->appId = $this->appId ?: $this->config->getConfig('app.appid', ''); + $this->secret = $this->secret ?: $this->config->getConfig('app.secret', ''); + $this->token = $this->token ?: $this->config->getConfig('app.token', ''); + $this->aesKey = $this->aesKey ?: $this->config->getConfig('app.key', ''); + } +} diff --git a/crmeb/services/wechat/config/OpenWebConfig.php b/crmeb/services/wechat/config/OpenWebConfig.php new file mode 100644 index 0000000..bb3291e --- /dev/null +++ b/crmeb/services/wechat/config/OpenWebConfig.php @@ -0,0 +1,130 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\config; + + +use crmeb\services\wechat\contract\ConfigHandlerInterface; + +/** + * 开放平台网页端配置 + * Class OpenWebConfig + * @package crmeb\services\wechat\config + */ +class OpenWebConfig implements ConfigHandlerInterface +{ + + /** + * Appid + * @var string + */ + protected $appId; + + /** + * Appsecret + * @var string + */ + protected $secret; + + /** + * @var string + */ + protected $token; + + /** + * @var string + */ + protected $aesKey; + + /** + * @var bool + */ + protected $init = false; + + /** + * @var HttpCommonConfig + */ + protected $config; + + /** + * OpenWebConfig constructor. + * @param HttpCommonConfig $config + */ + public function __construct(HttpCommonConfig $config) + { + $this->config = $config; + } + + /** + * OpenWebConfig constructor. + */ + public function init() + { + if ($this->init) { + return; + } + $this->init = true; + $this->appId = $this->appId ?: $this->config->getConfig('web.appid', ''); + $this->secret = $this->secret ?: $this->config->getConfig('web.secret', ''); + $this->token = $this->token ?: $this->config->getConfig('web.token', ''); + $this->aesKey = $this->aesKey ?: $this->config->getConfig('web.key', ''); + } + + /** + * 获取配置 + * @param string $key + * @param null $default + * @return mixed + */ + public function getConfig(string $key, $default = null) + { + return $this->config->getConfig($key, $default); + } + + /** + * @param string $key + * @param $value + * @return $this|mixed + */ + public function set(string $key, $value) + { + $this->{$key} = $value; + return $this; + } + + /** + * @param string|null $key + * @return mixed + */ + public function get(string $key = null) + { + $this->init(); + if ('http' === $key) { + return $this->config->all(); + } + return $this->{$key}; + } + + /** + * @return array + */ + public function all(): array + { + $this->init(); + return [ + 'app_id' => $this->appId, + 'secret' => $this->secret, + 'token' => $this->token, + 'aes_key' => $this->aesKey, + 'http' => $this->config->all() + ]; + } +} diff --git a/crmeb/services/wechat/config/PaymentConfig.php b/crmeb/services/wechat/config/PaymentConfig.php new file mode 100644 index 0000000..8e12e4a --- /dev/null +++ b/crmeb/services/wechat/config/PaymentConfig.php @@ -0,0 +1,174 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\config; + +use crmeb\services\wechat\contract\ConfigHandlerInterface; +use crmeb\services\wechat\DefaultConfig; + +/** + * 支付配置 + * Class PaymentConfig + * @package crmeb\services\wechat\config + */ +class PaymentConfig implements ConfigHandlerInterface +{ + + /** + * appid + * @var string + */ + protected $appId; + + /** + * 商户密钥 + * @var string + */ + protected $mchId; + + /** + * 小程序商户号 + * @var string + */ + protected $routineMchId; + + /** + * API密钥 + * @var string + */ + protected $key; + + /** + * 证书cert + * @var string + */ + protected $certPath; + + /** + * 证书key + * @var string + */ + protected $keyPath; + + /** + * 支付异步回调地址 + * @var string + */ + protected $notifyUrl; + + /** + * 退款异步通知 + * @var string + */ + protected $refundUrl; + + /** + * @var LogCommonConfig + */ + protected $logConfig; + + /** + * @var HttpCommonConfig + */ + protected $httpConfig; + + /** + * @var bool + */ + protected $init = false; + + /** + * PaymentConfig constructor. + * @param LogCommonConfig $config + * @param HttpCommonConfig $commonConfig + */ + public function __construct(LogCommonConfig $config, HttpCommonConfig $commonConfig) + { + $this->logConfig = $config; + $this->httpConfig = $commonConfig; + } + + + protected function init() + { + if ($this->init) { + return; + } + $this->init = true; + $this->appId = $this->appId ?: $this->httpConfig->getConfig(DefaultConfig::OFFICIAL_APPID, ''); + $this->mchId = $this->mchId ?: $this->httpConfig->getConfig(DefaultConfig::PAY_MCHID, ''); + $this->routineMchId = $this->routineMchId ?: $this->httpConfig->getConfig('pay.routine_mchid', ''); + $this->key = $this->key ?: $this->httpConfig->getConfig('pay.key', ''); + $this->certPath = $this->certPath ?: str_replace('//', '/', public_path() . $this->httpConfig->getConfig('pay.client_cert', '')); + $this->keyPath = $this->keyPath ?: str_replace('//', '/', public_path() . $this->httpConfig->getConfig('pay.client_key', '')); + $this->notifyUrl = $this->notifyUrl ?: trim($this->httpConfig->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('pay.notifyUrl'); + $this->refundUrl = $this->refundUrl ?: trim($this->httpConfig->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('pay.refundUrl'); + } + + /** + * 获取配置 + * @param string $key + * @param null $default + * @return mixed + */ + public function getConfig(string $key, $default = null) + { + return $this->httpConfig->getConfig($key, $default); + } + + /** + * 设置单个配置 + * @param string $key + * @param $value + * @return $this|mixed + */ + public function set(string $key, $value) + { + $this->{$key} = $value; + return $this; + } + + /** + * 获取单个配置 + * @param string|null $key + * @return array|mixed + */ + public function get(string $key = null) + { + $this->init(); + if ('log' === $key) { + return $this->logConfig->all(); + } + if ('http' === $key) { + return $this->httpConfig->all(); + } + return $this->{$key}; + } + + /** + * 全部配置 + * @return array + */ + public function all(): array + { + $this->init(); + return [ + 'app_id' => $this->appId, + 'mch_id' => $this->mchId, + 'key' => $this->key, + 'cert_path' => $this->certPath, + 'key_path' => $this->keyPath, + 'notify_url' => $this->notifyUrl, + 'log' => $this->logConfig->all(), + 'http' => $this->httpConfig->all() + ]; + } +} diff --git a/crmeb/services/wechat/config/V3PaymentConfig.php b/crmeb/services/wechat/config/V3PaymentConfig.php new file mode 100644 index 0000000..7e6e75b --- /dev/null +++ b/crmeb/services/wechat/config/V3PaymentConfig.php @@ -0,0 +1,195 @@ + + * +---------------------------------------------------------------------- + */ + +namespace crmeb\services\wechat\config; + + +use crmeb\services\wechat\contract\ConfigHandlerInterface; +use crmeb\services\wechat\DefaultConfig; + +/** + * Class V3PaymentConfig + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/9/30 + * @package crmeb\services\wechat\config + */ +class V3PaymentConfig implements ConfigHandlerInterface +{ + + /** + * appid + * @var string + */ + protected $appId; + + /** + * 商户密钥 + * @var string + */ + protected $mchId; + + /** + * API密钥 + * @var string + */ + protected $key; + + /** + * 证书序列号 + * @var string + */ + protected $serialNo; + + /** + * 证书cert + * @var string + */ + protected $certPath; + + /** + * 证书key + * @var string + */ + protected $keyPath; + + /** + * 支付异步回调地址 + * @var string + */ + protected $notifyUrl; + + /** + * 退款异步通知 + * @var string + */ + protected $refundUrl; + + /** + * 是否v3支付 + * @var bool + */ + protected $isV3PAy = true; + + /** + * @var LogCommonConfig + */ + protected $logConfig; + + /** + * @var HttpCommonConfig + */ + protected $httpConfig; + + /** + * @var bool + */ + protected $init = false; + + /** + * PaymentConfig constructor. + * @param LogCommonConfig $config + * @param HttpCommonConfig $commonConfig + */ + public function __construct(LogCommonConfig $config, HttpCommonConfig $commonConfig) + { + $this->logConfig = $config; + $this->httpConfig = $commonConfig; + } + + protected function init() + { + if ($this->init) { + return; + } + $this->init = true; + $this->appId = $this->appId ?: $this->httpConfig->getConfig(DefaultConfig::OFFICIAL_APPID, ''); + $this->mchId = $this->mchId ?: $this->httpConfig->getConfig(DefaultConfig::PAY_MCHID, ''); + $this->serialNo = $this->serialNo ?: $this->httpConfig->getConfig('v3_pay.serial_no', ''); + $this->key = $this->key ?: $this->httpConfig->getConfig('v3_pay.key', ''); + $this->isV3PAy = !!$this->httpConfig->getConfig('v3_pay.pay_type', false); + $this->certPath = $this->certPath ?: str_replace('//', '/', public_path() . $this->httpConfig->getConfig('pay.client_cert', '')); + $this->keyPath = $this->keyPath ?: str_replace('//', '/', public_path() . $this->httpConfig->getConfig('pay.client_key', '')); + $this->notifyUrl = $this->notifyUrl ?: trim($this->httpConfig->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('pay.notifyUrl'); + $this->refundUrl = $this->refundUrl ?: trim($this->httpConfig->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('pay.refundUrl'); + } + + /** + * @param string $key + * @param $value + * @return $this|mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/9/30 + */ + public function set(string $key, $value) + { + $this->{$key} = $value; + return $this; + } + + /** + * @param string|null $key + * @return array|bool[]|false[]|mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/9/30 + */ + public function get(string $key = null) + { + $this->init(); + if ('log' === $key) { + return $this->logConfig->all(); + } + if ('http' === $key) { + return $this->httpConfig->all(); + } + return $this->{$key}; + } + + /** + * @return array + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/9/30 + */ + public function all(): array + { + $this->init(); + return [ + 'app_id' => $this->appId, + 'serial_no' => $this->serialNo, + 'mch_id' => $this->mchId, + 'key' => $this->key, + 'cert_path' => $this->certPath, + 'key_path' => $this->keyPath, + 'notify_url' => $this->notifyUrl, + 'log' => $this->logConfig->all(), + 'http' => $this->httpConfig->all(), + 'other' => [ + 'wechat' => [ + 'appid' => $this->httpConfig->getConfig(DefaultConfig::OFFICIAL_APPID, ''), + ], + 'web' => [ + 'appid' => $this->httpConfig->getConfig(DefaultConfig::WEB_APPID, ''), + ], + 'app' => [ + 'appid' => $this->httpConfig->getConfig(DefaultConfig::APP_APPID, ''), + ], + 'miniprog' => [ + 'appid' => $this->httpConfig->getConfig(DefaultConfig::MINI_APPID, ''), + ] + ], + ]; + } +} diff --git a/crmeb/services/wechat/config/WorkConfig.php b/crmeb/services/wechat/config/WorkConfig.php new file mode 100644 index 0000000..ce611c1 --- /dev/null +++ b/crmeb/services/wechat/config/WorkConfig.php @@ -0,0 +1,188 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\config; + + +use crmeb\services\wechat\contract\ConfigHandlerInterface; +use crmeb\services\wechat\contract\WorkAppConfigHandlerInterface; +use crmeb\services\wechat\DefaultConfig; + +/** + * 企业微信配置 + * Class WorkConfig + * @package crmeb\services\wechat\config + */ +class WorkConfig implements ConfigHandlerInterface +{ + + //应用 + const TYPE_APP = 'app'; + //客户联系 + const TYPE_USER = 'user'; + //通讯录同步 + const TYPE_ADDRESS = 'address'; + //客服 + const TYPE_KEFU = 'kefu'; + //审批 + const TYPE_APPROVE = 'approve'; + //会议室 + const TYPE_MEETING = 'meeting'; + //自建应用 + const TYPE_USER_APP = 'build'; + + /** + * @var string + */ + protected $corpId; + + /** + * @var string + */ + protected $token; + + /** + * @var string + */ + protected $aesKey; + + /** + * @var string + */ + protected $responseType = 'array'; + + /** + * @var LogCommonConfig + */ + protected $logConfig; + + /** + * @var HttpCommonConfig + */ + protected $httpConfig; + + /** + * @var bool + */ + protected $init = false; + + /** + * @var WorkAppConfigHandlerInterface + */ + protected $handler; + + /** + * @var array + */ + protected $appConfig; + + /** + * WorkConfig constructor. + * @param LogCommonConfig $config + * @param HttpCommonConfig $commonConfig + */ + public function __construct(LogCommonConfig $config, HttpCommonConfig $commonConfig) + { + $this->logConfig = $config; + $this->httpConfig = $commonConfig; + } + + protected function init() + { + if ($this->init) { + return; + } + $this->init = true; + $this->corpId = $this->corpId ?: $this->httpConfig->getConfig(DefaultConfig::WORK_CORP_ID, ''); + $this->token = $this->token ?: $this->httpConfig->getConfig('work.token', ''); + $this->aesKey = $this->aesKey ?: $this->httpConfig->getConfig('work.key', ''); + } + + /** + * 设置 + * @param string $key + * @param $value + * @return $this|mixed + */ + public function set(string $key, $value) + { + $this->{$key} = $value; + return $this; + } + + /** + * 获取配置 + * @param string|null $key + * @return array|mixed|null + */ + public function get(string $key = null) + { + $this->init(); + if ($key) { + if (isset($this->{$key})) { + return $this->{$key}; + } + if ('log' === $key) { + return $this->logConfig->all(); + } + if ('http' === $key) { + return $this->httpConfig->all(); + } + } + return null; + } + + /** + * 获取全部值 + * @return array + */ + public function all(): array + { + $this->init(); + return [ + 'corp_id' => $this->corpId, + 'token' => $this->token, + 'aes_key' => $this->aesKey, + 'response_type' => $this->responseType, + 'log' => $this->logConfig->all(), + 'http' => $this->httpConfig->all() + ]; + } + + /** + * 获取应用配置 + * @param string $type + * @return array + */ + public function getAppConfig(string $type): array + { + if (!isset($this->appConfig[$type])) { + /** @var WorkAppConfigHandlerInterface $make */ + $make = app()->make($this->handler); + if (!$this->corpId) { + $this->init(); + } + $this->appConfig[$type] = $make->getAppConfig($this->corpId, $type); + } + return $this->appConfig[$type]; + } + + /** + * 设置 + * @param string $handler + * @return $this + */ + public function setHandler(string $handler) + { + $this->handler = $handler; + return $this; + } +} diff --git a/crmeb/services/wechat/contract/BaseApplicationInterface.php b/crmeb/services/wechat/contract/BaseApplicationInterface.php new file mode 100644 index 0000000..62548af --- /dev/null +++ b/crmeb/services/wechat/contract/BaseApplicationInterface.php @@ -0,0 +1,30 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\contract; + +/** + * Interface BaseApplicationInterface + * @package crmeb\services\wechat\contract + */ +interface BaseApplicationInterface +{ + + /** + * @return mixed + */ + public static function instance(); + + /** + * @return mixed + */ + public function application(); +} diff --git a/crmeb/services/wechat/contract/ConfigHandlerInterface.php b/crmeb/services/wechat/contract/ConfigHandlerInterface.php new file mode 100644 index 0000000..72064df --- /dev/null +++ b/crmeb/services/wechat/contract/ConfigHandlerInterface.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\contract; + +/** + * 配置 + * Interface ConfigHandlerInterface + * @package crmeb\services\wechat\contract + */ +interface ConfigHandlerInterface +{ + + /** + * 设置 + * @param string $key + * @param $value + * @return mixed + */ + public function set(string $key, $value); + + /** + * 获取单个 + * @param string|null $key + * @return mixed + */ + public function get(string $key = null); + + /** + * 获取全部 + * @return array + */ + public function all(): array; +} diff --git a/crmeb/services/wechat/contract/ServeConfigInterface.php b/crmeb/services/wechat/contract/ServeConfigInterface.php new file mode 100644 index 0000000..d093542 --- /dev/null +++ b/crmeb/services/wechat/contract/ServeConfigInterface.php @@ -0,0 +1,28 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\contract; + +/** + * Interface ServeConfigInterface + * @package crmeb\services\wechat\contract + */ +interface ServeConfigInterface +{ + + /** + * @param string $key + * @param null $default + * @return mixed + */ + public function getConfig(string $key, $default = null); + +} diff --git a/crmeb/services/wechat/contract/WorkAppConfigHandlerInterface.php b/crmeb/services/wechat/contract/WorkAppConfigHandlerInterface.php new file mode 100644 index 0000000..e7ebd70 --- /dev/null +++ b/crmeb/services/wechat/contract/WorkAppConfigHandlerInterface.php @@ -0,0 +1,30 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\contract; + +/** + * 企业微信获取应用配置 + * Interface WorkAppConfigHandlerInterface + * @package crmeb\services\wechat\contract + */ +interface WorkAppConfigHandlerInterface +{ + + /** + * 获取应用配置 + * @param string $corpId + * @param string $type 应用标识 + * @return array + */ + public function getAppConfig(string $corpId, string $type): array; + +} diff --git a/crmeb/services/wechat/department/Client.php b/crmeb/services/wechat/department/Client.php new file mode 100644 index 0000000..505a59f --- /dev/null +++ b/crmeb/services/wechat/department/Client.php @@ -0,0 +1,45 @@ + + * +---------------------------------------------------------------------- + */ + +namespace crmeb\services\wechat\department; + +use EasyWeChat\Work\Department\Client as WorkClient; + +class Client extends WorkClient +{ + + /** + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/10/9 + * @param int $limit + * @param string $cursor + * @return array|\EasyWeChat\Kernel\Support\Collection|object|\Psr\Http\Message\ResponseInterface|string + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function getUserListIds(int $limit = 0, string $cursor = '') + { + $data = []; + + if ($limit) { + $data['limit'] = $limit; + } + + if ($cursor) { + $data['cursor'] = $cursor; + } + + return $this->httpPostJson('cgi-bin/user/list_id', $data); + } +} diff --git a/crmeb/services/wechat/department/ServiceProvider.php b/crmeb/services/wechat/department/ServiceProvider.php new file mode 100644 index 0000000..373c655 --- /dev/null +++ b/crmeb/services/wechat/department/ServiceProvider.php @@ -0,0 +1,41 @@ + + * +---------------------------------------------------------------------- + */ + +namespace crmeb\services\wechat\department; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/10/9 + * @package crmeb\services\wechat\department + */ +class ServiceProvider implements ServiceProviderInterface +{ + + /** + * @param Container $app + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/10/9 + */ + public function register(Container $app) + { + $app['department'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/crmeb/services/wechat/groupChat/Client.php b/crmeb/services/wechat/groupChat/Client.php new file mode 100644 index 0000000..6fcdb5a --- /dev/null +++ b/crmeb/services/wechat/groupChat/Client.php @@ -0,0 +1,109 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\groupChat; + +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\Work\ExternalContact\Client as WorkClient; +use GuzzleHttp\Exception\GuzzleException; + +/** + * 客户群聊配置 + * Class Client + * @package crmeb\services\wechat\groupChat + */ +class Client extends WorkClient +{ + + + /** + * 配置客户群进群方式 + * @param string $roomName 自动建群的群名前缀 + * @param array $chatIdList 使用该配置的客户群ID列表 + * @param string $state 企业自定义的state参数 + * @param int $autoCreateRoom 当群满了后,是否自动新建群。0-否;1-是。 默认为1 + * @param string|null $remark 联系方式的备注信息 + * @param int $scene 场景。1 - 群的小程序插件 2 - 群的二维码插件 + * @param int $roomBaseId 自动建群的群起始序号 + * @return array + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function addJoinWay(string $roomName, array $chatIdList, string $state, int $autoCreateRoom = 1, int $roomBaseId = 1, string $remark = null, int $scene = 2) + { + $data = [ + 'scene' => $scene, + 'remark' => $remark, + 'chat_id_list' => $chatIdList, + 'auto_create_room' => $autoCreateRoom, + 'room_base_name' => $roomName, + 'room_base_id' => $roomBaseId, + 'state' => $state + ]; + + return $this->httpPostJson('cgi-bin/externalcontact/groupchat/add_join_way', $data); + } + + /** + * 更新客户群进群方式配置 + * @param string $configId + * @param string $roomBaseName + * @param array $chatIdList + * @param string $state + * @param int $autoCreateRoom + * @param string|null $remark + * @param int $scene + * @param int $roomBaseId + * @return array + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function updateJoinWay(string $configId, string $roomBaseName, array $chatIdList, string $state, int $autoCreateRoom = 1, int $roomBaseId = 1, string $remark = null, int $scene = 2) + { + $data = [ + 'config_id' => $configId, + 'scene' => $scene, + 'remark' => $remark, + 'auto_create_room' => $autoCreateRoom, + 'room_base_name' => $roomBaseName, + 'room_base_id' => $roomBaseId, + 'chat_id_list' => $chatIdList, + 'state' => $state, + ]; + return $this->httpPostJson('cgi-bin/externalcontact/groupchat/update_join_way', $data); + } + + /** + * 获取客户群进群方式配置 + * @param string $configId + * @return array + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function getJoinWay(string $configId) + { + return $this->httpPostJson('cgi-bin/externalcontact/groupchat/get_join_way', ['config_id' => $configId]); + } + + /** + * 删除客户群进群方式配置 + * @param string $configId + * @return array + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function deleteJoinWay(string $configId) + { + return $this->httpPostJson('cgi-bin/externalcontact/groupchat/del_join_way', ['config_id' => $configId]); + } + + +} diff --git a/crmeb/services/wechat/groupChat/ServiceProvider.php b/crmeb/services/wechat/groupChat/ServiceProvider.php new file mode 100644 index 0000000..3d4ca47 --- /dev/null +++ b/crmeb/services/wechat/groupChat/ServiceProvider.php @@ -0,0 +1,33 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\groupChat; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider + * @package crmeb\services\wechat\groupChat + */ +class ServiceProvider implements ServiceProviderInterface +{ + + /** + * @param Container $app + */ + public function register(Container $app) + { + $app['external_contact'] = function ($app) { + return new Client($app); + }; + } +} diff --git a/crmeb/services/wechat/live/LiveClient.php b/crmeb/services/wechat/live/LiveClient.php new file mode 100644 index 0000000..e00895b --- /dev/null +++ b/crmeb/services/wechat/live/LiveClient.php @@ -0,0 +1,267 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\live; + +use EasyWeChat\Kernel\Exceptions\InvalidConfigException; +use EasyWeChat\Kernel\Support\Collection; +use EasyWeChat\MiniProgram\Live\Client; +use GuzzleHttp\Exception\GuzzleException; +use Psr\Http\Message\ResponseInterface; + +/** + * 小程序直播间服务 + * Class LiveClient + * @package crmeb\services\wechat\live + */ +class LiveClient extends Client +{ + + //创建直播间 + const CREATE_LIVE_ROOM = 'wxaapi/broadcast/room/create'; + + //直播间导入商品 + const LIVE_ROOM_ADD_GOODS = 'wxaapi/broadcast/room/addgoods'; + + //获取商品列表信息 + const GOODS_LIST = 'wxaapi/broadcast/goods/getapproved'; + + //商品添加并审核 + const GOODS_ADD = 'wxaapi/broadcast/goods/add'; + + //撤回审核 + const GOODS_RESET_AUDIT = 'wxaapi/broadcast/goods/resetaudit'; + + //重新提交审核 + const GOODS_AUDIT = 'wxaapi/broadcast/goods/autdit'; + + //删除商品 + const GOODS_DELETE = 'wxaapi/broadcast/goods/delete'; + + //更新商品 + const GOODS_UPDATE = 'wxaapi/broadcast/goods/update'; + + //获取商品状态 + const GOODS_INFO = 'wxa/business/getgoodswarehouse'; + + //获取成员列表 + const ROLE_LIST = 'wxaapi/broadcast/role/getrolelist'; + + /** + * 添加直播间参数 + * @var array + */ + protected $create_data = [ + 'name' => '', // 房间名字 + 'coverImg' => '', // 通过 uploadfile 上传,填写 mediaID + 'startTime' => 0, // 开始时间 + 'endTime' => 0, // 结束时间 + 'anchorName' => '', // 主播昵称 + 'anchorWechat' => '', // 主播微信号 + 'shareImg' => '', //通过 uploadfile 上传,填写 mediaID + 'feedsImg' => '', //通过 uploadfile 上传,填写 mediaID + 'isFeedsPublic' => 1, // 是否开启官方收录,1 开启,0 关闭 + 'type' => 1, // 直播类型,1 推流 0 手机直播 + 'screenType' => 0, // 1:横屏 0:竖屏 + 'closeLike' => 0, // 是否 关闭点赞 1 关闭 + 'closeGoods' => 0, // 是否 关闭商品货架,1:关闭 + 'closeComment' => 0, // 是否开启评论,1:关闭 + 'closeReplay' => 1, // 是否关闭回放 1 关闭 + 'closeShare' => 0, // 是否关闭分享 1 关闭 + 'closeKf' => 0 // 是否关闭客服,1 关闭 + ]; + + /** + * 创建直播间 + * @param array $data + * @return array|Collection|object|ResponseInterface|string + * @throws InvalidConfigException + * @throws GuzzleException + */ + public function createRoom(array $data) + { + $params = array_merge($this->create_data, $data); + return $this->httpPostJson(self::CREATE_LIVE_ROOM, $params); + } + + /** + * 直播间导入商品 + * @param int $room_id + * @param $ids + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function roomAddGoods(int $room_id, $ids) + { + $params = [ + 'ids' => $ids, + 'roomId' => $room_id + ]; + return $this->httpPostJson(self::LIVE_ROOM_ADD_GOODS, $params); + } + + /** + * 获取商品列表 + * @param $status + * @param int $page + * @param int $limit + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function getGoodsList($status, int $page = 0, $limit = 30) + { + $params = [ + 'offset' => $page * $limit, + 'limit' => $limit, + 'status' => $status + ]; + return $this->httpGet(self::GOODS_LIST, $params); + } + + /** + * 获取商品详情 + * @param $ids + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function getGooodsInfo($ids) + { + $params = [ + 'goods_ids' => $ids + ]; + return $this->httpPostJson(self::GOODS_INFO, $params); + } + + /** + * 添加商品 + * @param string $coverImgUrl + * @param string $name + * @param int $priceType + * @param string $url + * @param $price + * @param string $price2 + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function addGoods(string $coverImgUrl, string $name, int $priceType, string $url, $price, $price2 = '') + { + $params = [ + 'goodsInfo' => [ + 'coverImgUrl' => $coverImgUrl, + 'name' => $name, + 'priceType' => $priceType, + 'price' => $price, + 'url' => $url + ] + ]; + if ($priceType != 1) $params['goodsInfo']['price2'] = $price2; + return $this->httpPostJson(self::GOODS_ADD, $params); + } + + /** + * 商品撤回审核 + * @param int $goodsId + * @param int $auditId + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function resetauditGoods(int $goodsId, int $auditId) + { + $params = [ + 'goodsId' => $goodsId, + 'auditId' => $auditId + ]; + return $this->httpPostJson(self::GOODS_RESET_AUDIT, $params); + } + + /** + * 商品重新提交审核 + * @param int $goodsId + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function auditGoods(int $goodsId) + { + $params = [ + 'goodsId' => $goodsId + ]; + return $this->httpPostJson(self::GOODS_AUDIT, $params); + } + + /** + * 删除商品 + * @param int $goodsId + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function deleteGoods(int $goodsId) + { + $params = [ + 'goodsId' => $goodsId + ]; + return $this->httpPostJson(self::GOODS_DELETE, $params); + } + + /** + * 更新商品 + * @param int $goodsId + * @param string $coverImgUrl + * @param string $name + * @param int $priceType + * @param string $url + * @param $price + * @param string $price2 + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function updateGoods(int $goodsId, string $coverImgUrl, string $name, int $priceType, string $url, $price, $price2 = '') + { + $params = ['goodsInfo' => [ + 'goodsId' => $goodsId, + 'coverImgUrl' => $coverImgUrl, + 'name' => $name, + 'priceType' => $priceType, + 'price' => $price, + 'url' => $url + ]]; + if ($priceType != 1) $params['goodsInfo']['price2'] = $price2; + return $this->httpPostJson(self::GOODS_UPDATE, $params); + } + + /** + * 获取成员列表 + * @param int $role + * @param int $page + * @param int $limit + * @param string $keyword + * @return array|Collection|object|ResponseInterface|string + * @throws GuzzleException + * @throws InvalidConfigException + */ + public function getRoleList($role = 2, int $page = 0, $limit = 30, $keyword = '') + { + $params = [ + 'role' => $role, + 'offset' => $page * $limit, + 'limit' => $limit, + 'keyword' => $keyword + ]; + return $this->httpGet(self::ROLE_LIST, $params); + } +} diff --git a/crmeb/services/wechat/live/ServiceProvider.php b/crmeb/services/wechat/live/ServiceProvider.php new file mode 100644 index 0000000..936c263 --- /dev/null +++ b/crmeb/services/wechat/live/ServiceProvider.php @@ -0,0 +1,34 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\services\wechat\live; + + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * Class ServiceProvider. + * + * @author onekb <1@1kb.ren> + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $app) + { + $app['live'] = function ($app) { + return new LiveClient($app); + }; + } +} diff --git a/crmeb/services/wechat/orderShipping/BaseClient.php b/crmeb/services/wechat/orderShipping/BaseClient.php new file mode 100644 index 0000000..8b5cdb1 --- /dev/null +++ b/crmeb/services/wechat/orderShipping/BaseClient.php @@ -0,0 +1,130 @@ +resultHandle($this->httpPostJson(self::ORDER . 'upload_shipping_info', $params)); + } + + /** + * 合单 + * @param $params + * @return array + * @throws \EasyWeChat\Core\Exceptions\HttpException + * + * + */ + public function combinedShipping($params) + { + return $this->resultHandle($this->httpPostJson(self::ORDER . 'upload_combined_shipping_info', $params)); + } + + + /** + * 签收消息提醒 + * @param $params + * @return array + * @throws \EasyWeChat\Core\Exceptions\HttpException + * + * + */ + public function notifyConfirm($params) + { + return $this->resultHandle($this->httpPostJson(self::ORDER . 'notify_confirm_receive', $params)); + } + + + /** + * 查询小程序是否已开通发货信息管理服务 + * @return array + * @throws \EasyWeChat\Core\Exceptions\HttpException + * + * + */ + public function isManaged() + { + /** @var MiniProgramConfig $make */ + $make = app()->make(MiniProgramConfig::class); + $params = [ + 'appid' => $make->get('appId') + ]; + return $this->resultHandle($this->httpPostJson(self::ORDER . 'is_trade_managed', $params)); + } + + /** + * 设置跳转连接 + * @param $path + * @return array + * @throws \EasyWeChat\Core\Exceptions\HttpException + * + * + */ + public function setMesJumpPath($path) + { + $params = [ + 'path' => $path + ]; + return $this->resultHandle($this->httpPostJson(self::ORDER . 'set_msg_jump_path', $params)); + } + + /** + * 获取运力id列表get_delivery_list + * @return array + * @throws \EasyWeChat\Core\Exceptions\HttpException + * + * + */ + public function getDeliveryList() + { + return $this->resultHandle($this->httpPostJson(self::EXPRESS . 'get_delivery_list', [])); + } +} diff --git a/crmeb/services/wechat/orderShipping/OrderClient.php b/crmeb/services/wechat/orderShipping/OrderClient.php new file mode 100644 index 0000000..709af01 --- /dev/null +++ b/crmeb/services/wechat/orderShipping/OrderClient.php @@ -0,0 +1,286 @@ +redis)) { + $this->redis = Cache::store('redis')->handler(); + } + return $this->redis; + } + + /** + * 处理联系人 + * @param array $contact + * @return array + * + * + */ + protected function handleContact(array $contact = []): array + { + if (isset($contact)) { + if (isset($contact['consignor_contact']) && $contact['consignor_contact']) { + $contact['consignor_contact'] = Utility::encryptTel($contact['consignor_contact']); + } + if (isset($contact['receiver_contact']) && $contact['receiver_contact']) { + $contact['receiver_contact'] = Utility::encryptTel($contact['receiver_contact']); + } + } + return $contact; + } + + /** + * 发货 + * @param string $out_trade_no + * @param int $logistics_type + * @param array $shipping_list + * @param string $payer_openid + * @param int $delivery_mode + * @param bool $is_all_delivered + * @return array + * @throws HttpException + * + * + */ + public function shippingByTradeNo(string $out_trade_no, int $logistics_type, array $shipping_list, string $payer_openid, $path, int $delivery_mode = 1, bool $is_all_delivered = true) + { + if (!$this->checkManaged()) { + throw new AdminException('开通小程序订单管理服务后重试'); + } + /** @var PaymentConfig $make */ + $make = app()->make(PaymentConfig::class); + $params = [ + 'order_key' => [ + 'order_number_type' => 2, + 'mchid' => $make->get('mchId'), +// 'out_trade_no' => $out_trade_no, + 'transaction_id' => $out_trade_no + ], + 'logistics_type' => $logistics_type, + 'delivery_mode' => $delivery_mode, + 'upload_time' => date(DATE_RFC3339), + 'payer' => [ + 'openid' => $payer_openid + ] + ]; + if ($delivery_mode == 2) { + $params['is_all_delivered'] = $is_all_delivered; + } + + foreach ($shipping_list as $shipping) { + $contact = $this->handleContact($shipping['contact'] ?? []); + $params['shipping_list'][] = [ + 'tracking_no' => $shipping['tracking_no'] ?? '', + 'express_company' => isset($shipping['express_company']) ? $this->getDelivery($shipping['express_company']) : '', + 'item_desc' => $shipping['item_desc'], + 'contact' => $contact + ]; + } + // 跳转路径 +// $this->setMesJumpPath($path); + return $this->shipping($params); + } + + + /** + * 合单 + * @param string $out_trade_no + * @param int $logistics_type + * @param array $sub_orders + * @param string $payer_openid + * @param int $delivery_mode + * @param bool $is_all_delivered + * @return array + * @throws HttpException + * + * + */ + public function combinedShippingByTradeNo(string $out_trade_no, int $logistics_type, array $sub_orders, string $payer_openid, int $delivery_mode = 2, bool $is_all_delivered = false) + { + if (!$this->checkManaged()) { + throw new AdminException('开通小程序订单管理服务后重试'); + } + $params = [ + 'order_key' => [ + 'order_number_type' => 1, + 'mchid' => $this->config['config']['mini_program']['merchant_id'], + 'out_trade_no' => $out_trade_no, + ], + 'upload_time' => date(DATE_RFC3339), + 'payer' => [ + 'openid' => $payer_openid + ] + ]; + + foreach ($sub_orders as $order) { + $sub_order = [ + 'order_key' => [ + 'order_number_type' => 1, + 'mchid' => $this->config['config']['mini_program']['merchant_id'], + 'out_trade_no' => $order['out_trade_no'], + 'logistics_type' => $logistics_type, + ], + 'delivery_mode' => $delivery_mode, + 'is_all_delivered' => $is_all_delivered + ]; + foreach ($sub_orders['shipping_list'] as $shipping) { + $contact = $this->handleContact($shipping['contact'] ?? []); + $sub_order['shipping_list'][] = [ + 'tracking_no' => $shipping['tracking_no'] ?? '', + 'express_company' => isset($shipping['express_company']) ? $this->getDelivery($shipping['express_company']) : '', + 'item_desc' => $shipping['item_desc'], + 'contact' => $contact + ]; + } + $params['sub_orders'][] = $sub_order; + } + + return $this->combinedShipping($params); + } + + + /** + * 签收通知 + * @param string $merchant_trade_no + * @param string $received_time + * @return array + * @throws HttpException + * + * + */ + public function notifyConfirmByTradeNo(string $merchant_trade_no, string $received_time) + { + $params = [ + 'merchant_id' => $this->config['config']['mini_program']['merchant_id'], + 'merchant_trade_no' => $merchant_trade_no, + 'received_time' => $received_time + ]; + return $this->notifyConfirm($params); + } + + /** + * 设置跳转连接 + * @param $path + * @return array + * @throws \EasyWeChat\Core\Exceptions\HttpException + * + * + */ + public function setMesJumpPathAndCheck($path) + { + if (!$this->checkManaged()) { + throw new AdminException('开通小程序订单管理服务后重试'); + } + return $this->setMesJumpPath($path); + } + + /** + * 设置小程序管理服务开通状态 + * @return bool + * @throws HttpException + * + * + */ + public function setManaged() + { + try { + $res = $this->isManaged(); + if ($res['is_trade_managed']) { + $key = self::redis_prefix . '_is_trade_managed'; + $this->getRedis()->set($key, $res['is_trade_managed']); + return true; + } else { + return false; + } + } catch (\Throwable $e) { + return false; + } + } + + /** + * @return bool + * @throws HttpException + * + * + */ + public function checkManaged() + { + $key = self::redis_prefix . '_is_trade_managed'; + if ($this->getRedis()->exists($key)) { + return true; + } else { + return $this->setManaged(); + } + } + + /** + * 同步去微信物流列表 + * @return array + * @throws HttpException + * + * + */ + public function setDeliveryList() + { + $list = $this->getDeliveryList(); + if ($list) { + $key = self::redis_prefix . '_delivery_list'; + $date = array_column($list['delivery_list'], 'delivery_id', 'delivery_name'); + // 创建缓存 + $this->getRedis()->hMSet($key, $date); + + return $date; + } else { + throw new AdminException('物流公司列表异常'); + } + } + + /** + * 获取物流公司编码 + * @param $company_name + * @return array|mixed + * @throws HttpException + * + * + */ + public function getDelivery($company_name) + { + $key = self::redis_prefix . '_delivery_list'; + if (!$this->getRedis()->exists($key)) { + $date = $this->setDeliveryList(); + $express_company = $date[$company_name] ?? ''; + } else { + $express_company = $this->getRedis()->hMGet($key, [$company_name])[$company_name] ?? ''; + } + if (empty($express_company)) { + $express_company = self::express_company; + } + + return $express_company; + } +} diff --git a/crmeb/services/wechat/orderShipping/ServiceProvider.php b/crmeb/services/wechat/orderShipping/ServiceProvider.php new file mode 100644 index 0000000..a1b472a --- /dev/null +++ b/crmeb/services/wechat/orderShipping/ServiceProvider.php @@ -0,0 +1,34 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +namespace crmeb\services\wechat\orderShipping; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + + +/** + * 小程序订单管理 + * Class ServiceProvider + * @package crmeb\services\easywechat\orderShipping + */ +class ServiceProvider implements ServiceProviderInterface +{ + /** + * {@inheritdoc}. + */ + public function register(Container $pimple) + { + $pimple['orderShipping'] = function ($pimple) { + return new OrderClient($pimple); + }; + } +} diff --git a/crmeb/services/wechat/orderShipping/Utility.php b/crmeb/services/wechat/orderShipping/Utility.php new file mode 100644 index 0000000..1836332 --- /dev/null +++ b/crmeb/services/wechat/orderShipping/Utility.php @@ -0,0 +1,42 @@ +getPublicKey(), $padding)) { + throw new AdminException('Encrypting the input $plaintext failed, please checking your $publicKey whether or nor correct.'); + } + + return base64_encode($encrypted); + } + + public static function encryptTel($tel) + { + $new_tel = substr_replace($tel, '****', 3, 4); + return $new_tel; + } + +} diff --git a/crmeb/services/wechat/v3pay/BaseClient.php b/crmeb/services/wechat/v3pay/BaseClient.php new file mode 100644 index 0000000..a1bd60a --- /dev/null +++ b/crmeb/services/wechat/v3pay/BaseClient.php @@ -0,0 +1,265 @@ + + * +---------------------------------------------------------------------- + */ + +namespace crmeb\services\wechat\v3pay; + + +use crmeb\exceptions\PayException; +use crmeb\services\wechat\WechatException; +use think\exception\InvalidArgumentException; +use EasyWeChat\Kernel\BaseClient as EasyWeChatBaseClient; + +/** + * Class BaseClient + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/9/30 + * @package crmeb\services\wechat\v3pay + */ +class BaseClient extends EasyWeChatBaseClient +{ + + use Certficates; + + const BASE_URL = 'https://api.mch.weixin.qq.com/'; + + const KEY_LENGTH_BYTE = 32; + + const AUTH_TAG_LENGTH_BYTE = 16; + + /** + * request. + * + * @param string $endpoint + * @param string $method + * @param array $options + * @param bool $returnResponse + */ + public function request(string $endpoint, string $method = 'POST', array $options = [], $serial = true) + { + $body = $options['body'] ?? ''; + + if (isset($options['json'])) { + $body = json_encode($options['json']); + $options['body'] = $body; + unset($options['json']); + } + + $headers = [ + 'Content-Type' => 'application/json', + 'User-Agent' => 'curl', + 'Accept' => 'application/json', + 'Authorization' => $this->getAuthorization($endpoint, $method, $body), + ]; + + $options['headers'] = array_merge($headers, ($options['headers'] ?? [])); + + if ($serial) { + $options['headers']['Wechatpay-Serial'] = $this->getCertficatescAttr('serial_no'); + } + + return $this->_doRequestCurl($method, self::BASE_URL . $endpoint, $options); + } + + /** + * @param $method + * @param $location + * @param array $options + * @return mixed + */ + private function _doRequestCurl($method, $location, $options = []) + { + $curl = curl_init(); + // POST数据设置 + if (strtolower($method) === 'post') { + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data'] ?? $options['body'] ?? ''); + } + // CURL头信息设置 + if (!empty($options['headers'])) { + $headers = []; + foreach ($options['headers'] as $k => $v) { + $headers[] = "$k: $v"; + } + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + } + curl_setopt($curl, CURLOPT_URL, $location); + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_TIMEOUT, 60); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); + $content = curl_exec($curl); + $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + curl_close($curl); + return json_decode(substr($content, $headerSize), true); + } + + /** + * To id card, mobile phone number and other fields sensitive information encryption. + * + * @param string $string + * + * @return string + */ + protected function encryptSensitiveInformation(string $string) + { + $certificates = $this->app->certficates->get()['certificates']; + if (null === $certificates) { + throw new WechatException('config certificate connot be empty.'); + } + $encrypted = ''; + if (openssl_public_encrypt($string, $encrypted, $certificates, OPENSSL_PKCS1_OAEP_PADDING)) { + //base64编码 + $sign = base64_encode($encrypted); + } else { + throw new WechatException('Encryption of sensitive information failed'); + } + return $sign; + } + + + /** + * @param string $url + * @param string $method + * @param string $body + * @return string + */ + protected function getAuthorization(string $url, string $method, string $body) + { + $nonceStr = uniqid(); + $timestamp = time(); + $message = $method . "\n" . + '/' . $url . "\n" . + $timestamp . "\n" . + $nonceStr . "\n" . + $body . "\n"; + openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption'); + $sign = base64_encode($raw_sign); + $schema = 'WECHATPAY2-SHA256-RSA2048 '; + $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', + $this->app['config']['v3_payment']['mch_id'], $nonceStr, $timestamp, $this->app['config']['v3_payment']['serial_no'], $sign); + return $schema . $token; + } + + /** + * 获取商户私钥 + * @return bool|resource + */ + protected function getPrivateKey() + { + $key_path = $this->app['config']['key_path']; + if (!file_exists($key_path)) { + throw new \InvalidArgumentException( + "SSL certificate not found: {$key_path}" + ); + } + return openssl_pkey_get_private(file_get_contents($key_path)); + } + + /** + * 获取商户公钥 + * @return bool|resource + */ + protected function getPublicKey() + { + $key_path = $this->app['config']['cert_path']; + if (!file_exists($key_path)) { + throw new \InvalidArgumentException( + "SSL certificate not found: {$key_path}" + ); + } + return openssl_pkey_get_public(file_get_contents($key_path)); + } + + /** + * 替换url + * @param string $url + * @param $search + * @param $replace + * @return array|string|string[] + */ + public function getApiUrl(string $url, $search, $replace) + { + $newSearch = []; + foreach ($search as $key) { + $newSearch[] = '{' . $key . '}'; + } + return str_replace($newSearch, $replace, $url); + } + + /** + * @param int $padding + */ + private static function paddingModeLimitedCheck(int $padding): void + { + if (!($padding === OPENSSL_PKCS1_OAEP_PADDING || $padding === OPENSSL_PKCS1_PADDING)) { + throw new PayException(sprintf("Doesn't supported padding mode(%d), here only support OPENSSL_PKCS1_OAEP_PADDING or OPENSSL_PKCS1_PADDING.", $padding)); + } + } + + /** + * 加密数据 + * @param string $plaintext + * @param int $padding + * @return string + */ + public function encryptor(string $plaintext, int $padding = OPENSSL_PKCS1_OAEP_PADDING) + { + self::paddingModeLimitedCheck($padding); + + if (!openssl_public_encrypt($plaintext, $encrypted, $this->getPublicKey(), $padding)) { + throw new PayException('Encrypting the input $plaintext failed, please checking your $publicKey whether or nor correct.'); + } + + return base64_encode($encrypted); + } + + /** + * decrypt ciphertext. + * + * @param array $encryptCertificate + * + * @return string + */ + public function decrypt(array $encryptCertificate) + { + $ciphertext = base64_decode($encryptCertificate['ciphertext'], true); + $associatedData = $encryptCertificate['associated_data']; + $nonceStr = $encryptCertificate['nonce']; + $aesKey = $this->app['config']['v3_payment']['key']; + + try { + // ext-sodium (default installed on >= PHP 7.2) + if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) { + return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey); + } + // ext-libsodium (need install libsodium-php 1.x via pecl) + if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) { + return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey); + } + // openssl (PHP >= 7.1 support AEAD) + if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) { + $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE); + $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE); + return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData); + } + } catch (\Exception $exception) { + throw new InvalidArgumentException($exception->getMessage(), $exception->getCode()); + } catch (\SodiumException $exception) { + throw new InvalidArgumentException($exception->getMessage(), $exception->getCode()); + } + throw new InvalidArgumentException('AEAD_AES_256_GCM 需要 PHP 7.1 以上或者安装 libsodium-php'); + } +} + diff --git a/crmeb/services/wechat/v3pay/Certficates.php b/crmeb/services/wechat/v3pay/Certficates.php new file mode 100644 index 0000000..8174f00 --- /dev/null +++ b/crmeb/services/wechat/v3pay/Certficates.php @@ -0,0 +1,68 @@ + + * +---------------------------------------------------------------------- + */ + +namespace crmeb\services\wechat\v3pay; + + +use crmeb\exceptions\PayException; +use think\facade\Cache; + +/** + * Class Certficates + * @package crmeb\services\easywechat\v3pay + */ +trait Certficates +{ + + /** + * @param string|null $key + * @return array|mixed|null + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function getCertficatescAttr(string $key = null) + { + $driver = Cache::store('file'); + $cacheKey = '_wx_v3' . $this->app['config']['v3_payment']['serial_no']; + if ($driver->has($cacheKey)) { + $res = $driver->get($cacheKey); + if ($key && $res) { + return $res[$key] ?? null; + } else { + return $res; + } + } + $certficates = $this->getCertficates(); + $driver->set($cacheKey, $certficates, 3600 * 24 * 30); + if ($key && $certficates) { + return $certficates[$key] ?? null; + } + return $certficates; + } + + /** + * get certficates. + * + * @return array + */ + public function getCertficates() + { + $response = $this->request('v3/certificates', 'GET', [], false); + if (isset($response['code'])) { + throw new PayException($response['message']); + } + $certificates = $response['data'][0]; + $certificates['certificates'] = $this->decrypt($certificates['encrypt_certificate']); + unset($certificates['encrypt_certificate']); + return $certificates; + } +} diff --git a/crmeb/services/wechat/v3pay/PayClient.php b/crmeb/services/wechat/v3pay/PayClient.php new file mode 100644 index 0000000..ef594ee --- /dev/null +++ b/crmeb/services/wechat/v3pay/PayClient.php @@ -0,0 +1,433 @@ + + * +---------------------------------------------------------------------- + */ + +namespace crmeb\services\wechat\v3pay; + + +use crmeb\exceptions\PayException; +use crmeb\services\wechat\Payment; + +/** + * v3支付 + * Class PayClient + * @package crmeb\services\easywechat\v3pay + */ +class PayClient extends BaseClient +{ + //app支付 + const API_APP_APY_URL = 'v3/pay/transactions/app'; + //二维码支付截图 + const API_NATIVE_URL = 'v3/pay/transactions/native'; + //h5支付接口 + const API_H5_URL = 'v3/pay/transactions/h5'; + //jsapi支付接口 + const API_JSAPI_URL = 'v3/pay/transactions/jsapi'; + //发起商家转账API + const API_BATCHES_URL = 'v3/transfer/batches'; + //退款 + const API_REFUND_URL = 'v3/refund/domestic/refunds'; + //退款查询接口 + const API_REFUND_QUERY_URL = 'v3/refund/domestic/refunds/{out_refund_no}'; + + /** + * @var string + */ + protected $type = Payment::WEB; + + /** + * @param string $type + * @return $this + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/18 + */ + public function setType(string $type) + { + $this->type = $type; + return $this; + } + + /** + * 公众号jsapi支付下单 + * @param string $outTradeNo + * @param string $total + * @param string $description + * @param string $attach + * @return mixed + */ + public function jsapiPay(string $openid, string $outTradeNo, string $total, string $description, string $attach) + { + $appId = $this->app['config']['v3_payment']['other']['wechat']['appid']; + $res = $this->pay('jsapi', $appId, $outTradeNo, $total, $description, $attach, ['openid' => $openid]); + return $this->configForJSSDKPayment($appId, $res['prepay_id']); + } + + /** + * 小程序支付 + * @param string $outTradeNo + * @param string $total + * @param string $description + * @param string $attach + * @return array|false|string + */ + public function miniprogPay(string $openid, string $outTradeNo, string $total, string $description, string $attach) + { + $appId = $this->app['config']['v3_payment']['other']['miniprog']['appid']; + $res = $this->pay('jsapi', $appId, $outTradeNo, $total, $description, $attach, ['openid' => $openid]); + return $this->configForJSSDKPayment($appId, $res['prepay_id']); + } + + /** + * APP支付下单 + * @param string $outTradeNo + * @param string $total + * @param string $description + * @param string $attach + * @return mixed + */ + public function appPay(string $outTradeNo, string $total, string $description, string $attach) + { + $res = $this->pay('app', $this->app['config']['v3_payment']['other']['app']['appid'], $outTradeNo, $total, $description, $attach); + return $this->configForAppPayment($res['prepay_id']); + } + + /** + * native支付下单 + * @param string $outTradeNo + * @param string $total + * @param string $description + * @param string $attach + * @return mixed + */ + public function nativePay(string $outTradeNo, string $total, string $description, string $attach) + { + return $this->pay('native', $this->app['config']['v3_payment']['other']['wechat']['appid'], $outTradeNo, $total, $description, $attach); + } + + /** + * h5支付下单 + * @param string $outTradeNo + * @param string $total + * @param string $description + * @param string $attach + * @return mixed + */ + public function h5Pay(string $outTradeNo, string $total, string $description, string $attach) + { + $res = $this->pay('h5', $this->app['config']['v3_payment']['other']['wechat']['appid'], $outTradeNo, $total, $description, $attach); + return ['mweb_url' => $res['h5_url']]; + } + + /** + * 下单 + * @param string $type + * @param string $appid + * @param string $outTradeNo + * @param string $total + * @param string $description + * @param string $attach + * @param array $payer + * @return mixed + */ + public function pay(string $type, string $appid, string $outTradeNo, string $total, string $description, string $attach, array $payer = []) + { + $totalFee = (int)bcmul($total, '100'); + + $data = [ + 'appid' => $appid, + 'mchid' => $this->app['config']['v3_payment']['mch_id'], + 'out_trade_no' => $outTradeNo, + 'attach' => $attach, + 'description' => $description, + 'notify_url' => $this->app['config']['v3_payment']['notify_url'], + 'amount' => [ + 'total' => $totalFee, + 'currency' => 'CNY' + ], + ]; + + if ($payer) { + $data['payer'] = $payer; + } + + $url = ''; + switch ($type) { + case 'h5': + $url = self::API_H5_URL; + $data['scene_info'] = [ + 'payer_client_ip' => request()->ip(), + 'h5_info' => [ + 'type' => 'Wap' + ] + ]; + break; + case 'native': + $url = self::API_NATIVE_URL; + break; + case 'app': + $url = self::API_APP_APY_URL; + break; + case 'jsapi': + $url = self::API_JSAPI_URL; + break; + } + + if (!$url) { + throw new PayException('缺少请求地址'); + } + + $res = $this->request($url, 'POST', ['json' => $data]); + + if (!$res) { + throw new PayException('微信支付:下单失败'); + } + if (isset($res['code']) && isset($res['message'])) { + throw new PayException($res['message']); + } + + return $res; + } + + /** + * 发起商家转账API + * @param string $outBatchNo + * @param string $amount + * @param string $batchName + * @param string $remark + * @param array $transferDetailList + * @return mixed + */ + public function batches(string $outBatchNo, string $amount, string $batchName, string $remark, array $transferDetailList) + { + $totalFee = '0'; + $amount = bcadd($amount, '0', 2); + foreach ($transferDetailList as &$item) { + if ($item['transfer_amount'] >= 2000 && empty($item['user_name'])) { + throw new PayException('明细金额大于等于2000时,收款人姓名必须填写'); + } + $totalFee = bcadd($totalFee, $item['transfer_amount'], 2); + $item['transfer_amount'] = (int)bcmul($item['transfer_amount'], 100, 0); + if (isset($item['user_name'])) { + $item['user_name'] = $this->encryptor($item['user_name']); + } + } + + if ($totalFee !== $amount) { + throw new PayException('转账明细金额总和和转账总金额不一致'); + } + + $amount = (int)bcmul($amount, 100, 0); + + $appid = null; + if ($this->type === Payment::WEB) { + $appid = $this->app['config']['v3_payment']['other']['wechat']['appid']; + } else if ($this->type === Payment::MINI) { + $appid = $this->app['config']['v3_payment']['other']['miniprog']['appid']; + } else if ($this->type === Payment::APP) { + $appid = $this->app['config']['v3_payment']['other']['app']['appid']; + } + + if (!$appid) { + throw new PayException('暂时只支持微信用户、小程序用户、APP微信登录用户提现'); + } + + $data = [ + 'appid' => $appid, + 'out_batch_no' => $outBatchNo, + 'batch_name' => $batchName, + 'batch_remark' => $remark, + 'total_amount' => $amount, + 'total_num' => count($transferDetailList), + 'transfer_detail_list' => $transferDetailList + ]; + + $res = $this->request(self::API_BATCHES_URL, 'POST', ['json' => $data]); + + if (!$res) { + throw new PayException('微信支付:发起商家转账失败'); + } + + if (isset($res['code']) && isset($res['message'])) { + throw new PayException($res['message']); + } + + return $res; + + } + + /** + * 退款 + * @param string $outTradeNo + * @param array $options + * @return mixed + */ + public function refund(string $outTradeNo, array $options = []) + { + if (!isset($options['pay_price'])) { + throw new PayException(400730); + } + $totalFee = floatval(bcmul($options['pay_price'], 100, 0)); + $refundFee = isset($options['refund_price']) ? floatval(bcmul($options['refund_price'], 100, 0)) : null; + $refundReason = $options['desc'] ?? ''; + $refundNo = $options['refund_id'] ?? $outTradeNo; + /*仅针对老资金流商户使用 + REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款) + REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款 + */ + $refundAccount = $opt['refund_account'] ?? 'AVAILABLE'; + + $data = [ + 'transaction_id' => $outTradeNo, + 'out_refund_no' => $refundNo, + 'amount' => [ + 'refund' => (int)$refundFee, + 'currency' => 'CNY', + 'total' => (int)$totalFee + ], + 'funds_account' => $refundAccount + ]; + + if ($refundReason) { + $data['reason'] = $refundReason; + } + + $res = $this->request(self::API_REFUND_URL, 'POST', ['json' => $data]); + + if (!$res) { + throw new PayException('微信支付:发起退款失败'); + } + + if (isset($res['code']) && isset($res['message'])) { + throw new PayException($res['message']); + } + + return $res; + } + + /** + * 查询退款 + * @param string $outRefundNo + * @return mixed + */ + public function queryRefund(string $outRefundNo) + { + $res = $this->request($this->getApiUrl(self::API_REFUND_QUERY_URL, ['out_refund_no'], [$outRefundNo]), 'GET'); + + if (!$res) { + throw new PayException(500000); + } + + if (isset($res['code']) && isset($res['message'])) { + throw new PayException($res['message']); + } + + return $res; + } + + /** + * jsapi支付 + * @param string $appid + * @param string $prepayId + * @param bool $json + * @return array|false|string + */ + public function configForPayment(string $appid, string $prepayId, bool $json = true) + { + $params = [ + 'appId' => $appid, + 'timeStamp' => strval(time()), + 'nonceStr' => uniqid(), + 'package' => "prepay_id=$prepayId", + 'signType' => 'RSA', + ]; + $message = $params['appId'] . "\n" . + $params['timeStamp'] . "\n" . + $params['nonceStr'] . "\n" . + $params['package'] . "\n"; + openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption'); + $sign = base64_encode($raw_sign); + + $params['paySign'] = $sign; + + return $json ? json_encode($params) : $params; + } + + /** + * Generate app payment parameters. + * @param string $prepayId + * @return array + */ + public function configForAppPayment(string $prepayId): array + { + $params = [ + 'appid' => $this->app['config']['v3_payment']['other']['app']['appid'], + 'partnerid' => $this->app['config']['v3_payment']['mch_id'], + 'prepayid' => $prepayId, + 'noncestr' => uniqid(), + 'timestamp' => time(), + 'package' => 'Sign=WXPay', + ]; + $message = $params['appid'] . "\n" . + $params['timestamp'] . "\n" . + $params['noncestr'] . "\n" . + $params['prepayid'] . "\n"; + openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption'); + $sign = base64_encode($raw_sign); + + $params['sign'] = $sign; + + return $params; + } + + /** + * 小程序支付 + * @param string $appid + * @param string $prepayId + * @return array|false|string + */ + public function configForJSSDKPayment(string $appid, string $prepayId) + { + $config = $this->configForPayment($appid, $prepayId, false); + + $config['timestamp'] = $config['timeStamp']; + unset($config['timeStamp']); + + return $config; + } + + /** + * @param $callback + * @return \think\Response + */ + public function handleNotify($callback) + { + $request = request(); + $success = $request->post('event_type') === 'TRANSACTION.SUCCESS'; + $data = $this->decrypt($request->post('resource', [])); + + $handleResult = call_user_func_array($callback, [json_decode($data, true), $success]); + if (is_bool($handleResult) && $handleResult) { + $response = [ + 'code' => 'SUCCESS', + 'message' => 'OK', + ]; + } else { + $response = [ + 'code' => 'FAIL', + 'message' => $handleResult, + ]; + } + + return response($response, 200, [], 'json'); + } +} diff --git a/crmeb/services/wechat/v3pay/ServiceProvider.php b/crmeb/services/wechat/v3pay/ServiceProvider.php new file mode 100644 index 0000000..06549f9 --- /dev/null +++ b/crmeb/services/wechat/v3pay/ServiceProvider.php @@ -0,0 +1,37 @@ + + * +---------------------------------------------------------------------- + */ + +namespace crmeb\services\wechat\v3pay; + + +use Pimple\Container; +use Pimple\ServiceProviderInterface; + +/** + * V3支付 + * Class ServiceProvider + * @package crmeb\services\easywechat\v3pay + */ +class ServiceProvider implements ServiceProviderInterface +{ + + /** + * @param Container $pimple + */ + public function register(Container $pimple) + { + $pimple['v3pay'] = function ($pimple) { + return new PayClient($pimple, $pimple['access_token']); + }; + } +} diff --git a/crmeb/traits/ModelTrait.php b/crmeb/traits/ModelTrait.php new file mode 100644 index 0000000..8e3f50b --- /dev/null +++ b/crmeb/traits/ModelTrait.php @@ -0,0 +1,129 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\traits; + +use think\Model; + +/** + * Trait ModelTrait + * @package crmeb\traits + */ +trait ModelTrait +{ + /** + * 时间段搜索器 + * @param Model $query + * @param $value + */ + public function searchTimeAttr($query, $value, $data) + { + if ($value) { + $timeKey = $data['timeKey'] ?? (property_exists($this, 'timeKey') && $this->timeKey ? $this->timeKey : 'add_time'); + if (is_array($value)) { + $startTime = $value[0] ?? 0; + $endTime = $value[1] ?? 0; + if ($startTime || $endTime) { + try { + date('Y-m-d', $startTime); + } catch (\Throwable $e) { + $startTime = strtotime($startTime); + } + try { + date('Y-m-d', $endTime); + } catch (\Throwable $e) { + $endTime = strtotime($endTime); + } + if ($startTime == $endTime || $endTime == strtotime(date('Y-m-d', $endTime))) { + $endTime = $endTime + 86400; + } + $query->whereBetween($timeKey, [$startTime, $endTime]); + } + } elseif (is_string($value)) { + switch ($value) { + case 'today': + case 'week': + case 'month': + case 'year': + case 'yesterday': + case 'last year': + case 'last week': + case 'last month': + $query->whereTime($timeKey, $value); + break; + case 'quarter': + [$startTime, $endTime] = $this->getMonth(); + $query->whereBetween($timeKey, [strtotime($startTime), strtotime($endTime)]); + break; + case 'lately7': + $query->whereBetween($timeKey, [strtotime("-7 day"), time()]); + break; + case 'lately30': + $query->whereBetween($timeKey, [strtotime("-30 day"), time()]); + break; + default: + if (strstr($value, '-') !== false) { + [$startTime, $endTime] = explode('-', $value); + $startTime = trim($startTime) ? strtotime($startTime) : 0; + $endTime = trim($endTime) ? strtotime($endTime) : 0; + if ($startTime && $endTime) { + if ($startTime == $endTime || $endTime == strtotime(date('Y-m-d', $endTime))) { + $endTime = $endTime + 86400; + } + $query->whereBetween($timeKey, [$startTime, $endTime]); + } else if (!$startTime && $endTime) { + $query->whereTime($timeKey, '<', $endTime + 86400); + } else if ($startTime && !$endTime) { + $query->whereTime($timeKey, '>=', $startTime); + } + } + break; + } + } + } + } + + /** + * 获取本季度 time + * @param int $ceil + * @return array + */ + public function getMonth(int $ceil = 0) + { + if ($ceil != 0) { + $season = ceil(date('n') / 3) - $ceil; + } else { + $season = ceil(date('n') / 3); + } + $firstday = date('Y-m-01', mktime(0, 0, 0, ($season - 1) * 3 + 1, 1, date('Y'))); + $lastday = date('Y-m-t', mktime(0, 0, 0, $season * 3, 1, date('Y'))); + return [$firstday, $lastday]; + } + + /** + * 获取某个字段内的值 + * @param $value + * @param string $filed + * @param string $valueKey + * @param array|string[] $where + * @return mixed + */ + public function getFieldValue($value, string $filed, ?string $valueKey = '', ?array $where = []) + { + $model = $this->where($filed, $value); + if ($where) { + $model->where(...$where); + } + return $model->value($valueKey ?: $filed); + } + + +} diff --git a/crmeb/traits/OptionTrait.php b/crmeb/traits/OptionTrait.php new file mode 100644 index 0000000..61e144b --- /dev/null +++ b/crmeb/traits/OptionTrait.php @@ -0,0 +1,53 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\traits; + +/** + * 设置参数 + * Trait OptionTrait + * @package crmeb\traits + */ +trait OptionTrait +{ + + protected $item = []; + + /** + * @param string $key + * @param $default + * @return mixed + */ + public function getItem(string $key, $default = null) + { + return $this->item[$key] ?? $default; + } + + /** + * @param string $key + * @param $value + * @return $this + */ + public function setItem(string $key, $value) + { + $this->item[$key] = $value; + return $this; + } + + /** + * 重置 + */ + public function reset() + { + $this->item = []; + } + +} diff --git a/crmeb/traits/QueueTrait.php b/crmeb/traits/QueueTrait.php new file mode 100644 index 0000000..276c60d --- /dev/null +++ b/crmeb/traits/QueueTrait.php @@ -0,0 +1,106 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\traits; + + +use crmeb\utils\Queue; + +/** + * 快捷加入消息队列 + * Trait QueueTrait + * @package crmeb\traits + */ +trait QueueTrait +{ + + /** + * 列名 + * @return string + */ + protected static function queueName() + { + return null; + } + + /** + * 加入队列 + * @param $action + * @param array $data + * @param string|null $queueName + * @return mixed + */ + public static function dispatch($action = null, array $data = [], string $queueName = null) + { + $queue = Queue::instance()->job(__CLASS__); + if (is_array($action)) { + $queue->data(...$action); + } else if (is_string($action)) { + $queue->do($action)->data(...$data); + } + if ($queueName) { + $queue->setQueueName($queueName); + } else if (self::queueName()) { + $queue->setQueueName(self::queueName()); + } + return $queue->push(); + } + + /** + * 延迟加入消息队列 + * @param int $secs + * @param $action + * @param array $data + * @param string|null $queueName + * @return mixed + */ + public static function dispatchSece(int $secs, $action = null, array $data = [], string $queueName = null) + { + $queue = Queue::instance()->job(__CLASS__)->secs($secs); + if (is_array($action)) { + $queue->data(...$action); + } else if (is_string($action)) { + $queue->do($action)->data(...$data); + } + if ($queueName) { + $queue->setQueueName($queueName); + } else if (self::queueName()) { + $queue->setQueueName(self::queueName()); + } + return $queue->push(); + } + + /** + * 加入小队列 + * @param string $do + * @param array $data + * @param int|null $secs + * @param string|null $queueName + * @return mixed + */ + public static function dispatchDo(string $do, array $data = [], int $secs = null, string $queueName = null) + { + $queue = Queue::instance()->job(__CLASS__)->do($do); + if ($secs) { + $queue->secs($secs); + } + if ($data) { + $queue->data(...$data); + } + if ($queueName) { + $queue->setQueueName($queueName); + } else if (self::queueName()) { + $queue->setQueueName(self::queueName()); + } + return $queue->push(); + } + +} diff --git a/crmeb/traits/SearchDaoTrait.php b/crmeb/traits/SearchDaoTrait.php new file mode 100644 index 0000000..8bc6a1c --- /dev/null +++ b/crmeb/traits/SearchDaoTrait.php @@ -0,0 +1,86 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\traits; + +use app\common\dao\BaseDao; +use crmeb\basic\BaseAuth; + +/** + * Trait SearchDaoTrait + * @package crmeb\traits + * @mixin BaseDao + */ +trait SearchDaoTrait +{ + + /** + * 搜索(没有进入搜索器的自动进入where条件) + * @param array $where + * @param bool $authWhere + * @return \crmeb\basic\BaseModel + */ + public function searchWhere(array $where, bool $authWhere = true) + { + [$with, $whereKey] = app()->make(BaseAuth::class)->________(array_keys($where), $this->setModel()); + $whereData = []; + foreach ($whereKey as $key) { + if (isset($where[$key]) && 'timeKey' !== $key) { + $whereData[$key] = $where[$key]; + } + } + + return $this->getModel()->withSearch($with, $where)->when($authWhere && $whereData, function ($query) use ($whereData) { + $query->where($whereData); + }); + } + + /** + * @param array $where + * @param bool $authWhere + * @return int + */ + public function count(array $where = [], bool $authWhere = true): int + { + return $this->searchWhere($where, $authWhere)->count(); + } + + /** + * 搜索 + * @param array $where + * @param array|string[] $field + * @param int $page + * @param int $limit + * @param null $sort + * @param array $with + * @return array + */ + public function getDataList(array $where, array $field = ['*'], int $page = 0, int $limit = 0, $sort = null, array $with = []) + { + return $this->searchWhere($where)->when($page && $limit, function ($query) use ($page, $limit) { + $query->page($page, $limit); + })->when(!$page && $limit, function ($query) use ($limit) { + $query->limit($limit); + })->when($sort, function ($query, $sort) { + if (is_array($sort)) { + foreach ($sort as $k => $v) { + if (is_numeric($k)) { + $query->order($v, 'desc'); + } else { + $query->order($k, $v); + } + } + } else { + $query->order($sort, 'desc'); + } + })->field($field)->with($with)->select()->toArray(); + } +} diff --git a/crmeb/traits/ServicesTrait.php b/crmeb/traits/ServicesTrait.php new file mode 100644 index 0000000..6b42a70 --- /dev/null +++ b/crmeb/traits/ServicesTrait.php @@ -0,0 +1,42 @@ + +// +---------------------------------------------------------------------- +namespace crmeb\traits; + +use crmeb\basic\BaseModel; +use think\Model; + +/** + * Trait ServicesTrait + * @package crmeb\traits + * @method array|Model|null get($id, ?array $field = [], ?array $with = []) 获取一条数据 + * @method array|Model|null getOne(array $where, ?string $field = '*', ?array $with = []) 获取一条数据(不走搜素器) + * @method string|null batchUpdate(array $ids, array $data, ?string $key = null) 批量修改 + * @method float sum(array $where, string $field, bool $search = false) 求和 + * @method mixed update($id, array $data, ?string $field = null) 修改数据 + * @method bool be($map, string $field = '') 查询一条数据是否存在 + * @method mixed value(array $where, string $field) 获取指定条件下的数据 + * @method int count(array $where = [], string $filed = '*') 读取数据条数 + * @method int getCount(array $where = []) 获取某些条件总数(不走搜素器) + * @method array getColumn(array $where, string $field, string $key = '') 获取某个字段数组(不走搜素器) + * @method mixed delete($id, ?string $key = null) 删除 + * @method BaseModel|Model save(array $data) 保存数据 + * @method mixed saveAll(array $data) 批量保存数据 + * @method bool bcInc($key, string $incField, string $inc, string $keyField = null, int $acc = 2) 高精度加法 + * @method bool bcDec($key, string $decField, string $dec, string $keyField = null, int $acc = 2) 高精度 减法 + * @method mixed decStockIncSales(array $where, int $num, string $stock = 'stock', string $sales = 'sales') 减库存加销量 + * @method mixed incStockDecSales(array $where, int $num, string $stock = 'stock', string $sales = 'sales') 加库存减销量 + * @method mixed incUpdate($where, string $field, int $number = 1) 自增单个数据 + * @method mixed decUpdate($where, string $field, int $number = 1) 自减单个数据 + */ +trait ServicesTrait +{ + +} diff --git a/crmeb/traits/dao/CacheDaoTrait.php b/crmeb/traits/dao/CacheDaoTrait.php new file mode 100644 index 0000000..fd078a8 --- /dev/null +++ b/crmeb/traits/dao/CacheDaoTrait.php @@ -0,0 +1,521 @@ + + * +---------------------------------------------------------------------- + */ + +namespace crmeb\traits\dao; + + +use crmeb\services\CacheService; +use crmeb\utils\Tag; +use think\cache\TagSet; +use think\Container; +use think\facade\Log; +use think\Model; + +/** + * Trait CacheDaoTrait + * @package crmeb\traits\dao + * @method Model getModel() + * @method Model getPk() + */ +trait CacheDaoTrait +{ + + /** + * 获取redis + * @return \Redis + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/10/29 + */ + private function getRedisConnect() + { + return CacheService::redisHandler()->handler(); + } + + /** + * 获取缓存 + * @return TagSet|\think\facade\Cache + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/10 + */ + private function getCacheHander() + { + return CacheService::redisHandler(); + } + + /** + * 对外开放方法 + * @return TagSet|\think\facade\Cache + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/10 + */ + public function cacheHander() + { + return $this->getCacheHander(); + } + + /** + * 缓存标签 + * @param null $tag + * @return Tag + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/10 + */ + public function cacheTag($tag = null) + { + $key = $this->cacheKey() . 'tag'; + $tag = $tag ? $key . ':' . $tag : $key; + $redis = CacheService::redisHandler($tag); + + return new Tag($redis, $tag); + } + + /** + * 总缓存数据量 + * @return mixed + */ + public function cacheCount() + { + $cacheKey = $this->cacheKey(); + $rds = $this->getRedisConnect(); + + return $rds->hLen($cacheKey . 'map'); + } + + /** + * 读取缓存全部数据 + * @return mixed + */ + public function cacheList(string $key = '') + { + $cacheKey = $this->cacheKey() . $key; + $rds = $this->getRedisConnect(); + $map = $rds->hGetAll($cacheKey . 'map'); + //key排序 + ksort($map); + + $list = array_values($map) ?: []; + + foreach ($list as $key => $item) { + $list[$key] = $this->unserialize($item); + } + + return $list; + } + + /** + * 读取缓存分页数据 + * @param int $page + * @param int $limit + * @param string $key + * @return array + */ + public function cachePageData(int $page = 1, int $limit = 10, string $key = '') + { + $cacheKey = $this->cacheKey() . $key; + $rds = $this->getRedisConnect(); + + $page = max($page, 1); + $limit = max($limit, 1); + + //先读排序 + $pageList = $rds->zRangeByScore($cacheKey . 'page', ($page - 1) * $limit, ($page * $limit) - 1); + + //再读数据 + $list = $rds->hMGet($cacheKey . 'map', $pageList) ?: []; + + if (is_array($list)) { + $newList = []; + foreach ($list as $value) { + $newList[] = $this->unserialize($value); + } + $list = $newList; + } + + $count = $rds->hLen($cacheKey . 'map'); + + return compact('list', 'count'); + } + + /** + * 单个查询数据 + * @param int|string $id + * @return false|string|array + */ + public function cacheInfoById($id) + { + $cacheKey = $this->cacheKey(); + $rds = $this->getRedisConnect(); + + $value = $rds->hGet($cacheKey . 'map', $id); + + return $value === null ? null : $this->unserialize($value); + } + + /** + * 批量查询数据 + * @param $ids + * @return mixed + */ + public function cacheInfoByIds(array $ids) + { + $cacheKey = $this->cacheKey(); + $rds = $this->getRedisConnect(); + + $arr = $rds->hMGet($cacheKey . 'map', $ids); + if (is_array($arr)) { + $newList = []; + foreach ($arr as $key => $value) { + $arr[$key] = $this->unserialize($value); + } + $arr = $newList; + } + + return $arr; + } + + /** + * 更新单个缓存 + * @param $info + * @return false|mixed + */ + public function cacheUpdate(array $info, $key = null) + { + $pk = $this->getPk(); + if ((empty($info) || !isset($info[$pk])) && !$key) { + return false; + } + $cacheKey = $this->cacheKey(); + $rds = $this->getRedisConnect(); + $key = $key ?: $info[$pk]; + return $rds->hSet($cacheKey . 'map', $key, $this->serialize($info)); + } + + /** + * 序列化数据 + * @param $value + * @return string + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/10 + */ + private function serialize($value) + { + try { + return serialize($value); + } catch (\Throwable $e) { + Log::error('序列化发生错误:' . $e->getMessage()); + return $value; + } + } + + /** + * 反序列化数据 + * @param $value + * @return mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/10 + */ + private function unserialize($value) + { + try { + return unserialize($value); + } catch (\Throwable $e) { + Log::error('反序列化发生错误:' . $e->getMessage()); + return $value; + } + } + + /** + * @param int $id + * @param $field + * @param null $value + * @return false|mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/1 + */ + public function cacheSaveValue(int $id, $field, $value = null) + { + $pk = $this->getPk(); + $info = $this->cacheInfoById($id); + if (!$info) { + $newInfo = $this->get($id); + $info = $newInfo ? $newInfo->toArray() : []; + } + if (is_array($field)) { + foreach ($field as $k => $v) { + $info[$k] = $v; + } + } else { + $info[$field] = $value; + } + + $info[$pk] = $id; + return $this->cacheUpdate($info); + } + + /** + * 不存在则写入,存在则返回 + * @param $key + * @param callable|null $fn + * @param null $default + * @return array|false|mixed|string|null + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/1 + */ + public function cacheRemember($key, callable $fn = null, $default = null) + { + + //不开启数据缓存直接返回 + if (!app()->config->get('cache.is_data')) { + + if ($fn instanceof \Closure) { + return Container::getInstance()->invokeFunction($fn); + } else { + return $default; + } + + } + + $info = $this->cacheInfoById($key); + + if ((null === $info || false === $info) && is_callable($fn)) { + + //读取数据库缓存 + $newInfo = $fn(); + + if (null !== $newInfo) { + //缓存数据存在则更新 + $this->cacheUpdate($newInfo, $key); + } + + $info = $newInfo; + } + + return null !== $info ? $info : $default; + } + + /** + * 批量更新缓存 + * @param $list + * @return false|mixed + */ + public function cacheUpdateList(array $list, string $key = '') + { + if (empty($list)) { + return false; + } + $cacheKey = $this->cacheKey() . $key; + $pk = $this->getPk(); + $rds = $this->getRedisConnect(); + $map = []; + foreach ($list as $item) { + if (empty($item) || !isset($item[$pk])) { + continue; + } + $map[$item[$pk]] = $this->serialize($item); + } + return $rds->hMSet($cacheKey . 'map', $map); + } + + /** + * 删除单条缓存 + * @param $id + */ + public function cacheDelById(int $id) + { + $cacheKey = $this->cacheKey(); + $rds = $this->getRedisConnect(); + $rds->hDel($cacheKey . 'map', $id); + $rds->zRem($cacheKey . 'page', $id); + } + + /** + * 批量删除缓存 + * @param $ids + */ + public function cacheDelByIds(array $ids) + { + $cacheKey = $this->cacheKey(); + $rds = $this->getRedisConnect(); + foreach ($ids as $id) { + $rds->hDel($cacheKey . 'map', $id); + $rds->zRem($cacheKey . 'page', $id); + } + } + + /** + * 创建缓存 + * @param array $list + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/10/29 + */ + public function cacheCreate(array $list, string $key = '') + { + $pk = $this->getPk(); + $cacheKey = $this->cacheKey() . $key; + + $rds = $this->getRedisConnect(); + + //启动事务 + $rds->multi(); + + //删除旧数据 + $rds->del($cacheKey . 'map'); + $rds->del($cacheKey . 'page'); + //组合数据 + $map = []; + foreach ($list as $i => $item) { + $map[$item[$pk]] = $item; + //存zset 排序 + $rds->zAdd($cacheKey . 'page', $i, $item[$pk]); + } + foreach ($map as $k => &$item) { + $item = $this->serialize($item); + } + //存hmset 数据 + $rds->hMSet($cacheKey . 'map', $map); + + //执行事务 + $rds->exec(); + } + + protected function cacheKey() + { + return 'mc:' . $this->getModel()->getName() . ':'; + } + + /** + * + * @param $key + * @return string + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/10 + */ + public function getCacheKey($key) + { + return $this->cacheKey() . $key; + } + + /** + * 更新缓存 + * @param string $key + * @param $value + * @return bool + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/1 + */ + public function cacheStrUpdate(string $key, $value, int $expire = null) + { + return $this->getCacheHander()->set($this->cacheKey() . 'str:' . $key, $value, $expire); + } + + /** + * 获取缓存 + * @param string $key + * @return false|mixed|string + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/1 + */ + public function cacheStrGet(string $key) + { + return $this->getCacheHander()->get($this->cacheKey() . 'str:' . $key); + } + + /** + * 获取表缓存是否有数据 + * @return false|mixed|string + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/1 + */ + public function cacheStrTable() + { + return $this->getRedisConnect()->get($this->cacheKey()); + } + + /** + * 设置表缓存是否有数据 + * @param int $value + * @return bool + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/1 + */ + public function cacheStrSetTable(int $value = 1) + { + return $this->getRedisConnect()->set($this->cacheKey(), $value); + } + + /** + * 删除缓存 + * @param string $key + * @return int + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/1 + */ + public function cacheStrDel(string $key) + { + return $this->getCacheHander()->delete($this->cacheKey() . 'str:' . $key); + } + + /** + * 获取缓存,没有则写入缓存并返回 + * @param string $key + * @param callable|null $fn + * @param null $default + * @return false|mixed|string|null + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/1 + */ + public function cacheStrRemember(string $key, callable $fn = null, int $expire = null, $default = null) + { + + //不开启数据缓存直接返回 + if (!app()->config->get('cache.is_data')) { + + if ($fn instanceof \Closure) { + return Container::getInstance()->invokeFunction($fn); + } else { + return $default; + } + } + + $value = $this->cacheStrGet($key); + + if ((null === $value || false === $value) && is_callable($fn)) { + + $newValue = $fn(); + + if (null !== $newValue) { + $this->cacheStrUpdate($key, $value, $expire); + } + + $value = $newValue; + } + + return null !== $value ? $value : $default; + } +} diff --git a/crmeb/traits/service/ContactWayQrCode.php b/crmeb/traits/service/ContactWayQrCode.php new file mode 100644 index 0000000..730cc0d --- /dev/null +++ b/crmeb/traits/service/ContactWayQrCode.php @@ -0,0 +1,181 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\traits\service; + + +use app\dao\BaseDao; +use app\services\work\WorkMediaServices; +use crmeb\services\wechat\ErrorMessage; +use crmeb\services\wechat\WechatResponse; +use crmeb\services\wechat\Work; +use think\exception\ValidateException; + +/** + * 客户联系 + * Trait ContactWayQrCode + * @package crmeb\traits\service + * @property BaseDao $dao + */ +trait ContactWayQrCode +{ + + + /** + * 检测欢迎语字段 + * @param array $welcomeWords + * @param int $type + */ + public function checkWelcome(array $welcomeWords, int $type) + { + if (1 === $type) { + return; + } + + if (empty($welcomeWords['text']['content']) && empty($welcomeWords['attachments'])) { + throw new ValidateException('请填写欢迎语'); + } + + if (!empty($welcomeWords['text']['content']) && strlen($welcomeWords['text']['content']) > 3000) { + throw new ValidateException('内容不能超过4000字'); + } + + foreach ($welcomeWords['attachments'] as $item) { + switch ($item['msgtype']) { + case 'image': + if (empty($item['image']['pic_url'])) { + throw new ValidateException('请上传欢迎语图片'); + } + break; + case 'link': + if (empty($item['link']['title'])) { + throw new ValidateException('请填写连接标题'); + } + if (empty($item['link']['url'])) { + throw new ValidateException('请填写连接地址'); + } + break; + case 'miniprogram': + if (empty($item['miniprogram']['title'])) { + throw new ValidateException('请填写小程序消息标题'); + } + if (empty($item['miniprogram']['appid'])) { + throw new ValidateException('请填写小程序Appid'); + } + if (empty($item['miniprogram']['page'])) { + throw new ValidateException('请填写小程序页面路径'); + } + if (empty($item['miniprogram']['pic_url'])) { + throw new ValidateException('请选择小程序消息封面图'); + } + break; + case 'video': + if (empty($item['video']['url'])) { + throw new ValidateException('请上传视频文件'); + } + break; + case 'file': + if (empty($item['file']['url'])) { + throw new ValidateException('请上传文件'); + } + break; + } + } + } + + /** + * 执行创建或者修改【联系我】成员情况 + * @param int $channleId + * @param array $userIds + * @param bool $skipVerify + * @param string|null $wxConfigId + * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function handleQrCode(int $channleId, array $userIds, bool $skipVerify = true, string $wxConfigId = null) + { + if (!$wxConfigId) { + $qrCodeRes = Work::createQrCode($channleId, $userIds, $skipVerify); + } else { + $qrCodeRes = Work::updateQrCode($channleId, $userIds, $wxConfigId, $skipVerify); + } + + if ($qrCodeRes['errcode'] !== 0) { + throw new ValidateException(ErrorMessage::getWorkMessage($qrCodeRes['errcode'], $qrCodeRes['errmsg'] ?? '生成企业渠道码失败')); + } + + if (!$wxConfigId) { + $this->dao->update($channleId, [ + 'qrcode_url' => $qrCodeRes['qr_code'], + 'config_id' => $qrCodeRes['config_id'] + ]); + } + } + + /** + * 创建企业微信群发 + * @param array $externalUserid + * @param array $attachments + * @param string $chatType + * @param string|null $sender + * @return WechatResponse + */ + public function sendMsgTemplate(array $externalUserid, array $attachments, string $chatType = 'single', string $sender = null) + { + $msg = [ + 'chat_type' => $chatType, + 'external_userid' => $externalUserid, + ]; + if ('group' == $chatType) { + if (!$sender) { + throw new ValidateException('群发消息成员userid为必须填写'); + } + } + if ($sender) { + $msg['sender'] = $sender; + } + if (empty($msg['external_userid'])) { + unset($msg['external_userid']); + } + + //转换欢迎语当中的图片为素材库中 + /** @var WorkMediaServices $mediaService */ + $mediaService = app()->make(WorkMediaServices::class); + $attachments = $mediaService->resolvingWelcome($attachments); + $msg = array_merge($msg, $attachments); + + return Work::addMsgTemplate($msg); + } + + /** + * 创建发送朋友圈 + * @param array $attachments + * @param array $userIds + * @param array $tag + * @return WechatResponse + */ + public function addMomentTask(array $attachments, array $userIds = [], array $tag = []) + { + //转换欢迎语当中的图片为素材库中 + /** @var WorkMediaServices $mediaService */ + $mediaService = app()->make(WorkMediaServices::class); + $data = $mediaService->resolvingWelcome($attachments, 1); + if ($userIds) { + $data['visible_range']['sender_list']['user_list'] = $userIds; + } + + if ($tag) { + $data['visible_range']['external_contact_list']['tag_list'] = $tag; + } + + return Work::addMomentTask($data); + } +} diff --git a/crmeb/utils/Arr.php b/crmeb/utils/Arr.php new file mode 100644 index 0000000..8ff9bb5 --- /dev/null +++ b/crmeb/utils/Arr.php @@ -0,0 +1,291 @@ + +// +---------------------------------------------------------------------- +namespace crmeb\utils; + +/** + * 操作数组帮助类 + * Class Arr + * @package crmeb\utils + */ +class Arr +{ + /** + * 对数组增加默认值 + * @param array $keys + * @return array + */ + public static function getDefaultValue(array $keys, array $configList = []) + { + $value = []; + foreach ($keys as $val) { + if (is_array($val)) { + $k = $val[0] ?? ''; + $v = $val[1] ?? ''; + } else { + $k = $val; + $v = ''; + } + $value[$k] = $configList[$k] ?? $v; + } + return $value; + } + + /** + * 获取ivew菜单列表 + * @param array $data + * @return array + */ + public static function getMenuIviewList(array $data) + { + return Arr::toIviewUi(Arr::getTree($data)); + } + + /** + * 转化iviewUi需要的key值 + * @param $data + * @return array + */ + public static function toIviewUi($data) + { + $newData = []; + foreach ($data as $k => $v) { + $temp = []; + $temp['path'] = $v['menu_path']; + $temp['title'] = $v['menu_name']; + $temp['icon'] = $v['icon']; + $temp['header'] = $v['header']; + $temp['is_header'] = $v['is_header']; + if ($v['is_show_path']) { + $temp['auth'] = ['hidden']; + } + if (!empty($v['children'])) { + $temp['children'] = self::toIviewUi($v['children']); + } + $newData[] = $temp; + } + return $newData; + } + + /** + * 获取树型菜单 + * @param $data + * @param int $pid + * @param int $level + * @return array + */ + public static function getTree($data, $pid = 0, $level = 1) + { + $childs = self::getChild($data, $pid, $level); + $dataSort = array_column($childs, 'sort'); + array_multisort($dataSort, SORT_DESC, $childs); + foreach ($childs as $key => $navItem) { + $resChild = self::getTree($data, $navItem['id']); + if (null != $resChild) { + $childs[$key]['children'] = $resChild; + } + } + return $childs; + } + + /** + * 获取子菜单 + * @param $arr + * @param $id + * @param $lev + * @return array + */ + private static function getChild(&$arr, $id, $lev) + { + $child = []; + foreach ($arr as $k => $value) { + if ($value['pid'] == $id) { + $value['level'] = $lev; + $child[] = $value; + } + } + return $child; + } + + /** + * 格式化数据 + * @param array $array + * @param $value + * @param int $default + * @return mixed + */ + public static function setValeTime(array $array, $value, $default = 0) + { + foreach ($array as $item) { + if (!isset($value[$item])) + $value[$item] = $default; + else if (is_string($value[$item])) + $value[$item] = (float)$value[$item]; + } + return $value; + } + + /** + * 获取二维数组中某个值的集合重新组成数组,并判断数组中的每一项是否为真 + * @param array $data + * @param string $filed + * @return array + */ + public static function getArrayFilterValeu(array $data, string $filed) + { + return array_filter(array_unique(array_column($data, $filed)), function ($item) { + if ($item) { + return $item; + } + }); + } + + /** + * 获取二维数组中最大的值 + * @param $arr + * @param $field + * @return int|string + */ + public static function getArrayMax($arr, $field) + { + $temp = []; + foreach ($arr as $k => $v) { + $temp[] = $v[$field]; + } + if (!count($temp)) return 0; + $maxNumber = max($temp); + foreach ($arr as $k => $v) { + if ($maxNumber == $v[$field]) return $k; + } + return 0; + } + + /** + * 获取二维数组中最小的值 + * @param $arr + * @param $field + * @return int|string + */ + public static function getArrayMin($arr, $field) + { + $temp = []; + foreach ($arr as $k => $v) { + $temp[] = $v[$field]; + } + if (!count($temp)) return 0; + $minNumber = min($temp); + foreach ($arr as $k => $v) { + if ($minNumber == $v[$field]) return $k; + } + return 0; + } + + /** + * 数组转字符串去重复 + * @param array $data + * @return false|string[] + */ + public static function unique(array $data) + { + return array_unique(explode(',', implode(',', $data))); + } + + /** + * 获取数组中去重复过后的指定key值 + * @param array $list + * @param string $key + * @return array + */ + public static function getUniqueKey(array $list, string $key) + { + return array_unique(array_column($list, $key)); + } + + /** + * 获取数组钟随机值 + * @param array $data + * @return bool|mixed + */ + public static function getArrayRandKey(array $data) + { + if (!$data) { + return false; + } + mt_srand(); + $mun = rand(0, count($data)); + if (!isset($data[$mun])) { + return self::getArrayRandKey($data); + } + return $data[$mun]; + } + + /** + * 格式化数据 + * @param array $list + * @return array + */ + public static function formatShipping(array $list) + { + $freeDate = []; + foreach ($list as $item) { + $freeDate[$item['uniqid']][] = $item; + } + $data = []; + foreach ($freeDate as $item) { + $cityIds = []; + $cityId = []; + $p = []; + foreach ($item as $value) { + $cityId[] = $value['city_id']; + $cityIds[] = is_array($value['value']) ? $value['value'] : json_decode($value['value'], true); + unset($value['city_id'], $value['value']); + $p = $value; + } + $p['city_id'] = $cityId; + $p['city_ids'] = $cityIds; + $data[] = $p; + } + return $data; + } + + /** + * 过滤字段 + * @param $value + * @param array $filter + * @return mixed + */ + public static function StrFilterValue($value, array $filter = []) + { + $filter = $filter ?: ['strip_tags', 'addslashes', 'trim', 'htmlspecialchars']; + foreach ($filter as $closure) { + if (function_exists($closure)) { + $value = $closure($value); + } + } + return $value; + } + + /** + * 过滤字段 + * @param array $data + * @return array + */ + public static function filterValue(array $data) + { + foreach ($data as &$item) { + if (is_array($item)) { + $item = self::filterValue($item); + } else { + $item = self::StrFilterValue($item); + } + } + return $data; + } +} diff --git a/crmeb/utils/JwtAuth.php b/crmeb/utils/JwtAuth.php new file mode 100644 index 0000000..0375100 --- /dev/null +++ b/crmeb/utils/JwtAuth.php @@ -0,0 +1,112 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\utils; + + +use crmeb\exceptions\AdminException; +use crmeb\services\CacheService; +use Firebase\JWT\JWT; +use think\facade\Env; + +/** + * Jwt + * Class JwtAuth + * @package crmeb\utils + */ +class JwtAuth +{ + + /** + * token + * @var string + */ + protected $token; + + /** + * @var string + */ + protected $app_key = 'crmeb_app_key'; + + /** + * 获取token + * @param int $id + * @param string $type + * @param array $params + * @return array + */ + public function getToken(int $id, string $type, array $params = []): array + { + $host = app()->request->host(); + $time = time(); + $exp_time = strtotime('+ 7day'); + if (app()->request->isApp()) { + $exp_time = strtotime('+ 30day'); + } + if ($type == 'out') { + $exp_time = strtotime('+ 1day'); + } + $params += [ + 'iss' => $host, + 'aud' => $host, + 'iat' => $time, + 'nbf' => $time, + 'exp' => $exp_time, + ]; + $params['jti'] = compact('id', 'type'); + $token = JWT::encode($params, Env::get('app.app_key', $this->app_key) ?: $this->app_key); + + return compact('token', 'params'); + } + + /** + * 解析token + * @param string $jwt + * @return array + */ + public function parseToken(string $jwt): array + { + $this->token = $jwt; + [$headb64, $bodyb64, $cryptob64] = explode('.', $this->token); + $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64)); + return [$payload->jti->id, $payload->jti->type, $payload->auth ?? '']; + } + + /** + * 验证token + */ + public function verifyToken() + { + JWT::$leeway = 60; + + JWT::decode($this->token, Env::get('app.app_key', $this->app_key) ?: $this->app_key, array('HS256')); + + $this->token = null; + } + + /** + * 获取token并放入令牌桶 + * @param int $id + * @param string $type + * @param array $params + * @return array + */ + public function createToken(int $id, string $type, array $params = []) + { + $tokenInfo = $this->getToken($id, $type, $params); + $exp = $tokenInfo['params']['exp'] - $tokenInfo['params']['iat'] + 60; + $res = CacheService::setTokenBucket(md5($tokenInfo['token']), ['uid' => $id, 'type' => $type, 'token' => $tokenInfo['token'], 'exp' => $exp], (int)$exp, $type); + if (!$res) { + throw new AdminException(ApiErrorCode::ERR_SAVE_TOKEN); + } + return $tokenInfo; + } +} diff --git a/crmeb/utils/Queue.php b/crmeb/utils/Queue.php new file mode 100644 index 0000000..688b191 --- /dev/null +++ b/crmeb/utils/Queue.php @@ -0,0 +1,209 @@ + +// +---------------------------------------------------------------------- + +namespace crmeb\utils; + +use crmeb\traits\ErrorTrait; +use think\exception\ValidateException; +use think\facade\Config; +use think\facade\Queue as QueueThink; +use think\facade\Log; + +/** + * Class Queue + * @package crmeb\utils + * @method $this do(string $do) 设置任务执行方法 + * @method $this job(string $job) 设置任务执行类名 + * @method $this errorCount(int $errorCount) 执行失败次数 + * @method $this data(...$data) 执行数据 + * @method $this secs(int $secs) 延迟执行秒数 + * @method $this log($log) 记录日志 + */ +class Queue +{ + + use ErrorTrait; + + /** + * 任务执行 + * @var string + */ + protected $do = 'doJob'; + + /** + * 默认任务执行方法名 + * @var string + */ + protected $defaultDo; + + /** + * 任务类名 + * @var string + */ + protected $job; + + /** + * 错误次数 + * @var int + */ + protected $errorCount = 3; + + /** + * 数据 + * @var array|string + */ + protected $data; + + /** + * 队列名 + * @var null + */ + protected $queueName = null; + + /** + * 延迟执行秒数 + * @var int + */ + protected $secs = 0; + + /** + * 记录日志 + * @var string|callable|array + */ + protected $log; + + /** + * @var array + */ + protected $rules = ['do', 'data', 'errorCount', 'job', 'secs', 'log']; + + /** + * @var static + */ + protected static $instance; + + /** + * Queue constructor. + */ + protected function __construct() + { + $this->defaultDo = $this->do; + } + + /** + * @return static + */ + public static function instance() + { + if (is_null(self::$instance)) { + self::$instance = new static(); + } + return self::$instance; + } + + /** + * 设置列名 + * @param string $queueName + * @return $this + */ + public function setQueueName(string $queueName) + { + $this->queueName = $queueName; + return $this; + } + + /** + * 放入消息队列 + * @param array|null $data + * @return mixed + */ + public function push(?array $data = null) + { + if (!$this->job) { + return $this->setError('需要执行的队列类必须存在'); + } + $jodValue = $this->getValues($data); + $res = QueueThink::{$this->action()}(...$jodValue); + if (!$res) { + $res = QueueThink::{$this->action()}(...$jodValue); + if (!$res) { + Log::error('加入队列失败,参数:' . json_encode($jodValue)); + } + } + $this->clean(); + return $res; + } + + /** + * 清除数据 + */ + public function clean() + { + $this->secs = 0; + $this->data = []; + $this->log = null; + $this->queueName = null; + $this->errorCount = 3; + $this->do = $this->defaultDo; + } + + /** + * 获取任务方式 + * @return string + */ + protected function action() + { + return $this->secs ? 'later' : 'push'; + } + + /** + * 获取参数 + * @param $data + * @return array + */ + protected function getValues($data) + { + $jobData['data'] = $data ?: $this->data; + $jobData['do'] = $this->do; + $jobData['errorCount'] = $this->errorCount; + $jobData['log'] = $this->log; + if (!class_exists($this->job)) { + throw new ValidateException('需要执行的队列类不存在'); + } + if ($this->do != $this->defaultDo) { + $this->job .= '@' . Config::get('queue.prefix', 'eb_') . $this->do; + } + if ($this->secs) { + return [$this->secs, $this->job, $jobData, $this->queueName]; + } else { + return [$this->job, $jobData, $this->queueName]; + } + } + + /** + * @param $name + * @param $arguments + * @return $this + */ + public function __call($name, $arguments) + { + if (in_array($name, $this->rules)) { + if ($name === 'data') { + $this->{$name} = $arguments; + } else { + $this->{$name} = $arguments[0] ?? null; + } + return $this; + } else { + throw new \RuntimeException('Method does not exist' . __CLASS__ . '->' . $name . '()'); + } + } +} diff --git a/crmeb/utils/Tag.php b/crmeb/utils/Tag.php new file mode 100644 index 0000000..6466056 --- /dev/null +++ b/crmeb/utils/Tag.php @@ -0,0 +1,84 @@ + + * +---------------------------------------------------------------------- + */ + +namespace crmeb\utils; + + +use think\cache\TagSet; +use think\Container; + +/** + * Class Tag + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/10 + * @package crmeb\utils + * @mixin TagSet + */ +class Tag +{ + + protected $tag; + + /** + * @var string + */ + protected $tagStr; + + /** + * Tag constructor. + * @param TagSet $set + * @param string $tagStr + */ + public function __construct(TagSet $set, string $tagStr) + { + $this->tag = $set; + $this->tagStr = $tagStr; + } + + /** + * @param string $name + * @param $value + * @param null $expire + * @return mixed + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/10 + */ + public function remember(string $name, $value, $expire = null) + { + //不开启数据缓存直接返回 + if (!app()->config->get('cache.is_data')) { + + if ($value instanceof \Closure) { + $value = Container::getInstance()->invokeFunction($value); + } + return $value; + } + + $name = $this->tagStr . $name; + return $this->tag->remember($name, $value, $expire); + } + + /** + * @param $name + * @param $arguments + * @author 等风来 + * @email 136327134@qq.com + * @date 2022/11/10 + */ + public function __call($name, $arguments) + { + $this->tag->{$name}(...$arguments); + } +} diff --git a/route/supplier.php b/route/supplier.php new file mode 100644 index 0000000..a4ca305 --- /dev/null +++ b/route/supplier.php @@ -0,0 +1,500 @@ +name('ajcaptcha'); + //图形验证码 + Route::post('ajcheck', 'Login/ajcheck')->name('ajcheck'); + //是否需要滑块验证接口 + Route::post('is_captcha', 'Login/getAjCaptcha')->name('getAjCaptcha'); + Route::get('code', 'Test/code')->name('code')->option(['real_name' => '测试验证码']); + Route::get('index', 'Test/index')->name('index')->option(['real_name' => '测试主页']); + Route::post('login', 'Login/login')->name('login')->option(['real_name' => '账号密码登录']); + Route::get('login/info', 'Login/info')->name('loginInfo')->option(['real_name' => '登录信息']); + Route::get('captcha_store', 'Login/captcha')->name('captcha')->option(['real_name' => '图片验证码']); + //获取版权 + Route::get('copyright', 'Common/getCopyright')->option(['real_name' => '获取版权']); + }); + + /** + * 只需登录不验证权限 + */ + Route::group(function () { + //获取logo + Route::get('logo', 'Common/getLogo')->option(['real_name' => '获取logo']); + //获取配置 + Route::get('config', 'Common/getConfig')->option(['real_name' => '获取配置']); + //获取未读消息 + Route::get('jnotice', 'Common/jnotice')->option(['real_name' => '获取未读消息']); + //获取省市区街道 + Route::get('city', 'Common/city')->option(['real_name' => '获取省市区街道']); + //获取搜索菜单列表 + Route::get('menusList', 'Common/menusList')->option(['real_name' => '搜索菜单列表']); + //退出登录 + Route::get('logout', 'Login/logOut')->option(['real_name' => '退出登录']); + //修改密码 + Route::put('updatePwd', 'staff.StoreStaff/updateStaffPwd')->option(['real_name' => '修改密码']); + //获取供应商信息 + Route::get('supplier', 'system.Supplier/read')->name('read')->option([['real_name' => '获取供应商信息']]); + //更新供应商信息 + Route::put('supplier', 'system.Supplier/update')->name('update')->option([['real_name' => '更新供应商信息']]); + //获取小票打印信息 + Route::get('printing', 'system.SupplierTicketPrint/read')->name('read')->option([['real_name' => '获取小票打印信息']]); + //更新供应商信息 + Route::put('printing', 'system.SupplierTicketPrint/update')->name('update')->option([['real_name' => '更新小票打印']]); + + //管理员资源路由 + Route::resource('admin', 'system.SupplierAdmin')->option(['real_name' => [ + 'index' => '获取管理员列表', + 'read' => '获取管理员详情', + 'create' => '获取创建管理员表单', + 'save' => '保存管理员', + 'edit' => '获取修改管理员表单', + 'update' => '修改管理员', + 'delete' => '删除管理员' + ]]); + + //修改管理员状态 + Route::put('admin/set_status/:id/:status', 'system.SupplierAdmin/set_status')->option(['real_name' => '修改管理员状态']); + //首页统计数据 + Route::get('home/header', 'Common/homeStatics')->option(['real_name' => '首页统计数据']); + //首页订单图表 + Route::get('home/order', 'Common/orderChart')->option(['real_name' => '首页订单图表']); + //订单来源分析 + Route::get('home/order_channel', 'Common/orderChannel')->option(['real_name' => '订单来源分析']); + //订单类型分析 + Route::get('home/order_type', 'Common/orderType')->option(['real_name' => '订单订单类型分析']); + })->middleware(AuthTokenMiddleware::class); + + + /** + * 基础管理 + */ + Route::group('system', function () { + //获取系统表单信息 + Route::get('form/info/:id', 'system.form.SystemForm/getInfo')->option(['real_name' => '获取系统表单信息']); + //获取所有系统表单 + Route::get('form/all_system_form', 'system.form.SystemForm/allSystemForm')->option(['real_name' => '获取所有系统表单']); + + Route::get('config/edit_new_build/:type', 'system.Config/getFormBuild')->option(['real_name' => '供应商配置表单']); + Route::post('config', 'system.Config/save')->option(['real_name' => '保存供应商配置']); + + })->middleware([ + AuthTokenMiddleware::class, + \app\http\middleware\supplier\SupplierCheckRoleMiddleware::class, + \app\http\middleware\supplier\SupplierLogMiddleware::class + ]); + + + /** + * 财务 + */ + Route::group('finance', function () { + //获取供应商财务信息 + Route::get('info', 'system.Supplier/getFinanceInfo')->option(['real_name' => '获取供应商财务信息']); + //设置供应商财务信息 + Route::post('info', 'system.Supplier/setFinanceInfo')->option(['real_name' => '设置供应商财务信息']); + //供应商转账列表 + Route::get('supplier_extract/list', 'finance.SupplierExtract/index')->option(['real_name' => '供应商转账列表']); + //供应商转账记录备注 + Route::post('supplier_extract/mark/:id', 'finance.SupplierExtract/mark')->option(['real_name' => '供应商转账记录备注']); + //供应商申请转账 + Route::post('supplier_extract/cash', 'finance.SupplierExtract/cash')->option(['real_name' => '供应商申请转账']); + //供应商流水列表 + Route::get('supplier_flowing_water/list', 'finance.SupplierFlowingWater/index')->option(['real_name' => '供应商流水列表']); + //获取交易类型 + Route::get('supplier_flowing_water/type', 'finance.SupplierFlowingWater/getType')->option(['real_name' => '获取交易类型']); + //供应商流水备注 + Route::post('supplier_flowing_water/mark/:id', 'finance.SupplierFlowingWater/mark')->option(['real_name' => '供应商流水备注']); + //供应商账单记录 + Route::get('supplier_flowing_water/fund_record', 'finance.SupplierFlowingWater/fundRecord')->option(['real_name' => '供应商账单记录']); + //供应商账单详情 + Route::get('supplier_flowing_water/fund_record_info', 'finance.SupplierFlowingWater/fundRecordInfo')->option(['real_name' => '供应商账单详情']); + })->middleware([ + AuthTokenMiddleware::class, + \app\http\middleware\supplier\SupplierCheckRoleMiddleware::class, + \app\http\middleware\supplier\SupplierLogMiddleware::class + ]); + + + /** + * 运费模版 + */ + Route::group('setting', function () { + //运费模板列表 + Route::get('shipping_templates/list', 'product.shipping.ShippingTemplates/temp_list')->option(['real_name' => '运费模板列表']); + //修改运费模板数据 + Route::get('shipping_templates/:id/edit', 'product.shipping.ShippingTemplates/edit')->option(['real_name' => '修改运费模板数据']); + //新增或修改运费模版 + Route::post('shipping_templates/save/:id', 'product.shipping.ShippingTemplates/save')->option(['real_name' => '新增或修改运费模版']); + //删除运费模板 + Route::delete('shipping_templates/del/:id', 'product.shipping.ShippingTemplates/delete')->option(['real_name' => '删除运费模板']); + //城市数据接口 + Route::get('shipping_templates/city_list', 'product.shipping.ShippingTemplates/city_list')->option(['real_name' => '城市数据接口']); + + })->middleware([ + AuthTokenMiddleware::class, + \app\http\middleware\supplier\SupplierCheckRoleMiddleware::class, + \app\http\middleware\supplier\SupplierLogMiddleware::class + ]); + + /** + * 商品 + */ + Route::group('product', function () { + //商品批量操作 + Route::post('batch_process', 'product.StoreProduct/batchProcess')->option(['real_name' => '商品批量操作']); + //商品分类列表 + Route::get('category', 'product.StoreProductCategory/index')->option(['real_name' => '商品分类列表']); + + //商品标签(分类)树形列表 + Route::get('product_label', 'product.label.StoreProductLabel/tree_list')->option(['real_name' => '用户标签(分类)树形列表']); + Route::get('all_label', 'product.label.StoreProductLabel/allLabel')->option(['real_name' => '所有的用户标签']); + Route::get('all_ensure', 'product.ensure.StoreProductEnsure/allEnsure')->option(['real_name' => '所有的保障服务']); + Route::get('all_specs', 'product.specs.StoreProductSpecs/allSpecs')->option(['real_name' => '所有的参数模版']); + Route::get('get_all_unit', 'product.StoreProductUnit/getAllUnit')->option(['real_name' => '获取所有商品单位']); + + //商品分类树形列表 + Route::get('category/tree/:type', 'product.StoreProductCategory/tree_list')->option(['real_name' => '商品分类树形列表']); + //商品分类cascader行列表 + Route::get('category/cascader_list/[:type]', 'product.StoreProductCategory/cascader_list')->option(['real_name' => '商品分类cascader行列表']); + //商品分类新增表单 + Route::get('category/create', 'product.StoreProductCategory/create')->option(['real_name' => '商品分类新增表单']); + //商品分类新增 + Route::post('category', 'product.StoreProductCategory/save')->option(['real_name' => '商品分类新增']); + //商品分类编辑表单 + Route::get('category/:id', 'product.StoreProductCategory/edit')->option(['real_name' => '商品分类编辑表单']); + //商品分类编辑 + Route::put('category/:id', 'product.StoreProductCategory/update')->option(['real_name' => '商品分类编辑']); + //删除商品分类 + Route::delete('category/:id', 'product.StoreProductCategory/delete')->option(['real_name' => '删除商品分类']); + //商品分类修改状态 + Route::put('category/set_show/:id/:is_show', 'product.StoreProductCategory/set_show')->option(['real_name' => '商品分类修改状态']); + //商品分类快捷编辑 + Route::put('category/set_category/:id', 'product.StoreProductCategory/set_category')->option(['real_name' => '商品分类快捷编辑']); + //获取运费模板 + Route::get('product/get_template', 'product.StoreProduct/get_template')->option(['real_name' => '获取运费模板']); + //上传视频密钥接口 + Route::get('product/get_temp_keys', 'product.StoreProduct/getTempKeys')->option(['real_name' => '上传视频密钥接口']); + //获取商品规则属性模板 + Route::get('product/get_rule', 'product.StoreProduct/get_rule')->option(['real_name' => '获取商品规则属性模板']); + //获取所有商品列表 + Route::get('product/list', 'product.StoreProduct/search_list')->option(['real_name' => '获取所有商品列表']); + //获取商品规格 + Route::get('product/attrs/:id', 'product.StoreProduct/getAttrs')->option(['real_name' => '获取商品规格']); + //快速批量修改库存 + Route::put('product/saveStocks/:id', 'product.StoreProduct/saveProductAttrsStock')->option(['real_name' => '快速批量修改库存']); + + //商品列表 + Route::get('product', 'product.StoreProduct/index')->option(['real_name' => '商品列表']); + //新建或修改商品 + Route::post('product/:id', 'product.StoreProduct/save')->option(['real_name' => '新建或修改商品']); + //商品放入回收站 + Route::delete('product/:id', 'product.StoreProduct/delete')->option(['real_name' => '商品放入回收站']); + //修改商品状态 + Route::put('product/set_show/:id/:is_show', 'product.StoreProduct/set_show')->option(['real_name' => '修改商品状态']); + //设置批量商品上架 + Route::put('product/product_show', 'product.StoreProduct/product_show')->option(['real_name' => '设置批量商品上架']); + //设置批量商品下架 + Route::put('product/product_unshow', 'product.StoreProduct/product_unshow')->option(['real_name' => '设置批量商品下架']); + + //获取关联用户标签 + Route::get('getUserLabel', 'product.StoreProduct/getUserLabel')->option(['real_name' => '获取关联用户标签']); + //商品规则列表 + Route::get('product/rule', 'product.StoreProductRule/index')->option(['real_name' => '商品规则列表']); + //新建或编辑商品规则 + Route::post('product/rule/:id', 'product.StoreProductRule/save')->option(['real_name' => '新建或编辑商品规则']); + //商品规则详情 + Route::get('product/rule/:id', 'product.StoreProductRule/read')->option(['real_name' => '商品规则详情']); + //删除商品规则 + Route::delete('product/rule/delete/:id', 'product.StoreProductRule/delete')->option(['real_name' => '删除商品规则']); + + //商品详情 + Route::get('product/:id', 'product.StoreProduct/get_product_info')->option(['real_name' => '商品详情']); + + //商品列表头部数据 + Route::get('type_header', 'product.StoreProduct/type_header')->option(['real_name' => '商品列表头部数据']); + //修改商品状态 + Route::put('product/set_show/:id/:is_show', 'product.StoreProduct/set_show')->option(['real_name' => '修改商品状态']); + //生成商品规格列表 + Route::post('generate_attr/:id/:type', 'product.StoreProduct/is_format_attr')->option(['real_name' => '生成商品规格列表']); + //商品评价 + //商品评论列表 + Route::get('reply', 'product.StoreProductReply/index')->option(['real_name' => '商品评论列表']); + //商品回复评论 + Route::put('reply/set_reply/:id', 'product.StoreProductReply/set_reply')->option(['real_name' => '商品回复评论']); + //删除商品评论 + Route::delete('reply/:id', 'product.StoreProductReply/delete')->option(['real_name' => '删除商品评论']); + + //商品品牌cascader行列表 + Route::get('brand/cascader_list/[:type]', 'product.StoreBrand/cascader_list')->option(['real_name' => '商品品牌cascader行列表']); + + //商品标签 + Route::post('label/:id', 'product.label.StoreProductLabel/save')->option(['real_name' => '保存商品标签']); + Route::delete('label/:id', 'product.label.StoreProductLabel/delete')->option(['real_name' => '删除商品标签']); + Route::get('label/form', 'product.label.StoreProductLabel/getLabelForm')->option(['real_name' => '获取商品标签表单']); + + + })->middleware([ + AuthTokenMiddleware::class, + \app\http\middleware\supplier\SupplierCheckRoleMiddleware::class, + \app\http\middleware\supplier\SupplierLogMiddleware::class + ]); + + /** + * 附件相关路由 + */ + Route::group('file', function () { + //图片附件列表 + Route::get('file', 'file.SystemAttachment/index')->option(['real_name' => '图片附件列表']); + //删除图片 + Route::post('file/delete', 'file.SystemAttachment/delete')->option(['real_name' => '删除图片']); + //移动图片分类表单 + Route::get('file/move', 'file.SystemAttachment/move')->option(['real_name' => '移动图片分类表单']); + //移动图片分类 + Route::put('file/do_move', 'file.SystemAttachment/moveImageCate')->option(['real_name' => '移动图片分类']); + //修改图片名称 + Route::put('file/update/:id', 'file.SystemAttachment/update')->option(['real_name' => '修改图片名称']); + //上传图片 + Route::post('upload/[:upload_type]', 'file.SystemAttachment/upload')->option(['real_name' => '上传图片']); + //获取上传类型 + Route::get('upload_type', 'file.SystemAttachment/uploadType')->option(['real_name' => '上传类型']); + //分片上传本地视频 + Route::post('video_upload', 'file.SystemAttachment/videoUpload')->option(['real_name' => '分片上传本地视频']); + //oss视频素材保存 + Route::post('video_attachment', 'file.SystemAttachment/saveVideoAttachment')->option(['real_name' => '视频素材保存']); + + //获取扫码上传页面链接以及参数 + Route::get('scan/qrcode', 'file.SystemAttachment/scanUploadQrcode')->option(['real_name' => '获取扫码上传页面链接以及参数']); + //删除二维码 + Route::get('remove/qrcode', 'file.SystemAttachment/removeUploadQrcode')->option(['real_name' => '删除二维码']); + //获取扫码上传的图片数据 + Route::get('scan/image/list/:scan_token', 'file.SystemAttachment/scanUploadImage')->option(['real_name' => '获取扫码上传的图片数据']); + //网络图片上传 + Route::post('online/upload', 'file.SystemAttachment/onlineUpload')->option(['real_name' => '网络图片上传']); + + //获取上传信息 + Route::get('get/way_data', 'file.SystemAttachment/getAdminsData')->option(['real_name' => '获取上传信息']); + //保存上传信息 + Route::get('set/way_data/:is_way', 'file.SystemAttachment/setAdminsData')->option(['real_name' => '保存上传信息']); + + //附件分类管理资源路由 + Route::resource('category', 'file.SystemAttachmentCategory')->option(['real_name' => [ + 'index' => '获取附件分类列表', + 'read' => '获取附件分类详情', + 'create' => '获取创建附件分类表单', + 'save' => '保存附件分类', + 'edit' => '获取修改附件分类表单', + 'update' => '修改附件分类', + 'delete' => '删除附件分类' + ]]); + })->middleware([AuthTokenMiddleware::class]); + + /** + * 订单路由 + */ + Route::group('order', function () { + //订单列表 + Route::get('list', 'Order/lst')->name('lst')->option(['real_name' => '订单列表']); + //订单列表获取配送员 + Route::get('delivery/list', 'Order/get_delivery_list')->option(['real_name' => '订单列表获取配送员']); + //获取物流公司 + Route::get('express_list', 'Order/express')->name('StoreOrdeRexpressList')->option(['real_name' => '获取物流公司']); + //获取订单可拆分商品列表 + Route::get('split_cart_info/:id', 'Order/split_cart_info')->name('StoreOrderSplitCartInfo')->option(['real_name' => '获取订单可拆分商品列表']); + //拆单发送货 + Route::put('split_delivery/:id', 'Order/split_delivery')->name('StoreOrderSplitDelivery')->option(['real_name' => '拆单发送货']); + //面单默认配置信息 + Route::get('sheet_info', 'Order/getDeliveryInfo')->option(['real_name' => '面单默认配置信息']); + //获取物流信息 + Route::get('express/:id', 'Order/get_express')->name('StoreOrderUpdateExpress')->option(['real_name' => '获取物流信息']); + //快递公司电子面单模版 + Route::get('express/temp', 'Order/express_temp')->option(['real_name' => '快递公司电子面单模版']); + //订单发送货 + Route::put('delivery/:id', 'Order/update_delivery')->name('StoreOrderUpdateDelivery')->option(['real_name' => '订单发送货']); + //打印订单 + Route::get('print/:id', 'Order/order_print')->name('StoreOrderPrint')->option(['real_name' => '打印订单']); + //确认收货 + Route::put('take/:id', 'Order/take_delivery')->name('StoreOrderTakeDelivery')->option(['real_name' => '确认收货']); + //修改备注信息 + Route::put('remark/:id', 'Order/remark')->name('StoreOrderorRemark')->option(['real_name' => '修改备注信息']); + //获取订单状态 + Route::get('status/:id', 'Order/status')->name('StoreOrderorStatus')->option(['real_name' => '获取订单状态']); + //拆单发送货 + Route::put('split_delivery/:id', 'Order/split_delivery')->name('StoreOrderSplitDelivery')->option(['real_name' => '拆单发送货']); + //获取订单拆分子订单列表 + Route::get('split_order/:id', 'Order/split_order')->name('StoreOrderSplitOrder')->option(['real_name' => '获取订单拆分子订单列表']); + //订单退款表单 + Route::get('refund/:id', 'Order/refund')->name('StoreOrderRefund')->option(['real_name' => '订单退款表单']); + //订单退款 + Route::put('refund/:id', 'Order/update_refund')->name('StoreOrderUpdateRefund')->option(['real_name' => '订单退款']); + //订单详情 + Route::get('info/:id', 'Order/order_info')->name('SupplierOrderInfo')->option(['real_name' => '订单详情']); + //批量发货 + Route::get('hand/batch_delivery', 'Order/hand_batch_delivery')->option(['real_name' => '批量发货']); + //面单默认配置信息 + Route::get('sheet_info', 'Order/getDeliveryInfo')->option(['real_name' => '面单默认配置信息']); + //获取不退款表单 + Route::get('no_refund/:id', 'Order/no_refund')->name('StoreOrderorNoRefund')->option(['real_name' => '获取不退款表单']); + //修改不退款理由 + Route::put('no_refund/:id', 'Order/update_un_refund')->name('StoreOrderorUpdateNoRefund')->option(['real_name' => '修改不退款理由']); + //线下支付 + Route::post('pay_offline/:id', 'Order/pay_offline')->name('StoreOrderorPayOffline')->option(['real_name' => '线下支付']); + //获取退积分表单 + Route::get('refund_integral/:id', 'Order/refund_integral')->name('StoreOrderorRefundIntegral')->option(['real_name' => '获取退积分表单']); + //修改退积分 + Route::put('refund_integral/:id', 'Order/update_refund_integral')->name('StoreOrderorUpdateRefundIntegral')->option(['real_name' => '修改退积分']); + //更多操作打印电子面单 + Route::get('order_dump/:order_id', 'Order/order_dump')->option(['real_name' => '更多操作打印电子面单']); + //删除单个订单 + Route::delete('del/:id', 'Order/del')->name('StoreOrderorDel')->option(['real_name' => '删除订单单个']); + //批量删除订单 + Route::post('dels', 'Order/del_orders')->name('StoreOrderorDels')->option(['real_name' => '批量删除订单']); + //获取订单编辑表单 + Route::get('edit/:id', 'Order/edit')->name('StoreOrderEdit')->option(['real_name' => '获取订单编辑表单']); + //修改订单 + Route::put('update/:id', 'Order/update')->name('StoreOrderUpdate')->option(['real_name' => '修改订单']); + //获取配送信息表单 + Route::get('distribution/:id', 'Order/distribution')->name('StoreOrderDistribution')->option(['real_name' => '获取配送信息表单']); + //修改配送信息 + Route::put('distribution/:id', 'Order/update_distribution')->name('StoreOrderUpdateDistribution')->option(['real_name' => '修改配送信息']); + // //订单核销 TODO:供应商暂时无需核销 + // Route::post('write', 'Order/write_order')->name('writeOrder')->option(['real_name' => '订单核销']); + // //订单号核销 + // Route::put('write_update/:order_id', 'Order/write_update')->name('writeOrderUpdate')->option(['real_name' => '订单号核销']); + //快递公司电子面单模版 + Route::get('express/temp', 'Order/express_temp')->option(['real_name' => '快递公司电子面单模版']); + //打印配货单信息 + Route::get('distribution_info', 'Order/distributionInfo')->name('StoreOrderDistributionInfo')->option(['real_name' => '打印配货单信息']); + + //获取线下付款二维码 + Route::get('offline_scan', 'v1.order.OtherOrder/offline_scan')->name('OfflineScan')->option(['real_name' => '获取线下付款二维码']); + //线下收银列表 + Route::get('scan_list', 'v1.order.OtherOrder/scan_list')->name('ScanList')->option(['real_name' => '线下收银列表']); + //发票列表头部统计 + Route::get('invoice/chart', 'v1.order.StoreOrderInvoice/chart')->name('StoreOrderorInvoiceChart')->option(['real_name' => '发票列表头部统计']); + //申请发票列表 + Route::get('invoice/list', 'v1.order.StoreOrderInvoice/list')->name('StoreOrderorInvoiceList')->option(['real_name' => '申请发票列表']); + //设置发票状态 + Route::post('invoice/set/:id', 'v1.order.StoreOrderInvoice/set_invoice')->name('StoreOrderorInvoiceSet')->option(['real_name' => '设置发票状态']); + //开票订单详情 + Route::get('invoice_order_info/:id', 'v1.order.StoreOrderInvoice/orderInfo')->name('StoreOrderorInvoiceOrderInfo')->option(['real_name' => '开票订单详情']); + //电子面单模板列表 + Route::get('expr/temp', 'v1.order.StoreOrder/expr_temp')->option(['real_name' => '电子面单模板列表']); + + })->middleware([ + AuthTokenMiddleware::class, + \app\http\middleware\supplier\SupplierCheckRoleMiddleware::class, + \app\http\middleware\supplier\SupplierLogMiddleware::class + ]); + + /** + * 导出excel相关路由 + */ + Route::group('export', function () { + //订单 + Route::get('storeOrder', 'export.ExportExcel/storeOrder')->option(['real_name' => '订单导出']); + //物流公司对照表导出 + Route::get('expressList', 'export.ExportExcel/expressList')->option(['real_name' => '物流公司对照表导出']); + //导出批量发货记录 + Route::get('batchOrderDelivery/:id/:queueType/:cacheType', 'export.ExportExcel/batchOrderDelivery')->option(['real_name' => '批量发货记录导出']); + //供应商账单导出 + Route::get('financeRecord', 'export.ExportExcel/financeRecord')->option(['real_name' => '供应商账单导出']); + })->middleware([ + AuthTokenMiddleware::class, + \app\http\middleware\supplier\SupplierCheckRoleMiddleware::class, + \app\http\middleware\supplier\SupplierLogMiddleware::class + ]); + + /** + * 用户模块 相关路由 + */ + Route::group('user', function () { + //用户信息 + Route::get('user/:id', 'user.User/read')->name('read')->option(['real_name' => '用户信息']); + //获取指定用户的信息 + Route::get('one_info/:id', 'user.User/oneUserInfo')->name('oneUserInfo')->option(['real_name' => '获取指定用户的信息']); + //商品浏览列表 + Route::get('visit_list/:id', 'user.User/visitList')->name('visitList')->option(['real_name' => '商品浏览列表']); + //推荐人记录列表 + Route::get('spread_list/:id', 'user.User/spreadList')->name('spreadList')->option(['real_name' => '推荐人记录列表']); + })->middleware([ + AuthTokenMiddleware::class, + \app\http\middleware\supplier\SupplierCheckRoleMiddleware::class, + \app\http\middleware\supplier\SupplierLogMiddleware::class + ]); + + /** + * 队列任务 相关路由 + */ + Route::group('queue', function () { + //队列任务列表 + Route::get('index', 'queue.Queue/index')->name('index')->option(['real_name' => '队列任务列表']); + //队列批量发货记录 + Route::get('delivery/log/:id/:type', 'queue.Queue/delivery_log')->option(['real_name' => '队列批量发货记录']); + //再次执行批量队列任务 + Route::get('again/do_queue/:id/:type', 'queue.Queue/again_do_queue')->option(['real_name' => '再次执行批量队列任务']); + //清除异常任务队列 + Route::get('del/wrong_queue/:id/:type', 'queue.Queue/del_wrong_queue')->option(['real_name' => '清除异常任务队列']); + //停止队列任务 + Route::get('stop/wrong_queue/:id', 'queue.Queue/stop_wrong_queue')->option(['real_name' => '停止队列任务']); + })->middleware([ + AuthTokenMiddleware::class, + \app\http\middleware\supplier\SupplierCheckRoleMiddleware::class, + \app\http\middleware\supplier\SupplierLogMiddleware::class + ]); + + /** + * 售后 相关路由 + */ + Route::group('refund', function () { + //售后列表 + Route::get('list', 'Refund/getRefundList')->option(['real_name' => '售后订单列表']); + //商家同意退款,等待用户退货 + Route::get('agree/:order_id', 'Refund/agreeRefund')->option(['real_name' => '商家同意退款,等待用户退货']); + //售后订单备注 + Route::put('remark/:id', 'Refund/remark')->option(['real_name' => '售后订单备注']); + //售后订单退款表单 + Route::get('refund/:id', 'Refund/refund')->name('StoreOrderRefund')->option(['real_name' => '售后订单退款表单']); + //售后订单退款 + Route::put('refund/:id', 'Refund/update_refund')->name('StoreOrderUpdateRefund')->option(['real_name' => '售后订单退款']); + //售后详情 + Route::get('detail/:id', 'Refund/detail')->option(['real_name' => '售后订单详情']); + })->middleware([ + AuthTokenMiddleware::class, + \app\http\middleware\supplier\SupplierCheckRoleMiddleware::class, + \app\http\middleware\supplier\SupplierLogMiddleware::class + ]); + + /** + * miss 路由 + */ + Route::miss(function () { + if (app()->request->isOptions()) { + $header = Config::get('cookie.header'); + $header['Access-Control-Allow-Origin'] = app()->request->header('origin'); + return Response::create('ok')->code(200)->header($header); + } else + return Response::create()->code(404); + }); + +})->prefix('supplier.')->middleware(InstallMiddleware::class)->middleware(AllowOriginMiddleware::class)->middleware(StationOpenMiddleware::class); + + diff --git a/vendor/doctrine/annotations/README.md b/vendor/doctrine/annotations/README.md index ebb30e0..c2c7eb7 100644 --- a/vendor/doctrine/annotations/README.md +++ b/vendor/doctrine/annotations/README.md @@ -1,19 +1,18 @@ # Doctrine Annotations -[![Build Status](https://travis-ci.org/doctrine/annotations.png?branch=master)](https://travis-ci.org/doctrine/annotations) +[![Build Status](https://github.com/doctrine/annotations/workflows/Continuous%20Integration/badge.svg?label=build)](https://github.com/doctrine/persistence/actions) +[![Dependency Status](https://www.versioneye.com/package/php--doctrine--annotations/badge.png)](https://www.versioneye.com/package/php--doctrine--annotations) +[![Reference Status](https://www.versioneye.com/php/doctrine:annotations/reference_badge.svg)](https://www.versioneye.com/php/doctrine:annotations/references) +[![Total Downloads](https://poser.pugx.org/doctrine/annotations/downloads.png)](https://packagist.org/packages/doctrine/annotations) +[![Latest Stable Version](https://img.shields.io/packagist/v/doctrine/annotations.svg?label=stable)](https://packagist.org/packages/doctrine/annotations) Docblock Annotations Parser library (extracted from [Doctrine Common](https://github.com/doctrine/common)). -## Changelog +## Documentation -### v1.2.0 +See the [doctrine-project website](https://www.doctrine-project.org/projects/doctrine-annotations/en/latest/index.html). - * HHVM support - * Allowing dangling comma in annotations - * Excluded annotations are no longer autoloaded - * Importing namespaces also in traits - * Added support for `::class` 5.5-style constant, works also in 5.3 and 5.4 +## Contributing -### v1.1 - - * Add Exception when ZendOptimizer+ or Opcache is configured to drop comments +When making a pull request, make sure your changes follow the +[Coding Standard Guidelines](https://www.doctrine-project.org/projects/doctrine-coding-standard/en/current/reference/index.html#introduction). diff --git a/vendor/doctrine/annotations/composer.json b/vendor/doctrine/annotations/composer.json index 1c65f6c..b4dfbd3 100644 --- a/vendor/doctrine/annotations/composer.json +++ b/vendor/doctrine/annotations/composer.json @@ -1,31 +1,69 @@ { "name": "doctrine/annotations", - "type": "library", "description": "Docblock Annotations Parser", - "keywords": ["annotations", "docblock", "parser"], - "homepage": "http://www.doctrine-project.org", "license": "MIT", - "authors": [ - {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, - {"name": "Roman Borschel", "email": "roman@code-factory.org"}, - {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"}, - {"name": "Jonathan Wage", "email": "jonwage@gmail.com"}, - {"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"} + "type": "library", + "keywords": [ + "annotations", + "docblock", + "parser" ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "homepage": "https://www.doctrine-project.org/projects/annotations.html", "require": { - "php": ">=5.3.2", - "doctrine/lexer": "1.*" + "php": "^7.1 || ^8.0", + "ext-tokenizer": "*", + "doctrine/lexer": "1.*", + "psr/cache": "^1 || ^2 || ^3" }, "require-dev": { - "doctrine/cache": "1.*", - "phpunit/phpunit": "4.*" + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^6.0 || ^8.1", + "phpstan/phpstan": "^1.4.10 || ^1.8.0", + "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", + "symfony/cache": "^4.4 || ^5.2", + "vimeo/psalm": "^4.10" }, "autoload": { - "psr-0": { "Doctrine\\Common\\Annotations\\": "lib/" } - }, - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" } + }, + "autoload-dev": { + "psr-4": { + "Doctrine\\Performance\\Common\\Annotations\\": "tests/Doctrine/Performance/Common/Annotations", + "Doctrine\\Tests\\Common\\Annotations\\": "tests/Doctrine/Tests/Common/Annotations" + }, + "files": [ + "tests/Doctrine/Tests/Common/Annotations/Fixtures/functions.php", + "tests/Doctrine/Tests/Common/Annotations/Fixtures/SingleClassLOC1000.php" + ] + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "sort-packages": true } } diff --git a/vendor/doctrine/annotations/docs/en/annotations.rst b/vendor/doctrine/annotations/docs/en/annotations.rst new file mode 100644 index 0000000..2c3c428 --- /dev/null +++ b/vendor/doctrine/annotations/docs/en/annotations.rst @@ -0,0 +1,252 @@ +Handling Annotations +==================== + +There are several different approaches to handling annotations in PHP. +Doctrine Annotations maps docblock annotations to PHP classes. Because +not all docblock annotations are used for metadata purposes a filter is +applied to ignore or skip classes that are not Doctrine annotations. + +Take a look at the following code snippet: + +.. code-block:: php + + namespace MyProject\Entities; + + use Doctrine\ORM\Mapping AS ORM; + use Symfony\Component\Validator\Constraints AS Assert; + + /** + * @author Benjamin Eberlei + * @ORM\Entity + * @MyProject\Annotations\Foobarable + */ + class User + { + /** + * @ORM\Id @ORM\Column @ORM\GeneratedValue + * @dummy + * @var int + */ + private $id; + + /** + * @ORM\Column(type="string") + * @Assert\NotEmpty + * @Assert\Email + * @var string + */ + private $email; + } + +In this snippet you can see a variety of different docblock annotations: + +- Documentation annotations such as ``@var`` and ``@author``. These + annotations are ignored and never considered for throwing an + exception due to wrongly used annotations. +- Annotations imported through use statements. The statement ``use + Doctrine\ORM\Mapping AS ORM`` makes all classes under that namespace + available as ``@ORM\ClassName``. Same goes for the import of + ``@Assert``. +- The ``@dummy`` annotation. It is not a documentation annotation and + not ignored. For Doctrine Annotations it is not entirely clear how + to handle this annotation. Depending on the configuration an exception + (unknown annotation) will be thrown when parsing this annotation. +- The fully qualified annotation ``@MyProject\Annotations\Foobarable``. + This is transformed directly into the given class name. + +How are these annotations loaded? From looking at the code you could +guess that the ORM Mapping, Assert Validation and the fully qualified +annotation can just be loaded using +the defined PHP autoloaders. This is not the case however: For error +handling reasons every check for class existence inside the +``AnnotationReader`` sets the second parameter $autoload +of ``class_exists($name, $autoload)`` to false. To work flawlessly the +``AnnotationReader`` requires silent autoloaders which many autoloaders are +not. Silent autoloading is NOT part of the `PSR-0 specification +`_ +for autoloading. + +This is why Doctrine Annotations uses its own autoloading mechanism +through a global registry. If you are wondering about the annotation +registry being global, there is no other way to solve the architectural +problems of autoloading annotation classes in a straightforward fashion. +Additionally if you think about PHP autoloading then you recognize it is +a global as well. + +To anticipate the configuration section, making the above PHP class work +with Doctrine Annotations requires this setup: + +.. code-block:: php + + use Doctrine\Common\Annotations\AnnotationReader; + use Doctrine\Common\Annotations\AnnotationRegistry; + + AnnotationRegistry::registerFile("/path/to/doctrine/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php"); + AnnotationRegistry::registerAutoloadNamespace("Symfony\Component\Validator\Constraint", "/path/to/symfony/src"); + AnnotationRegistry::registerAutoloadNamespace("MyProject\Annotations", "/path/to/myproject/src"); + + $reader = new AnnotationReader(); + AnnotationReader::addGlobalIgnoredName('dummy'); + +The second block with the annotation registry calls registers all the +three different annotation namespaces that are used. +Doctrine Annotations saves all its annotations in a single file, that is +why ``AnnotationRegistry#registerFile`` is used in contrast to +``AnnotationRegistry#registerAutoloadNamespace`` which creates a PSR-0 +compatible loading mechanism for class to file names. + +In the third block, we create the actual ``AnnotationReader`` instance. +Note that we also add ``dummy`` to the global list of ignored +annotations for which we do not throw exceptions. Setting this is +necessary in our example case, otherwise ``@dummy`` would trigger an +exception to be thrown during the parsing of the docblock of +``MyProject\Entities\User#id``. + +Setup and Configuration +----------------------- + +To use the annotations library is simple, you just need to create a new +``AnnotationReader`` instance: + +.. code-block:: php + + $reader = new \Doctrine\Common\Annotations\AnnotationReader(); + +This creates a simple annotation reader with no caching other than in +memory (in php arrays). Since parsing docblocks can be expensive you +should cache this process by using a caching reader. + +To cache annotations, you can create a ``Doctrine\Common\Annotations\PsrCachedReader``. +This reader decorates the original reader and stores all annotations in a PSR-6 +cache: + +.. code-block:: php + + use Doctrine\Common\Annotations\AnnotationReader; + use Doctrine\Common\Annotations\PsrCachedReader; + + $cache = ... // instantiate a PSR-6 Cache pool + + $reader = new PsrCachedReader( + new AnnotationReader(), + $cache, + $debug = true + ); + +The ``debug`` flag is used here as well to invalidate the cache files +when the PHP class with annotations changed and should be used during +development. + +.. warning :: + + The ``AnnotationReader`` works and caches under the + assumption that all annotations of a doc-block are processed at + once. That means that annotation classes that do not exist and + aren't loaded and cannot be autoloaded (using the + AnnotationRegistry) would never be visible and not accessible if a + cache is used unless the cache is cleared and the annotations + requested again, this time with all annotations defined. + +By default the annotation reader returns a list of annotations with +numeric indexes. If you want your annotations to be indexed by their +class name you can wrap the reader in an ``IndexedReader``: + +.. code-block:: php + + use Doctrine\Common\Annotations\AnnotationReader; + use Doctrine\Common\Annotations\IndexedReader; + + $reader = new IndexedReader(new AnnotationReader()); + +.. warning:: + + You should never wrap the indexed reader inside a cached reader, + only the other way around. This way you can re-use the cache with + indexed or numeric keys, otherwise your code may experience failures + due to caching in a numerical or indexed format. + +Registering Annotations +~~~~~~~~~~~~~~~~~~~~~~~ + +As explained in the introduction, Doctrine Annotations uses its own +autoloading mechanism to determine if a given annotation has a +corresponding PHP class that can be autoloaded. For annotation +autoloading you have to configure the +``Doctrine\Common\Annotations\AnnotationRegistry``. There are three +different mechanisms to configure annotation autoloading: + +- Calling ``AnnotationRegistry#registerFile($file)`` to register a file + that contains one or more annotation classes. +- Calling ``AnnotationRegistry#registerNamespace($namespace, $dirs = + null)`` to register that the given namespace contains annotations and + that their base directory is located at the given $dirs or in the + include path if ``NULL`` is passed. The given directories should *NOT* + be the directory where classes of the namespace are in, but the base + directory of the root namespace. The AnnotationRegistry uses a + namespace to directory separator approach to resolve the correct path. +- Calling ``AnnotationRegistry#registerLoader($callable)`` to register + an autoloader callback. The callback accepts the class as first and + only parameter and has to return ``true`` if the corresponding file + was found and included. + +.. note:: + + Loaders have to fail silently, if a class is not found even if it + matches for example the namespace prefix of that loader. Never is a + loader to throw a warning or exception if the loading failed + otherwise parsing doc block annotations will become a huge pain. + +A sample loader callback could look like: + +.. code-block:: php + + use Doctrine\Common\Annotations\AnnotationRegistry; + use Symfony\Component\ClassLoader\UniversalClassLoader; + + AnnotationRegistry::registerLoader(function($class) { + $file = str_replace("\\", DIRECTORY_SEPARATOR, $class) . ".php"; + + if (file_exists("/my/base/path/" . $file)) { + // file_exists() makes sure that the loader fails silently + require "/my/base/path/" . $file; + } + }); + + $loader = new UniversalClassLoader(); + AnnotationRegistry::registerLoader(array($loader, "loadClass")); + + +Ignoring missing exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default an exception is thrown from the ``AnnotationReader`` if an +annotation was found that: + +- is not part of the list of ignored "documentation annotations"; +- was not imported through a use statement; +- is not a fully qualified class that exists. + +You can disable this behavior for specific names if your docblocks do +not follow strict requirements: + +.. code-block:: php + + $reader = new \Doctrine\Common\Annotations\AnnotationReader(); + AnnotationReader::addGlobalIgnoredName('foo'); + +PHP Imports +~~~~~~~~~~~ + +By default the annotation reader parses the use-statement of a php file +to gain access to the import rules and register them for the annotation +processing. Only if you are using PHP Imports can you validate the +correct usage of annotations and throw exceptions if you misspelled an +annotation. This mechanism is enabled by default. + +To ease the upgrade path, we still allow you to disable this mechanism. +Note however that we will remove this in future versions: + +.. code-block:: php + + $reader = new \Doctrine\Common\Annotations\AnnotationReader(); + $reader->setEnabledPhpImports(false); diff --git a/vendor/doctrine/annotations/docs/en/custom.rst b/vendor/doctrine/annotations/docs/en/custom.rst new file mode 100644 index 0000000..11fbe1a --- /dev/null +++ b/vendor/doctrine/annotations/docs/en/custom.rst @@ -0,0 +1,443 @@ +Custom Annotation Classes +========================= + +If you want to define your own annotations, you just have to group them +in a namespace and register this namespace in the ``AnnotationRegistry``. +Annotation classes have to contain a class-level docblock with the text +``@Annotation``: + +.. code-block:: php + + namespace MyCompany\Annotations; + + /** @Annotation */ + class Bar + { + // some code + } + +Inject annotation values +------------------------ + +The annotation parser checks if the annotation constructor has arguments, +if so then it will pass the value array, otherwise it will try to inject +values into public properties directly: + + +.. code-block:: php + + namespace MyCompany\Annotations; + + /** + * @Annotation + * + * Some Annotation using a constructor + */ + class Bar + { + private $foo; + + public function __construct(array $values) + { + $this->foo = $values['foo']; + } + } + + /** + * @Annotation + * + * Some Annotation without a constructor + */ + class Foo + { + public $bar; + } + +Optional: Constructors with Named Parameters +-------------------------------------------- + +Starting with Annotations v1.11 a new annotation instantiation strategy +is available that aims at compatibility of Annotation classes with the PHP 8 +attribute feature. You need to declare a constructor with regular parameter +names that match the named arguments in the annotation syntax. + +To enable this feature, you can tag your annotation class with +``@NamedArgumentConstructor`` (available from v1.12) or implement the +``Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation`` interface +(available from v1.11 and deprecated as of v1.12). +When using the ``@NamedArgumentConstructor`` tag, the first argument of the +constructor is considered as the default one. + + +Usage with the ``@NamedArgumentContrustor`` tag + +.. code-block:: php + + namespace MyCompany\Annotations; + + /** + * @Annotation + * @NamedArgumentConstructor + */ + class Bar implements NamedArgumentConstructorAnnotation + { + private $foo; + + public function __construct(string $foo) + { + $this->foo = $foo; + } + } + + /** Usable with @Bar(foo="baz") */ + /** Usable with @Bar("baz") */ + +In combination with PHP 8's constructor property promotion feature +you can simplify this to: + +.. code-block:: php + + namespace MyCompany\Annotations; + + /** + * @Annotation + * @NamedArgumentConstructor + */ + class Bar implements NamedArgumentConstructorAnnotation + { + public function __construct(private string $foo) {} + } + + +Usage with the +``Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation`` +interface (v1.11, deprecated as of v1.12): +.. code-block:: php + + namespace MyCompany\Annotations; + + use Doctrine\Common\Annotations\NamedArgumentConstructorAnnotation; + + /** @Annotation */ + class Bar implements NamedArgumentConstructorAnnotation + { + private $foo; + + public function __construct(private string $foo) {} + } + + /** Usable with @Bar(foo="baz") */ + +Annotation Target +----------------- + +``@Target`` indicates the kinds of class elements to which an annotation +type is applicable. Then you could define one or more targets: + +- ``CLASS`` Allowed in class docblocks +- ``PROPERTY`` Allowed in property docblocks +- ``METHOD`` Allowed in the method docblocks +- ``FUNCTION`` Allowed in function dockblocks +- ``ALL`` Allowed in class, property, method and function docblocks +- ``ANNOTATION`` Allowed inside other annotations + +If the annotations is not allowed in the current context, an +``AnnotationException`` is thrown. + +.. code-block:: php + + namespace MyCompany\Annotations; + + /** + * @Annotation + * @Target({"METHOD","PROPERTY"}) + */ + class Bar + { + // some code + } + + /** + * @Annotation + * @Target("CLASS") + */ + class Foo + { + // some code + } + +Attribute types +--------------- + +The annotation parser checks the given parameters using the phpdoc +annotation ``@var``, The data type could be validated using the ``@var`` +annotation on the annotation properties or using the ``@Attributes`` and +``@Attribute`` annotations. + +If the data type does not match you get an ``AnnotationException`` + +.. code-block:: php + + namespace MyCompany\Annotations; + + /** + * @Annotation + * @Target({"METHOD","PROPERTY"}) + */ + class Bar + { + /** @var mixed */ + public $mixed; + + /** @var boolean */ + public $boolean; + + /** @var bool */ + public $bool; + + /** @var float */ + public $float; + + /** @var string */ + public $string; + + /** @var integer */ + public $integer; + + /** @var array */ + public $array; + + /** @var SomeAnnotationClass */ + public $annotation; + + /** @var array */ + public $arrayOfIntegers; + + /** @var array */ + public $arrayOfAnnotations; + } + + /** + * @Annotation + * @Target({"METHOD","PROPERTY"}) + * @Attributes({ + * @Attribute("stringProperty", type = "string"), + * @Attribute("annotProperty", type = "SomeAnnotationClass"), + * }) + */ + class Foo + { + public function __construct(array $values) + { + $this->stringProperty = $values['stringProperty']; + $this->annotProperty = $values['annotProperty']; + } + + // some code + } + +Annotation Required +------------------- + +``@Required`` indicates that the field must be specified when the +annotation is used. If it is not used you get an ``AnnotationException`` +stating that this value can not be null. + +Declaring a required field: + +.. code-block:: php + + /** + * @Annotation + * @Target("ALL") + */ + class Foo + { + /** @Required */ + public $requiredField; + } + +Usage: + +.. code-block:: php + + /** @Foo(requiredField="value") */ + public $direction; // Valid + + /** @Foo */ + public $direction; // Required field missing, throws an AnnotationException + + +Enumerated values +----------------- + +- An annotation property marked with ``@Enum`` is a field that accepts a + fixed set of scalar values. +- You should use ``@Enum`` fields any time you need to represent fixed + values. +- The annotation parser checks the given value and throws an + ``AnnotationException`` if the value does not match. + + +Declaring an enumerated property: + +.. code-block:: php + + /** + * @Annotation + * @Target("ALL") + */ + class Direction + { + /** + * @Enum({"NORTH", "SOUTH", "EAST", "WEST"}) + */ + public $value; + } + +Annotation usage: + +.. code-block:: php + + /** @Direction("NORTH") */ + public $direction; // Valid value + + /** @Direction("NORTHEAST") */ + public $direction; // Invalid value, throws an AnnotationException + + +Constants +--------- + +The use of constants and class constants is available on the annotations +parser. + +The following usages are allowed: + +.. code-block:: php + + namespace MyCompany\Entity; + + use MyCompany\Annotations\Foo; + use MyCompany\Annotations\Bar; + use MyCompany\Entity\SomeClass; + + /** + * @Foo(PHP_EOL) + * @Bar(Bar::FOO) + * @Foo({SomeClass::FOO, SomeClass::BAR}) + * @Bar({SomeClass::FOO_KEY = SomeClass::BAR_VALUE}) + */ + class User + { + } + + +Be careful with constants and the cache ! + +.. note:: + + The cached reader will not re-evaluate each time an annotation is + loaded from cache. When a constant is changed the cache must be + cleaned. + + +Usage +----- + +Using the library API is simple. Using the annotations described in the +previous section, you can now annotate other classes with your +annotations: + +.. code-block:: php + + namespace MyCompany\Entity; + + use MyCompany\Annotations\Foo; + use MyCompany\Annotations\Bar; + + /** + * @Foo(bar="foo") + * @Bar(foo="bar") + */ + class User + { + } + +Now we can write a script to get the annotations above: + +.. code-block:: php + + $reflClass = new ReflectionClass('MyCompany\Entity\User'); + $classAnnotations = $reader->getClassAnnotations($reflClass); + + foreach ($classAnnotations AS $annot) { + if ($annot instanceof \MyCompany\Annotations\Foo) { + echo $annot->bar; // prints "foo"; + } else if ($annot instanceof \MyCompany\Annotations\Bar) { + echo $annot->foo; // prints "bar"; + } + } + +You have a complete API for retrieving annotation class instances from a +class, property or method docblock: + + +Reader API +~~~~~~~~~~ + +Access all annotations of a class +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: php + + public function getClassAnnotations(\ReflectionClass $class); + +Access one annotation of a class +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: php + + public function getClassAnnotation(\ReflectionClass $class, $annotationName); + +Access all annotations of a method +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: php + + public function getMethodAnnotations(\ReflectionMethod $method); + +Access one annotation of a method +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: php + + public function getMethodAnnotation(\ReflectionMethod $method, $annotationName); + +Access all annotations of a property +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: php + + public function getPropertyAnnotations(\ReflectionProperty $property); + +Access one annotation of a property +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: php + + public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName); + +Access all annotations of a function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: php + + public function getFunctionAnnotations(\ReflectionFunction $property); + +Access one annotation of a function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: php + + public function getFunctionAnnotation(\ReflectionFunction $property, $annotationName); diff --git a/vendor/doctrine/annotations/docs/en/index.rst b/vendor/doctrine/annotations/docs/en/index.rst new file mode 100644 index 0000000..95476c3 --- /dev/null +++ b/vendor/doctrine/annotations/docs/en/index.rst @@ -0,0 +1,101 @@ +Introduction +============ + +Doctrine Annotations allows to implement custom annotation +functionality for PHP classes and functions. + +.. code-block:: php + + class Foo + { + /** + * @MyAnnotation(myProperty="value") + */ + private $bar; + } + +Annotations aren't implemented in PHP itself which is why this component +offers a way to use the PHP doc-blocks as a place for the well known +annotation syntax using the ``@`` char. + +Annotations in Doctrine are used for the ORM configuration to build the +class mapping, but it can be used in other projects for other purposes +too. + +Installation +============ + +You can install the Annotation component with composer: + +.. code-block:: + +   $ composer require doctrine/annotations + +Create an annotation class +========================== + +An annotation class is a representation of the later used annotation +configuration in classes. The annotation class of the previous example +looks like this: + +.. code-block:: php + + /** + * @Annotation + */ + final class MyAnnotation + { + public $myProperty; + } + +The annotation class is declared as an annotation by ``@Annotation``. + +:ref:`Read more about custom annotations. ` + +Reading annotations +=================== + +The access to the annotations happens by reflection of the class or function +containing them. There are multiple reader-classes implementing the +``Doctrine\Common\Annotations\Reader`` interface, that can access the +annotations of a class. A common one is +``Doctrine\Common\Annotations\AnnotationReader``: + +.. code-block:: php + + use Doctrine\Common\Annotations\AnnotationReader; + use Doctrine\Common\Annotations\AnnotationRegistry; + + // Deprecated and will be removed in 2.0 but currently needed + AnnotationRegistry::registerLoader('class_exists'); + + $reflectionClass = new ReflectionClass(Foo::class); + $property = $reflectionClass->getProperty('bar'); + + $reader = new AnnotationReader(); + $myAnnotation = $reader->getPropertyAnnotation( + $property, + MyAnnotation::class + ); + + echo $myAnnotation->myProperty; // result: "value" + +Note that ``AnnotationRegistry::registerLoader('class_exists')`` only works +if you already have an autoloader configured (i.e. composer autoloader). +Otherwise, :ref:`please take a look to the other annotation autoload mechanisms `. + +A reader has multiple methods to access the annotations of a class or +function. + +:ref:`Read more about handling annotations. ` + +IDE Support +----------- + +Some IDEs already provide support for annotations: + +- Eclipse via the `Symfony2 Plugin `_ +- PhpStorm via the `PHP Annotations Plugin `_ or the `Symfony Plugin `_ + +.. _Read more about handling annotations.: annotations +.. _Read more about custom annotations.: custom diff --git a/vendor/doctrine/annotations/docs/en/sidebar.rst b/vendor/doctrine/annotations/docs/en/sidebar.rst new file mode 100644 index 0000000..6f5d13c --- /dev/null +++ b/vendor/doctrine/annotations/docs/en/sidebar.rst @@ -0,0 +1,6 @@ +.. toctree:: + :depth: 3 + + index + annotations + custom diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation.php index a79a0f8..750270e 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation.php @@ -1,47 +1,27 @@ . - */ namespace Doctrine\Common\Annotations; +use BadMethodCallException; + +use function sprintf; + /** * Annotations class. - * - * @author Benjamin Eberlei - * @author Guilherme Blanco - * @author Jonathan Wage - * @author Roman Borschel */ class Annotation { /** * Value property. Common among all derived classes. * - * @var string + * @var mixed */ public $value; /** - * Constructor. - * - * @param array $data Key-value for properties to be defined in this class. + * @param array $data Key-value for properties to be defined in this class. */ - public final function __construct(array $data) + final public function __construct(array $data) { foreach ($data as $key => $value) { $this->$key = $value; @@ -53,12 +33,12 @@ class Annotation * * @param string $name Unknown property name. * - * @throws \BadMethodCallException + * @throws BadMethodCallException */ public function __get($name) { - throw new \BadMethodCallException( - sprintf("Unknown property '%s' on annotation '%s'.", $name, get_class($this)) + throw new BadMethodCallException( + sprintf("Unknown property '%s' on annotation '%s'.", $name, static::class) ); } @@ -68,12 +48,12 @@ class Annotation * @param string $name Unknown property name. * @param mixed $value Property value. * - * @throws \BadMethodCallException + * @throws BadMethodCallException */ public function __set($name, $value) { - throw new \BadMethodCallException( - sprintf("Unknown property '%s' on annotation '%s'.", $name, get_class($this)) + throw new BadMethodCallException( + sprintf("Unknown property '%s' on annotation '%s'.", $name, static::class) ); } } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attribute.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attribute.php index dbef6df..b1f8514 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attribute.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attribute.php @@ -1,47 +1,21 @@ . - */ - namespace Doctrine\Common\Annotations\Annotation; /** * Annotation that can be used to signal to the parser * to check the attribute type during the parsing process. * - * @author Fabio B. Silva - * * @Annotation */ final class Attribute { - /** - * @var string - */ + /** @var string */ public $name; - /** - * @var string - */ + /** @var string */ public $type; - /** - * @var boolean - */ + /** @var bool */ public $required = false; } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attributes.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attributes.php index 53134e3..8f758f3 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attributes.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Attributes.php @@ -1,37 +1,15 @@ . - */ - namespace Doctrine\Common\Annotations\Annotation; /** * Annotation that can be used to signal to the parser * to check the types of all declared attributes during the parsing process. * - * @author Fabio B. Silva - * * @Annotation */ final class Attributes { - /** - * @var array - */ + /** @var array */ public $value; } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Enum.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Enum.php index e122a75..35d6410 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Enum.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Enum.php @@ -1,32 +1,20 @@ . - */ - namespace Doctrine\Common\Annotations\Annotation; +use InvalidArgumentException; + +use function get_class; +use function gettype; +use function in_array; +use function is_object; +use function is_scalar; +use function sprintf; + /** * Annotation that can be used to signal to the parser * to check the available values during the parsing process. * - * @since 2.4 - * @author Fabio B. Silva - * * @Annotation * @Attributes({ * @Attribute("value", required = true, type = "array"), @@ -35,34 +23,30 @@ namespace Doctrine\Common\Annotations\Annotation; */ final class Enum { - /** - * @var array - */ + /** @phpstan-var list */ public $value; /** * Literal target declaration. * - * @var array + * @var mixed[] */ public $literal; /** - * Annotation constructor. + * @throws InvalidArgumentException * - * @param array $values - * - * @throws \InvalidArgumentException + * @phpstan-param array{literal?: mixed[], value: list} $values */ public function __construct(array $values) { - if ( ! isset($values['literal'])) { - $values['literal'] = array(); + if (! isset($values['literal'])) { + $values['literal'] = []; } foreach ($values['value'] as $var) { - if( ! is_scalar($var)) { - throw new \InvalidArgumentException(sprintf( + if (! is_scalar($var)) { + throw new InvalidArgumentException(sprintf( '@Enum supports only scalar values "%s" given.', is_object($var) ? get_class($var) : gettype($var) )); @@ -70,15 +54,16 @@ final class Enum } foreach ($values['literal'] as $key => $var) { - if( ! in_array($key, $values['value'])) { - throw new \InvalidArgumentException(sprintf( + if (! in_array($key, $values['value'])) { + throw new InvalidArgumentException(sprintf( 'Undefined enumerator value "%s" for literal "%s".', - $key , $var + $key, + $var )); } } - $this->value = $values['value']; - $this->literal = $values['literal']; + $this->value = $values['value']; + $this->literal = $values['literal']; } } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php index 175226a..ae60f7d 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php @@ -1,52 +1,41 @@ . - */ namespace Doctrine\Common\Annotations\Annotation; +use RuntimeException; + +use function is_array; +use function is_string; +use function json_encode; +use function sprintf; + /** * Annotation that can be used to signal to the parser to ignore specific * annotations during the parsing process. * * @Annotation - * @author Johannes M. Schmitt */ final class IgnoreAnnotation { - /** - * @var array - */ + /** @phpstan-var list */ public $names; /** - * Constructor. + * @throws RuntimeException * - * @param array $values - * - * @throws \RuntimeException + * @phpstan-param array{value: string|list} $values */ public function __construct(array $values) { if (is_string($values['value'])) { - $values['value'] = array($values['value']); + $values['value'] = [$values['value']]; } - if (!is_array($values['value'])) { - throw new \RuntimeException(sprintf('@IgnoreAnnotation expects either a string name, or an array of strings, but got %s.', json_encode($values['value']))); + + if (! is_array($values['value'])) { + throw new RuntimeException(sprintf( + '@IgnoreAnnotation expects either a string name, or an array of strings, but got %s.', + json_encode($values['value']) + )); } $this->names = $values['value']; diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/NamedArgumentConstructor.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/NamedArgumentConstructor.php new file mode 100644 index 0000000..1690601 --- /dev/null +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/NamedArgumentConstructor.php @@ -0,0 +1,13 @@ +. - */ - namespace Doctrine\Common\Annotations\Annotation; /** * Annotation that can be used to signal to the parser * to check if that attribute is required during the parsing process. * - * @author Fabio B. Silva - * * @Annotation */ final class Required diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Target.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Target.php index f6c5445..7fd75e2 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Target.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Annotation/Target.php @@ -1,89 +1,79 @@ . - */ - namespace Doctrine\Common\Annotations\Annotation; +use InvalidArgumentException; + +use function array_keys; +use function get_class; +use function gettype; +use function implode; +use function is_array; +use function is_object; +use function is_string; +use function sprintf; + /** * Annotation that can be used to signal to the parser * to check the annotation target during the parsing process. * - * @author Fabio B. Silva - * * @Annotation */ final class Target { - const TARGET_CLASS = 1; - const TARGET_METHOD = 2; - const TARGET_PROPERTY = 4; - const TARGET_ANNOTATION = 8; - const TARGET_ALL = 15; + public const TARGET_CLASS = 1; + public const TARGET_METHOD = 2; + public const TARGET_PROPERTY = 4; + public const TARGET_ANNOTATION = 8; + public const TARGET_FUNCTION = 16; + public const TARGET_ALL = 31; - /** - * @var array - */ - private static $map = array( + /** @var array */ + private static $map = [ 'ALL' => self::TARGET_ALL, 'CLASS' => self::TARGET_CLASS, 'METHOD' => self::TARGET_METHOD, 'PROPERTY' => self::TARGET_PROPERTY, + 'FUNCTION' => self::TARGET_FUNCTION, 'ANNOTATION' => self::TARGET_ANNOTATION, - ); + ]; - /** - * @var array - */ + /** @phpstan-var list */ public $value; /** * Targets as bitmask. * - * @var integer + * @var int */ public $targets; /** * Literal target declaration. * - * @var integer + * @var string */ public $literal; /** - * Annotation constructor. + * @throws InvalidArgumentException * - * @param array $values - * - * @throws \InvalidArgumentException + * @phpstan-param array{value?: string|list} $values */ public function __construct(array $values) { - if (!isset($values['value'])){ + if (! isset($values['value'])) { $values['value'] = null; } - if (is_string($values['value'])){ - $values['value'] = array($values['value']); + + if (is_string($values['value'])) { + $values['value'] = [$values['value']]; } - if (!is_array($values['value'])){ - throw new \InvalidArgumentException( - sprintf('@Target expects either a string value, or an array of strings, "%s" given.', + + if (! is_array($values['value'])) { + throw new InvalidArgumentException( + sprintf( + '@Target expects either a string value, or an array of strings, "%s" given.', is_object($values['value']) ? get_class($values['value']) : gettype($values['value']) ) ); @@ -91,17 +81,21 @@ final class Target $bitmask = 0; foreach ($values['value'] as $literal) { - if(!isset(self::$map[$literal])){ - throw new \InvalidArgumentException( - sprintf('Invalid Target "%s". Available targets: [%s]', - $literal, implode(', ', array_keys(self::$map))) + if (! isset(self::$map[$literal])) { + throw new InvalidArgumentException( + sprintf( + 'Invalid Target "%s". Available targets: [%s]', + $literal, + implode(', ', array_keys(self::$map)) + ) ); } + $bitmask |= self::$map[$literal]; } - $this->targets = $bitmask; - $this->value = $values['value']; - $this->literal = implode(', ', $this->value); + $this->targets = $bitmask; + $this->value = $values['value']; + $this->literal = implode(', ', $this->value); } } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationException.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationException.php index d06fe66..4d91825 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationException.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationException.php @@ -1,34 +1,20 @@ . - */ namespace Doctrine\Common\Annotations; +use Exception; +use Throwable; + +use function get_class; +use function gettype; +use function implode; +use function is_object; +use function sprintf; + /** * Description of AnnotationException - * - * @since 2.0 - * @author Benjamin Eberlei - * @author Guilherme Blanco - * @author Jonathan Wage - * @author Roman Borschel */ -class AnnotationException extends \Exception +class AnnotationException extends Exception { /** * Creates a new AnnotationException describing a Syntax error. @@ -58,22 +44,18 @@ class AnnotationException extends \Exception * Creates a new AnnotationException describing an error which occurred during * the creation of the annotation. * - * @since 2.2 - * * @param string $message * * @return AnnotationException */ - public static function creationError($message) + public static function creationError($message, ?Throwable $previous = null) { - return new self('[Creation Error] ' . $message); + return new self('[Creation Error] ' . $message, 0, $previous); } /** * Creates a new AnnotationException describing a type error. * - * @since 1.1 - * * @param string $message * * @return AnnotationException @@ -86,8 +68,6 @@ class AnnotationException extends \Exception /** * Creates a new AnnotationException describing a constant semantical error. * - * @since 2.3 - * * @param string $identifier * @param string $context * @@ -105,8 +85,6 @@ class AnnotationException extends \Exception /** * Creates a new AnnotationException describing an type error of an attribute. * - * @since 2.2 - * * @param string $attributeName * @param string $annotationName * @param string $context @@ -130,8 +108,6 @@ class AnnotationException extends \Exception /** * Creates a new AnnotationException describing an required error of an attribute. * - * @since 2.2 - * * @param string $attributeName * @param string $annotationName * @param string $context @@ -153,21 +129,20 @@ class AnnotationException extends \Exception /** * Creates a new AnnotationException describing a invalid enummerator. * - * @since 2.4 - * * @param string $attributeName * @param string $annotationName * @param string $context - * @param array $available * @param mixed $given * * @return AnnotationException + * + * @phpstan-param list $available */ public static function enumeratorError($attributeName, $annotationName, $context, $available, $given) { return new self(sprintf( - '[Enum Error] Attribute "%s" of @%s declared on %s accept only [%s], but got %s.', - $attributeName, + '[Enum Error] Attribute "%s" of @%s declared on %s accepts only [%s], but got %s.', + $attributeName, $annotationName, $context, implode(', ', $available), @@ -181,7 +156,7 @@ class AnnotationException extends \Exception public static function optimizerPlusSaveComments() { return new self( - "You have to enable opcache.save_comments=1 or zend_optimizerplus.save_comments=1." + 'You have to enable opcache.save_comments=1 or zend_optimizerplus.save_comments=1.' ); } @@ -191,7 +166,7 @@ class AnnotationException extends \Exception public static function optimizerPlusLoadComments() { return new self( - "You have to enable opcache.load_comments=1 or zend_optimizerplus.load_comments=1." + 'You have to enable opcache.load_comments=1 or zend_optimizerplus.load_comments=1.' ); } } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationReader.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationReader.php index 4ebd1fb..1f538ee 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationReader.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationReader.php @@ -1,186 +1,136 @@ . - */ namespace Doctrine\Common\Annotations; use Doctrine\Common\Annotations\Annotation\IgnoreAnnotation; use Doctrine\Common\Annotations\Annotation\Target; use ReflectionClass; +use ReflectionFunction; use ReflectionMethod; use ReflectionProperty; +use function array_merge; +use function class_exists; +use function extension_loaded; +use function ini_get; + /** * A reader for docblock annotations. - * - * @author Benjamin Eberlei - * @author Guilherme Blanco - * @author Jonathan Wage - * @author Roman Borschel - * @author Johannes M. Schmitt */ class AnnotationReader implements Reader { /** * Global map for imports. * - * @var array + * @var array */ - private static $globalImports = array( - 'ignoreannotation' => 'Doctrine\Common\Annotations\Annotation\IgnoreAnnotation', - ); + private static $globalImports = [ + 'ignoreannotation' => Annotation\IgnoreAnnotation::class, + ]; /** * A list with annotations that are not causing exceptions when not resolved to an annotation class. * * The names are case sensitive. * - * @var array + * @var array */ - private static $globalIgnoredNames = array( - // Annotation tags - 'Annotation' => true, 'Attribute' => true, 'Attributes' => true, - /* Can we enable this? 'Enum' => true, */ - 'Required' => true, - 'Target' => true, - // Widely used tags (but not existent in phpdoc) - 'fix' => true , 'fixme' => true, - 'override' => true, - // PHPDocumentor 1 tags - 'abstract'=> true, 'access'=> true, - 'code' => true, - 'deprec'=> true, - 'endcode' => true, 'exception'=> true, - 'final'=> true, - 'ingroup' => true, 'inheritdoc'=> true, 'inheritDoc'=> true, - 'magic' => true, - 'name'=> true, - 'toc' => true, 'tutorial'=> true, - 'private' => true, - 'static'=> true, 'staticvar'=> true, 'staticVar'=> true, - 'throw' => true, - // PHPDocumentor 2 tags. - 'api' => true, 'author'=> true, - 'category'=> true, 'copyright'=> true, - 'deprecated'=> true, - 'example'=> true, - 'filesource'=> true, - 'global'=> true, - 'ignore'=> true, /* Can we enable this? 'index' => true, */ 'internal'=> true, - 'license'=> true, 'link'=> true, - 'method' => true, - 'package'=> true, 'param'=> true, 'property' => true, 'property-read' => true, 'property-write' => true, - 'return'=> true, - 'see'=> true, 'since'=> true, 'source' => true, 'subpackage'=> true, - 'throws'=> true, 'todo'=> true, 'TODO'=> true, - 'usedby'=> true, 'uses' => true, - 'var'=> true, 'version'=> true, - // PHPUnit tags - 'codeCoverageIgnore' => true, 'codeCoverageIgnoreStart' => true, 'codeCoverageIgnoreEnd' => true, - // PHPCheckStyle - 'SuppressWarnings' => true, - // PHPStorm - 'noinspection' => true, - // PEAR - 'package_version' => true, - // PlantUML - 'startuml' => true, 'enduml' => true, - ); + private static $globalIgnoredNames = ImplicitlyIgnoredAnnotationNames::LIST; + + /** + * A list with annotations that are not causing exceptions when not resolved to an annotation class. + * + * The names are case sensitive. + * + * @var array + */ + private static $globalIgnoredNamespaces = []; /** * Add a new annotation to the globally ignored annotation names with regard to exception handling. * * @param string $name */ - static public function addGlobalIgnoredName($name) + public static function addGlobalIgnoredName($name) { self::$globalIgnoredNames[$name] = true; } + /** + * Add a new annotation to the globally ignored annotation namespaces with regard to exception handling. + * + * @param string $namespace + */ + public static function addGlobalIgnoredNamespace($namespace) + { + self::$globalIgnoredNamespaces[$namespace] = true; + } + /** * Annotations parser. * - * @var \Doctrine\Common\Annotations\DocParser + * @var DocParser */ private $parser; /** * Annotations parser used to collect parsing metadata. * - * @var \Doctrine\Common\Annotations\DocParser + * @var DocParser */ private $preParser; /** * PHP parser used to collect imports. * - * @var \Doctrine\Common\Annotations\PhpParser + * @var PhpParser */ private $phpParser; /** * In-memory cache mechanism to store imported annotations per class. * - * @var array + * @psalm-var array<'class'|'function', array>> */ - private $imports = array(); + private $imports = []; /** * In-memory cache mechanism to store ignored annotations per class. * - * @var array + * @psalm-var array<'class'|'function', array>> */ - private $ignoredAnnotationNames = array(); + private $ignoredAnnotationNames = []; /** - * Constructor. - * * Initializes a new AnnotationReader. + * + * @throws AnnotationException */ - public function __construct() + public function __construct(?DocParser $parser = null) { - if (extension_loaded('Zend Optimizer+') && (ini_get('zend_optimizerplus.save_comments') === "0" || ini_get('opcache.save_comments') === "0")) { + if ( + extension_loaded('Zend Optimizer+') && (ini_get('zend_optimizerplus.save_comments') === '0' || + ini_get('opcache.save_comments') === '0') + ) { throw AnnotationException::optimizerPlusSaveComments(); } - if (extension_loaded('Zend OPcache') && ini_get('opcache.save_comments') == 0) { + if (extension_loaded('Zend OPcache') && ini_get('opcache.save_comments') === 0) { throw AnnotationException::optimizerPlusSaveComments(); } - if (PHP_VERSION_ID < 70000) { - if (extension_loaded('Zend Optimizer+') && (ini_get('zend_optimizerplus.load_comments') === "0" || ini_get('opcache.load_comments') === "0")) { - throw AnnotationException::optimizerPlusLoadComments(); - } + // Make sure that the IgnoreAnnotation annotation is loaded + class_exists(IgnoreAnnotation::class); - if (extension_loaded('Zend OPcache') && ini_get('opcache.load_comments') == 0) { - throw AnnotationException::optimizerPlusLoadComments(); - } - } + $this->parser = $parser ?: new DocParser(); - AnnotationRegistry::registerFile(__DIR__ . '/Annotation/IgnoreAnnotation.php'); - - $this->parser = new DocParser; - $this->preParser = new DocParser; + $this->preParser = new DocParser(); $this->preParser->setImports(self::$globalImports); $this->preParser->setIgnoreNotImportedAnnotations(true); + $this->preParser->setIgnoredAnnotationNames(self::$globalIgnoredNames); - $this->phpParser = new PhpParser; + $this->phpParser = new PhpParser(); } /** @@ -189,8 +139,9 @@ class AnnotationReader implements Reader public function getClassAnnotations(ReflectionClass $class) { $this->parser->setTarget(Target::TARGET_CLASS); - $this->parser->setImports($this->getClassImports($class)); + $this->parser->setImports($this->getImports($class)); $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); + $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName()); } @@ -217,11 +168,12 @@ class AnnotationReader implements Reader public function getPropertyAnnotations(ReflectionProperty $property) { $class = $property->getDeclaringClass(); - $context = 'property ' . $class->getName() . "::\$" . $property->getName(); + $context = 'property ' . $class->getName() . '::$' . $property->getName(); $this->parser->setTarget(Target::TARGET_PROPERTY); $this->parser->setImports($this->getPropertyImports($property)); $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); + $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); return $this->parser->parse($property->getDocComment(), $context); } @@ -253,6 +205,7 @@ class AnnotationReader implements Reader $this->parser->setTarget(Target::TARGET_METHOD); $this->parser->setImports($this->getMethodImports($method)); $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); + $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); return $this->parser->parse($method->getDocComment(), $context); } @@ -274,64 +227,103 @@ class AnnotationReader implements Reader } /** - * Returns the ignored annotations for the given class. + * Gets the annotations applied to a function. * - * @param \ReflectionClass $class - * - * @return array + * @phpstan-return list An array of Annotations. */ - private function getIgnoredAnnotationNames(ReflectionClass $class) + public function getFunctionAnnotations(ReflectionFunction $function): array { - if (isset($this->ignoredAnnotationNames[$name = $class->getName()])) { - return $this->ignoredAnnotationNames[$name]; - } + $context = 'function ' . $function->getName(); - $this->collectParsingMetadata($class); + $this->parser->setTarget(Target::TARGET_FUNCTION); + $this->parser->setImports($this->getImports($function)); + $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($function)); + $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); - return $this->ignoredAnnotationNames[$name]; + return $this->parser->parse($function->getDocComment(), $context); } /** - * Retrieves imports. + * Gets a function annotation. * - * @param \ReflectionClass $class - * - * @return array + * @return object|null The Annotation or NULL, if the requested annotation does not exist. */ - private function getClassImports(ReflectionClass $class) + public function getFunctionAnnotation(ReflectionFunction $function, string $annotationName) { - if (isset($this->imports[$name = $class->getName()])) { - return $this->imports[$name]; + $annotations = $this->getFunctionAnnotations($function); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $annotationName) { + return $annotation; + } } - $this->collectParsingMetadata($class); + return null; + } - return $this->imports[$name]; + /** + * Returns the ignored annotations for the given class or function. + * + * @param ReflectionClass|ReflectionFunction $reflection + * + * @return array + */ + private function getIgnoredAnnotationNames($reflection): array + { + $type = $reflection instanceof ReflectionClass ? 'class' : 'function'; + $name = $reflection->getName(); + + if (isset($this->ignoredAnnotationNames[$type][$name])) { + return $this->ignoredAnnotationNames[$type][$name]; + } + + $this->collectParsingMetadata($reflection); + + return $this->ignoredAnnotationNames[$type][$name]; + } + + /** + * Retrieves imports for a class or a function. + * + * @param ReflectionClass|ReflectionFunction $reflection + * + * @return array + */ + private function getImports($reflection): array + { + $type = $reflection instanceof ReflectionClass ? 'class' : 'function'; + $name = $reflection->getName(); + + if (isset($this->imports[$type][$name])) { + return $this->imports[$type][$name]; + } + + $this->collectParsingMetadata($reflection); + + return $this->imports[$type][$name]; } /** * Retrieves imports for methods. * - * @param \ReflectionMethod $method - * - * @return array + * @return array */ private function getMethodImports(ReflectionMethod $method) { - $class = $method->getDeclaringClass(); - $classImports = $this->getClassImports($class); - if (!method_exists($class, 'getTraits')) { - return $classImports; - } + $class = $method->getDeclaringClass(); + $classImports = $this->getImports($class); - $traitImports = array(); + $traitImports = []; foreach ($class->getTraits() as $trait) { - if ($trait->hasMethod($method->getName()) - && $trait->getFileName() === $method->getFileName() + if ( + ! $trait->hasMethod($method->getName()) + || $trait->getFileName() !== $method->getFileName() ) { - $traitImports = array_merge($traitImports, $this->phpParser->parseClass($trait)); + continue; } + + $traitImports = array_merge($traitImports, $this->phpParser->parseUseStatements($trait)); } return array_merge($classImports, $traitImports); @@ -340,55 +332,58 @@ class AnnotationReader implements Reader /** * Retrieves imports for properties. * - * @param \ReflectionProperty $property - * - * @return array + * @return array */ private function getPropertyImports(ReflectionProperty $property) { - $class = $property->getDeclaringClass(); - $classImports = $this->getClassImports($class); - if (!method_exists($class, 'getTraits')) { - return $classImports; - } + $class = $property->getDeclaringClass(); + $classImports = $this->getImports($class); - $traitImports = array(); + $traitImports = []; foreach ($class->getTraits() as $trait) { - if ($trait->hasProperty($property->getName())) { - $traitImports = array_merge($traitImports, $this->phpParser->parseClass($trait)); + if (! $trait->hasProperty($property->getName())) { + continue; } + + $traitImports = array_merge($traitImports, $this->phpParser->parseUseStatements($trait)); } return array_merge($classImports, $traitImports); } /** - * Collects parsing metadata for a given class. + * Collects parsing metadata for a given class or function. * - * @param \ReflectionClass $class + * @param ReflectionClass|ReflectionFunction $reflection */ - private function collectParsingMetadata(ReflectionClass $class) + private function collectParsingMetadata($reflection): void { + $type = $reflection instanceof ReflectionClass ? 'class' : 'function'; + $name = $reflection->getName(); + $ignoredAnnotationNames = self::$globalIgnoredNames; - $annotations = $this->preParser->parse($class->getDocComment(), 'class ' . $class->name); + $annotations = $this->preParser->parse($reflection->getDocComment(), $type . ' ' . $name); foreach ($annotations as $annotation) { - if ($annotation instanceof IgnoreAnnotation) { - foreach ($annotation->names AS $annot) { - $ignoredAnnotationNames[$annot] = true; - } + if (! ($annotation instanceof IgnoreAnnotation)) { + continue; + } + + foreach ($annotation->names as $annot) { + $ignoredAnnotationNames[$annot] = true; } } - $name = $class->getName(); - - $this->imports[$name] = array_merge( + $this->imports[$type][$name] = array_merge( self::$globalImports, - $this->phpParser->parseClass($class), - array('__NAMESPACE__' => $class->getNamespaceName()) + $this->phpParser->parseUseStatements($reflection), + [ + '__NAMESPACE__' => $reflection->getNamespaceName(), + 'self' => $name, + ] ); - $this->ignoredAnnotationNames[$name] = $ignoredAnnotationNames; + $this->ignoredAnnotationNames[$type][$name] = $ignoredAnnotationNames; } } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationRegistry.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationRegistry.php index 13ceb63..259d497 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationRegistry.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/AnnotationRegistry.php @@ -1,27 +1,18 @@ . - */ namespace Doctrine\Common\Annotations; -/** - * AnnotationRegistry. - */ +use function array_key_exists; +use function array_merge; +use function class_exists; +use function in_array; +use function is_file; +use function str_replace; +use function stream_resolve_include_path; +use function strpos; + +use const DIRECTORY_SEPARATOR; + final class AnnotationRegistry { /** @@ -32,35 +23,49 @@ final class AnnotationRegistry * * This autoloading mechanism does not utilize the PHP autoloading but implements autoloading on its own. * - * @var array + * @var string[][]|string[]|null[] */ - static private $autoloadNamespaces = array(); + private static $autoloadNamespaces = []; /** * A map of autoloader callables. * - * @var array + * @var callable[] */ - static private $loaders = array(); + private static $loaders = []; /** - * @return void + * An array of classes which cannot be found + * + * @var null[] indexed by class name */ - static public function reset() + private static $failedToAutoload = []; + + /** + * Whenever registerFile() was used. Disables use of standard autoloader. + * + * @var bool + */ + private static $registerFileUsed = false; + + public static function reset(): void { - self::$autoloadNamespaces = array(); - self::$loaders = array(); + self::$autoloadNamespaces = []; + self::$loaders = []; + self::$failedToAutoload = []; + self::$registerFileUsed = false; } /** * Registers file. * - * @param string $file - * - * @return void + * @deprecated This method is deprecated and will be removed in + * doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. */ - static public function registerFile($file) + public static function registerFile(string $file): void { + self::$registerFileUsed = true; + require_once $file; } @@ -69,12 +74,12 @@ final class AnnotationRegistry * * Loading of this namespaces will be done with a PSR-0 namespace loading algorithm. * - * @param string $namespace - * @param string|array|null $dirs + * @deprecated This method is deprecated and will be removed in + * doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. * - * @return void + * @phpstan-param string|list|null $dirs */ - static public function registerAutoloadNamespace($namespace, $dirs = null) + public static function registerAutoloadNamespace(string $namespace, $dirs = null): void { self::$autoloadNamespaces[$namespace] = $dirs; } @@ -84,11 +89,12 @@ final class AnnotationRegistry * * Loading of this namespaces will be done with a PSR-0 namespace loading algorithm. * - * @param array $namespaces + * @deprecated This method is deprecated and will be removed in + * doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. * - * @return void + * @param string[][]|string[]|null[] $namespaces indexed by namespace name */ - static public function registerAutoloadNamespaces(array $namespaces) + public static function registerAutoloadNamespaces(array $namespaces): void { self::$autoloadNamespaces = array_merge(self::$autoloadNamespaces, $namespaces); } @@ -99,53 +105,86 @@ final class AnnotationRegistry * NOTE: These class loaders HAVE to be silent when a class was not found! * IMPORTANT: Loaders have to return true if they loaded a class that could contain the searched annotation class. * - * @param callable $callable - * - * @return void - * - * @throws \InvalidArgumentException + * @deprecated This method is deprecated and will be removed in + * doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. */ - static public function registerLoader($callable) + public static function registerLoader(callable $callable): void { - if (!is_callable($callable)) { - throw new \InvalidArgumentException("A callable is expected in AnnotationRegistry::registerLoader()."); + // Reset our static cache now that we have a new loader to work with + self::$failedToAutoload = []; + self::$loaders[] = $callable; + } + + /** + * Registers an autoloading callable for annotations, if it is not already registered + * + * @deprecated This method is deprecated and will be removed in + * doctrine/annotations 2.0. Annotations will be autoloaded in 2.0. + */ + public static function registerUniqueLoader(callable $callable): void + { + if (in_array($callable, self::$loaders, true)) { + return; } - self::$loaders[] = $callable; + + self::registerLoader($callable); } /** * Autoloads an annotation class silently. - * - * @param string $class - * - * @return boolean */ - static public function loadAnnotationClass($class) + public static function loadAnnotationClass(string $class): bool { - foreach (self::$autoloadNamespaces AS $namespace => $dirs) { - if (strpos($class, $namespace) === 0) { - $file = str_replace("\\", DIRECTORY_SEPARATOR, $class) . ".php"; - if ($dirs === null) { - if ($path = stream_resolve_include_path($file)) { - require $path; + if (class_exists($class, false)) { + return true; + } + + if (array_key_exists($class, self::$failedToAutoload)) { + return false; + } + + foreach (self::$autoloadNamespaces as $namespace => $dirs) { + if (strpos($class, $namespace) !== 0) { + continue; + } + + $file = str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php'; + + if ($dirs === null) { + $path = stream_resolve_include_path($file); + if ($path) { + require $path; + + return true; + } + } else { + foreach ((array) $dirs as $dir) { + if (is_file($dir . DIRECTORY_SEPARATOR . $file)) { + require $dir . DIRECTORY_SEPARATOR . $file; + return true; } - } else { - foreach((array)$dirs AS $dir) { - if (is_file($dir . DIRECTORY_SEPARATOR . $file)) { - require $dir . DIRECTORY_SEPARATOR . $file; - return true; - } - } } } } - foreach (self::$loaders AS $loader) { - if (call_user_func($loader, $class) === true) { + foreach (self::$loaders as $loader) { + if ($loader($class) === true) { return true; } } + + if ( + self::$loaders === [] && + self::$autoloadNamespaces === [] && + self::$registerFileUsed === false && + class_exists($class) + ) { + return true; + } + + self::$failedToAutoload[$class] = null; + return false; } } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/CachedReader.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/CachedReader.php index e6dc593..c036b2d 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/CachedReader.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/CachedReader.php @@ -1,77 +1,57 @@ . - */ namespace Doctrine\Common\Annotations; use Doctrine\Common\Cache\Cache; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; + +use function array_map; +use function array_merge; +use function assert; +use function filemtime; +use function max; +use function time; /** * A cache aware annotation reader. * - * @author Johannes M. Schmitt - * @author Benjamin Eberlei + * @deprecated the CachedReader is deprecated and will be removed + * in version 2.0.0 of doctrine/annotations. Please use the + * {@see \Doctrine\Common\Annotations\PsrCachedReader} instead. */ final class CachedReader implements Reader { - /** - * @var string - */ - private static $CACHE_SALT = '@[Annot]'; - - /** - * @var Reader - */ + /** @var Reader */ private $delegate; - /** - * @var Cache - */ + /** @var Cache */ private $cache; - /** - * @var boolean - */ + /** @var bool */ private $debug; - /** - * @var array - */ - private $loadedAnnotations = array(); + /** @var array> */ + private $loadedAnnotations = []; + + /** @var int[] */ + private $loadedFilemtimes = []; /** - * Constructor. - * - * @param Reader $reader - * @param Cache $cache - * @param bool $debug + * @param bool $debug */ public function __construct(Reader $reader, Cache $cache, $debug = false) { $this->delegate = $reader; - $this->cache = $cache; - $this->debug = (boolean) $debug; + $this->cache = $cache; + $this->debug = (bool) $debug; } /** * {@inheritDoc} */ - public function getClassAnnotations(\ReflectionClass $class) + public function getClassAnnotations(ReflectionClass $class) { $cacheKey = $class->getName(); @@ -79,7 +59,8 @@ final class CachedReader implements Reader return $this->loadedAnnotations[$cacheKey]; } - if (false === ($annots = $this->fetchFromCache($cacheKey, $class))) { + $annots = $this->fetchFromCache($cacheKey, $class); + if ($annots === false) { $annots = $this->delegate->getClassAnnotations($class); $this->saveToCache($cacheKey, $annots); } @@ -90,7 +71,7 @@ final class CachedReader implements Reader /** * {@inheritDoc} */ - public function getClassAnnotation(\ReflectionClass $class, $annotationName) + public function getClassAnnotation(ReflectionClass $class, $annotationName) { foreach ($this->getClassAnnotations($class) as $annot) { if ($annot instanceof $annotationName) { @@ -104,16 +85,17 @@ final class CachedReader implements Reader /** * {@inheritDoc} */ - public function getPropertyAnnotations(\ReflectionProperty $property) + public function getPropertyAnnotations(ReflectionProperty $property) { - $class = $property->getDeclaringClass(); - $cacheKey = $class->getName().'$'.$property->getName(); + $class = $property->getDeclaringClass(); + $cacheKey = $class->getName() . '$' . $property->getName(); if (isset($this->loadedAnnotations[$cacheKey])) { return $this->loadedAnnotations[$cacheKey]; } - if (false === ($annots = $this->fetchFromCache($cacheKey, $class))) { + $annots = $this->fetchFromCache($cacheKey, $class); + if ($annots === false) { $annots = $this->delegate->getPropertyAnnotations($property); $this->saveToCache($cacheKey, $annots); } @@ -124,7 +106,7 @@ final class CachedReader implements Reader /** * {@inheritDoc} */ - public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName) + public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) { foreach ($this->getPropertyAnnotations($property) as $annot) { if ($annot instanceof $annotationName) { @@ -138,16 +120,17 @@ final class CachedReader implements Reader /** * {@inheritDoc} */ - public function getMethodAnnotations(\ReflectionMethod $method) + public function getMethodAnnotations(ReflectionMethod $method) { - $class = $method->getDeclaringClass(); - $cacheKey = $class->getName().'#'.$method->getName(); + $class = $method->getDeclaringClass(); + $cacheKey = $class->getName() . '#' . $method->getName(); if (isset($this->loadedAnnotations[$cacheKey])) { return $this->loadedAnnotations[$cacheKey]; } - if (false === ($annots = $this->fetchFromCache($cacheKey, $class))) { + $annots = $this->fetchFromCache($cacheKey, $class); + if ($annots === false) { $annots = $this->delegate->getMethodAnnotations($method); $this->saveToCache($cacheKey, $annots); } @@ -158,7 +141,7 @@ final class CachedReader implements Reader /** * {@inheritDoc} */ - public function getMethodAnnotation(\ReflectionMethod $method, $annotationName) + public function getMethodAnnotation(ReflectionMethod $method, $annotationName) { foreach ($this->getMethodAnnotations($method) as $annot) { if ($annot instanceof $annotationName) { @@ -176,22 +159,22 @@ final class CachedReader implements Reader */ public function clearLoadedAnnotations() { - $this->loadedAnnotations = array(); + $this->loadedAnnotations = []; + $this->loadedFilemtimes = []; } /** * Fetches a value from the cache. * - * @param string $rawCacheKey The cache key. - * @param \ReflectionClass $class The related class. + * @param string $cacheKey The cache key. * * @return mixed The cached value or false when the value is not in cache. */ - private function fetchFromCache($rawCacheKey, \ReflectionClass $class) + private function fetchFromCache($cacheKey, ReflectionClass $class) { - $cacheKey = $rawCacheKey . self::$CACHE_SALT; - if (($data = $this->cache->fetch($cacheKey)) !== false) { - if (!$this->debug || $this->isCacheFresh($cacheKey, $class)) { + $data = $this->cache->fetch($cacheKey); + if ($data !== false) { + if (! $this->debug || $this->isCacheFresh($cacheKey, $class)) { return $data; } } @@ -202,34 +185,84 @@ final class CachedReader implements Reader /** * Saves a value to the cache. * - * @param string $rawCacheKey The cache key. - * @param mixed $value The value. + * @param string $cacheKey The cache key. + * @param mixed $value The value. * * @return void */ - private function saveToCache($rawCacheKey, $value) + private function saveToCache($cacheKey, $value) { - $cacheKey = $rawCacheKey . self::$CACHE_SALT; $this->cache->save($cacheKey, $value); - if ($this->debug) { - $this->cache->save('[C]'.$cacheKey, time()); + if (! $this->debug) { + return; } + + $this->cache->save('[C]' . $cacheKey, time()); } /** * Checks if the cache is fresh. * - * @param string $cacheKey - * @param \ReflectionClass $class + * @param string $cacheKey * - * @return boolean + * @return bool */ - private function isCacheFresh($cacheKey, \ReflectionClass $class) + private function isCacheFresh($cacheKey, ReflectionClass $class) { - if (false === $filename = $class->getFilename()) { + $lastModification = $this->getLastModification($class); + if ($lastModification === 0) { return true; } - return $this->cache->fetch('[C]'.$cacheKey) >= filemtime($filename); + return $this->cache->fetch('[C]' . $cacheKey) >= $lastModification; + } + + /** + * Returns the time the class was last modified, testing traits and parents + */ + private function getLastModification(ReflectionClass $class): int + { + $filename = $class->getFileName(); + + if (isset($this->loadedFilemtimes[$filename])) { + return $this->loadedFilemtimes[$filename]; + } + + $parent = $class->getParentClass(); + + $lastModification = max(array_merge( + [$filename ? filemtime($filename) : 0], + array_map(function (ReflectionClass $reflectionTrait): int { + return $this->getTraitLastModificationTime($reflectionTrait); + }, $class->getTraits()), + array_map(function (ReflectionClass $class): int { + return $this->getLastModification($class); + }, $class->getInterfaces()), + $parent ? [$this->getLastModification($parent)] : [] + )); + + assert($lastModification !== false); + + return $this->loadedFilemtimes[$filename] = $lastModification; + } + + private function getTraitLastModificationTime(ReflectionClass $reflectionTrait): int + { + $fileName = $reflectionTrait->getFileName(); + + if (isset($this->loadedFilemtimes[$fileName])) { + return $this->loadedFilemtimes[$fileName]; + } + + $lastModificationTime = max(array_merge( + [$fileName ? filemtime($fileName) : 0], + array_map(function (ReflectionClass $reflectionTrait): int { + return $this->getTraitLastModificationTime($reflectionTrait); + }, $reflectionTrait->getTraits()) + )); + + assert($lastModificationTime !== false); + + return $this->loadedFilemtimes[$fileName] = $lastModificationTime; } } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocLexer.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocLexer.php index d864540..f6567c5 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocLexer.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocLexer.php @@ -1,61 +1,46 @@ . - */ namespace Doctrine\Common\Annotations; use Doctrine\Common\Lexer\AbstractLexer; +use function ctype_alpha; +use function is_numeric; +use function str_replace; +use function stripos; +use function strlen; +use function strpos; +use function strtolower; +use function substr; + /** * Simple lexer for docblock annotations. - * - * @author Benjamin Eberlei - * @author Guilherme Blanco - * @author Jonathan Wage - * @author Roman Borschel - * @author Johannes M. Schmitt */ final class DocLexer extends AbstractLexer { - const T_NONE = 1; - const T_INTEGER = 2; - const T_STRING = 3; - const T_FLOAT = 4; + public const T_NONE = 1; + public const T_INTEGER = 2; + public const T_STRING = 3; + public const T_FLOAT = 4; // All tokens that are also identifiers should be >= 100 - const T_IDENTIFIER = 100; - const T_AT = 101; - const T_CLOSE_CURLY_BRACES = 102; - const T_CLOSE_PARENTHESIS = 103; - const T_COMMA = 104; - const T_EQUALS = 105; - const T_FALSE = 106; - const T_NAMESPACE_SEPARATOR = 107; - const T_OPEN_CURLY_BRACES = 108; - const T_OPEN_PARENTHESIS = 109; - const T_TRUE = 110; - const T_NULL = 111; - const T_COLON = 112; + public const T_IDENTIFIER = 100; + public const T_AT = 101; + public const T_CLOSE_CURLY_BRACES = 102; + public const T_CLOSE_PARENTHESIS = 103; + public const T_COMMA = 104; + public const T_EQUALS = 105; + public const T_FALSE = 106; + public const T_NAMESPACE_SEPARATOR = 107; + public const T_OPEN_CURLY_BRACES = 108; + public const T_OPEN_PARENTHESIS = 109; + public const T_TRUE = 110; + public const T_NULL = 111; + public const T_COLON = 112; + public const T_MINUS = 113; - /** - * @var array - */ - protected $noCase = array( + /** @var array */ + protected $noCase = [ '@' => self::T_AT, ',' => self::T_COMMA, '(' => self::T_OPEN_PARENTHESIS, @@ -64,28 +49,38 @@ final class DocLexer extends AbstractLexer '}' => self::T_CLOSE_CURLY_BRACES, '=' => self::T_EQUALS, ':' => self::T_COLON, - '\\' => self::T_NAMESPACE_SEPARATOR - ); + '-' => self::T_MINUS, + '\\' => self::T_NAMESPACE_SEPARATOR, + ]; - /** - * @var array - */ - protected $withCase = array( + /** @var array */ + protected $withCase = [ 'true' => self::T_TRUE, 'false' => self::T_FALSE, - 'null' => self::T_NULL - ); + 'null' => self::T_NULL, + ]; + + /** + * Whether the next token starts immediately, or if there were + * non-captured symbols before that + */ + public function nextTokenIsAdjacent(): bool + { + return $this->token === null + || ($this->lookahead !== null + && ($this->lookahead['position'] - $this->token['position']) === strlen($this->token['value'])); + } /** * {@inheritdoc} */ protected function getCatchablePatterns() { - return array( + return [ '[a-z_\\\][a-z0-9_\:\\\]*[a-z_][a-z0-9_]*', '(?:[+-]?[0-9]+(?:[\.][0-9]+)*)(?:[eE][+-]?[0-9]+)?', '"(?:""|[^"])*+"', - ); + ]; } /** @@ -93,7 +88,7 @@ final class DocLexer extends AbstractLexer */ protected function getNonCatchablePatterns() { - return array('\s+', '\*+', '(.)'); + return ['\s+', '\*+', '(.)']; } /** @@ -125,7 +120,7 @@ final class DocLexer extends AbstractLexer // Checking numeric value if (is_numeric($value)) { - return (strpos($value, '.') !== false || stripos($value, 'e') !== false) + return strpos($value, '.') !== false || stripos($value, 'e') !== false ? self::T_FLOAT : self::T_INTEGER; } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php index db66846..80f307c 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/DocParser.php @@ -1,81 +1,94 @@ . - */ namespace Doctrine\Common\Annotations; use Doctrine\Common\Annotations\Annotation\Attribute; -use ReflectionClass; -use Doctrine\Common\Annotations\Annotation\Enum; -use Doctrine\Common\Annotations\Annotation\Target; use Doctrine\Common\Annotations\Annotation\Attributes; +use Doctrine\Common\Annotations\Annotation\Enum; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; +use Doctrine\Common\Annotations\Annotation\Target; +use ReflectionClass; +use ReflectionException; +use ReflectionProperty; +use RuntimeException; +use stdClass; +use Throwable; + +use function array_keys; +use function array_map; +use function array_pop; +use function array_values; +use function class_exists; +use function constant; +use function count; +use function defined; +use function explode; +use function gettype; +use function implode; +use function in_array; +use function interface_exists; +use function is_array; +use function is_object; +use function json_encode; +use function ltrim; +use function preg_match; +use function reset; +use function rtrim; +use function sprintf; +use function stripos; +use function strlen; +use function strpos; +use function strrpos; +use function strtolower; +use function substr; +use function trim; + +use const PHP_VERSION_ID; /** * A parser for docblock annotations. * * It is strongly discouraged to change the default annotation parsing process. - * - * @author Benjamin Eberlei - * @author Guilherme Blanco - * @author Jonathan Wage - * @author Roman Borschel - * @author Johannes M. Schmitt - * @author Fabio B. Silva */ final class DocParser { /** * An array of all valid tokens for a class name. * - * @var array + * @phpstan-var list */ - private static $classIdentifiers = array( + private static $classIdentifiers = [ DocLexer::T_IDENTIFIER, DocLexer::T_TRUE, DocLexer::T_FALSE, - DocLexer::T_NULL - ); + DocLexer::T_NULL, + ]; /** * The lexer. * - * @var \Doctrine\Common\Annotations\DocLexer + * @var DocLexer */ private $lexer; /** * Current target context. * - * @var string + * @var int */ private $target; /** * Doc parser used to collect annotation target. * - * @var \Doctrine\Common\Annotations\DocParser + * @var DocParser */ private static $metadataParser; /** * Flag to control if the current annotation is nested or not. * - * @var boolean + * @var bool */ private $isNestedAnnotation = false; @@ -83,157 +96,172 @@ final class DocParser * Hashmap containing all use-statements that are to be used when parsing * the given doc block. * - * @var array + * @var array */ - private $imports = array(); + private $imports = []; /** * This hashmap is used internally to cache results of class_exists() * look-ups. * - * @var array + * @var array */ - private $classExists = array(); + private $classExists = []; /** * Whether annotations that have not been imported should be ignored. * - * @var boolean + * @var bool */ private $ignoreNotImportedAnnotations = false; /** * An array of default namespaces if operating in simple mode. * - * @var array + * @var string[] */ - private $namespaces = array(); + private $namespaces = []; /** * A list with annotations that are not causing exceptions when not resolved to an annotation class. * * The names must be the raw names as used in the class, not the fully qualified - * class names. * - * @var array + * @var bool[] indexed by annotation name */ - private $ignoredAnnotationNames = array(); + private $ignoredAnnotationNames = []; /** - * @var string + * A list with annotations in namespaced format + * that are not causing exceptions when not resolved to an annotation class. + * + * @var bool[] indexed by namespace name */ + private $ignoredAnnotationNamespaces = []; + + /** @var string */ private $context = ''; /** * Hash-map for caching annotation metadata. * - * @var array + * @var array */ - private static $annotationMetadata = array( - 'Doctrine\Common\Annotations\Annotation\Target' => array( - 'is_annotation' => true, - 'has_constructor' => true, - 'properties' => array(), - 'targets_literal' => 'ANNOTATION_CLASS', - 'targets' => Target::TARGET_CLASS, - 'default_property' => 'value', - 'attribute_types' => array( - 'value' => array( - 'required' => false, - 'type' =>'array', - 'array_type'=>'string', - 'value' =>'array' - ) - ), - ), - 'Doctrine\Common\Annotations\Annotation\Attribute' => array( - 'is_annotation' => true, - 'has_constructor' => false, - 'targets_literal' => 'ANNOTATION_ANNOTATION', - 'targets' => Target::TARGET_ANNOTATION, - 'default_property' => 'name', - 'properties' => array( + private static $annotationMetadata = [ + Annotation\Target::class => [ + 'is_annotation' => true, + 'has_constructor' => true, + 'has_named_argument_constructor' => false, + 'properties' => [], + 'targets_literal' => 'ANNOTATION_CLASS', + 'targets' => Target::TARGET_CLASS, + 'default_property' => 'value', + 'attribute_types' => [ + 'value' => [ + 'required' => false, + 'type' => 'array', + 'array_type' => 'string', + 'value' => 'array', + ], + ], + ], + Annotation\Attribute::class => [ + 'is_annotation' => true, + 'has_constructor' => false, + 'has_named_argument_constructor' => false, + 'targets_literal' => 'ANNOTATION_ANNOTATION', + 'targets' => Target::TARGET_ANNOTATION, + 'default_property' => 'name', + 'properties' => [ 'name' => 'name', 'type' => 'type', - 'required' => 'required' - ), - 'attribute_types' => array( - 'value' => array( + 'required' => 'required', + ], + 'attribute_types' => [ + 'value' => [ 'required' => true, - 'type' =>'string', - 'value' =>'string' - ), - 'type' => array( - 'required' =>true, - 'type' =>'string', - 'value' =>'string' - ), - 'required' => array( - 'required' =>false, - 'type' =>'boolean', - 'value' =>'boolean' - ) - ), - ), - 'Doctrine\Common\Annotations\Annotation\Attributes' => array( - 'is_annotation' => true, - 'has_constructor' => false, - 'targets_literal' => 'ANNOTATION_CLASS', - 'targets' => Target::TARGET_CLASS, - 'default_property' => 'value', - 'properties' => array( - 'value' => 'value' - ), - 'attribute_types' => array( - 'value' => array( - 'type' =>'array', - 'required' =>true, - 'array_type'=>'Doctrine\Common\Annotations\Annotation\Attribute', - 'value' =>'array' - ) - ), - ), - 'Doctrine\Common\Annotations\Annotation\Enum' => array( - 'is_annotation' => true, - 'has_constructor' => true, - 'targets_literal' => 'ANNOTATION_PROPERTY', - 'targets' => Target::TARGET_PROPERTY, - 'default_property' => 'value', - 'properties' => array( - 'value' => 'value' - ), - 'attribute_types' => array( - 'value' => array( + 'type' => 'string', + 'value' => 'string', + ], + 'type' => [ + 'required' => true, + 'type' => 'string', + 'value' => 'string', + ], + 'required' => [ + 'required' => false, + 'type' => 'boolean', + 'value' => 'boolean', + ], + ], + ], + Annotation\Attributes::class => [ + 'is_annotation' => true, + 'has_constructor' => false, + 'has_named_argument_constructor' => false, + 'targets_literal' => 'ANNOTATION_CLASS', + 'targets' => Target::TARGET_CLASS, + 'default_property' => 'value', + 'properties' => ['value' => 'value'], + 'attribute_types' => [ + 'value' => [ 'type' => 'array', 'required' => true, - ), - 'literal' => array( + 'array_type' => Annotation\Attribute::class, + 'value' => 'array<' . Annotation\Attribute::class . '>', + ], + ], + ], + Annotation\Enum::class => [ + 'is_annotation' => true, + 'has_constructor' => true, + 'has_named_argument_constructor' => false, + 'targets_literal' => 'ANNOTATION_PROPERTY', + 'targets' => Target::TARGET_PROPERTY, + 'default_property' => 'value', + 'properties' => ['value' => 'value'], + 'attribute_types' => [ + 'value' => [ + 'type' => 'array', + 'required' => true, + ], + 'literal' => [ 'type' => 'array', 'required' => false, - ), - ), - ), - ); + ], + ], + ], + Annotation\NamedArgumentConstructor::class => [ + 'is_annotation' => true, + 'has_constructor' => false, + 'has_named_argument_constructor' => false, + 'targets_literal' => 'ANNOTATION_CLASS', + 'targets' => Target::TARGET_CLASS, + 'default_property' => null, + 'properties' => [], + 'attribute_types' => [], + ], + ]; /** * Hash-map for handle types declaration. * - * @var array + * @var array */ - private static $typeMap = array( + private static $typeMap = [ 'float' => 'double', 'bool' => 'boolean', // allow uppercase Boolean in honor of George Boole 'Boolean' => 'boolean', 'int' => 'integer', - ); + ]; /** * Constructs a new DocParser. */ public function __construct() { - $this->lexer = new DocLexer; + $this->lexer = new DocLexer(); } /** @@ -242,7 +270,7 @@ final class DocParser * The names are supposed to be the raw names as used in the class, not the * fully qualified class names. * - * @param array $names + * @param bool[] $names indexed by annotation name * * @return void */ @@ -251,31 +279,43 @@ final class DocParser $this->ignoredAnnotationNames = $names; } + /** + * Sets the annotation namespaces that are ignored during the parsing process. + * + * @param bool[] $ignoredAnnotationNamespaces indexed by annotation namespace name + * + * @return void + */ + public function setIgnoredAnnotationNamespaces($ignoredAnnotationNamespaces) + { + $this->ignoredAnnotationNamespaces = $ignoredAnnotationNamespaces; + } + /** * Sets ignore on not-imported annotations. * - * @param boolean $bool + * @param bool $bool * * @return void */ public function setIgnoreNotImportedAnnotations($bool) { - $this->ignoreNotImportedAnnotations = (boolean) $bool; + $this->ignoreNotImportedAnnotations = (bool) $bool; } /** * Sets the default namespaces. * - * @param array $namespace + * @param string $namespace * * @return void * - * @throws \RuntimeException + * @throws RuntimeException */ public function addNamespace($namespace) { if ($this->imports) { - throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); + throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); } $this->namespaces[] = $namespace; @@ -284,16 +324,16 @@ final class DocParser /** * Sets the imports. * - * @param array $imports + * @param array $imports * * @return void * - * @throws \RuntimeException + * @throws RuntimeException */ public function setImports(array $imports) { if ($this->namespaces) { - throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); + throw new RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); } $this->imports = $imports; @@ -302,7 +342,7 @@ final class DocParser /** * Sets current target context as bitmask. * - * @param integer $target + * @param int $target * * @return void */ @@ -317,13 +357,16 @@ final class DocParser * @param string $input The docblock string to parse. * @param string $context The parsing context. * - * @return array Array of annotations. If no annotations are found, an empty array is returned. + * @throws AnnotationException + * @throws ReflectionException + * + * @phpstan-return list Array of annotations. If no annotations are found, an empty array is returned. */ public function parse($input, $context = '') { $pos = $this->findInitialTokenPosition($input); if ($pos === null) { - return array(); + return []; } $this->context = $context; @@ -338,17 +381,17 @@ final class DocParser * Finds the first valid annotation * * @param string $input The docblock string to parse - * - * @return int|null */ - private function findInitialTokenPosition($input) + private function findInitialTokenPosition($input): ?int { $pos = 0; // search for first valid annotation while (($pos = strpos($input, '@', $pos)) !== false) { - // if the @ is preceded by a space or * it is valid - if ($pos === 0 || $input[$pos - 1] === ' ' || $input[$pos - 1] === '*') { + $preceding = substr($input, $pos - 1, 1); + + // if the @ is preceded by a space, a tab or * it is valid + if ($pos === 0 || $preceding === ' ' || $preceding === '*' || $preceding === "\t") { return $pos; } @@ -362,14 +405,16 @@ final class DocParser * Attempts to match the given token with the current lookahead token. * If they match, updates the lookahead token; otherwise raises a syntax error. * - * @param integer $token Type of token. + * @param int $token Type of token. * - * @return boolean True if tokens match; false otherwise. + * @return bool True if tokens match; false otherwise. + * + * @throws AnnotationException */ - private function match($token) + private function match(int $token): bool { - if ( ! $this->lexer->isNextToken($token) ) { - $this->syntaxError($this->lexer->getLiteral($token)); + if (! $this->lexer->isNextToken($token)) { + throw $this->syntaxError($this->lexer->getLiteral($token)); } return $this->lexer->moveNext(); @@ -381,14 +426,14 @@ final class DocParser * If any of them matches, this method updates the lookahead token; otherwise * a syntax error is raised. * - * @param array $tokens + * @throws AnnotationException * - * @return boolean + * @phpstan-param list $tokens */ - private function matchAny(array $tokens) + private function matchAny(array $tokens): bool { - if ( ! $this->lexer->isNextTokenAny($tokens)) { - $this->syntaxError(implode(' or ', array_map(array($this->lexer, 'getLiteral'), $tokens))); + if (! $this->lexer->isNextTokenAny($tokens)) { + throw $this->syntaxError(implode(' or ', array_map([$this->lexer, 'getLiteral'], $tokens))); } return $this->lexer->moveNext(); @@ -397,21 +442,17 @@ final class DocParser /** * Generates a new syntax error. * - * @param string $expected Expected string. - * @param array|null $token Optional token. - * - * @return void - * - * @throws AnnotationException + * @param string $expected Expected string. + * @param mixed[]|null $token Optional token. */ - private function syntaxError($expected, $token = null) + private function syntaxError(string $expected, ?array $token = null): AnnotationException { if ($token === null) { $token = $this->lexer->lookahead; } $message = sprintf('Expected %s, got ', $expected); - $message .= ($this->lexer->lookahead === null) + $message .= $this->lexer->lookahead === null ? 'end of string' : sprintf("'%s' at position %s", $token['value'], $token['position']); @@ -421,18 +462,16 @@ final class DocParser $message .= '.'; - throw AnnotationException::syntaxError($message); + return AnnotationException::syntaxError($message); } /** * Attempts to check if a class exists or not. This never goes through the PHP autoloading mechanism * but uses the {@link AnnotationRegistry} to load classes. * - * @param string $fqcn - * - * @return boolean + * @param class-string $fqcn */ - private function classExists($fqcn) + private function classExists(string $fqcn): bool { if (isset($this->classExists[$fqcn])) { return $this->classExists[$fqcn]; @@ -450,44 +489,53 @@ final class DocParser /** * Collects parsing metadata for a given annotation class * - * @param string $name The annotation name + * @param class-string $name The annotation name * - * @return void + * @throws AnnotationException + * @throws ReflectionException */ - private function collectAnnotationMetadata($name) + private function collectAnnotationMetadata(string $name): void { if (self::$metadataParser === null) { self::$metadataParser = new self(); self::$metadataParser->setIgnoreNotImportedAnnotations(true); self::$metadataParser->setIgnoredAnnotationNames($this->ignoredAnnotationNames); - self::$metadataParser->setImports(array( - 'enum' => 'Doctrine\Common\Annotations\Annotation\Enum', - 'target' => 'Doctrine\Common\Annotations\Annotation\Target', - 'attribute' => 'Doctrine\Common\Annotations\Annotation\Attribute', - 'attributes' => 'Doctrine\Common\Annotations\Annotation\Attributes' - )); + self::$metadataParser->setImports([ + 'enum' => Enum::class, + 'target' => Target::class, + 'attribute' => Attribute::class, + 'attributes' => Attributes::class, + 'namedargumentconstructor' => NamedArgumentConstructor::class, + ]); - AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Enum.php'); - AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Target.php'); - AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php'); - AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attributes.php'); + // Make sure that annotations from metadata are loaded + class_exists(Enum::class); + class_exists(Target::class); + class_exists(Attribute::class); + class_exists(Attributes::class); + class_exists(NamedArgumentConstructor::class); } - $class = new \ReflectionClass($name); + $class = new ReflectionClass($name); $docComment = $class->getDocComment(); // Sets default values for annotation metadata - $metadata = array( + $constructor = $class->getConstructor(); + $metadata = [ 'default_property' => null, - 'has_constructor' => (null !== $constructor = $class->getConstructor()) && $constructor->getNumberOfParameters() > 0, - 'properties' => array(), - 'property_types' => array(), - 'attribute_types' => array(), + 'has_constructor' => $constructor !== null && $constructor->getNumberOfParameters() > 0, + 'constructor_args' => [], + 'properties' => [], + 'property_types' => [], + 'attribute_types' => [], 'targets_literal' => null, 'targets' => Target::TARGET_ALL, - 'is_annotation' => false !== strpos($docComment, '@Annotation'), - ); + 'is_annotation' => strpos($docComment, '@Annotation') !== false, + ]; + + $metadata['has_named_argument_constructor'] = $metadata['has_constructor'] + && $class->implementsInterface(NamedArgumentConstructorAnnotation::class); // verify that the class is really meant to be an annotation if ($metadata['is_annotation']) { @@ -501,54 +549,75 @@ final class DocParser continue; } - if ($annotation instanceof Attributes) { - foreach ($annotation->value as $attribute) { - $this->collectAttributeTypeMetadata($metadata, $attribute); + if ($annotation instanceof NamedArgumentConstructor) { + $metadata['has_named_argument_constructor'] = $metadata['has_constructor']; + if ($metadata['has_named_argument_constructor']) { + // choose the first argument as the default property + $metadata['default_property'] = $constructor->getParameters()[0]->getName(); } } + + if (! ($annotation instanceof Attributes)) { + continue; + } + + foreach ($annotation->value as $attribute) { + $this->collectAttributeTypeMetadata($metadata, $attribute); + } } // if not has a constructor will inject values into public properties - if (false === $metadata['has_constructor']) { + if ($metadata['has_constructor'] === false) { // collect all public properties - foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + foreach ($class->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { $metadata['properties'][$property->name] = $property->name; - if (false === ($propertyComment = $property->getDocComment())) { + $propertyComment = $property->getDocComment(); + if ($propertyComment === false) { continue; } $attribute = new Attribute(); - $attribute->required = (false !== strpos($propertyComment, '@Required')); + $attribute->required = (strpos($propertyComment, '@Required') !== false); $attribute->name = $property->name; - $attribute->type = (false !== strpos($propertyComment, '@var') && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches)) + $attribute->type = (strpos($propertyComment, '@var') !== false && + preg_match('/@var\s+([^\s]+)/', $propertyComment, $matches)) ? $matches[1] : 'mixed'; $this->collectAttributeTypeMetadata($metadata, $attribute); // checks if the property has @Enum - if (false !== strpos($propertyComment, '@Enum')) { - $context = 'property ' . $class->name . "::\$" . $property->name; + if (strpos($propertyComment, '@Enum') === false) { + continue; + } - self::$metadataParser->setTarget(Target::TARGET_PROPERTY); + $context = 'property ' . $class->name . '::$' . $property->name; - foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) { - if ( ! $annotation instanceof Enum) { - continue; - } + self::$metadataParser->setTarget(Target::TARGET_PROPERTY); - $metadata['enum'][$property->name]['value'] = $annotation->value; - $metadata['enum'][$property->name]['literal'] = ( ! empty($annotation->literal)) - ? $annotation->literal - : $annotation->value; + foreach (self::$metadataParser->parse($propertyComment, $context) as $annotation) { + if (! $annotation instanceof Enum) { + continue; } + + $metadata['enum'][$property->name]['value'] = $annotation->value; + $metadata['enum'][$property->name]['literal'] = (! empty($annotation->literal)) + ? $annotation->literal + : $annotation->value; } } // choose the first property as default property $metadata['default_property'] = reset($metadata['properties']); + } elseif ($metadata['has_named_argument_constructor']) { + foreach ($constructor->getParameters() as $parameter) { + $metadata['constructor_args'][$parameter->getName()] = [ + 'position' => $parameter->getPosition(), + 'default' => $parameter->isOptional() ? $parameter->getDefaultValue() : null, + ]; + } } } @@ -558,48 +627,43 @@ final class DocParser /** * Collects parsing metadata for a given attribute. * - * @param array $metadata - * @param Attribute $attribute - * - * @return void + * @param mixed[] $metadata */ - private function collectAttributeTypeMetadata(&$metadata, Attribute $attribute) + private function collectAttributeTypeMetadata(array &$metadata, Attribute $attribute): void { // handle internal type declaration - $type = isset(self::$typeMap[$attribute->type]) - ? self::$typeMap[$attribute->type] - : $attribute->type; + $type = self::$typeMap[$attribute->type] ?? $attribute->type; // handle the case if the property type is mixed - if ('mixed' === $type) { + if ($type === 'mixed') { return; } // Evaluate type - switch (true) { + $pos = strpos($type, '<'); + if ($pos !== false) { // Checks if the property has array - case (false !== $pos = strpos($type, '<')): - $arrayType = substr($type, $pos + 1, -1); - $type = 'array'; + $arrayType = substr($type, $pos + 1, -1); + $type = 'array'; - if (isset(self::$typeMap[$arrayType])) { - $arrayType = self::$typeMap[$arrayType]; - } - - $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; - break; + if (isset(self::$typeMap[$arrayType])) { + $arrayType = self::$typeMap[$arrayType]; + } + $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; + } else { // Checks if the property has type[] - case (false !== $pos = strrpos($type, '[')): - $arrayType = substr($type, 0, $pos); - $type = 'array'; + $pos = strrpos($type, '['); + if ($pos !== false) { + $arrayType = substr($type, 0, $pos); + $type = 'array'; if (isset(self::$typeMap[$arrayType])) { $arrayType = self::$typeMap[$arrayType]; } $metadata['attribute_types'][$attribute->name]['array_type'] = $arrayType; - break; + } } $metadata['attribute_types'][$attribute->name]['type'] = $type; @@ -610,37 +674,55 @@ final class DocParser /** * Annotations ::= Annotation {[ "*" ]* [Annotation]}* * - * @return array + * @throws AnnotationException + * @throws ReflectionException + * + * @phpstan-return list */ - private function Annotations() + private function Annotations(): array { - $annotations = array(); + $annotations = []; - while (null !== $this->lexer->lookahead) { - if (DocLexer::T_AT !== $this->lexer->lookahead['type']) { + while ($this->lexer->lookahead !== null) { + if ($this->lexer->lookahead['type'] !== DocLexer::T_AT) { $this->lexer->moveNext(); continue; } // make sure the @ is preceded by non-catchable pattern - if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) { + if ( + $this->lexer->token !== null && + $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen( + $this->lexer->token['value'] + ) + ) { $this->lexer->moveNext(); continue; } // make sure the @ is followed by either a namespace separator, or // an identifier token - if ((null === $peek = $this->lexer->glimpse()) - || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true)) - || $peek['position'] !== $this->lexer->lookahead['position'] + 1) { + $peek = $this->lexer->glimpse(); + if ( + ($peek === null) + || ($peek['type'] !== DocLexer::T_NAMESPACE_SEPARATOR && ! in_array( + $peek['type'], + self::$classIdentifiers, + true + )) + || $peek['position'] !== $this->lexer->lookahead['position'] + 1 + ) { $this->lexer->moveNext(); continue; } $this->isNestedAnnotation = false; - if (false !== $annot = $this->Annotation()) { - $annotations[] = $annot; + $annot = $this->Annotation(); + if ($annot === false) { + continue; } + + $annotations[] = $annot; } return $annotations; @@ -653,9 +735,10 @@ final class DocParser * NameSpacePart ::= identifier | null | false | true * SimpleName ::= identifier | null | false | true * - * @return mixed False if it is not a valid annotation. + * @return object|false False if it is not a valid annotation. * * @throws AnnotationException + * @throws ReflectionException */ private function Annotation() { @@ -664,67 +747,103 @@ final class DocParser // check if we have an annotation $name = $this->Identifier(); + if ( + $this->lexer->isNextToken(DocLexer::T_MINUS) + && $this->lexer->nextTokenIsAdjacent() + ) { + // Annotations with dashes, such as "@foo-" or "@foo-bar", are to be discarded + return false; + } + // only process names which are not fully qualified, yet // fully qualified names must start with a \ $originalName = $name; - if ('\\' !== $name[0]) { - $alias = (false === $pos = strpos($name, '\\'))? $name : substr($name, 0, $pos); - $found = false; + if ($name[0] !== '\\') { + $pos = strpos($name, '\\'); + $alias = ($pos === false) ? $name : substr($name, 0, $pos); + $found = false; + $loweredAlias = strtolower($alias); if ($this->namespaces) { foreach ($this->namespaces as $namespace) { - if ($this->classExists($namespace.'\\'.$name)) { - $name = $namespace.'\\'.$name; + if ($this->classExists($namespace . '\\' . $name)) { + $name = $namespace . '\\' . $name; $found = true; break; } } - } elseif (isset($this->imports[$loweredAlias = strtolower($alias)])) { - $found = true; - $name = (false !== $pos) - ? $this->imports[$loweredAlias] . substr($name, $pos) - : $this->imports[$loweredAlias]; - } elseif ( ! isset($this->ignoredAnnotationNames[$name]) + } elseif (isset($this->imports[$loweredAlias])) { + $namespace = ltrim($this->imports[$loweredAlias], '\\'); + $name = ($pos !== false) + ? $namespace . substr($name, $pos) + : $namespace; + $found = $this->classExists($name); + } elseif ( + ! isset($this->ignoredAnnotationNames[$name]) && isset($this->imports['__NAMESPACE__']) && $this->classExists($this->imports['__NAMESPACE__'] . '\\' . $name) ) { - $name = $this->imports['__NAMESPACE__'].'\\'.$name; + $name = $this->imports['__NAMESPACE__'] . '\\' . $name; $found = true; } elseif (! isset($this->ignoredAnnotationNames[$name]) && $this->classExists($name)) { $found = true; } - if ( ! $found) { - if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) { + if (! $found) { + if ($this->isIgnoredAnnotation($name)) { return false; } - throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s was never imported. Did you maybe forget to add a "use" statement for this annotation?', $name, $this->context)); + throw AnnotationException::semanticalError(sprintf( + <<<'EXCEPTION' +The annotation "@%s" in %s was never imported. Did you maybe forget to add a "use" statement for this annotation? +EXCEPTION + , + $name, + $this->context + )); } } - if ( ! $this->classExists($name)) { - throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context)); + $name = ltrim($name, '\\'); + + if (! $this->classExists($name)) { + throw AnnotationException::semanticalError(sprintf( + 'The annotation "@%s" in %s does not exist, or could not be auto-loaded.', + $name, + $this->context + )); } // at this point, $name contains the fully qualified class name of the // annotation, and it is also guaranteed that this class exists, and // that it is loaded - // collects the metadata annotation only if there is not yet - if ( ! isset(self::$annotationMetadata[$name])) { + if (! isset(self::$annotationMetadata[$name])) { $this->collectAnnotationMetadata($name); } // verify that the class is really meant to be an annotation and not just any ordinary class if (self::$annotationMetadata[$name]['is_annotation'] === false) { - if (isset($this->ignoredAnnotationNames[$originalName])) { + if ($this->isIgnoredAnnotation($originalName) || $this->isIgnoredAnnotation($name)) { return false; } - throw AnnotationException::semanticalError(sprintf('The class "%s" is not annotated with @Annotation. Are you sure this class can be used as annotation? If so, then you need to add @Annotation to the _class_ doc comment of "%s". If it is indeed no annotation, then you need to add @IgnoreAnnotation("%s") to the _class_ doc comment of %s.', $name, $name, $originalName, $this->context)); + throw AnnotationException::semanticalError(sprintf( + <<<'EXCEPTION' +The class "%s" is not annotated with @Annotation. +Are you sure this class can be used as annotation? +If so, then you need to add @Annotation to the _class_ doc comment of "%s". +If it is indeed no annotation, then you need to add @IgnoreAnnotation("%s") to the _class_ doc comment of %s. +EXCEPTION + , + $name, + $name, + $originalName, + $this->context + )); } //if target is nested annotation @@ -734,36 +853,57 @@ final class DocParser $this->isNestedAnnotation = true; //if annotation does not support current target - if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) { + if ((self::$annotationMetadata[$name]['targets'] & $target) === 0 && $target) { throw AnnotationException::semanticalError( - sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.', - $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal']) + sprintf( + <<<'EXCEPTION' +Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s. +EXCEPTION + , + $originalName, + $this->context, + self::$annotationMetadata[$name]['targets_literal'] + ) ); } - $values = $this->MethodCall(); + $arguments = $this->MethodCall(); + $values = $this->resolvePositionalValues($arguments, $name); if (isset(self::$annotationMetadata[$name]['enum'])) { // checks all declared attributes foreach (self::$annotationMetadata[$name]['enum'] as $property => $enum) { // checks if the attribute is a valid enumerator if (isset($values[$property]) && ! in_array($values[$property], $enum['value'])) { - throw AnnotationException::enumeratorError($property, $name, $this->context, $enum['literal'], $values[$property]); + throw AnnotationException::enumeratorError( + $property, + $name, + $this->context, + $enum['literal'], + $values[$property] + ); } } } // checks all declared attributes foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) { - if ($property === self::$annotationMetadata[$name]['default_property'] - && !isset($values[$property]) && isset($values['value'])) { + if ( + $property === self::$annotationMetadata[$name]['default_property'] + && ! isset($values[$property]) && isset($values['value']) + ) { $property = 'value'; } // handle a not given attribute or null value - if (!isset($values[$property])) { + if (! isset($values[$property])) { if ($type['required']) { - throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']); + throw AnnotationException::requiredError( + $property, + $originalName, + $this->context, + 'a(n) ' . $type['value'] + ); } continue; @@ -771,40 +911,100 @@ final class DocParser if ($type['type'] === 'array') { // handle the case of a single value - if ( ! is_array($values[$property])) { - $values[$property] = array($values[$property]); + if (! is_array($values[$property])) { + $values[$property] = [$values[$property]]; } // checks if the attribute has array type declaration, such as "array" if (isset($type['array_type'])) { foreach ($values[$property] as $item) { - if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) { - throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item); + if (gettype($item) !== $type['array_type'] && ! $item instanceof $type['array_type']) { + throw AnnotationException::attributeTypeError( + $property, + $originalName, + $this->context, + 'either a(n) ' . $type['array_type'] . ', or an array of ' . $type['array_type'] . 's', + $item + ); } } } - } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) { - throw AnnotationException::attributeTypeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]); + } elseif (gettype($values[$property]) !== $type['type'] && ! $values[$property] instanceof $type['type']) { + throw AnnotationException::attributeTypeError( + $property, + $originalName, + $this->context, + 'a(n) ' . $type['value'], + $values[$property] + ); } } + if (self::$annotationMetadata[$name]['has_named_argument_constructor']) { + if (PHP_VERSION_ID >= 80000) { + return $this->instantiateAnnotiation($originalName, $this->context, $name, $values); + } + + $positionalValues = []; + foreach (self::$annotationMetadata[$name]['constructor_args'] as $property => $parameter) { + $positionalValues[$parameter['position']] = $parameter['default']; + } + + foreach ($values as $property => $value) { + if (! isset(self::$annotationMetadata[$name]['constructor_args'][$property])) { + throw AnnotationException::creationError(sprintf( + <<<'EXCEPTION' +The annotation @%s declared on %s does not have a property named "%s" +that can be set through its named arguments constructor. +Available named arguments: %s +EXCEPTION + , + $originalName, + $this->context, + $property, + implode(', ', array_keys(self::$annotationMetadata[$name]['constructor_args'])) + )); + } + + $positionalValues[self::$annotationMetadata[$name]['constructor_args'][$property]['position']] = $value; + } + + return $this->instantiateAnnotiation($originalName, $this->context, $name, $positionalValues); + } + // check if the annotation expects values via the constructor, // or directly injected into public properties if (self::$annotationMetadata[$name]['has_constructor'] === true) { - return new $name($values); + return $this->instantiateAnnotiation($originalName, $this->context, $name, [$values]); } - $instance = new $name(); + $instance = $this->instantiateAnnotiation($originalName, $this->context, $name, []); foreach ($values as $property => $value) { - if (!isset(self::$annotationMetadata[$name]['properties'][$property])) { - if ('value' !== $property) { - throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not have a property named "%s". Available properties: %s', $originalName, $this->context, $property, implode(', ', self::$annotationMetadata[$name]['properties']))); + if (! isset(self::$annotationMetadata[$name]['properties'][$property])) { + if ($property !== 'value') { + throw AnnotationException::creationError(sprintf( + <<<'EXCEPTION' +The annotation @%s declared on %s does not have a property named "%s". +Available properties: %s +EXCEPTION + , + $originalName, + $this->context, + $property, + implode(', ', self::$annotationMetadata[$name]['properties']) + )); } // handle the case if the property has no annotations - if ( ! $property = self::$annotationMetadata[$name]['default_property']) { - throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not accept any values, but got %s.', $originalName, $this->context, json_encode($values))); + $property = self::$annotationMetadata[$name]['default_property']; + if (! $property) { + throw AnnotationException::creationError(sprintf( + 'The annotation @%s declared on %s does not accept any values, but got %s.', + $originalName, + $this->context, + json_encode($values) + )); } } @@ -817,19 +1017,22 @@ final class DocParser /** * MethodCall ::= ["(" [Values] ")"] * - * @return array + * @return mixed[] + * + * @throws AnnotationException + * @throws ReflectionException */ - private function MethodCall() + private function MethodCall(): array { - $values = array(); + $values = []; - if ( ! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) { + if (! $this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) { return $values; } $this->match(DocLexer::T_OPEN_PARENTHESIS); - if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { + if (! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { $values = $this->Values(); } @@ -841,11 +1044,14 @@ final class DocParser /** * Values ::= Array | Value {"," Value}* [","] * - * @return array + * @return mixed[] + * + * @throws AnnotationException + * @throws ReflectionException */ - private function Values() + private function Values(): array { - $values = array($this->Value()); + $values = [$this->Value()]; while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { $this->match(DocLexer::T_COMMA); @@ -857,30 +1063,20 @@ final class DocParser $token = $this->lexer->lookahead; $value = $this->Value(); - if ( ! is_object($value) && ! is_array($value)) { - $this->syntaxError('Value', $token); - } - $values[] = $value; } + $namedArguments = []; + $positionalArguments = []; foreach ($values as $k => $value) { - if (is_object($value) && $value instanceof \stdClass) { - $values[$value->name] = $value->value; - } else if ( ! isset($values['value'])){ - $values['value'] = $value; + if (is_object($value) && $value instanceof stdClass) { + $namedArguments[$value->name] = $value->value; } else { - if ( ! is_array($values['value'])) { - $values['value'] = array($values['value']); - } - - $values['value'][] = $value; + $positionalArguments[$k] = $value; } - - unset($values[$k]); } - return $values; + return ['named_arguments' => $namedArguments, 'positional_arguments' => $positionalArguments]; } /** @@ -894,79 +1090,112 @@ final class DocParser { $identifier = $this->Identifier(); - if ( ! defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) { - list($className, $const) = explode('::', $identifier); + if (! defined($identifier) && strpos($identifier, '::') !== false && $identifier[0] !== '\\') { + [$className, $const] = explode('::', $identifier); - $alias = (false === $pos = strpos($className, '\\')) ? $className : substr($className, 0, $pos); - $found = false; + $pos = strpos($className, '\\'); + $alias = ($pos === false) ? $className : substr($className, 0, $pos); + $found = false; + $loweredAlias = strtolower($alias); switch (true) { - case !empty ($this->namespaces): + case ! empty($this->namespaces): foreach ($this->namespaces as $ns) { - if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) { - $className = $ns.'\\'.$className; - $found = true; - break; + if (class_exists($ns . '\\' . $className) || interface_exists($ns . '\\' . $className)) { + $className = $ns . '\\' . $className; + $found = true; + break; } } + break; - case isset($this->imports[$loweredAlias = strtolower($alias)]): + case isset($this->imports[$loweredAlias]): $found = true; - $className = (false !== $pos) + $className = ($pos !== false) ? $this->imports[$loweredAlias] . substr($className, $pos) : $this->imports[$loweredAlias]; break; default: - if(isset($this->imports['__NAMESPACE__'])) { + if (isset($this->imports['__NAMESPACE__'])) { $ns = $this->imports['__NAMESPACE__']; - if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) { - $className = $ns.'\\'.$className; - $found = true; + if (class_exists($ns . '\\' . $className) || interface_exists($ns . '\\' . $className)) { + $className = $ns . '\\' . $className; + $found = true; } } + break; } if ($found) { - $identifier = $className . '::' . $const; + $identifier = $className . '::' . $const; } } - // checks if identifier ends with ::class, \strlen('::class') === 7 - $classPos = stripos($identifier, '::class'); - if ($classPos === strlen($identifier) - 7) { - return substr($identifier, 0, $classPos); + /** + * Checks if identifier ends with ::class and remove the leading backslash if it exists. + */ + if ( + $this->identifierEndsWithClassConstant($identifier) && + ! $this->identifierStartsWithBackslash($identifier) + ) { + return substr($identifier, 0, $this->getClassConstantPositionInIdentifier($identifier)); } - if (!defined($identifier)) { + if ($this->identifierEndsWithClassConstant($identifier) && $this->identifierStartsWithBackslash($identifier)) { + return substr($identifier, 1, $this->getClassConstantPositionInIdentifier($identifier) - 1); + } + + if (! defined($identifier)) { throw AnnotationException::semanticalErrorConstants($identifier, $this->context); } return constant($identifier); } + private function identifierStartsWithBackslash(string $identifier): bool + { + return $identifier[0] === '\\'; + } + + private function identifierEndsWithClassConstant(string $identifier): bool + { + return $this->getClassConstantPositionInIdentifier($identifier) === strlen($identifier) - strlen('::class'); + } + + /** + * @return int|false + */ + private function getClassConstantPositionInIdentifier(string $identifier) + { + return stripos($identifier, '::class'); + } + /** * Identifier ::= string * - * @return string + * @throws AnnotationException */ - private function Identifier() + private function Identifier(): string { // check if we have an annotation - if ( ! $this->lexer->isNextTokenAny(self::$classIdentifiers)) { - $this->syntaxError('namespace separator or identifier'); + if (! $this->lexer->isNextTokenAny(self::$classIdentifiers)) { + throw $this->syntaxError('namespace separator or identifier'); } $this->lexer->moveNext(); $className = $this->lexer->token['value']; - while ($this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value'])) - && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) { - + while ( + $this->lexer->lookahead !== null && + $this->lexer->lookahead['position'] === ($this->lexer->token['position'] + + strlen($this->lexer->token['value'])) && + $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR) + ) { $this->match(DocLexer::T_NAMESPACE_SEPARATOR); $this->matchAny(self::$classIdentifiers); @@ -980,12 +1209,15 @@ final class DocParser * Value ::= PlainValue | FieldAssignment * * @return mixed + * + * @throws AnnotationException + * @throws ReflectionException */ private function Value() { $peek = $this->lexer->glimpse(); - if (DocLexer::T_EQUALS === $peek['type']) { + if ($peek['type'] === DocLexer::T_EQUALS) { return $this->FieldAssignment(); } @@ -996,6 +1228,9 @@ final class DocParser * PlainValue ::= integer | string | float | boolean | Array | Annotation * * @return mixed + * + * @throws AnnotationException + * @throws ReflectionException */ private function PlainValue() { @@ -1014,30 +1249,36 @@ final class DocParser switch ($this->lexer->lookahead['type']) { case DocLexer::T_STRING: $this->match(DocLexer::T_STRING); + return $this->lexer->token['value']; case DocLexer::T_INTEGER: $this->match(DocLexer::T_INTEGER); - return (int)$this->lexer->token['value']; + + return (int) $this->lexer->token['value']; case DocLexer::T_FLOAT: $this->match(DocLexer::T_FLOAT); - return (float)$this->lexer->token['value']; + + return (float) $this->lexer->token['value']; case DocLexer::T_TRUE: $this->match(DocLexer::T_TRUE); + return true; case DocLexer::T_FALSE: $this->match(DocLexer::T_FALSE); + return false; case DocLexer::T_NULL: $this->match(DocLexer::T_NULL); + return null; default: - $this->syntaxError('PlainValue'); + throw $this->syntaxError('PlainValue'); } } @@ -1045,16 +1286,17 @@ final class DocParser * FieldAssignment ::= FieldName "=" PlainValue * FieldName ::= identifier * - * @return array + * @throws AnnotationException + * @throws ReflectionException */ - private function FieldAssignment() + private function FieldAssignment(): stdClass { $this->match(DocLexer::T_IDENTIFIER); $fieldName = $this->lexer->token['value']; $this->match(DocLexer::T_EQUALS); - $item = new \stdClass(); + $item = new stdClass(); $item->name = $fieldName; $item->value = $this->PlainValue(); @@ -1064,11 +1306,14 @@ final class DocParser /** * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}" * - * @return array + * @return mixed[] + * + * @throws AnnotationException + * @throws ReflectionException */ - private function Arrayx() + private function Arrayx(): array { - $array = $values = array(); + $array = $values = []; $this->match(DocLexer::T_OPEN_CURLY_BRACES); @@ -1095,7 +1340,7 @@ final class DocParser $this->match(DocLexer::T_CLOSE_CURLY_BRACES); foreach ($values as $value) { - list ($key, $val) = $value; + [$key, $val] = $value; if ($key !== null) { $array[$key] = $val; @@ -1112,27 +1357,131 @@ final class DocParser * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant * Key ::= string | integer | Constant * - * @return array + * @throws AnnotationException + * @throws ReflectionException + * + * @phpstan-return array{mixed, mixed} */ - private function ArrayEntry() + private function ArrayEntry(): array { $peek = $this->lexer->glimpse(); - if (DocLexer::T_EQUALS === $peek['type'] - || DocLexer::T_COLON === $peek['type']) { - + if ( + $peek['type'] === DocLexer::T_EQUALS + || $peek['type'] === DocLexer::T_COLON + ) { if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { $key = $this->Constant(); } else { - $this->matchAny(array(DocLexer::T_INTEGER, DocLexer::T_STRING)); + $this->matchAny([DocLexer::T_INTEGER, DocLexer::T_STRING]); $key = $this->lexer->token['value']; } - $this->matchAny(array(DocLexer::T_EQUALS, DocLexer::T_COLON)); + $this->matchAny([DocLexer::T_EQUALS, DocLexer::T_COLON]); - return array($key, $this->PlainValue()); + return [$key, $this->PlainValue()]; } - return array(null, $this->Value()); + return [null, $this->Value()]; + } + + /** + * Checks whether the given $name matches any ignored annotation name or namespace + */ + private function isIgnoredAnnotation(string $name): bool + { + if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) { + return true; + } + + foreach (array_keys($this->ignoredAnnotationNamespaces) as $ignoredAnnotationNamespace) { + $ignoredAnnotationNamespace = rtrim($ignoredAnnotationNamespace, '\\') . '\\'; + + if (stripos(rtrim($name, '\\') . '\\', $ignoredAnnotationNamespace) === 0) { + return true; + } + } + + return false; + } + + /** + * Resolve positional arguments (without name) to named ones + * + * @param array $arguments + * + * @return array + */ + private function resolvePositionalValues(array $arguments, string $name): array + { + $positionalArguments = $arguments['positional_arguments'] ?? []; + $values = $arguments['named_arguments'] ?? []; + + if ( + self::$annotationMetadata[$name]['has_named_argument_constructor'] + && self::$annotationMetadata[$name]['default_property'] !== null + ) { + // We must ensure that we don't have positional arguments after named ones + $positions = array_keys($positionalArguments); + $lastPosition = null; + foreach ($positions as $position) { + if ( + ($lastPosition === null && $position !== 0) || + ($lastPosition !== null && $position !== $lastPosition + 1) + ) { + throw $this->syntaxError('Positional arguments after named arguments is not allowed'); + } + + $lastPosition = $position; + } + + foreach (self::$annotationMetadata[$name]['constructor_args'] as $property => $parameter) { + $position = $parameter['position']; + if (isset($values[$property]) || ! isset($positionalArguments[$position])) { + continue; + } + + $values[$property] = $positionalArguments[$position]; + } + } else { + if (count($positionalArguments) > 0 && ! isset($values['value'])) { + if (count($positionalArguments) === 1) { + $value = array_pop($positionalArguments); + } else { + $value = array_values($positionalArguments); + } + + $values['value'] = $value; + } + } + + return $values; + } + + /** + * Try to instantiate the annotation and catch and process any exceptions related to failure + * + * @param class-string $name + * @param array $arguments + * + * @return object + * + * @throws AnnotationException + */ + private function instantiateAnnotiation(string $originalName, string $context, string $name, array $arguments) + { + try { + return new $name(...$arguments); + } catch (Throwable $exception) { + throw AnnotationException::creationError( + sprintf( + 'An error occurred while instantiating the annotation @%s declared on %s: "%s".', + $originalName, + $context, + $exception->getMessage() + ), + $exception + ); + } } } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/FileCacheReader.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/FileCacheReader.php index 24add1b..6c6c22c 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/FileCacheReader.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/FileCacheReader.php @@ -1,89 +1,84 @@ . - */ namespace Doctrine\Common\Annotations; +use InvalidArgumentException; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; +use RuntimeException; + +use function chmod; +use function file_put_contents; +use function filemtime; +use function gettype; +use function is_dir; +use function is_file; +use function is_int; +use function is_writable; +use function mkdir; +use function rename; +use function rtrim; +use function serialize; +use function sha1; +use function sprintf; +use function strtr; +use function tempnam; +use function uniqid; +use function unlink; +use function var_export; + /** * File cache reader for annotations. * - * @author Johannes M. Schmitt - * @author Benjamin Eberlei - * * @deprecated the FileCacheReader is deprecated and will be removed * in version 2.0.0 of doctrine/annotations. Please use the - * {@see \Doctrine\Common\Annotations\CachedReader} instead. + * {@see \Doctrine\Common\Annotations\PsrCachedReader} instead. */ class FileCacheReader implements Reader { - /** - * @var Reader - */ + /** @var Reader */ private $reader; - /** - * @var string - */ + /** @var string */ private $dir; - /** - * @var bool - */ + /** @var bool */ private $debug; - /** - * @var array - */ - private $loadedAnnotations = array(); + /** @phpstan-var array> */ + private $loadedAnnotations = []; - /** - * @var array - */ - private $classNameHashes = array(); + /** @var array */ + private $classNameHashes = []; - /** - * @var int - */ + /** @var int */ private $umask; /** - * Constructor. + * @param string $cacheDir + * @param bool $debug + * @param int $umask * - * @param Reader $reader - * @param string $cacheDir - * @param boolean $debug - * - * @throws \InvalidArgumentException + * @throws InvalidArgumentException */ public function __construct(Reader $reader, $cacheDir, $debug = false, $umask = 0002) { - if ( ! is_int($umask)) { - throw new \InvalidArgumentException(sprintf( + if (! is_int($umask)) { + throw new InvalidArgumentException(sprintf( 'The parameter umask must be an integer, was: %s', gettype($umask) )); } $this->reader = $reader; - $this->umask = $umask; + $this->umask = $umask; - if (!is_dir($cacheDir) && !@mkdir($cacheDir, 0777 & (~$this->umask), true)) { - throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $cacheDir)); + if (! is_dir($cacheDir) && ! @mkdir($cacheDir, 0777 & (~$this->umask), true)) { + throw new InvalidArgumentException(sprintf( + 'The directory "%s" does not exist and could not be created.', + $cacheDir + )); } $this->dir = rtrim($cacheDir, '\\/'); @@ -93,31 +88,37 @@ class FileCacheReader implements Reader /** * {@inheritDoc} */ - public function getClassAnnotations(\ReflectionClass $class) + public function getClassAnnotations(ReflectionClass $class) { - if ( ! isset($this->classNameHashes[$class->name])) { + if (! isset($this->classNameHashes[$class->name])) { $this->classNameHashes[$class->name] = sha1($class->name); } + $key = $this->classNameHashes[$class->name]; if (isset($this->loadedAnnotations[$key])) { return $this->loadedAnnotations[$key]; } - $path = $this->dir.'/'.strtr($key, '\\', '-').'.cache.php'; - if (!is_file($path)) { + $path = $this->dir . '/' . strtr($key, '\\', '-') . '.cache.php'; + if (! is_file($path)) { $annot = $this->reader->getClassAnnotations($class); $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; } - if ($this->debug - && (false !== $filename = $class->getFilename()) - && filemtime($path) < filemtime($filename)) { + $filename = $class->getFilename(); + if ( + $this->debug + && $filename !== false + && filemtime($path) < filemtime($filename) + ) { @unlink($path); $annot = $this->reader->getClassAnnotations($class); $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; } @@ -127,32 +128,38 @@ class FileCacheReader implements Reader /** * {@inheritDoc} */ - public function getPropertyAnnotations(\ReflectionProperty $property) + public function getPropertyAnnotations(ReflectionProperty $property) { $class = $property->getDeclaringClass(); - if ( ! isset($this->classNameHashes[$class->name])) { + if (! isset($this->classNameHashes[$class->name])) { $this->classNameHashes[$class->name] = sha1($class->name); } - $key = $this->classNameHashes[$class->name].'$'.$property->getName(); + + $key = $this->classNameHashes[$class->name] . '$' . $property->getName(); if (isset($this->loadedAnnotations[$key])) { return $this->loadedAnnotations[$key]; } - $path = $this->dir.'/'.strtr($key, '\\', '-').'.cache.php'; - if (!is_file($path)) { + $path = $this->dir . '/' . strtr($key, '\\', '-') . '.cache.php'; + if (! is_file($path)) { $annot = $this->reader->getPropertyAnnotations($property); $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; } - if ($this->debug - && (false !== $filename = $class->getFilename()) - && filemtime($path) < filemtime($filename)) { + $filename = $class->getFilename(); + if ( + $this->debug + && $filename !== false + && filemtime($path) < filemtime($filename) + ) { @unlink($path); $annot = $this->reader->getPropertyAnnotations($property); $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; } @@ -162,32 +169,38 @@ class FileCacheReader implements Reader /** * {@inheritDoc} */ - public function getMethodAnnotations(\ReflectionMethod $method) + public function getMethodAnnotations(ReflectionMethod $method) { $class = $method->getDeclaringClass(); - if ( ! isset($this->classNameHashes[$class->name])) { + if (! isset($this->classNameHashes[$class->name])) { $this->classNameHashes[$class->name] = sha1($class->name); } - $key = $this->classNameHashes[$class->name].'#'.$method->getName(); + + $key = $this->classNameHashes[$class->name] . '#' . $method->getName(); if (isset($this->loadedAnnotations[$key])) { return $this->loadedAnnotations[$key]; } - $path = $this->dir.'/'.strtr($key, '\\', '-').'.cache.php'; - if (!is_file($path)) { + $path = $this->dir . '/' . strtr($key, '\\', '-') . '.cache.php'; + if (! is_file($path)) { $annot = $this->reader->getMethodAnnotations($method); $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; } - if ($this->debug - && (false !== $filename = $class->getFilename()) - && filemtime($path) < filemtime($filename)) { + $filename = $class->getFilename(); + if ( + $this->debug + && $filename !== false + && filemtime($path) < filemtime($filename) + ) { @unlink($path); $annot = $this->reader->getMethodAnnotations($method); $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; } @@ -204,34 +217,48 @@ class FileCacheReader implements Reader */ private function saveCacheFile($path, $data) { - if (!is_writable($this->dir)) { - throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable. Both, the webserver and the console user need access. You can manage access rights for multiple users with "chmod +a". If your system does not support this, check out the acl package.', $this->dir)); + if (! is_writable($this->dir)) { + throw new InvalidArgumentException(sprintf( + <<<'EXCEPTION' +The directory "%s" is not writable. Both the webserver and the console user need access. +You can manage access rights for multiple users with "chmod +a". +If your system does not support this, check out the acl package., +EXCEPTION + , + $this->dir + )); } $tempfile = tempnam($this->dir, uniqid('', true)); - if (false === $tempfile) { - throw new \RuntimeException(sprintf('Unable to create tempfile in directory: %s', $this->dir)); - } - - $written = file_put_contents($tempfile, 'dir)); } @chmod($tempfile, 0666 & (~$this->umask)); - if (false === rename($tempfile, $path)) { + $written = file_put_contents( + $tempfile, + 'umask)); + + if (rename($tempfile, $path) === false) { @unlink($tempfile); - throw new \RuntimeException(sprintf('Unable to rename %s to %s', $tempfile, $path)); + + throw new RuntimeException(sprintf('Unable to rename %s to %s', $tempfile, $path)); } } /** * {@inheritDoc} */ - public function getClassAnnotation(\ReflectionClass $class, $annotationName) + public function getClassAnnotation(ReflectionClass $class, $annotationName) { $annotations = $this->getClassAnnotations($class); @@ -247,7 +274,7 @@ class FileCacheReader implements Reader /** * {@inheritDoc} */ - public function getMethodAnnotation(\ReflectionMethod $method, $annotationName) + public function getMethodAnnotation(ReflectionMethod $method, $annotationName) { $annotations = $this->getMethodAnnotations($method); @@ -263,7 +290,7 @@ class FileCacheReader implements Reader /** * {@inheritDoc} */ - public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName) + public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) { $annotations = $this->getPropertyAnnotations($property); @@ -283,6 +310,6 @@ class FileCacheReader implements Reader */ public function clearLoadedAnnotations() { - $this->loadedAnnotations = array(); + $this->loadedAnnotations = []; } } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php new file mode 100644 index 0000000..ab27f8a --- /dev/null +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/ImplicitlyIgnoredAnnotationNames.php @@ -0,0 +1,178 @@ + true, + 'Attribute' => true, + 'Attributes' => true, + /* Can we enable this? 'Enum' => true, */ + 'Required' => true, + 'Target' => true, + 'NamedArgumentConstructor' => true, + ]; + + private const WidelyUsedNonStandard = [ + 'fix' => true, + 'fixme' => true, + 'override' => true, + ]; + + private const PhpDocumentor1 = [ + 'abstract' => true, + 'access' => true, + 'code' => true, + 'deprec' => true, + 'endcode' => true, + 'exception' => true, + 'final' => true, + 'ingroup' => true, + 'inheritdoc' => true, + 'inheritDoc' => true, + 'magic' => true, + 'name' => true, + 'private' => true, + 'static' => true, + 'staticvar' => true, + 'staticVar' => true, + 'toc' => true, + 'tutorial' => true, + 'throw' => true, + ]; + + private const PhpDocumentor2 = [ + 'api' => true, + 'author' => true, + 'category' => true, + 'copyright' => true, + 'deprecated' => true, + 'example' => true, + 'filesource' => true, + 'global' => true, + 'ignore' => true, + /* Can we enable this? 'index' => true, */ + 'internal' => true, + 'license' => true, + 'link' => true, + 'method' => true, + 'package' => true, + 'param' => true, + 'property' => true, + 'property-read' => true, + 'property-write' => true, + 'return' => true, + 'see' => true, + 'since' => true, + 'source' => true, + 'subpackage' => true, + 'throws' => true, + 'todo' => true, + 'TODO' => true, + 'usedby' => true, + 'uses' => true, + 'var' => true, + 'version' => true, + ]; + + private const PHPUnit = [ + 'author' => true, + 'after' => true, + 'afterClass' => true, + 'backupGlobals' => true, + 'backupStaticAttributes' => true, + 'before' => true, + 'beforeClass' => true, + 'codeCoverageIgnore' => true, + 'codeCoverageIgnoreStart' => true, + 'codeCoverageIgnoreEnd' => true, + 'covers' => true, + 'coversDefaultClass' => true, + 'coversNothing' => true, + 'dataProvider' => true, + 'depends' => true, + 'doesNotPerformAssertions' => true, + 'expectedException' => true, + 'expectedExceptionCode' => true, + 'expectedExceptionMessage' => true, + 'expectedExceptionMessageRegExp' => true, + 'group' => true, + 'large' => true, + 'medium' => true, + 'preserveGlobalState' => true, + 'requires' => true, + 'runTestsInSeparateProcesses' => true, + 'runInSeparateProcess' => true, + 'small' => true, + 'test' => true, + 'testdox' => true, + 'testWith' => true, + 'ticket' => true, + 'uses' => true, + ]; + + private const PhpCheckStyle = ['SuppressWarnings' => true]; + + private const PhpStorm = ['noinspection' => true]; + + private const PEAR = ['package_version' => true]; + + private const PlainUML = [ + 'startuml' => true, + 'enduml' => true, + ]; + + private const Symfony = ['experimental' => true]; + + private const PhpCodeSniffer = [ + 'codingStandardsIgnoreStart' => true, + 'codingStandardsIgnoreEnd' => true, + ]; + + private const SlevomatCodingStandard = ['phpcsSuppress' => true]; + + private const Phan = ['suppress' => true]; + + private const Rector = ['noRector' => true]; + + private const StaticAnalysis = [ + // PHPStan, Psalm + 'extends' => true, + 'implements' => true, + 'readonly' => true, + 'template' => true, + 'use' => true, + + // Psalm + 'pure' => true, + 'immutable' => true, + ]; + + public const LIST = self::Reserved + + self::WidelyUsedNonStandard + + self::PhpDocumentor1 + + self::PhpDocumentor2 + + self::PHPUnit + + self::PhpCheckStyle + + self::PhpStorm + + self::PEAR + + self::PlainUML + + self::Symfony + + self::SlevomatCodingStandard + + self::PhpCodeSniffer + + self::Phan + + self::Rector + + self::StaticAnalysis; + + private function __construct() + { + } +} diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/IndexedReader.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/IndexedReader.php index bf7fbdc..62dcf74 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/IndexedReader.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/IndexedReader.php @@ -1,41 +1,22 @@ . - */ namespace Doctrine\Common\Annotations; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; + +use function call_user_func_array; +use function get_class; + /** * Allows the reader to be used in-place of Doctrine's reader. - * - * @author Johannes M. Schmitt */ class IndexedReader implements Reader { - /** - * @var Reader - */ + /** @var Reader */ private $delegate; - /** - * Constructor. - * - * @param Reader $reader - */ public function __construct(Reader $reader) { $this->delegate = $reader; @@ -44,9 +25,9 @@ class IndexedReader implements Reader /** * {@inheritDoc} */ - public function getClassAnnotations(\ReflectionClass $class) + public function getClassAnnotations(ReflectionClass $class) { - $annotations = array(); + $annotations = []; foreach ($this->delegate->getClassAnnotations($class) as $annot) { $annotations[get_class($annot)] = $annot; } @@ -57,17 +38,17 @@ class IndexedReader implements Reader /** * {@inheritDoc} */ - public function getClassAnnotation(\ReflectionClass $class, $annotation) + public function getClassAnnotation(ReflectionClass $class, $annotationName) { - return $this->delegate->getClassAnnotation($class, $annotation); + return $this->delegate->getClassAnnotation($class, $annotationName); } /** * {@inheritDoc} */ - public function getMethodAnnotations(\ReflectionMethod $method) + public function getMethodAnnotations(ReflectionMethod $method) { - $annotations = array(); + $annotations = []; foreach ($this->delegate->getMethodAnnotations($method) as $annot) { $annotations[get_class($annot)] = $annot; } @@ -78,17 +59,17 @@ class IndexedReader implements Reader /** * {@inheritDoc} */ - public function getMethodAnnotation(\ReflectionMethod $method, $annotation) + public function getMethodAnnotation(ReflectionMethod $method, $annotationName) { - return $this->delegate->getMethodAnnotation($method, $annotation); + return $this->delegate->getMethodAnnotation($method, $annotationName); } /** * {@inheritDoc} */ - public function getPropertyAnnotations(\ReflectionProperty $property) + public function getPropertyAnnotations(ReflectionProperty $property) { - $annotations = array(); + $annotations = []; foreach ($this->delegate->getPropertyAnnotations($property) as $annot) { $annotations[get_class($annot)] = $annot; } @@ -99,21 +80,21 @@ class IndexedReader implements Reader /** * {@inheritDoc} */ - public function getPropertyAnnotation(\ReflectionProperty $property, $annotation) + public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) { - return $this->delegate->getPropertyAnnotation($property, $annotation); + return $this->delegate->getPropertyAnnotation($property, $annotationName); } /** * Proxies all methods to the delegate. * - * @param string $method - * @param array $args + * @param string $method + * @param mixed[] $args * * @return mixed */ public function __call($method, $args) { - return call_user_func_array(array($this->delegate, $method), $args); + return call_user_func_array([$this->delegate, $method], $args); } } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/NamedArgumentConstructorAnnotation.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/NamedArgumentConstructorAnnotation.php new file mode 100644 index 0000000..8af224c --- /dev/null +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/NamedArgumentConstructorAnnotation.php @@ -0,0 +1,14 @@ +. - */ namespace Doctrine\Common\Annotations; +use ReflectionClass; +use ReflectionFunction; use SplFileObject; +use function is_file; +use function method_exists; +use function preg_quote; +use function preg_replace; + /** * Parses a file for namespaces/use/class declarations. - * - * @author Fabien Potencier - * @author Christian Kaps */ final class PhpParser { /** * Parses a class. * - * @param \ReflectionClass $class A ReflectionClass object. + * @deprecated use parseUseStatements instead * - * @return array A list with use statements in the form (Alias => FQN). + * @param ReflectionClass $class A ReflectionClass object. + * + * @return array A list with use statements in the form (Alias => FQN). */ - public function parseClass(\ReflectionClass $class) + public function parseClass(ReflectionClass $class) { - if (method_exists($class, 'getUseStatements')) { - return $class->getUseStatements(); + return $this->parseUseStatements($class); + } + + /** + * Parse a class or function for use statements. + * + * @param ReflectionClass|ReflectionFunction $reflection + * + * @psalm-return array a list with use statements in the form (Alias => FQN). + */ + public function parseUseStatements($reflection): array + { + if (method_exists($reflection, 'getUseStatements')) { + return $reflection->getUseStatements(); } - if (false === $filename = $class->getFilename()) { - return array(); + $filename = $reflection->getFileName(); + + if ($filename === false) { + return []; } - $content = $this->getFileContent($filename, $class->getStartLine()); + $content = $this->getFileContent($filename, $reflection->getStartLine()); - if (null === $content) { - return array(); + if ($content === null) { + return []; } - $namespace = preg_quote($class->getNamespaceName()); - $content = preg_replace('/^.*?(\bnamespace\s+' . $namespace . '\s*[;{].*)$/s', '\\1', $content); + $namespace = preg_quote($reflection->getNamespaceName()); + $content = preg_replace('/^.*?(\bnamespace\s+' . $namespace . '\s*[;{].*)$/s', '\\1', $content); $tokenizer = new TokenParser('parseUseStatements($class->getNamespaceName()); - - return $statements; + return $tokenizer->parseUseStatements($reflection->getNamespaceName()); } /** * Gets the content of the file right up to the given line number. * - * @param string $filename The name of the file to load. - * @param integer $lineNumber The number of lines to read from file. + * @param string $filename The name of the file to load. + * @param int $lineNumber The number of lines to read from file. * - * @return string The content of the file. + * @return string|null The content of the file or null if the file does not exist. */ private function getFileContent($filename, $lineNumber) { - if ( ! is_file($filename)) { + if (! is_file($filename)) { return null; } $content = ''; $lineCnt = 0; - $file = new SplFileObject($filename); - while (!$file->eof()) { - if ($lineCnt++ == $lineNumber) { + $file = new SplFileObject($filename); + while (! $file->eof()) { + if ($lineCnt++ === $lineNumber) { break; } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/PsrCachedReader.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/PsrCachedReader.php new file mode 100644 index 0000000..a7099d5 --- /dev/null +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/PsrCachedReader.php @@ -0,0 +1,232 @@ +> */ + private $loadedAnnotations = []; + + /** @var int[] */ + private $loadedFilemtimes = []; + + public function __construct(Reader $reader, CacheItemPoolInterface $cache, bool $debug = false) + { + $this->delegate = $reader; + $this->cache = $cache; + $this->debug = (bool) $debug; + } + + /** + * {@inheritDoc} + */ + public function getClassAnnotations(ReflectionClass $class) + { + $cacheKey = $class->getName(); + + if (isset($this->loadedAnnotations[$cacheKey])) { + return $this->loadedAnnotations[$cacheKey]; + } + + $annots = $this->fetchFromCache($cacheKey, $class, 'getClassAnnotations', $class); + + return $this->loadedAnnotations[$cacheKey] = $annots; + } + + /** + * {@inheritDoc} + */ + public function getClassAnnotation(ReflectionClass $class, $annotationName) + { + foreach ($this->getClassAnnotations($class) as $annot) { + if ($annot instanceof $annotationName) { + return $annot; + } + } + + return null; + } + + /** + * {@inheritDoc} + */ + public function getPropertyAnnotations(ReflectionProperty $property) + { + $class = $property->getDeclaringClass(); + $cacheKey = $class->getName() . '$' . $property->getName(); + + if (isset($this->loadedAnnotations[$cacheKey])) { + return $this->loadedAnnotations[$cacheKey]; + } + + $annots = $this->fetchFromCache($cacheKey, $class, 'getPropertyAnnotations', $property); + + return $this->loadedAnnotations[$cacheKey] = $annots; + } + + /** + * {@inheritDoc} + */ + public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) + { + foreach ($this->getPropertyAnnotations($property) as $annot) { + if ($annot instanceof $annotationName) { + return $annot; + } + } + + return null; + } + + /** + * {@inheritDoc} + */ + public function getMethodAnnotations(ReflectionMethod $method) + { + $class = $method->getDeclaringClass(); + $cacheKey = $class->getName() . '#' . $method->getName(); + + if (isset($this->loadedAnnotations[$cacheKey])) { + return $this->loadedAnnotations[$cacheKey]; + } + + $annots = $this->fetchFromCache($cacheKey, $class, 'getMethodAnnotations', $method); + + return $this->loadedAnnotations[$cacheKey] = $annots; + } + + /** + * {@inheritDoc} + */ + public function getMethodAnnotation(ReflectionMethod $method, $annotationName) + { + foreach ($this->getMethodAnnotations($method) as $annot) { + if ($annot instanceof $annotationName) { + return $annot; + } + } + + return null; + } + + public function clearLoadedAnnotations(): void + { + $this->loadedAnnotations = []; + $this->loadedFilemtimes = []; + } + + /** @return mixed[] */ + private function fetchFromCache( + string $cacheKey, + ReflectionClass $class, + string $method, + Reflector $reflector + ): array { + $cacheKey = rawurlencode($cacheKey); + + $item = $this->cache->getItem($cacheKey); + if (($this->debug && ! $this->refresh($cacheKey, $class)) || ! $item->isHit()) { + $this->cache->save($item->set($this->delegate->{$method}($reflector))); + } + + return $item->get(); + } + + /** + * Used in debug mode to check if the cache is fresh. + * + * @return bool Returns true if the cache was fresh, or false if the class + * being read was modified since writing to the cache. + */ + private function refresh(string $cacheKey, ReflectionClass $class): bool + { + $lastModification = $this->getLastModification($class); + if ($lastModification === 0) { + return true; + } + + $item = $this->cache->getItem('[C]' . $cacheKey); + if ($item->isHit() && $item->get() >= $lastModification) { + return true; + } + + $this->cache->save($item->set(time())); + + return false; + } + + /** + * Returns the time the class was last modified, testing traits and parents + */ + private function getLastModification(ReflectionClass $class): int + { + $filename = $class->getFileName(); + + if (isset($this->loadedFilemtimes[$filename])) { + return $this->loadedFilemtimes[$filename]; + } + + $parent = $class->getParentClass(); + + $lastModification = max(array_merge( + [$filename ? filemtime($filename) : 0], + array_map(function (ReflectionClass $reflectionTrait): int { + return $this->getTraitLastModificationTime($reflectionTrait); + }, $class->getTraits()), + array_map(function (ReflectionClass $class): int { + return $this->getLastModification($class); + }, $class->getInterfaces()), + $parent ? [$this->getLastModification($parent)] : [] + )); + + assert($lastModification !== false); + + return $this->loadedFilemtimes[$filename] = $lastModification; + } + + private function getTraitLastModificationTime(ReflectionClass $reflectionTrait): int + { + $fileName = $reflectionTrait->getFileName(); + + if (isset($this->loadedFilemtimes[$fileName])) { + return $this->loadedFilemtimes[$fileName]; + } + + $lastModificationTime = max(array_merge( + [$fileName ? filemtime($fileName) : 0], + array_map(function (ReflectionClass $reflectionTrait): int { + return $this->getTraitLastModificationTime($reflectionTrait); + }, $reflectionTrait->getTraits()) + )); + + assert($lastModificationTime !== false); + + return $this->loadedFilemtimes[$fileName] = $lastModificationTime; + } +} diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Reader.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Reader.php index 4774f87..0663ffd 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Reader.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/Reader.php @@ -1,89 +1,80 @@ . - */ namespace Doctrine\Common\Annotations; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; + /** * Interface for annotation readers. - * - * @author Johannes M. Schmitt */ interface Reader { /** * Gets the annotations applied to a class. * - * @param \ReflectionClass $class The ReflectionClass of the class from which - * the class annotations should be read. + * @param ReflectionClass $class The ReflectionClass of the class from which + * the class annotations should be read. * - * @return array An array of Annotations. + * @return array An array of Annotations. */ - function getClassAnnotations(\ReflectionClass $class); + public function getClassAnnotations(ReflectionClass $class); /** * Gets a class annotation. * - * @param \ReflectionClass $class The ReflectionClass of the class from which - * the class annotations should be read. - * @param string $annotationName The name of the annotation. + * @param ReflectionClass $class The ReflectionClass of the class from which + * the class annotations should be read. + * @param class-string $annotationName The name of the annotation. * - * @return object|null The Annotation or NULL, if the requested annotation does not exist. + * @return T|null The Annotation or NULL, if the requested annotation does not exist. + * + * @template T */ - function getClassAnnotation(\ReflectionClass $class, $annotationName); + public function getClassAnnotation(ReflectionClass $class, $annotationName); /** * Gets the annotations applied to a method. * - * @param \ReflectionMethod $method The ReflectionMethod of the method from which - * the annotations should be read. + * @param ReflectionMethod $method The ReflectionMethod of the method from which + * the annotations should be read. * - * @return array An array of Annotations. + * @return array An array of Annotations. */ - function getMethodAnnotations(\ReflectionMethod $method); + public function getMethodAnnotations(ReflectionMethod $method); /** * Gets a method annotation. * - * @param \ReflectionMethod $method The ReflectionMethod to read the annotations from. - * @param string $annotationName The name of the annotation. + * @param ReflectionMethod $method The ReflectionMethod to read the annotations from. + * @param class-string $annotationName The name of the annotation. * - * @return object|null The Annotation or NULL, if the requested annotation does not exist. + * @return T|null The Annotation or NULL, if the requested annotation does not exist. + * + * @template T */ - function getMethodAnnotation(\ReflectionMethod $method, $annotationName); + public function getMethodAnnotation(ReflectionMethod $method, $annotationName); /** * Gets the annotations applied to a property. * - * @param \ReflectionProperty $property The ReflectionProperty of the property - * from which the annotations should be read. + * @param ReflectionProperty $property The ReflectionProperty of the property + * from which the annotations should be read. * - * @return array An array of Annotations. + * @return array An array of Annotations. */ - function getPropertyAnnotations(\ReflectionProperty $property); + public function getPropertyAnnotations(ReflectionProperty $property); /** * Gets a property annotation. * - * @param \ReflectionProperty $property The ReflectionProperty to read the annotations from. - * @param string $annotationName The name of the annotation. + * @param ReflectionProperty $property The ReflectionProperty to read the annotations from. + * @param class-string $annotationName The name of the annotation. * - * @return object|null The Annotation or NULL, if the requested annotation does not exist. + * @return T|null The Annotation or NULL, if the requested annotation does not exist. + * + * @template T */ - function getPropertyAnnotation(\ReflectionProperty $property, $annotationName); + public function getPropertyAnnotation(ReflectionProperty $property, $annotationName); } diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php index d4757ee..8a78c11 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php @@ -1,44 +1,25 @@ . - */ namespace Doctrine\Common\Annotations; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; + /** * Simple Annotation Reader. * * This annotation reader is intended to be used in projects where you have * full-control over all annotations that are available. * - * @since 2.2 - * @author Johannes M. Schmitt - * @author Fabio B. Silva + * @deprecated Deprecated in favour of using AnnotationReader */ class SimpleAnnotationReader implements Reader { - /** - * @var DocParser - */ + /** @var DocParser */ private $parser; /** - * Constructor. - * * Initializes a new SimpleAnnotationReader. */ public function __construct() @@ -62,31 +43,37 @@ class SimpleAnnotationReader implements Reader /** * {@inheritDoc} */ - public function getClassAnnotations(\ReflectionClass $class) + public function getClassAnnotations(ReflectionClass $class) { - return $this->parser->parse($class->getDocComment(), 'class '.$class->getName()); + return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName()); } /** * {@inheritDoc} */ - public function getMethodAnnotations(\ReflectionMethod $method) + public function getMethodAnnotations(ReflectionMethod $method) { - return $this->parser->parse($method->getDocComment(), 'method '.$method->getDeclaringClass()->name.'::'.$method->getName().'()'); + return $this->parser->parse( + $method->getDocComment(), + 'method ' . $method->getDeclaringClass()->name . '::' . $method->getName() . '()' + ); } /** * {@inheritDoc} */ - public function getPropertyAnnotations(\ReflectionProperty $property) + public function getPropertyAnnotations(ReflectionProperty $property) { - return $this->parser->parse($property->getDocComment(), 'property '.$property->getDeclaringClass()->name.'::$'.$property->getName()); + return $this->parser->parse( + $property->getDocComment(), + 'property ' . $property->getDeclaringClass()->name . '::$' . $property->getName() + ); } /** * {@inheritDoc} */ - public function getClassAnnotation(\ReflectionClass $class, $annotationName) + public function getClassAnnotation(ReflectionClass $class, $annotationName) { foreach ($this->getClassAnnotations($class) as $annot) { if ($annot instanceof $annotationName) { @@ -100,7 +87,7 @@ class SimpleAnnotationReader implements Reader /** * {@inheritDoc} */ - public function getMethodAnnotation(\ReflectionMethod $method, $annotationName) + public function getMethodAnnotation(ReflectionMethod $method, $annotationName) { foreach ($this->getMethodAnnotations($method) as $annot) { if ($annot instanceof $annotationName) { @@ -114,7 +101,7 @@ class SimpleAnnotationReader implements Reader /** * {@inheritDoc} */ - public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName) + public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) { foreach ($this->getPropertyAnnotations($property) as $annot) { if ($annot instanceof $annotationName) { diff --git a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/TokenParser.php b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/TokenParser.php index 9bdccce..9605fb8 100644 --- a/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/TokenParser.php +++ b/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/TokenParser.php @@ -1,36 +1,34 @@ . - */ namespace Doctrine\Common\Annotations; +use function array_merge; +use function count; +use function explode; +use function strtolower; +use function token_get_all; + +use const PHP_VERSION_ID; +use const T_AS; +use const T_COMMENT; +use const T_DOC_COMMENT; +use const T_NAME_FULLY_QUALIFIED; +use const T_NAME_QUALIFIED; +use const T_NAMESPACE; +use const T_NS_SEPARATOR; +use const T_STRING; +use const T_USE; +use const T_WHITESPACE; + /** * Parses a file for namespaces/use/class declarations. - * - * @author Fabien Potencier - * @author Christian Kaps */ class TokenParser { /** * The token list. * - * @var array + * @phpstan-var list */ private $tokens; @@ -70,19 +68,20 @@ class TokenParser /** * Gets the next non whitespace and non comment token. * - * @param boolean $docCommentIsComment If TRUE then a doc comment is considered a comment and skipped. - * If FALSE then only whitespace and normal comments are skipped. + * @param bool $docCommentIsComment If TRUE then a doc comment is considered a comment and skipped. + * If FALSE then only whitespace and normal comments are skipped. * - * @return array|null The token if exists, null otherwise. + * @return mixed[]|string|null The token if exists, null otherwise. */ - public function next($docCommentIsComment = TRUE) + public function next($docCommentIsComment = true) { for ($i = $this->pointer; $i < $this->numTokens; $i++) { $this->pointer++; - if ($this->tokens[$i][0] === T_WHITESPACE || + if ( + $this->tokens[$i][0] === T_WHITESPACE || $this->tokens[$i][0] === T_COMMENT || - ($docCommentIsComment && $this->tokens[$i][0] === T_DOC_COMMENT)) { - + ($docCommentIsComment && $this->tokens[$i][0] === T_DOC_COMMENT) + ) { continue; } @@ -95,32 +94,48 @@ class TokenParser /** * Parses a single use statement. * - * @return array A list with all found class names for a use statement. + * @return array A list with all found class names for a use statement. */ public function parseUseStatement() { - $class = ''; - $alias = ''; - $statements = array(); + $groupRoot = ''; + $class = ''; + $alias = ''; + $statements = []; $explicitAlias = false; while (($token = $this->next())) { - $isNameToken = $token[0] === T_STRING || $token[0] === T_NS_SEPARATOR; - if (!$explicitAlias && $isNameToken) { + if (! $explicitAlias && $token[0] === T_STRING) { $class .= $token[1]; + $alias = $token[1]; + } elseif ($explicitAlias && $token[0] === T_STRING) { $alias = $token[1]; - } else if ($explicitAlias && $isNameToken) { - $alias .= $token[1]; - } else if ($token[0] === T_AS) { + } elseif ( + PHP_VERSION_ID >= 80000 && + ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED) + ) { + $class .= $token[1]; + + $classSplit = explode('\\', $token[1]); + $alias = $classSplit[count($classSplit) - 1]; + } elseif ($token[0] === T_NS_SEPARATOR) { + $class .= '\\'; + $alias = ''; + } elseif ($token[0] === T_AS) { $explicitAlias = true; - $alias = ''; - } else if ($token === ',') { - $statements[strtolower($alias)] = $class; - $class = ''; - $alias = ''; - $explicitAlias = false; - } else if ($token === ';') { - $statements[strtolower($alias)] = $class; + $alias = ''; + } elseif ($token === ',') { + $statements[strtolower($alias)] = $groupRoot . $class; + $class = ''; + $alias = ''; + $explicitAlias = false; + } elseif ($token === ';') { + $statements[strtolower($alias)] = $groupRoot . $class; break; + } elseif ($token === '{') { + $groupRoot = $class; + $class = ''; + } elseif ($token === '}') { + continue; } else { break; } @@ -134,24 +149,25 @@ class TokenParser * * @param string $namespaceName The namespace name of the reflected class. * - * @return array A list with all found use statements. + * @return array A list with all found use statements. */ public function parseUseStatements($namespaceName) { - $statements = array(); + $statements = []; while (($token = $this->next())) { if ($token[0] === T_USE) { $statements = array_merge($statements, $this->parseUseStatement()); continue; } - if ($token[0] !== T_NAMESPACE || $this->parseNamespace() != $namespaceName) { + + if ($token[0] !== T_NAMESPACE || $this->parseNamespace() !== $namespaceName) { continue; } // Get fresh array for new namespace. This is to prevent the parser to collect the use statements // for a previous namespace with the same name. This is the case if a namespace is defined twice // or if a namespace with the same name is commented out. - $statements = array(); + $statements = []; } return $statements; @@ -165,7 +181,12 @@ class TokenParser public function parseNamespace() { $name = ''; - while (($token = $this->next()) && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR)) { + while ( + ($token = $this->next()) && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR || ( + PHP_VERSION_ID >= 80000 && + ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED) + )) + ) { $name .= $token[1]; } diff --git a/vendor/doctrine/annotations/psalm.xml b/vendor/doctrine/annotations/psalm.xml new file mode 100644 index 0000000..e6af389 --- /dev/null +++ b/vendor/doctrine/annotations/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/vendor/doctrine/lexer/README.md b/vendor/doctrine/lexer/README.md index e1b419a..784f2a2 100644 --- a/vendor/doctrine/lexer/README.md +++ b/vendor/doctrine/lexer/README.md @@ -1,6 +1,6 @@ # Doctrine Lexer -Build Status: [![Build Status](https://travis-ci.org/doctrine/lexer.svg?branch=master)](https://travis-ci.org/doctrine/lexer) +[![Build Status](https://github.com/doctrine/lexer/workflows/Continuous%20Integration/badge.svg)](https://github.com/doctrine/lexer/actions) Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers. diff --git a/vendor/doctrine/lexer/composer.json b/vendor/doctrine/lexer/composer.json index 0d8e5ad..c435647 100644 --- a/vendor/doctrine/lexer/composer.json +++ b/vendor/doctrine/lexer/composer.json @@ -17,12 +17,13 @@ {"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"} ], "require": { - "php": "^7.2" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^8.2" + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" }, "autoload": { "psr-4": { "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" } @@ -30,12 +31,11 @@ "autoload-dev": { "psr-4": { "Doctrine\\Tests\\": "tests/Doctrine" } }, - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "config": { + "allow-plugins": { + "composer/package-versions-deprecated": true, + "dealerdirect/phpcodesniffer-composer-installer": true + }, "sort-packages": true } } diff --git a/vendor/doctrine/lexer/lib/Doctrine/Common/Lexer/AbstractLexer.php b/vendor/doctrine/lexer/lib/Doctrine/Common/Lexer/AbstractLexer.php index 385643a..7e8a11d 100644 --- a/vendor/doctrine/lexer/lib/Doctrine/Common/Lexer/AbstractLexer.php +++ b/vendor/doctrine/lexer/lib/Doctrine/Common/Lexer/AbstractLexer.php @@ -5,17 +5,21 @@ declare(strict_types=1); namespace Doctrine\Common\Lexer; use ReflectionClass; -use const PREG_SPLIT_DELIM_CAPTURE; -use const PREG_SPLIT_NO_EMPTY; -use const PREG_SPLIT_OFFSET_CAPTURE; + use function implode; use function in_array; use function preg_split; use function sprintf; use function substr; +use const PREG_SPLIT_DELIM_CAPTURE; +use const PREG_SPLIT_NO_EMPTY; +use const PREG_SPLIT_OFFSET_CAPTURE; + /** * Base class for writing simple lexers, i.e. for creating small DSLs. + * + * @psalm-type Token = array{value: int|string, type:string|int|null, position:int} */ abstract class AbstractLexer { @@ -35,7 +39,8 @@ abstract class AbstractLexer * parameter, none) * - 'position' : the position of the token in the input string * - * @var array + * @var mixed[][] + * @psalm-var list */ private $tokens = []; @@ -56,21 +61,23 @@ abstract class AbstractLexer /** * The next token in the input. * - * @var array|null + * @var mixed[]|null + * @psalm-var Token|null */ public $lookahead; /** * The last matched/seen token. * - * @var array|null + * @var mixed[]|null + * @psalm-var Token|null */ public $token; /** * Composed regex for input parsing. * - * @var string + * @var string|null */ private $regex; @@ -143,25 +150,25 @@ abstract class AbstractLexer /** * Checks whether a given token matches the current lookahead. * - * @param int|string $token + * @param int|string $type * * @return bool */ - public function isNextToken($token) + public function isNextToken($type) { - return $this->lookahead !== null && $this->lookahead['type'] === $token; + return $this->lookahead !== null && $this->lookahead['type'] === $type; } /** * Checks whether any of the given tokens matches the current lookahead. * - * @param array $tokens + * @param list $types * * @return bool */ - public function isNextTokenAny(array $tokens) + public function isNextTokenAny(array $types) { - return $this->lookahead !== null && in_array($this->lookahead['type'], $tokens, true); + return $this->lookahead !== null && in_array($this->lookahead['type'], $types, true); } /** @@ -209,7 +216,8 @@ abstract class AbstractLexer /** * Moves the lookahead token forward. * - * @return array|null The next token or NULL if there are no more tokens ahead. + * @return mixed[]|null The next token or NULL if there are no more tokens ahead. + * @psalm-return Token|null */ public function peek() { @@ -223,7 +231,8 @@ abstract class AbstractLexer /** * Peeks at the next token, returns it and immediately resets the peek. * - * @return array|null The next token or NULL if there are no more tokens ahead. + * @return mixed[]|null The next token or NULL if there are no more tokens ahead. + * @psalm-return Token|null */ public function glimpse() { @@ -306,14 +315,14 @@ abstract class AbstractLexer /** * Lexical catchable patterns. * - * @return array + * @return string[] */ abstract protected function getCatchablePatterns(); /** * Lexical non-catchable patterns. * - * @return array + * @return string[] */ abstract protected function getNonCatchablePatterns(); diff --git a/vendor/doctrine/lexer/psalm.xml b/vendor/doctrine/lexer/psalm.xml new file mode 100644 index 0000000..f331e50 --- /dev/null +++ b/vendor/doctrine/lexer/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/vendor/symfony/cache-contracts/.gitignore b/vendor/symfony/cache-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/vendor/symfony/cache-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/vendor/symfony/cache-contracts/CHANGELOG.md b/vendor/symfony/cache-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/vendor/symfony/cache-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/vendor/symfony/cache-contracts/CacheInterface.php b/vendor/symfony/cache-contracts/CacheInterface.php new file mode 100644 index 0000000..67e4dfd --- /dev/null +++ b/vendor/symfony/cache-contracts/CacheInterface.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\InvalidArgumentException; + +/** + * Covers most simple to advanced caching needs. + * + * @author Nicolas Grekas + */ +interface CacheInterface +{ + /** + * Fetches a value from the pool or computes it if not found. + * + * On cache misses, a callback is called that should return the missing value. + * This callback is given a PSR-6 CacheItemInterface instance corresponding to the + * requested key, that could be used e.g. for expiration control. It could also + * be an ItemInterface instance when its additional features are needed. + * + * @param string $key The key of the item to retrieve from the cache + * @param callable|CallbackInterface $callback Should return the computed value for the given key/item + * @param float|null $beta A float that, as it grows, controls the likeliness of triggering + * early expiration. 0 disables it, INF forces immediate expiration. + * The default (or providing null) is implementation dependent but should + * typically be 1.0, which should provide optimal stampede protection. + * See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration + * @param array &$metadata The metadata of the cached item {@see ItemInterface::getMetadata()} + * + * @return mixed + * + * @throws InvalidArgumentException When $key is not valid or when $beta is negative + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null); + + /** + * Removes an item from the pool. + * + * @param string $key The key to delete + * + * @throws InvalidArgumentException When $key is not valid + * + * @return bool True if the item was successfully removed, false if there was any error + */ + public function delete(string $key): bool; +} diff --git a/vendor/symfony/cache-contracts/CacheTrait.php b/vendor/symfony/cache-contracts/CacheTrait.php new file mode 100644 index 0000000..d340e06 --- /dev/null +++ b/vendor/symfony/cache-contracts/CacheTrait.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\InvalidArgumentException; +use Psr\Log\LoggerInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(InvalidArgumentException::class); + +/** + * An implementation of CacheInterface for PSR-6 CacheItemPoolInterface classes. + * + * @author Nicolas Grekas + */ +trait CacheTrait +{ + /** + * {@inheritdoc} + * + * @return mixed + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + { + return $this->doGet($this, $key, $callback, $beta, $metadata); + } + + /** + * {@inheritdoc} + */ + public function delete(string $key): bool + { + return $this->deleteItem($key); + } + + private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, ?float $beta, array &$metadata = null, LoggerInterface $logger = null) + { + if (0 > $beta = $beta ?? 1.0) { + throw new class(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta)) extends \InvalidArgumentException implements InvalidArgumentException { }; + } + + $item = $pool->getItem($key); + $recompute = !$item->isHit() || \INF === $beta; + $metadata = $item instanceof ItemInterface ? $item->getMetadata() : []; + + if (!$recompute && $metadata) { + $expiry = $metadata[ItemInterface::METADATA_EXPIRY] ?? false; + $ctime = $metadata[ItemInterface::METADATA_CTIME] ?? false; + + if ($recompute = $ctime && $expiry && $expiry <= ($now = microtime(true)) - $ctime / 1000 * $beta * log(random_int(1, \PHP_INT_MAX) / \PHP_INT_MAX)) { + // force applying defaultLifetime to expiry + $item->expiresAt(null); + $logger && $logger->info('Item "{key}" elected for early recomputation {delta}s before its expiration', [ + 'key' => $key, + 'delta' => sprintf('%.1f', $expiry - $now), + ]); + } + } + + if ($recompute) { + $save = true; + $item->set($callback($item, $save)); + if ($save) { + $pool->save($item); + } + } + + return $item->get(); + } +} diff --git a/vendor/symfony/cache-contracts/CallbackInterface.php b/vendor/symfony/cache-contracts/CallbackInterface.php new file mode 100644 index 0000000..7dae2aa --- /dev/null +++ b/vendor/symfony/cache-contracts/CallbackInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\CacheItemInterface; + +/** + * Computes and returns the cached value of an item. + * + * @author Nicolas Grekas + */ +interface CallbackInterface +{ + /** + * @param CacheItemInterface|ItemInterface $item The item to compute the value for + * @param bool &$save Should be set to false when the value should not be saved in the pool + * + * @return mixed The computed value for the passed item + */ + public function __invoke(CacheItemInterface $item, bool &$save); +} diff --git a/vendor/symfony/cache-contracts/ItemInterface.php b/vendor/symfony/cache-contracts/ItemInterface.php new file mode 100644 index 0000000..10c0488 --- /dev/null +++ b/vendor/symfony/cache-contracts/ItemInterface.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\CacheException; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\InvalidArgumentException; + +/** + * Augments PSR-6's CacheItemInterface with support for tags and metadata. + * + * @author Nicolas Grekas + */ +interface ItemInterface extends CacheItemInterface +{ + /** + * References the Unix timestamp stating when the item will expire. + */ + public const METADATA_EXPIRY = 'expiry'; + + /** + * References the time the item took to be created, in milliseconds. + */ + public const METADATA_CTIME = 'ctime'; + + /** + * References the list of tags that were assigned to the item, as string[]. + */ + public const METADATA_TAGS = 'tags'; + + /** + * Reserved characters that cannot be used in a key or tag. + */ + public const RESERVED_CHARACTERS = '{}()/\@:'; + + /** + * Adds a tag to a cache item. + * + * Tags are strings that follow the same validation rules as keys. + * + * @param string|string[] $tags A tag or array of tags + * + * @return $this + * + * @throws InvalidArgumentException When $tag is not valid + * @throws CacheException When the item comes from a pool that is not tag-aware + */ + public function tag($tags): self; + + /** + * Returns a list of metadata info that were saved alongside with the cached value. + * + * See ItemInterface::METADATA_* consts for keys potentially found in the returned array. + */ + public function getMetadata(): array; +} diff --git a/vendor/symfony/cache-contracts/LICENSE b/vendor/symfony/cache-contracts/LICENSE new file mode 100644 index 0000000..74cdc2d --- /dev/null +++ b/vendor/symfony/cache-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/cache-contracts/README.md b/vendor/symfony/cache-contracts/README.md new file mode 100644 index 0000000..7085a69 --- /dev/null +++ b/vendor/symfony/cache-contracts/README.md @@ -0,0 +1,9 @@ +Symfony Cache Contracts +======================= + +A set of abstractions extracted out of the Symfony components. + +Can be used to build on semantics that the Symfony components proved useful - and +that already have battle tested implementations. + +See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/vendor/symfony/cache-contracts/TagAwareCacheInterface.php b/vendor/symfony/cache-contracts/TagAwareCacheInterface.php new file mode 100644 index 0000000..7c4cf11 --- /dev/null +++ b/vendor/symfony/cache-contracts/TagAwareCacheInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\InvalidArgumentException; + +/** + * Allows invalidating cached items using tags. + * + * @author Nicolas Grekas + */ +interface TagAwareCacheInterface extends CacheInterface +{ + /** + * Invalidates cached items using tags. + * + * When implemented on a PSR-6 pool, invalidation should not apply + * to deferred items. Instead, they should be committed as usual. + * This allows replacing old tagged values by new ones without + * race conditions. + * + * @param string[] $tags An array of tags to invalidate + * + * @return bool True on success + * + * @throws InvalidArgumentException When $tags is not valid + */ + public function invalidateTags(array $tags); +} diff --git a/vendor/symfony/cache-contracts/composer.json b/vendor/symfony/cache-contracts/composer.json new file mode 100644 index 0000000..9f45e17 --- /dev/null +++ b/vendor/symfony/cache-contracts/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/cache-contracts", + "type": "library", + "description": "Generic abstractions related to caching", + "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0|^3.0" + }, + "suggest": { + "symfony/cache-implementation": "" + }, + "autoload": { + "psr-4": { "Symfony\\Contracts\\Cache\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/vendor/symfony/cache/Adapter/AbstractAdapter.php b/vendor/symfony/cache/Adapter/AbstractAdapter.php new file mode 100644 index 0000000..3d01409 --- /dev/null +++ b/vendor/symfony/cache/Adapter/AbstractAdapter.php @@ -0,0 +1,208 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\AbstractAdapterTrait; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * @author Nicolas Grekas + */ +abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface +{ + use AbstractAdapterTrait; + use ContractsTrait; + + /** + * @internal + */ + protected const NS_SEPARATOR = ':'; + + private static $apcuSupported; + private static $phpFilesSupported; + + protected function __construct(string $namespace = '', int $defaultLifetime = 0) + { + $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR; + $this->defaultLifetime = $defaultLifetime; + if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) { + throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace)); + } + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $value, $isHit) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $v = $value; + $item->isHit = $isHit; + // Detect wrapped values that encode for their expiry and creation duration + // For compactness, these values are packed in the key of an array using + // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { + $item->value = $v[$k]; + $v = unpack('Ve/Nc', substr($k, 1, -1)); + $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; + $item->metadata[CacheItem::METADATA_CTIME] = $v['c']; + } + + return $item; + }, + null, + CacheItem::class + ); + self::$mergeByLifetime ?? self::$mergeByLifetime = \Closure::bind( + static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime) { + $byLifetime = []; + $now = microtime(true); + $expiredIds = []; + + foreach ($deferred as $key => $item) { + $key = (string) $key; + if (null === $item->expiry) { + $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0; + } elseif (!$item->expiry) { + $ttl = 0; + } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) { + $expiredIds[] = $getId($key); + continue; + } + if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) { + unset($metadata[CacheItem::METADATA_TAGS]); + } + // For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators + $byLifetime[$ttl][$getId($key)] = $metadata ? ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item->value] : $item->value; + } + + return $byLifetime; + }, + null, + CacheItem::class + ); + } + + /** + * Returns the best possible adapter that your runtime supports. + * + * Using ApcuAdapter makes system caches compatible with read-only filesystems. + * + * @return AdapterInterface + */ + public static function createSystemCache(string $namespace, int $defaultLifetime, string $version, string $directory, LoggerInterface $logger = null) + { + $opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true); + if (null !== $logger) { + $opcache->setLogger($logger); + } + + if (!self::$apcuSupported = self::$apcuSupported ?? ApcuAdapter::isSupported()) { + return $opcache; + } + + if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) { + return $opcache; + } + + $apcu = new ApcuAdapter($namespace, intdiv($defaultLifetime, 5), $version); + if (null !== $logger) { + $apcu->setLogger($logger); + } + + return new ChainAdapter([$apcu, $opcache]); + } + + public static function createConnection(string $dsn, array $options = []) + { + if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) { + return RedisAdapter::createConnection($dsn, $options); + } + if (str_starts_with($dsn, 'memcached:')) { + return MemcachedAdapter::createConnection($dsn, $options); + } + if (0 === strpos($dsn, 'couchbase:')) { + if (CouchbaseBucketAdapter::isSupported()) { + return CouchbaseBucketAdapter::createConnection($dsn, $options); + } + + return CouchbaseCollectionAdapter::createConnection($dsn, $options); + } + + throw new InvalidArgumentException(sprintf('Unsupported DSN: "%s".', $dsn)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + $ok = true; + $byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, \Closure::fromCallable([$this, 'getId']), $this->defaultLifetime); + $retry = $this->deferred = []; + + if ($expiredIds) { + try { + $this->doDelete($expiredIds); + } catch (\Exception $e) { + $ok = false; + CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]); + } + } + foreach ($byLifetime as $lifetime => $values) { + try { + $e = $this->doSave($values, $lifetime); + } catch (\Exception $e) { + } + if (true === $e || [] === $e) { + continue; + } + if (\is_array($e) || 1 === \count($values)) { + foreach (\is_array($e) ? $e : array_keys($values) as $id) { + $ok = false; + $v = $values[$id]; + $type = get_debug_type($v); + $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } + } else { + foreach ($values as $id => $v) { + $retry[$lifetime][] = $id; + } + } + } + + // When bulk-save failed, retry each item individually + foreach ($retry as $lifetime => $ids) { + foreach ($ids as $id) { + try { + $v = $byLifetime[$lifetime][$id]; + $e = $this->doSave([$id => $v], $lifetime); + } catch (\Exception $e) { + } + if (true === $e || [] === $e) { + continue; + } + $ok = false; + $type = get_debug_type($v); + $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } + } + + return $ok; + } +} diff --git a/vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php b/vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php new file mode 100644 index 0000000..a384b16 --- /dev/null +++ b/vendor/symfony/cache/Adapter/AbstractTagAwareAdapter.php @@ -0,0 +1,330 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Log\LoggerAwareInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\AbstractAdapterTrait; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Contracts\Cache\TagAwareCacheInterface; + +/** + * Abstract for native TagAware adapters. + * + * To keep info on tags, the tags are both serialized as part of cache value and provided as tag ids + * to Adapters on operations when needed for storage to doSave(), doDelete() & doInvalidate(). + * + * @author Nicolas Grekas + * @author André Rømcke + * + * @internal + */ +abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface +{ + use AbstractAdapterTrait; + use ContractsTrait; + + private const TAGS_PREFIX = "\0tags\0"; + + protected function __construct(string $namespace = '', int $defaultLifetime = 0) + { + $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':'; + $this->defaultLifetime = $defaultLifetime; + if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) { + throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace)); + } + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $value, $isHit) { + $item = new CacheItem(); + $item->key = $key; + $item->isTaggable = true; + // If structure does not match what we expect return item as is (no value and not a hit) + if (!\is_array($value) || !\array_key_exists('value', $value)) { + return $item; + } + $item->isHit = $isHit; + // Extract value, tags and meta data from the cache value + $item->value = $value['value']; + $item->metadata[CacheItem::METADATA_TAGS] = $value['tags'] ?? []; + if (isset($value['meta'])) { + // For compactness these values are packed, & expiry is offset to reduce size + $v = unpack('Ve/Nc', $value['meta']); + $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; + $item->metadata[CacheItem::METADATA_CTIME] = $v['c']; + } + + return $item; + }, + null, + CacheItem::class + ); + self::$mergeByLifetime ?? self::$mergeByLifetime = \Closure::bind( + static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) { + $byLifetime = []; + $now = microtime(true); + $expiredIds = []; + + foreach ($deferred as $key => $item) { + $key = (string) $key; + if (null === $item->expiry) { + $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0; + } elseif (!$item->expiry) { + $ttl = 0; + } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) { + $expiredIds[] = $getId($key); + continue; + } + // Store Value and Tags on the cache value + if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) { + $value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]]; + unset($metadata[CacheItem::METADATA_TAGS]); + } else { + $value = ['value' => $item->value, 'tags' => []]; + } + + if ($metadata) { + // For compactness, expiry and creation duration are packed, using magic numbers as separators + $value['meta'] = pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME]); + } + + // Extract tag changes, these should be removed from values in doSave() + $value['tag-operations'] = ['add' => [], 'remove' => []]; + $oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? []; + foreach (array_diff($value['tags'], $oldTags) as $addedTag) { + $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag); + } + foreach (array_diff($oldTags, $value['tags']) as $removedTag) { + $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag); + } + + $byLifetime[$ttl][$getId($key)] = $value; + $item->metadata = $item->newMetadata; + } + + return $byLifetime; + }, + null, + CacheItem::class + ); + } + + /** + * Persists several cache items immediately. + * + * @param array $values The values to cache, indexed by their cache identifier + * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning + * @param array[] $addTagData Hash where key is tag id, and array value is list of cache id's to add to tag + * @param array[] $removeTagData Hash where key is tag id, and array value is list of cache id's to remove to tag + * + * @return array The identifiers that failed to be cached or a boolean stating if caching succeeded or not + */ + abstract protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array; + + /** + * Removes multiple items from the pool and their corresponding tags. + * + * @param array $ids An array of identifiers that should be removed from the pool + * + * @return bool + */ + abstract protected function doDelete(array $ids); + + /** + * Removes relations between tags and deleted items. + * + * @param array $tagData Array of tag => key identifiers that should be removed from the pool + */ + abstract protected function doDeleteTagRelations(array $tagData): bool; + + /** + * Invalidates cached items using tags. + * + * @param string[] $tagIds An array of tags to invalidate, key is tag and value is tag id + */ + abstract protected function doInvalidate(array $tagIds): bool; + + /** + * Delete items and yields the tags they were bound to. + */ + protected function doDeleteYieldTags(array $ids): iterable + { + foreach ($this->doFetch($ids) as $id => $value) { + yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : []; + } + + $this->doDelete($ids); + } + + /** + * {@inheritdoc} + */ + public function commit(): bool + { + $ok = true; + $byLifetime = (self::$mergeByLifetime)($this->deferred, $expiredIds, \Closure::fromCallable([$this, 'getId']), self::TAGS_PREFIX, $this->defaultLifetime); + $retry = $this->deferred = []; + + if ($expiredIds) { + // Tags are not cleaned up in this case, however that is done on invalidateTags(). + try { + $this->doDelete($expiredIds); + } catch (\Exception $e) { + $ok = false; + CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]); + } + } + foreach ($byLifetime as $lifetime => $values) { + try { + $values = $this->extractTagData($values, $addTagData, $removeTagData); + $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData); + } catch (\Exception $e) { + } + if (true === $e || [] === $e) { + continue; + } + if (\is_array($e) || 1 === \count($values)) { + foreach (\is_array($e) ? $e : array_keys($values) as $id) { + $ok = false; + $v = $values[$id]; + $type = get_debug_type($v); + $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } + } else { + foreach ($values as $id => $v) { + $retry[$lifetime][] = $id; + } + } + } + + // When bulk-save failed, retry each item individually + foreach ($retry as $lifetime => $ids) { + foreach ($ids as $id) { + try { + $v = $byLifetime[$lifetime][$id]; + $values = $this->extractTagData([$id => $v], $addTagData, $removeTagData); + $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData); + } catch (\Exception $e) { + } + if (true === $e || [] === $e) { + continue; + } + $ok = false; + $type = get_debug_type($v); + $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + public function deleteItems(array $keys): bool + { + if (!$keys) { + return true; + } + + $ok = true; + $ids = []; + $tagData = []; + + foreach ($keys as $key) { + $ids[$key] = $this->getId($key); + unset($this->deferred[$key]); + } + + try { + foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) { + foreach ($tags as $tag) { + $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id; + } + } + } catch (\Exception $e) { + $ok = false; + } + + try { + if ((!$tagData || $this->doDeleteTagRelations($tagData)) && $ok) { + return true; + } + } catch (\Exception $e) { + } + + // When bulk-delete failed, retry each item individually + foreach ($ids as $key => $id) { + try { + $e = null; + if ($this->doDelete([$id])) { + continue; + } + } catch (\Exception $e) { + } + $message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + $ok = false; + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) + { + if (empty($tags)) { + return false; + } + + $tagIds = []; + foreach (array_unique($tags) as $tag) { + $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag); + } + + try { + if ($this->doInvalidate($tagIds)) { + return true; + } + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to invalidate tags: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]); + } + + return false; + } + + /** + * Extracts tags operation data from $values set in mergeByLifetime, and returns values without it. + */ + private function extractTagData(array $values, ?array &$addTagData, ?array &$removeTagData): array + { + $addTagData = $removeTagData = []; + foreach ($values as $id => $value) { + foreach ($value['tag-operations']['add'] as $tag => $tagId) { + $addTagData[$tagId][] = $id; + } + + foreach ($value['tag-operations']['remove'] as $tag => $tagId) { + $removeTagData[$tagId][] = $id; + } + + unset($values[$id]['tag-operations']); + } + + return $values; + } +} diff --git a/vendor/symfony/cache/Adapter/AdapterInterface.php b/vendor/symfony/cache/Adapter/AdapterInterface.php new file mode 100644 index 0000000..f8dce86 --- /dev/null +++ b/vendor/symfony/cache/Adapter/AdapterInterface.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; + +// Help opcache.preload discover always-needed symbols +class_exists(CacheItem::class); + +/** + * Interface for adapters managing instances of Symfony's CacheItem. + * + * @author Kévin Dunglas + */ +interface AdapterInterface extends CacheItemPoolInterface +{ + /** + * {@inheritdoc} + * + * @return CacheItem + */ + public function getItem($key); + + /** + * {@inheritdoc} + * + * @return \Traversable + */ + public function getItems(array $keys = []); + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = ''); +} diff --git a/vendor/symfony/cache/Adapter/ApcuAdapter.php b/vendor/symfony/cache/Adapter/ApcuAdapter.php new file mode 100644 index 0000000..270a139 --- /dev/null +++ b/vendor/symfony/cache/Adapter/ApcuAdapter.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Nicolas Grekas + */ +class ApcuAdapter extends AbstractAdapter +{ + private $marshaller; + + /** + * @throws CacheException if APCu is not enabled + */ + public function __construct(string $namespace = '', int $defaultLifetime = 0, string $version = null, MarshallerInterface $marshaller = null) + { + if (!static::isSupported()) { + throw new CacheException('APCu is not enabled.'); + } + if ('cli' === \PHP_SAPI) { + ini_set('apc.use_request_time', 0); + } + parent::__construct($namespace, $defaultLifetime); + + if (null !== $version) { + CacheItem::validateKey($version); + + if (!apcu_exists($version.'@'.$namespace)) { + $this->doClear($namespace); + apcu_add($version.'@'.$namespace, null); + } + } + + $this->marshaller = $marshaller; + } + + public static function isSupported() + { + return \function_exists('apcu_fetch') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN); + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); + try { + $values = []; + $ids = array_flip($ids); + foreach (apcu_fetch(array_keys($ids), $ok) ?: [] as $k => $v) { + if (!isset($ids[$k])) { + // work around https://github.com/krakjoe/apcu/issues/247 + $k = key($ids); + } + unset($ids[$k]); + + if (null !== $v || $ok) { + $values[$k] = null !== $this->marshaller ? $this->marshaller->unmarshall($v) : $v; + } + } + + return $values; + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + return apcu_exists($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + return isset($namespace[0]) && class_exists(\APCUIterator::class, false) && ('cli' !== \PHP_SAPI || filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) + ? apcu_delete(new \APCUIterator(sprintf('/^%s/', preg_quote($namespace, '/')), \APC_ITER_KEY)) + : apcu_clear_cache(); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + foreach ($ids as $id) { + apcu_delete($id); + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (null !== $this->marshaller && (!$values = $this->marshaller->marshall($values, $failed))) { + return $failed; + } + + try { + if (false === $failures = apcu_store($values, null, $lifetime)) { + $failures = $values; + } + + return array_keys($failures); + } catch (\Throwable $e) { + if (1 === \count($values)) { + // Workaround https://github.com/krakjoe/apcu/issues/170 + apcu_delete(array_key_first($values)); + } + + throw $e; + } + } +} diff --git a/vendor/symfony/cache/Adapter/ArrayAdapter.php b/vendor/symfony/cache/Adapter/ArrayAdapter.php new file mode 100644 index 0000000..d8695b7 --- /dev/null +++ b/vendor/symfony/cache/Adapter/ArrayAdapter.php @@ -0,0 +1,407 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * An in-memory cache storage. + * + * Acts as a least-recently-used (LRU) storage when configured with a maximum number of items. + * + * @author Nicolas Grekas + */ +class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface +{ + use LoggerAwareTrait; + + private $storeSerialized; + private $values = []; + private $expiries = []; + private $defaultLifetime; + private $maxLifetime; + private $maxItems; + + private static $createCacheItem; + + /** + * @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise + */ + public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true, float $maxLifetime = 0, int $maxItems = 0) + { + if (0 > $maxLifetime) { + throw new InvalidArgumentException(sprintf('Argument $maxLifetime must be positive, %F passed.', $maxLifetime)); + } + + if (0 > $maxItems) { + throw new InvalidArgumentException(sprintf('Argument $maxItems must be a positive integer, %d passed.', $maxItems)); + } + + $this->defaultLifetime = $defaultLifetime; + $this->storeSerialized = $storeSerialized; + $this->maxLifetime = $maxLifetime; + $this->maxItems = $maxItems; + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $value, $isHit) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->isHit = $isHit; + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + { + $item = $this->getItem($key); + $metadata = $item->getMetadata(); + + // ArrayAdapter works in memory, we don't care about stampede protection + if (\INF === $beta || !$item->isHit()) { + $save = true; + $item->set($callback($item, $save)); + if ($save) { + $this->save($item); + } + } + + return $item->get(); + } + + /** + * {@inheritdoc} + */ + public function delete(string $key): bool + { + return $this->deleteItem($key); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) { + if ($this->maxItems) { + // Move the item last in the storage + $value = $this->values[$key]; + unset($this->values[$key]); + $this->values[$key] = $value; + } + + return true; + } + \assert('' !== CacheItem::validateKey($key)); + + return isset($this->expiries[$key]) && !$this->deleteItem($key); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + if (!$isHit = $this->hasItem($key)) { + $value = null; + + if (!$this->maxItems) { + // Track misses in non-LRU mode only + $this->values[$key] = null; + } + } else { + $value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key]; + } + + return (self::$createCacheItem)($key, $value, $isHit); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + \assert(self::validateKeys($keys)); + + return $this->generateItems($keys, microtime(true), self::$createCacheItem); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + \assert('' !== CacheItem::validateKey($key)); + unset($this->values[$key], $this->expiries[$key]); + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + foreach ($keys as $key) { + $this->deleteItem($key); + } + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $item = (array) $item; + $key = $item["\0*\0key"]; + $value = $item["\0*\0value"]; + $expiry = $item["\0*\0expiry"]; + + $now = microtime(true); + + if (null !== $expiry) { + if (!$expiry) { + $expiry = \PHP_INT_MAX; + } elseif ($expiry <= $now) { + $this->deleteItem($key); + + return true; + } + } + if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) { + return false; + } + if (null === $expiry && 0 < $this->defaultLifetime) { + $expiry = $this->defaultLifetime; + $expiry = $now + ($expiry > ($this->maxLifetime ?: $expiry) ? $this->maxLifetime : $expiry); + } elseif ($this->maxLifetime && (null === $expiry || $expiry > $now + $this->maxLifetime)) { + $expiry = $now + $this->maxLifetime; + } + + if ($this->maxItems) { + unset($this->values[$key]); + + // Iterate items and vacuum expired ones while we are at it + foreach ($this->values as $k => $v) { + if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) { + break; + } + + unset($this->values[$k], $this->expiries[$k]); + } + } + + $this->values[$key] = $value; + $this->expiries[$key] = $expiry ?? \PHP_INT_MAX; + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + return $this->save($item); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + if ('' !== $prefix) { + $now = microtime(true); + + foreach ($this->values as $key => $value) { + if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || 0 === strpos($key, $prefix)) { + unset($this->values[$key], $this->expiries[$key]); + } + } + + if ($this->values) { + return true; + } + } + + $this->values = $this->expiries = []; + + return true; + } + + /** + * Returns all cached values, with cache miss as null. + * + * @return array + */ + public function getValues() + { + if (!$this->storeSerialized) { + return $this->values; + } + + $values = $this->values; + foreach ($values as $k => $v) { + if (null === $v || 'N;' === $v) { + continue; + } + if (!\is_string($v) || !isset($v[2]) || ':' !== $v[1]) { + $values[$k] = serialize($v); + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function reset() + { + $this->clear(); + } + + private function generateItems(array $keys, float $now, \Closure $f): \Generator + { + foreach ($keys as $i => $key) { + if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > $now || !$this->deleteItem($key))) { + $value = null; + + if (!$this->maxItems) { + // Track misses in non-LRU mode only + $this->values[$key] = null; + } + } else { + if ($this->maxItems) { + // Move the item last in the storage + $value = $this->values[$key]; + unset($this->values[$key]); + $this->values[$key] = $value; + } + + $value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key]; + } + unset($keys[$i]); + + yield $key => $f($key, $value, $isHit); + } + + foreach ($keys as $key) { + yield $key => $f($key, null, false); + } + } + + private function freeze($value, string $key) + { + if (null === $value) { + return 'N;'; + } + if (\is_string($value)) { + // Serialize strings if they could be confused with serialized objects or arrays + if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) { + return serialize($value); + } + } elseif (!\is_scalar($value)) { + try { + $serialized = serialize($value); + } catch (\Exception $e) { + unset($this->values[$key]); + $type = get_debug_type($value); + $message = sprintf('Failed to save key "{key}" of type %s: %s', $type, $e->getMessage()); + CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + + return; + } + // Keep value serialized if it contains any objects or any internal references + if ('C' === $serialized[0] || 'O' === $serialized[0] || preg_match('/;[OCRr]:[1-9]/', $serialized)) { + return $serialized; + } + } + + return $value; + } + + private function unfreeze(string $key, bool &$isHit) + { + if ('N;' === $value = $this->values[$key]) { + return null; + } + if (\is_string($value) && isset($value[2]) && ':' === $value[1]) { + try { + $value = unserialize($value); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to unserialize key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + $value = false; + } + if (false === $value) { + $value = null; + $isHit = false; + + if (!$this->maxItems) { + $this->values[$key] = null; + } + } + } + + return $value; + } + + private function validateKeys(array $keys): bool + { + foreach ($keys as $key) { + if (!\is_string($key) || !isset($this->expiries[$key])) { + CacheItem::validateKey($key); + } + } + + return true; + } +} diff --git a/vendor/symfony/cache/Adapter/ChainAdapter.php b/vendor/symfony/cache/Adapter/ChainAdapter.php new file mode 100644 index 0000000..059c0ed --- /dev/null +++ b/vendor/symfony/cache/Adapter/ChainAdapter.php @@ -0,0 +1,342 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Chains several adapters together. + * + * Cached items are fetched from the first adapter having them in its data store. + * They are saved and deleted in all adapters at once. + * + * @author Kévin Dunglas + */ +class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface +{ + use ContractsTrait; + + private $adapters = []; + private $adapterCount; + private $defaultLifetime; + + private static $syncItem; + + /** + * @param CacheItemPoolInterface[] $adapters The ordered list of adapters used to fetch cached items + * @param int $defaultLifetime The default lifetime of items propagated from lower adapters to upper ones + */ + public function __construct(array $adapters, int $defaultLifetime = 0) + { + if (!$adapters) { + throw new InvalidArgumentException('At least one adapter must be specified.'); + } + + foreach ($adapters as $adapter) { + if (!$adapter instanceof CacheItemPoolInterface) { + throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', get_debug_type($adapter), CacheItemPoolInterface::class)); + } + if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $adapter instanceof ApcuAdapter && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) { + continue; // skip putting APCu in the chain when the backend is disabled + } + + if ($adapter instanceof AdapterInterface) { + $this->adapters[] = $adapter; + } else { + $this->adapters[] = new ProxyAdapter($adapter); + } + } + $this->adapterCount = \count($this->adapters); + $this->defaultLifetime = $defaultLifetime; + + self::$syncItem ?? self::$syncItem = \Closure::bind( + static function ($sourceItem, $item, $defaultLifetime, $sourceMetadata = null) { + $sourceItem->isTaggable = false; + $sourceMetadata = $sourceMetadata ?? $sourceItem->metadata; + unset($sourceMetadata[CacheItem::METADATA_TAGS]); + + $item->value = $sourceItem->value; + $item->isHit = $sourceItem->isHit; + $item->metadata = $item->newMetadata = $sourceItem->metadata = $sourceMetadata; + + if (isset($item->metadata[CacheItem::METADATA_EXPIRY])) { + $item->expiresAt(\DateTime::createFromFormat('U.u', sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY]))); + } elseif (0 < $defaultLifetime) { + $item->expiresAfter($defaultLifetime); + } + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + { + $doSave = true; + $callback = static function (CacheItem $item, bool &$save) use ($callback, &$doSave) { + $value = $callback($item, $save); + $doSave = $save; + + return $value; + }; + + $lastItem = null; + $i = 0; + $wrap = function (CacheItem $item = null, bool &$save = true) use ($key, $callback, $beta, &$wrap, &$i, &$doSave, &$lastItem, &$metadata) { + $adapter = $this->adapters[$i]; + if (isset($this->adapters[++$i])) { + $callback = $wrap; + $beta = \INF === $beta ? \INF : 0; + } + if ($adapter instanceof CacheInterface) { + $value = $adapter->get($key, $callback, $beta, $metadata); + } else { + $value = $this->doGet($adapter, $key, $callback, $beta, $metadata); + } + if (null !== $item) { + (self::$syncItem)($lastItem = $lastItem ?? $item, $item, $this->defaultLifetime, $metadata); + } + $save = $doSave; + + return $value; + }; + + return $wrap(); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $syncItem = self::$syncItem; + $misses = []; + + foreach ($this->adapters as $i => $adapter) { + $item = $adapter->getItem($key); + + if ($item->isHit()) { + while (0 <= --$i) { + $this->adapters[$i]->save($syncItem($item, $misses[$i], $this->defaultLifetime)); + } + + return $item; + } + + $misses[$i] = $item; + } + + return $item; + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + return $this->generateItems($this->adapters[0]->getItems($keys), 0); + } + + private function generateItems(iterable $items, int $adapterIndex): \Generator + { + $missing = []; + $misses = []; + $nextAdapterIndex = $adapterIndex + 1; + $nextAdapter = $this->adapters[$nextAdapterIndex] ?? null; + + foreach ($items as $k => $item) { + if (!$nextAdapter || $item->isHit()) { + yield $k => $item; + } else { + $missing[] = $k; + $misses[$k] = $item; + } + } + + if ($missing) { + $syncItem = self::$syncItem; + $adapter = $this->adapters[$adapterIndex]; + $items = $this->generateItems($nextAdapter->getItems($missing), $nextAdapterIndex); + + foreach ($items as $k => $item) { + if ($item->isHit()) { + $adapter->save($syncItem($item, $misses[$k], $this->defaultLifetime)); + } + + yield $k => $item; + } + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + foreach ($this->adapters as $adapter) { + if ($adapter->hasItem($key)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + $cleared = true; + $i = $this->adapterCount; + + while ($i--) { + if ($this->adapters[$i] instanceof AdapterInterface) { + $cleared = $this->adapters[$i]->clear($prefix) && $cleared; + } else { + $cleared = $this->adapters[$i]->clear() && $cleared; + } + } + + return $cleared; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + $deleted = true; + $i = $this->adapterCount; + + while ($i--) { + $deleted = $this->adapters[$i]->deleteItem($key) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + $deleted = true; + $i = $this->adapterCount; + + while ($i--) { + $deleted = $this->adapters[$i]->deleteItems($keys) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + $saved = true; + $i = $this->adapterCount; + + while ($i--) { + $saved = $this->adapters[$i]->save($item) && $saved; + } + + return $saved; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + $saved = true; + $i = $this->adapterCount; + + while ($i--) { + $saved = $this->adapters[$i]->saveDeferred($item) && $saved; + } + + return $saved; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + $committed = true; + $i = $this->adapterCount; + + while ($i--) { + $committed = $this->adapters[$i]->commit() && $committed; + } + + return $committed; + } + + /** + * {@inheritdoc} + */ + public function prune() + { + $pruned = true; + + foreach ($this->adapters as $adapter) { + if ($adapter instanceof PruneableInterface) { + $pruned = $adapter->prune() && $pruned; + } + } + + return $pruned; + } + + /** + * {@inheritdoc} + */ + public function reset() + { + foreach ($this->adapters as $adapter) { + if ($adapter instanceof ResetInterface) { + $adapter->reset(); + } + } + } +} diff --git a/vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php b/vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php new file mode 100644 index 0000000..36d5249 --- /dev/null +++ b/vendor/symfony/cache/Adapter/CouchbaseBucketAdapter.php @@ -0,0 +1,252 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Antonio Jose Cerezo Aranda + */ +class CouchbaseBucketAdapter extends AbstractAdapter +{ + private const THIRTY_DAYS_IN_SECONDS = 2592000; + private const MAX_KEY_LENGTH = 250; + private const KEY_NOT_FOUND = 13; + private const VALID_DSN_OPTIONS = [ + 'operationTimeout', + 'configTimeout', + 'configNodeTimeout', + 'n1qlTimeout', + 'httpTimeout', + 'configDelay', + 'htconfigIdleTimeout', + 'durabilityInterval', + 'durabilityTimeout', + ]; + + private $bucket; + private $marshaller; + + public function __construct(\CouchbaseBucket $bucket, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) + { + if (!static::isSupported()) { + throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.'); + } + + $this->maxIdLength = static::MAX_KEY_LENGTH; + + $this->bucket = $bucket; + + parent::__construct($namespace, $defaultLifetime); + $this->enableVersioning(); + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + /** + * @param array|string $servers + */ + public static function createConnection($servers, array $options = []): \CouchbaseBucket + { + if (\is_string($servers)) { + $servers = [$servers]; + } elseif (!\is_array($servers)) { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be array or string, "%s" given.', __METHOD__, get_debug_type($servers))); + } + + if (!static::isSupported()) { + throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.'); + } + + set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); }); + + $dsnPattern = '/^(?couchbase(?:s)?)\:\/\/(?:(?[^\:]+)\:(?[^\@]{6,})@)?' + .'(?[^\:]+(?:\:\d+)?)(?:\/(?[^\?]+))(?:\?(?.*))?$/i'; + + $newServers = []; + $protocol = 'couchbase'; + try { + $options = self::initOptions($options); + $username = $options['username']; + $password = $options['password']; + + foreach ($servers as $dsn) { + if (0 !== strpos($dsn, 'couchbase:')) { + throw new InvalidArgumentException(sprintf('Invalid Couchbase DSN: "%s" does not start with "couchbase:".', $dsn)); + } + + preg_match($dsnPattern, $dsn, $matches); + + $username = $matches['username'] ?: $username; + $password = $matches['password'] ?: $password; + $protocol = $matches['protocol'] ?: $protocol; + + if (isset($matches['options'])) { + $optionsInDsn = self::getOptions($matches['options']); + + foreach ($optionsInDsn as $parameter => $value) { + $options[$parameter] = $value; + } + } + + $newServers[] = $matches['host']; + } + + $connectionString = $protocol.'://'.implode(',', $newServers); + + $client = new \CouchbaseCluster($connectionString); + $client->authenticateAs($username, $password); + + $bucket = $client->openBucket($matches['bucketName']); + + unset($options['username'], $options['password']); + foreach ($options as $option => $value) { + if (!empty($value)) { + $bucket->$option = $value; + } + } + + return $bucket; + } finally { + restore_error_handler(); + } + } + + public static function isSupported(): bool + { + return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>=') && version_compare(phpversion('couchbase'), '3.0', '<'); + } + + private static function getOptions(string $options): array + { + $results = []; + $optionsInArray = explode('&', $options); + + foreach ($optionsInArray as $option) { + [$key, $value] = explode('=', $option); + + if (\in_array($key, static::VALID_DSN_OPTIONS, true)) { + $results[$key] = $value; + } + } + + return $results; + } + + private static function initOptions(array $options): array + { + $options['username'] = $options['username'] ?? ''; + $options['password'] = $options['password'] ?? ''; + $options['operationTimeout'] = $options['operationTimeout'] ?? 0; + $options['configTimeout'] = $options['configTimeout'] ?? 0; + $options['configNodeTimeout'] = $options['configNodeTimeout'] ?? 0; + $options['n1qlTimeout'] = $options['n1qlTimeout'] ?? 0; + $options['httpTimeout'] = $options['httpTimeout'] ?? 0; + $options['configDelay'] = $options['configDelay'] ?? 0; + $options['htconfigIdleTimeout'] = $options['htconfigIdleTimeout'] ?? 0; + $options['durabilityInterval'] = $options['durabilityInterval'] ?? 0; + $options['durabilityTimeout'] = $options['durabilityTimeout'] ?? 0; + + return $options; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $resultsCouchbase = $this->bucket->get($ids); + + $results = []; + foreach ($resultsCouchbase as $key => $value) { + if (null !== $value->error) { + continue; + } + $results[$key] = $this->marshaller->unmarshall($value->value); + } + + return $results; + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id): bool + { + return false !== $this->bucket->get($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace): bool + { + if ('' === $namespace) { + $this->bucket->manager()->flush(); + + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids): bool + { + $results = $this->bucket->remove(array_values($ids)); + + foreach ($results as $key => $result) { + if (null !== $result->error && static::KEY_NOT_FOUND !== $result->error->getCode()) { + continue; + } + unset($results[$key]); + } + + return 0 === \count($results); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + $lifetime = $this->normalizeExpiry($lifetime); + + $ko = []; + foreach ($values as $key => $value) { + $result = $this->bucket->upsert($key, $value, ['expiry' => $lifetime]); + + if (null !== $result->error) { + $ko[$key] = $result; + } + } + + return [] === $ko ? true : $ko; + } + + private function normalizeExpiry(int $expiry): int + { + if ($expiry && $expiry > static::THIRTY_DAYS_IN_SECONDS) { + $expiry += time(); + } + + return $expiry; + } +} diff --git a/vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php b/vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php new file mode 100644 index 0000000..79f6485 --- /dev/null +++ b/vendor/symfony/cache/Adapter/CouchbaseCollectionAdapter.php @@ -0,0 +1,222 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Couchbase\Bucket; +use Couchbase\Cluster; +use Couchbase\ClusterOptions; +use Couchbase\Collection; +use Couchbase\DocumentNotFoundException; +use Couchbase\UpsertOptions; +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Antonio Jose Cerezo Aranda + */ +class CouchbaseCollectionAdapter extends AbstractAdapter +{ + private const MAX_KEY_LENGTH = 250; + + /** @var Collection */ + private $connection; + private $marshaller; + + public function __construct(Collection $connection, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) + { + if (!static::isSupported()) { + throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.'); + } + + $this->maxIdLength = static::MAX_KEY_LENGTH; + + $this->connection = $connection; + + parent::__construct($namespace, $defaultLifetime); + $this->enableVersioning(); + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + /** + * @param array|string $dsn + * + * @return Bucket|Collection + */ + public static function createConnection($dsn, array $options = []) + { + if (\is_string($dsn)) { + $dsn = [$dsn]; + } elseif (!\is_array($dsn)) { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be array or string, "%s" given.', __METHOD__, get_debug_type($dsn))); + } + + if (!static::isSupported()) { + throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.'); + } + + set_error_handler(function ($type, $msg, $file, $line): bool { throw new \ErrorException($msg, 0, $type, $file, $line); }); + + $dsnPattern = '/^(?couchbase(?:s)?)\:\/\/(?:(?[^\:]+)\:(?[^\@]{6,})@)?' + .'(?[^\:]+(?:\:\d+)?)(?:\/(?[^\/\?]+))(?:(?:\/(?[^\/]+))' + .'(?:\/(?[^\/\?]+)))?(?:\/)?(?:\?(?.*))?$/i'; + + $newServers = []; + $protocol = 'couchbase'; + try { + $username = $options['username'] ?? ''; + $password = $options['password'] ?? ''; + + foreach ($dsn as $server) { + if (0 !== strpos($server, 'couchbase:')) { + throw new InvalidArgumentException(sprintf('Invalid Couchbase DSN: "%s" does not start with "couchbase:".', $server)); + } + + preg_match($dsnPattern, $server, $matches); + + $username = $matches['username'] ?: $username; + $password = $matches['password'] ?: $password; + $protocol = $matches['protocol'] ?: $protocol; + + if (isset($matches['options'])) { + $optionsInDsn = self::getOptions($matches['options']); + + foreach ($optionsInDsn as $parameter => $value) { + $options[$parameter] = $value; + } + } + + $newServers[] = $matches['host']; + } + + $option = isset($matches['options']) ? '?'.$matches['options'] : ''; + $connectionString = $protocol.'://'.implode(',', $newServers).$option; + + $clusterOptions = new ClusterOptions(); + $clusterOptions->credentials($username, $password); + + $client = new Cluster($connectionString, $clusterOptions); + + $bucket = $client->bucket($matches['bucketName']); + $collection = $bucket->defaultCollection(); + if (!empty($matches['scopeName'])) { + $scope = $bucket->scope($matches['scopeName']); + $collection = $scope->collection($matches['collectionName']); + } + + return $collection; + } finally { + restore_error_handler(); + } + } + + public static function isSupported(): bool + { + return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '3.0.5', '>=') && version_compare(phpversion('couchbase'), '4.0', '<'); + } + + private static function getOptions(string $options): array + { + $results = []; + $optionsInArray = explode('&', $options); + + foreach ($optionsInArray as $option) { + [$key, $value] = explode('=', $option); + + $results[$key] = $value; + } + + return $results; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids): array + { + $results = []; + foreach ($ids as $id) { + try { + $resultCouchbase = $this->connection->get($id); + } catch (DocumentNotFoundException $exception) { + continue; + } + + $content = $resultCouchbase->value ?? $resultCouchbase->content(); + + $results[$id] = $this->marshaller->unmarshall($content); + } + + return $results; + } + + /** + * {@inheritdoc} + */ + protected function doHave($id): bool + { + return $this->connection->exists($id)->exists(); + } + + /** + * {@inheritdoc} + */ + protected function doClear($namespace): bool + { + return false; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids): bool + { + $idsErrors = []; + foreach ($ids as $id) { + try { + $result = $this->connection->remove($id); + + if (null === $result->mutationToken()) { + $idsErrors[] = $id; + } + } catch (DocumentNotFoundException $exception) { + } + } + + return 0 === \count($idsErrors); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + $upsertOptions = new UpsertOptions(); + $upsertOptions->expiry($lifetime); + + $ko = []; + foreach ($values as $key => $value) { + try { + $this->connection->upsert($key, $value, $upsertOptions); + } catch (\Exception $exception) { + $ko[$key] = ''; + } + } + + return [] === $ko ? true : $ko; + } +} diff --git a/vendor/symfony/cache/Adapter/DoctrineAdapter.php b/vendor/symfony/cache/Adapter/DoctrineAdapter.php new file mode 100644 index 0000000..efa30c8 --- /dev/null +++ b/vendor/symfony/cache/Adapter/DoctrineAdapter.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Doctrine\Common\Cache\CacheProvider; +use Doctrine\Common\Cache\Psr6\CacheAdapter; + +/** + * @author Nicolas Grekas + * + * @deprecated Since Symfony 5.4, use Doctrine\Common\Cache\Psr6\CacheAdapter instead + */ +class DoctrineAdapter extends AbstractAdapter +{ + private $provider; + + public function __construct(CacheProvider $provider, string $namespace = '', int $defaultLifetime = 0) + { + trigger_deprecation('symfony/cache', '5.4', '"%s" is deprecated, use "%s" instead.', __CLASS__, CacheAdapter::class); + + parent::__construct('', $defaultLifetime); + $this->provider = $provider; + $provider->setNamespace($namespace); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + parent::reset(); + $this->provider->setNamespace($this->provider->getNamespace()); + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback'); + try { + return $this->provider->fetchMultiple($ids); + } catch (\Error $e) { + $trace = $e->getTrace(); + + if (isset($trace[0]['function']) && !isset($trace[0]['class'])) { + switch ($trace[0]['function']) { + case 'unserialize': + case 'apcu_fetch': + case 'apc_fetch': + throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); + } + } + + throw $e; + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + return $this->provider->contains($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + $namespace = $this->provider->getNamespace(); + + return isset($namespace[0]) + ? $this->provider->deleteAll() + : $this->provider->flushAll(); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $ok = true; + foreach ($ids as $id) { + $ok = $this->provider->delete($id) && $ok; + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + return $this->provider->saveMultiple($values, $lifetime); + } +} diff --git a/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php b/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php new file mode 100644 index 0000000..73f0ea6 --- /dev/null +++ b/vendor/symfony/cache/Adapter/DoctrineDbalAdapter.php @@ -0,0 +1,397 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Driver\ServerInfoAwareConnection; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Exception as DBALException; +use Doctrine\DBAL\Exception\TableNotFoundException; +use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Schema\Schema; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\PruneableInterface; + +class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface +{ + protected $maxIdLength = 255; + + private $marshaller; + private $conn; + private $platformName; + private $serverVersion; + private $table = 'cache_items'; + private $idCol = 'item_id'; + private $dataCol = 'item_data'; + private $lifetimeCol = 'item_lifetime'; + private $timeCol = 'item_time'; + private $namespace; + + /** + * You can either pass an existing database Doctrine DBAL Connection or + * a DSN string that will be used to connect to the database. + * + * The cache table is created automatically when possible. + * Otherwise, use the createTable() method. + * + * List of available options: + * * db_table: The name of the table [default: cache_items] + * * db_id_col: The column where to store the cache id [default: item_id] + * * db_data_col: The column where to store the cache data [default: item_data] + * * db_lifetime_col: The column where to store the lifetime [default: item_lifetime] + * * db_time_col: The column where to store the timestamp [default: item_time] + * + * @param Connection|string $connOrDsn + * + * @throws InvalidArgumentException When namespace contains invalid characters + */ + public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], MarshallerInterface $marshaller = null) + { + if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0])); + } + + if ($connOrDsn instanceof Connection) { + $this->conn = $connOrDsn; + } elseif (\is_string($connOrDsn)) { + if (!class_exists(DriverManager::class)) { + throw new InvalidArgumentException(sprintf('Failed to parse the DSN "%s". Try running "composer require doctrine/dbal".', $connOrDsn)); + } + $this->conn = DriverManager::getConnection(['url' => $connOrDsn]); + } else { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be "%s" or string, "%s" given.', __METHOD__, Connection::class, get_debug_type($connOrDsn))); + } + + $this->table = $options['db_table'] ?? $this->table; + $this->idCol = $options['db_id_col'] ?? $this->idCol; + $this->dataCol = $options['db_data_col'] ?? $this->dataCol; + $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol; + $this->timeCol = $options['db_time_col'] ?? $this->timeCol; + $this->namespace = $namespace; + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + + parent::__construct($namespace, $defaultLifetime); + } + + /** + * Creates the table to store cache items which can be called once for setup. + * + * Cache ID are saved in a column of maximum length 255. Cache data is + * saved in a BLOB. + * + * @throws DBALException When the table already exists + */ + public function createTable() + { + $schema = new Schema(); + $this->addTableToSchema($schema); + + foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) { + $this->conn->executeStatement($sql); + } + } + + /** + * {@inheritdoc} + */ + public function configureSchema(Schema $schema, Connection $forConnection): void + { + // only update the schema for this connection + if ($forConnection !== $this->conn) { + return; + } + + if ($schema->hasTable($this->table)) { + return; + } + + $this->addTableToSchema($schema); + } + + /** + * {@inheritdoc} + */ + public function prune(): bool + { + $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ?"; + $params = [time()]; + $paramTypes = [ParameterType::INTEGER]; + + if ('' !== $this->namespace) { + $deleteSql .= " AND $this->idCol LIKE ?"; + $params[] = sprintf('%s%%', $this->namespace); + $paramTypes[] = ParameterType::STRING; + } + + try { + $this->conn->executeStatement($deleteSql, $params, $paramTypes); + } catch (TableNotFoundException $e) { + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids): iterable + { + $now = time(); + $expired = []; + + $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN (?)"; + $result = $this->conn->executeQuery($sql, [ + $now, + $ids, + ], [ + ParameterType::INTEGER, + Connection::PARAM_STR_ARRAY, + ])->iterateNumeric(); + + foreach ($result as $row) { + if (null === $row[1]) { + $expired[] = $row[0]; + } else { + yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]); + } + } + + if ($expired) { + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN (?)"; + $this->conn->executeStatement($sql, [ + $now, + $expired, + ], [ + ParameterType::INTEGER, + Connection::PARAM_STR_ARRAY, + ]); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id): bool + { + $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = ? AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ?)"; + $result = $this->conn->executeQuery($sql, [ + $id, + time(), + ], [ + ParameterType::STRING, + ParameterType::INTEGER, + ]); + + return (bool) $result->fetchOne(); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace): bool + { + if ('' === $namespace) { + if ('sqlite' === $this->getPlatformName()) { + $sql = "DELETE FROM $this->table"; + } else { + $sql = "TRUNCATE TABLE $this->table"; + } + } else { + $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'"; + } + + try { + $this->conn->executeStatement($sql); + } catch (TableNotFoundException $e) { + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids): bool + { + $sql = "DELETE FROM $this->table WHERE $this->idCol IN (?)"; + try { + $this->conn->executeStatement($sql, [array_values($ids)], [Connection::PARAM_STR_ARRAY]); + } catch (TableNotFoundException $e) { + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + $platformName = $this->getPlatformName(); + $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?)"; + + switch (true) { + case 'mysql' === $platformName: + $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; + break; + case 'oci' === $platformName: + // DUAL is Oracle specific dummy table + $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?"; + break; + case 'sqlsrv' === $platformName && version_compare($this->getServerVersion(), '10', '>='): + // MERGE is only available since SQL Server 2008 and must be terminated by semicolon + // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx + $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; + break; + case 'sqlite' === $platformName: + $sql = 'INSERT OR REPLACE'.substr($insertSql, 6); + break; + case 'pgsql' === $platformName && version_compare($this->getServerVersion(), '9.5', '>='): + $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; + break; + default: + $platformName = null; + $sql = "UPDATE $this->table SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ? WHERE $this->idCol = ?"; + break; + } + + $now = time(); + $lifetime = $lifetime ?: null; + try { + $stmt = $this->conn->prepare($sql); + } catch (TableNotFoundException $e) { + if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + $this->createTable(); + } + $stmt = $this->conn->prepare($sql); + } + + // $id and $data are defined later in the loop. Binding is done by reference, values are read on execution. + if ('sqlsrv' === $platformName || 'oci' === $platformName) { + $stmt->bindParam(1, $id); + $stmt->bindParam(2, $id); + $stmt->bindParam(3, $data, ParameterType::LARGE_OBJECT); + $stmt->bindValue(4, $lifetime, ParameterType::INTEGER); + $stmt->bindValue(5, $now, ParameterType::INTEGER); + $stmt->bindParam(6, $data, ParameterType::LARGE_OBJECT); + $stmt->bindValue(7, $lifetime, ParameterType::INTEGER); + $stmt->bindValue(8, $now, ParameterType::INTEGER); + } elseif (null !== $platformName) { + $stmt->bindParam(1, $id); + $stmt->bindParam(2, $data, ParameterType::LARGE_OBJECT); + $stmt->bindValue(3, $lifetime, ParameterType::INTEGER); + $stmt->bindValue(4, $now, ParameterType::INTEGER); + } else { + $stmt->bindParam(1, $data, ParameterType::LARGE_OBJECT); + $stmt->bindValue(2, $lifetime, ParameterType::INTEGER); + $stmt->bindValue(3, $now, ParameterType::INTEGER); + $stmt->bindParam(4, $id); + + $insertStmt = $this->conn->prepare($insertSql); + $insertStmt->bindParam(1, $id); + $insertStmt->bindParam(2, $data, ParameterType::LARGE_OBJECT); + $insertStmt->bindValue(3, $lifetime, ParameterType::INTEGER); + $insertStmt->bindValue(4, $now, ParameterType::INTEGER); + } + + foreach ($values as $id => $data) { + try { + $rowCount = $stmt->executeStatement(); + } catch (TableNotFoundException $e) { + if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + $this->createTable(); + } + $rowCount = $stmt->executeStatement(); + } + if (null === $platformName && 0 === $rowCount) { + try { + $insertStmt->executeStatement(); + } catch (DBALException $e) { + // A concurrent write won, let it be + } + } + } + + return $failed; + } + + private function getPlatformName(): string + { + if (isset($this->platformName)) { + return $this->platformName; + } + + $platform = $this->conn->getDatabasePlatform(); + + switch (true) { + case $platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform: + case $platform instanceof \Doctrine\DBAL\Platforms\MySQL57Platform: + return $this->platformName = 'mysql'; + + case $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform: + return $this->platformName = 'sqlite'; + + case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform: + case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform: + return $this->platformName = 'pgsql'; + + case $platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform: + return $this->platformName = 'oci'; + + case $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform: + case $platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform: + return $this->platformName = 'sqlsrv'; + + default: + return $this->platformName = \get_class($platform); + } + } + + private function getServerVersion(): string + { + if (isset($this->serverVersion)) { + return $this->serverVersion; + } + + $conn = $this->conn->getWrappedConnection(); + if ($conn instanceof ServerInfoAwareConnection) { + return $this->serverVersion = $conn->getServerVersion(); + } + + return $this->serverVersion = '0'; + } + + private function addTableToSchema(Schema $schema): void + { + $types = [ + 'mysql' => 'binary', + 'sqlite' => 'text', + ]; + + $table = $schema->createTable($this->table); + $table->addColumn($this->idCol, $types[$this->getPlatformName()] ?? 'string', ['length' => 255]); + $table->addColumn($this->dataCol, 'blob', ['length' => 16777215]); + $table->addColumn($this->lifetimeCol, 'integer', ['unsigned' => true, 'notnull' => false]); + $table->addColumn($this->timeCol, 'integer', ['unsigned' => true]); + $table->setPrimaryKey([$this->idCol]); + } +} diff --git a/vendor/symfony/cache/Adapter/FilesystemAdapter.php b/vendor/symfony/cache/Adapter/FilesystemAdapter.php new file mode 100644 index 0000000..7185dd4 --- /dev/null +++ b/vendor/symfony/cache/Adapter/FilesystemAdapter.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Traits\FilesystemTrait; + +class FilesystemAdapter extends AbstractAdapter implements PruneableInterface +{ + use FilesystemTrait; + + public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null) + { + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + parent::__construct('', $defaultLifetime); + $this->init($namespace, $directory); + } +} diff --git a/vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php b/vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php new file mode 100644 index 0000000..afde843 --- /dev/null +++ b/vendor/symfony/cache/Adapter/FilesystemTagAwareAdapter.php @@ -0,0 +1,239 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\Marshaller\TagAwareMarshaller; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Traits\FilesystemTrait; + +/** + * Stores tag id <> cache id relationship as a symlink, and lookup on invalidation calls. + * + * @author Nicolas Grekas + * @author André Rømcke + */ +class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements PruneableInterface +{ + use FilesystemTrait { + doClear as private doClearCache; + doSave as private doSaveCache; + } + + /** + * Folder used for tag symlinks. + */ + private const TAG_FOLDER = 'tags'; + + public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null) + { + $this->marshaller = new TagAwareMarshaller($marshaller); + parent::__construct('', $defaultLifetime); + $this->init($namespace, $directory); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + $ok = $this->doClearCache($namespace); + + if ('' !== $namespace) { + return $ok; + } + + set_error_handler(static function () {}); + $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + try { + foreach ($this->scanHashDir($this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR) as $dir) { + if (rename($dir, $renamed = substr_replace($dir, bin2hex(random_bytes(4)), -8))) { + $dir = $renamed.\DIRECTORY_SEPARATOR; + } else { + $dir .= \DIRECTORY_SEPARATOR; + $renamed = null; + } + + for ($i = 0; $i < 38; ++$i) { + if (!is_dir($dir.$chars[$i])) { + continue; + } + for ($j = 0; $j < 38; ++$j) { + if (!is_dir($d = $dir.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) { + continue; + } + foreach (scandir($d, \SCANDIR_SORT_NONE) ?: [] as $link) { + if ('.' !== $link && '..' !== $link && (null !== $renamed || !realpath($d.\DIRECTORY_SEPARATOR.$link))) { + unlink($d.\DIRECTORY_SEPARATOR.$link); + } + } + null === $renamed ?: rmdir($d); + } + null === $renamed ?: rmdir($dir.$chars[$i]); + } + null === $renamed ?: rmdir($renamed); + } + } finally { + restore_error_handler(); + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array + { + $failed = $this->doSaveCache($values, $lifetime); + + // Add Tags as symlinks + foreach ($addTagData as $tagId => $ids) { + $tagFolder = $this->getTagFolder($tagId); + foreach ($ids as $id) { + if ($failed && \in_array($id, $failed, true)) { + continue; + } + + $file = $this->getFile($id); + + if (!@symlink($file, $tagLink = $this->getFile($id, true, $tagFolder)) && !is_link($tagLink)) { + @unlink($file); + $failed[] = $id; + } + } + } + + // Unlink removed Tags + foreach ($removeTagData as $tagId => $ids) { + $tagFolder = $this->getTagFolder($tagId); + foreach ($ids as $id) { + if ($failed && \in_array($id, $failed, true)) { + continue; + } + + @unlink($this->getFile($id, false, $tagFolder)); + } + } + + return $failed; + } + + /** + * {@inheritdoc} + */ + protected function doDeleteYieldTags(array $ids): iterable + { + foreach ($ids as $id) { + $file = $this->getFile($id); + if (!is_file($file) || !$h = @fopen($file, 'r')) { + continue; + } + + if ((\PHP_VERSION_ID >= 70300 || '\\' !== \DIRECTORY_SEPARATOR) && !@unlink($file)) { + fclose($h); + continue; + } + + $meta = explode("\n", fread($h, 4096), 3)[2] ?? ''; + + // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (13 < \strlen($meta) && "\x9D" === $meta[0] && "\0" === $meta[5] && "\x5F" === $meta[9]) { + $meta[9] = "\0"; + $tagLen = unpack('Nlen', $meta, 9)['len']; + $meta = substr($meta, 13, $tagLen); + + if (0 < $tagLen -= \strlen($meta)) { + $meta .= fread($h, $tagLen); + } + + try { + yield $id => '' === $meta ? [] : $this->marshaller->unmarshall($meta); + } catch (\Exception $e) { + yield $id => []; + } + } + + fclose($h); + + if (\PHP_VERSION_ID < 70300 && '\\' === \DIRECTORY_SEPARATOR) { + @unlink($file); + } + } + } + + /** + * {@inheritdoc} + */ + protected function doDeleteTagRelations(array $tagData): bool + { + foreach ($tagData as $tagId => $idList) { + $tagFolder = $this->getTagFolder($tagId); + foreach ($idList as $id) { + @unlink($this->getFile($id, false, $tagFolder)); + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doInvalidate(array $tagIds): bool + { + foreach ($tagIds as $tagId) { + if (!is_dir($tagFolder = $this->getTagFolder($tagId))) { + continue; + } + + set_error_handler(static function () {}); + + try { + if (rename($tagFolder, $renamed = substr_replace($tagFolder, bin2hex(random_bytes(4)), -9))) { + $tagFolder = $renamed.\DIRECTORY_SEPARATOR; + } else { + $renamed = null; + } + + foreach ($this->scanHashDir($tagFolder) as $itemLink) { + unlink(realpath($itemLink) ?: $itemLink); + unlink($itemLink); + } + + if (null === $renamed) { + continue; + } + + $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + for ($i = 0; $i < 38; ++$i) { + for ($j = 0; $j < 38; ++$j) { + rmdir($tagFolder.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j]); + } + rmdir($tagFolder.$chars[$i]); + } + rmdir($renamed); + } finally { + restore_error_handler(); + } + } + + return true; + } + + private function getTagFolder(string $tagId): string + { + return $this->getFile($tagId, false, $this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR; + } +} diff --git a/vendor/symfony/cache/Adapter/MemcachedAdapter.php b/vendor/symfony/cache/Adapter/MemcachedAdapter.php new file mode 100644 index 0000000..5c2933f --- /dev/null +++ b/vendor/symfony/cache/Adapter/MemcachedAdapter.php @@ -0,0 +1,353 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Rob Frawley 2nd + * @author Nicolas Grekas + */ +class MemcachedAdapter extends AbstractAdapter +{ + /** + * We are replacing characters that are illegal in Memcached keys with reserved characters from + * {@see \Symfony\Contracts\Cache\ItemInterface::RESERVED_CHARACTERS} that are legal in Memcached. + * Note: don’t use {@see \Symfony\Component\Cache\Adapter\AbstractAdapter::NS_SEPARATOR}. + */ + private const RESERVED_MEMCACHED = " \n\r\t\v\f\0"; + private const RESERVED_PSR6 = '@()\{}/'; + + protected $maxIdLength = 250; + + private const DEFAULT_CLIENT_OPTIONS = [ + 'persistent_id' => null, + 'username' => null, + 'password' => null, + \Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_PHP, + ]; + + private $marshaller; + private $client; + private $lazyClient; + + /** + * Using a MemcachedAdapter with a TagAwareAdapter for storing tags is discouraged. + * Using a RedisAdapter is recommended instead. If you cannot do otherwise, be aware that: + * - the Memcached::OPT_BINARY_PROTOCOL must be enabled + * (that's the default when using MemcachedAdapter::createConnection()); + * - tags eviction by Memcached's LRU algorithm will break by-tags invalidation; + * your Memcached memory should be large enough to never trigger LRU. + * + * Using a MemcachedAdapter as a pure items store is fine. + */ + public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) + { + if (!static::isSupported()) { + throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0').' is required.'); + } + if ('Memcached' === \get_class($client)) { + $opt = $client->getOption(\Memcached::OPT_SERIALIZER); + if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) { + throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".'); + } + $this->maxIdLength -= \strlen($client->getOption(\Memcached::OPT_PREFIX_KEY)); + $this->client = $client; + } else { + $this->lazyClient = $client; + } + + parent::__construct($namespace, $defaultLifetime); + $this->enableVersioning(); + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + public static function isSupported() + { + return \extension_loaded('memcached') && version_compare(phpversion('memcached'), \PHP_VERSION_ID >= 80100 ? '3.1.6' : '2.2.0', '>='); + } + + /** + * Creates a Memcached instance. + * + * By default, the binary protocol, no block, and libketama compatible options are enabled. + * + * Examples for servers: + * - 'memcached://user:pass@localhost?weight=33' + * - [['localhost', 11211, 33]] + * + * @param array[]|string|string[] $servers An array of servers, a DSN, or an array of DSNs + * + * @return \Memcached + * + * @throws \ErrorException When invalid options or servers are provided + */ + public static function createConnection($servers, array $options = []) + { + if (\is_string($servers)) { + $servers = [$servers]; + } elseif (!\is_array($servers)) { + throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, "%s" given.', get_debug_type($servers))); + } + if (!static::isSupported()) { + throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0').' is required.'); + } + set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); }); + try { + $options += static::DEFAULT_CLIENT_OPTIONS; + $client = new \Memcached($options['persistent_id']); + $username = $options['username']; + $password = $options['password']; + + // parse any DSN in $servers + foreach ($servers as $i => $dsn) { + if (\is_array($dsn)) { + continue; + } + if (!str_starts_with($dsn, 'memcached:')) { + throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s" does not start with "memcached:".', $dsn)); + } + $params = preg_replace_callback('#^memcached:(//)?(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) { + if (!empty($m[2])) { + [$username, $password] = explode(':', $m[2], 2) + [1 => null]; + } + + return 'file:'.($m[1] ?? ''); + }, $dsn); + if (false === $params = parse_url($params)) { + throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".', $dsn)); + } + $query = $hosts = []; + if (isset($params['query'])) { + parse_str($params['query'], $query); + + if (isset($query['host'])) { + if (!\is_array($hosts = $query['host'])) { + throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".', $dsn)); + } + foreach ($hosts as $host => $weight) { + if (false === $port = strrpos($host, ':')) { + $hosts[$host] = [$host, 11211, (int) $weight]; + } else { + $hosts[$host] = [substr($host, 0, $port), (int) substr($host, 1 + $port), (int) $weight]; + } + } + $hosts = array_values($hosts); + unset($query['host']); + } + if ($hosts && !isset($params['host']) && !isset($params['path'])) { + unset($servers[$i]); + $servers = array_merge($servers, $hosts); + continue; + } + } + if (!isset($params['host']) && !isset($params['path'])) { + throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: "%s".', $dsn)); + } + if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) { + $params['weight'] = $m[1]; + $params['path'] = substr($params['path'], 0, -\strlen($m[0])); + } + $params += [ + 'host' => $params['host'] ?? $params['path'], + 'port' => isset($params['host']) ? 11211 : null, + 'weight' => 0, + ]; + if ($query) { + $params += $query; + $options = $query + $options; + } + + $servers[$i] = [$params['host'], $params['port'], $params['weight']]; + + if ($hosts) { + $servers = array_merge($servers, $hosts); + } + } + + // set client's options + unset($options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']); + $options = array_change_key_case($options, \CASE_UPPER); + $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + $client->setOption(\Memcached::OPT_NO_BLOCK, true); + $client->setOption(\Memcached::OPT_TCP_NODELAY, true); + if (!\array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !\array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) { + $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true); + } + foreach ($options as $name => $value) { + if (\is_int($name)) { + continue; + } + if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) { + $value = \constant('Memcached::'.$name.'_'.strtoupper($value)); + } + unset($options[$name]); + + if (\defined('Memcached::OPT_'.$name)) { + $options[\constant('Memcached::OPT_'.$name)] = $value; + } + } + $client->setOptions($options); + + // set client's servers, taking care of persistent connections + if (!$client->isPristine()) { + $oldServers = []; + foreach ($client->getServerList() as $server) { + $oldServers[] = [$server['host'], $server['port']]; + } + + $newServers = []; + foreach ($servers as $server) { + if (1 < \count($server)) { + $server = array_values($server); + unset($server[2]); + $server[1] = (int) $server[1]; + } + $newServers[] = $server; + } + + if ($oldServers !== $newServers) { + $client->resetServerList(); + $client->addServers($servers); + } + } else { + $client->addServers($servers); + } + + if (null !== $username || null !== $password) { + if (!method_exists($client, 'setSaslAuthData')) { + trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.'); + } + $client->setSaslAuthData($username, $password); + } + + return $client; + } finally { + restore_error_handler(); + } + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + if ($lifetime && $lifetime > 30 * 86400) { + $lifetime += time(); + } + + $encodedValues = []; + foreach ($values as $key => $value) { + $encodedValues[self::encodeKey($key)] = $value; + } + + return $this->checkResultCode($this->getClient()->setMulti($encodedValues, $lifetime)) ? $failed : false; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + try { + $encodedIds = array_map([__CLASS__, 'encodeKey'], $ids); + + $encodedResult = $this->checkResultCode($this->getClient()->getMulti($encodedIds)); + + $result = []; + foreach ($encodedResult as $key => $value) { + $result[self::decodeKey($key)] = $this->marshaller->unmarshall($value); + } + + return $result; + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + return false !== $this->getClient()->get(self::encodeKey($id)) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode()); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $ok = true; + $encodedIds = array_map([__CLASS__, 'encodeKey'], $ids); + foreach ($this->checkResultCode($this->getClient()->deleteMulti($encodedIds)) as $result) { + if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) { + $ok = false; + } + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + return '' === $namespace && $this->getClient()->flush(); + } + + private function checkResultCode($result) + { + $code = $this->client->getResultCode(); + + if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) { + return $result; + } + + throw new CacheException('MemcachedAdapter client error: '.strtolower($this->client->getResultMessage())); + } + + private function getClient(): \Memcached + { + if ($this->client) { + return $this->client; + } + + $opt = $this->lazyClient->getOption(\Memcached::OPT_SERIALIZER); + if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) { + throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".'); + } + if ('' !== $prefix = (string) $this->lazyClient->getOption(\Memcached::OPT_PREFIX_KEY)) { + throw new CacheException(sprintf('MemcachedAdapter: "prefix_key" option must be empty when using proxified connections, "%s" given.', $prefix)); + } + + return $this->client = $this->lazyClient; + } + + private static function encodeKey(string $key): string + { + return strtr($key, self::RESERVED_MEMCACHED, self::RESERVED_PSR6); + } + + private static function decodeKey(string $key): string + { + return strtr($key, self::RESERVED_PSR6, self::RESERVED_MEMCACHED); + } +} diff --git a/vendor/symfony/cache/Adapter/NullAdapter.php b/vendor/symfony/cache/Adapter/NullAdapter.php new file mode 100644 index 0000000..15f7f8c --- /dev/null +++ b/vendor/symfony/cache/Adapter/NullAdapter.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * @author Titouan Galopin + */ +class NullAdapter implements AdapterInterface, CacheInterface +{ + private static $createCacheItem; + + public function __construct() + { + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key) { + $item = new CacheItem(); + $item->key = $key; + $item->isHit = false; + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + { + $save = true; + + return $callback((self::$createCacheItem)($key), $save); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + return (self::$createCacheItem)($key); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + return $this->generateItems($keys); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + return false; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function delete(string $key): bool + { + return $this->deleteItem($key); + } + + private function generateItems(array $keys): \Generator + { + $f = self::$createCacheItem; + + foreach ($keys as $key) { + yield $key => $f($key); + } + } +} diff --git a/vendor/symfony/cache/Adapter/ParameterNormalizer.php b/vendor/symfony/cache/Adapter/ParameterNormalizer.php new file mode 100644 index 0000000..e33ae9f --- /dev/null +++ b/vendor/symfony/cache/Adapter/ParameterNormalizer.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +/** + * @author Lars Strojny + */ +final class ParameterNormalizer +{ + public static function normalizeDuration(string $duration): int + { + if (is_numeric($duration)) { + return $duration; + } + + if (false !== $time = strtotime($duration, 0)) { + return $time; + } + + try { + return \DateTime::createFromFormat('U', 0)->add(new \DateInterval($duration))->getTimestamp(); + } catch (\Exception $e) { + throw new \InvalidArgumentException(sprintf('Cannot parse date interval "%s".', $duration), 0, $e); + } + } +} diff --git a/vendor/symfony/cache/Adapter/PdoAdapter.php b/vendor/symfony/cache/Adapter/PdoAdapter.php new file mode 100644 index 0000000..5d10724 --- /dev/null +++ b/vendor/symfony/cache/Adapter/PdoAdapter.php @@ -0,0 +1,583 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Schema\Schema; +use Psr\Cache\CacheItemInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\PruneableInterface; + +class PdoAdapter extends AbstractAdapter implements PruneableInterface +{ + protected $maxIdLength = 255; + + private $marshaller; + private $conn; + private $dsn; + private $driver; + private $serverVersion; + private $table = 'cache_items'; + private $idCol = 'item_id'; + private $dataCol = 'item_data'; + private $lifetimeCol = 'item_lifetime'; + private $timeCol = 'item_time'; + private $username = ''; + private $password = ''; + private $connectionOptions = []; + private $namespace; + + private $dbalAdapter; + + /** + * You can either pass an existing database connection as PDO instance or + * a DSN string that will be used to lazy-connect to the database when the + * cache is actually used. + * + * List of available options: + * * db_table: The name of the table [default: cache_items] + * * db_id_col: The column where to store the cache id [default: item_id] + * * db_data_col: The column where to store the cache data [default: item_data] + * * db_lifetime_col: The column where to store the lifetime [default: item_lifetime] + * * db_time_col: The column where to store the timestamp [default: item_time] + * * db_username: The username when lazy-connect [default: ''] + * * db_password: The password when lazy-connect [default: ''] + * * db_connection_options: An array of driver-specific connection options [default: []] + * + * @param \PDO|string $connOrDsn + * + * @throws InvalidArgumentException When first argument is not PDO nor Connection nor string + * @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION + * @throws InvalidArgumentException When namespace contains invalid characters + */ + public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], MarshallerInterface $marshaller = null) + { + if ($connOrDsn instanceof Connection || (\is_string($connOrDsn) && str_contains($connOrDsn, '://'))) { + trigger_deprecation('symfony/cache', '5.4', 'Usage of a DBAL Connection with "%s" is deprecated and will be removed in symfony 6.0. Use "%s" instead.', __CLASS__, DoctrineDbalAdapter::class); + $this->dbalAdapter = new DoctrineDbalAdapter($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller); + + return; + } + + if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0])); + } + + if ($connOrDsn instanceof \PDO) { + if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { + throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__)); + } + + $this->conn = $connOrDsn; + } elseif (\is_string($connOrDsn)) { + $this->dsn = $connOrDsn; + } else { + throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, get_debug_type($connOrDsn))); + } + + $this->table = $options['db_table'] ?? $this->table; + $this->idCol = $options['db_id_col'] ?? $this->idCol; + $this->dataCol = $options['db_data_col'] ?? $this->dataCol; + $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol; + $this->timeCol = $options['db_time_col'] ?? $this->timeCol; + $this->username = $options['db_username'] ?? $this->username; + $this->password = $options['db_password'] ?? $this->password; + $this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions; + $this->namespace = $namespace; + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + + parent::__construct($namespace, $defaultLifetime); + } + + /** + * {@inheritDoc} + */ + public function getItem($key) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->getItem($key); + } + + return parent::getItem($key); + } + + /** + * {@inheritDoc} + */ + public function getItems(array $keys = []) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->getItems($keys); + } + + return parent::getItems($keys); + } + + /** + * {@inheritDoc} + */ + public function hasItem($key) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->hasItem($key); + } + + return parent::hasItem($key); + } + + /** + * {@inheritDoc} + */ + public function deleteItem($key) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->deleteItem($key); + } + + return parent::deleteItem($key); + } + + /** + * {@inheritDoc} + */ + public function deleteItems(array $keys) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->deleteItems($keys); + } + + return parent::deleteItems($keys); + } + + /** + * {@inheritDoc} + */ + public function clear(string $prefix = '') + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->clear($prefix); + } + + return parent::clear($prefix); + } + + /** + * {@inheritDoc} + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->get($key, $callback, $beta, $metadata); + } + + return parent::get($key, $callback, $beta, $metadata); + } + + /** + * {@inheritDoc} + */ + public function delete(string $key): bool + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->delete($key); + } + + return parent::delete($key); + } + + /** + * {@inheritDoc} + */ + public function save(CacheItemInterface $item) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->save($item); + } + + return parent::save($item); + } + + /** + * {@inheritDoc} + */ + public function saveDeferred(CacheItemInterface $item) + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->saveDeferred($item); + } + + return parent::saveDeferred($item); + } + + /** + * {@inheritDoc} + */ + public function setLogger(LoggerInterface $logger): void + { + if (isset($this->dbalAdapter)) { + $this->dbalAdapter->setLogger($logger); + + return; + } + + parent::setLogger($logger); + } + + /** + * {@inheritDoc} + */ + public function commit() + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->commit(); + } + + return parent::commit(); + } + + /** + * {@inheritDoc} + */ + public function reset() + { + if (isset($this->dbalAdapter)) { + $this->dbalAdapter->reset(); + + return; + } + + parent::reset(); + } + + /** + * Creates the table to store cache items which can be called once for setup. + * + * Cache ID are saved in a column of maximum length 255. Cache data is + * saved in a BLOB. + * + * @throws \PDOException When the table already exists + * @throws \DomainException When an unsupported PDO driver is used + */ + public function createTable() + { + if (isset($this->dbalAdapter)) { + $this->dbalAdapter->createTable(); + + return; + } + + // connect if we are not yet + $conn = $this->getConnection(); + + switch ($this->driver) { + case 'mysql': + // We use varbinary for the ID column because it prevents unwanted conversions: + // - character set conversions between server and client + // - trailing space removal + // - case-insensitivity + // - language processing like é == e + $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB"; + break; + case 'sqlite': + $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + case 'pgsql': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + case 'oci': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + case 'sqlsrv': + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)"; + break; + default: + throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver)); + } + + $conn->exec($sql); + } + + /** + * Adds the Table to the Schema if the adapter uses this Connection. + * + * @deprecated since symfony/cache 5.4 use DoctrineDbalAdapter instead + */ + public function configureSchema(Schema $schema, Connection $forConnection): void + { + if (isset($this->dbalAdapter)) { + $this->dbalAdapter->configureSchema($schema, $forConnection); + } + } + + /** + * {@inheritdoc} + */ + public function prune() + { + if (isset($this->dbalAdapter)) { + return $this->dbalAdapter->prune(); + } + + $deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= :time"; + + if ('' !== $this->namespace) { + $deleteSql .= " AND $this->idCol LIKE :namespace"; + } + + $connection = $this->getConnection(); + + try { + $delete = $connection->prepare($deleteSql); + } catch (\PDOException $e) { + return true; + } + $delete->bindValue(':time', time(), \PDO::PARAM_INT); + + if ('' !== $this->namespace) { + $delete->bindValue(':namespace', sprintf('%s%%', $this->namespace), \PDO::PARAM_STR); + } + try { + return $delete->execute(); + } catch (\PDOException $e) { + return true; + } + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $connection = $this->getConnection(); + + $now = time(); + $expired = []; + + $sql = str_pad('', (\count($ids) << 1) - 1, '?,'); + $sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)"; + $stmt = $connection->prepare($sql); + $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT); + foreach ($ids as $id) { + $stmt->bindValue(++$i, $id); + } + $result = $stmt->execute(); + + if (\is_object($result)) { + $result = $result->iterateNumeric(); + } else { + $stmt->setFetchMode(\PDO::FETCH_NUM); + $result = $stmt; + } + + foreach ($result as $row) { + if (null === $row[1]) { + $expired[] = $row[0]; + } else { + yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]); + } + } + + if ($expired) { + $sql = str_pad('', (\count($expired) << 1) - 1, '?,'); + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)"; + $stmt = $connection->prepare($sql); + $stmt->bindValue($i = 1, $now, \PDO::PARAM_INT); + foreach ($expired as $id) { + $stmt->bindValue(++$i, $id); + } + $stmt->execute(); + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + $connection = $this->getConnection(); + + $sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)"; + $stmt = $connection->prepare($sql); + + $stmt->bindValue(':id', $id); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->execute(); + + return (bool) $stmt->fetchColumn(); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + $conn = $this->getConnection(); + + if ('' === $namespace) { + if ('sqlite' === $this->driver) { + $sql = "DELETE FROM $this->table"; + } else { + $sql = "TRUNCATE TABLE $this->table"; + } + } else { + $sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'"; + } + + try { + $conn->exec($sql); + } catch (\PDOException $e) { + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $sql = str_pad('', (\count($ids) << 1) - 1, '?,'); + $sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)"; + try { + $stmt = $this->getConnection()->prepare($sql); + $stmt->execute(array_values($ids)); + } catch (\PDOException $e) { + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + $conn = $this->getConnection(); + + $driver = $this->driver; + $insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"; + + switch (true) { + case 'mysql' === $driver: + $sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; + break; + case 'oci' === $driver: + // DUAL is Oracle specific dummy table + $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?"; + break; + case 'sqlsrv' === $driver && version_compare($this->getServerVersion(), '10', '>='): + // MERGE is only available since SQL Server 2008 and must be terminated by semicolon + // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx + $sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; + break; + case 'sqlite' === $driver: + $sql = 'INSERT OR REPLACE'.substr($insertSql, 6); + break; + case 'pgsql' === $driver && version_compare($this->getServerVersion(), '9.5', '>='): + $sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; + break; + default: + $driver = null; + $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"; + break; + } + + $now = time(); + $lifetime = $lifetime ?: null; + try { + $stmt = $conn->prepare($sql); + } catch (\PDOException $e) { + if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + $this->createTable(); + } + $stmt = $conn->prepare($sql); + } + + // $id and $data are defined later in the loop. Binding is done by reference, values are read on execution. + if ('sqlsrv' === $driver || 'oci' === $driver) { + $stmt->bindParam(1, $id); + $stmt->bindParam(2, $id); + $stmt->bindParam(3, $data, \PDO::PARAM_LOB); + $stmt->bindValue(4, $lifetime, \PDO::PARAM_INT); + $stmt->bindValue(5, $now, \PDO::PARAM_INT); + $stmt->bindParam(6, $data, \PDO::PARAM_LOB); + $stmt->bindValue(7, $lifetime, \PDO::PARAM_INT); + $stmt->bindValue(8, $now, \PDO::PARAM_INT); + } else { + $stmt->bindParam(':id', $id); + $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT); + $stmt->bindValue(':time', $now, \PDO::PARAM_INT); + } + if (null === $driver) { + $insertStmt = $conn->prepare($insertSql); + + $insertStmt->bindParam(':id', $id); + $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $insertStmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT); + $insertStmt->bindValue(':time', $now, \PDO::PARAM_INT); + } + + foreach ($values as $id => $data) { + try { + $stmt->execute(); + } catch (\PDOException $e) { + if (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true)) { + $this->createTable(); + } + $stmt->execute(); + } + if (null === $driver && !$stmt->rowCount()) { + try { + $insertStmt->execute(); + } catch (\PDOException $e) { + // A concurrent write won, let it be + } + } + } + + return $failed; + } + + private function getConnection(): \PDO + { + if (null === $this->conn) { + $this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions); + $this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + } + if (null === $this->driver) { + $this->driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME); + } + + return $this->conn; + } + + private function getServerVersion(): string + { + if (null === $this->serverVersion) { + $this->serverVersion = $this->conn->getAttribute(\PDO::ATTR_SERVER_VERSION); + } + + return $this->serverVersion; + } +} diff --git a/vendor/symfony/cache/Adapter/PhpArrayAdapter.php b/vendor/symfony/cache/Adapter/PhpArrayAdapter.php new file mode 100644 index 0000000..8c8fb91 --- /dev/null +++ b/vendor/symfony/cache/Adapter/PhpArrayAdapter.php @@ -0,0 +1,435 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Component\Cache\Traits\ProxyTrait; +use Symfony\Component\VarExporter\VarExporter; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0. + * Warmed up items are read-only and run-time discovered items are cached using a fallback adapter. + * + * @author Titouan Galopin + * @author Nicolas Grekas + */ +class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface +{ + use ContractsTrait; + use ProxyTrait; + + private $file; + private $keys; + private $values; + + private static $createCacheItem; + private static $valuesCache = []; + + /** + * @param string $file The PHP file were values are cached + * @param AdapterInterface $fallbackPool A pool to fallback on when an item is not hit + */ + public function __construct(string $file, AdapterInterface $fallbackPool) + { + $this->file = $file; + $this->pool = $fallbackPool; + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $value, $isHit) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->isHit = $isHit; + + return $item; + }, + null, + CacheItem::class + ); + } + + /** + * This adapter takes advantage of how PHP stores arrays in its latest versions. + * + * @param string $file The PHP file were values are cached + * @param CacheItemPoolInterface $fallbackPool A pool to fallback on when an item is not hit + * + * @return CacheItemPoolInterface + */ + public static function create(string $file, CacheItemPoolInterface $fallbackPool) + { + if (!$fallbackPool instanceof AdapterInterface) { + $fallbackPool = new ProxyAdapter($fallbackPool); + } + + return new static($file, $fallbackPool); + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + { + if (null === $this->values) { + $this->initialize(); + } + if (!isset($this->keys[$key])) { + get_from_pool: + if ($this->pool instanceof CacheInterface) { + return $this->pool->get($key, $callback, $beta, $metadata); + } + + return $this->doGet($this->pool, $key, $callback, $beta, $metadata); + } + $value = $this->values[$this->keys[$key]]; + + if ('N;' === $value) { + return null; + } + try { + if ($value instanceof \Closure) { + return $value(); + } + } catch (\Throwable $e) { + unset($this->keys[$key]); + goto get_from_pool; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + if (null === $this->values) { + $this->initialize(); + } + if (!isset($this->keys[$key])) { + return $this->pool->getItem($key); + } + + $value = $this->values[$this->keys[$key]]; + $isHit = true; + + if ('N;' === $value) { + $value = null; + } elseif ($value instanceof \Closure) { + try { + $value = $value(); + } catch (\Throwable $e) { + $value = null; + $isHit = false; + } + } + + return (self::$createCacheItem)($key, $value, $isHit); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + foreach ($keys as $key) { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + } + if (null === $this->values) { + $this->initialize(); + } + + return $this->generateItems($keys); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + if (null === $this->values) { + $this->initialize(); + } + + return isset($this->keys[$key]) || $this->pool->hasItem($key); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->keys[$key]) && $this->pool->deleteItem($key); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + $deleted = true; + $fallbackKeys = []; + + foreach ($keys as $key) { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + + if (isset($this->keys[$key])) { + $deleted = false; + } else { + $fallbackKeys[] = $key; + } + } + if (null === $this->values) { + $this->initialize(); + } + + if ($fallbackKeys) { + $deleted = $this->pool->deleteItems($fallbackKeys) && $deleted; + } + + return $deleted; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->keys[$item->getKey()]) && $this->pool->save($item); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + if (null === $this->values) { + $this->initialize(); + } + + return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + return $this->pool->commit(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + $this->keys = $this->values = []; + + $cleared = @unlink($this->file) || !file_exists($this->file); + unset(self::$valuesCache[$this->file]); + + if ($this->pool instanceof AdapterInterface) { + return $this->pool->clear($prefix) && $cleared; + } + + return $this->pool->clear() && $cleared; + } + + /** + * Store an array of cached values. + * + * @param array $values The cached values + * + * @return string[] A list of classes to preload on PHP 7.4+ + */ + public function warmUp(array $values) + { + if (file_exists($this->file)) { + if (!is_file($this->file)) { + throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: "%s".', $this->file)); + } + + if (!is_writable($this->file)) { + throw new InvalidArgumentException(sprintf('Cache file is not writable: "%s".', $this->file)); + } + } else { + $directory = \dirname($this->file); + + if (!is_dir($directory) && !@mkdir($directory, 0777, true)) { + throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: "%s".', $directory)); + } + + if (!is_writable($directory)) { + throw new InvalidArgumentException(sprintf('Cache directory is not writable: "%s".', $directory)); + } + } + + $preload = []; + $dumpedValues = ''; + $dumpedMap = []; + $dump = <<<'EOF' + $value) { + CacheItem::validateKey(\is_int($key) ? (string) $key : $key); + $isStaticValue = true; + + if (null === $value) { + $value = "'N;'"; + } elseif (\is_object($value) || \is_array($value)) { + try { + $value = VarExporter::export($value, $isStaticValue, $preload); + } catch (\Exception $e) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e); + } + } elseif (\is_string($value)) { + // Wrap "N;" in a closure to not confuse it with an encoded `null` + if ('N;' === $value) { + $isStaticValue = false; + } + $value = var_export($value, true); + } elseif (!\is_scalar($value)) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value))); + } else { + $value = var_export($value, true); + } + + if (!$isStaticValue) { + $value = str_replace("\n", "\n ", $value); + $value = "static function () {\n return {$value};\n}"; + } + $hash = hash('md5', $value); + + if (null === $id = $dumpedMap[$hash] ?? null) { + $id = $dumpedMap[$hash] = \count($dumpedMap); + $dumpedValues .= "{$id} => {$value},\n"; + } + + $dump .= var_export($key, true)." => {$id},\n"; + } + + $dump .= "\n], [\n\n{$dumpedValues}\n]];\n"; + + $tmpFile = uniqid($this->file, true); + + file_put_contents($tmpFile, $dump); + @chmod($tmpFile, 0666 & ~umask()); + unset($serialized, $value, $dump); + + @rename($tmpFile, $this->file); + unset(self::$valuesCache[$this->file]); + + $this->initialize(); + + return $preload; + } + + /** + * Load the cache file. + */ + private function initialize() + { + if (isset(self::$valuesCache[$this->file])) { + $values = self::$valuesCache[$this->file]; + } elseif (!is_file($this->file)) { + $this->keys = $this->values = []; + + return; + } else { + $values = self::$valuesCache[$this->file] = (include $this->file) ?: [[], []]; + } + + if (2 !== \count($values) || !isset($values[0], $values[1])) { + $this->keys = $this->values = []; + } else { + [$this->keys, $this->values] = $values; + } + } + + private function generateItems(array $keys): \Generator + { + $f = self::$createCacheItem; + $fallbackKeys = []; + + foreach ($keys as $key) { + if (isset($this->keys[$key])) { + $value = $this->values[$this->keys[$key]]; + + if ('N;' === $value) { + yield $key => $f($key, null, true); + } elseif ($value instanceof \Closure) { + try { + yield $key => $f($key, $value(), true); + } catch (\Throwable $e) { + yield $key => $f($key, null, false); + } + } else { + yield $key => $f($key, $value, true); + } + } else { + $fallbackKeys[] = $key; + } + } + + if ($fallbackKeys) { + yield from $this->pool->getItems($fallbackKeys); + } + } +} diff --git a/vendor/symfony/cache/Adapter/PhpFilesAdapter.php b/vendor/symfony/cache/Adapter/PhpFilesAdapter.php new file mode 100644 index 0000000..2fb0837 --- /dev/null +++ b/vendor/symfony/cache/Adapter/PhpFilesAdapter.php @@ -0,0 +1,330 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\Traits\FilesystemCommonTrait; +use Symfony\Component\VarExporter\VarExporter; + +/** + * @author Piotr Stankowski + * @author Nicolas Grekas + * @author Rob Frawley 2nd + */ +class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface +{ + use FilesystemCommonTrait { + doClear as private doCommonClear; + doDelete as private doCommonDelete; + } + + private $includeHandler; + private $appendOnly; + private $values = []; + private $files = []; + + private static $startTime; + private static $valuesCache = []; + + /** + * @param $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire. + * Doing so is encouraged because it fits perfectly OPcache's memory model. + * + * @throws CacheException if OPcache is not enabled + */ + public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, bool $appendOnly = false) + { + $this->appendOnly = $appendOnly; + self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time(); + parent::__construct('', $defaultLifetime); + $this->init($namespace, $directory); + $this->includeHandler = static function ($type, $msg, $file, $line) { + throw new \ErrorException($msg, 0, $type, $file, $line); + }; + } + + public static function isSupported() + { + self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time(); + + return \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN)); + } + + /** + * @return bool + */ + public function prune() + { + $time = time(); + $pruned = true; + $getExpiry = true; + + set_error_handler($this->includeHandler); + try { + foreach ($this->scanHashDir($this->directory) as $file) { + try { + if (\is_array($expiresAt = include $file)) { + $expiresAt = $expiresAt[0]; + } + } catch (\ErrorException $e) { + $expiresAt = $time; + } + + if ($time >= $expiresAt) { + $pruned = $this->doUnlink($file) && !file_exists($file) && $pruned; + } + } + } finally { + restore_error_handler(); + } + + return $pruned; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + if ($this->appendOnly) { + $now = 0; + $missingIds = []; + } else { + $now = time(); + $missingIds = $ids; + $ids = []; + } + $values = []; + + begin: + $getExpiry = false; + + foreach ($ids as $id) { + if (null === $value = $this->values[$id] ?? null) { + $missingIds[] = $id; + } elseif ('N;' === $value) { + $values[$id] = null; + } elseif (!\is_object($value)) { + $values[$id] = $value; + } elseif (!$value instanceof LazyValue) { + $values[$id] = $value(); + } elseif (false === $values[$id] = include $value->file) { + unset($values[$id], $this->values[$id]); + $missingIds[] = $id; + } + if (!$this->appendOnly) { + unset($this->values[$id]); + } + } + + if (!$missingIds) { + return $values; + } + + set_error_handler($this->includeHandler); + try { + $getExpiry = true; + + foreach ($missingIds as $k => $id) { + try { + $file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id); + + if (isset(self::$valuesCache[$file])) { + [$expiresAt, $this->values[$id]] = self::$valuesCache[$file]; + } elseif (\is_array($expiresAt = include $file)) { + if ($this->appendOnly) { + self::$valuesCache[$file] = $expiresAt; + } + + [$expiresAt, $this->values[$id]] = $expiresAt; + } elseif ($now < $expiresAt) { + $this->values[$id] = new LazyValue($file); + } + + if ($now >= $expiresAt) { + unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]); + } + } catch (\ErrorException $e) { + unset($missingIds[$k]); + } + } + } finally { + restore_error_handler(); + } + + $ids = $missingIds; + $missingIds = []; + goto begin; + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + if ($this->appendOnly && isset($this->values[$id])) { + return true; + } + + set_error_handler($this->includeHandler); + try { + $file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id); + $getExpiry = true; + + if (isset(self::$valuesCache[$file])) { + [$expiresAt, $value] = self::$valuesCache[$file]; + } elseif (\is_array($expiresAt = include $file)) { + if ($this->appendOnly) { + self::$valuesCache[$file] = $expiresAt; + } + + [$expiresAt, $value] = $expiresAt; + } elseif ($this->appendOnly) { + $value = new LazyValue($file); + } + } catch (\ErrorException $e) { + return false; + } finally { + restore_error_handler(); + } + if ($this->appendOnly) { + $now = 0; + $this->values[$id] = $value; + } else { + $now = time(); + } + + return $now < $expiresAt; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + $ok = true; + $expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX'; + $allowCompile = self::isSupported(); + + foreach ($values as $key => $value) { + unset($this->values[$key]); + $isStaticValue = true; + if (null === $value) { + $value = "'N;'"; + } elseif (\is_object($value) || \is_array($value)) { + try { + $value = VarExporter::export($value, $isStaticValue); + } catch (\Exception $e) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e); + } + } elseif (\is_string($value)) { + // Wrap "N;" in a closure to not confuse it with an encoded `null` + if ('N;' === $value) { + $isStaticValue = false; + } + $value = var_export($value, true); + } elseif (!\is_scalar($value)) { + throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value))); + } else { + $value = var_export($value, true); + } + + $encodedKey = rawurlencode($key); + + if ($isStaticValue) { + $value = "return [{$expiry}, {$value}];"; + } elseif ($this->appendOnly) { + $value = "return [{$expiry}, static function () { return {$value}; }];"; + } else { + // We cannot use a closure here because of https://bugs.php.net/76982 + $value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value); + $value = "namespace Symfony\Component\VarExporter\Internal;\n\nreturn \$getExpiry ? {$expiry} : {$value};"; + } + + $file = $this->files[$key] = $this->getFile($key, true); + // Since OPcache only compiles files older than the script execution start, set the file's mtime in the past + $ok = $this->write($file, "directory)) { + throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory)); + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + $this->values = []; + + return $this->doCommonClear($namespace); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + foreach ($ids as $id) { + unset($this->values[$id]); + } + + return $this->doCommonDelete($ids); + } + + protected function doUnlink(string $file) + { + unset(self::$valuesCache[$file]); + + if (self::isSupported()) { + @opcache_invalidate($file, true); + } + + return @unlink($file); + } + + private function getFileKey(string $file): string + { + if (!$h = @fopen($file, 'r')) { + return ''; + } + + $encodedKey = substr(fgets($h), 8); + fclose($h); + + return rawurldecode(rtrim($encodedKey)); + } +} + +/** + * @internal + */ +class LazyValue +{ + public $file; + + public function __construct(string $file) + { + $this->file = $file; + } +} diff --git a/vendor/symfony/cache/Adapter/ProxyAdapter.php b/vendor/symfony/cache/Adapter/ProxyAdapter.php new file mode 100644 index 0000000..c715cad --- /dev/null +++ b/vendor/symfony/cache/Adapter/ProxyAdapter.php @@ -0,0 +1,268 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Component\Cache\Traits\ProxyTrait; +use Symfony\Contracts\Cache\CacheInterface; + +/** + * @author Nicolas Grekas + */ +class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface +{ + use ContractsTrait; + use ProxyTrait; + + private $namespace = ''; + private $namespaceLen; + private $poolHash; + private $defaultLifetime; + + private static $createCacheItem; + private static $setInnerItem; + + public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0) + { + $this->pool = $pool; + $this->poolHash = $poolHash = spl_object_hash($pool); + if ('' !== $namespace) { + \assert('' !== CacheItem::validateKey($namespace)); + $this->namespace = $namespace; + } + $this->namespaceLen = \strlen($namespace); + $this->defaultLifetime = $defaultLifetime; + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $innerItem, $poolHash) { + $item = new CacheItem(); + $item->key = $key; + + if (null === $innerItem) { + return $item; + } + + $item->value = $v = $innerItem->get(); + $item->isHit = $innerItem->isHit(); + $item->innerItem = $innerItem; + $item->poolHash = $poolHash; + + // Detect wrapped values that encode for their expiry and creation duration + // For compactness, these values are packed in the key of an array using + // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { + $item->value = $v[$k]; + $v = unpack('Ve/Nc', substr($k, 1, -1)); + $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; + $item->metadata[CacheItem::METADATA_CTIME] = $v['c']; + } elseif ($innerItem instanceof CacheItem) { + $item->metadata = $innerItem->metadata; + } + $innerItem->set(null); + + return $item; + }, + null, + CacheItem::class + ); + self::$setInnerItem ?? self::$setInnerItem = \Closure::bind( + /** + * @param array $item A CacheItem cast to (array); accessing protected properties requires adding the "\0*\0" PHP prefix + */ + static function (CacheItemInterface $innerItem, array $item) { + // Tags are stored separately, no need to account for them when considering this item's newly set metadata + if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) { + unset($metadata[CacheItem::METADATA_TAGS]); + } + if ($metadata) { + // For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators + $item["\0*\0value"] = ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item["\0*\0value"]]; + } + $innerItem->set($item["\0*\0value"]); + $innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U.u', sprintf('%.6F', $item["\0*\0expiry"])) : null); + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + { + if (!$this->pool instanceof CacheInterface) { + return $this->doGet($this, $key, $callback, $beta, $metadata); + } + + return $this->pool->get($this->getId($key), function ($innerItem, bool &$save) use ($key, $callback) { + $item = (self::$createCacheItem)($key, $innerItem, $this->poolHash); + $item->set($value = $callback($item, $save)); + (self::$setInnerItem)($innerItem, (array) $item); + + return $value; + }, $beta, $metadata); + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $item = $this->pool->getItem($this->getId($key)); + + return (self::$createCacheItem)($key, $item, $this->poolHash); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + if ($this->namespaceLen) { + foreach ($keys as $i => $key) { + $keys[$i] = $this->getId($key); + } + } + + return $this->generateItems($this->pool->getItems($keys)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + return $this->pool->hasItem($this->getId($key)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + if ($this->pool instanceof AdapterInterface) { + return $this->pool->clear($this->namespace.$prefix); + } + + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + return $this->pool->deleteItem($this->getId($key)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + if ($this->namespaceLen) { + foreach ($keys as $i => $key) { + $keys[$i] = $this->getId($key); + } + } + + return $this->pool->deleteItems($keys); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + return $this->doSave($item, __FUNCTION__); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + return $this->doSave($item, __FUNCTION__); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + return $this->pool->commit(); + } + + private function doSave(CacheItemInterface $item, string $method) + { + if (!$item instanceof CacheItem) { + return false; + } + $item = (array) $item; + if (null === $item["\0*\0expiry"] && 0 < $this->defaultLifetime) { + $item["\0*\0expiry"] = microtime(true) + $this->defaultLifetime; + } + + if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) { + $innerItem = $item["\0*\0innerItem"]; + } elseif ($this->pool instanceof AdapterInterface) { + // this is an optimization specific for AdapterInterface implementations + // so we can save a round-trip to the backend by just creating a new item + $innerItem = (self::$createCacheItem)($this->namespace.$item["\0*\0key"], null, $this->poolHash); + } else { + $innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]); + } + + (self::$setInnerItem)($innerItem, $item); + + return $this->pool->$method($innerItem); + } + + private function generateItems(iterable $items): \Generator + { + $f = self::$createCacheItem; + + foreach ($items as $key => $item) { + if ($this->namespaceLen) { + $key = substr($key, $this->namespaceLen); + } + + yield $key => $f($key, $item, $this->poolHash); + } + } + + private function getId($key): string + { + \assert('' !== CacheItem::validateKey($key)); + + return $this->namespace.$key; + } +} diff --git a/vendor/symfony/cache/Adapter/Psr16Adapter.php b/vendor/symfony/cache/Adapter/Psr16Adapter.php new file mode 100644 index 0000000..a56aa39 --- /dev/null +++ b/vendor/symfony/cache/Adapter/Psr16Adapter.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ProxyTrait; + +/** + * Turns a PSR-16 cache into a PSR-6 one. + * + * @author Nicolas Grekas + */ +class Psr16Adapter extends AbstractAdapter implements PruneableInterface, ResettableInterface +{ + use ProxyTrait; + + /** + * @internal + */ + protected const NS_SEPARATOR = '_'; + + private $miss; + + public function __construct(CacheInterface $pool, string $namespace = '', int $defaultLifetime = 0) + { + parent::__construct($namespace, $defaultLifetime); + + $this->pool = $pool; + $this->miss = new \stdClass(); + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) { + if ($this->miss !== $value) { + yield $key => $value; + } + } + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + return $this->pool->has($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + return $this->pool->deleteMultiple($ids); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime); + } +} diff --git a/vendor/symfony/cache/Adapter/RedisAdapter.php b/vendor/symfony/cache/Adapter/RedisAdapter.php new file mode 100644 index 0000000..eb5950e --- /dev/null +++ b/vendor/symfony/cache/Adapter/RedisAdapter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\Traits\RedisClusterProxy; +use Symfony\Component\Cache\Traits\RedisProxy; +use Symfony\Component\Cache\Traits\RedisTrait; + +class RedisAdapter extends AbstractAdapter +{ + use RedisTrait; + + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis The redis client + * @param string $namespace The default namespace + * @param int $defaultLifetime The default lifetime + */ + public function __construct($redis, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) + { + $this->init($redis, $namespace, $defaultLifetime, $marshaller); + } +} diff --git a/vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php b/vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php new file mode 100644 index 0000000..865491e --- /dev/null +++ b/vendor/symfony/cache/Adapter/RedisTagAwareAdapter.php @@ -0,0 +1,325 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Predis\Connection\Aggregate\ClusterInterface; +use Predis\Connection\Aggregate\PredisCluster; +use Predis\Connection\Aggregate\ReplicationInterface; +use Predis\Response\ErrorInterface; +use Predis\Response\Status; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Exception\LogicException; +use Symfony\Component\Cache\Marshaller\DeflateMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; +use Symfony\Component\Cache\Marshaller\TagAwareMarshaller; +use Symfony\Component\Cache\Traits\RedisClusterProxy; +use Symfony\Component\Cache\Traits\RedisProxy; +use Symfony\Component\Cache\Traits\RedisTrait; + +/** + * Stores tag id <> cache id relationship as a Redis Set. + * + * Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even + * if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache + * relationship survives eviction (cache cleanup when Redis runs out of memory). + * + * Redis server 2.8+ with any `volatile-*` eviction policy, OR `noeviction` if you're sure memory will NEVER fill up + * + * Design limitations: + * - Max 4 billion cache keys per cache tag as limited by Redis Set datatype. + * E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 4 billion cache items also. + * + * @see https://redis.io/topics/lru-cache#eviction-policies Documentation for Redis eviction policies. + * @see https://redis.io/topics/data-types#sets Documentation for Redis Set datatype. + * + * @author Nicolas Grekas + * @author André Rømcke + */ +class RedisTagAwareAdapter extends AbstractTagAwareAdapter +{ + use RedisTrait; + + /** + * On cache items without a lifetime set, we set it to 100 days. This is to make sure cache items are + * preferred to be evicted over tag Sets, if eviction policy is configured according to requirements. + */ + private const DEFAULT_CACHE_TTL = 8640000; + + /** + * @var string|null detected eviction policy used on Redis server + */ + private $redisEvictionPolicy; + private $namespace; + + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis The redis client + * @param string $namespace The default namespace + * @param int $defaultLifetime The default lifetime + */ + public function __construct($redis, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) + { + if ($redis instanceof \Predis\ClientInterface && $redis->getConnection() instanceof ClusterInterface && !$redis->getConnection() instanceof PredisCluster) { + throw new InvalidArgumentException(sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, get_debug_type($redis->getConnection()))); + } + + if (\defined('Redis::OPT_COMPRESSION') && ($redis instanceof \Redis || $redis instanceof \RedisArray || $redis instanceof \RedisCluster)) { + $compression = $redis->getOption(\Redis::OPT_COMPRESSION); + + foreach (\is_array($compression) ? $compression : [$compression] as $c) { + if (\Redis::COMPRESSION_NONE !== $c) { + throw new InvalidArgumentException(sprintf('phpredis compression must be disabled when using "%s", use "%s" instead.', static::class, DeflateMarshaller::class)); + } + } + } + + $this->init($redis, $namespace, $defaultLifetime, new TagAwareMarshaller($marshaller)); + $this->namespace = $namespace; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime, array $addTagData = [], array $delTagData = []): array + { + $eviction = $this->getRedisEvictionPolicy(); + if ('noeviction' !== $eviction && !str_starts_with($eviction, 'volatile-')) { + throw new LogicException(sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.', $eviction)); + } + + // serialize values + if (!$serialized = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + // While pipeline isn't supported on RedisCluster, other setups will at least benefit from doing this in one op + $results = $this->pipeline(static function () use ($serialized, $lifetime, $addTagData, $delTagData, $failed) { + // Store cache items, force a ttl if none is set, as there is no MSETEX we need to set each one + foreach ($serialized as $id => $value) { + yield 'setEx' => [ + $id, + 0 >= $lifetime ? self::DEFAULT_CACHE_TTL : $lifetime, + $value, + ]; + } + + // Add and Remove Tags + foreach ($addTagData as $tagId => $ids) { + if (!$failed || $ids = array_diff($ids, $failed)) { + yield 'sAdd' => array_merge([$tagId], $ids); + } + } + + foreach ($delTagData as $tagId => $ids) { + if (!$failed || $ids = array_diff($ids, $failed)) { + yield 'sRem' => array_merge([$tagId], $ids); + } + } + }); + + foreach ($results as $id => $result) { + // Skip results of SADD/SREM operations, they'll be 1 or 0 depending on if set value already existed or not + if (is_numeric($result)) { + continue; + } + // setEx results + if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) { + $failed[] = $id; + } + } + + return $failed; + } + + /** + * {@inheritdoc} + */ + protected function doDeleteYieldTags(array $ids): iterable + { + $lua = <<<'EOLUA' + local v = redis.call('GET', KEYS[1]) + local e = redis.pcall('UNLINK', KEYS[1]) + + if type(e) ~= 'number' then + redis.call('DEL', KEYS[1]) + end + + if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then + return '' + end + + return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536) +EOLUA; + + $results = $this->pipeline(function () use ($ids, $lua) { + foreach ($ids as $id) { + yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id] : [$lua, [$id], 1]; + } + }); + + foreach ($results as $id => $result) { + if ($result instanceof \RedisException || $result instanceof ErrorInterface) { + CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $result]); + + continue; + } + + try { + yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result); + } catch (\Exception $e) { + yield $id => []; + } + } + } + + /** + * {@inheritdoc} + */ + protected function doDeleteTagRelations(array $tagData): bool + { + $results = $this->pipeline(static function () use ($tagData) { + foreach ($tagData as $tagId => $idList) { + array_unshift($idList, $tagId); + yield 'sRem' => $idList; + } + }); + foreach ($results as $result) { + // no-op + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doInvalidate(array $tagIds): bool + { + // This script scans the set of items linked to tag: it empties the set + // and removes the linked items. When the set is still not empty after + // the scan, it means we're in cluster mode and that the linked items + // are on other nodes: we move the links to a temporary set and we + // gargage collect that set from the client side. + + $lua = <<<'EOLUA' + redis.replicate_commands() + + local cursor = '0' + local id = KEYS[1] + repeat + local result = redis.call('SSCAN', id, cursor, 'COUNT', 5000); + cursor = result[1]; + local rems = {} + + for _, v in ipairs(result[2]) do + local ok, _ = pcall(redis.call, 'DEL', ARGV[1]..v) + if ok then + table.insert(rems, v) + end + end + if 0 < #rems then + redis.call('SREM', id, unpack(rems)) + end + until '0' == cursor; + + redis.call('SUNIONSTORE', '{'..id..'}'..id, id) + redis.call('DEL', id) + + return redis.call('SSCAN', '{'..id..'}'..id, '0', 'COUNT', 5000) +EOLUA; + + $results = $this->pipeline(function () use ($tagIds, $lua) { + if ($this->redis instanceof \Predis\ClientInterface) { + $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : ''; + } elseif (\is_array($prefix = $this->redis->getOption(\Redis::OPT_PREFIX) ?? '')) { + $prefix = current($prefix); + } + + foreach ($tagIds as $id) { + yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id, $prefix] : [$lua, [$id, $prefix], 1]; + } + }); + + $lua = <<<'EOLUA' + redis.replicate_commands() + + local id = KEYS[1] + local cursor = table.remove(ARGV) + redis.call('SREM', '{'..id..'}'..id, unpack(ARGV)) + + return redis.call('SSCAN', '{'..id..'}'..id, cursor, 'COUNT', 5000) +EOLUA; + + $success = true; + foreach ($results as $id => $values) { + if ($values instanceof \RedisException || $values instanceof ErrorInterface) { + CacheItem::log($this->logger, 'Failed to invalidate key "{key}": '.$values->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $values]); + $success = false; + + continue; + } + + [$cursor, $ids] = $values; + + while ($ids || '0' !== $cursor) { + $this->doDelete($ids); + + $evalArgs = [$id, $cursor]; + array_splice($evalArgs, 1, 0, $ids); + + if ($this->redis instanceof \Predis\ClientInterface) { + array_unshift($evalArgs, $lua, 1); + } else { + $evalArgs = [$lua, $evalArgs, 1]; + } + + $results = $this->pipeline(function () use ($evalArgs) { + yield 'eval' => $evalArgs; + }); + + foreach ($results as [$cursor, $ids]) { + // no-op + } + } + } + + return $success; + } + + private function getRedisEvictionPolicy(): string + { + if (null !== $this->redisEvictionPolicy) { + return $this->redisEvictionPolicy; + } + + $hosts = $this->getHosts(); + $host = reset($hosts); + if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) { + // Predis supports info command only on the master in replication environments + $hosts = [$host->getClientFor('master')]; + } + + foreach ($hosts as $host) { + $info = $host->info('Memory'); + + if ($info instanceof ErrorInterface) { + continue; + } + + $info = $info['Memory'] ?? $info; + + return $this->redisEvictionPolicy = $info['maxmemory_policy']; + } + + return $this->redisEvictionPolicy = ''; + } +} diff --git a/vendor/symfony/cache/Adapter/TagAwareAdapter.php b/vendor/symfony/cache/Adapter/TagAwareAdapter.php new file mode 100644 index 0000000..ff22e5a --- /dev/null +++ b/vendor/symfony/cache/Adapter/TagAwareAdapter.php @@ -0,0 +1,428 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Psr\Cache\InvalidArgumentException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Component\Cache\Traits\ContractsTrait; +use Symfony\Component\Cache\Traits\ProxyTrait; +use Symfony\Contracts\Cache\TagAwareCacheInterface; + +/** + * @author Nicolas Grekas + */ +class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface, LoggerAwareInterface +{ + use ContractsTrait; + use LoggerAwareTrait; + use ProxyTrait; + + public const TAGS_PREFIX = "\0tags\0"; + + private $deferred = []; + private $tags; + private $knownTagVersions = []; + private $knownTagVersionsTtl; + + private static $createCacheItem; + private static $setCacheItemTags; + private static $getTagsByKey; + private static $saveTags; + + public function __construct(AdapterInterface $itemsPool, AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15) + { + $this->pool = $itemsPool; + $this->tags = $tagsPool ?: $itemsPool; + $this->knownTagVersionsTtl = $knownTagVersionsTtl; + self::$createCacheItem ?? self::$createCacheItem = \Closure::bind( + static function ($key, $value, CacheItem $protoItem) { + $item = new CacheItem(); + $item->key = $key; + $item->value = $value; + $item->expiry = $protoItem->expiry; + $item->poolHash = $protoItem->poolHash; + + return $item; + }, + null, + CacheItem::class + ); + self::$setCacheItemTags ?? self::$setCacheItemTags = \Closure::bind( + static function (CacheItem $item, $key, array &$itemTags) { + $item->isTaggable = true; + if (!$item->isHit) { + return $item; + } + if (isset($itemTags[$key])) { + foreach ($itemTags[$key] as $tag => $version) { + $item->metadata[CacheItem::METADATA_TAGS][$tag] = $tag; + } + unset($itemTags[$key]); + } else { + $item->value = null; + $item->isHit = false; + } + + return $item; + }, + null, + CacheItem::class + ); + self::$getTagsByKey ?? self::$getTagsByKey = \Closure::bind( + static function ($deferred) { + $tagsByKey = []; + foreach ($deferred as $key => $item) { + $tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? []; + $item->metadata = $item->newMetadata; + } + + return $tagsByKey; + }, + null, + CacheItem::class + ); + self::$saveTags ?? self::$saveTags = \Closure::bind( + static function (AdapterInterface $tagsAdapter, array $tags) { + ksort($tags); + + foreach ($tags as $v) { + $v->expiry = 0; + $tagsAdapter->saveDeferred($v); + } + + return $tagsAdapter->commit(); + }, + null, + CacheItem::class + ); + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) + { + $ids = []; + foreach ($tags as $tag) { + \assert('' !== CacheItem::validateKey($tag)); + unset($this->knownTagVersions[$tag]); + $ids[] = $tag.static::TAGS_PREFIX; + } + + return !$tags || $this->tags->deleteItems($ids); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + if (\is_string($key) && isset($this->deferred[$key])) { + $this->commit(); + } + + if (!$this->pool->hasItem($key)) { + return false; + } + + $itemTags = $this->pool->getItem(static::TAGS_PREFIX.$key); + + if (!$itemTags->isHit()) { + return false; + } + + if (!$itemTags = $itemTags->get()) { + return true; + } + + foreach ($this->getTagVersions([$itemTags]) as $tag => $version) { + if ($itemTags[$tag] !== $version) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + foreach ($this->getItems([$key]) as $item) { + return $item; + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + $tagKeys = []; + $commit = false; + + foreach ($keys as $key) { + if ('' !== $key && \is_string($key)) { + $commit = $commit || isset($this->deferred[$key]); + $key = static::TAGS_PREFIX.$key; + $tagKeys[$key] = $key; + } + } + + if ($commit) { + $this->commit(); + } + + try { + $items = $this->pool->getItems($tagKeys + $keys); + } catch (InvalidArgumentException $e) { + $this->pool->getItems($keys); // Should throw an exception + + throw $e; + } + + return $this->generateItems($items, $tagKeys); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + if ('' !== $prefix) { + foreach ($this->deferred as $key => $item) { + if (str_starts_with($key, $prefix)) { + unset($this->deferred[$key]); + } + } + } else { + $this->deferred = []; + } + + if ($this->pool instanceof AdapterInterface) { + return $this->pool->clear($prefix); + } + + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + return $this->deleteItems([$key]); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + foreach ($keys as $key) { + if ('' !== $key && \is_string($key)) { + $keys[] = static::TAGS_PREFIX.$key; + } + } + + return $this->pool->deleteItems($keys); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return $this->commit(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return true; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + if (!$this->deferred) { + return true; + } + + $ok = true; + foreach ($this->deferred as $key => $item) { + if (!$this->pool->saveDeferred($item)) { + unset($this->deferred[$key]); + $ok = false; + } + } + + $items = $this->deferred; + $tagsByKey = (self::$getTagsByKey)($items); + $this->deferred = []; + + $tagVersions = $this->getTagVersions($tagsByKey); + $f = self::$createCacheItem; + + foreach ($tagsByKey as $key => $tags) { + $this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key])); + } + + return $this->pool->commit() && $ok; + } + + /** + * @return array + */ + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->commit(); + } + + private function generateItems(iterable $items, array $tagKeys): \Generator + { + $bufferedItems = $itemTags = []; + $f = self::$setCacheItemTags; + + foreach ($items as $key => $item) { + if (!$tagKeys) { + yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags); + continue; + } + if (!isset($tagKeys[$key])) { + $bufferedItems[$key] = $item; + continue; + } + + unset($tagKeys[$key]); + + if ($item->isHit()) { + $itemTags[$key] = $item->get() ?: []; + } + + if (!$tagKeys) { + $tagVersions = $this->getTagVersions($itemTags); + + foreach ($itemTags as $key => $tags) { + foreach ($tags as $tag => $version) { + if ($tagVersions[$tag] !== $version) { + unset($itemTags[$key]); + continue 2; + } + } + } + $tagVersions = $tagKeys = null; + + foreach ($bufferedItems as $key => $item) { + yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags); + } + $bufferedItems = null; + } + } + } + + private function getTagVersions(array $tagsByKey) + { + $tagVersions = []; + $fetchTagVersions = false; + + foreach ($tagsByKey as $tags) { + $tagVersions += $tags; + + foreach ($tags as $tag => $version) { + if ($tagVersions[$tag] !== $version) { + unset($this->knownTagVersions[$tag]); + } + } + } + + if (!$tagVersions) { + return []; + } + + $now = microtime(true); + $tags = []; + foreach ($tagVersions as $tag => $version) { + $tags[$tag.static::TAGS_PREFIX] = $tag; + if ($fetchTagVersions || ($this->knownTagVersions[$tag][1] ?? null) !== $version || $now - $this->knownTagVersions[$tag][0] >= $this->knownTagVersionsTtl) { + // reuse previously fetched tag versions up to the ttl + $fetchTagVersions = true; + } + } + + if (!$fetchTagVersions) { + return $tagVersions; + } + + $newTags = []; + $newVersion = null; + foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) { + if (!$version->isHit()) { + $newTags[$tag] = $version->set($newVersion ?? $newVersion = random_int(\PHP_INT_MIN, \PHP_INT_MAX)); + } + $tagVersions[$tag = $tags[$tag]] = $version->get(); + $this->knownTagVersions[$tag] = [$now, $tagVersions[$tag]]; + } + + if ($newTags) { + (self::$saveTags)($this->tags, $newTags); + } + + return $tagVersions; + } +} diff --git a/vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php b/vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php new file mode 100644 index 0000000..afa18d3 --- /dev/null +++ b/vendor/symfony/cache/Adapter/TagAwareAdapterInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\InvalidArgumentException; + +/** + * Interface for invalidating cached items using tags. + * + * @author Nicolas Grekas + */ +interface TagAwareAdapterInterface extends AdapterInterface +{ + /** + * Invalidates cached items using tags. + * + * @param string[] $tags An array of tags to invalidate + * + * @return bool + * + * @throws InvalidArgumentException When $tags is not valid + */ + public function invalidateTags(array $tags); +} diff --git a/vendor/symfony/cache/Adapter/TraceableAdapter.php b/vendor/symfony/cache/Adapter/TraceableAdapter.php new file mode 100644 index 0000000..4b06557 --- /dev/null +++ b/vendor/symfony/cache/Adapter/TraceableAdapter.php @@ -0,0 +1,295 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Psr\Cache\CacheItemInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\Cache\ResettableInterface; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * An adapter that collects data about all cache calls. + * + * @author Aaron Scherer + * @author Tobias Nyholm + * @author Nicolas Grekas + */ +class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface +{ + protected $pool; + private $calls = []; + + public function __construct(AdapterInterface $pool) + { + $this->pool = $pool; + } + + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + { + if (!$this->pool instanceof CacheInterface) { + throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class)); + } + + $isHit = true; + $callback = function (CacheItem $item, bool &$save) use ($callback, &$isHit) { + $isHit = $item->isHit(); + + return $callback($item, $save); + }; + + $event = $this->start(__FUNCTION__); + try { + $value = $this->pool->get($key, $callback, $beta, $metadata); + $event->result[$key] = get_debug_type($value); + } finally { + $event->end = microtime(true); + } + if ($isHit) { + ++$event->hits; + } else { + ++$event->misses; + } + + return $value; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $event = $this->start(__FUNCTION__); + try { + $item = $this->pool->getItem($key); + } finally { + $event->end = microtime(true); + } + if ($event->result[$key] = $item->isHit()) { + ++$event->hits; + } else { + ++$event->misses; + } + + return $item; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->hasItem($key); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->deleteItem($key); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$item->getKey()] = $this->pool->save($item); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$item->getKey()] = $this->pool->saveDeferred($item); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + $event = $this->start(__FUNCTION__); + try { + $result = $this->pool->getItems($keys); + } finally { + $event->end = microtime(true); + } + $f = function () use ($result, $event) { + $event->result = []; + foreach ($result as $key => $item) { + if ($event->result[$key] = $item->isHit()) { + ++$event->hits; + } else { + ++$event->misses; + } + yield $key => $item; + } + }; + + return $f(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + $event = $this->start(__FUNCTION__); + try { + if ($this->pool instanceof AdapterInterface) { + return $event->result = $this->pool->clear($prefix); + } + + return $event->result = $this->pool->clear(); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + $event = $this->start(__FUNCTION__); + $event->result['keys'] = $keys; + try { + return $event->result['result'] = $this->pool->deleteItems($keys); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function commit() + { + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->commit(); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function prune() + { + if (!$this->pool instanceof PruneableInterface) { + return false; + } + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->prune(); + } finally { + $event->end = microtime(true); + } + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->pool instanceof ResetInterface) { + $this->pool->reset(); + } + + $this->clearCalls(); + } + + /** + * {@inheritdoc} + */ + public function delete(string $key): bool + { + $event = $this->start(__FUNCTION__); + try { + return $event->result[$key] = $this->pool->deleteItem($key); + } finally { + $event->end = microtime(true); + } + } + + public function getCalls() + { + return $this->calls; + } + + public function clearCalls() + { + $this->calls = []; + } + + protected function start(string $name) + { + $this->calls[] = $event = new TraceableAdapterEvent(); + $event->name = $name; + $event->start = microtime(true); + + return $event; + } +} + +class TraceableAdapterEvent +{ + public $name; + public $start; + public $end; + public $result; + public $hits = 0; + public $misses = 0; +} diff --git a/vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php b/vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php new file mode 100644 index 0000000..69461b8 --- /dev/null +++ b/vendor/symfony/cache/Adapter/TraceableTagAwareAdapter.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Adapter; + +use Symfony\Contracts\Cache\TagAwareCacheInterface; + +/** + * @author Robin Chalas + */ +class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface +{ + public function __construct(TagAwareAdapterInterface $pool) + { + parent::__construct($pool); + } + + /** + * {@inheritdoc} + */ + public function invalidateTags(array $tags) + { + $event = $this->start(__FUNCTION__); + try { + return $event->result = $this->pool->invalidateTags($tags); + } finally { + $event->end = microtime(true); + } + } +} diff --git a/vendor/symfony/cache/CHANGELOG.md b/vendor/symfony/cache/CHANGELOG.md new file mode 100644 index 0000000..60a8627 --- /dev/null +++ b/vendor/symfony/cache/CHANGELOG.md @@ -0,0 +1,108 @@ +CHANGELOG +========= + +5.4 +--- + + * Deprecate `DoctrineProvider` and `DoctrineAdapter` because these classes have been added to the `doctrine/cache` package + * Add `DoctrineDbalAdapter` identical to `PdoAdapter` for `Doctrine\DBAL\Connection` or DBAL URL + * Deprecate usage of `PdoAdapter` with `Doctrine\DBAL\Connection` or DBAL URL + +5.3 +--- + + * added support for connecting to Redis Sentinel clusters when using the Redis PHP extension + * add support for a custom serializer to the `ApcuAdapter` class + +5.2.0 +----- + + * added integration with Messenger to allow computing cached values in a worker + * allow ISO 8601 time intervals to specify default lifetime + +5.1.0 +----- + + * added max-items + LRU + max-lifetime capabilities to `ArrayCache` + * added `CouchbaseBucketAdapter` + * added context `cache-adapter` to log messages + +5.0.0 +----- + + * removed all PSR-16 implementations in the `Simple` namespace + * removed `SimpleCacheAdapter` + * removed `AbstractAdapter::unserialize()` + * removed `CacheItem::getPreviousTags()` + +4.4.0 +----- + + * added support for connecting to Redis Sentinel clusters + * added argument `$prefix` to `AdapterInterface::clear()` + * improved `RedisTagAwareAdapter` to support Redis server >= 2.8 and up to 4B items per tag + * added `TagAwareMarshaller` for optimized data storage when using `AbstractTagAwareAdapter` + * added `DeflateMarshaller` to compress serialized values + * removed support for phpredis 4 `compression` + * [BC BREAK] `RedisTagAwareAdapter` is not compatible with `RedisCluster` from `Predis` anymore, use `phpredis` instead + * Marked the `CacheDataCollector` class as `@final`. + * added `SodiumMarshaller` to encrypt/decrypt values using libsodium + +4.3.0 +----- + + * removed `psr/simple-cache` dependency, run `composer require psr/simple-cache` if you need it + * deprecated all PSR-16 adapters, use `Psr16Cache` or `Symfony\Contracts\Cache\CacheInterface` implementations instead + * deprecated `SimpleCacheAdapter`, use `Psr16Adapter` instead + +4.2.0 +----- + + * added support for connecting to Redis clusters via DSN + * added support for configuring multiple Memcached servers via DSN + * added `MarshallerInterface` and `DefaultMarshaller` to allow changing the serializer and provide one that automatically uses igbinary when available + * implemented `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache + * added sub-second expiry accuracy for backends that support it + * added support for phpredis 4 `compression` and `tcp_keepalive` options + * added automatic table creation when using Doctrine DBAL with PDO-based backends + * throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool + * deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead + * deprecated the `AbstractAdapter::unserialize()` and `AbstractCache::unserialize()` methods + * added `CacheCollectorPass` (originally in `FrameworkBundle`) + * added `CachePoolClearerPass` (originally in `FrameworkBundle`) + * added `CachePoolPass` (originally in `FrameworkBundle`) + * added `CachePoolPrunerPass` (originally in `FrameworkBundle`) + +3.4.0 +----- + + * added using options from Memcached DSN + * added PruneableInterface so PSR-6 or PSR-16 cache implementations can declare support for manual stale cache pruning + * added prune logic to FilesystemTrait, PhpFilesTrait, PdoTrait, TagAwareAdapter and ChainTrait + * now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, PhpFilesCache, PdoAdapter, PdoCache, ChainAdapter, and + ChainCache implement PruneableInterface and support manual stale cache pruning + +3.3.0 +----- + + * added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any + * added PSR-16 "Simple Cache" implementations for all existing PSR-6 adapters + * added Psr6Cache and SimpleCacheAdapter for bidirectional interoperability between PSR-6 and PSR-16 + * added MemcachedAdapter (PSR-6) and MemcachedCache (PSR-16) + * added TraceableAdapter (PSR-6) and TraceableCache (PSR-16) + +3.2.0 +----- + + * added TagAwareAdapter for tags-based invalidation + * added PdoAdapter with PDO and Doctrine DBAL support + * added PhpArrayAdapter and PhpFilesAdapter for OPcache-backed shared memory storage (PHP 7+ only) + * added NullAdapter + +3.1.0 +----- + + * added the component with strict PSR-6 implementations + * added ApcuAdapter, ArrayAdapter, FilesystemAdapter and RedisAdapter + * added AbstractAdapter, ChainAdapter and ProxyAdapter + * added DoctrineAdapter and DoctrineProvider for bidirectional interoperability with Doctrine Cache diff --git a/vendor/symfony/cache/CacheItem.php b/vendor/symfony/cache/CacheItem.php new file mode 100644 index 0000000..091d9e9 --- /dev/null +++ b/vendor/symfony/cache/CacheItem.php @@ -0,0 +1,192 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Exception\LogicException; +use Symfony\Contracts\Cache\ItemInterface; + +/** + * @author Nicolas Grekas + */ +final class CacheItem implements ItemInterface +{ + private const METADATA_EXPIRY_OFFSET = 1527506807; + + protected $key; + protected $value; + protected $isHit = false; + protected $expiry; + protected $metadata = []; + protected $newMetadata = []; + protected $innerItem; + protected $poolHash; + protected $isTaggable = false; + + /** + * {@inheritdoc} + */ + public function getKey(): string + { + return $this->key; + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function get() + { + return $this->value; + } + + /** + * {@inheritdoc} + */ + public function isHit(): bool + { + return $this->isHit; + } + + /** + * {@inheritdoc} + * + * @return $this + */ + public function set($value): self + { + $this->value = $value; + + return $this; + } + + /** + * {@inheritdoc} + * + * @return $this + */ + public function expiresAt($expiration): self + { + if (null === $expiration) { + $this->expiry = null; + } elseif ($expiration instanceof \DateTimeInterface) { + $this->expiry = (float) $expiration->format('U.u'); + } else { + throw new InvalidArgumentException(sprintf('Expiration date must implement DateTimeInterface or be null, "%s" given.', get_debug_type($expiration))); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return $this + */ + public function expiresAfter($time): self + { + if (null === $time) { + $this->expiry = null; + } elseif ($time instanceof \DateInterval) { + $this->expiry = microtime(true) + \DateTime::createFromFormat('U', 0)->add($time)->format('U.u'); + } elseif (\is_int($time)) { + $this->expiry = $time + microtime(true); + } else { + throw new InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given.', get_debug_type($time))); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function tag($tags): ItemInterface + { + if (!$this->isTaggable) { + throw new LogicException(sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key)); + } + if (!is_iterable($tags)) { + $tags = [$tags]; + } + foreach ($tags as $tag) { + if (!\is_string($tag) && !(\is_object($tag) && method_exists($tag, '__toString'))) { + throw new InvalidArgumentException(sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', \is_object($tag) ? \get_class($tag) : \gettype($tag))); + } + $tag = (string) $tag; + if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) { + continue; + } + if ('' === $tag) { + throw new InvalidArgumentException('Cache tag length must be greater than zero.'); + } + if (false !== strpbrk($tag, self::RESERVED_CHARACTERS)) { + throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters "%s".', $tag, self::RESERVED_CHARACTERS)); + } + $this->newMetadata[self::METADATA_TAGS][$tag] = $tag; + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getMetadata(): array + { + return $this->metadata; + } + + /** + * Validates a cache key according to PSR-6. + * + * @param mixed $key The key to validate + * + * @throws InvalidArgumentException When $key is not valid + */ + public static function validateKey($key): string + { + if (!\is_string($key)) { + throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); + } + if ('' === $key) { + throw new InvalidArgumentException('Cache key length must be greater than zero.'); + } + if (false !== strpbrk($key, self::RESERVED_CHARACTERS)) { + throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS)); + } + + return $key; + } + + /** + * Internal logging helper. + * + * @internal + */ + public static function log(?LoggerInterface $logger, string $message, array $context = []) + { + if ($logger) { + $logger->warning($message, $context); + } else { + $replace = []; + foreach ($context as $k => $v) { + if (\is_scalar($v)) { + $replace['{'.$k.'}'] = $v; + } + } + @trigger_error(strtr($message, $replace), \E_USER_WARNING); + } + } +} diff --git a/vendor/symfony/cache/DataCollector/CacheDataCollector.php b/vendor/symfony/cache/DataCollector/CacheDataCollector.php new file mode 100644 index 0000000..9590436 --- /dev/null +++ b/vendor/symfony/cache/DataCollector/CacheDataCollector.php @@ -0,0 +1,185 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DataCollector; + +use Symfony\Component\Cache\Adapter\TraceableAdapter; +use Symfony\Component\Cache\Adapter\TraceableAdapterEvent; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; + +/** + * @author Aaron Scherer + * @author Tobias Nyholm + * + * @final + */ +class CacheDataCollector extends DataCollector implements LateDataCollectorInterface +{ + /** + * @var TraceableAdapter[] + */ + private $instances = []; + + public function addInstance(string $name, TraceableAdapter $instance) + { + $this->instances[$name] = $instance; + } + + /** + * {@inheritdoc} + */ + public function collect(Request $request, Response $response, \Throwable $exception = null) + { + $empty = ['calls' => [], 'config' => [], 'options' => [], 'statistics' => []]; + $this->data = ['instances' => $empty, 'total' => $empty]; + foreach ($this->instances as $name => $instance) { + $this->data['instances']['calls'][$name] = $instance->getCalls(); + } + + $this->data['instances']['statistics'] = $this->calculateStatistics(); + $this->data['total']['statistics'] = $this->calculateTotalStatistics(); + } + + public function reset() + { + $this->data = []; + foreach ($this->instances as $instance) { + $instance->clearCalls(); + } + } + + public function lateCollect() + { + $this->data['instances']['calls'] = $this->cloneVar($this->data['instances']['calls']); + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return 'cache'; + } + + /** + * Method returns amount of logged Cache reads: "get" calls. + */ + public function getStatistics(): array + { + return $this->data['instances']['statistics']; + } + + /** + * Method returns the statistic totals. + */ + public function getTotals(): array + { + return $this->data['total']['statistics']; + } + + /** + * Method returns all logged Cache call objects. + * + * @return mixed + */ + public function getCalls() + { + return $this->data['instances']['calls']; + } + + private function calculateStatistics(): array + { + $statistics = []; + foreach ($this->data['instances']['calls'] as $name => $calls) { + $statistics[$name] = [ + 'calls' => 0, + 'time' => 0, + 'reads' => 0, + 'writes' => 0, + 'deletes' => 0, + 'hits' => 0, + 'misses' => 0, + ]; + /** @var TraceableAdapterEvent $call */ + foreach ($calls as $call) { + ++$statistics[$name]['calls']; + $statistics[$name]['time'] += $call->end - $call->start; + if ('get' === $call->name) { + ++$statistics[$name]['reads']; + if ($call->hits) { + ++$statistics[$name]['hits']; + } else { + ++$statistics[$name]['misses']; + ++$statistics[$name]['writes']; + } + } elseif ('getItem' === $call->name) { + ++$statistics[$name]['reads']; + if ($call->hits) { + ++$statistics[$name]['hits']; + } else { + ++$statistics[$name]['misses']; + } + } elseif ('getItems' === $call->name) { + $statistics[$name]['reads'] += $call->hits + $call->misses; + $statistics[$name]['hits'] += $call->hits; + $statistics[$name]['misses'] += $call->misses; + } elseif ('hasItem' === $call->name) { + ++$statistics[$name]['reads']; + if (false === $call->result) { + ++$statistics[$name]['misses']; + } else { + ++$statistics[$name]['hits']; + } + } elseif ('save' === $call->name) { + ++$statistics[$name]['writes']; + } elseif ('deleteItem' === $call->name) { + ++$statistics[$name]['deletes']; + } + } + if ($statistics[$name]['reads']) { + $statistics[$name]['hit_read_ratio'] = round(100 * $statistics[$name]['hits'] / $statistics[$name]['reads'], 2); + } else { + $statistics[$name]['hit_read_ratio'] = null; + } + } + + return $statistics; + } + + private function calculateTotalStatistics(): array + { + $statistics = $this->getStatistics(); + $totals = [ + 'calls' => 0, + 'time' => 0, + 'reads' => 0, + 'writes' => 0, + 'deletes' => 0, + 'hits' => 0, + 'misses' => 0, + ]; + foreach ($statistics as $name => $values) { + foreach ($totals as $key => $value) { + $totals[$key] += $statistics[$name][$key]; + } + } + if ($totals['reads']) { + $totals['hit_read_ratio'] = round(100 * $totals['hits'] / $totals['reads'], 2); + } else { + $totals['hit_read_ratio'] = null; + } + + return $totals; + } +} diff --git a/vendor/symfony/cache/DependencyInjection/CacheCollectorPass.php b/vendor/symfony/cache/DependencyInjection/CacheCollectorPass.php new file mode 100644 index 0000000..843232e --- /dev/null +++ b/vendor/symfony/cache/DependencyInjection/CacheCollectorPass.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DependencyInjection; + +use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; +use Symfony\Component\Cache\Adapter\TraceableAdapter; +use Symfony\Component\Cache\Adapter\TraceableTagAwareAdapter; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Inject a data collector to all the cache services to be able to get detailed statistics. + * + * @author Tobias Nyholm + */ +class CacheCollectorPass implements CompilerPassInterface +{ + private $dataCollectorCacheId; + private $cachePoolTag; + private $cachePoolRecorderInnerSuffix; + + public function __construct(string $dataCollectorCacheId = 'data_collector.cache', string $cachePoolTag = 'cache.pool', string $cachePoolRecorderInnerSuffix = '.recorder_inner') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->dataCollectorCacheId = $dataCollectorCacheId; + $this->cachePoolTag = $cachePoolTag; + $this->cachePoolRecorderInnerSuffix = $cachePoolRecorderInnerSuffix; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->dataCollectorCacheId)) { + return; + } + + foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $attributes) { + $poolName = $attributes[0]['name'] ?? $id; + + $this->addToCollector($id, $poolName, $container); + } + } + + private function addToCollector(string $id, string $name, ContainerBuilder $container) + { + $definition = $container->getDefinition($id); + if ($definition->isAbstract()) { + return; + } + + $collectorDefinition = $container->getDefinition($this->dataCollectorCacheId); + $recorder = new Definition(is_subclass_of($definition->getClass(), TagAwareAdapterInterface::class) ? TraceableTagAwareAdapter::class : TraceableAdapter::class); + $recorder->setTags($definition->getTags()); + if (!$definition->isPublic() || !$definition->isPrivate()) { + $recorder->setPublic($definition->isPublic()); + } + $recorder->setArguments([new Reference($innerId = $id.$this->cachePoolRecorderInnerSuffix)]); + + foreach ($definition->getMethodCalls() as [$method, $args]) { + if ('setCallbackWrapper' !== $method || !$args[0] instanceof Definition || !($args[0]->getArguments()[2] ?? null) instanceof Definition) { + continue; + } + if ([new Reference($id), 'setCallbackWrapper'] == $args[0]->getArguments()[2]->getFactory()) { + $args[0]->getArguments()[2]->setFactory([new Reference($innerId), 'setCallbackWrapper']); + } + } + + $definition->setTags([]); + $definition->setPublic(false); + + $container->setDefinition($innerId, $definition); + $container->setDefinition($id, $recorder); + + // Tell the collector to add the new instance + $collectorDefinition->addMethodCall('addInstance', [$name, new Reference($id)]); + $collectorDefinition->setPublic(false); + } +} diff --git a/vendor/symfony/cache/DependencyInjection/CachePoolClearerPass.php b/vendor/symfony/cache/DependencyInjection/CachePoolClearerPass.php new file mode 100644 index 0000000..c9b04ad --- /dev/null +++ b/vendor/symfony/cache/DependencyInjection/CachePoolClearerPass.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Nicolas Grekas + */ +class CachePoolClearerPass implements CompilerPassInterface +{ + private $cachePoolClearerTag; + + public function __construct(string $cachePoolClearerTag = 'cache.pool.clearer') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->cachePoolClearerTag = $cachePoolClearerTag; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $container->getParameterBag()->remove('cache.prefix.seed'); + + foreach ($container->findTaggedServiceIds($this->cachePoolClearerTag) as $id => $attr) { + $clearer = $container->getDefinition($id); + $pools = []; + foreach ($clearer->getArgument(0) as $name => $ref) { + if ($container->hasDefinition($ref)) { + $pools[$name] = new Reference($ref); + } + } + $clearer->replaceArgument(0, $pools); + } + } +} diff --git a/vendor/symfony/cache/DependencyInjection/CachePoolPass.php b/vendor/symfony/cache/DependencyInjection/CachePoolPass.php new file mode 100644 index 0000000..14ac2bd --- /dev/null +++ b/vendor/symfony/cache/DependencyInjection/CachePoolPass.php @@ -0,0 +1,274 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DependencyInjection; + +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\ChainAdapter; +use Symfony\Component\Cache\Adapter\NullAdapter; +use Symfony\Component\Cache\Adapter\ParameterNormalizer; +use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Nicolas Grekas + */ +class CachePoolPass implements CompilerPassInterface +{ + private $cachePoolTag; + private $kernelResetTag; + private $cacheClearerId; + private $cachePoolClearerTag; + private $cacheSystemClearerId; + private $cacheSystemClearerTag; + private $reverseContainerId; + private $reversibleTag; + private $messageHandlerId; + + public function __construct(string $cachePoolTag = 'cache.pool', string $kernelResetTag = 'kernel.reset', string $cacheClearerId = 'cache.global_clearer', string $cachePoolClearerTag = 'cache.pool.clearer', string $cacheSystemClearerId = 'cache.system_clearer', string $cacheSystemClearerTag = 'kernel.cache_clearer', string $reverseContainerId = 'reverse_container', string $reversibleTag = 'container.reversible', string $messageHandlerId = 'cache.early_expiration_handler') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->cachePoolTag = $cachePoolTag; + $this->kernelResetTag = $kernelResetTag; + $this->cacheClearerId = $cacheClearerId; + $this->cachePoolClearerTag = $cachePoolClearerTag; + $this->cacheSystemClearerId = $cacheSystemClearerId; + $this->cacheSystemClearerTag = $cacheSystemClearerTag; + $this->reverseContainerId = $reverseContainerId; + $this->reversibleTag = $reversibleTag; + $this->messageHandlerId = $messageHandlerId; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if ($container->hasParameter('cache.prefix.seed')) { + $seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed')); + } else { + $seed = '_'.$container->getParameter('kernel.project_dir'); + $seed .= '.'.$container->getParameter('kernel.container_class'); + } + + $needsMessageHandler = false; + $allPools = []; + $clearers = []; + $attributes = [ + 'provider', + 'name', + 'namespace', + 'default_lifetime', + 'early_expiration_message_bus', + 'reset', + ]; + foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) { + $adapter = $pool = $container->getDefinition($id); + if ($pool->isAbstract()) { + continue; + } + $class = $adapter->getClass(); + while ($adapter instanceof ChildDefinition) { + $adapter = $container->findDefinition($adapter->getParent()); + $class = $class ?: $adapter->getClass(); + if ($t = $adapter->getTag($this->cachePoolTag)) { + $tags[0] += $t[0]; + } + } + $name = $tags[0]['name'] ?? $id; + if (!isset($tags[0]['namespace'])) { + $namespaceSeed = $seed; + if (null !== $class) { + $namespaceSeed .= '.'.$class; + } + + $tags[0]['namespace'] = $this->getNamespace($namespaceSeed, $name); + } + if (isset($tags[0]['clearer'])) { + $clearer = $tags[0]['clearer']; + while ($container->hasAlias($clearer)) { + $clearer = (string) $container->getAlias($clearer); + } + } else { + $clearer = null; + } + unset($tags[0]['clearer'], $tags[0]['name']); + + if (isset($tags[0]['provider'])) { + $tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider'])); + } + + if (ChainAdapter::class === $class) { + $adapters = []; + foreach ($adapter->getArgument(0) as $provider => $adapter) { + if ($adapter instanceof ChildDefinition) { + $chainedPool = $adapter; + } else { + $chainedPool = $adapter = new ChildDefinition($adapter); + } + + $chainedTags = [\is_int($provider) ? [] : ['provider' => $provider]]; + $chainedClass = ''; + + while ($adapter instanceof ChildDefinition) { + $adapter = $container->findDefinition($adapter->getParent()); + $chainedClass = $chainedClass ?: $adapter->getClass(); + if ($t = $adapter->getTag($this->cachePoolTag)) { + $chainedTags[0] += $t[0]; + } + } + + if (ChainAdapter::class === $chainedClass) { + throw new InvalidArgumentException(sprintf('Invalid service "%s": chain of adapters cannot reference another chain, found "%s".', $id, $chainedPool->getParent())); + } + + $i = 0; + + if (isset($chainedTags[0]['provider'])) { + $chainedPool->replaceArgument($i++, new Reference(static::getServiceProvider($container, $chainedTags[0]['provider']))); + } + + if (isset($tags[0]['namespace']) && !\in_array($adapter->getClass(), [ArrayAdapter::class, NullAdapter::class], true)) { + $chainedPool->replaceArgument($i++, $tags[0]['namespace']); + } + + if (isset($tags[0]['default_lifetime'])) { + $chainedPool->replaceArgument($i++, $tags[0]['default_lifetime']); + } + + $adapters[] = $chainedPool; + } + + $pool->replaceArgument(0, $adapters); + unset($tags[0]['provider'], $tags[0]['namespace']); + $i = 1; + } else { + $i = 0; + } + + foreach ($attributes as $attr) { + if (!isset($tags[0][$attr])) { + // no-op + } elseif ('reset' === $attr) { + if ($tags[0][$attr]) { + $pool->addTag($this->kernelResetTag, ['method' => $tags[0][$attr]]); + } + } elseif ('early_expiration_message_bus' === $attr) { + $needsMessageHandler = true; + $pool->addMethodCall('setCallbackWrapper', [(new Definition(EarlyExpirationDispatcher::class)) + ->addArgument(new Reference($tags[0]['early_expiration_message_bus'])) + ->addArgument(new Reference($this->reverseContainerId)) + ->addArgument((new Definition('callable')) + ->setFactory([new Reference($id), 'setCallbackWrapper']) + ->addArgument(null) + ), + ]); + $pool->addTag($this->reversibleTag); + } elseif ('namespace' !== $attr || !\in_array($class, [ArrayAdapter::class, NullAdapter::class], true)) { + $argument = $tags[0][$attr]; + + if ('default_lifetime' === $attr && !is_numeric($argument)) { + $argument = (new Definition('int', [$argument])) + ->setFactory([ParameterNormalizer::class, 'normalizeDuration']); + } + + $pool->replaceArgument($i++, $argument); + } + unset($tags[0][$attr]); + } + if (!empty($tags[0])) { + throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime", "early_expiration_message_bus" and "reset", found "%s".', $this->cachePoolTag, $id, implode('", "', array_keys($tags[0])))); + } + + if (null !== $clearer) { + $clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); + } + + $allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); + } + + if (!$needsMessageHandler) { + $container->removeDefinition($this->messageHandlerId); + } + + $notAliasedCacheClearerId = $this->cacheClearerId; + while ($container->hasAlias($this->cacheClearerId)) { + $this->cacheClearerId = (string) $container->getAlias($this->cacheClearerId); + } + if ($container->hasDefinition($this->cacheClearerId)) { + $clearers[$notAliasedCacheClearerId] = $allPools; + } + + foreach ($clearers as $id => $pools) { + $clearer = $container->getDefinition($id); + if ($clearer instanceof ChildDefinition) { + $clearer->replaceArgument(0, $pools); + } else { + $clearer->setArgument(0, $pools); + } + $clearer->addTag($this->cachePoolClearerTag); + + if ($this->cacheSystemClearerId === $id) { + $clearer->addTag($this->cacheSystemClearerTag); + } + } + + $allPoolsKeys = array_keys($allPools); + + if ($container->hasDefinition('console.command.cache_pool_list')) { + $container->getDefinition('console.command.cache_pool_list')->replaceArgument(0, $allPoolsKeys); + } + + if ($container->hasDefinition('console.command.cache_pool_clear')) { + $container->getDefinition('console.command.cache_pool_clear')->addArgument($allPoolsKeys); + } + + if ($container->hasDefinition('console.command.cache_pool_delete')) { + $container->getDefinition('console.command.cache_pool_delete')->addArgument($allPoolsKeys); + } + } + + private function getNamespace(string $seed, string $id) + { + return substr(str_replace('/', '-', base64_encode(hash('sha256', $id.$seed, true))), 0, 10); + } + + /** + * @internal + */ + public static function getServiceProvider(ContainerBuilder $container, string $name) + { + $container->resolveEnvPlaceholders($name, null, $usedEnvs); + + if ($usedEnvs || preg_match('#^[a-z]++:#', $name)) { + $dsn = $name; + + if (!$container->hasDefinition($name = '.cache_connection.'.ContainerBuilder::hash($dsn))) { + $definition = new Definition(AbstractAdapter::class); + $definition->setPublic(false); + $definition->setFactory([AbstractAdapter::class, 'createConnection']); + $definition->setArguments([$dsn, ['lazy' => true]]); + $container->setDefinition($name, $definition); + } + } + + return $name; + } +} diff --git a/vendor/symfony/cache/DependencyInjection/CachePoolPrunerPass.php b/vendor/symfony/cache/DependencyInjection/CachePoolPrunerPass.php new file mode 100644 index 0000000..86a1906 --- /dev/null +++ b/vendor/symfony/cache/DependencyInjection/CachePoolPrunerPass.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\DependencyInjection; + +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Rob Frawley 2nd + */ +class CachePoolPrunerPass implements CompilerPassInterface +{ + private $cacheCommandServiceId; + private $cachePoolTag; + + public function __construct(string $cacheCommandServiceId = 'console.command.cache_pool_prune', string $cachePoolTag = 'cache.pool') + { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->cacheCommandServiceId = $cacheCommandServiceId; + $this->cachePoolTag = $cachePoolTag; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->cacheCommandServiceId)) { + return; + } + + $services = []; + + foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) { + $class = $container->getParameterBag()->resolveValue($container->getDefinition($id)->getClass()); + + if (!$reflection = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + + if ($reflection->implementsInterface(PruneableInterface::class)) { + $services[$id] = new Reference($id); + } + } + + $container->getDefinition($this->cacheCommandServiceId)->replaceArgument(0, new IteratorArgument($services)); + } +} diff --git a/vendor/symfony/cache/DoctrineProvider.php b/vendor/symfony/cache/DoctrineProvider.php new file mode 100644 index 0000000..7b55aae --- /dev/null +++ b/vendor/symfony/cache/DoctrineProvider.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Doctrine\Common\Cache\CacheProvider; +use Psr\Cache\CacheItemPoolInterface; +use Symfony\Contracts\Service\ResetInterface; + +if (!class_exists(CacheProvider::class)) { + return; +} + +/** + * @author Nicolas Grekas + * + * @deprecated Use Doctrine\Common\Cache\Psr6\DoctrineProvider instead + */ +class DoctrineProvider extends CacheProvider implements PruneableInterface, ResettableInterface +{ + private $pool; + + public function __construct(CacheItemPoolInterface $pool) + { + trigger_deprecation('symfony/cache', '5.4', '"%s" is deprecated, use "Doctrine\Common\Cache\Psr6\DoctrineProvider" instead.', __CLASS__); + + $this->pool = $pool; + } + + /** + * {@inheritdoc} + */ + public function prune() + { + return $this->pool instanceof PruneableInterface && $this->pool->prune(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->pool instanceof ResetInterface) { + $this->pool->reset(); + } + $this->setNamespace($this->getNamespace()); + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + protected function doFetch($id) + { + $item = $this->pool->getItem(rawurlencode($id)); + + return $item->isHit() ? $item->get() : false; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + protected function doContains($id) + { + return $this->pool->hasItem(rawurlencode($id)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + protected function doSave($id, $data, $lifeTime = 0) + { + $item = $this->pool->getItem(rawurlencode($id)); + + if (0 < $lifeTime) { + $item->expiresAfter($lifeTime); + } + + return $this->pool->save($item->set($data)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + protected function doDelete($id) + { + return $this->pool->deleteItem(rawurlencode($id)); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + protected function doFlush() + { + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + * + * @return array|null + */ + protected function doGetStats() + { + return null; + } +} diff --git a/vendor/symfony/cache/Exception/CacheException.php b/vendor/symfony/cache/Exception/CacheException.php new file mode 100644 index 0000000..d2e975b --- /dev/null +++ b/vendor/symfony/cache/Exception/CacheException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Exception; + +use Psr\Cache\CacheException as Psr6CacheInterface; +use Psr\SimpleCache\CacheException as SimpleCacheInterface; + +if (interface_exists(SimpleCacheInterface::class)) { + class CacheException extends \Exception implements Psr6CacheInterface, SimpleCacheInterface + { + } +} else { + class CacheException extends \Exception implements Psr6CacheInterface + { + } +} diff --git a/vendor/symfony/cache/Exception/InvalidArgumentException.php b/vendor/symfony/cache/Exception/InvalidArgumentException.php new file mode 100644 index 0000000..7f9584a --- /dev/null +++ b/vendor/symfony/cache/Exception/InvalidArgumentException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Exception; + +use Psr\Cache\InvalidArgumentException as Psr6CacheInterface; +use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInterface; + +if (interface_exists(SimpleCacheInterface::class)) { + class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface, SimpleCacheInterface + { + } +} else { + class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface + { + } +} diff --git a/vendor/symfony/cache/Exception/LogicException.php b/vendor/symfony/cache/Exception/LogicException.php new file mode 100644 index 0000000..9ffa7ed --- /dev/null +++ b/vendor/symfony/cache/Exception/LogicException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Exception; + +use Psr\Cache\CacheException as Psr6CacheInterface; +use Psr\SimpleCache\CacheException as SimpleCacheInterface; + +if (interface_exists(SimpleCacheInterface::class)) { + class LogicException extends \LogicException implements Psr6CacheInterface, SimpleCacheInterface + { + } +} else { + class LogicException extends \LogicException implements Psr6CacheInterface + { + } +} diff --git a/vendor/symfony/cache/LICENSE b/vendor/symfony/cache/LICENSE new file mode 100644 index 0000000..7fa9539 --- /dev/null +++ b/vendor/symfony/cache/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/cache/LockRegistry.php b/vendor/symfony/cache/LockRegistry.php new file mode 100644 index 0000000..65f20bb --- /dev/null +++ b/vendor/symfony/cache/LockRegistry.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Psr\Log\LoggerInterface; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; + +/** + * LockRegistry is used internally by existing adapters to protect against cache stampede. + * + * It does so by wrapping the computation of items in a pool of locks. + * Foreach each apps, there can be at most 20 concurrent processes that + * compute items at the same time and only one per cache-key. + * + * @author Nicolas Grekas + */ +final class LockRegistry +{ + private static $openedFiles = []; + private static $lockedFiles; + private static $signalingException; + private static $signalingCallback; + + /** + * The number of items in this list controls the max number of concurrent processes. + */ + private static $files = [ + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractTagAwareAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AdapterInterface.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ApcuAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseBucketAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseCollectionAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineDbalAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'MemcachedAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'NullAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ParameterNormalizer.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PdoAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpArrayAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpFilesAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ProxyAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'Psr16Adapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisTagAwareAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapterInterface.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableAdapter.php', + __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableTagAwareAdapter.php', + ]; + + /** + * Defines a set of existing files that will be used as keys to acquire locks. + * + * @return array The previously defined set of files + */ + public static function setFiles(array $files): array + { + $previousFiles = self::$files; + self::$files = $files; + + foreach (self::$openedFiles as $file) { + if ($file) { + flock($file, \LOCK_UN); + fclose($file); + } + } + self::$openedFiles = self::$lockedFiles = []; + + return $previousFiles; + } + + public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata = null, LoggerInterface $logger = null) + { + if ('\\' === \DIRECTORY_SEPARATOR && null === self::$lockedFiles) { + // disable locking on Windows by default + self::$files = self::$lockedFiles = []; + } + + $key = self::$files ? abs(crc32($item->getKey())) % \count(self::$files) : -1; + + if ($key < 0 || self::$lockedFiles || !$lock = self::open($key)) { + return $callback($item, $save); + } + + self::$signalingException ?? self::$signalingException = unserialize("O:9:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}"); + self::$signalingCallback ?? self::$signalingCallback = function () { throw self::$signalingException; }; + + while (true) { + try { + $locked = false; + // race to get the lock in non-blocking mode + $locked = flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock); + + if ($locked || !$wouldBlock) { + $logger && $logger->info(sprintf('Lock %s, now computing item "{key}"', $locked ? 'acquired' : 'not supported'), ['key' => $item->getKey()]); + self::$lockedFiles[$key] = true; + + $value = $callback($item, $save); + + if ($save) { + if ($setMetadata) { + $setMetadata($item); + } + + $pool->save($item->set($value)); + $save = false; + } + + return $value; + } + // if we failed the race, retry locking in blocking mode to wait for the winner + $logger && $logger->info('Item "{key}" is locked, waiting for it to be released', ['key' => $item->getKey()]); + flock($lock, \LOCK_SH); + } finally { + flock($lock, \LOCK_UN); + unset(self::$lockedFiles[$key]); + } + + try { + $value = $pool->get($item->getKey(), self::$signalingCallback, 0); + $logger && $logger->info('Item "{key}" retrieved after lock was released', ['key' => $item->getKey()]); + $save = false; + + return $value; + } catch (\Exception $e) { + if (self::$signalingException !== $e) { + throw $e; + } + $logger && $logger->info('Item "{key}" not found while lock was released, now retrying', ['key' => $item->getKey()]); + } + } + + return null; + } + + private static function open(int $key) + { + if (null !== $h = self::$openedFiles[$key] ?? null) { + return $h; + } + set_error_handler(function () {}); + try { + $h = fopen(self::$files[$key], 'r+'); + } finally { + restore_error_handler(); + } + + return self::$openedFiles[$key] = $h ?: @fopen(self::$files[$key], 'r'); + } +} diff --git a/vendor/symfony/cache/Marshaller/DefaultMarshaller.php b/vendor/symfony/cache/Marshaller/DefaultMarshaller.php new file mode 100644 index 0000000..3202dd6 --- /dev/null +++ b/vendor/symfony/cache/Marshaller/DefaultMarshaller.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +use Symfony\Component\Cache\Exception\CacheException; + +/** + * Serializes/unserializes values using igbinary_serialize() if available, serialize() otherwise. + * + * @author Nicolas Grekas + */ +class DefaultMarshaller implements MarshallerInterface +{ + private $useIgbinarySerialize = true; + private $throwOnSerializationFailure; + + public function __construct(bool $useIgbinarySerialize = null, bool $throwOnSerializationFailure = false) + { + if (null === $useIgbinarySerialize) { + $useIgbinarySerialize = \extension_loaded('igbinary') && (\PHP_VERSION_ID < 70400 || version_compare('3.1.6', phpversion('igbinary'), '<=')); + } elseif ($useIgbinarySerialize && (!\extension_loaded('igbinary') || (\PHP_VERSION_ID >= 70400 && version_compare('3.1.6', phpversion('igbinary'), '>')))) { + throw new CacheException(\extension_loaded('igbinary') && \PHP_VERSION_ID >= 70400 ? 'Please upgrade the "igbinary" PHP extension to v3.1.6 or higher.' : 'The "igbinary" PHP extension is not loaded.'); + } + $this->useIgbinarySerialize = $useIgbinarySerialize; + $this->throwOnSerializationFailure = $throwOnSerializationFailure; + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + $serialized = $failed = []; + + foreach ($values as $id => $value) { + try { + if ($this->useIgbinarySerialize) { + $serialized[$id] = igbinary_serialize($value); + } else { + $serialized[$id] = serialize($value); + } + } catch (\Exception $e) { + if ($this->throwOnSerializationFailure) { + throw new \ValueError($e->getMessage(), 0, $e); + } + $failed[] = $id; + } + } + + return $serialized; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + if ('b:0;' === $value) { + return false; + } + if ('N;' === $value) { + return null; + } + static $igbinaryNull; + if ($value === ($igbinaryNull ?? $igbinaryNull = \extension_loaded('igbinary') ? igbinary_serialize(null) : false)) { + return null; + } + $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); + try { + if (':' === ($value[1] ?? ':')) { + if (false !== $value = unserialize($value)) { + return $value; + } + } elseif (false === $igbinaryNull) { + throw new \RuntimeException('Failed to unserialize values, did you forget to install the "igbinary" extension?'); + } elseif (null !== $value = igbinary_unserialize($value)) { + return $value; + } + + throw new \DomainException(error_get_last() ? error_get_last()['message'] : 'Failed to unserialize values.'); + } catch (\Error $e) { + throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); + } finally { + ini_set('unserialize_callback_func', $unserializeCallbackHandler); + } + } + + /** + * @internal + */ + public static function handleUnserializeCallback(string $class) + { + throw new \DomainException('Class not found: '.$class); + } +} diff --git a/vendor/symfony/cache/Marshaller/DeflateMarshaller.php b/vendor/symfony/cache/Marshaller/DeflateMarshaller.php new file mode 100644 index 0000000..5544806 --- /dev/null +++ b/vendor/symfony/cache/Marshaller/DeflateMarshaller.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +use Symfony\Component\Cache\Exception\CacheException; + +/** + * Compresses values using gzdeflate(). + * + * @author Nicolas Grekas + */ +class DeflateMarshaller implements MarshallerInterface +{ + private $marshaller; + + public function __construct(MarshallerInterface $marshaller) + { + if (!\function_exists('gzdeflate')) { + throw new CacheException('The "zlib" PHP extension is not loaded.'); + } + + $this->marshaller = $marshaller; + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + return array_map('gzdeflate', $this->marshaller->marshall($values, $failed)); + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + if (false !== $inflatedValue = @gzinflate($value)) { + $value = $inflatedValue; + } + + return $this->marshaller->unmarshall($value); + } +} diff --git a/vendor/symfony/cache/Marshaller/MarshallerInterface.php b/vendor/symfony/cache/Marshaller/MarshallerInterface.php new file mode 100644 index 0000000..cdd6c40 --- /dev/null +++ b/vendor/symfony/cache/Marshaller/MarshallerInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +/** + * Serializes/unserializes PHP values. + * + * Implementations of this interface MUST deal with errors carefully. They MUST + * also deal with forward and backward compatibility at the storage format level. + * + * @author Nicolas Grekas + */ +interface MarshallerInterface +{ + /** + * Serializes a list of values. + * + * When serialization fails for a specific value, no exception should be + * thrown. Instead, its key should be listed in $failed. + */ + public function marshall(array $values, ?array &$failed): array; + + /** + * Unserializes a single value and throws an exception if anything goes wrong. + * + * @return mixed + * + * @throws \Exception Whenever unserialization fails + */ + public function unmarshall(string $value); +} diff --git a/vendor/symfony/cache/Marshaller/SodiumMarshaller.php b/vendor/symfony/cache/Marshaller/SodiumMarshaller.php new file mode 100644 index 0000000..dbf486a --- /dev/null +++ b/vendor/symfony/cache/Marshaller/SodiumMarshaller.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * Encrypt/decrypt values using Libsodium. + * + * @author Ahmed TAILOULOUTE + */ +class SodiumMarshaller implements MarshallerInterface +{ + private $marshaller; + private $decryptionKeys; + + /** + * @param string[] $decryptionKeys The key at index "0" is required and is used to decrypt and encrypt values; + * more rotating keys can be provided to decrypt values; + * each key must be generated using sodium_crypto_box_keypair() + */ + public function __construct(array $decryptionKeys, MarshallerInterface $marshaller = null) + { + if (!self::isSupported()) { + throw new CacheException('The "sodium" PHP extension is not loaded.'); + } + + if (!isset($decryptionKeys[0])) { + throw new InvalidArgumentException('At least one decryption key must be provided at index "0".'); + } + + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + $this->decryptionKeys = $decryptionKeys; + } + + public static function isSupported(): bool + { + return \function_exists('sodium_crypto_box_seal'); + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + $encryptionKey = sodium_crypto_box_publickey($this->decryptionKeys[0]); + + $encryptedValues = []; + foreach ($this->marshaller->marshall($values, $failed) as $k => $v) { + $encryptedValues[$k] = sodium_crypto_box_seal($v, $encryptionKey); + } + + return $encryptedValues; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + foreach ($this->decryptionKeys as $k) { + if (false !== $decryptedValue = @sodium_crypto_box_seal_open($value, $k)) { + $value = $decryptedValue; + break; + } + } + + return $this->marshaller->unmarshall($value); + } +} diff --git a/vendor/symfony/cache/Marshaller/TagAwareMarshaller.php b/vendor/symfony/cache/Marshaller/TagAwareMarshaller.php new file mode 100644 index 0000000..5d1e303 --- /dev/null +++ b/vendor/symfony/cache/Marshaller/TagAwareMarshaller.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Marshaller; + +/** + * A marshaller optimized for data structures generated by AbstractTagAwareAdapter. + * + * @author Nicolas Grekas + */ +class TagAwareMarshaller implements MarshallerInterface +{ + private $marshaller; + + public function __construct(MarshallerInterface $marshaller = null) + { + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + $failed = $notSerialized = $serialized = []; + + foreach ($values as $id => $value) { + if (\is_array($value) && \is_array($value['tags'] ?? null) && \array_key_exists('value', $value) && \count($value) === 2 + (\is_string($value['meta'] ?? null) && 8 === \strlen($value['meta']))) { + // if the value is an array with keys "tags", "value" and "meta", use a compact serialization format + // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F allow detecting this format quickly in unmarshall() + + $v = $this->marshaller->marshall($value, $f); + + if ($f) { + $f = []; + $failed[] = $id; + } else { + if ([] === $value['tags']) { + $v['tags'] = ''; + } + + $serialized[$id] = "\x9D".($value['meta'] ?? "\0\0\0\0\0\0\0\0").pack('N', \strlen($v['tags'])).$v['tags'].$v['value']; + $serialized[$id][9] = "\x5F"; + } + } else { + // other arbitratry values are serialized using the decorated marshaller below + $notSerialized[$id] = $value; + } + } + + if ($notSerialized) { + $serialized += $this->marshaller->marshall($notSerialized, $f); + $failed = array_merge($failed, $f); + } + + return $serialized; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value) + { + // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F + if (13 >= \strlen($value) || "\x9D" !== $value[0] || "\0" !== $value[5] || "\x5F" !== $value[9]) { + return $this->marshaller->unmarshall($value); + } + + // data consists of value, tags and metadata which we need to unpack + $meta = substr($value, 1, 12); + $meta[8] = "\0"; + $tagLen = unpack('Nlen', $meta, 8)['len']; + $meta = substr($meta, 0, 8); + + return [ + 'value' => $this->marshaller->unmarshall(substr($value, 13 + $tagLen)), + 'tags' => $tagLen ? $this->marshaller->unmarshall(substr($value, 13, $tagLen)) : [], + 'meta' => "\0\0\0\0\0\0\0\0" === $meta ? null : $meta, + ]; + } +} diff --git a/vendor/symfony/cache/Messenger/EarlyExpirationDispatcher.php b/vendor/symfony/cache/Messenger/EarlyExpirationDispatcher.php new file mode 100644 index 0000000..6f11b8b --- /dev/null +++ b/vendor/symfony/cache/Messenger/EarlyExpirationDispatcher.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Stamp\HandledStamp; + +/** + * Sends the computation of cached values to a message bus. + */ +class EarlyExpirationDispatcher +{ + private $bus; + private $reverseContainer; + private $callbackWrapper; + + public function __construct(MessageBusInterface $bus, ReverseContainer $reverseContainer, callable $callbackWrapper = null) + { + $this->bus = $bus; + $this->reverseContainer = $reverseContainer; + $this->callbackWrapper = $callbackWrapper; + } + + public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, LoggerInterface $logger = null) + { + if (!$item->isHit() || null === $message = EarlyExpirationMessage::create($this->reverseContainer, $callback, $item, $pool)) { + // The item is stale or the callback cannot be reversed: we must compute the value now + $logger && $logger->info('Computing item "{key}" online: '.($item->isHit() ? 'callback cannot be reversed' : 'item is stale'), ['key' => $item->getKey()]); + + return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger) : $callback($item, $save); + } + + $envelope = $this->bus->dispatch($message); + + if ($logger) { + if ($envelope->last(HandledStamp::class)) { + $logger->info('Item "{key}" was computed online', ['key' => $item->getKey()]); + } else { + $logger->info('Item "{key}" sent for recomputation', ['key' => $item->getKey()]); + } + } + + // The item's value is not stale, no need to write it to the backend + $save = false; + + return $message->getItem()->get() ?? $item->get(); + } +} diff --git a/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php b/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php new file mode 100644 index 0000000..1f0bd56 --- /dev/null +++ b/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; +use Symfony\Component\Messenger\Handler\MessageHandlerInterface; + +/** + * Computes cached values sent to a message bus. + */ +class EarlyExpirationHandler implements MessageHandlerInterface +{ + private $reverseContainer; + private $processedNonces = []; + + public function __construct(ReverseContainer $reverseContainer) + { + $this->reverseContainer = $reverseContainer; + } + + public function __invoke(EarlyExpirationMessage $message) + { + $item = $message->getItem(); + $metadata = $item->getMetadata(); + $expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? 0; + $ctime = $metadata[CacheItem::METADATA_CTIME] ?? 0; + + if ($expiry && $ctime) { + // skip duplicate or expired messages + + $processingNonce = [$expiry, $ctime]; + $pool = $message->getPool(); + $key = $item->getKey(); + + if (($this->processedNonces[$pool][$key] ?? null) === $processingNonce) { + return; + } + + if (microtime(true) >= $expiry) { + return; + } + + $this->processedNonces[$pool] = [$key => $processingNonce] + ($this->processedNonces[$pool] ?? []); + + if (\count($this->processedNonces[$pool]) > 100) { + array_pop($this->processedNonces[$pool]); + } + } + + static $setMetadata; + + $setMetadata ?? $setMetadata = \Closure::bind( + function (CacheItem $item, float $startTime) { + if ($item->expiry > $endTime = microtime(true)) { + $item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry; + $item->newMetadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime)); + } + }, + null, + CacheItem::class + ); + + $startTime = microtime(true); + $pool = $message->findPool($this->reverseContainer); + $callback = $message->findCallback($this->reverseContainer); + $value = $callback($item); + $setMetadata($item, $startTime); + $pool->save($item->set($value)); + } +} diff --git a/vendor/symfony/cache/Messenger/EarlyExpirationMessage.php b/vendor/symfony/cache/Messenger/EarlyExpirationMessage.php new file mode 100644 index 0000000..e25c07e --- /dev/null +++ b/vendor/symfony/cache/Messenger/EarlyExpirationMessage.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Messenger; + +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\DependencyInjection\ReverseContainer; + +/** + * Conveys a cached value that needs to be computed. + */ +final class EarlyExpirationMessage +{ + private $item; + private $pool; + private $callback; + + public static function create(ReverseContainer $reverseContainer, callable $callback, CacheItem $item, AdapterInterface $pool): ?self + { + try { + $item = clone $item; + $item->set(null); + } catch (\Exception $e) { + return null; + } + + $pool = $reverseContainer->getId($pool); + + if (\is_object($callback)) { + if (null === $id = $reverseContainer->getId($callback)) { + return null; + } + + $callback = '@'.$id; + } elseif (!\is_array($callback)) { + $callback = (string) $callback; + } elseif (!\is_object($callback[0])) { + $callback = [(string) $callback[0], (string) $callback[1]]; + } else { + if (null === $id = $reverseContainer->getId($callback[0])) { + return null; + } + + $callback = ['@'.$id, (string) $callback[1]]; + } + + return new self($item, $pool, $callback); + } + + public function getItem(): CacheItem + { + return $this->item; + } + + public function getPool(): string + { + return $this->pool; + } + + public function getCallback() + { + return $this->callback; + } + + public function findPool(ReverseContainer $reverseContainer): AdapterInterface + { + return $reverseContainer->getService($this->pool); + } + + public function findCallback(ReverseContainer $reverseContainer): callable + { + if (\is_string($callback = $this->callback)) { + return '@' === $callback[0] ? $reverseContainer->getService(substr($callback, 1)) : $callback; + } + if ('@' === $callback[0][0]) { + $callback[0] = $reverseContainer->getService(substr($callback[0], 1)); + } + + return $callback; + } + + private function __construct(CacheItem $item, string $pool, $callback) + { + $this->item = $item; + $this->pool = $pool; + $this->callback = $callback; + } +} diff --git a/vendor/symfony/cache/PruneableInterface.php b/vendor/symfony/cache/PruneableInterface.php new file mode 100644 index 0000000..4261525 --- /dev/null +++ b/vendor/symfony/cache/PruneableInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +/** + * Interface extends psr-6 and psr-16 caches to allow for pruning (deletion) of all expired cache items. + */ +interface PruneableInterface +{ + /** + * @return bool + */ + public function prune(); +} diff --git a/vendor/symfony/cache/Psr16Cache.php b/vendor/symfony/cache/Psr16Cache.php new file mode 100644 index 0000000..28c7de6 --- /dev/null +++ b/vendor/symfony/cache/Psr16Cache.php @@ -0,0 +1,289 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Psr\Cache\CacheException as Psr6CacheException; +use Psr\Cache\CacheItemPoolInterface; +use Psr\SimpleCache\CacheException as SimpleCacheException; +use Psr\SimpleCache\CacheInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Traits\ProxyTrait; + +if (null !== (new \ReflectionMethod(CacheInterface::class, 'get'))->getReturnType()) { + throw new \LogicException('psr/simple-cache 3.0+ is not compatible with this version of symfony/cache. Please upgrade symfony/cache to 6.0+ or downgrade psr/simple-cache to 1.x or 2.x.'); +} + +/** + * Turns a PSR-6 cache into a PSR-16 one. + * + * @author Nicolas Grekas + */ +class Psr16Cache implements CacheInterface, PruneableInterface, ResettableInterface +{ + use ProxyTrait; + + private const METADATA_EXPIRY_OFFSET = 1527506807; + + private $createCacheItem; + private $cacheItemPrototype; + + public function __construct(CacheItemPoolInterface $pool) + { + $this->pool = $pool; + + if (!$pool instanceof AdapterInterface) { + return; + } + $cacheItemPrototype = &$this->cacheItemPrototype; + $createCacheItem = \Closure::bind( + static function ($key, $value, $allowInt = false) use (&$cacheItemPrototype) { + $item = clone $cacheItemPrototype; + $item->poolHash = $item->innerItem = null; + if ($allowInt && \is_int($key)) { + $item->key = (string) $key; + } else { + \assert('' !== CacheItem::validateKey($key)); + $item->key = $key; + } + $item->value = $value; + $item->isHit = false; + + return $item; + }, + null, + CacheItem::class + ); + $this->createCacheItem = function ($key, $value, $allowInt = false) use ($createCacheItem) { + if (null === $this->cacheItemPrototype) { + $this->get($allowInt && \is_int($key) ? (string) $key : $key); + } + $this->createCacheItem = $createCacheItem; + + return $createCacheItem($key, null, $allowInt)->set($value); + }; + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function get($key, $default = null) + { + try { + $item = $this->pool->getItem($key); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + if (null === $this->cacheItemPrototype) { + $this->cacheItemPrototype = clone $item; + $this->cacheItemPrototype->set(null); + } + + return $item->isHit() ? $item->get() : $default; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function set($key, $value, $ttl = null) + { + try { + if (null !== $f = $this->createCacheItem) { + $item = $f($key, $value); + } else { + $item = $this->pool->getItem($key)->set($value); + } + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + if (null !== $ttl) { + $item->expiresAfter($ttl); + } + + return $this->pool->save($item); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function delete($key) + { + try { + return $this->pool->deleteItem($key); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear() + { + return $this->pool->clear(); + } + + /** + * {@inheritdoc} + * + * @return iterable + */ + public function getMultiple($keys, $default = null) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!\is_array($keys)) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys))); + } + + try { + $items = $this->pool->getItems($keys); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + $values = []; + + if (!$this->pool instanceof AdapterInterface) { + foreach ($items as $key => $item) { + $values[$key] = $item->isHit() ? $item->get() : $default; + } + + return $values; + } + + foreach ($items as $key => $item) { + if (!$item->isHit()) { + $values[$key] = $default; + continue; + } + $values[$key] = $item->get(); + + if (!$metadata = $item->getMetadata()) { + continue; + } + unset($metadata[CacheItem::METADATA_TAGS]); + + if ($metadata) { + $values[$key] = ["\x9D".pack('VN', (int) (0.1 + $metadata[CacheItem::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[CacheItem::METADATA_CTIME])."\x5F" => $values[$key]]; + } + } + + return $values; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function setMultiple($values, $ttl = null) + { + $valuesIsArray = \is_array($values); + if (!$valuesIsArray && !$values instanceof \Traversable) { + throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given.', get_debug_type($values))); + } + $items = []; + + try { + if (null !== $f = $this->createCacheItem) { + $valuesIsArray = false; + foreach ($values as $key => $value) { + $items[$key] = $f($key, $value, true); + } + } elseif ($valuesIsArray) { + $items = []; + foreach ($values as $key => $value) { + $items[] = (string) $key; + } + $items = $this->pool->getItems($items); + } else { + foreach ($values as $key => $value) { + if (\is_int($key)) { + $key = (string) $key; + } + $items[$key] = $this->pool->getItem($key)->set($value); + } + } + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + $ok = true; + + foreach ($items as $key => $item) { + if ($valuesIsArray) { + $item->set($values[$key]); + } + if (null !== $ttl) { + $item->expiresAfter($ttl); + } + $ok = $this->pool->saveDeferred($item) && $ok; + } + + return $this->pool->commit() && $ok; + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteMultiple($keys) + { + if ($keys instanceof \Traversable) { + $keys = iterator_to_array($keys, false); + } elseif (!\is_array($keys)) { + throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys))); + } + + try { + return $this->pool->deleteItems($keys); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function has($key) + { + try { + return $this->pool->hasItem($key); + } catch (SimpleCacheException $e) { + throw $e; + } catch (Psr6CacheException $e) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } +} diff --git a/vendor/symfony/cache/README.md b/vendor/symfony/cache/README.md new file mode 100644 index 0000000..7405205 --- /dev/null +++ b/vendor/symfony/cache/README.md @@ -0,0 +1,19 @@ +Symfony PSR-6 implementation for caching +======================================== + +The Cache component provides an extended +[PSR-6](http://www.php-fig.org/psr/psr-6/) implementation for adding cache to +your applications. It is designed to have a low overhead so that caching is +fastest. It ships with a few caching adapters for the most widespread and +suited to caching backends. It also provides a `doctrine/cache` proxy adapter +to cover more advanced caching needs and a proxy adapter for greater +interoperability between PSR-6 implementations. + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/cache.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/cache/ResettableInterface.php b/vendor/symfony/cache/ResettableInterface.php new file mode 100644 index 0000000..7b0a853 --- /dev/null +++ b/vendor/symfony/cache/ResettableInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache; + +use Symfony\Contracts\Service\ResetInterface; + +/** + * Resets a pool's local state. + */ +interface ResettableInterface extends ResetInterface +{ +} diff --git a/vendor/symfony/cache/Traits/AbstractAdapterTrait.php b/vendor/symfony/cache/Traits/AbstractAdapterTrait.php new file mode 100644 index 0000000..f0173c4 --- /dev/null +++ b/vendor/symfony/cache/Traits/AbstractAdapterTrait.php @@ -0,0 +1,424 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Psr\Cache\CacheItemInterface; +use Psr\Log\LoggerAwareTrait; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait AbstractAdapterTrait +{ + use LoggerAwareTrait; + + /** + * @var \Closure needs to be set by class, signature is function(string , mixed , bool ) + */ + private static $createCacheItem; + + /** + * @var \Closure needs to be set by class, signature is function(array , string , array <&expiredIds>) + */ + private static $mergeByLifetime; + + private $namespace = ''; + private $defaultLifetime; + private $namespaceVersion = ''; + private $versioningIsEnabled = false; + private $deferred = []; + private $ids = []; + + /** + * @var int|null The maximum length to enforce for identifiers or null when no limit applies + */ + protected $maxIdLength; + + /** + * Fetches several cache items. + * + * @param array $ids The cache identifiers to fetch + * + * @return array|\Traversable + */ + abstract protected function doFetch(array $ids); + + /** + * Confirms if the cache contains specified cache item. + * + * @param string $id The identifier for which to check existence + * + * @return bool + */ + abstract protected function doHave(string $id); + + /** + * Deletes all items in the pool. + * + * @param string $namespace The prefix used for all identifiers managed by this pool + * + * @return bool + */ + abstract protected function doClear(string $namespace); + + /** + * Removes multiple items from the pool. + * + * @param array $ids An array of identifiers that should be removed from the pool + * + * @return bool + */ + abstract protected function doDelete(array $ids); + + /** + * Persists several cache items immediately. + * + * @param array $values The values to cache, indexed by their cache identifier + * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning + * + * @return array|bool The identifiers that failed to be cached or a boolean stating if caching succeeded or not + */ + abstract protected function doSave(array $values, int $lifetime); + + /** + * {@inheritdoc} + * + * @return bool + */ + public function hasItem($key) + { + $id = $this->getId($key); + + if (isset($this->deferred[$key])) { + $this->commit(); + } + + try { + return $this->doHave($id); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to check if key "{key}" is cached: '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + + return false; + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function clear(string $prefix = '') + { + $this->deferred = []; + if ($cleared = $this->versioningIsEnabled) { + if ('' === $namespaceVersionToClear = $this->namespaceVersion) { + foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) { + $namespaceVersionToClear = $v; + } + } + $namespaceToClear = $this->namespace.$namespaceVersionToClear; + $namespaceVersion = self::formatNamespaceVersion(mt_rand()); + try { + $e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $namespaceVersion], 0); + } catch (\Exception $e) { + } + if (true !== $e && [] !== $e) { + $cleared = false; + $message = 'Failed to save the new namespace'.($e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } else { + $this->namespaceVersion = $namespaceVersion; + $this->ids = []; + } + } else { + $namespaceToClear = $this->namespace.$prefix; + } + + try { + return $this->doClear($namespaceToClear) || $cleared; + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to clear the cache: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]); + + return false; + } + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItem($key) + { + return $this->deleteItems([$key]); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function deleteItems(array $keys) + { + $ids = []; + + foreach ($keys as $key) { + $ids[$key] = $this->getId($key); + unset($this->deferred[$key]); + } + + try { + if ($this->doDelete($ids)) { + return true; + } + } catch (\Exception $e) { + } + + $ok = true; + + // When bulk-delete failed, retry each item individually + foreach ($ids as $key => $id) { + try { + $e = null; + if ($this->doDelete([$id])) { + continue; + } + } catch (\Exception $e) { + } + $message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + $ok = false; + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + public function getItem($key) + { + $id = $this->getId($key); + + if (isset($this->deferred[$key])) { + $this->commit(); + } + + $isHit = false; + $value = null; + + try { + foreach ($this->doFetch([$id]) as $value) { + $isHit = true; + } + + return (self::$createCacheItem)($key, $value, $isHit); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + } + + return (self::$createCacheItem)($key, null, false); + } + + /** + * {@inheritdoc} + */ + public function getItems(array $keys = []) + { + $ids = []; + $commit = false; + + foreach ($keys as $key) { + $ids[] = $this->getId($key); + $commit = $commit || isset($this->deferred[$key]); + } + + if ($commit) { + $this->commit(); + } + + try { + $items = $this->doFetch($ids); + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch items: '.$e->getMessage(), ['keys' => $keys, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + $items = []; + } + $ids = array_combine($ids, $keys); + + return $this->generateItems($items, $ids); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function save(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return $this->commit(); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function saveDeferred(CacheItemInterface $item) + { + if (!$item instanceof CacheItem) { + return false; + } + $this->deferred[$item->getKey()] = $item; + + return true; + } + + /** + * Enables/disables versioning of items. + * + * When versioning is enabled, clearing the cache is atomic and doesn't require listing existing keys to proceed, + * but old keys may need garbage collection and extra round-trips to the back-end are required. + * + * Calling this method also clears the memoized namespace version and thus forces a resynchonization of it. + * + * @return bool the previous state of versioning + */ + public function enableVersioning(bool $enable = true) + { + $wasEnabled = $this->versioningIsEnabled; + $this->versioningIsEnabled = $enable; + $this->namespaceVersion = ''; + $this->ids = []; + + return $wasEnabled; + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->deferred) { + $this->commit(); + } + $this->namespaceVersion = ''; + $this->ids = []; + } + + /** + * @return array + */ + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + if ($this->deferred) { + $this->commit(); + } + } + + private function generateItems(iterable $items, array &$keys): \Generator + { + $f = self::$createCacheItem; + + try { + foreach ($items as $id => $value) { + if (!isset($keys[$id])) { + throw new InvalidArgumentException(sprintf('Could not match value id "%s" to keys "%s".', $id, implode('", "', $keys))); + } + $key = $keys[$id]; + unset($keys[$id]); + yield $key => $f($key, $value, true); + } + } catch (\Exception $e) { + CacheItem::log($this->logger, 'Failed to fetch items: '.$e->getMessage(), ['keys' => array_values($keys), 'exception' => $e, 'cache-adapter' => get_debug_type($this)]); + } + + foreach ($keys as $key) { + yield $key => $f($key, null, false); + } + } + + private function getId($key) + { + if ($this->versioningIsEnabled && '' === $this->namespaceVersion) { + $this->ids = []; + $this->namespaceVersion = '1'.static::NS_SEPARATOR; + try { + foreach ($this->doFetch([static::NS_SEPARATOR.$this->namespace]) as $v) { + $this->namespaceVersion = $v; + } + $e = true; + if ('1'.static::NS_SEPARATOR === $this->namespaceVersion) { + $this->namespaceVersion = self::formatNamespaceVersion(time()); + $e = $this->doSave([static::NS_SEPARATOR.$this->namespace => $this->namespaceVersion], 0); + } + } catch (\Exception $e) { + } + if (true !== $e && [] !== $e) { + $message = 'Failed to save the new namespace'.($e instanceof \Exception ? ': '.$e->getMessage() : '.'); + CacheItem::log($this->logger, $message, ['exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); + } + } + + if (\is_string($key) && isset($this->ids[$key])) { + return $this->namespace.$this->namespaceVersion.$this->ids[$key]; + } + \assert('' !== CacheItem::validateKey($key)); + $this->ids[$key] = $key; + + if (\count($this->ids) > 1000) { + array_splice($this->ids, 0, 500); // stop memory leak if there are many keys + } + + if (null === $this->maxIdLength) { + return $this->namespace.$this->namespaceVersion.$key; + } + if (\strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) { + // Use MD5 to favor speed over security, which is not an issue here + $this->ids[$key] = $id = substr_replace(base64_encode(hash('md5', $key, true)), static::NS_SEPARATOR, -(\strlen($this->namespaceVersion) + 2)); + $id = $this->namespace.$this->namespaceVersion.$id; + } + + return $id; + } + + /** + * @internal + */ + public static function handleUnserializeCallback(string $class) + { + throw new \DomainException('Class not found: '.$class); + } + + private static function formatNamespaceVersion(int $value): string + { + return strtr(substr_replace(base64_encode(pack('V', $value)), static::NS_SEPARATOR, 5), '/', '_'); + } +} diff --git a/vendor/symfony/cache/Traits/ContractsTrait.php b/vendor/symfony/cache/Traits/ContractsTrait.php new file mode 100644 index 0000000..9a491ad --- /dev/null +++ b/vendor/symfony/cache/Traits/ContractsTrait.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\CacheItem; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\LockRegistry; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\CacheTrait; +use Symfony\Contracts\Cache\ItemInterface; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait ContractsTrait +{ + use CacheTrait { + doGet as private contractsGet; + } + + private $callbackWrapper; + private $computing = []; + + /** + * Wraps the callback passed to ->get() in a callable. + * + * @return callable the previous callback wrapper + */ + public function setCallbackWrapper(?callable $callbackWrapper): callable + { + if (!isset($this->callbackWrapper)) { + $this->callbackWrapper = \Closure::fromCallable([LockRegistry::class, 'compute']); + + if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { + $this->setCallbackWrapper(null); + } + } + + $previousWrapper = $this->callbackWrapper; + $this->callbackWrapper = $callbackWrapper ?? static function (callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger) { + return $callback($item, $save); + }; + + return $previousWrapper; + } + + private function doGet(AdapterInterface $pool, string $key, callable $callback, ?float $beta, array &$metadata = null) + { + if (0 > $beta = $beta ?? 1.0) { + throw new InvalidArgumentException(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta)); + } + + static $setMetadata; + + $setMetadata ?? $setMetadata = \Closure::bind( + static function (CacheItem $item, float $startTime, ?array &$metadata) { + if ($item->expiry > $endTime = microtime(true)) { + $item->newMetadata[CacheItem::METADATA_EXPIRY] = $metadata[CacheItem::METADATA_EXPIRY] = $item->expiry; + $item->newMetadata[CacheItem::METADATA_CTIME] = $metadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime)); + } else { + unset($metadata[CacheItem::METADATA_EXPIRY], $metadata[CacheItem::METADATA_CTIME]); + } + }, + null, + CacheItem::class + ); + + return $this->contractsGet($pool, $key, function (CacheItem $item, bool &$save) use ($pool, $callback, $setMetadata, &$metadata, $key) { + // don't wrap nor save recursive calls + if (isset($this->computing[$key])) { + $value = $callback($item, $save); + $save = false; + + return $value; + } + + $this->computing[$key] = $key; + $startTime = microtime(true); + + if (!isset($this->callbackWrapper)) { + $this->setCallbackWrapper($this->setCallbackWrapper(null)); + } + + try { + $value = ($this->callbackWrapper)($callback, $item, $save, $pool, function (CacheItem $item) use ($setMetadata, $startTime, &$metadata) { + $setMetadata($item, $startTime, $metadata); + }, $this->logger ?? null); + $setMetadata($item, $startTime, $metadata); + + return $value; + } finally { + unset($this->computing[$key]); + } + }, $beta, $metadata, $this->logger ?? null); + } +} diff --git a/vendor/symfony/cache/Traits/FilesystemCommonTrait.php b/vendor/symfony/cache/Traits/FilesystemCommonTrait.php new file mode 100644 index 0000000..c06cc30 --- /dev/null +++ b/vendor/symfony/cache/Traits/FilesystemCommonTrait.php @@ -0,0 +1,196 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\Exception\InvalidArgumentException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait FilesystemCommonTrait +{ + private $directory; + private $tmp; + + private function init(string $namespace, ?string $directory) + { + if (!isset($directory[0])) { + $directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'symfony-cache'; + } else { + $directory = realpath($directory) ?: $directory; + } + if (isset($namespace[0])) { + if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); + } + $directory .= \DIRECTORY_SEPARATOR.$namespace; + } else { + $directory .= \DIRECTORY_SEPARATOR.'@'; + } + if (!is_dir($directory)) { + @mkdir($directory, 0777, true); + } + $directory .= \DIRECTORY_SEPARATOR; + // On Windows the whole path is limited to 258 chars + if ('\\' === \DIRECTORY_SEPARATOR && \strlen($directory) > 234) { + throw new InvalidArgumentException(sprintf('Cache directory too long (%s).', $directory)); + } + + $this->directory = $directory; + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + $ok = true; + + foreach ($this->scanHashDir($this->directory) as $file) { + if ('' !== $namespace && !str_starts_with($this->getFileKey($file), $namespace)) { + continue; + } + + $ok = ($this->doUnlink($file) || !file_exists($file)) && $ok; + } + + return $ok; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + $ok = true; + + foreach ($ids as $id) { + $file = $this->getFile($id); + $ok = (!is_file($file) || $this->doUnlink($file) || !file_exists($file)) && $ok; + } + + return $ok; + } + + protected function doUnlink(string $file) + { + return @unlink($file); + } + + private function write(string $file, string $data, int $expiresAt = null) + { + set_error_handler(__CLASS__.'::throwError'); + try { + if (null === $this->tmp) { + $this->tmp = $this->directory.bin2hex(random_bytes(6)); + } + try { + $h = fopen($this->tmp, 'x'); + } catch (\ErrorException $e) { + if (!str_contains($e->getMessage(), 'File exists')) { + throw $e; + } + + $this->tmp = $this->directory.bin2hex(random_bytes(6)); + $h = fopen($this->tmp, 'x'); + } + fwrite($h, $data); + fclose($h); + + if (null !== $expiresAt) { + touch($this->tmp, $expiresAt ?: time() + 31556952); // 1 year in seconds + } + + return rename($this->tmp, $file); + } finally { + restore_error_handler(); + } + } + + private function getFile(string $id, bool $mkdir = false, string $directory = null) + { + // Use MD5 to favor speed over security, which is not an issue here + $hash = str_replace('/', '-', base64_encode(hash('md5', static::class.$id, true))); + $dir = ($directory ?? $this->directory).strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR); + + if ($mkdir && !is_dir($dir)) { + @mkdir($dir, 0777, true); + } + + return $dir.substr($hash, 2, 20); + } + + private function getFileKey(string $file): string + { + return ''; + } + + private function scanHashDir(string $directory): \Generator + { + if (!is_dir($directory)) { + return; + } + + $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + + for ($i = 0; $i < 38; ++$i) { + if (!is_dir($directory.$chars[$i])) { + continue; + } + + for ($j = 0; $j < 38; ++$j) { + if (!is_dir($dir = $directory.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) { + continue; + } + + foreach (@scandir($dir, \SCANDIR_SORT_NONE) ?: [] as $file) { + if ('.' !== $file && '..' !== $file) { + yield $dir.\DIRECTORY_SEPARATOR.$file; + } + } + } + } + } + + /** + * @internal + */ + public static function throwError(int $type, string $message, string $file, int $line) + { + throw new \ErrorException($message, 0, $type, $file, $line); + } + + /** + * @return array + */ + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + if (method_exists(parent::class, '__destruct')) { + parent::__destruct(); + } + if (null !== $this->tmp && is_file($this->tmp)) { + unlink($this->tmp); + } + } +} diff --git a/vendor/symfony/cache/Traits/FilesystemTrait.php b/vendor/symfony/cache/Traits/FilesystemTrait.php new file mode 100644 index 0000000..38b741f --- /dev/null +++ b/vendor/symfony/cache/Traits/FilesystemTrait.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\Exception\CacheException; + +/** + * @author Nicolas Grekas + * @author Rob Frawley 2nd + * + * @internal + */ +trait FilesystemTrait +{ + use FilesystemCommonTrait; + + private $marshaller; + + /** + * @return bool + */ + public function prune() + { + $time = time(); + $pruned = true; + + foreach ($this->scanHashDir($this->directory) as $file) { + if (!$h = @fopen($file, 'r')) { + continue; + } + + if (($expiresAt = (int) fgets($h)) && $time >= $expiresAt) { + fclose($h); + $pruned = @unlink($file) && !file_exists($file) && $pruned; + } else { + fclose($h); + } + } + + return $pruned; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + $values = []; + $now = time(); + + foreach ($ids as $id) { + $file = $this->getFile($id); + if (!is_file($file) || !$h = @fopen($file, 'r')) { + continue; + } + if (($expiresAt = (int) fgets($h)) && $now >= $expiresAt) { + fclose($h); + @unlink($file); + } else { + $i = rawurldecode(rtrim(fgets($h))); + $value = stream_get_contents($h); + fclose($h); + if ($i === $id) { + $values[$id] = $this->marshaller->unmarshall($value); + } + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + $file = $this->getFile($id); + + return is_file($file) && (@filemtime($file) > time() || $this->doFetch([$id])); + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + $expiresAt = $lifetime ? (time() + $lifetime) : 0; + $values = $this->marshaller->marshall($values, $failed); + + foreach ($values as $id => $value) { + if (!$this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".$value, $expiresAt)) { + $failed[] = $id; + } + } + + if ($failed && !is_writable($this->directory)) { + throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory)); + } + + return $failed; + } + + private function getFileKey(string $file): string + { + if (!$h = @fopen($file, 'r')) { + return ''; + } + + fgets($h); // expiry + $encodedKey = fgets($h); + fclose($h); + + return rawurldecode(rtrim($encodedKey)); + } +} diff --git a/vendor/symfony/cache/Traits/ProxyTrait.php b/vendor/symfony/cache/Traits/ProxyTrait.php new file mode 100644 index 0000000..c86f360 --- /dev/null +++ b/vendor/symfony/cache/Traits/ProxyTrait.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Symfony\Component\Cache\PruneableInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * @author Nicolas Grekas + * + * @internal + */ +trait ProxyTrait +{ + private $pool; + + /** + * {@inheritdoc} + */ + public function prune() + { + return $this->pool instanceof PruneableInterface && $this->pool->prune(); + } + + /** + * {@inheritdoc} + */ + public function reset() + { + if ($this->pool instanceof ResetInterface) { + $this->pool->reset(); + } + } +} diff --git a/vendor/symfony/cache/Traits/RedisClusterNodeProxy.php b/vendor/symfony/cache/Traits/RedisClusterNodeProxy.php new file mode 100644 index 0000000..deba74f --- /dev/null +++ b/vendor/symfony/cache/Traits/RedisClusterNodeProxy.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +/** + * This file acts as a wrapper to the \RedisCluster implementation so it can accept the same type of calls as + * individual \Redis objects. + * + * Calls are made to individual nodes via: RedisCluster->{method}($host, ...args)' + * according to https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#directed-node-commands + * + * @author Jack Thomas + * + * @internal + */ +class RedisClusterNodeProxy +{ + private $host; + private $redis; + + /** + * @param \RedisCluster|RedisClusterProxy $redis + */ + public function __construct(array $host, $redis) + { + $this->host = $host; + $this->redis = $redis; + } + + public function __call(string $method, array $args) + { + return $this->redis->{$method}($this->host, ...$args); + } + + public function scan(&$iIterator, $strPattern = null, $iCount = null) + { + return $this->redis->scan($iIterator, $this->host, $strPattern, $iCount); + } + + public function getOption($name) + { + return $this->redis->getOption($name); + } +} diff --git a/vendor/symfony/cache/Traits/RedisClusterProxy.php b/vendor/symfony/cache/Traits/RedisClusterProxy.php new file mode 100644 index 0000000..73c6a4f --- /dev/null +++ b/vendor/symfony/cache/Traits/RedisClusterProxy.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +/** + * @author Alessandro Chitolina + * + * @internal + */ +class RedisClusterProxy +{ + private $redis; + private $initializer; + + public function __construct(\Closure $initializer) + { + $this->initializer = $initializer; + } + + public function __call(string $method, array $args) + { + $this->redis ?: $this->redis = $this->initializer->__invoke(); + + return $this->redis->{$method}(...$args); + } + + public function hscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->redis ?: $this->redis = $this->initializer->__invoke(); + + return $this->redis->hscan($strKey, $iIterator, $strPattern, $iCount); + } + + public function scan(&$iIterator, $strPattern = null, $iCount = null) + { + $this->redis ?: $this->redis = $this->initializer->__invoke(); + + return $this->redis->scan($iIterator, $strPattern, $iCount); + } + + public function sscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->redis ?: $this->redis = $this->initializer->__invoke(); + + return $this->redis->sscan($strKey, $iIterator, $strPattern, $iCount); + } + + public function zscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->redis ?: $this->redis = $this->initializer->__invoke(); + + return $this->redis->zscan($strKey, $iIterator, $strPattern, $iCount); + } +} diff --git a/vendor/symfony/cache/Traits/RedisProxy.php b/vendor/symfony/cache/Traits/RedisProxy.php new file mode 100644 index 0000000..ec5cfab --- /dev/null +++ b/vendor/symfony/cache/Traits/RedisProxy.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class RedisProxy +{ + private $redis; + private $initializer; + private $ready = false; + + public function __construct(\Redis $redis, \Closure $initializer) + { + $this->redis = $redis; + $this->initializer = $initializer; + } + + public function __call(string $method, array $args) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->{$method}(...$args); + } + + public function hscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->hscan($strKey, $iIterator, $strPattern, $iCount); + } + + public function scan(&$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->scan($iIterator, $strPattern, $iCount); + } + + public function sscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->sscan($strKey, $iIterator, $strPattern, $iCount); + } + + public function zscan($strKey, &$iIterator, $strPattern = null, $iCount = null) + { + $this->ready ?: $this->ready = $this->initializer->__invoke($this->redis); + + return $this->redis->zscan($strKey, $iIterator, $strPattern, $iCount); + } +} diff --git a/vendor/symfony/cache/Traits/RedisTrait.php b/vendor/symfony/cache/Traits/RedisTrait.php new file mode 100644 index 0000000..accee44 --- /dev/null +++ b/vendor/symfony/cache/Traits/RedisTrait.php @@ -0,0 +1,603 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Cache\Traits; + +use Predis\Command\Redis\UNLINK; +use Predis\Connection\Aggregate\ClusterInterface; +use Predis\Connection\Aggregate\RedisCluster; +use Predis\Connection\Aggregate\ReplicationInterface; +use Predis\Response\ErrorInterface; +use Predis\Response\Status; +use Symfony\Component\Cache\Exception\CacheException; +use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Cache\Marshaller\DefaultMarshaller; +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Aurimas Niekis + * @author Nicolas Grekas + * + * @internal + */ +trait RedisTrait +{ + private static $defaultConnectionOptions = [ + 'class' => null, + 'persistent' => 0, + 'persistent_id' => null, + 'timeout' => 30, + 'read_timeout' => 0, + 'retry_interval' => 0, + 'tcp_keepalive' => 0, + 'lazy' => null, + 'redis_cluster' => false, + 'redis_sentinel' => null, + 'dbindex' => 0, + 'failover' => 'none', + 'ssl' => null, // see https://php.net/context.ssl + ]; + private $redis; + private $marshaller; + + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis + */ + private function init($redis, string $namespace, int $defaultLifetime, ?MarshallerInterface $marshaller) + { + parent::__construct($namespace, $defaultLifetime); + + if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { + throw new InvalidArgumentException(sprintf('RedisAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); + } + + if (!$redis instanceof \Redis && !$redis instanceof \RedisArray && !$redis instanceof \RedisCluster && !$redis instanceof \Predis\ClientInterface && !$redis instanceof RedisProxy && !$redis instanceof RedisClusterProxy) { + throw new InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, get_debug_type($redis))); + } + + if ($redis instanceof \Predis\ClientInterface && $redis->getOptions()->exceptions) { + $options = clone $redis->getOptions(); + \Closure::bind(function () { $this->options['exceptions'] = false; }, $options, $options)(); + $redis = new $redis($redis->getConnection(), $options); + } + + $this->redis = $redis; + $this->marshaller = $marshaller ?? new DefaultMarshaller(); + } + + /** + * Creates a Redis connection using a DSN configuration. + * + * Example DSN: + * - redis://localhost + * - redis://example.com:1234 + * - redis://secret@example.com/13 + * - redis:///var/run/redis.sock + * - redis://secret@/var/run/redis.sock/13 + * + * @param array $options See self::$defaultConnectionOptions + * + * @return \Redis|\RedisArray|\RedisCluster|RedisClusterProxy|RedisProxy|\Predis\ClientInterface According to the "class" option + * + * @throws InvalidArgumentException when the DSN is invalid + */ + public static function createConnection(string $dsn, array $options = []) + { + if (str_starts_with($dsn, 'redis:')) { + $scheme = 'redis'; + } elseif (str_starts_with($dsn, 'rediss:')) { + $scheme = 'rediss'; + } else { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s" does not start with "redis:" or "rediss".', $dsn)); + } + + if (!\extension_loaded('redis') && !class_exists(\Predis\Client::class)) { + throw new CacheException(sprintf('Cannot find the "redis" extension nor the "predis/predis" package: "%s".', $dsn)); + } + + $params = preg_replace_callback('#^'.$scheme.':(//)?(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) { + if (isset($m[2])) { + $auth = $m[2]; + + if ('' === $auth) { + $auth = null; + } + } + + return 'file:'.($m[1] ?? ''); + }, $dsn); + + if (false === $params = parse_url($params)) { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s".', $dsn)); + } + + $query = $hosts = []; + + $tls = 'rediss' === $scheme; + $tcpScheme = $tls ? 'tls' : 'tcp'; + + if (isset($params['query'])) { + parse_str($params['query'], $query); + + if (isset($query['host'])) { + if (!\is_array($hosts = $query['host'])) { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s".', $dsn)); + } + foreach ($hosts as $host => $parameters) { + if (\is_string($parameters)) { + parse_str($parameters, $parameters); + } + if (false === $i = strrpos($host, ':')) { + $hosts[$host] = ['scheme' => $tcpScheme, 'host' => $host, 'port' => 6379] + $parameters; + } elseif ($port = (int) substr($host, 1 + $i)) { + $hosts[$host] = ['scheme' => $tcpScheme, 'host' => substr($host, 0, $i), 'port' => $port] + $parameters; + } else { + $hosts[$host] = ['scheme' => 'unix', 'path' => substr($host, 0, $i)] + $parameters; + } + } + $hosts = array_values($hosts); + } + } + + if (isset($params['host']) || isset($params['path'])) { + if (!isset($params['dbindex']) && isset($params['path'])) { + if (preg_match('#/(\d+)$#', $params['path'], $m)) { + $params['dbindex'] = $m[1]; + $params['path'] = substr($params['path'], 0, -\strlen($m[0])); + } elseif (isset($params['host'])) { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s", the "dbindex" parameter must be a number.', $dsn)); + } + } + + if (isset($params['host'])) { + array_unshift($hosts, ['scheme' => $tcpScheme, 'host' => $params['host'], 'port' => $params['port'] ?? 6379]); + } else { + array_unshift($hosts, ['scheme' => 'unix', 'path' => $params['path']]); + } + } + + if (!$hosts) { + throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s".', $dsn)); + } + + $params += $query + $options + self::$defaultConnectionOptions; + + if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class) && !class_exists(\RedisSentinel::class)) { + throw new CacheException(sprintf('Redis Sentinel support requires the "predis/predis" package or the "redis" extension v5.2 or higher: "%s".', $dsn)); + } + + if ($params['redis_cluster'] && isset($params['redis_sentinel'])) { + throw new InvalidArgumentException(sprintf('Cannot use both "redis_cluster" and "redis_sentinel" at the same time: "%s".', $dsn)); + } + + if (null === $params['class'] && \extension_loaded('redis')) { + $class = $params['redis_cluster'] ? \RedisCluster::class : (1 < \count($hosts) && !isset($params['redis_sentinel']) ? \RedisArray::class : \Redis::class); + } else { + $class = $params['class'] ?? \Predis\Client::class; + + if (isset($params['redis_sentinel']) && !is_a($class, \Predis\Client::class, true) && !class_exists(\RedisSentinel::class)) { + throw new CacheException(sprintf('Cannot use Redis Sentinel: class "%s" does not extend "Predis\Client" and ext-redis >= 5.2 not found: "%s".', $class, $dsn)); + } + } + + if (is_a($class, \Redis::class, true)) { + $connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect'; + $redis = new $class(); + + $initializer = static function ($redis) use ($connect, $params, $dsn, $auth, $hosts, $tls) { + $hostIndex = 0; + do { + $host = $hosts[$hostIndex]['host'] ?? $hosts[$hostIndex]['path']; + $port = $hosts[$hostIndex]['port'] ?? 0; + $address = false; + + if (isset($hosts[$hostIndex]['host']) && $tls) { + $host = 'tls://'.$host; + } + + if (!isset($params['redis_sentinel'])) { + break; + } + + $sentinel = new \RedisSentinel($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout']); + + if ($address = $sentinel->getMasterAddrByName($params['redis_sentinel'])) { + [$host, $port] = $address; + } + } while (++$hostIndex < \count($hosts) && !$address); + + if (isset($params['redis_sentinel']) && !$address) { + throw new InvalidArgumentException(sprintf('Failed to retrieve master information from sentinel "%s" and dsn "%s".', $params['redis_sentinel'], $dsn)); + } + + try { + @$redis->{$connect}($host, $port, $params['timeout'], (string) $params['persistent_id'], $params['retry_interval'], $params['read_timeout'], ...\defined('Redis::SCAN_PREFIX') ? [['stream' => $params['ssl'] ?? null]] : []); + + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + try { + $isConnected = $redis->isConnected(); + } finally { + restore_error_handler(); + } + if (!$isConnected) { + $error = preg_match('/^Redis::p?connect\(\): (.*)/', $error, $error) ? sprintf(' (%s)', $error[1]) : ''; + throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: ', $dsn).$error.'.'); + } + + if ((null !== $auth && !$redis->auth($auth)) + || ($params['dbindex'] && !$redis->select($params['dbindex'])) + ) { + $e = preg_replace('/^ERR /', '', $redis->getLastError()); + throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: ', $dsn).$e.'.'); + } + + if (0 < $params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) { + $redis->setOption(\Redis::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']); + } + } catch (\RedisException $e) { + throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: ', $dsn).$e->getMessage()); + } + + return true; + }; + + if ($params['lazy']) { + $redis = new RedisProxy($redis, $initializer); + } else { + $initializer($redis); + } + } elseif (is_a($class, \RedisArray::class, true)) { + foreach ($hosts as $i => $host) { + switch ($host['scheme']) { + case 'tcp': $hosts[$i] = $host['host'].':'.$host['port']; break; + case 'tls': $hosts[$i] = 'tls://'.$host['host'].':'.$host['port']; break; + default: $hosts[$i] = $host['path']; + } + } + $params['lazy_connect'] = $params['lazy'] ?? true; + $params['connect_timeout'] = $params['timeout']; + + try { + $redis = new $class($hosts, $params); + } catch (\RedisClusterException $e) { + throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: ', $dsn).$e->getMessage()); + } + + if (0 < $params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) { + $redis->setOption(\Redis::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']); + } + } elseif (is_a($class, \RedisCluster::class, true)) { + $initializer = static function () use ($class, $params, $dsn, $hosts) { + foreach ($hosts as $i => $host) { + switch ($host['scheme']) { + case 'tcp': $hosts[$i] = $host['host'].':'.$host['port']; break; + case 'tls': $hosts[$i] = 'tls://'.$host['host'].':'.$host['port']; break; + default: $hosts[$i] = $host['path']; + } + } + + try { + $redis = new $class(null, $hosts, $params['timeout'], $params['read_timeout'], (bool) $params['persistent'], $params['auth'] ?? '', ...\defined('Redis::SCAN_PREFIX') ? [$params['ssl'] ?? null] : []); + } catch (\RedisClusterException $e) { + throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: ', $dsn).$e->getMessage()); + } + + if (0 < $params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) { + $redis->setOption(\Redis::OPT_TCP_KEEPALIVE, $params['tcp_keepalive']); + } + switch ($params['failover']) { + case 'error': $redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, \RedisCluster::FAILOVER_ERROR); break; + case 'distribute': $redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, \RedisCluster::FAILOVER_DISTRIBUTE); break; + case 'slaves': $redis->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, \RedisCluster::FAILOVER_DISTRIBUTE_SLAVES); break; + } + + return $redis; + }; + + $redis = $params['lazy'] ? new RedisClusterProxy($initializer) : $initializer(); + } elseif (is_a($class, \Predis\ClientInterface::class, true)) { + if ($params['redis_cluster']) { + $params['cluster'] = 'redis'; + } elseif (isset($params['redis_sentinel'])) { + $params['replication'] = 'sentinel'; + $params['service'] = $params['redis_sentinel']; + } + $params += ['parameters' => []]; + $params['parameters'] += [ + 'persistent' => $params['persistent'], + 'timeout' => $params['timeout'], + 'read_write_timeout' => $params['read_timeout'], + 'tcp_nodelay' => true, + ]; + if ($params['dbindex']) { + $params['parameters']['database'] = $params['dbindex']; + } + if (null !== $auth) { + $params['parameters']['password'] = $auth; + } + if (1 === \count($hosts) && !($params['redis_cluster'] || $params['redis_sentinel'])) { + $hosts = $hosts[0]; + } elseif (\in_array($params['failover'], ['slaves', 'distribute'], true) && !isset($params['replication'])) { + $params['replication'] = true; + $hosts[0] += ['alias' => 'master']; + } + $params['exceptions'] = false; + + $redis = new $class($hosts, array_diff_key($params, array_diff_key(self::$defaultConnectionOptions, ['ssl' => null]))); + if (isset($params['redis_sentinel'])) { + $redis->getConnection()->setSentinelTimeout($params['timeout']); + } + } elseif (class_exists($class, false)) { + throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis", "RedisArray", "RedisCluster" nor "Predis\ClientInterface".', $class)); + } else { + throw new InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); + } + + return $redis; + } + + /** + * {@inheritdoc} + */ + protected function doFetch(array $ids) + { + if (!$ids) { + return []; + } + + $result = []; + + if ($this->redis instanceof \Predis\ClientInterface && $this->redis->getConnection() instanceof ClusterInterface) { + $values = $this->pipeline(function () use ($ids) { + foreach ($ids as $id) { + yield 'get' => [$id]; + } + }); + } else { + $values = $this->redis->mget($ids); + + if (!\is_array($values) || \count($values) !== \count($ids)) { + return []; + } + + $values = array_combine($ids, $values); + } + + foreach ($values as $id => $v) { + if ($v) { + $result[$id] = $this->marshaller->unmarshall($v); + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + protected function doHave(string $id) + { + return (bool) $this->redis->exists($id); + } + + /** + * {@inheritdoc} + */ + protected function doClear(string $namespace) + { + if ($this->redis instanceof \Predis\ClientInterface) { + $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : ''; + $prefixLen = \strlen($prefix ?? ''); + } + + $cleared = true; + $hosts = $this->getHosts(); + $host = reset($hosts); + if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) { + // Predis supports info command only on the master in replication environments + $hosts = [$host->getClientFor('master')]; + } + + foreach ($hosts as $host) { + if (!isset($namespace[0])) { + $cleared = $host->flushDb() && $cleared; + continue; + } + + $info = $host->info('Server'); + $info = !$info instanceof ErrorInterface ? $info['Server'] ?? $info : ['redis_version' => '2.0']; + + if (!$host instanceof \Predis\ClientInterface) { + $prefix = \defined('Redis::SCAN_PREFIX') && (\Redis::SCAN_PREFIX & $host->getOption(\Redis::OPT_SCAN)) ? '' : $host->getOption(\Redis::OPT_PREFIX); + $prefixLen = \strlen($host->getOption(\Redis::OPT_PREFIX) ?? ''); + } + $pattern = $prefix.$namespace.'*'; + + if (!version_compare($info['redis_version'], '2.8', '>=')) { + // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS + // can hang your server when it is executed against large databases (millions of items). + // Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above. + $unlink = version_compare($info['redis_version'], '4.0', '>=') ? 'UNLINK' : 'DEL'; + $args = $this->redis instanceof \Predis\ClientInterface ? [0, $pattern] : [[$pattern], 0]; + $cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]) for i=1,#keys,5000 do redis.call('$unlink',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $args[0], $args[1]) && $cleared; + continue; + } + + $cursor = null; + do { + $keys = $host instanceof \Predis\ClientInterface ? $host->scan($cursor, 'MATCH', $pattern, 'COUNT', 1000) : $host->scan($cursor, $pattern, 1000); + if (isset($keys[1]) && \is_array($keys[1])) { + $cursor = $keys[0]; + $keys = $keys[1]; + } + if ($keys) { + if ($prefixLen) { + foreach ($keys as $i => $key) { + $keys[$i] = substr($key, $prefixLen); + } + } + $this->doDelete($keys); + } + } while ($cursor = (int) $cursor); + } + + return $cleared; + } + + /** + * {@inheritdoc} + */ + protected function doDelete(array $ids) + { + if (!$ids) { + return true; + } + + if ($this->redis instanceof \Predis\ClientInterface && $this->redis->getConnection() instanceof ClusterInterface) { + static $del; + $del = $del ?? (class_exists(UNLINK::class) ? 'unlink' : 'del'); + + $this->pipeline(function () use ($ids, $del) { + foreach ($ids as $id) { + yield $del => [$id]; + } + })->rewind(); + } else { + static $unlink = true; + + if ($unlink) { + try { + $unlink = false !== $this->redis->unlink($ids); + } catch (\Throwable $e) { + $unlink = false; + } + } + + if (!$unlink) { + $this->redis->del($ids); + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doSave(array $values, int $lifetime) + { + if (!$values = $this->marshaller->marshall($values, $failed)) { + return $failed; + } + + $results = $this->pipeline(function () use ($values, $lifetime) { + foreach ($values as $id => $value) { + if (0 >= $lifetime) { + yield 'set' => [$id, $value]; + } else { + yield 'setEx' => [$id, $lifetime, $value]; + } + } + }); + + foreach ($results as $id => $result) { + if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) { + $failed[] = $id; + } + } + + return $failed; + } + + private function pipeline(\Closure $generator, object $redis = null): \Generator + { + $ids = []; + $redis = $redis ?? $this->redis; + + if ($redis instanceof RedisClusterProxy || $redis instanceof \RedisCluster || ($redis instanceof \Predis\ClientInterface && $redis->getConnection() instanceof RedisCluster)) { + // phpredis & predis don't support pipelining with RedisCluster + // see https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#pipelining + // see https://github.com/nrk/predis/issues/267#issuecomment-123781423 + $results = []; + foreach ($generator() as $command => $args) { + $results[] = $redis->{$command}(...$args); + $ids[] = 'eval' === $command ? ($redis instanceof \Predis\ClientInterface ? $args[2] : $args[1][0]) : $args[0]; + } + } elseif ($redis instanceof \Predis\ClientInterface) { + $results = $redis->pipeline(static function ($redis) use ($generator, &$ids) { + foreach ($generator() as $command => $args) { + $redis->{$command}(...$args); + $ids[] = 'eval' === $command ? $args[2] : $args[0]; + } + }); + } elseif ($redis instanceof \RedisArray) { + $connections = $results = $ids = []; + foreach ($generator() as $command => $args) { + $id = 'eval' === $command ? $args[1][0] : $args[0]; + if (!isset($connections[$h = $redis->_target($id)])) { + $connections[$h] = [$redis->_instance($h), -1]; + $connections[$h][0]->multi(\Redis::PIPELINE); + } + $connections[$h][0]->{$command}(...$args); + $results[] = [$h, ++$connections[$h][1]]; + $ids[] = $id; + } + foreach ($connections as $h => $c) { + $connections[$h] = $c[0]->exec(); + } + foreach ($results as $k => [$h, $c]) { + $results[$k] = $connections[$h][$c]; + } + } else { + $redis->multi(\Redis::PIPELINE); + foreach ($generator() as $command => $args) { + $redis->{$command}(...$args); + $ids[] = 'eval' === $command ? $args[1][0] : $args[0]; + } + $results = $redis->exec(); + } + + if (!$redis instanceof \Predis\ClientInterface && 'eval' === $command && $redis->getLastError()) { + $e = new \RedisException($redis->getLastError()); + $results = array_map(function ($v) use ($e) { return false === $v ? $e : $v; }, $results); + } + + foreach ($ids as $k => $id) { + yield $id => $results[$k]; + } + } + + private function getHosts(): array + { + $hosts = [$this->redis]; + if ($this->redis instanceof \Predis\ClientInterface) { + $connection = $this->redis->getConnection(); + if ($connection instanceof ClusterInterface && $connection instanceof \Traversable) { + $hosts = []; + foreach ($connection as $c) { + $hosts[] = new \Predis\Client($c); + } + } + } elseif ($this->redis instanceof \RedisArray) { + $hosts = []; + foreach ($this->redis->_hosts() as $host) { + $hosts[] = $this->redis->_instance($host); + } + } elseif ($this->redis instanceof RedisClusterProxy || $this->redis instanceof \RedisCluster) { + $hosts = []; + foreach ($this->redis->_masters() as $host) { + $hosts[] = new RedisClusterNodeProxy($host, $this->redis); + } + } + + return $hosts; + } +} diff --git a/vendor/symfony/cache/composer.json b/vendor/symfony/cache/composer.json new file mode 100644 index 0000000..1ebcfc9 --- /dev/null +++ b/vendor/symfony/cache/composer.json @@ -0,0 +1,60 @@ +{ + "name": "symfony/cache", + "type": "library", + "description": "Provides an extended PSR-6, PSR-16 (and tags) implementation", + "keywords": ["caching", "psr6"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "provide": { + "psr/cache-implementation": "1.0|2.0", + "psr/simple-cache-implementation": "1.0|2.0", + "symfony/cache-implementation": "1.0|2.0" + }, + "require": { + "php": ">=7.2.5", + "psr/cache": "^1.0|^2.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^1.1.7|^2", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/cache": "^1.6|^2.0", + "doctrine/dbal": "^2.13.1|^3.0", + "predis/predis": "^1.1", + "psr/simple-cache": "^1.0|^2.0", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<4.4", + "symfony/http-kernel": "<4.4", + "symfony/var-dumper": "<4.4" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\Cache\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/vendor/symfony/event-dispatcher-contracts/.gitignore b/vendor/symfony/event-dispatcher-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/vendor/symfony/event-dispatcher-contracts/CHANGELOG.md b/vendor/symfony/event-dispatcher-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/vendor/symfony/event-dispatcher-contracts/Event.php b/vendor/symfony/event-dispatcher-contracts/Event.php new file mode 100644 index 0000000..46dcb2b --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/Event.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\EventDispatcher; + +use Psr\EventDispatcher\StoppableEventInterface; + +/** + * Event is the base class for classes containing event data. + * + * This class contains no event data. It is used by events that do not pass + * state information to an event handler when an event is raised. + * + * You can call the method stopPropagation() to abort the execution of + * further listeners in your event listener. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + * @author Nicolas Grekas + */ +class Event implements StoppableEventInterface +{ + private $propagationStopped = false; + + /** + * {@inheritdoc} + */ + public function isPropagationStopped(): bool + { + return $this->propagationStopped; + } + + /** + * Stops the propagation of the event to further event listeners. + * + * If multiple event listeners are connected to the same event, no + * further event listener will be triggered once any trigger calls + * stopPropagation(). + */ + public function stopPropagation(): void + { + $this->propagationStopped = true; + } +} diff --git a/vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php b/vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php new file mode 100644 index 0000000..351dc51 --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/EventDispatcherInterface.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\EventDispatcher; + +use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface; + +/** + * Allows providing hooks on domain-specific lifecycles by dispatching events. + */ +interface EventDispatcherInterface extends PsrEventDispatcherInterface +{ + /** + * Dispatches an event to all registered listeners. + * + * @param object $event The event to pass to the event handlers/listeners + * @param string|null $eventName The name of the event to dispatch. If not supplied, + * the class of $event should be used instead. + * + * @return object The passed $event MUST be returned + */ + public function dispatch(object $event, string $eventName = null): object; +} diff --git a/vendor/symfony/event-dispatcher-contracts/LICENSE b/vendor/symfony/event-dispatcher-contracts/LICENSE new file mode 100644 index 0000000..74cdc2d --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/event-dispatcher-contracts/README.md b/vendor/symfony/event-dispatcher-contracts/README.md new file mode 100644 index 0000000..b1ab4c0 --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/README.md @@ -0,0 +1,9 @@ +Symfony EventDispatcher Contracts +================================= + +A set of abstractions extracted out of the Symfony components. + +Can be used to build on semantics that the Symfony components proved useful - and +that already have battle tested implementations. + +See https://github.com/symfony/contracts/blob/main/README.md for more information. diff --git a/vendor/symfony/event-dispatcher-contracts/composer.json b/vendor/symfony/event-dispatcher-contracts/composer.json new file mode 100644 index 0000000..660df81 --- /dev/null +++ b/vendor/symfony/event-dispatcher-contracts/composer.json @@ -0,0 +1,38 @@ +{ + "name": "symfony/event-dispatcher-contracts", + "type": "library", + "description": "Generic abstractions related to dispatching event", + "keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" + }, + "suggest": { + "symfony/event-dispatcher-implementation": "" + }, + "autoload": { + "psr-4": { "Symfony\\Contracts\\EventDispatcher\\": "" } + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + } +} diff --git a/vendor/symfony/event-dispatcher/Attribute/AsEventListener.php b/vendor/symfony/event-dispatcher/Attribute/AsEventListener.php new file mode 100644 index 0000000..bb931b8 --- /dev/null +++ b/vendor/symfony/event-dispatcher/Attribute/AsEventListener.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Attribute; + +/** + * Service tag to autoconfigure event listeners. + * + * @author Alexander M. Turek + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class AsEventListener +{ + public function __construct( + public ?string $event = null, + public ?string $method = null, + public int $priority = 0, + public ?string $dispatcher = null, + ) { + } +} diff --git a/vendor/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php b/vendor/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php new file mode 100644 index 0000000..6e7292b --- /dev/null +++ b/vendor/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * This pass allows bundles to extend the list of event aliases. + * + * @author Alexander M. Turek + */ +class AddEventAliasesPass implements CompilerPassInterface +{ + private $eventAliases; + private $eventAliasesParameter; + + public function __construct(array $eventAliases, string $eventAliasesParameter = 'event_dispatcher.event_aliases') + { + if (1 < \func_num_args()) { + trigger_deprecation('symfony/event-dispatcher', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + + $this->eventAliases = $eventAliases; + $this->eventAliasesParameter = $eventAliasesParameter; + } + + public function process(ContainerBuilder $container): void + { + $eventAliases = $container->hasParameter($this->eventAliasesParameter) ? $container->getParameter($this->eventAliasesParameter) : []; + + $container->setParameter( + $this->eventAliasesParameter, + array_merge($eventAliases, $this->eventAliases) + ); + } +} diff --git a/vendor/symfony/event-dispatcher/LegacyEventDispatcherProxy.php b/vendor/symfony/event-dispatcher/LegacyEventDispatcherProxy.php new file mode 100644 index 0000000..6e17c8f --- /dev/null +++ b/vendor/symfony/event-dispatcher/LegacyEventDispatcherProxy.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; + +trigger_deprecation('symfony/event-dispatcher', '5.1', '%s is deprecated, use the event dispatcher without the proxy.', LegacyEventDispatcherProxy::class); + +/** + * A helper class to provide BC/FC with the legacy signature of EventDispatcherInterface::dispatch(). + * + * @author Nicolas Grekas + * + * @deprecated since Symfony 5.1 + */ +final class LegacyEventDispatcherProxy +{ + public static function decorate(?EventDispatcherInterface $dispatcher): ?EventDispatcherInterface + { + return $dispatcher; + } +} diff --git a/vendor/symfony/finder/Comparator/Comparator.php b/vendor/symfony/finder/Comparator/Comparator.php index 6aee21c..3af551f 100644 --- a/vendor/symfony/finder/Comparator/Comparator.php +++ b/vendor/symfony/finder/Comparator/Comparator.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Finder\Comparator; /** - * Comparator. - * * @author Fabien Potencier */ class Comparator @@ -21,30 +19,44 @@ class Comparator private $target; private $operator = '=='; + public function __construct(string $target = null, string $operator = '==') + { + if (null === $target) { + trigger_deprecation('symfony/finder', '5.4', 'Constructing a "%s" without setting "$target" is deprecated.', __CLASS__); + } + + $this->target = $target; + $this->doSetOperator($operator); + } + /** * Gets the target value. * - * @return string The target value + * @return string */ public function getTarget() { + if (null === $this->target) { + trigger_deprecation('symfony/finder', '5.4', 'Calling "%s" without initializing the target is deprecated.', __METHOD__); + } + return $this->target; } /** - * Sets the target value. - * - * @param string $target The target value + * @deprecated set the target via the constructor instead */ - public function setTarget($target) + public function setTarget(string $target) { + trigger_deprecation('symfony/finder', '5.4', '"%s" is deprecated. Set the target via the constructor instead.', __METHOD__); + $this->target = $target; } /** * Gets the comparison operator. * - * @return string The operator + * @return string */ public function getOperator() { @@ -54,21 +66,15 @@ class Comparator /** * Sets the comparison operator. * - * @param string $operator A valid operator - * * @throws \InvalidArgumentException + * + * @deprecated set the operator via the constructor instead */ - public function setOperator($operator) + public function setOperator(string $operator) { - if (!$operator) { - $operator = '=='; - } + trigger_deprecation('symfony/finder', '5.4', '"%s" is deprecated. Set the operator via the constructor instead.', __METHOD__); - if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) { - throw new \InvalidArgumentException(sprintf('Invalid operator "%s".', $operator)); - } - - $this->operator = $operator; + $this->doSetOperator('' === $operator ? '==' : $operator); } /** @@ -80,6 +86,10 @@ class Comparator */ public function test($test) { + if (null === $this->target) { + trigger_deprecation('symfony/finder', '5.4', 'Calling "%s" without initializing the target is deprecated.', __METHOD__); + } + switch ($this->operator) { case '>': return $test > $this->target; @@ -95,4 +105,13 @@ class Comparator return $test == $this->target; } + + private function doSetOperator(string $operator): void + { + if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) { + throw new \InvalidArgumentException(sprintf('Invalid operator "%s".', $operator)); + } + + $this->operator = $operator; + } } diff --git a/vendor/symfony/finder/Comparator/DateComparator.php b/vendor/symfony/finder/Comparator/DateComparator.php index d17c77a..8f651e1 100644 --- a/vendor/symfony/finder/Comparator/DateComparator.php +++ b/vendor/symfony/finder/Comparator/DateComparator.php @@ -36,7 +36,7 @@ class DateComparator extends Comparator throw new \InvalidArgumentException(sprintf('"%s" is not a valid date.', $matches[2])); } - $operator = isset($matches[1]) ? $matches[1] : '=='; + $operator = $matches[1] ?? '=='; if ('since' === $operator || 'after' === $operator) { $operator = '>'; } @@ -45,7 +45,6 @@ class DateComparator extends Comparator $operator = '<'; } - $this->setOperator($operator); - $this->setTarget($target); + parent::__construct($target, $operator); } } diff --git a/vendor/symfony/finder/Comparator/NumberComparator.php b/vendor/symfony/finder/Comparator/NumberComparator.php index 80667c9..ff85d96 100644 --- a/vendor/symfony/finder/Comparator/NumberComparator.php +++ b/vendor/symfony/finder/Comparator/NumberComparator.php @@ -41,8 +41,8 @@ class NumberComparator extends Comparator */ public function __construct(?string $test) { - if (!preg_match('#^\s*(==|!=|[<>]=?)?\s*([0-9\.]+)\s*([kmg]i?)?\s*$#i', $test, $matches)) { - throw new \InvalidArgumentException(sprintf('Don\'t understand "%s" as a number test.', $test)); + if (null === $test || !preg_match('#^\s*(==|!=|[<>]=?)?\s*([0-9\.]+)\s*([kmg]i?)?\s*$#i', $test, $matches)) { + throw new \InvalidArgumentException(sprintf('Don\'t understand "%s" as a number test.', $test ?? 'null')); } $target = $matches[2]; @@ -73,7 +73,6 @@ class NumberComparator extends Comparator } } - $this->setTarget($target); - $this->setOperator(isset($matches[1]) ? $matches[1] : '=='); + parent::__construct($target, $matches[1] ?: '=='); } } diff --git a/vendor/symfony/finder/Iterator/LazyIterator.php b/vendor/symfony/finder/Iterator/LazyIterator.php new file mode 100644 index 0000000..32cc37f --- /dev/null +++ b/vendor/symfony/finder/Iterator/LazyIterator.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +/** + * @author Jérémy Derussé + * + * @internal + */ +class LazyIterator implements \IteratorAggregate +{ + private $iteratorFactory; + + public function __construct(callable $iteratorFactory) + { + $this->iteratorFactory = $iteratorFactory; + } + + public function getIterator(): \Traversable + { + yield from ($this->iteratorFactory)(); + } +} diff --git a/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php b/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php new file mode 100644 index 0000000..e27158c --- /dev/null +++ b/vendor/symfony/finder/Iterator/VcsIgnoredFilterIterator.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Iterator; + +use Symfony\Component\Finder\Gitignore; + +final class VcsIgnoredFilterIterator extends \FilterIterator +{ + /** + * @var string + */ + private $baseDir; + + /** + * @var array + */ + private $gitignoreFilesCache = []; + + /** + * @var array + */ + private $ignoredPathsCache = []; + + public function __construct(\Iterator $iterator, string $baseDir) + { + $this->baseDir = $this->normalizePath($baseDir); + + parent::__construct($iterator); + } + + public function accept(): bool + { + $file = $this->current(); + + $fileRealPath = $this->normalizePath($file->getRealPath()); + + return !$this->isIgnored($fileRealPath); + } + + private function isIgnored(string $fileRealPath): bool + { + if (is_dir($fileRealPath) && !str_ends_with($fileRealPath, '/')) { + $fileRealPath .= '/'; + } + + if (isset($this->ignoredPathsCache[$fileRealPath])) { + return $this->ignoredPathsCache[$fileRealPath]; + } + + $ignored = false; + + foreach ($this->parentsDirectoryDownward($fileRealPath) as $parentDirectory) { + if ($this->isIgnored($parentDirectory)) { + // rules in ignored directories are ignored, no need to check further. + break; + } + + $fileRelativePath = substr($fileRealPath, \strlen($parentDirectory) + 1); + + if (null === $regexps = $this->readGitignoreFile("{$parentDirectory}/.gitignore")) { + continue; + } + + [$exclusionRegex, $inclusionRegex] = $regexps; + + if (preg_match($exclusionRegex, $fileRelativePath)) { + $ignored = true; + + continue; + } + + if (preg_match($inclusionRegex, $fileRelativePath)) { + $ignored = false; + } + } + + return $this->ignoredPathsCache[$fileRealPath] = $ignored; + } + + /** + * @return list + */ + private function parentsDirectoryDownward(string $fileRealPath): array + { + $parentDirectories = []; + + $parentDirectory = $fileRealPath; + + while (true) { + $newParentDirectory = \dirname($parentDirectory); + + // dirname('/') = '/' + if ($newParentDirectory === $parentDirectory) { + break; + } + + $parentDirectory = $newParentDirectory; + + if (0 !== strpos($parentDirectory, $this->baseDir)) { + break; + } + + $parentDirectories[] = $parentDirectory; + } + + return array_reverse($parentDirectories); + } + + /** + * @return array{0: string, 1: string}|null + */ + private function readGitignoreFile(string $path): ?array + { + if (\array_key_exists($path, $this->gitignoreFilesCache)) { + return $this->gitignoreFilesCache[$path]; + } + + if (!file_exists($path)) { + return $this->gitignoreFilesCache[$path] = null; + } + + if (!is_file($path) || !is_readable($path)) { + throw new \RuntimeException("The \"ignoreVCSIgnored\" option cannot be used by the Finder as the \"{$path}\" file is not readable."); + } + + $gitignoreFileContent = file_get_contents($path); + + return $this->gitignoreFilesCache[$path] = [ + Gitignore::toRegex($gitignoreFileContent), + Gitignore::toRegexMatchingNegatedPatterns($gitignoreFileContent), + ]; + } + + private function normalizePath(string $path): string + { + if ('\\' === \DIRECTORY_SEPARATOR) { + return str_replace('\\', '/', $path); + } + + return $path; + } +} diff --git a/vendor/symfony/http-client/Internal/CurlClientState.php b/vendor/symfony/http-client/Internal/CurlClientState.php index 7d51c15..80473fe 100644 --- a/vendor/symfony/http-client/Internal/CurlClientState.php +++ b/vendor/symfony/http-client/Internal/CurlClientState.php @@ -36,6 +36,7 @@ final class CurlClientState extends ClientState public $execCounter = \PHP_INT_MIN; /** @var LoggerInterface|null */ public $logger; + public $performing = false; public static $curlVersion; diff --git a/vendor/symfony/http-client/Internal/HttplugWaitLoop.php b/vendor/symfony/http-client/Internal/HttplugWaitLoop.php index 9f5658f..c61be22 100644 --- a/vendor/symfony/http-client/Internal/HttplugWaitLoop.php +++ b/vendor/symfony/http-client/Internal/HttplugWaitLoop.php @@ -120,7 +120,11 @@ final class HttplugWaitLoop foreach ($response->getHeaders(false) as $name => $values) { foreach ($values as $value) { - $psrResponse = $psrResponse->withAddedHeader($name, $value); + try { + $psrResponse = $psrResponse->withAddedHeader($name, $value); + } catch (\InvalidArgumentException $e) { + // ignore invalid header + } } } diff --git a/vendor/symfony/http-client/Response/AmpResponse.php b/vendor/symfony/http-client/Response/AmpResponse.php index 6d0ce6e..900c70d 100644 --- a/vendor/symfony/http-client/Response/AmpResponse.php +++ b/vendor/symfony/http-client/Response/AmpResponse.php @@ -47,7 +47,6 @@ final class AmpResponse implements ResponseInterface, StreamableInterface private $multi; private $options; - private $canceller; private $onProgress; private static $delay; @@ -73,7 +72,7 @@ final class AmpResponse implements ResponseInterface, StreamableInterface $info = &$this->info; $headers = &$this->headers; - $canceller = $this->canceller = new CancellationTokenSource(); + $canceller = new CancellationTokenSource(); $handle = &$this->handle; $info['url'] = (string) $request->getUri(); @@ -358,7 +357,7 @@ final class AmpResponse implements ResponseInterface, StreamableInterface } foreach ($originRequest->getRawHeaders() as [$name, $value]) { - $request->setHeader($name, $value); + $request->addHeader($name, $value); } if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) { diff --git a/vendor/symfony/http-client/Response/CurlResponse.php b/vendor/symfony/http-client/Response/CurlResponse.php index b03a49a..2418203 100644 --- a/vendor/symfony/http-client/Response/CurlResponse.php +++ b/vendor/symfony/http-client/Response/CurlResponse.php @@ -32,7 +32,6 @@ final class CurlResponse implements ResponseInterface, StreamableInterface } use TransportResponseTrait; - private static $performing = false; private $multi; private $debugBuffer; @@ -77,17 +76,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface } curl_setopt($ch, \CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int { - if (0 !== substr_compare($data, "\r\n", -2)) { - return 0; - } - - $len = 0; - - foreach (explode("\r\n", substr($data, 0, -2)) as $data) { - $len += 2 + self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); - } - - return $len; + return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); }); if (null === $options) { @@ -179,7 +168,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface unset($multi->pauseExpiries[$id], $multi->openHandles[$id], $multi->handlesActivity[$id]); curl_setopt($ch, \CURLOPT_PRIVATE, '_0'); - if (self::$performing) { + if ($multi->performing) { return; } @@ -237,13 +226,13 @@ final class CurlResponse implements ResponseInterface, StreamableInterface */ public function getContent(bool $throw = true): string { - $performing = self::$performing; - self::$performing = $performing || '_0' === curl_getinfo($this->handle, \CURLINFO_PRIVATE); + $performing = $this->multi->performing; + $this->multi->performing = $performing || '_0' === curl_getinfo($this->handle, \CURLINFO_PRIVATE); try { return $this->doGetContent($throw); } finally { - self::$performing = $performing; + $this->multi->performing = $performing; } } @@ -287,7 +276,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface */ private static function perform(ClientState $multi, array &$responses = null): void { - if (self::$performing) { + if ($multi->performing) { if ($responses) { $response = current($responses); $multi->handlesActivity[(int) $response->handle][] = null; @@ -298,7 +287,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface } try { - self::$performing = true; + $multi->performing = true; ++$multi->execCounter; $active = 0; while (\CURLM_CALL_MULTI_PERFORM === ($err = curl_multi_exec($multi->handle, $active))) { @@ -335,7 +324,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface $multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL))); } } finally { - self::$performing = false; + $multi->performing = false; } } @@ -382,19 +371,29 @@ final class CurlResponse implements ResponseInterface, StreamableInterface */ private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int { + if (!str_ends_with($data, "\r\n")) { + return 0; + } + $waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0'; if ('H' !== $waitFor[0]) { return \strlen($data); // Ignore HTTP trailers } - if ('' !== $data) { + $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE); + + if ($statusCode !== $info['http_code'] && !preg_match("#^HTTP/\d+(?:\.\d+)? {$statusCode}(?: |\r\n$)#", $data)) { + return \strlen($data); // Ignore headers from responses to CONNECT requests + } + + if ("\r\n" !== $data) { // Regular header line: add it to the list - self::addResponseHeaders([$data], $info, $headers); + self::addResponseHeaders([substr($data, 0, -2)], $info, $headers); if (!str_starts_with($data, 'HTTP/')) { if (0 === stripos($data, 'Location:')) { - $location = trim(substr($data, 9)); + $location = trim(substr($data, 9, -2)); } return \strlen($data); @@ -417,7 +416,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface // End of headers: handle informational responses, redirects, etc. - if (200 > $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE)) { + if (200 > $statusCode) { $multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers); $location = null; diff --git a/vendor/symfony/http-client/Response/MockResponse.php b/vendor/symfony/http-client/Response/MockResponse.php index 6420aa0..2c00108 100644 --- a/vendor/symfony/http-client/Response/MockResponse.php +++ b/vendor/symfony/http-client/Response/MockResponse.php @@ -110,6 +110,10 @@ class MockResponse implements ResponseInterface, StreamableInterface } catch (TransportException $e) { // ignore errors when canceling } + + $onProgress = $this->requestOptions['on_progress'] ?? static function () {}; + $dlSize = isset($this->headers['content-encoding']) || 'HEAD' === ($this->info['http_method'] ?? null) || \in_array($this->info['http_code'], [204, 304], true) ? 0 : (int) ($this->headers['content-length'][0] ?? 0); + $onProgress($this->offset, $dlSize, $this->info); } /** diff --git a/vendor/symfony/http-client/Retry/GenericRetryStrategy.php b/vendor/symfony/http-client/Retry/GenericRetryStrategy.php index ebe10a2..3241a5e 100644 --- a/vendor/symfony/http-client/Retry/GenericRetryStrategy.php +++ b/vendor/symfony/http-client/Retry/GenericRetryStrategy.php @@ -102,7 +102,7 @@ class GenericRetryStrategy implements RetryStrategyInterface $delay = $this->delayMs * $this->multiplier ** $context->getInfo('retry_count'); if ($this->jitter > 0) { - $randomness = $delay * $this->jitter; + $randomness = (int) ($delay * $this->jitter); $delay = $delay + random_int(-$randomness, +$randomness); } diff --git a/vendor/symfony/http-foundation/Exception/BadRequestException.php b/vendor/symfony/http-foundation/Exception/BadRequestException.php new file mode 100644 index 0000000..e4bb309 --- /dev/null +++ b/vendor/symfony/http-foundation/Exception/BadRequestException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Raised when a user sends a malformed request. + */ +class BadRequestException extends \UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/vendor/symfony/http-foundation/Exception/JsonException.php b/vendor/symfony/http-foundation/Exception/JsonException.php new file mode 100644 index 0000000..5990e76 --- /dev/null +++ b/vendor/symfony/http-foundation/Exception/JsonException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Thrown by Request::toArray() when the content cannot be JSON-decoded. + * + * @author Tobias Nyholm + */ +final class JsonException extends \UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/vendor/symfony/http-foundation/Exception/SessionNotFoundException.php b/vendor/symfony/http-foundation/Exception/SessionNotFoundException.php new file mode 100644 index 0000000..9c719aa --- /dev/null +++ b/vendor/symfony/http-foundation/Exception/SessionNotFoundException.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Raised when a session does not exists. This happens in the following cases: + * - the session is not enabled + * - attempt to read a session outside a request context (ie. cli script). + * + * @author Jérémy Derussé + */ +class SessionNotFoundException extends \LogicException implements RequestExceptionInterface +{ + public function __construct(string $message = 'There is currently no session available.', int $code = 0, \Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/vendor/symfony/http-foundation/File/Exception/AccessDeniedException.php b/vendor/symfony/http-foundation/File/Exception/AccessDeniedException.php index 3b8e41d..136d2a9 100644 --- a/vendor/symfony/http-foundation/File/Exception/AccessDeniedException.php +++ b/vendor/symfony/http-foundation/File/Exception/AccessDeniedException.php @@ -18,10 +18,7 @@ namespace Symfony\Component\HttpFoundation\File\Exception; */ class AccessDeniedException extends FileException { - /** - * @param string $path The path to the accessed file - */ - public function __construct($path) + public function __construct(string $path) { parent::__construct(sprintf('The file %s could not be accessed', $path)); } diff --git a/vendor/symfony/http-foundation/File/Exception/CannotWriteFileException.php b/vendor/symfony/http-foundation/File/Exception/CannotWriteFileException.php new file mode 100644 index 0000000..c49f53a --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/CannotWriteFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_CANT_WRITE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class CannotWriteFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/ExtensionFileException.php b/vendor/symfony/http-foundation/File/Exception/ExtensionFileException.php new file mode 100644 index 0000000..ed83499 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/ExtensionFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_EXTENSION error occurred with UploadedFile. + * + * @author Florent Mata + */ +class ExtensionFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/FileNotFoundException.php b/vendor/symfony/http-foundation/File/Exception/FileNotFoundException.php index bfcc37e..31bdf68 100644 --- a/vendor/symfony/http-foundation/File/Exception/FileNotFoundException.php +++ b/vendor/symfony/http-foundation/File/Exception/FileNotFoundException.php @@ -18,10 +18,7 @@ namespace Symfony\Component\HttpFoundation\File\Exception; */ class FileNotFoundException extends FileException { - /** - * @param string $path The path to the file that was not found - */ - public function __construct($path) + public function __construct(string $path) { parent::__construct(sprintf('The file "%s" does not exist', $path)); } diff --git a/vendor/symfony/http-foundation/File/Exception/FormSizeFileException.php b/vendor/symfony/http-foundation/File/Exception/FormSizeFileException.php new file mode 100644 index 0000000..8741be0 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/FormSizeFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_FORM_SIZE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class FormSizeFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/IniSizeFileException.php b/vendor/symfony/http-foundation/File/Exception/IniSizeFileException.php new file mode 100644 index 0000000..c8fde61 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/IniSizeFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_INI_SIZE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class IniSizeFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/NoFileException.php b/vendor/symfony/http-foundation/File/Exception/NoFileException.php new file mode 100644 index 0000000..4b48cc7 --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/NoFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_NO_FILE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class NoFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/NoTmpDirFileException.php b/vendor/symfony/http-foundation/File/Exception/NoTmpDirFileException.php new file mode 100644 index 0000000..bdead2d --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/NoTmpDirFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_NO_TMP_DIR error occurred with UploadedFile. + * + * @author Florent Mata + */ +class NoTmpDirFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/PartialFileException.php b/vendor/symfony/http-foundation/File/Exception/PartialFileException.php new file mode 100644 index 0000000..4641efb --- /dev/null +++ b/vendor/symfony/http-foundation/File/Exception/PartialFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_PARTIAL error occurred with UploadedFile. + * + * @author Florent Mata + */ +class PartialFileException extends FileException +{ +} diff --git a/vendor/symfony/http-foundation/File/Exception/UnexpectedTypeException.php b/vendor/symfony/http-foundation/File/Exception/UnexpectedTypeException.php index 62005d3..8533f99 100644 --- a/vendor/symfony/http-foundation/File/Exception/UnexpectedTypeException.php +++ b/vendor/symfony/http-foundation/File/Exception/UnexpectedTypeException.php @@ -13,8 +13,8 @@ namespace Symfony\Component\HttpFoundation\File\Exception; class UnexpectedTypeException extends FileException { - public function __construct($value, $expectedType) + public function __construct($value, string $expectedType) { - parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, \is_object($value) ? \get_class($value) : \gettype($value))); + parent::__construct(sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value))); } } diff --git a/vendor/symfony/http-foundation/File/File.php b/vendor/symfony/http-foundation/File/File.php index 3422058..d941577 100644 --- a/vendor/symfony/http-foundation/File/File.php +++ b/vendor/symfony/http-foundation/File/File.php @@ -13,8 +13,7 @@ namespace Symfony\Component\HttpFoundation\File; use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; -use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser; -use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesser; +use Symfony\Component\Mime\MimeTypes; /** * A file in the file system. @@ -31,7 +30,7 @@ class File extends \SplFileInfo * * @throws FileNotFoundException If the given path is not a file */ - public function __construct($path, $checkPath = true) + public function __construct(string $path, bool $checkPath = true) { if ($checkPath && !is_file($path)) { throw new FileNotFoundException($path); @@ -48,56 +47,59 @@ class File extends \SplFileInfo * This method uses the mime type as guessed by getMimeType() * to guess the file extension. * - * @return string|null The guessed extension or null if it cannot be guessed + * @return string|null * - * @see ExtensionGuesser + * @see MimeTypes * @see getMimeType() */ public function guessExtension() { - $type = $this->getMimeType(); - $guesser = ExtensionGuesser::getInstance(); + if (!class_exists(MimeTypes::class)) { + throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); + } - return $guesser->guess($type); + return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null; } /** * Returns the mime type of the file. * - * The mime type is guessed using a MimeTypeGuesser instance, which uses finfo(), - * mime_content_type() and the system binary "file" (in this order), depending on - * which of those are available. + * The mime type is guessed using a MimeTypeGuesserInterface instance, + * which uses finfo_file() then the "file" system binary, + * depending on which of those are available. * - * @return string|null The guessed mime type (e.g. "application/pdf") + * @return string|null * - * @see MimeTypeGuesser + * @see MimeTypes */ public function getMimeType() { - $guesser = MimeTypeGuesser::getInstance(); + if (!class_exists(MimeTypes::class)) { + throw new \LogicException('You cannot guess the mime type as the Mime component is not installed. Try running "composer require symfony/mime".'); + } - return $guesser->guess($this->getPathname()); + return MimeTypes::getDefault()->guessMimeType($this->getPathname()); } /** * Moves the file to a new location. * - * @param string $directory The destination folder - * @param string $name The new file name - * - * @return self A File object representing the new file + * @return self * * @throws FileException if the target file could not be created */ - public function move($directory, $name = null) + public function move(string $directory, string $name = null) { $target = $this->getTargetFile($directory, $name); set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); - $renamed = rename($this->getPathname(), $target); - restore_error_handler(); + try { + $renamed = rename($this->getPathname(), $target); + } finally { + restore_error_handler(); + } if (!$renamed) { - throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error))); + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); } @chmod($target, 0666 & ~umask()); @@ -105,14 +107,28 @@ class File extends \SplFileInfo return $target; } - protected function getTargetFile($directory, $name = null) + public function getContent(): string + { + $content = file_get_contents($this->getPathname()); + + if (false === $content) { + throw new FileException(sprintf('Could not get the content of the file "%s".', $this->getPathname())); + } + + return $content; + } + + /** + * @return self + */ + protected function getTargetFile(string $directory, string $name = null) { if (!is_dir($directory)) { if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { - throw new FileException(sprintf('Unable to create the "%s" directory', $directory)); + throw new FileException(sprintf('Unable to create the "%s" directory.', $directory)); } } elseif (!is_writable($directory)) { - throw new FileException(sprintf('Unable to write in the "%s" directory', $directory)); + throw new FileException(sprintf('Unable to write in the "%s" directory.', $directory)); } $target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name)); @@ -123,11 +139,9 @@ class File extends \SplFileInfo /** * Returns locale independent base name of the given path. * - * @param string $name The new file name - * - * @return string containing + * @return string */ - protected function getName($name) + protected function getName(string $name) { $originalName = str_replace('\\', '/', $name); $pos = strrpos($originalName, '/'); diff --git a/vendor/symfony/http-foundation/File/Stream.php b/vendor/symfony/http-foundation/File/Stream.php index 69ae74c..cef3e03 100644 --- a/vendor/symfony/http-foundation/File/Stream.php +++ b/vendor/symfony/http-foundation/File/Stream.php @@ -20,7 +20,10 @@ class Stream extends File { /** * {@inheritdoc} + * + * @return int|false */ + #[\ReturnTypeWillChange] public function getSize() { return false; diff --git a/vendor/symfony/http-foundation/File/UploadedFile.php b/vendor/symfony/http-foundation/File/UploadedFile.php index 86153ed..fcc6299 100644 --- a/vendor/symfony/http-foundation/File/UploadedFile.php +++ b/vendor/symfony/http-foundation/File/UploadedFile.php @@ -11,9 +11,16 @@ namespace Symfony\Component\HttpFoundation\File; +use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException; +use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException; use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; -use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser; +use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException; +use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException; +use Symfony\Component\HttpFoundation\File\Exception\NoFileException; +use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException; +use Symfony\Component\HttpFoundation\File\Exception\PartialFileException; +use Symfony\Component\Mime\MimeTypes; /** * A file uploaded through a form. @@ -24,10 +31,9 @@ use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser; */ class UploadedFile extends File { - private $test = false; + private $test; private $originalName; private $mimeType; - private $size; private $error; /** @@ -47,7 +53,6 @@ class UploadedFile extends File * @param string $path The full temporary path to the file * @param string $originalName The original file name of the uploaded file * @param string|null $mimeType The type of the file as provided by PHP; null defaults to application/octet-stream - * @param int|null $size The file size provided by the uploader * @param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK * @param bool $test Whether the test mode is active * Local files are used in test mode hence the code should not enforce HTTP uploads @@ -55,15 +60,14 @@ class UploadedFile extends File * @throws FileException If file_uploads is disabled * @throws FileNotFoundException If the file does not exist */ - public function __construct($path, $originalName, $mimeType = null, $size = null, $error = null, $test = false) + public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false) { $this->originalName = $this->getName($originalName); $this->mimeType = $mimeType ?: 'application/octet-stream'; - $this->size = $size; - $this->error = $error ?: UPLOAD_ERR_OK; - $this->test = (bool) $test; + $this->error = $error ?: \UPLOAD_ERR_OK; + $this->test = $test; - parent::__construct($path, UPLOAD_ERR_OK === $this->error); + parent::__construct($path, \UPLOAD_ERR_OK === $this->error); } /** @@ -72,7 +76,7 @@ class UploadedFile extends File * It is extracted from the request from which the file has been uploaded. * Then it should not be considered as a safe value. * - * @return string|null The original name + * @return string */ public function getClientOriginalName() { @@ -85,11 +89,11 @@ class UploadedFile extends File * It is extracted from the original file name that was uploaded. * Then it should not be considered as a safe value. * - * @return string The extension + * @return string */ public function getClientOriginalExtension() { - return pathinfo($this->originalName, PATHINFO_EXTENSION); + return pathinfo($this->originalName, \PATHINFO_EXTENSION); } /** @@ -101,7 +105,7 @@ class UploadedFile extends File * For a trusted mime type, use getMimeType() instead (which guesses the mime * type based on the file content). * - * @return string|null The mime type + * @return string * * @see getMimeType() */ @@ -122,30 +126,18 @@ class UploadedFile extends File * For a trusted extension, use guessExtension() instead (which guesses * the extension based on the guessed mime type for the file). * - * @return string|null The guessed extension or null if it cannot be guessed + * @return string|null * * @see guessExtension() * @see getClientMimeType() */ public function guessClientExtension() { - $type = $this->getClientMimeType(); - $guesser = ExtensionGuesser::getInstance(); + if (!class_exists(MimeTypes::class)) { + throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); + } - return $guesser->guess($type); - } - - /** - * Returns the file size. - * - * It is extracted from the request from which the file has been uploaded. - * Then it should not be considered as a safe value. - * - * @return int|null The file size - */ - public function getClientSize() - { - return $this->size; + return MimeTypes::getDefault()->getExtensions($this->getClientMimeType())[0] ?? null; } /** @@ -154,7 +146,7 @@ class UploadedFile extends File * If the upload was successful, the constant UPLOAD_ERR_OK is returned. * Otherwise one of the other UPLOAD_ERR_XXX constants is returned. * - * @return int The upload error + * @return int */ public function getError() { @@ -162,13 +154,13 @@ class UploadedFile extends File } /** - * Returns whether the file was uploaded successfully. + * Returns whether the file has been uploaded with HTTP and no error occurred. * - * @return bool True if the file has been uploaded with HTTP and no error occurred + * @return bool */ public function isValid() { - $isOk = UPLOAD_ERR_OK === $this->error; + $isOk = \UPLOAD_ERR_OK === $this->error; return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname()); } @@ -176,14 +168,11 @@ class UploadedFile extends File /** * Moves the file to a new location. * - * @param string $directory The destination folder - * @param string $name The new file name - * - * @return File A File object representing the new file + * @return File * * @throws FileException if, for any reason, the file could not have been moved */ - public function move($directory, $name = null) + public function move(string $directory, string $name = null) { if ($this->isValid()) { if ($this->test) { @@ -193,10 +182,13 @@ class UploadedFile extends File $target = $this->getTargetFile($directory, $name); set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); - $moved = move_uploaded_file($this->getPathname(), $target); - restore_error_handler(); + try { + $moved = move_uploaded_file($this->getPathname(), $target); + } finally { + restore_error_handler(); + } if (!$moved) { - throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, strip_tags($error))); + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); } @chmod($target, 0666 & ~umask()); @@ -204,28 +196,45 @@ class UploadedFile extends File return $target; } + switch ($this->error) { + case \UPLOAD_ERR_INI_SIZE: + throw new IniSizeFileException($this->getErrorMessage()); + case \UPLOAD_ERR_FORM_SIZE: + throw new FormSizeFileException($this->getErrorMessage()); + case \UPLOAD_ERR_PARTIAL: + throw new PartialFileException($this->getErrorMessage()); + case \UPLOAD_ERR_NO_FILE: + throw new NoFileException($this->getErrorMessage()); + case \UPLOAD_ERR_CANT_WRITE: + throw new CannotWriteFileException($this->getErrorMessage()); + case \UPLOAD_ERR_NO_TMP_DIR: + throw new NoTmpDirFileException($this->getErrorMessage()); + case \UPLOAD_ERR_EXTENSION: + throw new ExtensionFileException($this->getErrorMessage()); + } + throw new FileException($this->getErrorMessage()); } /** * Returns the maximum size of an uploaded file as configured in php.ini. * - * @return int The maximum size of an uploaded file in bytes + * @return int|float The maximum size of an uploaded file in bytes (returns float if size > PHP_INT_MAX) */ public static function getMaxFilesize() { - $sizePostMax = self::parseFilesize(ini_get('post_max_size')); - $sizeUploadMax = self::parseFilesize(ini_get('upload_max_filesize')); + $sizePostMax = self::parseFilesize(\ini_get('post_max_size')); + $sizeUploadMax = self::parseFilesize(\ini_get('upload_max_filesize')); - return min($sizePostMax ?: PHP_INT_MAX, $sizeUploadMax ?: PHP_INT_MAX); + return min($sizePostMax ?: \PHP_INT_MAX, $sizeUploadMax ?: \PHP_INT_MAX); } /** * Returns the given size from an ini value in bytes. * - * @return int The given size in bytes + * @return int|float Returns float if size > PHP_INT_MAX */ - private static function parseFilesize($size) + private static function parseFilesize(string $size) { if ('' === $size) { return 0; @@ -234,9 +243,9 @@ class UploadedFile extends File $size = strtolower($size); $max = ltrim($size, '+'); - if (0 === strpos($max, '0x')) { + if (str_starts_with($max, '0x')) { $max = \intval($max, 16); - } elseif (0 === strpos($max, '0')) { + } elseif (str_starts_with($max, '0')) { $max = \intval($max, 8); } else { $max = (int) $max; @@ -244,11 +253,11 @@ class UploadedFile extends File switch (substr($size, -1)) { case 't': $max *= 1024; - // no break + // no break case 'g': $max *= 1024; - // no break + // no break case 'm': $max *= 1024; - // no break + // no break case 'k': $max *= 1024; } @@ -258,23 +267,23 @@ class UploadedFile extends File /** * Returns an informative upload error message. * - * @return string The error message regarding the specified error code + * @return string */ public function getErrorMessage() { static $errors = [ - UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).', - UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.', - UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.', - UPLOAD_ERR_NO_FILE => 'No file was uploaded.', - UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.', - UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.', - UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.', + \UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).', + \UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.', + \UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.', + \UPLOAD_ERR_NO_FILE => 'No file was uploaded.', + \UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.', + \UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.', + \UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.', ]; $errorCode = $this->error; - $maxFilesize = UPLOAD_ERR_INI_SIZE === $errorCode ? self::getMaxFilesize() / 1024 : 0; - $message = isset($errors[$errorCode]) ? $errors[$errorCode] : 'The file "%s" was not uploaded due to an unknown error.'; + $maxFilesize = \UPLOAD_ERR_INI_SIZE === $errorCode ? self::getMaxFilesize() / 1024 : 0; + $message = $errors[$errorCode] ?? 'The file "%s" was not uploaded due to an unknown error.'; return sprintf($message, $this->getClientOriginalName(), $maxFilesize); } diff --git a/vendor/symfony/http-foundation/HeaderUtils.php b/vendor/symfony/http-foundation/HeaderUtils.php new file mode 100644 index 0000000..1d56be0 --- /dev/null +++ b/vendor/symfony/http-foundation/HeaderUtils.php @@ -0,0 +1,293 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * HTTP header utility functions. + * + * @author Christian Schmidt + */ +class HeaderUtils +{ + public const DISPOSITION_ATTACHMENT = 'attachment'; + public const DISPOSITION_INLINE = 'inline'; + + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Splits an HTTP header by one or more separators. + * + * Example: + * + * HeaderUtils::split("da, en-gb;q=0.8", ",;") + * // => ['da'], ['en-gb', 'q=0.8']] + * + * @param string $separators List of characters to split on, ordered by + * precedence, e.g. ",", ";=", or ",;=" + * + * @return array Nested array with as many levels as there are characters in + * $separators + */ + public static function split(string $header, string $separators): array + { + $quotedSeparators = preg_quote($separators, '/'); + + preg_match_all(' + / + (?!\s) + (?: + # quoted-string + "(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$) + | + # token + [^"'.$quotedSeparators.']+ + )+ + (?['.$quotedSeparators.']) + \s* + /x', trim($header), $matches, \PREG_SET_ORDER); + + return self::groupParts($matches, $separators); + } + + /** + * Combines an array of arrays into one associative array. + * + * Each of the nested arrays should have one or two elements. The first + * value will be used as the keys in the associative array, and the second + * will be used as the values, or true if the nested array only contains one + * element. Array keys are lowercased. + * + * Example: + * + * HeaderUtils::combine([["foo", "abc"], ["bar"]]) + * // => ["foo" => "abc", "bar" => true] + */ + public static function combine(array $parts): array + { + $assoc = []; + foreach ($parts as $part) { + $name = strtolower($part[0]); + $value = $part[1] ?? true; + $assoc[$name] = $value; + } + + return $assoc; + } + + /** + * Joins an associative array into a string for use in an HTTP header. + * + * The key and value of each entry are joined with "=", and all entries + * are joined with the specified separator and an additional space (for + * readability). Values are quoted if necessary. + * + * Example: + * + * HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",") + * // => 'foo=abc, bar, baz="a b c"' + */ + public static function toString(array $assoc, string $separator): string + { + $parts = []; + foreach ($assoc as $name => $value) { + if (true === $value) { + $parts[] = $name; + } else { + $parts[] = $name.'='.self::quote($value); + } + } + + return implode($separator.' ', $parts); + } + + /** + * Encodes a string as a quoted string, if necessary. + * + * If a string contains characters not allowed by the "token" construct in + * the HTTP specification, it is backslash-escaped and enclosed in quotes + * to match the "quoted-string" construct. + */ + public static function quote(string $s): string + { + if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) { + return $s; + } + + return '"'.addcslashes($s, '"\\"').'"'; + } + + /** + * Decodes a quoted string. + * + * If passed an unquoted string that matches the "token" construct (as + * defined in the HTTP specification), it is passed through verbatimly. + */ + public static function unquote(string $s): string + { + return preg_replace('/\\\\(.)|"/', '$1', $s); + } + + /** + * Generates an HTTP Content-Disposition field-value. + * + * @param string $disposition One of "inline" or "attachment" + * @param string $filename A unicode string + * @param string $filenameFallback A string containing only ASCII characters that + * is semantically equivalent to $filename. If the filename is already ASCII, + * it can be omitted, or just copied from $filename + * + * @throws \InvalidArgumentException + * + * @see RFC 6266 + */ + public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string + { + if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) { + throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE)); + } + + if ('' === $filenameFallback) { + $filenameFallback = $filename; + } + + // filenameFallback is not ASCII. + if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) { + throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.'); + } + + // percent characters aren't safe in fallback. + if (str_contains($filenameFallback, '%')) { + throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.'); + } + + // path separators aren't allowed in either. + if (str_contains($filename, '/') || str_contains($filename, '\\') || str_contains($filenameFallback, '/') || str_contains($filenameFallback, '\\')) { + throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.'); + } + + $params = ['filename' => $filenameFallback]; + if ($filename !== $filenameFallback) { + $params['filename*'] = "utf-8''".rawurlencode($filename); + } + + return $disposition.'; '.self::toString($params, ';'); + } + + /** + * Like parse_str(), but preserves dots in variable names. + */ + public static function parseQuery(string $query, bool $ignoreBrackets = false, string $separator = '&'): array + { + $q = []; + + foreach (explode($separator, $query) as $v) { + if (false !== $i = strpos($v, "\0")) { + $v = substr($v, 0, $i); + } + + if (false === $i = strpos($v, '=')) { + $k = urldecode($v); + $v = ''; + } else { + $k = urldecode(substr($v, 0, $i)); + $v = substr($v, $i); + } + + if (false !== $i = strpos($k, "\0")) { + $k = substr($k, 0, $i); + } + + $k = ltrim($k, ' '); + + if ($ignoreBrackets) { + $q[$k][] = urldecode(substr($v, 1)); + + continue; + } + + if (false === $i = strpos($k, '[')) { + $q[] = bin2hex($k).$v; + } else { + $q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v; + } + } + + if ($ignoreBrackets) { + return $q; + } + + parse_str(implode('&', $q), $q); + + $query = []; + + foreach ($q as $k => $v) { + if (false !== $i = strpos($k, '_')) { + $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v; + } else { + $query[hex2bin($k)] = $v; + } + } + + return $query; + } + + private static function groupParts(array $matches, string $separators, bool $first = true): array + { + $separator = $separators[0]; + $partSeparators = substr($separators, 1); + + $i = 0; + $partMatches = []; + $previousMatchWasSeparator = false; + foreach ($matches as $match) { + if (!$first && $previousMatchWasSeparator && isset($match['separator']) && $match['separator'] === $separator) { + $previousMatchWasSeparator = true; + $partMatches[$i][] = $match; + } elseif (isset($match['separator']) && $match['separator'] === $separator) { + $previousMatchWasSeparator = true; + ++$i; + } else { + $previousMatchWasSeparator = false; + $partMatches[$i][] = $match; + } + } + + $parts = []; + if ($partSeparators) { + foreach ($partMatches as $matches) { + $parts[] = self::groupParts($matches, $partSeparators, false); + } + } else { + foreach ($partMatches as $matches) { + $parts[] = self::unquote($matches[0][0]); + } + + if (!$first && 2 < \count($parts)) { + $parts = [ + $parts[0], + implode($separator, \array_slice($parts, 1)), + ]; + } + } + + return $parts; + } +} diff --git a/vendor/symfony/http-foundation/InputBag.php b/vendor/symfony/http-foundation/InputBag.php new file mode 100644 index 0000000..a9d3cd8 --- /dev/null +++ b/vendor/symfony/http-foundation/InputBag.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Exception\BadRequestException; + +/** + * InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE. + * + * @author Saif Eddin Gmati + */ +final class InputBag extends ParameterBag +{ + /** + * Returns a scalar input value by name. + * + * @param string|int|float|bool|null $default The default value if the input key does not exist + * + * @return string|int|float|bool|null + */ + public function get(string $key, $default = null) + { + if (null !== $default && !\is_scalar($default) && !(\is_object($default) && method_exists($default, '__toString'))) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Passing a non-scalar value as 2nd argument to "%s()" is deprecated, pass a scalar or null instead.', __METHOD__); + } + + $value = parent::get($key, $this); + + if (null !== $value && $this !== $value && !\is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Retrieving a non-scalar value from "%s()" is deprecated, and will throw a "%s" exception in Symfony 6.0, use "%s::all($key)" instead.', __METHOD__, BadRequestException::class, __CLASS__); + } + + return $this === $value ? $default : $value; + } + + /** + * {@inheritdoc} + */ + public function all(string $key = null): array + { + return parent::all($key); + } + + /** + * Replaces the current input values by a new set. + */ + public function replace(array $inputs = []) + { + $this->parameters = []; + $this->add($inputs); + } + + /** + * Adds input values. + */ + public function add(array $inputs = []) + { + foreach ($inputs as $input => $value) { + $this->set($input, $value); + } + } + + /** + * Sets an input by name. + * + * @param string|int|float|bool|array|null $value + */ + public function set(string $key, $value) + { + if (null !== $value && !\is_scalar($value) && !\is_array($value) && !method_exists($value, '__toString')) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Passing "%s" as a 2nd Argument to "%s()" is deprecated, pass a scalar, array, or null instead.', get_debug_type($value), __METHOD__); + } + + $this->parameters[$key] = $value; + } + + /** + * {@inheritdoc} + */ + public function filter(string $key, $default = null, int $filter = \FILTER_DEFAULT, $options = []) + { + $value = $this->has($key) ? $this->all()[$key] : $default; + + // Always turn $options into an array - this allows filter_var option shortcuts. + if (!\is_array($options) && $options) { + $options = ['flags' => $options]; + } + + if (\is_array($value) && !(($options['flags'] ?? 0) & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) { + trigger_deprecation('symfony/http-foundation', '5.1', 'Filtering an array value with "%s()" without passing the FILTER_REQUIRE_ARRAY or FILTER_FORCE_ARRAY flag is deprecated', __METHOD__); + + if (!isset($options['flags'])) { + $options['flags'] = \FILTER_REQUIRE_ARRAY; + } + } + + if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { + trigger_deprecation('symfony/http-foundation', '5.2', 'Not passing a Closure together with FILTER_CALLBACK to "%s()" is deprecated. Wrap your filter in a closure instead.', __METHOD__); + // throw new \InvalidArgumentException(sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); + } + + return filter_var($value, $filter, $options); + } +} diff --git a/vendor/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php b/vendor/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php new file mode 100644 index 0000000..a6dd993 --- /dev/null +++ b/vendor/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RateLimiter; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\RateLimiter\LimiterInterface; +use Symfony\Component\RateLimiter\Policy\NoLimiter; +use Symfony\Component\RateLimiter\RateLimit; + +/** + * An implementation of RequestRateLimiterInterface that + * fits most use-cases. + * + * @author Wouter de Jong + */ +abstract class AbstractRequestRateLimiter implements RequestRateLimiterInterface +{ + public function consume(Request $request): RateLimit + { + $limiters = $this->getLimiters($request); + if (0 === \count($limiters)) { + $limiters = [new NoLimiter()]; + } + + $minimalRateLimit = null; + foreach ($limiters as $limiter) { + $rateLimit = $limiter->consume(1); + + $minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit; + } + + return $minimalRateLimit; + } + + public function reset(Request $request): void + { + foreach ($this->getLimiters($request) as $limiter) { + $limiter->reset(); + } + } + + /** + * @return LimiterInterface[] a set of limiters using keys extracted from the request + */ + abstract protected function getLimiters(Request $request): array; + + private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit + { + if ($first->isAccepted() !== $second->isAccepted()) { + return $first->isAccepted() ? $second : $first; + } + + $firstRemainingTokens = $first->getRemainingTokens(); + $secondRemainingTokens = $second->getRemainingTokens(); + + if ($firstRemainingTokens === $secondRemainingTokens) { + return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first; + } + + return $firstRemainingTokens > $secondRemainingTokens ? $second : $first; + } +} diff --git a/vendor/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php b/vendor/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php new file mode 100644 index 0000000..4c87a40 --- /dev/null +++ b/vendor/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RateLimiter; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\RateLimiter\RateLimit; + +/** + * A special type of limiter that deals with requests. + * + * This allows to limit on different types of information + * from the requests. + * + * @author Wouter de Jong + */ +interface RequestRateLimiterInterface +{ + public function consume(Request $request): RateLimit; + + public function reset(Request $request): void; +} diff --git a/vendor/symfony/http-foundation/Session/Session.php b/vendor/symfony/http-foundation/Session/Session.php index db0b9ae..022e398 100644 --- a/vendor/symfony/http-foundation/Session/Session.php +++ b/vendor/symfony/http-foundation/Session/Session.php @@ -18,9 +18,16 @@ use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; +// Help opcache.preload discover always-needed symbols +class_exists(AttributeBag::class); +class_exists(FlashBag::class); +class_exists(SessionBagProxy::class); + /** * @author Fabien Potencier * @author Drak + * + * @implements \IteratorAggregate */ class Session implements SessionInterface, \IteratorAggregate, \Countable { @@ -30,21 +37,18 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable private $attributeName; private $data = []; private $usageIndex = 0; + private $usageReporter; - /** - * @param SessionStorageInterface $storage A SessionStorageInterface instance - * @param AttributeBagInterface $attributes An AttributeBagInterface instance, (defaults null for default AttributeBag) - * @param FlashBagInterface $flashes A FlashBagInterface instance (defaults null for default FlashBag) - */ - public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null) + public function __construct(SessionStorageInterface $storage = null, AttributeBagInterface $attributes = null, FlashBagInterface $flashes = null, callable $usageReporter = null) { - $this->storage = $storage ?: new NativeSessionStorage(); + $this->storage = $storage ?? new NativeSessionStorage(); + $this->usageReporter = $usageReporter; - $attributes = $attributes ?: new AttributeBag(); + $attributes = $attributes ?? new AttributeBag(); $this->attributeName = $attributes->getName(); $this->registerBag($attributes); - $flashes = $flashes ?: new FlashBag(); + $flashes = $flashes ?? new FlashBag(); $this->flashName = $flashes->getName(); $this->registerBag($flashes); } @@ -60,7 +64,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable /** * {@inheritdoc} */ - public function has($name) + public function has(string $name) { return $this->getAttributeBag()->has($name); } @@ -68,7 +72,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable /** * {@inheritdoc} */ - public function get($name, $default = null) + public function get(string $name, $default = null) { return $this->getAttributeBag()->get($name, $default); } @@ -76,7 +80,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable /** * {@inheritdoc} */ - public function set($name, $value) + public function set(string $name, $value) { $this->getAttributeBag()->set($name, $value); } @@ -100,7 +104,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable /** * {@inheritdoc} */ - public function remove($name) + public function remove(string $name) { return $this->getAttributeBag()->remove($name); } @@ -124,8 +128,9 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable /** * Returns an iterator for attributes. * - * @return \ArrayIterator An \ArrayIterator instance + * @return \ArrayIterator */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->getAttributeBag()->all()); @@ -134,32 +139,29 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable /** * Returns the number of attributes. * - * @return int The number of attributes + * @return int */ + #[\ReturnTypeWillChange] public function count() { return \count($this->getAttributeBag()->all()); } - /** - * @return int - * - * @internal - */ - public function getUsageIndex() + public function &getUsageIndex(): int { return $this->usageIndex; } /** - * @return bool - * * @internal */ - public function isEmpty() + public function isEmpty(): bool { if ($this->isStarted()) { ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } } foreach ($this->data as &$data) { if (!empty($data)) { @@ -173,7 +175,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable /** * {@inheritdoc} */ - public function invalidate($lifetime = null) + public function invalidate(int $lifetime = null) { $this->storage->clear(); @@ -183,7 +185,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable /** * {@inheritdoc} */ - public function migrate($destroy = false, $lifetime = null) + public function migrate(bool $destroy = false, int $lifetime = null) { return $this->storage->regenerate($destroy, $lifetime); } @@ -207,7 +209,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable /** * {@inheritdoc} */ - public function setId($id) + public function setId(string $id) { if ($this->storage->getId() !== $id) { $this->storage->setId($id); @@ -225,7 +227,7 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable /** * {@inheritdoc} */ - public function setName($name) + public function setName(string $name) { $this->storage->setName($name); } @@ -236,6 +238,9 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable public function getMetadataBag() { ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } return $this->storage->getMetadataBag(); } @@ -245,13 +250,13 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable */ public function registerBag(SessionBagInterface $bag) { - $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex)); + $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter)); } /** * {@inheritdoc} */ - public function getBag($name) + public function getBag(string $name) { $bag = $this->storage->getBag($name); @@ -272,10 +277,8 @@ class Session implements SessionInterface, \IteratorAggregate, \Countable * Gets the attributebag interface. * * Note that this method was added to help with IDE autocompletion. - * - * @return AttributeBagInterface */ - private function getAttributeBag() + private function getAttributeBag(): AttributeBagInterface { return $this->getBag($this->attributeName); } diff --git a/vendor/symfony/http-foundation/Session/SessionBagProxy.php b/vendor/symfony/http-foundation/Session/SessionBagProxy.php index 3504bdf..90aa010 100644 --- a/vendor/symfony/http-foundation/Session/SessionBagProxy.php +++ b/vendor/symfony/http-foundation/Session/SessionBagProxy.php @@ -21,33 +21,35 @@ final class SessionBagProxy implements SessionBagInterface private $bag; private $data; private $usageIndex; + private $usageReporter; - public function __construct(SessionBagInterface $bag, array &$data, &$usageIndex) + public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter) { $this->bag = $bag; $this->data = &$data; $this->usageIndex = &$usageIndex; + $this->usageReporter = $usageReporter; } - /** - * @return SessionBagInterface - */ - public function getBag() + public function getBag(): SessionBagInterface { ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } return $this->bag; } - /** - * @return bool - */ - public function isEmpty() + public function isEmpty(): bool { if (!isset($this->data[$this->bag->getStorageKey()])) { return true; } ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } return empty($this->data[$this->bag->getStorageKey()]); } @@ -55,7 +57,7 @@ final class SessionBagProxy implements SessionBagInterface /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return $this->bag->getName(); } @@ -63,9 +65,13 @@ final class SessionBagProxy implements SessionBagInterface /** * {@inheritdoc} */ - public function initialize(array &$array) + public function initialize(array &$array): void { ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + $this->data[$this->bag->getStorageKey()] = &$array; $this->bag->initialize($array); @@ -74,7 +80,7 @@ final class SessionBagProxy implements SessionBagInterface /** * {@inheritdoc} */ - public function getStorageKey() + public function getStorageKey(): string { return $this->bag->getStorageKey(); } diff --git a/vendor/symfony/http-foundation/Session/SessionFactory.php b/vendor/symfony/http-foundation/Session/SessionFactory.php new file mode 100644 index 0000000..04c4b06 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/SessionFactory.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(Session::class); + +/** + * @author Jérémy Derussé + */ +class SessionFactory implements SessionFactoryInterface +{ + private $requestStack; + private $storageFactory; + private $usageReporter; + + public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, callable $usageReporter = null) + { + $this->requestStack = $requestStack; + $this->storageFactory = $storageFactory; + $this->usageReporter = $usageReporter; + } + + public function createSession(): SessionInterface + { + return new Session($this->storageFactory->createStorage($this->requestStack->getMainRequest()), null, null, $this->usageReporter); + } +} diff --git a/vendor/symfony/http-foundation/Session/SessionFactoryInterface.php b/vendor/symfony/http-foundation/Session/SessionFactoryInterface.php new file mode 100644 index 0000000..b24fdc4 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/SessionFactoryInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +/** + * @author Kevin Bond + */ +interface SessionFactoryInterface +{ + public function createSession(): SessionInterface; +} diff --git a/vendor/symfony/http-foundation/Session/SessionInterface.php b/vendor/symfony/http-foundation/Session/SessionInterface.php index 95fca85..b2f09fd 100644 --- a/vendor/symfony/http-foundation/Session/SessionInterface.php +++ b/vendor/symfony/http-foundation/Session/SessionInterface.php @@ -23,7 +23,7 @@ interface SessionInterface /** * Starts the session storage. * - * @return bool True if session started + * @return bool * * @throws \RuntimeException if session fails to start */ @@ -32,30 +32,26 @@ interface SessionInterface /** * Returns the session ID. * - * @return string The session ID + * @return string */ public function getId(); /** * Sets the session ID. - * - * @param string $id */ - public function setId($id); + public function setId(string $id); /** * Returns the session name. * - * @return mixed The session name + * @return string */ public function getName(); /** * Sets the session name. - * - * @param string $name */ - public function setName($name); + public function setName(string $name); /** * Invalidates the current session. @@ -68,9 +64,9 @@ interface SessionInterface * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. * - * @return bool True if session invalidated, false if error + * @return bool */ - public function invalidate($lifetime = null); + public function invalidate(int $lifetime = null); /** * Migrates the current session to a new session id while maintaining all @@ -82,9 +78,9 @@ interface SessionInterface * to expire with browser session. Time is in seconds, and is * not a Unix timestamp. * - * @return bool True if session migrated, false if error + * @return bool */ - public function migrate($destroy = false, $lifetime = null); + public function migrate(bool $destroy = false, int $lifetime = null); /** * Force the session to be saved and closed. @@ -98,52 +94,44 @@ interface SessionInterface /** * Checks if an attribute is defined. * - * @param string $name The attribute name - * - * @return bool true if the attribute is defined, false otherwise + * @return bool */ - public function has($name); + public function has(string $name); /** * Returns an attribute. * - * @param string $name The attribute name - * @param mixed $default The default value if not found + * @param mixed $default The default value if not found * * @return mixed */ - public function get($name, $default = null); + public function get(string $name, $default = null); /** * Sets an attribute. * - * @param string $name - * @param mixed $value + * @param mixed $value */ - public function set($name, $value); + public function set(string $name, $value); /** * Returns attributes. * - * @return array Attributes + * @return array */ public function all(); /** * Sets attributes. - * - * @param array $attributes Attributes */ public function replace(array $attributes); /** * Removes an attribute. * - * @param string $name - * * @return mixed The removed value or null when it does not exist */ - public function remove($name); + public function remove(string $name); /** * Clears all attributes. @@ -165,11 +153,9 @@ interface SessionInterface /** * Gets a bag instance by name. * - * @param string $name - * * @return SessionBagInterface */ - public function getBag($name); + public function getBag(string $name); /** * Gets session meta. diff --git a/vendor/symfony/http-foundation/Session/SessionUtils.php b/vendor/symfony/http-foundation/Session/SessionUtils.php index 04a25f7..b5bce4a 100644 --- a/vendor/symfony/http-foundation/Session/SessionUtils.php +++ b/vendor/symfony/http-foundation/Session/SessionUtils.php @@ -22,10 +22,10 @@ namespace Symfony\Component\HttpFoundation\Session; final class SessionUtils { /** - * Find the session header amongst the headers that are to be sent, remove it, and return + * Finds the session header amongst the headers that are to be sent, removes it, and returns * it so the caller can process it further. */ - public static function popSessionCookie($sessionName, $sessionId) + public static function popSessionCookie(string $sessionName, string $sessionId): ?string { $sessionCookie = null; $sessionCookiePrefix = sprintf(' %s=', urlencode($sessionName)); diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php b/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php new file mode 100644 index 0000000..bea3a32 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Ahmed TAILOULOUTE + */ +class IdentityMarshaller implements MarshallerInterface +{ + /** + * {@inheritdoc} + */ + public function marshall(array $values, ?array &$failed): array + { + foreach ($values as $key => $value) { + if (!\is_string($value)) { + throw new \LogicException(sprintf('%s accepts only string as data.', __METHOD__)); + } + } + + return $values; + } + + /** + * {@inheritdoc} + */ + public function unmarshall(string $value): string + { + return $value; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php new file mode 100644 index 0000000..c321c8c --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Ahmed TAILOULOUTE + */ +class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + private $handler; + private $marshaller; + + public function __construct(AbstractSessionHandler $handler, MarshallerInterface $marshaller) + { + $this->handler = $handler; + $this->marshaller = $marshaller; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function open($savePath, $name) + { + return $this->handler->open($savePath, $name); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function close() + { + return $this->handler->close(); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function destroy($sessionId) + { + return $this->handler->destroy($sessionId); + } + + /** + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + return $this->handler->gc($maxlifetime); + } + + /** + * @return string + */ + #[\ReturnTypeWillChange] + public function read($sessionId) + { + return $this->marshaller->unmarshall($this->handler->read($sessionId)); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function write($sessionId, $data) + { + $failed = []; + $marshalledData = $this->marshaller->marshall(['data' => $data], $failed); + + if (isset($failed['data'])) { + return false; + } + + return $this->handler->write($sessionId, $marshalledData['data']); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function validateId($sessionId) + { + return $this->handler->validateId($sessionId); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $data) + { + return $this->handler->updateTimestamp($sessionId, $data); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php new file mode 100644 index 0000000..bf27ca6 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Migrating session handler for migrating from one handler to another. It reads + * from the current handler and writes both the current and new ones. + * + * It ignores errors from the new handler. + * + * @author Ross Motley + * @author Oliver Radwell + */ +class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + /** + * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface + */ + private $currentHandler; + + /** + * @var \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface + */ + private $writeOnlyHandler; + + public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler) + { + if (!$currentHandler instanceof \SessionUpdateTimestampHandlerInterface) { + $currentHandler = new StrictSessionHandler($currentHandler); + } + if (!$writeOnlyHandler instanceof \SessionUpdateTimestampHandlerInterface) { + $writeOnlyHandler = new StrictSessionHandler($writeOnlyHandler); + } + + $this->currentHandler = $currentHandler; + $this->writeOnlyHandler = $writeOnlyHandler; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function close() + { + $result = $this->currentHandler->close(); + $this->writeOnlyHandler->close(); + + return $result; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function destroy($sessionId) + { + $result = $this->currentHandler->destroy($sessionId); + $this->writeOnlyHandler->destroy($sessionId); + + return $result; + } + + /** + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + $result = $this->currentHandler->gc($maxlifetime); + $this->writeOnlyHandler->gc($maxlifetime); + + return $result; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function open($savePath, $sessionName) + { + $result = $this->currentHandler->open($savePath, $sessionName); + $this->writeOnlyHandler->open($savePath, $sessionName); + + return $result; + } + + /** + * @return string + */ + #[\ReturnTypeWillChange] + public function read($sessionId) + { + // No reading from new handler until switch-over + return $this->currentHandler->read($sessionId); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function write($sessionId, $sessionData) + { + $result = $this->currentHandler->write($sessionId, $sessionData); + $this->writeOnlyHandler->write($sessionId, $sessionData); + + return $result; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function validateId($sessionId) + { + // No reading from new handler until switch-over + return $this->currentHandler->validateId($sessionId); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $sessionData) + { + $result = $this->currentHandler->updateTimestamp($sessionId, $sessionData); + $this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData); + + return $result; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php b/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php new file mode 100644 index 0000000..31954e6 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Predis\Response\ErrorInterface; +use Symfony\Component\Cache\Traits\RedisClusterProxy; +use Symfony\Component\Cache\Traits\RedisProxy; + +/** + * Redis based session storage handler based on the Redis class + * provided by the PHP redis extension. + * + * @author Dalibor Karlović + */ +class RedisSessionHandler extends AbstractSessionHandler +{ + private $redis; + + /** + * @var string Key prefix for shared environments + */ + private $prefix; + + /** + * @var int Time to live in seconds + */ + private $ttl; + + /** + * List of available options: + * * prefix: The prefix to use for the keys in order to avoid collision on the Redis server + * * ttl: The time to live in seconds. + * + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis + * + * @throws \InvalidArgumentException When unsupported client or options are passed + */ + public function __construct($redis, array $options = []) + { + if ( + !$redis instanceof \Redis && + !$redis instanceof \RedisArray && + !$redis instanceof \RedisCluster && + !$redis instanceof \Predis\ClientInterface && + !$redis instanceof RedisProxy && + !$redis instanceof RedisClusterProxy + ) { + throw new \InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, get_debug_type($redis))); + } + + if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) { + throw new \InvalidArgumentException(sprintf('The following options are not supported "%s".', implode(', ', $diff))); + } + + $this->redis = $redis; + $this->prefix = $options['prefix'] ?? 'sf_s'; + $this->ttl = $options['ttl'] ?? null; + } + + /** + * {@inheritdoc} + */ + protected function doRead(string $sessionId): string + { + return $this->redis->get($this->prefix.$sessionId) ?: ''; + } + + /** + * {@inheritdoc} + */ + protected function doWrite(string $sessionId, string $data): bool + { + $result = $this->redis->setEx($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime')), $data); + + return $result && !$result instanceof ErrorInterface; + } + + /** + * {@inheritdoc} + */ + protected function doDestroy(string $sessionId): bool + { + static $unlink = true; + + if ($unlink) { + try { + $unlink = false !== $this->redis->unlink($this->prefix.$sessionId); + } catch (\Throwable $e) { + $unlink = false; + } + } + + if (!$unlink) { + $this->redis->del($this->prefix.$sessionId); + } + + return true; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function close(): bool + { + return true; + } + + /** + * {@inheritdoc} + * + * @return int|false + */ + #[\ReturnTypeWillChange] + public function gc($maxlifetime) + { + return 0; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function updateTimestamp($sessionId, $data) + { + return (bool) $this->redis->expire($this->prefix.$sessionId, (int) ($this->ttl ?? \ini_get('session.gc_maxlifetime'))); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php b/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php new file mode 100644 index 0000000..f3f7b20 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Doctrine\DBAL\DriverManager; +use Symfony\Component\Cache\Adapter\AbstractAdapter; +use Symfony\Component\Cache\Traits\RedisClusterProxy; +use Symfony\Component\Cache\Traits\RedisProxy; + +/** + * @author Nicolas Grekas + */ +class SessionHandlerFactory +{ + /** + * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy|\Memcached|\PDO|string $connection Connection or DSN + */ + public static function createHandler($connection): AbstractSessionHandler + { + if (!\is_string($connection) && !\is_object($connection)) { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a string or a connection object, "%s" given.', __METHOD__, get_debug_type($connection))); + } + + if ($options = \is_string($connection) ? parse_url($connection) : false) { + parse_str($options['query'] ?? '', $options); + } + + switch (true) { + case $connection instanceof \Redis: + case $connection instanceof \RedisArray: + case $connection instanceof \RedisCluster: + case $connection instanceof \Predis\ClientInterface: + case $connection instanceof RedisProxy: + case $connection instanceof RedisClusterProxy: + return new RedisSessionHandler($connection); + + case $connection instanceof \Memcached: + return new MemcachedSessionHandler($connection); + + case $connection instanceof \PDO: + return new PdoSessionHandler($connection); + + case !\is_string($connection): + throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', get_debug_type($connection))); + case str_starts_with($connection, 'file://'): + $savePath = substr($connection, 7); + + return new StrictSessionHandler(new NativeFileSessionHandler('' === $savePath ? null : $savePath)); + + case str_starts_with($connection, 'redis:'): + case str_starts_with($connection, 'rediss:'): + case str_starts_with($connection, 'memcached:'): + if (!class_exists(AbstractAdapter::class)) { + throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection)); + } + $handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class; + $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); + + return new $handlerClass($connection, array_intersect_key($options ?: [], ['prefix' => 1, 'ttl' => 1])); + + case str_starts_with($connection, 'pdo_oci://'): + if (!class_exists(DriverManager::class)) { + throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection)); + } + $connection = DriverManager::getConnection(['url' => $connection])->getWrappedConnection(); + // no break; + + case str_starts_with($connection, 'mssql://'): + case str_starts_with($connection, 'mysql://'): + case str_starts_with($connection, 'mysql2://'): + case str_starts_with($connection, 'pgsql://'): + case str_starts_with($connection, 'postgres://'): + case str_starts_with($connection, 'postgresql://'): + case str_starts_with($connection, 'sqlsrv://'): + case str_starts_with($connection, 'sqlite://'): + case str_starts_with($connection, 'sqlite3://'): + return new PdoSessionHandler($connection, $options ?: []); + } + + throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection)); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php b/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php new file mode 100644 index 0000000..d0da1e1 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +// Help opcache.preload discover always-needed symbols +class_exists(MockFileSessionStorage::class); + +/** + * @author Jérémy Derussé + */ +class MockFileSessionStorageFactory implements SessionStorageFactoryInterface +{ + private $savePath; + private $name; + private $metaBag; + + /** + * @see MockFileSessionStorage constructor. + */ + public function __construct(string $savePath = null, string $name = 'MOCKSESSID', MetadataBag $metaBag = null) + { + $this->savePath = $savePath; + $this->name = $name; + $this->metaBag = $metaBag; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + return new MockFileSessionStorage($this->savePath, $this->name, $this->metaBag); + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php b/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php new file mode 100644 index 0000000..a7d7411 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +// Help opcache.preload discover always-needed symbols +class_exists(NativeSessionStorage::class); + +/** + * @author Jérémy Derussé + */ +class NativeSessionStorageFactory implements SessionStorageFactoryInterface +{ + private $options; + private $handler; + private $metaBag; + private $secure; + + /** + * @see NativeSessionStorage constructor. + */ + public function __construct(array $options = [], $handler = null, MetadataBag $metaBag = null, bool $secure = false) + { + $this->options = $options; + $this->handler = $handler; + $this->metaBag = $metaBag; + $this->secure = $secure; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + $storage = new NativeSessionStorage($this->options, $this->handler, $this->metaBag); + if ($this->secure && $request && $request->isSecure()) { + $storage->setOptions(['cookie_secure' => true]); + } + + return $storage; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php b/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php new file mode 100644 index 0000000..173ef71 --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +// Help opcache.preload discover always-needed symbols +class_exists(PhpBridgeSessionStorage::class); + +/** + * @author Jérémy Derussé + */ +class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface +{ + private $handler; + private $metaBag; + private $secure; + + /** + * @see PhpBridgeSessionStorage constructor. + */ + public function __construct($handler = null, MetadataBag $metaBag = null, bool $secure = false) + { + $this->handler = $handler; + $this->metaBag = $metaBag; + $this->secure = $secure; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + $storage = new PhpBridgeSessionStorage($this->handler, $this->metaBag); + if ($this->secure && $request && $request->isSecure()) { + $storage->setOptions(['cookie_secure' => true]); + } + + return $storage; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php b/vendor/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php new file mode 100644 index 0000000..d17c60a --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/ServiceSessionFactory.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Jérémy Derussé + * + * @internal to be removed in Symfony 6 + */ +final class ServiceSessionFactory implements SessionStorageFactoryInterface +{ + private $storage; + + public function __construct(SessionStorageInterface $storage) + { + $this->storage = $storage; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + if ($this->storage instanceof NativeSessionStorage && $request && $request->isSecure()) { + $this->storage->setOptions(['cookie_secure' => true]); + } + + return $this->storage; + } +} diff --git a/vendor/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php b/vendor/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php new file mode 100644 index 0000000..d03f0da --- /dev/null +++ b/vendor/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Jérémy Derussé + */ +interface SessionStorageFactoryInterface +{ + /** + * Creates a new instance of SessionStorageInterface. + */ + public function createStorage(?Request $request): SessionStorageInterface; +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php b/vendor/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php new file mode 100644 index 0000000..cb216ea --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/RequestAttributeValueSame.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Request; + +final class RequestAttributeValueSame extends Constraint +{ + private $name; + private $value; + + public function __construct(string $name, string $value) + { + $this->name = $name; + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has attribute "%s" with value "%s"', $this->name, $this->value); + } + + /** + * @param Request $request + * + * {@inheritdoc} + */ + protected function matches($request): bool + { + return $this->value === $request->attributes->get($this->name); + } + + /** + * @param Request $request + * + * {@inheritdoc} + */ + protected function failureDescription($request): string + { + return 'the Request '.$this->toString(); + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php new file mode 100644 index 0000000..554e1a1 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseCookieValueSame.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseCookieValueSame extends Constraint +{ + private $name; + private $value; + private $path; + private $domain; + + public function __construct(string $name, string $value, string $path = '/', string $domain = null) + { + $this->name = $name; + $this->value = $value; + $this->path = $path; + $this->domain = $domain; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + $str = sprintf('has cookie "%s"', $this->name); + if ('/' !== $this->path) { + $str .= sprintf(' with path "%s"', $this->path); + } + if ($this->domain) { + $str .= sprintf(' for domain "%s"', $this->domain); + } + $str .= sprintf(' with value "%s"', $this->value); + + return $str; + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + $cookie = $this->getCookie($response); + if (!$cookie) { + return false; + } + + return $this->value === $cookie->getValue(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + protected function getCookie(Response $response): ?Cookie + { + $cookies = $response->headers->getCookies(); + + $filteredCookies = array_filter($cookies, function (Cookie $cookie) { + return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain; + }); + + return reset($filteredCookies) ?: null; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php new file mode 100644 index 0000000..f73aedf --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseFormatSame.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Asserts that the response is in the given format. + * + * @author Kévin Dunglas + */ +final class ResponseFormatSame extends Constraint +{ + private $request; + private $format; + + public function __construct(Request $request, ?string $format) + { + $this->request = $request; + $this->format = $format; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'format is '.($this->format ?? 'null'); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $this->format === $this->request->getFormat($response->headers->get('Content-Type')); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function additionalFailureDescription($response): string + { + return (string) $response; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php new file mode 100644 index 0000000..eae9e27 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasCookie.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseHasCookie extends Constraint +{ + private $name; + private $path; + private $domain; + + public function __construct(string $name, string $path = '/', string $domain = null) + { + $this->name = $name; + $this->path = $path; + $this->domain = $domain; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + $str = sprintf('has cookie "%s"', $this->name); + if ('/' !== $this->path) { + $str .= sprintf(' with path "%s"', $this->path); + } + if ($this->domain) { + $str .= sprintf(' for domain "%s"', $this->domain); + } + + return $str; + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return null !== $this->getCookie($response); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + private function getCookie(Response $response): ?Cookie + { + $cookies = $response->headers->getCookies(); + + $filteredCookies = array_filter($cookies, function (Cookie $cookie) { + return $cookie->getName() === $this->name && $cookie->getPath() === $this->path && $cookie->getDomain() === $this->domain; + }); + + return reset($filteredCookies) ?: null; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php new file mode 100644 index 0000000..68ad827 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseHasHeader.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseHasHeader extends Constraint +{ + private $headerName; + + public function __construct(string $headerName) + { + $this->headerName = $headerName; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has header "%s"', $this->headerName); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $response->headers->has($this->headerName); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php new file mode 100644 index 0000000..a27d0c7 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseHeaderSame.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseHeaderSame extends Constraint +{ + private $headerName; + private $expectedValue; + + public function __construct(string $headerName, string $expectedValue) + { + $this->headerName = $headerName; + $this->expectedValue = $expectedValue; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return sprintf('has header "%s" with value "%s"', $this->headerName, $this->expectedValue); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $this->expectedValue === $response->headers->get($this->headerName, null); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php new file mode 100644 index 0000000..8c4b883 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsRedirected.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseIsRedirected extends Constraint +{ + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'is redirected'; + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $response->isRedirect(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function additionalFailureDescription($response): string + { + return (string) $response; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php new file mode 100644 index 0000000..9c66558 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsSuccessful.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseIsSuccessful extends Constraint +{ + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'is successful'; + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $response->isSuccessful(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function additionalFailureDescription($response): string + { + return (string) $response; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php new file mode 100644 index 0000000..880c781 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseIsUnprocessable.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseIsUnprocessable extends Constraint +{ + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'is unprocessable'; + } + + /** + * @param Response $other + * + * {@inheritdoc} + */ + protected function matches($other): bool + { + return Response::HTTP_UNPROCESSABLE_ENTITY === $other->getStatusCode(); + } + + /** + * @param Response $other + * + * {@inheritdoc} + */ + protected function failureDescription($other): string + { + return 'the Response '.$this->toString(); + } + + /** + * @param Response $other + * + * {@inheritdoc} + */ + protected function additionalFailureDescription($other): string + { + return (string) $other; + } +} diff --git a/vendor/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php b/vendor/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php new file mode 100644 index 0000000..72bb000 --- /dev/null +++ b/vendor/symfony/http-foundation/Test/Constraint/ResponseStatusCodeSame.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\HttpFoundation\Response; + +final class ResponseStatusCodeSame extends Constraint +{ + private $statusCode; + + public function __construct(int $statusCode) + { + $this->statusCode = $statusCode; + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'status code is '.$this->statusCode; + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function matches($response): bool + { + return $this->statusCode === $response->getStatusCode(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function failureDescription($response): string + { + return 'the Response '.$this->toString(); + } + + /** + * @param Response $response + * + * {@inheritdoc} + */ + protected function additionalFailureDescription($response): string + { + return (string) $response; + } +} diff --git a/vendor/symfony/http-foundation/UrlHelper.php b/vendor/symfony/http-foundation/UrlHelper.php new file mode 100644 index 0000000..c15f101 --- /dev/null +++ b/vendor/symfony/http-foundation/UrlHelper.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\Routing\RequestContext; + +/** + * A helper service for manipulating URLs within and outside the request scope. + * + * @author Valentin Udaltsov + */ +final class UrlHelper +{ + private $requestStack; + private $requestContext; + + public function __construct(RequestStack $requestStack, RequestContext $requestContext = null) + { + $this->requestStack = $requestStack; + $this->requestContext = $requestContext; + } + + public function getAbsoluteUrl(string $path): string + { + if (str_contains($path, '://') || '//' === substr($path, 0, 2)) { + return $path; + } + + if (null === $request = $this->requestStack->getMainRequest()) { + return $this->getAbsoluteUrlFromContext($path); + } + + if ('#' === $path[0]) { + $path = $request->getRequestUri().$path; + } elseif ('?' === $path[0]) { + $path = $request->getPathInfo().$path; + } + + if (!$path || '/' !== $path[0]) { + $prefix = $request->getPathInfo(); + $last = \strlen($prefix) - 1; + if ($last !== $pos = strrpos($prefix, '/')) { + $prefix = substr($prefix, 0, $pos).'/'; + } + + return $request->getUriForPath($prefix.$path); + } + + return $request->getSchemeAndHttpHost().$path; + } + + public function getRelativePath(string $path): string + { + if (str_contains($path, '://') || '//' === substr($path, 0, 2)) { + return $path; + } + + if (null === $request = $this->requestStack->getMainRequest()) { + return $path; + } + + return $request->getRelativeUriForPath($path); + } + + private function getAbsoluteUrlFromContext(string $path): string + { + if (null === $this->requestContext || '' === $host = $this->requestContext->getHost()) { + return $path; + } + + $scheme = $this->requestContext->getScheme(); + $port = ''; + + if ('http' === $scheme && 80 !== $this->requestContext->getHttpPort()) { + $port = ':'.$this->requestContext->getHttpPort(); + } elseif ('https' === $scheme && 443 !== $this->requestContext->getHttpsPort()) { + $port = ':'.$this->requestContext->getHttpsPort(); + } + + if ('#' === $path[0]) { + $queryString = $this->requestContext->getQueryString(); + $path = $this->requestContext->getPathInfo().($queryString ? '?'.$queryString : '').$path; + } elseif ('?' === $path[0]) { + $path = $this->requestContext->getPathInfo().$path; + } + + if ('/' !== $path[0]) { + $path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path; + } + + return $scheme.'://'.$host.$port.$path; + } +} diff --git a/vendor/symfony/polyfill-ctype/bootstrap80.php b/vendor/symfony/polyfill-ctype/bootstrap80.php new file mode 100644 index 0000000..ab2f861 --- /dev/null +++ b/vendor/symfony/polyfill-ctype/bootstrap80.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Ctype as p; + +if (!function_exists('ctype_alnum')) { + function ctype_alnum(mixed $text): bool { return p\Ctype::ctype_alnum($text); } +} +if (!function_exists('ctype_alpha')) { + function ctype_alpha(mixed $text): bool { return p\Ctype::ctype_alpha($text); } +} +if (!function_exists('ctype_cntrl')) { + function ctype_cntrl(mixed $text): bool { return p\Ctype::ctype_cntrl($text); } +} +if (!function_exists('ctype_digit')) { + function ctype_digit(mixed $text): bool { return p\Ctype::ctype_digit($text); } +} +if (!function_exists('ctype_graph')) { + function ctype_graph(mixed $text): bool { return p\Ctype::ctype_graph($text); } +} +if (!function_exists('ctype_lower')) { + function ctype_lower(mixed $text): bool { return p\Ctype::ctype_lower($text); } +} +if (!function_exists('ctype_print')) { + function ctype_print(mixed $text): bool { return p\Ctype::ctype_print($text); } +} +if (!function_exists('ctype_punct')) { + function ctype_punct(mixed $text): bool { return p\Ctype::ctype_punct($text); } +} +if (!function_exists('ctype_space')) { + function ctype_space(mixed $text): bool { return p\Ctype::ctype_space($text); } +} +if (!function_exists('ctype_upper')) { + function ctype_upper(mixed $text): bool { return p\Ctype::ctype_upper($text); } +} +if (!function_exists('ctype_xdigit')) { + function ctype_xdigit(mixed $text): bool { return p\Ctype::ctype_xdigit($text); } +} diff --git a/vendor/symfony/polyfill-intl-grapheme/bootstrap80.php b/vendor/symfony/polyfill-intl-grapheme/bootstrap80.php new file mode 100644 index 0000000..53b4529 --- /dev/null +++ b/vendor/symfony/polyfill-intl-grapheme/bootstrap80.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Grapheme as p; + +if (!defined('GRAPHEME_EXTR_COUNT')) { + define('GRAPHEME_EXTR_COUNT', 0); +} +if (!defined('GRAPHEME_EXTR_MAXBYTES')) { + define('GRAPHEME_EXTR_MAXBYTES', 1); +} +if (!defined('GRAPHEME_EXTR_MAXCHARS')) { + define('GRAPHEME_EXTR_MAXCHARS', 2); +} + +if (!function_exists('grapheme_extract')) { + function grapheme_extract(?string $haystack, ?int $size, ?int $type = GRAPHEME_EXTR_COUNT, ?int $offset = 0, &$next = null): string|false { return p\Grapheme::grapheme_extract((string) $haystack, (int) $size, (int) $type, (int) $offset, $next); } +} +if (!function_exists('grapheme_stripos')) { + function grapheme_stripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_stripos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_stristr')) { + function grapheme_stristr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_stristr((string) $haystack, (string) $needle, (bool) $beforeNeedle); } +} +if (!function_exists('grapheme_strlen')) { + function grapheme_strlen(?string $string): int|false|null { return p\Grapheme::grapheme_strlen((string) $string); } +} +if (!function_exists('grapheme_strpos')) { + function grapheme_strpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strpos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_strripos')) { + function grapheme_strripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strripos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_strrpos')) { + function grapheme_strrpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strrpos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_strstr')) { + function grapheme_strstr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_strstr((string) $haystack, (string) $needle, (bool) $beforeNeedle); } +} +if (!function_exists('grapheme_substr')) { + function grapheme_substr(?string $string, ?int $offset, ?int $length = null): string|false { return p\Grapheme::grapheme_substr((string) $string, (int) $offset, (int) $length); } +} diff --git a/vendor/symfony/polyfill-intl-idn/Idn.php b/vendor/symfony/polyfill-intl-idn/Idn.php new file mode 100644 index 0000000..fee3026 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/Idn.php @@ -0,0 +1,925 @@ + and Trevor Rowbotham + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Idn; + +use Exception; +use Normalizer; +use Symfony\Polyfill\Intl\Idn\Resources\unidata\DisallowedRanges; +use Symfony\Polyfill\Intl\Idn\Resources\unidata\Regex; + +/** + * @see https://www.unicode.org/reports/tr46/ + * + * @internal + */ +final class Idn +{ + public const ERROR_EMPTY_LABEL = 1; + public const ERROR_LABEL_TOO_LONG = 2; + public const ERROR_DOMAIN_NAME_TOO_LONG = 4; + public const ERROR_LEADING_HYPHEN = 8; + public const ERROR_TRAILING_HYPHEN = 0x10; + public const ERROR_HYPHEN_3_4 = 0x20; + public const ERROR_LEADING_COMBINING_MARK = 0x40; + public const ERROR_DISALLOWED = 0x80; + public const ERROR_PUNYCODE = 0x100; + public const ERROR_LABEL_HAS_DOT = 0x200; + public const ERROR_INVALID_ACE_LABEL = 0x400; + public const ERROR_BIDI = 0x800; + public const ERROR_CONTEXTJ = 0x1000; + public const ERROR_CONTEXTO_PUNCTUATION = 0x2000; + public const ERROR_CONTEXTO_DIGITS = 0x4000; + + public const INTL_IDNA_VARIANT_2003 = 0; + public const INTL_IDNA_VARIANT_UTS46 = 1; + + public const IDNA_DEFAULT = 0; + public const IDNA_ALLOW_UNASSIGNED = 1; + public const IDNA_USE_STD3_RULES = 2; + public const IDNA_CHECK_BIDI = 4; + public const IDNA_CHECK_CONTEXTJ = 8; + public const IDNA_NONTRANSITIONAL_TO_ASCII = 16; + public const IDNA_NONTRANSITIONAL_TO_UNICODE = 32; + + public const MAX_DOMAIN_SIZE = 253; + public const MAX_LABEL_SIZE = 63; + + public const BASE = 36; + public const TMIN = 1; + public const TMAX = 26; + public const SKEW = 38; + public const DAMP = 700; + public const INITIAL_BIAS = 72; + public const INITIAL_N = 128; + public const DELIMITER = '-'; + public const MAX_INT = 2147483647; + + /** + * Contains the numeric value of a basic code point (for use in representing integers) in the + * range 0 to BASE-1, or -1 if b is does not represent a value. + * + * @var array + */ + private static $basicToDigit = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, -1, + + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + ]; + + /** + * @var array + */ + private static $virama; + + /** + * @var array + */ + private static $mapped; + + /** + * @var array + */ + private static $ignored; + + /** + * @var array + */ + private static $deviation; + + /** + * @var array + */ + private static $disallowed; + + /** + * @var array + */ + private static $disallowed_STD3_mapped; + + /** + * @var array + */ + private static $disallowed_STD3_valid; + + /** + * @var bool + */ + private static $mappingTableLoaded = false; + + /** + * @see https://www.unicode.org/reports/tr46/#ToASCII + * + * @param string $domainName + * @param int $options + * @param int $variant + * @param array $idna_info + * + * @return string|false + */ + public static function idn_to_ascii($domainName, $options = self::IDNA_DEFAULT, $variant = self::INTL_IDNA_VARIANT_UTS46, &$idna_info = []) + { + if (\PHP_VERSION_ID >= 70200 && self::INTL_IDNA_VARIANT_2003 === $variant) { + @trigger_error('idn_to_ascii(): INTL_IDNA_VARIANT_2003 is deprecated', \E_USER_DEPRECATED); + } + + $options = [ + 'CheckHyphens' => true, + 'CheckBidi' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 !== ($options & self::IDNA_CHECK_BIDI), + 'CheckJoiners' => self::INTL_IDNA_VARIANT_UTS46 === $variant && 0 !== ($options & self::IDNA_CHECK_CONTEXTJ), + 'UseSTD3ASCIIRules' => 0 !== ($options & self::IDNA_USE_STD3_RULES), + 'Transitional_Processing' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 === ($options & self::IDNA_NONTRANSITIONAL_TO_ASCII), + 'VerifyDnsLength' => true, + ]; + $info = new Info(); + $labels = self::process((string) $domainName, $options, $info); + + foreach ($labels as $i => $label) { + // Only convert labels to punycode that contain non-ASCII code points + if (1 === preg_match('/[^\x00-\x7F]/', $label)) { + try { + $label = 'xn--'.self::punycodeEncode($label); + } catch (Exception $e) { + $info->errors |= self::ERROR_PUNYCODE; + } + + $labels[$i] = $label; + } + } + + if ($options['VerifyDnsLength']) { + self::validateDomainAndLabelLength($labels, $info); + } + + $idna_info = [ + 'result' => implode('.', $labels), + 'isTransitionalDifferent' => $info->transitionalDifferent, + 'errors' => $info->errors, + ]; + + return 0 === $info->errors ? $idna_info['result'] : false; + } + + /** + * @see https://www.unicode.org/reports/tr46/#ToUnicode + * + * @param string $domainName + * @param int $options + * @param int $variant + * @param array $idna_info + * + * @return string|false + */ + public static function idn_to_utf8($domainName, $options = self::IDNA_DEFAULT, $variant = self::INTL_IDNA_VARIANT_UTS46, &$idna_info = []) + { + if (\PHP_VERSION_ID >= 70200 && self::INTL_IDNA_VARIANT_2003 === $variant) { + @trigger_error('idn_to_utf8(): INTL_IDNA_VARIANT_2003 is deprecated', \E_USER_DEPRECATED); + } + + $info = new Info(); + $labels = self::process((string) $domainName, [ + 'CheckHyphens' => true, + 'CheckBidi' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 !== ($options & self::IDNA_CHECK_BIDI), + 'CheckJoiners' => self::INTL_IDNA_VARIANT_UTS46 === $variant && 0 !== ($options & self::IDNA_CHECK_CONTEXTJ), + 'UseSTD3ASCIIRules' => 0 !== ($options & self::IDNA_USE_STD3_RULES), + 'Transitional_Processing' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 === ($options & self::IDNA_NONTRANSITIONAL_TO_UNICODE), + ], $info); + $idna_info = [ + 'result' => implode('.', $labels), + 'isTransitionalDifferent' => $info->transitionalDifferent, + 'errors' => $info->errors, + ]; + + return 0 === $info->errors ? $idna_info['result'] : false; + } + + /** + * @param string $label + * + * @return bool + */ + private static function isValidContextJ(array $codePoints, $label) + { + if (!isset(self::$virama)) { + self::$virama = require __DIR__.\DIRECTORY_SEPARATOR.'Resources'.\DIRECTORY_SEPARATOR.'unidata'.\DIRECTORY_SEPARATOR.'virama.php'; + } + + $offset = 0; + + foreach ($codePoints as $i => $codePoint) { + if (0x200C !== $codePoint && 0x200D !== $codePoint) { + continue; + } + + if (!isset($codePoints[$i - 1])) { + return false; + } + + // If Canonical_Combining_Class(Before(cp)) .eq. Virama Then True; + if (isset(self::$virama[$codePoints[$i - 1]])) { + continue; + } + + // If RegExpMatch((Joining_Type:{L,D})(Joining_Type:T)*\u200C(Joining_Type:T)*(Joining_Type:{R,D})) Then + // True; + // Generated RegExp = ([Joining_Type:{L,D}][Joining_Type:T]*\u200C[Joining_Type:T]*)[Joining_Type:{R,D}] + if (0x200C === $codePoint && 1 === preg_match(Regex::ZWNJ, $label, $matches, \PREG_OFFSET_CAPTURE, $offset)) { + $offset += \strlen($matches[1][0]); + + continue; + } + + return false; + } + + return true; + } + + /** + * @see https://www.unicode.org/reports/tr46/#ProcessingStepMap + * + * @param string $input + * @param array $options + * + * @return string + */ + private static function mapCodePoints($input, array $options, Info $info) + { + $str = ''; + $useSTD3ASCIIRules = $options['UseSTD3ASCIIRules']; + $transitional = $options['Transitional_Processing']; + + foreach (self::utf8Decode($input) as $codePoint) { + $data = self::lookupCodePointStatus($codePoint, $useSTD3ASCIIRules); + + switch ($data['status']) { + case 'disallowed': + $info->errors |= self::ERROR_DISALLOWED; + + // no break. + + case 'valid': + $str .= mb_chr($codePoint, 'utf-8'); + + break; + + case 'ignored': + // Do nothing. + break; + + case 'mapped': + $str .= $data['mapping']; + + break; + + case 'deviation': + $info->transitionalDifferent = true; + $str .= ($transitional ? $data['mapping'] : mb_chr($codePoint, 'utf-8')); + + break; + } + } + + return $str; + } + + /** + * @see https://www.unicode.org/reports/tr46/#Processing + * + * @param string $domain + * @param array $options + * + * @return array + */ + private static function process($domain, array $options, Info $info) + { + // If VerifyDnsLength is not set, we are doing ToUnicode otherwise we are doing ToASCII and + // we need to respect the VerifyDnsLength option. + $checkForEmptyLabels = !isset($options['VerifyDnsLength']) || $options['VerifyDnsLength']; + + if ($checkForEmptyLabels && '' === $domain) { + $info->errors |= self::ERROR_EMPTY_LABEL; + + return [$domain]; + } + + // Step 1. Map each code point in the domain name string + $domain = self::mapCodePoints($domain, $options, $info); + + // Step 2. Normalize the domain name string to Unicode Normalization Form C. + if (!Normalizer::isNormalized($domain, Normalizer::FORM_C)) { + $domain = Normalizer::normalize($domain, Normalizer::FORM_C); + } + + // Step 3. Break the string into labels at U+002E (.) FULL STOP. + $labels = explode('.', $domain); + $lastLabelIndex = \count($labels) - 1; + + // Step 4. Convert and validate each label in the domain name string. + foreach ($labels as $i => $label) { + $validationOptions = $options; + + if ('xn--' === substr($label, 0, 4)) { + try { + $label = self::punycodeDecode(substr($label, 4)); + } catch (Exception $e) { + $info->errors |= self::ERROR_PUNYCODE; + + continue; + } + + $validationOptions['Transitional_Processing'] = false; + $labels[$i] = $label; + } + + self::validateLabel($label, $info, $validationOptions, $i > 0 && $i === $lastLabelIndex); + } + + if ($info->bidiDomain && !$info->validBidiDomain) { + $info->errors |= self::ERROR_BIDI; + } + + // Any input domain name string that does not record an error has been successfully + // processed according to this specification. Conversely, if an input domain_name string + // causes an error, then the processing of the input domain_name string fails. Determining + // what to do with error input is up to the caller, and not in the scope of this document. + return $labels; + } + + /** + * @see https://tools.ietf.org/html/rfc5893#section-2 + * + * @param string $label + */ + private static function validateBidiLabel($label, Info $info) + { + if (1 === preg_match(Regex::RTL_LABEL, $label)) { + $info->bidiDomain = true; + + // Step 1. The first character must be a character with Bidi property L, R, or AL. + // If it has the R or AL property, it is an RTL label + if (1 !== preg_match(Regex::BIDI_STEP_1_RTL, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 2. In an RTL label, only characters with the Bidi properties R, AL, AN, EN, ES, + // CS, ET, ON, BN, or NSM are allowed. + if (1 === preg_match(Regex::BIDI_STEP_2, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 3. In an RTL label, the end of the label must be a character with Bidi property + // R, AL, EN, or AN, followed by zero or more characters with Bidi property NSM. + if (1 !== preg_match(Regex::BIDI_STEP_3, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 4. In an RTL label, if an EN is present, no AN may be present, and vice versa. + if (1 === preg_match(Regex::BIDI_STEP_4_AN, $label) && 1 === preg_match(Regex::BIDI_STEP_4_EN, $label)) { + $info->validBidiDomain = false; + + return; + } + + return; + } + + // We are a LTR label + // Step 1. The first character must be a character with Bidi property L, R, or AL. + // If it has the L property, it is an LTR label. + if (1 !== preg_match(Regex::BIDI_STEP_1_LTR, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 5. In an LTR label, only characters with the Bidi properties L, EN, + // ES, CS, ET, ON, BN, or NSM are allowed. + if (1 === preg_match(Regex::BIDI_STEP_5, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 6.In an LTR label, the end of the label must be a character with Bidi property L or + // EN, followed by zero or more characters with Bidi property NSM. + if (1 !== preg_match(Regex::BIDI_STEP_6, $label)) { + $info->validBidiDomain = false; + + return; + } + } + + /** + * @param array $labels + */ + private static function validateDomainAndLabelLength(array $labels, Info $info) + { + $maxDomainSize = self::MAX_DOMAIN_SIZE; + $length = \count($labels); + + // Number of "." delimiters. + $domainLength = $length - 1; + + // If the last label is empty and it is not the first label, then it is the root label. + // Increase the max size by 1, making it 254, to account for the root label's "." + // delimiter. This also means we don't need to check the last label's length for being too + // long. + if ($length > 1 && '' === $labels[$length - 1]) { + ++$maxDomainSize; + --$length; + } + + for ($i = 0; $i < $length; ++$i) { + $bytes = \strlen($labels[$i]); + $domainLength += $bytes; + + if ($bytes > self::MAX_LABEL_SIZE) { + $info->errors |= self::ERROR_LABEL_TOO_LONG; + } + } + + if ($domainLength > $maxDomainSize) { + $info->errors |= self::ERROR_DOMAIN_NAME_TOO_LONG; + } + } + + /** + * @see https://www.unicode.org/reports/tr46/#Validity_Criteria + * + * @param string $label + * @param array $options + * @param bool $canBeEmpty + */ + private static function validateLabel($label, Info $info, array $options, $canBeEmpty) + { + if ('' === $label) { + if (!$canBeEmpty && (!isset($options['VerifyDnsLength']) || $options['VerifyDnsLength'])) { + $info->errors |= self::ERROR_EMPTY_LABEL; + } + + return; + } + + // Step 1. The label must be in Unicode Normalization Form C. + if (!Normalizer::isNormalized($label, Normalizer::FORM_C)) { + $info->errors |= self::ERROR_INVALID_ACE_LABEL; + } + + $codePoints = self::utf8Decode($label); + + if ($options['CheckHyphens']) { + // Step 2. If CheckHyphens, the label must not contain a U+002D HYPHEN-MINUS character + // in both the thrid and fourth positions. + if (isset($codePoints[2], $codePoints[3]) && 0x002D === $codePoints[2] && 0x002D === $codePoints[3]) { + $info->errors |= self::ERROR_HYPHEN_3_4; + } + + // Step 3. If CheckHyphens, the label must neither begin nor end with a U+002D + // HYPHEN-MINUS character. + if ('-' === substr($label, 0, 1)) { + $info->errors |= self::ERROR_LEADING_HYPHEN; + } + + if ('-' === substr($label, -1, 1)) { + $info->errors |= self::ERROR_TRAILING_HYPHEN; + } + } + + // Step 4. The label must not contain a U+002E (.) FULL STOP. + if (false !== strpos($label, '.')) { + $info->errors |= self::ERROR_LABEL_HAS_DOT; + } + + // Step 5. The label must not begin with a combining mark, that is: General_Category=Mark. + if (1 === preg_match(Regex::COMBINING_MARK, $label)) { + $info->errors |= self::ERROR_LEADING_COMBINING_MARK; + } + + // Step 6. Each code point in the label must only have certain status values according to + // Section 5, IDNA Mapping Table: + $transitional = $options['Transitional_Processing']; + $useSTD3ASCIIRules = $options['UseSTD3ASCIIRules']; + + foreach ($codePoints as $codePoint) { + $data = self::lookupCodePointStatus($codePoint, $useSTD3ASCIIRules); + $status = $data['status']; + + if ('valid' === $status || (!$transitional && 'deviation' === $status)) { + continue; + } + + $info->errors |= self::ERROR_DISALLOWED; + + break; + } + + // Step 7. If CheckJoiners, the label must satisify the ContextJ rules from Appendix A, in + // The Unicode Code Points and Internationalized Domain Names for Applications (IDNA) + // [IDNA2008]. + if ($options['CheckJoiners'] && !self::isValidContextJ($codePoints, $label)) { + $info->errors |= self::ERROR_CONTEXTJ; + } + + // Step 8. If CheckBidi, and if the domain name is a Bidi domain name, then the label must + // satisfy all six of the numbered conditions in [IDNA2008] RFC 5893, Section 2. + if ($options['CheckBidi'] && (!$info->bidiDomain || $info->validBidiDomain)) { + self::validateBidiLabel($label, $info); + } + } + + /** + * @see https://tools.ietf.org/html/rfc3492#section-6.2 + * + * @param string $input + * + * @return string + */ + private static function punycodeDecode($input) + { + $n = self::INITIAL_N; + $out = 0; + $i = 0; + $bias = self::INITIAL_BIAS; + $lastDelimIndex = strrpos($input, self::DELIMITER); + $b = false === $lastDelimIndex ? 0 : $lastDelimIndex; + $inputLength = \strlen($input); + $output = []; + $bytes = array_map('ord', str_split($input)); + + for ($j = 0; $j < $b; ++$j) { + if ($bytes[$j] > 0x7F) { + throw new Exception('Invalid input'); + } + + $output[$out++] = $input[$j]; + } + + if ($b > 0) { + ++$b; + } + + for ($in = $b; $in < $inputLength; ++$out) { + $oldi = $i; + $w = 1; + + for ($k = self::BASE; /* no condition */; $k += self::BASE) { + if ($in >= $inputLength) { + throw new Exception('Invalid input'); + } + + $digit = self::$basicToDigit[$bytes[$in++] & 0xFF]; + + if ($digit < 0) { + throw new Exception('Invalid input'); + } + + if ($digit > intdiv(self::MAX_INT - $i, $w)) { + throw new Exception('Integer overflow'); + } + + $i += $digit * $w; + + if ($k <= $bias) { + $t = self::TMIN; + } elseif ($k >= $bias + self::TMAX) { + $t = self::TMAX; + } else { + $t = $k - $bias; + } + + if ($digit < $t) { + break; + } + + $baseMinusT = self::BASE - $t; + + if ($w > intdiv(self::MAX_INT, $baseMinusT)) { + throw new Exception('Integer overflow'); + } + + $w *= $baseMinusT; + } + + $outPlusOne = $out + 1; + $bias = self::adaptBias($i - $oldi, $outPlusOne, 0 === $oldi); + + if (intdiv($i, $outPlusOne) > self::MAX_INT - $n) { + throw new Exception('Integer overflow'); + } + + $n += intdiv($i, $outPlusOne); + $i %= $outPlusOne; + array_splice($output, $i++, 0, [mb_chr($n, 'utf-8')]); + } + + return implode('', $output); + } + + /** + * @see https://tools.ietf.org/html/rfc3492#section-6.3 + * + * @param string $input + * + * @return string + */ + private static function punycodeEncode($input) + { + $n = self::INITIAL_N; + $delta = 0; + $out = 0; + $bias = self::INITIAL_BIAS; + $inputLength = 0; + $output = ''; + $iter = self::utf8Decode($input); + + foreach ($iter as $codePoint) { + ++$inputLength; + + if ($codePoint < 0x80) { + $output .= \chr($codePoint); + ++$out; + } + } + + $h = $out; + $b = $out; + + if ($b > 0) { + $output .= self::DELIMITER; + ++$out; + } + + while ($h < $inputLength) { + $m = self::MAX_INT; + + foreach ($iter as $codePoint) { + if ($codePoint >= $n && $codePoint < $m) { + $m = $codePoint; + } + } + + if ($m - $n > intdiv(self::MAX_INT - $delta, $h + 1)) { + throw new Exception('Integer overflow'); + } + + $delta += ($m - $n) * ($h + 1); + $n = $m; + + foreach ($iter as $codePoint) { + if ($codePoint < $n && 0 === ++$delta) { + throw new Exception('Integer overflow'); + } + + if ($codePoint === $n) { + $q = $delta; + + for ($k = self::BASE; /* no condition */; $k += self::BASE) { + if ($k <= $bias) { + $t = self::TMIN; + } elseif ($k >= $bias + self::TMAX) { + $t = self::TMAX; + } else { + $t = $k - $bias; + } + + if ($q < $t) { + break; + } + + $qMinusT = $q - $t; + $baseMinusT = self::BASE - $t; + $output .= self::encodeDigit($t + ($qMinusT) % ($baseMinusT), false); + ++$out; + $q = intdiv($qMinusT, $baseMinusT); + } + + $output .= self::encodeDigit($q, false); + ++$out; + $bias = self::adaptBias($delta, $h + 1, $h === $b); + $delta = 0; + ++$h; + } + } + + ++$delta; + ++$n; + } + + return $output; + } + + /** + * @see https://tools.ietf.org/html/rfc3492#section-6.1 + * + * @param int $delta + * @param int $numPoints + * @param bool $firstTime + * + * @return int + */ + private static function adaptBias($delta, $numPoints, $firstTime) + { + // xxx >> 1 is a faster way of doing intdiv(xxx, 2) + $delta = $firstTime ? intdiv($delta, self::DAMP) : $delta >> 1; + $delta += intdiv($delta, $numPoints); + $k = 0; + + while ($delta > ((self::BASE - self::TMIN) * self::TMAX) >> 1) { + $delta = intdiv($delta, self::BASE - self::TMIN); + $k += self::BASE; + } + + return $k + intdiv((self::BASE - self::TMIN + 1) * $delta, $delta + self::SKEW); + } + + /** + * @param int $d + * @param bool $flag + * + * @return string + */ + private static function encodeDigit($d, $flag) + { + return \chr($d + 22 + 75 * ($d < 26 ? 1 : 0) - (($flag ? 1 : 0) << 5)); + } + + /** + * Takes a UTF-8 encoded string and converts it into a series of integer code points. Any + * invalid byte sequences will be replaced by a U+FFFD replacement code point. + * + * @see https://encoding.spec.whatwg.org/#utf-8-decoder + * + * @param string $input + * + * @return array + */ + private static function utf8Decode($input) + { + $bytesSeen = 0; + $bytesNeeded = 0; + $lowerBoundary = 0x80; + $upperBoundary = 0xBF; + $codePoint = 0; + $codePoints = []; + $length = \strlen($input); + + for ($i = 0; $i < $length; ++$i) { + $byte = \ord($input[$i]); + + if (0 === $bytesNeeded) { + if ($byte >= 0x00 && $byte <= 0x7F) { + $codePoints[] = $byte; + + continue; + } + + if ($byte >= 0xC2 && $byte <= 0xDF) { + $bytesNeeded = 1; + $codePoint = $byte & 0x1F; + } elseif ($byte >= 0xE0 && $byte <= 0xEF) { + if (0xE0 === $byte) { + $lowerBoundary = 0xA0; + } elseif (0xED === $byte) { + $upperBoundary = 0x9F; + } + + $bytesNeeded = 2; + $codePoint = $byte & 0xF; + } elseif ($byte >= 0xF0 && $byte <= 0xF4) { + if (0xF0 === $byte) { + $lowerBoundary = 0x90; + } elseif (0xF4 === $byte) { + $upperBoundary = 0x8F; + } + + $bytesNeeded = 3; + $codePoint = $byte & 0x7; + } else { + $codePoints[] = 0xFFFD; + } + + continue; + } + + if ($byte < $lowerBoundary || $byte > $upperBoundary) { + $codePoint = 0; + $bytesNeeded = 0; + $bytesSeen = 0; + $lowerBoundary = 0x80; + $upperBoundary = 0xBF; + --$i; + $codePoints[] = 0xFFFD; + + continue; + } + + $lowerBoundary = 0x80; + $upperBoundary = 0xBF; + $codePoint = ($codePoint << 6) | ($byte & 0x3F); + + if (++$bytesSeen !== $bytesNeeded) { + continue; + } + + $codePoints[] = $codePoint; + $codePoint = 0; + $bytesNeeded = 0; + $bytesSeen = 0; + } + + // String unexpectedly ended, so append a U+FFFD code point. + if (0 !== $bytesNeeded) { + $codePoints[] = 0xFFFD; + } + + return $codePoints; + } + + /** + * @param int $codePoint + * @param bool $useSTD3ASCIIRules + * + * @return array{status: string, mapping?: string} + */ + private static function lookupCodePointStatus($codePoint, $useSTD3ASCIIRules) + { + if (!self::$mappingTableLoaded) { + self::$mappingTableLoaded = true; + self::$mapped = require __DIR__.'/Resources/unidata/mapped.php'; + self::$ignored = require __DIR__.'/Resources/unidata/ignored.php'; + self::$deviation = require __DIR__.'/Resources/unidata/deviation.php'; + self::$disallowed = require __DIR__.'/Resources/unidata/disallowed.php'; + self::$disallowed_STD3_mapped = require __DIR__.'/Resources/unidata/disallowed_STD3_mapped.php'; + self::$disallowed_STD3_valid = require __DIR__.'/Resources/unidata/disallowed_STD3_valid.php'; + } + + if (isset(self::$mapped[$codePoint])) { + return ['status' => 'mapped', 'mapping' => self::$mapped[$codePoint]]; + } + + if (isset(self::$ignored[$codePoint])) { + return ['status' => 'ignored']; + } + + if (isset(self::$deviation[$codePoint])) { + return ['status' => 'deviation', 'mapping' => self::$deviation[$codePoint]]; + } + + if (isset(self::$disallowed[$codePoint]) || DisallowedRanges::inRange($codePoint)) { + return ['status' => 'disallowed']; + } + + $isDisallowedMapped = isset(self::$disallowed_STD3_mapped[$codePoint]); + + if ($isDisallowedMapped || isset(self::$disallowed_STD3_valid[$codePoint])) { + $status = 'disallowed'; + + if (!$useSTD3ASCIIRules) { + $status = $isDisallowedMapped ? 'mapped' : 'valid'; + } + + if ($isDisallowedMapped) { + return ['status' => $status, 'mapping' => self::$disallowed_STD3_mapped[$codePoint]]; + } + + return ['status' => $status]; + } + + return ['status' => 'valid']; + } +} diff --git a/vendor/symfony/polyfill-intl-idn/Info.php b/vendor/symfony/polyfill-intl-idn/Info.php new file mode 100644 index 0000000..25c3582 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/Info.php @@ -0,0 +1,23 @@ + and Trevor Rowbotham + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Idn; + +/** + * @internal + */ +class Info +{ + public $bidiDomain = false; + public $errors = 0; + public $validBidiDomain = true; + public $transitionalDifferent = false; +} diff --git a/vendor/symfony/polyfill-intl-idn/LICENSE b/vendor/symfony/polyfill-intl-idn/LICENSE new file mode 100644 index 0000000..03c5e25 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2019 Fabien Potencier and Trevor Rowbotham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/polyfill-intl-idn/README.md b/vendor/symfony/polyfill-intl-idn/README.md new file mode 100644 index 0000000..cae5517 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/README.md @@ -0,0 +1,12 @@ +Symfony Polyfill / Intl: Idn +============================ + +This component provides [`idn_to_ascii`](https://php.net/idn-to-ascii) and [`idn_to_utf8`](https://php.net/idn-to-utf8) functions to users who run php versions without the [Intl](https://php.net/intl) extension. + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/vendor/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php b/vendor/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php new file mode 100644 index 0000000..5bb70e4 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php @@ -0,0 +1,375 @@ += 128 && $codePoint <= 159) { + return true; + } + + if ($codePoint >= 2155 && $codePoint <= 2207) { + return true; + } + + if ($codePoint >= 3676 && $codePoint <= 3712) { + return true; + } + + if ($codePoint >= 3808 && $codePoint <= 3839) { + return true; + } + + if ($codePoint >= 4059 && $codePoint <= 4095) { + return true; + } + + if ($codePoint >= 4256 && $codePoint <= 4293) { + return true; + } + + if ($codePoint >= 6849 && $codePoint <= 6911) { + return true; + } + + if ($codePoint >= 11859 && $codePoint <= 11903) { + return true; + } + + if ($codePoint >= 42955 && $codePoint <= 42996) { + return true; + } + + if ($codePoint >= 55296 && $codePoint <= 57343) { + return true; + } + + if ($codePoint >= 57344 && $codePoint <= 63743) { + return true; + } + + if ($codePoint >= 64218 && $codePoint <= 64255) { + return true; + } + + if ($codePoint >= 64976 && $codePoint <= 65007) { + return true; + } + + if ($codePoint >= 65630 && $codePoint <= 65663) { + return true; + } + + if ($codePoint >= 65953 && $codePoint <= 65999) { + return true; + } + + if ($codePoint >= 66046 && $codePoint <= 66175) { + return true; + } + + if ($codePoint >= 66518 && $codePoint <= 66559) { + return true; + } + + if ($codePoint >= 66928 && $codePoint <= 67071) { + return true; + } + + if ($codePoint >= 67432 && $codePoint <= 67583) { + return true; + } + + if ($codePoint >= 67760 && $codePoint <= 67807) { + return true; + } + + if ($codePoint >= 67904 && $codePoint <= 67967) { + return true; + } + + if ($codePoint >= 68256 && $codePoint <= 68287) { + return true; + } + + if ($codePoint >= 68528 && $codePoint <= 68607) { + return true; + } + + if ($codePoint >= 68681 && $codePoint <= 68735) { + return true; + } + + if ($codePoint >= 68922 && $codePoint <= 69215) { + return true; + } + + if ($codePoint >= 69298 && $codePoint <= 69375) { + return true; + } + + if ($codePoint >= 69466 && $codePoint <= 69551) { + return true; + } + + if ($codePoint >= 70207 && $codePoint <= 70271) { + return true; + } + + if ($codePoint >= 70517 && $codePoint <= 70655) { + return true; + } + + if ($codePoint >= 70874 && $codePoint <= 71039) { + return true; + } + + if ($codePoint >= 71134 && $codePoint <= 71167) { + return true; + } + + if ($codePoint >= 71370 && $codePoint <= 71423) { + return true; + } + + if ($codePoint >= 71488 && $codePoint <= 71679) { + return true; + } + + if ($codePoint >= 71740 && $codePoint <= 71839) { + return true; + } + + if ($codePoint >= 72026 && $codePoint <= 72095) { + return true; + } + + if ($codePoint >= 72441 && $codePoint <= 72703) { + return true; + } + + if ($codePoint >= 72887 && $codePoint <= 72959) { + return true; + } + + if ($codePoint >= 73130 && $codePoint <= 73439) { + return true; + } + + if ($codePoint >= 73465 && $codePoint <= 73647) { + return true; + } + + if ($codePoint >= 74650 && $codePoint <= 74751) { + return true; + } + + if ($codePoint >= 75076 && $codePoint <= 77823) { + return true; + } + + if ($codePoint >= 78905 && $codePoint <= 82943) { + return true; + } + + if ($codePoint >= 83527 && $codePoint <= 92159) { + return true; + } + + if ($codePoint >= 92784 && $codePoint <= 92879) { + return true; + } + + if ($codePoint >= 93072 && $codePoint <= 93759) { + return true; + } + + if ($codePoint >= 93851 && $codePoint <= 93951) { + return true; + } + + if ($codePoint >= 94112 && $codePoint <= 94175) { + return true; + } + + if ($codePoint >= 101590 && $codePoint <= 101631) { + return true; + } + + if ($codePoint >= 101641 && $codePoint <= 110591) { + return true; + } + + if ($codePoint >= 110879 && $codePoint <= 110927) { + return true; + } + + if ($codePoint >= 111356 && $codePoint <= 113663) { + return true; + } + + if ($codePoint >= 113828 && $codePoint <= 118783) { + return true; + } + + if ($codePoint >= 119366 && $codePoint <= 119519) { + return true; + } + + if ($codePoint >= 119673 && $codePoint <= 119807) { + return true; + } + + if ($codePoint >= 121520 && $codePoint <= 122879) { + return true; + } + + if ($codePoint >= 122923 && $codePoint <= 123135) { + return true; + } + + if ($codePoint >= 123216 && $codePoint <= 123583) { + return true; + } + + if ($codePoint >= 123648 && $codePoint <= 124927) { + return true; + } + + if ($codePoint >= 125143 && $codePoint <= 125183) { + return true; + } + + if ($codePoint >= 125280 && $codePoint <= 126064) { + return true; + } + + if ($codePoint >= 126133 && $codePoint <= 126208) { + return true; + } + + if ($codePoint >= 126270 && $codePoint <= 126463) { + return true; + } + + if ($codePoint >= 126652 && $codePoint <= 126703) { + return true; + } + + if ($codePoint >= 126706 && $codePoint <= 126975) { + return true; + } + + if ($codePoint >= 127406 && $codePoint <= 127461) { + return true; + } + + if ($codePoint >= 127590 && $codePoint <= 127743) { + return true; + } + + if ($codePoint >= 129202 && $codePoint <= 129279) { + return true; + } + + if ($codePoint >= 129751 && $codePoint <= 129791) { + return true; + } + + if ($codePoint >= 129995 && $codePoint <= 130031) { + return true; + } + + if ($codePoint >= 130042 && $codePoint <= 131069) { + return true; + } + + if ($codePoint >= 173790 && $codePoint <= 173823) { + return true; + } + + if ($codePoint >= 191457 && $codePoint <= 194559) { + return true; + } + + if ($codePoint >= 195102 && $codePoint <= 196605) { + return true; + } + + if ($codePoint >= 201547 && $codePoint <= 262141) { + return true; + } + + if ($codePoint >= 262144 && $codePoint <= 327677) { + return true; + } + + if ($codePoint >= 327680 && $codePoint <= 393213) { + return true; + } + + if ($codePoint >= 393216 && $codePoint <= 458749) { + return true; + } + + if ($codePoint >= 458752 && $codePoint <= 524285) { + return true; + } + + if ($codePoint >= 524288 && $codePoint <= 589821) { + return true; + } + + if ($codePoint >= 589824 && $codePoint <= 655357) { + return true; + } + + if ($codePoint >= 655360 && $codePoint <= 720893) { + return true; + } + + if ($codePoint >= 720896 && $codePoint <= 786429) { + return true; + } + + if ($codePoint >= 786432 && $codePoint <= 851965) { + return true; + } + + if ($codePoint >= 851968 && $codePoint <= 917501) { + return true; + } + + if ($codePoint >= 917536 && $codePoint <= 917631) { + return true; + } + + if ($codePoint >= 917632 && $codePoint <= 917759) { + return true; + } + + if ($codePoint >= 918000 && $codePoint <= 983037) { + return true; + } + + if ($codePoint >= 983040 && $codePoint <= 1048573) { + return true; + } + + if ($codePoint >= 1048576 && $codePoint <= 1114109) { + return true; + } + + return false; + } +} diff --git a/vendor/symfony/polyfill-intl-idn/Resources/unidata/Regex.php b/vendor/symfony/polyfill-intl-idn/Resources/unidata/Regex.php new file mode 100644 index 0000000..5c1c51d --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/Resources/unidata/Regex.php @@ -0,0 +1,24 @@ + 'ss', + 962 => 'σ', + 8204 => '', + 8205 => '', +); diff --git a/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php b/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php new file mode 100644 index 0000000..25a5f56 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php @@ -0,0 +1,2638 @@ + true, + 889 => true, + 896 => true, + 897 => true, + 898 => true, + 899 => true, + 907 => true, + 909 => true, + 930 => true, + 1216 => true, + 1328 => true, + 1367 => true, + 1368 => true, + 1419 => true, + 1420 => true, + 1424 => true, + 1480 => true, + 1481 => true, + 1482 => true, + 1483 => true, + 1484 => true, + 1485 => true, + 1486 => true, + 1487 => true, + 1515 => true, + 1516 => true, + 1517 => true, + 1518 => true, + 1525 => true, + 1526 => true, + 1527 => true, + 1528 => true, + 1529 => true, + 1530 => true, + 1531 => true, + 1532 => true, + 1533 => true, + 1534 => true, + 1535 => true, + 1536 => true, + 1537 => true, + 1538 => true, + 1539 => true, + 1540 => true, + 1541 => true, + 1564 => true, + 1565 => true, + 1757 => true, + 1806 => true, + 1807 => true, + 1867 => true, + 1868 => true, + 1970 => true, + 1971 => true, + 1972 => true, + 1973 => true, + 1974 => true, + 1975 => true, + 1976 => true, + 1977 => true, + 1978 => true, + 1979 => true, + 1980 => true, + 1981 => true, + 1982 => true, + 1983 => true, + 2043 => true, + 2044 => true, + 2094 => true, + 2095 => true, + 2111 => true, + 2140 => true, + 2141 => true, + 2143 => true, + 2229 => true, + 2248 => true, + 2249 => true, + 2250 => true, + 2251 => true, + 2252 => true, + 2253 => true, + 2254 => true, + 2255 => true, + 2256 => true, + 2257 => true, + 2258 => true, + 2274 => true, + 2436 => true, + 2445 => true, + 2446 => true, + 2449 => true, + 2450 => true, + 2473 => true, + 2481 => true, + 2483 => true, + 2484 => true, + 2485 => true, + 2490 => true, + 2491 => true, + 2501 => true, + 2502 => true, + 2505 => true, + 2506 => true, + 2511 => true, + 2512 => true, + 2513 => true, + 2514 => true, + 2515 => true, + 2516 => true, + 2517 => true, + 2518 => true, + 2520 => true, + 2521 => true, + 2522 => true, + 2523 => true, + 2526 => true, + 2532 => true, + 2533 => true, + 2559 => true, + 2560 => true, + 2564 => true, + 2571 => true, + 2572 => true, + 2573 => true, + 2574 => true, + 2577 => true, + 2578 => true, + 2601 => true, + 2609 => true, + 2612 => true, + 2615 => true, + 2618 => true, + 2619 => true, + 2621 => true, + 2627 => true, + 2628 => true, + 2629 => true, + 2630 => true, + 2633 => true, + 2634 => true, + 2638 => true, + 2639 => true, + 2640 => true, + 2642 => true, + 2643 => true, + 2644 => true, + 2645 => true, + 2646 => true, + 2647 => true, + 2648 => true, + 2653 => true, + 2655 => true, + 2656 => true, + 2657 => true, + 2658 => true, + 2659 => true, + 2660 => true, + 2661 => true, + 2679 => true, + 2680 => true, + 2681 => true, + 2682 => true, + 2683 => true, + 2684 => true, + 2685 => true, + 2686 => true, + 2687 => true, + 2688 => true, + 2692 => true, + 2702 => true, + 2706 => true, + 2729 => true, + 2737 => true, + 2740 => true, + 2746 => true, + 2747 => true, + 2758 => true, + 2762 => true, + 2766 => true, + 2767 => true, + 2769 => true, + 2770 => true, + 2771 => true, + 2772 => true, + 2773 => true, + 2774 => true, + 2775 => true, + 2776 => true, + 2777 => true, + 2778 => true, + 2779 => true, + 2780 => true, + 2781 => true, + 2782 => true, + 2783 => true, + 2788 => true, + 2789 => true, + 2802 => true, + 2803 => true, + 2804 => true, + 2805 => true, + 2806 => true, + 2807 => true, + 2808 => true, + 2816 => true, + 2820 => true, + 2829 => true, + 2830 => true, + 2833 => true, + 2834 => true, + 2857 => true, + 2865 => true, + 2868 => true, + 2874 => true, + 2875 => true, + 2885 => true, + 2886 => true, + 2889 => true, + 2890 => true, + 2894 => true, + 2895 => true, + 2896 => true, + 2897 => true, + 2898 => true, + 2899 => true, + 2900 => true, + 2904 => true, + 2905 => true, + 2906 => true, + 2907 => true, + 2910 => true, + 2916 => true, + 2917 => true, + 2936 => true, + 2937 => true, + 2938 => true, + 2939 => true, + 2940 => true, + 2941 => true, + 2942 => true, + 2943 => true, + 2944 => true, + 2945 => true, + 2948 => true, + 2955 => true, + 2956 => true, + 2957 => true, + 2961 => true, + 2966 => true, + 2967 => true, + 2968 => true, + 2971 => true, + 2973 => true, + 2976 => true, + 2977 => true, + 2978 => true, + 2981 => true, + 2982 => true, + 2983 => true, + 2987 => true, + 2988 => true, + 2989 => true, + 3002 => true, + 3003 => true, + 3004 => true, + 3005 => true, + 3011 => true, + 3012 => true, + 3013 => true, + 3017 => true, + 3022 => true, + 3023 => true, + 3025 => true, + 3026 => true, + 3027 => true, + 3028 => true, + 3029 => true, + 3030 => true, + 3032 => true, + 3033 => true, + 3034 => true, + 3035 => true, + 3036 => true, + 3037 => true, + 3038 => true, + 3039 => true, + 3040 => true, + 3041 => true, + 3042 => true, + 3043 => true, + 3044 => true, + 3045 => true, + 3067 => true, + 3068 => true, + 3069 => true, + 3070 => true, + 3071 => true, + 3085 => true, + 3089 => true, + 3113 => true, + 3130 => true, + 3131 => true, + 3132 => true, + 3141 => true, + 3145 => true, + 3150 => true, + 3151 => true, + 3152 => true, + 3153 => true, + 3154 => true, + 3155 => true, + 3156 => true, + 3159 => true, + 3163 => true, + 3164 => true, + 3165 => true, + 3166 => true, + 3167 => true, + 3172 => true, + 3173 => true, + 3184 => true, + 3185 => true, + 3186 => true, + 3187 => true, + 3188 => true, + 3189 => true, + 3190 => true, + 3213 => true, + 3217 => true, + 3241 => true, + 3252 => true, + 3258 => true, + 3259 => true, + 3269 => true, + 3273 => true, + 3278 => true, + 3279 => true, + 3280 => true, + 3281 => true, + 3282 => true, + 3283 => true, + 3284 => true, + 3287 => true, + 3288 => true, + 3289 => true, + 3290 => true, + 3291 => true, + 3292 => true, + 3293 => true, + 3295 => true, + 3300 => true, + 3301 => true, + 3312 => true, + 3315 => true, + 3316 => true, + 3317 => true, + 3318 => true, + 3319 => true, + 3320 => true, + 3321 => true, + 3322 => true, + 3323 => true, + 3324 => true, + 3325 => true, + 3326 => true, + 3327 => true, + 3341 => true, + 3345 => true, + 3397 => true, + 3401 => true, + 3408 => true, + 3409 => true, + 3410 => true, + 3411 => true, + 3428 => true, + 3429 => true, + 3456 => true, + 3460 => true, + 3479 => true, + 3480 => true, + 3481 => true, + 3506 => true, + 3516 => true, + 3518 => true, + 3519 => true, + 3527 => true, + 3528 => true, + 3529 => true, + 3531 => true, + 3532 => true, + 3533 => true, + 3534 => true, + 3541 => true, + 3543 => true, + 3552 => true, + 3553 => true, + 3554 => true, + 3555 => true, + 3556 => true, + 3557 => true, + 3568 => true, + 3569 => true, + 3573 => true, + 3574 => true, + 3575 => true, + 3576 => true, + 3577 => true, + 3578 => true, + 3579 => true, + 3580 => true, + 3581 => true, + 3582 => true, + 3583 => true, + 3584 => true, + 3643 => true, + 3644 => true, + 3645 => true, + 3646 => true, + 3715 => true, + 3717 => true, + 3723 => true, + 3748 => true, + 3750 => true, + 3774 => true, + 3775 => true, + 3781 => true, + 3783 => true, + 3790 => true, + 3791 => true, + 3802 => true, + 3803 => true, + 3912 => true, + 3949 => true, + 3950 => true, + 3951 => true, + 3952 => true, + 3992 => true, + 4029 => true, + 4045 => true, + 4294 => true, + 4296 => true, + 4297 => true, + 4298 => true, + 4299 => true, + 4300 => true, + 4302 => true, + 4303 => true, + 4447 => true, + 4448 => true, + 4681 => true, + 4686 => true, + 4687 => true, + 4695 => true, + 4697 => true, + 4702 => true, + 4703 => true, + 4745 => true, + 4750 => true, + 4751 => true, + 4785 => true, + 4790 => true, + 4791 => true, + 4799 => true, + 4801 => true, + 4806 => true, + 4807 => true, + 4823 => true, + 4881 => true, + 4886 => true, + 4887 => true, + 4955 => true, + 4956 => true, + 4989 => true, + 4990 => true, + 4991 => true, + 5018 => true, + 5019 => true, + 5020 => true, + 5021 => true, + 5022 => true, + 5023 => true, + 5110 => true, + 5111 => true, + 5118 => true, + 5119 => true, + 5760 => true, + 5789 => true, + 5790 => true, + 5791 => true, + 5881 => true, + 5882 => true, + 5883 => true, + 5884 => true, + 5885 => true, + 5886 => true, + 5887 => true, + 5901 => true, + 5909 => true, + 5910 => true, + 5911 => true, + 5912 => true, + 5913 => true, + 5914 => true, + 5915 => true, + 5916 => true, + 5917 => true, + 5918 => true, + 5919 => true, + 5943 => true, + 5944 => true, + 5945 => true, + 5946 => true, + 5947 => true, + 5948 => true, + 5949 => true, + 5950 => true, + 5951 => true, + 5972 => true, + 5973 => true, + 5974 => true, + 5975 => true, + 5976 => true, + 5977 => true, + 5978 => true, + 5979 => true, + 5980 => true, + 5981 => true, + 5982 => true, + 5983 => true, + 5997 => true, + 6001 => true, + 6004 => true, + 6005 => true, + 6006 => true, + 6007 => true, + 6008 => true, + 6009 => true, + 6010 => true, + 6011 => true, + 6012 => true, + 6013 => true, + 6014 => true, + 6015 => true, + 6068 => true, + 6069 => true, + 6110 => true, + 6111 => true, + 6122 => true, + 6123 => true, + 6124 => true, + 6125 => true, + 6126 => true, + 6127 => true, + 6138 => true, + 6139 => true, + 6140 => true, + 6141 => true, + 6142 => true, + 6143 => true, + 6150 => true, + 6158 => true, + 6159 => true, + 6170 => true, + 6171 => true, + 6172 => true, + 6173 => true, + 6174 => true, + 6175 => true, + 6265 => true, + 6266 => true, + 6267 => true, + 6268 => true, + 6269 => true, + 6270 => true, + 6271 => true, + 6315 => true, + 6316 => true, + 6317 => true, + 6318 => true, + 6319 => true, + 6390 => true, + 6391 => true, + 6392 => true, + 6393 => true, + 6394 => true, + 6395 => true, + 6396 => true, + 6397 => true, + 6398 => true, + 6399 => true, + 6431 => true, + 6444 => true, + 6445 => true, + 6446 => true, + 6447 => true, + 6460 => true, + 6461 => true, + 6462 => true, + 6463 => true, + 6465 => true, + 6466 => true, + 6467 => true, + 6510 => true, + 6511 => true, + 6517 => true, + 6518 => true, + 6519 => true, + 6520 => true, + 6521 => true, + 6522 => true, + 6523 => true, + 6524 => true, + 6525 => true, + 6526 => true, + 6527 => true, + 6572 => true, + 6573 => true, + 6574 => true, + 6575 => true, + 6602 => true, + 6603 => true, + 6604 => true, + 6605 => true, + 6606 => true, + 6607 => true, + 6619 => true, + 6620 => true, + 6621 => true, + 6684 => true, + 6685 => true, + 6751 => true, + 6781 => true, + 6782 => true, + 6794 => true, + 6795 => true, + 6796 => true, + 6797 => true, + 6798 => true, + 6799 => true, + 6810 => true, + 6811 => true, + 6812 => true, + 6813 => true, + 6814 => true, + 6815 => true, + 6830 => true, + 6831 => true, + 6988 => true, + 6989 => true, + 6990 => true, + 6991 => true, + 7037 => true, + 7038 => true, + 7039 => true, + 7156 => true, + 7157 => true, + 7158 => true, + 7159 => true, + 7160 => true, + 7161 => true, + 7162 => true, + 7163 => true, + 7224 => true, + 7225 => true, + 7226 => true, + 7242 => true, + 7243 => true, + 7244 => true, + 7305 => true, + 7306 => true, + 7307 => true, + 7308 => true, + 7309 => true, + 7310 => true, + 7311 => true, + 7355 => true, + 7356 => true, + 7368 => true, + 7369 => true, + 7370 => true, + 7371 => true, + 7372 => true, + 7373 => true, + 7374 => true, + 7375 => true, + 7419 => true, + 7420 => true, + 7421 => true, + 7422 => true, + 7423 => true, + 7674 => true, + 7958 => true, + 7959 => true, + 7966 => true, + 7967 => true, + 8006 => true, + 8007 => true, + 8014 => true, + 8015 => true, + 8024 => true, + 8026 => true, + 8028 => true, + 8030 => true, + 8062 => true, + 8063 => true, + 8117 => true, + 8133 => true, + 8148 => true, + 8149 => true, + 8156 => true, + 8176 => true, + 8177 => true, + 8181 => true, + 8191 => true, + 8206 => true, + 8207 => true, + 8228 => true, + 8229 => true, + 8230 => true, + 8232 => true, + 8233 => true, + 8234 => true, + 8235 => true, + 8236 => true, + 8237 => true, + 8238 => true, + 8289 => true, + 8290 => true, + 8291 => true, + 8293 => true, + 8294 => true, + 8295 => true, + 8296 => true, + 8297 => true, + 8298 => true, + 8299 => true, + 8300 => true, + 8301 => true, + 8302 => true, + 8303 => true, + 8306 => true, + 8307 => true, + 8335 => true, + 8349 => true, + 8350 => true, + 8351 => true, + 8384 => true, + 8385 => true, + 8386 => true, + 8387 => true, + 8388 => true, + 8389 => true, + 8390 => true, + 8391 => true, + 8392 => true, + 8393 => true, + 8394 => true, + 8395 => true, + 8396 => true, + 8397 => true, + 8398 => true, + 8399 => true, + 8433 => true, + 8434 => true, + 8435 => true, + 8436 => true, + 8437 => true, + 8438 => true, + 8439 => true, + 8440 => true, + 8441 => true, + 8442 => true, + 8443 => true, + 8444 => true, + 8445 => true, + 8446 => true, + 8447 => true, + 8498 => true, + 8579 => true, + 8588 => true, + 8589 => true, + 8590 => true, + 8591 => true, + 9255 => true, + 9256 => true, + 9257 => true, + 9258 => true, + 9259 => true, + 9260 => true, + 9261 => true, + 9262 => true, + 9263 => true, + 9264 => true, + 9265 => true, + 9266 => true, + 9267 => true, + 9268 => true, + 9269 => true, + 9270 => true, + 9271 => true, + 9272 => true, + 9273 => true, + 9274 => true, + 9275 => true, + 9276 => true, + 9277 => true, + 9278 => true, + 9279 => true, + 9291 => true, + 9292 => true, + 9293 => true, + 9294 => true, + 9295 => true, + 9296 => true, + 9297 => true, + 9298 => true, + 9299 => true, + 9300 => true, + 9301 => true, + 9302 => true, + 9303 => true, + 9304 => true, + 9305 => true, + 9306 => true, + 9307 => true, + 9308 => true, + 9309 => true, + 9310 => true, + 9311 => true, + 9352 => true, + 9353 => true, + 9354 => true, + 9355 => true, + 9356 => true, + 9357 => true, + 9358 => true, + 9359 => true, + 9360 => true, + 9361 => true, + 9362 => true, + 9363 => true, + 9364 => true, + 9365 => true, + 9366 => true, + 9367 => true, + 9368 => true, + 9369 => true, + 9370 => true, + 9371 => true, + 11124 => true, + 11125 => true, + 11158 => true, + 11311 => true, + 11359 => true, + 11508 => true, + 11509 => true, + 11510 => true, + 11511 => true, + 11512 => true, + 11558 => true, + 11560 => true, + 11561 => true, + 11562 => true, + 11563 => true, + 11564 => true, + 11566 => true, + 11567 => true, + 11624 => true, + 11625 => true, + 11626 => true, + 11627 => true, + 11628 => true, + 11629 => true, + 11630 => true, + 11633 => true, + 11634 => true, + 11635 => true, + 11636 => true, + 11637 => true, + 11638 => true, + 11639 => true, + 11640 => true, + 11641 => true, + 11642 => true, + 11643 => true, + 11644 => true, + 11645 => true, + 11646 => true, + 11671 => true, + 11672 => true, + 11673 => true, + 11674 => true, + 11675 => true, + 11676 => true, + 11677 => true, + 11678 => true, + 11679 => true, + 11687 => true, + 11695 => true, + 11703 => true, + 11711 => true, + 11719 => true, + 11727 => true, + 11735 => true, + 11743 => true, + 11930 => true, + 12020 => true, + 12021 => true, + 12022 => true, + 12023 => true, + 12024 => true, + 12025 => true, + 12026 => true, + 12027 => true, + 12028 => true, + 12029 => true, + 12030 => true, + 12031 => true, + 12246 => true, + 12247 => true, + 12248 => true, + 12249 => true, + 12250 => true, + 12251 => true, + 12252 => true, + 12253 => true, + 12254 => true, + 12255 => true, + 12256 => true, + 12257 => true, + 12258 => true, + 12259 => true, + 12260 => true, + 12261 => true, + 12262 => true, + 12263 => true, + 12264 => true, + 12265 => true, + 12266 => true, + 12267 => true, + 12268 => true, + 12269 => true, + 12270 => true, + 12271 => true, + 12272 => true, + 12273 => true, + 12274 => true, + 12275 => true, + 12276 => true, + 12277 => true, + 12278 => true, + 12279 => true, + 12280 => true, + 12281 => true, + 12282 => true, + 12283 => true, + 12284 => true, + 12285 => true, + 12286 => true, + 12287 => true, + 12352 => true, + 12439 => true, + 12440 => true, + 12544 => true, + 12545 => true, + 12546 => true, + 12547 => true, + 12548 => true, + 12592 => true, + 12644 => true, + 12687 => true, + 12772 => true, + 12773 => true, + 12774 => true, + 12775 => true, + 12776 => true, + 12777 => true, + 12778 => true, + 12779 => true, + 12780 => true, + 12781 => true, + 12782 => true, + 12783 => true, + 12831 => true, + 13250 => true, + 13255 => true, + 13272 => true, + 40957 => true, + 40958 => true, + 40959 => true, + 42125 => true, + 42126 => true, + 42127 => true, + 42183 => true, + 42184 => true, + 42185 => true, + 42186 => true, + 42187 => true, + 42188 => true, + 42189 => true, + 42190 => true, + 42191 => true, + 42540 => true, + 42541 => true, + 42542 => true, + 42543 => true, + 42544 => true, + 42545 => true, + 42546 => true, + 42547 => true, + 42548 => true, + 42549 => true, + 42550 => true, + 42551 => true, + 42552 => true, + 42553 => true, + 42554 => true, + 42555 => true, + 42556 => true, + 42557 => true, + 42558 => true, + 42559 => true, + 42744 => true, + 42745 => true, + 42746 => true, + 42747 => true, + 42748 => true, + 42749 => true, + 42750 => true, + 42751 => true, + 42944 => true, + 42945 => true, + 43053 => true, + 43054 => true, + 43055 => true, + 43066 => true, + 43067 => true, + 43068 => true, + 43069 => true, + 43070 => true, + 43071 => true, + 43128 => true, + 43129 => true, + 43130 => true, + 43131 => true, + 43132 => true, + 43133 => true, + 43134 => true, + 43135 => true, + 43206 => true, + 43207 => true, + 43208 => true, + 43209 => true, + 43210 => true, + 43211 => true, + 43212 => true, + 43213 => true, + 43226 => true, + 43227 => true, + 43228 => true, + 43229 => true, + 43230 => true, + 43231 => true, + 43348 => true, + 43349 => true, + 43350 => true, + 43351 => true, + 43352 => true, + 43353 => true, + 43354 => true, + 43355 => true, + 43356 => true, + 43357 => true, + 43358 => true, + 43389 => true, + 43390 => true, + 43391 => true, + 43470 => true, + 43482 => true, + 43483 => true, + 43484 => true, + 43485 => true, + 43519 => true, + 43575 => true, + 43576 => true, + 43577 => true, + 43578 => true, + 43579 => true, + 43580 => true, + 43581 => true, + 43582 => true, + 43583 => true, + 43598 => true, + 43599 => true, + 43610 => true, + 43611 => true, + 43715 => true, + 43716 => true, + 43717 => true, + 43718 => true, + 43719 => true, + 43720 => true, + 43721 => true, + 43722 => true, + 43723 => true, + 43724 => true, + 43725 => true, + 43726 => true, + 43727 => true, + 43728 => true, + 43729 => true, + 43730 => true, + 43731 => true, + 43732 => true, + 43733 => true, + 43734 => true, + 43735 => true, + 43736 => true, + 43737 => true, + 43738 => true, + 43767 => true, + 43768 => true, + 43769 => true, + 43770 => true, + 43771 => true, + 43772 => true, + 43773 => true, + 43774 => true, + 43775 => true, + 43776 => true, + 43783 => true, + 43784 => true, + 43791 => true, + 43792 => true, + 43799 => true, + 43800 => true, + 43801 => true, + 43802 => true, + 43803 => true, + 43804 => true, + 43805 => true, + 43806 => true, + 43807 => true, + 43815 => true, + 43823 => true, + 43884 => true, + 43885 => true, + 43886 => true, + 43887 => true, + 44014 => true, + 44015 => true, + 44026 => true, + 44027 => true, + 44028 => true, + 44029 => true, + 44030 => true, + 44031 => true, + 55204 => true, + 55205 => true, + 55206 => true, + 55207 => true, + 55208 => true, + 55209 => true, + 55210 => true, + 55211 => true, + 55212 => true, + 55213 => true, + 55214 => true, + 55215 => true, + 55239 => true, + 55240 => true, + 55241 => true, + 55242 => true, + 55292 => true, + 55293 => true, + 55294 => true, + 55295 => true, + 64110 => true, + 64111 => true, + 64263 => true, + 64264 => true, + 64265 => true, + 64266 => true, + 64267 => true, + 64268 => true, + 64269 => true, + 64270 => true, + 64271 => true, + 64272 => true, + 64273 => true, + 64274 => true, + 64280 => true, + 64281 => true, + 64282 => true, + 64283 => true, + 64284 => true, + 64311 => true, + 64317 => true, + 64319 => true, + 64322 => true, + 64325 => true, + 64450 => true, + 64451 => true, + 64452 => true, + 64453 => true, + 64454 => true, + 64455 => true, + 64456 => true, + 64457 => true, + 64458 => true, + 64459 => true, + 64460 => true, + 64461 => true, + 64462 => true, + 64463 => true, + 64464 => true, + 64465 => true, + 64466 => true, + 64832 => true, + 64833 => true, + 64834 => true, + 64835 => true, + 64836 => true, + 64837 => true, + 64838 => true, + 64839 => true, + 64840 => true, + 64841 => true, + 64842 => true, + 64843 => true, + 64844 => true, + 64845 => true, + 64846 => true, + 64847 => true, + 64912 => true, + 64913 => true, + 64968 => true, + 64969 => true, + 64970 => true, + 64971 => true, + 64972 => true, + 64973 => true, + 64974 => true, + 64975 => true, + 65022 => true, + 65023 => true, + 65042 => true, + 65049 => true, + 65050 => true, + 65051 => true, + 65052 => true, + 65053 => true, + 65054 => true, + 65055 => true, + 65072 => true, + 65106 => true, + 65107 => true, + 65127 => true, + 65132 => true, + 65133 => true, + 65134 => true, + 65135 => true, + 65141 => true, + 65277 => true, + 65278 => true, + 65280 => true, + 65440 => true, + 65471 => true, + 65472 => true, + 65473 => true, + 65480 => true, + 65481 => true, + 65488 => true, + 65489 => true, + 65496 => true, + 65497 => true, + 65501 => true, + 65502 => true, + 65503 => true, + 65511 => true, + 65519 => true, + 65520 => true, + 65521 => true, + 65522 => true, + 65523 => true, + 65524 => true, + 65525 => true, + 65526 => true, + 65527 => true, + 65528 => true, + 65529 => true, + 65530 => true, + 65531 => true, + 65532 => true, + 65533 => true, + 65534 => true, + 65535 => true, + 65548 => true, + 65575 => true, + 65595 => true, + 65598 => true, + 65614 => true, + 65615 => true, + 65787 => true, + 65788 => true, + 65789 => true, + 65790 => true, + 65791 => true, + 65795 => true, + 65796 => true, + 65797 => true, + 65798 => true, + 65844 => true, + 65845 => true, + 65846 => true, + 65935 => true, + 65949 => true, + 65950 => true, + 65951 => true, + 66205 => true, + 66206 => true, + 66207 => true, + 66257 => true, + 66258 => true, + 66259 => true, + 66260 => true, + 66261 => true, + 66262 => true, + 66263 => true, + 66264 => true, + 66265 => true, + 66266 => true, + 66267 => true, + 66268 => true, + 66269 => true, + 66270 => true, + 66271 => true, + 66300 => true, + 66301 => true, + 66302 => true, + 66303 => true, + 66340 => true, + 66341 => true, + 66342 => true, + 66343 => true, + 66344 => true, + 66345 => true, + 66346 => true, + 66347 => true, + 66348 => true, + 66379 => true, + 66380 => true, + 66381 => true, + 66382 => true, + 66383 => true, + 66427 => true, + 66428 => true, + 66429 => true, + 66430 => true, + 66431 => true, + 66462 => true, + 66500 => true, + 66501 => true, + 66502 => true, + 66503 => true, + 66718 => true, + 66719 => true, + 66730 => true, + 66731 => true, + 66732 => true, + 66733 => true, + 66734 => true, + 66735 => true, + 66772 => true, + 66773 => true, + 66774 => true, + 66775 => true, + 66812 => true, + 66813 => true, + 66814 => true, + 66815 => true, + 66856 => true, + 66857 => true, + 66858 => true, + 66859 => true, + 66860 => true, + 66861 => true, + 66862 => true, + 66863 => true, + 66916 => true, + 66917 => true, + 66918 => true, + 66919 => true, + 66920 => true, + 66921 => true, + 66922 => true, + 66923 => true, + 66924 => true, + 66925 => true, + 66926 => true, + 67383 => true, + 67384 => true, + 67385 => true, + 67386 => true, + 67387 => true, + 67388 => true, + 67389 => true, + 67390 => true, + 67391 => true, + 67414 => true, + 67415 => true, + 67416 => true, + 67417 => true, + 67418 => true, + 67419 => true, + 67420 => true, + 67421 => true, + 67422 => true, + 67423 => true, + 67590 => true, + 67591 => true, + 67593 => true, + 67638 => true, + 67641 => true, + 67642 => true, + 67643 => true, + 67645 => true, + 67646 => true, + 67670 => true, + 67743 => true, + 67744 => true, + 67745 => true, + 67746 => true, + 67747 => true, + 67748 => true, + 67749 => true, + 67750 => true, + 67827 => true, + 67830 => true, + 67831 => true, + 67832 => true, + 67833 => true, + 67834 => true, + 67868 => true, + 67869 => true, + 67870 => true, + 67898 => true, + 67899 => true, + 67900 => true, + 67901 => true, + 67902 => true, + 68024 => true, + 68025 => true, + 68026 => true, + 68027 => true, + 68048 => true, + 68049 => true, + 68100 => true, + 68103 => true, + 68104 => true, + 68105 => true, + 68106 => true, + 68107 => true, + 68116 => true, + 68120 => true, + 68150 => true, + 68151 => true, + 68155 => true, + 68156 => true, + 68157 => true, + 68158 => true, + 68169 => true, + 68170 => true, + 68171 => true, + 68172 => true, + 68173 => true, + 68174 => true, + 68175 => true, + 68185 => true, + 68186 => true, + 68187 => true, + 68188 => true, + 68189 => true, + 68190 => true, + 68191 => true, + 68327 => true, + 68328 => true, + 68329 => true, + 68330 => true, + 68343 => true, + 68344 => true, + 68345 => true, + 68346 => true, + 68347 => true, + 68348 => true, + 68349 => true, + 68350 => true, + 68351 => true, + 68406 => true, + 68407 => true, + 68408 => true, + 68438 => true, + 68439 => true, + 68467 => true, + 68468 => true, + 68469 => true, + 68470 => true, + 68471 => true, + 68498 => true, + 68499 => true, + 68500 => true, + 68501 => true, + 68502 => true, + 68503 => true, + 68504 => true, + 68509 => true, + 68510 => true, + 68511 => true, + 68512 => true, + 68513 => true, + 68514 => true, + 68515 => true, + 68516 => true, + 68517 => true, + 68518 => true, + 68519 => true, + 68520 => true, + 68787 => true, + 68788 => true, + 68789 => true, + 68790 => true, + 68791 => true, + 68792 => true, + 68793 => true, + 68794 => true, + 68795 => true, + 68796 => true, + 68797 => true, + 68798 => true, + 68799 => true, + 68851 => true, + 68852 => true, + 68853 => true, + 68854 => true, + 68855 => true, + 68856 => true, + 68857 => true, + 68904 => true, + 68905 => true, + 68906 => true, + 68907 => true, + 68908 => true, + 68909 => true, + 68910 => true, + 68911 => true, + 69247 => true, + 69290 => true, + 69294 => true, + 69295 => true, + 69416 => true, + 69417 => true, + 69418 => true, + 69419 => true, + 69420 => true, + 69421 => true, + 69422 => true, + 69423 => true, + 69580 => true, + 69581 => true, + 69582 => true, + 69583 => true, + 69584 => true, + 69585 => true, + 69586 => true, + 69587 => true, + 69588 => true, + 69589 => true, + 69590 => true, + 69591 => true, + 69592 => true, + 69593 => true, + 69594 => true, + 69595 => true, + 69596 => true, + 69597 => true, + 69598 => true, + 69599 => true, + 69623 => true, + 69624 => true, + 69625 => true, + 69626 => true, + 69627 => true, + 69628 => true, + 69629 => true, + 69630 => true, + 69631 => true, + 69710 => true, + 69711 => true, + 69712 => true, + 69713 => true, + 69744 => true, + 69745 => true, + 69746 => true, + 69747 => true, + 69748 => true, + 69749 => true, + 69750 => true, + 69751 => true, + 69752 => true, + 69753 => true, + 69754 => true, + 69755 => true, + 69756 => true, + 69757 => true, + 69758 => true, + 69821 => true, + 69826 => true, + 69827 => true, + 69828 => true, + 69829 => true, + 69830 => true, + 69831 => true, + 69832 => true, + 69833 => true, + 69834 => true, + 69835 => true, + 69836 => true, + 69837 => true, + 69838 => true, + 69839 => true, + 69865 => true, + 69866 => true, + 69867 => true, + 69868 => true, + 69869 => true, + 69870 => true, + 69871 => true, + 69882 => true, + 69883 => true, + 69884 => true, + 69885 => true, + 69886 => true, + 69887 => true, + 69941 => true, + 69960 => true, + 69961 => true, + 69962 => true, + 69963 => true, + 69964 => true, + 69965 => true, + 69966 => true, + 69967 => true, + 70007 => true, + 70008 => true, + 70009 => true, + 70010 => true, + 70011 => true, + 70012 => true, + 70013 => true, + 70014 => true, + 70015 => true, + 70112 => true, + 70133 => true, + 70134 => true, + 70135 => true, + 70136 => true, + 70137 => true, + 70138 => true, + 70139 => true, + 70140 => true, + 70141 => true, + 70142 => true, + 70143 => true, + 70162 => true, + 70279 => true, + 70281 => true, + 70286 => true, + 70302 => true, + 70314 => true, + 70315 => true, + 70316 => true, + 70317 => true, + 70318 => true, + 70319 => true, + 70379 => true, + 70380 => true, + 70381 => true, + 70382 => true, + 70383 => true, + 70394 => true, + 70395 => true, + 70396 => true, + 70397 => true, + 70398 => true, + 70399 => true, + 70404 => true, + 70413 => true, + 70414 => true, + 70417 => true, + 70418 => true, + 70441 => true, + 70449 => true, + 70452 => true, + 70458 => true, + 70469 => true, + 70470 => true, + 70473 => true, + 70474 => true, + 70478 => true, + 70479 => true, + 70481 => true, + 70482 => true, + 70483 => true, + 70484 => true, + 70485 => true, + 70486 => true, + 70488 => true, + 70489 => true, + 70490 => true, + 70491 => true, + 70492 => true, + 70500 => true, + 70501 => true, + 70509 => true, + 70510 => true, + 70511 => true, + 70748 => true, + 70754 => true, + 70755 => true, + 70756 => true, + 70757 => true, + 70758 => true, + 70759 => true, + 70760 => true, + 70761 => true, + 70762 => true, + 70763 => true, + 70764 => true, + 70765 => true, + 70766 => true, + 70767 => true, + 70768 => true, + 70769 => true, + 70770 => true, + 70771 => true, + 70772 => true, + 70773 => true, + 70774 => true, + 70775 => true, + 70776 => true, + 70777 => true, + 70778 => true, + 70779 => true, + 70780 => true, + 70781 => true, + 70782 => true, + 70783 => true, + 70856 => true, + 70857 => true, + 70858 => true, + 70859 => true, + 70860 => true, + 70861 => true, + 70862 => true, + 70863 => true, + 71094 => true, + 71095 => true, + 71237 => true, + 71238 => true, + 71239 => true, + 71240 => true, + 71241 => true, + 71242 => true, + 71243 => true, + 71244 => true, + 71245 => true, + 71246 => true, + 71247 => true, + 71258 => true, + 71259 => true, + 71260 => true, + 71261 => true, + 71262 => true, + 71263 => true, + 71277 => true, + 71278 => true, + 71279 => true, + 71280 => true, + 71281 => true, + 71282 => true, + 71283 => true, + 71284 => true, + 71285 => true, + 71286 => true, + 71287 => true, + 71288 => true, + 71289 => true, + 71290 => true, + 71291 => true, + 71292 => true, + 71293 => true, + 71294 => true, + 71295 => true, + 71353 => true, + 71354 => true, + 71355 => true, + 71356 => true, + 71357 => true, + 71358 => true, + 71359 => true, + 71451 => true, + 71452 => true, + 71468 => true, + 71469 => true, + 71470 => true, + 71471 => true, + 71923 => true, + 71924 => true, + 71925 => true, + 71926 => true, + 71927 => true, + 71928 => true, + 71929 => true, + 71930 => true, + 71931 => true, + 71932 => true, + 71933 => true, + 71934 => true, + 71943 => true, + 71944 => true, + 71946 => true, + 71947 => true, + 71956 => true, + 71959 => true, + 71990 => true, + 71993 => true, + 71994 => true, + 72007 => true, + 72008 => true, + 72009 => true, + 72010 => true, + 72011 => true, + 72012 => true, + 72013 => true, + 72014 => true, + 72015 => true, + 72104 => true, + 72105 => true, + 72152 => true, + 72153 => true, + 72165 => true, + 72166 => true, + 72167 => true, + 72168 => true, + 72169 => true, + 72170 => true, + 72171 => true, + 72172 => true, + 72173 => true, + 72174 => true, + 72175 => true, + 72176 => true, + 72177 => true, + 72178 => true, + 72179 => true, + 72180 => true, + 72181 => true, + 72182 => true, + 72183 => true, + 72184 => true, + 72185 => true, + 72186 => true, + 72187 => true, + 72188 => true, + 72189 => true, + 72190 => true, + 72191 => true, + 72264 => true, + 72265 => true, + 72266 => true, + 72267 => true, + 72268 => true, + 72269 => true, + 72270 => true, + 72271 => true, + 72355 => true, + 72356 => true, + 72357 => true, + 72358 => true, + 72359 => true, + 72360 => true, + 72361 => true, + 72362 => true, + 72363 => true, + 72364 => true, + 72365 => true, + 72366 => true, + 72367 => true, + 72368 => true, + 72369 => true, + 72370 => true, + 72371 => true, + 72372 => true, + 72373 => true, + 72374 => true, + 72375 => true, + 72376 => true, + 72377 => true, + 72378 => true, + 72379 => true, + 72380 => true, + 72381 => true, + 72382 => true, + 72383 => true, + 72713 => true, + 72759 => true, + 72774 => true, + 72775 => true, + 72776 => true, + 72777 => true, + 72778 => true, + 72779 => true, + 72780 => true, + 72781 => true, + 72782 => true, + 72783 => true, + 72813 => true, + 72814 => true, + 72815 => true, + 72848 => true, + 72849 => true, + 72872 => true, + 72967 => true, + 72970 => true, + 73015 => true, + 73016 => true, + 73017 => true, + 73019 => true, + 73022 => true, + 73032 => true, + 73033 => true, + 73034 => true, + 73035 => true, + 73036 => true, + 73037 => true, + 73038 => true, + 73039 => true, + 73050 => true, + 73051 => true, + 73052 => true, + 73053 => true, + 73054 => true, + 73055 => true, + 73062 => true, + 73065 => true, + 73103 => true, + 73106 => true, + 73113 => true, + 73114 => true, + 73115 => true, + 73116 => true, + 73117 => true, + 73118 => true, + 73119 => true, + 73649 => true, + 73650 => true, + 73651 => true, + 73652 => true, + 73653 => true, + 73654 => true, + 73655 => true, + 73656 => true, + 73657 => true, + 73658 => true, + 73659 => true, + 73660 => true, + 73661 => true, + 73662 => true, + 73663 => true, + 73714 => true, + 73715 => true, + 73716 => true, + 73717 => true, + 73718 => true, + 73719 => true, + 73720 => true, + 73721 => true, + 73722 => true, + 73723 => true, + 73724 => true, + 73725 => true, + 73726 => true, + 74863 => true, + 74869 => true, + 74870 => true, + 74871 => true, + 74872 => true, + 74873 => true, + 74874 => true, + 74875 => true, + 74876 => true, + 74877 => true, + 74878 => true, + 74879 => true, + 78895 => true, + 78896 => true, + 78897 => true, + 78898 => true, + 78899 => true, + 78900 => true, + 78901 => true, + 78902 => true, + 78903 => true, + 78904 => true, + 92729 => true, + 92730 => true, + 92731 => true, + 92732 => true, + 92733 => true, + 92734 => true, + 92735 => true, + 92767 => true, + 92778 => true, + 92779 => true, + 92780 => true, + 92781 => true, + 92910 => true, + 92911 => true, + 92918 => true, + 92919 => true, + 92920 => true, + 92921 => true, + 92922 => true, + 92923 => true, + 92924 => true, + 92925 => true, + 92926 => true, + 92927 => true, + 92998 => true, + 92999 => true, + 93000 => true, + 93001 => true, + 93002 => true, + 93003 => true, + 93004 => true, + 93005 => true, + 93006 => true, + 93007 => true, + 93018 => true, + 93026 => true, + 93048 => true, + 93049 => true, + 93050 => true, + 93051 => true, + 93052 => true, + 94027 => true, + 94028 => true, + 94029 => true, + 94030 => true, + 94088 => true, + 94089 => true, + 94090 => true, + 94091 => true, + 94092 => true, + 94093 => true, + 94094 => true, + 94181 => true, + 94182 => true, + 94183 => true, + 94184 => true, + 94185 => true, + 94186 => true, + 94187 => true, + 94188 => true, + 94189 => true, + 94190 => true, + 94191 => true, + 94194 => true, + 94195 => true, + 94196 => true, + 94197 => true, + 94198 => true, + 94199 => true, + 94200 => true, + 94201 => true, + 94202 => true, + 94203 => true, + 94204 => true, + 94205 => true, + 94206 => true, + 94207 => true, + 100344 => true, + 100345 => true, + 100346 => true, + 100347 => true, + 100348 => true, + 100349 => true, + 100350 => true, + 100351 => true, + 110931 => true, + 110932 => true, + 110933 => true, + 110934 => true, + 110935 => true, + 110936 => true, + 110937 => true, + 110938 => true, + 110939 => true, + 110940 => true, + 110941 => true, + 110942 => true, + 110943 => true, + 110944 => true, + 110945 => true, + 110946 => true, + 110947 => true, + 110952 => true, + 110953 => true, + 110954 => true, + 110955 => true, + 110956 => true, + 110957 => true, + 110958 => true, + 110959 => true, + 113771 => true, + 113772 => true, + 113773 => true, + 113774 => true, + 113775 => true, + 113789 => true, + 113790 => true, + 113791 => true, + 113801 => true, + 113802 => true, + 113803 => true, + 113804 => true, + 113805 => true, + 113806 => true, + 113807 => true, + 113818 => true, + 113819 => true, + 119030 => true, + 119031 => true, + 119032 => true, + 119033 => true, + 119034 => true, + 119035 => true, + 119036 => true, + 119037 => true, + 119038 => true, + 119039 => true, + 119079 => true, + 119080 => true, + 119155 => true, + 119156 => true, + 119157 => true, + 119158 => true, + 119159 => true, + 119160 => true, + 119161 => true, + 119162 => true, + 119273 => true, + 119274 => true, + 119275 => true, + 119276 => true, + 119277 => true, + 119278 => true, + 119279 => true, + 119280 => true, + 119281 => true, + 119282 => true, + 119283 => true, + 119284 => true, + 119285 => true, + 119286 => true, + 119287 => true, + 119288 => true, + 119289 => true, + 119290 => true, + 119291 => true, + 119292 => true, + 119293 => true, + 119294 => true, + 119295 => true, + 119540 => true, + 119541 => true, + 119542 => true, + 119543 => true, + 119544 => true, + 119545 => true, + 119546 => true, + 119547 => true, + 119548 => true, + 119549 => true, + 119550 => true, + 119551 => true, + 119639 => true, + 119640 => true, + 119641 => true, + 119642 => true, + 119643 => true, + 119644 => true, + 119645 => true, + 119646 => true, + 119647 => true, + 119893 => true, + 119965 => true, + 119968 => true, + 119969 => true, + 119971 => true, + 119972 => true, + 119975 => true, + 119976 => true, + 119981 => true, + 119994 => true, + 119996 => true, + 120004 => true, + 120070 => true, + 120075 => true, + 120076 => true, + 120085 => true, + 120093 => true, + 120122 => true, + 120127 => true, + 120133 => true, + 120135 => true, + 120136 => true, + 120137 => true, + 120145 => true, + 120486 => true, + 120487 => true, + 120780 => true, + 120781 => true, + 121484 => true, + 121485 => true, + 121486 => true, + 121487 => true, + 121488 => true, + 121489 => true, + 121490 => true, + 121491 => true, + 121492 => true, + 121493 => true, + 121494 => true, + 121495 => true, + 121496 => true, + 121497 => true, + 121498 => true, + 121504 => true, + 122887 => true, + 122905 => true, + 122906 => true, + 122914 => true, + 122917 => true, + 123181 => true, + 123182 => true, + 123183 => true, + 123198 => true, + 123199 => true, + 123210 => true, + 123211 => true, + 123212 => true, + 123213 => true, + 123642 => true, + 123643 => true, + 123644 => true, + 123645 => true, + 123646 => true, + 125125 => true, + 125126 => true, + 125260 => true, + 125261 => true, + 125262 => true, + 125263 => true, + 125274 => true, + 125275 => true, + 125276 => true, + 125277 => true, + 126468 => true, + 126496 => true, + 126499 => true, + 126501 => true, + 126502 => true, + 126504 => true, + 126515 => true, + 126520 => true, + 126522 => true, + 126524 => true, + 126525 => true, + 126526 => true, + 126527 => true, + 126528 => true, + 126529 => true, + 126531 => true, + 126532 => true, + 126533 => true, + 126534 => true, + 126536 => true, + 126538 => true, + 126540 => true, + 126544 => true, + 126547 => true, + 126549 => true, + 126550 => true, + 126552 => true, + 126554 => true, + 126556 => true, + 126558 => true, + 126560 => true, + 126563 => true, + 126565 => true, + 126566 => true, + 126571 => true, + 126579 => true, + 126584 => true, + 126589 => true, + 126591 => true, + 126602 => true, + 126620 => true, + 126621 => true, + 126622 => true, + 126623 => true, + 126624 => true, + 126628 => true, + 126634 => true, + 127020 => true, + 127021 => true, + 127022 => true, + 127023 => true, + 127124 => true, + 127125 => true, + 127126 => true, + 127127 => true, + 127128 => true, + 127129 => true, + 127130 => true, + 127131 => true, + 127132 => true, + 127133 => true, + 127134 => true, + 127135 => true, + 127151 => true, + 127152 => true, + 127168 => true, + 127184 => true, + 127222 => true, + 127223 => true, + 127224 => true, + 127225 => true, + 127226 => true, + 127227 => true, + 127228 => true, + 127229 => true, + 127230 => true, + 127231 => true, + 127232 => true, + 127491 => true, + 127492 => true, + 127493 => true, + 127494 => true, + 127495 => true, + 127496 => true, + 127497 => true, + 127498 => true, + 127499 => true, + 127500 => true, + 127501 => true, + 127502 => true, + 127503 => true, + 127548 => true, + 127549 => true, + 127550 => true, + 127551 => true, + 127561 => true, + 127562 => true, + 127563 => true, + 127564 => true, + 127565 => true, + 127566 => true, + 127567 => true, + 127570 => true, + 127571 => true, + 127572 => true, + 127573 => true, + 127574 => true, + 127575 => true, + 127576 => true, + 127577 => true, + 127578 => true, + 127579 => true, + 127580 => true, + 127581 => true, + 127582 => true, + 127583 => true, + 128728 => true, + 128729 => true, + 128730 => true, + 128731 => true, + 128732 => true, + 128733 => true, + 128734 => true, + 128735 => true, + 128749 => true, + 128750 => true, + 128751 => true, + 128765 => true, + 128766 => true, + 128767 => true, + 128884 => true, + 128885 => true, + 128886 => true, + 128887 => true, + 128888 => true, + 128889 => true, + 128890 => true, + 128891 => true, + 128892 => true, + 128893 => true, + 128894 => true, + 128895 => true, + 128985 => true, + 128986 => true, + 128987 => true, + 128988 => true, + 128989 => true, + 128990 => true, + 128991 => true, + 129004 => true, + 129005 => true, + 129006 => true, + 129007 => true, + 129008 => true, + 129009 => true, + 129010 => true, + 129011 => true, + 129012 => true, + 129013 => true, + 129014 => true, + 129015 => true, + 129016 => true, + 129017 => true, + 129018 => true, + 129019 => true, + 129020 => true, + 129021 => true, + 129022 => true, + 129023 => true, + 129036 => true, + 129037 => true, + 129038 => true, + 129039 => true, + 129096 => true, + 129097 => true, + 129098 => true, + 129099 => true, + 129100 => true, + 129101 => true, + 129102 => true, + 129103 => true, + 129114 => true, + 129115 => true, + 129116 => true, + 129117 => true, + 129118 => true, + 129119 => true, + 129160 => true, + 129161 => true, + 129162 => true, + 129163 => true, + 129164 => true, + 129165 => true, + 129166 => true, + 129167 => true, + 129198 => true, + 129199 => true, + 129401 => true, + 129484 => true, + 129620 => true, + 129621 => true, + 129622 => true, + 129623 => true, + 129624 => true, + 129625 => true, + 129626 => true, + 129627 => true, + 129628 => true, + 129629 => true, + 129630 => true, + 129631 => true, + 129646 => true, + 129647 => true, + 129653 => true, + 129654 => true, + 129655 => true, + 129659 => true, + 129660 => true, + 129661 => true, + 129662 => true, + 129663 => true, + 129671 => true, + 129672 => true, + 129673 => true, + 129674 => true, + 129675 => true, + 129676 => true, + 129677 => true, + 129678 => true, + 129679 => true, + 129705 => true, + 129706 => true, + 129707 => true, + 129708 => true, + 129709 => true, + 129710 => true, + 129711 => true, + 129719 => true, + 129720 => true, + 129721 => true, + 129722 => true, + 129723 => true, + 129724 => true, + 129725 => true, + 129726 => true, + 129727 => true, + 129731 => true, + 129732 => true, + 129733 => true, + 129734 => true, + 129735 => true, + 129736 => true, + 129737 => true, + 129738 => true, + 129739 => true, + 129740 => true, + 129741 => true, + 129742 => true, + 129743 => true, + 129939 => true, + 131070 => true, + 131071 => true, + 177973 => true, + 177974 => true, + 177975 => true, + 177976 => true, + 177977 => true, + 177978 => true, + 177979 => true, + 177980 => true, + 177981 => true, + 177982 => true, + 177983 => true, + 178206 => true, + 178207 => true, + 183970 => true, + 183971 => true, + 183972 => true, + 183973 => true, + 183974 => true, + 183975 => true, + 183976 => true, + 183977 => true, + 183978 => true, + 183979 => true, + 183980 => true, + 183981 => true, + 183982 => true, + 183983 => true, + 194664 => true, + 194676 => true, + 194847 => true, + 194911 => true, + 195007 => true, + 196606 => true, + 196607 => true, + 262142 => true, + 262143 => true, + 327678 => true, + 327679 => true, + 393214 => true, + 393215 => true, + 458750 => true, + 458751 => true, + 524286 => true, + 524287 => true, + 589822 => true, + 589823 => true, + 655358 => true, + 655359 => true, + 720894 => true, + 720895 => true, + 786430 => true, + 786431 => true, + 851966 => true, + 851967 => true, + 917502 => true, + 917503 => true, + 917504 => true, + 917505 => true, + 917506 => true, + 917507 => true, + 917508 => true, + 917509 => true, + 917510 => true, + 917511 => true, + 917512 => true, + 917513 => true, + 917514 => true, + 917515 => true, + 917516 => true, + 917517 => true, + 917518 => true, + 917519 => true, + 917520 => true, + 917521 => true, + 917522 => true, + 917523 => true, + 917524 => true, + 917525 => true, + 917526 => true, + 917527 => true, + 917528 => true, + 917529 => true, + 917530 => true, + 917531 => true, + 917532 => true, + 917533 => true, + 917534 => true, + 917535 => true, + 983038 => true, + 983039 => true, + 1048574 => true, + 1048575 => true, + 1114110 => true, + 1114111 => true, +); diff --git a/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php b/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php new file mode 100644 index 0000000..54f21cc --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php @@ -0,0 +1,308 @@ + ' ', + 168 => ' ̈', + 175 => ' ̄', + 180 => ' ́', + 184 => ' ̧', + 728 => ' ̆', + 729 => ' ̇', + 730 => ' ̊', + 731 => ' ̨', + 732 => ' ̃', + 733 => ' ̋', + 890 => ' ι', + 894 => ';', + 900 => ' ́', + 901 => ' ̈́', + 8125 => ' ̓', + 8127 => ' ̓', + 8128 => ' ͂', + 8129 => ' ̈͂', + 8141 => ' ̓̀', + 8142 => ' ̓́', + 8143 => ' ̓͂', + 8157 => ' ̔̀', + 8158 => ' ̔́', + 8159 => ' ̔͂', + 8173 => ' ̈̀', + 8174 => ' ̈́', + 8175 => '`', + 8189 => ' ́', + 8190 => ' ̔', + 8192 => ' ', + 8193 => ' ', + 8194 => ' ', + 8195 => ' ', + 8196 => ' ', + 8197 => ' ', + 8198 => ' ', + 8199 => ' ', + 8200 => ' ', + 8201 => ' ', + 8202 => ' ', + 8215 => ' ̳', + 8239 => ' ', + 8252 => '!!', + 8254 => ' ̅', + 8263 => '??', + 8264 => '?!', + 8265 => '!?', + 8287 => ' ', + 8314 => '+', + 8316 => '=', + 8317 => '(', + 8318 => ')', + 8330 => '+', + 8332 => '=', + 8333 => '(', + 8334 => ')', + 8448 => 'a/c', + 8449 => 'a/s', + 8453 => 'c/o', + 8454 => 'c/u', + 9332 => '(1)', + 9333 => '(2)', + 9334 => '(3)', + 9335 => '(4)', + 9336 => '(5)', + 9337 => '(6)', + 9338 => '(7)', + 9339 => '(8)', + 9340 => '(9)', + 9341 => '(10)', + 9342 => '(11)', + 9343 => '(12)', + 9344 => '(13)', + 9345 => '(14)', + 9346 => '(15)', + 9347 => '(16)', + 9348 => '(17)', + 9349 => '(18)', + 9350 => '(19)', + 9351 => '(20)', + 9372 => '(a)', + 9373 => '(b)', + 9374 => '(c)', + 9375 => '(d)', + 9376 => '(e)', + 9377 => '(f)', + 9378 => '(g)', + 9379 => '(h)', + 9380 => '(i)', + 9381 => '(j)', + 9382 => '(k)', + 9383 => '(l)', + 9384 => '(m)', + 9385 => '(n)', + 9386 => '(o)', + 9387 => '(p)', + 9388 => '(q)', + 9389 => '(r)', + 9390 => '(s)', + 9391 => '(t)', + 9392 => '(u)', + 9393 => '(v)', + 9394 => '(w)', + 9395 => '(x)', + 9396 => '(y)', + 9397 => '(z)', + 10868 => '::=', + 10869 => '==', + 10870 => '===', + 12288 => ' ', + 12443 => ' ゙', + 12444 => ' ゚', + 12800 => '(ᄀ)', + 12801 => '(ᄂ)', + 12802 => '(ᄃ)', + 12803 => '(ᄅ)', + 12804 => '(ᄆ)', + 12805 => '(ᄇ)', + 12806 => '(ᄉ)', + 12807 => '(ᄋ)', + 12808 => '(ᄌ)', + 12809 => '(ᄎ)', + 12810 => '(ᄏ)', + 12811 => '(ᄐ)', + 12812 => '(ᄑ)', + 12813 => '(ᄒ)', + 12814 => '(가)', + 12815 => '(나)', + 12816 => '(다)', + 12817 => '(라)', + 12818 => '(마)', + 12819 => '(바)', + 12820 => '(사)', + 12821 => '(아)', + 12822 => '(자)', + 12823 => '(차)', + 12824 => '(카)', + 12825 => '(타)', + 12826 => '(파)', + 12827 => '(하)', + 12828 => '(주)', + 12829 => '(오전)', + 12830 => '(오후)', + 12832 => '(一)', + 12833 => '(二)', + 12834 => '(三)', + 12835 => '(四)', + 12836 => '(五)', + 12837 => '(六)', + 12838 => '(七)', + 12839 => '(八)', + 12840 => '(九)', + 12841 => '(十)', + 12842 => '(月)', + 12843 => '(火)', + 12844 => '(水)', + 12845 => '(木)', + 12846 => '(金)', + 12847 => '(土)', + 12848 => '(日)', + 12849 => '(株)', + 12850 => '(有)', + 12851 => '(社)', + 12852 => '(名)', + 12853 => '(特)', + 12854 => '(財)', + 12855 => '(祝)', + 12856 => '(労)', + 12857 => '(代)', + 12858 => '(呼)', + 12859 => '(学)', + 12860 => '(監)', + 12861 => '(企)', + 12862 => '(資)', + 12863 => '(協)', + 12864 => '(祭)', + 12865 => '(休)', + 12866 => '(自)', + 12867 => '(至)', + 64297 => '+', + 64606 => ' ٌّ', + 64607 => ' ٍّ', + 64608 => ' َّ', + 64609 => ' ُّ', + 64610 => ' ِّ', + 64611 => ' ّٰ', + 65018 => 'صلى الله عليه وسلم', + 65019 => 'جل جلاله', + 65040 => ',', + 65043 => ':', + 65044 => ';', + 65045 => '!', + 65046 => '?', + 65075 => '_', + 65076 => '_', + 65077 => '(', + 65078 => ')', + 65079 => '{', + 65080 => '}', + 65095 => '[', + 65096 => ']', + 65097 => ' ̅', + 65098 => ' ̅', + 65099 => ' ̅', + 65100 => ' ̅', + 65101 => '_', + 65102 => '_', + 65103 => '_', + 65104 => ',', + 65108 => ';', + 65109 => ':', + 65110 => '?', + 65111 => '!', + 65113 => '(', + 65114 => ')', + 65115 => '{', + 65116 => '}', + 65119 => '#', + 65120 => '&', + 65121 => '*', + 65122 => '+', + 65124 => '<', + 65125 => '>', + 65126 => '=', + 65128 => '\\', + 65129 => '$', + 65130 => '%', + 65131 => '@', + 65136 => ' ً', + 65138 => ' ٌ', + 65140 => ' ٍ', + 65142 => ' َ', + 65144 => ' ُ', + 65146 => ' ِ', + 65148 => ' ّ', + 65150 => ' ْ', + 65281 => '!', + 65282 => '"', + 65283 => '#', + 65284 => '$', + 65285 => '%', + 65286 => '&', + 65287 => '\'', + 65288 => '(', + 65289 => ')', + 65290 => '*', + 65291 => '+', + 65292 => ',', + 65295 => '/', + 65306 => ':', + 65307 => ';', + 65308 => '<', + 65309 => '=', + 65310 => '>', + 65311 => '?', + 65312 => '@', + 65339 => '[', + 65340 => '\\', + 65341 => ']', + 65342 => '^', + 65343 => '_', + 65344 => '`', + 65371 => '{', + 65372 => '|', + 65373 => '}', + 65374 => '~', + 65507 => ' ̄', + 127233 => '0,', + 127234 => '1,', + 127235 => '2,', + 127236 => '3,', + 127237 => '4,', + 127238 => '5,', + 127239 => '6,', + 127240 => '7,', + 127241 => '8,', + 127242 => '9,', + 127248 => '(a)', + 127249 => '(b)', + 127250 => '(c)', + 127251 => '(d)', + 127252 => '(e)', + 127253 => '(f)', + 127254 => '(g)', + 127255 => '(h)', + 127256 => '(i)', + 127257 => '(j)', + 127258 => '(k)', + 127259 => '(l)', + 127260 => '(m)', + 127261 => '(n)', + 127262 => '(o)', + 127263 => '(p)', + 127264 => '(q)', + 127265 => '(r)', + 127266 => '(s)', + 127267 => '(t)', + 127268 => '(u)', + 127269 => '(v)', + 127270 => '(w)', + 127271 => '(x)', + 127272 => '(y)', + 127273 => '(z)', +); diff --git a/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php b/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php new file mode 100644 index 0000000..223396e --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php @@ -0,0 +1,71 @@ + true, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + 5 => true, + 6 => true, + 7 => true, + 8 => true, + 9 => true, + 10 => true, + 11 => true, + 12 => true, + 13 => true, + 14 => true, + 15 => true, + 16 => true, + 17 => true, + 18 => true, + 19 => true, + 20 => true, + 21 => true, + 22 => true, + 23 => true, + 24 => true, + 25 => true, + 26 => true, + 27 => true, + 28 => true, + 29 => true, + 30 => true, + 31 => true, + 32 => true, + 33 => true, + 34 => true, + 35 => true, + 36 => true, + 37 => true, + 38 => true, + 39 => true, + 40 => true, + 41 => true, + 42 => true, + 43 => true, + 44 => true, + 47 => true, + 58 => true, + 59 => true, + 60 => true, + 61 => true, + 62 => true, + 63 => true, + 64 => true, + 91 => true, + 92 => true, + 93 => true, + 94 => true, + 95 => true, + 96 => true, + 123 => true, + 124 => true, + 125 => true, + 126 => true, + 127 => true, + 8800 => true, + 8814 => true, + 8815 => true, +); diff --git a/vendor/symfony/polyfill-intl-idn/Resources/unidata/ignored.php b/vendor/symfony/polyfill-intl-idn/Resources/unidata/ignored.php new file mode 100644 index 0000000..b377844 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/Resources/unidata/ignored.php @@ -0,0 +1,273 @@ + true, + 847 => true, + 6155 => true, + 6156 => true, + 6157 => true, + 8203 => true, + 8288 => true, + 8292 => true, + 65024 => true, + 65025 => true, + 65026 => true, + 65027 => true, + 65028 => true, + 65029 => true, + 65030 => true, + 65031 => true, + 65032 => true, + 65033 => true, + 65034 => true, + 65035 => true, + 65036 => true, + 65037 => true, + 65038 => true, + 65039 => true, + 65279 => true, + 113824 => true, + 113825 => true, + 113826 => true, + 113827 => true, + 917760 => true, + 917761 => true, + 917762 => true, + 917763 => true, + 917764 => true, + 917765 => true, + 917766 => true, + 917767 => true, + 917768 => true, + 917769 => true, + 917770 => true, + 917771 => true, + 917772 => true, + 917773 => true, + 917774 => true, + 917775 => true, + 917776 => true, + 917777 => true, + 917778 => true, + 917779 => true, + 917780 => true, + 917781 => true, + 917782 => true, + 917783 => true, + 917784 => true, + 917785 => true, + 917786 => true, + 917787 => true, + 917788 => true, + 917789 => true, + 917790 => true, + 917791 => true, + 917792 => true, + 917793 => true, + 917794 => true, + 917795 => true, + 917796 => true, + 917797 => true, + 917798 => true, + 917799 => true, + 917800 => true, + 917801 => true, + 917802 => true, + 917803 => true, + 917804 => true, + 917805 => true, + 917806 => true, + 917807 => true, + 917808 => true, + 917809 => true, + 917810 => true, + 917811 => true, + 917812 => true, + 917813 => true, + 917814 => true, + 917815 => true, + 917816 => true, + 917817 => true, + 917818 => true, + 917819 => true, + 917820 => true, + 917821 => true, + 917822 => true, + 917823 => true, + 917824 => true, + 917825 => true, + 917826 => true, + 917827 => true, + 917828 => true, + 917829 => true, + 917830 => true, + 917831 => true, + 917832 => true, + 917833 => true, + 917834 => true, + 917835 => true, + 917836 => true, + 917837 => true, + 917838 => true, + 917839 => true, + 917840 => true, + 917841 => true, + 917842 => true, + 917843 => true, + 917844 => true, + 917845 => true, + 917846 => true, + 917847 => true, + 917848 => true, + 917849 => true, + 917850 => true, + 917851 => true, + 917852 => true, + 917853 => true, + 917854 => true, + 917855 => true, + 917856 => true, + 917857 => true, + 917858 => true, + 917859 => true, + 917860 => true, + 917861 => true, + 917862 => true, + 917863 => true, + 917864 => true, + 917865 => true, + 917866 => true, + 917867 => true, + 917868 => true, + 917869 => true, + 917870 => true, + 917871 => true, + 917872 => true, + 917873 => true, + 917874 => true, + 917875 => true, + 917876 => true, + 917877 => true, + 917878 => true, + 917879 => true, + 917880 => true, + 917881 => true, + 917882 => true, + 917883 => true, + 917884 => true, + 917885 => true, + 917886 => true, + 917887 => true, + 917888 => true, + 917889 => true, + 917890 => true, + 917891 => true, + 917892 => true, + 917893 => true, + 917894 => true, + 917895 => true, + 917896 => true, + 917897 => true, + 917898 => true, + 917899 => true, + 917900 => true, + 917901 => true, + 917902 => true, + 917903 => true, + 917904 => true, + 917905 => true, + 917906 => true, + 917907 => true, + 917908 => true, + 917909 => true, + 917910 => true, + 917911 => true, + 917912 => true, + 917913 => true, + 917914 => true, + 917915 => true, + 917916 => true, + 917917 => true, + 917918 => true, + 917919 => true, + 917920 => true, + 917921 => true, + 917922 => true, + 917923 => true, + 917924 => true, + 917925 => true, + 917926 => true, + 917927 => true, + 917928 => true, + 917929 => true, + 917930 => true, + 917931 => true, + 917932 => true, + 917933 => true, + 917934 => true, + 917935 => true, + 917936 => true, + 917937 => true, + 917938 => true, + 917939 => true, + 917940 => true, + 917941 => true, + 917942 => true, + 917943 => true, + 917944 => true, + 917945 => true, + 917946 => true, + 917947 => true, + 917948 => true, + 917949 => true, + 917950 => true, + 917951 => true, + 917952 => true, + 917953 => true, + 917954 => true, + 917955 => true, + 917956 => true, + 917957 => true, + 917958 => true, + 917959 => true, + 917960 => true, + 917961 => true, + 917962 => true, + 917963 => true, + 917964 => true, + 917965 => true, + 917966 => true, + 917967 => true, + 917968 => true, + 917969 => true, + 917970 => true, + 917971 => true, + 917972 => true, + 917973 => true, + 917974 => true, + 917975 => true, + 917976 => true, + 917977 => true, + 917978 => true, + 917979 => true, + 917980 => true, + 917981 => true, + 917982 => true, + 917983 => true, + 917984 => true, + 917985 => true, + 917986 => true, + 917987 => true, + 917988 => true, + 917989 => true, + 917990 => true, + 917991 => true, + 917992 => true, + 917993 => true, + 917994 => true, + 917995 => true, + 917996 => true, + 917997 => true, + 917998 => true, + 917999 => true, +); diff --git a/vendor/symfony/polyfill-intl-idn/Resources/unidata/mapped.php b/vendor/symfony/polyfill-intl-idn/Resources/unidata/mapped.php new file mode 100644 index 0000000..9b85fe9 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/Resources/unidata/mapped.php @@ -0,0 +1,5778 @@ + 'a', + 66 => 'b', + 67 => 'c', + 68 => 'd', + 69 => 'e', + 70 => 'f', + 71 => 'g', + 72 => 'h', + 73 => 'i', + 74 => 'j', + 75 => 'k', + 76 => 'l', + 77 => 'm', + 78 => 'n', + 79 => 'o', + 80 => 'p', + 81 => 'q', + 82 => 'r', + 83 => 's', + 84 => 't', + 85 => 'u', + 86 => 'v', + 87 => 'w', + 88 => 'x', + 89 => 'y', + 90 => 'z', + 170 => 'a', + 178 => '2', + 179 => '3', + 181 => 'μ', + 185 => '1', + 186 => 'o', + 188 => '1⁄4', + 189 => '1⁄2', + 190 => '3⁄4', + 192 => 'à', + 193 => 'á', + 194 => 'â', + 195 => 'ã', + 196 => 'ä', + 197 => 'å', + 198 => 'æ', + 199 => 'ç', + 200 => 'è', + 201 => 'é', + 202 => 'ê', + 203 => 'ë', + 204 => 'ì', + 205 => 'í', + 206 => 'î', + 207 => 'ï', + 208 => 'ð', + 209 => 'ñ', + 210 => 'ò', + 211 => 'ó', + 212 => 'ô', + 213 => 'õ', + 214 => 'ö', + 216 => 'ø', + 217 => 'ù', + 218 => 'ú', + 219 => 'û', + 220 => 'ü', + 221 => 'ý', + 222 => 'þ', + 256 => 'ā', + 258 => 'ă', + 260 => 'ą', + 262 => 'ć', + 264 => 'ĉ', + 266 => 'ċ', + 268 => 'č', + 270 => 'ď', + 272 => 'đ', + 274 => 'ē', + 276 => 'ĕ', + 278 => 'ė', + 280 => 'ę', + 282 => 'ě', + 284 => 'ĝ', + 286 => 'ğ', + 288 => 'ġ', + 290 => 'ģ', + 292 => 'ĥ', + 294 => 'ħ', + 296 => 'ĩ', + 298 => 'ī', + 300 => 'ĭ', + 302 => 'į', + 304 => 'i̇', + 306 => 'ij', + 307 => 'ij', + 308 => 'ĵ', + 310 => 'ķ', + 313 => 'ĺ', + 315 => 'ļ', + 317 => 'ľ', + 319 => 'l·', + 320 => 'l·', + 321 => 'ł', + 323 => 'ń', + 325 => 'ņ', + 327 => 'ň', + 329 => 'ʼn', + 330 => 'ŋ', + 332 => 'ō', + 334 => 'ŏ', + 336 => 'ő', + 338 => 'œ', + 340 => 'ŕ', + 342 => 'ŗ', + 344 => 'ř', + 346 => 'ś', + 348 => 'ŝ', + 350 => 'ş', + 352 => 'š', + 354 => 'ţ', + 356 => 'ť', + 358 => 'ŧ', + 360 => 'ũ', + 362 => 'ū', + 364 => 'ŭ', + 366 => 'ů', + 368 => 'ű', + 370 => 'ų', + 372 => 'ŵ', + 374 => 'ŷ', + 376 => 'ÿ', + 377 => 'ź', + 379 => 'ż', + 381 => 'ž', + 383 => 's', + 385 => 'ɓ', + 386 => 'ƃ', + 388 => 'ƅ', + 390 => 'ɔ', + 391 => 'ƈ', + 393 => 'ɖ', + 394 => 'ɗ', + 395 => 'ƌ', + 398 => 'ǝ', + 399 => 'ə', + 400 => 'ɛ', + 401 => 'ƒ', + 403 => 'ɠ', + 404 => 'ɣ', + 406 => 'ɩ', + 407 => 'ɨ', + 408 => 'ƙ', + 412 => 'ɯ', + 413 => 'ɲ', + 415 => 'ɵ', + 416 => 'ơ', + 418 => 'ƣ', + 420 => 'ƥ', + 422 => 'ʀ', + 423 => 'ƨ', + 425 => 'ʃ', + 428 => 'ƭ', + 430 => 'ʈ', + 431 => 'ư', + 433 => 'ʊ', + 434 => 'ʋ', + 435 => 'ƴ', + 437 => 'ƶ', + 439 => 'ʒ', + 440 => 'ƹ', + 444 => 'ƽ', + 452 => 'dž', + 453 => 'dž', + 454 => 'dž', + 455 => 'lj', + 456 => 'lj', + 457 => 'lj', + 458 => 'nj', + 459 => 'nj', + 460 => 'nj', + 461 => 'ǎ', + 463 => 'ǐ', + 465 => 'ǒ', + 467 => 'ǔ', + 469 => 'ǖ', + 471 => 'ǘ', + 473 => 'ǚ', + 475 => 'ǜ', + 478 => 'ǟ', + 480 => 'ǡ', + 482 => 'ǣ', + 484 => 'ǥ', + 486 => 'ǧ', + 488 => 'ǩ', + 490 => 'ǫ', + 492 => 'ǭ', + 494 => 'ǯ', + 497 => 'dz', + 498 => 'dz', + 499 => 'dz', + 500 => 'ǵ', + 502 => 'ƕ', + 503 => 'ƿ', + 504 => 'ǹ', + 506 => 'ǻ', + 508 => 'ǽ', + 510 => 'ǿ', + 512 => 'ȁ', + 514 => 'ȃ', + 516 => 'ȅ', + 518 => 'ȇ', + 520 => 'ȉ', + 522 => 'ȋ', + 524 => 'ȍ', + 526 => 'ȏ', + 528 => 'ȑ', + 530 => 'ȓ', + 532 => 'ȕ', + 534 => 'ȗ', + 536 => 'ș', + 538 => 'ț', + 540 => 'ȝ', + 542 => 'ȟ', + 544 => 'ƞ', + 546 => 'ȣ', + 548 => 'ȥ', + 550 => 'ȧ', + 552 => 'ȩ', + 554 => 'ȫ', + 556 => 'ȭ', + 558 => 'ȯ', + 560 => 'ȱ', + 562 => 'ȳ', + 570 => 'ⱥ', + 571 => 'ȼ', + 573 => 'ƚ', + 574 => 'ⱦ', + 577 => 'ɂ', + 579 => 'ƀ', + 580 => 'ʉ', + 581 => 'ʌ', + 582 => 'ɇ', + 584 => 'ɉ', + 586 => 'ɋ', + 588 => 'ɍ', + 590 => 'ɏ', + 688 => 'h', + 689 => 'ɦ', + 690 => 'j', + 691 => 'r', + 692 => 'ɹ', + 693 => 'ɻ', + 694 => 'ʁ', + 695 => 'w', + 696 => 'y', + 736 => 'ɣ', + 737 => 'l', + 738 => 's', + 739 => 'x', + 740 => 'ʕ', + 832 => '̀', + 833 => '́', + 835 => '̓', + 836 => '̈́', + 837 => 'ι', + 880 => 'ͱ', + 882 => 'ͳ', + 884 => 'ʹ', + 886 => 'ͷ', + 895 => 'ϳ', + 902 => 'ά', + 903 => '·', + 904 => 'έ', + 905 => 'ή', + 906 => 'ί', + 908 => 'ό', + 910 => 'ύ', + 911 => 'ώ', + 913 => 'α', + 914 => 'β', + 915 => 'γ', + 916 => 'δ', + 917 => 'ε', + 918 => 'ζ', + 919 => 'η', + 920 => 'θ', + 921 => 'ι', + 922 => 'κ', + 923 => 'λ', + 924 => 'μ', + 925 => 'ν', + 926 => 'ξ', + 927 => 'ο', + 928 => 'π', + 929 => 'ρ', + 931 => 'σ', + 932 => 'τ', + 933 => 'υ', + 934 => 'φ', + 935 => 'χ', + 936 => 'ψ', + 937 => 'ω', + 938 => 'ϊ', + 939 => 'ϋ', + 975 => 'ϗ', + 976 => 'β', + 977 => 'θ', + 978 => 'υ', + 979 => 'ύ', + 980 => 'ϋ', + 981 => 'φ', + 982 => 'π', + 984 => 'ϙ', + 986 => 'ϛ', + 988 => 'ϝ', + 990 => 'ϟ', + 992 => 'ϡ', + 994 => 'ϣ', + 996 => 'ϥ', + 998 => 'ϧ', + 1000 => 'ϩ', + 1002 => 'ϫ', + 1004 => 'ϭ', + 1006 => 'ϯ', + 1008 => 'κ', + 1009 => 'ρ', + 1010 => 'σ', + 1012 => 'θ', + 1013 => 'ε', + 1015 => 'ϸ', + 1017 => 'σ', + 1018 => 'ϻ', + 1021 => 'ͻ', + 1022 => 'ͼ', + 1023 => 'ͽ', + 1024 => 'ѐ', + 1025 => 'ё', + 1026 => 'ђ', + 1027 => 'ѓ', + 1028 => 'є', + 1029 => 'ѕ', + 1030 => 'і', + 1031 => 'ї', + 1032 => 'ј', + 1033 => 'љ', + 1034 => 'њ', + 1035 => 'ћ', + 1036 => 'ќ', + 1037 => 'ѝ', + 1038 => 'ў', + 1039 => 'џ', + 1040 => 'а', + 1041 => 'б', + 1042 => 'в', + 1043 => 'г', + 1044 => 'д', + 1045 => 'е', + 1046 => 'ж', + 1047 => 'з', + 1048 => 'и', + 1049 => 'й', + 1050 => 'к', + 1051 => 'л', + 1052 => 'м', + 1053 => 'н', + 1054 => 'о', + 1055 => 'п', + 1056 => 'р', + 1057 => 'с', + 1058 => 'т', + 1059 => 'у', + 1060 => 'ф', + 1061 => 'х', + 1062 => 'ц', + 1063 => 'ч', + 1064 => 'ш', + 1065 => 'щ', + 1066 => 'ъ', + 1067 => 'ы', + 1068 => 'ь', + 1069 => 'э', + 1070 => 'ю', + 1071 => 'я', + 1120 => 'ѡ', + 1122 => 'ѣ', + 1124 => 'ѥ', + 1126 => 'ѧ', + 1128 => 'ѩ', + 1130 => 'ѫ', + 1132 => 'ѭ', + 1134 => 'ѯ', + 1136 => 'ѱ', + 1138 => 'ѳ', + 1140 => 'ѵ', + 1142 => 'ѷ', + 1144 => 'ѹ', + 1146 => 'ѻ', + 1148 => 'ѽ', + 1150 => 'ѿ', + 1152 => 'ҁ', + 1162 => 'ҋ', + 1164 => 'ҍ', + 1166 => 'ҏ', + 1168 => 'ґ', + 1170 => 'ғ', + 1172 => 'ҕ', + 1174 => 'җ', + 1176 => 'ҙ', + 1178 => 'қ', + 1180 => 'ҝ', + 1182 => 'ҟ', + 1184 => 'ҡ', + 1186 => 'ң', + 1188 => 'ҥ', + 1190 => 'ҧ', + 1192 => 'ҩ', + 1194 => 'ҫ', + 1196 => 'ҭ', + 1198 => 'ү', + 1200 => 'ұ', + 1202 => 'ҳ', + 1204 => 'ҵ', + 1206 => 'ҷ', + 1208 => 'ҹ', + 1210 => 'һ', + 1212 => 'ҽ', + 1214 => 'ҿ', + 1217 => 'ӂ', + 1219 => 'ӄ', + 1221 => 'ӆ', + 1223 => 'ӈ', + 1225 => 'ӊ', + 1227 => 'ӌ', + 1229 => 'ӎ', + 1232 => 'ӑ', + 1234 => 'ӓ', + 1236 => 'ӕ', + 1238 => 'ӗ', + 1240 => 'ә', + 1242 => 'ӛ', + 1244 => 'ӝ', + 1246 => 'ӟ', + 1248 => 'ӡ', + 1250 => 'ӣ', + 1252 => 'ӥ', + 1254 => 'ӧ', + 1256 => 'ө', + 1258 => 'ӫ', + 1260 => 'ӭ', + 1262 => 'ӯ', + 1264 => 'ӱ', + 1266 => 'ӳ', + 1268 => 'ӵ', + 1270 => 'ӷ', + 1272 => 'ӹ', + 1274 => 'ӻ', + 1276 => 'ӽ', + 1278 => 'ӿ', + 1280 => 'ԁ', + 1282 => 'ԃ', + 1284 => 'ԅ', + 1286 => 'ԇ', + 1288 => 'ԉ', + 1290 => 'ԋ', + 1292 => 'ԍ', + 1294 => 'ԏ', + 1296 => 'ԑ', + 1298 => 'ԓ', + 1300 => 'ԕ', + 1302 => 'ԗ', + 1304 => 'ԙ', + 1306 => 'ԛ', + 1308 => 'ԝ', + 1310 => 'ԟ', + 1312 => 'ԡ', + 1314 => 'ԣ', + 1316 => 'ԥ', + 1318 => 'ԧ', + 1320 => 'ԩ', + 1322 => 'ԫ', + 1324 => 'ԭ', + 1326 => 'ԯ', + 1329 => 'ա', + 1330 => 'բ', + 1331 => 'գ', + 1332 => 'դ', + 1333 => 'ե', + 1334 => 'զ', + 1335 => 'է', + 1336 => 'ը', + 1337 => 'թ', + 1338 => 'ժ', + 1339 => 'ի', + 1340 => 'լ', + 1341 => 'խ', + 1342 => 'ծ', + 1343 => 'կ', + 1344 => 'հ', + 1345 => 'ձ', + 1346 => 'ղ', + 1347 => 'ճ', + 1348 => 'մ', + 1349 => 'յ', + 1350 => 'ն', + 1351 => 'շ', + 1352 => 'ո', + 1353 => 'չ', + 1354 => 'պ', + 1355 => 'ջ', + 1356 => 'ռ', + 1357 => 'ս', + 1358 => 'վ', + 1359 => 'տ', + 1360 => 'ր', + 1361 => 'ց', + 1362 => 'ւ', + 1363 => 'փ', + 1364 => 'ք', + 1365 => 'օ', + 1366 => 'ֆ', + 1415 => 'եւ', + 1653 => 'اٴ', + 1654 => 'وٴ', + 1655 => 'ۇٴ', + 1656 => 'يٴ', + 2392 => 'क़', + 2393 => 'ख़', + 2394 => 'ग़', + 2395 => 'ज़', + 2396 => 'ड़', + 2397 => 'ढ़', + 2398 => 'फ़', + 2399 => 'य़', + 2524 => 'ড়', + 2525 => 'ঢ়', + 2527 => 'য়', + 2611 => 'ਲ਼', + 2614 => 'ਸ਼', + 2649 => 'ਖ਼', + 2650 => 'ਗ਼', + 2651 => 'ਜ਼', + 2654 => 'ਫ਼', + 2908 => 'ଡ଼', + 2909 => 'ଢ଼', + 3635 => 'ํา', + 3763 => 'ໍາ', + 3804 => 'ຫນ', + 3805 => 'ຫມ', + 3852 => '་', + 3907 => 'གྷ', + 3917 => 'ཌྷ', + 3922 => 'དྷ', + 3927 => 'བྷ', + 3932 => 'ཛྷ', + 3945 => 'ཀྵ', + 3955 => 'ཱི', + 3957 => 'ཱུ', + 3958 => 'ྲྀ', + 3959 => 'ྲཱྀ', + 3960 => 'ླྀ', + 3961 => 'ླཱྀ', + 3969 => 'ཱྀ', + 3987 => 'ྒྷ', + 3997 => 'ྜྷ', + 4002 => 'ྡྷ', + 4007 => 'ྦྷ', + 4012 => 'ྫྷ', + 4025 => 'ྐྵ', + 4295 => 'ⴧ', + 4301 => 'ⴭ', + 4348 => 'ნ', + 5112 => 'Ᏸ', + 5113 => 'Ᏹ', + 5114 => 'Ᏺ', + 5115 => 'Ᏻ', + 5116 => 'Ᏼ', + 5117 => 'Ᏽ', + 7296 => 'в', + 7297 => 'д', + 7298 => 'о', + 7299 => 'с', + 7300 => 'т', + 7301 => 'т', + 7302 => 'ъ', + 7303 => 'ѣ', + 7304 => 'ꙋ', + 7312 => 'ა', + 7313 => 'ბ', + 7314 => 'გ', + 7315 => 'დ', + 7316 => 'ე', + 7317 => 'ვ', + 7318 => 'ზ', + 7319 => 'თ', + 7320 => 'ი', + 7321 => 'კ', + 7322 => 'ლ', + 7323 => 'მ', + 7324 => 'ნ', + 7325 => 'ო', + 7326 => 'პ', + 7327 => 'ჟ', + 7328 => 'რ', + 7329 => 'ს', + 7330 => 'ტ', + 7331 => 'უ', + 7332 => 'ფ', + 7333 => 'ქ', + 7334 => 'ღ', + 7335 => 'ყ', + 7336 => 'შ', + 7337 => 'ჩ', + 7338 => 'ც', + 7339 => 'ძ', + 7340 => 'წ', + 7341 => 'ჭ', + 7342 => 'ხ', + 7343 => 'ჯ', + 7344 => 'ჰ', + 7345 => 'ჱ', + 7346 => 'ჲ', + 7347 => 'ჳ', + 7348 => 'ჴ', + 7349 => 'ჵ', + 7350 => 'ჶ', + 7351 => 'ჷ', + 7352 => 'ჸ', + 7353 => 'ჹ', + 7354 => 'ჺ', + 7357 => 'ჽ', + 7358 => 'ჾ', + 7359 => 'ჿ', + 7468 => 'a', + 7469 => 'æ', + 7470 => 'b', + 7472 => 'd', + 7473 => 'e', + 7474 => 'ǝ', + 7475 => 'g', + 7476 => 'h', + 7477 => 'i', + 7478 => 'j', + 7479 => 'k', + 7480 => 'l', + 7481 => 'm', + 7482 => 'n', + 7484 => 'o', + 7485 => 'ȣ', + 7486 => 'p', + 7487 => 'r', + 7488 => 't', + 7489 => 'u', + 7490 => 'w', + 7491 => 'a', + 7492 => 'ɐ', + 7493 => 'ɑ', + 7494 => 'ᴂ', + 7495 => 'b', + 7496 => 'd', + 7497 => 'e', + 7498 => 'ə', + 7499 => 'ɛ', + 7500 => 'ɜ', + 7501 => 'g', + 7503 => 'k', + 7504 => 'm', + 7505 => 'ŋ', + 7506 => 'o', + 7507 => 'ɔ', + 7508 => 'ᴖ', + 7509 => 'ᴗ', + 7510 => 'p', + 7511 => 't', + 7512 => 'u', + 7513 => 'ᴝ', + 7514 => 'ɯ', + 7515 => 'v', + 7516 => 'ᴥ', + 7517 => 'β', + 7518 => 'γ', + 7519 => 'δ', + 7520 => 'φ', + 7521 => 'χ', + 7522 => 'i', + 7523 => 'r', + 7524 => 'u', + 7525 => 'v', + 7526 => 'β', + 7527 => 'γ', + 7528 => 'ρ', + 7529 => 'φ', + 7530 => 'χ', + 7544 => 'н', + 7579 => 'ɒ', + 7580 => 'c', + 7581 => 'ɕ', + 7582 => 'ð', + 7583 => 'ɜ', + 7584 => 'f', + 7585 => 'ɟ', + 7586 => 'ɡ', + 7587 => 'ɥ', + 7588 => 'ɨ', + 7589 => 'ɩ', + 7590 => 'ɪ', + 7591 => 'ᵻ', + 7592 => 'ʝ', + 7593 => 'ɭ', + 7594 => 'ᶅ', + 7595 => 'ʟ', + 7596 => 'ɱ', + 7597 => 'ɰ', + 7598 => 'ɲ', + 7599 => 'ɳ', + 7600 => 'ɴ', + 7601 => 'ɵ', + 7602 => 'ɸ', + 7603 => 'ʂ', + 7604 => 'ʃ', + 7605 => 'ƫ', + 7606 => 'ʉ', + 7607 => 'ʊ', + 7608 => 'ᴜ', + 7609 => 'ʋ', + 7610 => 'ʌ', + 7611 => 'z', + 7612 => 'ʐ', + 7613 => 'ʑ', + 7614 => 'ʒ', + 7615 => 'θ', + 7680 => 'ḁ', + 7682 => 'ḃ', + 7684 => 'ḅ', + 7686 => 'ḇ', + 7688 => 'ḉ', + 7690 => 'ḋ', + 7692 => 'ḍ', + 7694 => 'ḏ', + 7696 => 'ḑ', + 7698 => 'ḓ', + 7700 => 'ḕ', + 7702 => 'ḗ', + 7704 => 'ḙ', + 7706 => 'ḛ', + 7708 => 'ḝ', + 7710 => 'ḟ', + 7712 => 'ḡ', + 7714 => 'ḣ', + 7716 => 'ḥ', + 7718 => 'ḧ', + 7720 => 'ḩ', + 7722 => 'ḫ', + 7724 => 'ḭ', + 7726 => 'ḯ', + 7728 => 'ḱ', + 7730 => 'ḳ', + 7732 => 'ḵ', + 7734 => 'ḷ', + 7736 => 'ḹ', + 7738 => 'ḻ', + 7740 => 'ḽ', + 7742 => 'ḿ', + 7744 => 'ṁ', + 7746 => 'ṃ', + 7748 => 'ṅ', + 7750 => 'ṇ', + 7752 => 'ṉ', + 7754 => 'ṋ', + 7756 => 'ṍ', + 7758 => 'ṏ', + 7760 => 'ṑ', + 7762 => 'ṓ', + 7764 => 'ṕ', + 7766 => 'ṗ', + 7768 => 'ṙ', + 7770 => 'ṛ', + 7772 => 'ṝ', + 7774 => 'ṟ', + 7776 => 'ṡ', + 7778 => 'ṣ', + 7780 => 'ṥ', + 7782 => 'ṧ', + 7784 => 'ṩ', + 7786 => 'ṫ', + 7788 => 'ṭ', + 7790 => 'ṯ', + 7792 => 'ṱ', + 7794 => 'ṳ', + 7796 => 'ṵ', + 7798 => 'ṷ', + 7800 => 'ṹ', + 7802 => 'ṻ', + 7804 => 'ṽ', + 7806 => 'ṿ', + 7808 => 'ẁ', + 7810 => 'ẃ', + 7812 => 'ẅ', + 7814 => 'ẇ', + 7816 => 'ẉ', + 7818 => 'ẋ', + 7820 => 'ẍ', + 7822 => 'ẏ', + 7824 => 'ẑ', + 7826 => 'ẓ', + 7828 => 'ẕ', + 7834 => 'aʾ', + 7835 => 'ṡ', + 7838 => 'ss', + 7840 => 'ạ', + 7842 => 'ả', + 7844 => 'ấ', + 7846 => 'ầ', + 7848 => 'ẩ', + 7850 => 'ẫ', + 7852 => 'ậ', + 7854 => 'ắ', + 7856 => 'ằ', + 7858 => 'ẳ', + 7860 => 'ẵ', + 7862 => 'ặ', + 7864 => 'ẹ', + 7866 => 'ẻ', + 7868 => 'ẽ', + 7870 => 'ế', + 7872 => 'ề', + 7874 => 'ể', + 7876 => 'ễ', + 7878 => 'ệ', + 7880 => 'ỉ', + 7882 => 'ị', + 7884 => 'ọ', + 7886 => 'ỏ', + 7888 => 'ố', + 7890 => 'ồ', + 7892 => 'ổ', + 7894 => 'ỗ', + 7896 => 'ộ', + 7898 => 'ớ', + 7900 => 'ờ', + 7902 => 'ở', + 7904 => 'ỡ', + 7906 => 'ợ', + 7908 => 'ụ', + 7910 => 'ủ', + 7912 => 'ứ', + 7914 => 'ừ', + 7916 => 'ử', + 7918 => 'ữ', + 7920 => 'ự', + 7922 => 'ỳ', + 7924 => 'ỵ', + 7926 => 'ỷ', + 7928 => 'ỹ', + 7930 => 'ỻ', + 7932 => 'ỽ', + 7934 => 'ỿ', + 7944 => 'ἀ', + 7945 => 'ἁ', + 7946 => 'ἂ', + 7947 => 'ἃ', + 7948 => 'ἄ', + 7949 => 'ἅ', + 7950 => 'ἆ', + 7951 => 'ἇ', + 7960 => 'ἐ', + 7961 => 'ἑ', + 7962 => 'ἒ', + 7963 => 'ἓ', + 7964 => 'ἔ', + 7965 => 'ἕ', + 7976 => 'ἠ', + 7977 => 'ἡ', + 7978 => 'ἢ', + 7979 => 'ἣ', + 7980 => 'ἤ', + 7981 => 'ἥ', + 7982 => 'ἦ', + 7983 => 'ἧ', + 7992 => 'ἰ', + 7993 => 'ἱ', + 7994 => 'ἲ', + 7995 => 'ἳ', + 7996 => 'ἴ', + 7997 => 'ἵ', + 7998 => 'ἶ', + 7999 => 'ἷ', + 8008 => 'ὀ', + 8009 => 'ὁ', + 8010 => 'ὂ', + 8011 => 'ὃ', + 8012 => 'ὄ', + 8013 => 'ὅ', + 8025 => 'ὑ', + 8027 => 'ὓ', + 8029 => 'ὕ', + 8031 => 'ὗ', + 8040 => 'ὠ', + 8041 => 'ὡ', + 8042 => 'ὢ', + 8043 => 'ὣ', + 8044 => 'ὤ', + 8045 => 'ὥ', + 8046 => 'ὦ', + 8047 => 'ὧ', + 8049 => 'ά', + 8051 => 'έ', + 8053 => 'ή', + 8055 => 'ί', + 8057 => 'ό', + 8059 => 'ύ', + 8061 => 'ώ', + 8064 => 'ἀι', + 8065 => 'ἁι', + 8066 => 'ἂι', + 8067 => 'ἃι', + 8068 => 'ἄι', + 8069 => 'ἅι', + 8070 => 'ἆι', + 8071 => 'ἇι', + 8072 => 'ἀι', + 8073 => 'ἁι', + 8074 => 'ἂι', + 8075 => 'ἃι', + 8076 => 'ἄι', + 8077 => 'ἅι', + 8078 => 'ἆι', + 8079 => 'ἇι', + 8080 => 'ἠι', + 8081 => 'ἡι', + 8082 => 'ἢι', + 8083 => 'ἣι', + 8084 => 'ἤι', + 8085 => 'ἥι', + 8086 => 'ἦι', + 8087 => 'ἧι', + 8088 => 'ἠι', + 8089 => 'ἡι', + 8090 => 'ἢι', + 8091 => 'ἣι', + 8092 => 'ἤι', + 8093 => 'ἥι', + 8094 => 'ἦι', + 8095 => 'ἧι', + 8096 => 'ὠι', + 8097 => 'ὡι', + 8098 => 'ὢι', + 8099 => 'ὣι', + 8100 => 'ὤι', + 8101 => 'ὥι', + 8102 => 'ὦι', + 8103 => 'ὧι', + 8104 => 'ὠι', + 8105 => 'ὡι', + 8106 => 'ὢι', + 8107 => 'ὣι', + 8108 => 'ὤι', + 8109 => 'ὥι', + 8110 => 'ὦι', + 8111 => 'ὧι', + 8114 => 'ὰι', + 8115 => 'αι', + 8116 => 'άι', + 8119 => 'ᾶι', + 8120 => 'ᾰ', + 8121 => 'ᾱ', + 8122 => 'ὰ', + 8123 => 'ά', + 8124 => 'αι', + 8126 => 'ι', + 8130 => 'ὴι', + 8131 => 'ηι', + 8132 => 'ήι', + 8135 => 'ῆι', + 8136 => 'ὲ', + 8137 => 'έ', + 8138 => 'ὴ', + 8139 => 'ή', + 8140 => 'ηι', + 8147 => 'ΐ', + 8152 => 'ῐ', + 8153 => 'ῑ', + 8154 => 'ὶ', + 8155 => 'ί', + 8163 => 'ΰ', + 8168 => 'ῠ', + 8169 => 'ῡ', + 8170 => 'ὺ', + 8171 => 'ύ', + 8172 => 'ῥ', + 8178 => 'ὼι', + 8179 => 'ωι', + 8180 => 'ώι', + 8183 => 'ῶι', + 8184 => 'ὸ', + 8185 => 'ό', + 8186 => 'ὼ', + 8187 => 'ώ', + 8188 => 'ωι', + 8209 => '‐', + 8243 => '′′', + 8244 => '′′′', + 8246 => '‵‵', + 8247 => '‵‵‵', + 8279 => '′′′′', + 8304 => '0', + 8305 => 'i', + 8308 => '4', + 8309 => '5', + 8310 => '6', + 8311 => '7', + 8312 => '8', + 8313 => '9', + 8315 => '−', + 8319 => 'n', + 8320 => '0', + 8321 => '1', + 8322 => '2', + 8323 => '3', + 8324 => '4', + 8325 => '5', + 8326 => '6', + 8327 => '7', + 8328 => '8', + 8329 => '9', + 8331 => '−', + 8336 => 'a', + 8337 => 'e', + 8338 => 'o', + 8339 => 'x', + 8340 => 'ə', + 8341 => 'h', + 8342 => 'k', + 8343 => 'l', + 8344 => 'm', + 8345 => 'n', + 8346 => 'p', + 8347 => 's', + 8348 => 't', + 8360 => 'rs', + 8450 => 'c', + 8451 => '°c', + 8455 => 'ɛ', + 8457 => '°f', + 8458 => 'g', + 8459 => 'h', + 8460 => 'h', + 8461 => 'h', + 8462 => 'h', + 8463 => 'ħ', + 8464 => 'i', + 8465 => 'i', + 8466 => 'l', + 8467 => 'l', + 8469 => 'n', + 8470 => 'no', + 8473 => 'p', + 8474 => 'q', + 8475 => 'r', + 8476 => 'r', + 8477 => 'r', + 8480 => 'sm', + 8481 => 'tel', + 8482 => 'tm', + 8484 => 'z', + 8486 => 'ω', + 8488 => 'z', + 8490 => 'k', + 8491 => 'å', + 8492 => 'b', + 8493 => 'c', + 8495 => 'e', + 8496 => 'e', + 8497 => 'f', + 8499 => 'm', + 8500 => 'o', + 8501 => 'א', + 8502 => 'ב', + 8503 => 'ג', + 8504 => 'ד', + 8505 => 'i', + 8507 => 'fax', + 8508 => 'π', + 8509 => 'γ', + 8510 => 'γ', + 8511 => 'π', + 8512 => '∑', + 8517 => 'd', + 8518 => 'd', + 8519 => 'e', + 8520 => 'i', + 8521 => 'j', + 8528 => '1⁄7', + 8529 => '1⁄9', + 8530 => '1⁄10', + 8531 => '1⁄3', + 8532 => '2⁄3', + 8533 => '1⁄5', + 8534 => '2⁄5', + 8535 => '3⁄5', + 8536 => '4⁄5', + 8537 => '1⁄6', + 8538 => '5⁄6', + 8539 => '1⁄8', + 8540 => '3⁄8', + 8541 => '5⁄8', + 8542 => '7⁄8', + 8543 => '1⁄', + 8544 => 'i', + 8545 => 'ii', + 8546 => 'iii', + 8547 => 'iv', + 8548 => 'v', + 8549 => 'vi', + 8550 => 'vii', + 8551 => 'viii', + 8552 => 'ix', + 8553 => 'x', + 8554 => 'xi', + 8555 => 'xii', + 8556 => 'l', + 8557 => 'c', + 8558 => 'd', + 8559 => 'm', + 8560 => 'i', + 8561 => 'ii', + 8562 => 'iii', + 8563 => 'iv', + 8564 => 'v', + 8565 => 'vi', + 8566 => 'vii', + 8567 => 'viii', + 8568 => 'ix', + 8569 => 'x', + 8570 => 'xi', + 8571 => 'xii', + 8572 => 'l', + 8573 => 'c', + 8574 => 'd', + 8575 => 'm', + 8585 => '0⁄3', + 8748 => '∫∫', + 8749 => '∫∫∫', + 8751 => '∮∮', + 8752 => '∮∮∮', + 9001 => '〈', + 9002 => '〉', + 9312 => '1', + 9313 => '2', + 9314 => '3', + 9315 => '4', + 9316 => '5', + 9317 => '6', + 9318 => '7', + 9319 => '8', + 9320 => '9', + 9321 => '10', + 9322 => '11', + 9323 => '12', + 9324 => '13', + 9325 => '14', + 9326 => '15', + 9327 => '16', + 9328 => '17', + 9329 => '18', + 9330 => '19', + 9331 => '20', + 9398 => 'a', + 9399 => 'b', + 9400 => 'c', + 9401 => 'd', + 9402 => 'e', + 9403 => 'f', + 9404 => 'g', + 9405 => 'h', + 9406 => 'i', + 9407 => 'j', + 9408 => 'k', + 9409 => 'l', + 9410 => 'm', + 9411 => 'n', + 9412 => 'o', + 9413 => 'p', + 9414 => 'q', + 9415 => 'r', + 9416 => 's', + 9417 => 't', + 9418 => 'u', + 9419 => 'v', + 9420 => 'w', + 9421 => 'x', + 9422 => 'y', + 9423 => 'z', + 9424 => 'a', + 9425 => 'b', + 9426 => 'c', + 9427 => 'd', + 9428 => 'e', + 9429 => 'f', + 9430 => 'g', + 9431 => 'h', + 9432 => 'i', + 9433 => 'j', + 9434 => 'k', + 9435 => 'l', + 9436 => 'm', + 9437 => 'n', + 9438 => 'o', + 9439 => 'p', + 9440 => 'q', + 9441 => 'r', + 9442 => 's', + 9443 => 't', + 9444 => 'u', + 9445 => 'v', + 9446 => 'w', + 9447 => 'x', + 9448 => 'y', + 9449 => 'z', + 9450 => '0', + 10764 => '∫∫∫∫', + 10972 => '⫝̸', + 11264 => 'ⰰ', + 11265 => 'ⰱ', + 11266 => 'ⰲ', + 11267 => 'ⰳ', + 11268 => 'ⰴ', + 11269 => 'ⰵ', + 11270 => 'ⰶ', + 11271 => 'ⰷ', + 11272 => 'ⰸ', + 11273 => 'ⰹ', + 11274 => 'ⰺ', + 11275 => 'ⰻ', + 11276 => 'ⰼ', + 11277 => 'ⰽ', + 11278 => 'ⰾ', + 11279 => 'ⰿ', + 11280 => 'ⱀ', + 11281 => 'ⱁ', + 11282 => 'ⱂ', + 11283 => 'ⱃ', + 11284 => 'ⱄ', + 11285 => 'ⱅ', + 11286 => 'ⱆ', + 11287 => 'ⱇ', + 11288 => 'ⱈ', + 11289 => 'ⱉ', + 11290 => 'ⱊ', + 11291 => 'ⱋ', + 11292 => 'ⱌ', + 11293 => 'ⱍ', + 11294 => 'ⱎ', + 11295 => 'ⱏ', + 11296 => 'ⱐ', + 11297 => 'ⱑ', + 11298 => 'ⱒ', + 11299 => 'ⱓ', + 11300 => 'ⱔ', + 11301 => 'ⱕ', + 11302 => 'ⱖ', + 11303 => 'ⱗ', + 11304 => 'ⱘ', + 11305 => 'ⱙ', + 11306 => 'ⱚ', + 11307 => 'ⱛ', + 11308 => 'ⱜ', + 11309 => 'ⱝ', + 11310 => 'ⱞ', + 11360 => 'ⱡ', + 11362 => 'ɫ', + 11363 => 'ᵽ', + 11364 => 'ɽ', + 11367 => 'ⱨ', + 11369 => 'ⱪ', + 11371 => 'ⱬ', + 11373 => 'ɑ', + 11374 => 'ɱ', + 11375 => 'ɐ', + 11376 => 'ɒ', + 11378 => 'ⱳ', + 11381 => 'ⱶ', + 11388 => 'j', + 11389 => 'v', + 11390 => 'ȿ', + 11391 => 'ɀ', + 11392 => 'ⲁ', + 11394 => 'ⲃ', + 11396 => 'ⲅ', + 11398 => 'ⲇ', + 11400 => 'ⲉ', + 11402 => 'ⲋ', + 11404 => 'ⲍ', + 11406 => 'ⲏ', + 11408 => 'ⲑ', + 11410 => 'ⲓ', + 11412 => 'ⲕ', + 11414 => 'ⲗ', + 11416 => 'ⲙ', + 11418 => 'ⲛ', + 11420 => 'ⲝ', + 11422 => 'ⲟ', + 11424 => 'ⲡ', + 11426 => 'ⲣ', + 11428 => 'ⲥ', + 11430 => 'ⲧ', + 11432 => 'ⲩ', + 11434 => 'ⲫ', + 11436 => 'ⲭ', + 11438 => 'ⲯ', + 11440 => 'ⲱ', + 11442 => 'ⲳ', + 11444 => 'ⲵ', + 11446 => 'ⲷ', + 11448 => 'ⲹ', + 11450 => 'ⲻ', + 11452 => 'ⲽ', + 11454 => 'ⲿ', + 11456 => 'ⳁ', + 11458 => 'ⳃ', + 11460 => 'ⳅ', + 11462 => 'ⳇ', + 11464 => 'ⳉ', + 11466 => 'ⳋ', + 11468 => 'ⳍ', + 11470 => 'ⳏ', + 11472 => 'ⳑ', + 11474 => 'ⳓ', + 11476 => 'ⳕ', + 11478 => 'ⳗ', + 11480 => 'ⳙ', + 11482 => 'ⳛ', + 11484 => 'ⳝ', + 11486 => 'ⳟ', + 11488 => 'ⳡ', + 11490 => 'ⳣ', + 11499 => 'ⳬ', + 11501 => 'ⳮ', + 11506 => 'ⳳ', + 11631 => 'ⵡ', + 11935 => '母', + 12019 => '龟', + 12032 => '一', + 12033 => '丨', + 12034 => '丶', + 12035 => '丿', + 12036 => '乙', + 12037 => '亅', + 12038 => '二', + 12039 => '亠', + 12040 => '人', + 12041 => '儿', + 12042 => '入', + 12043 => '八', + 12044 => '冂', + 12045 => '冖', + 12046 => '冫', + 12047 => '几', + 12048 => '凵', + 12049 => '刀', + 12050 => '力', + 12051 => '勹', + 12052 => '匕', + 12053 => '匚', + 12054 => '匸', + 12055 => '十', + 12056 => '卜', + 12057 => '卩', + 12058 => '厂', + 12059 => '厶', + 12060 => '又', + 12061 => '口', + 12062 => '囗', + 12063 => '土', + 12064 => '士', + 12065 => '夂', + 12066 => '夊', + 12067 => '夕', + 12068 => '大', + 12069 => '女', + 12070 => '子', + 12071 => '宀', + 12072 => '寸', + 12073 => '小', + 12074 => '尢', + 12075 => '尸', + 12076 => '屮', + 12077 => '山', + 12078 => '巛', + 12079 => '工', + 12080 => '己', + 12081 => '巾', + 12082 => '干', + 12083 => '幺', + 12084 => '广', + 12085 => '廴', + 12086 => '廾', + 12087 => '弋', + 12088 => '弓', + 12089 => '彐', + 12090 => '彡', + 12091 => '彳', + 12092 => '心', + 12093 => '戈', + 12094 => '戶', + 12095 => '手', + 12096 => '支', + 12097 => '攴', + 12098 => '文', + 12099 => '斗', + 12100 => '斤', + 12101 => '方', + 12102 => '无', + 12103 => '日', + 12104 => '曰', + 12105 => '月', + 12106 => '木', + 12107 => '欠', + 12108 => '止', + 12109 => '歹', + 12110 => '殳', + 12111 => '毋', + 12112 => '比', + 12113 => '毛', + 12114 => '氏', + 12115 => '气', + 12116 => '水', + 12117 => '火', + 12118 => '爪', + 12119 => '父', + 12120 => '爻', + 12121 => '爿', + 12122 => '片', + 12123 => '牙', + 12124 => '牛', + 12125 => '犬', + 12126 => '玄', + 12127 => '玉', + 12128 => '瓜', + 12129 => '瓦', + 12130 => '甘', + 12131 => '生', + 12132 => '用', + 12133 => '田', + 12134 => '疋', + 12135 => '疒', + 12136 => '癶', + 12137 => '白', + 12138 => '皮', + 12139 => '皿', + 12140 => '目', + 12141 => '矛', + 12142 => '矢', + 12143 => '石', + 12144 => '示', + 12145 => '禸', + 12146 => '禾', + 12147 => '穴', + 12148 => '立', + 12149 => '竹', + 12150 => '米', + 12151 => '糸', + 12152 => '缶', + 12153 => '网', + 12154 => '羊', + 12155 => '羽', + 12156 => '老', + 12157 => '而', + 12158 => '耒', + 12159 => '耳', + 12160 => '聿', + 12161 => '肉', + 12162 => '臣', + 12163 => '自', + 12164 => '至', + 12165 => '臼', + 12166 => '舌', + 12167 => '舛', + 12168 => '舟', + 12169 => '艮', + 12170 => '色', + 12171 => '艸', + 12172 => '虍', + 12173 => '虫', + 12174 => '血', + 12175 => '行', + 12176 => '衣', + 12177 => '襾', + 12178 => '見', + 12179 => '角', + 12180 => '言', + 12181 => '谷', + 12182 => '豆', + 12183 => '豕', + 12184 => '豸', + 12185 => '貝', + 12186 => '赤', + 12187 => '走', + 12188 => '足', + 12189 => '身', + 12190 => '車', + 12191 => '辛', + 12192 => '辰', + 12193 => '辵', + 12194 => '邑', + 12195 => '酉', + 12196 => '釆', + 12197 => '里', + 12198 => '金', + 12199 => '長', + 12200 => '門', + 12201 => '阜', + 12202 => '隶', + 12203 => '隹', + 12204 => '雨', + 12205 => '靑', + 12206 => '非', + 12207 => '面', + 12208 => '革', + 12209 => '韋', + 12210 => '韭', + 12211 => '音', + 12212 => '頁', + 12213 => '風', + 12214 => '飛', + 12215 => '食', + 12216 => '首', + 12217 => '香', + 12218 => '馬', + 12219 => '骨', + 12220 => '高', + 12221 => '髟', + 12222 => '鬥', + 12223 => '鬯', + 12224 => '鬲', + 12225 => '鬼', + 12226 => '魚', + 12227 => '鳥', + 12228 => '鹵', + 12229 => '鹿', + 12230 => '麥', + 12231 => '麻', + 12232 => '黃', + 12233 => '黍', + 12234 => '黑', + 12235 => '黹', + 12236 => '黽', + 12237 => '鼎', + 12238 => '鼓', + 12239 => '鼠', + 12240 => '鼻', + 12241 => '齊', + 12242 => '齒', + 12243 => '龍', + 12244 => '龜', + 12245 => '龠', + 12290 => '.', + 12342 => '〒', + 12344 => '十', + 12345 => '卄', + 12346 => '卅', + 12447 => 'より', + 12543 => 'コト', + 12593 => 'ᄀ', + 12594 => 'ᄁ', + 12595 => 'ᆪ', + 12596 => 'ᄂ', + 12597 => 'ᆬ', + 12598 => 'ᆭ', + 12599 => 'ᄃ', + 12600 => 'ᄄ', + 12601 => 'ᄅ', + 12602 => 'ᆰ', + 12603 => 'ᆱ', + 12604 => 'ᆲ', + 12605 => 'ᆳ', + 12606 => 'ᆴ', + 12607 => 'ᆵ', + 12608 => 'ᄚ', + 12609 => 'ᄆ', + 12610 => 'ᄇ', + 12611 => 'ᄈ', + 12612 => 'ᄡ', + 12613 => 'ᄉ', + 12614 => 'ᄊ', + 12615 => 'ᄋ', + 12616 => 'ᄌ', + 12617 => 'ᄍ', + 12618 => 'ᄎ', + 12619 => 'ᄏ', + 12620 => 'ᄐ', + 12621 => 'ᄑ', + 12622 => 'ᄒ', + 12623 => 'ᅡ', + 12624 => 'ᅢ', + 12625 => 'ᅣ', + 12626 => 'ᅤ', + 12627 => 'ᅥ', + 12628 => 'ᅦ', + 12629 => 'ᅧ', + 12630 => 'ᅨ', + 12631 => 'ᅩ', + 12632 => 'ᅪ', + 12633 => 'ᅫ', + 12634 => 'ᅬ', + 12635 => 'ᅭ', + 12636 => 'ᅮ', + 12637 => 'ᅯ', + 12638 => 'ᅰ', + 12639 => 'ᅱ', + 12640 => 'ᅲ', + 12641 => 'ᅳ', + 12642 => 'ᅴ', + 12643 => 'ᅵ', + 12645 => 'ᄔ', + 12646 => 'ᄕ', + 12647 => 'ᇇ', + 12648 => 'ᇈ', + 12649 => 'ᇌ', + 12650 => 'ᇎ', + 12651 => 'ᇓ', + 12652 => 'ᇗ', + 12653 => 'ᇙ', + 12654 => 'ᄜ', + 12655 => 'ᇝ', + 12656 => 'ᇟ', + 12657 => 'ᄝ', + 12658 => 'ᄞ', + 12659 => 'ᄠ', + 12660 => 'ᄢ', + 12661 => 'ᄣ', + 12662 => 'ᄧ', + 12663 => 'ᄩ', + 12664 => 'ᄫ', + 12665 => 'ᄬ', + 12666 => 'ᄭ', + 12667 => 'ᄮ', + 12668 => 'ᄯ', + 12669 => 'ᄲ', + 12670 => 'ᄶ', + 12671 => 'ᅀ', + 12672 => 'ᅇ', + 12673 => 'ᅌ', + 12674 => 'ᇱ', + 12675 => 'ᇲ', + 12676 => 'ᅗ', + 12677 => 'ᅘ', + 12678 => 'ᅙ', + 12679 => 'ᆄ', + 12680 => 'ᆅ', + 12681 => 'ᆈ', + 12682 => 'ᆑ', + 12683 => 'ᆒ', + 12684 => 'ᆔ', + 12685 => 'ᆞ', + 12686 => 'ᆡ', + 12690 => '一', + 12691 => '二', + 12692 => '三', + 12693 => '四', + 12694 => '上', + 12695 => '中', + 12696 => '下', + 12697 => '甲', + 12698 => '乙', + 12699 => '丙', + 12700 => '丁', + 12701 => '天', + 12702 => '地', + 12703 => '人', + 12868 => '問', + 12869 => '幼', + 12870 => '文', + 12871 => '箏', + 12880 => 'pte', + 12881 => '21', + 12882 => '22', + 12883 => '23', + 12884 => '24', + 12885 => '25', + 12886 => '26', + 12887 => '27', + 12888 => '28', + 12889 => '29', + 12890 => '30', + 12891 => '31', + 12892 => '32', + 12893 => '33', + 12894 => '34', + 12895 => '35', + 12896 => 'ᄀ', + 12897 => 'ᄂ', + 12898 => 'ᄃ', + 12899 => 'ᄅ', + 12900 => 'ᄆ', + 12901 => 'ᄇ', + 12902 => 'ᄉ', + 12903 => 'ᄋ', + 12904 => 'ᄌ', + 12905 => 'ᄎ', + 12906 => 'ᄏ', + 12907 => 'ᄐ', + 12908 => 'ᄑ', + 12909 => 'ᄒ', + 12910 => '가', + 12911 => '나', + 12912 => '다', + 12913 => '라', + 12914 => '마', + 12915 => '바', + 12916 => '사', + 12917 => '아', + 12918 => '자', + 12919 => '차', + 12920 => '카', + 12921 => '타', + 12922 => '파', + 12923 => '하', + 12924 => '참고', + 12925 => '주의', + 12926 => '우', + 12928 => '一', + 12929 => '二', + 12930 => '三', + 12931 => '四', + 12932 => '五', + 12933 => '六', + 12934 => '七', + 12935 => '八', + 12936 => '九', + 12937 => '十', + 12938 => '月', + 12939 => '火', + 12940 => '水', + 12941 => '木', + 12942 => '金', + 12943 => '土', + 12944 => '日', + 12945 => '株', + 12946 => '有', + 12947 => '社', + 12948 => '名', + 12949 => '特', + 12950 => '財', + 12951 => '祝', + 12952 => '労', + 12953 => '秘', + 12954 => '男', + 12955 => '女', + 12956 => '適', + 12957 => '優', + 12958 => '印', + 12959 => '注', + 12960 => '項', + 12961 => '休', + 12962 => '写', + 12963 => '正', + 12964 => '上', + 12965 => '中', + 12966 => '下', + 12967 => '左', + 12968 => '右', + 12969 => '医', + 12970 => '宗', + 12971 => '学', + 12972 => '監', + 12973 => '企', + 12974 => '資', + 12975 => '協', + 12976 => '夜', + 12977 => '36', + 12978 => '37', + 12979 => '38', + 12980 => '39', + 12981 => '40', + 12982 => '41', + 12983 => '42', + 12984 => '43', + 12985 => '44', + 12986 => '45', + 12987 => '46', + 12988 => '47', + 12989 => '48', + 12990 => '49', + 12991 => '50', + 12992 => '1月', + 12993 => '2月', + 12994 => '3月', + 12995 => '4月', + 12996 => '5月', + 12997 => '6月', + 12998 => '7月', + 12999 => '8月', + 13000 => '9月', + 13001 => '10月', + 13002 => '11月', + 13003 => '12月', + 13004 => 'hg', + 13005 => 'erg', + 13006 => 'ev', + 13007 => 'ltd', + 13008 => 'ア', + 13009 => 'イ', + 13010 => 'ウ', + 13011 => 'エ', + 13012 => 'オ', + 13013 => 'カ', + 13014 => 'キ', + 13015 => 'ク', + 13016 => 'ケ', + 13017 => 'コ', + 13018 => 'サ', + 13019 => 'シ', + 13020 => 'ス', + 13021 => 'セ', + 13022 => 'ソ', + 13023 => 'タ', + 13024 => 'チ', + 13025 => 'ツ', + 13026 => 'テ', + 13027 => 'ト', + 13028 => 'ナ', + 13029 => 'ニ', + 13030 => 'ヌ', + 13031 => 'ネ', + 13032 => 'ノ', + 13033 => 'ハ', + 13034 => 'ヒ', + 13035 => 'フ', + 13036 => 'ヘ', + 13037 => 'ホ', + 13038 => 'マ', + 13039 => 'ミ', + 13040 => 'ム', + 13041 => 'メ', + 13042 => 'モ', + 13043 => 'ヤ', + 13044 => 'ユ', + 13045 => 'ヨ', + 13046 => 'ラ', + 13047 => 'リ', + 13048 => 'ル', + 13049 => 'レ', + 13050 => 'ロ', + 13051 => 'ワ', + 13052 => 'ヰ', + 13053 => 'ヱ', + 13054 => 'ヲ', + 13055 => '令和', + 13056 => 'アパート', + 13057 => 'アルファ', + 13058 => 'アンペア', + 13059 => 'アール', + 13060 => 'イニング', + 13061 => 'インチ', + 13062 => 'ウォン', + 13063 => 'エスクード', + 13064 => 'エーカー', + 13065 => 'オンス', + 13066 => 'オーム', + 13067 => 'カイリ', + 13068 => 'カラット', + 13069 => 'カロリー', + 13070 => 'ガロン', + 13071 => 'ガンマ', + 13072 => 'ギガ', + 13073 => 'ギニー', + 13074 => 'キュリー', + 13075 => 'ギルダー', + 13076 => 'キロ', + 13077 => 'キログラム', + 13078 => 'キロメートル', + 13079 => 'キロワット', + 13080 => 'グラム', + 13081 => 'グラムトン', + 13082 => 'クルゼイロ', + 13083 => 'クローネ', + 13084 => 'ケース', + 13085 => 'コルナ', + 13086 => 'コーポ', + 13087 => 'サイクル', + 13088 => 'サンチーム', + 13089 => 'シリング', + 13090 => 'センチ', + 13091 => 'セント', + 13092 => 'ダース', + 13093 => 'デシ', + 13094 => 'ドル', + 13095 => 'トン', + 13096 => 'ナノ', + 13097 => 'ノット', + 13098 => 'ハイツ', + 13099 => 'パーセント', + 13100 => 'パーツ', + 13101 => 'バーレル', + 13102 => 'ピアストル', + 13103 => 'ピクル', + 13104 => 'ピコ', + 13105 => 'ビル', + 13106 => 'ファラッド', + 13107 => 'フィート', + 13108 => 'ブッシェル', + 13109 => 'フラン', + 13110 => 'ヘクタール', + 13111 => 'ペソ', + 13112 => 'ペニヒ', + 13113 => 'ヘルツ', + 13114 => 'ペンス', + 13115 => 'ページ', + 13116 => 'ベータ', + 13117 => 'ポイント', + 13118 => 'ボルト', + 13119 => 'ホン', + 13120 => 'ポンド', + 13121 => 'ホール', + 13122 => 'ホーン', + 13123 => 'マイクロ', + 13124 => 'マイル', + 13125 => 'マッハ', + 13126 => 'マルク', + 13127 => 'マンション', + 13128 => 'ミクロン', + 13129 => 'ミリ', + 13130 => 'ミリバール', + 13131 => 'メガ', + 13132 => 'メガトン', + 13133 => 'メートル', + 13134 => 'ヤード', + 13135 => 'ヤール', + 13136 => 'ユアン', + 13137 => 'リットル', + 13138 => 'リラ', + 13139 => 'ルピー', + 13140 => 'ルーブル', + 13141 => 'レム', + 13142 => 'レントゲン', + 13143 => 'ワット', + 13144 => '0点', + 13145 => '1点', + 13146 => '2点', + 13147 => '3点', + 13148 => '4点', + 13149 => '5点', + 13150 => '6点', + 13151 => '7点', + 13152 => '8点', + 13153 => '9点', + 13154 => '10点', + 13155 => '11点', + 13156 => '12点', + 13157 => '13点', + 13158 => '14点', + 13159 => '15点', + 13160 => '16点', + 13161 => '17点', + 13162 => '18点', + 13163 => '19点', + 13164 => '20点', + 13165 => '21点', + 13166 => '22点', + 13167 => '23点', + 13168 => '24点', + 13169 => 'hpa', + 13170 => 'da', + 13171 => 'au', + 13172 => 'bar', + 13173 => 'ov', + 13174 => 'pc', + 13175 => 'dm', + 13176 => 'dm2', + 13177 => 'dm3', + 13178 => 'iu', + 13179 => '平成', + 13180 => '昭和', + 13181 => '大正', + 13182 => '明治', + 13183 => '株式会社', + 13184 => 'pa', + 13185 => 'na', + 13186 => 'μa', + 13187 => 'ma', + 13188 => 'ka', + 13189 => 'kb', + 13190 => 'mb', + 13191 => 'gb', + 13192 => 'cal', + 13193 => 'kcal', + 13194 => 'pf', + 13195 => 'nf', + 13196 => 'μf', + 13197 => 'μg', + 13198 => 'mg', + 13199 => 'kg', + 13200 => 'hz', + 13201 => 'khz', + 13202 => 'mhz', + 13203 => 'ghz', + 13204 => 'thz', + 13205 => 'μl', + 13206 => 'ml', + 13207 => 'dl', + 13208 => 'kl', + 13209 => 'fm', + 13210 => 'nm', + 13211 => 'μm', + 13212 => 'mm', + 13213 => 'cm', + 13214 => 'km', + 13215 => 'mm2', + 13216 => 'cm2', + 13217 => 'm2', + 13218 => 'km2', + 13219 => 'mm3', + 13220 => 'cm3', + 13221 => 'm3', + 13222 => 'km3', + 13223 => 'm∕s', + 13224 => 'm∕s2', + 13225 => 'pa', + 13226 => 'kpa', + 13227 => 'mpa', + 13228 => 'gpa', + 13229 => 'rad', + 13230 => 'rad∕s', + 13231 => 'rad∕s2', + 13232 => 'ps', + 13233 => 'ns', + 13234 => 'μs', + 13235 => 'ms', + 13236 => 'pv', + 13237 => 'nv', + 13238 => 'μv', + 13239 => 'mv', + 13240 => 'kv', + 13241 => 'mv', + 13242 => 'pw', + 13243 => 'nw', + 13244 => 'μw', + 13245 => 'mw', + 13246 => 'kw', + 13247 => 'mw', + 13248 => 'kω', + 13249 => 'mω', + 13251 => 'bq', + 13252 => 'cc', + 13253 => 'cd', + 13254 => 'c∕kg', + 13256 => 'db', + 13257 => 'gy', + 13258 => 'ha', + 13259 => 'hp', + 13260 => 'in', + 13261 => 'kk', + 13262 => 'km', + 13263 => 'kt', + 13264 => 'lm', + 13265 => 'ln', + 13266 => 'log', + 13267 => 'lx', + 13268 => 'mb', + 13269 => 'mil', + 13270 => 'mol', + 13271 => 'ph', + 13273 => 'ppm', + 13274 => 'pr', + 13275 => 'sr', + 13276 => 'sv', + 13277 => 'wb', + 13278 => 'v∕m', + 13279 => 'a∕m', + 13280 => '1日', + 13281 => '2日', + 13282 => '3日', + 13283 => '4日', + 13284 => '5日', + 13285 => '6日', + 13286 => '7日', + 13287 => '8日', + 13288 => '9日', + 13289 => '10日', + 13290 => '11日', + 13291 => '12日', + 13292 => '13日', + 13293 => '14日', + 13294 => '15日', + 13295 => '16日', + 13296 => '17日', + 13297 => '18日', + 13298 => '19日', + 13299 => '20日', + 13300 => '21日', + 13301 => '22日', + 13302 => '23日', + 13303 => '24日', + 13304 => '25日', + 13305 => '26日', + 13306 => '27日', + 13307 => '28日', + 13308 => '29日', + 13309 => '30日', + 13310 => '31日', + 13311 => 'gal', + 42560 => 'ꙁ', + 42562 => 'ꙃ', + 42564 => 'ꙅ', + 42566 => 'ꙇ', + 42568 => 'ꙉ', + 42570 => 'ꙋ', + 42572 => 'ꙍ', + 42574 => 'ꙏ', + 42576 => 'ꙑ', + 42578 => 'ꙓ', + 42580 => 'ꙕ', + 42582 => 'ꙗ', + 42584 => 'ꙙ', + 42586 => 'ꙛ', + 42588 => 'ꙝ', + 42590 => 'ꙟ', + 42592 => 'ꙡ', + 42594 => 'ꙣ', + 42596 => 'ꙥ', + 42598 => 'ꙧ', + 42600 => 'ꙩ', + 42602 => 'ꙫ', + 42604 => 'ꙭ', + 42624 => 'ꚁ', + 42626 => 'ꚃ', + 42628 => 'ꚅ', + 42630 => 'ꚇ', + 42632 => 'ꚉ', + 42634 => 'ꚋ', + 42636 => 'ꚍ', + 42638 => 'ꚏ', + 42640 => 'ꚑ', + 42642 => 'ꚓ', + 42644 => 'ꚕ', + 42646 => 'ꚗ', + 42648 => 'ꚙ', + 42650 => 'ꚛ', + 42652 => 'ъ', + 42653 => 'ь', + 42786 => 'ꜣ', + 42788 => 'ꜥ', + 42790 => 'ꜧ', + 42792 => 'ꜩ', + 42794 => 'ꜫ', + 42796 => 'ꜭ', + 42798 => 'ꜯ', + 42802 => 'ꜳ', + 42804 => 'ꜵ', + 42806 => 'ꜷ', + 42808 => 'ꜹ', + 42810 => 'ꜻ', + 42812 => 'ꜽ', + 42814 => 'ꜿ', + 42816 => 'ꝁ', + 42818 => 'ꝃ', + 42820 => 'ꝅ', + 42822 => 'ꝇ', + 42824 => 'ꝉ', + 42826 => 'ꝋ', + 42828 => 'ꝍ', + 42830 => 'ꝏ', + 42832 => 'ꝑ', + 42834 => 'ꝓ', + 42836 => 'ꝕ', + 42838 => 'ꝗ', + 42840 => 'ꝙ', + 42842 => 'ꝛ', + 42844 => 'ꝝ', + 42846 => 'ꝟ', + 42848 => 'ꝡ', + 42850 => 'ꝣ', + 42852 => 'ꝥ', + 42854 => 'ꝧ', + 42856 => 'ꝩ', + 42858 => 'ꝫ', + 42860 => 'ꝭ', + 42862 => 'ꝯ', + 42864 => 'ꝯ', + 42873 => 'ꝺ', + 42875 => 'ꝼ', + 42877 => 'ᵹ', + 42878 => 'ꝿ', + 42880 => 'ꞁ', + 42882 => 'ꞃ', + 42884 => 'ꞅ', + 42886 => 'ꞇ', + 42891 => 'ꞌ', + 42893 => 'ɥ', + 42896 => 'ꞑ', + 42898 => 'ꞓ', + 42902 => 'ꞗ', + 42904 => 'ꞙ', + 42906 => 'ꞛ', + 42908 => 'ꞝ', + 42910 => 'ꞟ', + 42912 => 'ꞡ', + 42914 => 'ꞣ', + 42916 => 'ꞥ', + 42918 => 'ꞧ', + 42920 => 'ꞩ', + 42922 => 'ɦ', + 42923 => 'ɜ', + 42924 => 'ɡ', + 42925 => 'ɬ', + 42926 => 'ɪ', + 42928 => 'ʞ', + 42929 => 'ʇ', + 42930 => 'ʝ', + 42931 => 'ꭓ', + 42932 => 'ꞵ', + 42934 => 'ꞷ', + 42936 => 'ꞹ', + 42938 => 'ꞻ', + 42940 => 'ꞽ', + 42942 => 'ꞿ', + 42946 => 'ꟃ', + 42948 => 'ꞔ', + 42949 => 'ʂ', + 42950 => 'ᶎ', + 42951 => 'ꟈ', + 42953 => 'ꟊ', + 42997 => 'ꟶ', + 43000 => 'ħ', + 43001 => 'œ', + 43868 => 'ꜧ', + 43869 => 'ꬷ', + 43870 => 'ɫ', + 43871 => 'ꭒ', + 43881 => 'ʍ', + 43888 => 'Ꭰ', + 43889 => 'Ꭱ', + 43890 => 'Ꭲ', + 43891 => 'Ꭳ', + 43892 => 'Ꭴ', + 43893 => 'Ꭵ', + 43894 => 'Ꭶ', + 43895 => 'Ꭷ', + 43896 => 'Ꭸ', + 43897 => 'Ꭹ', + 43898 => 'Ꭺ', + 43899 => 'Ꭻ', + 43900 => 'Ꭼ', + 43901 => 'Ꭽ', + 43902 => 'Ꭾ', + 43903 => 'Ꭿ', + 43904 => 'Ꮀ', + 43905 => 'Ꮁ', + 43906 => 'Ꮂ', + 43907 => 'Ꮃ', + 43908 => 'Ꮄ', + 43909 => 'Ꮅ', + 43910 => 'Ꮆ', + 43911 => 'Ꮇ', + 43912 => 'Ꮈ', + 43913 => 'Ꮉ', + 43914 => 'Ꮊ', + 43915 => 'Ꮋ', + 43916 => 'Ꮌ', + 43917 => 'Ꮍ', + 43918 => 'Ꮎ', + 43919 => 'Ꮏ', + 43920 => 'Ꮐ', + 43921 => 'Ꮑ', + 43922 => 'Ꮒ', + 43923 => 'Ꮓ', + 43924 => 'Ꮔ', + 43925 => 'Ꮕ', + 43926 => 'Ꮖ', + 43927 => 'Ꮗ', + 43928 => 'Ꮘ', + 43929 => 'Ꮙ', + 43930 => 'Ꮚ', + 43931 => 'Ꮛ', + 43932 => 'Ꮜ', + 43933 => 'Ꮝ', + 43934 => 'Ꮞ', + 43935 => 'Ꮟ', + 43936 => 'Ꮠ', + 43937 => 'Ꮡ', + 43938 => 'Ꮢ', + 43939 => 'Ꮣ', + 43940 => 'Ꮤ', + 43941 => 'Ꮥ', + 43942 => 'Ꮦ', + 43943 => 'Ꮧ', + 43944 => 'Ꮨ', + 43945 => 'Ꮩ', + 43946 => 'Ꮪ', + 43947 => 'Ꮫ', + 43948 => 'Ꮬ', + 43949 => 'Ꮭ', + 43950 => 'Ꮮ', + 43951 => 'Ꮯ', + 43952 => 'Ꮰ', + 43953 => 'Ꮱ', + 43954 => 'Ꮲ', + 43955 => 'Ꮳ', + 43956 => 'Ꮴ', + 43957 => 'Ꮵ', + 43958 => 'Ꮶ', + 43959 => 'Ꮷ', + 43960 => 'Ꮸ', + 43961 => 'Ꮹ', + 43962 => 'Ꮺ', + 43963 => 'Ꮻ', + 43964 => 'Ꮼ', + 43965 => 'Ꮽ', + 43966 => 'Ꮾ', + 43967 => 'Ꮿ', + 63744 => '豈', + 63745 => '更', + 63746 => '車', + 63747 => '賈', + 63748 => '滑', + 63749 => '串', + 63750 => '句', + 63751 => '龜', + 63752 => '龜', + 63753 => '契', + 63754 => '金', + 63755 => '喇', + 63756 => '奈', + 63757 => '懶', + 63758 => '癩', + 63759 => '羅', + 63760 => '蘿', + 63761 => '螺', + 63762 => '裸', + 63763 => '邏', + 63764 => '樂', + 63765 => '洛', + 63766 => '烙', + 63767 => '珞', + 63768 => '落', + 63769 => '酪', + 63770 => '駱', + 63771 => '亂', + 63772 => '卵', + 63773 => '欄', + 63774 => '爛', + 63775 => '蘭', + 63776 => '鸞', + 63777 => '嵐', + 63778 => '濫', + 63779 => '藍', + 63780 => '襤', + 63781 => '拉', + 63782 => '臘', + 63783 => '蠟', + 63784 => '廊', + 63785 => '朗', + 63786 => '浪', + 63787 => '狼', + 63788 => '郎', + 63789 => '來', + 63790 => '冷', + 63791 => '勞', + 63792 => '擄', + 63793 => '櫓', + 63794 => '爐', + 63795 => '盧', + 63796 => '老', + 63797 => '蘆', + 63798 => '虜', + 63799 => '路', + 63800 => '露', + 63801 => '魯', + 63802 => '鷺', + 63803 => '碌', + 63804 => '祿', + 63805 => '綠', + 63806 => '菉', + 63807 => '錄', + 63808 => '鹿', + 63809 => '論', + 63810 => '壟', + 63811 => '弄', + 63812 => '籠', + 63813 => '聾', + 63814 => '牢', + 63815 => '磊', + 63816 => '賂', + 63817 => '雷', + 63818 => '壘', + 63819 => '屢', + 63820 => '樓', + 63821 => '淚', + 63822 => '漏', + 63823 => '累', + 63824 => '縷', + 63825 => '陋', + 63826 => '勒', + 63827 => '肋', + 63828 => '凜', + 63829 => '凌', + 63830 => '稜', + 63831 => '綾', + 63832 => '菱', + 63833 => '陵', + 63834 => '讀', + 63835 => '拏', + 63836 => '樂', + 63837 => '諾', + 63838 => '丹', + 63839 => '寧', + 63840 => '怒', + 63841 => '率', + 63842 => '異', + 63843 => '北', + 63844 => '磻', + 63845 => '便', + 63846 => '復', + 63847 => '不', + 63848 => '泌', + 63849 => '數', + 63850 => '索', + 63851 => '參', + 63852 => '塞', + 63853 => '省', + 63854 => '葉', + 63855 => '說', + 63856 => '殺', + 63857 => '辰', + 63858 => '沈', + 63859 => '拾', + 63860 => '若', + 63861 => '掠', + 63862 => '略', + 63863 => '亮', + 63864 => '兩', + 63865 => '凉', + 63866 => '梁', + 63867 => '糧', + 63868 => '良', + 63869 => '諒', + 63870 => '量', + 63871 => '勵', + 63872 => '呂', + 63873 => '女', + 63874 => '廬', + 63875 => '旅', + 63876 => '濾', + 63877 => '礪', + 63878 => '閭', + 63879 => '驪', + 63880 => '麗', + 63881 => '黎', + 63882 => '力', + 63883 => '曆', + 63884 => '歷', + 63885 => '轢', + 63886 => '年', + 63887 => '憐', + 63888 => '戀', + 63889 => '撚', + 63890 => '漣', + 63891 => '煉', + 63892 => '璉', + 63893 => '秊', + 63894 => '練', + 63895 => '聯', + 63896 => '輦', + 63897 => '蓮', + 63898 => '連', + 63899 => '鍊', + 63900 => '列', + 63901 => '劣', + 63902 => '咽', + 63903 => '烈', + 63904 => '裂', + 63905 => '說', + 63906 => '廉', + 63907 => '念', + 63908 => '捻', + 63909 => '殮', + 63910 => '簾', + 63911 => '獵', + 63912 => '令', + 63913 => '囹', + 63914 => '寧', + 63915 => '嶺', + 63916 => '怜', + 63917 => '玲', + 63918 => '瑩', + 63919 => '羚', + 63920 => '聆', + 63921 => '鈴', + 63922 => '零', + 63923 => '靈', + 63924 => '領', + 63925 => '例', + 63926 => '禮', + 63927 => '醴', + 63928 => '隸', + 63929 => '惡', + 63930 => '了', + 63931 => '僚', + 63932 => '寮', + 63933 => '尿', + 63934 => '料', + 63935 => '樂', + 63936 => '燎', + 63937 => '療', + 63938 => '蓼', + 63939 => '遼', + 63940 => '龍', + 63941 => '暈', + 63942 => '阮', + 63943 => '劉', + 63944 => '杻', + 63945 => '柳', + 63946 => '流', + 63947 => '溜', + 63948 => '琉', + 63949 => '留', + 63950 => '硫', + 63951 => '紐', + 63952 => '類', + 63953 => '六', + 63954 => '戮', + 63955 => '陸', + 63956 => '倫', + 63957 => '崙', + 63958 => '淪', + 63959 => '輪', + 63960 => '律', + 63961 => '慄', + 63962 => '栗', + 63963 => '率', + 63964 => '隆', + 63965 => '利', + 63966 => '吏', + 63967 => '履', + 63968 => '易', + 63969 => '李', + 63970 => '梨', + 63971 => '泥', + 63972 => '理', + 63973 => '痢', + 63974 => '罹', + 63975 => '裏', + 63976 => '裡', + 63977 => '里', + 63978 => '離', + 63979 => '匿', + 63980 => '溺', + 63981 => '吝', + 63982 => '燐', + 63983 => '璘', + 63984 => '藺', + 63985 => '隣', + 63986 => '鱗', + 63987 => '麟', + 63988 => '林', + 63989 => '淋', + 63990 => '臨', + 63991 => '立', + 63992 => '笠', + 63993 => '粒', + 63994 => '狀', + 63995 => '炙', + 63996 => '識', + 63997 => '什', + 63998 => '茶', + 63999 => '刺', + 64000 => '切', + 64001 => '度', + 64002 => '拓', + 64003 => '糖', + 64004 => '宅', + 64005 => '洞', + 64006 => '暴', + 64007 => '輻', + 64008 => '行', + 64009 => '降', + 64010 => '見', + 64011 => '廓', + 64012 => '兀', + 64013 => '嗀', + 64016 => '塚', + 64018 => '晴', + 64021 => '凞', + 64022 => '猪', + 64023 => '益', + 64024 => '礼', + 64025 => '神', + 64026 => '祥', + 64027 => '福', + 64028 => '靖', + 64029 => '精', + 64030 => '羽', + 64032 => '蘒', + 64034 => '諸', + 64037 => '逸', + 64038 => '都', + 64042 => '飯', + 64043 => '飼', + 64044 => '館', + 64045 => '鶴', + 64046 => '郞', + 64047 => '隷', + 64048 => '侮', + 64049 => '僧', + 64050 => '免', + 64051 => '勉', + 64052 => '勤', + 64053 => '卑', + 64054 => '喝', + 64055 => '嘆', + 64056 => '器', + 64057 => '塀', + 64058 => '墨', + 64059 => '層', + 64060 => '屮', + 64061 => '悔', + 64062 => '慨', + 64063 => '憎', + 64064 => '懲', + 64065 => '敏', + 64066 => '既', + 64067 => '暑', + 64068 => '梅', + 64069 => '海', + 64070 => '渚', + 64071 => '漢', + 64072 => '煮', + 64073 => '爫', + 64074 => '琢', + 64075 => '碑', + 64076 => '社', + 64077 => '祉', + 64078 => '祈', + 64079 => '祐', + 64080 => '祖', + 64081 => '祝', + 64082 => '禍', + 64083 => '禎', + 64084 => '穀', + 64085 => '突', + 64086 => '節', + 64087 => '練', + 64088 => '縉', + 64089 => '繁', + 64090 => '署', + 64091 => '者', + 64092 => '臭', + 64093 => '艹', + 64094 => '艹', + 64095 => '著', + 64096 => '褐', + 64097 => '視', + 64098 => '謁', + 64099 => '謹', + 64100 => '賓', + 64101 => '贈', + 64102 => '辶', + 64103 => '逸', + 64104 => '難', + 64105 => '響', + 64106 => '頻', + 64107 => '恵', + 64108 => '𤋮', + 64109 => '舘', + 64112 => '並', + 64113 => '况', + 64114 => '全', + 64115 => '侀', + 64116 => '充', + 64117 => '冀', + 64118 => '勇', + 64119 => '勺', + 64120 => '喝', + 64121 => '啕', + 64122 => '喙', + 64123 => '嗢', + 64124 => '塚', + 64125 => '墳', + 64126 => '奄', + 64127 => '奔', + 64128 => '婢', + 64129 => '嬨', + 64130 => '廒', + 64131 => '廙', + 64132 => '彩', + 64133 => '徭', + 64134 => '惘', + 64135 => '慎', + 64136 => '愈', + 64137 => '憎', + 64138 => '慠', + 64139 => '懲', + 64140 => '戴', + 64141 => '揄', + 64142 => '搜', + 64143 => '摒', + 64144 => '敖', + 64145 => '晴', + 64146 => '朗', + 64147 => '望', + 64148 => '杖', + 64149 => '歹', + 64150 => '殺', + 64151 => '流', + 64152 => '滛', + 64153 => '滋', + 64154 => '漢', + 64155 => '瀞', + 64156 => '煮', + 64157 => '瞧', + 64158 => '爵', + 64159 => '犯', + 64160 => '猪', + 64161 => '瑱', + 64162 => '甆', + 64163 => '画', + 64164 => '瘝', + 64165 => '瘟', + 64166 => '益', + 64167 => '盛', + 64168 => '直', + 64169 => '睊', + 64170 => '着', + 64171 => '磌', + 64172 => '窱', + 64173 => '節', + 64174 => '类', + 64175 => '絛', + 64176 => '練', + 64177 => '缾', + 64178 => '者', + 64179 => '荒', + 64180 => '華', + 64181 => '蝹', + 64182 => '襁', + 64183 => '覆', + 64184 => '視', + 64185 => '調', + 64186 => '諸', + 64187 => '請', + 64188 => '謁', + 64189 => '諾', + 64190 => '諭', + 64191 => '謹', + 64192 => '變', + 64193 => '贈', + 64194 => '輸', + 64195 => '遲', + 64196 => '醙', + 64197 => '鉶', + 64198 => '陼', + 64199 => '難', + 64200 => '靖', + 64201 => '韛', + 64202 => '響', + 64203 => '頋', + 64204 => '頻', + 64205 => '鬒', + 64206 => '龜', + 64207 => '𢡊', + 64208 => '𢡄', + 64209 => '𣏕', + 64210 => '㮝', + 64211 => '䀘', + 64212 => '䀹', + 64213 => '𥉉', + 64214 => '𥳐', + 64215 => '𧻓', + 64216 => '齃', + 64217 => '龎', + 64256 => 'ff', + 64257 => 'fi', + 64258 => 'fl', + 64259 => 'ffi', + 64260 => 'ffl', + 64261 => 'st', + 64262 => 'st', + 64275 => 'մն', + 64276 => 'մե', + 64277 => 'մի', + 64278 => 'վն', + 64279 => 'մխ', + 64285 => 'יִ', + 64287 => 'ײַ', + 64288 => 'ע', + 64289 => 'א', + 64290 => 'ד', + 64291 => 'ה', + 64292 => 'כ', + 64293 => 'ל', + 64294 => 'ם', + 64295 => 'ר', + 64296 => 'ת', + 64298 => 'שׁ', + 64299 => 'שׂ', + 64300 => 'שּׁ', + 64301 => 'שּׂ', + 64302 => 'אַ', + 64303 => 'אָ', + 64304 => 'אּ', + 64305 => 'בּ', + 64306 => 'גּ', + 64307 => 'דּ', + 64308 => 'הּ', + 64309 => 'וּ', + 64310 => 'זּ', + 64312 => 'טּ', + 64313 => 'יּ', + 64314 => 'ךּ', + 64315 => 'כּ', + 64316 => 'לּ', + 64318 => 'מּ', + 64320 => 'נּ', + 64321 => 'סּ', + 64323 => 'ףּ', + 64324 => 'פּ', + 64326 => 'צּ', + 64327 => 'קּ', + 64328 => 'רּ', + 64329 => 'שּ', + 64330 => 'תּ', + 64331 => 'וֹ', + 64332 => 'בֿ', + 64333 => 'כֿ', + 64334 => 'פֿ', + 64335 => 'אל', + 64336 => 'ٱ', + 64337 => 'ٱ', + 64338 => 'ٻ', + 64339 => 'ٻ', + 64340 => 'ٻ', + 64341 => 'ٻ', + 64342 => 'پ', + 64343 => 'پ', + 64344 => 'پ', + 64345 => 'پ', + 64346 => 'ڀ', + 64347 => 'ڀ', + 64348 => 'ڀ', + 64349 => 'ڀ', + 64350 => 'ٺ', + 64351 => 'ٺ', + 64352 => 'ٺ', + 64353 => 'ٺ', + 64354 => 'ٿ', + 64355 => 'ٿ', + 64356 => 'ٿ', + 64357 => 'ٿ', + 64358 => 'ٹ', + 64359 => 'ٹ', + 64360 => 'ٹ', + 64361 => 'ٹ', + 64362 => 'ڤ', + 64363 => 'ڤ', + 64364 => 'ڤ', + 64365 => 'ڤ', + 64366 => 'ڦ', + 64367 => 'ڦ', + 64368 => 'ڦ', + 64369 => 'ڦ', + 64370 => 'ڄ', + 64371 => 'ڄ', + 64372 => 'ڄ', + 64373 => 'ڄ', + 64374 => 'ڃ', + 64375 => 'ڃ', + 64376 => 'ڃ', + 64377 => 'ڃ', + 64378 => 'چ', + 64379 => 'چ', + 64380 => 'چ', + 64381 => 'چ', + 64382 => 'ڇ', + 64383 => 'ڇ', + 64384 => 'ڇ', + 64385 => 'ڇ', + 64386 => 'ڍ', + 64387 => 'ڍ', + 64388 => 'ڌ', + 64389 => 'ڌ', + 64390 => 'ڎ', + 64391 => 'ڎ', + 64392 => 'ڈ', + 64393 => 'ڈ', + 64394 => 'ژ', + 64395 => 'ژ', + 64396 => 'ڑ', + 64397 => 'ڑ', + 64398 => 'ک', + 64399 => 'ک', + 64400 => 'ک', + 64401 => 'ک', + 64402 => 'گ', + 64403 => 'گ', + 64404 => 'گ', + 64405 => 'گ', + 64406 => 'ڳ', + 64407 => 'ڳ', + 64408 => 'ڳ', + 64409 => 'ڳ', + 64410 => 'ڱ', + 64411 => 'ڱ', + 64412 => 'ڱ', + 64413 => 'ڱ', + 64414 => 'ں', + 64415 => 'ں', + 64416 => 'ڻ', + 64417 => 'ڻ', + 64418 => 'ڻ', + 64419 => 'ڻ', + 64420 => 'ۀ', + 64421 => 'ۀ', + 64422 => 'ہ', + 64423 => 'ہ', + 64424 => 'ہ', + 64425 => 'ہ', + 64426 => 'ھ', + 64427 => 'ھ', + 64428 => 'ھ', + 64429 => 'ھ', + 64430 => 'ے', + 64431 => 'ے', + 64432 => 'ۓ', + 64433 => 'ۓ', + 64467 => 'ڭ', + 64468 => 'ڭ', + 64469 => 'ڭ', + 64470 => 'ڭ', + 64471 => 'ۇ', + 64472 => 'ۇ', + 64473 => 'ۆ', + 64474 => 'ۆ', + 64475 => 'ۈ', + 64476 => 'ۈ', + 64477 => 'ۇٴ', + 64478 => 'ۋ', + 64479 => 'ۋ', + 64480 => 'ۅ', + 64481 => 'ۅ', + 64482 => 'ۉ', + 64483 => 'ۉ', + 64484 => 'ې', + 64485 => 'ې', + 64486 => 'ې', + 64487 => 'ې', + 64488 => 'ى', + 64489 => 'ى', + 64490 => 'ئا', + 64491 => 'ئا', + 64492 => 'ئە', + 64493 => 'ئە', + 64494 => 'ئو', + 64495 => 'ئو', + 64496 => 'ئۇ', + 64497 => 'ئۇ', + 64498 => 'ئۆ', + 64499 => 'ئۆ', + 64500 => 'ئۈ', + 64501 => 'ئۈ', + 64502 => 'ئې', + 64503 => 'ئې', + 64504 => 'ئې', + 64505 => 'ئى', + 64506 => 'ئى', + 64507 => 'ئى', + 64508 => 'ی', + 64509 => 'ی', + 64510 => 'ی', + 64511 => 'ی', + 64512 => 'ئج', + 64513 => 'ئح', + 64514 => 'ئم', + 64515 => 'ئى', + 64516 => 'ئي', + 64517 => 'بج', + 64518 => 'بح', + 64519 => 'بخ', + 64520 => 'بم', + 64521 => 'بى', + 64522 => 'بي', + 64523 => 'تج', + 64524 => 'تح', + 64525 => 'تخ', + 64526 => 'تم', + 64527 => 'تى', + 64528 => 'تي', + 64529 => 'ثج', + 64530 => 'ثم', + 64531 => 'ثى', + 64532 => 'ثي', + 64533 => 'جح', + 64534 => 'جم', + 64535 => 'حج', + 64536 => 'حم', + 64537 => 'خج', + 64538 => 'خح', + 64539 => 'خم', + 64540 => 'سج', + 64541 => 'سح', + 64542 => 'سخ', + 64543 => 'سم', + 64544 => 'صح', + 64545 => 'صم', + 64546 => 'ضج', + 64547 => 'ضح', + 64548 => 'ضخ', + 64549 => 'ضم', + 64550 => 'طح', + 64551 => 'طم', + 64552 => 'ظم', + 64553 => 'عج', + 64554 => 'عم', + 64555 => 'غج', + 64556 => 'غم', + 64557 => 'فج', + 64558 => 'فح', + 64559 => 'فخ', + 64560 => 'فم', + 64561 => 'فى', + 64562 => 'في', + 64563 => 'قح', + 64564 => 'قم', + 64565 => 'قى', + 64566 => 'قي', + 64567 => 'كا', + 64568 => 'كج', + 64569 => 'كح', + 64570 => 'كخ', + 64571 => 'كل', + 64572 => 'كم', + 64573 => 'كى', + 64574 => 'كي', + 64575 => 'لج', + 64576 => 'لح', + 64577 => 'لخ', + 64578 => 'لم', + 64579 => 'لى', + 64580 => 'لي', + 64581 => 'مج', + 64582 => 'مح', + 64583 => 'مخ', + 64584 => 'مم', + 64585 => 'مى', + 64586 => 'مي', + 64587 => 'نج', + 64588 => 'نح', + 64589 => 'نخ', + 64590 => 'نم', + 64591 => 'نى', + 64592 => 'ني', + 64593 => 'هج', + 64594 => 'هم', + 64595 => 'هى', + 64596 => 'هي', + 64597 => 'يج', + 64598 => 'يح', + 64599 => 'يخ', + 64600 => 'يم', + 64601 => 'يى', + 64602 => 'يي', + 64603 => 'ذٰ', + 64604 => 'رٰ', + 64605 => 'ىٰ', + 64612 => 'ئر', + 64613 => 'ئز', + 64614 => 'ئم', + 64615 => 'ئن', + 64616 => 'ئى', + 64617 => 'ئي', + 64618 => 'بر', + 64619 => 'بز', + 64620 => 'بم', + 64621 => 'بن', + 64622 => 'بى', + 64623 => 'بي', + 64624 => 'تر', + 64625 => 'تز', + 64626 => 'تم', + 64627 => 'تن', + 64628 => 'تى', + 64629 => 'تي', + 64630 => 'ثر', + 64631 => 'ثز', + 64632 => 'ثم', + 64633 => 'ثن', + 64634 => 'ثى', + 64635 => 'ثي', + 64636 => 'فى', + 64637 => 'في', + 64638 => 'قى', + 64639 => 'قي', + 64640 => 'كا', + 64641 => 'كل', + 64642 => 'كم', + 64643 => 'كى', + 64644 => 'كي', + 64645 => 'لم', + 64646 => 'لى', + 64647 => 'لي', + 64648 => 'ما', + 64649 => 'مم', + 64650 => 'نر', + 64651 => 'نز', + 64652 => 'نم', + 64653 => 'نن', + 64654 => 'نى', + 64655 => 'ني', + 64656 => 'ىٰ', + 64657 => 'ير', + 64658 => 'يز', + 64659 => 'يم', + 64660 => 'ين', + 64661 => 'يى', + 64662 => 'يي', + 64663 => 'ئج', + 64664 => 'ئح', + 64665 => 'ئخ', + 64666 => 'ئم', + 64667 => 'ئه', + 64668 => 'بج', + 64669 => 'بح', + 64670 => 'بخ', + 64671 => 'بم', + 64672 => 'به', + 64673 => 'تج', + 64674 => 'تح', + 64675 => 'تخ', + 64676 => 'تم', + 64677 => 'ته', + 64678 => 'ثم', + 64679 => 'جح', + 64680 => 'جم', + 64681 => 'حج', + 64682 => 'حم', + 64683 => 'خج', + 64684 => 'خم', + 64685 => 'سج', + 64686 => 'سح', + 64687 => 'سخ', + 64688 => 'سم', + 64689 => 'صح', + 64690 => 'صخ', + 64691 => 'صم', + 64692 => 'ضج', + 64693 => 'ضح', + 64694 => 'ضخ', + 64695 => 'ضم', + 64696 => 'طح', + 64697 => 'ظم', + 64698 => 'عج', + 64699 => 'عم', + 64700 => 'غج', + 64701 => 'غم', + 64702 => 'فج', + 64703 => 'فح', + 64704 => 'فخ', + 64705 => 'فم', + 64706 => 'قح', + 64707 => 'قم', + 64708 => 'كج', + 64709 => 'كح', + 64710 => 'كخ', + 64711 => 'كل', + 64712 => 'كم', + 64713 => 'لج', + 64714 => 'لح', + 64715 => 'لخ', + 64716 => 'لم', + 64717 => 'له', + 64718 => 'مج', + 64719 => 'مح', + 64720 => 'مخ', + 64721 => 'مم', + 64722 => 'نج', + 64723 => 'نح', + 64724 => 'نخ', + 64725 => 'نم', + 64726 => 'نه', + 64727 => 'هج', + 64728 => 'هم', + 64729 => 'هٰ', + 64730 => 'يج', + 64731 => 'يح', + 64732 => 'يخ', + 64733 => 'يم', + 64734 => 'يه', + 64735 => 'ئم', + 64736 => 'ئه', + 64737 => 'بم', + 64738 => 'به', + 64739 => 'تم', + 64740 => 'ته', + 64741 => 'ثم', + 64742 => 'ثه', + 64743 => 'سم', + 64744 => 'سه', + 64745 => 'شم', + 64746 => 'شه', + 64747 => 'كل', + 64748 => 'كم', + 64749 => 'لم', + 64750 => 'نم', + 64751 => 'نه', + 64752 => 'يم', + 64753 => 'يه', + 64754 => 'ـَّ', + 64755 => 'ـُّ', + 64756 => 'ـِّ', + 64757 => 'طى', + 64758 => 'طي', + 64759 => 'عى', + 64760 => 'عي', + 64761 => 'غى', + 64762 => 'غي', + 64763 => 'سى', + 64764 => 'سي', + 64765 => 'شى', + 64766 => 'شي', + 64767 => 'حى', + 64768 => 'حي', + 64769 => 'جى', + 64770 => 'جي', + 64771 => 'خى', + 64772 => 'خي', + 64773 => 'صى', + 64774 => 'صي', + 64775 => 'ضى', + 64776 => 'ضي', + 64777 => 'شج', + 64778 => 'شح', + 64779 => 'شخ', + 64780 => 'شم', + 64781 => 'شر', + 64782 => 'سر', + 64783 => 'صر', + 64784 => 'ضر', + 64785 => 'طى', + 64786 => 'طي', + 64787 => 'عى', + 64788 => 'عي', + 64789 => 'غى', + 64790 => 'غي', + 64791 => 'سى', + 64792 => 'سي', + 64793 => 'شى', + 64794 => 'شي', + 64795 => 'حى', + 64796 => 'حي', + 64797 => 'جى', + 64798 => 'جي', + 64799 => 'خى', + 64800 => 'خي', + 64801 => 'صى', + 64802 => 'صي', + 64803 => 'ضى', + 64804 => 'ضي', + 64805 => 'شج', + 64806 => 'شح', + 64807 => 'شخ', + 64808 => 'شم', + 64809 => 'شر', + 64810 => 'سر', + 64811 => 'صر', + 64812 => 'ضر', + 64813 => 'شج', + 64814 => 'شح', + 64815 => 'شخ', + 64816 => 'شم', + 64817 => 'سه', + 64818 => 'شه', + 64819 => 'طم', + 64820 => 'سج', + 64821 => 'سح', + 64822 => 'سخ', + 64823 => 'شج', + 64824 => 'شح', + 64825 => 'شخ', + 64826 => 'طم', + 64827 => 'ظم', + 64828 => 'اً', + 64829 => 'اً', + 64848 => 'تجم', + 64849 => 'تحج', + 64850 => 'تحج', + 64851 => 'تحم', + 64852 => 'تخم', + 64853 => 'تمج', + 64854 => 'تمح', + 64855 => 'تمخ', + 64856 => 'جمح', + 64857 => 'جمح', + 64858 => 'حمي', + 64859 => 'حمى', + 64860 => 'سحج', + 64861 => 'سجح', + 64862 => 'سجى', + 64863 => 'سمح', + 64864 => 'سمح', + 64865 => 'سمج', + 64866 => 'سمم', + 64867 => 'سمم', + 64868 => 'صحح', + 64869 => 'صحح', + 64870 => 'صمم', + 64871 => 'شحم', + 64872 => 'شحم', + 64873 => 'شجي', + 64874 => 'شمخ', + 64875 => 'شمخ', + 64876 => 'شمم', + 64877 => 'شمم', + 64878 => 'ضحى', + 64879 => 'ضخم', + 64880 => 'ضخم', + 64881 => 'طمح', + 64882 => 'طمح', + 64883 => 'طمم', + 64884 => 'طمي', + 64885 => 'عجم', + 64886 => 'عمم', + 64887 => 'عمم', + 64888 => 'عمى', + 64889 => 'غمم', + 64890 => 'غمي', + 64891 => 'غمى', + 64892 => 'فخم', + 64893 => 'فخم', + 64894 => 'قمح', + 64895 => 'قمم', + 64896 => 'لحم', + 64897 => 'لحي', + 64898 => 'لحى', + 64899 => 'لجج', + 64900 => 'لجج', + 64901 => 'لخم', + 64902 => 'لخم', + 64903 => 'لمح', + 64904 => 'لمح', + 64905 => 'محج', + 64906 => 'محم', + 64907 => 'محي', + 64908 => 'مجح', + 64909 => 'مجم', + 64910 => 'مخج', + 64911 => 'مخم', + 64914 => 'مجخ', + 64915 => 'همج', + 64916 => 'همم', + 64917 => 'نحم', + 64918 => 'نحى', + 64919 => 'نجم', + 64920 => 'نجم', + 64921 => 'نجى', + 64922 => 'نمي', + 64923 => 'نمى', + 64924 => 'يمم', + 64925 => 'يمم', + 64926 => 'بخي', + 64927 => 'تجي', + 64928 => 'تجى', + 64929 => 'تخي', + 64930 => 'تخى', + 64931 => 'تمي', + 64932 => 'تمى', + 64933 => 'جمي', + 64934 => 'جحى', + 64935 => 'جمى', + 64936 => 'سخى', + 64937 => 'صحي', + 64938 => 'شحي', + 64939 => 'ضحي', + 64940 => 'لجي', + 64941 => 'لمي', + 64942 => 'يحي', + 64943 => 'يجي', + 64944 => 'يمي', + 64945 => 'ممي', + 64946 => 'قمي', + 64947 => 'نحي', + 64948 => 'قمح', + 64949 => 'لحم', + 64950 => 'عمي', + 64951 => 'كمي', + 64952 => 'نجح', + 64953 => 'مخي', + 64954 => 'لجم', + 64955 => 'كمم', + 64956 => 'لجم', + 64957 => 'نجح', + 64958 => 'جحي', + 64959 => 'حجي', + 64960 => 'مجي', + 64961 => 'فمي', + 64962 => 'بحي', + 64963 => 'كمم', + 64964 => 'عجم', + 64965 => 'صمم', + 64966 => 'سخي', + 64967 => 'نجي', + 65008 => 'صلے', + 65009 => 'قلے', + 65010 => 'الله', + 65011 => 'اكبر', + 65012 => 'محمد', + 65013 => 'صلعم', + 65014 => 'رسول', + 65015 => 'عليه', + 65016 => 'وسلم', + 65017 => 'صلى', + 65020 => 'ریال', + 65041 => '、', + 65047 => '〖', + 65048 => '〗', + 65073 => '—', + 65074 => '–', + 65081 => '〔', + 65082 => '〕', + 65083 => '【', + 65084 => '】', + 65085 => '《', + 65086 => '》', + 65087 => '〈', + 65088 => '〉', + 65089 => '「', + 65090 => '」', + 65091 => '『', + 65092 => '』', + 65105 => '、', + 65112 => '—', + 65117 => '〔', + 65118 => '〕', + 65123 => '-', + 65137 => 'ـً', + 65143 => 'ـَ', + 65145 => 'ـُ', + 65147 => 'ـِ', + 65149 => 'ـّ', + 65151 => 'ـْ', + 65152 => 'ء', + 65153 => 'آ', + 65154 => 'آ', + 65155 => 'أ', + 65156 => 'أ', + 65157 => 'ؤ', + 65158 => 'ؤ', + 65159 => 'إ', + 65160 => 'إ', + 65161 => 'ئ', + 65162 => 'ئ', + 65163 => 'ئ', + 65164 => 'ئ', + 65165 => 'ا', + 65166 => 'ا', + 65167 => 'ب', + 65168 => 'ب', + 65169 => 'ب', + 65170 => 'ب', + 65171 => 'ة', + 65172 => 'ة', + 65173 => 'ت', + 65174 => 'ت', + 65175 => 'ت', + 65176 => 'ت', + 65177 => 'ث', + 65178 => 'ث', + 65179 => 'ث', + 65180 => 'ث', + 65181 => 'ج', + 65182 => 'ج', + 65183 => 'ج', + 65184 => 'ج', + 65185 => 'ح', + 65186 => 'ح', + 65187 => 'ح', + 65188 => 'ح', + 65189 => 'خ', + 65190 => 'خ', + 65191 => 'خ', + 65192 => 'خ', + 65193 => 'د', + 65194 => 'د', + 65195 => 'ذ', + 65196 => 'ذ', + 65197 => 'ر', + 65198 => 'ر', + 65199 => 'ز', + 65200 => 'ز', + 65201 => 'س', + 65202 => 'س', + 65203 => 'س', + 65204 => 'س', + 65205 => 'ش', + 65206 => 'ش', + 65207 => 'ش', + 65208 => 'ش', + 65209 => 'ص', + 65210 => 'ص', + 65211 => 'ص', + 65212 => 'ص', + 65213 => 'ض', + 65214 => 'ض', + 65215 => 'ض', + 65216 => 'ض', + 65217 => 'ط', + 65218 => 'ط', + 65219 => 'ط', + 65220 => 'ط', + 65221 => 'ظ', + 65222 => 'ظ', + 65223 => 'ظ', + 65224 => 'ظ', + 65225 => 'ع', + 65226 => 'ع', + 65227 => 'ع', + 65228 => 'ع', + 65229 => 'غ', + 65230 => 'غ', + 65231 => 'غ', + 65232 => 'غ', + 65233 => 'ف', + 65234 => 'ف', + 65235 => 'ف', + 65236 => 'ف', + 65237 => 'ق', + 65238 => 'ق', + 65239 => 'ق', + 65240 => 'ق', + 65241 => 'ك', + 65242 => 'ك', + 65243 => 'ك', + 65244 => 'ك', + 65245 => 'ل', + 65246 => 'ل', + 65247 => 'ل', + 65248 => 'ل', + 65249 => 'م', + 65250 => 'م', + 65251 => 'م', + 65252 => 'م', + 65253 => 'ن', + 65254 => 'ن', + 65255 => 'ن', + 65256 => 'ن', + 65257 => 'ه', + 65258 => 'ه', + 65259 => 'ه', + 65260 => 'ه', + 65261 => 'و', + 65262 => 'و', + 65263 => 'ى', + 65264 => 'ى', + 65265 => 'ي', + 65266 => 'ي', + 65267 => 'ي', + 65268 => 'ي', + 65269 => 'لآ', + 65270 => 'لآ', + 65271 => 'لأ', + 65272 => 'لأ', + 65273 => 'لإ', + 65274 => 'لإ', + 65275 => 'لا', + 65276 => 'لا', + 65293 => '-', + 65294 => '.', + 65296 => '0', + 65297 => '1', + 65298 => '2', + 65299 => '3', + 65300 => '4', + 65301 => '5', + 65302 => '6', + 65303 => '7', + 65304 => '8', + 65305 => '9', + 65313 => 'a', + 65314 => 'b', + 65315 => 'c', + 65316 => 'd', + 65317 => 'e', + 65318 => 'f', + 65319 => 'g', + 65320 => 'h', + 65321 => 'i', + 65322 => 'j', + 65323 => 'k', + 65324 => 'l', + 65325 => 'm', + 65326 => 'n', + 65327 => 'o', + 65328 => 'p', + 65329 => 'q', + 65330 => 'r', + 65331 => 's', + 65332 => 't', + 65333 => 'u', + 65334 => 'v', + 65335 => 'w', + 65336 => 'x', + 65337 => 'y', + 65338 => 'z', + 65345 => 'a', + 65346 => 'b', + 65347 => 'c', + 65348 => 'd', + 65349 => 'e', + 65350 => 'f', + 65351 => 'g', + 65352 => 'h', + 65353 => 'i', + 65354 => 'j', + 65355 => 'k', + 65356 => 'l', + 65357 => 'm', + 65358 => 'n', + 65359 => 'o', + 65360 => 'p', + 65361 => 'q', + 65362 => 'r', + 65363 => 's', + 65364 => 't', + 65365 => 'u', + 65366 => 'v', + 65367 => 'w', + 65368 => 'x', + 65369 => 'y', + 65370 => 'z', + 65375 => '⦅', + 65376 => '⦆', + 65377 => '.', + 65378 => '「', + 65379 => '」', + 65380 => '、', + 65381 => '・', + 65382 => 'ヲ', + 65383 => 'ァ', + 65384 => 'ィ', + 65385 => 'ゥ', + 65386 => 'ェ', + 65387 => 'ォ', + 65388 => 'ャ', + 65389 => 'ュ', + 65390 => 'ョ', + 65391 => 'ッ', + 65392 => 'ー', + 65393 => 'ア', + 65394 => 'イ', + 65395 => 'ウ', + 65396 => 'エ', + 65397 => 'オ', + 65398 => 'カ', + 65399 => 'キ', + 65400 => 'ク', + 65401 => 'ケ', + 65402 => 'コ', + 65403 => 'サ', + 65404 => 'シ', + 65405 => 'ス', + 65406 => 'セ', + 65407 => 'ソ', + 65408 => 'タ', + 65409 => 'チ', + 65410 => 'ツ', + 65411 => 'テ', + 65412 => 'ト', + 65413 => 'ナ', + 65414 => 'ニ', + 65415 => 'ヌ', + 65416 => 'ネ', + 65417 => 'ノ', + 65418 => 'ハ', + 65419 => 'ヒ', + 65420 => 'フ', + 65421 => 'ヘ', + 65422 => 'ホ', + 65423 => 'マ', + 65424 => 'ミ', + 65425 => 'ム', + 65426 => 'メ', + 65427 => 'モ', + 65428 => 'ヤ', + 65429 => 'ユ', + 65430 => 'ヨ', + 65431 => 'ラ', + 65432 => 'リ', + 65433 => 'ル', + 65434 => 'レ', + 65435 => 'ロ', + 65436 => 'ワ', + 65437 => 'ン', + 65438 => '゙', + 65439 => '゚', + 65441 => 'ᄀ', + 65442 => 'ᄁ', + 65443 => 'ᆪ', + 65444 => 'ᄂ', + 65445 => 'ᆬ', + 65446 => 'ᆭ', + 65447 => 'ᄃ', + 65448 => 'ᄄ', + 65449 => 'ᄅ', + 65450 => 'ᆰ', + 65451 => 'ᆱ', + 65452 => 'ᆲ', + 65453 => 'ᆳ', + 65454 => 'ᆴ', + 65455 => 'ᆵ', + 65456 => 'ᄚ', + 65457 => 'ᄆ', + 65458 => 'ᄇ', + 65459 => 'ᄈ', + 65460 => 'ᄡ', + 65461 => 'ᄉ', + 65462 => 'ᄊ', + 65463 => 'ᄋ', + 65464 => 'ᄌ', + 65465 => 'ᄍ', + 65466 => 'ᄎ', + 65467 => 'ᄏ', + 65468 => 'ᄐ', + 65469 => 'ᄑ', + 65470 => 'ᄒ', + 65474 => 'ᅡ', + 65475 => 'ᅢ', + 65476 => 'ᅣ', + 65477 => 'ᅤ', + 65478 => 'ᅥ', + 65479 => 'ᅦ', + 65482 => 'ᅧ', + 65483 => 'ᅨ', + 65484 => 'ᅩ', + 65485 => 'ᅪ', + 65486 => 'ᅫ', + 65487 => 'ᅬ', + 65490 => 'ᅭ', + 65491 => 'ᅮ', + 65492 => 'ᅯ', + 65493 => 'ᅰ', + 65494 => 'ᅱ', + 65495 => 'ᅲ', + 65498 => 'ᅳ', + 65499 => 'ᅴ', + 65500 => 'ᅵ', + 65504 => '¢', + 65505 => '£', + 65506 => '¬', + 65508 => '¦', + 65509 => '¥', + 65510 => '₩', + 65512 => '│', + 65513 => '←', + 65514 => '↑', + 65515 => '→', + 65516 => '↓', + 65517 => '■', + 65518 => '○', + 66560 => '𐐨', + 66561 => '𐐩', + 66562 => '𐐪', + 66563 => '𐐫', + 66564 => '𐐬', + 66565 => '𐐭', + 66566 => '𐐮', + 66567 => '𐐯', + 66568 => '𐐰', + 66569 => '𐐱', + 66570 => '𐐲', + 66571 => '𐐳', + 66572 => '𐐴', + 66573 => '𐐵', + 66574 => '𐐶', + 66575 => '𐐷', + 66576 => '𐐸', + 66577 => '𐐹', + 66578 => '𐐺', + 66579 => '𐐻', + 66580 => '𐐼', + 66581 => '𐐽', + 66582 => '𐐾', + 66583 => '𐐿', + 66584 => '𐑀', + 66585 => '𐑁', + 66586 => '𐑂', + 66587 => '𐑃', + 66588 => '𐑄', + 66589 => '𐑅', + 66590 => '𐑆', + 66591 => '𐑇', + 66592 => '𐑈', + 66593 => '𐑉', + 66594 => '𐑊', + 66595 => '𐑋', + 66596 => '𐑌', + 66597 => '𐑍', + 66598 => '𐑎', + 66599 => '𐑏', + 66736 => '𐓘', + 66737 => '𐓙', + 66738 => '𐓚', + 66739 => '𐓛', + 66740 => '𐓜', + 66741 => '𐓝', + 66742 => '𐓞', + 66743 => '𐓟', + 66744 => '𐓠', + 66745 => '𐓡', + 66746 => '𐓢', + 66747 => '𐓣', + 66748 => '𐓤', + 66749 => '𐓥', + 66750 => '𐓦', + 66751 => '𐓧', + 66752 => '𐓨', + 66753 => '𐓩', + 66754 => '𐓪', + 66755 => '𐓫', + 66756 => '𐓬', + 66757 => '𐓭', + 66758 => '𐓮', + 66759 => '𐓯', + 66760 => '𐓰', + 66761 => '𐓱', + 66762 => '𐓲', + 66763 => '𐓳', + 66764 => '𐓴', + 66765 => '𐓵', + 66766 => '𐓶', + 66767 => '𐓷', + 66768 => '𐓸', + 66769 => '𐓹', + 66770 => '𐓺', + 66771 => '𐓻', + 68736 => '𐳀', + 68737 => '𐳁', + 68738 => '𐳂', + 68739 => '𐳃', + 68740 => '𐳄', + 68741 => '𐳅', + 68742 => '𐳆', + 68743 => '𐳇', + 68744 => '𐳈', + 68745 => '𐳉', + 68746 => '𐳊', + 68747 => '𐳋', + 68748 => '𐳌', + 68749 => '𐳍', + 68750 => '𐳎', + 68751 => '𐳏', + 68752 => '𐳐', + 68753 => '𐳑', + 68754 => '𐳒', + 68755 => '𐳓', + 68756 => '𐳔', + 68757 => '𐳕', + 68758 => '𐳖', + 68759 => '𐳗', + 68760 => '𐳘', + 68761 => '𐳙', + 68762 => '𐳚', + 68763 => '𐳛', + 68764 => '𐳜', + 68765 => '𐳝', + 68766 => '𐳞', + 68767 => '𐳟', + 68768 => '𐳠', + 68769 => '𐳡', + 68770 => '𐳢', + 68771 => '𐳣', + 68772 => '𐳤', + 68773 => '𐳥', + 68774 => '𐳦', + 68775 => '𐳧', + 68776 => '𐳨', + 68777 => '𐳩', + 68778 => '𐳪', + 68779 => '𐳫', + 68780 => '𐳬', + 68781 => '𐳭', + 68782 => '𐳮', + 68783 => '𐳯', + 68784 => '𐳰', + 68785 => '𐳱', + 68786 => '𐳲', + 71840 => '𑣀', + 71841 => '𑣁', + 71842 => '𑣂', + 71843 => '𑣃', + 71844 => '𑣄', + 71845 => '𑣅', + 71846 => '𑣆', + 71847 => '𑣇', + 71848 => '𑣈', + 71849 => '𑣉', + 71850 => '𑣊', + 71851 => '𑣋', + 71852 => '𑣌', + 71853 => '𑣍', + 71854 => '𑣎', + 71855 => '𑣏', + 71856 => '𑣐', + 71857 => '𑣑', + 71858 => '𑣒', + 71859 => '𑣓', + 71860 => '𑣔', + 71861 => '𑣕', + 71862 => '𑣖', + 71863 => '𑣗', + 71864 => '𑣘', + 71865 => '𑣙', + 71866 => '𑣚', + 71867 => '𑣛', + 71868 => '𑣜', + 71869 => '𑣝', + 71870 => '𑣞', + 71871 => '𑣟', + 93760 => '𖹠', + 93761 => '𖹡', + 93762 => '𖹢', + 93763 => '𖹣', + 93764 => '𖹤', + 93765 => '𖹥', + 93766 => '𖹦', + 93767 => '𖹧', + 93768 => '𖹨', + 93769 => '𖹩', + 93770 => '𖹪', + 93771 => '𖹫', + 93772 => '𖹬', + 93773 => '𖹭', + 93774 => '𖹮', + 93775 => '𖹯', + 93776 => '𖹰', + 93777 => '𖹱', + 93778 => '𖹲', + 93779 => '𖹳', + 93780 => '𖹴', + 93781 => '𖹵', + 93782 => '𖹶', + 93783 => '𖹷', + 93784 => '𖹸', + 93785 => '𖹹', + 93786 => '𖹺', + 93787 => '𖹻', + 93788 => '𖹼', + 93789 => '𖹽', + 93790 => '𖹾', + 93791 => '𖹿', + 119134 => '𝅗𝅥', + 119135 => '𝅘𝅥', + 119136 => '𝅘𝅥𝅮', + 119137 => '𝅘𝅥𝅯', + 119138 => '𝅘𝅥𝅰', + 119139 => '𝅘𝅥𝅱', + 119140 => '𝅘𝅥𝅲', + 119227 => '𝆹𝅥', + 119228 => '𝆺𝅥', + 119229 => '𝆹𝅥𝅮', + 119230 => '𝆺𝅥𝅮', + 119231 => '𝆹𝅥𝅯', + 119232 => '𝆺𝅥𝅯', + 119808 => 'a', + 119809 => 'b', + 119810 => 'c', + 119811 => 'd', + 119812 => 'e', + 119813 => 'f', + 119814 => 'g', + 119815 => 'h', + 119816 => 'i', + 119817 => 'j', + 119818 => 'k', + 119819 => 'l', + 119820 => 'm', + 119821 => 'n', + 119822 => 'o', + 119823 => 'p', + 119824 => 'q', + 119825 => 'r', + 119826 => 's', + 119827 => 't', + 119828 => 'u', + 119829 => 'v', + 119830 => 'w', + 119831 => 'x', + 119832 => 'y', + 119833 => 'z', + 119834 => 'a', + 119835 => 'b', + 119836 => 'c', + 119837 => 'd', + 119838 => 'e', + 119839 => 'f', + 119840 => 'g', + 119841 => 'h', + 119842 => 'i', + 119843 => 'j', + 119844 => 'k', + 119845 => 'l', + 119846 => 'm', + 119847 => 'n', + 119848 => 'o', + 119849 => 'p', + 119850 => 'q', + 119851 => 'r', + 119852 => 's', + 119853 => 't', + 119854 => 'u', + 119855 => 'v', + 119856 => 'w', + 119857 => 'x', + 119858 => 'y', + 119859 => 'z', + 119860 => 'a', + 119861 => 'b', + 119862 => 'c', + 119863 => 'd', + 119864 => 'e', + 119865 => 'f', + 119866 => 'g', + 119867 => 'h', + 119868 => 'i', + 119869 => 'j', + 119870 => 'k', + 119871 => 'l', + 119872 => 'm', + 119873 => 'n', + 119874 => 'o', + 119875 => 'p', + 119876 => 'q', + 119877 => 'r', + 119878 => 's', + 119879 => 't', + 119880 => 'u', + 119881 => 'v', + 119882 => 'w', + 119883 => 'x', + 119884 => 'y', + 119885 => 'z', + 119886 => 'a', + 119887 => 'b', + 119888 => 'c', + 119889 => 'd', + 119890 => 'e', + 119891 => 'f', + 119892 => 'g', + 119894 => 'i', + 119895 => 'j', + 119896 => 'k', + 119897 => 'l', + 119898 => 'm', + 119899 => 'n', + 119900 => 'o', + 119901 => 'p', + 119902 => 'q', + 119903 => 'r', + 119904 => 's', + 119905 => 't', + 119906 => 'u', + 119907 => 'v', + 119908 => 'w', + 119909 => 'x', + 119910 => 'y', + 119911 => 'z', + 119912 => 'a', + 119913 => 'b', + 119914 => 'c', + 119915 => 'd', + 119916 => 'e', + 119917 => 'f', + 119918 => 'g', + 119919 => 'h', + 119920 => 'i', + 119921 => 'j', + 119922 => 'k', + 119923 => 'l', + 119924 => 'm', + 119925 => 'n', + 119926 => 'o', + 119927 => 'p', + 119928 => 'q', + 119929 => 'r', + 119930 => 's', + 119931 => 't', + 119932 => 'u', + 119933 => 'v', + 119934 => 'w', + 119935 => 'x', + 119936 => 'y', + 119937 => 'z', + 119938 => 'a', + 119939 => 'b', + 119940 => 'c', + 119941 => 'd', + 119942 => 'e', + 119943 => 'f', + 119944 => 'g', + 119945 => 'h', + 119946 => 'i', + 119947 => 'j', + 119948 => 'k', + 119949 => 'l', + 119950 => 'm', + 119951 => 'n', + 119952 => 'o', + 119953 => 'p', + 119954 => 'q', + 119955 => 'r', + 119956 => 's', + 119957 => 't', + 119958 => 'u', + 119959 => 'v', + 119960 => 'w', + 119961 => 'x', + 119962 => 'y', + 119963 => 'z', + 119964 => 'a', + 119966 => 'c', + 119967 => 'd', + 119970 => 'g', + 119973 => 'j', + 119974 => 'k', + 119977 => 'n', + 119978 => 'o', + 119979 => 'p', + 119980 => 'q', + 119982 => 's', + 119983 => 't', + 119984 => 'u', + 119985 => 'v', + 119986 => 'w', + 119987 => 'x', + 119988 => 'y', + 119989 => 'z', + 119990 => 'a', + 119991 => 'b', + 119992 => 'c', + 119993 => 'd', + 119995 => 'f', + 119997 => 'h', + 119998 => 'i', + 119999 => 'j', + 120000 => 'k', + 120001 => 'l', + 120002 => 'm', + 120003 => 'n', + 120005 => 'p', + 120006 => 'q', + 120007 => 'r', + 120008 => 's', + 120009 => 't', + 120010 => 'u', + 120011 => 'v', + 120012 => 'w', + 120013 => 'x', + 120014 => 'y', + 120015 => 'z', + 120016 => 'a', + 120017 => 'b', + 120018 => 'c', + 120019 => 'd', + 120020 => 'e', + 120021 => 'f', + 120022 => 'g', + 120023 => 'h', + 120024 => 'i', + 120025 => 'j', + 120026 => 'k', + 120027 => 'l', + 120028 => 'm', + 120029 => 'n', + 120030 => 'o', + 120031 => 'p', + 120032 => 'q', + 120033 => 'r', + 120034 => 's', + 120035 => 't', + 120036 => 'u', + 120037 => 'v', + 120038 => 'w', + 120039 => 'x', + 120040 => 'y', + 120041 => 'z', + 120042 => 'a', + 120043 => 'b', + 120044 => 'c', + 120045 => 'd', + 120046 => 'e', + 120047 => 'f', + 120048 => 'g', + 120049 => 'h', + 120050 => 'i', + 120051 => 'j', + 120052 => 'k', + 120053 => 'l', + 120054 => 'm', + 120055 => 'n', + 120056 => 'o', + 120057 => 'p', + 120058 => 'q', + 120059 => 'r', + 120060 => 's', + 120061 => 't', + 120062 => 'u', + 120063 => 'v', + 120064 => 'w', + 120065 => 'x', + 120066 => 'y', + 120067 => 'z', + 120068 => 'a', + 120069 => 'b', + 120071 => 'd', + 120072 => 'e', + 120073 => 'f', + 120074 => 'g', + 120077 => 'j', + 120078 => 'k', + 120079 => 'l', + 120080 => 'm', + 120081 => 'n', + 120082 => 'o', + 120083 => 'p', + 120084 => 'q', + 120086 => 's', + 120087 => 't', + 120088 => 'u', + 120089 => 'v', + 120090 => 'w', + 120091 => 'x', + 120092 => 'y', + 120094 => 'a', + 120095 => 'b', + 120096 => 'c', + 120097 => 'd', + 120098 => 'e', + 120099 => 'f', + 120100 => 'g', + 120101 => 'h', + 120102 => 'i', + 120103 => 'j', + 120104 => 'k', + 120105 => 'l', + 120106 => 'm', + 120107 => 'n', + 120108 => 'o', + 120109 => 'p', + 120110 => 'q', + 120111 => 'r', + 120112 => 's', + 120113 => 't', + 120114 => 'u', + 120115 => 'v', + 120116 => 'w', + 120117 => 'x', + 120118 => 'y', + 120119 => 'z', + 120120 => 'a', + 120121 => 'b', + 120123 => 'd', + 120124 => 'e', + 120125 => 'f', + 120126 => 'g', + 120128 => 'i', + 120129 => 'j', + 120130 => 'k', + 120131 => 'l', + 120132 => 'm', + 120134 => 'o', + 120138 => 's', + 120139 => 't', + 120140 => 'u', + 120141 => 'v', + 120142 => 'w', + 120143 => 'x', + 120144 => 'y', + 120146 => 'a', + 120147 => 'b', + 120148 => 'c', + 120149 => 'd', + 120150 => 'e', + 120151 => 'f', + 120152 => 'g', + 120153 => 'h', + 120154 => 'i', + 120155 => 'j', + 120156 => 'k', + 120157 => 'l', + 120158 => 'm', + 120159 => 'n', + 120160 => 'o', + 120161 => 'p', + 120162 => 'q', + 120163 => 'r', + 120164 => 's', + 120165 => 't', + 120166 => 'u', + 120167 => 'v', + 120168 => 'w', + 120169 => 'x', + 120170 => 'y', + 120171 => 'z', + 120172 => 'a', + 120173 => 'b', + 120174 => 'c', + 120175 => 'd', + 120176 => 'e', + 120177 => 'f', + 120178 => 'g', + 120179 => 'h', + 120180 => 'i', + 120181 => 'j', + 120182 => 'k', + 120183 => 'l', + 120184 => 'm', + 120185 => 'n', + 120186 => 'o', + 120187 => 'p', + 120188 => 'q', + 120189 => 'r', + 120190 => 's', + 120191 => 't', + 120192 => 'u', + 120193 => 'v', + 120194 => 'w', + 120195 => 'x', + 120196 => 'y', + 120197 => 'z', + 120198 => 'a', + 120199 => 'b', + 120200 => 'c', + 120201 => 'd', + 120202 => 'e', + 120203 => 'f', + 120204 => 'g', + 120205 => 'h', + 120206 => 'i', + 120207 => 'j', + 120208 => 'k', + 120209 => 'l', + 120210 => 'm', + 120211 => 'n', + 120212 => 'o', + 120213 => 'p', + 120214 => 'q', + 120215 => 'r', + 120216 => 's', + 120217 => 't', + 120218 => 'u', + 120219 => 'v', + 120220 => 'w', + 120221 => 'x', + 120222 => 'y', + 120223 => 'z', + 120224 => 'a', + 120225 => 'b', + 120226 => 'c', + 120227 => 'd', + 120228 => 'e', + 120229 => 'f', + 120230 => 'g', + 120231 => 'h', + 120232 => 'i', + 120233 => 'j', + 120234 => 'k', + 120235 => 'l', + 120236 => 'm', + 120237 => 'n', + 120238 => 'o', + 120239 => 'p', + 120240 => 'q', + 120241 => 'r', + 120242 => 's', + 120243 => 't', + 120244 => 'u', + 120245 => 'v', + 120246 => 'w', + 120247 => 'x', + 120248 => 'y', + 120249 => 'z', + 120250 => 'a', + 120251 => 'b', + 120252 => 'c', + 120253 => 'd', + 120254 => 'e', + 120255 => 'f', + 120256 => 'g', + 120257 => 'h', + 120258 => 'i', + 120259 => 'j', + 120260 => 'k', + 120261 => 'l', + 120262 => 'm', + 120263 => 'n', + 120264 => 'o', + 120265 => 'p', + 120266 => 'q', + 120267 => 'r', + 120268 => 's', + 120269 => 't', + 120270 => 'u', + 120271 => 'v', + 120272 => 'w', + 120273 => 'x', + 120274 => 'y', + 120275 => 'z', + 120276 => 'a', + 120277 => 'b', + 120278 => 'c', + 120279 => 'd', + 120280 => 'e', + 120281 => 'f', + 120282 => 'g', + 120283 => 'h', + 120284 => 'i', + 120285 => 'j', + 120286 => 'k', + 120287 => 'l', + 120288 => 'm', + 120289 => 'n', + 120290 => 'o', + 120291 => 'p', + 120292 => 'q', + 120293 => 'r', + 120294 => 's', + 120295 => 't', + 120296 => 'u', + 120297 => 'v', + 120298 => 'w', + 120299 => 'x', + 120300 => 'y', + 120301 => 'z', + 120302 => 'a', + 120303 => 'b', + 120304 => 'c', + 120305 => 'd', + 120306 => 'e', + 120307 => 'f', + 120308 => 'g', + 120309 => 'h', + 120310 => 'i', + 120311 => 'j', + 120312 => 'k', + 120313 => 'l', + 120314 => 'm', + 120315 => 'n', + 120316 => 'o', + 120317 => 'p', + 120318 => 'q', + 120319 => 'r', + 120320 => 's', + 120321 => 't', + 120322 => 'u', + 120323 => 'v', + 120324 => 'w', + 120325 => 'x', + 120326 => 'y', + 120327 => 'z', + 120328 => 'a', + 120329 => 'b', + 120330 => 'c', + 120331 => 'd', + 120332 => 'e', + 120333 => 'f', + 120334 => 'g', + 120335 => 'h', + 120336 => 'i', + 120337 => 'j', + 120338 => 'k', + 120339 => 'l', + 120340 => 'm', + 120341 => 'n', + 120342 => 'o', + 120343 => 'p', + 120344 => 'q', + 120345 => 'r', + 120346 => 's', + 120347 => 't', + 120348 => 'u', + 120349 => 'v', + 120350 => 'w', + 120351 => 'x', + 120352 => 'y', + 120353 => 'z', + 120354 => 'a', + 120355 => 'b', + 120356 => 'c', + 120357 => 'd', + 120358 => 'e', + 120359 => 'f', + 120360 => 'g', + 120361 => 'h', + 120362 => 'i', + 120363 => 'j', + 120364 => 'k', + 120365 => 'l', + 120366 => 'm', + 120367 => 'n', + 120368 => 'o', + 120369 => 'p', + 120370 => 'q', + 120371 => 'r', + 120372 => 's', + 120373 => 't', + 120374 => 'u', + 120375 => 'v', + 120376 => 'w', + 120377 => 'x', + 120378 => 'y', + 120379 => 'z', + 120380 => 'a', + 120381 => 'b', + 120382 => 'c', + 120383 => 'd', + 120384 => 'e', + 120385 => 'f', + 120386 => 'g', + 120387 => 'h', + 120388 => 'i', + 120389 => 'j', + 120390 => 'k', + 120391 => 'l', + 120392 => 'm', + 120393 => 'n', + 120394 => 'o', + 120395 => 'p', + 120396 => 'q', + 120397 => 'r', + 120398 => 's', + 120399 => 't', + 120400 => 'u', + 120401 => 'v', + 120402 => 'w', + 120403 => 'x', + 120404 => 'y', + 120405 => 'z', + 120406 => 'a', + 120407 => 'b', + 120408 => 'c', + 120409 => 'd', + 120410 => 'e', + 120411 => 'f', + 120412 => 'g', + 120413 => 'h', + 120414 => 'i', + 120415 => 'j', + 120416 => 'k', + 120417 => 'l', + 120418 => 'm', + 120419 => 'n', + 120420 => 'o', + 120421 => 'p', + 120422 => 'q', + 120423 => 'r', + 120424 => 's', + 120425 => 't', + 120426 => 'u', + 120427 => 'v', + 120428 => 'w', + 120429 => 'x', + 120430 => 'y', + 120431 => 'z', + 120432 => 'a', + 120433 => 'b', + 120434 => 'c', + 120435 => 'd', + 120436 => 'e', + 120437 => 'f', + 120438 => 'g', + 120439 => 'h', + 120440 => 'i', + 120441 => 'j', + 120442 => 'k', + 120443 => 'l', + 120444 => 'm', + 120445 => 'n', + 120446 => 'o', + 120447 => 'p', + 120448 => 'q', + 120449 => 'r', + 120450 => 's', + 120451 => 't', + 120452 => 'u', + 120453 => 'v', + 120454 => 'w', + 120455 => 'x', + 120456 => 'y', + 120457 => 'z', + 120458 => 'a', + 120459 => 'b', + 120460 => 'c', + 120461 => 'd', + 120462 => 'e', + 120463 => 'f', + 120464 => 'g', + 120465 => 'h', + 120466 => 'i', + 120467 => 'j', + 120468 => 'k', + 120469 => 'l', + 120470 => 'm', + 120471 => 'n', + 120472 => 'o', + 120473 => 'p', + 120474 => 'q', + 120475 => 'r', + 120476 => 's', + 120477 => 't', + 120478 => 'u', + 120479 => 'v', + 120480 => 'w', + 120481 => 'x', + 120482 => 'y', + 120483 => 'z', + 120484 => 'ı', + 120485 => 'ȷ', + 120488 => 'α', + 120489 => 'β', + 120490 => 'γ', + 120491 => 'δ', + 120492 => 'ε', + 120493 => 'ζ', + 120494 => 'η', + 120495 => 'θ', + 120496 => 'ι', + 120497 => 'κ', + 120498 => 'λ', + 120499 => 'μ', + 120500 => 'ν', + 120501 => 'ξ', + 120502 => 'ο', + 120503 => 'π', + 120504 => 'ρ', + 120505 => 'θ', + 120506 => 'σ', + 120507 => 'τ', + 120508 => 'υ', + 120509 => 'φ', + 120510 => 'χ', + 120511 => 'ψ', + 120512 => 'ω', + 120513 => '∇', + 120514 => 'α', + 120515 => 'β', + 120516 => 'γ', + 120517 => 'δ', + 120518 => 'ε', + 120519 => 'ζ', + 120520 => 'η', + 120521 => 'θ', + 120522 => 'ι', + 120523 => 'κ', + 120524 => 'λ', + 120525 => 'μ', + 120526 => 'ν', + 120527 => 'ξ', + 120528 => 'ο', + 120529 => 'π', + 120530 => 'ρ', + 120531 => 'σ', + 120532 => 'σ', + 120533 => 'τ', + 120534 => 'υ', + 120535 => 'φ', + 120536 => 'χ', + 120537 => 'ψ', + 120538 => 'ω', + 120539 => '∂', + 120540 => 'ε', + 120541 => 'θ', + 120542 => 'κ', + 120543 => 'φ', + 120544 => 'ρ', + 120545 => 'π', + 120546 => 'α', + 120547 => 'β', + 120548 => 'γ', + 120549 => 'δ', + 120550 => 'ε', + 120551 => 'ζ', + 120552 => 'η', + 120553 => 'θ', + 120554 => 'ι', + 120555 => 'κ', + 120556 => 'λ', + 120557 => 'μ', + 120558 => 'ν', + 120559 => 'ξ', + 120560 => 'ο', + 120561 => 'π', + 120562 => 'ρ', + 120563 => 'θ', + 120564 => 'σ', + 120565 => 'τ', + 120566 => 'υ', + 120567 => 'φ', + 120568 => 'χ', + 120569 => 'ψ', + 120570 => 'ω', + 120571 => '∇', + 120572 => 'α', + 120573 => 'β', + 120574 => 'γ', + 120575 => 'δ', + 120576 => 'ε', + 120577 => 'ζ', + 120578 => 'η', + 120579 => 'θ', + 120580 => 'ι', + 120581 => 'κ', + 120582 => 'λ', + 120583 => 'μ', + 120584 => 'ν', + 120585 => 'ξ', + 120586 => 'ο', + 120587 => 'π', + 120588 => 'ρ', + 120589 => 'σ', + 120590 => 'σ', + 120591 => 'τ', + 120592 => 'υ', + 120593 => 'φ', + 120594 => 'χ', + 120595 => 'ψ', + 120596 => 'ω', + 120597 => '∂', + 120598 => 'ε', + 120599 => 'θ', + 120600 => 'κ', + 120601 => 'φ', + 120602 => 'ρ', + 120603 => 'π', + 120604 => 'α', + 120605 => 'β', + 120606 => 'γ', + 120607 => 'δ', + 120608 => 'ε', + 120609 => 'ζ', + 120610 => 'η', + 120611 => 'θ', + 120612 => 'ι', + 120613 => 'κ', + 120614 => 'λ', + 120615 => 'μ', + 120616 => 'ν', + 120617 => 'ξ', + 120618 => 'ο', + 120619 => 'π', + 120620 => 'ρ', + 120621 => 'θ', + 120622 => 'σ', + 120623 => 'τ', + 120624 => 'υ', + 120625 => 'φ', + 120626 => 'χ', + 120627 => 'ψ', + 120628 => 'ω', + 120629 => '∇', + 120630 => 'α', + 120631 => 'β', + 120632 => 'γ', + 120633 => 'δ', + 120634 => 'ε', + 120635 => 'ζ', + 120636 => 'η', + 120637 => 'θ', + 120638 => 'ι', + 120639 => 'κ', + 120640 => 'λ', + 120641 => 'μ', + 120642 => 'ν', + 120643 => 'ξ', + 120644 => 'ο', + 120645 => 'π', + 120646 => 'ρ', + 120647 => 'σ', + 120648 => 'σ', + 120649 => 'τ', + 120650 => 'υ', + 120651 => 'φ', + 120652 => 'χ', + 120653 => 'ψ', + 120654 => 'ω', + 120655 => '∂', + 120656 => 'ε', + 120657 => 'θ', + 120658 => 'κ', + 120659 => 'φ', + 120660 => 'ρ', + 120661 => 'π', + 120662 => 'α', + 120663 => 'β', + 120664 => 'γ', + 120665 => 'δ', + 120666 => 'ε', + 120667 => 'ζ', + 120668 => 'η', + 120669 => 'θ', + 120670 => 'ι', + 120671 => 'κ', + 120672 => 'λ', + 120673 => 'μ', + 120674 => 'ν', + 120675 => 'ξ', + 120676 => 'ο', + 120677 => 'π', + 120678 => 'ρ', + 120679 => 'θ', + 120680 => 'σ', + 120681 => 'τ', + 120682 => 'υ', + 120683 => 'φ', + 120684 => 'χ', + 120685 => 'ψ', + 120686 => 'ω', + 120687 => '∇', + 120688 => 'α', + 120689 => 'β', + 120690 => 'γ', + 120691 => 'δ', + 120692 => 'ε', + 120693 => 'ζ', + 120694 => 'η', + 120695 => 'θ', + 120696 => 'ι', + 120697 => 'κ', + 120698 => 'λ', + 120699 => 'μ', + 120700 => 'ν', + 120701 => 'ξ', + 120702 => 'ο', + 120703 => 'π', + 120704 => 'ρ', + 120705 => 'σ', + 120706 => 'σ', + 120707 => 'τ', + 120708 => 'υ', + 120709 => 'φ', + 120710 => 'χ', + 120711 => 'ψ', + 120712 => 'ω', + 120713 => '∂', + 120714 => 'ε', + 120715 => 'θ', + 120716 => 'κ', + 120717 => 'φ', + 120718 => 'ρ', + 120719 => 'π', + 120720 => 'α', + 120721 => 'β', + 120722 => 'γ', + 120723 => 'δ', + 120724 => 'ε', + 120725 => 'ζ', + 120726 => 'η', + 120727 => 'θ', + 120728 => 'ι', + 120729 => 'κ', + 120730 => 'λ', + 120731 => 'μ', + 120732 => 'ν', + 120733 => 'ξ', + 120734 => 'ο', + 120735 => 'π', + 120736 => 'ρ', + 120737 => 'θ', + 120738 => 'σ', + 120739 => 'τ', + 120740 => 'υ', + 120741 => 'φ', + 120742 => 'χ', + 120743 => 'ψ', + 120744 => 'ω', + 120745 => '∇', + 120746 => 'α', + 120747 => 'β', + 120748 => 'γ', + 120749 => 'δ', + 120750 => 'ε', + 120751 => 'ζ', + 120752 => 'η', + 120753 => 'θ', + 120754 => 'ι', + 120755 => 'κ', + 120756 => 'λ', + 120757 => 'μ', + 120758 => 'ν', + 120759 => 'ξ', + 120760 => 'ο', + 120761 => 'π', + 120762 => 'ρ', + 120763 => 'σ', + 120764 => 'σ', + 120765 => 'τ', + 120766 => 'υ', + 120767 => 'φ', + 120768 => 'χ', + 120769 => 'ψ', + 120770 => 'ω', + 120771 => '∂', + 120772 => 'ε', + 120773 => 'θ', + 120774 => 'κ', + 120775 => 'φ', + 120776 => 'ρ', + 120777 => 'π', + 120778 => 'ϝ', + 120779 => 'ϝ', + 120782 => '0', + 120783 => '1', + 120784 => '2', + 120785 => '3', + 120786 => '4', + 120787 => '5', + 120788 => '6', + 120789 => '7', + 120790 => '8', + 120791 => '9', + 120792 => '0', + 120793 => '1', + 120794 => '2', + 120795 => '3', + 120796 => '4', + 120797 => '5', + 120798 => '6', + 120799 => '7', + 120800 => '8', + 120801 => '9', + 120802 => '0', + 120803 => '1', + 120804 => '2', + 120805 => '3', + 120806 => '4', + 120807 => '5', + 120808 => '6', + 120809 => '7', + 120810 => '8', + 120811 => '9', + 120812 => '0', + 120813 => '1', + 120814 => '2', + 120815 => '3', + 120816 => '4', + 120817 => '5', + 120818 => '6', + 120819 => '7', + 120820 => '8', + 120821 => '9', + 120822 => '0', + 120823 => '1', + 120824 => '2', + 120825 => '3', + 120826 => '4', + 120827 => '5', + 120828 => '6', + 120829 => '7', + 120830 => '8', + 120831 => '9', + 125184 => '𞤢', + 125185 => '𞤣', + 125186 => '𞤤', + 125187 => '𞤥', + 125188 => '𞤦', + 125189 => '𞤧', + 125190 => '𞤨', + 125191 => '𞤩', + 125192 => '𞤪', + 125193 => '𞤫', + 125194 => '𞤬', + 125195 => '𞤭', + 125196 => '𞤮', + 125197 => '𞤯', + 125198 => '𞤰', + 125199 => '𞤱', + 125200 => '𞤲', + 125201 => '𞤳', + 125202 => '𞤴', + 125203 => '𞤵', + 125204 => '𞤶', + 125205 => '𞤷', + 125206 => '𞤸', + 125207 => '𞤹', + 125208 => '𞤺', + 125209 => '𞤻', + 125210 => '𞤼', + 125211 => '𞤽', + 125212 => '𞤾', + 125213 => '𞤿', + 125214 => '𞥀', + 125215 => '𞥁', + 125216 => '𞥂', + 125217 => '𞥃', + 126464 => 'ا', + 126465 => 'ب', + 126466 => 'ج', + 126467 => 'د', + 126469 => 'و', + 126470 => 'ز', + 126471 => 'ح', + 126472 => 'ط', + 126473 => 'ي', + 126474 => 'ك', + 126475 => 'ل', + 126476 => 'م', + 126477 => 'ن', + 126478 => 'س', + 126479 => 'ع', + 126480 => 'ف', + 126481 => 'ص', + 126482 => 'ق', + 126483 => 'ر', + 126484 => 'ش', + 126485 => 'ت', + 126486 => 'ث', + 126487 => 'خ', + 126488 => 'ذ', + 126489 => 'ض', + 126490 => 'ظ', + 126491 => 'غ', + 126492 => 'ٮ', + 126493 => 'ں', + 126494 => 'ڡ', + 126495 => 'ٯ', + 126497 => 'ب', + 126498 => 'ج', + 126500 => 'ه', + 126503 => 'ح', + 126505 => 'ي', + 126506 => 'ك', + 126507 => 'ل', + 126508 => 'م', + 126509 => 'ن', + 126510 => 'س', + 126511 => 'ع', + 126512 => 'ف', + 126513 => 'ص', + 126514 => 'ق', + 126516 => 'ش', + 126517 => 'ت', + 126518 => 'ث', + 126519 => 'خ', + 126521 => 'ض', + 126523 => 'غ', + 126530 => 'ج', + 126535 => 'ح', + 126537 => 'ي', + 126539 => 'ل', + 126541 => 'ن', + 126542 => 'س', + 126543 => 'ع', + 126545 => 'ص', + 126546 => 'ق', + 126548 => 'ش', + 126551 => 'خ', + 126553 => 'ض', + 126555 => 'غ', + 126557 => 'ں', + 126559 => 'ٯ', + 126561 => 'ب', + 126562 => 'ج', + 126564 => 'ه', + 126567 => 'ح', + 126568 => 'ط', + 126569 => 'ي', + 126570 => 'ك', + 126572 => 'م', + 126573 => 'ن', + 126574 => 'س', + 126575 => 'ع', + 126576 => 'ف', + 126577 => 'ص', + 126578 => 'ق', + 126580 => 'ش', + 126581 => 'ت', + 126582 => 'ث', + 126583 => 'خ', + 126585 => 'ض', + 126586 => 'ظ', + 126587 => 'غ', + 126588 => 'ٮ', + 126590 => 'ڡ', + 126592 => 'ا', + 126593 => 'ب', + 126594 => 'ج', + 126595 => 'د', + 126596 => 'ه', + 126597 => 'و', + 126598 => 'ز', + 126599 => 'ح', + 126600 => 'ط', + 126601 => 'ي', + 126603 => 'ل', + 126604 => 'م', + 126605 => 'ن', + 126606 => 'س', + 126607 => 'ع', + 126608 => 'ف', + 126609 => 'ص', + 126610 => 'ق', + 126611 => 'ر', + 126612 => 'ش', + 126613 => 'ت', + 126614 => 'ث', + 126615 => 'خ', + 126616 => 'ذ', + 126617 => 'ض', + 126618 => 'ظ', + 126619 => 'غ', + 126625 => 'ب', + 126626 => 'ج', + 126627 => 'د', + 126629 => 'و', + 126630 => 'ز', + 126631 => 'ح', + 126632 => 'ط', + 126633 => 'ي', + 126635 => 'ل', + 126636 => 'م', + 126637 => 'ن', + 126638 => 'س', + 126639 => 'ع', + 126640 => 'ف', + 126641 => 'ص', + 126642 => 'ق', + 126643 => 'ر', + 126644 => 'ش', + 126645 => 'ت', + 126646 => 'ث', + 126647 => 'خ', + 126648 => 'ذ', + 126649 => 'ض', + 126650 => 'ظ', + 126651 => 'غ', + 127274 => '〔s〕', + 127275 => 'c', + 127276 => 'r', + 127277 => 'cd', + 127278 => 'wz', + 127280 => 'a', + 127281 => 'b', + 127282 => 'c', + 127283 => 'd', + 127284 => 'e', + 127285 => 'f', + 127286 => 'g', + 127287 => 'h', + 127288 => 'i', + 127289 => 'j', + 127290 => 'k', + 127291 => 'l', + 127292 => 'm', + 127293 => 'n', + 127294 => 'o', + 127295 => 'p', + 127296 => 'q', + 127297 => 'r', + 127298 => 's', + 127299 => 't', + 127300 => 'u', + 127301 => 'v', + 127302 => 'w', + 127303 => 'x', + 127304 => 'y', + 127305 => 'z', + 127306 => 'hv', + 127307 => 'mv', + 127308 => 'sd', + 127309 => 'ss', + 127310 => 'ppv', + 127311 => 'wc', + 127338 => 'mc', + 127339 => 'md', + 127340 => 'mr', + 127376 => 'dj', + 127488 => 'ほか', + 127489 => 'ココ', + 127490 => 'サ', + 127504 => '手', + 127505 => '字', + 127506 => '双', + 127507 => 'デ', + 127508 => '二', + 127509 => '多', + 127510 => '解', + 127511 => '天', + 127512 => '交', + 127513 => '映', + 127514 => '無', + 127515 => '料', + 127516 => '前', + 127517 => '後', + 127518 => '再', + 127519 => '新', + 127520 => '初', + 127521 => '終', + 127522 => '生', + 127523 => '販', + 127524 => '声', + 127525 => '吹', + 127526 => '演', + 127527 => '投', + 127528 => '捕', + 127529 => '一', + 127530 => '三', + 127531 => '遊', + 127532 => '左', + 127533 => '中', + 127534 => '右', + 127535 => '指', + 127536 => '走', + 127537 => '打', + 127538 => '禁', + 127539 => '空', + 127540 => '合', + 127541 => '満', + 127542 => '有', + 127543 => '月', + 127544 => '申', + 127545 => '割', + 127546 => '営', + 127547 => '配', + 127552 => '〔本〕', + 127553 => '〔三〕', + 127554 => '〔二〕', + 127555 => '〔安〕', + 127556 => '〔点〕', + 127557 => '〔打〕', + 127558 => '〔盗〕', + 127559 => '〔勝〕', + 127560 => '〔敗〕', + 127568 => '得', + 127569 => '可', + 130032 => '0', + 130033 => '1', + 130034 => '2', + 130035 => '3', + 130036 => '4', + 130037 => '5', + 130038 => '6', + 130039 => '7', + 130040 => '8', + 130041 => '9', + 194560 => '丽', + 194561 => '丸', + 194562 => '乁', + 194563 => '𠄢', + 194564 => '你', + 194565 => '侮', + 194566 => '侻', + 194567 => '倂', + 194568 => '偺', + 194569 => '備', + 194570 => '僧', + 194571 => '像', + 194572 => '㒞', + 194573 => '𠘺', + 194574 => '免', + 194575 => '兔', + 194576 => '兤', + 194577 => '具', + 194578 => '𠔜', + 194579 => '㒹', + 194580 => '內', + 194581 => '再', + 194582 => '𠕋', + 194583 => '冗', + 194584 => '冤', + 194585 => '仌', + 194586 => '冬', + 194587 => '况', + 194588 => '𩇟', + 194589 => '凵', + 194590 => '刃', + 194591 => '㓟', + 194592 => '刻', + 194593 => '剆', + 194594 => '割', + 194595 => '剷', + 194596 => '㔕', + 194597 => '勇', + 194598 => '勉', + 194599 => '勤', + 194600 => '勺', + 194601 => '包', + 194602 => '匆', + 194603 => '北', + 194604 => '卉', + 194605 => '卑', + 194606 => '博', + 194607 => '即', + 194608 => '卽', + 194609 => '卿', + 194610 => '卿', + 194611 => '卿', + 194612 => '𠨬', + 194613 => '灰', + 194614 => '及', + 194615 => '叟', + 194616 => '𠭣', + 194617 => '叫', + 194618 => '叱', + 194619 => '吆', + 194620 => '咞', + 194621 => '吸', + 194622 => '呈', + 194623 => '周', + 194624 => '咢', + 194625 => '哶', + 194626 => '唐', + 194627 => '啓', + 194628 => '啣', + 194629 => '善', + 194630 => '善', + 194631 => '喙', + 194632 => '喫', + 194633 => '喳', + 194634 => '嗂', + 194635 => '圖', + 194636 => '嘆', + 194637 => '圗', + 194638 => '噑', + 194639 => '噴', + 194640 => '切', + 194641 => '壮', + 194642 => '城', + 194643 => '埴', + 194644 => '堍', + 194645 => '型', + 194646 => '堲', + 194647 => '報', + 194648 => '墬', + 194649 => '𡓤', + 194650 => '売', + 194651 => '壷', + 194652 => '夆', + 194653 => '多', + 194654 => '夢', + 194655 => '奢', + 194656 => '𡚨', + 194657 => '𡛪', + 194658 => '姬', + 194659 => '娛', + 194660 => '娧', + 194661 => '姘', + 194662 => '婦', + 194663 => '㛮', + 194665 => '嬈', + 194666 => '嬾', + 194667 => '嬾', + 194668 => '𡧈', + 194669 => '寃', + 194670 => '寘', + 194671 => '寧', + 194672 => '寳', + 194673 => '𡬘', + 194674 => '寿', + 194675 => '将', + 194677 => '尢', + 194678 => '㞁', + 194679 => '屠', + 194680 => '屮', + 194681 => '峀', + 194682 => '岍', + 194683 => '𡷤', + 194684 => '嵃', + 194685 => '𡷦', + 194686 => '嵮', + 194687 => '嵫', + 194688 => '嵼', + 194689 => '巡', + 194690 => '巢', + 194691 => '㠯', + 194692 => '巽', + 194693 => '帨', + 194694 => '帽', + 194695 => '幩', + 194696 => '㡢', + 194697 => '𢆃', + 194698 => '㡼', + 194699 => '庰', + 194700 => '庳', + 194701 => '庶', + 194702 => '廊', + 194703 => '𪎒', + 194704 => '廾', + 194705 => '𢌱', + 194706 => '𢌱', + 194707 => '舁', + 194708 => '弢', + 194709 => '弢', + 194710 => '㣇', + 194711 => '𣊸', + 194712 => '𦇚', + 194713 => '形', + 194714 => '彫', + 194715 => '㣣', + 194716 => '徚', + 194717 => '忍', + 194718 => '志', + 194719 => '忹', + 194720 => '悁', + 194721 => '㤺', + 194722 => '㤜', + 194723 => '悔', + 194724 => '𢛔', + 194725 => '惇', + 194726 => '慈', + 194727 => '慌', + 194728 => '慎', + 194729 => '慌', + 194730 => '慺', + 194731 => '憎', + 194732 => '憲', + 194733 => '憤', + 194734 => '憯', + 194735 => '懞', + 194736 => '懲', + 194737 => '懶', + 194738 => '成', + 194739 => '戛', + 194740 => '扝', + 194741 => '抱', + 194742 => '拔', + 194743 => '捐', + 194744 => '𢬌', + 194745 => '挽', + 194746 => '拼', + 194747 => '捨', + 194748 => '掃', + 194749 => '揤', + 194750 => '𢯱', + 194751 => '搢', + 194752 => '揅', + 194753 => '掩', + 194754 => '㨮', + 194755 => '摩', + 194756 => '摾', + 194757 => '撝', + 194758 => '摷', + 194759 => '㩬', + 194760 => '敏', + 194761 => '敬', + 194762 => '𣀊', + 194763 => '旣', + 194764 => '書', + 194765 => '晉', + 194766 => '㬙', + 194767 => '暑', + 194768 => '㬈', + 194769 => '㫤', + 194770 => '冒', + 194771 => '冕', + 194772 => '最', + 194773 => '暜', + 194774 => '肭', + 194775 => '䏙', + 194776 => '朗', + 194777 => '望', + 194778 => '朡', + 194779 => '杞', + 194780 => '杓', + 194781 => '𣏃', + 194782 => '㭉', + 194783 => '柺', + 194784 => '枅', + 194785 => '桒', + 194786 => '梅', + 194787 => '𣑭', + 194788 => '梎', + 194789 => '栟', + 194790 => '椔', + 194791 => '㮝', + 194792 => '楂', + 194793 => '榣', + 194794 => '槪', + 194795 => '檨', + 194796 => '𣚣', + 194797 => '櫛', + 194798 => '㰘', + 194799 => '次', + 194800 => '𣢧', + 194801 => '歔', + 194802 => '㱎', + 194803 => '歲', + 194804 => '殟', + 194805 => '殺', + 194806 => '殻', + 194807 => '𣪍', + 194808 => '𡴋', + 194809 => '𣫺', + 194810 => '汎', + 194811 => '𣲼', + 194812 => '沿', + 194813 => '泍', + 194814 => '汧', + 194815 => '洖', + 194816 => '派', + 194817 => '海', + 194818 => '流', + 194819 => '浩', + 194820 => '浸', + 194821 => '涅', + 194822 => '𣴞', + 194823 => '洴', + 194824 => '港', + 194825 => '湮', + 194826 => '㴳', + 194827 => '滋', + 194828 => '滇', + 194829 => '𣻑', + 194830 => '淹', + 194831 => '潮', + 194832 => '𣽞', + 194833 => '𣾎', + 194834 => '濆', + 194835 => '瀹', + 194836 => '瀞', + 194837 => '瀛', + 194838 => '㶖', + 194839 => '灊', + 194840 => '災', + 194841 => '灷', + 194842 => '炭', + 194843 => '𠔥', + 194844 => '煅', + 194845 => '𤉣', + 194846 => '熜', + 194848 => '爨', + 194849 => '爵', + 194850 => '牐', + 194851 => '𤘈', + 194852 => '犀', + 194853 => '犕', + 194854 => '𤜵', + 194855 => '𤠔', + 194856 => '獺', + 194857 => '王', + 194858 => '㺬', + 194859 => '玥', + 194860 => '㺸', + 194861 => '㺸', + 194862 => '瑇', + 194863 => '瑜', + 194864 => '瑱', + 194865 => '璅', + 194866 => '瓊', + 194867 => '㼛', + 194868 => '甤', + 194869 => '𤰶', + 194870 => '甾', + 194871 => '𤲒', + 194872 => '異', + 194873 => '𢆟', + 194874 => '瘐', + 194875 => '𤾡', + 194876 => '𤾸', + 194877 => '𥁄', + 194878 => '㿼', + 194879 => '䀈', + 194880 => '直', + 194881 => '𥃳', + 194882 => '𥃲', + 194883 => '𥄙', + 194884 => '𥄳', + 194885 => '眞', + 194886 => '真', + 194887 => '真', + 194888 => '睊', + 194889 => '䀹', + 194890 => '瞋', + 194891 => '䁆', + 194892 => '䂖', + 194893 => '𥐝', + 194894 => '硎', + 194895 => '碌', + 194896 => '磌', + 194897 => '䃣', + 194898 => '𥘦', + 194899 => '祖', + 194900 => '𥚚', + 194901 => '𥛅', + 194902 => '福', + 194903 => '秫', + 194904 => '䄯', + 194905 => '穀', + 194906 => '穊', + 194907 => '穏', + 194908 => '𥥼', + 194909 => '𥪧', + 194910 => '𥪧', + 194912 => '䈂', + 194913 => '𥮫', + 194914 => '篆', + 194915 => '築', + 194916 => '䈧', + 194917 => '𥲀', + 194918 => '糒', + 194919 => '䊠', + 194920 => '糨', + 194921 => '糣', + 194922 => '紀', + 194923 => '𥾆', + 194924 => '絣', + 194925 => '䌁', + 194926 => '緇', + 194927 => '縂', + 194928 => '繅', + 194929 => '䌴', + 194930 => '𦈨', + 194931 => '𦉇', + 194932 => '䍙', + 194933 => '𦋙', + 194934 => '罺', + 194935 => '𦌾', + 194936 => '羕', + 194937 => '翺', + 194938 => '者', + 194939 => '𦓚', + 194940 => '𦔣', + 194941 => '聠', + 194942 => '𦖨', + 194943 => '聰', + 194944 => '𣍟', + 194945 => '䏕', + 194946 => '育', + 194947 => '脃', + 194948 => '䐋', + 194949 => '脾', + 194950 => '媵', + 194951 => '𦞧', + 194952 => '𦞵', + 194953 => '𣎓', + 194954 => '𣎜', + 194955 => '舁', + 194956 => '舄', + 194957 => '辞', + 194958 => '䑫', + 194959 => '芑', + 194960 => '芋', + 194961 => '芝', + 194962 => '劳', + 194963 => '花', + 194964 => '芳', + 194965 => '芽', + 194966 => '苦', + 194967 => '𦬼', + 194968 => '若', + 194969 => '茝', + 194970 => '荣', + 194971 => '莭', + 194972 => '茣', + 194973 => '莽', + 194974 => '菧', + 194975 => '著', + 194976 => '荓', + 194977 => '菊', + 194978 => '菌', + 194979 => '菜', + 194980 => '𦰶', + 194981 => '𦵫', + 194982 => '𦳕', + 194983 => '䔫', + 194984 => '蓱', + 194985 => '蓳', + 194986 => '蔖', + 194987 => '𧏊', + 194988 => '蕤', + 194989 => '𦼬', + 194990 => '䕝', + 194991 => '䕡', + 194992 => '𦾱', + 194993 => '𧃒', + 194994 => '䕫', + 194995 => '虐', + 194996 => '虜', + 194997 => '虧', + 194998 => '虩', + 194999 => '蚩', + 195000 => '蚈', + 195001 => '蜎', + 195002 => '蛢', + 195003 => '蝹', + 195004 => '蜨', + 195005 => '蝫', + 195006 => '螆', + 195008 => '蟡', + 195009 => '蠁', + 195010 => '䗹', + 195011 => '衠', + 195012 => '衣', + 195013 => '𧙧', + 195014 => '裗', + 195015 => '裞', + 195016 => '䘵', + 195017 => '裺', + 195018 => '㒻', + 195019 => '𧢮', + 195020 => '𧥦', + 195021 => '䚾', + 195022 => '䛇', + 195023 => '誠', + 195024 => '諭', + 195025 => '變', + 195026 => '豕', + 195027 => '𧲨', + 195028 => '貫', + 195029 => '賁', + 195030 => '贛', + 195031 => '起', + 195032 => '𧼯', + 195033 => '𠠄', + 195034 => '跋', + 195035 => '趼', + 195036 => '跰', + 195037 => '𠣞', + 195038 => '軔', + 195039 => '輸', + 195040 => '𨗒', + 195041 => '𨗭', + 195042 => '邔', + 195043 => '郱', + 195044 => '鄑', + 195045 => '𨜮', + 195046 => '鄛', + 195047 => '鈸', + 195048 => '鋗', + 195049 => '鋘', + 195050 => '鉼', + 195051 => '鏹', + 195052 => '鐕', + 195053 => '𨯺', + 195054 => '開', + 195055 => '䦕', + 195056 => '閷', + 195057 => '𨵷', + 195058 => '䧦', + 195059 => '雃', + 195060 => '嶲', + 195061 => '霣', + 195062 => '𩅅', + 195063 => '𩈚', + 195064 => '䩮', + 195065 => '䩶', + 195066 => '韠', + 195067 => '𩐊', + 195068 => '䪲', + 195069 => '𩒖', + 195070 => '頋', + 195071 => '頋', + 195072 => '頩', + 195073 => '𩖶', + 195074 => '飢', + 195075 => '䬳', + 195076 => '餩', + 195077 => '馧', + 195078 => '駂', + 195079 => '駾', + 195080 => '䯎', + 195081 => '𩬰', + 195082 => '鬒', + 195083 => '鱀', + 195084 => '鳽', + 195085 => '䳎', + 195086 => '䳭', + 195087 => '鵧', + 195088 => '𪃎', + 195089 => '䳸', + 195090 => '𪄅', + 195091 => '𪈎', + 195092 => '𪊑', + 195093 => '麻', + 195094 => '䵖', + 195095 => '黹', + 195096 => '黾', + 195097 => '鼅', + 195098 => '鼏', + 195099 => '鼖', + 195100 => '鼻', + 195101 => '𪘀', +); diff --git a/vendor/symfony/polyfill-intl-idn/Resources/unidata/virama.php b/vendor/symfony/polyfill-intl-idn/Resources/unidata/virama.php new file mode 100644 index 0000000..1958e37 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/Resources/unidata/virama.php @@ -0,0 +1,65 @@ + 9, + 2509 => 9, + 2637 => 9, + 2765 => 9, + 2893 => 9, + 3021 => 9, + 3149 => 9, + 3277 => 9, + 3387 => 9, + 3388 => 9, + 3405 => 9, + 3530 => 9, + 3642 => 9, + 3770 => 9, + 3972 => 9, + 4153 => 9, + 4154 => 9, + 5908 => 9, + 5940 => 9, + 6098 => 9, + 6752 => 9, + 6980 => 9, + 7082 => 9, + 7083 => 9, + 7154 => 9, + 7155 => 9, + 11647 => 9, + 43014 => 9, + 43052 => 9, + 43204 => 9, + 43347 => 9, + 43456 => 9, + 43766 => 9, + 44013 => 9, + 68159 => 9, + 69702 => 9, + 69759 => 9, + 69817 => 9, + 69939 => 9, + 69940 => 9, + 70080 => 9, + 70197 => 9, + 70378 => 9, + 70477 => 9, + 70722 => 9, + 70850 => 9, + 71103 => 9, + 71231 => 9, + 71350 => 9, + 71467 => 9, + 71737 => 9, + 71997 => 9, + 71998 => 9, + 72160 => 9, + 72244 => 9, + 72263 => 9, + 72345 => 9, + 72767 => 9, + 73028 => 9, + 73029 => 9, + 73111 => 9, +); diff --git a/vendor/symfony/polyfill-intl-idn/bootstrap.php b/vendor/symfony/polyfill-intl-idn/bootstrap.php new file mode 100644 index 0000000..57c7835 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/bootstrap.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Idn as p; + +if (extension_loaded('intl')) { + return; +} + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!defined('U_IDNA_PROHIBITED_ERROR')) { + define('U_IDNA_PROHIBITED_ERROR', 66560); +} +if (!defined('U_IDNA_ERROR_START')) { + define('U_IDNA_ERROR_START', 66560); +} +if (!defined('U_IDNA_UNASSIGNED_ERROR')) { + define('U_IDNA_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_IDNA_CHECK_BIDI_ERROR')) { + define('U_IDNA_CHECK_BIDI_ERROR', 66562); +} +if (!defined('U_IDNA_STD3_ASCII_RULES_ERROR')) { + define('U_IDNA_STD3_ASCII_RULES_ERROR', 66563); +} +if (!defined('U_IDNA_ACE_PREFIX_ERROR')) { + define('U_IDNA_ACE_PREFIX_ERROR', 66564); +} +if (!defined('U_IDNA_VERIFICATION_ERROR')) { + define('U_IDNA_VERIFICATION_ERROR', 66565); +} +if (!defined('U_IDNA_LABEL_TOO_LONG_ERROR')) { + define('U_IDNA_LABEL_TOO_LONG_ERROR', 66566); +} +if (!defined('U_IDNA_ZERO_LENGTH_LABEL_ERROR')) { + define('U_IDNA_ZERO_LENGTH_LABEL_ERROR', 66567); +} +if (!defined('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR')) { + define('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR', 66568); +} +if (!defined('U_IDNA_ERROR_LIMIT')) { + define('U_IDNA_ERROR_LIMIT', 66569); +} +if (!defined('U_STRINGPREP_PROHIBITED_ERROR')) { + define('U_STRINGPREP_PROHIBITED_ERROR', 66560); +} +if (!defined('U_STRINGPREP_UNASSIGNED_ERROR')) { + define('U_STRINGPREP_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_STRINGPREP_CHECK_BIDI_ERROR')) { + define('U_STRINGPREP_CHECK_BIDI_ERROR', 66562); +} +if (!defined('IDNA_DEFAULT')) { + define('IDNA_DEFAULT', 0); +} +if (!defined('IDNA_ALLOW_UNASSIGNED')) { + define('IDNA_ALLOW_UNASSIGNED', 1); +} +if (!defined('IDNA_USE_STD3_RULES')) { + define('IDNA_USE_STD3_RULES', 2); +} +if (!defined('IDNA_CHECK_BIDI')) { + define('IDNA_CHECK_BIDI', 4); +} +if (!defined('IDNA_CHECK_CONTEXTJ')) { + define('IDNA_CHECK_CONTEXTJ', 8); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_ASCII')) { + define('IDNA_NONTRANSITIONAL_TO_ASCII', 16); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_UNICODE')) { + define('IDNA_NONTRANSITIONAL_TO_UNICODE', 32); +} +if (!defined('INTL_IDNA_VARIANT_2003')) { + define('INTL_IDNA_VARIANT_2003', 0); +} +if (!defined('INTL_IDNA_VARIANT_UTS46')) { + define('INTL_IDNA_VARIANT_UTS46', 1); +} +if (!defined('IDNA_ERROR_EMPTY_LABEL')) { + define('IDNA_ERROR_EMPTY_LABEL', 1); +} +if (!defined('IDNA_ERROR_LABEL_TOO_LONG')) { + define('IDNA_ERROR_LABEL_TOO_LONG', 2); +} +if (!defined('IDNA_ERROR_DOMAIN_NAME_TOO_LONG')) { + define('IDNA_ERROR_DOMAIN_NAME_TOO_LONG', 4); +} +if (!defined('IDNA_ERROR_LEADING_HYPHEN')) { + define('IDNA_ERROR_LEADING_HYPHEN', 8); +} +if (!defined('IDNA_ERROR_TRAILING_HYPHEN')) { + define('IDNA_ERROR_TRAILING_HYPHEN', 16); +} +if (!defined('IDNA_ERROR_HYPHEN_3_4')) { + define('IDNA_ERROR_HYPHEN_3_4', 32); +} +if (!defined('IDNA_ERROR_LEADING_COMBINING_MARK')) { + define('IDNA_ERROR_LEADING_COMBINING_MARK', 64); +} +if (!defined('IDNA_ERROR_DISALLOWED')) { + define('IDNA_ERROR_DISALLOWED', 128); +} +if (!defined('IDNA_ERROR_PUNYCODE')) { + define('IDNA_ERROR_PUNYCODE', 256); +} +if (!defined('IDNA_ERROR_LABEL_HAS_DOT')) { + define('IDNA_ERROR_LABEL_HAS_DOT', 512); +} +if (!defined('IDNA_ERROR_INVALID_ACE_LABEL')) { + define('IDNA_ERROR_INVALID_ACE_LABEL', 1024); +} +if (!defined('IDNA_ERROR_BIDI')) { + define('IDNA_ERROR_BIDI', 2048); +} +if (!defined('IDNA_ERROR_CONTEXTJ')) { + define('IDNA_ERROR_CONTEXTJ', 4096); +} + +if (\PHP_VERSION_ID < 70400) { + if (!function_exists('idn_to_ascii')) { + function idn_to_ascii($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_2003, &$idna_info = null) { return p\Idn::idn_to_ascii($domain, $flags, $variant, $idna_info); } + } + if (!function_exists('idn_to_utf8')) { + function idn_to_utf8($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_2003, &$idna_info = null) { return p\Idn::idn_to_utf8($domain, $flags, $variant, $idna_info); } + } +} else { + if (!function_exists('idn_to_ascii')) { + function idn_to_ascii($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_UTS46, &$idna_info = null) { return p\Idn::idn_to_ascii($domain, $flags, $variant, $idna_info); } + } + if (!function_exists('idn_to_utf8')) { + function idn_to_utf8($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_UTS46, &$idna_info = null) { return p\Idn::idn_to_utf8($domain, $flags, $variant, $idna_info); } + } +} diff --git a/vendor/symfony/polyfill-intl-idn/bootstrap80.php b/vendor/symfony/polyfill-intl-idn/bootstrap80.php new file mode 100644 index 0000000..a62c2d6 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/bootstrap80.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Idn as p; + +if (!defined('U_IDNA_PROHIBITED_ERROR')) { + define('U_IDNA_PROHIBITED_ERROR', 66560); +} +if (!defined('U_IDNA_ERROR_START')) { + define('U_IDNA_ERROR_START', 66560); +} +if (!defined('U_IDNA_UNASSIGNED_ERROR')) { + define('U_IDNA_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_IDNA_CHECK_BIDI_ERROR')) { + define('U_IDNA_CHECK_BIDI_ERROR', 66562); +} +if (!defined('U_IDNA_STD3_ASCII_RULES_ERROR')) { + define('U_IDNA_STD3_ASCII_RULES_ERROR', 66563); +} +if (!defined('U_IDNA_ACE_PREFIX_ERROR')) { + define('U_IDNA_ACE_PREFIX_ERROR', 66564); +} +if (!defined('U_IDNA_VERIFICATION_ERROR')) { + define('U_IDNA_VERIFICATION_ERROR', 66565); +} +if (!defined('U_IDNA_LABEL_TOO_LONG_ERROR')) { + define('U_IDNA_LABEL_TOO_LONG_ERROR', 66566); +} +if (!defined('U_IDNA_ZERO_LENGTH_LABEL_ERROR')) { + define('U_IDNA_ZERO_LENGTH_LABEL_ERROR', 66567); +} +if (!defined('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR')) { + define('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR', 66568); +} +if (!defined('U_IDNA_ERROR_LIMIT')) { + define('U_IDNA_ERROR_LIMIT', 66569); +} +if (!defined('U_STRINGPREP_PROHIBITED_ERROR')) { + define('U_STRINGPREP_PROHIBITED_ERROR', 66560); +} +if (!defined('U_STRINGPREP_UNASSIGNED_ERROR')) { + define('U_STRINGPREP_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_STRINGPREP_CHECK_BIDI_ERROR')) { + define('U_STRINGPREP_CHECK_BIDI_ERROR', 66562); +} +if (!defined('IDNA_DEFAULT')) { + define('IDNA_DEFAULT', 0); +} +if (!defined('IDNA_ALLOW_UNASSIGNED')) { + define('IDNA_ALLOW_UNASSIGNED', 1); +} +if (!defined('IDNA_USE_STD3_RULES')) { + define('IDNA_USE_STD3_RULES', 2); +} +if (!defined('IDNA_CHECK_BIDI')) { + define('IDNA_CHECK_BIDI', 4); +} +if (!defined('IDNA_CHECK_CONTEXTJ')) { + define('IDNA_CHECK_CONTEXTJ', 8); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_ASCII')) { + define('IDNA_NONTRANSITIONAL_TO_ASCII', 16); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_UNICODE')) { + define('IDNA_NONTRANSITIONAL_TO_UNICODE', 32); +} +if (!defined('INTL_IDNA_VARIANT_UTS46')) { + define('INTL_IDNA_VARIANT_UTS46', 1); +} +if (!defined('IDNA_ERROR_EMPTY_LABEL')) { + define('IDNA_ERROR_EMPTY_LABEL', 1); +} +if (!defined('IDNA_ERROR_LABEL_TOO_LONG')) { + define('IDNA_ERROR_LABEL_TOO_LONG', 2); +} +if (!defined('IDNA_ERROR_DOMAIN_NAME_TOO_LONG')) { + define('IDNA_ERROR_DOMAIN_NAME_TOO_LONG', 4); +} +if (!defined('IDNA_ERROR_LEADING_HYPHEN')) { + define('IDNA_ERROR_LEADING_HYPHEN', 8); +} +if (!defined('IDNA_ERROR_TRAILING_HYPHEN')) { + define('IDNA_ERROR_TRAILING_HYPHEN', 16); +} +if (!defined('IDNA_ERROR_HYPHEN_3_4')) { + define('IDNA_ERROR_HYPHEN_3_4', 32); +} +if (!defined('IDNA_ERROR_LEADING_COMBINING_MARK')) { + define('IDNA_ERROR_LEADING_COMBINING_MARK', 64); +} +if (!defined('IDNA_ERROR_DISALLOWED')) { + define('IDNA_ERROR_DISALLOWED', 128); +} +if (!defined('IDNA_ERROR_PUNYCODE')) { + define('IDNA_ERROR_PUNYCODE', 256); +} +if (!defined('IDNA_ERROR_LABEL_HAS_DOT')) { + define('IDNA_ERROR_LABEL_HAS_DOT', 512); +} +if (!defined('IDNA_ERROR_INVALID_ACE_LABEL')) { + define('IDNA_ERROR_INVALID_ACE_LABEL', 1024); +} +if (!defined('IDNA_ERROR_BIDI')) { + define('IDNA_ERROR_BIDI', 2048); +} +if (!defined('IDNA_ERROR_CONTEXTJ')) { + define('IDNA_ERROR_CONTEXTJ', 4096); +} + +if (!function_exists('idn_to_ascii')) { + function idn_to_ascii(?string $domain, ?int $flags = IDNA_DEFAULT, ?int $variant = INTL_IDNA_VARIANT_UTS46, &$idna_info = null): string|false { return p\Idn::idn_to_ascii((string) $domain, (int) $flags, (int) $variant, $idna_info); } +} +if (!function_exists('idn_to_utf8')) { + function idn_to_utf8(?string $domain, ?int $flags = IDNA_DEFAULT, ?int $variant = INTL_IDNA_VARIANT_UTS46, &$idna_info = null): string|false { return p\Idn::idn_to_utf8((string) $domain, (int) $flags, (int) $variant, $idna_info); } +} diff --git a/vendor/symfony/polyfill-intl-idn/composer.json b/vendor/symfony/polyfill-intl-idn/composer.json new file mode 100644 index 0000000..71030a2 --- /dev/null +++ b/vendor/symfony/polyfill-intl-idn/composer.json @@ -0,0 +1,44 @@ +{ + "name": "symfony/polyfill-intl-idn", + "type": "library", + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "idn"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Intl\\Idn\\": "" }, + "files": [ "bootstrap.php" ] + }, + "suggest": { + "ext-intl": "For best performance" + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/vendor/symfony/process/ExecutableFinder.php b/vendor/symfony/process/ExecutableFinder.php index cb4345e..e2dd064 100644 --- a/vendor/symfony/process/ExecutableFinder.php +++ b/vendor/symfony/process/ExecutableFinder.php @@ -50,8 +50,8 @@ class ExecutableFinder */ public function find($name, $default = null, array $extraDirs = []) { - if (ini_get('open_basedir')) { - $searchPath = array_merge(explode(PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs); + if (\ini_get('open_basedir')) { + $searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs); $dirs = []; foreach ($searchPath as $path) { // Silencing against https://bugs.php.net/69240 @@ -65,7 +65,7 @@ class ExecutableFinder } } else { $dirs = array_merge( - explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), + explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), $extraDirs ); } @@ -73,7 +73,7 @@ class ExecutableFinder $suffixes = ['']; if ('\\' === \DIRECTORY_SEPARATOR) { $pathExt = getenv('PATHEXT'); - $suffixes = array_merge($pathExt ? explode(PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes); + $suffixes = array_merge($pathExt ? explode(\PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes); } foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { diff --git a/vendor/symfony/process/InputStream.php b/vendor/symfony/process/InputStream.php index c952daf..4f8f713 100644 --- a/vendor/symfony/process/InputStream.php +++ b/vendor/symfony/process/InputStream.php @@ -45,7 +45,7 @@ class InputStream implements \IteratorAggregate return; } if ($this->isClosed()) { - throw new RuntimeException(sprintf('%s is closed', static::class)); + throw new RuntimeException(sprintf('"%s" is closed.', static::class)); } $this->input[] = ProcessUtils::validateInput(__METHOD__, $input); } @@ -69,6 +69,7 @@ class InputStream implements \IteratorAggregate /** * @return \Traversable */ + #[\ReturnTypeWillChange] public function getIterator() { $this->open = true; diff --git a/vendor/symfony/process/LICENSE b/vendor/symfony/process/LICENSE index 9e936ec..88bf75b 100644 --- a/vendor/symfony/process/LICENSE +++ b/vendor/symfony/process/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2020 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/vendor/symfony/process/PhpExecutableFinder.php b/vendor/symfony/process/PhpExecutableFinder.php index 5b8f1fc..92e0262 100644 --- a/vendor/symfony/process/PhpExecutableFinder.php +++ b/vendor/symfony/process/PhpExecutableFinder.php @@ -38,7 +38,7 @@ class PhpExecutableFinder if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v'; - if ($php = strtok(exec($command.' '.escapeshellarg($php)), PHP_EOL)) { + if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { if (!is_executable($php)) { return false; } @@ -47,6 +47,10 @@ class PhpExecutableFinder } } + if (@is_dir($php)) { + return false; + } + return $php; } @@ -54,12 +58,12 @@ class PhpExecutableFinder $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; // PHP_BINARY return the current sapi executable - if (PHP_BINARY && \in_array(\PHP_SAPI, ['cgi-fcgi', 'cli', 'cli-server', 'phpdbg'], true)) { - return PHP_BINARY.$args; + if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cgi-fcgi', 'cli', 'cli-server', 'phpdbg'], true)) { + return \PHP_BINARY.$args; } if ($php = getenv('PHP_PATH')) { - if (!@is_executable($php)) { + if (!@is_executable($php) || @is_dir($php)) { return false; } @@ -67,16 +71,16 @@ class PhpExecutableFinder } if ($php = getenv('PHP_PEAR_PHP_BIN')) { - if (@is_executable($php)) { + if (@is_executable($php) && !@is_dir($php)) { return $php; } } - if (@is_executable($php = PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php'))) { + if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php')) && !@is_dir($php)) { return $php; } - $dirs = [PHP_BINDIR]; + $dirs = [\PHP_BINDIR]; if ('\\' === \DIRECTORY_SEPARATOR) { $dirs[] = 'C:\xampp\php\\'; } diff --git a/vendor/symfony/process/PhpProcess.php b/vendor/symfony/process/PhpProcess.php index 22fc1b3..dc064e0 100644 --- a/vendor/symfony/process/PhpProcess.php +++ b/vendor/symfony/process/PhpProcess.php @@ -65,7 +65,7 @@ class PhpProcess extends Process */ public function setPhpBinary($php) { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the $php argument of the constructor instead.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the $php argument of the constructor instead.', __METHOD__), \E_USER_DEPRECATED); $this->setCommandLine($php); } diff --git a/vendor/symfony/process/Process.php b/vendor/symfony/process/Process.php index 3b8c4cb..09cd960 100644 --- a/vendor/symfony/process/Process.php +++ b/vendor/symfony/process/Process.php @@ -30,30 +30,30 @@ use Symfony\Component\Process\Pipes\WindowsPipes; */ class Process implements \IteratorAggregate { - const ERR = 'err'; - const OUT = 'out'; + public const ERR = 'err'; + public const OUT = 'out'; - const STATUS_READY = 'ready'; - const STATUS_STARTED = 'started'; - const STATUS_TERMINATED = 'terminated'; + public const STATUS_READY = 'ready'; + public const STATUS_STARTED = 'started'; + public const STATUS_TERMINATED = 'terminated'; - const STDIN = 0; - const STDOUT = 1; - const STDERR = 2; + public const STDIN = 0; + public const STDOUT = 1; + public const STDERR = 2; // Timeout Precision in seconds. - const TIMEOUT_PRECISION = 0.2; + public const TIMEOUT_PRECISION = 0.2; - const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking - const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory - const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating - const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating + public const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking + public const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory + public const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating + public const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating private $callback; private $hasCallback = false; private $commandline; private $cwd; - private $env; + private $env = []; private $input; private $starttime; private $lastOutputTime; @@ -132,7 +132,7 @@ class Process implements \IteratorAggregate * @param array $command The command to run and its arguments listed as separate entries * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process - * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input + * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input * @param int|float|null $timeout The timeout in seconds or null to disable * * @throws LogicException When proc_open is not installed @@ -144,7 +144,7 @@ class Process implements \IteratorAggregate } if (!\is_array($command)) { - @trigger_error(sprintf('Passing a command as string when creating a "%s" instance is deprecated since Symfony 4.2, pass it as an array of its arguments instead, or use the "Process::fromShellCommandline()" constructor if you need features provided by the shell.', __CLASS__), E_USER_DEPRECATED); + @trigger_error(sprintf('Passing a command as string when creating a "%s" instance is deprecated since Symfony 4.2, pass it as an array of its arguments instead, or use the "Process::fromShellCommandline()" constructor if you need features provided by the shell.', __CLASS__), \E_USER_DEPRECATED); } $this->commandline = $command; @@ -177,13 +177,13 @@ class Process implements \IteratorAggregate * In order to inject dynamic values into command-lines, we strongly recommend using placeholders. * This will save escaping values, which is not portable nor secure anyway: * - * $process = Process::fromShellCommandline('my_command "$MY_VAR"'); + * $process = Process::fromShellCommandline('my_command "${:MY_VAR}"'); * $process->run(null, ['MY_VAR' => $theValue]); * * @param string $command The command line to pass to the shell of the OS * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process - * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input + * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input * @param int|float|null $timeout The timeout in seconds or null to disable * * @return static @@ -198,6 +198,19 @@ class Process implements \IteratorAggregate return $process; } + /** + * @return array + */ + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + public function __destruct() { $this->stop(0); @@ -281,7 +294,7 @@ class Process implements \IteratorAggregate public function start(callable $callback = null, array $env = []) { if ($this->isRunning()) { - throw new RuntimeException('Process is already running'); + throw new RuntimeException('Process is already running.'); } $this->resetProcessData(); @@ -291,10 +304,10 @@ class Process implements \IteratorAggregate $descriptors = $this->getDescriptors(); if ($this->env) { - $env += $this->env; + $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env; } - $env += $this->getDefaultEnv(); + $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv(); if (\is_array($commandline = $this->commandline)) { $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline)); @@ -327,7 +340,7 @@ class Process implements \IteratorAggregate $envPairs = []; foreach ($env as $k => $v) { - if (false !== $v) { + if (false !== $v && false === \in_array($k, ['argc', 'argv', 'ARGC', 'ARGV'], true)) { $envPairs[] = $k.'='.$v; } } @@ -336,7 +349,7 @@ class Process implements \IteratorAggregate throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd)); } - $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $options); + $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $options); if (!\is_resource($this->process)) { throw new RuntimeException('Unable to launch a new process.'); @@ -375,7 +388,7 @@ class Process implements \IteratorAggregate public function restart(callable $callback = null, array $env = []): self { if ($this->isRunning()) { - throw new RuntimeException('Process is already running'); + throw new RuntimeException('Process is already running.'); } $process = clone $this; @@ -408,7 +421,7 @@ class Process implements \IteratorAggregate if (null !== $callback) { if (!$this->processPipes->haveReadSupport()) { $this->stop(0); - throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait"'); + throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".'); } $this->callback = $this->buildCallback($callback); } @@ -609,6 +622,7 @@ class Process implements \IteratorAggregate * * @return \Generator */ + #[\ReturnTypeWillChange] public function getIterator($flags = 0) { $this->readPipesForOutput(__FUNCTION__, false); @@ -758,7 +772,7 @@ class Process implements \IteratorAggregate return null; } - return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; + return self::$exitCodes[$exitcode] ?? 'Unknown error'; } /** @@ -938,7 +952,7 @@ class Process implements \IteratorAggregate { $this->lastOutputTime = microtime(true); - fseek($this->stdout, 0, SEEK_END); + fseek($this->stdout, 0, \SEEK_END); fwrite($this->stdout, $line); fseek($this->stdout, $this->incrementalOutputOffset); } @@ -952,15 +966,13 @@ class Process implements \IteratorAggregate { $this->lastOutputTime = microtime(true); - fseek($this->stderr, 0, SEEK_END); + fseek($this->stderr, 0, \SEEK_END); fwrite($this->stderr, $line); fseek($this->stderr, $this->incrementalErrorOutputOffset); } /** * Gets the last output time in seconds. - * - * @return float|null The last output time in seconds or null if it isn't started */ public function getLastOutputTime(): ?float { @@ -988,7 +1000,7 @@ class Process implements \IteratorAggregate */ public function setCommandLine($commandline) { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); $this->commandline = $commandline; @@ -1157,25 +1169,12 @@ class Process implements \IteratorAggregate /** * Sets the environment variables. * - * Each environment variable value should be a string. - * If it is an array, the variable is ignored. - * If it is false or null, it will be removed when - * env vars are otherwise inherited. - * - * That happens in PHP when 'argv' is registered into - * the $_ENV array for instance. - * - * @param array $env The new environment variables + * @param array $env The new environment variables * * @return $this */ public function setEnv(array $env) { - // Process can not handle env values that are arrays - $env = array_filter($env, function ($value) { - return !\is_array($value); - }); - $this->env = $env; return $this; @@ -1224,7 +1223,7 @@ class Process implements \IteratorAggregate */ public function inheritEnvironmentVariables($inheritEnv = true) { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.4, env variables are always inherited.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.4, env variables are always inherited.', __METHOD__), \E_USER_DEPRECATED); if (!$inheritEnv) { throw new InvalidArgumentException('Not inheriting environment variables is not supported.'); @@ -1383,9 +1382,9 @@ class Process implements \IteratorAggregate } ob_start(); - phpinfo(INFO_GENERAL); + phpinfo(\INFO_GENERAL); - return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild'); } /** @@ -1487,8 +1486,8 @@ class Process implements \IteratorAggregate $this->exitcode = null; $this->fallbackStatus = []; $this->processInformation = null; - $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b'); - $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b'); + $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); + $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); $this->process = null; $this->latestSignal = null; $this->status = self::STATUS_READY; @@ -1502,8 +1501,6 @@ class Process implements \IteratorAggregate * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) * @param bool $throwException Whether to throw exception in case signal failed * - * @return bool True if the signal was sent successfully, false otherwise - * * @throws LogicException In case the process is not running * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed * @throws RuntimeException In case of failure @@ -1572,7 +1569,7 @@ class Process implements \IteratorAggregate if (isset($varCache[$m[0]])) { return $varCache[$m[0]]; } - if (false !== strpos($value = $m[1], "\0")) { + if (str_contains($value = $m[1], "\0")) { $value = str_replace("\0", '?', $value); } if (false === strpbrk($value, "\"%!\n")) { @@ -1606,7 +1603,7 @@ class Process implements \IteratorAggregate private function requireProcessIsStarted(string $functionName) { if (!$this->isStarted()) { - throw new LogicException(sprintf('Process must be started before calling %s.', $functionName)); + throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName)); } } @@ -1618,7 +1615,7 @@ class Process implements \IteratorAggregate private function requireProcessIsTerminated(string $functionName) { if (!$this->isTerminated()) { - throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); + throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName)); } } @@ -1633,7 +1630,7 @@ class Process implements \IteratorAggregate if ('\\' !== \DIRECTORY_SEPARATOR) { return "'".str_replace("'", "'\\''", $argument)."'"; } - if (false !== strpos($argument, "\0")) { + if (str_contains($argument, "\0")) { $argument = str_replace("\0", '?', $argument); } if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { @@ -1648,7 +1645,7 @@ class Process implements \IteratorAggregate { return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) { if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { - throw new InvalidArgumentException(sprintf('Command line is missing a value for parameter "%s": %s.', $matches[1], $commandline)); + throw new InvalidArgumentException(sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline); } return $this->escapeArgument($env[$matches[1]]); @@ -1657,20 +1654,9 @@ class Process implements \IteratorAggregate private function getDefaultEnv(): array { - $env = []; + $env = getenv(); + $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env; - foreach ($_SERVER as $k => $v) { - if (\is_string($v) && false !== $v = getenv($k)) { - $env[$k] = $v; - } - } - - foreach ($_ENV as $k => $v) { - if (\is_string($v)) { - $env[$k] = $v; - } - } - - return $env; + return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env); } } diff --git a/vendor/symfony/process/ProcessUtils.php b/vendor/symfony/process/ProcessUtils.php index 2f9c4be..121693b 100644 --- a/vendor/symfony/process/ProcessUtils.php +++ b/vendor/symfony/process/ProcessUtils.php @@ -48,7 +48,7 @@ class ProcessUtils if (\is_string($input)) { return $input; } - if (is_scalar($input)) { + if (\is_scalar($input)) { return (string) $input; } if ($input instanceof Process) { @@ -61,7 +61,7 @@ class ProcessUtils return new \IteratorIterator($input); } - throw new InvalidArgumentException(sprintf('%s only accepts strings, Traversable objects or stream resources.', $caller)); + throw new InvalidArgumentException(sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); } return $input; diff --git a/vendor/symfony/process/README.md b/vendor/symfony/process/README.md index b7ca5b4..afce5e4 100644 --- a/vendor/symfony/process/README.md +++ b/vendor/symfony/process/README.md @@ -6,8 +6,8 @@ The Process component executes commands in sub-processes. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/process.html) - * [Contributing](https://symfony.com/doc/current/contributing/index.html) - * [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) + * [Documentation](https://symfony.com/doc/current/components/process.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/process/composer.json b/vendor/symfony/process/composer.json index e0174de..c0f7599 100644 --- a/vendor/symfony/process/composer.json +++ b/vendor/symfony/process/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/process", "type": "library", - "description": "Symfony Process Component", + "description": "Executes commands in sub-processes", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,7 +16,8 @@ } ], "require": { - "php": "^7.1.3" + "php": ">=7.1.3", + "symfony/polyfill-php80": "^1.16" }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" }, @@ -24,10 +25,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/vendor/symfony/property-info/DependencyInjection/PropertyInfoConstructorPass.php b/vendor/symfony/property-info/DependencyInjection/PropertyInfoConstructorPass.php new file mode 100644 index 0000000..2fb4f94 --- /dev/null +++ b/vendor/symfony/property-info/DependencyInjection/PropertyInfoConstructorPass.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\DependencyInjection; + +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * Adds extractors to the property_info.constructor_extractor service. + * + * @author Dmitrii Poddubnyi + */ +final class PropertyInfoConstructorPass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + private $service; + private $tag; + + public function __construct(string $service = 'property_info.constructor_extractor', string $tag = 'property_info.constructor_extractor') + { + $this->service = $service; + $this->tag = $tag; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->service)) { + return; + } + $definition = $container->getDefinition($this->service); + + $listExtractors = $this->findAndSortTaggedServices($this->tag, $container); + $definition->replaceArgument(0, new IteratorArgument($listExtractors)); + } +} diff --git a/vendor/symfony/property-info/Extractor/ConstructorArgumentTypeExtractorInterface.php b/vendor/symfony/property-info/Extractor/ConstructorArgumentTypeExtractorInterface.php new file mode 100644 index 0000000..cbde902 --- /dev/null +++ b/vendor/symfony/property-info/Extractor/ConstructorArgumentTypeExtractorInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use Symfony\Component\PropertyInfo\Type; + +/** + * Infers the constructor argument type. + * + * @author Dmitrii Poddubnyi + * + * @internal + */ +interface ConstructorArgumentTypeExtractorInterface +{ + /** + * Gets types of an argument from constructor. + * + * @return Type[]|null + * + * @internal + */ + public function getTypesFromConstructor(string $class, string $property): ?array; +} diff --git a/vendor/symfony/property-info/Extractor/ConstructorExtractor.php b/vendor/symfony/property-info/Extractor/ConstructorExtractor.php new file mode 100644 index 0000000..702251c --- /dev/null +++ b/vendor/symfony/property-info/Extractor/ConstructorExtractor.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Extractor; + +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; + +/** + * Extracts the constructor argument type using ConstructorArgumentTypeExtractorInterface implementations. + * + * @author Dmitrii Poddubnyi + */ +final class ConstructorExtractor implements PropertyTypeExtractorInterface +{ + /** @var iterable|ConstructorArgumentTypeExtractorInterface[] */ + private $extractors; + + /** + * @param iterable|ConstructorArgumentTypeExtractorInterface[] $extractors + */ + public function __construct(iterable $extractors = []) + { + $this->extractors = $extractors; + } + + /** + * {@inheritdoc} + */ + public function getTypes($class, $property, array $context = []) + { + foreach ($this->extractors as $extractor) { + $value = $extractor->getTypesFromConstructor($class, $property); + if (null !== $value) { + return $value; + } + } + + return null; + } +} diff --git a/vendor/symfony/psr-http-message-bridge/.github/workflows/ci.yml b/vendor/symfony/psr-http-message-bridge/.github/workflows/ci.yml new file mode 100644 index 0000000..4c0e8ae --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + pull_request: + push: + +jobs: + test: + name: 'Test ${{ matrix.deps }} on PHP ${{ matrix.php }}' + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: ['7.1.3', '7.2', '7.3', '7.4', '8.0', '8.1'] + include: + - php: '7.4' + deps: lowest + deprecations: max[self]=0 + - php: '8.0' + deps: highest + deprecations: max[indirect]=5 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '${{ matrix.php }}' + coverage: none + + - name: Configure composer + if: "${{ matrix.deps == 'highest' }}" + run: composer config minimum-stability dev + + - name: Composer install + uses: ramsey/composer-install@v1 + with: + dependency-versions: '${{ matrix.deps }}' + + - name: Install PHPUnit + run: vendor/bin/simple-phpunit install + + - name: Run tests + run: vendor/bin/simple-phpunit + env: + SYMFONY_DEPRECATIONS_HELPER: '${{ matrix.deprecations }}' diff --git a/vendor/symfony/psr-http-message-bridge/ArgumentValueResolver/PsrServerRequestResolver.php b/vendor/symfony/psr-http-message-bridge/ArgumentValueResolver/PsrServerRequestResolver.php new file mode 100644 index 0000000..29dc7dc --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/ArgumentValueResolver/PsrServerRequestResolver.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage\ArgumentValueResolver; + +use Psr\Http\Message\MessageInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +/** + * Injects the RequestInterface, MessageInterface or ServerRequestInterface when requested. + * + * @author Iltar van der Berg + * @author Alexander M. Turek + */ +final class PsrServerRequestResolver implements ArgumentValueResolverInterface +{ + private const SUPPORTED_TYPES = [ + ServerRequestInterface::class => true, + RequestInterface::class => true, + MessageInterface::class => true, + ]; + + private $httpMessageFactory; + + public function __construct(HttpMessageFactoryInterface $httpMessageFactory) + { + $this->httpMessageFactory = $httpMessageFactory; + } + + /** + * {@inheritdoc} + */ + public function supports(Request $request, ArgumentMetadata $argument): bool + { + return self::SUPPORTED_TYPES[$argument->getType()] ?? false; + } + + /** + * {@inheritdoc} + */ + public function resolve(Request $request, ArgumentMetadata $argument): \Traversable + { + yield $this->httpMessageFactory->createRequest($request); + } +} diff --git a/vendor/symfony/psr-http-message-bridge/EventListener/PsrResponseListener.php b/vendor/symfony/psr-http-message-bridge/EventListener/PsrResponseListener.php new file mode 100644 index 0000000..ee0e047 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/EventListener/PsrResponseListener.php @@ -0,0 +1,50 @@ + + * @author Alexander M. Turek + */ +final class PsrResponseListener implements EventSubscriberInterface +{ + private $httpFoundationFactory; + + public function __construct(HttpFoundationFactoryInterface $httpFoundationFactory = null) + { + $this->httpFoundationFactory = $httpFoundationFactory ?? new HttpFoundationFactory(); + } + + /** + * Do the conversion if applicable and update the response of the event. + */ + public function onKernelView(ViewEvent $event): void + { + $controllerResult = $event->getControllerResult(); + + if (!$controllerResult instanceof ResponseInterface) { + return; + } + + $event->setResponse($this->httpFoundationFactory->createResponse($controllerResult)); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array + { + return [ + KernelEvents::VIEW => 'onKernelView', + ]; + } +} diff --git a/vendor/symfony/psr-http-message-bridge/Factory/UploadedFile.php b/vendor/symfony/psr-http-message-bridge/Factory/UploadedFile.php new file mode 100644 index 0000000..53aa37a --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/Factory/UploadedFile.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage\Factory; + +use Psr\Http\Message\UploadedFileInterface; +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\File\UploadedFile as BaseUploadedFile; + +/** + * @author Nicolas Grekas + */ +class UploadedFile extends BaseUploadedFile +{ + private $psrUploadedFile; + private $test = false; + + public function __construct(UploadedFileInterface $psrUploadedFile, callable $getTemporaryPath) + { + $error = $psrUploadedFile->getError(); + $path = ''; + + if (\UPLOAD_ERR_NO_FILE !== $error) { + $path = $psrUploadedFile->getStream()->getMetadata('uri') ?? ''; + + if ($this->test = !\is_string($path) || !is_uploaded_file($path)) { + $path = $getTemporaryPath(); + $psrUploadedFile->moveTo($path); + } + } + + parent::__construct( + $path, + (string) $psrUploadedFile->getClientFilename(), + $psrUploadedFile->getClientMediaType(), + $psrUploadedFile->getError(), + $this->test + ); + + $this->psrUploadedFile = $psrUploadedFile; + } + + /** + * {@inheritdoc} + */ + public function move($directory, $name = null): File + { + if (!$this->isValid() || $this->test) { + return parent::move($directory, $name); + } + + $target = $this->getTargetFile($directory, $name); + + try { + $this->psrUploadedFile->moveTo($target); + } catch (\RuntimeException $e) { + throw new FileException(sprintf('Could not move the file "%s" to "%s" (%s)', $this->getPathname(), $target, $e->getMessage()), 0, $e); + } + + @chmod($target, 0666 & ~umask()); + + return $target; + } +} diff --git a/vendor/symfony/psr-http-message-bridge/Tests/ArgumentValueResolver/PsrServerRequestResolverTest.php b/vendor/symfony/psr-http-message-bridge/Tests/ArgumentValueResolver/PsrServerRequestResolverTest.php new file mode 100644 index 0000000..662b186 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/Tests/ArgumentValueResolver/PsrServerRequestResolverTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage\Tests\ArgumentValueResolver; + +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\MessageInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Bridge\PsrHttpMessage\ArgumentValueResolver\PsrServerRequestResolver; +use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver; + +/** + * @author Alexander M. Turek + */ +final class PsrServerRequestResolverTest extends TestCase +{ + public function testServerRequest() + { + $symfonyRequest = $this->createMock(Request::class); + $psrRequest = $this->createMock(ServerRequestInterface::class); + + $resolver = $this->bootstrapResolver($symfonyRequest, $psrRequest); + + self::assertSame([$psrRequest], $resolver->getArguments($symfonyRequest, static function (ServerRequestInterface $serverRequest): void {})); + } + + public function testRequest() + { + $symfonyRequest = $this->createMock(Request::class); + $psrRequest = $this->createMock(ServerRequestInterface::class); + + $resolver = $this->bootstrapResolver($symfonyRequest, $psrRequest); + + self::assertSame([$psrRequest], $resolver->getArguments($symfonyRequest, static function (RequestInterface $request): void {})); + } + + public function testMessage() + { + $symfonyRequest = $this->createMock(Request::class); + $psrRequest = $this->createMock(ServerRequestInterface::class); + + $resolver = $this->bootstrapResolver($symfonyRequest, $psrRequest); + + self::assertSame([$psrRequest], $resolver->getArguments($symfonyRequest, static function (MessageInterface $request): void {})); + } + + private function bootstrapResolver(Request $symfonyRequest, ServerRequestInterface $psrRequest): ArgumentResolver + { + $messageFactory = $this->createMock(HttpMessageFactoryInterface::class); + $messageFactory->expects(self::once()) + ->method('createRequest') + ->with(self::identicalTo($symfonyRequest)) + ->willReturn($psrRequest); + + return new ArgumentResolver(null, [new PsrServerRequestResolver($messageFactory)]); + } +} diff --git a/vendor/symfony/psr-http-message-bridge/Tests/EventListener/PsrResponseListenerTest.php b/vendor/symfony/psr-http-message-bridge/Tests/EventListener/PsrResponseListenerTest.php new file mode 100644 index 0000000..9a94b20 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/Tests/EventListener/PsrResponseListenerTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PsrHttpMessage\EventListener\PsrResponseListener; +use Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\Response; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\ViewEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +/** + * @author Kévin Dunglas + */ +class PsrResponseListenerTest extends TestCase +{ + public function testConvertsControllerResult() + { + $listener = new PsrResponseListener(); + $event = $this->createEventMock(new Response()); + $listener->onKernelView($event); + + self::assertTrue($event->hasResponse()); + } + + public function testDoesNotConvertControllerResult() + { + $listener = new PsrResponseListener(); + $event = $this->createEventMock([]); + + $listener->onKernelView($event); + self::assertFalse($event->hasResponse()); + + $event = $this->createEventMock(null); + + $listener->onKernelView($event); + self::assertFalse($event->hasResponse()); + } + + private function createEventMock($controllerResult): ViewEvent + { + return new ViewEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $controllerResult); + } +} diff --git a/vendor/symfony/psr-http-message-bridge/Tests/Fixtures/App/Controller/PsrRequestController.php b/vendor/symfony/psr-http-message-bridge/Tests/Fixtures/App/Controller/PsrRequestController.php new file mode 100644 index 0000000..18b7741 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/Tests/Fixtures/App/Controller/PsrRequestController.php @@ -0,0 +1,45 @@ +responseFactory = $responseFactory; + $this->streamFactory = $streamFactory; + } + + public function serverRequestAction(ServerRequestInterface $request): ResponseInterface + { + return $this->responseFactory + ->createResponse() + ->withBody($this->streamFactory->createStream(sprintf('%s', $request->getMethod()))); + } + + public function requestAction(RequestInterface $request): ResponseInterface + { + return $this->responseFactory + ->createResponse() + ->withStatus(403) + ->withBody($this->streamFactory->createStream(sprintf('%s %s', $request->getMethod(), $request->getBody()->getContents()))); + } + + public function messageAction(MessageInterface $request): ResponseInterface + { + return $this->responseFactory + ->createResponse() + ->withStatus(422) + ->withBody($this->streamFactory->createStream(sprintf('%s', $request->getHeader('X-My-Header')[0]))); + } +} diff --git a/vendor/symfony/psr-http-message-bridge/Tests/Fixtures/App/Kernel.php b/vendor/symfony/psr-http-message-bridge/Tests/Fixtures/App/Kernel.php new file mode 100644 index 0000000..aef8193 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/Tests/Fixtures/App/Kernel.php @@ -0,0 +1,76 @@ +add('server_request', '/server-request')->controller([PsrRequestController::class, 'serverRequestAction'])->methods(['GET']) + ->add('request', '/request')->controller([PsrRequestController::class, 'requestAction'])->methods(['POST']) + ->add('message', '/message')->controller([PsrRequestController::class, 'messageAction'])->methods(['PUT']) + ; + } + + protected function configureContainer(ContainerConfigurator $container): void + { + $container->extension('framework', [ + 'router' => ['utf8' => true], + 'secret' => 'for your eyes only', + 'test' => true, + ]); + + $container->services() + ->set('nyholm.psr_factory', Psr17Factory::class) + ->alias(ResponseFactoryInterface::class, 'nyholm.psr_factory') + ->alias(ServerRequestFactoryInterface::class, 'nyholm.psr_factory') + ->alias(StreamFactoryInterface::class, 'nyholm.psr_factory') + ->alias(UploadedFileFactoryInterface::class, 'nyholm.psr_factory') + ; + + $container->services() + ->defaults()->autowire()->autoconfigure() + ->set(HttpFoundationFactoryInterface::class, HttpFoundationFactory::class) + ->set(HttpMessageFactoryInterface::class, PsrHttpFactory::class) + ->set(PsrResponseListener::class) + ->set(PsrServerRequestResolver::class) + ; + + $container->services() + ->set('logger', NullLogger::class) + ->set(PsrRequestController::class)->public()->autowire() + ; + } +} diff --git a/vendor/symfony/psr-http-message-bridge/Tests/Fixtures/App/Kernel44.php b/vendor/symfony/psr-http-message-bridge/Tests/Fixtures/App/Kernel44.php new file mode 100644 index 0000000..e976ae2 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/Tests/Fixtures/App/Kernel44.php @@ -0,0 +1,67 @@ +add('/server-request', PsrRequestController::class.'::serverRequestAction')->setMethods(['GET']); + $routes->add('/request', PsrRequestController::class.'::requestAction')->setMethods(['POST']); + $routes->add('/message', PsrRequestController::class.'::messageAction')->setMethods(['PUT']); + } + + protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void + { + $container->loadFromExtension('framework', [ + 'secret' => 'for your eyes only', + 'test' => true, + ]); + + $container->register('nyholm.psr_factory', Psr17Factory::class); + $container->setAlias(ResponseFactoryInterface::class, 'nyholm.psr_factory'); + $container->setAlias(ServerRequestFactoryInterface::class, 'nyholm.psr_factory'); + $container->setAlias(StreamFactoryInterface::class, 'nyholm.psr_factory'); + $container->setAlias(UploadedFileFactoryInterface::class, 'nyholm.psr_factory'); + + $container->register(HttpFoundationFactoryInterface::class, HttpFoundationFactory::class)->setAutowired(true)->setAutoconfigured(true); + $container->register(HttpMessageFactoryInterface::class, PsrHttpFactory::class)->setAutowired(true)->setAutoconfigured(true); + $container->register(PsrResponseListener::class)->setAutowired(true)->setAutoconfigured(true); + $container->register(PsrServerRequestResolver::class)->setAutowired(true)->setAutoconfigured(true); + + $container->register('logger', NullLogger::class); + $container->register(PsrRequestController::class)->setPublic(true)->setAutowired(true); + } +} diff --git a/vendor/symfony/psr-http-message-bridge/Tests/Functional/ControllerTest.php b/vendor/symfony/psr-http-message-bridge/Tests/Functional/ControllerTest.php new file mode 100644 index 0000000..0b88405 --- /dev/null +++ b/vendor/symfony/psr-http-message-bridge/Tests/Functional/ControllerTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PsrHttpMessage\Tests\Functional; + +use Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\App\Kernel; +use Symfony\Bridge\PsrHttpMessage\Tests\Fixtures\App\Kernel44; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\HttpKernel\Kernel as SymfonyKernel; + +/** + * @author Alexander M. Turek + */ +final class ControllerTest extends WebTestCase +{ + public function testServerRequestAction() + { + $client = self::createClient(); + $crawler = $client->request('GET', '/server-request'); + + self::assertResponseStatusCodeSame(200); + self::assertSame('GET', $crawler->text()); + } + + public function testRequestAction() + { + $client = self::createClient(); + $crawler = $client->request('POST', '/request', [], [], [], 'some content'); + + self::assertResponseStatusCodeSame(403); + self::assertSame('POST some content', $crawler->text()); + } + + public function testMessageAction() + { + $client = self::createClient(); + $crawler = $client->request('PUT', '/message', [], [], ['HTTP_X_MY_HEADER' => 'some content']); + + self::assertResponseStatusCodeSame(422); + self::assertSame('some content', $crawler->text()); + } + + protected static function getKernelClass(): string + { + return SymfonyKernel::VERSION_ID >= 50200 ? Kernel::class : Kernel44::class; + } +} diff --git a/vendor/symfony/service-contracts/Attribute/Required.php b/vendor/symfony/service-contracts/Attribute/Required.php index 8ba6183..9df8511 100644 --- a/vendor/symfony/service-contracts/Attribute/Required.php +++ b/vendor/symfony/service-contracts/Attribute/Required.php @@ -11,8 +11,6 @@ namespace Symfony\Contracts\Service\Attribute; -use Attribute; - /** * A required dependency. * @@ -21,7 +19,7 @@ use Attribute; * * @author Alexander M. Turek */ -#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)] +#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] final class Required { } diff --git a/vendor/symfony/service-contracts/Attribute/SubscribedService.php b/vendor/symfony/service-contracts/Attribute/SubscribedService.php new file mode 100644 index 0000000..10d1bc3 --- /dev/null +++ b/vendor/symfony/service-contracts/Attribute/SubscribedService.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service\Attribute; + +use Symfony\Contracts\Service\ServiceSubscriberTrait; + +/** + * Use with {@see ServiceSubscriberTrait} to mark a method's return type + * as a subscribed service. + * + * @author Kevin Bond + */ +#[\Attribute(\Attribute::TARGET_METHOD)] +final class SubscribedService +{ + /** + * @param string|null $key The key to use for the service + * If null, use "ClassName::methodName" + */ + public function __construct( + public ?string $key = null + ) { + } +} diff --git a/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php b/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php index 5ed9149..2a1b565 100644 --- a/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php +++ b/vendor/symfony/service-contracts/Test/ServiceLocatorTest.php @@ -17,6 +17,9 @@ use Symfony\Contracts\Service\ServiceLocatorTrait; abstract class ServiceLocatorTest extends TestCase { + /** + * @return ContainerInterface + */ protected function getServiceLocator(array $factories) { return new class($factories) implements ContainerInterface { @@ -67,7 +70,7 @@ abstract class ServiceLocatorTest extends TestCase public function testThrowsOnUndefinedInternalService() { if (!$this->getExpectedException()) { - $this->expectException('Psr\Container\NotFoundExceptionInterface'); + $this->expectException(\Psr\Container\NotFoundExceptionInterface::class); $this->expectExceptionMessage('The service "foo" has a dependency on a non-existent service "bar". This locator only knows about the "foo" service.'); } $locator = $this->getServiceLocator([ @@ -79,7 +82,7 @@ abstract class ServiceLocatorTest extends TestCase public function testThrowsOnCircularReference() { - $this->expectException('Psr\Container\ContainerExceptionInterface'); + $this->expectException(\Psr\Container\ContainerExceptionInterface::class); $this->expectExceptionMessage('Circular reference detected for service "bar", path: "bar -> baz -> bar".'); $locator = $this->getServiceLocator([ 'foo' => function () use (&$locator) { return $locator->get('bar'); }, diff --git a/vendor/symfony/string/Inflector/FrenchInflector.php b/vendor/symfony/string/Inflector/FrenchInflector.php new file mode 100644 index 0000000..42f6125 --- /dev/null +++ b/vendor/symfony/string/Inflector/FrenchInflector.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Inflector; + +/** + * French inflector. + * + * This class does only inflect nouns; not adjectives nor composed words like "soixante-dix". + */ +final class FrenchInflector implements InflectorInterface +{ + /** + * A list of all rules for pluralise. + * + * @see https://la-conjugaison.nouvelobs.com/regles/grammaire/le-pluriel-des-noms-121.php + */ + private const PLURALIZE_REGEXP = [ + // First entry: regexp + // Second entry: replacement + + // Words finishing with "s", "x" or "z" are invariables + // Les mots finissant par "s", "x" ou "z" sont invariables + ['/(s|x|z)$/i', '\1'], + + // Words finishing with "eau" are pluralized with a "x" + // Les mots finissant par "eau" prennent tous un "x" au pluriel + ['/(eau)$/i', '\1x'], + + // Words finishing with "au" are pluralized with a "x" excepted "landau" + // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" + ['/^(landau)$/i', '\1s'], + ['/(au)$/i', '\1x'], + + // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" + // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" + ['/^(pneu|bleu|émeu)$/i', '\1s'], + ['/(eu)$/i', '\1x'], + + // Words finishing with "al" are pluralized with a "aux" excepted + // Les mots finissant en "al" se terminent en "aux" sauf + ['/^(bal|carnaval|caracal|chacal|choral|corral|étal|festival|récital|val)$/i', '\1s'], + ['/al$/i', '\1aux'], + + // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux + ['/^(aspir|b|cor|ém|ferm|soupir|trav|vant|vitr)ail$/i', '\1aux'], + + // Bijou, caillou, chou, genou, hibou, joujou et pou qui prennent un x au pluriel + ['/^(bij|caill|ch|gen|hib|jouj|p)ou$/i', '\1oux'], + + // Invariable words + ['/^(cinquante|soixante|mille)$/i', '\1'], + + // French titles + ['/^(mon|ma)(sieur|dame|demoiselle|seigneur)$/', 'mes\2s'], + ['/^(Mon|Ma)(sieur|dame|demoiselle|seigneur)$/', 'Mes\2s'], + ]; + + /** + * A list of all rules for singularize. + */ + private const SINGULARIZE_REGEXP = [ + // First entry: regexp + // Second entry: replacement + + // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux + ['/((aspir|b|cor|ém|ferm|soupir|trav|vant|vitr))aux$/i', '\1ail'], + + // Words finishing with "eau" are pluralized with a "x" + // Les mots finissant par "eau" prennent tous un "x" au pluriel + ['/(eau)x$/i', '\1'], + + // Words finishing with "al" are pluralized with a "aux" expected + // Les mots finissant en "al" se terminent en "aux" sauf + ['/(amir|anim|arsen|boc|can|capit|capor|chev|crist|génér|hopit|hôpit|idé|journ|littor|loc|m|mét|minér|princip|radic|termin)aux$/i', '\1al'], + + // Words finishing with "au" are pluralized with a "x" excepted "landau" + // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" + ['/(au)x$/i', '\1'], + + // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" + // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" + ['/(eu)x$/i', '\1'], + + // Words finishing with "ou" are pluralized with a "s" excepted bijou, caillou, chou, genou, hibou, joujou, pou + // Les mots finissant par "ou" prennent un "s" sauf bijou, caillou, chou, genou, hibou, joujou, pou + ['/(bij|caill|ch|gen|hib|jouj|p)oux$/i', '\1ou'], + + // French titles + ['/^mes(dame|demoiselle)s$/', 'ma\1'], + ['/^Mes(dame|demoiselle)s$/', 'Ma\1'], + ['/^mes(sieur|seigneur)s$/', 'mon\1'], + ['/^Mes(sieur|seigneur)s$/', 'Mon\1'], + + //Default rule + ['/s$/i', ''], + ]; + + /** + * A list of words which should not be inflected. + * This list is only used by singularize. + */ + private const UNINFLECTED = '/^(abcès|accès|abus|albatros|anchois|anglais|autobus|bois|brebis|carquois|cas|chas|colis|concours|corps|cours|cyprès|décès|devis|discours|dos|embarras|engrais|entrelacs|excès|fils|fois|gâchis|gars|glas|héros|intrus|jars|jus|kermès|lacis|legs|lilas|marais|mars|matelas|mépris|mets|mois|mors|obus|os|palais|paradis|parcours|pardessus|pays|plusieurs|poids|pois|pouls|printemps|processus|progrès|puits|pus|rabais|radis|recors|recours|refus|relais|remords|remous|rictus|rhinocéros|repas|rubis|sas|secours|sens|souris|succès|talus|tapis|tas|taudis|temps|tiers|univers|velours|verglas|vernis|virus)$/i'; + + /** + * {@inheritdoc} + */ + public function singularize(string $plural): array + { + if ($this->isInflectedWord($plural)) { + return [$plural]; + } + + foreach (self::SINGULARIZE_REGEXP as $rule) { + [$regexp, $replace] = $rule; + + if (1 === preg_match($regexp, $plural)) { + return [preg_replace($regexp, $replace, $plural)]; + } + } + + return [$plural]; + } + + /** + * {@inheritdoc} + */ + public function pluralize(string $singular): array + { + if ($this->isInflectedWord($singular)) { + return [$singular]; + } + + foreach (self::PLURALIZE_REGEXP as $rule) { + [$regexp, $replace] = $rule; + + if (1 === preg_match($regexp, $singular)) { + return [preg_replace($regexp, $replace, $singular)]; + } + } + + return [$singular.'s']; + } + + private function isInflectedWord(string $word): bool + { + return 1 === preg_match(self::UNINFLECTED, $word); + } +} diff --git a/vendor/symfony/translation-contracts/Test/TranslatorTest.php b/vendor/symfony/translation-contracts/Test/TranslatorTest.php index 5bfb0f8..a3e9b20 100644 --- a/vendor/symfony/translation-contracts/Test/TranslatorTest.php +++ b/vendor/symfony/translation-contracts/Test/TranslatorTest.php @@ -30,6 +30,22 @@ use Symfony\Contracts\Translation\TranslatorTrait; */ class TranslatorTest extends TestCase { + private $defaultLocale; + + protected function setUp(): void + { + $this->defaultLocale = \Locale::getDefault(); + \Locale::setDefault('en'); + } + + protected function tearDown(): void + { + \Locale::setDefault($this->defaultLocale); + } + + /** + * @return TranslatorInterface + */ public function getTranslator() { return new class() implements TranslatorInterface { @@ -53,7 +69,18 @@ class TranslatorTest extends TestCase public function testTransChoiceWithExplicitLocale($expected, $id, $number) { $translator = $this->getTranslator(); - $translator->setLocale('en'); + + $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number])); + } + + /** + * @requires extension intl + * + * @dataProvider getTransChoiceTests + */ + public function testTransChoiceWithDefaultLocale($expected, $id, $number) + { + $translator = $this->getTranslator(); $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number])); } @@ -61,11 +88,10 @@ class TranslatorTest extends TestCase /** * @dataProvider getTransChoiceTests */ - public function testTransChoiceWithDefaultLocale($expected, $id, $number) + public function testTransChoiceWithEnUsPosix($expected, $id, $number) { - \Locale::setDefault('en'); - $translator = $this->getTranslator(); + $translator->setLocale('en_US_POSIX'); $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number])); } @@ -73,7 +99,6 @@ class TranslatorTest extends TestCase public function testGetSetLocale() { $translator = $this->getTranslator(); - $translator->setLocale('en'); $this->assertEquals('en', $translator->getLocale()); } @@ -142,11 +167,11 @@ class TranslatorTest extends TestCase /** * @dataProvider getChooseTests */ - public function testChoose($expected, $id, $number) + public function testChoose($expected, $id, $number, $locale = null) { $translator = $this->getTranslator(); - $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number])); + $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number], null, $locale)); } public function testReturnMessageIfExactlyOneStandardRuleIsGiven() @@ -161,7 +186,7 @@ class TranslatorTest extends TestCase */ public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number) { - $this->expectException('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $translator = $this->getTranslator(); $translator->trans($id, ['%count%' => $number]); @@ -255,6 +280,18 @@ class TranslatorTest extends TestCase ['', '|', 1], // Empty plural set (3 plural forms) from a .PO file ['', '||', 1], + + // Floating values + ['1.5 liters', '%count% liter|%count% liters', 1.5], + ['1.5 litre', '%count% litre|%count% litres', 1.5, 'fr'], + + // Negative values + ['-1 degree', '%count% degree|%count% degrees', -1], + ['-1 degré', '%count% degré|%count% degrés', -1], + ['-1.5 degrees', '%count% degree|%count% degrees', -1.5], + ['-1.5 degré', '%count% degré|%count% degrés', -1.5, 'fr'], + ['-2 degrees', '%count% degree|%count% degrees', -2], + ['-2 degrés', '%count% degré|%count% degrés', -2], ]; } @@ -287,7 +324,7 @@ class TranslatorTest extends TestCase { return [ ['1', ['ay', 'bo', 'cgg', 'dz', 'id', 'ja', 'jbo', 'ka', 'kk', 'km', 'ko', 'ky']], - ['2', ['nl', 'fr', 'en', 'de', 'de_GE', 'hy', 'hy_AM']], + ['2', ['nl', 'fr', 'en', 'de', 'de_GE', 'hy', 'hy_AM', 'en_US_POSIX']], ['3', ['be', 'bs', 'cs', 'hr']], ['4', ['cy', 'mt', 'sl']], ['6', ['ar']], @@ -325,7 +362,7 @@ class TranslatorTest extends TestCase foreach ($matrix as $langCode => $data) { $indexes = array_flip($data); if ($expectSuccess) { - $this->assertEquals($nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms."); + $this->assertCount($nplural, $indexes, "Langcode '$langCode' has '$nplural' plural forms."); } else { $this->assertNotEquals((int) $nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms."); } diff --git a/vendor/symfony/translation/Command/TranslationPullCommand.php b/vendor/symfony/translation/Command/TranslationPullCommand.php new file mode 100644 index 0000000..e2e7c00 --- /dev/null +++ b/vendor/symfony/translation/Command/TranslationPullCommand.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Catalogue\TargetOperation; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Provider\TranslationProviderCollection; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; + +/** + * @author Mathieu Santostefano + */ +final class TranslationPullCommand extends Command +{ + use TranslationTrait; + + protected static $defaultName = 'translation:pull'; + protected static $defaultDescription = 'Pull translations from a given provider.'; + + private $providerCollection; + private $writer; + private $reader; + private $defaultLocale; + private $transPaths; + private $enabledLocales; + + public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = []) + { + $this->providerCollection = $providerCollection; + $this->writer = $writer; + $this->reader = $reader; + $this->defaultLocale = $defaultLocale; + $this->transPaths = $transPaths; + $this->enabledLocales = $enabledLocales; + + parent::__construct(); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('provider')) { + $suggestions->suggestValues($this->providerCollection->keys()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domains')) { + $provider = $this->providerCollection->get($input->getArgument('provider')); + + if ($provider && method_exists($provider, 'getDomains')) { + $domains = $provider->getDomains(); + $suggestions->suggestValues($domains); + } + + return; + } + + if ($input->mustSuggestOptionValuesFor('locales')) { + $suggestions->suggestValues($this->enabledLocales); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['php', 'xlf', 'xlf12', 'xlf20', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'json', 'ini', 'res']); + } + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $keys = $this->providerCollection->keys(); + $defaultProvider = 1 === \count($keys) ? $keys[0] : null; + + $this + ->setDefinition([ + new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider), + new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'), + new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'), + new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'), + new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'), + new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'), + ]) + ->setHelp(<<<'EOF' +The %command.name% command pulls translations from the given provider. Only +new translations are pulled, existing ones are not overwritten. + +You can overwrite existing translations (and remove the missing ones on local side) by using the --force flag: + + php %command.full_name% --force provider + +Full example: + + php %command.full_name% provider --force --domains=messages --domains=validators --locales=en + +This command pulls all translations associated with the messages and validators domains for the en locale. +Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case. +Local translations for others domains and locales are ignored. +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $provider = $this->providerCollection->get($input->getArgument('provider')); + $force = $input->getOption('force'); + $intlIcu = $input->getOption('intl-icu'); + $locales = $input->getOption('locales') ?: $this->enabledLocales; + $domains = $input->getOption('domains'); + $format = $input->getOption('format'); + $xliffVersion = '1.2'; + + if ($intlIcu && !$force) { + $io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.'); + } + + switch ($format) { + case 'xlf20': $xliffVersion = '2.0'; + // no break + case 'xlf12': $format = 'xlf'; + } + + $writeOptions = [ + 'path' => end($this->transPaths), + 'xliff_version' => $xliffVersion, + 'default_locale' => $this->defaultLocale, + ]; + + if (!$domains) { + $domains = $provider->getDomains(); + } + + $providerTranslations = $provider->read($domains, $locales); + + if ($force) { + foreach ($providerTranslations->getCatalogues() as $catalogue) { + $operation = new TargetOperation(new MessageCatalogue($catalogue->getLocale()), $catalogue); + if ($intlIcu) { + $operation->moveMessagesToIntlDomainsIfPossible(); + } + $this->writer->write($operation->getResult(), $format, $writeOptions); + } + + $io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); + + // Append pulled translations to local ones. + $localTranslations->addBag($providerTranslations->diff($localTranslations)); + + foreach ($localTranslations->getCatalogues() as $catalogue) { + $this->writer->write($catalogue, $format, $writeOptions); + } + + $io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } +} diff --git a/vendor/symfony/translation/Command/TranslationPushCommand.php b/vendor/symfony/translation/Command/TranslationPushCommand.php new file mode 100644 index 0000000..bf6e8c9 --- /dev/null +++ b/vendor/symfony/translation/Command/TranslationPushCommand.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Provider\FilteringProvider; +use Symfony\Component\Translation\Provider\TranslationProviderCollection; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\TranslatorBag; + +/** + * @author Mathieu Santostefano + */ +final class TranslationPushCommand extends Command +{ + use TranslationTrait; + + protected static $defaultName = 'translation:push'; + protected static $defaultDescription = 'Push translations to a given provider.'; + + private $providers; + private $reader; + private $transPaths; + private $enabledLocales; + + public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = []) + { + $this->providers = $providers; + $this->reader = $reader; + $this->transPaths = $transPaths; + $this->enabledLocales = $enabledLocales; + + parent::__construct(); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('provider')) { + $suggestions->suggestValues($this->providers->keys()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domains')) { + $provider = $this->providers->get($input->getArgument('provider')); + + if ($provider && method_exists($provider, 'getDomains')) { + $domains = $provider->getDomains(); + $suggestions->suggestValues($domains); + } + + return; + } + + if ($input->mustSuggestOptionValuesFor('locales')) { + $suggestions->suggestValues($this->enabledLocales); + } + } + + /** + * {@inheritdoc} + */ + protected function configure() + { + $keys = $this->providers->keys(); + $defaultProvider = 1 === \count($keys) ? $keys[0] : null; + + $this + ->setDefinition([ + new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider), + new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'), + new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'), + new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'), + new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales), + ]) + ->setHelp(<<<'EOF' +The %command.name% command pushes translations to the given provider. Only new +translations are pushed, existing ones are not overwritten. + +You can overwrite existing translations by using the --force flag: + + php %command.full_name% --force provider + +You can delete provider translations which are not present locally by using the --delete-missing flag: + + php %command.full_name% --delete-missing provider + +Full example: + + php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en + +This command pushes all translations associated with the messages and validators domains for the en locale. +Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case. +Provider translations for others domains and locales are ignored. +EOF + ) + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $provider = $this->providers->get($input->getArgument('provider')); + + if (!$this->enabledLocales) { + throw new InvalidArgumentException(sprintf('You must define "framework.translator.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME))); + } + + $io = new SymfonyStyle($input, $output); + $domains = $input->getOption('domains'); + $locales = $input->getOption('locales'); + $force = $input->getOption('force'); + $deleteMissing = $input->getOption('delete-missing'); + + if (!$domains && $provider instanceof FilteringProvider) { + $domains = $provider->getDomains(); + } + + // Reading local translations must be done after retrieving the domains from the provider + // in order to manage only translations from configured domains + $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); + + if (!$domains) { + $domains = $this->getDomainsFromTranslatorBag($localTranslations); + } + + if (!$deleteMissing && $force) { + $provider->write($localTranslations); + + $io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + $providerTranslations = $provider->read($domains, $locales); + + if ($deleteMissing) { + $provider->delete($providerTranslations->diff($localTranslations)); + + $io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + // Read provider translations again, after missing translations deletion, + // to avoid push freshly deleted translations. + $providerTranslations = $provider->read($domains, $locales); + } + + $translationsToWrite = $localTranslations->diff($providerTranslations); + + if ($force) { + $translationsToWrite->addBag($localTranslations->intersect($providerTranslations)); + } + + $provider->write($translationsToWrite); + + $io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array + { + $domains = []; + + foreach ($translatorBag->getCatalogues() as $catalogue) { + $domains += $catalogue->getDomains(); + } + + return array_unique($domains); + } +} diff --git a/vendor/symfony/translation/Command/TranslationTrait.php b/vendor/symfony/translation/Command/TranslationTrait.php new file mode 100644 index 0000000..eafaffd --- /dev/null +++ b/vendor/symfony/translation/Command/TranslationTrait.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCatalogueInterface; +use Symfony\Component\Translation\TranslatorBag; + +/** + * @internal + */ +trait TranslationTrait +{ + private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag + { + $bag = new TranslatorBag(); + + foreach ($locales as $locale) { + $catalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + $this->reader->read($path, $catalogue); + } + + if ($domains) { + foreach ($domains as $domain) { + $bag->addCatalogue($this->filterCatalogue($catalogue, $domain)); + } + } else { + $bag->addCatalogue($catalogue); + } + } + + return $bag; + } + + private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue + { + $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); + + // extract intl-icu messages only + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + if ($intlMessages = $catalogue->all($intlDomain)) { + $filteredCatalogue->add($intlMessages, $intlDomain); + } + + // extract all messages and subtract intl-icu messages + if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { + $filteredCatalogue->add($messages, $domain); + } + foreach ($catalogue->getResources() as $resource) { + $filteredCatalogue->addResource($resource); + } + + if ($metadata = $catalogue->getMetadata('', $intlDomain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $intlDomain); + } + } + + if ($metadata = $catalogue->getMetadata('', $domain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $domain); + } + } + + return $filteredCatalogue; + } +} diff --git a/vendor/symfony/translation/DependencyInjection/TranslationDumperPass.php b/vendor/symfony/translation/DependencyInjection/TranslationDumperPass.php index 930f36d..6d78342 100644 --- a/vendor/symfony/translation/DependencyInjection/TranslationDumperPass.php +++ b/vendor/symfony/translation/DependencyInjection/TranslationDumperPass.php @@ -25,6 +25,10 @@ class TranslationDumperPass implements CompilerPassInterface public function __construct(string $writerServiceId = 'translation.writer', string $dumperTag = 'translation.dumper') { + if (1 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->writerServiceId = $writerServiceId; $this->dumperTag = $dumperTag; } diff --git a/vendor/symfony/translation/DependencyInjection/TranslationExtractorPass.php b/vendor/symfony/translation/DependencyInjection/TranslationExtractorPass.php index d08b2ba..fab6b20 100644 --- a/vendor/symfony/translation/DependencyInjection/TranslationExtractorPass.php +++ b/vendor/symfony/translation/DependencyInjection/TranslationExtractorPass.php @@ -26,6 +26,10 @@ class TranslationExtractorPass implements CompilerPassInterface public function __construct(string $extractorServiceId = 'translation.extractor', string $extractorTag = 'translation.extractor') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->extractorServiceId = $extractorServiceId; $this->extractorTag = $extractorTag; } diff --git a/vendor/symfony/translation/DependencyInjection/TranslatorPass.php b/vendor/symfony/translation/DependencyInjection/TranslatorPass.php index 27315f6..e6a4b36 100644 --- a/vendor/symfony/translation/DependencyInjection/TranslatorPass.php +++ b/vendor/symfony/translation/DependencyInjection/TranslatorPass.php @@ -24,8 +24,12 @@ class TranslatorPass implements CompilerPassInterface private $debugCommandServiceId; private $updateCommandServiceId; - public function __construct(string $translatorServiceId = 'translator.default', string $readerServiceId = 'translation.reader', string $loaderTag = 'translation.loader', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update') + public function __construct(string $translatorServiceId = 'translator.default', string $readerServiceId = 'translation.reader', string $loaderTag = 'translation.loader', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_extract') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->translatorServiceId = $translatorServiceId; $this->readerServiceId = $readerServiceId; $this->loaderTag = $loaderTag; diff --git a/vendor/symfony/translation/DependencyInjection/TranslatorPathsPass.php b/vendor/symfony/translation/DependencyInjection/TranslatorPathsPass.php index 04975b1..18a71c4 100644 --- a/vendor/symfony/translation/DependencyInjection/TranslatorPathsPass.php +++ b/vendor/symfony/translation/DependencyInjection/TranslatorPathsPass.php @@ -27,12 +27,28 @@ class TranslatorPathsPass extends AbstractRecursivePass private $updateCommandServiceId; private $resolverServiceId; private $level = 0; + + /** + * @var array + */ private $paths = []; + + /** + * @var array + */ private $definitions = []; + + /** + * @var array> + */ private $controllers = []; - public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update', string $resolverServiceId = 'argument_resolver.service') + public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_extract', string $resolverServiceId = 'argument_resolver.service') { + if (0 < \func_num_args()) { + trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__); + } + $this->translatorServiceId = $translatorServiceId; $this->debugCommandServiceId = $debugCommandServiceId; $this->updateCommandServiceId = $updateCommandServiceId; @@ -48,7 +64,7 @@ class TranslatorPathsPass extends AbstractRecursivePass foreach ($this->findControllerArguments($container) as $controller => $argument) { $id = substr($controller, 0, strpos($controller, ':') ?: \strlen($controller)); if ($container->hasDefinition($id)) { - list($locatorRef) = $argument->getValues(); + [$locatorRef] = $argument->getValues(); $this->controllers[(string) $locatorRef][$container->getDefinition($id)->getClass()] = true; } } @@ -60,6 +76,9 @@ class TranslatorPathsPass extends AbstractRecursivePass foreach ($this->paths as $class => $_) { if (($r = $container->getReflectionClass($class)) && !$r->isInterface()) { $paths[] = $r->getFileName(); + foreach ($r->getTraits() as $trait) { + $paths[] = $trait->getFileName(); + } } } if ($paths) { diff --git a/vendor/symfony/translation/Exception/IncompleteDsnException.php b/vendor/symfony/translation/Exception/IncompleteDsnException.php new file mode 100644 index 0000000..cb0ce02 --- /dev/null +++ b/vendor/symfony/translation/Exception/IncompleteDsnException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +class IncompleteDsnException extends InvalidArgumentException +{ + public function __construct(string $message, string $dsn = null, \Throwable $previous = null) + { + if ($dsn) { + $message = sprintf('Invalid "%s" provider DSN: ', $dsn).$message; + } + + parent::__construct($message, 0, $previous); + } +} diff --git a/vendor/symfony/translation/Exception/MissingRequiredOptionException.php b/vendor/symfony/translation/Exception/MissingRequiredOptionException.php new file mode 100644 index 0000000..2b5f808 --- /dev/null +++ b/vendor/symfony/translation/Exception/MissingRequiredOptionException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * @author Oskar Stark + */ +class MissingRequiredOptionException extends IncompleteDsnException +{ + public function __construct(string $option, string $dsn = null, \Throwable $previous = null) + { + $message = sprintf('The option "%s" is required but missing.', $option); + + parent::__construct($message, $dsn, $previous); + } +} diff --git a/vendor/symfony/translation/Exception/ProviderException.php b/vendor/symfony/translation/Exception/ProviderException.php new file mode 100644 index 0000000..571920d --- /dev/null +++ b/vendor/symfony/translation/Exception/ProviderException.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Fabien Potencier + */ +class ProviderException extends RuntimeException implements ProviderExceptionInterface +{ + private $response; + private $debug; + + public function __construct(string $message, ResponseInterface $response, int $code = 0, \Exception $previous = null) + { + $this->response = $response; + $this->debug = $response->getInfo('debug') ?? ''; + + parent::__construct($message, $code, $previous); + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } + + public function getDebug(): string + { + return $this->debug; + } +} diff --git a/vendor/symfony/translation/Exception/ProviderExceptionInterface.php b/vendor/symfony/translation/Exception/ProviderExceptionInterface.php new file mode 100644 index 0000000..922e827 --- /dev/null +++ b/vendor/symfony/translation/Exception/ProviderExceptionInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * @author Fabien Potencier + */ +interface ProviderExceptionInterface extends ExceptionInterface +{ + /* + * Returns debug info coming from the Symfony\Contracts\HttpClient\ResponseInterface + */ + public function getDebug(): string; +} diff --git a/vendor/symfony/translation/Exception/UnsupportedSchemeException.php b/vendor/symfony/translation/Exception/UnsupportedSchemeException.php new file mode 100644 index 0000000..7fbaa8f --- /dev/null +++ b/vendor/symfony/translation/Exception/UnsupportedSchemeException.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +use Symfony\Component\Translation\Bridge; +use Symfony\Component\Translation\Provider\Dsn; + +class UnsupportedSchemeException extends LogicException +{ + private const SCHEME_TO_PACKAGE_MAP = [ + 'crowdin' => [ + 'class' => Bridge\Crowdin\CrowdinProviderFactory::class, + 'package' => 'symfony/crowdin-translation-provider', + ], + 'loco' => [ + 'class' => Bridge\Loco\LocoProviderFactory::class, + 'package' => 'symfony/loco-translation-provider', + ], + 'lokalise' => [ + 'class' => Bridge\Lokalise\LokaliseProviderFactory::class, + 'package' => 'symfony/lokalise-translation-provider', + ], + ]; + + public function __construct(Dsn $dsn, string $name = null, array $supported = []) + { + $provider = $dsn->getScheme(); + if (false !== $pos = strpos($provider, '+')) { + $provider = substr($provider, 0, $pos); + } + $package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null; + if ($package && !class_exists($package['class'])) { + parent::__construct(sprintf('Unable to synchronize translations via "%s" as the provider is not installed; try running "composer require %s".', $provider, $package['package'])); + + return; + } + + $message = sprintf('The "%s" scheme is not supported', $dsn->getScheme()); + if ($name && $supported) { + $message .= sprintf('; supported schemes for translation provider "%s" are: "%s"', $name, implode('", "', $supported)); + } + + parent::__construct($message.'.'); + } +} diff --git a/vendor/symfony/translation/Provider/AbstractProviderFactory.php b/vendor/symfony/translation/Provider/AbstractProviderFactory.php new file mode 100644 index 0000000..17442fd --- /dev/null +++ b/vendor/symfony/translation/Provider/AbstractProviderFactory.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\IncompleteDsnException; + +abstract class AbstractProviderFactory implements ProviderFactoryInterface +{ + public function supports(Dsn $dsn): bool + { + return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true); + } + + /** + * @return string[] + */ + abstract protected function getSupportedSchemes(): array; + + protected function getUser(Dsn $dsn): string + { + if (null === $user = $dsn->getUser()) { + throw new IncompleteDsnException('User is not set.', $dsn->getOriginalDsn()); + } + + return $user; + } + + protected function getPassword(Dsn $dsn): string + { + if (null === $password = $dsn->getPassword()) { + throw new IncompleteDsnException('Password is not set.', $dsn->getOriginalDsn()); + } + + return $password; + } +} diff --git a/vendor/symfony/translation/Provider/Dsn.php b/vendor/symfony/translation/Provider/Dsn.php new file mode 100644 index 0000000..820cabf --- /dev/null +++ b/vendor/symfony/translation/Provider/Dsn.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\MissingRequiredOptionException; + +/** + * @author Fabien Potencier + * @author Oskar Stark + */ +final class Dsn +{ + private $scheme; + private $host; + private $user; + private $password; + private $port; + private $path; + private $options; + private $originalDsn; + + public function __construct(string $dsn) + { + $this->originalDsn = $dsn; + + if (false === $parsedDsn = parse_url($dsn)) { + throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN is invalid.', $dsn)); + } + + if (!isset($parsedDsn['scheme'])) { + throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a scheme.', $dsn)); + } + $this->scheme = $parsedDsn['scheme']; + + if (!isset($parsedDsn['host'])) { + throw new InvalidArgumentException(sprintf('The "%s" translation provider DSN must contain a host (use "default" by default).', $dsn)); + } + $this->host = $parsedDsn['host']; + + $this->user = '' !== ($parsedDsn['user'] ?? '') ? urldecode($parsedDsn['user']) : null; + $this->password = '' !== ($parsedDsn['pass'] ?? '') ? urldecode($parsedDsn['pass']) : null; + $this->port = $parsedDsn['port'] ?? null; + $this->path = $parsedDsn['path'] ?? null; + parse_str($parsedDsn['query'] ?? '', $this->options); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHost(): string + { + return $this->host; + } + + public function getUser(): ?string + { + return $this->user; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getPort(int $default = null): ?int + { + return $this->port ?? $default; + } + + public function getOption(string $key, $default = null) + { + return $this->options[$key] ?? $default; + } + + public function getRequiredOption(string $key) + { + if (!\array_key_exists($key, $this->options) || '' === trim($this->options[$key])) { + throw new MissingRequiredOptionException($key); + } + + return $this->options[$key]; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getOriginalDsn(): string + { + return $this->originalDsn; + } +} diff --git a/vendor/symfony/translation/Provider/FilteringProvider.php b/vendor/symfony/translation/Provider/FilteringProvider.php new file mode 100644 index 0000000..5f970a2 --- /dev/null +++ b/vendor/symfony/translation/Provider/FilteringProvider.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +/** + * Filters domains and locales between the Translator config values and those specific to each provider. + * + * @author Mathieu Santostefano + */ +class FilteringProvider implements ProviderInterface +{ + private $provider; + private $locales; + private $domains; + + public function __construct(ProviderInterface $provider, array $locales, array $domains = []) + { + $this->provider = $provider; + $this->locales = $locales; + $this->domains = $domains; + } + + public function __toString(): string + { + return (string) $this->provider; + } + + /** + * {@inheritdoc} + */ + public function write(TranslatorBagInterface $translatorBag): void + { + $this->provider->write($translatorBag); + } + + public function read(array $domains, array $locales): TranslatorBag + { + $domains = !$this->domains ? $domains : array_intersect($this->domains, $domains); + $locales = array_intersect($this->locales, $locales); + + return $this->provider->read($domains, $locales); + } + + public function delete(TranslatorBagInterface $translatorBag): void + { + $this->provider->delete($translatorBag); + } + + public function getDomains(): array + { + return $this->domains; + } +} diff --git a/vendor/symfony/translation/Provider/NullProvider.php b/vendor/symfony/translation/Provider/NullProvider.php new file mode 100644 index 0000000..f00392e --- /dev/null +++ b/vendor/symfony/translation/Provider/NullProvider.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +/** + * @author Mathieu Santostefano + */ +class NullProvider implements ProviderInterface +{ + public function __toString(): string + { + return 'null'; + } + + public function write(TranslatorBagInterface $translatorBag, bool $override = false): void + { + } + + public function read(array $domains, array $locales): TranslatorBag + { + return new TranslatorBag(); + } + + public function delete(TranslatorBagInterface $translatorBag): void + { + } +} diff --git a/vendor/symfony/translation/Provider/NullProviderFactory.php b/vendor/symfony/translation/Provider/NullProviderFactory.php new file mode 100644 index 0000000..f350f16 --- /dev/null +++ b/vendor/symfony/translation/Provider/NullProviderFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +/** + * @author Mathieu Santostefano + */ +final class NullProviderFactory extends AbstractProviderFactory +{ + public function create(Dsn $dsn): ProviderInterface + { + if ('null' === $dsn->getScheme()) { + return new NullProvider(); + } + + throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['null']; + } +} diff --git a/vendor/symfony/translation/Provider/ProviderFactoryInterface.php b/vendor/symfony/translation/Provider/ProviderFactoryInterface.php new file mode 100644 index 0000000..3fd4494 --- /dev/null +++ b/vendor/symfony/translation/Provider/ProviderFactoryInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\IncompleteDsnException; +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +interface ProviderFactoryInterface +{ + /** + * @throws UnsupportedSchemeException + * @throws IncompleteDsnException + */ + public function create(Dsn $dsn): ProviderInterface; + + public function supports(Dsn $dsn): bool; +} diff --git a/vendor/symfony/translation/Provider/ProviderInterface.php b/vendor/symfony/translation/Provider/ProviderInterface.php new file mode 100644 index 0000000..a32193f --- /dev/null +++ b/vendor/symfony/translation/Provider/ProviderInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +interface ProviderInterface +{ + public function __toString(): string; + + /** + * Translations available in the TranslatorBag only must be created. + * Translations available in both the TranslatorBag and on the provider + * must be overwritten. + * Translations available on the provider only must be kept. + */ + public function write(TranslatorBagInterface $translatorBag): void; + + public function read(array $domains, array $locales): TranslatorBag; + + public function delete(TranslatorBagInterface $translatorBag): void; +} diff --git a/vendor/symfony/translation/Provider/TranslationProviderCollection.php b/vendor/symfony/translation/Provider/TranslationProviderCollection.php new file mode 100644 index 0000000..61ac641 --- /dev/null +++ b/vendor/symfony/translation/Provider/TranslationProviderCollection.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; + +/** + * @author Mathieu Santostefano + */ +final class TranslationProviderCollection +{ + /** + * @var array + */ + private $providers; + + /** + * @param array $providers + */ + public function __construct(iterable $providers) + { + $this->providers = \is_array($providers) ? $providers : iterator_to_array($providers); + } + + public function __toString(): string + { + return '['.implode(',', array_keys($this->providers)).']'; + } + + public function has(string $name): bool + { + return isset($this->providers[$name]); + } + + public function get(string $name): ProviderInterface + { + if (!$this->has($name)) { + throw new InvalidArgumentException(sprintf('Provider "%s" not found. Available: "%s".', $name, (string) $this)); + } + + return $this->providers[$name]; + } + + public function keys(): array + { + return array_keys($this->providers); + } +} diff --git a/vendor/symfony/translation/Provider/TranslationProviderCollectionFactory.php b/vendor/symfony/translation/Provider/TranslationProviderCollectionFactory.php new file mode 100644 index 0000000..81db3a5 --- /dev/null +++ b/vendor/symfony/translation/Provider/TranslationProviderCollectionFactory.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +/** + * @author Mathieu Santostefano + */ +class TranslationProviderCollectionFactory +{ + private $factories; + private $enabledLocales; + + /** + * @param iterable $factories + */ + public function __construct(iterable $factories, array $enabledLocales) + { + $this->factories = $factories; + $this->enabledLocales = $enabledLocales; + } + + public function fromConfig(array $config): TranslationProviderCollection + { + $providers = []; + foreach ($config as $name => $currentConfig) { + $providers[$name] = $this->fromDsnObject( + new Dsn($currentConfig['dsn']), + !$currentConfig['locales'] ? $this->enabledLocales : $currentConfig['locales'], + !$currentConfig['domains'] ? [] : $currentConfig['domains'] + ); + } + + return new TranslationProviderCollection($providers); + } + + public function fromDsnObject(Dsn $dsn, array $locales, array $domains = []): ProviderInterface + { + foreach ($this->factories as $factory) { + if ($factory->supports($dsn)) { + return new FilteringProvider($factory->create($dsn), $locales, $domains); + } + } + + throw new UnsupportedSchemeException($dsn); + } +} diff --git a/vendor/symfony/translation/PseudoLocalizationTranslator.php b/vendor/symfony/translation/PseudoLocalizationTranslator.php new file mode 100644 index 0000000..c769bda --- /dev/null +++ b/vendor/symfony/translation/PseudoLocalizationTranslator.php @@ -0,0 +1,368 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * This translator should only be used in a development environment. + */ +final class PseudoLocalizationTranslator implements TranslatorInterface +{ + private const EXPANSION_CHARACTER = '~'; + + private $translator; + private $accents; + private $expansionFactor; + private $brackets; + private $parseHTML; + + /** + * @var string[] + */ + private $localizableHTMLAttributes; + + /** + * Available options: + * * accents: + * type: boolean + * default: true + * description: replace ASCII characters of the translated string with accented versions or similar characters + * example: if true, "foo" => "ƒöö". + * + * * expansion_factor: + * type: float + * default: 1 + * validation: it must be greater than or equal to 1 + * description: expand the translated string by the given factor with spaces and tildes + * example: if 2, "foo" => "~foo ~" + * + * * brackets: + * type: boolean + * default: true + * description: wrap the translated string with brackets + * example: if true, "foo" => "[foo]" + * + * * parse_html: + * type: boolean + * default: false + * description: parse the translated string as HTML - looking for HTML tags has a performance impact but allows to preserve them from alterations - it also allows to compute the visible translated string length which is useful to correctly expand ot when it contains HTML + * warning: unclosed tags are unsupported, they will be fixed (closed) by the parser - eg, "foo
bar" => "foo
bar
" + * + * * localizable_html_attributes: + * type: string[] + * default: [] + * description: the list of HTML attributes whose values can be altered - it is only useful when the "parse_html" option is set to true + * example: if ["title"], and with the "accents" option set to true, "Profile" => "Þŕöƒîļé" - if "title" was not in the "localizable_html_attributes" list, the title attribute data would be left unchanged. + */ + public function __construct(TranslatorInterface $translator, array $options = []) + { + $this->translator = $translator; + $this->accents = $options['accents'] ?? true; + + if (1.0 > ($this->expansionFactor = $options['expansion_factor'] ?? 1.0)) { + throw new \InvalidArgumentException('The expansion factor must be greater than or equal to 1.'); + } + + $this->brackets = $options['brackets'] ?? true; + + $this->parseHTML = $options['parse_html'] ?? false; + if ($this->parseHTML && !$this->accents && 1.0 === $this->expansionFactor) { + $this->parseHTML = false; + } + + $this->localizableHTMLAttributes = $options['localizable_html_attributes'] ?? []; + } + + /** + * {@inheritdoc} + */ + public function trans(string $id, array $parameters = [], string $domain = null, string $locale = null): string + { + $trans = ''; + $visibleText = ''; + + foreach ($this->getParts($this->translator->trans($id, $parameters, $domain, $locale)) as [$visible, $localizable, $text]) { + if ($visible) { + $visibleText .= $text; + } + + if (!$localizable) { + $trans .= $text; + + continue; + } + + $this->addAccents($trans, $text); + } + + $this->expand($trans, $visibleText); + + $this->addBrackets($trans); + + return $trans; + } + + public function getLocale(): string + { + return $this->translator->getLocale(); + } + + private function getParts(string $originalTrans): array + { + if (!$this->parseHTML) { + return [[true, true, $originalTrans]]; + } + + $html = mb_encode_numericentity($originalTrans, [0x80, 0xFFFF, 0, 0xFFFF], mb_detect_encoding($originalTrans, null, true) ?: 'UTF-8'); + + $useInternalErrors = libxml_use_internal_errors(true); + + $dom = new \DOMDocument(); + $dom->loadHTML(''.$html.''); + + libxml_clear_errors(); + libxml_use_internal_errors($useInternalErrors); + + return $this->parseNode($dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0)); + } + + private function parseNode(\DOMNode $node): array + { + $parts = []; + + foreach ($node->childNodes as $childNode) { + if (!$childNode instanceof \DOMElement) { + $parts[] = [true, true, $childNode->nodeValue]; + + continue; + } + + $parts[] = [false, false, '<'.$childNode->tagName]; + + /** @var \DOMAttr $attribute */ + foreach ($childNode->attributes as $attribute) { + $parts[] = [false, false, ' '.$attribute->nodeName.'="']; + + $localizableAttribute = \in_array($attribute->nodeName, $this->localizableHTMLAttributes, true); + foreach (preg_split('/(&(?:amp|quot|#039|lt|gt);+)/', htmlspecialchars($attribute->nodeValue, \ENT_QUOTES, 'UTF-8'), -1, \PREG_SPLIT_DELIM_CAPTURE) as $i => $match) { + if ('' === $match) { + continue; + } + + $parts[] = [false, $localizableAttribute && 0 === $i % 2, $match]; + } + + $parts[] = [false, false, '"']; + } + + $parts[] = [false, false, '>']; + + $parts = array_merge($parts, $this->parseNode($childNode, $parts)); + + $parts[] = [false, false, 'tagName.'>']; + } + + return $parts; + } + + private function addAccents(string &$trans, string $text): void + { + $trans .= $this->accents ? strtr($text, [ + ' ' => ' ', + '!' => '¡', + '"' => '″', + '#' => '♯', + '$' => '€', + '%' => '‰', + '&' => '⅋', + '\'' => '´', + '(' => '{', + ')' => '}', + '*' => '⁎', + '+' => '⁺', + ',' => '،', + '-' => '‐', + '.' => '·', + '/' => '⁄', + '0' => '⓪', + '1' => '①', + '2' => '②', + '3' => '③', + '4' => '④', + '5' => '⑤', + '6' => '⑥', + '7' => '⑦', + '8' => '⑧', + '9' => '⑨', + ':' => '∶', + ';' => '⁏', + '<' => '≤', + '=' => '≂', + '>' => '≥', + '?' => '¿', + '@' => '՞', + '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' => 'Ž', + '[' => '⁅', + '\\' => '∖', + ']' => '⁆', + '^' => '˄', + '_' => '‿', + '`' => '‵', + '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' => 'ž', + '{' => '(', + '|' => '¦', + '}' => ')', + '~' => '˞', + ]) : $text; + } + + private function expand(string &$trans, string $visibleText): void + { + if (1.0 >= $this->expansionFactor) { + return; + } + + $visibleLength = $this->strlen($visibleText); + $missingLength = (int) ceil($visibleLength * $this->expansionFactor) - $visibleLength; + if ($this->brackets) { + $missingLength -= 2; + } + + if (0 >= $missingLength) { + return; + } + + $words = []; + $wordsCount = 0; + foreach (preg_split('/ +/', $visibleText, -1, \PREG_SPLIT_NO_EMPTY) as $word) { + $wordLength = $this->strlen($word); + + if ($wordLength >= $missingLength) { + continue; + } + + if (!isset($words[$wordLength])) { + $words[$wordLength] = 0; + } + + ++$words[$wordLength]; + ++$wordsCount; + } + + if (!$words) { + $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); + + return; + } + + arsort($words, \SORT_NUMERIC); + + $longestWordLength = max(array_keys($words)); + + while (true) { + $r = mt_rand(1, $wordsCount); + + foreach ($words as $length => $count) { + $r -= $count; + if ($r <= 0) { + break; + } + } + + $trans .= ' '.str_repeat(self::EXPANSION_CHARACTER, $length); + + $missingLength -= $length + 1; + + if (0 === $missingLength) { + return; + } + + while ($longestWordLength >= $missingLength) { + $wordsCount -= $words[$longestWordLength]; + unset($words[$longestWordLength]); + + if (!$words) { + $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); + + return; + } + + $longestWordLength = max(array_keys($words)); + } + } + } + + private function addBrackets(string &$trans): void + { + if (!$this->brackets) { + return; + } + + $trans = '['.$trans.']'; + } + + private function strlen(string $s): int + { + return false === ($encoding = mb_detect_encoding($s, null, true)) ? \strlen($s) : mb_strlen($s, $encoding); + } +} diff --git a/vendor/symfony/translation/Reader/TranslationReader.php b/vendor/symfony/translation/Reader/TranslationReader.php index 9e51b15..e8e8638 100644 --- a/vendor/symfony/translation/Reader/TranslationReader.php +++ b/vendor/symfony/translation/Reader/TranslationReader.php @@ -25,7 +25,7 @@ class TranslationReader implements TranslationReaderInterface /** * Loaders used for import. * - * @var array + * @var array */ private $loaders = []; diff --git a/vendor/symfony/translation/Resources/functions.php b/vendor/symfony/translation/Resources/functions.php new file mode 100644 index 0000000..901d2f8 --- /dev/null +++ b/vendor/symfony/translation/Resources/functions.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +if (!\function_exists(t::class)) { + /** + * @author Nate Wiebe + */ + function t(string $message, array $parameters = [], string $domain = null): TranslatableMessage + { + return new TranslatableMessage($message, $parameters, $domain); + } +} diff --git a/vendor/symfony/translation/Test/ProviderFactoryTestCase.php b/vendor/symfony/translation/Test/ProviderFactoryTestCase.php new file mode 100644 index 0000000..6d5f4b7 --- /dev/null +++ b/vendor/symfony/translation/Test/ProviderFactoryTestCase.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Test; + +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Translation\Dumper\XliffFileDumper; +use Symfony\Component\Translation\Exception\IncompleteDsnException; +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; +use Symfony\Component\Translation\Loader\LoaderInterface; +use Symfony\Component\Translation\Provider\Dsn; +use Symfony\Component\Translation\Provider\ProviderFactoryInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * A test case to ease testing a translation provider factory. + * + * @author Mathieu Santostefano + * + * @internal + */ +abstract class ProviderFactoryTestCase extends TestCase +{ + protected $client; + protected $logger; + protected $defaultLocale; + protected $loader; + protected $xliffFileDumper; + + abstract public function createFactory(): ProviderFactoryInterface; + + /** + * @return iterable + */ + abstract public function supportsProvider(): iterable; + + /** + * @return iterable + */ + abstract public function createProvider(): iterable; + + /** + * @return iterable + */ + public function unsupportedSchemeProvider(): iterable + { + return []; + } + + /** + * @return iterable + */ + public function incompleteDsnProvider(): iterable + { + return []; + } + + /** + * @dataProvider supportsProvider + */ + public function testSupports(bool $expected, string $dsn) + { + $factory = $this->createFactory(); + + $this->assertSame($expected, $factory->supports(new Dsn($dsn))); + } + + /** + * @dataProvider createProvider + */ + public function testCreate(string $expected, string $dsn) + { + $factory = $this->createFactory(); + $provider = $factory->create(new Dsn($dsn)); + + $this->assertSame($expected, (string) $provider); + } + + /** + * @dataProvider unsupportedSchemeProvider + */ + public function testUnsupportedSchemeException(string $dsn, string $message = null) + { + $factory = $this->createFactory(); + + $dsn = new Dsn($dsn); + + $this->expectException(UnsupportedSchemeException::class); + if (null !== $message) { + $this->expectExceptionMessage($message); + } + + $factory->create($dsn); + } + + /** + * @dataProvider incompleteDsnProvider + */ + public function testIncompleteDsnException(string $dsn, string $message = null) + { + $factory = $this->createFactory(); + + $dsn = new Dsn($dsn); + + $this->expectException(IncompleteDsnException::class); + if (null !== $message) { + $this->expectExceptionMessage($message); + } + + $factory->create($dsn); + } + + protected function getClient(): HttpClientInterface + { + return $this->client ?? $this->client = new MockHttpClient(); + } + + protected function getLogger(): LoggerInterface + { + return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class); + } + + protected function getDefaultLocale(): string + { + return $this->defaultLocale ?? $this->defaultLocale = 'en'; + } + + protected function getLoader(): LoaderInterface + { + return $this->loader ?? $this->loader = $this->createMock(LoaderInterface::class); + } + + protected function getXliffFileDumper(): XliffFileDumper + { + return $this->xliffFileDumper ?? $this->xliffFileDumper = $this->createMock(XliffFileDumper::class); + } +} diff --git a/vendor/symfony/translation/Test/ProviderTestCase.php b/vendor/symfony/translation/Test/ProviderTestCase.php new file mode 100644 index 0000000..4eb0860 --- /dev/null +++ b/vendor/symfony/translation/Test/ProviderTestCase.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Test; + +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Translation\Dumper\XliffFileDumper; +use Symfony\Component\Translation\Loader\LoaderInterface; +use Symfony\Component\Translation\Provider\ProviderInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * A test case to ease testing a translation provider. + * + * @author Mathieu Santostefano + * + * @internal + */ +abstract class ProviderTestCase extends TestCase +{ + protected $client; + protected $logger; + protected $defaultLocale; + protected $loader; + protected $xliffFileDumper; + + abstract public function createProvider(HttpClientInterface $client, LoaderInterface $loader, LoggerInterface $logger, string $defaultLocale, string $endpoint): ProviderInterface; + + /** + * @return iterable + */ + abstract public function toStringProvider(): iterable; + + /** + * @dataProvider toStringProvider + */ + public function testToString(ProviderInterface $provider, string $expected) + { + $this->assertSame($expected, (string) $provider); + } + + protected function getClient(): MockHttpClient + { + return $this->client ?? $this->client = new MockHttpClient(); + } + + protected function getLoader(): LoaderInterface + { + return $this->loader ?? $this->loader = $this->createMock(LoaderInterface::class); + } + + protected function getLogger(): LoggerInterface + { + return $this->logger ?? $this->logger = $this->createMock(LoggerInterface::class); + } + + protected function getDefaultLocale(): string + { + return $this->defaultLocale ?? $this->defaultLocale = 'en'; + } + + protected function getXliffFileDumper(): XliffFileDumper + { + return $this->xliffFileDumper ?? $this->xliffFileDumper = $this->createMock(XliffFileDumper::class); + } +} diff --git a/vendor/symfony/translation/TranslatableMessage.php b/vendor/symfony/translation/TranslatableMessage.php new file mode 100644 index 0000000..282d289 --- /dev/null +++ b/vendor/symfony/translation/TranslatableMessage.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Nate Wiebe + */ +class TranslatableMessage implements TranslatableInterface +{ + private $message; + private $parameters; + private $domain; + + public function __construct(string $message, array $parameters = [], string $domain = null) + { + $this->message = $message; + $this->parameters = $parameters; + $this->domain = $domain; + } + + public function __toString(): string + { + return $this->getMessage(); + } + + public function getMessage(): string + { + return $this->message; + } + + public function getParameters(): array + { + return $this->parameters; + } + + public function getDomain(): ?string + { + return $this->domain; + } + + public function trans(TranslatorInterface $translator, string $locale = null): string + { + return $translator->trans($this->getMessage(), array_map( + static function ($parameter) use ($translator, $locale) { + return $parameter instanceof TranslatableInterface ? $parameter->trans($translator, $locale) : $parameter; + }, + $this->getParameters() + ), $this->getDomain(), $locale); + } +} diff --git a/vendor/symfony/translation/TranslatorBag.php b/vendor/symfony/translation/TranslatorBag.php new file mode 100644 index 0000000..555a9e8 --- /dev/null +++ b/vendor/symfony/translation/TranslatorBag.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Translation\Catalogue\AbstractOperation; +use Symfony\Component\Translation\Catalogue\TargetOperation; + +final class TranslatorBag implements TranslatorBagInterface +{ + /** @var MessageCatalogue[] */ + private $catalogues = []; + + public function addCatalogue(MessageCatalogue $catalogue): void + { + if (null !== $existingCatalogue = $this->getCatalogue($catalogue->getLocale())) { + $catalogue->addCatalogue($existingCatalogue); + } + + $this->catalogues[$catalogue->getLocale()] = $catalogue; + } + + public function addBag(TranslatorBagInterface $bag): void + { + foreach ($bag->getCatalogues() as $catalogue) { + $this->addCatalogue($catalogue); + } + } + + /** + * {@inheritdoc} + */ + public function getCatalogue(string $locale = null): MessageCatalogueInterface + { + if (null === $locale || !isset($this->catalogues[$locale])) { + $this->catalogues[$locale] = new MessageCatalogue($locale); + } + + return $this->catalogues[$locale]; + } + + /** + * {@inheritdoc} + */ + public function getCatalogues(): array + { + return array_values($this->catalogues); + } + + public function diff(TranslatorBagInterface $diffBag): self + { + $diff = new self(); + + foreach ($this->catalogues as $locale => $catalogue) { + if (null === $diffCatalogue = $diffBag->getCatalogue($locale)) { + $diff->addCatalogue($catalogue); + + continue; + } + + $operation = new TargetOperation($diffCatalogue, $catalogue); + $operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::NEW_BATCH); + $newCatalogue = new MessageCatalogue($locale); + + foreach ($operation->getDomains() as $domain) { + $newCatalogue->add($operation->getNewMessages($domain), $domain); + } + + $diff->addCatalogue($newCatalogue); + } + + return $diff; + } + + public function intersect(TranslatorBagInterface $intersectBag): self + { + $diff = new self(); + + foreach ($this->catalogues as $locale => $catalogue) { + if (null === $intersectCatalogue = $intersectBag->getCatalogue($locale)) { + continue; + } + + $operation = new TargetOperation($catalogue, $intersectCatalogue); + $operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::OBSOLETE_BATCH); + $obsoleteCatalogue = new MessageCatalogue($locale); + + foreach ($operation->getDomains() as $domain) { + $obsoleteCatalogue->add( + array_diff($operation->getMessages($domain), $operation->getNewMessages($domain)), + $domain + ); + } + + $diff->addCatalogue($obsoleteCatalogue); + } + + return $diff; + } +} diff --git a/vendor/symfony/var-dumper/Caster/MysqliCaster.php b/vendor/symfony/var-dumper/Caster/MysqliCaster.php new file mode 100644 index 0000000..bfe6f08 --- /dev/null +++ b/vendor/symfony/var-dumper/Caster/MysqliCaster.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Caster; + +use Symfony\Component\VarDumper\Cloner\Stub; + +/** + * @author Nicolas Grekas + * + * @internal + */ +final class MysqliCaster +{ + public static function castMysqliDriver(\mysqli_driver $c, array $a, Stub $stub, bool $isNested): array + { + foreach ($a as $k => $v) { + if (isset($c->$k)) { + $a[$k] = $c->$k; + } + } + + return $a; + } +} diff --git a/vendor/symfony/var-dumper/Command/Descriptor/CliDescriptor.php b/vendor/symfony/var-dumper/Command/Descriptor/CliDescriptor.php index dc77d03..7d9ec0e 100644 --- a/vendor/symfony/var-dumper/Command/Descriptor/CliDescriptor.php +++ b/vendor/symfony/var-dumper/Command/Descriptor/CliDescriptor.php @@ -42,7 +42,7 @@ class CliDescriptor implements DumpDescriptorInterface $io = $output instanceof SymfonyStyle ? $output : new SymfonyStyle(new ArrayInput([]), $output); $this->dumper->setColors($output->isDecorated()); - $rows = [['date', date('r', $context['timestamp'])]]; + $rows = [['date', date('r', (int) $context['timestamp'])]]; $lastIdentifier = $this->lastIdentifier; $this->lastIdentifier = $clientId; diff --git a/vendor/symfony/var-dumper/Command/Descriptor/HtmlDescriptor.php b/vendor/symfony/var-dumper/Command/Descriptor/HtmlDescriptor.php index 35a203b..636b618 100644 --- a/vendor/symfony/var-dumper/Command/Descriptor/HtmlDescriptor.php +++ b/vendor/symfony/var-dumper/Command/Descriptor/HtmlDescriptor.php @@ -94,7 +94,7 @@ HTML private function extractDate(array $context, string $format = 'r'): string { - return date($format, $context['timestamp']); + return date($format, (int) $context['timestamp']); } private function renderTags(array $tags): string diff --git a/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php b/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php index 6f4caba..2e2c818 100644 --- a/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php +++ b/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php @@ -40,7 +40,7 @@ final class SourceContextProvider implements ContextProviderInterface public function getContext(): ?array { - $trace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, $this->limit); + $trace = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, $this->limit); $file = $trace[1]['file']; $line = $trace[1]['line']; @@ -56,7 +56,7 @@ final class SourceContextProvider implements ContextProviderInterface $line = $trace[$i]['line'] ?? $line; while (++$i < $this->limit) { - if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && 0 !== strpos($trace[$i]['function'], 'call_user_func')) { + if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && !str_starts_with($trace[$i]['function'], 'call_user_func')) { $file = $trace[$i]['file']; $line = $trace[$i]['line']; @@ -98,7 +98,7 @@ final class SourceContextProvider implements ContextProviderInterface if (null !== $this->projectDir) { $context['project_dir'] = $this->projectDir; - if (0 === strpos($file, $this->projectDir)) { + if (str_starts_with($file, $this->projectDir)) { $context['file_relative'] = ltrim(substr($file, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR); } } diff --git a/vendor/symfony/var-exporter/CHANGELOG.md b/vendor/symfony/var-exporter/CHANGELOG.md new file mode 100644 index 0000000..3406c30 --- /dev/null +++ b/vendor/symfony/var-exporter/CHANGELOG.md @@ -0,0 +1,12 @@ +CHANGELOG +========= + +5.1.0 +----- + + * added argument `array &$foundClasses` to `VarExporter::export()` to ease with preloading exported values + +4.2.0 +----- + + * added the component diff --git a/vendor/symfony/var-exporter/Exception/ClassNotFoundException.php b/vendor/symfony/var-exporter/Exception/ClassNotFoundException.php new file mode 100644 index 0000000..4cebe44 --- /dev/null +++ b/vendor/symfony/var-exporter/Exception/ClassNotFoundException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Exception; + +class ClassNotFoundException extends \Exception implements ExceptionInterface +{ + public function __construct(string $class, \Throwable $previous = null) + { + parent::__construct(sprintf('Class "%s" not found.', $class), 0, $previous); + } +} diff --git a/vendor/symfony/var-exporter/Exception/ExceptionInterface.php b/vendor/symfony/var-exporter/Exception/ExceptionInterface.php new file mode 100644 index 0000000..adfaed4 --- /dev/null +++ b/vendor/symfony/var-exporter/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Exception; + +interface ExceptionInterface extends \Throwable +{ +} diff --git a/vendor/symfony/var-exporter/Exception/NotInstantiableTypeException.php b/vendor/symfony/var-exporter/Exception/NotInstantiableTypeException.php new file mode 100644 index 0000000..771ee61 --- /dev/null +++ b/vendor/symfony/var-exporter/Exception/NotInstantiableTypeException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Exception; + +class NotInstantiableTypeException extends \Exception implements ExceptionInterface +{ + public function __construct(string $type, \Throwable $previous = null) + { + parent::__construct(sprintf('Type "%s" is not instantiable.', $type), 0, $previous); + } +} diff --git a/vendor/symfony/var-exporter/Instantiator.php b/vendor/symfony/var-exporter/Instantiator.php new file mode 100644 index 0000000..368c769 --- /dev/null +++ b/vendor/symfony/var-exporter/Instantiator.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter; + +use Symfony\Component\VarExporter\Exception\ExceptionInterface; +use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; +use Symfony\Component\VarExporter\Internal\Hydrator; +use Symfony\Component\VarExporter\Internal\Registry; + +/** + * A utility class to create objects without calling their constructor. + * + * @author Nicolas Grekas + */ +final class Instantiator +{ + /** + * Creates an object and sets its properties without calling its constructor nor any other methods. + * + * For example: + * + * // creates an empty instance of Foo + * Instantiator::instantiate(Foo::class); + * + * // creates a Foo instance and sets one of its properties + * Instantiator::instantiate(Foo::class, ['propertyName' => $propertyValue]); + * + * // creates a Foo instance and sets a private property defined on its parent Bar class + * Instantiator::instantiate(Foo::class, [], [ + * Bar::class => ['privateBarProperty' => $propertyValue], + * ]); + * + * Instances of ArrayObject, ArrayIterator and SplObjectStorage can be created + * by using the special "\0" property name to define their internal value: + * + * // creates an SplObjectStorage where $info1 is attached to $obj1, etc. + * Instantiator::instantiate(SplObjectStorage::class, ["\0" => [$obj1, $info1, $obj2, $info2...]]); + * + * // creates an ArrayObject populated with $inputArray + * Instantiator::instantiate(ArrayObject::class, ["\0" => [$inputArray]]); + * + * @param string $class The class of the instance to create + * @param array $properties The properties to set on the instance + * @param array $privateProperties The private properties to set on the instance, + * keyed by their declaring class + * + * @throws ExceptionInterface When the instance cannot be created + */ + public static function instantiate(string $class, array $properties = [], array $privateProperties = []): object + { + $reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class); + + if (Registry::$cloneable[$class]) { + $wrappedInstance = [clone Registry::$prototypes[$class]]; + } elseif (Registry::$instantiableWithoutConstructor[$class]) { + $wrappedInstance = [$reflector->newInstanceWithoutConstructor()]; + } elseif (null === Registry::$prototypes[$class]) { + throw new NotInstantiableTypeException($class); + } elseif ($reflector->implementsInterface('Serializable') && (\PHP_VERSION_ID < 70400 || !method_exists($class, '__unserialize'))) { + $wrappedInstance = [unserialize('C:'.\strlen($class).':"'.$class.'":0:{}')]; + } else { + $wrappedInstance = [unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')]; + } + + if ($properties) { + $privateProperties[$class] = isset($privateProperties[$class]) ? $properties + $privateProperties[$class] : $properties; + } + + foreach ($privateProperties as $class => $properties) { + if (!$properties) { + continue; + } + foreach ($properties as $name => $value) { + // because they're also used for "unserialization", hydrators + // deal with array of instances, so we need to wrap values + $properties[$name] = [$value]; + } + (Hydrator::$hydrators[$class] ?? Hydrator::getHydrator($class))($properties, $wrappedInstance); + } + + return $wrappedInstance[0]; + } +} diff --git a/vendor/symfony/var-exporter/Internal/Exporter.php b/vendor/symfony/var-exporter/Internal/Exporter.php new file mode 100644 index 0000000..a034ddd --- /dev/null +++ b/vendor/symfony/var-exporter/Internal/Exporter.php @@ -0,0 +1,406 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Internal; + +use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Exporter +{ + /** + * Prepares an array of values for VarExporter. + * + * For performance this method is public and has no type-hints. + * + * @param array &$values + * @param \SplObjectStorage $objectsPool + * @param array &$refsPool + * @param int &$objectsCount + * @param bool &$valuesAreStatic + * + * @throws NotInstantiableTypeException When a value cannot be serialized + */ + public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount, &$valuesAreStatic): array + { + $refs = $values; + foreach ($values as $k => $value) { + if (\is_resource($value)) { + throw new NotInstantiableTypeException(get_resource_type($value).' resource'); + } + $refs[$k] = $objectsPool; + + if ($isRef = !$valueIsStatic = $values[$k] !== $objectsPool) { + $values[$k] = &$value; // Break hard references to make $values completely + unset($value); // independent from the original structure + $refs[$k] = $value = $values[$k]; + if ($value instanceof Reference && 0 > $value->id) { + $valuesAreStatic = false; + ++$value->count; + continue; + } + $refsPool[] = [&$refs[$k], $value, &$value]; + $refs[$k] = $values[$k] = new Reference(-\count($refsPool), $value); + } + + if (\is_array($value)) { + if ($value) { + $value = self::prepare($value, $objectsPool, $refsPool, $objectsCount, $valueIsStatic); + } + goto handle_value; + } elseif (!\is_object($value) || $value instanceof \UnitEnum) { + goto handle_value; + } + + $valueIsStatic = false; + if (isset($objectsPool[$value])) { + ++$objectsCount; + $value = new Reference($objectsPool[$value][0]); + goto handle_value; + } + + $class = \get_class($value); + $reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class); + + if ($reflector->hasMethod('__serialize')) { + if (!$reflector->getMethod('__serialize')->isPublic()) { + throw new \Error(sprintf('Call to %s method "%s::__serialize()".', $reflector->getMethod('__serialize')->isProtected() ? 'protected' : 'private', $class)); + } + + if (!\is_array($properties = $value->__serialize())) { + throw new \TypeError($class.'::__serialize() must return an array'); + } + + goto prepare_value; + } + + $properties = []; + $sleep = null; + $proto = Registry::$prototypes[$class]; + + if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) { + // ArrayIterator and ArrayObject need special care because their "flags" + // option changes the behavior of the (array) casting operator. + [$arrayValue, $properties] = self::getArrayObjectProperties($value, $proto); + + // populates Registry::$prototypes[$class] with a new instance + Registry::getClassReflector($class, Registry::$instantiableWithoutConstructor[$class], Registry::$cloneable[$class]); + } elseif ($value instanceof \SplObjectStorage && Registry::$cloneable[$class] && null !== $proto) { + // By implementing Serializable, SplObjectStorage breaks + // internal references; let's deal with it on our own. + foreach (clone $value as $v) { + $properties[] = $v; + $properties[] = $value[$v]; + } + $properties = ['SplObjectStorage' => ["\0" => $properties]]; + $arrayValue = (array) $value; + } elseif ($value instanceof \Serializable + || $value instanceof \__PHP_Incomplete_Class + || \PHP_VERSION_ID < 80200 && $value instanceof \DatePeriod + ) { + ++$objectsCount; + $objectsPool[$value] = [$id = \count($objectsPool), serialize($value), [], 0]; + $value = new Reference($id); + goto handle_value; + } else { + if (method_exists($class, '__sleep')) { + if (!\is_array($sleep = $value->__sleep())) { + trigger_error('serialize(): __sleep should return an array only containing the names of instance-variables to serialize', \E_USER_NOTICE); + $value = null; + goto handle_value; + } + $sleep = array_flip($sleep); + } + + $arrayValue = (array) $value; + } + + $proto = (array) $proto; + + foreach ($arrayValue as $name => $v) { + $i = 0; + $n = (string) $name; + if ('' === $n || "\0" !== $n[0]) { + $c = \PHP_VERSION_ID >= 80100 && $reflector->hasProperty($n) && ($p = $reflector->getProperty($n))->isReadOnly() ? $p->class : 'stdClass'; + } elseif ('*' === $n[1]) { + $n = substr($n, 3); + $c = $reflector->getProperty($n)->class; + if ('Error' === $c) { + $c = 'TypeError'; + } elseif ('Exception' === $c) { + $c = 'ErrorException'; + } + } else { + $i = strpos($n, "\0", 2); + $c = substr($n, 1, $i - 1); + $n = substr($n, 1 + $i); + } + if (null !== $sleep) { + if (!isset($sleep[$n]) || ($i && $c !== $class)) { + continue; + } + $sleep[$n] = false; + } + if (!\array_key_exists($name, $proto) || $proto[$name] !== $v || "\x00Error\x00trace" === $name || "\x00Exception\x00trace" === $name) { + $properties[$c][$n] = $v; + } + } + if ($sleep) { + foreach ($sleep as $n => $v) { + if (false !== $v) { + trigger_error(sprintf('serialize(): "%s" returned as member variable from __sleep() but does not exist', $n), \E_USER_NOTICE); + } + } + } + + prepare_value: + $objectsPool[$value] = [$id = \count($objectsPool)]; + $properties = self::prepare($properties, $objectsPool, $refsPool, $objectsCount, $valueIsStatic); + ++$objectsCount; + $objectsPool[$value] = [$id, $class, $properties, method_exists($class, '__unserialize') ? -$objectsCount : (method_exists($class, '__wakeup') ? $objectsCount : 0)]; + + $value = new Reference($id); + + handle_value: + if ($isRef) { + unset($value); // Break the hard reference created above + } elseif (!$valueIsStatic) { + $values[$k] = $value; + } + $valuesAreStatic = $valueIsStatic && $valuesAreStatic; + } + + return $values; + } + + public static function export($value, string $indent = '') + { + switch (true) { + case \is_int($value) || \is_float($value): return var_export($value, true); + case [] === $value: return '[]'; + case false === $value: return 'false'; + case true === $value: return 'true'; + case null === $value: return 'null'; + case '' === $value: return "''"; + case $value instanceof \UnitEnum: return ltrim(var_export($value, true), '\\'); + } + + if ($value instanceof Reference) { + if (0 <= $value->id) { + return '$o['.$value->id.']'; + } + if (!$value->count) { + return self::export($value->value, $indent); + } + $value = -$value->id; + + return '&$r['.$value.']'; + } + $subIndent = $indent.' '; + + if (\is_string($value)) { + $code = sprintf("'%s'", addcslashes($value, "'\\")); + + $code = preg_replace_callback("/((?:[\\0\\r\\n]|\u{202A}|\u{202B}|\u{202D}|\u{202E}|\u{2066}|\u{2067}|\u{2068}|\u{202C}|\u{2069})++)(.)/", function ($m) use ($subIndent) { + $m[1] = sprintf('\'."%s".\'', str_replace( + ["\0", "\r", "\n", "\u{202A}", "\u{202B}", "\u{202D}", "\u{202E}", "\u{2066}", "\u{2067}", "\u{2068}", "\u{202C}", "\u{2069}", '\n\\'], + ['\0', '\r', '\n', '\u{202A}', '\u{202B}', '\u{202D}', '\u{202E}', '\u{2066}', '\u{2067}', '\u{2068}', '\u{202C}', '\u{2069}', '\n"'."\n".$subIndent.'."\\'], + $m[1] + )); + + if ("'" === $m[2]) { + return substr($m[1], 0, -2); + } + + if ('n".\'' === substr($m[1], -4)) { + return substr_replace($m[1], "\n".$subIndent.".'".$m[2], -2); + } + + return $m[1].$m[2]; + }, $code, -1, $count); + + if ($count && str_starts_with($code, "''.")) { + $code = substr($code, 3); + } + + return $code; + } + + if (\is_array($value)) { + $j = -1; + $code = ''; + foreach ($value as $k => $v) { + $code .= $subIndent; + if (!\is_int($k) || 1 !== $k - $j) { + $code .= self::export($k, $subIndent).' => '; + } + if (\is_int($k) && $k > $j) { + $j = $k; + } + $code .= self::export($v, $subIndent).",\n"; + } + + return "[\n".$code.$indent.']'; + } + + if ($value instanceof Values) { + $code = $subIndent."\$r = [],\n"; + foreach ($value->values as $k => $v) { + $code .= $subIndent.'$r['.$k.'] = '.self::export($v, $subIndent).",\n"; + } + + return "[\n".$code.$indent.']'; + } + + if ($value instanceof Registry) { + return self::exportRegistry($value, $indent, $subIndent); + } + + if ($value instanceof Hydrator) { + return self::exportHydrator($value, $indent, $subIndent); + } + + throw new \UnexpectedValueException(sprintf('Cannot export value of type "%s".', get_debug_type($value))); + } + + private static function exportRegistry(Registry $value, string $indent, string $subIndent): string + { + $code = ''; + $serializables = []; + $seen = []; + $prototypesAccess = 0; + $factoriesAccess = 0; + $r = '\\'.Registry::class; + $j = -1; + + foreach ($value->classes as $k => $class) { + if (':' === ($class[1] ?? null)) { + $serializables[$k] = $class; + continue; + } + if (!Registry::$instantiableWithoutConstructor[$class]) { + if (is_subclass_of($class, 'Serializable') && !method_exists($class, '__unserialize')) { + $serializables[$k] = 'C:'.\strlen($class).':"'.$class.'":0:{}'; + } else { + $serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":0:{}'; + } + if (is_subclass_of($class, 'Throwable')) { + $eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0"; + $serializables[$k] = substr_replace($serializables[$k], '1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}', -4); + } + continue; + } + $code .= $subIndent.(1 !== $k - $j ? $k.' => ' : ''); + $j = $k; + $eol = ",\n"; + $c = '['.self::export($class).']'; + + if ($seen[$class] ?? false) { + if (Registry::$cloneable[$class]) { + ++$prototypesAccess; + $code .= 'clone $p'.$c; + } else { + ++$factoriesAccess; + $code .= '$f'.$c.'()'; + } + } else { + $seen[$class] = true; + if (Registry::$cloneable[$class]) { + $code .= 'clone ('.($prototypesAccess++ ? '$p' : '($p = &'.$r.'::$prototypes)').$c.' ?? '.$r.'::p'; + } else { + $code .= '('.($factoriesAccess++ ? '$f' : '($f = &'.$r.'::$factories)').$c.' ?? '.$r.'::f'; + $eol = '()'.$eol; + } + $code .= '('.substr($c, 1, -1).'))'; + } + $code .= $eol; + } + + if (1 === $prototypesAccess) { + $code = str_replace('($p = &'.$r.'::$prototypes)', $r.'::$prototypes', $code); + } + if (1 === $factoriesAccess) { + $code = str_replace('($f = &'.$r.'::$factories)', $r.'::$factories', $code); + } + if ('' !== $code) { + $code = "\n".$code.$indent; + } + + if ($serializables) { + $code = $r.'::unserialize(['.$code.'], '.self::export($serializables, $indent).')'; + } else { + $code = '['.$code.']'; + } + + return '$o = '.$code; + } + + private static function exportHydrator(Hydrator $value, string $indent, string $subIndent): string + { + $code = ''; + foreach ($value->properties as $class => $properties) { + $code .= $subIndent.' '.self::export($class).' => '.self::export($properties, $subIndent.' ').",\n"; + } + + $code = [ + self::export($value->registry, $subIndent), + self::export($value->values, $subIndent), + '' !== $code ? "[\n".$code.$subIndent.']' : '[]', + self::export($value->value, $subIndent), + self::export($value->wakeups, $subIndent), + ]; + + return '\\'.\get_class($value)."::hydrate(\n".$subIndent.implode(",\n".$subIndent, $code)."\n".$indent.')'; + } + + /** + * @param \ArrayIterator|\ArrayObject $value + * @param \ArrayIterator|\ArrayObject $proto + */ + private static function getArrayObjectProperties($value, $proto): array + { + $reflector = $value instanceof \ArrayIterator ? 'ArrayIterator' : 'ArrayObject'; + $reflector = Registry::$reflectors[$reflector] ?? Registry::getClassReflector($reflector); + + $properties = [ + $arrayValue = (array) $value, + $reflector->getMethod('getFlags')->invoke($value), + $value instanceof \ArrayObject ? $reflector->getMethod('getIteratorClass')->invoke($value) : 'ArrayIterator', + ]; + + $reflector = $reflector->getMethod('setFlags'); + $reflector->invoke($proto, \ArrayObject::STD_PROP_LIST); + + if ($properties[1] & \ArrayObject::STD_PROP_LIST) { + $reflector->invoke($value, 0); + $properties[0] = (array) $value; + } else { + $reflector->invoke($value, \ArrayObject::STD_PROP_LIST); + $arrayValue = (array) $value; + } + $reflector->invoke($value, $properties[1]); + + if ([[], 0, 'ArrayIterator'] === $properties) { + $properties = []; + } else { + if ('ArrayIterator' === $properties[2]) { + unset($properties[2]); + } + $properties = [$reflector->class => ["\0" => $properties]]; + } + + return [$arrayValue, $properties]; + } +} diff --git a/vendor/symfony/var-exporter/Internal/Hydrator.php b/vendor/symfony/var-exporter/Internal/Hydrator.php new file mode 100644 index 0000000..5ed6bdc --- /dev/null +++ b/vendor/symfony/var-exporter/Internal/Hydrator.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Internal; + +use Symfony\Component\VarExporter\Exception\ClassNotFoundException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Hydrator +{ + public static $hydrators = []; + + public $registry; + public $values; + public $properties; + public $value; + public $wakeups; + + public function __construct(?Registry $registry, ?Values $values, array $properties, $value, array $wakeups) + { + $this->registry = $registry; + $this->values = $values; + $this->properties = $properties; + $this->value = $value; + $this->wakeups = $wakeups; + } + + public static function hydrate($objects, $values, $properties, $value, $wakeups) + { + foreach ($properties as $class => $vars) { + (self::$hydrators[$class] ?? self::getHydrator($class))($vars, $objects); + } + foreach ($wakeups as $k => $v) { + if (\is_array($v)) { + $objects[-$k]->__unserialize($v); + } else { + $objects[$v]->__wakeup(); + } + } + + return $value; + } + + public static function getHydrator($class) + { + switch ($class) { + case 'stdClass': + return self::$hydrators[$class] = static function ($properties, $objects) { + foreach ($properties as $name => $values) { + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + }; + + case 'ErrorException': + return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, new class() extends \ErrorException { + }); + + case 'TypeError': + return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, new class() extends \Error { + }); + + case 'SplObjectStorage': + return self::$hydrators[$class] = static function ($properties, $objects) { + foreach ($properties as $name => $values) { + if ("\0" === $name) { + foreach ($values as $i => $v) { + for ($j = 0; $j < \count($v); ++$j) { + $objects[$i]->attach($v[$j], $v[++$j]); + } + } + continue; + } + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + }; + } + + if (!class_exists($class) && !interface_exists($class, false) && !trait_exists($class, false)) { + throw new ClassNotFoundException($class); + } + $classReflector = new \ReflectionClass($class); + + switch ($class) { + case 'ArrayIterator': + case 'ArrayObject': + $constructor = \Closure::fromCallable([$classReflector->getConstructor(), 'invokeArgs']); + + return self::$hydrators[$class] = static function ($properties, $objects) use ($constructor) { + foreach ($properties as $name => $values) { + if ("\0" !== $name) { + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + } + foreach ($properties["\0"] ?? [] as $i => $v) { + $constructor($objects[$i], $v); + } + }; + } + + if (!$classReflector->isInternal()) { + return self::$hydrators[$class] = (self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'))->bindTo(null, $class); + } + + if ($classReflector->name !== $class) { + return self::$hydrators[$classReflector->name] ?? self::getHydrator($classReflector->name); + } + + $propertySetters = []; + foreach ($classReflector->getProperties() as $propertyReflector) { + if (!$propertyReflector->isStatic()) { + $propertyReflector->setAccessible(true); + $propertySetters[$propertyReflector->name] = \Closure::fromCallable([$propertyReflector, 'setValue']); + } + } + + if (!$propertySetters) { + return self::$hydrators[$class] = self::$hydrators['stdClass'] ?? self::getHydrator('stdClass'); + } + + return self::$hydrators[$class] = static function ($properties, $objects) use ($propertySetters) { + foreach ($properties as $name => $values) { + if ($setValue = $propertySetters[$name] ?? null) { + foreach ($values as $i => $v) { + $setValue($objects[$i], $v); + } + continue; + } + foreach ($values as $i => $v) { + $objects[$i]->$name = $v; + } + } + }; + } +} diff --git a/vendor/symfony/var-exporter/Internal/Reference.php b/vendor/symfony/var-exporter/Internal/Reference.php new file mode 100644 index 0000000..e371c07 --- /dev/null +++ b/vendor/symfony/var-exporter/Internal/Reference.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Internal; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Reference +{ + public $id; + public $value; + public $count = 0; + + public function __construct(int $id, $value = null) + { + $this->id = $id; + $this->value = $value; + } +} diff --git a/vendor/symfony/var-exporter/Internal/Registry.php b/vendor/symfony/var-exporter/Internal/Registry.php new file mode 100644 index 0000000..24b77b9 --- /dev/null +++ b/vendor/symfony/var-exporter/Internal/Registry.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Internal; + +use Symfony\Component\VarExporter\Exception\ClassNotFoundException; +use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Registry +{ + public static $reflectors = []; + public static $prototypes = []; + public static $factories = []; + public static $cloneable = []; + public static $instantiableWithoutConstructor = []; + + public $classes = []; + + public function __construct(array $classes) + { + $this->classes = $classes; + } + + public static function unserialize($objects, $serializables) + { + $unserializeCallback = ini_set('unserialize_callback_func', __CLASS__.'::getClassReflector'); + + try { + foreach ($serializables as $k => $v) { + $objects[$k] = unserialize($v); + } + } finally { + ini_set('unserialize_callback_func', $unserializeCallback); + } + + return $objects; + } + + public static function p($class) + { + self::getClassReflector($class, true, true); + + return self::$prototypes[$class]; + } + + public static function f($class) + { + $reflector = self::$reflectors[$class] ?? self::getClassReflector($class, true, false); + + return self::$factories[$class] = \Closure::fromCallable([$reflector, 'newInstanceWithoutConstructor']); + } + + public static function getClassReflector($class, $instantiableWithoutConstructor = false, $cloneable = null) + { + if (!($isClass = class_exists($class)) && !interface_exists($class, false) && !trait_exists($class, false)) { + throw new ClassNotFoundException($class); + } + $reflector = new \ReflectionClass($class); + + if ($instantiableWithoutConstructor) { + $proto = $reflector->newInstanceWithoutConstructor(); + } elseif (!$isClass || $reflector->isAbstract()) { + throw new NotInstantiableTypeException($class); + } elseif ($reflector->name !== $class) { + $reflector = self::$reflectors[$name = $reflector->name] ?? self::getClassReflector($name, false, $cloneable); + self::$cloneable[$class] = self::$cloneable[$name]; + self::$instantiableWithoutConstructor[$class] = self::$instantiableWithoutConstructor[$name]; + self::$prototypes[$class] = self::$prototypes[$name]; + + return self::$reflectors[$class] = $reflector; + } else { + try { + $proto = $reflector->newInstanceWithoutConstructor(); + $instantiableWithoutConstructor = true; + } catch (\ReflectionException $e) { + $proto = $reflector->implementsInterface('Serializable') && !method_exists($class, '__unserialize') ? 'C:' : 'O:'; + if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) { + $proto = null; + } else { + try { + $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}'); + } catch (\Exception $e) { + if (__FILE__ !== $e->getFile()) { + throw $e; + } + throw new NotInstantiableTypeException($class, $e); + } + if (false === $proto) { + throw new NotInstantiableTypeException($class); + } + } + } + if (null !== $proto && !$proto instanceof \Throwable && !$proto instanceof \Serializable && !method_exists($class, '__sleep') && (\PHP_VERSION_ID < 70400 || !method_exists($class, '__serialize'))) { + try { + serialize($proto); + } catch (\Exception $e) { + throw new NotInstantiableTypeException($class, $e); + } + } + } + + if (null === $cloneable) { + if (($proto instanceof \Reflector || $proto instanceof \ReflectionGenerator || $proto instanceof \ReflectionType || $proto instanceof \IteratorIterator || $proto instanceof \RecursiveIteratorIterator) && (!$proto instanceof \Serializable && !method_exists($proto, '__wakeup') && (\PHP_VERSION_ID < 70400 || !method_exists($class, '__unserialize')))) { + throw new NotInstantiableTypeException($class); + } + + $cloneable = $reflector->isCloneable() && !$reflector->hasMethod('__clone'); + } + + self::$cloneable[$class] = $cloneable; + self::$instantiableWithoutConstructor[$class] = $instantiableWithoutConstructor; + self::$prototypes[$class] = $proto; + + if ($proto instanceof \Throwable) { + static $setTrace; + + if (null === $setTrace) { + $setTrace = [ + new \ReflectionProperty(\Error::class, 'trace'), + new \ReflectionProperty(\Exception::class, 'trace'), + ]; + $setTrace[0]->setAccessible(true); + $setTrace[1]->setAccessible(true); + $setTrace[0] = \Closure::fromCallable([$setTrace[0], 'setValue']); + $setTrace[1] = \Closure::fromCallable([$setTrace[1], 'setValue']); + } + + $setTrace[$proto instanceof \Exception]($proto, []); + } + + return self::$reflectors[$class] = $reflector; + } +} diff --git a/vendor/symfony/var-exporter/Internal/Values.php b/vendor/symfony/var-exporter/Internal/Values.php new file mode 100644 index 0000000..21ae04e --- /dev/null +++ b/vendor/symfony/var-exporter/Internal/Values.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter\Internal; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class Values +{ + public $values; + + public function __construct(array $values) + { + $this->values = $values; + } +} diff --git a/vendor/symfony/var-exporter/LICENSE b/vendor/symfony/var-exporter/LICENSE new file mode 100644 index 0000000..74cdc2d --- /dev/null +++ b/vendor/symfony/var-exporter/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-2022 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/symfony/var-exporter/README.md b/vendor/symfony/var-exporter/README.md new file mode 100644 index 0000000..a34e4c2 --- /dev/null +++ b/vendor/symfony/var-exporter/README.md @@ -0,0 +1,38 @@ +VarExporter Component +===================== + +The VarExporter component allows exporting any serializable PHP data structure to +plain PHP code. While doing so, it preserves all the semantics associated with +the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`, +`__serialize`, `__unserialize`). + +It also provides an instantiator that allows creating and populating objects +without calling their constructor nor any other methods. + +The reason to use this component *vs* `serialize()` or +[igbinary](https://github.com/igbinary/igbinary) is performance: thanks to +OPcache, the resulting code is significantly faster and more memory efficient +than using `unserialize()` or `igbinary_unserialize()`. + +Unlike `var_export()`, this works on any serializable PHP value. + +It also provides a few improvements over `var_export()`/`serialize()`: + + * the output is PSR-2 compatible; + * the output can be re-indented without messing up with `\r` or `\n` in the data + * missing classes throw a `ClassNotFoundException` instead of being unserialized to + `PHP_Incomplete_Class` objects; + * references involving `SplObjectStorage`, `ArrayObject` or `ArrayIterator` + instances are preserved; + * `Reflection*`, `IteratorIterator` and `RecursiveIteratorIterator` classes + throw an exception when being serialized (their unserialized version is broken + anyway, see https://bugs.php.net/76737). + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/var_exporter.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/vendor/symfony/var-exporter/VarExporter.php b/vendor/symfony/var-exporter/VarExporter.php new file mode 100644 index 0000000..003388e --- /dev/null +++ b/vendor/symfony/var-exporter/VarExporter.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarExporter; + +use Symfony\Component\VarExporter\Exception\ExceptionInterface; +use Symfony\Component\VarExporter\Internal\Exporter; +use Symfony\Component\VarExporter\Internal\Hydrator; +use Symfony\Component\VarExporter\Internal\Registry; +use Symfony\Component\VarExporter\Internal\Values; + +/** + * Exports serializable PHP values to PHP code. + * + * VarExporter allows serializing PHP data structures to plain PHP code (like var_export()) + * while preserving all the semantics associated with serialize() (unlike var_export()). + * + * By leveraging OPcache, the generated PHP code is faster than doing the same with unserialize(). + * + * @author Nicolas Grekas + */ +final class VarExporter +{ + /** + * Exports a serializable PHP value to PHP code. + * + * @param mixed $value The value to export + * @param bool &$isStaticValue Set to true after execution if the provided value is static, false otherwise + * @param bool &$classes Classes found in the value are added to this list as both keys and values + * + * @throws ExceptionInterface When the provided value cannot be serialized + */ + public static function export($value, bool &$isStaticValue = null, array &$foundClasses = []): string + { + $isStaticValue = true; + + if (!\is_object($value) && !(\is_array($value) && $value) && !\is_resource($value) || $value instanceof \UnitEnum) { + return Exporter::export($value); + } + + $objectsPool = new \SplObjectStorage(); + $refsPool = []; + $objectsCount = 0; + + try { + $value = Exporter::prepare([$value], $objectsPool, $refsPool, $objectsCount, $isStaticValue)[0]; + } finally { + $references = []; + foreach ($refsPool as $i => $v) { + if ($v[0]->count) { + $references[1 + $i] = $v[2]; + } + $v[0] = $v[1]; + } + } + + if ($isStaticValue) { + return Exporter::export($value); + } + + $classes = []; + $values = []; + $states = []; + foreach ($objectsPool as $i => $v) { + [, $class, $values[], $wakeup] = $objectsPool[$v]; + $foundClasses[$class] = $classes[] = $class; + + if (0 < $wakeup) { + $states[$wakeup] = $i; + } elseif (0 > $wakeup) { + $states[-$wakeup] = [$i, array_pop($values)]; + $values[] = []; + } + } + ksort($states); + + $wakeups = [null]; + foreach ($states as $k => $v) { + if (\is_array($v)) { + $wakeups[-$v[0]] = $v[1]; + } else { + $wakeups[] = $v; + } + } + + if (null === $wakeups[0]) { + unset($wakeups[0]); + } + + $properties = []; + foreach ($values as $i => $vars) { + foreach ($vars as $class => $values) { + foreach ($values as $name => $v) { + $properties[$class][$name][$i] = $v; + } + } + } + + if ($classes || $references) { + $value = new Hydrator(new Registry($classes), $references ? new Values($references) : null, $properties, $value, $wakeups); + } else { + $isStaticValue = true; + } + + return Exporter::export($value); + } +} diff --git a/vendor/symfony/var-exporter/composer.json b/vendor/symfony/var-exporter/composer.json new file mode 100644 index 0000000..29d4901 --- /dev/null +++ b/vendor/symfony/var-exporter/composer.json @@ -0,0 +1,32 @@ +{ + "name": "symfony/var-exporter", + "type": "library", + "description": "Allows exporting any serializable PHP data structure to plain PHP code", + "keywords": ["export", "serialize", "instantiate", "hydrate", "construct", "clone"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "symfony/var-dumper": "^4.4.9|^5.0.9|^6.0" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\VarExporter\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev" +} diff --git a/vendor/yurunsoft/composer-include-files/.gitignore b/vendor/yurunsoft/composer-include-files/.gitignore new file mode 100644 index 0000000..49ce3c1 --- /dev/null +++ b/vendor/yurunsoft/composer-include-files/.gitignore @@ -0,0 +1 @@ +/vendor \ No newline at end of file diff --git a/vendor/yurunsoft/composer-include-files/LICENSE b/vendor/yurunsoft/composer-include-files/LICENSE new file mode 100644 index 0000000..0901109 --- /dev/null +++ b/vendor/yurunsoft/composer-include-files/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Tim Robertson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/yurunsoft/composer-include-files/README.md b/vendor/yurunsoft/composer-include-files/README.md new file mode 100644 index 0000000..ca424c2 --- /dev/null +++ b/vendor/yurunsoft/composer-include-files/README.md @@ -0,0 +1,48 @@ +# Composer - Include Files Plugin + +When using the Composer Autoloader if you need project files included prior to files autoloaded by any of your dependencies your out of luck. No longer! + +## Installation + +```bash +composer require funkjedi/composer-include-files +``` + +## Usage + +Just add the files you need included using `"include_files"` and they will be include prior to any files included by your dependencies. + +```json +// composer.json (project) +{ + "extra": { + "include_files": [ + "/path/to/file/you/want/to/include", + "/path/to/another/file/you/want/to/include" + ] + }, +} +``` + +## Specific Use Case + +A good example of where this is required is when overriding helpers provided by Laravel. + +In the past simply modifying `bootstrap/autoload.php` to include helpers was sufficient. However new versions of PHPUnit include the Composer Autoloader prior to executing the PHPUnit bootstrap file. Consequently this method of overriding helpers is no longer viable as it will trigger a fatal error when your bootstrap file is included. + +But now we can use *Composer - Include Files Plugin* to have Composer include the files in the necessary order. + +```json +// composer.json (project) +{ + "require": { + "laravel/framework": "^5.2", + "funkjedi/composer-include-files": "^1.0", + }, + "extra": { + "include_files": [ + "app/helpers.php" + ] + }, +} +``` diff --git a/vendor/yurunsoft/composer-include-files/composer.json b/vendor/yurunsoft/composer-include-files/composer.json new file mode 100644 index 0000000..6390f19 --- /dev/null +++ b/vendor/yurunsoft/composer-include-files/composer.json @@ -0,0 +1,25 @@ +{ + "name": "yurunsoft/composer-include-files", + "type": "composer-plugin", + "description": "Include files at a higher priority than autoload files.", + "keywords": ["composer", "autoload"], + "homepage": "https://github.com/funkjedi/composer-include-files", + "license": "MIT", + "authors": [ + { + "name": "Tim Robertson", + "email": "funkjedi@gmail.com" + } + ], + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, + "autoload": { + "psr-4": { + "ComposerIncludeFiles\\": "src" + } + }, + "extra": { + "class": "ComposerIncludeFiles\\Plugin" + } +} diff --git a/vendor/yurunsoft/composer-include-files/src/Composer/AutoloadGenerator.php b/vendor/yurunsoft/composer-include-files/src/Composer/AutoloadGenerator.php new file mode 100644 index 0000000..1d41b9d --- /dev/null +++ b/vendor/yurunsoft/composer-include-files/src/Composer/AutoloadGenerator.php @@ -0,0 +1,97 @@ +getTargetDir() && !is_readable($installPath.'/'.$path)) { + // remove target-dir from file paths of the root package + $targetDir = str_replace('\\', '[\\\\/]', preg_quote(str_replace(array('/', '\\'), '', $mainPackage->getTargetDir()))); + $path = ltrim(preg_replace('{^'.$targetDir.'}', '', ltrim($path, '\\/')), '\\/'); + } + + $relativePath = empty($installPath) ? (empty($path) ? '.' : $path) : $installPath.'/'.$path; + + $autoloads[$this->getFileIdentifier($mainPackage, $path)] = $relativePath; + } + + return $autoloads; + } + + /** + * @param \Composer\Composer + * @param string + * @param string + * + * @see https://github.com/composer/composer/blob/master/src/Composer/Autoload/AutoloadGenerator.php#L115 + */ + public function dumpFiles(Composer $composer, $paths, $targetDir = 'composer', $suffix = '', $staticPhpVersion = 70000) + { + $installationManager = $composer->getInstallationManager(); + $localRepo = $composer->getRepositoryManager()->getLocalRepository(); + $mainPackage = $composer->getPackage(); + $config = $composer->getConfig(); + + $filesystem = new Filesystem(); + $filesystem->ensureDirectoryExists($config->get('vendor-dir')); + // Do not remove double realpath() calls. + // Fixes failing Windows realpath() implementation. + // See https://bugs.php.net/bug.php?id=72738 + $basePath = $filesystem->normalizePath(realpath(realpath(getcwd()))); + $vendorPath = $filesystem->normalizePath(realpath(realpath($config->get('vendor-dir')))); + $targetDir = $vendorPath.'/'.$targetDir; + $filesystem->ensureDirectoryExists($targetDir); + + $vendorPathCode = $filesystem->findShortestPathCode(realpath($targetDir), $vendorPath, true); + $vendorPathCode52 = str_replace('__DIR__', 'dirname(__FILE__)', $vendorPathCode); + $vendorPathToTargetDirCode = $filesystem->findShortestPathCode($vendorPath, realpath($targetDir), true); + + $appBaseDirCode = $filesystem->findShortestPathCode($vendorPath, $basePath, true); + $appBaseDirCode = str_replace('__DIR__', '$vendorDir', $appBaseDirCode); + + // Collect information from all packages. + $packageMap = $this->buildPackageMap($installationManager, $mainPackage, $localRepo->getCanonicalPackages()); + $autoloads = $this->parseAutoloads($packageMap, $mainPackage); + + if (!$suffix) { + if (!$config->get('autoloader-suffix') && is_readable($vendorPath.'/autoload.php')) { + $content = file_get_contents($vendorPath.'/autoload.php'); + if (preg_match('{ComposerAutoloaderInit([^:\s]+)::}', $content, $match)) { + $suffix = $match[1]; + } + } + if (!$suffix) { + $suffix = $config->get('autoloader-suffix') ?: md5(uniqid('', true)); + } + } + + $paths = $this->parseAutoloadsTypeFiles($paths, $mainPackage); + + $autoloads['files'] = array_merge($paths, $autoloads['files']); + + $includeFilesFilePath = $targetDir.'/autoload_files.php'; + if ($includeFilesFileContents = $this->getIncludeFilesFile($autoloads['files'], $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) { + file_put_contents($includeFilesFilePath, $includeFilesFileContents); + } elseif (file_exists($includeFilesFilePath)) { + unlink($includeFilesFilePath); + } + file_put_contents($targetDir.'/autoload_static.php', $this->getStaticFile($suffix, $targetDir, $vendorPath, $basePath, $staticPhpVersion)); + } +} diff --git a/vendor/yurunsoft/composer-include-files/src/Plugin.php b/vendor/yurunsoft/composer-include-files/src/Plugin.php new file mode 100644 index 0000000..7506998 --- /dev/null +++ b/vendor/yurunsoft/composer-include-files/src/Plugin.php @@ -0,0 +1,74 @@ +composer = $composer; + $this->generator = new AutoloadGenerator($composer->getEventDispatcher(), $io); + } + + /** + * @param Composer $composer + * @param IOInterface $io + */ + public function deactivate(Composer $composer, IOInterface $io) + { + // do nothing + } + + /** + * @param Composer $composer + * @param IOInterface $io + */ + public function uninstall(Composer $composer, IOInterface $io) + { + // do nothing + } + + /** + * @return array + */ + public static function getSubscribedEvents() + { + return array( + 'post-autoload-dump' => 'dumpFiles', + ); + } + + public function dumpFiles() + { + $extraConfig = $this->composer->getPackage()->getExtra(); + + if (!array_key_exists('include_files', $extraConfig) || !is_array($extraConfig['include_files'])) { + return; + } + + $this->generator->dumpFiles($this->composer, $extraConfig['include_files']); + } +} diff --git a/vendor/yurunsoft/guzzle-swoole/.php_cs.dist b/vendor/yurunsoft/guzzle-swoole/.php_cs.dist new file mode 100644 index 0000000..b257376 --- /dev/null +++ b/vendor/yurunsoft/guzzle-swoole/.php_cs.dist @@ -0,0 +1,41 @@ +setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + 'php_unit_dedicate_assert' => ['target' => '5.6'], + 'array_syntax' => ['syntax' => 'short'], + 'array_indentation' => true, + 'binary_operator_spaces' => [ + 'operators' => [ + '=>' => 'align_single_space', + ], + ], + 'concat_space' => [ + 'spacing' => 'one', + ], + 'fopen_flags' => false, + 'protected_to_private' => false, + 'native_constant_invocation' => true, + 'combine_nested_dirname' => true, + 'single_quote' => true, + 'braces' => [ + 'position_after_control_structures' => 'next', + ], + 'no_superfluous_phpdoc_tags' => false, + ]) + ->setRiskyAllowed(true) + ->setFinder( + PhpCsFixer\Finder::create() + ->exclude(__DIR__ . '/vendor') + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/tests') + ->append([__FILE__]) + ) +; diff --git a/vendor/yurunsoft/guzzle-swoole/LICENSE b/vendor/yurunsoft/guzzle-swoole/LICENSE new file mode 100644 index 0000000..688e911 --- /dev/null +++ b/vendor/yurunsoft/guzzle-swoole/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2018 宇润 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/yurunsoft/guzzle-swoole/README.md b/vendor/yurunsoft/guzzle-swoole/README.md new file mode 100644 index 0000000..c071646 --- /dev/null +++ b/vendor/yurunsoft/guzzle-swoole/README.md @@ -0,0 +1,77 @@ +# Guzzle-Swoole + +[![Latest Version](https://img.shields.io/packagist/v/yurunsoft/guzzle-swoole.svg)](https://packagist.org/packages/yurunsoft/guzzle-swoole) +[![Php Version](https://img.shields.io/badge/php-%3E=7.1-brightgreen.svg)](https://secure.php.net/) +[![Swoole Version](https://img.shields.io/badge/swoole-%3E=4.0.0-brightgreen.svg)](https://github.com/swoole/swoole-src) +[![IMI License](https://img.shields.io/github/license/Yurunsoft/Guzzle-Swoole.svg)](https://github.com/Yurunsoft/Guzzle-Swoole/blob/master/LICENSE) + +## 介绍 + +guzzle-swoole 可以无损支持 Guzzle 在 Swoole 协程环境下的运行,不需要修改任何一行第三方包代码,即可支持协程化。 + +支持 Guzzle v6.x、v7.x。 + +支持 Composer v1.x、v2.x。 + +可以用于 `ElasticSearch`、`AWS` 等 SDK 当中。 + +兼容所有 Swoole 框架。 + +QQ群:17916227 [![点击加群](https://pub.idqqimg.com/wpa/images/group.png "点击加群")](https://jq.qq.com/?_wv=1027&k=5wXf4Zq) + +## 使用说明 + +### 安装 + +手动改 `composer.json`:`"yurunsoft/guzzle-swoole":"^2.2"` + +命令行安装:`composer require yurunsoft/guzzle-swoole` + +### 全局设定处理器 + +```php +request('GET', 'http://www.baidu.com', [ + 'verify' => false, + ]); + var_dump($response->getStatusCode()); +}); + +``` + +### 手动指定 Swoole 处理器 + +```php +use GuzzleHttp\Client; +use GuzzleHttp\HandlerStack; +use Yurun\Util\Swoole\Guzzle\SwooleHandler; + +go(function(){ + $handler = new SwooleHandler(); + $stack = HandlerStack::create($handler); + $client = new Client(['handler' => $stack]); + $response = $client->request('GET', 'http://www.baidu.com', [ + 'verify' => false, + ]); + var_dump($response->getBody()->__toString(), $response->getHeaders()); +}); +``` + +更加详细的示例代码请看`test`目录下代码。 + +### ElasticSearch + +```php +$client = \Elasticsearch\ClientBuilder::create()->setHosts(['192.168.0.233:9200'])->setHandler(new \Yurun\Util\Swoole\Guzzle\Ring\SwooleHandler())->build(); +``` diff --git a/vendor/yurunsoft/guzzle-swoole/composer.json b/vendor/yurunsoft/guzzle-swoole/composer.json new file mode 100644 index 0000000..4f063dc --- /dev/null +++ b/vendor/yurunsoft/guzzle-swoole/composer.json @@ -0,0 +1,45 @@ +{ + "name": "yurunsoft/guzzle-swoole", + "type": "composer-plugin", + "license": "MIT", + "description": "让 Guzzle 支持 Swoole 协程,可以用于 ElasticSearch、AWS 等 SDK 当中", + "require": { + "php": ">=7.1", + "composer-plugin-api": "^1.0|^2.0", + "yurunsoft/composer-include-files": "^1.0|^2.0", + "yurunsoft/yurun-http": "^4.0", + "guzzlehttp/guzzle": "~6.0|~7.0", + "guzzlehttp/ringphp": "~1.0" + }, + "require-dev": { + "swoole/ide-helper": "*", + "composer/composer": "^1.0|^2.0", + "phpunit/phpunit": ">=7" + }, + "autoload": { + "psr-4": { + "Yurun\\Util\\Swoole\\Guzzle\\": "src/", + "GuzzleHttp\\": "src/GuzzleHttp/" + } + }, + "autoload-dev": { + "psr-4": { + "Yurun\\Util\\Swoole\\Guzzle\\Test\\": "tests/test/" + } + }, + "prefer-stable": true, + "extra": { + "class": "Yurun\\Util\\Swoole\\Guzzle\\Plugin\\Plugin", + "include_files": [ + "src/load_include.php", + "src/functions.php" + ] + }, + "scripts": { + "post-autoload-dump": [ + "Yurun\\Util\\Swoole\\Guzzle\\Plugin\\Plugin::dev" + ], + "test": "./tests/run", + "test-guzzle": "./tests/test-guzzle" + } +} \ No newline at end of file diff --git a/vendor/yurunsoft/guzzle-swoole/src/GuzzleHttp/DefaultHandler.php b/vendor/yurunsoft/guzzle-swoole/src/GuzzleHttp/DefaultHandler.php new file mode 100644 index 0000000..bb3a908 --- /dev/null +++ b/vendor/yurunsoft/guzzle-swoole/src/GuzzleHttp/DefaultHandler.php @@ -0,0 +1,39 @@ +dev = $dev; + } + + /** + * Apply plugin modifications to Composer. + * + * @param Composer $composer + * @param IOInterface $io + */ + public function activate(Composer $composer, IOInterface $io) + { + $this->composer = $composer; + $this->io = $io; + } + + /** + * Remove any hooks from Composer. + * + * This will be called when a plugin is deactivated before being + * uninstalled, but also before it gets upgraded to a new version + * so the old one can be deactivated and the new one activated. + * + * @param Composer $composer + * @param IOInterface $io + */ + public function deactivate(Composer $composer, IOInterface $io) + { + } + + /** + * Prepare the plugin to be uninstalled. + * + * This will be called after deactivate. + * + * @param Composer $composer + * @param IOInterface $io + */ + public function uninstall(Composer $composer, IOInterface $io) + { + } + + /** + * @return array + */ + public static function getSubscribedEvents() + { + return [ + 'post-autoload-dump' => 'dumpFiles', + ]; + } + + public function dumpFiles(): void + { + $this->parseGuzzle(); + + if (!$this->dev) + { + $this->appendIncludeFiles(); + } + } + + public static function dev(\Composer\Script\Event $event): void + { + $plugin = new static(true); + $plugin->activate($event->getComposer(), $event->getIO()); + $plugin->dumpFiles(); + } + + /** + * 处理Guzzle代码 + * + * @return void + */ + protected function parseGuzzle(): void + { + $loadFilePath = \dirname(__DIR__) . '/load.php'; + $config = $this->composer->getConfig(); + + $filesystem = new Filesystem(); + $filesystem->ensureDirectoryExists($config->get('vendor-dir')); + $vendorPath = $filesystem->normalizePath(realpath(realpath($config->get('vendor-dir')))); + $autoloadFilesFile = $vendorPath . '/composer/autoload_files.php'; + + $lockData = $this->composer->getLocker()->getLockData(); + if (!isset($lockData['packages'])) + { + throw new \RuntimeException('Cannot found packages'); + } + foreach ($lockData['packages'] as $item) + { + if ('guzzlehttp/guzzle' === $item['name']) + { + $guzzleVersion = $item['version']; + break; + } + } + if (!isset($guzzleVersion)) + { + throw new \RuntimeException('Not found guzzlehttp/guzzle'); + } + + $files = include $autoloadFilesFile; + + foreach ($files as $fileName) + { + if (preg_match('/^(.+guzzlehttp\/guzzle)\//', $fileName, $matches) > 0) + { + $guzzlePath = $matches[1]; + break; + } + } + if (!isset($guzzlePath)) + { + throw new \RuntimeException('Not found guzzlehttp/guzzle path'); + } + + [$guzzleBigVersion] = explode('.', $guzzleVersion); + + switch ($guzzleBigVersion) + { + case '6': + $path = $guzzlePath . '/src/functions.php'; + if (!\function_exists('GuzzleHttp\choose_handler')) + { + include $path; + } + $refFunction = new \ReflectionFunction('GuzzleHttp\choose_handler'); + $content = file_get_contents($path); + $eol = $this->getEOL($content); + $contents = explode($eol, $content); + for ($i = $refFunction->getStartLine() - 1; $i < $refFunction->getEndLine(); ++$i) + { + unset($contents[$i]); + } + $content = implode($eol, $contents); + file_put_contents($loadFilePath, $content); + break; + case '7': + $path = $guzzlePath . '/src/Utils.php'; + if (!method_exists('GuzzleHttp\Utils', 'chooseHandler')) + { + include $path; + } + $refMethod = new \ReflectionMethod('GuzzleHttp\Utils', 'chooseHandler'); + $content = file_get_contents($path); + $eol = $this->getEOL($content); + $contents = explode($eol, $content); + array_splice($contents, $refMethod->getStartLine() - 1, $refMethod->getEndLine() - $refMethod->getStartLine() + 1, <<composer->getEventDispatcher(), $this->io); + + $config = $this->composer->getConfig(); + $filesystem = new Filesystem(); + $filesystem->ensureDirectoryExists($config->get('vendor-dir')); + $vendorPath = $filesystem->normalizePath(realpath(realpath($config->get('vendor-dir')))); + + $generator->dumpFiles($this->composer, [ + $vendorPath . '/yurunsoft/guzzle-swoole/src/load_include.php', + $vendorPath . '/yurunsoft/guzzle-swoole/src/functions.php', + ]); + } + + /** + * 字符串是否以另一个字符串结尾. + * + * @param string $string + * @param string $compare + * + * @return bool + */ + protected function stringEndwith(string $string, string $compare): bool + { + return substr($string, -\strlen($compare)) === $compare; + } + + /** + * 获取换行符. + * + * @param string $content + * + * @return string + */ + protected function getEOL(string $content): string + { + static $eols = [ + "\r\n", + "\n", + "\r", + ]; + foreach ($eols as $eol) + { + if (strpos($content, $eol)) + { + return $eol; + } + } + + return \PHP_EOL; + } +} diff --git a/vendor/yurunsoft/guzzle-swoole/src/Ring/SwooleHandler.php b/vendor/yurunsoft/guzzle-swoole/src/Ring/SwooleHandler.php new file mode 100644 index 0000000..9de858e --- /dev/null +++ b/vendor/yurunsoft/guzzle-swoole/src/Ring/SwooleHandler.php @@ -0,0 +1,227 @@ +options = $options; + } + + /** + * @param array $request + * + * @return CompletedFutureArray + */ + public function __invoke(array $request) + { + $httpRequest = new HttpRequest(); + $yurunResponse = $this->getYurunResponse($httpRequest, $request); + $response = $this->getResponse($httpRequest, $yurunResponse); + + return new CompletedFutureArray($response); + } + + /** + * 获取 YurunHttp Response. + * + * @param \Yurun\Util\HttpRequest $httpRequest + * @param array $request + * + * @return \Yurun\Util\YurunHttp\Http\Response + */ + protected function getYurunResponse(HttpRequest $httpRequest, array $request): Response + { + foreach ($request['client'] ?? [] as $key => $value) + { + switch ($key) + { + // Violating PSR-4 to provide more room. + case 'verify': + if ($httpRequest->isVerifyCA = $value && \is_string($value)) + { + if (!file_exists($value)) + { + throw new \InvalidArgumentException("SSL CA bundle not found: $value"); + } + $httpRequest->caCert = $value; + } + break; + case 'decode_content': + break; + case 'save_to': + break; + case 'timeout': + $httpRequest->timeout = $value * 1000; + break; + case 'connect_timeout': + $httpRequest->connectTimeout = $value * 1000; + break; + case 'proxy': + if (!\is_array($value)) + { + $uri = new Uri($value); + } + elseif (isset($request['scheme'])) + { + $scheme = $request['scheme']; + if (isset($value[$scheme])) + { + $uri = new Uri($value[$scheme]); + } + else + { + break; + } + } + $httpRequest->proxy($uri->getHost(), Uri::getServerPort($uri), $uri->getScheme()); + break; + + case 'cert': + if (\is_array($value)) + { + $httpRequest->certPassword = $value[1]; + $value = $value[0]; + } + if (!file_exists($value)) + { + throw new \InvalidArgumentException("SSL certificate not found: {$value}"); + } + $httpRequest->certType = $value; + break; + case 'ssl_key': + if (\is_array($value)) + { + $httpRequest->keyPassword = $value[1]; + // $options[CURLOPT_SSLKEYPASSWD] = $value[1]; + $value = $value[0]; + } + + if (!file_exists($value)) + { + throw new \InvalidArgumentException("SSL private key not found: {$value}"); + } + $httpRequest->keyPath = $value; + break; + case 'progress': + break; + case 'debug': + break; + } + } + // headers + foreach ($request['headers'] as $name => $list) + { + $httpRequest->header($name, implode(', ', $list)); + } + if (isset($httpRequest->headers['Content-Length'])) + { + unset($httpRequest->headers['Content-Length']); + } + // request + $method = $request['http_method'] ?? 'GET'; + $url = Core::url($request); + if (isset($request['client']['curl'][\CURLOPT_PORT])) + { + $uri = new Uri($url); + $uri = $uri->withPort($request['client']['curl'][\CURLOPT_PORT]); + $url = (string) $uri; + } + if (isset($request['client']['curl'][\CURLOPT_USERPWD])) + { + $httpRequest->userPwd(...explode(':', $request['client']['curl'][\CURLOPT_USERPWD], 2)); + } + $body = Core::body($request); + $httpRequest->url = $url; + $httpRequest->requestBody((string) $body); + $yurunRequest = $httpRequest->buildRequest(null, null, $method); + + return YurunHttp::send($yurunRequest, Coroutine::getuid() > -1 ? \Yurun\Util\YurunHttp\Handler\Swoole::class : \Yurun\Util\YurunHttp\Handler\Curl::class); + } + + /** + * 获取响应数组. + * + * @param \Yurun\Util\HttpRequest $httpRequest + * @param \Yurun\Util\YurunHttp\Http\Response $yurunResponse + * + * @return array + */ + protected function getResponse(HttpRequest $httpRequest, Response $yurunResponse): array + { + $uri = new Uri($httpRequest->url); + $transferStatus = [ + 'url' => $httpRequest->url, + 'content_type' => $yurunResponse->getHeaderLine('content-type'), + 'http_code' => $yurunResponse->getStatusCode(), + 'header_size' => 0, + 'request_size' => 0, + 'filetime' => 0, + 'ssl_verify_result' => true, + 'redirect_count' => 0, + 'total_time' => $yurunResponse->totalTime(), + 'namelookup_time' => 0, + 'connect_time' => 0, + 'pretransfer_time' => 0, + 'size_upload' => 0, + 'size_download' => 0, + 'speed_download' => 0, + 'speed_upload' => 0, + 'download_content_length' => 0, + 'upload_content_length' => 0, + 'starttransfer_time' => 0, + 'redirect_time' => 0, + 'certinfo' => '', + 'primary_ip' => $uri->getHost(), + 'primary_port' => Uri::getServerPort($uri), + 'local_ip' => '127.0.0.1', + 'local_port' => 12345, + ]; + if (!$yurunResponse->success) + { + $error = new RingException($yurunResponse->getError()); + } + $version = $yurunResponse->getProtocolVersion(); + $status = $yurunResponse->getStatusCode(); + $reason = $yurunResponse->getReasonPhrase(); + $body = fopen('php://temp', 'r+'); + fwrite($body, (string) $yurunResponse->getBody()); + fseek($body, 0); + $response = [ + 'curl' => [ + 'errno' => 0, + 'error' => '', + ], + 'transfer_stats' => $transferStatus, + 'effective_url' => $transferStatus['url'], + 'headers' => $yurunResponse->getHeaders(), + 'version' => $version, + 'status' => $status, + 'reason' => $reason, + 'body' => $body, + ]; + if (isset($error)) + { + $response['error'] = $error; + } + + return $response; + } +} diff --git a/vendor/yurunsoft/guzzle-swoole/src/SwooleHandler.php b/vendor/yurunsoft/guzzle-swoole/src/SwooleHandler.php new file mode 100644 index 0000000..298c899 --- /dev/null +++ b/vendor/yurunsoft/guzzle-swoole/src/SwooleHandler.php @@ -0,0 +1,214 @@ +getUri(), $request->getHeaders(), (string) $request->getBody(), $request->getMethod(), $request->getProtocolVersion()); + $yurunRequest = $yurunRequest->withoutHeader('Content-Length') + // 是否验证 CA + ->withAttribute(Attributes::IS_VERIFY_CA, false) + // 禁止重定向 + ->withAttribute(Attributes::FOLLOW_LOCATION, false) + // 超时 + ->withAttribute(Attributes::TIMEOUT, ($options['timeout'] ?? -1) * 1000) + // 连接超时 + ->withAttribute(Attributes::CONNECT_TIMEOUT, ($options['connect_timeout'] ?? -1) * 1000); + // 用户名密码认证处理 + $auth = isset($options['auth']) ? $options['auth'] : []; + if (isset($auth[1])) + { + list($username, $password) = $auth; + $auth = base64_encode($username . ':' . $password); + $yurunRequest = $yurunRequest->withAddedHeader('Authorization', 'Basic ' . $auth); + } + if (!$yurunRequest->hasHeader('Content-Type')) + { + $yurunRequest = $yurunRequest->withHeader('Content-Type', ''); + } + // 证书 + $cert = isset($options['cert']) ? (array) $options['cert'] : []; + if (isset($cert[0])) + { + $yurunRequest = $yurunRequest->withAttribute(Attributes::CERT_PATH, $cert[0]); + } + if (isset($cert[1])) + { + $yurunRequest = $yurunRequest->withAttribute(Attributes::CERT_PASSWORD, $cert[1]); + } + // ssl_key + $key = isset($options['ssl_key']) ? (array) $options['ssl_key'] : []; + if (isset($key[0])) + { + $yurunRequest = $yurunRequest->withAttribute(Attributes::KEY_PATH, $key[0]); + } + if (isset($key[1])) + { + $yurunRequest = $yurunRequest->withAttribute(Attributes::KEY_PASSWORD, $key[1]); + } + // 代理 + $proxy = isset($options['proxy']) ? $options['proxy'] : []; + if (\is_string($proxy)) + { + $proxy = [ + 'http' => $proxy, + ]; + } + if (!(isset($proxy['no']) && \GuzzleHttp\is_host_in_noproxy($request->getUri()->getHost(), $proxy['no']))) + { + $scheme = $request->getUri()->getScheme(); + $proxyUri = isset($proxy[$scheme]) ? $proxy[$scheme] : null; + if (null !== $proxyUri && '' !== $proxyUri) + { + $proxyUri = new Uri($proxyUri); + $userinfo = explode(':', $proxyUri->getUserInfo()); + if (isset($userinfo[1])) + { + list($username, $password) = $userinfo; + if ('' === $password) + { + $password = null; + } + } + else + { + $username = '' === $userinfo[0] ? null : $userinfo[0]; + $password = null; + } + switch ($options['curl'][\CURLOPT_PROXYTYPE] ?? \CURLPROXY_HTTP) + { + case \CURLPROXY_HTTP: + case \CURLPROXY_HTTP_1_0: + case \CURLPROXY_HTTPS: + $proxyScheme = 'http'; + break; + case \CURLPROXY_SOCKS5: + case \CURLPROXY_SOCKS5_HOSTNAME: + $proxyScheme = 'socks5'; + break; + default: + throw new \RuntimeException('Guzzle-Swoole only supports HTTP and socks5 proxies'); + } + $yurunRequest = $yurunRequest->withAttribute(Attributes::USE_PROXY, true) + ->withAttribute(Attributes::PROXY_TYPE, $proxyScheme) + ->withAttribute(Attributes::PROXY_SERVER, $proxyUri->getHost()) + ->withAttribute(Attributes::PROXY_PORT, Uri::getServerPort($proxyUri)) + ->withAttribute(Attributes::PROXY_USERNAME, $username) + ->withAttribute(Attributes::PROXY_PASSWORD, $password); + } + } + // 发送请求 + $yurunResponse = YurunHttp::send($yurunRequest, \Yurun\Util\YurunHttp\Handler\Swoole::class); + if (($statusCode = $yurunResponse->getStatusCode()) < 0) + { + switch ($statusCode) + { + case -1: + return new RejectedPromise(new ConnectException(sprintf('Connect failed: errorCode: %s, errorMessage: %s', $yurunResponse->errno(), $yurunResponse->error()), $request)); + case -2: + $message = 'Request timeout'; + break; + case -3: + $message = 'Server reset'; + break; + case -4: + $message = 'Send failed'; + break; + default: + $message = 'Unknown'; + } + + return new RejectedPromise(new ConnectException($message, $request)); + } + else + { + if (isset($options['sink'])) + { + $this->parseSink($options['sink'], $yurunResponse); + } + $response = $this->getResponse($yurunResponse); + + return new FulfilledPromise($response); + } + } + + /** + * 获取 Guzzle Response. + * + * @param \Yurun\Util\YurunHttp\Http\Response $yurunResponse + * + * @return \GuzzleHttp\Psr7\Response + */ + private function getResponse(\Yurun\Util\YurunHttp\Http\Response $yurunResponse): \GuzzleHttp\Psr7\Response + { + $headers = []; + foreach ($yurunResponse->getHeaders() as $name => $str) + { + $headers[$name] = implode(', ', $str); + } + $response = new \GuzzleHttp\Psr7\Response($yurunResponse->getStatusCode(), $headers, $yurunResponse->getBody()); + + return $response; + } + + /** + * 处理 sink 选项. + * + * @see https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html#sink + * + * @param string|resource|\Psr\Http\Message\StreamInterface $sink + * @param \Yurun\Util\YurunHttp\Http\Response $yurunResponse + * + * @return void + */ + private function parseSink($sink, \Yurun\Util\YurunHttp\Http\Response &$yurunResponse): void + { + if (\is_string($sink)) + { + $fp = fopen($sink, 'w'); + if (false === $fp) + { + throw new \RuntimeException(sprintf('Open file %s failed', $sink)); + } + try + { + fwrite($fp, $yurunResponse->getBody()->__toString()); + } + finally + { + fclose($fp); + } + } + elseif (\is_resource($sink)) + { + fwrite($sink, $yurunResponse->getBody()->__toString()); + } + elseif ($sink instanceof StreamInterface) + { + $sink->write($yurunResponse->getBody()->__toString()); + } + else + { + throw new \RuntimeException('Option sink must be string or resource or \Psr\Http\Message\StreamInterface'); + } + } +} diff --git a/vendor/yurunsoft/guzzle-swoole/src/functions.php b/vendor/yurunsoft/guzzle-swoole/src/functions.php new file mode 100644 index 0000000..1947fc8 --- /dev/null +++ b/vendor/yurunsoft/guzzle-swoole/src/functions.php @@ -0,0 +1,78 @@ +expand($template, $variables); +} + +/** + * Debug function used to describe the provided value type and class. + * + * @param mixed $input + * + * @return string Returns a string containing the type of the variable and + * if a class is provided, the class name. + */ +function describe_type($input) +{ + switch (gettype($input)) { + case 'object': + return 'object(' . get_class($input) . ')'; + case 'array': + return 'array(' . count($input) . ')'; + default: + ob_start(); + var_dump($input); + // normalize float vs double + return str_replace('double(', 'float(', rtrim(ob_get_clean())); + } +} + +/** + * Parses an array of header lines into an associative array of headers. + * + * @param iterable $lines Header lines array of strings in the following + * format: "Name: Value" + * @return array + */ +function headers_from_lines($lines) +{ + $headers = []; + + foreach ($lines as $line) { + $parts = explode(':', $line, 2); + $headers[trim($parts[0])][] = isset($parts[1]) + ? trim($parts[1]) + : null; + } + + return $headers; +} + +/** + * Returns a debug stream based on the provided variable. + * + * @param mixed $value Optional value + * + * @return resource + */ +function debug_resource($value = null) +{ + if (is_resource($value)) { + return $value; + } elseif (defined('STDOUT')) { + return STDOUT; + } + + return fopen('php://output', 'w'); +} + +/** + * Chooses and creates a default handler to use based on the environment. + * + * The returned handler is not wrapped by any default middlewares. + * + * @return callable Returns the best handler for the given system. + * @throws \RuntimeException if no viable Handler is available. + */ + +/** + * Get the default User-Agent string to use with Guzzle + * + * @return string + */ +function default_user_agent() +{ + static $defaultAgent = ''; + + if (!$defaultAgent) { + $defaultAgent = 'GuzzleHttp/' . Client::VERSION; + if (extension_loaded('curl') && function_exists('curl_version')) { + $defaultAgent .= ' curl/' . \curl_version()['version']; + } + $defaultAgent .= ' PHP/' . PHP_VERSION; + } + + return $defaultAgent; +} + +/** + * Returns the default cacert bundle for the current system. + * + * First, the openssl.cafile and curl.cainfo php.ini settings are checked. + * If those settings are not configured, then the common locations for + * bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X + * and Windows are checked. If any of these file locations are found on + * disk, they will be utilized. + * + * Note: the result of this function is cached for subsequent calls. + * + * @return string + * @throws \RuntimeException if no bundle can be found. + */ +function default_ca_bundle() +{ + static $cached = null; + static $cafiles = [ + // Red Hat, CentOS, Fedora (provided by the ca-certificates package) + '/etc/pki/tls/certs/ca-bundle.crt', + // Ubuntu, Debian (provided by the ca-certificates package) + '/etc/ssl/certs/ca-certificates.crt', + // FreeBSD (provided by the ca_root_nss package) + '/usr/local/share/certs/ca-root-nss.crt', + // SLES 12 (provided by the ca-certificates package) + '/var/lib/ca-certificates/ca-bundle.pem', + // OS X provided by homebrew (using the default path) + '/usr/local/etc/openssl/cert.pem', + // Google app engine + '/etc/ca-certificates.crt', + // Windows? + 'C:\\windows\\system32\\curl-ca-bundle.crt', + 'C:\\windows\\curl-ca-bundle.crt', + ]; + + if ($cached) { + return $cached; + } + + if ($ca = ini_get('openssl.cafile')) { + return $cached = $ca; + } + + if ($ca = ini_get('curl.cainfo')) { + return $cached = $ca; + } + + foreach ($cafiles as $filename) { + if (file_exists($filename)) { + return $cached = $filename; + } + } + + throw new \RuntimeException( + <<< EOT +No system CA bundle could be found in any of the the common system locations. +PHP versions earlier than 5.6 are not properly configured to use the system's +CA bundle by default. In order to verify peer certificates, you will need to +supply the path on disk to a certificate bundle to the 'verify' request +option: http://docs.guzzlephp.org/en/latest/clients.html#verify. If you do not +need a specific certificate bundle, then Mozilla provides a commonly used CA +bundle which can be downloaded here (provided by the maintainer of cURL): +https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt. Once +you have a CA bundle available on disk, you can set the 'openssl.cafile' PHP +ini setting to point to the path to the file, allowing you to omit the 'verify' +request option. See http://curl.haxx.se/docs/sslcerts.html for more +information. +EOT + ); +} + +/** + * Creates an associative array of lowercase header names to the actual + * header casing. + * + * @param array $headers + * + * @return array + */ +function normalize_header_keys(array $headers) +{ + $result = []; + foreach (array_keys($headers) as $key) { + $result[strtolower($key)] = $key; + } + + return $result; +} + +/** + * Returns true if the provided host matches any of the no proxy areas. + * + * This method will strip a port from the host if it is present. Each pattern + * can be matched with an exact match (e.g., "foo.com" == "foo.com") or a + * partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" == + * "baz.foo.com", but ".foo.com" != "foo.com"). + * + * Areas are matched in the following cases: + * 1. "*" (without quotes) always matches any hosts. + * 2. An exact match. + * 3. The area starts with "." and the area is the last part of the host. e.g. + * '.mit.edu' will match any host that ends with '.mit.edu'. + * + * @param string $host Host to check against the patterns. + * @param array $noProxyArray An array of host patterns. + * + * @return bool + */ +function is_host_in_noproxy($host, array $noProxyArray) +{ + if (strlen($host) === 0) { + throw new \InvalidArgumentException('Empty host provided'); + } + + // Strip port if present. + if (strpos($host, ':')) { + $host = explode($host, ':', 2)[0]; + } + + foreach ($noProxyArray as $area) { + // Always match on wildcards. + if ($area === '*') { + return true; + } elseif (empty($area)) { + // Don't match on empty values. + continue; + } elseif ($area === $host) { + // Exact matches. + return true; + } else { + // Special match if the area when prefixed with ".". Remove any + // existing leading "." and add a new leading ".". + $area = '.' . ltrim($area, '.'); + if (substr($host, -(strlen($area))) === $area) { + return true; + } + } + } + + return false; +} + +/** + * Wrapper for json_decode that throws when an error occurs. + * + * @param string $json JSON data to parse + * @param bool $assoc When true, returned objects will be converted + * into associative arrays. + * @param int $depth User specified recursion depth. + * @param int $options Bitmask of JSON decode options. + * + * @return mixed + * @throws Exception\InvalidArgumentException if the JSON cannot be decoded. + * @link http://www.php.net/manual/en/function.json-decode.php + */ +function json_decode($json, $assoc = false, $depth = 512, $options = 0) +{ + $data = \json_decode($json, $assoc, $depth, $options); + if (JSON_ERROR_NONE !== json_last_error()) { + throw new Exception\InvalidArgumentException( + 'json_decode error: ' . json_last_error_msg() + ); + } + + return $data; +} + +/** + * Wrapper for JSON encoding that throws when an error occurs. + * + * @param mixed $value The value being encoded + * @param int $options JSON encode option bitmask + * @param int $depth Set the maximum depth. Must be greater than zero. + * + * @return string + * @throws Exception\InvalidArgumentException if the JSON cannot be encoded. + * @link http://www.php.net/manual/en/function.json-encode.php + */ +function json_encode($value, $options = 0, $depth = 512) +{ + $json = \json_encode($value, $options, $depth); + if (JSON_ERROR_NONE !== json_last_error()) { + throw new Exception\InvalidArgumentException( + 'json_encode error: ' . json_last_error_msg() + ); + } + + return $json; +} diff --git a/vendor/yurunsoft/guzzle-swoole/src/load_include.php b/vendor/yurunsoft/guzzle-swoole/src/load_include.php new file mode 100644 index 0000000..dc786ee --- /dev/null +++ b/vendor/yurunsoft/guzzle-swoole/src/load_include.php @@ -0,0 +1,11 @@ +setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + 'php_unit_dedicate_assert' => ['target' => '5.6'], + 'array_syntax' => ['syntax' => 'short'], + 'array_indentation' => true, + 'binary_operator_spaces' => [ + 'operators' => [ + '=>' => 'align_single_space', + ], + ], + 'concat_space' => [ + 'spacing' => 'one', + ], + 'fopen_flags' => false, + 'protected_to_private' => false, + 'native_constant_invocation' => true, + 'single_quote' => true, + 'braces' => [ + 'position_after_control_structures' => 'next', + ], + 'no_superfluous_phpdoc_tags' => false, + 'single_line_comment_style' => false, + 'combine_nested_dirname' => false, + 'backtick_to_shell_exec' => false, + ]) + ->setRiskyAllowed(true) + ->setFinder( + PhpCsFixer\Finder::create() + ->exclude(__DIR__ . '/vendor') + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/examples') + ->in(__DIR__ . '/tests') + ->append([__FILE__]) + ) +; diff --git a/vendor/yurunsoft/yurun-http/LICENSE b/vendor/yurunsoft/yurun-http/LICENSE new file mode 100644 index 0000000..86f495c --- /dev/null +++ b/vendor/yurunsoft/yurun-http/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2017 宇润 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/yurunsoft/yurun-http/README.md b/vendor/yurunsoft/yurun-http/README.md new file mode 100644 index 0000000..58484cc --- /dev/null +++ b/vendor/yurunsoft/yurun-http/README.md @@ -0,0 +1,323 @@ +# YurunHttp + +[![Latest Version](https://poser.pugx.org/yurunsoft/yurun-http/v/stable)](https://packagist.org/packages/yurunsoft/yurun-http) +![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/Yurunsoft/YurunHttp/ci/dev) +[![Php Version](https://img.shields.io/badge/php-%3E=5.5-brightgreen.svg)](https://secure.php.net/) +[![IMI Doc](https://img.shields.io/badge/docs-passing-green.svg)](http://doc.yurunsoft.com/YurunHttp) +[![IMI License](https://img.shields.io/github/license/Yurunsoft/YurunHttp.svg)](https://github.com/Yurunsoft/YurunHttp/blob/master/LICENSE) + +## 简介 + +YurunHttp,支持智能识别 Curl/Swoole 场景的高性能 Http Client。 + +支持链式操作,简单易用。支持并发批量请求、HTTP2、WebSocket 全双工通信协议。 + +非常适合用于开发通用 SDK 包,不必再为 Swoole 协程兼容而头疼! + +YurunHttp 的目标是做最好用的 PHP HTTP Client 开发包! + +### 特性 + +* GET/POST/PUT/DELETE/UPDATE 等请求方式 +* 浏览器级别 Cookies 管理 +* 上传及下载 +* 请求头和响应头 +* 失败重试 +* 自动重定向 +* HTTP 代理方式请求 +* SSL 证书(HTTPS) +* 并发批量请求 +* HTTP2 +* WebSocket +* Curl & Swoole 环境智能兼容 +* 连接池 + +--- + +开发手册文档: + +API 文档:[https://apidoc.gitee.com/yurunsoft/YurunHttp](https://apidoc.gitee.com/yurunsoft/YurunHttp) + +欢迎各位加入技术支持群17916227[![点击加群](https://pub.idqqimg.com/wpa/images/group.png "点击加群")](https://jq.qq.com/?_wv=1027&k=5wXf4Zq),如有问题可以及时解答和修复。 + +更加欢迎各位来提交PR([码云](https://gitee.com/yurunsoft/YurunHttp)/[Github](https://github.com/Yurunsoft/YurunHttp)),一起完善YurunHttp,让它能够更加好用。 + +## 重大版本更新日志 + +> 每个小版本的更新日志请移步到 Release 查看 + +v4.3.0 新增支持连接池 + +v4.2.0 重构 Swoole 处理器,并发请求性能大幅提升 (PHP 版本依赖降为 >= 5.5) + +v4.1.0 实现智能识别场景,自动选择适合 Curl/Swoole 环境的处理器 + +v4.0.0 新增支持 `Swoole` 并发批量请求 (PHP >= 7.1) + +v3.5.0 新增支持 `Curl` 并发批量请求 (PHP >= 5.5) + +v3.4.0 新增支持 `Http2` 全双工用法 + +v3.3.0 新增支持 `Http2` 兼容用法 + +v3.2.0 新增支持 `Swoole WebSocket` 客户端 + +v3.1.0 引入浏览器级别 `Cookies` 管理 + +v3.0.0 新增支持 `Swoole` 协程 + +v2.0.0 黑历史,不告诉你 + +v1.3.1 支持 `Composer` + +v1.0-1.3 初期版本迭代 + +## Composer + +本项目可以使用composer安装,遵循psr-4自动加载规则,在你的 `composer.json` 中加入下面的内容 + +```json +{ + "require": { + "yurunsoft/yurun-http": "^4.3.0" + } +} +``` + +然后执行 `composer update` 安装。 + +之后你便可以使用 `include "vendor/autoload.php";` 来自动加载类。(ps:不要忘了namespace) + +## 用法 + +更加详细的用法请看 `examples` 目录中的示例代码 + +### 简单调用 + +```php +header('aaa', 'value1') + ->headers([ + 'bbb' => 'value2', + 'ccc' => 'value3', + ]) + ->rawHeader('ddd:value4') + ->rawHeaders([ + 'eee:value5', + 'fff:value6', + ]); + +// 请求 +$response = $http->ua('YurunHttp') + ->get('http://www.baidu.com'); + +echo 'html:', PHP_EOL, $response->body(); +``` + +### 并发批量请求 + +```php +use \Yurun\Util\YurunHttp\Co\Batch; +use \Yurun\Util\HttpRequest; + +$result = Batch::run([ + (new HttpRequest)->url('https://www.imiphp.com'), + (new HttpRequest)->url('https://www.yurunsoft.com'), +]); + +var_dump($result[0]->getHeaders(), strlen($result[0]->body()), $result[0]->getStatusCode()); + +var_dump($result[1]->getHeaders(), strlen($result[1]->body()), $result[1]->getStatusCode()); +``` + +> 只有 Swoole 并发请求会受到连接池限制,Curl 不受影响 + +### Swoole 协程模式 + +```php +get('http://www.baidu.com'); + echo 'html:', PHP_EOL, $response->body(); +} +``` + +### 连接池 + +在 YurunHttp 中,连接池是全局的,默认不启用。 + +每个不同的 `host`、`port`、`ssl` 都在不同的连接池中,举个例子,下面两个 url 对应的连接池不是同一个: + +`http://www.imiphp.com`(`host=www.imiphp.com, port=80, ssl=false`) + +`https://www.imiphp.com`(`host=www.imiphp.com, port=443, ssl=true`) + +**启用全局连接池:** + +```php +\Yurun\Util\YurunHttp\ConnectionPool::enable(); +``` + +**禁用全局连接池:** + +```php +\Yurun\Util\YurunHttp\ConnectionPool::disable(); +``` + +**写入连接池设置:** + +```php +// 最大连接数=16个,连接数满等待超时时间(仅 Swoole 有效)=30s +// url 最后不要带斜杠 / +\Yurun\Util\YurunHttp\ConnectionPool::setConfig('https://imiphp.com', 16, 30); +``` + +> YurunHttp 不会限制未设置的域名的连接数 + +**特殊请求不启用连接池:** + +```php +$http = new HttpRequest; +$http->connectionPool(false); +``` + +**获取连接池对象及数据:** + +```php +use Yurun\Util\YurunHttp\Handler\Curl\CurlHttpConnectionManager; +use Yurun\Util\YurunHttp\Handler\Swoole\SwooleHttpConnectionManager; + +// 获取 Curl 连接池管理器,选择你所处环境对应的类,其实一般 Curl 不太需要连接池 +// $manager = CurlHttpConnectionManager::getInstance(); + +// 获取 Swoole 连接池管理器,选择你所处环境对应的类 +$manager = SwooleHttpConnectionManager::getInstance(); + +// 获取连接池对象集合 +$pool = $manager->getConnectionPool('https://imiphp.com'); + +// 获取连接总数 +$pool->getCount(); + +// 获取空闲连接总数 +$pool->getFree(); + +// 获取正在使用的连接总数 +$pool->getUsed(); + +// 获取连接池配置 +$config = $pool->getConfig(); +``` + +### WebSocket Client + +```php +go(function(){ + $url = 'ws://127.0.0.1:1234/'; + $http = new HttpRequest; + $client = $http->websocket($url); + if(!$client->isConnected()) + { + throw new \RuntimeException('Connect failed'); + } + $client->send('data'); + $recv = $client->recv(); + var_dump('recv:', $recv); + $client->close(); +}); +``` + +### Http2 兼容用法 + +```php +$http = new HttpRequest; +$http->protocolVersion = '2.0'; // 这句是关键 +$response = $http->get('https://wiki.swoole.com/'); +``` + +Curl、Swoole Handler 都支持 Http2,但需要注意的是编译时都需要带上启用 Http2 的参数。 + +查看是否支持: + +Curl: `php --ri curl` + +Swoole: `php --ri swoole` + +### Http2 全双工用法 + +> 该用法仅支持 Swoole + +```php +$uri = new Uri('https://wiki.swoole.com/'); + +// 客户端初始化和连接 +$client = new \Yurun\Util\YurunHttp\Http2\SwooleClient($uri->getHost(), Uri::getServerPort($uri), 'https' === $uri->getScheme()); +$client->connect(); + +// 请求构建 +$httpRequest = new HttpRequest; +$request = $httpRequest->header('aaa', 'bbb')->buildRequest($uri, [ + 'date' => $i, +], 'POST', 'json'); + +for($i = 0; $i < 10; ++$i) +{ + go(function() use($client, $request){ + // 发送(支持在多个协程执行) + $streamId = $client->send($request); + var_dump('send:' . $streamId); + + // 接收(支持在多个协程执行) + $response = $client->recv($streamId, 3); + $content = $response->body(); + var_dump($response); + }); +} +``` + +> 具体用法请看 `examples/http2Client.php` + +### PSR-7 请求构建 + +```php + + +开源不求盈利,多少都是心意,生活不易,随缘随缘…… diff --git a/vendor/yurunsoft/yurun-http/composer.json b/vendor/yurunsoft/yurun-http/composer.json new file mode 100644 index 0000000..b272bb8 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/composer.json @@ -0,0 +1,32 @@ +{ + "name": "yurunsoft/yurun-http", + "description": "YurunHttp 是开源的 PHP HTTP 类库,支持链式操作,简单易用。支持 Curl、Swoole,支持 Http、Http2、WebSocket!", + "require": { + "php": ">=5.5.0", + "psr/http-message": "~1.0" + }, + "require-dev": { + "swoole/ide-helper": "^4.5", + "phpunit/phpunit": ">=4", + "workerman/workerman": "^4.0" + }, + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "Yurun\\Util\\": "./src/" + } + }, + "autoload-dev": { + "psr-4": { + "Yurun\\Util\\YurunHttp\\Test\\": "tests/unit/" + } + }, + "scripts": { + "test": "./vendor/bin/phpunit -c ./tests/phpunit.xml", + "install-test": [ + "@composer install", + "@composer test" + ] + } +} \ No newline at end of file diff --git a/vendor/yurunsoft/yurun-http/phpstan.neon b/vendor/yurunsoft/yurun-http/phpstan.neon new file mode 100644 index 0000000..78fc1ce --- /dev/null +++ b/vendor/yurunsoft/yurun-http/phpstan.neon @@ -0,0 +1,26 @@ +parameters: + level: 6 + + paths: + - src + - tests + + bootstrapFiles: + - vendor/autoload.php + + excludePaths: + - vendor + + treatPhpDocTypesAsCertain: false + checkGenericClassInNonGenericObjectType: false + + ignoreErrors: + - '#Unsafe usage of new static\(\).+#' + - '#Method \S+ return type has no value type specified in iterable type array.#' + - '#Method \S+ has parameter \S+ with no value type specified in iterable type array.#' + - '#Property \S+ type has no value type specified in iterable type array.#' + - '#Access to an undefined property Workerman\\Connection\\TcpConnection::\$__request.#' + - + message: '#Method \S+ has no return typehint specified.#' + paths: + - tests/unit/**Test.php \ No newline at end of file diff --git a/vendor/yurunsoft/yurun-http/src/HttpRequest.php b/vendor/yurunsoft/yurun-http/src/HttpRequest.php new file mode 100644 index 0000000..7646616 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/HttpRequest.php @@ -0,0 +1,1171 @@ +open(); + } + + /** + * 析构方法. + */ + public function __destruct() + { + $this->close(); + } + + /** + * 打开一个新连接,初始化所有参数。一般不需要手动调用。 + * + * @return void + */ + public function open() + { + $this->handler = YurunHttp::getHandler(); + $this->retry = 0; + $this->headers = $this->options = []; + $this->url = $this->content = ''; + $this->useProxy = false; + $this->proxy = [ + 'auth' => 'basic', + 'type' => 'http', + ]; + $this->isVerifyCA = false; + $this->caCert = null; + $this->connectTimeout = 30000; + $this->timeout = 30000; + $this->downloadSpeed = null; + $this->uploadSpeed = null; + $this->username = null; + $this->password = null; + $this->saveFileOption = []; + } + + /** + * 关闭连接。一般不需要手动调用。 + * + * @return void + */ + public function close() + { + if ($this->handler) + { + $handler = $this->handler; + $this->handler = null; + $handler->close(); + } + } + + /** + * 创建一个新会话,等同于new. + * + * @return static + */ + public static function newSession() + { + return new static(); + } + + /** + * 获取处理器. + * + * @return \Yurun\Util\YurunHttp\Handler\IHandler|null + */ + public function getHandler() + { + return $this->handler; + } + + /** + * 设置请求地址 + * + * @param string $url 请求地址 + * + * @return static + */ + public function url($url) + { + $this->url = $url; + + return $this; + } + + /** + * 设置发送内容,requestBody的别名. + * + * @param mixed $content 发送内容,可以是字符串、数组 + * + * @return static + */ + public function content($content) + { + return $this->requestBody($content); + } + + /** + * 设置参数,requestBody的别名. + * + * @param mixed $params 发送内容,可以是字符串、数组 + * + * @return static + */ + public function params($params) + { + return $this->requestBody($params); + } + + /** + * 设置请求主体. + * + * @param string|string|array $requestBody 发送内容,可以是字符串、数组 + * + * @return static + */ + public function requestBody($requestBody) + { + $this->content = $requestBody; + + return $this; + } + + /** + * 批量设置CURL的Option. + * + * @param array $options curl_setopt_array()所需要的第二个参数 + * + * @return static + */ + public function options($options) + { + $thisOptions = &$this->options; + foreach ($options as $key => $value) + { + $thisOptions[$key] = $value; + } + + return $this; + } + + /** + * 设置CURL的Option. + * + * @param int $option 需要设置的CURLOPT_XXX选项 + * @param mixed $value 值 + * + * @return static + */ + public function option($option, $value) + { + $this->options[$option] = $value; + + return $this; + } + + /** + * 批量设置请求头. + * + * @param array $headers 键值数组 + * + * @return static + */ + public function headers($headers) + { + $thisHeaders = &$this->headers; + $thisHeaders = array_merge($thisHeaders, $headers); + + return $this; + } + + /** + * 设置请求头. + * + * @param string $header 请求头名称 + * @param string $value 值 + * + * @return static + */ + public function header($header, $value) + { + $this->headers[$header] = $value; + + return $this; + } + + /** + * 批量设置请求头,. + * + * @param array $headers 纯文本 header 数组 + * + * @return static + */ + public function rawHeaders($headers) + { + $thisHeaders = &$this->headers; + foreach ($headers as $header) + { + $list = explode(':', $header, 2); + $thisHeaders[trim($list[0])] = trim($list[1]); + } + + return $this; + } + + /** + * 设置请求头. + * + * @param string $header 纯文本 header + * + * @return static + */ + public function rawHeader($header) + { + $list = explode(':', $header, 2); + $this->headers[trim($list[0])] = trim($list[1]); + + return $this; + } + + /** + * 设置Accept. + * + * @param string $accept + * + * @return static + */ + public function accept($accept) + { + $this->headers['Accept'] = $accept; + + return $this; + } + + /** + * 设置Accept-Language. + * + * @param string $acceptLanguage + * + * @return static + */ + public function acceptLanguage($acceptLanguage) + { + $this->headers['Accept-Language'] = $acceptLanguage; + + return $this; + } + + /** + * 设置Accept-Encoding. + * + * @param string $acceptEncoding + * + * @return static + */ + public function acceptEncoding($acceptEncoding) + { + $this->headers['Accept-Encoding'] = $acceptEncoding; + + return $this; + } + + /** + * 设置Accept-Ranges. + * + * @param string $acceptRanges + * + * @return static + */ + public function acceptRanges($acceptRanges) + { + $this->headers['Accept-Ranges'] = $acceptRanges; + + return $this; + } + + /** + * 设置Cache-Control. + * + * @param string $cacheControl + * + * @return static + */ + public function cacheControl($cacheControl) + { + $this->headers['Cache-Control'] = $cacheControl; + + return $this; + } + + /** + * 批量设置Cookies. + * + * @param array $cookies 键值对应数组 + * + * @return static + */ + public function cookies($cookies) + { + $this->cookies = array_merge($this->cookies, $cookies); + + return $this; + } + + /** + * 设置Cookie. + * + * @param string $name 名称 + * @param string $value 值 + * + * @return static + */ + public function cookie($name, $value) + { + $this->cookies[$name] = $value; + + return $this; + } + + /** + * 设置Content-Type. + * + * @param string $contentType + * + * @return static + */ + public function contentType($contentType) + { + $this->headers['Content-Type'] = $contentType; + + return $this; + } + + /** + * 设置Range. + * + * @param string $range + * + * @return static + */ + public function range($range) + { + $this->headers['Range'] = $range; + + return $this; + } + + /** + * 设置Referer. + * + * @param string $referer + * + * @return static + */ + public function referer($referer) + { + $this->headers['Referer'] = $referer; + + return $this; + } + + /** + * 设置User-Agent. + * + * @param string $userAgent + * + * @return static + */ + public function userAgent($userAgent) + { + $this->headers['User-Agent'] = $userAgent; + + return $this; + } + + /** + * 设置User-Agent,userAgent的别名. + * + * @param string $userAgent + * + * @return static + */ + public function ua($userAgent) + { + return $this->userAgent($userAgent); + } + + /** + * 设置失败重试次数,状态码为5XX或者0才需要重试. + * + * @param string $retry + * + * @return static + */ + public function retry($retry) + { + $this->retry = $retry < 0 ? 0 : $retry; //至少请求1次,即重试0次 + + return $this; + } + + /** + * 代理. + * + * @param string $server 代理服务器地址 + * @param int $port 代理服务器端口 + * @param string $type 代理类型,支持:http、socks4、socks4a、socks5 + * @param string $auth 代理认证方式,支持:basic、ntlm。一般默认basic + * + * @return static + */ + public function proxy($server, $port, $type = 'http', $auth = 'basic') + { + $this->useProxy = true; + $this->proxy = [ + 'server' => $server, + 'port' => $port, + 'type' => $type, + 'auth' => $auth, + ]; + + return $this; + } + + /** + * 代理认证 + * + * @param string $username + * @param string $password + * + * @return static + */ + public function proxyAuth($username, $password) + { + $this->proxy['username'] = $username; + $this->proxy['password'] = $password; + + return $this; + } + + /** + * 设置超时时间. + * + * @param int $timeout 总超时时间,单位:毫秒 + * @param int $connectTimeout 连接超时时间,单位:毫秒 + * + * @return static + */ + public function timeout($timeout = null, $connectTimeout = null) + { + if (null !== $timeout) + { + $this->timeout = $timeout; + } + if (null !== $connectTimeout) + { + $this->connectTimeout = $connectTimeout; + } + + return $this; + } + + /** + * 限速 + * + * @param int $download 下载速度,为0则不限制,单位:字节 + * @param int $upload 上传速度,为0则不限制,单位:字节 + * + * @return static + */ + public function limitRate($download = 0, $upload = 0) + { + $this->downloadSpeed = $download; + $this->uploadSpeed = $upload; + + return $this; + } + + /** + * 设置用于连接中需要的用户名和密码 + * + * @param string $username 用户名 + * @param string $password 密码 + * + * @return static + */ + public function userPwd($username, $password) + { + $this->username = $username; + $this->password = $password; + + return $this; + } + + /** + * 保存至文件的设置. + * + * @param string $filePath 文件路径 + * @param string $fileMode 文件打开方式,默认w+ + * + * @return static + */ + public function saveFile($filePath, $fileMode = 'w+') + { + $this->saveFileOption['filePath'] = $filePath; + $this->saveFileOption['fileMode'] = $fileMode; + + return $this; + } + + /** + * 获取文件保存路径. + * + * @return string|null + */ + public function getSavePath() + { + $saveFileOption = $this->saveFileOption; + + return isset($saveFileOption['filePath']) ? $saveFileOption['filePath'] : null; + } + + /** + * 设置SSL证书. + * + * @param string $path 一个包含 PEM 格式证书的文件名 + * @param string $type 证书类型,支持的格式有”PEM”(默认值),“DER”和”ENG” + * @param string $password 使用证书需要的密码 + * + * @return static + */ + public function sslCert($path, $type = null, $password = null) + { + $this->certPath = $path; + if (null !== $type) + { + $this->certType = $type; + } + if (null !== $password) + { + $this->certPassword = $password; + } + + return $this; + } + + /** + * 设置SSL私钥. + * + * @param string $path 包含 SSL 私钥的文件名 + * @param string $type certType规定的私钥的加密类型,支持的密钥类型为”PEM”(默认值)、”DER”和”ENG” + * @param string $password SSL私钥的密码 + * + * @return static + */ + public function sslKey($path, $type = null, $password = null) + { + $this->keyPath = $path; + if (null !== $type) + { + $this->keyType = $type; + } + if (null !== $password) + { + $this->keyPassword = $password; + } + + return $this; + } + + /** + * 设置请求方法. + * + * @param string $method + * + * @return static + */ + public function method($method) + { + $this->method = $method; + + return $this; + } + + /** + * 设置是否启用连接池. + * + * @param bool $connectionPool + * + * @return static + */ + public function connectionPool($connectionPool) + { + $this->connectionPool = $connectionPool; + + return $this; + } + + /** + * 处理请求主体. + * + * @param string|string|array $requestBody + * @param string|null $contentType 内容类型,支持null/json,为null时不处理 + * + * @return array + */ + protected function parseRequestBody($requestBody, $contentType) + { + $body = $files = []; + if (\is_string($requestBody)) + { + $body = $requestBody; + } + elseif (\is_array($requestBody)) + { + switch ($contentType) + { + case 'json': + $body = json_encode($requestBody); + $this->header('Content-Type', MediaType::APPLICATION_JSON); + break; + default: + foreach ($requestBody as $k => $v) + { + if ($v instanceof UploadedFile) + { + $files[$k] = $v; + } + else + { + $body[$k] = $v; + } + } + $body = http_build_query($body, '', '&'); + } + } + else + { + throw new \InvalidArgumentException('$requestBody only can be string or array'); + } + + return [$body, $files]; + } + + /** + * 构建请求类. + * + * @param string $url 请求地址,如果为null则取url属性值 + * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值 + * @param string|null $method 请求方法,GET、POST等 + * @param string|null $contentType 内容类型,支持null/json,为null时不处理 + * + * @return \Yurun\Util\YurunHttp\Http\Request + */ + public function buildRequest($url = null, $requestBody = null, $method = null, $contentType = null) + { + if (null === $url) + { + $url = $this->url; + } + if (null === $method) + { + $method = $this->method; + } + list($body, $files) = $this->parseRequestBody(null === $requestBody ? $this->content : $requestBody, $contentType); + $request = new Request($url, $this->headers, $body, $method); + $saveFileOption = $this->saveFileOption; + $request = $request->withUploadedFiles($files) + ->withCookieParams($this->cookies) + ->withAttribute(Attributes::MAX_REDIRECTS, $this->maxRedirects) + ->withAttribute(Attributes::IS_VERIFY_CA, $this->isVerifyCA) + ->withAttribute(Attributes::CA_CERT, $this->caCert) + ->withAttribute(Attributes::CERT_PATH, $this->certPath) + ->withAttribute(Attributes::CERT_PASSWORD, $this->certPassword) + ->withAttribute(Attributes::CERT_TYPE, $this->certType) + ->withAttribute(Attributes::KEY_PATH, $this->keyPath) + ->withAttribute(Attributes::KEY_PASSWORD, $this->keyPassword) + ->withAttribute(Attributes::KEY_TYPE, $this->keyType) + ->withAttribute(Attributes::OPTIONS, $this->options) + ->withAttribute(Attributes::SAVE_FILE_PATH, isset($saveFileOption['filePath']) ? $saveFileOption['filePath'] : null) + ->withAttribute(Attributes::USE_PROXY, $this->useProxy) + ->withAttribute(Attributes::USERNAME, $this->username) + ->withAttribute(Attributes::PASSWORD, $this->password) + ->withAttribute(Attributes::CONNECT_TIMEOUT, $this->connectTimeout) + ->withAttribute(Attributes::TIMEOUT, $this->timeout) + ->withAttribute(Attributes::DOWNLOAD_SPEED, $this->downloadSpeed) + ->withAttribute(Attributes::UPLOAD_SPEED, $this->uploadSpeed) + ->withAttribute(Attributes::FOLLOW_LOCATION, $this->followLocation) + ->withAttribute(Attributes::CONNECTION_POOL, $this->connectionPool) + ->withProtocolVersion($this->protocolVersion) + ; + foreach ($this->proxy as $name => $value) + { + $request = $request->withAttribute('proxy.' . $name, $value); + } + + return $request; + } + + /** + * 发送请求,所有请求的老祖宗. + * + * @param string|null $url 请求地址,如果为null则取url属性值 + * @param string|array|null $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值 + * @param string $method 请求方法,GET、POST等 + * @param string|null $contentType 内容类型,支持null/json,为null时不处理 + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function send($url = null, $requestBody = null, $method = null, $contentType = null) + { + $request = $this->buildRequest($url, $requestBody, $method, $contentType); + + return YurunHttp::send($request, $this->handler); + } + + /** + * 发送 Http2 请求不调用 recv(). + * + * @param string|null $url 请求地址,如果为null则取url属性值 + * @param string|array|null $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值 + * @param string $method 请求方法,GET、POST等 + * @param string|null $contentType 内容类型,支持null/json,为null时不处理 + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function sendHttp2WithoutRecv($url = null, $requestBody = null, $method = 'GET', $contentType = null) + { + $request = $this->buildRequest($url, $requestBody, $method, $contentType) + ->withProtocolVersion('2.0') + ->withAttribute(Attributes::HTTP2_NOT_RECV, true); + + return YurunHttp::send($request, $this->handler); + } + + /** + * GET请求 + * + * @param string $url 请求地址,如果为null则取url属性值 + * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值 + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function get($url = null, $requestBody = null) + { + if (!empty($requestBody)) + { + if (strpos($url, '?')) + { + $url .= '&'; + } + else + { + $url .= '?'; + } + $url .= http_build_query($requestBody, '', '&'); + } + + return $this->send($url, [], 'GET'); + } + + /** + * POST请求 + * + * @param string $url 请求地址,如果为null则取url属性值 + * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值 + * @param string|null $contentType 内容类型,支持null/json,为null时不处理 + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function post($url = null, $requestBody = null, $contentType = null) + { + return $this->send($url, $requestBody, 'POST', $contentType); + } + + /** + * HEAD请求 + * + * @param string $url 请求地址,如果为null则取url属性值 + * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值 + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function head($url = null, $requestBody = null) + { + return $this->send($url, $requestBody, 'HEAD'); + } + + /** + * PUT请求 + * + * @param string $url 请求地址,如果为null则取url属性值 + * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值 + * @param string|null $contentType 内容类型,支持null/json,为null时不处理 + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function put($url = null, $requestBody = null, $contentType = null) + { + return $this->send($url, $requestBody, 'PUT', $contentType); + } + + /** + * PATCH请求 + * + * @param string $url 请求地址,如果为null则取url属性值 + * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值 + * @param string|null $contentType 内容类型,支持null/json,为null时不处理 + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function patch($url = null, $requestBody = null, $contentType = null) + { + return $this->send($url, $requestBody, 'PATCH', $contentType); + } + + /** + * DELETE请求 + * + * @param string $url 请求地址,如果为null则取url属性值 + * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值 + * @param string|null $contentType 内容类型,支持null/json,为null时不处理 + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function delete($url = null, $requestBody = null, $contentType = null) + { + return $this->send($url, $requestBody, 'DELETE', $contentType); + } + + /** + * 直接下载文件. + * + * @param string $fileName 保存路径,如果以 .* 结尾,则根据 Content-Type 自动决定扩展名 + * @param string $url 下载文件地址 + * @param string|array $requestBody 发送内容,可以是字符串、数组,如果为空则取content属性值 + * @param string $method 请求方法,GET、POST等,一般用GET + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function download($fileName, $url = null, $requestBody = null, $method = 'GET') + { + $isAutoExt = self::checkDownloadIsAutoExt($fileName, $fileName); + $result = $this->saveFile($fileName)->send($url, $requestBody, $method); + if ($isAutoExt) + { + self::parseDownloadAutoExt($result, $fileName); + } + $this->saveFileOption = []; + + return $result; + } + + /** + * WebSocket. + * + * @param string $url + * + * @return \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient + */ + public function websocket($url = null) + { + $request = $this->buildRequest($url); + + return YurunHttp::websocket($request, $this->handler); + } + + /** + * 检查下载文件名是否要自动扩展名. + * + * @param string $fileName + * @param string $tempFileName + * + * @return bool + */ + public static function checkDownloadIsAutoExt($fileName, &$tempFileName) + { + $flagLength = \strlen(self::AUTO_EXT_FLAG); + if (self::AUTO_EXT_FLAG !== substr($fileName, -$flagLength)) + { + return false; + } + $tempFileName = substr($fileName, 0, -$flagLength) . self::AUTO_EXT_TEMP_EXT; + + return true; + } + + /** + * 处理下载的自动扩展名. + * + * @param \Yurun\Util\YurunHttp\Http\Response $response + * @param string $tempFileName + * + * @return void + */ + public static function parseDownloadAutoExt(&$response, $tempFileName) + { + $ext = MediaType::getExt($response->getHeaderLine('Content-Type')); + if (null === $ext) + { + $ext = 'file'; + } + $savedFileName = substr($tempFileName, 0, -\strlen(self::AUTO_EXT_TEMP_EXT)) . '.' . $ext; + rename($tempFileName, $savedFileName); + $response = $response->withSavedFileName($savedFileName); + } +} + +if (\extension_loaded('curl')) +{ + // 代理认证方式 + HttpRequest::$proxyAuths = [ + 'basic' => \CURLAUTH_BASIC, + 'ntlm' => \CURLAUTH_NTLM, + ]; + + // 代理类型 + HttpRequest::$proxyType = [ + 'http' => \CURLPROXY_HTTP, + 'socks4' => \CURLPROXY_SOCKS4, + 'socks4a' => 6, // CURLPROXY_SOCKS4A + 'socks5' => \CURLPROXY_SOCKS5, + ]; +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp.php b/vendor/yurunsoft/yurun-http/src/YurunHttp.php new file mode 100644 index 0000000..66b692f --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp.php @@ -0,0 +1,200 @@ + -1) + { + $class = \Yurun\Util\YurunHttp\Handler\Swoole::class; + } + else + { + $class = \Yurun\Util\YurunHttp\Handler\Curl::class; + } + + return new $class(); + } + + /** + * 发送请求并获取结果. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param \Yurun\Util\YurunHttp\Handler\IHandler|string|null $handlerClass + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public static function send($request, $handlerClass = null) + { + if ($handlerClass instanceof IHandler) + { + $handler = $handlerClass; + $needClose = false; + } + else + { + $needClose = true; + if (null === $handlerClass) + { + $handler = static::getHandler(); + } + else + { + $handler = new $handlerClass(); + } + } + /** @var IHandler $handler */ + $time = microtime(true); + foreach (static::$attributes as $name => $value) + { + if (null === $request->getAttribute($name)) + { + $request = $request->withAttribute($name, $value); + } + } + $handler->send($request); + $response = $handler->recv(); + if (!$response) + { + return $response; + } + $response = $response->withTotalTime(microtime(true) - $time); + if ($needClose) + { + $handler->close(); + } + + return $response; + } + + /** + * 发起 WebSocket 连接. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param \Yurun\Util\YurunHttp\Handler\IHandler|string $handlerClass + * + * @return \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient + */ + public static function websocket($request, $handlerClass = null) + { + if ($handlerClass instanceof IHandler) + { + $handler = $handlerClass; + } + elseif (null === $handlerClass) + { + $handler = static::getHandler(); + } + else + { + $handler = new $handlerClass(); + } + foreach (static::$attributes as $name => $value) + { + if (null === $request->getAttribute($name)) + { + $request = $request->withAttribute($name, $value); + } + } + + return $handler->websocket($request); + } + + /** + * 获取所有全局属性. + * + * @return array + */ + public static function getAttributes() + { + return static::$attributes; + } + + /** + * 获取全局属性值 + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public static function getAttribute($name, $default = null) + { + if (\array_key_exists($name, static::$attributes)) + { + return static::$attributes[$name]; + } + else + { + return $default; + } + } + + /** + * 设置全局属性值 + * + * @param string $name + * @param mixed $value + * + * @return mixed + */ + public static function setAttribute($name, $value) + { + static::$attributes[$name] = $value; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Attributes.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Attributes.php new file mode 100644 index 0000000..37f032c --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Attributes.php @@ -0,0 +1,216 @@ + $request) + { + if ($request instanceof HttpRequest) + { + $savePath = $request->getSavePath(); + if (null !== $savePath && HttpRequest::checkDownloadIsAutoExt($savePath, $savePath)) + { + $request->saveFileOption['filePath'] = $savePath; + $downloadAutoExt[] = $i; + } + $batchRequests[$i] = $request->buildRequest(); + } + elseif (!$request instanceof \Yurun\Util\YurunHttp\Http\Request) + { + throw new \InvalidArgumentException('Request must be instance of \Yurun\Util\YurunHttp\Http\Request or \Yurun\Util\HttpRequest'); + } + } + if (null === $handlerClass) + { + $handler = YurunHttp::getHandler(); + } + else + { + $handler = new $handlerClass(); + } + /** @var \Yurun\Util\YurunHttp\Handler\IHandler $handler */ + $result = $handler->coBatch($batchRequests, $timeout); + foreach ($downloadAutoExt as $i) + { + if (isset($result[$i])) + { + $response = &$result[$i]; + } + else + { + $response = null; + } + if ($response) + { + HttpRequest::parseDownloadAutoExt($response, $response->getRequest()->getAttribute(Attributes::SAVE_FILE_PATH)); + } + unset($response); + } + + return $result; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/ConnectionPool.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/ConnectionPool.php new file mode 100644 index 0000000..c1bd526 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/ConnectionPool.php @@ -0,0 +1,169 @@ +setMaxConnections($maxConnections); + $config->setWaitTimeout($waitTimeout); + } + else + { + self::$connectionPoolConfigs[$url] = $config = new PoolConfig($url, $maxConnections, $waitTimeout); + } + foreach (self::$connectionManagers as $class) + { + /** @var IConnectionManager $connectionManager */ + $connectionManager = $class::getInstance(); + $connectionManagerConfig = $connectionManager->getConfig($url); + if ($connectionManagerConfig) + { + $connectionManagerConfig->setMaxConnections($maxConnections); + $connectionManagerConfig->setWaitTimeout($waitTimeout); + } + else + { + $connectionManager->setConfig($url, $maxConnections, $waitTimeout); + } + } + } + + /** + * 获取连接池配置. + * + * @param string $url + * + * @return PoolConfig|null + */ + public static function getConfig($url) + { + if (isset(self::$connectionPoolConfigs[$url])) + { + return self::$connectionPoolConfigs[$url]; + } + else + { + return null; + } + } + + /** + * 获取键. + * + * @param string|UriInterface $url + * + * @return string + */ + public static function getKey($url) + { + if ($url instanceof UriInterface) + { + return $url->getScheme() . '://' . Uri::getDomain($url); + } + else + { + return $url; + } + } + + /** + * Get 连接管理类列表. + * + * @return array + */ + public static function getConnectionManagers() + { + return self::$connectionManagers; + } + + /** + * Set 连接管理类列表. + * + * @param array $connectionManagers 连接管理类列表 + * + * @return void + */ + public static function setConnectionManagers(array $connectionManagers) + { + self::$connectionManagers = $connectionManagers; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Cookie/CookieItem.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Cookie/CookieItem.php new file mode 100644 index 0000000..7f469d4 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Cookie/CookieItem.php @@ -0,0 +1,144 @@ +name = $name; + $this->value = $value; + $this->expires = (int) $expires; + $this->path = $path; + $this->domain = $domain; + $this->secure = $secure; + $this->httpOnly = $httpOnly; + } + + /** + * 获取新实例对象 + * + * @param array $data + * + * @return static + */ + public static function newInstance($data) + { + $object = new static('', ''); + foreach ($data as $k => $v) + { + $object->$k = $v; + } + + return $object; + } + + /** + * 从 Set-Cookie 中解析. + * + * @param string $setCookieContent + * + * @return static|null + */ + public static function fromSetCookie($setCookieContent) + { + if (preg_match_all('/;?\s*((?P[^=;]+)=(?P[^;]+)|((?P[^=;]+)))/', $setCookieContent, $matches) > 0) + { + $name = $matches['name'][0]; + $value = $matches['value'][0]; + unset($matches['name'][0], $matches['value'][0]); + $data = array_combine(array_map('strtolower', $matches['name']), $matches['value']); + if (isset($data[''])) + { + unset($data['']); + } + if (isset($data['max-age'])) + { + $expires = time() + $data['max-age']; + } + elseif (isset($data['expires'])) + { + $expires = strtotime($data['expires']); + } + else + { + $expires = null; + } + foreach ($matches['name2'] as $boolItemName) + { + if ('' !== $boolItemName) + { + $data[strtolower($boolItemName)] = true; + } + } + $object = new static($name, $value, $expires, isset($data['path']) ? $data['path'] : '/', isset($data['domain']) ? $data['domain'] : '', isset($data['secure']) ? $data['secure'] : false, isset($data['httponly']) ? $data['httponly'] : false); + + return $object; + } + else + { + return null; + } + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Cookie/CookieManager.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Cookie/CookieManager.php new file mode 100644 index 0000000..04877c3 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Cookie/CookieManager.php @@ -0,0 +1,339 @@ +setCookieList($cookieList); + } + + /** + * 设置 Cookie 列表. + * + * @param array $cookieList + * + * @return void + */ + public function setCookieList($cookieList) + { + $this->autoIncrementId = 1; + $this->cookieList = []; + $this->relationMap = []; + foreach ($cookieList as $item) + { + $item = CookieItem::newInstance($item); + $this->insertCookie($item); + } + } + + /** + * 获取 Cookie 列表. + * + * @return array + */ + public function getCookieList() + { + return $this->cookieList; + } + + /** + * 添加 Set-Cookie. + * + * @param string $setCookie + * + * @return \Yurun\Util\YurunHttp\Cookie\CookieItem + */ + public function addSetCookie($setCookie) + { + $item = CookieItem::fromSetCookie($setCookie); + if (($id = $this->findCookie($item)) > 0) + { + $this->updateCookie($id, $item); + } + else + { + $this->insertCookie($item); + } + + return $item; + } + + /** + * 设置 Cookie. + * + * @param string $name + * @param string $value + * @param int $expires + * @param string $path + * @param string $domain + * @param bool $secure + * @param bool $httpOnly + * + * @return \Yurun\Util\YurunHttp\Cookie\CookieItem + */ + public function setCookie($name, $value, $expires = 0, $path = '/', $domain = '', $secure = false, $httpOnly = false) + { + $item = new CookieItem($name, $value, $expires, $path, $domain, $secure, $httpOnly); + if (($id = $this->findCookie($item)) > 0) + { + $this->updateCookie($id, $item); + } + else + { + $this->insertCookie($item); + } + + return $item; + } + + /** + * Cookie 数量. + * + * @return int + */ + public function count() + { + return \count($this->cookieList); + } + + /** + * 获取请求所需 Cookie 关联数组. + * + * @param \Psr\Http\Message\UriInterface $uri + * + * @return array + */ + public function getRequestCookies($uri) + { + // @phpstan-ignore-next-line + if (\defined('SWOOLE_VERSION') && \SWOOLE_VERSION < 4.4) + { + // Fix bug: https://github.com/swoole/swoole-src/pull/2644 + $result = json_decode('[]', true); + } + else + { + $result = []; + } + $uriDomain = Uri::getDomain($uri); + $uriPath = $uri->getPath(); + $cookieList = &$this->cookieList; + foreach ($this->relationMap as $relationDomain => $list1) + { + if ('' === $relationDomain || $this->checkDomain($uriDomain, $relationDomain)) + { + foreach ($list1 as $path => $idList) + { + if ($this->checkPath($uriPath, $path)) + { + foreach ($idList as $id) + { + $cookieItem = $cookieList[$id]; + if ((0 === $cookieItem->expires || $cookieItem->expires > time()) && (!$cookieItem->secure || 'https' === $uri->getScheme() || 'wss' === $uri->getScheme())) + { + $result[$cookieItem->name] = $cookieItem->value; + } + } + } + } + } + } + + return $result; + } + + /** + * 获取请求所需 Cookie 关联数组. + * + * @param \Psr\Http\Message\UriInterface $uri + * + * @return string + */ + public function getRequestCookieString($uri) + { + $content = ''; + foreach ($this->getRequestCookies($uri) as $name => $value) + { + $content .= "{$name}={$value}; "; + } + + return $content; + } + + /** + * 获取 CookieItem. + * + * @param string $name + * @param string $domain + * @param string $path + * + * @return \Yurun\Util\YurunHttp\Cookie\CookieItem|null + */ + public function getCookieItem($name, $domain = '', $path = '/') + { + if (isset($this->relationMap[$domain][$path][$name])) + { + $id = $this->relationMap[$domain][$path][$name]; + + return $this->cookieList[$id]; + } + + return null; + } + + /** + * 检查 uri 域名和 cookie 域名. + * + * @param string $uriDomain + * @param string $cookieDomain + * + * @return bool + */ + private function checkDomain($uriDomain, $cookieDomain) + { + return ($uriDomain === $cookieDomain) + || (isset($cookieDomain[0]) && '.' === $cookieDomain[0] && substr($uriDomain, -\strlen($cookieDomain) - 1) === '.' . $cookieDomain) + ; + } + + /** + * 检查 uri 路径和 cookie 路径. + * + * @param string $uriPath + * @param string $cookiePath + * + * @return bool + */ + private function checkPath($uriPath, $cookiePath) + { + $uriPath = rtrim($uriPath, '/'); + $cookiePath = rtrim($cookiePath, '/'); + if ($uriPath === $cookiePath) + { + return true; + } + $uriPathDSCount = substr_count($uriPath, '/'); + $cookiePathDSCount = substr_count($cookiePath, '/'); + if ('' === $uriPath) + { + $uriPath = '/'; + } + if ('' === $cookiePath) + { + $cookiePath = '/'; + } + if ($uriPathDSCount > $cookiePathDSCount) + { + if (version_compare(\PHP_VERSION, '7.0', '>=')) + { + $path = \dirname($uriPath, $uriPathDSCount - $cookiePathDSCount); + } + else + { + $count = $uriPathDSCount - $cookiePathDSCount; + $path = $uriPath; + while ($count--) + { + $path = \dirname($path); + } + } + if ('\\' === \DIRECTORY_SEPARATOR && false !== strpos($path, \DIRECTORY_SEPARATOR)) + { + $path = str_replace(\DIRECTORY_SEPARATOR, '/', $path); + } + + return $path === $cookiePath; + } + else + { + return false; + } + } + + /** + * 更新 Cookie 数据. + * + * @param int $id + * @param \Yurun\Util\YurunHttp\Cookie\CookieItem $item + * + * @return void + */ + private function updateCookie($id, $item) + { + if (isset($this->cookieList[$id])) + { + $object = $this->cookieList[$id]; + // @phpstan-ignore-next-line + foreach ($item as $k => $v) + { + $object->$k = $v; + } + } + } + + /** + * 插入 Cookie 数据. + * + * @param \Yurun\Util\YurunHttp\Cookie\CookieItem $item + * + * @return int + */ + private function insertCookie($item) + { + $id = $this->autoIncrementId++; + $this->cookieList[$id] = $item; + $this->relationMap[$item->domain][$item->path][$item->name] = $id; + + return $id; + } + + /** + * 查找 Cookie ID. + * + * @param \Yurun\Util\YurunHttp\Cookie\CookieItem $item + * + * @return int|null + */ + private function findCookie($item) + { + if (isset($this->relationMap[$item->domain][$item->path][$item->name])) + { + return $this->relationMap[$item->domain][$item->path][$item->name]; + } + else + { + return null; + } + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Exception/WebSocketException.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Exception/WebSocketException.php new file mode 100644 index 0000000..d708503 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Exception/WebSocketException.php @@ -0,0 +1,7 @@ + $v) + { + $result .= sprintf("--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n", $boundary, $k, $v); + } + foreach ($files as $name => $file) + { + $result .= sprintf("--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n", $boundary, $name, basename($file->getClientFilename()), $file->getClientMediaType()) . $file->getStream()->getContents() . "\r\n"; + } + $result .= sprintf("--%s--\r\n", $boundary); + + return $result; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Contract/IConnectionManager.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Contract/IConnectionManager.php new file mode 100644 index 0000000..fd18f8e --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Contract/IConnectionManager.php @@ -0,0 +1,89 @@ + \CURLAUTH_BASIC, + 'ntlm' => \CURLAUTH_NTLM, + ]; + + /** + * 代理类型. + * + * @var array + */ + public static $proxyType = [ + 'http' => \CURLPROXY_HTTP, + 'socks4' => \CURLPROXY_SOCKS4, + 'socks4a' => 6, // CURLPROXY_SOCKS4A + 'socks5' => \CURLPROXY_SOCKS5, + ]; + + /** + * 本 Handler 默认的 User-Agent. + * + * @var string + */ + private static $defaultUA; + + public function __construct() + { + if (null === static::$defaultUA) + { + $version = curl_version(); + static::$defaultUA = sprintf('Mozilla/5.0 YurunHttp/%s Curl/%s', YurunHttp::VERSION, isset($version['version']) ? $version['version'] : 'unknown'); + } + $this->initCookieManager(); + } + + /** + * 关闭并释放所有资源. + * + * @return void + */ + public function close() + { + if ($this->handler) + { + if ($this->poolIsEnabled) + { + curl_reset($this->handler); + CurlHttpConnectionManager::getInstance()->release($this->poolKey, $this->handler); + } + else + { + curl_close($this->handler); + } + $this->handler = null; + } + } + + /** + * 发送请求 + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * + * @return void + */ + public function send(&$request) + { + $this->poolIsEnabled = $poolIsEnabled = ConnectionPool::isEnabled() && false !== $request->getAttribute(Attributes::CONNECTION_POOL); + if ($poolIsEnabled) + { + $httpConnectionManager = CurlHttpConnectionManager::getInstance(); + } + try + { + $this->request = $request; + $request = &$this->request; + $handler = &$this->handler; + if (!$handler) + { + if ($poolIsEnabled) + { + $this->poolKey = $poolKey = ConnectionPool::getKey($request->getUri()); + $handler = $httpConnectionManager->getConnection($poolKey); + } + else + { + $handler = curl_init(); + } + } + $files = $request->getUploadedFiles(); + $body = (string) $request->getBody(); + + if (!empty($files)) + { + $body = FormDataBuilder::build($body, $files, $boundary); + $request = $request->withHeader('Content-Type', MediaType::MULTIPART_FORM_DATA . '; boundary=' . $boundary); + } + $this->buildCurlHandlerBase($request, $handler, $receiveHeaders, $saveFileFp); + if ([] !== ($queryParams = $request->getQueryParams())) + { + $request = $request->withUri($request->getUri()->withQuery(http_build_query($queryParams, '', '&'))); + } + $uri = $request->getUri(); + $isLocation = false; + $statusCode = 0; + $redirectCount = 0; + do + { + // 请求方法 + if ($isLocation && \in_array($statusCode, [301, 302, 303])) + { + $method = 'GET'; + } + else + { + $method = $request->getMethod(); + } + if ('GET' !== $method) + { + $bodyContent = $body; + } + else + { + $bodyContent = false; + } + $this->buildCurlHandlerEx($request, $handler, $uri, $method, $bodyContent); + $retry = $request->getAttribute(Attributes::RETRY, 0); + $result = null; + for ($i = 0; $i <= $retry; ++$i) + { + $receiveHeaders = []; + $curlResult = curl_exec($handler); + $this->result = $this->getResponse($request, $handler, $curlResult, $receiveHeaders); + $result = &$this->result; + $statusCode = $result->getStatusCode(); + // 状态码为5XX或者0才需要重试 + if (!(0 === $statusCode || (5 === (int) ($statusCode / 100)))) + { + break; + } + } + if ($request->getAttribute(Attributes::FOLLOW_LOCATION, true) && ($statusCode >= 300 && $statusCode < 400) && $result && '' !== ($location = $result->getHeaderLine('location'))) + { + $maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10); + if (++$redirectCount <= $maxRedirects) + { + // 重定向清除之前下载的文件 + if (null !== $saveFileFp) + { + ftruncate($saveFileFp, 0); + fseek($saveFileFp, 0); + } + $isLocation = true; + $uri = $this->parseRedirectLocation($location, $uri); + continue; + } + else + { + $result = $result->withErrno(-1) + ->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects)); + } + } + break; + } while (true); + // 关闭保存至文件的句柄 + if (null !== $saveFileFp) + { + fclose($saveFileFp); + $saveFileFp = null; + } + } + finally + { + if ($this->handler) + { + curl_reset($this->handler); + if ($poolIsEnabled) + { + // @phpstan-ignore-next-line + $httpConnectionManager->release($this->poolKey, $this->handler); + $this->handler = null; + } + } + } + } + + /** + * 构建基础 Curl Handler. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param resource $handler + * @param array|null $headers + * @param resource|null $saveFileFp + * + * @return void + */ + public function buildCurlHandlerBase(&$request, $handler, &$headers = null, &$saveFileFp = null) + { + $options = [ + // 返回内容 + \CURLOPT_RETURNTRANSFER => true, + // 保存cookie + \CURLOPT_COOKIEJAR => 'php://memory', + // 允许复用连接 + \CURLOPT_FORBID_REUSE => false, + ]; + // 自动重定向 + $options[\CURLOPT_MAXREDIRS] = $request->getAttribute(Attributes::MAX_REDIRECTS, 10); + + // 自动解压缩支持 + $acceptEncoding = $request->getHeaderLine('Accept-Encoding'); + if ('' !== $acceptEncoding) + { + $options[\CURLOPT_ENCODING] = $acceptEncoding; + } + else + { + $options[\CURLOPT_ENCODING] = ''; + } + curl_setopt_array($handler, $options); + $this->parseSSL($request, $handler); + $this->parseOptions($request, $handler, $headers, $saveFileFp); + $this->parseProxy($request, $handler); + $this->parseHeaders($request, $handler); + $this->parseCookies($request, $handler); + $this->parseNetwork($request, $handler); + } + + /** + * 构建扩展 Curl Handler. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param resource $handler + * @param UriInterface|null $uri + * @param string|null $method + * @param string|null $body + * + * @return void + */ + public function buildCurlHandlerEx(&$request, $handler, $uri = null, $method = null, $body = null) + { + if (null === $uri) + { + $uri = $request->getUri(); + } + if (null === $method) + { + $method = $request->getMethod(); + } + if (null === $body) + { + $body = (string) $request->getBody(); + } + switch ($request->getProtocolVersion()) + { + case '1.0': + $httpVersion = \CURL_HTTP_VERSION_1_0; + break; + case '2.0': + $ssl = 'https' === $uri->getScheme(); + if ($ssl) + { + $httpVersion = \CURL_HTTP_VERSION_2TLS; + } + else + { + $httpVersion = \CURL_HTTP_VERSION_2; + } + break; + default: + $httpVersion = \CURL_HTTP_VERSION_1_1; + } + $requestOptions = [ + \CURLOPT_URL => (string) $uri, + \CURLOPT_HTTP_VERSION => $httpVersion, + ]; + // 请求方法 + if ($body && 'GET' !== $method) + { + $requestOptions[\CURLOPT_POSTFIELDS] = $body; + } + $requestOptions[\CURLOPT_CUSTOMREQUEST] = $method; + if ('HEAD' === $method) + { + $requestOptions[\CURLOPT_NOBODY] = true; + } + curl_setopt_array($handler, $requestOptions); + } + + /** + * 接收请求 + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function recv() + { + return $this->result; + } + + /** + * 获取响应对象 + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param resource $handler + * @param string|bool $body + * @param array $receiveHeaders + * + * @return \Yurun\Util\YurunHttp\Http\Response + */ + private function getResponse($request, $handler, $body, $receiveHeaders) + { + // PHP 7.0.0开始substr()的 string 字符串长度与 start 相同时将返回一个空字符串。在之前的版本中,这种情况将返回 FALSE 。 + if (false === $body) + { + $body = ''; + } + + // body + $result = new Response($body, curl_getinfo($handler, \CURLINFO_HTTP_CODE)); + + // headers + $rawHeaders = implode('', $receiveHeaders); + $headers = $this->parseHeaderOneRequest($rawHeaders); + foreach ($headers as $name => $value) + { + $result = $result->withAddedHeader($name, $value); + } + + // cookies + $cookies = []; + $count = preg_match_all('/([^\r\n]+)/i', implode(\PHP_EOL, $result->getHeader('set-cookie')), $matches); + $cookieManager = $this->cookieManager; + for ($i = 0; $i < $count; ++$i) + { + $cookieItem = $cookieManager->addSetCookie($matches[1][$i]); + $cookies[$cookieItem->name] = (array) $cookieItem; + } + + // 下载文件名 + if ($savedFileName = $request->getAttribute(Attributes::SAVE_FILE_PATH)) + { + $result = $result->withSavedFileName($savedFileName); + } + + return $result->withRequest($request) + ->withCookieOriginParams($cookies) + ->withError(curl_error($handler)) + ->withErrno(curl_errno($handler)); + } + + /** + * parseHeaderOneRequest. + * + * @param string $piece + * + * @return array + */ + private function parseHeaderOneRequest($piece) + { + $tmpHeaders = []; + $lines = explode("\r\n", $piece); + $linesCount = \count($lines); + //从1开始,第0行包含了协议信息和状态信息,排除该行 + for ($i = 1; $i < $linesCount; ++$i) + { + $line = trim($lines[$i]); + if (empty($line) || false == strstr($line, ':')) + { + continue; + } + list($key, $value) = explode(':', $line, 2); + $key = trim($key); + $value = trim($value); + if (isset($tmpHeaders[$key])) + { + if (\is_array($tmpHeaders[$key])) + { + $tmpHeaders[$key][] = $value; + } + else + { + $tmp = $tmpHeaders[$key]; + $tmpHeaders[$key] = [ + $tmp, + $value, + ]; + } + } + else + { + $tmpHeaders[$key] = $value; + } + } + + return $tmpHeaders; + } + + /** + * 处理加密访问. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param resource $handler + * + * @return void + */ + private function parseSSL(&$request, $handler) + { + if ($request->getAttribute(Attributes::IS_VERIFY_CA, false)) + { + curl_setopt_array($handler, [ + \CURLOPT_SSL_VERIFYPEER => true, + \CURLOPT_CAINFO => $request->getAttribute(Attributes::CA_CERT), + \CURLOPT_SSL_VERIFYHOST => 2, + ]); + } + else + { + curl_setopt_array($handler, [ + \CURLOPT_SSL_VERIFYPEER => false, + \CURLOPT_SSL_VERIFYHOST => 0, + ]); + } + $certPath = $request->getAttribute(Attributes::CERT_PATH, ''); + if ('' !== $certPath) + { + curl_setopt_array($handler, [ + \CURLOPT_SSLCERT => $certPath, + \CURLOPT_SSLCERTPASSWD => $request->getAttribute(Attributes::CERT_PASSWORD), + \CURLOPT_SSLCERTTYPE => $request->getAttribute(Attributes::CERT_TYPE, 'pem'), + ]); + } + $keyPath = $request->getAttribute(Attributes::KEY_PATH, ''); + if ('' !== $keyPath) + { + curl_setopt_array($handler, [ + \CURLOPT_SSLKEY => $keyPath, + \CURLOPT_SSLKEYPASSWD => $request->getAttribute(Attributes::KEY_PASSWORD), + \CURLOPT_SSLKEYTYPE => $request->getAttribute(Attributes::KEY_TYPE, 'pem'), + ]); + } + } + + /** + * 处理设置项. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param resource $handler + * @param array $headers + * @param resource|null $saveFileFp + * + * @return void + */ + private function parseOptions(&$request, $handler, &$headers = null, &$saveFileFp = null) + { + $options = $request->getAttribute(Attributes::OPTIONS, []); + if (isset($options[\CURLOPT_HEADERFUNCTION])) + { + $headerCallable = $options[\CURLOPT_HEADERFUNCTION]; + } + else + { + $headerCallable = null; + } + $headers = []; + $options[\CURLOPT_HEADERFUNCTION] = function ($handler, $header) use ($headerCallable, &$headers) { + $headers[] = $header; + if ($headerCallable) + { + $headerCallable($handler, $header); + } + + return \strlen($header); + }; + curl_setopt_array($handler, $options); + // 请求结果保存为文件 + if (null !== ($saveFilePath = $request->getAttribute(Attributes::SAVE_FILE_PATH))) + { + $last = substr($saveFilePath, -1, 1); + if ('/' === $last || '\\' === $last) + { + // 自动获取文件名 + $saveFilePath .= basename($request->getUri()->__toString()); + } + $saveFileFp = fopen($saveFilePath, $request->getAttribute(Attributes::SAVE_FILE_MODE, 'w+')); + curl_setopt_array($handler, [ + \CURLOPT_HEADER => false, + \CURLOPT_RETURNTRANSFER => false, + \CURLOPT_FILE => $saveFileFp, + ]); + } + } + + /** + * 处理代理. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param resource $handler + * + * @return void + */ + private function parseProxy(&$request, $handler) + { + if ($request->getAttribute(Attributes::USE_PROXY, false)) + { + $type = $request->getAttribute(Attributes::PROXY_TYPE, 'http'); + curl_setopt_array($handler, [ + \CURLOPT_PROXYAUTH => self::$proxyAuths[$request->getAttribute(Attributes::PROXY_AUTH, 'basic')], + \CURLOPT_PROXY => $request->getAttribute(Attributes::PROXY_SERVER), + \CURLOPT_PROXYPORT => $request->getAttribute(Attributes::PROXY_PORT), + \CURLOPT_PROXYUSERPWD => $request->getAttribute(Attributes::PROXY_USERNAME, '') . ':' . $request->getAttribute(Attributes::PROXY_PASSWORD, ''), + \CURLOPT_PROXYTYPE => 'socks5' === $type ? (\defined('CURLPROXY_SOCKS5_HOSTNAME') ? \CURLPROXY_SOCKS5_HOSTNAME : self::$proxyType[$type]) : self::$proxyType[$type], + ]); + } + } + + /** + * 处理headers. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param resource $handler + * + * @return void + */ + private function parseHeaders(&$request, $handler) + { + if (!$request->hasHeader('User-Agent')) + { + $request = $request->withHeader('User-Agent', $request->getAttribute(Attributes::USER_AGENT, static::$defaultUA)); + } + if (!$request->hasHeader('Connection')) + { + $request = $request->withHeader('Connection', 'Keep-Alive')->withHeader('Keep-Alive', '300'); + } + curl_setopt($handler, \CURLOPT_HTTPHEADER, $this->parseHeadersFormat($request)); + } + + /** + * 处理成CURL可以识别的headers格式. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * + * @return array + */ + private function parseHeadersFormat($request) + { + $headers = []; + foreach ($request->getHeaders() as $name => $value) + { + $headers[] = $name . ': ' . implode(',', $value); + } + + return $headers; + } + + /** + * 处理cookie. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param resource $handler + * + * @return void + */ + private function parseCookies(&$request, $handler) + { + $cookieManager = $this->cookieManager; + foreach ($request->getCookieParams() as $name => $value) + { + $cookieManager->setCookie($name, $value); + } + $cookie = $cookieManager->getRequestCookieString($request->getUri()); + curl_setopt($handler, \CURLOPT_COOKIE, $cookie); + } + + /** + * 处理网络相关. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param resource $handler + * + * @return void + */ + private function parseNetwork(&$request, $handler) + { + // 用户名密码处理 + $username = $request->getAttribute(Attributes::USERNAME); + if (null != $username) + { + $userPwd = $username . ':' . $request->getAttribute(Attributes::PASSWORD, ''); + } + else + { + $userPwd = ''; + } + curl_setopt_array($handler, [ + // 连接超时 + \CURLOPT_CONNECTTIMEOUT_MS => $request->getAttribute(Attributes::CONNECT_TIMEOUT, 30000), + // 总超时 + \CURLOPT_TIMEOUT_MS => $request->getAttribute(Attributes::TIMEOUT, 0), + // 下载限速 + \CURLOPT_MAX_RECV_SPEED_LARGE => $request->getAttribute(Attributes::DOWNLOAD_SPEED), + // 上传限速 + \CURLOPT_MAX_SEND_SPEED_LARGE => $request->getAttribute(Attributes::UPLOAD_SPEED), + // 连接中用到的用户名和密码 + \CURLOPT_USERPWD => $userPwd, + ]); + } + + /** + * 连接 WebSocket. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient $websocketClient + * + * @return \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient + */ + public function websocket(&$request, $websocketClient = null) + { + throw new \RuntimeException('Curl Handler does not support WebSocket'); + } + + /** + * 获取原始处理器对象 + * + * @return mixed + */ + public function getHandler() + { + return $this->handler; + } + + /** + * 批量运行并发请求 + * + * @param \Yurun\Util\YurunHttp\Http\Request[] $requests + * @param float|null $timeout 超时时间,单位:秒。默认为 null 不限制 + * + * @return \Yurun\Util\YurunHttp\Http\Response[] + */ + public function coBatch($requests, $timeout = null) + { + $this->checkRequests($requests); + $mh = curl_multi_init(); + $curlHandlers = $recvHeaders = $saveFileFps = []; + $result = []; + $needRedirectRequests = []; + try + { + foreach ($requests as $k => $request) + { + $result[$k] = null; + $curlHandler = curl_init(); + $recvHeaders[$k] = $saveFileFps[$k] = null; + $this->buildCurlHandlerBase($request, $curlHandler, $recvHeaders[$k], $saveFileFps[$k]); + $files = $request->getUploadedFiles(); + $body = (string) $request->getBody(); + if (!empty($files)) + { + $body = FormDataBuilder::build($body, $files, $boundary); + $request = $request->withHeader('Content-Type', MediaType::MULTIPART_FORM_DATA . '; boundary=' . $boundary); + } + $this->buildCurlHandlerEx($request, $curlHandler, null, null, $body); + curl_multi_add_handle($mh, $curlHandler); + $curlHandlers[$k] = $curlHandler; + } + $running = null; + $beginTime = microtime(true); + // 执行批处理句柄 + do + { + curl_multi_exec($mh, $running); + if ($running > 0) + { + if ($timeout && microtime(true) - $beginTime >= $timeout) + { + break; + } + usleep(5000); // 每次延时 5 毫秒 + } + else + { + break; + } + } while (true); + foreach ($requests as $k => $request) + { + $handler = $curlHandlers[$k]; + $receiveHeaders = $recvHeaders[$k]; + $curlResult = curl_multi_getcontent($handler); + // @phpstan-ignore-next-line + $response = $this->getResponse($request, $handler, $curlResult, $receiveHeaders); + // 重定向处理 + $statusCode = $response->getStatusCode(); + $redirectCount = $request->getAttribute(Attributes::PRIVATE_REDIRECT_COUNT); + if ($request->getAttribute(Attributes::FOLLOW_LOCATION, true) && ($statusCode >= 300 && $statusCode < 400) && '' !== ($location = $response->getHeaderLine('location'))) + { + $maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10); + if (++$redirectCount <= $maxRedirects) + { + $request = $request->withAttribute(Attributes::PRIVATE_REDIRECT_COUNT, $redirectCount); + if (\in_array($statusCode, [301, 302, 303])) + { + $request = $request->withMethod('GET')->withBody(new MemoryStream()); + } + $request = $request->withUri($this->parseRedirectLocation($location, $request->getUri())); + $needRedirectRequests[$k] = $request; + continue; + } + else + { + $response = $response->withErrno(-1) + ->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects)); + } + } + $result[$k] = $response; + } + } + finally + { + foreach ($saveFileFps as $fp) + { + // @phpstan-ignore-next-line + if ($fp) + { + fclose($fp); + } + } + foreach ($curlHandlers as $curlHandler) + { + curl_multi_remove_handle($mh, $curlHandler); + curl_close($curlHandler); + } + curl_multi_close($mh); + } + if ($needRedirectRequests) + { + foreach ($this->coBatch($needRedirectRequests, $timeout) as $k => $response) + { + $result[$k] = $response; + } + } + + return $result; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl/CurlConnectionPool.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl/CurlConnectionPool.php new file mode 100644 index 0000000..7ca5ad8 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl/CurlConnectionPool.php @@ -0,0 +1,127 @@ +queue = new \SplQueue(); + } + + /** + * 关闭连接池和连接池中的连接. + * + * @return void + */ + public function close() + { + $connections = $this->connections; + $this->connections = []; + $this->queue = new \SplQueue(); + foreach ($connections as $connection) + { + curl_close($connection); + } + } + + /** + * 创建一个连接,但不受连接池管理. + * + * @return mixed + */ + public function createConnection() + { + return curl_init(); + } + + /** + * 获取连接. + * + * @return mixed + */ + public function getConnection() + { + if ($this->getFree() > 0) + { + return $this->queue->dequeue(); + } + else + { + $maxConnections = $this->getConfig()->getMaxConnections(); + if (0 != $maxConnections && $this->getCount() >= $maxConnections) + { + return false; + } + else + { + return $this->connections[] = $this->createConnection(); + } + } + } + + /** + * 释放连接占用. + * + * @param mixed $connection + * + * @return void + */ + public function release($connection) + { + if (\in_array($connection, $this->connections)) + { + $this->queue->enqueue($connection); + } + } + + /** + * 获取当前池子中连接总数. + * + * @return int + */ + public function getCount() + { + return \count($this->connections); + } + + /** + * 获取当前池子中空闲连接总数. + * + * @return int + */ + public function getFree() + { + return $this->queue->count(); + } + + /** + * 获取当前池子中正在使用的连接总数. + * + * @return int + */ + public function getUsed() + { + return $this->getCount() - $this->getFree(); + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl/CurlHttpConnectionManager.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl/CurlHttpConnectionManager.php new file mode 100644 index 0000000..9daeb5e --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Curl/CurlHttpConnectionManager.php @@ -0,0 +1,143 @@ +connectionPools; + } + + /** + * 获取连接池对象 + * + * @param string $url + * + * @return IConnectionPool + */ + public function getConnectionPool($url) + { + if (isset($this->connectionPools[$url])) + { + return $this->connectionPools[$url]; + } + else + { + $config = ConnectionPool::getConfig($url); + if (null === $config) + { + ConnectionPool::setConfig($url); + } + $config = ConnectionPool::getConfig($url); + + return $this->connectionPools[$url] = new CurlConnectionPool($config); + } + } + + /** + * 获取连接. + * + * @param string $url + * + * @return mixed + */ + public function getConnection($url) + { + $connectionPool = $this->getConnectionPool($url); + + return $connectionPool->getConnection(); + } + + /** + * 释放连接占用. + * + * @param string $url + * @param mixed $connection + * + * @return void + */ + public function release($url, $connection) + { + $connectionPool = $this->getConnectionPool($url); + $connectionPool->release($connection); + } + + /** + * 关闭指定连接. + * + * @param string $url + * + * @return bool + */ + public function closeConnection($url) + { + return false; + } + + /** + * 创建新连接,但不归本管理器管理. + * + * @param string $url + * + * @return mixed + */ + public function createConnection($url) + { + $connectionPool = $this->getConnectionPool($url); + + return $connectionPool->createConnection(); + } + + /** + * 关闭连接管理器. + * + * @return void + */ + public function close() + { + $connectionPools = $this->connectionPools; + $this->connectionPools = []; + foreach ($connectionPools as $connectionPool) + { + $connectionPool->close(); + } + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/IHandler.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/IHandler.php new file mode 100644 index 0000000..a09d79c --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/IHandler.php @@ -0,0 +1,63 @@ +initCookieManager(); + $this->httpConnectionManager = new SwooleHttpConnectionManager(); + $this->http2ConnectionManager = new SwooleHttp2ConnectionManager(); + } + + /** + * 关闭并释放所有资源. + * + * @return void + */ + public function close() + { + $this->httpConnectionManager->close(); + $this->http2ConnectionManager->close(); + } + + /** + * 构建请求 + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param \Swoole\Coroutine\Http\Client|\Swoole\Coroutine\Http2\Client $connection + * @param Http2Request $http2Request + * + * @return void + */ + public function buildRequest($request, $connection, &$http2Request) + { + if ($isHttp2 = '2.0' === $request->getProtocolVersion()) + { + $http2Request = new Http2Request(); + } + else + { + $http2Request = null; + } + $uri = $request->getUri(); + // method + if ($isHttp2) + { + $http2Request->method = $request->getMethod(); + } + else + { + $connection->setMethod($request->getMethod()); + } + // cookie + $this->parseCookies($request, $connection, $http2Request); + // body + $hasFile = false; + $redirectCount = $request->getAttribute(Attributes::PRIVATE_REDIRECT_COUNT, 0); + if ($redirectCount <= 0) + { + $files = $request->getUploadedFiles(); + $body = (string) $request->getBody(); + if (!empty($files)) + { + if ($isHttp2) + { + throw new \RuntimeException('Http2 swoole handler does not support upload file'); + } + $hasFile = true; + foreach ($files as $name => $file) + { + $connection->addFile($file->getTempFileName(), $name, $file->getClientMediaType(), basename($file->getClientFilename())); + } + parse_str($body, $body); + } + if ($isHttp2) + { + $http2Request->data = $body; + } + else + { + $connection->setData($body); + } + } + // 其它处理 + $this->parseSSL($request); + $this->parseProxy($request); + $this->parseNetwork($request); + // 设置客户端参数 + $settings = $request->getAttribute(Attributes::OPTIONS, []); + if ($settings) + { + $connection->set($settings); + } + // headers + if (!$request->hasHeader('Host')) + { + $request = $request->withHeader('Host', Uri::getDomain($uri)); + } + if (!$hasFile && !$request->hasHeader('Content-Type')) + { + $request = $request->withHeader('Content-Type', MediaType::APPLICATION_FORM_URLENCODED); + } + if (!$request->hasHeader('User-Agent')) + { + $request = $request->withHeader('User-Agent', $request->getAttribute(Attributes::USER_AGENT, static::$defaultUA)); + } + $headers = []; + foreach ($request->getHeaders() as $name => $value) + { + $headers[$name] = implode(',', $value); + } + if ($isHttp2) + { + $http2Request->headers = $headers; + $http2Request->pipeline = $request->getAttribute(Attributes::HTTP2_PIPELINE, false); + $path = $uri->getPath(); + if ('' === $path) + { + $path = '/'; + } + $query = $uri->getQuery(); + if ('' !== $query) + { + $path .= '?' . $query; + } + $http2Request->path = $path; + } + else + { + $connection->setHeaders($headers); + } + } + + /** + * 发送请求 + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * + * @return bool + */ + public function send(&$request) + { + $this->poolIsEnabled = ConnectionPool::isEnabled() && false !== $request->getAttribute(Attributes::CONNECTION_POOL); + $request = $this->sendDefer($request); + if ($request->getAttribute(Attributes::PRIVATE_IS_HTTP2) && $request->getAttribute(Attributes::HTTP2_NOT_RECV)) + { + return true; + } + + return (bool) $this->recvDefer($request); + } + + /** + * 发送请求,但延迟接收. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * + * @return \Yurun\Util\YurunHttp\Http\Request + */ + public function sendDefer($request) + { + $isHttp2 = '2.0' === $request->getProtocolVersion(); + if ($poolIsEnabled = $this->poolIsEnabled) + { + if ($isHttp2) + { + $http2ConnectionManager = SwooleHttp2ConnectionManager::getInstance(); + } + else + { + $httpConnectionManager = SwooleHttpConnectionManager::getInstance(); + } + } + else + { + if ($isHttp2) + { + $http2ConnectionManager = $this->http2ConnectionManager; + } + else + { + $httpConnectionManager = $this->httpConnectionManager; + } + } + if ([] !== ($queryParams = $request->getQueryParams())) + { + $request = $request->withUri($request->getUri()->withQuery(http_build_query($queryParams, '', '&'))); + } + $uri = $request->getUri(); + try + { + $this->poolKey = $poolKey = ConnectionPool::getKey($uri); + if ($isHttp2) + { + /** @var \Swoole\Coroutine\Http2\Client $connection */ + $connection = $http2ConnectionManager->getConnection($poolKey); + } + else + { + /** @var \Swoole\Coroutine\Http\Client $connection */ + $connection = $httpConnectionManager->getConnection($poolKey); + $connection->setDefer(true); + } + $request = $request->withAttribute(Attributes::PRIVATE_POOL_KEY, $poolKey); + $isWebSocket = $request->getAttribute(Attributes::PRIVATE_WEBSOCKET, false); + // 构建 + $this->buildRequest($request, $connection, $http2Request); + // 发送 + $path = $uri->getPath(); + if ('' === $path) + { + $path = '/'; + } + $query = $uri->getQuery(); + if ('' !== $query) + { + $path .= '?' . $query; + } + if ($isWebSocket) + { + if ($isHttp2) + { + throw new \RuntimeException('Http2 swoole handler does not support websocket'); + } + if (!$connection->upgrade($path)) + { + throw new WebSocketException(sprintf('WebSocket connect faled, statusCode: %s, error: %s, errorCode: %s', $connection->statusCode, swoole_strerror($connection->errCode), $connection->errCode), $connection->errCode); + } + } + elseif (null === ($saveFilePath = $request->getAttribute(Attributes::SAVE_FILE_PATH))) + { + if ($isHttp2) + { + $result = $connection->send($http2Request); + $request = $request->withAttribute(Attributes::PRIVATE_HTTP2_STREAM_ID, $result); + } + else + { + $connection->execute($path); + } + } + else + { + if ($isHttp2) + { + throw new \RuntimeException('Http2 swoole handler does not support download file'); + } + $connection->download($path, $saveFilePath); + } + + return $request->withAttribute(Attributes::PRIVATE_IS_HTTP2, $isHttp2) + ->withAttribute(Attributes::PRIVATE_IS_WEBSOCKET, $isHttp2) + ->withAttribute(Attributes::PRIVATE_CONNECTION, $connection); + } + catch (\Throwable $th) + { + throw $th; + } + finally + { + if ($poolIsEnabled && isset($connection) && isset($th)) + { + if ($isHttp2) + { + // @phpstan-ignore-next-line + $http2ConnectionManager->release($poolKey, $connection); + } + else + { + // @phpstan-ignore-next-line + $httpConnectionManager->release($poolKey, $connection); + } + } + } + } + + /** + * 延迟接收. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param float|null $timeout + * + * @return \Yurun\Util\YurunHttp\Http\Response|bool + */ + public function recvDefer($request, $timeout = null) + { + /** @var \Swoole\Coroutine\Http\Client|\Swoole\Coroutine\Http2\Client $connection */ + $connection = $request->getAttribute(Attributes::PRIVATE_CONNECTION); + $poolKey = $request->getAttribute(Attributes::PRIVATE_POOL_KEY); + $retryCount = $request->getAttribute(Attributes::PRIVATE_RETRY_COUNT, 0); + $redirectCount = $request->getAttribute(Attributes::PRIVATE_REDIRECT_COUNT, 0); + $isHttp2 = '2.0' === $request->getProtocolVersion(); + $isWebSocket = $request->getAttribute(Attributes::PRIVATE_WEBSOCKET, false); + try + { + $this->getResponse($request, $connection, $isWebSocket, $isHttp2, $timeout); + } + finally + { + if (!$isWebSocket) + { + if ($isHttp2) + { + if ($this->poolIsEnabled) + { + $http2ConnectionManager = SwooleHttp2ConnectionManager::getInstance(); + } + else + { + $http2ConnectionManager = $this->http2ConnectionManager; + } + $http2ConnectionManager->release($poolKey, $connection); + } + else + { + if ($this->poolIsEnabled) + { + $httpConnectionManager = SwooleHttpConnectionManager::getInstance(); + } + else + { + $httpConnectionManager = $this->httpConnectionManager; + } + $httpConnectionManager->release($poolKey, $connection); + } + } + } + $result = &$this->result; + $statusCode = $result->getStatusCode(); + // 状态码为5XX或者0才需要重试 + if ((0 === $statusCode || (5 === (int) ($statusCode / 100))) && $retryCount < $request->getAttribute(Attributes::RETRY, 0)) + { + $request = $request->withAttribute(Attributes::RETRY, ++$retryCount); + $deferRequest = $this->sendDefer($request); + + return $this->recvDefer($deferRequest, $timeout); + } + if (!$isWebSocket && $statusCode >= 300 && $statusCode < 400 && $request->getAttribute(Attributes::FOLLOW_LOCATION, true) && '' !== ($location = $result->getHeaderLine('location'))) + { + if (++$redirectCount <= ($maxRedirects = $request->getAttribute(Attributes::MAX_REDIRECTS, 10))) + { + // 自己实现重定向 + $uri = $this->parseRedirectLocation($location, $request->getUri()); + if (\in_array($statusCode, [301, 302, 303])) + { + $method = 'GET'; + } + else + { + $method = $request->getMethod(); + } + $request = $request->withMethod($method) + ->withUri($uri) + ->withAttribute(Attributes::PRIVATE_REDIRECT_COUNT, $redirectCount); + $deferRequest = $this->sendDefer($request); + + return $this->recvDefer($deferRequest, $timeout); + } + else + { + $result = $result->withErrno(-1) + ->withError(sprintf('Maximum (%s) redirects followed', $maxRedirects)); + + return false; + } + } + // 下载文件名 + $savedFileName = $request->getAttribute(Attributes::SAVE_FILE_PATH); + if (null !== $savedFileName) + { + $result = $result->withSavedFileName($savedFileName); + } + + return $result; + } + + /** + * 连接 WebSocket. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient $websocketClient + * + * @return \Yurun\Util\YurunHttp\WebSocket\IWebSocketClient + */ + public function websocket(&$request, $websocketClient = null) + { + if (!$websocketClient) + { + $websocketClient = new \Yurun\Util\YurunHttp\WebSocket\Swoole(); + } + $request = $request->withAttribute(Attributes::PRIVATE_WEBSOCKET, true); + $this->send($request); + $websocketClient->init($this, $request, $this->result); + + return $websocketClient; + } + + /** + * 接收请求 + * + * @return \Yurun\Util\YurunHttp\Http\Response|null + */ + public function recv() + { + return $this->result; + } + + /** + * 处理cookie. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param mixed $connection + * @param Http2Request $http2Request + * + * @return void + */ + private function parseCookies(&$request, $connection, $http2Request) + { + $cookieParams = $request->getCookieParams(); + $cookieManager = $this->cookieManager; + foreach ($cookieParams as $name => $value) + { + $cookieManager->setCookie($name, $value); + } + $cookies = $cookieManager->getRequestCookies($request->getUri()); + if ($http2Request) + { + $http2Request->cookies = $cookies; + } + else + { + $connection->setCookies($cookies); + } + } + + /** + * 构建 Http2 Response. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param \Swoole\Coroutine\Http2\Client $connection + * @param \Swoole\Http2\Response|bool $response + * + * @return \Yurun\Util\YurunHttp\Http\Response + */ + public function buildHttp2Response($request, $connection, $response) + { + $success = false !== $response; + $result = new Response($response->data ?? '', $success ? $response->statusCode : 0); + if ($success) + { + // streamId + $result = $result->withStreamId($response->streamId); + + // headers + if ($response->headers) + { + foreach ($response->headers as $name => $value) + { + $result = $result->withHeader($name, $value); + } + } + + // cookies + $cookies = []; + if (isset($response->set_cookie_headers)) + { + $cookieManager = $this->cookieManager; + foreach ($response->set_cookie_headers as $value) + { + $cookieItem = $cookieManager->addSetCookie($value); + $cookies[$cookieItem->name] = (array) $cookieItem; + } + } + $result = $result->withCookieOriginParams($cookies); + } + if ($connection) + { + $result = $result->withError(swoole_strerror($connection->errCode)) + ->withErrno($connection->errCode); + } + + return $result->withRequest($request); + } + + /** + * 获取响应对象 + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param \Swoole\Coroutine\Http\Client|\Swoole\Coroutine\Http2\Client $connection + * @param bool $isWebSocket + * @param bool $isHttp2 + * @param float|null $timeout + * + * @return \Yurun\Util\YurunHttp\Http\Response + */ + private function getResponse($request, $connection, $isWebSocket, $isHttp2, $timeout = null) + { + $result = &$this->result; + if ($isHttp2) + { + $response = $connection->recv((float) $timeout); + $result = $this->buildHttp2Response($request, $connection, $response); + } + else + { + $success = $isWebSocket ? true : $connection->recv((float) $timeout); + $result = new Response((string) $connection->body, $connection->statusCode); + if ($success) + { + // headers + if ($connection->headers) + { + foreach ($connection->headers as $name => $value) + { + $result = $result->withHeader($name, $value); + } + } + + // cookies + $cookies = []; + if (isset($connection->set_cookie_headers)) + { + foreach ($connection->set_cookie_headers as $value) + { + $cookieItem = $this->cookieManager->addSetCookie($value); + $cookies[$cookieItem->name] = (array) $cookieItem; + } + } + $result = $result->withCookieOriginParams($cookies); + } + $result = $result->withRequest($request) + ->withError(swoole_strerror($connection->errCode)) + ->withErrno($connection->errCode); + } + + return $result; + } + + /** + * 处理加密访问. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * + * @return void + */ + private function parseSSL(&$request) + { + $settings = $request->getAttribute(Attributes::OPTIONS, []); + if ($request->getAttribute(Attributes::IS_VERIFY_CA, false)) + { + $settings['ssl_verify_peer'] = true; + $caCert = $request->getAttribute(Attributes::CA_CERT); + if (null !== $caCert) + { + $settings['ssl_cafile'] = $caCert; + } + } + else + { + $settings['ssl_verify_peer'] = false; + } + $certPath = $request->getAttribute(Attributes::CERT_PATH, ''); + if ('' !== $certPath) + { + $settings['ssl_cert_file'] = $certPath; + } + $password = $request->getAttribute(Attributes::CERT_PASSWORD, ''); + if ('' === $password) + { + $password = $request->getAttribute(Attributes::KEY_PASSWORD, ''); + } + if ('' !== $password) + { + $settings['ssl_passphrase'] = $password; + } + $keyPath = $request->getAttribute(Attributes::KEY_PATH, ''); + if ('' !== $keyPath) + { + $settings['ssl_key_file'] = $keyPath; + } + $request = $request->withAttribute(Attributes::OPTIONS, $settings); + } + + /** + * 处理代理. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * + * @return void + */ + private function parseProxy(&$request) + { + $settings = $request->getAttribute(Attributes::OPTIONS, []); + if ($request->getAttribute(Attributes::USE_PROXY, false)) + { + $type = $request->getAttribute(Attributes::PROXY_TYPE); + switch ($type) + { + case 'http': + $settings['http_proxy_host'] = $request->getAttribute(Attributes::PROXY_SERVER); + $port = $request->getAttribute(Attributes::PROXY_PORT); + if (null !== $port) + { + $settings['http_proxy_port'] = $port; + } + $settings['http_proxy_user'] = $request->getAttribute(Attributes::PROXY_USERNAME); + $password = $request->getAttribute(Attributes::PROXY_PASSWORD); + if (null !== $password) + { + $settings['http_proxy_password'] = $password; + } + break; + case 'socks5': + $settings['socks5_host'] = $request->getAttribute(Attributes::PROXY_SERVER); + $port = $request->getAttribute(Attributes::PROXY_PORT); + if (null !== $port) + { + $settings['socks5_port'] = $port; + } + $settings['socks5_username'] = $request->getAttribute(Attributes::PROXY_USERNAME); + $password = $request->getAttribute(Attributes::PROXY_PASSWORD); + if (null !== $password) + { + $settings['socks5_password'] = $password; + } + break; + } + } + $request = $request->withAttribute(Attributes::OPTIONS, $settings); + } + + /** + * 处理网络相关. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * + * @return void + */ + private function parseNetwork(&$request) + { + $settings = $request->getAttribute(Attributes::OPTIONS, []); + // 用户名密码认证处理 + $username = $request->getAttribute(Attributes::USERNAME); + if (null === $username) + { + $uri = $request->getUri(); + $userInfo = $uri->getUserInfo(); + if ($userInfo) + { + $authorization = 'Basic ' . base64_encode($userInfo); + } + else + { + $authorization = null; + } + } + else + { + $authorization = 'Basic ' . base64_encode($username . ':' . $request->getAttribute(Attributes::PASSWORD, '')); + } + if ($authorization) + { + $request = $request->withHeader('Authorization', $authorization); + } + // 超时 + $settings['timeout'] = $request->getAttribute(Attributes::TIMEOUT, 30000) / 1000; + if ($settings['timeout'] < 0) + { + $settings['timeout'] = -1; + } + // 长连接 + $settings['keep_alive'] = $request->getAttribute(Attributes::KEEP_ALIVE, true); + $request = $request->withAttribute(Attributes::OPTIONS, $settings); + } + + /** + * 获取原始处理器对象 + * + * @return mixed + */ + public function getHandler() + { + return null; + } + + /** + * Get http 连接管理器. + * + * @return \Yurun\Util\YurunHttp\Handler\Swoole\SwooleHttpConnectionManager + */ + public function getSwooleHttpConnectionManager() + { + return $this->httpConnectionManager; + } + + /** + * Get http2 连接管理器. + * + * @return SwooleHttp2ConnectionManager + */ + public function getHttp2ConnectionManager() + { + return $this->http2ConnectionManager; + } + + /** + * 批量运行并发请求 + * + * @param \Yurun\Util\YurunHttp\Http\Request[] $requests + * @param float|null $timeout 超时时间,单位:秒。默认为 null 不限制 + * + * @return \Yurun\Util\YurunHttp\Http\Response[] + */ + public function coBatch($requests, $timeout = null) + { + /** @var Swoole[] $handlers */ + $handlers = []; + $results = []; + foreach ($requests as $i => &$request) + { + $results[$i] = null; + $handlers[$i] = $handler = new self(); + $request = $handler->sendDefer($request); + } + unset($request); + $beginTime = microtime(true); + $recvTimeout = null; + foreach ($requests as $i => $request) + { + if (null !== $timeout) + { + $recvTimeout = $timeout - (microtime(true) - $beginTime); + if ($recvTimeout <= 0) + { + break; + } + } + $results[$i] = $handlers[$i]->recvDefer($request, $recvTimeout); + } + + return $results; + } + + public function getHttpConnectionManager(): SwooleHttpConnectionManager + { + if (ConnectionPool::isEnabled()) + { + return SwooleHttpConnectionManager::getInstance(); + } + else + { + return $this->httpConnectionManager; + } + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttp2ConnectionManager.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttp2ConnectionManager.php new file mode 100644 index 0000000..3ee7c46 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttp2ConnectionManager.php @@ -0,0 +1,143 @@ +connectionPools; + } + + /** + * 获取连接池对象 + * + * @param string $url + * + * @return IConnectionPool + */ + public function getConnectionPool($url) + { + if (isset($this->connectionPools[$url])) + { + return $this->connectionPools[$url]; + } + else + { + $config = ConnectionPool::getConfig($url); + if (null === $config) + { + ConnectionPool::setConfig($url); + } + $config = ConnectionPool::getConfig($url); + + return $this->connectionPools[$url] = new SwooleHttp2ConnectionPool($config); + } + } + + /** + * 获取连接. + * + * @param string $url + * + * @return mixed + */ + public function getConnection($url) + { + $connectionPool = $this->getConnectionPool($url); + + return $connectionPool->getConnection(); + } + + /** + * 释放连接占用. + * + * @param string $url + * @param mixed $connection + * + * @return void + */ + public function release($url, $connection) + { + $connectionPool = $this->getConnectionPool($url); + $connectionPool->release($connection); + } + + /** + * 关闭指定连接. + * + * @param string $url + * + * @return bool + */ + public function closeConnection($url) + { + return false; + } + + /** + * 创建新连接,但不归本管理器管理. + * + * @param string $url + * + * @return mixed + */ + public function createConnection($url) + { + $connectionPool = $this->getConnectionPool($url); + + return $connectionPool->createConnection(); + } + + /** + * 关闭连接管理器. + * + * @return void + */ + public function close() + { + $connectionPools = $this->connectionPools; + $this->connectionPools = []; + foreach ($connectionPools as $connectionPool) + { + $connectionPool->close(); + } + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttp2ConnectionPool.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttp2ConnectionPool.php new file mode 100644 index 0000000..9118b90 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttp2ConnectionPool.php @@ -0,0 +1,137 @@ +channel = new Channel(1024); + } + + /** + * 关闭连接池和连接池中的连接. + * + * @return void + */ + public function close() + { + $connections = $this->connections; + $this->connections = []; + $this->channel = new Channel(1024); + foreach ($connections as $connection) + { + $connection->close(); + } + } + + /** + * 创建一个连接,但不受连接池管理. + * + * @return mixed + */ + public function createConnection() + { + $config = $this->config; + $uri = new Uri($config->getUrl()); + $scheme = $uri->getScheme(); + $client = new Client($uri->getHost(), Uri::getServerPort($uri), 'https' === $scheme || 'wss' === $scheme); + if ($client->connect()) + { + return $client; + } + else + { + throw new \RuntimeException(sprintf('Http2 connect failed! errCode: %s, errMsg:%s', $client->errCode, swoole_strerror($client->errCode))); + } + } + + /** + * 获取连接. + * + * @return mixed + */ + public function getConnection() + { + $config = $this->getConfig(); + $maxConnections = $this->getConfig()->getMaxConnections(); + if ($this->getFree() > 0 || (0 != $maxConnections && $this->getCount() >= $maxConnections)) + { + $timeout = $config->getWaitTimeout(); + + return $this->channel->pop(null === $timeout ? -1 : $timeout); + } + else + { + return $this->connections[] = $this->createConnection(); + } + } + + /** + * 释放连接占用. + * + * @param mixed $connection + * + * @return void + */ + public function release($connection) + { + if (\in_array($connection, $this->connections)) + { + $this->channel->push($connection); + } + } + + /** + * 获取当前池子中连接总数. + * + * @return int + */ + public function getCount() + { + return \count($this->connections); + } + + /** + * 获取当前池子中空闲连接总数. + * + * @return int + */ + public function getFree() + { + return $this->channel->length(); + } + + /** + * 获取当前池子中正在使用的连接总数. + * + * @return int + */ + public function getUsed() + { + return $this->getCount() - $this->getFree(); + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttpConnectionManager.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttpConnectionManager.php new file mode 100644 index 0000000..d0dfee6 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttpConnectionManager.php @@ -0,0 +1,143 @@ +connectionPools; + } + + /** + * 获取连接池对象 + * + * @param string $url + * + * @return IConnectionPool + */ + public function getConnectionPool($url) + { + if (isset($this->connectionPools[$url])) + { + return $this->connectionPools[$url]; + } + else + { + $config = ConnectionPool::getConfig($url); + if (null === $config) + { + ConnectionPool::setConfig($url); + } + $config = ConnectionPool::getConfig($url); + + return $this->connectionPools[$url] = new SwooleHttpConnectionPool($config); + } + } + + /** + * 获取连接. + * + * @param string $url + * + * @return mixed + */ + public function getConnection($url) + { + $connectionPool = $this->getConnectionPool($url); + + return $connectionPool->getConnection(); + } + + /** + * 释放连接占用. + * + * @param string $url + * @param mixed $connection + * + * @return void + */ + public function release($url, $connection) + { + $connectionPool = $this->getConnectionPool($url); + $connectionPool->release($connection); + } + + /** + * 关闭指定连接. + * + * @param string $url + * + * @return bool + */ + public function closeConnection($url) + { + return false; + } + + /** + * 创建新连接,但不归本管理器管理. + * + * @param string $url + * + * @return mixed + */ + public function createConnection($url) + { + $connectionPool = $this->getConnectionPool($url); + + return $connectionPool->createConnection(); + } + + /** + * 关闭连接管理器. + * + * @return void + */ + public function close() + { + $connectionPools = $this->connectionPools; + $this->connectionPools = []; + foreach ($connectionPools as $connectionPool) + { + $connectionPool->close(); + } + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttpConnectionPool.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttpConnectionPool.php new file mode 100644 index 0000000..ddc908d --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Handler/Swoole/SwooleHttpConnectionPool.php @@ -0,0 +1,130 @@ +channel = new Channel(1024); + } + + /** + * 关闭连接池和连接池中的连接. + * + * @return void + */ + public function close() + { + $connections = $this->connections; + $this->connections = []; + $this->channel = new Channel(1024); + foreach ($connections as $connection) + { + $connection->close(); + } + } + + /** + * 创建一个连接,但不受连接池管理. + * + * @return mixed + */ + public function createConnection() + { + $config = $this->config; + $uri = new Uri($config->getUrl()); + $scheme = $uri->getScheme(); + + return new Client($uri->getHost(), Uri::getServerPort($uri), 'https' === $scheme || 'wss' === $scheme); + } + + /** + * 获取连接. + * + * @return mixed + */ + public function getConnection() + { + $config = $this->getConfig(); + $maxConnections = $this->getConfig()->getMaxConnections(); + if ($this->getFree() > 0 || (0 != $maxConnections && $this->getCount() >= $maxConnections)) + { + $timeout = $config->getWaitTimeout(); + + return $this->channel->pop(null === $timeout ? -1 : $timeout); + } + else + { + return $this->connections[] = $this->createConnection(); + } + } + + /** + * 释放连接占用. + * + * @param mixed $connection + * + * @return void + */ + public function release($connection) + { + if (\in_array($connection, $this->connections)) + { + $this->channel->push($connection); + } + } + + /** + * 获取当前池子中连接总数. + * + * @return int + */ + public function getCount() + { + return \count($this->connections); + } + + /** + * 获取当前池子中空闲连接总数. + * + * @return int + */ + public function getFree() + { + return $this->channel->length(); + } + + /** + * 获取当前池子中正在使用的连接总数. + * + * @return int + */ + public function getUsed() + { + return $this->getCount() - $this->getFree(); + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/AbstractMessage.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/AbstractMessage.php new file mode 100644 index 0000000..d906ab6 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/AbstractMessage.php @@ -0,0 +1,396 @@ + 第一次使用的头名称. + * + * @var array + */ + protected $headerNames = []; + + /** + * 消息主体. + * + * @var \Psr\Http\Message\StreamInterface + */ + protected $body; + + /** + * @param string|\Psr\Http\Message\StreamInterface $body + */ + public function __construct($body) + { + if ($body instanceof \Psr\Http\Message\StreamInterface) + { + $this->body = $body; + } + else + { + $this->body = new MemoryStream($body); + } + } + + /** + * Retrieves the HTTP protocol version as a string. + * + * The string MUST contain only the HTTP version number (e.g., "1.1", "1.0"). + * + * @return string HTTP protocol version + */ + public function getProtocolVersion() + { + return $this->protocolVersion; + } + + /** + * Return an instance with the specified HTTP protocol version. + * + * The version string MUST contain only the HTTP version number (e.g., + * "1.1", "1.0"). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new protocol version. + * + * @param string $version HTTP protocol version + * + * @return static + */ + public function withProtocolVersion($version) + { + $self = clone $this; + $self->protocolVersion = $version; + + return $self; + } + + /** + * Retrieves all message header values. + * + * The keys represent the header name as it will be sent over the wire, and + * each value is an array of strings associated with the header. + * + * // Represent the headers as a string + * foreach ($message->getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return array Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name case-insensitive header field name + * + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name) + { + $lowerName = strtolower($name); + if (isset($this->headerNames[$lowerName])) + { + $name = $this->headerNames[$lowerName]; + } + + return isset($this->headers[$name]); + } + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name case-insensitive header field name + * + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($name) + { + $lowerName = strtolower($name); + if (isset($this->headerNames[$lowerName])) + { + $name = $this->headerNames[$lowerName]; + } + if (isset($this->headers[$name])) + { + return $this->headers[$name]; + } + else + { + return []; + } + } + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name case-insensitive header field name + * + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine($name) + { + $lowerName = strtolower($name); + if (isset($this->headerNames[$lowerName])) + { + $name = $this->headerNames[$lowerName]; + } + if (!isset($this->headers[$name])) + { + return ''; + } + + return implode(',', $this->headers[$name]); + } + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name case-insensitive header field name + * @param string|string[] $value header value(s) + * + * @return static + * + * @throws \InvalidArgumentException for invalid header names or values + */ + public function withHeader($name, $value) + { + $self = clone $this; + + return $this->setHeader($self, $name, $value); + } + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name case-insensitive header field name to add + * @param string|string[] $value header value(s) + * + * @return static + * + * @throws \InvalidArgumentException for invalid header names or values + */ + public function withAddedHeader($name, $value) + { + $self = clone $this; + $lowerName = strtolower($name); + if (isset($self->headerNames[$lowerName])) + { + $name = $self->headerNames[$lowerName]; + } + else + { + $self->headerNames[$lowerName] = $name; + } + + if (\is_string($value)) + { + $value = [$value]; + } + elseif (!\is_array($value)) + { + throw new \InvalidArgumentException('invalid header names or values'); + } + + if (isset($self->headers[$name])) + { + $self->headers[$name] = array_merge($self->headers[$name], $value); + } + else + { + $self->headers[$name] = $value; + } + + return $self; + } + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name case-insensitive header field name to remove + * + * @return static + */ + public function withoutHeader($name) + { + $self = clone $this; + $lowerName = strtolower($name); + if (isset($self->headerNames[$lowerName])) + { + $name = $self->headerNames[$lowerName]; + } + if (isset($self->headers[$name])) + { + unset($self->headers[$name]); + } + + return $self; + } + + /** + * Gets the body of the message. + * + * @return StreamInterface returns the body as a stream + */ + public function getBody() + { + return $this->body; + } + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body body + * + * @return static + * + * @throws \InvalidArgumentException when the body is not valid + */ + public function withBody(StreamInterface $body) + { + $self = clone $this; + $self->body = $body; + + return $self; + } + + /** + * 在当前实例下设置头. + * + * @param array $headers + * + * @return static + */ + protected function setHeaders(array $headers) + { + foreach ($headers as $name => $value) + { + $this->setHeader($this, $name, $value); + } + + return $this; + } + + /** + * 设置header. + * + * @param static $object + * @param string $name + * @param string $value + * + * @return static + */ + protected function setHeader($object, $name, $value) + { + $lowerName = strtolower($name); + if (isset($object->headerNames[$lowerName])) + { + $name = $object->headerNames[$lowerName]; + } + else + { + $object->headerNames[$lowerName] = $name; + } + if (\is_string($value)) + { + $object->headers[$name] = [$value]; + } + elseif (\is_array($value)) + { + $object->headers[$name] = $value; + } + else + { + throw new \InvalidArgumentException('invalid header names or values'); + } + + return $object; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/MediaType.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/MediaType.php new file mode 100644 index 0000000..7516af3 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/MediaType.php @@ -0,0 +1,211 @@ + 'Extension', + 'text/h323' => '323', + 'application/internet-property-stream' => 'acx', + 'application/postscript' => 'ai', + 'audio/x-aiff' => 'aiff', + 'video/x-ms-asf' => 'asf', + 'audio/basic' => 'au', + 'video/x-msvideo' => 'avi', + 'application/olescript' => 'axs', + 'application/x-bcpio' => 'bcpio', + 'image/bmp' => 'bmp', + 'application/vnd.ms-pkiseccat' => 'cat', + 'application/x-cdf' => 'cdf', + 'application/x-msclip' => 'clp', + 'image/x-cmx' => 'cmx', + 'image/cis-cod' => 'cod', + 'application/x-cpio' => 'cpio', + 'application/x-mscardfile' => 'crd', + 'application/pkix-crl' => 'crl', + 'application/x-x509-ca-cert' => 'crt', + 'application/x-csh' => 'csh', + 'text/css' => 'css', + 'application/x-msdownload' => 'dll', + 'application/msword' => 'doc', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => 'dotx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', + 'application/x-dvi' => 'dvi', + 'application/x-director' => 'dxr', + 'text/x-setext' => 'etx', + 'application/envoy' => 'evy', + 'application/fractals' => 'fif', + 'image/gif' => 'gif', + 'application/x-gtar' => 'gtar', + 'application/x-gzip' => 'gz', + 'application/x-hdf' => 'hdf', + 'application/winhlp' => 'hlp', + 'application/mac-binhex40' => 'hqx', + 'application/hta' => 'hta', + 'text/x-component' => 'htc', + 'text/html' => 'html', + 'text/webviewhtml' => 'htt', + 'image/x-icon' => 'ico', + 'image/ief' => 'ief', + 'application/x-iphone' => 'iii', + 'image/pipeg' => 'jfif', + 'image/jpeg' => 'jpg', + 'application/x-javascript' => 'js', + 'application/x-latex' => 'latex', + 'video/x-la-asf' => 'lsf', + 'audio/x-mpegurl' => 'm3u', + 'application/x-troff-man' => 'man', + 'application/x-msaccess' => 'mdb', + 'application/x-troff-me' => 'me', + 'message/rfc822' => 'mhtml', + 'audio/mid' => 'mid', + 'application/x-msmoney' => 'mny', + 'video/quicktime' => 'mov', + 'video/x-sgi-movie' => 'movie', + 'video/mpeg' => 'mpeg', + 'application/vnd.ms-project' => 'mpp', + 'application/x-troff-ms' => 'ms', + 'application/x-msmediaview' => 'mvb', + 'application/oda' => 'oda', + 'application/pkcs10' => 'p10', + 'application/x-pkcs7-mime' => 'p7m', + 'application/x-pkcs7-certreqresp' => 'p7r', + 'application/x-pkcs7-signature' => 'p7s', + 'image/x-portable-bitmap' => 'pbm', + 'application/pdf' => 'pdf', + 'application/x-pkcs12' => 'pfx', + 'image/x-portable-graymap' => 'pgm', + 'application/ynd.ms-pkipko' => 'pko', + 'image/x-portable-anymap' => 'pnm', + 'image/x-portable-pixmap' => 'ppm', + 'application/vnd.ms-powerpoint' => 'ppt', + 'application/pics-rules' => 'prf', + 'application/x-mspublisher' => 'pub', + 'audio/x-pn-realaudio' => 'ram', + 'image/x-cmu-raster' => 'ras', + 'image/x-rgb' => 'rgb', + 'application/rtf' => 'rtf', + 'text/richtext' => 'rtx', + 'application/x-msschedule' => 'scd', + 'text/scriptlet' => 'sct', + 'application/set-payment-initiation' => 'setpay', + 'application/set-registration-initiation' => 'setreg', + 'application/x-sh' => 'sh', + 'application/x-shar' => 'shar', + 'application/x-stuffit' => 'sit', + 'application/x-pkcs7-certificates' => 'spc', + 'application/futuresplash' => 'spl', + 'application/x-wais-source' => 'src', + 'application/vnd.ms-pkicertstore' => 'sst', + 'application/vnd.ms-pkistl' => 'stl', + 'image/svg+xml' => 'svg', + 'application/x-sv4cpio' => 'sv4cpio', + 'application/x-sv4crc' => 'sv4crc', + 'application/x-shockwave-flash' => 'swf', + 'application/x-tar' => 'tar', + 'application/x-tcl' => 'tcl', + 'application/x-tex' => 'tex', + 'application/x-texinfo' => 'texi', + 'application/x-compressed' => 'tgz', + 'image/tiff' => 'tiff', + 'application/x-msterminal' => 'trm', + 'text/tab-separated-values' => 'tsv', + 'text/plain' => 'txt', + 'text/iuls' => 'uls', + 'application/x-ustar' => 'ustar', + 'text/x-vcard' => 'vcf', + 'audio/x-wav' => 'wav', + 'application/x-msmetafile' => 'wmf', + 'application/x-mswrite' => 'wri', + 'image/x-xbitmap' => 'xbm', + 'application/vnd.ms-excel' => 'xls', + 'image/x-xpixmap' => 'xpm', + 'image/x-xwindowdump' => 'xwd', + 'application/x-compress' => 'z', + 'application/zip' => 'zip', + 'application/vnd.android.package-archive' => 'apk', + 'application/x-silverlight-app' => 'xap', + 'application/vnd.iphone' => 'ipa', + 'text/markdown' => 'md', + 'text/xml' => 'xml', + 'image/webp' => 'webp', + 'image/png' => 'png', + ]; + + /** + * 获取 ContentType 对应的扩展名(不包含点). + * + * @param string $contentType + * + * @return string|null + */ + public static function getExt($contentType) + { + list($firstContentType) = explode(';', $contentType, 2); + if (isset(static::$extMap[$firstContentType])) + { + return static::$extMap[$firstContentType]; + } + else + { + return null; + } + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/RequestHeader.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/RequestHeader.php new file mode 100644 index 0000000..d70c6d4 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Consts/RequestHeader.php @@ -0,0 +1,42 @@ + 'Continue', + self::SWITCHING_PROTOCOLS => 'Switching Protocols', + self::PROCESSING => 'Processing', + self::OK => 'OK', + self::CREATED => 'Created', + self::ACCEPTED => 'Accepted', + self::NON_AUTHORITATIVE_INFORMATION => 'Non-Authoritative Information', + self::NO_CONTENT => 'No Content', + self::RESET_CONTENT => 'Reset Content', + self::PARTIAL_CONTENT => 'Partial Content', + self::MULTI_STATUS => 'Multi-status', + self::ALREADY_REPORTED => 'Already Reported', + self::IM_USED => 'IM Used', + self::MULTIPLE_CHOICES => 'Multiple Choices', + self::MOVED_PERMANENTLY => 'Moved Permanently', + self::FOUND => 'Found', + self::SEE_OTHER => 'See Other', + self::NOT_MODIFIED => 'Not Modified', + self::USE_PROXY => 'Use Proxy', + self::SWITCH_PROXY => 'Switch Proxy', + self::TEMPORARY_REDIRECT => 'Temporary Redirect', + self::PERMANENT_REDIRECT => 'Permanent Redirect', + self::BAD_REQUEST => 'Bad Request', + self::UNAUTHORIZED => 'Unauthorized', + self::PAYMENT_REQUIRED => 'Payment Required', + self::FORBIDDEN => 'Forbidden', + self::NOT_FOUND => 'Not Found', + self::METHOD_NOT_ALLOWED => 'Method Not Allowed', + self::NOT_ACCEPTABLE => 'Not Acceptable', + self::PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required', + self::REQUEST_TIME_OUT => 'Request Time-out', + self::CONFLICT => 'Conflict', + self::GONE => 'Gone', + self::LENGTH_REQUIRED => 'Length Required', + self::PRECONDITION_FAILED => 'Precondition Failed', + self::REQUEST_ENTITY_TOO_LARGE => 'Request Entity Too Large', + self::REQUEST_URI_TOO_LARGE => 'Request-URI Too Large', + self::UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type', + self::REQUESTED_RANGE_NOT_SATISFIABLE => 'Requested range not satisfiable', + self::EXPECTATION_FAILED => 'Expectation Failed', + self::MISDIRECTED_REQUEST => 'Misdirected Request', + self::UNPROCESSABLE_ENTITY => 'Unprocessable Entity', + self::LOCKED => 'Locked', + self::FAILED_DEPENDENCY => 'Failed Dependency', + self::UNORDERED_COLLECTION => 'Unordered Collection', + self::UPGRADE_REQUIRED => 'Upgrade Required', + self::PRECONDITION_REQUIRED => 'Precondition Required', + self::TOO_MANY_REQUESTS => 'Too Many Requests', + self::REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', + self::UNAVAILABLE_FOR_LEGAL_REASONS => 'Unavailable For Legal Reasons', + self::INTERNAL_SERVER_ERROR => 'Internal Server Error', + self::NOT_IMPLEMENTED => 'Not Implemented', + self::BAD_GATEWAY => 'Bad Gateway', + self::SERVICE_UNAVAILABLE => 'Service Unavailable', + self::GATEWAY_TIME_OUT => 'Gateway Time-out', + self::HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version not supported', + self::VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates', + self::INSUFFICIENT_STORAGE => 'Insufficient Storage', + self::LOOP_DETECTED => 'Loop Detected', + self::NOT_EXTENDED => 'Not Extended', + self::NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required', + ]; + + /** + * 根据状态码获取原因短语. + * + * @param int $value + * + * @return string + */ + public static function getReasonPhrase($value) + { + return isset(static::$reasonPhrases[$value]) ? static::$reasonPhrases[$value] : 'Unknown'; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Request.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Request.php new file mode 100644 index 0000000..1e4447c --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Request.php @@ -0,0 +1,199 @@ +uri = new Uri($uri); + } + elseif (null !== $uri) + { + $this->uri = $uri; + } + $this->setHeaders($headers); + $this->method = strtoupper($method); + $this->protocolVersion = $version; + } + + /** + * Retrieves the message's request target. + * + * Retrieves the message's request-target either as it will appear (for + * clients), as it appeared at request (for servers), or as it was + * specified for the instance (see withRequestTarget()). + * + * In most cases, this will be the origin-form of the composed URI, + * unless a value was provided to the concrete implementation (see + * withRequestTarget() below). + * + * If no URI is available, and no request-target has been specifically + * provided, this method MUST return the string "/". + * + * @return string + */ + public function getRequestTarget() + { + return null === $this->requestTarget ? (string) $this->uri : $this->requestTarget; + } + + /** + * Return an instance with the specific request-target. + * + * If the request needs a non-origin-form request-target — e.g., for + * specifying an absolute-form, authority-form, or asterisk-form — + * this method may be used to create an instance with the specified + * request-target, verbatim. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request target. + * + * @see http://tools.ietf.org/html/rfc7230#section-2.7 (for the various + * request-target forms allowed in request messages) + * + * @param mixed $requestTarget + * + * @return static + */ + public function withRequestTarget($requestTarget) + { + $self = clone $this; + $self->requestTarget = $requestTarget; + + return $self; + } + + /** + * Retrieves the HTTP method of the request. + * + * @return string returns the request method + */ + public function getMethod() + { + return $this->method; + } + + /** + * Return an instance with the provided HTTP method. + * + * While HTTP method names are typically all uppercase characters, HTTP + * method names are case-sensitive and thus implementations SHOULD NOT + * modify the given string. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * changed request method. + * + * @param string $method case-sensitive method + * + * @return static + * + * @throws \InvalidArgumentException for invalid HTTP methods + */ + public function withMethod($method) + { + $self = clone $this; + $self->method = $method; + + return $self; + } + + /** + * Retrieves the URI instance. + * + * This method MUST return a UriInterface instance. + * + * @see http://tools.ietf.org/html/rfc3986#section-4.3 + * + * @return UriInterface returns a UriInterface instance + * representing the URI of the request + */ + public function getUri() + { + return $this->uri; + } + + /** + * Returns an instance with the provided URI. + * + * This method MUST update the Host header of the returned request by + * default if the URI contains a host component. If the URI does not + * contain a host component, any pre-existing Host header MUST be carried + * over to the returned request. + * + * You can opt-in to preserving the original state of the Host header by + * setting `$preserveHost` to `true`. When `$preserveHost` is set to + * `true`, this method interacts with the Host header in the following ways: + * + * - If the the Host header is missing or empty, and the new URI contains + * a host component, this method MUST update the Host header in the returned + * request. + * - If the Host header is missing or empty, and the new URI does not contain a + * host component, this method MUST NOT update the Host header in the returned + * request. + * - If a Host header is present and non-empty, this method MUST NOT update + * the Host header in the returned request. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new UriInterface instance. + * + * @see http://tools.ietf.org/html/rfc3986#section-4.3 + * + * @param UriInterface $uri new request URI to use + * @param bool $preserveHost preserve the original state of the Host header + * + * @return static + */ + public function withUri(UriInterface $uri, $preserveHost = false) + { + $self = clone $this; + $self->uri = $uri; + if (!$preserveHost) + { + $self->headers = []; + $self->headerNames = []; + } + + return $self; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Response.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Response.php new file mode 100644 index 0000000..146fe31 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Response.php @@ -0,0 +1,106 @@ +statusCode = $statusCode; + $this->reasonPhrase = null === $reasonPhrase ? StatusCode::getReasonPhrase($this->statusCode) : $reasonPhrase; + } + + /** + * Gets the response status code. + * + * The status code is a 3-digit integer result code of the server's attempt + * to understand and satisfy the request. + * + * @return int status code + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * Return an instance with the specified status code and, optionally, reason phrase. + * + * If no reason phrase is specified, implementations MAY choose to default + * to the RFC 7231 or IANA recommended reason phrase for the response's + * status code. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated status and reason phrase. + * + * @see http://tools.ietf.org/html/rfc7231#section-6 + * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * + * @param int $code the 3-digit integer result code to set + * @param string $reasonPhrase the reason phrase to use with the + * provided status code; if none is provided, implementations MAY + * use the defaults as suggested in the HTTP specification + * + * @return static + * + * @throws \InvalidArgumentException for invalid status code arguments + */ + public function withStatus($code, $reasonPhrase = '') + { + $self = clone $this; + $self->statusCode = $code; + if ('' === $reasonPhrase) + { + $self->reasonPhrase = StatusCode::getReasonPhrase($code); + } + else + { + $self->reasonPhrase = $reasonPhrase; + } + + return $self; + } + + /** + * Gets the response reason phrase associated with the status code. + * + * Because a reason phrase is not a required element in a response + * status line, the reason phrase value MAY be null. Implementations MAY + * choose to return the default RFC 7231 recommended reason phrase (or those + * listed in the IANA HTTP Status Code Registry) for the response's + * status code. + * + * @see http://tools.ietf.org/html/rfc7231#section-6 + * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * + * @return string reason phrase; must return an empty string if none present + */ + public function getReasonPhrase() + { + return $this->reasonPhrase; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/ServerRequest.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/ServerRequest.php new file mode 100644 index 0000000..1f06fe5 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/ServerRequest.php @@ -0,0 +1,468 @@ +server = $server; + $this->cookies = $cookies; + parent::__construct($uri, $headers, $body, $method, $version); + $this->setUploadedFiles($this, $files); + } + + /** + * Retrieve server parameters. + * + * Retrieves data related to the incoming request environment, + * typically derived from PHP's $_SERVER superglobal. The data IS NOT + * REQUIRED to originate from $_SERVER. + * + * @return array + */ + public function getServerParams() + { + return $this->server; + } + + /** + * 获取server参数. + * + * @param string $name + * @param mixed $default + * + * @return string + */ + public function getServerParam($name, $default = null) + { + return isset($this->server[$name]) ? $this->server[$name] : $default; + } + + /** + * Retrieve cookies. + * + * Retrieves cookies sent by the client to the server. + * + * The data MUST be compatible with the structure of the $_COOKIE + * superglobal. + * + * @return array + */ + public function getCookieParams() + { + return $this->cookies; + } + + /** + * Return an instance with the specified cookies. + * + * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST + * be compatible with the structure of $_COOKIE. Typically, this data will + * be injected at instantiation. + * + * This method MUST NOT update the related Cookie header of the request + * instance, nor related values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated cookie values. + * + * @param array $cookies array of key/value pairs representing cookies + * + * @return static + */ + public function withCookieParams(array $cookies) + { + $self = clone $this; + $self->cookies = $cookies; + + return $self; + } + + /** + * 获取cookie值 + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getCookie($name, $default = null) + { + return isset($this->cookies[$name]) ? $this->cookies[$name] : $default; + } + + /** + * Retrieve query string arguments. + * + * Retrieves the deserialized query string arguments, if any. + * + * Note: the query params might not be in sync with the URI or server + * params. If you need to ensure you are only getting the original + * values, you may need to parse the query string from `getUri()->getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams() + { + return $this->get; + } + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query array of query string arguments, typically from + * $_GET + * + * @return static + */ + public function withQueryParams(array $query) + { + $self = clone $this; + $self->get = $query; + + return $self; + } + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return UploadedFile[] an array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present + */ + public function getUploadedFiles() + { + return $this->files; + } + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles an array tree of UploadedFileInterface instances + * + * @return static + * + * @throws \InvalidArgumentException if an invalid structure is provided + */ + public function withUploadedFiles(array $uploadedFiles) + { + $self = clone $this; + + return $this->setUploadedFiles($self, $uploadedFiles); + } + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return array|object|null The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody() + { + $parsedBody = &$this->parsedBody; + if (null === $parsedBody) + { + $body = $this->body; + $contentType = strtolower($this->getHeaderLine(RequestHeader::CONTENT_TYPE)); + // post + if ('POST' === $this->method && \in_array($contentType, [ + MediaType::APPLICATION_FORM_URLENCODED, + MediaType::MULTIPART_FORM_DATA, + ])) + { + $parsedBody = $this->post; + } + // json + elseif (\in_array($contentType, [ + MediaType::APPLICATION_JSON, + MediaType::APPLICATION_JSON_UTF8, + ])) + { + $parsedBody = json_decode($body, true); + } + // xml + elseif (\in_array($contentType, [ + MediaType::TEXT_XML, + MediaType::APPLICATION_ATOM_XML, + MediaType::APPLICATION_RSS_XML, + MediaType::APPLICATION_XHTML_XML, + MediaType::APPLICATION_XML, + ])) + { + $parsedBody = new \DOMDocument(); + $parsedBody->loadXML($body); + } + // 其它 + else + { + $parsedBody = (object) (string) $body; + } + } + + return $parsedBody; + } + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array|object|null $data The deserialized body data. This will + * typically be in an array or object. + * + * @return static + * + * @throws \InvalidArgumentException if an unsupported argument type is + * provided + */ + public function withParsedBody($data) + { + $self = clone $this; + $self->parsedBody = $data; + + return $self; + } + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array attributes derived from the request + */ + public function getAttributes() + { + return $this->attributes; + } + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * + * @param string $name the attribute name + * @param mixed $default default value to return if the attribute does not exist + * + * @return mixed + */ + public function getAttribute($name, $default = null) + { + $attributes = $this->attributes; + if (\array_key_exists($name, $attributes)) + { + return $attributes[$name]; + } + else + { + return YurunHttp::getAttribute($name, $default); + } + } + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * + * @param string $name the attribute name + * @param mixed $value the value of the attribute + * + * @return static + */ + public function withAttribute($name, $value) + { + $self = clone $this; + $self->attributes[$name] = $value; + + return $self; + } + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * + * @param string $name the attribute name + * + * @return static + */ + public function withoutAttribute($name) + { + $self = clone $this; + if (\array_key_exists($name, $self->attributes)) + { + unset($self->attributes[$name]); + } + + return $self; + } + + /** + * 设置上传的文件. + * + * @param static $object + * @param \Yurun\Util\YurunHttp\Http\Psr7\UploadedFile[] $files + * + * @return static + */ + protected function setUploadedFiles(self $object, array $files) + { + $object->files = []; + foreach ($files as $name => $file) + { + if ($file instanceof UploadedFile) + { + $object->files[$name] = $file; + } + else + { + $object->files[$name] = new UploadedFile($file['name'], $file['type'], $file['tmp_name'], $file['size'], $file['error']); + } + } + + return $object; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/UploadedFile.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/UploadedFile.php new file mode 100644 index 0000000..c4e7d58 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/UploadedFile.php @@ -0,0 +1,249 @@ +fileName = $fileName; + $this->mediaType = $mediaType; + $this->tmpFileName = $tmpFileName; + if (null === $size) + { + $this->size = filesize($tmpFileName); + } + else + { + $this->size = $size; + } + $this->error = $error; + } + + /** + * Retrieve a stream representing the uploaded file. + * + * This method MUST return a StreamInterface instance, representing the + * uploaded file. The purpose of this method is to allow utilizing native PHP + * stream functionality to manipulate the file upload, such as + * stream_copy_to_stream() (though the result will need to be decorated in a + * native PHP stream wrapper to work with such functions). + * + * If the moveTo() method has been called previously, this method MUST raise + * an exception. + * + * @return StreamInterface stream representation of the uploaded file + * + * @throws \RuntimeException in cases when no stream is available or can be + * created + */ + public function getStream() + { + if (null === $this->stream) + { + $this->stream = new FileStream($this->tmpFileName); + } + + return $this->stream; + } + + /** + * Move the uploaded file to a new location. + * + * Use this method as an alternative to move_uploaded_file(). This method is + * guaranteed to work in both SAPI and non-SAPI environments. + * Implementations must determine which environment they are in, and use the + * appropriate method (move_uploaded_file(), rename(), or a stream + * operation) to perform the operation. + * + * $targetPath may be an absolute path, or a relative path. If it is a + * relative path, resolution should be the same as used by PHP's rename() + * function. + * + * The original file or stream MUST be removed on completion. + * + * If this method is called more than once, any subsequent calls MUST raise + * an exception. + * + * When used in an SAPI environment where $_FILES is populated, when writing + * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be + * used to ensure permissions and upload status are verified correctly. + * + * If you wish to move to a stream, use getStream(), as SAPI operations + * cannot guarantee writing to stream destinations. + * + * @see http://php.net/is_uploaded_file + * @see http://php.net/move_uploaded_file + * + * @param string $targetPath path to which to move the uploaded file + * + * @return void + * + * @throws \InvalidArgumentException if the $path specified is invalid + * @throws \RuntimeException on any error during the move operation, or on + * the second or subsequent call to the method + */ + public function moveTo($targetPath) + { + if (!\is_string($targetPath)) + { + throw new \InvalidArgumentException('targetPath specified is invalid'); + } + if ($this->isMoved) + { + throw new \RuntimeException('file can not be moved'); + } + if (is_uploaded_file($this->tmpFileName)) + { + $this->isMoved = move_uploaded_file($this->tmpFileName, $targetPath); + } + else + { + $this->isMoved = rename($this->tmpFileName, $targetPath); + } + if (!$this->isMoved) + { + throw new \RuntimeException(sprintf('file %s move to %s fail', $this->tmpFileName, $targetPath)); + } + } + + /** + * Retrieve the file size. + * + * Implementations SHOULD return the value stored in the "size" key of + * the file in the $_FILES array if available, as PHP calculates this based + * on the actual size transmitted. + * + * @return int|null the file size in bytes or null if unknown + */ + public function getSize() + { + return $this->size; + } + + /** + * Retrieve the error associated with the uploaded file. + * + * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants. + * + * If the file was uploaded successfully, this method MUST return + * UPLOAD_ERR_OK. + * + * Implementations SHOULD return the value stored in the "error" key of + * the file in the $_FILES array. + * + * @see http://php.net/manual/en/features.file-upload.errors.php + * + * @return int one of PHP's UPLOAD_ERR_XXX constants + */ + public function getError() + { + return $this->error; + } + + /** + * Retrieve the filename sent by the client. + * + * Do not trust the value returned by this method. A client could send + * a malicious filename with the intention to corrupt or hack your + * application. + * + * Implementations SHOULD return the value stored in the "name" key of + * the file in the $_FILES array. + * + * @return string|null the filename sent by the client or null if none + * was provided + */ + public function getClientFilename() + { + return $this->fileName; + } + + /** + * Retrieve the media type sent by the client. + * + * Do not trust the value returned by this method. A client could send + * a malicious media type with the intention to corrupt or hack your + * application. + * + * Implementations SHOULD return the value stored in the "type" key of + * the file in the $_FILES array. + * + * @return string|null the media type sent by the client or null if none + * was provided + */ + public function getClientMediaType() + { + return $this->mediaType; + } + + /** + * 获取上传文件的临时文件路径. + * + * @return string + */ + public function getTempFileName() + { + return $this->tmpFileName; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Uri.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Uri.php new file mode 100644 index 0000000..b2711e9 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Psr7/Uri.php @@ -0,0 +1,590 @@ + 80, + 'https' => 443, + 'ftp' => 21, + ]; + + /** + * @param string $uri + */ + public function __construct($uri = '') + { + $uriOption = parse_url($uri); + if (false === $uriOption) + { + throw new \InvalidArgumentException(sprintf('uri %s parse error', $uri)); + } + $this->scheme = isset($uriOption['scheme']) ? $uriOption['scheme'] : ''; + $this->host = isset($uriOption['host']) ? $uriOption['host'] : ''; + $this->port = isset($uriOption['port']) ? $uriOption['port'] : null; + $this->userInfo = isset($uriOption['user']) ? $uriOption['user'] : ''; + if (isset($uriOption['pass'])) + { + $this->userInfo .= ':' . $uriOption['pass']; + } + $this->path = isset($uriOption['path']) ? $uriOption['path'] : ''; + $this->query = isset($uriOption['query']) ? $uriOption['query'] : ''; + $this->fragment = isset($uriOption['fragment']) ? $uriOption['fragment'] : ''; + } + + /** + * 生成Uri文本. + * + * @param string $host + * @param string $path + * @param string $query + * @param int $port + * @param string $scheme + * @param string $fragment + * @param string $userInfo + * + * @return string + */ + public static function makeUriString($host, $path, $query = '', $port = null, $scheme = 'http', $fragment = '', $userInfo = '') + { + $uri = ''; + // 协议 + if ('' !== $scheme) + { + $uri = $scheme . '://'; + } + // 用户信息 + if ('' !== $userInfo) + { + $uri .= $userInfo . '@'; + } + // 主机+端口 + $uri .= $host . (null === $port ? '' : (':' . $port)); + // 路径 + $uri .= '/' . ltrim($path, '/'); + // 查询参数 + $uri .= ('' === $query ? '' : ('?' . $query)); + // 锚点 + $uri .= ('' === $fragment ? '' : ('#' . $fragment)); + + return $uri; + } + + /** + * 生成Uri对象 + * + * @param string $host + * @param string $path + * @param string $query + * @param int $port + * @param string $scheme + * @param string $fragment + * @param string $userInfo + * + * @return static + */ + public static function makeUri($host, $path, $query = '', $port = 80, $scheme = 'http', $fragment = '', $userInfo = '') + { + return new static(static::makeUriString($host, $path, $query, $port, $scheme, $fragment, $userInfo)); + } + + /** + * 获取连接到服务器的端口. + * + * @param \Psr\Http\Message\UriInterface $uri + * + * @return int + */ + public static function getServerPort(UriInterface $uri) + { + $port = $uri->getPort(); + if (!$port) + { + $scheme = $uri->getScheme(); + $port = isset(static::$schemePorts[$scheme]) ? static::$schemePorts[$scheme] : null; + } + + return $port; + } + + /** + * 获取域名 + * 格式:host[:port]. + * + * @param \Psr\Http\Message\UriInterface $uri + * + * @return string + */ + public static function getDomain(UriInterface $uri) + { + $result = $uri->getHost(); + if (null !== ($port = $uri->getPort())) + { + $result .= ':' . $port; + } + + return $result; + } + + /** + * Retrieve the scheme component of the URI. + * + * If no scheme is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.1. + * + * The trailing ":" character is not part of the scheme and MUST NOT be + * added. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.1 + * + * @return string the URI scheme + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * Retrieve the authority component of the URI. + * + * If no authority information is present, this method MUST return an empty + * string. + * + * The authority syntax of the URI is: + * + *
+     * [user-info@]host[:port]
+     * 
+ * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * + * @return string the URI authority, in "[user-info@]host[:port]" format + */ + public function getAuthority() + { + $result = $this->host; + if ('' !== $this->userInfo) + { + $result = $this->userInfo . '@' . $result; + } + if (null !== $this->port) + { + $result .= ':' . $this->port; + } + + return $result; + } + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string the URI user information, in "username[:password]" format + */ + public function getUserInfo() + { + return $this->userInfo; + } + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * + * @return string the URI host + */ + public function getHost() + { + return $this->host; + } + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return int|null the URI port + */ + public function getPort() + { + return $this->port; + } + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * + * @return string the URI path + */ + public function getPath() + { + return $this->path; + } + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * + * @return string the URI query string + */ + public function getQuery() + { + return $this->query; + } + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * + * @return string the URI fragment + */ + public function getFragment() + { + return $this->fragment; + } + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme the scheme to use with the new instance + * + * @return static a new instance with the specified scheme + * + * @throws \InvalidArgumentException for invalid or unsupported schemes + */ + public function withScheme($scheme) + { + if (!\is_string($scheme)) + { + throw new \InvalidArgumentException('invalid or unsupported schemes'); + } + $self = clone $this; + $self->scheme = $scheme; + + return $self; + } + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user the user name to use for authority + * @param string|null $password the password associated with $user + * + * @return static a new instance with the specified user information + */ + public function withUserInfo($user, $password = null) + { + $self = clone $this; + $self->userInfo = $user; + if (null !== $password) + { + $self->userInfo .= ':' . $password; + } + + return $self; + } + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host the hostname to use with the new instance + * + * @return static a new instance with the specified host + * + * @throws \InvalidArgumentException for invalid hostnames + */ + public function withHost($host) + { + $self = clone $this; + $self->host = $host; + + return $self; + } + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param int|null $port the port to use with the new instance; a null value + * removes the port information + * + * @return static a new instance with the specified port + * + * @throws \InvalidArgumentException for invalid ports + */ + public function withPort($port) + { + $self = clone $this; + $self->port = $port; + + return $self; + } + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path the path to use with the new instance + * + * @return static a new instance with the specified path + * + * @throws \InvalidArgumentException for invalid paths + */ + public function withPath($path) + { + $self = clone $this; + $self->path = $path; + + return $self; + } + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query the query string to use with the new instance + * + * @return static a new instance with the specified query string + * + * @throws \InvalidArgumentException for invalid query strings + */ + public function withQuery($query) + { + $self = clone $this; + $self->query = $query; + + return $self; + } + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment the fragment to use with the new instance + * + * @return static a new instance with the specified fragment + */ + public function withFragment($fragment) + { + $self = clone $this; + $self->fragment = $fragment; + + return $self; + } + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * + * @return string + */ + public function __toString() + { + return static::makeUriString($this->host, $this->path, $this->query, $this->port, $this->scheme, $this->fragment, $this->userInfo); + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Request.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Request.php new file mode 100644 index 0000000..471317f --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http/Request.php @@ -0,0 +1,9 @@ +cookies; + } + + /** + * 获取cookie值 + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getCookie($name, $default = null) + { + return isset($this->cookies[$name]) ? $this->cookies[$name] : $default; + } + + /** + * 设置cookie原始参数,包含expires、path、domain等. + * + * @param array $cookiesOrigin + * + * @return static + */ + public function withCookieOriginParams(array $cookiesOrigin) + { + $self = clone $this; + $self->cookiesOrigin = $cookiesOrigin; + $self->cookies = []; + foreach ($cookiesOrigin as $name => $value) + { + $self->cookies[$name] = $value['value']; + } + + return $self; + } + + /** + * 获取所有cookie原始参数,包含expires、path、domain等. + * + * @return array + */ + public function getCookieOriginParams() + { + return $this->cookiesOrigin; + } + + /** + * 获取cookie原始参数值,包含expires、path、domain等. + * + * @param string $name + * @param mixed $default + * + * @return string + */ + public function getCookieOrigin($name, $default = null) + { + return isset($this->cookiesOrigin[$name]) ? $this->cookiesOrigin[$name] : $default; + } + + /** + * @param string|\Psr\Http\Message\StreamInterface $body + * @param int $statusCode + * @param string|null $reasonPhrase + */ + public function __construct($body = '', $statusCode = StatusCode::OK, $reasonPhrase = null) + { + parent::__construct($body, $statusCode, $reasonPhrase); + $this->success = $statusCode >= 100; + } + + /** + * 获取返回的主体内容. + * + * @param string $fromEncoding 请求返回数据的编码,如果不为空则进行编码转换 + * @param string $toEncoding 要转换到的编码,默认为UTF-8 + * + * @return string + */ + public function body($fromEncoding = null, $toEncoding = 'UTF-8') + { + if (null === $fromEncoding) + { + return (string) $this->getBody(); + } + else + { + return mb_convert_encoding((string) $this->getBody(), $toEncoding, $fromEncoding); + } + } + + /** + * 获取xml格式内容. + * + * @param bool $assoc 为true时返回数组,为false时返回对象 + * @param string $fromEncoding 请求返回数据的编码,如果不为空则进行编码转换 + * @param string $toEncoding 要转换到的编码,默认为UTF-8 + * + * @return mixed + */ + public function xml($assoc = false, $fromEncoding = null, $toEncoding = 'UTF-8') + { + $xml = simplexml_load_string($this->body($fromEncoding, $toEncoding), 'SimpleXMLElement', \LIBXML_NOCDATA | \LIBXML_COMPACT); + if ($assoc) + { + $xml = (array) $xml; + } + + return $xml; + } + + /** + * 获取json格式内容. + * + * @param bool $assoc 为true时返回数组,为false时返回对象 + * @param string $fromEncoding 请求返回数据的编码,如果不为空则进行编码转换 + * @param string $toEncoding 要转换到的编码,默认为UTF-8 + * + * @return mixed + */ + public function json($assoc = false, $fromEncoding = null, $toEncoding = 'UTF-8') + { + return json_decode($this->body($fromEncoding, $toEncoding), $assoc); + } + + /** + * 获取jsonp格式内容. + * + * @param bool $assoc 为true时返回数组,为false时返回对象 + * @param string $fromEncoding 请求返回数据的编码,如果不为空则进行编码转换 + * @param string $toEncoding 要转换到的编码,默认为UTF-8 + * + * @return mixed + */ + public function jsonp($assoc = false, $fromEncoding = null, $toEncoding = 'UTF-8') + { + $jsonp = trim($this->body($fromEncoding, $toEncoding)); + if (isset($jsonp[0]) && '[' !== $jsonp[0] && '{' !== $jsonp[0]) + { + $begin = strpos($jsonp, '('); + if (false !== $begin) + { + $end = strrpos($jsonp, ')'); + if (false !== $end) + { + $jsonp = substr($jsonp, $begin + 1, $end - $begin - 1); + } + } + } + + return json_decode($jsonp, $assoc); + } + + /** + * 获取http状态码 + * + * @return int + */ + public function httpCode() + { + return $this->getStatusCode(); + } + + /** + * 获取请求总耗时,单位:秒. + * + * @return float + */ + public function totalTime() + { + return $this->totalTime; + } + + /** + * 获取请求总耗时,单位:秒. + * + * @return float + */ + public function getTotalTime() + { + return $this->totalTime; + } + + /** + * 设置请求总耗时. + * + * @param float $totalTime + * + * @return static + */ + public function withTotalTime($totalTime) + { + $self = clone $this; + $self->totalTime = $totalTime; + + return $self; + } + + /** + * 返回错误信息. + * + * @return string + */ + public function error() + { + return $this->error; + } + + /** + * 获取错误信息. + * + * @return string + */ + public function getError() + { + return $this->error; + } + + /** + * 设置错误信息. + * + * @param string $error + * + * @return static + */ + public function withError($error) + { + $self = clone $this; + $self->error = $error; + + return $self; + } + + /** + * 返回错误代码 + * + * @return int + */ + public function errno() + { + return $this->errno; + } + + /** + * 获取错误代码 + * + * @return int + */ + public function getErrno() + { + return $this->errno; + } + + /** + * 设置错误代码 + * + * @param int $errno + * + * @return static + */ + public function withErrno($errno) + { + $self = clone $this; + $self->errno = $errno; + + return $self; + } + + /** + * 设置 Http2 streamId. + * + * @param int $streamId + * + * @return static + */ + public function withStreamId($streamId) + { + $self = clone $this; + $self->streamId = $streamId; + + return $self; + } + + /** + * Get http2 streamId. + * + * @return int + */ + public function getStreamId() + { + return $this->streamId; + } + + /** + * 设置请求体. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * + * @return static + */ + public function withRequest($request) + { + $self = clone $this; + $self->request = $request; + + return $self; + } + + /** + * Get request. + * + * @return \Yurun\Util\YurunHttp\Http\Request + */ + public function getRequest() + { + return $this->request; + } + + /** + * 设置保存到的文件名. + * + * @param string|null $savedFileName + * + * @return static + */ + public function withSavedFileName($savedFileName) + { + $self = clone $this; + $self->savedFileName = $savedFileName; + + return $self; + } + + /** + * 获取保存到的文件名. + * + * @return string|null + */ + public function getSavedFileName() + { + return $this->savedFileName; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Http2/IHttp2Client.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http2/IHttp2Client.php new file mode 100644 index 0000000..6f6d0aa --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Http2/IHttp2Client.php @@ -0,0 +1,135 @@ +host = $host; + $this->port = $port; + $this->ssl = $ssl; + if ($handler) + { + $this->handler = $handler; + } + else + { + $this->handler = new \Yurun\Util\YurunHttp\Handler\Swoole(); + } + } + + /** + * 连接. + * + * @return bool + */ + public function connect() + { + $url = ($this->ssl ? 'https://' : 'http://') . $this->host . ':' . $this->port; + $client = $this->handler->getHttp2ConnectionManager()->getConnection($url); + if ($client) + { + $this->http2Client = $client; + if ($this->timeout) + { + $client->set([ + 'timeout' => $this->timeout, + ]); + } + + return true; + } + else + { + return false; + } + } + + /** + * 获取 Http Handler. + * + * @return \Yurun\Util\YurunHttp\Handler\IHandler + */ + public function getHttpHandler() + { + return $this->handler; + } + + /** + * 关闭连接. + * + * @return void + */ + public function close() + { + $this->http2Client = null; + $url = ($this->ssl ? 'https://' : 'http://') . $this->host . ':' . $this->port; + $this->handler->getHttp2ConnectionManager()->closeConnection($url); + $recvChannels = &$this->recvChannels; + foreach ($recvChannels as $channel) + { + $channel->close(); + } + $recvChannels = []; + } + + /** + * 发送数据 + * 成功返回streamId,失败返回false. + * + * @param \Yurun\Util\YurunHttp\Http\Request $request + * @param bool $pipeline 默认send方法在发送请求之后,会结束当前的Http2 Stream,启用PIPELINE后,底层会保持stream流,可以多次调用write方法,向服务器发送数据帧,请参考write方法 + * @param bool $dropRecvResponse 丢弃接收到的响应数据 + * + * @return int|bool + */ + public function send($request, $pipeline = false, $dropRecvResponse = false) + { + if ('2.0' !== $request->getProtocolVersion()) + { + $request = $request->withProtocolVersion('2.0'); + } + $uri = $request->getUri(); + if ($this->host != $uri->getHost() || $this->port != Uri::getServerPort($uri) || $this->ssl != ('https' === $uri->getScheme() || 'wss' === $uri->getScheme())) + { + throw new \RuntimeException(sprintf('Current http2 connection instance just support %s://%s:%s, does not support %s', $this->ssl ? 'https' : 'http', $this->host, $this->port, $uri->__toString())); + } + $http2Client = $this->http2Client; + $request = $request->withAttribute(Attributes::HTTP2_PIPELINE, $pipeline); + $this->handler->buildRequest($request, $http2Client, $http2Request); + $streamId = $http2Client->send($http2Request); + if (!$streamId) + { + $this->close(); + } + if (!$dropRecvResponse) + { + $this->recvChannels[$streamId] = new Channel(1); + $this->requestMap[$streamId] = $request; + } + + return $streamId; + } + + /** + * 向一个流写入数据帧. + * + * @param int $streamId + * @param string $data + * @param bool $end 是否关闭流 + * + * @return bool + */ + public function write($streamId, $data, $end = false) + { + return $this->http2Client->write($streamId, $data, $end); + } + + /** + * 关闭一个流 + * + * @param int $streamId + * + * @return bool + */ + public function end($streamId) + { + return $this->http2Client->write($streamId, '', true); + } + + /** + * 接收数据. + * + * @param int|null $streamId 默认不传为 -1 时则监听服务端推送 + * @param float|null $timeout 超时时间,单位:秒。默认为 null 不限制 + * + * @return \Yurun\Util\YurunHttp\Http\Response|bool + */ + public function recv($streamId = -1, $timeout = null) + { + $recvCo = $this->recvCo; + if (!$recvCo || (true !== $recvCo && !Coroutine::exists($recvCo))) + { + $this->startRecvCo(); + } + $recvChannels = &$this->recvChannels; + if (isset($recvChannels[$streamId])) + { + $channel = $recvChannels[$streamId]; + } + else + { + $recvChannels[$streamId] = $channel = new Channel(-1 === $streamId ? $this->serverPushQueueLength : 1); + } + $swooleResponse = $channel->pop(null === $timeout ? -1 : $timeout); + if (-1 !== $streamId) + { + unset($recvChannels[$streamId]); + $channel->close(); + } + $requestMap = &$this->requestMap; + if (isset($requestMap[$streamId])) + { + $request = $requestMap[$streamId]; + unset($requestMap[$streamId]); + } + else + { + $request = null; + } + $response = $this->handler->buildHttp2Response($request, $this->http2Client, $swooleResponse); + + return $response; + } + + /** + * 是否已连接. + * + * @return bool + */ + public function isConnected() + { + return null !== $this->http2Client; + } + + /** + * 开始接收协程 + * 成功返回协程ID. + * + * @return int|bool + */ + private function startRecvCo() + { + if (!$this->isConnected()) + { + return false; + } + $recvCo = &$this->recvCo; + $recvCo = true; + + return $recvCo = Coroutine::create(function () { + $http2Client = &$this->http2Client; + $recvChannels = &$this->recvChannels; + while ($this->isConnected()) + { + if ($this->timeout > 0) + { + $swooleResponse = $http2Client->recv($this->timeout); + } + else + { + $swooleResponse = $http2Client->recv(); + } + if (!$swooleResponse) + { + if ($this->ping()) + { + continue; + } + $this->close(); + + return; + } + $streamId = $swooleResponse->streamId; + if (isset($recvChannels[$streamId]) || (0 === ($streamId & 1) && isset($recvChannels[$streamId = -1]))) + { + $recvChannels[$streamId]->push($swooleResponse); + } + } + }); + } + + /** + * Get 主机名. + * + * @return string + */ + public function getHost() + { + return $this->host; + } + + /** + * Get 端口. + * + * @return int + */ + public function getPort() + { + return $this->port; + } + + /** + * Get 是否使用 ssl. + * + * @return bool + */ + public function isSSL() + { + return $this->ssl; + } + + /** + * 获取正在接收的流数量. + * + * @return int + */ + public function getRecvingCount() + { + return \count($this->recvChannels); + } + + /** + * Get 服务端推送数据队列长度. + * + * @return int + */ + public function getServerPushQueueLength() + { + return $this->serverPushQueueLength; + } + + /** + * Set 服务端推送数据队列长度. + * + * @param int $serverPushQueueLength 服务端推送数据队列长度 + * + * @return self + */ + public function setServerPushQueueLength($serverPushQueueLength) + { + $this->serverPushQueueLength = $serverPushQueueLength; + + return $this; + } + + /** + * 设置超时时间,单位:秒. + * + * @param float|null $timeout + * + * @return void + */ + public function setTimeout($timeout) + { + $this->timeout = $timeout; + $http2Client = $this->http2Client; + if ($http2Client) + { + $http2Client->set([ + 'timeout' => $timeout, + ]); + } + } + + /** + * 获取超时时间,单位:秒. + * + * @return float|null + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * 发送 ping 帧检查连接. + * + * @return bool + */ + public function ping() + { + return $this->http2Client && $this->http2Client->ping(); + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/BaseConnectionPool.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/BaseConnectionPool.php new file mode 100644 index 0000000..de0b592 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/BaseConnectionPool.php @@ -0,0 +1,33 @@ +config = $config; + } + + /** + * 获取连接池配置. + * + * @return \Yurun\Util\YurunHttp\Pool\Config\PoolConfig + */ + public function getConfig() + { + return $this->config; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Config/PoolConfig.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Config/PoolConfig.php new file mode 100644 index 0000000..69e9718 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Config/PoolConfig.php @@ -0,0 +1,93 @@ +url = $url; + $this->maxConnections = $maxConnections; + $this->waitTimeout = $waitTimeout; + } + + /** + * 获取最大连接数量. + * + * @return int + */ + public function getMaxConnections() + { + return $this->maxConnections; + } + + /** + * 设置最大连接数量. + * + * @param int $maxConnections + * + * @return void + */ + public function setMaxConnections($maxConnections) + { + $this->maxConnections = $maxConnections; + } + + /** + * 获取等待超时时间. + * + * @return float|null + */ + public function getWaitTimeout() + { + return $this->waitTimeout; + } + + /** + * 设置等待超时时间. + * + * @param float|null $waitTimeout + * + * @return void + */ + public function setWaitTimeout($waitTimeout) + { + $this->waitTimeout = $waitTimeout; + } + + /** + * Get 地址 + * + * @return string + */ + public function getUrl() + { + return $this->url; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Contract/IConnectionPool.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Contract/IConnectionPool.php new file mode 100644 index 0000000..b732d82 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Pool/Contract/IConnectionPool.php @@ -0,0 +1,64 @@ +connectionPoolConfigs[$url])) + { + $config = $this->connectionPoolConfigs[$url]; + $config->setMaxConnections($maxConnections); + $config->setWaitTimeout($waitTimeout); + + return $config; + } + else + { + return $this->connectionPoolConfigs[$url] = new PoolConfig($url, $maxConnections, $waitTimeout); + } + } + + /** + * 获取连接池配置. + * + * @param string $url + * + * @return PoolConfig|null + */ + public function getConfig($url) + { + if (isset($this->connectionPoolConfigs[$url])) + { + return $this->connectionPoolConfigs[$url]; + } + else + { + return null; + } + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Random.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Random.php new file mode 100644 index 0000000..9caf1ee --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Random.php @@ -0,0 +1,94 @@ +uri = $uri = new Uri($uri); + } + elseif ($uri instanceof UriInterface) + { + $this->uri = $uri; + } + else + { + $uri = $this->uri; + } + $this->mode = $mode; + $stream = fopen($uri, $mode); + if (false === $stream) + { + throw new \RuntimeException(sprintf('Open stream %s error', (string) $uri)); + } + $this->stream = $stream; + } + + public function __destruct() + { + if ($this->stream) + { + $this->close(); + } + } + + /** + * Reads all data from the stream into a string, from the beginning to end. + * + * This method MUST attempt to seek to the beginning of the stream before + * reading data and read the stream until the end is reached. + * + * Warning: This could attempt to load a large amount of data into memory. + * + * This method MUST NOT raise an exception in order to conform with PHP's + * string casting operations. + * + * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring + * + * @return string + */ + public function __toString() + { + try + { + $this->rewind(); + + return stream_get_contents($this->stream); + } + catch (\Throwable $ex) + { + return ''; + } + } + + /** + * Closes the stream and any underlying resources. + * + * @return void + */ + public function close() + { + fclose($this->stream); + $this->stream = null; + } + + /** + * Separates any underlying resources from the stream. + * + * After the stream has been detached, the stream is in an unusable state. + * + * @return resource|null Underlying PHP stream, if any + */ + public function detach() + { + $stream = $this->stream; + $this->stream = null; + + return $stream; + } + + /** + * Get the size of the stream if known. + * + * @return int|null returns the size in bytes if known, or null if unknown + */ + public function getSize() + { + $stat = fstat($this->stream); + if (false === $stat) + { + throw new \RuntimeException('get stream size error'); + } + + return $stat['size']; + } + + /** + * Returns the current position of the file read/write pointer. + * + * @return int Position of the file pointer + * + * @throws \RuntimeException on error + */ + public function tell() + { + $result = ftell($this->stream); + if (false === $result) + { + throw new \RuntimeException('stream tell error'); + } + + return $result; + } + + /** + * Returns true if the stream is at the end of the stream. + * + * @return bool + */ + public function eof() + { + return feof($this->stream); + } + + /** + * Returns whether or not the stream is seekable. + * + * @return bool + */ + public function isSeekable() + { + return (bool) $this->getMetadata('seekable'); + } + + /** + * Seek to a position in the stream. + * + * @see http://www.php.net/manual/en/function.fseek.php + * + * @param int $offset Stream offset + * @param int $whence Specifies how the cursor position will be calculated + * based on the seek offset. Valid values are identical to the built-in + * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to + * offset bytes SEEK_CUR: Set position to current location plus offset + * SEEK_END: Set position to end-of-stream plus offset. + * + * @return void + * + * @throws \RuntimeException on failure + */ + public function seek($offset, $whence = \SEEK_SET) + { + if (-1 === fseek($this->stream, $offset, $whence)) + { + throw new \RuntimeException('seek stream error'); + } + } + + /** + * Seek to the beginning of the stream. + * + * If the stream is not seekable, this method will raise an exception; + * otherwise, it will perform a seek(0). + * + * @see seek() + * @see http://www.php.net/manual/en/function.fseek.php + * + * @return void + * + * @throws \RuntimeException on failure + */ + public function rewind() + { + if (!rewind($this->stream)) + { + throw new \RuntimeException('rewind stream failed'); + } + } + + /** + * Returns whether or not the stream is writable. + * + * @return bool + */ + public function isWritable() + { + return \in_array($this->mode, [ + StreamMode::WRITE_CLEAN, + StreamMode::WRITE_END, + StreamMode::CREATE_READ_WRITE, + StreamMode::CREATE_WRITE, + StreamMode::READ_WRITE, + StreamMode::READ_WRITE_CLEAN, + StreamMode::READ_WRITE_END, + ]); + } + + /** + * Write data to the stream. + * + * @param string $string the string that is to be written + * + * @return int returns the number of bytes written to the stream + * + * @throws \RuntimeException on failure + */ + public function write($string) + { + $result = fwrite($this->stream, $string); + if (false === $result) + { + throw new \RuntimeException('write stream failed'); + } + + return $result; + } + + /** + * Returns whether or not the stream is readable. + * + * @return bool + */ + public function isReadable() + { + return \in_array($this->mode, [ + StreamMode::READ_WRITE, + StreamMode::READ_WRITE_CLEAN, + StreamMode::READ_WRITE_END, + StreamMode::READONLY, + StreamMode::CREATE_READ_WRITE, + ]); + } + + /** + * Read data from the stream. + * + * @param int $length Read up to $length bytes from the object and return + * them. Fewer than $length bytes may be returned if underlying stream + * call returns fewer bytes. + * + * @return string returns the data read from the stream, or an empty string + * if no bytes are available + * + * @throws \RuntimeException if an error occurs + */ + public function read($length) + { + $result = fread($this->stream, $length); + if (false === $result) + { + throw new \RuntimeException('read stream error'); + } + + return $result; + } + + /** + * Returns the remaining contents in a string. + * + * @return string + * + * @throws \RuntimeException if unable to read or an error occurs while + * reading + */ + public function getContents() + { + $result = stream_get_contents($this->stream); + if (false === $result) + { + throw new \RuntimeException('stream getContents error'); + } + + return $result; + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * + * The keys returned are identical to the keys returned from PHP's + * stream_get_meta_data() function. + * + * @see http://php.net/manual/en/function.stream-get-meta-data.php + * + * @param string $key specific metadata to retrieve + * + * @return array|mixed|null Returns an associative array if no key is + * provided. Returns a specific key value if a key is provided and the + * value is found, or null if the key is not found. + */ + public function getMetadata($key = null) + { + $result = stream_get_meta_data($this->stream); + /* @phpstan-ignore-next-line */ + if (!$result) + { + throw new \RuntimeException('stream getMetadata error'); + } + if (null === $key) + { + return $result; + } + elseif (isset($result[$key])) + { + return $result[$key]; + } + else + { + return null; + } + } + + /** + * Get Uri. + * + * @return UriInterface + */ + public function getUri() + { + return $this->uri; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/MemoryStream.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/MemoryStream.php new file mode 100644 index 0000000..22b2fd6 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/MemoryStream.php @@ -0,0 +1,281 @@ +content = $content; + $this->size = \is_string($content) ? \strlen($content) : 0; + } + + /** + * Reads all data from the stream into a string, from the beginning to end. + * + * This method MUST attempt to seek to the beginning of the stream before + * reading data and read the stream until the end is reached. + * + * Warning: This could attempt to load a large amount of data into memory. + * + * This method MUST NOT raise an exception in order to conform with PHP's + * string casting operations. + * + * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring + * + * @return string + */ + public function __toString() + { + return $this->content; + } + + /** + * Closes the stream and any underlying resources. + * + * @return void + */ + public function close() + { + $this->content = ''; + $this->size = -1; + } + + /** + * Separates any underlying resources from the stream. + * + * After the stream has been detached, the stream is in an unusable state. + * + * @return resource|null Underlying PHP stream, if any + */ + public function detach() + { + return null; + } + + /** + * Get the size of the stream if known. + * + * @return int|null returns the size in bytes if known, or null if unknown + */ + public function getSize() + { + return $this->size; + } + + /** + * Returns the current position of the file read/write pointer. + * + * @return int Position of the file pointer + * + * @throws \RuntimeException on error + */ + public function tell() + { + return $this->position; + } + + /** + * Returns true if the stream is at the end of the stream. + * + * @return bool + */ + public function eof() + { + return $this->position > $this->size; + } + + /** + * Returns whether or not the stream is seekable. + * + * @return bool + */ + public function isSeekable() + { + return true; + } + + /** + * Seek to a position in the stream. + * + * @see http://www.php.net/manual/en/function.fseek.php + * + * @param int $offset Stream offset + * @param int $whence Specifies how the cursor position will be calculated + * based on the seek offset. Valid values are identical to the built-in + * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to + * offset bytes SEEK_CUR: Set position to current location plus offset + * SEEK_END: Set position to end-of-stream plus offset. + * + * @return void + * + * @throws \RuntimeException on failure + */ + public function seek($offset, $whence = \SEEK_SET) + { + switch ($whence) + { + case \SEEK_SET: + if ($offset < 0) + { + throw new \RuntimeException('offset failure'); + } + $this->position = $offset; + break; + case \SEEK_CUR: + $this->position += $offset; + break; + case \SEEK_END: + $this->position = $this->size - 1 + $offset; + break; + } + } + + /** + * Seek to the beginning of the stream. + * + * If the stream is not seekable, this method will raise an exception; + * otherwise, it will perform a seek(0). + * + * @see seek() + * @see http://www.php.net/manual/en/function.fseek.php + * + * @return void + * + * @throws \RuntimeException on failure + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Returns whether or not the stream is writable. + * + * @return bool + */ + public function isWritable() + { + return true; + } + + /** + * Write data to the stream. + * + * @param string $string the string that is to be written + * + * @return int returns the number of bytes written to the stream + * + * @throws \RuntimeException on failure + */ + public function write($string) + { + $content = &$this->content; + $position = &$this->position; + $content = substr_replace($content, $string, $position, 0); + $len = \is_string($string) ? \strlen($string) : 0; + $position += $len; + $this->size += $len; + + return $len; + } + + /** + * Returns whether or not the stream is readable. + * + * @return bool + */ + public function isReadable() + { + return true; + } + + /** + * Read data from the stream. + * + * @param int $length Read up to $length bytes from the object and return + * them. Fewer than $length bytes may be returned if underlying stream + * call returns fewer bytes. + * + * @return string returns the data read from the stream, or an empty string + * if no bytes are available + * + * @throws \RuntimeException if an error occurs + */ + public function read($length) + { + $position = &$this->position; + $result = substr($this->content, $position, $length); + $position += $length; + + return $result; + } + + /** + * Returns the remaining contents in a string. + * + * @return string + * + * @throws \RuntimeException if unable to read or an error occurs while + * reading + */ + public function getContents() + { + $position = &$this->position; + if (0 === $position) + { + $position = $this->size; + + return $this->content; + } + else + { + return $this->read($this->size - $position); + } + } + + /** + * Get stream metadata as an associative array or retrieve a specific key. + * + * The keys returned are identical to the keys returned from PHP's + * stream_get_meta_data() function. + * + * @see http://php.net/manual/en/function.stream-get-meta-data.php + * + * @param string $key specific metadata to retrieve + * + * @return array|mixed|null Returns an associative array if no key is + * provided. Returns a specific key value if a key is provided and the + * value is found, or null if the key is not found. + */ + public function getMetadata($key = null) + { + return null; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/StreamMode.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/StreamMode.php new file mode 100644 index 0000000..05c997b --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Stream/StreamMode.php @@ -0,0 +1,53 @@ +cookieManager = new CookieManager(); + } + + /** + * Get cookie 管理器. + * + * @return \Yurun\Util\YurunHttp\Cookie\CookieManager + */ + public function getCookieManager() + { + return $this->cookieManager; + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/Traits/THandler.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/Traits/THandler.php new file mode 100644 index 0000000..8200bf3 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/Traits/THandler.php @@ -0,0 +1,70 @@ +getHost()) + { + if (!isset($location[0])) + { + throw new InvalidArgumentException(sprintf('Invalid $location: %s', $location)); + } + if ('/' === $location[0]) + { + $uri = $currentUri->withQuery('')->withPath($location); + } + else + { + $uri = $currentUri; + $path = $currentUri->getPath(); + if ('/' !== substr($path, -1, 1)) + { + $path = $path . '/'; + } + $path .= $location; + $uri = $uri->withPath($path); + } + $uri = $uri->withHost($currentUri->getHost())->withPort($currentUri->getPort()); + } + else + { + $uri = $locationUri; + } + + return $uri; + } + + /** + * 检查请求对象 + * + * @param \Yurun\Util\YurunHttp\Http\Request[] $requests + * + * @return void + */ + protected function checkRequests($requests) + { + foreach ($requests as $request) + { + if (!$request instanceof \Yurun\Util\YurunHttp\Http\Request) + { + throw new \InvalidArgumentException('Request must be instance of \Yurun\Util\YurunHttp\Http\Request'); + } + } + } +} diff --git a/vendor/yurunsoft/yurun-http/src/YurunHttp/WebSocket/IWebSocketClient.php b/vendor/yurunsoft/yurun-http/src/YurunHttp/WebSocket/IWebSocketClient.php new file mode 100644 index 0000000..7fcb978 --- /dev/null +++ b/vendor/yurunsoft/yurun-http/src/YurunHttp/WebSocket/IWebSocketClient.php @@ -0,0 +1,98 @@ +httpHandler = $httpHandler; + $this->request = $request; + $this->response = $response; + $this->handler = $request->getAttribute(Attributes::PRIVATE_CONNECTION); + $this->connected = true; + } + + /** + * 获取 Http Handler. + * + * @return \Yurun\Util\YurunHttp\Handler\IHandler + */ + public function getHttpHandler() + { + return $this->httpHandler; + } + + /** + * 获取 Http Request. + * + * @return \Yurun\Util\YurunHttp\Http\Request + */ + public function getHttpRequest() + { + return $this->request; + } + + /** + * 获取 Http Response. + * + * @return \Yurun\Util\YurunHttp\Http\Response + */ + public function getHttpResponse() + { + return $this->response; + } + + /** + * 连接. + * + * @return bool + */ + public function connect() + { + $this->httpHandler->websocket($this->request, $this); + + return $this->isConnected(); + } + + /** + * 关闭连接. + * + * @return void + */ + public function close() + { + $this->handler->close(); + $this->connected = true; + } + + /** + * 发送数据. + * + * @param mixed $data + * + * @return bool + */ + public function send($data) + { + $handler = $this->handler; + $result = $handler->push($data); + if (!$result) + { + $errCode = $handler->errCode; + throw new WebSocketException(sprintf('Send Failed, error: %s, errorCode: %s', swoole_strerror($errCode), $errCode), $errCode); + } + + return $result; + } + + /** + * 接收数据. + * + * @param float|null $timeout 超时时间,单位:秒。默认为 null 不限制 + * + * @return mixed + */ + public function recv($timeout = null) + { + $result = $this->handler->recv((float) $timeout); + if (!$result) + { + return false; + } + + return $result->data; + } + + /** + * 是否已连接. + * + * @return bool + */ + public function isConnected() + { + return $this->connected; + } + + /** + * 获取错误码 + * + * @return int + */ + public function getErrorCode() + { + return $this->handler->errCode; + } + + /** + * 获取错误信息. + * + * @return string + */ + public function getErrorMessage() + { + return $this->handler->errMsg; + } + + /** + * 获取原始客户端对象 + * + * @return \Swoole\Coroutine\Http\Client + */ + public function getClient() + { + return $this->handler; + } +}