You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

179 lines
5.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Console\Commands;
use App\Models\SignupChannel;
use App\Support\ChannelEntryEncryption;
use App\Support\ChannelEntrySignature;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Throwable;
class GenerateChannelEntryUrls extends Command
{
protected $signature = 'channel:entry-urls
{--competition= : 赛事 ID 或 slug}
{--channel= : 渠道 ID 或 channel_code}
{--mobile=13800138000 : 测试手机号}
{--name=测试用户 : 测试姓名}
{--company-name= : 测试公司名称,空字符串也会参与签名}
{--state=local-test : 渠道透传 state}
{--timestamp= : 秒级 Unix 时间戳,默认当前时间}
{--base-url= : 入口基础地址,默认 config(app.url)}
{--include-disabled : 包含 disabled 渠道,默认只生成 enabled 渠道}
{--require-encryption : 仅为已启用入口敏感字段加密的渠道生成 URL}
{--plain : 只输出 URL,每行一个}';
protected $description = '根据数据库内的赛事和报名渠道生成 /channel-entry 测试访问入口 URL';
public function handle(): int
{
$timestamp = $this->resolveTimestamp();
if ($timestamp === null) {
return self::FAILURE;
}
$baseUrl = $this->resolveBaseUrl();
$query = SignupChannel::query()->with('competition');
$this->applyCompetitionFilter($query);
$this->applyChannelFilter($query);
if (! $this->option('include-disabled')) {
$query->where('status', SignupChannel::STATUS_ENABLED);
}
if ($this->option('require-encryption')) {
$query->where('entry_encryption_enabled', true);
}
try {
$channels = $query
->orderBy('competition_id')
->orderBy('id')
->get();
} catch (Throwable $exception) {
$this->error('读取渠道数据失败:'.$exception->getMessage());
return self::FAILURE;
}
if ($channels->isEmpty()) {
$this->warn('未找到符合条件的渠道。');
return self::FAILURE;
}
$rows = [];
foreach ($channels as $channel) {
$competition = $channel->competition;
$publicKey = (string) $channel->encryption_public_key;
$parameters = [
'channel_code' => (string) $channel->channel_code,
'timestamp' => (string) $timestamp,
'name' => $this->protectedValue($this->stringOption('name'), $channel, $publicKey),
'mobile' => $this->protectedValue($this->stringOption('mobile'), $channel, $publicKey),
'company_name' => $this->protectedValue($this->stringOption('company-name'), $channel, $publicKey),
'state' => $this->stringOption('state'),
];
$parameters['hash'] = ChannelEntrySignature::make($parameters, $channel->shared_secret);
$url = $baseUrl.'/channel-entry?'.http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
if ($this->option('plain')) {
$this->line($url);
continue;
}
$rows[] = [
'competition' => $competition ? "{$competition->id}:{$competition->slug}" : '(missing)',
'channel' => "{$channel->id}:{$channel->channel_name}",
'status' => $channel->status,
'encryption' => $channel->entry_encryption_enabled ? 'enabled' : 'disabled',
'channel_code' => $channel->channel_code,
'url' => $url,
];
}
if (! $this->option('plain')) {
$this->table(['赛事', '渠道', '状态', '入口加密', '渠道编码', '测试入口 URL'], $rows);
}
return self::SUCCESS;
}
private function resolveTimestamp(): ?int
{
$raw = $this->option('timestamp');
if ($raw === null || trim((string) $raw) === '') {
return time();
}
$timestamp = trim((string) $raw);
if (! ctype_digit($timestamp)) {
$this->error('--timestamp 必须是秒级 Unix 时间戳。');
return null;
}
return (int) $timestamp;
}
private function resolveBaseUrl(): string
{
$raw = trim((string) ($this->option('base-url') ?: config('app.url', 'http://localhost')));
$raw = $raw !== '' ? $raw : 'http://localhost';
return rtrim($raw, '/');
}
/**
* @param Builder<SignupChannel> $query
*/
private function applyCompetitionFilter(Builder $query): void
{
$competition = trim((string) $this->option('competition'));
if ($competition === '') {
return;
}
$query->whereHas('competition', function (Builder $builder) use ($competition): void {
if (ctype_digit($competition)) {
$builder->where('id', (int) $competition);
} else {
$builder->where('slug', $competition);
}
});
}
/**
* @param Builder<SignupChannel> $query
*/
private function applyChannelFilter(Builder $query): void
{
$channel = trim((string) $this->option('channel'));
if ($channel === '') {
return;
}
if (ctype_digit($channel)) {
$query->where('id', (int) $channel);
} else {
$query->where('channel_code', $channel);
}
}
private function stringOption(string $name): string
{
$value = $this->option($name);
return is_scalar($value) ? trim((string) $value) : '';
}
private function protectedValue(string $value, SignupChannel $channel, string $publicKey): string
{
return $value === '' || ! $channel->entry_encryption_enabled
? $value
: ChannelEntryEncryption::encrypt($value, $publicKey);
}
}