bootstrapDefaults(); $configs = Db::name('system_configs') ->whereIn('config_group', array_keys($this->definitions())) ->order('config_group', 'asc') ->order('config_key', 'asc') ->select() ->toArray(); $configMap = []; foreach ($configs as $item) { $configMap[$item['config_group'] . '.' . $item['config_key']] = $item['config_value'] ?? ''; } $this->applyDerivedConfigValues($configMap); $groups = []; foreach ($this->definitions() as $groupCode => $group) { $groups[] = [ 'group_code' => $groupCode, 'group_name' => $group['group_name'], 'group_desc' => $group['group_desc'], 'items' => array_map(function (array $item) use ($groupCode, $configMap) { return [ 'config_key' => $item['config_key'], 'title' => $item['title'], 'field_type' => $item['field_type'], 'placeholder' => $item['placeholder'], 'remark' => $item['remark'], 'is_secret' => (bool)$item['is_secret'], 'read_only' => (bool)($item['read_only'] ?? false), 'options' => $item['options'] ?? [], 'visible_when' => $item['visible_when'] ?? null, 'value' => $configMap[$groupCode . '.' . $item['config_key']] ?? '', ]; }, $group['items']), ]; } return api_success(['groups' => $groups]); } public function save(Request $request) { $items = $request->input('items', []); if (!is_array($items) || !$items) { return api_error('配置项不能为空', 422); } $definitions = $this->definitions(); $allowedMap = []; foreach ($definitions as $groupCode => $group) { foreach ($group['items'] as $item) { $allowedMap[$groupCode . '.' . $item['config_key']] = true; } } $configValueMap = []; foreach ($definitions as $groupCode => $group) { foreach ($group['items'] as $item) { $configValueMap[$groupCode . '.' . $item['config_key']] = (string)Db::name('system_configs') ->where('config_group', $groupCode) ->where('config_key', $item['config_key']) ->value('config_value'); } } $submittedConfigKeys = []; foreach ($items as $item) { if (!is_array($item)) { continue; } $groupCode = trim((string)($item['config_group'] ?? '')); $configKey = trim((string)($item['config_key'] ?? '')); $mapKey = $groupCode . '.' . $configKey; if ($groupCode === '' || $configKey === '' || !isset($allowedMap[$mapKey])) { continue; } $configValueMap[$mapKey] = (string)($item['config_value'] ?? ''); $submittedConfigKeys[$mapKey] = [ 'config_group' => $groupCode, 'config_key' => $configKey, ]; } $this->applyDerivedConfigValues($configValueMap); if (isset($submittedConfigKeys['h5.page_base_url']) || isset($submittedConfigKeys['h5.oauth_redirect_url'])) { $submittedConfigKeys['h5.oauth_redirect_url'] = [ 'config_group' => 'h5', 'config_key' => 'oauth_redirect_url', ]; } try { $this->validateConfigValues($configValueMap); } catch (\RuntimeException $e) { return api_error($e->getMessage(), 422); } $now = date('Y-m-d H:i:s'); Db::startTrans(); try { foreach ($submittedConfigKeys as $mapKey => $configMeta) { $groupCode = $configMeta['config_group']; $configKey = $configMeta['config_key']; $configValue = (string)($configValueMap[$mapKey] ?? ''); $mapKey = $groupCode . '.' . $configKey; if ($groupCode === '' || $configKey === '' || !isset($allowedMap[$mapKey])) { continue; } $exists = Db::name('system_configs') ->where('config_group', $groupCode) ->where('config_key', $configKey) ->find(); $payload = [ 'config_group' => $groupCode, 'config_key' => $configKey, 'config_value' => $configValue, 'remark' => '后台系统配置', 'updated_at' => $now, ]; if ($exists) { Db::name('system_configs')->where('id', $exists['id'])->update($payload); } else { $payload['created_at'] = $now; Db::name('system_configs')->insert($payload); } } Db::commit(); (new FileStorageConfigService())->clearCache(); } catch (\Throwable $e) { Db::rollback(); return api_error('系统配置保存失败', 500, [ 'detail' => $e->getMessage(), ]); } return api_success([], '系统配置已保存'); } public function uploadFile(Request $request) { $groupCode = trim((string)$request->input('config_group', '')); $configKey = trim((string)$request->input('config_key', '')); if ($groupCode === '' || $configKey === '') { return api_error('配置分组和配置项不能为空', 422); } $allowed = $this->uploadableConfigMap(); $mapKey = $groupCode . '.' . $configKey; if (!isset($allowed[$mapKey])) { return api_error('当前配置项不支持文件上传', 422); } $file = $request->file('file'); if (!$file || !$file->isValid()) { return api_error('上传文件无效', 422); } $originalName = (string)$file->getUploadName(); $extension = strtolower((string)$file->getUploadExtension()); if ($extension !== 'pem') { return api_error('仅支持上传 .pem 文件', 422); } $content = file_get_contents($file->getRealPath()); if (!is_string($content) || !str_contains($content, '-----BEGIN')) { return api_error('PEM 文件内容格式不正确', 422); } $storageDir = base_path() . '/storage/payment-certs'; if (!is_dir($storageDir)) { mkdir($storageDir, 0775, true); } $targetFilename = $allowed[$mapKey]['filename']; $targetPath = $storageDir . '/' . $targetFilename; file_put_contents($targetPath, $content); @chmod($targetPath, 0600); $now = date('Y-m-d H:i:s'); $exists = Db::name('system_configs') ->where('config_group', $groupCode) ->where('config_key', $configKey) ->find(); $payload = [ 'config_group' => $groupCode, 'config_key' => $configKey, 'config_value' => $targetPath, 'remark' => '后台系统配置', 'updated_at' => $now, ]; if ($exists) { Db::name('system_configs')->where('id', $exists['id'])->update($payload); } else { $payload['created_at'] = $now; Db::name('system_configs')->insert($payload); } return api_success([ 'config_group' => $groupCode, 'config_key' => $configKey, 'config_value' => $targetPath, 'file_name' => $targetFilename, 'original_name' => $originalName, ], '文件已上传'); } private function bootstrapDefaults(): void { $now = date('Y-m-d H:i:s'); foreach ($this->definitions() as $groupCode => $group) { foreach ($group['items'] as $item) { $exists = Db::name('system_configs') ->where('config_group', $groupCode) ->where('config_key', $item['config_key']) ->find(); if ($exists) { continue; } Db::name('system_configs')->insert([ 'config_group' => $groupCode, 'config_key' => $item['config_key'], 'config_value' => (string)($item['default_value'] ?? ''), 'remark' => '后台系统配置', 'created_at' => $now, 'updated_at' => $now, ]); } } } private function definitions(): array { return [ 'file_storage' => [ 'group_name' => '文件存储', 'group_desc' => '配置业务文件存储方式。支持本地磁盘或阿里云 OSS,切换为 OSS 后需填写对应 Bucket 与密钥资料。', 'items' => [ [ 'config_key' => 'driver', 'title' => '存储驱动', 'field_type' => 'select', 'placeholder' => '请选择文件存储方式', 'remark' => '本地模式写入服务器 public/uploads;OSS 模式写入阿里云对象存储。', 'is_secret' => false, 'default_value' => 'local', 'options' => [ ['label' => '本地存储', 'value' => 'local'], ['label' => '阿里云 OSS', 'value' => 'oss'], ['label' => '七牛云 Kodo', 'value' => 'qiniu'], ], ], [ 'config_key' => 'public_base_url', 'title' => '公开访问域名', 'field_type' => 'text', 'placeholder' => '例如 https://api.anxinjianyan.com 或 https://static.example.com', 'remark' => '用于生成文件公网访问地址;本地可填 API 域名,OSS 可填自定义 CDN/回源域名,不填则按驱动自动推导。', 'is_secret' => false, ], [ 'config_key' => 'oss_endpoint', 'title' => 'OSS Endpoint', 'field_type' => 'text', 'placeholder' => '例如 oss-cn-shenzhen.aliyuncs.com', 'remark' => '后台服务端 SDK 使用的 Endpoint。可填公网 Endpoint;如服务器在同地域内网,也可填内网 Endpoint。', 'is_secret' => false, 'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'], ], [ 'config_key' => 'oss_upload_endpoint', 'title' => 'OSS 直传 Endpoint', 'field_type' => 'text', 'placeholder' => '例如 oss-cn-shenzhen.aliyuncs.com', 'remark' => '前端直传 OSS 使用的公网 Endpoint。为空时沿用 OSS Endpoint;如 OSS Endpoint 填了内网地址,这里必须填写公网地址。', 'is_secret' => false, 'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'], ], [ 'config_key' => 'oss_bucket', 'title' => 'OSS Bucket', 'field_type' => 'text', 'placeholder' => '请输入 Bucket 名称', 'remark' => '将作为所有业务文件的目标 Bucket。', 'is_secret' => false, 'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'], ], [ 'config_key' => 'oss_access_key_id', 'title' => 'OSS AccessKey ID', 'field_type' => 'text', 'placeholder' => '请输入 OSS AccessKey ID', 'remark' => '用于 OSS 文件上传、删除和存在性校验。', 'is_secret' => false, 'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'], ], [ 'config_key' => 'oss_access_key_secret', 'title' => 'OSS AccessKey Secret', 'field_type' => 'password', 'placeholder' => '请输入 OSS AccessKey Secret', 'remark' => '请妥善保管,仅后台可见。', 'is_secret' => true, 'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'], ], [ 'config_key' => 'oss_bucket_domain', 'title' => 'OSS 绑定域名', 'field_type' => 'text', 'placeholder' => '例如 https://static.anxinjianyan.com', 'remark' => '如 Bucket 已绑定自定义域名,可填写;不填则默认使用 https://bucket.endpoint。', 'is_secret' => false, 'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'], ], [ 'config_key' => 'oss_path_prefix', 'title' => 'OSS 路径前缀', 'field_type' => 'text', 'placeholder' => '例如 anxinyan-prod', 'remark' => '可选。填写后 OSS 对象会统一写入此前缀目录下。', 'is_secret' => false, 'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'], ], [ 'config_key' => 'direct_upload_max_size_mb', 'title' => '直传文件大小上限 MB', 'field_type' => 'text', 'placeholder' => '默认 200', 'remark' => '前端直传 OSS 的单文件最大大小,单位 MB。建议按业务网络环境设置,允许范围 1-2048。', 'is_secret' => false, 'default_value' => '200', 'visible_when' => ['config_key' => 'driver', 'equals' => 'oss'], ], [ 'config_key' => 'qiniu_bucket', 'title' => '七牛 Bucket', 'field_type' => 'text', 'placeholder' => '请输入七牛 Kodo Bucket 名称', 'remark' => '将作为七牛云对象存储的目标 Bucket。', 'is_secret' => false, 'visible_when' => ['config_key' => 'driver', 'equals' => 'qiniu'], ], [ 'config_key' => 'qiniu_access_key', 'title' => '七牛 AccessKey', 'field_type' => 'text', 'placeholder' => '请输入七牛 AccessKey', 'remark' => '用于七牛文件上传、删除和存在性校验。', 'is_secret' => false, 'visible_when' => ['config_key' => 'driver', 'equals' => 'qiniu'], ], [ 'config_key' => 'qiniu_secret_key', 'title' => '七牛 SecretKey', 'field_type' => 'password', 'placeholder' => '请输入七牛 SecretKey', 'remark' => '请妥善保管,仅后台可见。', 'is_secret' => true, 'visible_when' => ['config_key' => 'driver', 'equals' => 'qiniu'], ], [ 'config_key' => 'qiniu_bucket_domain', 'title' => '七牛公网访问域名', 'field_type' => 'text', 'placeholder' => '例如 https://static.example.com 或 https://xxx.clouddn.com', 'remark' => '用于生成七牛文件公网访问地址。建议填写已绑定并可公开访问的域名。', 'is_secret' => false, 'visible_when' => ['config_key' => 'driver', 'equals' => 'qiniu'], ], [ 'config_key' => 'qiniu_path_prefix', 'title' => '七牛路径前缀', 'field_type' => 'text', 'placeholder' => '例如 anxinyan-prod', 'remark' => '可选。填写后七牛对象会统一写入此前缀目录下。', 'is_secret' => false, 'visible_when' => ['config_key' => 'driver', 'equals' => 'qiniu'], ], ], ], 'mini_program' => [ 'group_name' => '小程序配置', 'group_desc' => '配置微信小程序 AppID、密钥及消息通知相关参数。', 'items' => [ ['config_key' => 'app_id', 'title' => '小程序 AppID', 'field_type' => 'text', 'placeholder' => '请输入小程序 AppID', 'remark' => '用于小程序登录、消息与支付能力接入', 'is_secret' => false], ['config_key' => 'app_secret', 'title' => '小程序 AppSecret', 'field_type' => 'password', 'placeholder' => '请输入小程序 AppSecret', 'remark' => '请妥善保管,仅后台可见', 'is_secret' => true], ['config_key' => 'original_id', 'title' => '原始 ID', 'field_type' => 'text', 'placeholder' => '请输入原始 ID', 'remark' => '用于公众号/小程序主体识别', 'is_secret' => false], ], ], 'h5' => [ 'group_name' => 'H5 配置', 'group_desc' => '配置 H5 接入、开放平台、回调地址以及公开页面域名。', 'items' => [ ['config_key' => 'app_id', 'title' => 'H5 AppID', 'field_type' => 'text', 'placeholder' => '请输入 H5 AppID', 'remark' => '用于 H5 登录与开放平台接入', 'is_secret' => false], ['config_key' => 'app_secret', 'title' => 'H5 AppSecret', 'field_type' => 'password', 'placeholder' => '请输入 H5 AppSecret', 'remark' => '请妥善保管,仅后台可见', 'is_secret' => true], ['config_key' => 'oauth_redirect_url', 'title' => '授权回调地址', 'field_type' => 'text', 'placeholder' => '保存 H5 页面根地址后自动生成', 'remark' => '由 H5 页面根地址自动拼接,无需手动填写。', 'is_secret' => false, 'read_only' => true], ['config_key' => 'page_base_url', 'title' => 'H5 页面根地址', 'field_type' => 'text', 'placeholder' => '例如 https://m.anxinjianyan.com', 'remark' => '用于生成扫码查看报告和验真页的完整 H5 链接', 'is_secret' => false], ], ], 'payment' => [ 'group_name' => '支付与商户平台', 'group_desc' => '配置微信支付商户号、API 密钥、证书序列号等上线必要参数。', 'items' => [ ['config_key' => 'mch_id', 'title' => '商户号 MchID', 'field_type' => 'text', 'placeholder' => '请输入商户号', 'remark' => '微信支付商户平台分配的商户号', 'is_secret' => false], ['config_key' => 'api_v3_key', 'title' => 'APIv3 Key', 'field_type' => 'password', 'placeholder' => '请输入 APIv3 Key', 'remark' => '用于微信支付接口验签与解密', 'is_secret' => true], ['config_key' => 'merchant_serial_no', 'title' => '商户证书序列号', 'field_type' => 'text', 'placeholder' => '请输入商户证书序列号', 'remark' => '与商户 API 证书匹配', 'is_secret' => false], ['config_key' => 'apiclient_key_path', 'title' => 'apiclient_key.pem', 'field_type' => 'file', 'placeholder' => '请上传 apiclient_key.pem', 'remark' => '上传微信支付商户私钥文件,系统将保存到后端非公开目录', 'is_secret' => true], ['config_key' => 'apiclient_cert_path', 'title' => 'apiclient_cert.pem', 'field_type' => 'file', 'placeholder' => '请上传 apiclient_cert.pem', 'remark' => '上传微信支付商户证书文件,系统将保存到后端非公开目录', 'is_secret' => false], ['config_key' => 'merchant_private_key', 'title' => '商户私钥', 'field_type' => 'textarea', 'placeholder' => '请输入商户私钥内容', 'remark' => '用于支付签名,请妥善保管', 'is_secret' => true], ['config_key' => 'platform_certificate_serial', 'title' => '平台证书序列号', 'field_type' => 'text', 'placeholder' => '请输入微信支付平台证书序列号', 'remark' => '用于平台证书校验', 'is_secret' => false], ['config_key' => 'notify_url', 'title' => '支付回调地址', 'field_type' => 'text', 'placeholder' => '请输入支付回调通知地址', 'remark' => '支付成功后用于回调业务系统', 'is_secret' => false], ], ], 'sms' => [ 'group_name' => '短信配置', 'group_desc' => '配置阿里云短信服务 AccessKey、签名和登录验证码模板,用于手机号验证码登录。', 'items' => [ ['config_key' => 'access_key_id', 'title' => 'AccessKey ID', 'field_type' => 'text', 'placeholder' => '请输入阿里云 AccessKey ID', 'remark' => '用于调用阿里云短信 SendSms 接口', 'is_secret' => false], ['config_key' => 'access_key_secret', 'title' => 'AccessKey Secret', 'field_type' => 'password', 'placeholder' => '请输入阿里云 AccessKey Secret', 'remark' => '请妥善保管,仅后台可见', 'is_secret' => true], ['config_key' => 'sign_name', 'title' => '短信签名', 'field_type' => 'text', 'placeholder' => '请输入短信签名', 'remark' => '需与阿里云短信服务已审核通过的签名一致', 'is_secret' => false], ['config_key' => 'login_template_code', 'title' => '登录模板 Code', 'field_type' => 'text', 'placeholder' => '例如 SMS_123456789', 'remark' => '模板中需包含 code 变量', 'is_secret' => false], ['config_key' => 'region_id', 'title' => 'Region ID', 'field_type' => 'text', 'placeholder' => '默认 cn-hangzhou', 'remark' => '通常填写 cn-hangzhou', 'is_secret' => false], ['config_key' => 'endpoint', 'title' => '短信 Endpoint', 'field_type' => 'text', 'placeholder' => '默认可留空', 'remark' => '如不填写则按 SDK 默认规则解析', 'is_secret' => false], ], ], 'kuaidi100' => [ 'group_name' => '快递100', 'group_desc' => '配置快递100实时查询与物流订阅推送,用于订单寄送和回寄物流轨迹同步。', 'items' => [ [ 'config_key' => 'enabled', 'title' => '同步开关', 'field_type' => 'select', 'placeholder' => '请选择是否启用', 'remark' => '启用后,新提交的运单会尝试订阅快递100推送,后台进程会定时补查轨迹。', 'is_secret' => false, 'default_value' => 'disabled', 'options' => [ ['label' => '停用', 'value' => 'disabled'], ['label' => '启用', 'value' => 'enabled'], ], ], ['config_key' => 'customer', 'title' => 'Customer', 'field_type' => 'text', 'placeholder' => '请输入快递100 Customer', 'remark' => '实时查询接口签名使用的 Customer。', 'is_secret' => false, 'visible_when' => ['config_key' => 'enabled', 'equals' => 'enabled']], ['config_key' => 'key', 'title' => 'Key', 'field_type' => 'password', 'placeholder' => '请输入快递100 Key', 'remark' => '用于实时查询签名和订阅推送。请妥善保管。', 'is_secret' => true, 'visible_when' => ['config_key' => 'enabled', 'equals' => 'enabled']], ['config_key' => 'callback_url', 'title' => '推送回调地址', 'field_type' => 'text', 'placeholder' => '例如 https://api.example.com/api/open/kuaidi100/callback', 'remark' => '需公网可访问;生产建议填本系统 /api/open/kuaidi100/callback 的完整地址。', 'is_secret' => false, 'visible_when' => ['config_key' => 'enabled', 'equals' => 'enabled']], ['config_key' => 'callback_salt', 'title' => '回调 Salt', 'field_type' => 'password', 'placeholder' => '可选,需与快递100订阅参数保持一致', 'remark' => '用于快递100推送签名增强;如账号未配置可留空。', 'is_secret' => true, 'visible_when' => ['config_key' => 'enabled', 'equals' => 'enabled']], ['config_key' => 'query_min_interval_minutes', 'title' => '最小查询间隔(分钟)', 'field_type' => 'text', 'placeholder' => '默认 30', 'remark' => '定时补查同一运单的最小间隔,允许 5-1440。', 'is_secret' => false, 'default_value' => '30', 'visible_when' => ['config_key' => 'enabled', 'equals' => 'enabled']], ], ], ]; } private function uploadableConfigMap(): array { return [ 'payment.apiclient_key_path' => [ 'filename' => 'apiclient_key.pem', ], 'payment.apiclient_cert_path' => [ 'filename' => 'apiclient_cert.pem', ], ]; } private function validateConfigValues(array $configValueMap): void { $driver = (new FileStorageConfigService())->normalizeDriver((string)($configValueMap['file_storage.driver'] ?? 'local')); if ($driver === 'local') { $this->validateKuaidi100Config($configValueMap); return; } if ($driver === 'oss') { $required = [ 'file_storage.oss_endpoint' => 'OSS Endpoint', 'file_storage.oss_bucket' => 'OSS Bucket', 'file_storage.oss_access_key_id' => 'OSS AccessKey ID', 'file_storage.oss_access_key_secret' => 'OSS AccessKey Secret', ]; foreach ($required as $key => $label) { if (trim((string)($configValueMap[$key] ?? '')) === '') { throw new \RuntimeException(sprintf('当前已切换为 OSS 存储,请先填写 %s', $label)); } } $directUploadMaxSizeMb = trim((string)($configValueMap['file_storage.direct_upload_max_size_mb'] ?? '200')); if ($directUploadMaxSizeMb !== '' && (!ctype_digit($directUploadMaxSizeMb) || (int)$directUploadMaxSizeMb < 1 || (int)$directUploadMaxSizeMb > 2048)) { throw new \RuntimeException('直传文件大小上限需填写 1-2048 之间的整数'); } $this->validateKuaidi100Config($configValueMap); return; } if ($driver !== 'qiniu') { $this->validateKuaidi100Config($configValueMap); return; } $required = [ 'file_storage.qiniu_bucket' => '七牛 Bucket', 'file_storage.qiniu_access_key' => '七牛 AccessKey', 'file_storage.qiniu_secret_key' => '七牛 SecretKey', ]; foreach ($required as $key => $label) { if (trim((string)($configValueMap[$key] ?? '')) === '') { throw new \RuntimeException(sprintf('当前已切换为七牛云存储,请先填写 %s', $label)); } } $publicBaseUrl = trim((string)($configValueMap['file_storage.public_base_url'] ?? '')); $bucketDomain = trim((string)($configValueMap['file_storage.qiniu_bucket_domain'] ?? '')); if ($publicBaseUrl === '' && $bucketDomain === '') { throw new \RuntimeException('当前已切换为七牛云存储,请至少填写公开访问域名或七牛公网访问域名'); } $this->validateKuaidi100Config($configValueMap); } private function validateKuaidi100Config(array $configValueMap): void { $enabled = (string)($configValueMap['kuaidi100.enabled'] ?? 'disabled'); if (!in_array($enabled, ['enabled', 'disabled'], true)) { throw new \RuntimeException('快递100同步开关配置无效'); } if ($enabled !== 'enabled') { return; } $required = [ 'kuaidi100.customer' => '快递100 Customer', 'kuaidi100.key' => '快递100 Key', 'kuaidi100.callback_url' => '快递100推送回调地址', ]; foreach ($required as $key => $label) { if (trim((string)($configValueMap[$key] ?? '')) === '') { throw new \RuntimeException(sprintf('当前已启用快递100,请先填写 %s', $label)); } } $interval = trim((string)($configValueMap['kuaidi100.query_min_interval_minutes'] ?? '30')); if ($interval !== '' && (!ctype_digit($interval) || (int)$interval < 5 || (int)$interval > 1440)) { throw new \RuntimeException('快递100最小查询间隔需填写 5-1440 之间的整数'); } } private function applyDerivedConfigValues(array &$configValueMap): void { $configValueMap['h5.oauth_redirect_url'] = $this->buildH5OAuthRedirectUrl((string)($configValueMap['h5.page_base_url'] ?? '')); } private function buildH5OAuthRedirectUrl(string $pageBaseUrl): string { $baseUrl = $this->normalizeH5PageBaseUrl($pageBaseUrl); if ($baseUrl === '') { return ''; } return $baseUrl . self::H5_OAUTH_REDIRECT_HASH_PATH; } private function normalizeH5PageBaseUrl(string $value): string { $baseUrl = trim($value); if ($baseUrl === '') { return ''; } $hashPos = strpos($baseUrl, '#'); if ($hashPos !== false) { $baseUrl = substr($baseUrl, 0, $hashPos); } if (!preg_match('/^https?:\/\//i', $baseUrl)) { $baseUrl = 'https://' . ltrim($baseUrl, '/'); } return rtrim($baseUrl, '/'); } }