adminOnly(); } /** * AS1 基础配置 * GET → {appName,logo,servicePhone,aboutUs,shareTitle} * POST 入参 {appName,logo,servicePhone,aboutUs,shareTitle} → 合并后的完整配置 */ public function base() { if ($this->request->isPost()) { $appName = trim((string)$this->input('appName', '')); if ($appName === '') { $this->fail('小程序名称不能为空'); } $logo = (string)$this->input('logo', $this->cfg('base.logo', '')); $this->setCfg('base.appName', $appName); $this->setCfg('base.logo', $logo); $this->setCfg('base.servicePhone', (string)$this->input('servicePhone', $this->cfg('base.servicePhone', '400-000-0000'))); $this->setCfg('base.aboutUs', (string)$this->input('aboutUs', $this->cfg('base.aboutUs', ''))); $this->setCfg('base.shareTitle', (string)$this->input('shareTitle', $this->cfg('base.shareTitle', ''))); // 兼容旧 ui.* 键,保持名称/LOGO 一致 $this->setCfg('ui.siteName', $appName); $this->setCfg('ui.logo', $logo); $this->adminLog('修改基础配置'); } $this->ok($this->baseConfig()); } /** * AS2 微信支付配置 * POST /api/v1/admin/setting/pay 入参 {mchId,apiKey} → {configured:true} * * 【写入目标:epay 插件配置】支付能力已改由 epay 插件承载,商户号/密钥的真源是插件配置 * (后台「插件管理 → 微信支付宝整合插件 → 配置」,持久化在 addons/epay/config.php)。 * 因此本接口把 mchId/apiKey 合并写入插件的 wechat.mch_id / wechat.key,而不再写 fc_config 的 pay.* * (历史键 pay.mchId/pay.apiKey 保留在库中仅为兼容,代码已不读取)。 * 小程序 AppID(wechat.miniapp_id)与回调地址不在此维护:前者请到插件配置页填写,后者由服务端按请求域名动态生成。 */ public function pay() { $mchId = trim((string)$this->input('mchId', '')); $apiKey = trim((string)$this->input('apiKey', '')); if ($mchId === '') { $this->fail('微信支付商户号不能为空'); } if ($apiKey === '') { $this->fail('支付密钥不能为空'); } $addonConfig = (array)get_addon_config('epay'); $wechat = (isset($addonConfig['wechat']) && is_array($addonConfig['wechat'])) ? $addonConfig['wechat'] : []; $wechat['mch_id'] = $mchId; $wechat['key'] = $apiKey; try { // 与后台 Addon::config 同一套写入逻辑(VarExporter 导出回 addons/epay/config.php) set_addon_config('epay', ['wechat' => $wechat]); } catch (\Exception $e) { $this->fail('支付配置写入插件失败:' . $e->getMessage()); } $this->adminLog('修改微信支付配置(写入 epay 插件)'); // 返回体保持原契约({configured:true});是否已具备小程序支付条件(miniapp_id 是否填写) // 请看后台「插件管理 → 微信支付宝整合插件 → 配置」或 /pay/prepay 的返回提示。 $this->ok(['configured' => true]); } /** * AS3 费用配置 * GET → {singlePost,freePostPerDay,memberMonth,memberQuarter,memberYear} * POST 入参同字段 → 合并后配置 * 真源对齐:singlePost → post.singlePrice;freePostPerDay → post.freeQuota(按日); * 月/季/年卡价格 → fc_plan(与后台「运营推广 → 会员套餐」同源,改价即时生效) */ public function fee() { if ($this->request->isPost()) { $singlePost = (float)$this->input('singlePost', $this->cfg('post.singlePrice', 5)); if ($singlePost < 0) { $this->fail('单条发布价格不能为负'); } $freePostPerDay = (int)$this->input('freePostPerDay', $this->cfg('post.freeQuota', 0)); if ($freePostPerDay < 0) { $this->fail('免费发布条数不能为负'); } $this->setCfg('post.singlePrice', $singlePost); $this->setCfg('post.freeQuota', $freePostPerDay); $this->setCfg('post.freeQuotaCycle', 'day'); $now = time(); foreach (['M1' => 'memberMonth', 'M3' => 'memberQuarter', 'M12' => 'memberYear'] as $code => $field) { $raw = $this->input($field, null); if ($raw === null || $raw === '') { continue; } $price = round((float)$raw, 2); if ($price < 0) { $this->fail('套餐价格不能为负'); } Db::name('fc_plan')->where('plan_code', $code)->update(['price' => $price, 'updatetime' => $now]); } $this->adminLog('修改费用配置'); } $this->ok($this->feeConfig()); } /** * AS4 服务区域列表(平铺:每条记录自带省 / 市 / 区县) * GET /api/v1/admin/setting/areas → [{id,province,city,district,fullName,lat,lng,sort,enabled}] * 规则:按 province asc, city asc, sort asc, id asc;返回全量(含停用)。 */ public function areas() { $rows = Db::name('fc_area')->order('province asc, city asc, sort asc, id asc')->select(); $list = []; foreach ($rows as $row) { $list[] = [ 'id' => (int)$row['id'], 'province' => (string)$row['province'], 'city' => (string)$row['city'], 'district' => (string)$row['district'], 'fullName' => $row['province'] . ' / ' . $row['city'] . ' / ' . $row['district'], 'lat' => $row['lat'] !== null ? (float)$row['lat'] : null, 'lng' => $row['lng'] !== null ? (float)$row['lng'] : null, 'sort' => (int)$row['sort'], 'enabled' => (bool)$row['enabled'], ]; } $this->ok($list); } /** * AS5 保存服务区域 * POST /api/v1/admin/setting/area 入参 {id?,province,city,district,sort,enabled,lat?,lng?} → {id,fullName} * 规则:省 / 市 / 区县均必填;同一「省+市+区县」不允许重复。 */ public function saveArea() { $id = (int)$this->input('id', 0); $province = trim((string)$this->input('province', '')); $city = trim((string)$this->input('city', '')); $district = trim((string)$this->input('district', '')); $sort = (int)$this->input('sort', 0); $enabled = (bool)$this->input('enabled', true); $lat = $this->input('lat', null); $lng = $this->input('lng', null); $values = ['省份' => $province, '城市' => $city, '区县' => $district]; foreach ($values as $label => $value) { if ($value === '') { $this->fail($label . '不能为空'); } if (mb_strlen($value) > 32) { $this->fail($label . '不能超过 32 个字符'); } } // 同一条服务区域(省+市+区县)不允许重复 $duplicate = Db::name('fc_area') ->where('province', $province) ->where('city', $city) ->where('district', $district) ->where('id', '<>', $id) ->find(); if ($duplicate) { $this->fail('该省市区组合已存在'); } if ($lat !== null && $lat !== '') { if (!is_numeric($lat) || (float)$lat < -90 || (float)$lat > 90) { $this->fail('纬度格式不正确'); } $lat = round((float)$lat, 6); } else { $lat = null; } if ($lng !== null && $lng !== '') { if (!is_numeric($lng) || (float)$lng < -180 || (float)$lng > 180) { $this->fail('经度格式不正确'); } $lng = round((float)$lng, 6); } else { $lng = null; } $now = time(); $data = [ 'province' => $province, 'city' => $city, 'district' => $district, 'lat' => $lat, 'lng' => $lng, 'sort' => $sort, 'enabled' => $enabled ? 1 : 0, 'updatetime' => $now, ]; if ($id && Db::name('fc_area')->where('id', $id)->find()) { Db::name('fc_area')->where('id', $id)->update($data); } else { $data['createtime'] = $now; $id = Db::name('fc_area')->insertGetId($data); } $fullName = $province . ' / ' . $city . ' / ' . $district; $this->adminLog('保存服务区域 #' . $id . ' ' . $fullName); $this->ok(['id' => (int)$id, 'fullName' => $fullName]); } /** * AS6 站点 UI 配置(LOGO / 图标) * GET → {siteName,logo,icons,updatedAt} * POST 入参 {siteName?,logo?,icons?}(合并语义:只覆盖传入的 key) → 完整配置 */ public function ui() { if ($this->request->isPost()) { $siteName = $this->input('siteName', null); if ($siteName !== null) { $siteName = trim((string)$siteName); if ($siteName === '') { $this->fail('小程序名称不能为空'); } $this->setCfg('base.appName', $siteName); // 兼容旧 ui.siteName(用户端已统一优先读 base.appName) $this->setCfg('ui.siteName', $siteName); } $logo = $this->input('logo', null); if ($logo !== null) { $this->setCfg('base.logo', (string)$logo); // 兼容旧 ui.logo $this->setCfg('ui.logo', (string)$logo); } // 注意:icons 可能是数组,不能走 $this->input()(TP5.0 Request::input 默认按字符串强转, // 传入数组会抛 "variable type error:array" 导致 HTTP 500);统一从 inputs() 取原始值。 $allInputs = $this->inputs(); $icons = array_key_exists('icons', $allInputs) ? $allInputs['icons'] : null; if ($icons !== null) { if (is_string($icons)) { $icons = json_decode($icons, true); } $icons = is_array($icons) ? $icons : (array)$icons; $merged = (array)$this->cfg('ui.icons', []); foreach ($icons as $k => $v) { if (!is_string($v)) { $this->fail('图标 ' . $k . ' 的地址必须是字符串'); } $merged[$k] = $v; } $this->setCfg('ui.icons', $merged); } $this->setCfg('ui.updatedAt', date('c')); $this->adminLog('修改小程序名称/LOGO/图标配置'); } $this->ok($this->uiConfig()); } /** * AS7 重置 UI 配置 * POST /api/v1/admin/setting/ui/reset → {siteName,logo,icons,reset} */ public function uiReset() { // LOGO 已统一到 base.logo,重置时两处都清空,避免旧 ui.logo 回落“复活” $this->setCfg('base.logo', ''); $this->setCfg('ui.logo', ''); $this->setCfg('ui.icons', []); $this->setCfg('ui.updatedAt', date('c')); $this->adminLog('重置LOGO与图标配置'); $config = $this->uiConfig(); $config['reset'] = true; $this->ok($config); } /** * AS8 文件上传 * POST /api/v1/admin/upload multipart 字段 file → {url} */ public function upload() { $file = $this->request->file('file'); if (!$file) { $this->fail('未接收到上传文件'); } try { $upload = new \app\common\library\Upload($file); $attachment = $upload->upload(); } catch (\Exception $e) { $this->fail($e->getMessage()); } $this->ok(['url' => cdnurl($attachment->url, true)]); } /** 基础配置结构 */ protected function baseConfig() { return [ 'appName' => (string)$this->cfg('base.appName', '同城分类信息'), 'logo' => (string)$this->cfg('base.logo', ''), 'servicePhone' => (string)$this->cfg('base.servicePhone', '400-000-0000'), 'aboutUs' => (string)$this->cfg('base.aboutUs', ''), 'shareTitle' => (string)$this->cfg('base.shareTitle', ''), ]; } /** 费用配置结构(月/季/年卡价格真源=fc_plan;发布价格/免费条数真源=post.*) */ protected function feeConfig() { $plans = Db::name('fc_plan')->where('enabled', 1)->column('price', 'plan_code'); $planPrice = function ($code, $default) use ($plans) { return isset($plans[$code]) && $plans[$code] !== null && $plans[$code] !== '' ? round((float)$plans[$code], 2) : $default; }; return [ 'singlePost' => round((float)$this->cfg('post.singlePrice', 5), 2), 'freePostPerDay' => max(0, (int)$this->cfg('post.freeQuota', 0)), 'memberMonth' => $planPrice('M1', 19.9), 'memberQuarter' => $planPrice('M3', 49.9), 'memberYear' => $planPrice('M12', 168), ]; } /** UI 配置结构(名称/LOGO 以 base.* 为准,icons 保证为对象) */ protected function uiConfig() { $icons = $this->cfg('ui.icons', []); $icons = is_array($icons) ? $icons : (array)$icons; $baseName = trim((string)$this->cfg('base.appName', '')); $siteName = $baseName !== '' ? $baseName : (string)$this->cfg('ui.siteName', '同城分类信息'); $baseLogo = (string)$this->cfg('base.logo', ''); $logo = $baseLogo !== '' ? $baseLogo : (string)$this->cfg('ui.logo', ''); $shareTitle = trim((string)$this->cfg('base.shareTitle', '')); return [ 'appName' => $siteName, 'siteName' => $siteName, 'logo' => $logo, 'servicePhone' => (string)$this->cfg('base.servicePhone', ''), 'aboutUs' => (string)$this->cfg('base.aboutUs', ''), 'shareTitle' => $shareTitle !== '' ? $shareTitle : $siteName, 'icons' => (object)$icons, 'updatedAt' => (string)$this->cfg('ui.updatedAt', date('c')), ]; } }