|
|
<?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);
|
|
|
}
|
|
|
}
|