diff --git a/serve/app/Console/Commands/SystemInit.php b/serve/app/Console/Commands/SystemInit.php index 4178ee7..65acfcd 100755 --- a/serve/app/Console/Commands/SystemInit.php +++ b/serve/app/Console/Commands/SystemInit.php @@ -52,6 +52,26 @@ class SystemInit extends Command private function config_data(): array { return [ + [ + 'slug' => 'web_site_title', + 'value' => '蚂蚁快链-智慧引流工具', + 'desc' => '网站名称', + ], + [ + 'slug' => 'web_site_logo', + 'value' => '/image/toplogo.png', + 'desc' => '网站logo浅色', + ], + [ + 'slug' => 'web_site_bottom_logo', + 'value' => '/image/toplogo.png', + 'desc' => '网站logo深色', + ], + [ + 'slug' => 'web_site_customer_service', + 'value' => '/image/web-qr.png', + 'desc' => '客服二维码', + ], [ 'slug' => 'is_give_vip', 'value' => 1, diff --git a/serve/app/Enums/LinkType.php b/serve/app/Enums/LinkType.php index 38c05c0..8f2400a 100644 --- a/serve/app/Enums/LinkType.php +++ b/serve/app/Enums/LinkType.php @@ -10,6 +10,7 @@ enum LinkType: int case CLI_QR = 3; case WORK_WECHAT = 4; case LANDING_MINI = 5; + case QR_QQ = 11; // 腾讯优码 // 获取所有的类型 public static function getAllType(): array diff --git a/serve/app/Forms/BaseConfig.php b/serve/app/Forms/BaseConfig.php index 4ab829f..ef95f49 100644 --- a/serve/app/Forms/BaseConfig.php +++ b/serve/app/Forms/BaseConfig.php @@ -19,7 +19,6 @@ class BaseConfig implements SimpleForm 'recommend_commission_2' => 'nullable|integer', 'member_pay_commission_1' => 'nullable|integer', // 一级邀请人消费提成 1% 'member_pay_commission_2' => 'nullable|integer', // 二级邀请人消费提成 1% - 'send_code_mode' => 'required|integer|in:1,2', // 1短信2邮箱 ]); } diff --git a/serve/app/Forms/WebSite.php b/serve/app/Forms/WebSite.php new file mode 100644 index 0000000..60ac16b --- /dev/null +++ b/serve/app/Forms/WebSite.php @@ -0,0 +1,75 @@ +validate([ + 'web_site_customer_service' => 'nullable', + 'web_site_title' => 'nullable', + 'web_site_logo' => 'nullable', + 'web_site_bottom_logo' => 'nullable', + + 'verify_code_is_open' => 'required|boolean', + 'send_code_mode' => 'nullable|integer|in:1,2', // 1短信2邮箱 + 'ali_sms_key' => 'nullable', + 'ali_sms_secret' => 'nullable', + 'ali_sms_sign_name' => 'nullable', + + 'mail_from_name' => 'nullable', + 'mail_host' => 'nullable', + 'mail_port' => 'nullable', + 'mail_from_address' => 'nullable', + 'mail_username' => 'nullable', + 'mail_password' => 'nullable', + + 'wechat_pay_app_id' => 'nullable', + 'wechat_pay_mch_id' => 'nullable', + 'wechat_pay_secret_key' => 'nullable', + 'wechat_pay_private_cert' => 'nullable', + 'wechat_pay_certificate' => 'nullable', + ]); + } + + // 保存 + public function handle(array $input): void + { + SystemConfig::set($input); + } + + public function default(): array + { + return [ + 'web_site_customer_service' => SystemConfig::get('web_site_customer_service'), + 'web_site_title' => SystemConfig::get('web_site_title'), + 'web_site_logo' => SystemConfig::get('web_site_logo'), + 'web_site_bottom_logo' => SystemConfig::get('web_site_bottom_logo'), + + 'verify_code_is_open' => SystemConfig::get('verify_code_is_open'), // 开启 + 'send_code_mode' => SystemConfig::get('send_code_mode'), // + + 'ali_sms_key' => SystemConfig::get('ali_sms_key'), + 'ali_sms_secret' => SystemConfig::get('ali_sms_secret'), + 'ali_sms_sign_name' => SystemConfig::get('ali_sms_sign_name'), + + 'mail_from_name' => SystemConfig::get('mail_from_name'), + 'mail_host' => SystemConfig::get('mail_host'), + 'mail_port' => SystemConfig::get('mail_port'), + 'mail_from_address' => SystemConfig::get('mail_from_address'), + 'mail_username' => SystemConfig::get('mail_username'), + 'mail_password' => SystemConfig::get('mail_password'), + + 'wechat_pay_app_id' => SystemConfig::get('wechat_pay_app_id'), + 'wechat_pay_mch_id' => SystemConfig::get('wechat_pay_mch_id'), + 'wechat_pay_secret_key' => SystemConfig::get('wechat_pay_secret_key'), + 'wechat_pay_private_cert' => SystemConfig::get('wechat_pay_secret_key'), // 私钥证书 + 'wechat_pay_certificate' => SystemConfig::get('wechat_pay_certificate'), // 公钥证书 + ]; + } +} diff --git a/serve/app/Http/Controllers/Api/ConfigController.php b/serve/app/Http/Controllers/Api/ConfigController.php index 3486ec5..92f555b 100644 --- a/serve/app/Http/Controllers/Api/ConfigController.php +++ b/serve/app/Http/Controllers/Api/ConfigController.php @@ -3,11 +3,13 @@ namespace App\Http\Controllers\Api; use App\Forms\BaseConfig; +use App\Forms\WebSite; use Ugly\Base\Http\Controllers\SimpleFormController; class ConfigController extends SimpleFormController { protected array $form = [ 'base' => BaseConfig::class, + 'web_site' => WebSite::class, ]; } diff --git a/serve/app/Http/Controllers/Api/IndexController.php b/serve/app/Http/Controllers/Api/IndexController.php index a4bc038..d1c43b7 100644 --- a/serve/app/Http/Controllers/Api/IndexController.php +++ b/serve/app/Http/Controllers/Api/IndexController.php @@ -114,19 +114,22 @@ class IndexController extends Controller $configs = [ 'code_mode' => SystemConfig::get('send_code_mode'), 'package' => VipPackage::query()->orderBy('level')->get(['id', 'name', 'price', 'config']), + 'web_site_customer_service' => SystemConfig::get('web_site_customer_service'), + 'web_site_title' => SystemConfig::get('web_site_title'), + 'web_site_logo' => SystemConfig::get('web_site_logo'), + 'web_site_bottom_logo' => SystemConfig::get('web_site_bottom_logo'), + 'asset_url' => Storage::url(''), // 图片路径 ]; $user = auth('api')->user(); if ($user) { - // 图片路径 - $configs['asset_url'] = Storage::url(''); // 安全域名 // $configs['domains'] = Domain::query()->where('enable', true)->get(['id', 'title']); // 小程序 (区分是否有权限使用平台小程序池) $configs['mini_programs'] = MiniProgram::query() ->where( - fn($query) => $query->where('user_id', $user->id) - ->when($user->getPackageConfig('pre_min'), fn($query) => $query->orWhere('is_pre_min', true)) + fn ($query) => $query->where('user_id', $user->id) + ->when($user->getPackageConfig('pre_min'), fn ($query) => $query->orWhere('is_pre_min', true)) ) ->get(['id', 'name']); } diff --git a/serve/app/Http/Controllers/Api/JumpController.php b/serve/app/Http/Controllers/Api/JumpController.php new file mode 100644 index 0000000..a43885a --- /dev/null +++ b/serve/app/Http/Controllers/Api/JumpController.php @@ -0,0 +1,319 @@ +where('code', $code)->first(); + if (empty($link)) { + return $this->failed('此链接已被删除!'); + } + if ($link->expired_at < now()) { + return $this->failed('链接已过期!'); + } + $user = User::query()->findOrFail($link->user_id); + if (empty($user->status)) { + return $this->failed('账号被禁用!'); + } + $isAdmin = $user->type === UserType::Admin; + if (! $isAdmin && ! $user->vip_id) { + return $this->failed('会员已到期,禁止访问!'); + } + + $device_uid = $request->input('device_uid') ?: Str::uuid(); + $cache = data_get(LinkVisitLog::query() + ->where('link_id', $link->id) + ->where('device_uid', $device_uid) + ->orderByDesc('id') + ->first(), 'cache', []); + if (empty($cache)) { + if (! $isAdmin) { + // 会员UV限制 + $totalUV = LinkVisitLog::query() + ->where('link_id', $link->id) + ->groupBy('device_uid') + ->whereBetween('created_at', [$user->start_at, $user->end_at]) + ->count(); + $limit = (int) data_get(VipPackage::query()->find($user->vip_id), 'config.uv_limit'); + + if ($limit > 0 && $totalUV >= $limit) { + return $this->failed('当前访问已达上限!'); + } + } + + $cache = [ + 'title' => $link->title, + 'description' => $link->description, + 'icon' => Storage::url($link->icon), + ]; + if ( + $link->type === LinkType::MINI_PROGRAM || + $link->type === LinkType::LANDING_MINI + ) { + // 跳转到小程序 参数 + $mp = MiniProgram::query()->findOrFail(data_get($link, 'config.min_id')); + if (empty($mp) || ! $mp->is_enable) { + return $this->failed('当前小程序不能使用'); + } + if ($link->type === LinkType::LANDING_MINI) { + $qr = $this->getWechatNextQr($link); + if (empty($qr)) { + return $this->failed('当前小程序已无可用二维码!'); + } + } + $cache['params'] = [ + 'appid' => $mp->app_id, + 'secret' => $mp->secret, + 'qr' => $qr ?? null, + 'path' => data_get($link, 'config.url') ?: $mp->url, + ]; + } elseif ($link->type === LinkType::KING_DOC) { + $cache['params']['sid'] = str_replace('https://kdocs.cn/l/', '', data_get($link, 'config.url')); + } elseif ($link->type === LinkType::CLI_QR) { + /** + * 1、电脑端访问草料二维码官网:https://cli.im/img + * 2、上传微信二维码、公众号二维码、企业微信二维码、视频号二维码等等,然后点生成活码 + * 3、后台创建草料 + */ + $configUrl = str_replace('https://qr61.cn/', '', data_get($link, 'config.url')); + [$userID, $cliID] = explode('/', $configUrl); + $cache['params'] = [ + 'userID' => $userID, + 'cliID' => $cliID, + ]; + } elseif ($link->type === LinkType::WORK_WECHAT) { + $cache['params'] = [ + 'url' => data_get($link, 'config.url'), + ]; + } elseif ($link->type === LinkType::QR_QQ) { + $cache['params'] = [ + 'url' => data_get($link, 'config.url'), + ]; + } else { + return $this->failed('暂不支持此链接跳转!'); + } + } else { + unset($cache['target']); + } + + // 生成链接 + if ($link->type === LinkType::WORK_WECHAT) { + $cache['target'] = data_get($cache, 'params.url'); + } elseif ($link->type === LinkType::CLI_QR) { + $userID = data_get($cache, 'params.userID'); + $cliID = data_get($cache, 'params.cliID'); + $getTicketUrl = file_get_contents('https://nc.cli.im/api/weixin/getWxUrlScheme/?query=q=qr61.cn/'.$userID.'/'.$cliID.'&path=pages/code/code&appid=wx5db79bd23a923e8e&org_coding='.$userID); + $fetchUrlData = file_get_contents(data_get(json_decode($getTicketUrl, true), 'data.wx_url_scheme.fetchUrl')); + $arr = json_decode($fetchUrlData, true); + $cache['target'] = data_get($arr, 'data.urlScheme'); + } elseif ($link->type === LinkType::QR_QQ) { + $configUrl = data_get($cache, 'params.url'); + $getContent = file_get_contents($configUrl); + $pattern = '/weixin:\/\/dl\/business\/\?t=[^\\\']+/'; + preg_match($pattern, $getContent, $matches); + $cache['target'] = $matches[0]; + } elseif ($link->type === LinkType::KING_DOC) { + $sidUrl = data_get($cache, 'params.sid'); + preg_match('/https:\/\/kdocs\.cn\/l\/([a-zA-Z0-9]+)/', $sidUrl, $matches); + $url_link = Http::withUserAgent('Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36') + ->get('https://account.kdocs.cn/api/v3/miniprogram/urllink', [ + 'appid' => 'wx5b97b0686831c076', + 'path' => 'pages/navigate/navigate', + 'query' => http_build_query([ + 'url' => 'pages/preview/preview?from=wxminiprogram&fid=256465035925&sid='.$matches[1].'&fname='.urlencode('扫码加微信.docx'), + 'scene' => '102', + 'jump_from' => 'wechatlogin_guide_passive', + 'comp' => 'docx', + 'dw' => '1', + ]), + 'env_version' => 'release', + 'is_expire' => 'true', + 'expire_time' => time() + 7200, + ])->json('url_link'); + $jinShanResponse = Http::get($url_link)->throw(); + preg_match("/url_scheme:\s*'([^']+)'/", $jinShanResponse, $matches); + + $cache['target'] = $matches[1]; + } else { + // 小程序 + $miniApp = new WechatApp([ + 'app_id' => data_get($cache, 'params.appid'), + 'secret' => data_get($cache, 'params.secret'), + ]); + try { + $res = $miniApp->getClient()->postJson('wxa/generatescheme', [ + 'jump_wxa' => [ + 'path' => data_get($cache, 'params.path'), + 'query' => 'code='.data_get($link, 'code').'&device_uid='.$device_uid, + ], + ]) + ->toArray(); + if (isset($res['errcode']) && $res['errcode'] === 40001) { + $cache = new Psr16Cache(new FilesystemAdapter('easywechat', 1500)); + $cache->clear(); + } + } catch (\Exception $e) { + $res = [ + 'errcode' => 1, + 'errmsg' => '小程序配置错误', + ]; + } + if (! empty($res['errcode'])) { + return $this->failed($res['errmsg']); + } + + $cache['target'] = data_get($res, 'openlink'); + } + + if (isset($cache['target']) && $cache['target']) { + LinkVisitLog::query()->create([ + 'link_id' => $link->id, + 'user_id' => $link->user_id, + 'ip' => $request->getClientIp(), + 'device_uid' => $device_uid, + 'cache' => $cache, + ]); + unset($cache['params']); + + return $this->success($cache); + } else { + return $this->failed('访问错误!'); + } + } + + // 获取需要展示的二维码 + public function getShowQr($code): JsonResponse + { + $link = Link::query()->where('code', $code)->first(); + if (empty($link) || ! $link->status) { + return $this->success([ + 'id' => '', + 'avatar' => '', + 'title' => '数据不存在~', + 'sub_title' => '', + 'qr' => '', + ]); + } + $config = data_get($link, 'config.wx'); + $device_uid = request()->input('device_uid') ?: Str::uuid(); + $cache_qr = data_get(LinkVisitLog::query() + ->where('link_id', $link->id) + ->where('device_uid', $device_uid) + ->orderByDesc('id') + ->first(), 'cache.params.qr', []); + if (empty($cache_qr)) { + return $this->failed('参数错误!'); + } + + return $this->success([ + 'id' => $link->id, + 'avatar' => Storage::url(data_get($config, 'avatar')), + 'title' => data_get($config, 'title'), + 'sub_title' => data_get($config, 'sub_title'), + 'qr' => Storage::url(data_get($cache_qr, 'path')), + ]); + } + + // 获取下一次访问时展示的二维码 + private function getWechatNextQr($link) + { + $qrCodes = data_get($link, 'config.wx.qr', []); + // 对链接中的二维码们按照 sort 从小到大排序 + usort($qrCodes, function ($a, $b) { + return $a['sort'] <=> $b['sort']; + }); + + $UVLimitType = (int) data_get($link, 'config.wx.uv_limit_type'); + $keyPrefix = "link-wx-type{$UVLimitType}-{$link->id}"; + + $singleUVViews = []; + if ($UVLimitType === UVLimitType::ACCUMULATE->value) { + $key = "{$keyPrefix}-accumulate"; + $singleUVViews = Redis::hgetall($key); + } elseif ($UVLimitType === UVLimitType::DAILY->value) { + $ymdDate = date('Ymd'); + $key = "{$keyPrefix}-daily-{$ymdDate}"; + $singleUVViews = Redis::hgetall($key); + } + + // 排除掉已过期的二维码和超出访问上限的二维码 + $filteredQrCodes = array_filter($qrCodes, function ($qrCode) use ($singleUVViews) { + $expiredAt = ! empty($expiredAt) ? Carbon::parse($qrCode['expired_at']) : now()->addHour(); + $sort = $qrCode['sort']; + $visitUV = $singleUVViews[$sort] ?? 0; + $uvLimitNum = $qrCode['uv_limit_num'] ?? 99999999; + + return $expiredAt->greaterThan(now()) && (is_null($uvLimitNum) || $visitUV < $uvLimitNum); + }); + if (empty($filteredQrCodes)) { + return []; + } + $filteredQrCodes = array_values($filteredQrCodes); + + // 切换方式. + $nextIndex = 0; // 默认返回第一个二维码 + $switch_type = data_get($link, 'config.wx.switch_type'); + if ($switch_type === SwitchType::SEQUENCE->value) { + // 顺序切换 + $lockKey = "link-sequence-{$link->id}"; + $indexKey = "link-sequence-index-{$link->id}"; + $lock = Cache::lock($lockKey, 1); + if ($lock->get()) { + try { + $currentIndex = Cache::get($indexKey, -1); + $nextIndex = ($currentIndex + 1) % count($filteredQrCodes); + Cache::put($indexKey, $nextIndex); + } finally { + $lock->release(); + } + } else { + Log::warning('获取锁失败', ['key' => $lockKey]); + } + } elseif ($switch_type === SwitchType::RANDOM->value) { + // 随机切换 + $nextIndex = array_rand($filteredQrCodes); + } + + $selectedQrCode = $filteredQrCodes[$nextIndex]; + // 更新 Redis 中的访问计数 + if ($UVLimitType === UVLimitType::ACCUMULATE->value) { + $key = "{$keyPrefix}-accumulate"; + Redis::hincrby($key, $selectedQrCode['sort'], 1); + } elseif ($UVLimitType === UVLimitType::DAILY->value) { + $ymdDate = date('Ymd'); + $key = "{$keyPrefix}-daily-{$ymdDate}"; + Redis::hincrby($key, $selectedQrCode['sort'], 1); + } + + return $selectedQrCode; + } +} diff --git a/serve/app/Http/Controllers/Api/LinkController.php b/serve/app/Http/Controllers/Api/LinkController.php index c1e3026..f405bcf 100644 --- a/serve/app/Http/Controllers/Api/LinkController.php +++ b/serve/app/Http/Controllers/Api/LinkController.php @@ -8,25 +8,11 @@ use App\Enums\UserType; use App\Enums\UVLimitType; use App\Models\Domain; use App\Models\Link; -use App\Models\LinkVisitLog; -use App\Models\MiniProgram; -use App\Models\User; -use App\Models\VipPackage; -use EasyWeChat\MiniApp\Application as WechatApp; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Redis; -use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Enum; -use Symfony\Component\Cache\Adapter\FilesystemAdapter; -use Symfony\Component\Cache\Psr16Cache; use Ugly\Base\Http\Controllers\FormController; use Ugly\Base\Services\FormService; @@ -172,6 +158,10 @@ class LinkController extends FormController $res['config.url'] = 'required|url|starts_with:https://qr61.cn/'; } + if ($iptType === LinkType::QR_QQ->value) { + $res['config.url'] = 'required|url|starts_with:https://ym.link/'; + } + return $is_wx ? array_merge($res, [ 'config.wx' => [ 'required_if:type,'.LinkType::LANDING_MINI->value, @@ -192,283 +182,6 @@ class LinkController extends FormController ]) : $res; } - // 获取链接跳转目标 - public function target(Request $request, $code): JsonResponse - { - $link = Link::query()->where('code', $code)->first(); - if (empty($link)) { - return $this->failed('此链接已被删除!'); - } - if ($link->expired_at < now()) { - return $this->failed('链接已过期!'); - } - $user = User::query()->findOrFail($link->user_id); - if (empty($user->status)) { - return $this->failed('账号被禁用!'); - } - $isAdmin = $user->type === UserType::Admin; - if (! $isAdmin && ! $user->vip_id) { - return $this->failed('会员已到期,禁止访问!'); - } - - $device_uid = $request->input('device_uid') ?: Str::uuid(); - $cache = data_get(LinkVisitLog::query() - ->where('link_id', $link->id) - ->where('device_uid', $device_uid) - ->orderByDesc('id') - ->first(), 'cache', []); - if (empty($cache)) { - if (! $isAdmin) { - // 会员UV限制 - $totalUV = LinkVisitLog::query() - ->where('link_id', $link->id) - ->groupBy('device_uid') - ->whereBetween('created_at', [$user->start_at, $user->end_at]) - ->count(); - $limit = (int) data_get(VipPackage::query()->find($user->vip_id), 'config.uv_limit'); - - if ($limit > 0 && $totalUV >= $limit) { - return $this->failed('当前访问已达上限!'); - } - } - - $cache = [ - 'title' => $link->title, - 'description' => $link->description, - 'icon' => Storage::url($link->icon), - ]; - if ( - $link->type === LinkType::MINI_PROGRAM || - $link->type === LinkType::LANDING_MINI - ) { - // 跳转到小程序 参数 - $mp = MiniProgram::query()->findOrFail(data_get($link, 'config.min_id')); - if (empty($mp) || ! $mp->is_enable) { - return $this->failed('当前小程序不能使用'); - } - if ($link->type === LinkType::LANDING_MINI) { - $qr = $this->getWechatNextQr($link); - if (empty($qr)) { - return $this->failed('当前小程序已无可用二维码!'); - } - } - $cache['params'] = [ - 'appid' => $mp->app_id, - 'secret' => $mp->secret, - 'qr' => $qr ?? null, - 'path' => data_get($link, 'config.url') ?: $mp->url, - ]; - } elseif ($link->type === LinkType::KING_DOC) { - $cache['params']['sid'] = str_replace('https://kdocs.cn/l/', '', data_get($link, 'config.url')); - } elseif ($link->type === LinkType::CLI_QR) { - /** - * 1、电脑端访问草料二维码官网:https://cli.im/img - * 2、上传微信二维码、公众号二维码、企业微信二维码、视频号二维码等等,然后点生成活码 - * 3、后台创建草料 - */ - $configUrl = str_replace('https://qr61.cn/', '', data_get($link, 'config.url')); - [$userID, $cliID] = explode('/', $configUrl); - $cache['params'] = [ - 'userID' => $userID, - 'cliID' => $cliID, - ]; - } elseif ($link->type === LinkType::WORK_WECHAT) { - $cache['params'] = [ - 'url' => data_get($link, 'config.url'), - ]; - } else { - return $this->failed('暂不支持此链接跳转!'); - } - } else { - unset($cache['target']); - } - - // 生成链接 - if ($link->type === LinkType::WORK_WECHAT) { - $cache['target'] = data_get($cache, 'params.url'); - } elseif ($link->type === LinkType::CLI_QR) { - $userID = data_get($cache, 'params.userID'); - $cliID = data_get($cache, 'params.cliID'); - $getTicketUrl = file_get_contents('https://nc.cli.im/api/weixin/getWxUrlScheme/?query=q=qr61.cn/'.$userID.'/'.$cliID.'&path=pages/code/code&appid=wx5db79bd23a923e8e&org_coding='.$userID); - $fetchUrlData = file_get_contents(data_get(json_decode($getTicketUrl, true), 'data.wx_url_scheme.fetchUrl')); - $arr = json_decode($fetchUrlData, true); - $cache['target'] = data_get($arr, 'data.urlScheme'); - } elseif ($link->type === LinkType::KING_DOC) { - $sidUrl = data_get($cache, 'params.sid'); - preg_match('/https:\/\/kdocs\.cn\/l\/([a-zA-Z0-9]+)/', $sidUrl, $matches); - $url_link = Http::withUserAgent('Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36') - ->get('https://account.kdocs.cn/api/v3/miniprogram/urllink', [ - 'appid' => 'wx5b97b0686831c076', - 'path' => 'pages/navigate/navigate', - 'query' => http_build_query([ - 'url' => 'pages/preview/preview?from=wxminiprogram&fid=256465035925&sid='.$matches[1].'&fname='.urlencode('扫码加微信.docx'), - 'scene' => '102', - 'jump_from' => 'wechatlogin_guide_passive', - 'comp' => 'docx', - 'dw' => '1', - ]), - 'env_version' => 'release', - 'is_expire' => 'true', - 'expire_time' => time() + 7200, - ])->json('url_link'); - $jinShanResponse = Http::get($url_link)->throw(); - preg_match("/url_scheme:\s*'([^']+)'/", $jinShanResponse, $matches); - - $cache['target'] = $matches[1]; - } else { - // 小程序 - $miniApp = new WechatApp([ - 'app_id' => data_get($cache, 'params.appid'), - 'secret' => data_get($cache, 'params.secret'), - ]); - try { - $res = $miniApp->getClient()->postJson('wxa/generatescheme', [ - 'jump_wxa' => [ - 'path' => data_get($cache, 'params.path'), - 'query' => 'code='.data_get($link, 'code').'&device_uid='.$device_uid, - ], - ])->toArray(); - if (isset($res['errcode']) && $res['errcode'] === 40001) { - $cache = new Psr16Cache(new FilesystemAdapter('easywechat', 1500)); - $cache->clear(); - } - } catch (\Exception $e) { - $res = [ - 'errcode' => 1, - 'errmsg' => '小程序配置错误', - ]; - } - if (! empty($res['errcode'])) { - return $this->failed($res['errmsg']); - } - - $cache['target'] = data_get($res, 'openlink'); - } - - if (isset($cache['target']) && $cache['target']) { - LinkVisitLog::query()->create([ - 'link_id' => $link->id, - 'user_id' => $link->user_id, - 'ip' => $request->getClientIp(), - 'device_uid' => $device_uid, - 'cache' => $cache, - ]); - - unset($cache['params']); - - return $this->success($cache); - } else { - return $this->failed('访问错误!'); - } - } - - // 获取需要展示的二维码 - public function getShowQr($code): JsonResponse - { - $link = Link::query()->where('code', $code)->first(); - if (empty($link) || ! $link->status) { - return $this->success([ - 'id' => '', - 'avatar' => '', - 'title' => '数据不存在~', - 'sub_title' => '', - 'qr' => '', - ]); - } - $config = data_get($link, 'config.wx'); - $device_uid = request()->input('device_uid') ?: Str::uuid(); - $cache_qr = data_get(LinkVisitLog::query() - ->where('link_id', $link->id) - ->where('device_uid', $device_uid) - ->orderByDesc('id') - ->first(), 'cache.params.qr', []); - if (empty($cache_qr)) { - return $this->failed('参数错误!'); - } - - return $this->success([ - 'id' => $link->id, - 'avatar' => Storage::url(data_get($config, 'avatar')), - 'title' => data_get($config, 'title'), - 'sub_title' => data_get($config, 'sub_title'), - 'qr' => Storage::url(data_get($cache_qr, 'path')), - ]); - } - - // 获取下一次访问时展示的二维码 - private function getWechatNextQr($link) - { - $qrCodes = data_get($link, 'config.wx.qr', []); - // 对链接中的二维码们按照 sort 从小到大排序 - usort($qrCodes, function ($a, $b) { - return $a['sort'] <=> $b['sort']; - }); - - $UVLimitType = (int) data_get($link, 'config.wx.uv_limit_type'); - $keyPrefix = "link-wx-type{$UVLimitType}-{$link->id}"; - - $singleUVViews = []; - if ($UVLimitType === UVLimitType::ACCUMULATE->value) { - $key = "{$keyPrefix}-accumulate"; - $singleUVViews = Redis::hgetall($key); - } elseif ($UVLimitType === UVLimitType::DAILY->value) { - $ymdDate = date('Ymd'); - $key = "{$keyPrefix}-daily-{$ymdDate}"; - $singleUVViews = Redis::hgetall($key); - } - - // 排除掉已过期的二维码和超出访问上限的二维码 - $filteredQrCodes = array_filter($qrCodes, function ($qrCode) use ($singleUVViews) { - $expiredAt = ! empty($expiredAt) ? Carbon::parse($qrCode['expired_at']) : now()->addHour(); - $sort = $qrCode['sort']; - $visitUV = $singleUVViews[$sort] ?? 0; - $uvLimitNum = $qrCode['uv_limit_num'] ?? 99999999; - - return $expiredAt->greaterThan(now()) && (is_null($uvLimitNum) || $visitUV < $uvLimitNum); - }); - if (empty($filteredQrCodes)) { - return []; - } - $filteredQrCodes = array_values($filteredQrCodes); - - // 切换方式. - $nextIndex = 0; // 默认返回第一个二维码 - $switch_type = data_get($link, 'config.wx.switch_type'); - if ($switch_type === SwitchType::SEQUENCE->value) { - // 顺序切换 - $lockKey = "link-sequence-{$link->id}"; - $indexKey = "link-sequence-index-{$link->id}"; - $lock = Cache::lock($lockKey, 1); - if ($lock->get()) { - try { - $currentIndex = Cache::get($indexKey, -1); - $nextIndex = ($currentIndex + 1) % count($filteredQrCodes); - Cache::put($indexKey, $nextIndex); - } finally { - $lock->release(); - } - } else { - Log::warning('获取锁失败', ['key' => $lockKey]); - } - } elseif ($switch_type === SwitchType::RANDOM->value) { - // 随机切换 - $nextIndex = array_rand($filteredQrCodes); - } - - $selectedQrCode = $filteredQrCodes[$nextIndex]; - // 更新 Redis 中的访问计数 - if ($UVLimitType === UVLimitType::ACCUMULATE->value) { - $key = "{$keyPrefix}-accumulate"; - Redis::hincrby($key, $selectedQrCode['sort'], 1); - } elseif ($UVLimitType === UVLimitType::DAILY->value) { - $ymdDate = date('Ymd'); - $key = "{$keyPrefix}-daily-{$ymdDate}"; - Redis::hincrby($key, $selectedQrCode['sort'], 1); - } - - return $selectedQrCode; - } - // 获取下拉数据源 public function link_list(): JsonResponse { diff --git a/serve/app/Http/Requests/LoginRequest.php b/serve/app/Http/Requests/LoginRequest.php index a5ff87d..31e2f91 100644 --- a/serve/app/Http/Requests/LoginRequest.php +++ b/serve/app/Http/Requests/LoginRequest.php @@ -23,17 +23,29 @@ class LoginRequest extends FormRequest */ public function rules(): array { - $code_mode = SystemConfig::get('send_code_mode'); - $rule = $code_mode == CodeMode::Email->value ? 'email' : 'regex:/^1[3-9]\d{9}$/'; - - return [ + $rules = [ 'username' => [ 'required', - 'exclude_if:username,admin', - $rule, + 'exists:users,username', ], 'password' => 'required|min:6', ]; + if (request('username') === 'admin') { + return $rules; + } + $is_open = SystemConfig::get('verify_code_is_open'); + if ($is_open) { + $code_mode = SystemConfig::get('send_code_mode'); + $rule = $code_mode == CodeMode::Email->value ? 'email' : 'regex:/^1[3-9]\d{9}$/'; + + $rules['username'] = [ + 'required', + 'exists:users,username', + $rule, + ]; + } + + return $rules; } public function messages(): array diff --git a/serve/app/Http/Requests/RegisterRequest.php b/serve/app/Http/Requests/RegisterRequest.php index 93bf6e0..11e8c3c 100644 --- a/serve/app/Http/Requests/RegisterRequest.php +++ b/serve/app/Http/Requests/RegisterRequest.php @@ -24,33 +24,50 @@ class RegisterRequest extends FormRequest */ public function rules(): array { - $code_mode = SystemConfig::get('send_code_mode'); - $rule = $code_mode == CodeMode::Email->value ? 'email' : 'regex:/^1[3-9]\d{9}$/'; + $is_open = SystemConfig::get('verify_code_is_open'); + if ($is_open) { + $code_mode = SystemConfig::get('send_code_mode'); + $rule = ($code_mode == CodeMode::Email->value) ? 'email' : 'regex:/^1[3-9]\d{9}$/'; + + return [ + 'username' => [ + 'required', + $rule, + 'unique:users,username', + ], + 'password' => 'required|min:6|confirmed', + 'captcha' => ['required', function (string $attribute, mixed $value, \Closure $fail) { + $test_code = config('services.ali_sms.test_code'); + if (empty($test_code) || $value != $test_code) { + $tel = $this->input('username'); + $captcha = Cache::get('sms_captcha_'.$tel); + if ((int) $captcha !== (int) $value) { + $fail('验证码错误!'); + } + } + }], + 'referral_code' => '', // 推荐码 + ]; + } return [ 'username' => [ 'required', - $rule, 'unique:users,username', ], 'password' => 'required|min:6|confirmed', - 'captcha' => ['required', function (string $attribute, mixed $value, \Closure $fail) { - $test_code = config('services.ali_sms.test_code'); - if (empty($test_code) || $value != $test_code) { - $tel = $this->input('username'); - $captcha = Cache::get('sms_captcha_'.$tel); - if ((int) $captcha !== (int) $value) { - $fail('验证码错误!'); - } - } - }], 'referral_code' => '', // 推荐码 ]; } public function messages(): array { - $txt = CodeMode::Email->value ? '邮箱' : '手机号'; + $code_mode = SystemConfig::get('send_code_mode'); + $is_open = SystemConfig::get('verify_code_is_open'); + $txt = '用户名'; + if (! empty($is_open)) { + $txt = ($code_mode === CodeMode::Email->value) ? '邮箱' : '手机号'; + } return [ 'username.required' => "请输入{$txt}!", diff --git a/serve/app/Jobs/SendEmailJobs.php b/serve/app/Jobs/SendEmailJobs.php index 90bd989..5460422 100644 --- a/serve/app/Jobs/SendEmailJobs.php +++ b/serve/app/Jobs/SendEmailJobs.php @@ -4,11 +4,13 @@ namespace App\Jobs; use App\Jobs\Middleware\RateLimited; use App\Mail\SendEmail; +use App\Services\SystemConfig; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Mail; use Throwable; @@ -31,6 +33,13 @@ class SendEmailJobs implements ShouldQueue $this->to = $to; $this->code = $code; $this->title = $title ?? '邮箱验证码'; + + Config::set('mail.mailers.smtp.host', SystemConfig::get('mail_host')); + Config::set('mail.mailers.smtp.port', SystemConfig::get('mail_port')); + Config::set('mail.mailers.smtp.username', SystemConfig::get('mail_username')); + Config::set('mail.mailers.smtp.password', SystemConfig::get('mail_password')); + Config::set('mail.from.address', SystemConfig::get('mail_from_address')); + Config::set('mail.from.name', SystemConfig::get('mail_from_name')); } public function middleware() diff --git a/serve/app/PayChannels/WeChatPayNative.php b/serve/app/PayChannels/WeChatPayNative.php index 0853d1f..c419743 100644 --- a/serve/app/PayChannels/WeChatPayNative.php +++ b/serve/app/PayChannels/WeChatPayNative.php @@ -2,6 +2,7 @@ namespace App\PayChannels; +use App\Services\SystemConfig; use EasyWeChat\Pay\Application; use Illuminate\Support\Carbon; use Symfony\Component\HttpFoundation\Response; @@ -17,7 +18,21 @@ class WeChatPayNative public function __construct() { - $this->payApp = new Application(config('services.wechat_pay')); + $config = [ + 'appid' => SystemConfig::get('wechat_pay_app_id'), + 'mch_id' => SystemConfig::get('wechat_pay_mch_id'), + 'secret_key' => SystemConfig::get('wechat_pay_secret_key'), + // 商户证书 + 'private_key' => SystemConfig::get('wechat_pay_secret_key'), + 'certificate' => SystemConfig::get('wechat_pay_certificate'), + /*'platform_certs' => [ + storage_path('/certs/wechatpay.pem'), + ],*/ + 'http' => [ + 'throw' => false, + ], + ]; + $this->payApp = new Application($config); } // 支付 diff --git a/serve/app/Services/AliDySms.php b/serve/app/Services/AliDySms.php index 3bdf7b0..37de745 100644 --- a/serve/app/Services/AliDySms.php +++ b/serve/app/Services/AliDySms.php @@ -17,7 +17,13 @@ class AliDySms public function __construct() { - $this->config = config('services.ali_sms'); + $config = [ + 'key' => SystemConfig::get('ali_sms_key'), + 'secret' => SystemConfig::get('ali_sms_secret'), + 'sign_name' => SystemConfig::get('ali_sms_sign_name'), + 'test_code' => env('ALI_SMS_TEST_CODE'), + ]; + $this->config = $config; } /** diff --git a/serve/resources/views/welcome.blade.php b/serve/resources/views/welcome.blade.php index aa5c15a..a34e624 100644 --- a/serve/resources/views/welcome.blade.php +++ b/serve/resources/views/welcome.blade.php @@ -6,7 +6,7 @@ - 火星快链-智慧引流工具 + {!! \App\Services\SystemConfig::get('web_site_title') !!}