first commit

This commit is contained in:
root
2025-06-18 10:31:43 +08:00
commit d9f820b55d
981 changed files with 449311 additions and 0 deletions

68
app/Command/CoverCommand.php Executable file
View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Helpers\AppHelper;
use App\Model\AppArticle;
use Hyperf\Collection\Collection;
use Hyperf\Command\Annotation\Command;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\DbConnection\Db;
use Psr\Container\ContainerInterface;
use function Hyperf\Coroutine\co;
#[Command]
class CoverCommand extends HyperfCommand
{
protected $reids;
public function __construct(protected ContainerInterface $container)
{
parent::__construct('demo:cover');
}
public function configure()
{
parent::configure();
$this->setDescription('Hyperf Demo Command');
}
public function handle()
{
$this->line('Hello Hyperf!', 'info');
Db::table('app_articles')->where('id', '>', 11284)->orderBy('id')->chunk(20, function (Collection $item) {
$waitGroup = new \Hyperf\Coroutine\WaitGroup();
foreach ($item as $v) {;
if (!$v || $v->cover) {
if ($v->year) {
continue;
}
}
$waitGroup->add();
co(function () use ($waitGroup, $v) {
$cover = json_decode($v->images, true);
$v->cover = current($cover)['src'];
$model = AppArticle::find($v->id);
if ($model) {
$model->cover = current($cover)['src'];
preg_match('/([0-9]+)/', $model->title, $y);
$model->year = $y[1] ?? 0;
echo "update {$v->id} year: {$model->year} effect: {$model->update()}" . PHP_EOL;
}
$waitGroup->done();
});
}
$waitGroup->wait();
});
// var_dump(file_get_contents('https://www.vogue.com/fashion-shows/designer/AFKIR'));
}
}

184
app/Command/FooCommand.php Executable file
View File

@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Command\spider\VogueCommand;
use App\FormModel\spider\ReviewModel;
use App\Helpers\AppHelper;
use App\Helpers\TitleHelper;
use App\Model\AppArticle;
use App\Model\AppBrand;
use App\Model\AppSpiderArticle;
use Co\WaitGroup;
use GuzzleHttp\RequestOptions;
use Hyperf\Collection\Collection;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\Command\Annotation\Command;
use Hyperf\Context\ApplicationContext;
use Hyperf\Coroutine\Coroutine;
use Hyperf\DbConnection\Db;
use Hyperf\Logger\LoggerFactory;
use Laminas\Stdlib\ArrayUtils;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Spatie\Crawler\Crawler;
use Swoole\Timer;
use function Hyperf\Coroutine\co;
#[Command]
class FooCommand extends HyperfCommand
{
protected LoggerInterface $logger;
protected $reids;
public function __construct(protected ContainerInterface $container, LoggerFactory $loggerFactory)
{
parent::__construct('demo:command');
$this->logger = $loggerFactory->get('log', 'command');
}
public function configure()
{
parent::configure();
$this->setDescription('Hyperf Demo Command');
}
public function handle()
{
$this->line('Hello Hyperf!', 'info');
// Crawler::create([
// RequestOptions::TIMEOUT => 30.0, // 请求最大持续时间
// RequestOptions::CONNECT_TIMEOUT => 10.0, // 连接超时
// ])->setCrawlObserver(new TestClass())->startCrawling('https://theimpression.com/milan-street-style-fall-2025-day-6/');
$query = AppArticle::where('id', '>', 1);
// $model = new ReviewModel();
$map = [
// 'Fall 1996 Ready-to-Wear' => '/Fall ([0-9]*?) Ready-to-Wear/',
'/Fall ([0-9]*?) Ready-to-Wear/' => [
'trans' => '秋季成衣',
'style' => 0,
'location' => 0,
],
'/Spring ([0-9]*?) Ready-to-Wear/' => [
'trans' => '春季成衣',
'style' => 0,
'location' => 0,
],
'/Pre-Fall ([0-9]*?)/' => [
'trans' => '早秋',
'style' => 0,
'location' => 0,
],
'/Australia Resort ([0-9]*?)/' => [
'trans' => '时装',
'style' => 1,
'location' => 1,
],
'/Fall ([0-9]*?) Menswear/' => [
'trans' => '秋季男装',
'style' => 0,
'location' => 1, // 澳大利亚
],
'/Ukraine Fall ([0-9]*?)/' => [
'trans' => '秋季',
'style' => 0,
'location' => 2, // 乌克兰
],
'/Kiev Fall ([0-9]*?)/' => [
'trans' => '秋季',
'style' => 0,
'location' => 3, // 基辅
],
'/Stockholm Fall ([0-9]*?)/' => [
'trans' => '秋季',
'style' => 0,
'location' => 4, // 斯德哥尔摩
],
'/Tokyo Fall ([0-9]*?)/' => [
'trans' => '秋季',
'style' => 0,
'location' => 5, // 东京
],
'/Berlin Fall ([0-9]*?)/' => [
'trans' => '秋季',
'style' => 0,
'location' => 6, // 柏林
],
'/Copenhagen Fall ([0-9]*?)/' => [
'trans' => '秋季',
'style' => 0,
'location' => 7, // 哥本哈根
],
'/([0-9]*?) Spring Summer/' => [
'trans' => '春夏',
'style' => 0,
'location' => 0,
],
'/([0-9]*?) Autumn Winter/' => [
'trans' => '秋冬',
'style' => 0,
'location' => 0,
],
];
//
// var_dump(TitleHelper::translate('Pre-Fall 2019'));die;
foreach ($query->cursor() as $item) {
echo '正在同步' . $item->id . PHP_EOL;
if (in_array($item->title, ['春季', ' 春季', '秋季', ' 秋季', '早秋', ' 早秋', '时装', ' 时装']) || stripos($item->title, '|') !== false) {
$originTitle = AppSpiderArticle::find($item->spider_article_id)->title;
if ($title = TitleHelper::translate($originTitle)) {
var_dump($title);
$item->title = $title;
$item->save();
}
} else {
if ($title = TitleHelper::translate($item->title)) {
$item->title = $title;
$item->save();
}
}
// $model = AppArticle::find($item->id);
// $model->aid = strtr(uniqid(more_entropy:true), [
// '.' => ''
// ]);
//// $model->description = '1';
// $model->save();
// foreach ($map as $mapPreg => $mapItem) {
//
// preg_match_all($mapPreg, $item->title, $matches);
//
// if (count($matches) > 1 && $matches[1]) {
// echo current($matches[1]) . " {$mapItem['trans']}" . PHP_EOL;
// $model = AppArticle::find($item->id);
// $model->title = current($matches[1]) . " {$mapItem['trans']}";
// $model->location = $mapItem['location'];
// $model->style = $mapItem['style'];
//
// echo 'save';
// $model->save();
// continue;
// }
}
//
//// $model->pass($item->id);
////
////// var_dump($item->created_at->date);
////// $model->deleted_at = $item->created_at->timestamp;
////// var_dump($model->save());
// }
// Fall 1996 Ready-to-Wear
// Spring 2025 Ready-to-Wear
// Pre-Fall 2025
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\FormModel\spider\ReviewModel;
use App\Model\AppArticle;
use Hyperf\Collection\Collection;
use Hyperf\Command\Annotation\Command;
use Hyperf\Command\Command as HyperfCommand;
use Hyperf\DbConnection\Db;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function Hyperf\Coroutine\co;
#[Command]
class SpiderReviewCommand extends HyperfCommand
{
protected $reids;
public function __construct(protected ContainerInterface $container)
{
parent::__construct('spider:review');
}
public function configure()
{
parent::configure();
$this->setDescription('review spider article.');
$this->addOption('id', 'i', InputOption::VALUE_REQUIRED, '文章id', false);
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$reviewModel = new ReviewModel();
$reviewModel->pass($input->getOption('id'));
return 0;
}
}

26
app/Command/TestClass.php Executable file
View File

@ -0,0 +1,26 @@
<?php
namespace App\Command;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Spatie\Crawler\CrawlObservers\CrawlObserver;
class TestClass extends CrawlObserver
{
public function willCrawl(UriInterface $url, ?string $linkText): void
{
echo "即将爬取: {$url}\n";
}
public function crawled(
UriInterface $url, ResponseInterface $response, ?UriInterface $foundOnUrl = null, ?string $linkText = null): void {
var_dump($response);
echo "已成功爬取: {$url} 状态码: " . $response->getStatusCode() . "\n";
}
public function crawlFailed(UriInterface $url, \Throwable $exception, ?UriInterface $foundOnUrl = null, ?string $linkText = null): void
{
echo "爬取失败: {$url} 错误: {$exception->getMessage()}\n";
}
}

171
app/Command/spider/BaseSpider.php Executable file
View File

@ -0,0 +1,171 @@
<?php
namespace App\Command\spider;
use App\Model\AppArticle;
use App\Model\AppSpiderArticle;
use Hyperf\Command\Command;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Coroutine\Coroutine;
use Hyperf\Di\Annotation\Inject;
use Laminas\Stdlib\ArrayUtils;
use Swoole\Coroutine\Channel;
use Swoole\Timer;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function Hyperf\Coroutine\co;
class BaseSpider extends Command
{
/**
* 最大协程数量
* @var int
*/
protected int $maxCo = 10;
protected ?\Swoole\Coroutine\Channel $channel = null;
/**
* @var string
*/
protected string $baseUrl = '';
#[Inject]
protected ?StdoutLoggerInterface $logger = null;
protected array $coroutineList = [];
protected const PLATFORM = '';
private bool $isInit = false;
protected int|bool $timer = false;
protected array $commandConfigure = [];
private function init()
{
// 因为最外层还有个父协程, 所以加一
$this->channel = new Channel($this->maxCo + 1);
$this->timer = Timer::tick(1000 * 30, function () use (&$coList) {
// count(\Swoole\Coroutine::getElapsed());
var_dump(count($this->coroutineList));
// var_dump($list);
});
for ($i = 0; $i < $this->maxCo + 1; $i++) {
$this->channel->push(1);
}
}
public function configure()
{
parent::configure();
$this->addOption('prod', '', InputOption::VALUE_NEGATABLE, '是否关闭devMode.', false);
}
public static function getPlatform(): string
{
return static::PLATFORM;
}
public function getBaseUrl(): string
{
return rtrim($this->baseUrl, '/');
}
protected function getArticleModel(array $condition)
{
return AppSpiderArticle::query()->where($condition)->first() ?: new AppSpiderArticle();
}
protected function request(string $url): array
{
$ch = curl_init();
curl_setopt_array($ch, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 15,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'GET',
));
// curl_setopt($ch, CURLOPT_URL, $url);
// curl_setopt($ch, CURLOPT_HEADER, false);
// curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$result = curl_exec($ch);
curl_close($ch);
$httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);
return [$result, $httpCode];
}
protected function returnPool()
{
return $this->channel->push(1);
}
protected function getPool(): bool
{
return $this->channel->pop();
}
protected function createCoroutine(\Closure $func): void
{
if ($this->isInit === false) {
$this->isInit = true;
$this->init();
}
$this->getPool();
$cid = co(function () use ($func) {
\Co\defer(function() {
unset($this->coroutineList[Coroutine::id()]);
$this->returnPool();
});
$func();
});
$this->coroutineList[$cid] = 1;
}
protected function debugPrint(array|string $message = '', $level = 0)
{
if ($this->getCommandConfigure('prod') === false) {
$printTime = date('H:i:s');
echo "[spider-debug][$printTime]" . print_r($message, true) . PHP_EOL;
}
}
/**
* 用于单元测试
* @param string $methodName
* @param $args
* @return mixed
*/
public function testMethod(string $methodName, $args = [])
{
return $this->{$methodName}(...$args);
}
public function setCommandConfigure($options): void
{
$this->commandConfigure = $options;
}
public function getCommandConfigure($key = null, $defaultValue = null)
{
if (!$key) {
return $this->commandConfigure;
}
return $this->commandConfigure[$key] ?? $defaultValue;
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$this->setCommandConfigure($input->getOptions());
return 0;
}
}

View File

@ -0,0 +1,187 @@
<?php
namespace App\Command\spider;
use Hyperf\Command\Annotation\Command;
use Hyperf\DbConnection\Db;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function Swoole\Coroutine\run;
#[Command]
class ElleStreetCommand extends BaseSpider
{
/**
* @var string
*/
protected string $baseUrl = 'https://www.elle.com';
protected const PLATFORM = 'elle-street';
public function __construct(protected ContainerInterface $container, LoggerFactory $loggerFactory)
{
parent::__construct('spider:elle-street');
}
public function configure()
{
parent::configure();
$this->setDescription('elle.com/street elle街拍模块');
$this->addOption('brandId', 'b', InputOption::VALUE_OPTIONAL, '指定的品牌id', false);
}
public function execute(InputInterface $input, OutputInterface $output): int
{
run(function () {
$this->spiderStart();
});
return 0;
}
private function _getTask($brand): \Generator
{
$query = Db::table('app_brands');
if ($brand) {
$query->where(['id' => $brand]);
}
$query->where('id', '>', 1)->orderBy('id');
foreach ($query->cursor() as $row) {
yield $row;
}
}
private function _getTaskName($name): string
{
return strtolower(strtr($name, [
'.' => '-',
' ' => '-'
]));
}
public function spiderStart(): void
{
list($result, $httpCode) = $this->request($this->getBaseUrl() . '/fashion/street-style/');
preg_match_all('/<script id="json-ld" type="application\/ld\+json">([\s\S]*?)<\/script>/', $result, $matches);
if (!is_array($matches) && count($matches) < 1) {
$this->logger->info(self::getPlatform() . " 数据获取失败。");
return;
}
$val = json_decode(($matches[1][0]), true);
$articles = $val[0]['itemListElement'] ?? [];
if (!$articles) {
$this->logger->info(self::getPlatform() . " 文章数据获取失败。");
return;
}
$saveImages = [];
foreach ($articles as $article) {
list($result, $httpCode) = $this->request($article['url']);
preg_match_all('/<script id="json-ld" type="application\/ld\+json">([\s\S]*?)<\/script>/', $result, $matches);
if (isset($matches[1][0])) {
$val = json_decode($matches[1][0], true);
$images = $val['about']['itemListElement'];
foreach ($images as $image) {
$saveImages[] = $image['item']['image'];
}
}
}
var_dump($saveImages);
return;
$this->createCoroutine(function () use ($task) {
$brandName = $this->_getTaskName($task->name);
$url = $this->getBaseUrl() . '/fashion-shows/designer/' . $brandName;
$this->logger->info(sprintf("[Command] brandName: {$this->_getTaskName($task->name)}; spiderUrl: {$url}"));
// 取发布会列表
$showsList = $this->_getShowsList($url);
foreach ($showsList as $list) {
$this->createCoroutine(function () use ($task, $list) {
$this->_getDetail($task->id, $list);
});
}
});
}
private function _getShowsList($url)
{
list($request, $httpCode) = $this->request($url);
if ($httpCode == 200) {
preg_match_all('/window.__PRELOADED_STATE__ = ([\s\S]*?);<\/script>/', $request, $matches);
$val = json_decode(current(end($matches)), true);
return $val['transformed']['runwayDesignerContent']['designerCollections'] ?? [];
} else {
$this->logger->info('未找到数据.');
return [];
}
}
private function _getDetail(int $brandId, array $info)
{
$model = $this->getArticleModel(['brand' => $brandId, 'title' => $info['hed']]);
$model->title = $info['hed'];
$model->images = json_encode([]);
$model->platform = self::getPlatform();
// 获取图片
$pageUri = $info['url'];
$requestUrl = $this->getBaseUrl() . $pageUri . '/slideshow/collection';
$this->logger->info("正在匹配发布会详情 {$requestUrl}");
$matches = [];
list($result, $httpCode) = $this->request($requestUrl);
if ($httpCode != 200 || !$result) {
$this->logger->warning($requestUrl . '请求失败.');
return;
}
preg_match_all('/window\.__PRELOADED_STATE__ = (.*?);</s', $result, $matches);
$saveUrl = [];
if (count($matches) > 1) {
$val = json_decode(current($matches[1]), true);
$images = $val['transformed']['runwayGalleries']['galleries'][0]['items'] ?? false;
if ($images === false) {
$this->logger->warning($requestUrl . '获取图片失败.');
return;
}
foreach (is_array($images) ? $images : [] as $img) {
$saveUrl[] = [
'src' => $img['image']['sources']['xxl']['url']
];
}
$model->images = json_encode($saveUrl);
}
$model->save();
$this->logger->info("end: {$requestUrl}");
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace App\Command\spider;
use App\Helpers\AppHelper;
use App\Model\AppBrand;
use Hyperf\Command\Annotation\Command;
use Hyperf\Coroutine\Coroutine;
use Hyperf\DbConnection\Db;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Swoole\ExitException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function Swoole\Coroutine\run;
#[Command]
class FashionSnapCommand extends BaseSpider
{
protected const PLATFORM = 'fashionsnap';
protected string $baseUrl = 'https://www.fashionsnap.com';
public function __construct(protected ContainerInterface $container, LoggerFactory $loggerFactory)
{
parent::__construct('spider:fashionsnap');
}
public function configure()
{
parent::configure();
$this->setDescription('自动采集fashionsnap.com');
$this->addOption('brandId', 'b', InputOption::VALUE_OPTIONAL, '指定的品牌id', false);
}
private function _getTask($brand): \Generator
{
$query = Db::table('app_brands');
if ($brand) {
$query->whereIn('id', explode(',', $brand));
} else {
$query->where('spider_origin', '=', 'fashionsnap')->orderBy('id');
}
foreach ($query->cursor() as $row) {
if (!$row) {
throw new ExitException('END.');
}
yield $row;
}
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$brand = $input->getOption('brandId');
run(function () use ($brand) {
foreach ($this->_getTask($brand) as $task) {
list($result, $httpCode) = $this->request($this->getBaseUrl() . "/api/algolia/article/?blogIds=4&brandName={$task->name}&limit=50");
echo $task->name . '--' . $httpCode . PHP_EOL;
if ($httpCode == 200) {
$isSuccess = false;
$result = json_decode($result, true);
if ($result['totalCount'] == 0 || $result['totalCount'] > 200) {
continue;
}
foreach ($result['articles'] ?? [] as $item) {
$model = $this->getArticleModel(['title' => $item['mainCategory']['name'], 'platform' => static::PLATFORM, 'brand' => $task->id]);
$model->title = $item['mainCategory']['name'];
$model->year = AppHelper::getYear($model->title);
$model->brand = $task->id;
$model->module = 0;
$model->platform = self::getPlatform();
$saveImages = [];
foreach ($item['mainGalleryImages'] as $image) {
$saveImages[] = [
'src' => 'https://fashionsnap-assets.com/asset/width=4096' . $image
];
}
$model->images = json_encode($saveImages);
$model->cover = $saveImages[0]['src'] ?? '';
// permalink
$model->source_url = 'https://fashionsnap.com' . $item['permalink'];
if ($model->cover) {
$isSuccess = $model->save();
}
}
if ($isSuccess) {
$brandModel = AppBrand::find($task->id);
$brandModel->spider_origin = self::getPlatform();
$brandModel->save();
}
}
}
});
return 0;
}
}

View File

@ -0,0 +1,117 @@
<?php
namespace App\Command\spider;
use App\Enums\ArticleModuleEnum;
use App\Helpers\AppHelper;
use Hyperf\Command\Annotation\Command;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DomCrawler\Crawler;
#[Command]
class TheImpressionStreetCommand extends BaseSpider
{
protected const PLATFORM = 'theimpression-street';
public function __construct(protected ContainerInterface $container)
{
parent::__construct('spider:theimpression-street');
}
public function configure()
{
parent::configure();
$this->setDescription('自动采集 https://theimpression.com/street-style');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
parent::execute($input, $output);
$url = 'https://theimpression.com/street-style';
[$res, $httpCode] = $this->request($url);
if ($httpCode != 200) {
$this->debugPrint("{$url} 请求失败.");
return 0;
}
// 取banner的图
(new Crawler($res))
->filter('.parallax .mask-overlay')->each(function ($node) {
$href = $node->attr('href');
$text = trim($node->attr('aria-label'));
$this->debugPrint("标题: {$text}");
$this->debugPrint("链接: {$href}");
$this->getDetail($href, $text);
});
$articleList = [];
// 取前五十页
for ($i = 1; $i < 2; $i++) {
$url = "https://theimpression.com/wp-json/codetipi-zeen/v1/block?paged={$i}&type=1&data%5Bargs%5D%5Bcat%5D=1";
[$res, $httpCode] = $this->request($url);
if ($httpCode != 200) {
$this->debugPrint("{$url} 请求失败.");
return 0;
}
$res = json_decode($res, true);
(new Crawler($res[1]))
->filter('article')->each(function (Crawler $node) use (&$articleList) {
$href = $node->filter('.mask-img')->attr('href', '');
$title = $node->filter('.title-wrap')->text('');
if (!$href || !$title) {
$this->debugPrint("找不到标题或链接.");
return 0;
}
$this->getDetail($href, $title);
});
}
return 0;
// return 0;
}
public function getDetail(string $url, $title)
{
$model = $this->getArticleModel(['title' => $title, 'platform' => static::getPlatform(), 'brand' => 0]);
$model->title = $title;
$model->platform = static::getPlatform();
$model->module = ArticleModuleEnum::STREET->value;
$model->year = AppHelper::getYear($title);
[$res, $httpCode] = $this->request($url);
$model->source_url = $url;
if ($httpCode != 200) {
$this->debugPrint("{$url} 请求失败.");
return 0;
}
$images = [];
(new Crawler($res))
->filter('figure a img')->each(function ($node) use (&$images) {
if ($node->attr('src') && !isset($images[$node->attr('src')])) {
$this->debugPrint("采集图片: {$node->attr('src')}");
$images[$node->attr('src')] = [
'src' => $node->attr('src')
];
}
});
if ($images) {
$model->cover = current($images)['src'];
$model->images = json_encode(array_values($images));
$model->save();
}
}
}

View File

@ -0,0 +1,189 @@
<?php
namespace App\Command\spider;
use App\Helpers\AppHelper;
use App\Model\AppBrand;
use Hyperf\Command\Annotation\Command;
use Hyperf\Coroutine\Coroutine;
use Hyperf\DbConnection\Db;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function Swoole\Coroutine\run;
#[Command]
class VogueCommand extends BaseSpider
{
/**
* @var string
*/
protected string $baseUrl = 'https://www.vogue.com';
protected const PLATFORM = 'vogue';
public function __construct()
{
parent::__construct('spider:vogue');
ini_set('pcre.backtrack_limit', '-1');
}
public function configure()
{
parent::configure();
$this->setDescription('自动采集vogue.com');
$this->addOption('brandId', 'b', InputOption::VALUE_OPTIONAL, '指定的品牌id.', false);
$this->addOption('forceUpdate', 'f', InputOption::VALUE_NEGATABLE, '是否对已经保存的数据进行强制更新.', false);
$this->addOption('onlyPlatform', 'o', InputOption::VALUE_NEGATABLE, '是否只对当前平台品牌更新.', false);
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$this->setCommandConfigure($input->getOptions());
run(function () {
// 最大查询的品牌数量, 防止同时最大协程数都有子数据, 导致无法创建协程的问题。
$maxBrandExecuteCount = $this->maxCo / 2;
$currentBrandExecute = 0;
foreach ($this->_getTask() as $task) {
$currentBrandExecute++;
$this->createCoroutine(function () use ($task, &$currentBrandExecute) {
$this->spiderStart($task);
$currentBrandExecute--;
});
while (true) {
if ($currentBrandExecute > $maxBrandExecuteCount) {
Coroutine::sleep(1);
} else {
break;
}
}
}
Coroutine::sleep(60);
exit(0);
});
return 0;
}
private function _getTask(): \Generator
{
$query = AppBrand::query();
$brandId = $this->getCommandConfigure('brandId');
$onlyPlatform = $this->getCommandConfigure('onlyPlatform');
if ($brandId) {
$query->where(['id' => $brandId]);
} else {
$query->where('id', '>', 1)->when($onlyPlatform, fn($q) => $q->where('spider_origin', static::PLATFORM))->orderBy('id');
}
foreach ($query->cursor() as $row) {
yield $row;
}
}
protected function getTaskName($name): string
{
return strtolower(strtr($name, [
'.' => '-',
' ' => '-',
'&' => ''
]));
}
public function spiderStart($task): void
{
$brandName = $this->getTaskName($task->name);
$url = $this->getBaseUrl() . '/fashion-shows/designer/' . $brandName;
$this->logger->info(sprintf("[Command] brandName: {$this->getTaskName($task->name)}; spiderUrl: {$url}"));
// 取发布会列表
$showsList = $this->getShowsList($url);
foreach ($showsList as $list) {
$this->createCoroutine(function () use ($task, $list) {
$this->getDetail($task->id, $list);
});
}
}
protected function getShowsList($url)
{
list($request, $httpCode) = $this->request($url);
if ($httpCode == 200) {
preg_match_all('/window.__PRELOADED_STATE__ = ([\s\S]*?);<\/script>/', $request, $matches);
$val = json_decode(current(end($matches)), true);
return $val['transformed']['runwayDesignerContent']['designerCollections'] ?? [];
} else {
$this->logger->info('未找到数据.');
return [];
}
}
protected function getDetail(int $brandId, array $info)
{
$model = $this->getArticleModel(['brand' => $brandId, 'title' => $info['hed']]);
// 如果不是force update
// 不更新原来的数据
if ($model->id && $this->getCommandConfigure('forceUpdate') === false) {
return;
}
$model->title = $info['hed'];
$model->images = json_encode([]);
$model->platform = self::PLATFORM;
$model->brand = $brandId;
$model->module = 0;
$model->year = AppHelper::getYear($info['hed']);
// 获取图片
$pageUri = $info['url'];
$requestUrl = $this->getBaseUrl() . $pageUri . '/slideshow/collection';
$this->logger->info("正在匹配发布会详情 {$requestUrl}");
$model->source_url = $requestUrl;
$matches = [];
list($result, $httpCode) = $this->request($requestUrl);
if ($httpCode != 200 || !$result) {
$this->logger->warning($requestUrl . '请求失败.');
return;
}
preg_match_all('/window\.__PRELOADED_STATE__ = (.*?);</s', $result, $matches);
$saveUrl = $detailUrl = [];
if (count($matches) > 1) {
$val = json_decode(current($matches[1]), true);
$images = $val['transformed']['runwayGalleries']['galleries'][0]['items'] ?? false;
if ($images === false) {
$this->logger->warning($requestUrl . '获取图片失败.');
return;
}
foreach (is_array($images) ? $images : [] as $img) {
$saveUrl[] = [
'src' => $img['image']['sources']['xxl']['url']
];
foreach ($img['details'] ?? [] as $detail) {
$detailUrl[] = ['src' => $detail['image']['sources']['xxl']['url']];
}
}
$model->images = json_encode($saveUrl);
$model->cover = $saveUrl[0]['src'];
}
$model->save();
$this->logger->info("end: {$requestUrl}");
}
}

25
app/Constants/ErrorCode.php Executable file
View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Constants;
use Hyperf\Constants\AbstractConstants;
use Hyperf\Constants\Annotation\Constants;
#[Constants]
class ErrorCode extends AbstractConstants
{
/**
* @Message("Server Error")
*/
public const SERVER_ERROR = 500;
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Controller;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Container\ContainerInterface;
abstract class AbstractController
{
#[Inject]
protected ContainerInterface $container;
#[Inject]
protected RequestInterface $request;
#[Inject]
protected ResponseInterface $response;
}

17
app/Controller/Hicontroller.php Executable file
View File

@ -0,0 +1,17 @@
<?php
namespace App\Controller;
use Grpc\HiReply;
use Grpc\HiUser;
class Hicontroller extends AbstractController
{
public function sayHello(HiUser $user)
{
$message = new HiReply();
$message->setMessage("Hello World");
$message->setUser($user);
return $message;
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Controller;
use App\Helpers\ExcelHelper;
use App\Model\AppKeywordsMonitor;
use App\Model\AppKeywordsMonitorResult;
use App\Model\AppKeywordsMonitorTask;
use Hyperf\DbConnection\Db;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\View\RenderInterface;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
#[AutoController]
class IndexController extends AbstractController
{
public function index()
{
$user = $this->request->input('user', 'Hyperf');
$method = $this->request->getMethod();
return [
'method' => $method,
'message' => "Hello {$user}.",
];
}
public function test(RenderInterface $render)
{
return $render->render('index', ['name' => 'Hyperf']);
}
}

View File

@ -0,0 +1,159 @@
<?php
namespace App\Controller;
use App\Helpers\AppHelper;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Psr\Http\Message\RequestInterface;
use Qiniu\Auth;
use Qiniu\Storage\UploadManager;
use function Hyperf\Config\config;
#[Controller(prefix: 'upload')]
class UploadController extends AbstractController
{
/**
* @url common/uploadImage
* @return array
* @throws \Exception
*/
// #[RequestMapping(path: "upload-image", methods: "post")]
public function uploadImage(): array
{
$baseConfigKey = 'plugin.admin.upload.qiniu.';
// 需要填写你的 Access Key 和 Secret Key
$accessKey = config('upload.qiniu.access_key');
$secretKey = config('upload.qiniu.secret_key');
$bucket = config('upload.qiniu.bucket');
$file = $this->request->file('file');
// 构建鉴权对象
$auth = new Auth($accessKey, $secretKey);
$returnBody = '{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}';
$policy = array(
'returnBody' => $returnBody
);
// 生成上传 Token
$token = $auth->uploadToken($bucket, policy: $policy);
// 要上传文件的本地路径
// 上传到存储后保存的文件名
$filePath = $file->getRealPath();
$key = date('Y-m') . '/' . $file->getClientFilename();
// 初始化 UploadManager 对象并进行文件的上传。
$uploadMgr = new UploadManager();
// 调用 UploadManager 的 putFile 方法进行文件的上传。
list($ret, $err) = $uploadMgr->putFile($token, $key, $filePath, null, 'application/octet-stream', true, null, 'v2');
return [
'code' => 0,
'msg' => '上传成功@@',
'data' => [
'base_url' => AppHelper::getImageBaseUrl(),
'base_path' => $ret['key'],
'src' => AppHelper::getImageBaseUrl() . $ret['key'],
'name' => $ret['key'],
'size' => $ret['fsize'],
]
];
}
#[RequestMapping(path: "image", methods: "post")]
public function image()
{
$file = $file = $this->request->file('file');
if (!$file || $file->getError() !== UPLOAD_ERR_OK) {
return $this->response->json(['code' => 400, 'msg' => '文件上传失败']);
}
// 校验类型
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($file->getClientMediaType(), $allowedTypes)) {
return $this->response->json(['code' => 415, 'msg' => '不支持的图片格式']);
}
// 限制大小 (例如 5MB)
$maxSize = 5 * 1024 * 1024;
if ($file->getSize() > $maxSize) {
return $this->response->json(['code' => 413, 'msg' => '图片大小不能超过 5MB']);
}
// 保存路径
$filename = uniqid() . '.' . pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
$targetPath = BASE_PATH . '/uploads/' . $filename;
// 确保目录存在
if (!is_dir(dirname($targetPath))) {
mkdir(dirname($targetPath), 0777, true);
}
// 移动文件
$file->moveTo($targetPath);
// 构造返回 URL
$url = '/uploads/' . $filename;
return $this->response->json([
'errno' => 0,
'msg' => '上传成功!!',
'data' => [
'url' => 'http://' . '127.0.0.1:9503' . $url,
]
]);
}
/**
* 上传关键词监控页面
* @url upload/search-image
*/
#[RequestMapping(path: 'search-image', methods: 'post')]
public function uploadSearchImage()
{
$file = $file = $this->request->file('file');
if (!$file || $file->getError() !== UPLOAD_ERR_OK) {
return $this->response->json(['code' => 400, 'msg' => '文件上传失败']);
}
// 校验类型
// $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
// if (!in_array($file->getClientMediaType(), $allowedTypes)) {
// return $this->response->json(['code' => 415, 'msg' => '不支持的图片格式']);
// }
// 限制大小 (例如 5MB)
$maxSize = 5 * 1024 * 1024;
if ($file->getSize() > $maxSize) {
return $this->response->json(['code' => 413, 'msg' => '图片大小不能超过 5MB']);
}
// 保存路径
$filename = uniqid() . '.' . pathinfo($file->getClientFilename(), PATHINFO_EXTENSION);
$date = date('m-d');
$targetPath = BASE_PATH . '/uploads/' . $filename;
// 确保目录存在
if (!is_dir(dirname($targetPath))) {
mkdir(dirname($targetPath), 0777, true);
}
// 移动文件
$file->moveTo($targetPath);
// 构造返回 URL
$url = '/uploads/' . $filename;
return $this->response->json([
'errno' => 0,
'msg' => '上传成功~~',
'data' => [
'url' => 'http://' . '127.0.0.1:9503' . $url,
'file_name' => $url
]
]);
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace App\Controller\admin;
use App\Controller\AbstractController;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller]
class ConfigController extends AbstractController
{
#[RequestMapping(path: '', methods: 'get')]
public function index()
{
return '{
"logo": {
"title": "舆情管理后台",
"image": "/app/admin/admin/images/logo.png"
},
"menu": {
"data": "/admin/menu-config",
"method": "GET",
"accordion": true,
"collapse": false,
"control": false,
"controlWidth": 2000,
"select": "0",
"async": true
},
"tab": {
"enable": false,
"keepState": true,
"session": true,
"preload": false,
"max": "30",
"index": {
"id": "0",
"href": "\/admin\/dashboard",
"title": "仪表盘"
}
},
"theme": {
"defaultColor": "2",
"defaultMenu": "light-theme",
"defaultHeader": "light-theme",
"allowCustom": true,
"banner": false
},
"colors": [
{
"id": "1",
"color": "#36b368",
"second": "#f0f9eb"
},
{
"id": "2",
"color": "#2d8cf0",
"second": "#ecf5ff"
},
{
"id": "3",
"color": "#f6ad55",
"second": "#fdf6ec"
},
{
"id": "4",
"color": "#f56c6c",
"second": "#fef0f0"
},
{
"id": "5",
"color": "#3963bc",
"second": "#ecf5ff"
}
],
"other": {
"keepLoad": "500",
"autoHead": false,
"footer": false
},
"header": {
"message": false
}
}';
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Controller\admin;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\View\RenderInterface;
#[Controller]
class DashboardController
{
#[RequestMapping(path: '', methods: 'get')]
public function index(RenderInterface $render)
{
return $render->render('dashboard');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Controller\admin;
use App\Controller\AbstractController;
use App\Helpers\TreeHelper;
use App\Model\AppAdminMenu;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\View\RenderInterface;
#[Controller(prefix: 'admin')]
class IndexController extends AbstractController
{
#[RequestMapping(path: '', methods: 'get')]
public function index(RenderInterface $render)
{
return $render->render('admin');
}
#[RequestMapping(path: 'menu-config', methods: 'get')]
public function menuConfig()
{
$items = AppAdminMenu::orderBy('weight', 'DESC')->get()->toArray();
$formatted_items = [];
foreach ($items as $item) {
$item['pid'] = (int)$item['pid'];
$item['name'] = $item['title'];
$item['value'] = $item['id'];
$item['icon'] = $item['icon'] ? "layui-icon {$item['icon']}" : '';
$formatted_items[] = $item;
}
return [
'code' => 0,
'data' => TreeHelper::getTree($formatted_items)
];
return $data;
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Controller\admin;
use App\Controller\AbstractController;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\View\Render;
use Psr\Http\Message\ResponseInterface;
#[Controller(prefix: 'admin/keywords')]
class KeywordsController extends AbstractController
{
/**
* 舆情关键词列表
* @url /admin/keywords/monitor
*/
#[RequestMapping(path: 'monitor', methods: 'get')]
public function monitor(Render $render): \Psr\Http\Message\ResponseInterface
{
return $render->render('keywords/index');
}
/**
* 新增舆情监控关键词页面
* @url /admin/keywords/monitor/insert
* @param Render $render
* @return ResponseInterface
*/
#[RequestMapping(path: 'monitor/insert', methods: 'get')]
public function monitorInsert(Render $render): \Psr\Http\Message\ResponseInterface
{
return $render->render('keywords/insert');
}
/**
* 编辑舆情监控关键词页面
* @url /admin/keywords/monitor/view
* @param Render $render
* @return ResponseInterface
*/
#[RequestMapping(path: 'monitor/view', methods: 'get')]
public function monitorUpdate(Render $render): \Psr\Http\Message\ResponseInterface
{
return $render->render('keywords/view');
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Controller\admin;
use App\Controller\AbstractController;
use App\Helpers\TreeHelper;
use App\Model\AppAdminMenu;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\View\RenderInterface;
use plugin\admin\app\common\Tree;
#[Controller(prefix: 'admin/menu')]
class MenuController extends AbstractController
{
#[RequestMapping(path: '', methods: 'get')]
public function index(RenderInterface $render)
{
return $render->render('menu/index');
}
#[RequestMapping(path: 'insert', methods: 'get')]
public function insert(RenderInterface $render)
{
return $render->render('menu/insert');
}
#[RequestMapping(path: 'update', methods: 'get')]
public function update(RenderInterface $render)
{
return $render->render('menu/update');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Controller\admin;
use App\Controller\AbstractController;
use App\Model\AppNews;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\View\RenderInterface;
#[Controller(prefix: 'admin/news')]
class NewsController extends AbstractController
{
#[RequestMapping(path: '', methods: 'get')]
public function index(RenderInterface $render): \Psr\Http\Message\ResponseInterface
{
return $render->render('news/index');
}
/**
* 查看新闻详情
* @url /admin/news/view
*/
#[RequestMapping(path: 'view', methods: 'get')]
public function view(RenderInterface $render): \Psr\Http\Message\ResponseInterface
{
$id = $this->request->query('id');
$query = AppNews::where('id', $id)->first()->toArray();
return $render->render('news/view');
}
/**
* 新增新闻页面
* @url /admin/news/insert
*/
#[RequestMapping(path: 'insert', methods: 'get')]
public function insert(RenderInterface $render): \Psr\Http\Message\ResponseInterface
{
return $render->render('news/insert');
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Controller\admin;
use App\Controller\AbstractController;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\View\RenderInterface;
#[Controller(prefix: 'admin/spider-article')]
class SpiderArticleController extends AbstractController
{
#[RequestMapping(path: '', methods: 'get')]
public function index(RenderInterface $render): \Psr\Http\Message\ResponseInterface
{
return $render->render('spider-article/index');
}
#[RequestMapping(path: 'view', methods: 'get')]
public function view(RenderInterface $render): \Psr\Http\Message\ResponseInterface
{
return $render->render('spider-article/view');
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace App\Controller\admin\api;
use App\Controller\AbstractController;
use App\Enums\ArticlePublishedStatusEnum;
use App\FormModel\admin\articles\ModifyModel;
use App\Helpers\AppHelper;
use App\Model\AppArticle;
use App\Model\AppBrand;
use App\Model\AppNews;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller(prefix: 'admin/api')]
class ArticleController extends AbstractController
{
/**
* 文章列表
* @url /admin/api/article
* @return array
*/
#[RequestMapping(path:'articles', methods: 'get')]
public function index(): array
{
$publishedStatus = $this->request->query('published_status', '');
$createdFilter = $this->request->query('created_at', [date('Y-m-d', strtotime('-1 month')), date('Y-m-d', time())]);
foreach ($createdFilter as $index => &$item) {
if ($index == 0) {
$item = strtotime($item . ' 00:00:00');
}
if ($index == 1) {
$item = strtotime($item . ' 23:59:59');
}
}
$query = AppNews::formatQuery(['module', 'published_status', 'location'])
->select(['id'])
->when($publishedStatus !== '', fn($q) => $q->where('published_status', $publishedStatus))
->whereBetween('created_at', $createdFilter)
->orderBy('created_at', 'desc');
$pagination = $query->paginate($this->request->input('limit', 10), page: $this->request->input('page'));
foreach ($pagination->items() as $item) {
$ids[] = $item->id;
}
$value = AppNews::find($ids, ['aid', 'title', 'module', 'published_status', 'location'])->toArray();
return ['code' => 0, 'msg' => 'ok', 'count' => $pagination->total(), 'data' => $value];
}
// /**
// * 新增/编辑文章
// * @url /api/v1/articles/save
// * @return array
// */
// #[RequestMapping(path:'save', methods: 'post')]
// public function save(): array
// {
// $id = $this->request->input('id');
// $model = new ArticlesModel();
// if ($id) {
// $model->edit();
// } else {
// $model->create();
// }
//
// return [
// 'code' => 0,
// 'message' => 'ok'
// ];
// }
#[RequestMapping(path:'brand-search', methods: 'get')]
public function brandSearch(): array
{
$input = $this->request->input('brand');
$query = AppBrand::query()->select(['name', 'cn_name', 'id'])->where('name', 'like', "%$input%")
->orWhere('cn_name', 'like', "%$input%")
->get()->toArray();
return ['code' => 0, 'msg' => 'ok', 'data' => $query];
}
#[RequestMapping(path:'view', methods: 'get')]
public function view(): \Psr\Http\Message\ResponseInterface
{
$query = AppNews::formatQuery(['images', 'brand_name', 'translate_title'])
->select(['brand as brand_name', 'brand', 'images', 'id', 'title', 'title as translate_title', 'cover', 'aid', 'location'])
->where('aid', $this->request->query('id'));
return $this->response->json(['code' => 0, 'msg' => 'ok', 'data' => $query->first()]);
}
#[RequestMapping(path:'publish', methods: 'post')]
public function publish()
{
$query = AppArticle::where('aid', $this->request->post('aid'))->first();
$query->published_status = ArticlePublishedStatusEnum::TRUE->value;
$query->published_at = time();
$query->save();
return $this->response->json(['code' => 0, 'msg' => 'ok']);
}
#[RequestMapping(path:'update', methods: 'post')]
public function update()
{
$model = new ModifyModel();
$model->setAttributes($this->request->post());
$model->update();
return $this->response->json(['code' => 0, 'msg' => 'ok']);
}
}

View File

@ -0,0 +1,188 @@
<?php
namespace App\Controller\admin\api;
use App\Controller\AbstractController;
use App\Helpers\ExcelHelper;
use App\Model\AppKeywordsMonitor;
use App\Model\AppKeywordsMonitorResult;
use App\Model\AppKeywordsMonitorTask;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\View\RenderInterface;
use Laminas\Stdlib\ArrayUtils;
#[Controller(prefix: 'admin/api/keywords')]
class KeywordsController extends AbstractController
{
/**
* 列表数据
* @url /admin/api/keywords/monitor
* @return \Psr\Http\Message\ResponseInterface
*/
#[RequestMapping(path: 'monitor', methods: 'get')]
public function monitor(): \Psr\Http\Message\ResponseInterface
{
$ids = [];
$query = AppKeywordsMonitor::query()
->select(['id'])
->where('is_delete', 0)
->orderBy('id', 'desc');
$pagination = $query->paginate($this->request->input('limit', 10), page: $this->request->input('page'));
foreach ($pagination->items() as $item) {
$ids[] = $item->id;
}
$value = AppKeywordsMonitor::query()->whereIn('id', $ids)->orderBy('id', 'desc')->get()->toArray();
return $this->response->json(['code' => 0, 'msg' => 'ok', 'count' => $pagination->total(), 'data' => $value]);
}
/**
* 新增关键词
* @url /admin/api/keywords/monitor/insert
* @return \Psr\Http\Message\ResponseInterface
*/
#[RequestMapping(path: 'monitor/insert', methods: 'post')]
public function monitorInsert(): \Psr\Http\Message\ResponseInterface
{
$keyword = $this->request->post('keyword');
$query = AppKeywordsMonitor::query()->where([
['keyword', $keyword],
['is_delete', 0]
])->get()->toArray();
if ($query) {
return $this->response->json(['code' => 400, 'msg' => '已重复添加']);
}
$query = new AppKeywordsMonitor();
$query->keyword = $keyword;
$query->save();
foreach ($this->request->post('platform', []) as $platform) {
$task = new AppKeywordsMonitorTask();
$task->keyword = $keyword;
$task->aid = $query->id;
$task->platform = $platform;
$task->save();
}
return $this->response->json(['code' => 0, 'msg' => 'ok']);
}
/**
* 查看关键词
* @url /admin/api/keywords/monitor/view
* @return \Psr\Http\Message\ResponseInterface
*/
#[RequestMapping(path: 'monitor/view', methods: 'get')]
public function monitorView(): \Psr\Http\Message\ResponseInterface
{
$id = $this->request->input('id');
$query = AppKeywordsMonitor::query()->where(['id' => $id])->first()->toArray();
if (!$query) {
return $this->response->json(['code' => 400, 'msg' => 'id 有误']);
}
$query['platform'] = AppKeywordsMonitorTask::query()->select(['platform'])
->where([
['aid', $query['id']],
['is_delete', 0],
])
->get()?->pluck('platform');
return $this->response->json(['code' => 0, 'msg' => 'ok', 'data' => $query]);
}
/**
* 编辑关键词
* @url /admin/api/keywords/monitor/save
* @return \Psr\Http\Message\ResponseInterface
*/
#[RequestMapping(path: 'monitor/save', methods: 'post')]
public function monitorSave(): \Psr\Http\Message\ResponseInterface
{
$id = $this->request->post('id');
$keyword = $this->request->post('keyword');
$platform = $this->request->post('platform', []);
$query = AppKeywordsMonitor::find($id);
if (!$query) {
return $this->response->json(['code' => 400, 'msg' => 'id 有误']);
}
$query->keyword = $keyword;
$query->save();
// 先全部删掉
AppKeywordsMonitorTask::query()->where(['aid' => $id])->update(['is_delete' => 1]);
foreach ($platform as $platformItem) {
$query = AppKeywordsMonitorTask::query()->where('aid', $id)->first();
if ($query) {
$query->is_delete = 0;
$query->save();
} else {
$query = new AppKeywordsMonitorTask();
$query->platform = $platformItem;
$query->aid = $id;
$query->keyword = $keyword;
$query->save();
}
}
return $this->response->json(['code' => 0, 'msg' => 'ok', 'data' => $query]);
}
/**
* 删除关键词
* @url /admin/api/keywords/monitor/delete
* @return \Psr\Http\Message\ResponseInterface
*/
#[RequestMapping(path: 'monitor/delete', methods: 'post')]
public function monitorDelete(): \Psr\Http\Message\ResponseInterface
{
$id = $this->request->post('id');
$query = AppKeywordsMonitor::find($id);
if (!$query) {
return $this->response->json(['code' => 400, 'msg' => 'id 有误']);
}
$query->is_delete = 1;
$query->save();
AppKeywordsMonitorTask::query()->where(['aid' => $id])->update(['is_delete' => 1]);
return $this->response->json(['code' => 0, 'msg' => 'ok', 'data' => $query]);
}
/**
* 导出关键词报表
* @url /admin/api/keywords/monitor/export-all
* @return \Psr\Http\Message\ResponseInterface
*/
#[RequestMapping(path: 'monitor/export-all', methods: 'get')]
public function monitorExportAll(): \Psr\Http\Message\ResponseInterface
{
$res = AppKeywordsMonitorResult::query()->orderBy('aid', 'desc')->get()->toArray();
foreach ($res as &$v) {
$v['keyword'] = AppKeywordsMonitorTask::find($v['aid'])->keyword;
$v['screen_path'] = 'http://127..0.0.1:9503' . $v['screen_path'];
}
$fileName = date('Y-m-d') . '关键词监控结果';
return ExcelHelper::exportData($this->response, list: $res, header: [
['关键词', 'keyword', 'text'],
['标题', 'title', 'text'],
['排名', 'order', 'text'],
['链接地址', 'url', 'text'],
['ip归属地', 'ip_source', 'text'],
['截图地址', 'screen_path', 'text'],
], filename:$fileName);
return [];
}
}

View File

@ -0,0 +1,101 @@
<?php
namespace App\Controller\admin\api;
use App\Controller\AbstractController;
use App\Helpers\AppHelper;
use App\Helpers\TreeHelper;
use App\Model\AppAdminMenu;
use App\Model\AppArticle;
use App\Model\AppBrand;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Psr\Http\Message\ResponseInterface;
#[Controller(prefix: 'admin/api/menu')]
class MenuController extends AbstractController
{
#[RequestMapping(path:'', methods: 'get')]
public function index(): array
{
$request = $this->request;
$query = AppAdminMenu::query()->orderBy('id');
$pagination = $query->paginate($request->input('limit', 10), page: $request->input('page'));
$data = $query->get()->toArray();
// $data['cover'] = AppHelper::setImageUrl($query['cover']);
// foreach ($data as &$v) {
// $v['cover'] = AppHelper::setImageUrl($v['cover']);
// }
return ['code' => 0, 'msg' => 'ok', 'count' => $pagination->total(), 'data' => $data];
}
#[RequestMapping(path:'list', methods: 'get')]
public function list(): array
{
$items = AppAdminMenu::query()->whereIn('type', [0, 1])->orderBy('weight', 'DESC')->get()->toArray();
$formatted_items = [];
foreach ($items as $item) {
$item['pid'] = (int)$item['pid'];
$item['name'] = $item['title'];
$item['value'] = $item['id'];
$item['icon'] = $item['icon'] ? "layui-icon {$item['icon']}" : '';
$formatted_items[] = $item;
}
return [
'code' => 0,
'data' => TreeHelper::getTree($formatted_items)
];
}
/**
* 新增菜单
* @url /admin/api/menu/insert
* @return ResponseInterface
*/
#[RequestMapping(path: 'insert', methods: 'post')]
public function insert(): \Psr\Http\Message\ResponseInterface
{
$model = new AppAdminMenu();
$model->setRawAttributes($this->request->post());
$model->pid = $model->pid ?: 0;
$model->save();
return $this->response->json([
'code' => 0,
'msg' => 'ok'
]);
}
/**
* 编辑菜单数据
* @return ResponseInterface
*/
#[RequestMapping(path: 'update', methods: 'post')]
public function update(): \Psr\Http\Message\ResponseInterface
{
$model = AppAdminMenu::find($this->request->post('id'));
$model->setRawAttributes($this->request->post());
$model->save();
return $this->response->json([
'code' => 0,
'msg' => 'ok'
]);
}
/**
* 预览菜单数据
* @return ResponseInterface
*/
#[RequestMapping(path: 'view', methods: 'get')]
public function view(): ResponseInterface
{
$id = $this->request->input('id');
$model = AppAdminMenu::find($id)->toArray();
return $this->response->json([
'code' => 0,
'data' => $model,
'msg' => 'ok'
]);
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Controller\admin\api;
use App\Controller\AbstractController;
use App\Enums\ArticlePublishedStatusEnum;
use App\FormModel\admin\articles\ModifyModel;
use App\FormModel\admin\news\NewsFormModel;
use App\Helpers\AppHelper;
use App\Model\AppArticle;
use App\Model\AppBrand;
use App\Model\AppNews;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller(prefix: 'admin/api/news')]
class NewsController extends AbstractController
{
/**
* 文章列表
* @url /admin/api/news
* @return array
*/
#[RequestMapping(path:'', methods: 'get')]
public function index(): array
{
$createdFilter = $this->request->query('created_at', [date('Y-m-d', strtotime('-1 month')), date('Y-m-d', time())]);
foreach ($createdFilter as $index => &$item) {
if ($index == 0) {
$item = strtotime($item . ' 00:00:00');
}
if ($index == 1) {
$item = strtotime($item . ' 23:59:59');
}
}
$ids = [];
$query = AppNews::query()
->select(['id'])
->whereBetween('created_at', $createdFilter)
->orderBy('id', 'desc');
$pagination = $query->paginate($this->request->input('limit', 10), page: $this->request->input('page'));
foreach ($pagination->items() as $item) {
$ids[] = $item->id;
}
$value = AppNews::query()->whereIn('id', $ids)->orderBy('id', 'desc')->get()->toArray();
// $value = AppNews::find($ids, ['title', 'is_record'])->toArray();
return ['code' => 0, 'msg' => 'ok', 'count' => $pagination->total(), 'data' => $value];
}
/**
* 查看文章详情信息
* @url /admin/api/news/view
*/
#[RequestMapping(path:'view', methods: 'get')]
public function view(): \Psr\Http\Message\ResponseInterface
{
$query = AppNews::query()
->where('id', $this->request->query('id'));
return $this->response->json(['code' => 0, 'msg' => 'ok', 'data' => $query->first()]);
}
/**
* 更新文章内容
* @url /admin/api/news/update
*/
#[RequestMapping(path:'update', methods: 'post')]
public function update()
{
$model = new NewsFormModel();
$model->setAttributes($this->request->post());
$model->update();
return $this->response->json(['code' => 0, 'msg' => 'ok']);
}
/**
* 新增新新闻接口
* @url /admin/api/news/insert
*/
#[RequestMapping(path:'insert', methods: 'post')]
public function insert()
{
$model = new NewsFormModel();
$model->setAttributes($this->request->post(), ['title', 'keywords', 'description', 'cover', 'content']);
$model->insert();
return $this->response->json(['code' => 0, 'msg' => 'ok']);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Controller\admin\api;
use App\Controller\AbstractController;
use App\Model\AppAdminMenu;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller(prefix: 'admin/api/permission')]
class PermissionController extends AbstractController
{
#[RequestMapping(path:'', methods: 'get')]
public function index(): array
{
return ['code' => 0, 'msg' => 'ok', 'data' => ['*']];
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Controller\admin\api;
use App\Controller\AbstractController;
use App\Enums\SpiderArticlePublishedStatusEnum;
use App\FormModel\spiderArticle\ReviewModel;
use App\Model\AppSpiderArticle;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller(prefix: 'admin/api/spider-article')]
class SpiderArticleController extends AbstractController
{
#[RequestMapping(path: '', methods: 'get')]
public function index(): array
{
$createdFilter = $this->request->query('created_at', [date('Y-m-d', strtotime('-1 month')), date('Y-m-d', time())]);
foreach ($createdFilter as $index => &$item) {
if ($index == 0) {
$item = strtotime($item . ' 00:00:00');
}
if ($index == 1) {
$item = strtotime($item . ' 23:59:59');
}
}
$titleFilter = $this->request->query('title');
$publishedStatus = $this->request->query('published_status', SpiderArticlePublishedStatusEnum::FALSE);
$request = $this->request;
$query = AppSpiderArticle::formatQuery(['created_at', 'module', 'published_status'])
->select(['id', 'title', 'created_at', 'module', 'source_url', 'published_status'])
->when($publishedStatus !== null, fn($q) => $q->where('published_status', $publishedStatus))
->when($titleFilter, fn($q) => $q->where('title', 'like', "%{$titleFilter}%"))
->when($this->request->query('module', '') !== '', fn($q) => $q->where('module', $this->request->query('module')))
->whereBetween('created_at', $createdFilter)
->orderBy('created_at', 'desc');
$pagination = $query->paginate($request->input('limit', 10), page: $request->input('page'));
return ['code' => 0, 'msg' => 'ok', 'count' => $pagination->total(), 'data' => $pagination->items()];
}
#[RequestMapping(path: 'view', methods: 'get')]
public function view(): \Psr\Http\Message\ResponseInterface
{
$query = AppSpiderArticle::formatQuery(['images', 'module', 'brand'])->find($this->request->query('id'));
return $this->response->json(['code' => 0, 'msg' => 'ok', 'data' => $query->toArray()]);
}
#[RequestMapping(path: 'pre-publish', methods: 'post')]
public function prePublish(): \Psr\Http\Message\ResponseInterface
{
$prePublishModel = new ReviewModel();
$prePublishModel->prePublish($this->request->post('id'));
return $this->response->json(['code' => 0, 'msg' => 'ok']);
}
}

View File

@ -0,0 +1,118 @@
<?php
namespace App\Controller\api\expose\v1;
use App\Controller\AbstractController;
use App\Model\AppKeywordsMonitor;
use App\Model\AppKeywordsMonitorResult;
use App\Model\AppKeywordsMonitorTask;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller(prefix: "/api/expose/v1/tools")]
class ToolsController extends AbstractController
{
/**
* 获取关键词监控的关键词
* @url /api/expose/v1/tools/get-query-keyword
* @return \Psr\Http\Message\ResponseInterface
*/
#[RequestMapping(path:'get-query-keyword', methods:'get')]
public function getQueryKeyword(): \Psr\Http\Message\ResponseInterface
{
$keyword = AppKeywordsMonitorTask::formatQuery(['created_at'])
->where('queried_at', '<=', strtotime('-2 hours'))
->where('is_delete', 0);
if ($keyword) {
$query = $keyword->first()->toArray();
return $this->response->json([
'code' => 0,
'data' => $query
]);
}
return $this->response->json([
'code' => 0,
'data' => ''
]);
}
/**
* 提交监控数据
* @url /api/expose/v1/tools/submit-query-keyword
* @return \Psr\Http\Message\ResponseInterface
*/
#[RequestMapping(path:'submit-query-keyword', methods:'post')]
public function submitQueryKeyword(): \Psr\Http\Message\ResponseInterface
{
$requestData = $this->request->post();
if (!isset($requestData['length']) || !isset($requestData['keyword_id']) || !$requestData['keyword_id'] || !$requestData['image_name'] || !$requestData['ip_info']) {
return $this->response->json([
'code' => 0,
]);
}
// 被查的关键词id
$aid = $requestData['keyword_id'];
foreach ($requestData['items'] ?? [] as $key => $value) {
// 非负面不存储
if (!$value['matched'] || !$value['mu']) {
continue;
}
$query = AppKeywordsMonitorResult::query()->where([
['aid', $aid],
['url', $value['mu']],
['is_delete', 0],
])->first()?->toArray();
// 如果这个负面存过了就不管了
if ($query) {
continue;
}
list($title, $desc) = call_user_func(function () use ($value) {
$content = $value['content'];
$ex = explode(PHP_EOL . PHP_EOL, $content);
if (count($ex) > 1) {
return [$ex[0], $ex[1]];
}
return ['', ''];
});
if (!$title || !$desc) {
continue;
}
$query = new AppKeywordsMonitorResult();
$query->aid = $aid;
$query->title = $title;
$query->description = $desc;
$query->url = $value['mu'];
$query->order = $value['id'];
$ipInfo = json_decode($requestData['ip_info'], true);
$query->ip_address = $ipInfo['ip'];
$query->ip_source = "{$ipInfo['country']}{$ipInfo['province']}{$ipInfo['city']}";
$query->screen_path = $requestData['image_name'];
$query->save();
}
// 更新关键词最后查询时间
$query = AppKeywordsMonitorTask::find($aid);
$query->queried_at = time();
$query->save();
// 更新关键词最后查询时间
$query = AppKeywordsMonitor::query()->where('id', $query->aid)->first();
$query->queried_at = time();
$query->save();
return $this->response->json([]);
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Controller\api\v1;
use App\Controller\AbstractController;
use App\FormModel\api\v1\ArticlesModel;
use App\Helpers\AppHelper;
use App\Model\AppArticle;
use App\Model\AppBrand;
use Hyperf\Collection\Collection;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller]
class ArticlesController extends AbstractController
{
/**
* 文章列表
* @url /api/v1/articles
* @return array
*/
#[RequestMapping(path:'', methods: 'get')]
public function index(): array
{
$request = $this->request;
$query = Model::query()->orderBy('id');
$pagination = $query->paginate($request->input('limit', 10), page: $request->input('page'));
$data = $query->get()->toArray();
// $data['cover'] = AppHelper::setImageUrl($query['cover']);
foreach ($data as &$v) {
$v['cover'] = AppHelper::setImageUrl($v['cover']);
}
return ['code' => 0, 'msg' => 'ok', 'count' => $pagination->total(), 'data' => $data];
}
/**
* 新增/编辑文章
* @url /api/v1/articles/save
* @return array
*/
#[RequestMapping(path:'save', methods: 'post')]
public function save(): array
{
$id = $this->request->input('id');
$model = new ArticlesModel();
if ($id) {
$model->edit();
} else {
$model->create();
}
return [
'code' => 0,
'message' => 'ok'
];
}
#[RequestMapping(path:'brand-search', methods: 'get')]
public function brandSearch(): array
{
$input = $this->request->input('brand');
$query = Model::query()->select(['name', 'cn_name', 'id'])->where('name', 'like', "%$input%")
->orWhere('cn_name', 'like', "%$input%")
->get()->toArray();
return ['code' => 0, 'msg' => 'ok', 'data' => $query];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Controller\api\v1;
use App\Controller\AbstractController;
use App\FormModel\api\v1\UserModel;
use App\Request\Test;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
#[Controller]
class UserController extends AbstractController
{
#[RequestMapping(path: '/api/v1/user/register', methods: 'post')]
public function register(Test $validator)
{
$validator = $validator->validated();
$model = new UserModel();
$model->register();
return [];
}
public function login()
{
}
public function logout()
{
}
}

16
app/Enums/ArticleModuleEnum.php Executable file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Enums;
enum ArticleModuleEnum: int
{
case SHOW = 0;
case STREET = 1;
public function toString(): string {
return match($this) {
ArticleModuleEnum::SHOW => '秀场',
ArticleModuleEnum::STREET => '街拍',
};
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Enums;
enum ArticlePublishedStatusEnum: int
{
case FALSE = 0;
case TRUE = 1;
case DELETE = 2;
public function toString(): string {
return match($this) {
ArticlePublishedStatusEnum::TRUE => '已发布',
ArticlePublishedStatusEnum::FALSE => '未发布',
ArticlePublishedStatusEnum::DELETE => '已删除',
};
}
}

12
app/Enums/ArticleStyleEnum.php Executable file
View File

@ -0,0 +1,12 @@
<?php
namespace App\Enums;
enum ArticleStyleEnum: int
{
case NULL = 0;
case RESORT = 1;
case BRIDAL = 2;
}

69
app/Enums/LocationEnum.php Executable file
View File

@ -0,0 +1,69 @@
<?php
namespace App\Enums;
enum LocationEnum: int
{
case NULL = 0;
case AUSTRALIA = 1;
case UKRAINE = 2;
case KIEV = 3;
case STOCKHOLM = 4;
case TOKYO = 5;
case BERLIN = 6;
case COPENHAGEN = 7;
case SHANGHAI = 8;
case SAO_PAULO = 9;
case TBILISI = 10;
case MEXICO = 11;
case SEOUL = 12;
case RUSSIA = 13;
case MADRID = 14;
case SPAIN= 15;
case ISTANBUL = 16;
case LAGOS = 17;
case PARIS = 18;
public function toString(): string {
return match($this) {
LocationEnum::NULL => '',
LocationEnum::AUSTRALIA => '澳洲',
LocationEnum::UKRAINE => '乌克兰',
LocationEnum::KIEV => '基辅',
LocationEnum::STOCKHOLM => '斯德哥尔摩',
LocationEnum::TOKYO => '东京',
LocationEnum::BERLIN => '柏林',
LocationEnum::COPENHAGEN => '哥本哈根',
LocationEnum::SHANGHAI => '上海',
LocationEnum::SAO_PAULO => '圣保罗',
LocationEnum::TBILISI => '首都',
LocationEnum::MEXICO => '墨西哥',
LocationEnum::SEOUL => '首尔',
LocationEnum::RUSSIA => '俄罗斯',
LocationEnum::MADRID => '马德里',
LocationEnum::SPAIN => '西班牙',
LocationEnum::ISTANBUL => '伊斯坦布尔',
LocationEnum::LAGOS => '拉各斯',
LocationEnum::PARIS => '巴黎',
};
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Enums;
enum SpiderArticlePublishedStatusEnum: int
{
case FALSE = 0;
case TRUE = 1;
case DELETE = 2;
public function toString(): string {
return match($this) {
SpiderArticlePublishedStatusEnum::TRUE => '已同步',
SpiderArticlePublishedStatusEnum::FALSE => '未同步',
SpiderArticlePublishedStatusEnum::DELETE => '已删除',
};
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Exception;
use App\Constants\ErrorCode;
use Hyperf\Server\Exception\ServerException;
use Throwable;
class BusinessException extends ServerException
{
public function __construct(int $code = 0, string $message = null, Throwable $previous = null)
{
if (is_null($message)) {
$message = ErrorCode::getMessage($code);
}
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Exception\Handler;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use Throwable;
class AppExceptionHandler extends ExceptionHandler
{
public function __construct(protected StdoutLoggerInterface $logger)
{
}
public function handle(Throwable $throwable, ResponseInterface $response)
{
$this->logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile()));
$this->logger->error($throwable->getTraceAsString());
return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.'));
}
public function isValid(Throwable $throwable): bool
{
return true;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\FormModel\admin\articles;
class ArticlesModel
{
public function edit()
{
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\FormModel\admin\articles;
class BaseModify
{
protected array $attributes = [];
public function setAttributes(array $modifyData): static
{
$this->attributes = $modifyData;
return $this;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\FormModel\admin\articles;
use App\FormModel\admin\articles\trait\ModifyForUpdate;
class ModifyModel extends BaseModify
{
use ModifyForUpdate;
}

View File

@ -0,0 +1,12 @@
<?php
namespace App\FormModel\admin\articles\module;
class BaseModule
{
protected array $attributes = [];
public function setAttributes()
{
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\FormModel\admin\articles\module;
class ShowsModel
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace App\FormModel\admin\articles\module;
class StreetModel extends BaseModule
{
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\FormModel\admin\articles\trait;
use App\Model\AppArticle;
trait ModifyForUpdate
{
protected array $canUpdateFields = ['title', 'cover', 'images', 'brand', 'location'];
public function update()
{
$query = AppArticle::where(['aid' => $this->attributes['aid']])->first();
foreach ($this->canUpdateFields as $field) {
$val = $this->attributes[$field] ?? null;
if ($val !== null) {
$query->{$field} = $this->attributes[$field];
}
}
$query->save();
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\FormModel\admin\news;
use App\Model\AppNews;
class NewsFormModel
{
private array $attributes = [];
public function setAttributes(array $attr, $allowProp = [])
{
if (!$allowProp) {
$this->attributes = $attr;
}
foreach($allowProp as $prop) {
if (isset($attr[$prop])) {
$this->attributes[$prop] = $attr[$prop];
}
}
return $this;
}
public function insert()
{
$model = new AppNews();
$model->setRawAttributes($this->attributes);
$model->save();
}
public function update()
{
$model = AppNews::find($this->attributes['id']);
$model->title = $this->attributes['title'];
$model->keywords = $this->attributes['keywords'];
$model->description = $this->attributes['description'];
$model->cover = $this->attributes['cover'];
$model->content = $this->attributes['content'];
$model->save();
}
}

12
app/FormModel/api/BaseModel.php Executable file
View File

@ -0,0 +1,12 @@
<?php
namespace App\FormModel\api;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
class BaseModel
{
#[Inject]
protected RequestInterface $_request;
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\FormModel\api\v1;
use App\Helpers\AppHelper;
use App\Model\AppArticle;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
class ArticlesModel
{
#[Inject]
private readonly RequestInterface $_request;
public function edit()
{
$query = AppArticle::find($this->_request->post('id'));
$query->cover = AppHelper::extractImagePath($this->_request->post('cover'));
$query->title = $this->_request->post('title');
$query->brand = $this->_request->post('brand');
$images = array_values($this->_request->post('images'));
foreach ($images as &$image) {
$image['src'] = AppHelper::extractImagePath($image['src']);
}
$query->images = json_encode($images);
$query->save();
return true;
}
public function create()
{
$query = new AppArticle();
$query->cover = AppHelper::extractImagePath($this->_request->post('cover'));
$query->title = $this->_request->post('title');
$query->brand = $this->_request->post('brand');
$images = array_values($this->_request->post('images') ?: []);
foreach ($images as &$image) {
$image['src'] = AppHelper::extractImagePath($image['src']);
}
$query->images = json_encode($images);
$query->save();
return true;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\FormModel\api\v1;
use App\FormModel\api\BaseModel;
use App\Model\AppUser;
class UserModel extends BaseModel
{
public function register()
{
$query = new AppUser();
$query->name = $this->_request->post('name');
$query->password = md5($this->_request->post('password'));
$query->email = $this->_request->post('email');
$query->unique_id = uniqid("", true);
$query->save();
}
}

11
app/FormModel/rpc/BaseModel.php Executable file
View File

@ -0,0 +1,11 @@
<?php
namespace App\FormModel\rpc;
class BaseModel
{
protected function getResponse(): BaseResponse
{
return new BaseResponse();
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\FormModel\rpc\v1;
use App\Listener\DbQueryExecutedListener;
use App\Model\AppArticle;
use Hyperf\Database\Query\Builder;
use Hyperf\DbConnection\Db;
class RpcIndexModel
{
public function index(): array
{
$res = [];
Db::beforeExecuting(function ($q) {
var_dump($q);
});
$minPublishedTime = strtotime('-1 month');
// $res['shows'] = [];
$res['shows'] = AppArticle::formatQuery(['brand_name'])
->select(['cover', 'aid', 'title', 'brand as brand_name', 'images_count', 'view_count'])
->where([
['module', 0],
['published_status', 1],
['year', '>=', 2022],
['published_at', '>=', $minPublishedTime],
])->orderBy('year', 'DESC')->orderBy('published_at', 'DESC')->limit(25)->get()->toArray();
$res['street'] = AppArticle::formatQuery(['location'])
->select(['cover', 'aid', 'title', 'brand as brand_name', 'images_count', 'view_count', 'location'])
->where([
['module', 1],
['published_status', 1],
['year', '>=', 2022],
['published_at', '>=', $minPublishedTime],
])->orderBy('year', 'DESC')->orderBy('published_at', 'DESC')->limit(25)->get()->toArray();
return $res;
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\FormModel\spider;
use App\Helpers\AppHelper;
use App\Model\AppArticle;
use App\Model\AppSpiderArticle;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
class ReviewModel
{
#[Inject]
private readonly RequestInterface $_request;
public function pass($spiderArticleId)
{
$query = AppSpiderArticle::find($spiderArticleId);
$model = new AppArticle();
$model->title = $query->title;
$model->aid = AppHelper::generateAid();
$model->cover = $query->cover;
$model->images = $query->images;
$model->year = $query->year;
$model->module = $query->module;
$model->brand = $query->brand;
$model->spider_article_id = $spiderArticleId;
$model->save();
$query->deleted_at = time();
$query->save();
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\FormModel\spiderArticle;
use App\Enums\SpiderArticlePublishedStatusEnum;
use App\Helpers\AppHelper;
use App\Model\AppArticle;
use App\Model\AppSpiderArticle;
use Hyperf\DbConnection\Db;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
class ReviewModel
{
#[Inject]
private readonly RequestInterface $_request;
/**
* 爬虫文章预发布
* @param $spiderArticleId
* @return void
* @throws \Exception
*/
public function prePublish($spiderArticleId)
{
try {
Db::beginTransaction();
$query = AppSpiderArticle::find($spiderArticleId);
$model = new AppArticle();
$model->title = $query->title;
$model->aid = AppHelper::generateAid();
$model->cover = $query->cover;
$model->images = $query->images;
$model->year = $query->year;
$model->module = $query->module;
$model->brand = $query->brand;
$model->spider_article_id = $spiderArticleId;
$model->published_status = 0;
$model->save();
$query->published_status = SpiderArticlePublishedStatusEnum::TRUE->value;
$query->published_at = time();
$query->save();
Db::commit();
} catch (\Throwable $throwable) {
Db::rollBack();
throw new \Exception($throwable->getMessage());
}
}
public function delete($spiderArticleId)
{
try {
Db::beginTransaction();
$query = AppSpiderArticle::find($spiderArticleId);
$query->deleted_at = SpiderArticlePublishedStatusEnum::DELETE;
$query->deleted_at = time();
$query->save();
Db::commit();
} catch (\Throwable $throwable) {
Db::rollBack();
throw new \Exception($throwable->getMessage());
}
}
}

45
app/Helpers/AppHelper.php Executable file
View File

@ -0,0 +1,45 @@
<?php
namespace App\Helpers;
use function Hyperf\Config\config;
class AppHelper
{
public static function getImageBaseUrl(): string
{
return config('upload.qiniu.base_url');
}
public static function setImageUrl(string $imageUrl)
{
$imageUrl = self::extractImagePath($imageUrl);
return self::getImageBaseUrl() . $imageUrl;
}
public static function extractImagePath(string $url): string
{
// 解析URL
$parsedUrl = parse_url($url);
// 获取路径部分
$path = $parsedUrl['path'];
// 去除路径前的 "/"
return ltrim($path, '/');
}
public static function generateAid(): string
{
return strtr(uniqid(more_entropy: true), [
'.' => ''
]);
}
public static function getYear(string $title)
{
preg_match('/([0-9]+)/', $title, $y);
return $y[1] ?? date('Y');
}
}

352
app/Helpers/ExcelHelper.php Normal file
View File

@ -0,0 +1,352 @@
<?php
namespace App\Helpers;
use Exception;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpMessage\Stream\SwooleStream;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Html;
use PhpOffice\PhpSpreadsheet\Writer\Xls;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Writer\Csv;
use Hyperf\HttpServer\Contract\ResponseInterface;
class ExcelHelper
{
#[Inject]
protected ResponseInterface $response;
/**
* 导出Excel
*
* @param array $list
* @param array $header
* @param string $filename
* @param string $suffix
* @param string $path 输出绝对路径
* @return bool
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
*/
public static function exportData(ResponseInterface $response, $list = [], $header = [], $filename = '', $suffix = 'xlsx', $path = '')
{
if (!is_array($list) || !is_array($header)) {
return false;
}
!$filename && $filename = time();
// 初始化
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// 写入头部
$hk = 1;
foreach ($header as $k => $v) {
$sheet->setCellValue(Coordinate::stringFromColumnIndex($hk) . '1', $v[0]);
$sheet->getStyle(Coordinate::stringFromColumnIndex($hk) . '1')->getFont()->setBold(true);
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($hk))->setAutoSize(true);
$hk += 1;
}
// 开始写入内容
$column = 2;
$size = ceil(count($list) / 500);
for ($i = 0; $i < $size; $i++) {
$buffer = array_slice($list, $i * 500, 500);
foreach ($buffer as $k => $row) {
$span = 1;
foreach ($header as $key => $value) {
// 解析字段
$realData = self::formatting($header[$key], trim(self::formattingField($row, $value[1])), $row);
// 写入excel
$sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($span) . $column, $realData, DataType::TYPE_STRING);
// $sheet->setCellValue(Coordinate::stringFromColumnIndex($span) . $column, $realData);
$span++;
}
$column++;
unset($buffer[$k]);
}
}
// 清除之前的错误输出
// ob_end_clean();
ob_start();
// 直接输出下载
switch ($suffix) {
case 'xlsx' :
$writer = new Xlsx($spreadsheet);
if (!empty($path)) {
$writer->save($path);
} else {
// 2. 保存到内存(使用 php://memory
$tempFile = 'php://memory';
$stream = fopen($tempFile, 'w+');
$writer->save($stream);
rewind($stream);
$excelOutput = stream_get_contents($stream);
fclose($stream);
// 3. 返回给客户端下载
return $response->withHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
->withHeader('Content-Disposition', 'attachment; filename=' . $filename . '.xlsx')
->withHeader('Cache-Control', 'max-age=0')
->raw($excelOutput);
}
break;
case 'xls' :
$writer = new Xls($spreadsheet);
if (!empty($path)) {
$writer->save($path);
} else {
header("Content-Type:application/vnd.ms-excel;charset=utf-8;");
header("Content-Disposition:inline;filename=\"{$filename}.xls\"");
header('Cache-Control: max-age=0');
$writer->save('php://output');
}
break;
case 'csv' :
$writer = new Csv($spreadsheet);
if (!empty($path)) {
$writer->save($path);
} else {
header("Content-type:text/csv;charset=utf-8;");
header("Content-Disposition:attachment; filename={$filename}.csv");
header('Cache-Control: max-age=0');
$writer->save('php://output');
}
break;
case 'html' :
$writer = new Html($spreadsheet);
if (!empty($path)) {
$writer->save($path);
} else {
header("Content-Type:text/html;charset=utf-8;");
header("Content-Disposition:attachment;filename=\"{$filename}.{$suffix}\"");
header('Cache-Control: max-age=0');
$writer->save('php://output');
}
break;
}
/* 释放内存 */
$spreadsheet->disconnectWorksheets();
unset($spreadsheet);
$content = ob_end_flush();
// 创建一个临时流,并将内容写入其中
$tempStream = fopen('php://temp', 'r+');
if ($tempStream === false) {
throw new \RuntimeException('Unable to open temporary stream');
}
fwrite($tempStream, $content);
rewind($tempStream);
// $response = new Response();
$contentType = 'text/csv';
return $response->withHeader('content-description', 'File Transfer')
->withHeader('content-type', $contentType)
->withHeader('content-disposition', "attachment; filename=text.xlsx")
->withHeader('content-transfer-encoding', 'binary')
->withHeader('pragma', 'public')
->withBody(new SwooleStream($content));
return $response->withHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
->withHeader('Content-Disposition', 'attachment; filename="exported_file.xlsx"')
->withBody(new \GuzzleHttp\Psr7\Stream($tempStream));
// exit();
}
/**
* 导出的另外一种形式(不建议使用)
*
* @param array $list
* @param array $header
* @param string $filename
* @return bool
*/
public static function exportCsvData($list = [], $header = [], $filename = '')
{
if (!is_array($list) || !is_array($header)) {
return false;
}
// 清除之前的错误输出
ob_end_clean();
ob_start();
!$filename && $filename = time();
$html = "\xEF\xBB\xBF";
foreach ($header as $k => $v) {
$html .= $v[0] . "\t ,";
}
$html .= "\n";
if (!empty($list)) {
$info = [];
$size = ceil(count($list) / 500);
for ($i = 0; $i < $size; $i++) {
$buffer = array_slice($list, $i * 500, 500);
foreach ($buffer as $k => $row) {
$data = [];
foreach ($header as $key => $value) {
// 解析字段
$realData = self::formatting($header[$key], trim(self::formattingField($row, $value[1])), $row);
$data[] = '"' . $realData . '"';
}
$info[] = implode("\t ,", $data) . "\t ,";
unset($data, $buffer[$k]);
}
}
$html .= implode("\n", $info);
}
header("Content-type:text/csv");
header("Content-Disposition:attachment; filename={$filename}.csv");
echo $html;
exit();
}
/**
* 导入
*
* @param $filePath
* @param int $startRow
* @return array|mixed
* @throws Exception
* @throws \PhpOffice\PhpSpreadsheet\Exception
* @throws \PhpOffice\PhpSpreadsheet\Reader\Exception
*/
public static function import($filePath, $startRow = 1)
{
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
$reader->setReadDataOnly(true);
if (!$reader->canRead($filePath)) {
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xls();
// setReadDataOnly Set read data only 只读单元格的数据,不格式化 e.g. 读时间会变成一个数据等
$reader->setReadDataOnly(true);
if (!$reader->canRead($filePath)) {
throw new Exception('不能读取Excel');
}
}
$spreadsheet = $reader->load($filePath);
$sheetCount = $spreadsheet->getSheetCount();// 获取sheet的数量
// 获取所有的sheet表格数据
$excleDatas = [];
$emptyRowNum = 0;
for ($i = 0; $i < $sheetCount; $i++) {
$currentSheet = $spreadsheet->getSheet($i); // 读取excel文件中的第一个工作表
$allColumn = $currentSheet->getHighestColumn(); // 取得最大的列号
$allColumn = Coordinate::columnIndexFromString($allColumn); // 由列名转为列数('AB'->28)
$allRow = $currentSheet->getHighestRow(); // 取得一共有多少行
$arr = [];
for ($currentRow = $startRow; $currentRow <= $allRow; $currentRow++) {
// 从第1列开始输出
for ($currentColumn = 1; $currentColumn <= $allColumn; $currentColumn++) {
$val = $currentSheet->getCellByColumnAndRow($currentColumn, $currentRow)->getValue();
$arr[$currentRow][] = trim($val);
}
// $arr[$currentRow] = array_filter($arr[$currentRow]);
// 统计连续空行
if (empty($arr[$currentRow]) && $emptyRowNum <= 50) {
$emptyRowNum++;
} else {
$emptyRowNum = 0;
}
// 防止坑队友的同事在excel里面弄出很多的空行陷入很漫长的循环中设置如果连续超过50个空行就退出循环返回结果
// 连续50行数据为空不再读取后面行的数据防止读满内存
if ($emptyRowNum > 50) {
break;
}
}
$excleDatas[$i] = $arr; // 多个sheet的数组的集合
}
// 这里我只需要用到第一个sheet的数据所以只返回了第一个sheet的数据
$returnData = $excleDatas ? array_shift($excleDatas) : [];
// 第一行数据就是空的为了保留其原始数据第一行数据就不做array_fiter操作
$returnData = $returnData && isset($returnData[$startRow]) && !empty($returnData[$startRow]) ? array_filter($returnData) : $returnData;
return $returnData;
}
/**
* 格式化内容
*
* @param array $array 头部规则
* @return false|mixed|null|string 内容值
*/
protected static function formatting(array $array, $value, $row)
{
!isset($array[2]) && $array[2] = 'text';
switch ($array[2]) {
// 文本
case 'text' :
return $value;
break;
// 日期
case 'date' :
return !empty($value) ? date($array[3], $value) : null;
break;
// 选择框
case 'selectd' :
return $array[3][$value] ?? null;
break;
// 匿名函数
case 'function' :
return isset($array[3]) ? call_user_func($array[3], $row) : null;
break;
// 默认
default :
break;
}
return null;
}
/**
* 解析字段
*
* @param $row
* @param $field
* @return mixed
*/
protected static function formattingField($row, $field)
{
$newField = explode('.', $field);
if (count($newField) == 1) {
return $row[$field] ?? '';
}
foreach ($newField as $item) {
if (isset($row[$item])) {
$row = $row[$item];
} else {
break;
}
}
return is_array($row) ? false : $row;
}
}

355
app/Helpers/TitleHelper.php Executable file
View File

@ -0,0 +1,355 @@
<?php
namespace App\Helpers;
use App\Enums\ArticleStyleEnum;
use App\Enums\LocationEnum;
class TitleHelper
{
public static function translate(string $englishTitle)
{
$map = [
'Madrid' => [
[
'preg' => '/Madrid Spring ([0-9]*?)/',
'trans' => '春季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::MADRID->value,
],[
'preg' => '/Madrid Fall ([0-9]*?)/',
'trans' => '秋季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::MADRID->value,
],
],
'Spain' => [
[
'preg' => '/Spain Spring ([0-9]*?)/',
'trans' => '春季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::SPAIN->value,
],[
'preg' => '/Spain Fall ([0-9]*?)/',
'trans' => '秋季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::SPAIN->value,
],
],
'Istanbul' => [
[
'preg' => '/Istanbul Spring ([0-9]*?)/',
'trans' => '春季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::ISTANBUL->value,
],[
'preg' => '/Istanbul Fall ([0-9]*?)/',
'trans' => '秋季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::ISTANBUL->value,
],
],
'Lagos' => [
[
'preg' => '/Lagos Spring ([0-9]*?)/',
'trans' => '春季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::LAGOS->value,
],[
'preg' => '/Lagos Fall ([0-9]*?)/',
'trans' => '秋季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::LAGOS->value,
],
],
'Russia' => [
[
'preg' => '/Russia Spring ([0-9]*?)/',
'trans' => '春季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::RUSSIA->value,
],[
'preg' => '/Russia Fall ([0-9]*?)/',
'trans' => '秋季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::RUSSIA->value,
],
],
'Shanghai' => [
[
'preg' => '/Shanghai Spring ([0-9]*?)/',
'trans' => '春季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::SHANGHAI->value,
],[
'preg' => '/Shanghai Fall ([0-9]*?)/',
'trans' => '秋季',
'style' => ArticleStyleEnum::NULL->value,
'location' => LocationEnum::SHANGHAI->value,
],
],
'Bridal' => [
[
'preg' => '/Bridal Spring ([0-9]*?)/',
'trans' => '春季婚纱礼服',
'style' => ArticleStyleEnum::BRIDAL->value,
'location' => LocationEnum::NULL->value,
],[
'preg' => '/Bridal Fall ([0-9]*?)/',
'trans' => '秋季婚纱礼服',
'style' => ArticleStyleEnum::BRIDAL->value,
'location' => LocationEnum::NULL->value,
],
],
'Australia' => [
[
'preg' => '/Australia Resort ([0-9]*)/',
'trans' => '度假系列',
'style' => 1,
'location' => LocationEnum::AUSTRALIA->value,
],[
'preg' => '/Australia Spring ([0-9]*)/',
'trans' => '春季',
'style' => 0,
'location' => LocationEnum::AUSTRALIA->value,
],
],
'Menswear' => [
[
'preg' => '/Spring ([0-9]*?) Menswear/',
'trans' => '春季男装',
'style' => 0,
'location' => 0,
],[
'preg' => '/Fall ([0-9]*?) Menswear/',
'trans' => '秋季男装',
'style' => 0,
'location' => 0,
]
],
'Ukraine' => [
[
'preg' => '/Ukraine Fall ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::UKRAINE->value, // 乌克兰
],
[
'preg' => '/Ukraine Spring ([0-9]*)/',
'trans' => '春季',
'style' => 0,
'location' => LocationEnum::UKRAINE->value, // 乌克兰
],
],
'Kiev' => [
[
'preg' => '/Kiev Fall ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::KIEV->value, // 基辅
],[
'preg' => '/Kiev Spring ([0-9]*)/',
'trans' => '春季',
'style' => 0,
'location' => LocationEnum::KIEV->value, // 基辅
]
],
'Stockholm' => [
[
'preg' => '/Stockholm Fall ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::STOCKHOLM->value, // 斯德哥尔摩
],
[
'preg' => '/Stockholm Spring ([0-9]*)/',
'trans' => '春季',
'style' => 0,
'location' => LocationEnum::STOCKHOLM->value, // 斯德哥尔摩
],
],
'Tbilisi' => [
[
'preg' => '/Tbilisi Fall ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::TBILISI->value, // 斯德哥尔摩
],
[
'preg' => '/Tbilisi Spring ([0-9]*)/',
'trans' => '春季',
'style' => 0,
'location' => LocationEnum::TBILISI->value, // 斯德哥尔摩
],
],
'Mexico' => [
[
'preg' => '/Mexico Fall ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::MEXICO->value, // 斯德哥尔摩
],[
'preg' => '/Mexico Spring ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::MEXICO->value, // 斯德哥尔摩
],[
'preg' => '/Mexico City Fall ([0-9]*)/',
'trans' => '城市秋季',
'style' => 0,
'location' => LocationEnum::MEXICO->value, // 斯德哥尔摩
],[
'preg' => '/Mexico City Spring ([0-9]*)/',
'trans' => '城市春季',
'style' => 0,
'location' => LocationEnum::MEXICO->value, // 斯德哥尔摩
],
],
'Tokyo' => [
[
'preg' => '/Tokyo Spring ([0-9]*)/',
'trans' => '春季',
'style' => 0,
'location' => LocationEnum::TOKYO->value, // 东京
],[
'preg' => '/Tokyo Fall ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::TOKYO->value, // 东京
],
],
'Berlin' => [
[
'preg' => '/Berlin Fall ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::BERLIN->value, // 柏林
],
[
'preg' => '/Berlin Spring ([0-9]*)/',
'trans' => '春季',
'style' => 0,
'location' => LocationEnum::BERLIN->value, // 柏林
],
],
'Copenhagen' => [
[
'preg' => '/Copenhagen Fall ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::COPENHAGEN->value, // 哥本哈根
],[
'preg' => '/Copenhagen Spring ([0-9]*)/',
'trans' => '春季',
'style' => 0,
'location' => LocationEnum::COPENHAGEN->value, // 哥本哈根
],
],
'São Paulo' => [
[
'preg' => '/São Paulo Fall ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::SAO_PAULO->value, // 哥本哈根
],[
'preg' => '/São Paulo Spring ([0-9]*)/',
'trans' => '春季',
'style' => 0,
'location' => LocationEnum::SAO_PAULO->value, // 哥本哈根
],
],
'SEOUL' => [
[
'preg' => '/Seoul Spring ([0-9]*)/',
'trans' => '春季',
'style' => 0,
'location' => LocationEnum::SEOUL->value,
],[
'preg' => '/Seoul Fall ([0-9]*)/',
'trans' => '秋季',
'style' => 0,
'location' => LocationEnum::SEOUL->value,
],
],
'Spring' => [
[
'preg' => '/([0-9]*?) Spring Summer/',
'trans' => '春夏',
'style' => 0,
'location' => 0,
],
],
'Autumn Winter' => [
[
'preg' => '/([0-9]*?) Autumn Winter/',
'trans' => '秋冬',
'style' => 0,
'location' => 0,
],
],
'Ready-to-Wear' => [
[
'preg' => '/Fall ([0-9]*?) Ready-to-Wear/',
'trans' => '秋季成衣',
'style' => 0,
'location' => 0,
],[
'preg' => '/Spring ([0-9]*?) Ready-to-Wear/',
'trans' => '春季成衣',
'style' => 0,
'location' => 0,
],
],
'Resort' => [
[
'preg' => '/Resort ([0-9]*?)/',
'trans' => '度假系列',
'style' => ArticleStyleEnum::RESORT->value,
'location' => LocationEnum::NULL->value,
],
],
'Couture' => [
[
'preg' => '/Spring ([0-9]*?) Couture/',
'trans' => '春季高订系列',
'style' => ArticleStyleEnum::RESORT->value,
'location' => LocationEnum::NULL->value,
],
[
'preg' => '/Fall ([0-9]*?) Couture/',
'trans' => '秋季高订系列',
'style' => ArticleStyleEnum::RESORT->value,
'location' => LocationEnum::NULL->value,
],
],
'Pre-Fall' => [
[
'preg' => '/Pre-Fall ([0-9]*)/',
'trans' => '早秋',
'style' => 0,
'location' => 0,
],
],
];
$res = $englishTitle;
foreach ($map as $keyword => $pregMap) {
if (stripos($englishTitle, $keyword) !== false) {
foreach ($pregMap as $pregItem) {
preg_match_all($pregItem['preg'], $englishTitle, $matches);
if (count($matches) > 1 && $matches[1]) {
$res = trim(current($matches[1]) . " {$pregItem['trans']}");
break;
}
}
if ($res) {
break;
}
}
}
return $res;
}
}

196
app/Helpers/TreeHelper.php Executable file
View File

@ -0,0 +1,196 @@
<?php
namespace App\Helpers;
class TreeHelper
{
/**
* 获取完整的树结构,包含祖先节点
*/
const INCLUDE_ANCESTORS = 1;
/**
* 获取部分树,不包含祖先节点
*/
const EXCLUDE_ANCESTORS = 0;
/**
* 数据
* @var array
*/
protected array $data = [];
/**
* 哈希树
* @var array
*/
protected array $hashTree = [];
/**
* 父级字段名
* @var string
*/
protected string $pidName = 'pid';
/**
* @param $data
* @param string $pid_name
*/
public function __construct($data, string $pid_name = 'pid')
{
$this->pidName = $pid_name;
if (is_object($data) && method_exists($data, 'toArray')) {
$this->data = $data->toArray();
} else {
$this->data = (array)$data;
$this->data = array_map(function ($item) {
if (is_object($item) && method_exists($item, 'toArray')) {
return $item->toArray();
}
return $item;
}, $this->data);
}
$this->hashTree = $this->getHashTree();
}
/**
* 获取子孙节点
* @param array $include
* @param bool $with_self
* @return array
*/
protected function getDescendant(array $include, bool $with_self = false): array
{
$items = [];
foreach ($include as $id) {
if (!isset($this->hashTree[$id])) {
return [];
}
if ($with_self) {
$item = $this->hashTree[$id];
unset($item['children']);
$items[$item['id']] = $item;
}
foreach ($this->hashTree[$id]['children'] ?? [] as $item) {
unset($item['children']);
$items[$item['id']] = $item;
foreach ($this->getDescendant([$item['id']]) as $it) {
$items[$it['id']] = $it;
}
}
}
return array_values($items);
}
/**
* 获取哈希树
* @param array $data
* @return array
*/
protected function getHashTree(array $data = []): array
{
$data = $data ?: $this->data;
$hash_tree = [];
foreach ($data as $item) {
$hash_tree[$item['id']] = $item;
}
foreach ($hash_tree as $index => $item) {
if ($item[$this->pidName] && isset($hash_tree[$item[$this->pidName]])) {
$hash_tree[$item[$this->pidName]]['children'][$hash_tree[$index]['id']] = &$hash_tree[$index];
}
}
return $hash_tree;
}
/**
* 获取树
* @param array $include
* @param int $type
* @return array|null
*/
protected function _getTree(array $include = [], int $type = 1): ?array
{
// $type === static::EXCLUDE_ANCESTORS
if ($type === static::EXCLUDE_ANCESTORS) {
$items = [];
$include = array_unique($include);
foreach ($include as $id) {
if (!isset($this->hashTree[$id])) {
return [];
}
$items[] = $this->hashTree[$id];
}
return static::arrayValues($items);
}
// $type === static::INCLUDE_ANCESTORS
$hash_tree = $this->hashTree;
$items = [];
if ($include) {
$map = [];
foreach ($include as $id) {
if (!isset($hash_tree[$id])) {
continue;
}
$item = $hash_tree[$id];
$max_depth = 100;
while ($max_depth-- > 0 && $item[$this->pidName] && isset($hash_tree[$item[$this->pidName]])) {
$last_item = $item;
$pid = $item[$this->pidName];
$item = $hash_tree[$pid];
$item_id = $item['id'];
if (empty($map[$item_id])) {
$map[$item_id] = 1;
$hash_tree[$pid]['children'] = [];
}
$hash_tree[$pid]['children'][$last_item['id']] = $last_item;
$item = $hash_tree[$pid];
}
$items[$item['id']] = $item;
}
} else {
$items = $hash_tree;
}
$formatted_items = [];
foreach ($items as $item) {
if (!$item[$this->pidName] || !isset($hash_tree[$item[$this->pidName]])) {
$formatted_items[] = $item;
}
}
return static::arrayValues($formatted_items);
}
/**
* 递归重建数组下标
* @param $array
* @return array
*/
public static function arrayValues($array): array
{
if (!$array) {
return [];
}
if (!isset($array['children'])) {
$current = current($array);
if (!is_array($current)) {
return $array;
}
$tree = array_values($array);
foreach ($tree as $index => $item) {
$tree[$index] = static::arrayValues($item);
}
return $tree;
}
$array['children'] = array_values($array['children']);
foreach ($array['children'] as $index => $child) {
$array['children'][$index] = static::arrayValues($child);
}
return $array;
}
public static function getTree($data)
{
return (new self($data))->_getTree();
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Listener;
use Hyperf\Collection\Arr;
use Hyperf\Database\Events\QueryExecuted;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
#[Listener]
class DbQueryExecutedListener implements ListenerInterface
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(ContainerInterface $container)
{
$this->logger = $container->get(LoggerFactory::class)->get('sql');
}
public function listen(): array
{
return [
QueryExecuted::class,
];
}
/**
* @param QueryExecuted $event
*/
public function process(object $event): void
{
if ($event instanceof QueryExecuted) {
$sql = $event->sql;
if (! Arr::isAssoc($event->bindings)) {
$position = 0;
foreach ($event->bindings as $value) {
$position = strpos($sql, '?', $position);
if ($position === false) {
break;
}
$value = "'{$value}'";
$sql = substr_replace($sql, $value, $position, 1);
$position += strlen($value);
}
}
$this->logger->info(sprintf('[%s] %s', $event->time, $sql));
}
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Listener;
use Hyperf\AsyncQueue\AnnotationJob;
use Hyperf\AsyncQueue\Event\AfterHandle;
use Hyperf\AsyncQueue\Event\BeforeHandle;
use Hyperf\AsyncQueue\Event\Event;
use Hyperf\AsyncQueue\Event\FailedHandle;
use Hyperf\AsyncQueue\Event\RetryHandle;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Logger\LoggerFactory;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
#[Listener]
class QueueHandleListener implements ListenerInterface
{
protected LoggerInterface $logger;
public function __construct(ContainerInterface $container)
{
$this->logger = $container->get(LoggerFactory::class)->get('queue');
}
public function listen(): array
{
return [
AfterHandle::class,
BeforeHandle::class,
FailedHandle::class,
RetryHandle::class,
];
}
public function process(object $event): void
{
if ($event instanceof Event && $event->getMessage()->job()) {
$job = $event->getMessage()->job();
$jobClass = get_class($job);
if ($job instanceof AnnotationJob) {
$jobClass = sprintf('Job[%s@%s]', $job->class, $job->method);
}
$date = date('Y-m-d H:i:s');
switch (true) {
case $event instanceof BeforeHandle:
$this->logger->info(sprintf('[%s] Processing %s.', $date, $jobClass));
break;
case $event instanceof AfterHandle:
$this->logger->info(sprintf('[%s] Processed %s.', $date, $jobClass));
break;
case $event instanceof FailedHandle:
$this->logger->error(sprintf('[%s] Failed %s.', $date, $jobClass));
$this->logger->error((string) $event->getThrowable());
break;
case $event instanceof RetryHandle:
$this->logger->warning(sprintf('[%s] Retried %s.', $date, $jobClass));
break;
}
}
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Listener;
use Hyperf\Command\Event\AfterExecute;
use Hyperf\Coordinator\Constants;
use Hyperf\Coordinator\CoordinatorManager;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
#[Listener]
class ResumeExitCoordinatorListener implements ListenerInterface
{
public function listen(): array
{
return [
AfterExecute::class,
];
}
public function process(object $event): void
{
CoordinatorManager::until(Constants::WORKER_EXIT)->resume();
}
}

37
app/Model/AppAdminMenu.php Executable file
View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property string $title
* @property string $icon
* @property string $key
* @property int $pid
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property string $href
* @property int $type
* @property int $weight
*/
class AppAdminMenu extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'app_admin_menus';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'pid' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'type' => 'integer', 'weight' => 'integer'];
}

125
app/Model/AppArticle.php Executable file
View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace App\Model;
use App\Enums\ArticleModuleEnum;
use App\Enums\ArticlePublishedStatusEnum;
use App\Enums\LocationEnum;
use App\Helpers\TitleHelper;
use Hyperf\Database\Model\Events\Saving;
/**
* @property int $id
* @property string $aid
* @property string $title
* @property string $description
* @property string $cover
* @property int $year
* @property int $deleted_at
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property int $created_by
* @property int $updated_by
* @property int $spider_article_id
* @property int $style
* @property int $location
* @property int $images_count
* @property int $published_at
* @property mixed $images
* @property mixed $module
* @property mixed $brand
* @property mixed $brand_name
* @property mixed $published_status
*/
class AppArticle extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'app_articles';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'brand' => 'integer', 'year' => 'integer', 'module' => 'integer', 'deleted_at' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'created_by' => 'integer', 'updated_by' => 'integer', 'spider_article_id' => 'integer', 'style' => 'integer', 'location' => 'integer', 'images_count' => 'integer', 'published_status' => 'integer', 'published_at' => 'integer'];
protected ?string $dateFormat = 'U';
// public function saving(Saving $event)
// {
// if ($this->getAttribute('images_count') == 0) {
// $this->setAttribute('images_count', count(json_decode($this->getAttribute('images'), true)));
// }
// }
public function setImagesAttribute($value)
{
if (is_string($value)) {
$imagesValue = json_decode($value, true);
$this->setAttribute('images_count', count($imagesValue));
} elseif (is_array($value)) {
$this->setAttribute('images_count', count($value));
$value = json_encode(array_values($value));
}
$this->attributes['images'] = $value;
}
public function getImagesAttribute($value)
{
return $this->format('images', function (bool $isFormat) use ($value) {
return $isFormat ? json_decode($value, true) : $value;
});
}
public function getModuleAttribute($value)
{
return $this->format('module', function (bool $isFormat) use ($value) {
return $isFormat ? ArticleModuleEnum::from($value)->toString() : $value;
});
}
public function getLocationAttribute($value)
{
return $this->format('location', function (bool $isFormat) use ($value) {
return $isFormat ? LocationEnum::from($value)->toString() : $value;
});
}
public function getBrandAttribute($value)
{
return $this->format('brand', function (bool $isFormat) use ($value) {
return $isFormat ? AppBrand::find($value)?->name : $value;
});
}
public function getBrandNameAttribute($value)
{
return $this->format('brand_name', function (bool $isFormat) use ($value) {
return $isFormat ? AppBrand::find($value)?->name : $value;
});
}
public function getPublishedStatusAttribute($value)
{
return $this->format('published_status', function (bool $isFormat) use ($value) {
return $isFormat ? ArticlePublishedStatusEnum::from($value)->toString() : $value;
});
}
public function getTranslateTitleAttribute($value)
{
return $this->format('translate_title', function (bool $isFormat) use ($value) {
return $isFormat ? TitleHelper::translate($value) : $value;
});
}
}

41
app/Model/AppBrand.php Executable file
View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property string $name
* @property string $cn_name
* @property string $first_letter
* @property string $logo
* @property string $description
* @property int $is_del
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property int $created_by
* @property int $updated_by
* @property string $spider_origin
*/
class AppBrand extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'app_brands';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'is_del' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'created_by' => 'integer', 'updated_by' => 'integer'];
protected ?string $dateFormat = 'U';
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property string $keyword
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property int $deleted_at
* @property int $is_delete
* @property int $queried_at
* @property int $platform
*/
class AppKeywordsMonitor extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'app_keywords_monitor';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'integer', 'is_delete' => 'integer', 'queried_at' => 'integer', 'platform' => 'integer'];
function getQueriedAtAttribute($value): string
{
if (!$value) {
return '未查询';
}
return date('Y-m-d H:i:s', intval($value)) ?: '未查询';
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property int $aid
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property int $deleted_at
* @property int $is_delete
* @property int $queried_at
* @property string $title
* @property string $description
* @property string $platform
* @property string $url
* @property string $order
* @property string $ip_address
* @property string $ip_source
* @property string $screen_path
*/
class AppKeywordsMonitorResult extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'app_keywords_monitor_result';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'aid' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'integer', 'is_delete' => 'integer', 'queried_at' => 'integer'];
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property string $keyword
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property int $deleted_at
* @property int $is_delete
* @property int $queried_at
* @property int $platform
* @property int $aid
*/
class AppKeywordsMonitorTask extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'app_keywords_monitor_task';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'integer', 'is_delete' => 'integer', 'queried_at' => 'integer', 'platform' => 'integer', 'aid' => 'integer'];
}

48
app/Model/AppNews.php Normal file
View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property string $title
* @property string $keywords
* @property string $content
* @property string $description
* @property \Carbon\Carbon $created_at
* @property int $created_by
* @property \Carbon\Carbon $updated_at
* @property int $updated_by
* @property int $deleted_at
* @property int $deleted_by
* @property int $platform
* @property int $is_record
* @property string $cover
*/
class AppNews extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'app_news';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'created_at' => 'datetime', 'created_by' => 'integer', 'updated_at' => 'datetime', 'updated_by' => 'integer', 'deleted_at' => 'integer', 'deleted_by' => 'integer', 'platform' => 'integer', 'is_record' => 'integer'];
protected ?string $dateFormat = 'U';
public function getCreatedAtAttribute($value)
{
return date('Y-m-d H:i:s', intval($value));
}
}

84
app/Model/AppSpiderArticle.php Executable file
View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Model;
use App\Enums\ArticleModuleEnum;
use App\Enums\ArticlePublishedStatusEnum;
use App\Enums\SpiderArticlePublishedStatusEnum;
use Carbon\Carbon;
use Hyperf\Database\Model\Builder;
/**
* @property int $id
* @property string $title
* @property string $description
* @property string $cover
* @property int $brand
* @property int $year
* @property int $deleted_at
* @property Carbon $updated_at
* @property string $platform
* @property string $source_url
* @property int $published_status
* @property int $published_at
* @property mixed $created_at
* @property mixed $module
* @property mixed $images
*/
class AppSpiderArticle extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'app_spider_articles';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'brand' => 'integer', 'year' => 'integer', 'module' => 'integer', 'deleted_at' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime', 'published_status' => 'integer', 'published_at' => 'integer'];
public function getCreatedAtAttribute($value): false|string
{
return $this->format('created_at', function (bool $isFormat) use ($value) {
return $isFormat ? date('Y-m-d H:i:s', intval($value)) : $value;
});
}
public function getModuleAttribute($value)
{
return $this->format('module', function (bool $isFormat) use ($value) {
return $isFormat ? ArticleModuleEnum::from($value)->toString() : $value;
});
}
public function getImagesAttribute($value)
{
return $this->format('images', function (bool $isFormat) use ($value) {
return $isFormat ? json_decode($value, true) : $value;
});
}
public function getBrandAttribute($value)
{
return $this->format('brand', function (bool $isFormat) use ($value) {
return $isFormat ? AppBrand::find($value)?->name : $value;
});
}
public function getPublishedStatusAttribute($value)
{
return $this->format('published_status', function (bool $isFormat) use ($value) {
return $isFormat ? SpiderArticlePublishedStatusEnum::from($value)->toString() : $value;
});
}
}

37
app/Model/AppUser.php Executable file
View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Model;
/**
* @property int $id
* @property string $name
* @property string $unique_id
* @property string $avatar
* @property string $email
* @property string $email_verified_at
* @property string $password
* @property string $remember_token
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class AppUser extends Model
{
/**
* The table associated with the model.
*/
protected ?string $table = 'app_users';
/**
* The attributes that are mass assignable.
*/
protected array $fillable = [];
/**
* The attributes that should be cast to native types.
*/
protected array $casts = ['id' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];
}

46
app/Model/Model.php Executable file
View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Model;
use Hyperf\Context\Context;
use Hyperf\Coroutine\Coroutine;
use Hyperf\Database\Model\Builder;
use Hyperf\DbConnection\Model\Model as BaseModel;
use Hyperf\ModelCache\Cacheable;
use Hyperf\ModelCache\CacheableInterface;
abstract class Model extends BaseModel implements CacheableInterface
{
use Cacheable;
protected ?string $dateFormat = 'U';
const ENABLED_FORMATTER = __CLASS__ . 'ENABLED_FORMATTER';
public static function formatQuery(array $formatFields = []): Builder
{
Context::set(self::ENABLED_FORMATTER, $formatFields);
return (new static())->newQuery();
}
public static function query(): Builder
{
Context::set(self::ENABLED_FORMATTER, []);
return (new static())->newQuery();
}
protected function format(string $keyName, \Closure $callable)
{
$formatFields = Context::get(self::ENABLED_FORMATTER);
return $callable(in_array($keyName, $formatFields ?: []));
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace App\Process;
use Hyperf\AsyncQueue\Process\ConsumerProcess;
use Hyperf\Process\Annotation\Process;
#[Process]
class AsyncQueueConsumer extends ConsumerProcess
{
}

14
app/Proto/grpc.proto Executable file
View File

@ -0,0 +1,14 @@
syntax = "proto3";
package grpc;
service hi {
rpc sayHello (HiUser) returns (HiReply) {
}
}
message HiUser {
string name = 1;
int32 sex = 2;
}
message HiReply {
string message = 1;
HiUser user = 2;
}

33
app/Request/Test.php Executable file
View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Request;
use Hyperf\Validation\Request\FormRequest;
class Test extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'password' => 'required'
];
}
public function validationData(): array
{
return $this->getParsedBody();
}
}

11
app/Rpc/BaseService.php Executable file
View File

@ -0,0 +1,11 @@
<?php
namespace App\Rpc;
class BaseService
{
protected function getResponse()
{
return new RpcResponse();
}
}

27
app/Rpc/CalculatorService.php Executable file
View File

@ -0,0 +1,27 @@
<?php
namespace App\Rpc;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Rpc\Response;
use Hyperf\RpcServer\Annotation\RpcService;
// #[RpcService(name: "xxx-oo", server: "jsonrpc-http", protocol: "jsonrpc-http", publishTo: 'nacos')]
class CalculatorService
{
public function add(int $a, int $b)
{
// \Hyperf\Logger\LoggerFactory::get('default')->info("Add called with: $a + $b");
// 这里是服务方法的具体实现
return ['xxxx' => 888];
return $a + $b;
}
public function add2(int $a, int $b)
{
// \Hyperf\Logger\LoggerFactory::get('default')->info("Add called with: $a + $b");
// 这里是服务方法的具体实现
return ['xxxx' => 888999];
return $a + $b;
}
}

41
app/Rpc/RpcResponse.php Executable file
View File

@ -0,0 +1,41 @@
<?php
namespace App\Rpc;
class RpcResponse
{
public int $code = 0;
public string $msg = 'ok';
public array $data = [];
public array $meta = [];
public string $title = '';
public array $pageModule = [];
public function setData(array $data)
{
$this->data = $data;
return $this;
}
public function setCode(int $code)
{
$this->code = $code;
return $this;
}
public function setMsg(string $message)
{
$this->msg = $message;
return $this;
}
public function send()
{
return [
'code' => $this->code,
'msg' => $this->msg,
'data' => $this->data,
];
}
}

19
app/Rpc/v1/IndexService.php Executable file
View File

@ -0,0 +1,19 @@
<?php
namespace App\Rpc\v1;
use App\FormModel\rpc\v1\RpcIndexModel;
use App\Rpc\BaseService;
use Hyperf\RpcServer\Annotation\RpcService;
// #[RpcService(name: "index", server: "jsonrpc-http", protocol: "jsonrpc-http", publishTo: 'nacos')]
class IndexService extends BaseService
{
public function index(): array
{
$model = new RpcIndexModel();
$data = $model->index();
$response = $this->getResponse()->setPageModule($data);
return $response->send();
}
}

59
app/Rpc/v1/NewsService.php Executable file
View File

@ -0,0 +1,59 @@
<?php
namespace App\Rpc\v1;
use App\Model\AppNews;
use App\Rpc\BaseService;
use Hyperf\RpcServer\Annotation\RpcService;
#[RpcService(name: "news", server: "jsonrpc-http", protocol: "jsonrpc-http")]
class NewsService extends BaseService
{
/**
* 查看单个新闻详情
* @url /news/view
* @param $id
* @return array
*/
public function view($id): array
{
$query = AppNews::find($id)->toArray();
// 相关文章
$query['about'] = AppNews::formatQuery(['created_at'])
->select(['title', 'id'])
->limit(10)
->orderBy('id', 'desc')
->get()
->toArray();
// 上一篇文章
$query['prevNews'] = AppNews::find($id - 1, ['title', 'id'])?->toArray();
// 下一篇文章
$query['nextNews'] = AppNews::find($id + 1, ['title', 'id'])?->toArray();
return $this->getResponse()->setData($query)->setCode(0)->send();
}
/**
* 查看所有新闻
* @url /news/index
* @param int $limit
* @param int $page
* @return array
*/
public function index(int $limit = 10, int $page = 1): array
{
$query = AppNews::formatQuery(['created_at'])
->select(['id'])
->orderBy('id', 'desc');;
$pagination = $query->paginate($limit, page: $page);
$ids = [];
foreach ($pagination->items() as $item) {
$ids[] = $item->id;
}
$value = AppNews::query()->whereIn('id', $ids)->orderBy('id', 'desc')->get()->toArray();
return $this->getResponse()->setData($value)->setCode(0)->send();
}
}

27
app/Rpc/v1/ShowsService.php Executable file
View File

@ -0,0 +1,27 @@
<?php
namespace App\Rpc\v1;
use App\FormModel\rpc\v1\RpcIndexModel;
use App\Model\AppArticle;
use App\Rpc\BaseService;
use Hyperf\RpcServer\Annotation\RpcService;
// #[RpcService(name: "shows", server: "jsonrpc-http", protocol: "jsonrpc-http", publishTo: 'nacos')]
class ShowsService extends BaseService
{
public function view($aid): array
{
$data = AppArticle::formatQuery(['images', 'title', 'cover', 'brand_name'])
->select(['brand', 'aid', 'title', 'cover', 'brand as brand_name', 'images', 'description'])
->where('aid', $aid)
->first()
->toArray();
$data['more'] = AppArticle::formatQuery(['images', 'title', 'cover', 'brand_name'])
->where('brand', '=', $data['brand'])
->orderBy('published_at', 'DESC')
->limit(25);
return $this->getResponse()->setPageModule($data)->send();
}
}