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

<?php
namespace App\Console\Commands;
use App\Models\SignupChannel;
4 months ago
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;
4 months ago
$publicKey = (string) $channel->encryption_public_key;
$parameters = [
'channel_code' => (string) $channel->channel_code,
'timestamp' => (string) $timestamp,
4 months ago
'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);
4 months ago
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) : '';
}
4 months ago
private function protectedValue(string $value, SignupChannel $channel, string $publicKey): string
{
return $value === '' || ! $channel->entry_encryption_enabled
? $value
: ChannelEntryEncryption::encrypt($value, $publicKey);
}
}