[ 'name' => 'fc_miniapp', 'title' => '小程序配置', 'active' => false, 'list' => [ self::item('fc_base_appName', '小程序名称', 'string', $pick('base.appName', ''), '显示在小程序首页与分享卡片', 'required'), self::item('fc_base_logo', '小程序LOGO', 'image', $pick('base.logo', ''), '留空时使用默认 LOGO'), self::item('fc_base_servicePhone', '客服电话', 'string', $pick('base.servicePhone', ''), '展示在小程序「联系客服」入口'), self::item('fc_base_shareTitle', '分享标题', 'string', $pick('base.shareTitle', ''), '留空则默认使用小程序名称'), self::item('fc_base_aboutUs', '关于我们', 'text', $pick('base.aboutUs', ''), '展示在小程序「关于我们」页面'), self::item('fc_wx_appid', '小程序 AppID', 'string', $pick('wx.appid', ''), '微信公众平台 → 开发管理 → 开发设置 → AppID(微信登录用)'), self::item('fc_wx_secret', 'AppSecret', 'password', '', '仅服务端保存与调用;已配置时留空表示不修改(微信登录用)'), self::item('fc_wx_tplAudit', '审核结果模板 ID', 'string', $pick('wx.tplAudit', ''), '小程序订阅消息:审核结果通知(公众平台 → 订阅消息)'), self::item('fc_wx_tplTransfer', '提现到账模板 ID', 'string', $pick('wx.tplTransfer', ''), '小程序订阅消息:提现到账通知'), self::item('fc_pay_passwordEnabled', '余额支付密码', 'switch', $pick('pay.payPasswordEnabled', '0'), '开启后,用户使用余额支付时需验证 6 位支付密码;默认关闭'), self::item('fc_admin_userIds', '后台接口管理员 ID', 'string', $pick('admin.userIds', ''), '允许调用 /api/v1/admin/** 的用户 ID,多个用逗号分隔(留空 = 全部禁止)'), self::item('fc_admin_mobiles', '后台接口管理员手机号', 'string', $pick('admin.mobiles', ''), '允许调用管理接口的手机号,多个用逗号分隔'), ], ], ]; } /** * 获取「发布与浏览配置」当前值(供独立菜单 fc/publish 读取) * * @return array */ public static function getPublishConfig() { $cfg = self::values(); $pick = function ($key, $default = '') use ($cfg) { return (isset($cfg[$key]) && $cfg[$key] !== '') ? $cfg[$key] : $default; }; $postMode = (string)$pick('post.mode', 'paid'); if (!in_array($postMode, ['free', 'paid'], true)) { $postMode = 'paid'; } $payViewPrice = round((float)$pick('fee.payView', 2), 2); $viewModeRaw = (string)$pick('fee.viewMode', ''); if (!in_array($viewModeRaw, ['free', 'paid'], true)) { $viewMode = $payViewPrice <= 0 ? 'free' : 'paid'; } else { $viewMode = $viewModeRaw; } return [ 'postMode' => $postMode, 'freeQuota' => max(0, (int)$pick('post.freeQuota', 0)), 'freeQuotaCycle' => in_array($pick('post.freeQuotaCycle', 'day'), ['day', 'month', 'total'], true) ? $pick('post.freeQuotaCycle', 'day') : 'day', 'singlePrice' => max(0, round((float)$pick('post.singlePrice', 5), 2)), 'viewMode' => $viewMode, 'payViewPrice' => max(0, $payViewPrice), 'callNormalDailyQuota' => max(0, (int)$pick('call.normalDailyQuota', 5)), ]; } /** * 解析会员发布折扣数字(1 ~ 10,如 8 代表 8 折,10 代表不打折/原价) * 兼容纯数字("8" / "8.5" / "10")与历史中文格式("8 折" / "8.5折") * * @param mixed $raw 原始配置值 * @return float 范围 0.1 ~ 10 的折扣值 */ public static function parseDiscountNum($raw) { if (is_numeric($raw)) { $num = round((float)$raw, 1); if ($num > 0 && $num <= 10) { return $num; } } $str = trim((string)$raw); if ($str === '原价' || $str === '不打折' || $str === '无折扣') { return 10.0; } if (preg_match('/^(\d+(?:\.\d+)?)\s*折?$/u', $str, $m)) { $num = round((float)$m[1], 1); if ($num > 0 && $num <= 10) { return $num; } } return 8.0; } /** * 将折扣数字格式化为展示文本(如 8 -> "8 折",10 -> "原价") * * @param float $discountNum 1~10 的折扣数字 * @return string */ public static function formatDiscountText($discountNum) { $num = round((float)$discountNum, 1); if ($num <= 0 || $num >= 10) { return '原价'; } $clean = rtrim(rtrim(number_format($num, 1, '.', ''), '0'), '.'); return $clean . ' 折'; } /** * 根据后台实时配置与启用套餐,自动生成 100% 准确的会员权益文案列表(无需手写) * * @param int $viewQuota 每日免费浏览完整信息条数(0 = 不限) * @param int $callQuota 每日拨打电话次数(0 = 不限) * @param float $discountNum 超额单条发布折扣值(1~10) * @return array */ public static function buildAutoBenefits($viewQuota, $callQuota, $discountNum) { $list = []; // 1. 完整信息浏览特权 if ((int)$viewQuota > 0) { $list[] = '每日免费浏览 ' . (int)$viewQuota . ' 条完整信息'; } else { $list[] = '完整信息无限次免费浏览'; } // 2. 拨打电话特权 if ((int)$callQuota > 0) { $list[] = '每日可拨打 ' . (int)$callQuota . ' 次电话联系发布者'; } else { $list[] = '拨打电话不限次数'; } // 3. 套餐赠送免费发布额度特权(实时读取 fc_plan 启用套餐) try { $plans = Db::name('fc_plan')->where('enabled', 1)->order('sort asc')->select(); $quotaParts = []; foreach ($plans as $p) { $q = (int)(isset($p['post_quota']) ? $p['post_quota'] : 0); if ($q > 0) { $quotaParts[] = $p['name'] . '赠 ' . $q . ' 条'; } } if (!empty($quotaParts)) { $list[] = '开通立赠免费发布额度(' . implode(' / ', $quotaParts) . ')'; } else { $list[] = '会员有效期内尊享专属信息发布通道'; } } catch (\Exception $e) { $list[] = '开通套餐立赠海量免费发布额度'; } // 4. 超额单条发布折扣特权 $num = round((float)$discountNum, 1); if ($num > 0 && $num < 10) { $clean = rtrim(rtrim(number_format($num, 1, '.', ''), '0'), '.'); $list[] = '赠送额度用尽后,单条付费发布专享 ' . $clean . ' 折优惠'; } // 5. 尊贵身份标识 $list[] = '全站帖子详情与个人主页点亮 VIP 专属金标'; return $list; } /** * 为单个会员套餐生成专属权益文案列表(直接读取套餐行权益字段) * * @param array $plan fc_plan 表行数据 * @return array */ public static function buildPlanBenefits(array $plan) { $postQuota = isset($plan['post_quota']) ? max(0, (int)$plan['post_quota']) : 0; $viewQuota = isset($plan['view_quota']) ? max(0, (int)$plan['view_quota']) : 20; $callQuota = isset($plan['call_quota']) ? max(0, (int)$plan['call_quota']) : 30; $discount = isset($plan['post_discount']) ? self::parseDiscountNum($plan['post_discount']) : 8.0; $list = []; if ($viewQuota > 0) { $list[] = '每日免费浏览 ' . $viewQuota . ' 条完整信息'; } else { $list[] = '完整信息无限次免费浏览'; } if ($callQuota > 0) { $list[] = '每日可拨打 ' . $callQuota . ' 次电话联系发布者'; } else { $list[] = '拨打电话不限次数'; } if ($postQuota > 0) { $list[] = '开通本套餐立赠 ' . $postQuota . ' 条免费信息发布额度'; } else { $list[] = '会员有效期内尊享专属信息发布通道'; } if ($discount > 0 && $discount < 10) { $clean = rtrim(rtrim(number_format($discount, 1, '.', ''), '0'), '.'); $list[] = '赠送额度用尽后,单条付费发布专享 ' . $clean . ' 折优惠'; } $list[] = '全站帖子详情与个人主页点亮 VIP 专属金标'; return $list; } /** * 解析指定用户(或默认推荐套餐)对应的会员权益数据(真源:fc_plan 套餐表) * * @param array|null $ext fc_user_ext 行(可为空) * @return array{viewQuota:int,callQuota:int,postDiscountNum:float,postDiscount:string,benefits:array} */ public static function resolveMemberBenefit($ext = null) { $plan = null; try { if (is_array($ext) && !empty($ext['member_level'])) { $plan = Db::name('fc_plan') ->where('name', (string)$ext['member_level']) ->find(); } if (!$plan) { $plan = Db::name('fc_plan') ->where('enabled', 1) ->order('sort asc, id asc') ->find(); } } catch (\Exception $e) { $plan = null; } if ($plan) { $viewQuota = isset($plan['view_quota']) ? max(0, (int)$plan['view_quota']) : 20; $callQuota = isset($plan['call_quota']) ? max(0, (int)$plan['call_quota']) : 30; $discountNum = isset($plan['post_discount']) ? self::parseDiscountNum($plan['post_discount']) : 8.0; $postDiscount = self::formatDiscountText($discountNum); $benefitsArr = self::buildPlanBenefits($plan); return [ 'viewQuota' => $viewQuota, 'callQuota' => $callQuota, 'postDiscountNum' => $discountNum, 'postDiscount' => $postDiscount, 'benefits' => $benefitsArr, ]; } // 兜底:若套餐表为空,从历史配置回退 return self::getMemberBenefitConfig(); } public static function getMemberBenefitConfig() { $cfg = self::values(); $pick = function ($key, $default = '') use ($cfg) { return (isset($cfg[$key]) && $cfg[$key] !== '') ? $cfg[$key] : $default; }; $benefitRaw = $pick('member.benefit', ''); $benefitCfg = []; if ($benefitRaw) { $decoded = json_decode($benefitRaw, true); if (is_array($decoded)) { $benefitCfg = $decoded; } } $viewQuota = isset($benefitCfg['viewQuota']) ? max(0, (int)$benefitCfg['viewQuota']) : 20; $callQuota = isset($benefitCfg['callQuota']) ? max(0, (int)$benefitCfg['callQuota']) : 30; $rawDiscount = isset($benefitCfg['postDiscountNum']) ? $benefitCfg['postDiscountNum'] : (isset($benefitCfg['postDiscount']) ? $benefitCfg['postDiscount'] : 8); $discountNum = self::parseDiscountNum($rawDiscount); $postDiscount = self::formatDiscountText($discountNum); $benefitsArr = self::buildAutoBenefits($viewQuota, $callQuota, $discountNum); return [ 'viewQuota' => $viewQuota, 'callQuota' => $callQuota, 'postDiscountNum' => $discountNum, 'postDiscount' => $postDiscount, 'benefits' => $benefitsArr, ]; } /** * 获取「二级分销配置」当前值(供 fc/distrelation 页面读取) * * @return array */ public static function getDistributionConfig() { $cfg = self::values(); $pick = function ($key, $default = '') use ($cfg) { return (isset($cfg[$key]) && $cfg[$key] !== '') ? $cfg[$key] : $default; }; return [ 'distEnabled' => (int)$pick('dist.enabled', 1), 'distLevel' => (int)$pick('dist.level', 2), 'distLevel1' => round((float)$pick('dist.level1Rate', 20), 2), 'distLevel2' => round((float)$pick('dist.level2Rate', 10), 2), 'distSettle' => (string)$pick('dist.settleCycle', 'T+7'), 'distMinAmount' => round((float)$pick('dist.minWithdraw', 50), 2), ]; } /** * 保存业务配置(由系统配置页提交) * * @param array $row 系统配置页提交的 row 数组 * @return void * @throws \Exception */ public static function save(array $row) { $has = function ($prefix) use ($row) { foreach ($row as $key => $value) { if (strpos($key, $prefix) === 0) { return true; } } return false; }; if ($has('fc_base_')) { self::saveBase($row); } if ($has('fc_post_')) { self::savePublish($row); } if ($has('fc_member_')) { self::saveMember($row); } if ($has('fc_pay_')) { self::savePay($row); } if ($has('fc_dist_')) { self::saveDistribution($row); } } /** * 支付配置 * * @param array $row * @return void */ protected static function savePay(array $row) { $enabled = isset($row['fc_pay_passwordEnabled']) ? (int)$row['fc_pay_passwordEnabled'] : 0; self::put('pay.payPasswordEnabled', $enabled ? 1 : 0, 'pay', '余额支付密码开关(开启后余额支付需验证 6 位支付密码)'); } /** * 小程序基础信息 * * @param array $row * @return void * @throws \Exception */ protected static function saveBase(array $row) { $appName = isset($row['fc_base_appName']) ? trim((string)$row['fc_base_appName']) : ''; if ($appName === '') { throw new \Exception('小程序名称不能为空'); } $values = [ 'base.appName' => $appName, 'base.logo' => isset($row['fc_base_logo']) ? trim((string)$row['fc_base_logo']) : '', 'base.servicePhone' => isset($row['fc_base_servicePhone']) ? trim((string)$row['fc_base_servicePhone']) : '', 'base.shareTitle' => isset($row['fc_base_shareTitle']) ? trim((string)$row['fc_base_shareTitle']) : '', 'base.aboutUs' => isset($row['fc_base_aboutUs']) ? trim((string)$row['fc_base_aboutUs']) : '', ]; foreach ($values as $key => $value) { self::put($key, $value, 'base', '小程序配置'); } // 兼容旧 ui.* 键:名称/LOGO 同步一份,避免旧数据在用户端回落时“复活” self::put('ui.siteName', $appName, 'ui', '站点名称'); self::put('ui.logo', $values['base.logo'], 'ui', '自定义LOGO'); // 微信登录参数(真源同为 fa_fc_config,由 WxMini 读取): // AppSecret 不回显、留空表示不修改,避免表单提交把已保存的密钥冲成空串 $wxAppid = isset($row['fc_wx_appid']) ? trim((string)$row['fc_wx_appid']) : ''; if ($wxAppid !== '') { self::put('wx.appid', $wxAppid, 'wx', '小程序AppID(微信登录用)'); } $wxSecret = isset($row['fc_wx_secret']) ? trim((string)$row['fc_wx_secret']) : ''; if ($wxSecret !== '') { self::put('wx.secret', $wxSecret, 'wx', '小程序AppSecret(仅服务端持有,不下发前端)'); } // 管理接口白名单(/api/v1/admin/**):留空表示不允许任何小程序端账号调用 if (array_key_exists('fc_admin_userIds', $row)) { self::put('admin.userIds', trim((string)$row['fc_admin_userIds']), 'admin', '管理接口允许的用户ID(逗号分隔)'); } if (array_key_exists('fc_admin_mobiles', $row)) { self::put('admin.mobiles', trim((string)$row['fc_admin_mobiles']), 'admin', '管理接口允许的手机号(逗号分隔)'); } // 订阅消息模板 ID if (array_key_exists('fc_wx_tplAudit', $row)) { self::put('wx.tplAudit', trim((string)$row['fc_wx_tplAudit']), 'wx', '审核结果订阅消息模板ID'); } if (array_key_exists('fc_wx_tplTransfer', $row)) { self::put('wx.tplTransfer', trim((string)$row['fc_wx_tplTransfer']), 'wx', '提现到账订阅消息模板ID'); } } /** * 保存发布模式、免费额度与联系方式浏览收费配置 * * @param array $row * @return void * @throws \Exception */ public static function savePublish(array $row) { $mode = isset($row['fc_post_mode']) ? trim((string)$row['fc_post_mode']) : 'free'; if (!in_array($mode, ['free', 'paid'], true)) { throw new \Exception('发布模式只支持「免费发布」或「付费发布」'); } $cycle = isset($row['fc_post_freeQuotaCycle']) ? trim((string)$row['fc_post_freeQuotaCycle']) : 'day'; if (!in_array($cycle, ['day', 'month', 'total'], true)) { throw new \Exception('免费条数统计周期只支持「每日 / 每月 / 累计」'); } $num = function ($key, $default) use ($row) { if (!isset($row[$key]) || $row[$key] === '') { return $default; } return round((float)$row[$key], 2); }; $int = function ($key, $default) use ($row) { if (!isset($row[$key]) || $row[$key] === '') { return $default; } return (int)$row[$key]; }; $single = $num('fc_post_singlePrice', 5); $freeQuota = $int('fc_post_freeQuota', 0); if ($single < 0) { throw new \Exception('单条发布价格不能为负'); } if ($freeQuota < 0) { throw new \Exception('免费发布条数不能为负'); } $values = [ 'post.mode' => $mode, 'post.singlePrice' => $single, 'post.freeQuota' => $freeQuota, 'post.freeQuotaCycle' => $cycle, ]; foreach ($values as $key => $value) { self::put($key, $value, 'post', '发布配置'); } // 完整信息浏览规则(若表单同时传入则一并保存) if (isset($row['fc_fee_viewMode']) || isset($row['fc_fee_payView'])) { $viewMode = isset($row['fc_fee_viewMode']) ? trim((string)$row['fc_fee_viewMode']) : 'paid'; if (!in_array($viewMode, ['free', 'paid'], true)) { $viewMode = 'paid'; } $payView = $num('fc_fee_payView', 2); if ($payView < 0) { throw new \Exception('非会员单条解锁价格不能为负'); } // 若选了完全免费公开,或单次价格设为 0,统一保障免费可看 self::put('fee.viewMode', $viewMode, 'fee', '完整信息浏览模式:free免费公开 / paid付费或会员额度解锁'); self::put('fee.payView', $viewMode === 'free' ? 0 : $payView, 'fee', '非会员单条解锁完整信息价格'); } // 普通用户(非会员)每日拨打次数限制(会员拨打次数由各套餐 call_quota 控制) if (isset($row['fc_call_normalDailyQuota'])) { $normalQuota = max(0, $int('fc_call_normalDailyQuota', 5)); self::put('call.normalDailyQuota', $normalQuota, 'call', '普通用户每日免费拨打次数上限(0表示不限制)'); } } /** * 保存 VIP 会员通用特权配置(纯数值驱动,自动生成前端展示文案) * * @param array $row 提交的表单数组 * @return void * @throws \Exception */ public static function saveMember(array $row) { $viewQuota = isset($row['fc_member_viewQuota']) ? max(0, (int)$row['fc_member_viewQuota']) : 20; $callQuota = isset($row['fc_member_callQuota']) ? max(0, (int)$row['fc_member_callQuota']) : 30; $rawDiscount = isset($row['fc_member_postDiscountNum']) ? $row['fc_member_postDiscountNum'] : (isset($row['fc_member_postDiscount']) ? $row['fc_member_postDiscount'] : 8); $discountNum = self::parseDiscountNum($rawDiscount); if ($discountNum <= 0 || $discountNum > 10) { throw new \Exception('发布折扣需在 0.1 ~ 10 之间(10 表示不打折)'); } $postDiscount = self::formatDiscountText($discountNum); $benefits = self::buildAutoBenefits($viewQuota, $callQuota, $discountNum); $current = []; $raw = Db::name('fc_config')->where('key', 'member.benefit')->value('value'); if ($raw) { $decoded = json_decode($raw, true); if (is_array($decoded)) { $current = $decoded; } } $current['viewQuota'] = $viewQuota; $current['callQuota'] = $callQuota; $current['postDiscountNum'] = $discountNum; $current['postDiscount'] = $postDiscount; $current['benefits'] = $benefits; self::put('member.benefit', $current, 'member', '会员权益配置'); } /** * 分销参数保存 * * @param array $row * @return void * @throws \Exception */ public static function saveDistribution(array $row) { $enabled = isset($row['fc_dist_enabled']) ? (int)$row['fc_dist_enabled'] : 1; $level = isset($row['fc_dist_level']) ? (int)$row['fc_dist_level'] : 2; $rate1 = isset($row['fc_dist_level1Rate']) ? round((float)$row['fc_dist_level1Rate'], 2) : 20; $rate2 = isset($row['fc_dist_level2Rate']) ? round((float)$row['fc_dist_level2Rate'], 2) : 10; $settle = isset($row['fc_dist_settleCycle']) ? trim((string)$row['fc_dist_settleCycle']) : 'T+7'; $minWithdraw = isset($row['fc_dist_minWithdraw']) ? round((float)$row['fc_dist_minWithdraw'], 2) : 50; if (!in_array($level, [1, 2], true)) { throw new \Exception('分佣层级只支持一级或二级'); } if ($rate1 < 0 || $rate1 > 100 || $rate2 < 0 || $rate2 > 100) { throw new \Exception('分佣比例必须在 0-100 之间'); } if ($minWithdraw < 0) { throw new \Exception('最低提现金额不能为负'); } if ($settle === '') { $settle = 'T+7'; } $values = [ 'dist.enabled' => $enabled ? 1 : 0, 'dist.level' => $level, 'dist.level1Rate' => $rate1, 'dist.level2Rate' => $rate2, 'dist.settleCycle' => $settle, 'dist.minWithdraw' => $minWithdraw, ]; foreach ($values as $key => $value) { self::put($key, $value, 'dist', '分销配置'); } } /** * 读取 fc_config 全部配置 * * @return array */ protected static function values() { $result = []; try { foreach (Db::name('fc_config')->select() as $row) { $result[$row['key']] = $row['value']; } } catch (\Exception $e) { // 业务表尚未初始化时,系统配置页仍可正常打开(使用默认值) } return $result; } /** * 写入单项配置 * * @param string $key * @param mixed $value * @param string $group * @param string $remark * @return void */ protected static function put($key, $value, $group, $remark) { $now = time(); $value = is_scalar($value) || $value === null ? (string)$value : json_encode($value, JSON_UNESCAPED_UNICODE); $exist = Db::name('fc_config')->where('key', $key)->find(); if ($exist) { Db::name('fc_config')->where('key', $key)->update([ 'value' => $value, 'group_key' => $group, 'remark' => $remark, 'updatetime' => $now, ]); } else { Db::name('fc_config')->insert([ 'key' => $key, 'value' => $value, 'group_key' => $group, 'remark' => $remark, 'createtime' => $now, 'updatetime' => $now, ]); } } /** * 构造系统配置页的单个配置项 * * @param string $name * @param string $title * @param string $type * @param mixed $value * @param string $tip * @param string $rule * @param array $content * @return array */ protected static function item($name, $title, $type, $value, $tip = '', $rule = '', array $content = []) { return [ 'name' => $name, 'title' => $title, 'type' => $type, 'value' => $value, 'content' => $content, 'tip' => $tip, 'rule' => $rule, 'extend' => '', 'extend_html' => '', 'setting' => [], 'visible' => '', 'id' => 0, ]; } }