389 行
15 KiB
PHP
389 行
15 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use think\Db;
|
||
|
||
/**
|
||
* 后台管理端 · 系统设置(Module-ADM-SETTING)
|
||
* 路由:
|
||
* GET|POST /api/v1/admin/setting/base AS1 基础配置
|
||
* POST /api/v1/admin/setting/pay AS2 微信支付配置
|
||
* GET|POST /api/v1/admin/setting/fee AS3 费用配置
|
||
* GET /api/v1/admin/setting/areas AS4 区域列表
|
||
* POST /api/v1/admin/setting/area AS5 保存区域
|
||
* GET|POST /api/v1/admin/setting/ui AS6 站点 UI 配置
|
||
* POST /api/v1/admin/setting/ui/reset AS7 重置 UI 配置
|
||
* POST /api/v1/admin/upload AS8 文件上传
|
||
*/
|
||
class AdminSetting extends V1
|
||
{
|
||
/** 管理端接口必须校验管理员身份(V1 默认关闭了 URI 权限校验) */
|
||
public function _initialize()
|
||
{
|
||
parent::_initialize();
|
||
$this->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')),
|
||
];
|
||
}
|
||
}
|