文件
fenleixinxi/application/admin/controller/fc/Publish.php
linda d452be1d60 feat(api): v1.0.10 免费发布条数(周期 每日/每月/累计)+ 计费顺序(套餐额度 → 免费条数 → 单条付费)
需求:免费发布不能是「要么全免费、要么一开付费就是硬付费墙」,需要可配「前 N 条免费」,
超出的按付费发布标准计费。

- 后台「发布与费用配置」新增「免费发布条数 + 统计周期」:post.freeQuota / post.freeQuotaCycle
  (day 每日 / month 每月 / total 累计;0 = 不限制,免费发布模式下「免费到底」)
  Publish::save 校验周期枚举与条数非负;页面补说明(计费顺序:套餐额度 → 免费发布条数 → 单条付费)
- FcInstall::upgradePublish 与内置种子各补两枚配置(幂等,不覆盖运营已改的值;老库缺失时
  API 回落 fee.freePostPerDay 按日计,行为与升级前一致)
- POST /post/create 计费重构为三级顺序:
  ① 套餐额度(fc_user_ext.post_quota_left)→ ② 免费发布条数(按周期统计本人已发布条数,不含软删)
  → ③ 单条付费(post.singlePrice,即「付费发布」标准)
  免费发布模式下免费条数用完时明确提示「免费额度已用完,请按 X.XX 元/条付费发布」
  (服务端不主动扣款,需客户端切到 plan=single 并选支付方式后重新提交)
- GET /post/publish-config 字段调整:freePerDay/freeUsedToday/freeLeftToday
  → freeQuota / freeQuotaCycle / freeQuotaUsed / freeQuotaLeft / freeUnlimited(-1 = 不限制)
- tools/verify-api.mjs P9 同步断言新字段与「能否免费发布」判定;后台语言包补齐中文映射
- application/config.php:site.version 1.6.6.20261016 → 1.6.7.20261016(后台视图/JS 变更后强制刷新静态资源缓存)

自检:node tools/check-admin-i18n.mjs(错误 0/警告 0)、node tools/check-theme.mjs(错误 0/警告 0)、
5 个 PHP 文件 php -n -l 无语法错误。
2026-10-17 11:45:36 +08:00

159 行
6.2 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\admin\controller\fc;
use app\common\controller\Backend;
use think\Db;
/**
* 同城-发布与费用配置
*
* 本页是「发布模式 + 发布价格/条数」的唯一入口:
* 真源 = fa_fc_config 的 post.* 分组;保存时同步 fa_fc_plan 的月/年卡价格与含发布条数,
* 保证「发布页价格」「会员中心套餐价」「后端计费」三者一致。
*
* post.mode free 免费发布 / paid 付费发布
* post.singlePrice 单条发布价格(元)=「付费发布」标准价
* post.freeQuota 免费发布条数(0 = 不限制/免费到底;超出即按 singlePrice 计费)
* post.freeQuotaCycle 免费条数统计周期:day 每日 / month 每月 / total 累计
* post.monthPrice 按月发布价格(元)
* post.monthQuota 按月含发布条数
* post.yearPrice 按年发布价格(元)
* post.yearQuota 按年含发布条数
*
* @icon fa fa-cny
*/
class Publish extends Backend
{
/** 发布模式取值 */
const MODES = ['free', 'paid'];
/** 免费条数统计周期取值 */
const CYCLES = ['day', 'month', 'total'];
/**
* 配置页(GET 显示 / POST 保存)
*/
public function index()
{
if ($this->request->isPost()) {
return $this->save();
}
$this->view->assign('row', $this->current());
$this->view->assign('plans', Db::name('fc_plan')->order('sort asc')->select());
return $this->view->fetch();
}
/**
* 保存配置并同步会员套餐
*/
protected function save()
{
$params = $this->request->post('row/a');
if (!is_array($params)) {
$params = $this->request->post();
}
$mode = isset($params['mode']) ? trim((string)$params['mode']) : 'paid';
if (!in_array($mode, self::MODES, true)) {
$this->error('发布模式只支持「免费发布」或「付费发布」');
}
$num = function ($key, $default) use ($params) {
if (!isset($params[$key]) || $params[$key] === '') {
return $default;
}
return round((float)$params[$key], 2);
};
$int = function ($key, $default) use ($params) {
if (!isset($params[$key]) || $params[$key] === '') {
return $default;
}
return (int)$params[$key];
};
$single = $num('singlePrice', 5);
$monthPrice = $num('monthPrice', 19.9);
$yearPrice = $num('yearPrice', 168);
$monthQuota = $int('monthQuota', 30);
$yearQuota = $int('yearQuota', 500);
// 免费发布条数:0 = 不限制(免费到底);达到条数后按「单条发布价格」计费
$freeQuota = $int('freeQuota', 0);
$freeQuotaCycle = isset($params['freeQuotaCycle']) ? trim((string)$params['freeQuotaCycle']) : 'day';
if (!in_array($freeQuotaCycle, self::CYCLES, true)) {
$this->error('免费条数统计周期只支持「每日 / 每月 / 累计」');
}
foreach (['单条发布价格' => $single, '按月发布价格' => $monthPrice, '按年发布价格' => $yearPrice] as $label => $v) {
if ($v < 0) {
$this->error($label . '不能为负');
}
}
if ($monthQuota < 0 || $yearQuota < 0) {
$this->error('含发布条数不能为负');
}
if ($freeQuota < 0) {
$this->error('免费发布条数不能为负(0 表示不限制)');
}
$now = time();
$values = [
'post.mode' => $mode,
'post.singlePrice' => $single,
'post.freeQuota' => $freeQuota,
'post.freeQuotaCycle' => $freeQuotaCycle,
'post.monthPrice' => $monthPrice,
'post.monthQuota' => $monthQuota,
'post.yearPrice' => $yearPrice,
'post.yearQuota' => $yearQuota,
];
foreach ($values as $key => $val) {
$exist = Db::name('fc_config')->where('key', $key)->find();
if ($exist) {
Db::name('fc_config')->where('key', $key)->update(['value' => (string)$val, 'updatetime' => $now]);
} else {
Db::name('fc_config')->insert([
'key' => $key, 'value' => (string)$val, 'group_key' => 'post',
'remark' => '发布与费用配置', 'createtime' => $now, 'updatetime' => $now,
]);
}
}
// 同步月/年卡(价格 + 含发布条数);季卡价格按「月卡 ×3」的默认口径不动
Db::name('fc_plan')->where('plan_code', 'M1')->update([
'price' => $monthPrice, 'post_quota' => $monthQuota, 'updatetime' => $now,
]);
Db::name('fc_plan')->where('plan_code', 'M12')->update([
'price' => $yearPrice, 'post_quota' => $yearQuota, 'updatetime' => $now,
]);
$this->success('发布配置已保存并同步会员套餐');
}
/** 当前配置(缺失时回落 fee.* 与内置默认) */
protected function current()
{
$cfg = [];
foreach (Db::name('fc_config')->select() as $r) {
$cfg[$r['key']] = $r['value'];
}
$pick = function ($key, $default) use ($cfg) {
return (isset($cfg[$key]) && $cfg[$key] !== '') ? $cfg[$key] : $default;
};
$mode = (string)$pick('post.mode', 'paid');
return [
'mode' => in_array($mode, self::MODES, true) ? $mode : 'paid',
'singlePrice' => $pick('post.singlePrice', $pick('fee.singlePost', '5')),
// 免费发布条数:老库未保存过时回落 fee.freePostPerDay(默认 0 = 不限制)
'freeQuota' => $pick('post.freeQuota', $pick('fee.freePostPerDay', '0')),
'freeQuotaCycle' => $pick('post.freeQuotaCycle', 'day'),
'monthPrice' => $pick('post.monthPrice', $pick('fee.memberMonth', '19.9')),
'monthQuota' => $pick('post.monthQuota', '30'),
'yearPrice' => $pick('post.yearPrice', $pick('fee.memberYear', '168')),
'yearQuota' => $pick('post.yearQuota', '500'),
];
}
}