diff --git a/application/admin/command/FcInstall.php b/application/admin/command/FcInstall.php
index d520a57..a4a9d27 100644
--- a/application/admin/command/FcInstall.php
+++ b/application/admin/command/FcInstall.php
@@ -80,6 +80,9 @@ class FcInstall extends Command
$output->writeln('==> 发布模式与发布套餐升级');
$this->upgradePublish($output);
+ $output->writeln('==> 系统配置分组整理');
+ $this->upgradeConfigGroups($output);
+
$output->writeln('==> LBS / 店铺装修 / 支付 / 优惠券 / 举报升级(v1.7.0)');
$this->upgradeV170($output);
@@ -151,16 +154,16 @@ class FcInstall extends Command
/**
* 发布模式与发布套餐升级(幂等,可重复执行)
*
- * 背景:v1.0.7 新增「免费发布 / 付费发布」开关与付费发布的三档定价
+ * 背景:v1.0.7 新增「免费发布 / 付费发布」开关与付费发布定价
* · 单条价格 post.singlePrice
* · 免费发布条数 post.freeQuota / post.freeQuotaCycle(v1.0.10:达到条数后按单条价格计费)
- * · 按月(价格 + 含条数)post.monthPrice / post.monthQuota
- * · 按年(价格 + 含条数)post.yearPrice / post.yearQuota
+ * 套餐价格与含发布条数唯一真源 = fc_plan(后台「运营推广 → 会员套餐」维护),
+ * 不再使用 post.monthPrice / post.yearPrice 等配置键(历史键在本步骤清理)。
* 老库缺列/缺配置时补齐,**不覆盖运营已改过的值**:
* ① fc_user_ext 补 post_quota_left / post_quota_expire(剩余发布条数 + 有效期)
* ② fc_plan 补 post_quota(套餐含发布条数,月/年卡赠送额度)
- * ③ fc_config 补 post.* 八项(已存在则跳过)
- * ④ 月卡/年卡的价格与条数以配置为准做一次同步(配置缺失时反向用套餐值填配置)
+ * ③ fc_config 补 post.* 四项(已存在则跳过)
+ * ④ 清理历史遗留的套餐定价配置键(post.month* / post.year* / fee.*)
*/
private function upgradePublish(Output $output)
{
@@ -197,10 +200,6 @@ class FcInstall extends Command
['post.singlePrice', '5', 'post', '单条发布价格(元)=付费发布标准价'],
['post.freeQuota', '0', 'post', '免费发布条数(0 = 不限制/免费到底;达到条数后按单条发布价格计费)'],
['post.freeQuotaCycle', 'day', 'post', '免费条数统计周期:day 每日 / month 每月 / total 累计'],
- ['post.monthPrice', '19.9', 'post', '按月发布价格(元)'],
- ['post.monthQuota', '30', 'post', '按月含发布条数'],
- ['post.yearPrice', '168', 'post', '按年发布价格(元)'],
- ['post.yearQuota', '500', 'post', '按年含发布条数'],
];
$now = time();
$added = [];
@@ -219,27 +218,14 @@ class FcInstall extends Command
$changed++;
}
- // ④ 月/年卡「价格 + 含发布条数」与配置对齐
- $map = ['M1' => ['post.monthPrice', 'post.monthQuota'], 'M12' => ['post.yearPrice', 'post.yearQuota']];
- $synced = 0;
- foreach ($map as $code => $keys) {
- $plan = Db::name('fc_plan')->where('plan_code', $code)->find();
- if (!$plan) {
- continue;
- }
- $price = round((float)$this->cfgValue($keys[0], $plan['price']), 2);
- $quota = (int)$this->cfgValue($keys[1], $plan['post_quota']);
- if (round((float)$plan['price'], 2) !== $price || (int)$plan['post_quota'] !== $quota) {
- Db::name('fc_plan')->where('id', $plan['id'])->update([
- 'price' => $price,
- 'post_quota' => $quota,
- 'updatetime' => time(),
- ]);
- $synced++;
- }
- }
- if ($synced) {
- $output->writeln(' - 会员套餐已与发布配置对齐 ' . $synced . ' 条(月/年卡价格与含条数)');
+ // ④ 清理历史遗留配置键:套餐定价已统一到 fc_plan,发布免费条数只认 post.freeQuota
+ $deprecated = [
+ 'post.monthPrice', 'post.monthQuota', 'post.yearPrice', 'post.yearQuota',
+ 'fee.singlePost', 'fee.freePostPerDay', 'fee.memberMonth', 'fee.memberQuarter', 'fee.memberYear',
+ ];
+ $removedKeys = Db::name('fc_config')->where('key', 'in', $deprecated)->delete();
+ if ($removedKeys) {
+ $output->writeln(' - 清理废弃配置 ' . $removedKeys . ' 项(套餐定价真源已统一为 fc_plan)');
$changed++;
}
@@ -248,6 +234,43 @@ class FcInstall extends Command
: ' - 发布模式与套餐配置已是最新,无需处理');
}
+ /**
+ * 系统配置分组整理(幂等)
+ *
+ * 背景:fa_config 的 configgroup 字典保留了 FastAdmin 演示分组 user(会员配置)与
+ * example(示例分组),两者没有任何配置项,却会在「系统配置」页渲染出空白 tab,
+ * 且 user 与业务分组「会员配置」重名易混淆;这里从字典中移除。
+ */
+ private function upgradeConfigGroups(Output $output)
+ {
+ $row = Db::name('config')->where('name', 'configgroup')->find();
+ if (!$row) {
+ $output->writeln(' - 未找到 configgroup 字典,跳过');
+ return;
+ }
+ $decoded = json_decode((string)$row['value'], true);
+ if (!is_array($decoded)) {
+ $output->writeln(' - configgroup 字典格式异常,跳过');
+ return;
+ }
+ $target = ['basic', 'email', 'dictionary'];
+ $kept = [];
+ foreach ($decoded as $k => $v) {
+ if (in_array($k, $target, true) || strpos((string)$k, 'fc_') === 0) {
+ $kept[$k] = $v;
+ }
+ }
+ if ($kept === $decoded) {
+ $output->writeln(' - 系统配置分组已是最新,无需处理');
+ return;
+ }
+ Db::name('config')->where('name', 'configgroup')->update([
+ 'value' => json_encode($kept, JSON_UNESCAPED_UNICODE),
+ ]);
+ $removed = array_diff(array_keys($decoded), array_keys($kept));
+ $output->writeln(' - 已移除空分组:' . implode(' / ', $removed));
+ }
+
/**
* v1.7.0 升级:LBS 经纬度 / 店铺装修字段 / 收藏关注 / 支付与举报配置(幂等,可重复执行)
*
@@ -642,6 +665,21 @@ class FcInstall extends Command
protected function tables()
{
return [
+ 'wallet_flow' => "CREATE TABLE IF NOT EXISTS `#P#wallet_flow` (
+ `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID',
+ `flow_no` varchar(32) NOT NULL DEFAULT '' COMMENT '流水号',
+ `type` varchar(10) NOT NULL DEFAULT '' COMMENT 'income=入账 expense=支出(用户钱包口径)',
+ `scene` varchar(32) NOT NULL DEFAULT '' COMMENT '场景:余额充值/后台充值/会员开通/发布付费/付费查看/订单退款(余额)/佣金提现',
+ `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变动金额',
+ `balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '变动后钱包余额(fa_user.money)',
+ `related_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '关联单据ID(订单/信息/提现单)',
+ `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
+ `createtime` int(10) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `flow_no` (`flow_no`),
+ KEY `user_id` (`user_id`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-用户钱包流水';",
'config' => "CREATE TABLE IF NOT EXISTS `#P#config` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`key` varchar(64) NOT NULL DEFAULT '' COMMENT '配置键',
@@ -860,6 +898,7 @@ class FcInstall extends Command
`free_view_used` int(10) NOT NULL DEFAULT '0',
`post_quota_left` int(10) NOT NULL DEFAULT '0' COMMENT '剩余发布条数(月/年套餐赠送)',
`post_quota_expire` int(10) DEFAULT NULL COMMENT '发布额度有效期(=会员到期时间)',
+ `pay_password` varchar(255) NOT NULL DEFAULT '' COMMENT '支付密码(password_hash,空=未设置)',
`createtime` int(10) DEFAULT NULL,
`updatetime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
@@ -921,6 +960,7 @@ class FcInstall extends Command
'finance_flow' => "CREATE TABLE IF NOT EXISTS `#P#finance_flow` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '归属用户,0=平台/历史数据',
`flow_no` varchar(32) NOT NULL DEFAULT '',
`type` varchar(16) NOT NULL DEFAULT 'income' COMMENT 'income/expense',
`scene` varchar(32) NOT NULL DEFAULT '' COMMENT '业务场景',
@@ -930,7 +970,8 @@ class FcInstall extends Command
`remark` varchar(255) NOT NULL DEFAULT '',
`createtime` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
- UNIQUE KEY `flow_no` (`flow_no`)
+ UNIQUE KEY `flow_no` (`flow_no`),
+ KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同城-资金流水';",
'message' => "CREATE TABLE IF NOT EXISTS `#P#message` (
@@ -1035,20 +1076,12 @@ class FcInstall extends Command
['wx.secret', 'b023726713eebe3dad3876f5cea8ee6a', 'wx', '小程序AppSecret(仅服务端持有,不下发前端)'],
['pay.mchId', '', 'pay', '微信支付商户号'],
['pay.apiKey', '', 'pay', '微信支付密钥'],
- ['fee.singlePost', '5', 'fee', '单条发布价格(元)'],
- ['fee.freePostPerDay', '2', 'fee', '每日免费发布条数'],
- ['fee.memberMonth', '19.9', 'fee', '月卡价格'],
- ['fee.memberQuarter', '49.9', 'fee', '季卡价格'],
- ['fee.memberYear', '168', 'fee', '年卡价格'],
- // 发布模式与发布套餐(后台「发布与费用配置」页维护;真源=配置,保存时同步 fc_plan)
+ // 发布模式与发布价格(后台「系统配置 → 发布配置」维护)
+ // 套餐价格与含发布条数在 fc_plan(后台「运营推广 → 会员套餐」维护),不走配置键
['post.mode', 'free', 'post', '发布模式:free 免费发布 / paid 付费发布(默认免费,避免上线即撞付费墙)'],
['post.singlePrice', '5', 'post', '单条发布价格(元)=付费发布标准价'],
['post.freeQuota', '0', 'post', '免费发布条数(0 = 不限制/免费到底;达到条数后按单条发布价格计费)'],
['post.freeQuotaCycle', 'day', 'post', '免费条数统计周期:day 每日 / month 每月 / total 累计'],
- ['post.monthPrice', '19.9', 'post', '按月发布价格(元)'],
- ['post.monthQuota', '30', 'post', '按月含发布条数'],
- ['post.yearPrice', '168', 'post', '按年发布价格(元)'],
- ['post.yearQuota', '500', 'post', '按年含发布条数'],
['dist.enabled', '1', 'dist', '是否开启分销'],
['dist.level', '2', 'dist', '分佣层级'],
['dist.level1Rate', '20', 'dist', '一级分佣比例%'],
diff --git a/application/admin/command/FcVerifyDist.php b/application/admin/command/FcVerifyDist.php
index 54fbe27..02e18ec 100644
--- a/application/admin/command/FcVerifyDist.php
+++ b/application/admin/command/FcVerifyDist.php
@@ -134,6 +134,7 @@ class FcVerifyDist extends Command
'updatetime' => time(),
]);
Db::name('fc_finance_flow')->insert([
+ 'user_id' => $userBId,
'flow_no' => 'WD' . date('YmdHis') . $withdrawId,
'type' => 'expense',
'scene' => '佣金提现',
diff --git a/application/admin/controller/fc/Order.php b/application/admin/controller/fc/Order.php
index 38a5848..c029fea 100644
--- a/application/admin/controller/fc/Order.php
+++ b/application/admin/controller/fc/Order.php
@@ -101,6 +101,7 @@ class Order extends Backend
'updatetime' => time(),
]);
Db::name('fc_finance_flow')->insert([
+ 'user_id' => (int)$order['user_id'],
'flow_no' => 'SK' . date('YmdHis') . $order['id'],
'type' => 'income',
'scene' => '订单补单',
diff --git a/application/admin/controller/fc/Publish.php b/application/admin/controller/fc/Publish.php
deleted file mode 100644
index 6dcfb85..0000000
--- a/application/admin/controller/fc/Publish.php
+++ /dev/null
@@ -1,158 +0,0 @@
-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'),
- ];
- }
-}
diff --git a/application/admin/controller/fc/Userext.php b/application/admin/controller/fc/Userext.php
index af277b0..fe0086c 100644
--- a/application/admin/controller/fc/Userext.php
+++ b/application/admin/controller/fc/Userext.php
@@ -109,11 +109,25 @@ class Userext extends Backend
'updatetime' => $now,
]);
- // 资金流水(平台口径:余额充值属于收入)
+ // 资金流水(平台口径:充值属于收入;scene=后台充值 与用户微信充值区分)
Db::name('fc_finance_flow')->insert([
+ 'user_id' => $uid,
'flow_no' => 'R' . date('YmdHis') . mt_rand(100, 999) . $uid,
'type' => 'income',
- 'scene' => '余额充值',
+ 'scene' => '后台充值',
+ 'amount' => $amount,
+ 'balance' => $balance,
+ 'related_id' => $uid,
+ 'remark' => $remark !== '' ? $remark : ('后台充值给用户 #' . $uid),
+ 'createtime' => $now,
+ ]);
+
+ // 用户钱包流水(fa_user.money 增)→ 余额中心「余额明细」
+ Db::name('fc_wallet_flow')->insert([
+ 'user_id' => $uid,
+ 'flow_no' => 'W' . date('YmdHis') . mt_rand(100, 999) . $uid,
+ 'type' => 'income',
+ 'scene' => '后台充值',
'amount' => $amount,
'balance' => $balance,
'related_id' => $uid,
diff --git a/application/admin/lang/zh-cn/fc/publish.php b/application/admin/lang/zh-cn/fc/publish.php
deleted file mode 100644
index c2c974e..0000000
--- a/application/admin/lang/zh-cn/fc/publish.php
+++ /dev/null
@@ -1,48 +0,0 @@
- '说明',
- '发布模式' => '发布模式',
- '免费发布(所有用户均可免费发布)' => '免费发布(所有用户均可免费发布)',
- '付费发布(按下面价格计费,套餐额度内免费)' => '付费发布(按下面价格计费,套餐额度内免费)',
- '会员单条发布价格' => '会员单条发布价格',
- '免费发布条数' => '免费发布条数',
- '条' => '条',
- '每日' => '每日',
- '每月' => '每月',
- '累计' => '累计',
- '会员按月发布' => '会员按月发布',
- '会员按年发布' => '会员按年发布',
- '元/条' => '元/条',
- '元/月' => '元/月',
- '元/年' => '元/年',
- '条/月' => '条/月',
- '条/年' => '条/年',
- '保存' => '保存',
- '是否' => '是否',
- '是' => '是',
- '否' => '否',
- '当前会员套餐' => '当前会员套餐',
- '套餐编码' => '套餐编码',
- '套餐名称' => '套餐名称',
- '售价' => '售价',
- '有效天数' => '有效天数',
- '含发布条数' => '含发布条数',
- '是否启用' => '是否启用',
- '本页是「发布模式 + 发布价格/条数」的唯一入口;保存后会同步「会员套餐」的月/年卡价格与含发布条数,因此用户端发布页、会员中心与后端计费口径一致。'
- => '本页是「发布模式 + 发布价格/条数」的唯一入口;保存后会同步「会员套餐」的月/年卡价格与含发布条数,因此用户端发布页、会员中心与后端计费口径一致。',
- '「会员单条发布价格」= 未购买套餐时按条计费的价格;「按月/按年」= 会员套餐价格,并赠送对应条数的发布额度(额度随会员有效期,过期自动清零)。'
- => '「会员单条发布价格」= 未购买套餐时按条计费的价格;「按月/按年」= 会员套餐价格,并赠送对应条数的发布额度(额度随会员有效期,过期自动清零)。',
- '未开通套餐的用户按此价格付费发布(金额由服务端计算,前端不可传价)'
- => '未开通套餐的用户按此价格付费发布(金额由服务端计算,前端不可传价)',
- '免费发布条数:免费发布模式下前 N 条免费,达到条数后按「会员单条发布价格」计费(计费标准与付费发布一致);付费发布模式下同样保留这 N 条免费额度(避免一开付费就是硬付费墙)。填 0 = 不限制(免费到底)。'
- => '免费发布条数:免费发布模式下前 N 条免费,达到条数后按「会员单条发布价格」计费(计费标准与付费发布一致);付费发布模式下同样保留这 N 条免费额度(避免一开付费就是硬付费墙)。填 0 = 不限制(免费到底)。',
- '计费顺序:套餐额度 → 免费发布条数 → 单条付费;免费发布条数达到后即按「会员单条发布价格」计费,金额一律由服务端计算。'
- => '计费顺序:套餐额度 → 免费发布条数 → 单条付费;免费发布条数达到后即按「会员单条发布价格」计费,金额一律由服务端计算。',
- '价格与条数会同步到「会员套餐 → 月卡(M1)」;购买后立即赠送该条数的发布额度'
- => '价格与条数会同步到「会员套餐 → 月卡(M1)」;购买后立即赠送该条数的发布额度',
- '价格与条数会同步到「会员套餐 → 年卡(M12)」;购买后立即赠送该条数的发布额度'
- => '价格与条数会同步到「会员套餐 → 年卡(M12)」;购买后立即赠送该条数的发布额度',
- '(月/年卡的价格与含发布条数由本页同步,其余字段在「交易管理 → 会员套餐」维护)'
- => '(月/年卡的价格与含发布条数由本页同步,其余字段在「交易管理 → 会员套餐」维护)',
-];
diff --git a/application/admin/view/fc/publish/index.html b/application/admin/view/fc/publish/index.html
deleted file mode 100644
index 33d2622..0000000
--- a/application/admin/view/fc/publish/index.html
+++ /dev/null
@@ -1,138 +0,0 @@
-
- {:build_heading()}
-
-
-
- {:__('说明')}:
- {:__('本页是「发布模式 + 发布价格/条数」的唯一入口;保存后会同步「会员套餐」的月/年卡价格与含发布条数,因此用户端发布页、会员中心与后端计费口径一致。')}
-
- {:__('「会员单条发布价格」= 未购买套餐时按条计费的价格;「按月/按年」= 会员套餐价格,并赠送对应条数的发布额度(额度随会员有效期,过期自动清零)。')}
-
- {:__('计费顺序:套餐额度 → 免费发布条数 → 单条付费;免费发布条数达到后即按「会员单条发布价格」计费,金额一律由服务端计算。')}
-
-
-
-
-
-
- {:__('当前会员套餐')}
- {:__('(月/年卡的价格与含发布条数由本页同步,其余字段在「交易管理 → 会员套餐」维护)')}
-
-
-
-
- | {:__('套餐编码')} |
- {:__('套餐名称')} |
- {:__('售价')} |
- {:__('有效天数')} |
- {:__('含发布条数')} |
- {:__('是否启用')} |
-
-
-
- {foreach $plans as $p}
-
- | {$p.plan_code} |
- {$p.name} |
- ¥{$p.price} |
- {$p.days} |
- {$p.post_quota} |
- {if $p.enabled}{:__('是')}{else/}{:__('否')}{/if} |
-
- {/foreach}
-
-
-
-
-
diff --git a/application/api/controller/AdminSetting.php b/application/api/controller/AdminSetting.php
index 96a4e7c..1e2f40e 100644
--- a/application/api/controller/AdminSetting.php
+++ b/application/api/controller/AdminSetting.php
@@ -96,29 +96,38 @@ class AdminSetting extends V1
/**
* AS3 费用配置
* GET → {singlePost,freePostPerDay,memberMonth,memberQuarter,memberYear}
- * POST 入参同字段 → 合并后配置;同时同步 fc_plan 价格
+ * POST 入参同字段 → 合并后配置
+ * 真源对齐:singlePost → post.singlePrice;freePostPerDay → post.freeQuota(按日);
+ * 月/季/年卡价格 → fc_plan(与后台「运营推广 → 会员套餐」同源,改价即时生效)
*/
public function fee()
{
if ($this->request->isPost()) {
- $singlePost = (float)$this->input('singlePost', $this->cfg('fee.singlePost', 5));
+ $singlePost = (float)$this->input('singlePost', $this->cfg('post.singlePrice', 5));
if ($singlePost < 0) {
$this->fail('单条发布价格不能为负');
}
- $freePostPerDay = (int)$this->input('freePostPerDay', $this->cfg('fee.freePostPerDay', 2));
- $memberMonth = (float)$this->input('memberMonth', $this->cfg('fee.memberMonth', 19.9));
- $memberQuarter = (float)$this->input('memberQuarter', $this->cfg('fee.memberQuarter', 49.9));
- $memberYear = (float)$this->input('memberYear', $this->cfg('fee.memberYear', 168));
+ $freePostPerDay = (int)$this->input('freePostPerDay', $this->cfg('post.freeQuota', 0));
+ if ($freePostPerDay < 0) {
+ $this->fail('免费发布条数不能为负');
+ }
- $this->setCfg('fee.singlePost', $singlePost);
- $this->setCfg('fee.freePostPerDay', $freePostPerDay);
- $this->setCfg('fee.memberMonth', $memberMonth);
- $this->setCfg('fee.memberQuarter', $memberQuarter);
- $this->setCfg('fee.memberYear', $memberYear);
+ $this->setCfg('post.singlePrice', $singlePost);
+ $this->setCfg('post.freeQuota', $freePostPerDay);
+ $this->setCfg('post.freeQuotaCycle', 'day');
- Db::name('fc_plan')->where('plan_code', 'M1')->update(['price' => $memberMonth]);
- Db::name('fc_plan')->where('plan_code', 'M3')->update(['price' => $memberQuarter]);
- Db::name('fc_plan')->where('plan_code', 'M12')->update(['price' => $memberYear]);
+ $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('修改费用配置');
}
@@ -334,15 +343,20 @@ class AdminSetting extends V1
];
}
- /** 费用配置结构 */
+ /** 费用配置结构(月/季/年卡价格真源=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' => (float)$this->cfg('fee.singlePost', 5),
- 'freePostPerDay' => (int)$this->cfg('fee.freePostPerDay', 2),
- 'memberMonth' => (float)$this->cfg('fee.memberMonth', 19.9),
- 'memberQuarter' => (float)$this->cfg('fee.memberQuarter', 49.9),
- 'memberYear' => (float)$this->cfg('fee.memberYear', 168),
+ '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),
];
}
diff --git a/application/api/controller/Member.php b/application/api/controller/Member.php
index 15d8390..74a1955 100644
--- a/application/api/controller/Member.php
+++ b/application/api/controller/Member.php
@@ -157,8 +157,12 @@ class Member extends V1
$isBalance = $payType === 'balance';
// 余额校验必须在落订单之前:否则余额不足时已插入一条 status=paid 的假订单
- if ($isBalance && (float)$user['money'] < $amount) {
- $this->fail('余额不足');
+ if ($isBalance) {
+ if ((float)$user['money'] < $amount) {
+ $this->fail('余额不足');
+ }
+ // 后台开启支付密码时,余额支付需校验
+ $this->verifyPayPassword($user);
}
$orderNo = $this->orderNo('MO');
@@ -206,6 +210,9 @@ class Member extends V1
$grant = $this->grantMembership(Db::name('fc_order')->where('id', $orderId)->find());
// 券核销(微信支付在 OrderFulfill 里核销)
\app\common\library\CouponService::markUsed($orderNo);
+ // 钱包流水(fa_user.money 减)→ 余额中心「余额明细」
+ $balance = round((float)Db::name('user')->where('id', (int)$user['id'])->value('money'), 2);
+ $this->walletFlow($user['id'], 'expense', '会员开通', $amount, $balance, $orderId, (string)$plan['name']);
}
});
} catch (\Throwable $e) {
@@ -289,7 +296,7 @@ class Member extends V1
$amount = round((float)(isset($order['amount']) ? $order['amount'] : 0), 2);
$orderId = (int)(isset($order['id']) ? $order['id'] : 0);
- $this->flow('income', '会员开通', $amount, 0, $orderId);
+ $this->flow('income', '会员开通', $amount, 0, $orderId, '', $userId);
$this->notify(
$userId,
'system',
@@ -397,10 +404,104 @@ class Member extends V1
}
$this->ok([
- 'balance' => (float)$user['money'],
- 'coupons' => $coupons,
- 'couponCount' => count($coupons),
- 'points' => (int)$user['score'],
+ 'balance' => (float)$user['money'],
+ 'coupons' => $coupons,
+ 'couponCount' => count($coupons),
+ 'points' => (int)$user['score'],
+ 'payPasswordEnabled' => (int)$this->cfg('pay.payPasswordEnabled', 0) === 1,
+ 'hasPayPassword' => (string)$this->ext($user['id'])['pay_password'] !== '',
]);
}
+
+ /**
+ * 余额明细(余额中心交易记录)
+ * GET /api/v1/member/flows {page,size,type} → {list:[...],page,size,total,hasMore}
+ * type 筛选:recharge=充值 / expense=消费 / refund=退款,缺省=全部
+ * 数据源:fc_wallet_flow(用户钱包流水,仅记录 fa_user.money 增减;
+ * fc_finance_flow 是平台对账口径,含微信收入等非钱包变动,不在此展示)
+ */
+ public function flows()
+ {
+ $user = $this->auth();
+
+ $type = (string)$this->input('type', '');
+ list($rows, $total, $page, $size) = $this->pageQuery(function ($forCount) use ($user, $type) {
+ // TP5.0 禁止复用同一 Query 先 count 再 select,必须走 pageQuery 重建
+ $q = Db::name('fc_wallet_flow')->where('user_id', $user['id']);
+ if ($type === 'recharge') {
+ // 充值:微信充值入账与后台充值
+ $q->where('scene', 'in', ['余额充值', '后台充值']);
+ } elseif ($type === 'expense') {
+ // 消费:会员开通 / 发布付费 / 付费查看 / 充值退款扣回等支出
+ $q->where('type', 'expense');
+ } elseif ($type === 'refund') {
+ // 退款:订单退款(余额原路退回)
+ $q->where('scene', 'like', '%退款%');
+ }
+ return $forCount ? $q : $q->order('id desc');
+ }, 15);
+
+ $list = [];
+ foreach ($rows as $row) {
+ $list[] = [
+ 'flowNo' => (string)$row['flow_no'],
+ 'type' => (string)$row['type'],
+ 'scene' => (string)$row['scene'],
+ 'amount' => round((float)$row['amount'], 2),
+ 'balance' => round((float)$row['balance'], 2),
+ 'remark' => (string)$row['remark'],
+ 'createdAt' => !empty($row['createtime']) ? $this->iso((int)$row['createtime']) : null,
+ ];
+ }
+
+ $this->ok($this->paginate($list, $total, $page, $size));
+ }
+
+ /**
+ * 设置/修改支付密码(6 位数字)
+ * POST /api/v1/member/pay-password/set {password, oldPassword?}
+ * 首次设置无需 oldPassword;已设置时必须携带正确的 oldPassword(找回走人工/后续 SMS)。
+ */
+ public function setPayPassword()
+ {
+ $user = $this->auth();
+ $this->rateLimit('payswd:' . $user['id'], 10, 60);
+
+ $password = trim((string)$this->input('password', ''));
+ $oldPassword = trim((string)$this->input('oldPassword', ''));
+
+ if (!preg_match('/^\d{6}$/', $password)) {
+ $this->fail('支付密码必须是 6 位数字');
+ }
+
+ $ext = $this->ext($user['id']);
+ $hash = (string)(isset($ext['pay_password']) ? $ext['pay_password'] : '');
+
+ if ($hash !== '') {
+ // 修改密码:必须验证旧密码
+ $old = trim((string)$oldPassword);
+ if ($old === '') {
+ $this->fail('请输入原支付密码');
+ }
+ if (!preg_match('/^\d{6}$/', $old) || !password_verify($old, $hash)) {
+ $this->fail('原支付密码错误');
+ }
+ if ($old === $password) {
+ $this->fail('新密码不能与原密码相同');
+ }
+ }
+
+ // 弱密码拦截:6 位连号/同号(如 123456 / 888888)
+ if (preg_match('/^(\d)\1{5}$/', $password)
+ || in_array($password, ['012345', '123456', '234567', '345678', '456789', '987654', '876543'], true)) {
+ $this->fail('支付密码过于简单,请更换');
+ }
+
+ Db::name('fc_user_ext')->where('user_id', $user['id'])->update([
+ 'pay_password' => password_hash($password, PASSWORD_DEFAULT),
+ 'updatetime' => time(),
+ ]);
+
+ $this->ok(['hasPayPassword' => true]);
+ }
}
diff --git a/application/api/controller/Order.php b/application/api/controller/Order.php
index 7488766..5fdb7a3 100644
--- a/application/api/controller/Order.php
+++ b/application/api/controller/Order.php
@@ -99,7 +99,9 @@ class Order extends V1
$balance = round((float)Db::name('user')->where('id', $userId)->value('money'), 2);
Db::name('user')->where('id', $userId)->update(['updatetime' => $now]);
- $this->flow('income', '余额充值', $amount, $balance, $orderId, '余额充值');
+ $this->flow('income', '余额充值', $amount, $balance, $orderId, '余额充值', $userId);
+ // 钱包流水(fa_user.money 增)→ 余额中心「余额明细」
+ $this->walletFlow($userId, 'income', '余额充值', $amount, $balance, $orderId, '余额充值');
$this->notify(
$userId,
'system',
diff --git a/application/api/controller/Post.php b/application/api/controller/Post.php
index c22f1ac..6f4787b 100644
--- a/application/api/controller/Post.php
+++ b/application/api/controller/Post.php
@@ -225,8 +225,12 @@ class Post extends V1
}
// 余额校验须在落库之前(按抵扣后的实付金额判断)
- if ($paid && $payType === 'balance' && (float)$user['money'] < $amount) {
- $this->fail('余额不足');
+ if ($paid && $payType === 'balance') {
+ if ((float)$user['money'] < $amount) {
+ $this->fail('余额不足');
+ }
+ // 后台开启支付密码时,余额支付需校验
+ $this->verifyPayPassword($user);
}
// 区域:优先 districtId,其次 area 名称,最后按坐标匹配最近区县
@@ -344,7 +348,9 @@ class Post extends V1
]);
if ($paidNow) {
- $this->flow('income', '发布付费', $amount, $balanceAfter, $postId);
+ $this->flow('income', '发布付费', $amount, $balanceAfter, $postId, '', (int)$user['id']);
+ // 钱包流水(fa_user.money 减,用户口径为支出)→ 余额中心「余额明细」
+ $this->walletFlow($user['id'], 'expense', '发布付费', $amount, $balanceAfter, $postId, $title);
// 余额支付:核销优惠券 + 触发分销结算
CouponService::markUsed($orderNo);
@@ -514,7 +520,7 @@ class Post extends V1
}
/**
- * 发布规则(后台「发布与费用配置」维护;配置缺失时回落旧 fee.* 再回落内置默认)
+ * 发布规则(真源:发布规则=「系统配置 → 发布配置」post.*;套餐价格/条数=「运营推广 → 会员套餐」fc_plan)
* @return array
*/
protected function publishRule()
@@ -523,13 +529,22 @@ class Post extends V1
if (!in_array($mode, ['free', 'paid'], true)) {
$mode = 'paid';
}
+
+ // 套餐价格与含发布条数唯一真源:fc_plan(按 plan_code 取,找不到时用内置默认)
+ $plans = Db::name('fc_plan')->where('enabled', 1)->column('price,post_quota', 'plan_code');
+ $planValue = function ($code, $field, $default) use ($plans) {
+ return isset($plans[$code][$field]) && $plans[$code][$field] !== null && $plans[$code][$field] !== ''
+ ? $plans[$code][$field]
+ : $default;
+ };
+
return [
'mode' => $mode,
- 'singlePrice' => round((float)$this->cfg('post.singlePrice', $this->cfg('fee.singlePost', 5)), 2),
- 'monthPrice' => round((float)$this->cfg('post.monthPrice', $this->cfg('fee.memberMonth', 19.9)), 2),
- 'monthQuota' => max(0, (int)$this->cfg('post.monthQuota', 30)),
- 'yearPrice' => round((float)$this->cfg('post.yearPrice', $this->cfg('fee.memberYear', 168)), 2),
- 'yearQuota' => max(0, (int)$this->cfg('post.yearQuota', 500)),
+ 'singlePrice' => round((float)$this->cfg('post.singlePrice', 5), 2),
+ 'monthPrice' => round((float)$planValue('M1', 'price', 19.9), 2),
+ 'monthQuota' => max(0, (int)$planValue('M1', 'post_quota', 30)),
+ 'yearPrice' => round((float)$planValue('M12', 'price', 168), 2),
+ 'yearQuota' => max(0, (int)$planValue('M12', 'post_quota', 500)),
];
}
@@ -537,9 +552,8 @@ class Post extends V1
* 免费发布条数规则(v1.0.10)
* post.freeQuota 免费发布条数(0 = 不限制,免费发布模式下「免费到底」)
* post.freeQuotaCycle 统计周期:day 每日 / month 每月 / total 累计(不重置)
- * 兼容:配置项 **缺失** 时(老库未执行新版 fc:install)回落 `fee.freePostPerDay` 按日计;
- * 一旦配置项存在(后台「发布与费用配置」保存过或 fc:install 种过),就以配置为准,
- * 此时 `0` 表示「不限制」而不再回落旧值。
+ * 配置项缺失时(未执行过 fc:install)按 0 条处理;一旦配置存在即以配置为准,
+ * 此时 `0` 表示「不限制」。
* @return array{quota:int,cycle:string}
*/
protected function freeQuotaRule()
@@ -550,8 +564,7 @@ class Post extends V1
}
$raw = $this->cfg('post.freeQuota', null);
if ($raw === null || $raw === '') {
- $legacy = max(0, (int)$this->cfg('fee.freePostPerDay', 0));
- return ['quota' => $legacy, 'cycle' => $legacy > 0 ? 'day' : $cycle];
+ return ['quota' => 0, 'cycle' => $cycle];
}
return ['quota' => max(0, (int)$raw), 'cycle' => $cycle];
}
@@ -987,6 +1000,14 @@ class Post extends V1
'wechat' => $wechat,
];
+ $amount = round((float)$this->cfg('fee.payView', 2), 2);
+ $now = time();
+
+ $ext = $this->ext($user['id']);
+ $isMember = (int)$ext['is_member'] === 1
+ && !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now;
+ $quota = $this->memberFreeViewQuota();
+
// 已解锁直接返回(严格返回纯净扁平字段,杜绝 JSON 嵌套导致前端格式错乱)
$exist = Db::name('fc_pay_view')
->where('user_id', $user['id'])
@@ -1009,14 +1030,14 @@ class Post extends V1
$amount = round((float)$this->cfg('fee.payView', 2), 2);
$now = time();
- $ext = $this->ext($user['id']);
- $isMember = (int)$ext['is_member'] === 1
- && !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now;
- $quota = $this->memberFreeViewQuota();
-
$orderId = 0;
$pending = null;
+ // 后台开启支付密码时,余额支付需校验(会员免费额度未用完时走免费,不要求密码)
+ if ($payType === 'balance' && !($isMember && $quota > 0)) {
+ $this->verifyPayPassword($user);
+ }
+
try {
Db::transaction(function () use (&$orderId, &$pending, $isMember, $quota, $payType, $amount, $user, $id, $now) {
// 会员每日免费查看额度(当天用完后按正常付费)
@@ -1038,7 +1059,29 @@ class Post extends V1
throw new \RuntimeException('余额不足,请充值或使用微信支付');
}
$balance = (float)Db::name('user')->where('id', (int)$user['id'])->value('money');
- $this->flow('expense', '付费查看', $amount, $balance, $id);
+ // 余额支付为「即时支付」:同步落一笔已支付订单,保证「我的订单」与微信支付口径一致
+ $orderNo = $this->orderNo('VO');
+ $orderId = Db::name('fc_order')->insertGetId([
+ 'order_no' => $orderNo,
+ 'user_id' => $user['id'],
+ 'type' => 'payview',
+ 'title' => '付费查看 #' . $id,
+ 'amount' => $amount,
+ 'pay_type' => 'balance',
+ 'status' => 'paid',
+ 'paytime' => $now,
+ 'related_id' => $id,
+ 'createtime' => $now,
+ 'updatetime' => $now,
+ ]);
+ $this->flow('expense', '付费查看', $amount, $balance, $id, '', (int)$user['id']);
+ // 钱包流水(fa_user.money 减)→ 余额中心「余额明细」
+ $this->walletFlow($user['id'], 'expense', '付费查看', $amount, $balance, $id, '解锁信息 #' . $id);
+ // 余额支付同样触发分销结算(与微信回调 OrderFulfill::handle 口径一致)
+ $orderRow = Db::name('fc_order')->where('id', $orderId)->find();
+ if ($orderRow) {
+ DistributionService::settleOrder($orderRow);
+ }
} else {
// 微信支付:只落 pending 订单(不预写解锁记录),
// 支付成功后由 /pay/notify 按 type=payview + related_id 解锁(回调可独立完成)
diff --git a/application/api/controller/Setting.php b/application/api/controller/Setting.php
index e52e22c..aa3d510 100644
--- a/application/api/controller/Setting.php
+++ b/application/api/controller/Setting.php
@@ -46,6 +46,8 @@ class Setting extends V1
'audit' => (string)$this->cfg('wx.tplAudit', ''),
'transfer' => (string)$this->cfg('wx.tplTransfer', ''),
],
+ // 余额支付密码开关(开启后余额支付需验证 6 位支付密码,用户在余额中心设置)
+ 'payPasswordEnabled' => (int)$this->cfg('pay.payPasswordEnabled', 0) === 1,
]);
}
diff --git a/application/api/controller/V1.php b/application/api/controller/V1.php
index fa04d4d..6930c73 100644
--- a/application/api/controller/V1.php
+++ b/application/api/controller/V1.php
@@ -606,12 +606,16 @@ class V1 extends Api
return is_array($decoded) ? $decoded : [];
}
- /** 记一条资金流水 */
- protected function flow($type, $scene, $amount, $balance, $relatedId = 0, $remark = '')
+ /**
+ * 记一条资金流水(含归属用户,供余额中心按用户查询)
+ * @param int $userId 归属用户 ID(0=平台/不可归属)
+ */
+ protected function flow($type, $scene, $amount, $balance, $relatedId = 0, $remark = '', $userId = 0)
{
static $seq = 0;
$seq++;
Db::name('fc_finance_flow')->insert([
+ 'user_id' => (int)$userId,
'flow_no' => 'F' . date('YmdHis') . bin2hex(random_bytes(4)) . $seq,
'type' => $type,
'scene' => $scene,
@@ -623,6 +627,55 @@ class V1 extends Api
]);
}
+ /**
+ * 记一条「用户钱包流水」(fc_wallet_flow)
+ * 仅在 fa_user.money 发生增减时调用——余额中心的「余额明细」数据源。
+ * 平台对账口径(微信收入等)继续走 flow() / fc_finance_flow。
+ * @param int $userId 用户 ID
+ * @param string $type income=入账 expense=支出(用户钱包口径)
+ * @param string $scene 场景:余额充值/后台充值/会员开通/发布付费/付费查看/订单退款(余额)/佣金提现
+ * @param float $amount 变动金额(正数)
+ * @param float $balance 变动后钱包余额
+ */
+ protected function walletFlow($userId, $type, $scene, $amount, $balance, $relatedId = 0, $remark = '')
+ {
+ Db::name('fc_wallet_flow')->insert([
+ 'user_id' => (int)$userId,
+ 'flow_no' => 'W' . date('YmdHis') . bin2hex(random_bytes(4)),
+ 'type' => $type,
+ 'scene' => $scene,
+ 'amount' => $amount,
+ 'balance' => $balance,
+ 'related_id' => (int)$relatedId,
+ 'remark' => $remark,
+ 'createtime' => time(),
+ ]);
+ }
+
+ /**
+ * 余额支付前的支付密码校验(开关关闭时直接放行)
+ * 规则:后台开启「支付密码」后,所有余额支付必须携带正确的 payPassword;
+ * 未设置过密码时提示先设置(余额中心 → 支付密码)。
+ */
+ protected function verifyPayPassword($user)
+ {
+ if (!(int)$this->cfg('pay.payPasswordEnabled', 0)) {
+ return;
+ }
+ $ext = $this->ext($user['id']);
+ $hash = (string)(isset($ext['pay_password']) ? $ext['pay_password'] : '');
+ if ($hash === '') {
+ $this->fail('请先在「余额中心」设置支付密码');
+ }
+ $pwd = trim((string)$this->input('payPassword', ''));
+ if ($pwd === '') {
+ $this->fail('请输入支付密码');
+ }
+ if (!preg_match('/^\d{6}$/', $pwd) || !password_verify($pwd, $hash)) {
+ $this->fail('支付密码错误');
+ }
+ }
+
/** 发一条站内消息 */
protected function notify($userId, $type, $title, $content)
{
diff --git a/application/common/library/FcConfig.php b/application/common/library/FcConfig.php
index 99655d7..dd55134 100644
--- a/application/common/library/FcConfig.php
+++ b/application/common/library/FcConfig.php
@@ -81,23 +81,27 @@ class FcConfig
'total' => '累计',
]),
self::item('fc_post_singlePrice', '单条价格', 'number', $pick('post.singlePrice', '5'), '未开通套餐时的单条发布价格(元)', 'required;number'),
- self::item('fc_post_monthPrice', '月卡价格', 'number', $pick('post.monthPrice', '19.9'), '会员月卡价格(元),保存后同步会员套餐', 'required;number'),
- self::item('fc_post_monthQuota', '月卡条数', 'number', $pick('post.monthQuota', '30'), '月卡包含的发布条数', 'required;number'),
- self::item('fc_post_yearPrice', '年卡价格', 'number', $pick('post.yearPrice', '168'), '会员年卡价格(元),保存后同步会员套餐', 'required;number'),
- self::item('fc_post_yearQuota', '年卡条数', 'number', $pick('post.yearQuota', '500'), '年卡包含的发布条数', 'required;number'),
],
],
'fc_member' => [
'name' => 'fc_member',
- 'title' => '会员权益配置',
+ 'title' => '会员配置',
'active' => false,
'list' => [
self::item('fc_member_freeView', '每日免费查看条数', 'number', $freeView, '会员每日可免费查看联系方式条数(默认 20 条)', 'required;number'),
self::item('fc_member_postDiscount', '单条发布折扣', 'string', $postDiscount, '付费发布时会员专享折扣,如「8折」或「8.5折」(默认 8 折)', 'required'),
- self::item('fc_fee_payView', '非会员查看价格', 'number', $payViewPrice, '非会员单条解锁联系方式费用(元,默认 1 元)', 'required;number'),
+ self::item('fc_member_payView', '非会员查看价格', 'number', $payViewPrice, '非会员单条解锁联系方式费用(元,默认 1 元)', 'required;number'),
self::item('fc_member_benefits', '会员特权清单', 'text', $benefitsText, '小程序端展示的特权清单,每行一条(支持后台自由增删与修改)'),
],
],
+ 'fc_pay' => [
+ 'name' => 'fc_pay',
+ 'title' => '支付配置',
+ 'active' => false,
+ 'list' => [
+ self::item('fc_pay_passwordEnabled', '余额支付密码', 'switch', $pick('pay.payPasswordEnabled', '0'), '开启后,用户使用余额支付(会员开通/付费发布/付费查看)时需验证 6 位支付密码;用户在「余额中心」自行设置,默认关闭'),
+ ],
+ ],
'fc_dist' => [
'name' => 'fc_dist',
'title' => '分销配置',
@@ -141,14 +145,29 @@ class FcConfig
if ($has('fc_post_')) {
self::savePublish($row);
}
- if ($has('fc_member_') || $has('fc_fee_')) {
+ 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 位支付密码)');
+ }
+
/**
* 小程序基础信息
*
@@ -207,7 +226,7 @@ class FcConfig
}
/**
- * 发布模式与价格(同时同步会员套餐 M1/M12)
+ * 发布模式与价格(套餐定价在「运营推广 → 会员套餐」维护,本页不涉及)
*
* @param array $row
* @return void
@@ -239,19 +258,10 @@ class FcConfig
};
$single = $num('fc_post_singlePrice', 5);
- $monthPrice = $num('fc_post_monthPrice', 19.9);
- $yearPrice = $num('fc_post_yearPrice', 168);
- $monthQuota = $int('fc_post_monthQuota', 30);
- $yearQuota = $int('fc_post_yearQuota', 500);
$freeQuota = $int('fc_post_freeQuota', 0);
- foreach (['单条发布价格' => $single, '月卡价格' => $monthPrice, '年卡价格' => $yearPrice] as $label => $value) {
- if ($value < 0) {
- throw new \Exception($label . '不能为负');
- }
- }
- if ($monthQuota < 0 || $yearQuota < 0) {
- throw new \Exception('含发布条数不能为负');
+ if ($single < 0) {
+ throw new \Exception('单条发布价格不能为负');
}
if ($freeQuota < 0) {
throw new \Exception('免费发布条数不能为负(0 表示不限制)');
@@ -262,27 +272,10 @@ class FcConfig
'post.singlePrice' => $single,
'post.freeQuota' => $freeQuota,
'post.freeQuotaCycle' => $cycle,
- 'post.monthPrice' => $monthPrice,
- 'post.monthQuota' => $monthQuota,
- 'post.yearPrice' => $yearPrice,
- 'post.yearQuota' => $yearQuota,
];
foreach ($values as $key => $value) {
self::put($key, $value, 'post', '发布配置');
}
-
- // 与会员套餐保持一致(后台「运营推广 → 会员套餐」仍可单独维护,但价格/条数以本页为准)
- $now = time();
- 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,
- ]);
}
/**
@@ -299,7 +292,7 @@ class FcConfig
if ($postDiscount === '') {
$postDiscount = '8 折';
}
- $payView = isset($row['fc_fee_payView']) ? max(0, round((float)$row['fc_fee_payView'], 2)) : 1;
+ $payView = isset($row['fc_member_payView']) ? max(0, round((float)$row['fc_member_payView'], 2)) : 1;
$benefitsRaw = isset($row['fc_member_benefits']) ? trim((string)$row['fc_member_benefits']) : '';
$benefits = [];
diff --git a/application/common/library/OrderRefund.php b/application/common/library/OrderRefund.php
index bbaf253..77d9e8f 100644
--- a/application/common/library/OrderRefund.php
+++ b/application/common/library/OrderRefund.php
@@ -114,9 +114,11 @@ class OrderRefund
Db::name('user')->where('id', $userId)->setInc('money', $amount);
$balance = round((float)Db::name('user')->where('id', $userId)->value('money'), 2);
- self::addFlow('income', '订单退款(余额)', $amount, $balance, (int)$locked['id'], '余额退款');
+ self::addFlow('income', '订单退款(余额)', $amount, $balance, (int)$locked['id'], '余额退款', $userId);
+ // 钱包流水(fa_user.money 增)→ 余额中心「余额明细」
+ self::addWalletFlow($userId, 'income', '订单退款(余额)', $amount, $balance, (int)$locked['id'], '余额退款');
} else {
- self::addFlow('expense', '订单退款', $amount, 0, (int)$locked['id'], '微信退款 ' . $refundNo);
+ self::addFlow('expense', '订单退款', $amount, 0, (int)$locked['id'], '微信退款 ' . $refundNo, $userId);
}
// 3) 权益回滚
@@ -278,6 +280,11 @@ class OrderRefund
->setDec('money', $amount);
if (!$affected) {
Log::record('退款回滚余额失败(余额不足):订单 ' . $order['order_no'] . ',用户 ' . $userId, 'error');
+ } else {
+ $balance = round((float)Db::name('user')->where('id', $userId)->value('money'), 2);
+ self::addFlow('expense', '充值退款', $amount, $balance, (int)$order['id'], '充值订单退款扣回', $userId);
+ // 钱包流水(fa_user.money 减)→ 余额中心「余额明细」
+ self::addWalletFlow($userId, 'expense', '充值退款', $amount, $balance, (int)$order['id'], '充值订单退款扣回');
}
return;
}
@@ -369,9 +376,10 @@ class OrderRefund
}
/** 资金流水 */
- protected static function addFlow($type, $scene, $amount, $balance, $relatedId, $remark)
+ protected static function addFlow($type, $scene, $amount, $balance, $relatedId, $remark, $userId = 0)
{
Db::name('fc_finance_flow')->insert([
+ 'user_id' => (int)$userId,
'flow_no' => 'F' . date('YmdHis') . bin2hex(random_bytes(3)),
'type' => $type,
'scene' => $scene,
@@ -382,4 +390,20 @@ class OrderRefund
'createtime' => time(),
]);
}
+
+ /** 用户钱包流水(fa_user.money 增减)→ 余额中心「余额明细」 */
+ protected static function addWalletFlow($userId, $type, $scene, $amount, $balance, $relatedId, $remark)
+ {
+ Db::name('fc_wallet_flow')->insert([
+ 'user_id' => (int)$userId,
+ 'flow_no' => 'W' . date('YmdHis') . bin2hex(random_bytes(4)),
+ 'type' => $type,
+ 'scene' => $scene,
+ 'amount' => $amount,
+ 'balance' => $balance,
+ 'related_id' => (int)$relatedId,
+ 'remark' => (string)$remark,
+ 'createtime' => time(),
+ ]);
+ }
}
diff --git a/application/common/library/WxTransfer.php b/application/common/library/WxTransfer.php
index 6f9b5e1..fbf4c41 100644
--- a/application/common/library/WxTransfer.php
+++ b/application/common/library/WxTransfer.php
@@ -382,6 +382,7 @@ class WxTransfer
}
Db::name('fc_finance_flow')->insert([
+ 'user_id' => (int)$locked['user_id'],
'flow_no' => 'FWD' . date('YmdHis') . $locked['id'] . bin2hex(random_bytes(2)),
'type' => 'expense',
'scene' => '佣金提现',
diff --git a/application/extra/site.php b/application/extra/site.php
index f0dba68..f5235f6 100755
--- a/application/extra/site.php
+++ b/application/extra/site.php
@@ -20,13 +20,11 @@ return array (
'article' => 'Article',
'test' => 'Test',
),
- 'configgroup' =>
+ 'configgroup' =>
array (
'basic' => 'Basic',
'email' => 'Email',
'dictionary' => 'Dictionary',
- 'user' => 'User',
- 'example' => 'Example',
),
'mail_type' => '1',
'mail_smtp_host' => 'smtp.qq.com',
diff --git a/application/route.php b/application/route.php
index a33589c..30816f5 100755
--- a/application/route.php
+++ b/application/route.php
@@ -91,6 +91,8 @@ return [
'api/v1/member/open' => 'api/Member/open',
'api/v1/member/orders' => 'api/Member/orders',
'api/v1/member/assets' => 'api/Member/assets',
+ 'api/v1/member/flows' => 'api/Member/flows',
+ 'api/v1/member/pay-password/set' => 'api/Member/setPayPassword',
/* =============== 五、用户端 · 分销中心(Module-DIST) =============== */
'api/v1/distribution/overview' => 'api/Distribution/overview',
diff --git a/uniapp/common/api/member.js b/uniapp/common/api/member.js
index 321d9a2..84f4324 100644
--- a/uniapp/common/api/member.js
+++ b/uniapp/common/api/member.js
@@ -13,8 +13,12 @@ export const memberApi = {
open: (payload) => post('/member/open', payload, { silent: true }),
/** 订单管理(发布/会员/充值) */
orders: (params = {}) => get('/member/orders', params, { silent: true }),
- /** 我的资产(余额/优惠券/积分) */
+ /** 我的资产(余额/优惠券/积分/支付密码状态) */
assets: () => get('/member/assets', {}, { silent: true }),
+ /** 余额明细(余额中心交易记录) */
+ flows: (params = {}) => get('/member/flows', params, { silent: true }),
+ /** 设置/修改支付密码(6 位数字;已设置时需传 oldPassword) */
+ setPayPassword: (payload) => post('/member/pay-password/set', payload, { silent: true }),
}
export default memberApi
diff --git a/uniapp/common/mock/data.js b/uniapp/common/mock/data.js
index ec036fa..e8ad3b6 100644
--- a/uniapp/common/mock/data.js
+++ b/uniapp/common/mock/data.js
@@ -210,6 +210,16 @@ export const datasets = {
points: 320,
},
+ /** 余额明细(fc_wallet_flow 用户钱包流水,字段与后端 /member/flows 一致;type: income=入账 expense=支出) */
+ financeFlows: [
+ { flowNo: 'W202610240001', type: 'income', scene: '后台充值', amount: 2000, balance: 2128.5, createdAt: iso(0) },
+ { flowNo: 'W202610230002', type: 'expense', scene: '付费查看', amount: 2, balance: 128.5, createdAt: iso(-1) },
+ { flowNo: 'W202610220003', type: 'income', scene: '余额充值', amount: 100, balance: 130.5, createdAt: iso(-2) },
+ { flowNo: 'W202610210004', type: 'expense', scene: '发布付费', amount: 5, balance: 30.5, createdAt: iso(-3) },
+ { flowNo: 'W202610200005', type: 'income', scene: '订单退款(余额)', amount: 5, balance: 35.5, createdAt: iso(-4) },
+ { flowNo: 'W202610190006', type: 'expense', scene: '会员开通', amount: 168, balance: 30.5, createdAt: iso(-5) },
+ ],
+
/** 可领取的优惠券定义(fc_coupon) */
coupons: [
{ id: 1, name: '发布立减 3 元', type: 'fixed', amount: 3, minAmount: 0, total: 1000, receivedCount: 12, enabled: true, sort: 1, days: 30 },
diff --git a/uniapp/common/mock/index.js b/uniapp/common/mock/index.js
index e0de799..5b59943 100644
--- a/uniapp/common/mock/index.js
+++ b/uniapp/common/mock/index.js
@@ -508,6 +508,21 @@ export const mockRoutes = {
const coupons = buildUserCoupons()
return ok({ ...datasets.assets, coupons, couponCount: coupons.length })
},
+ /** 余额明细(支持 type 筛选:recharge=充值 / expense=消费 / refund=退款,与后端 Member::flows 口径一致) */
+ 'GET /member/flows': ({ data }) => {
+ let rows = datasets.financeFlows || []
+ const type = data && data.type
+ if (type === 'recharge') rows = rows.filter((f) => f.scene === '余额充值')
+ else if (type === 'expense') rows = rows.filter((f) => f.type === 'expense')
+ else if (type === 'refund') rows = rows.filter((f) => (f.scene || '').includes('退款'))
+ return ok(paginate(rows, data && data.page, data && data.size))
+ },
+ /** 设置/修改支付密码(6 位数字) */
+ 'POST /member/pay-password/set': ({ data }) => {
+ const password = data && data.password
+ if (!/^\d{6}$/.test(String(password || ''))) fail('支付密码必须是 6 位数字')
+ return ok({ hasPayPassword: true })
+ },
// ==================== 用户端 · 收藏与关注 ====================
'GET /favorite/list': ({ data }) => {
diff --git a/uniapp/common/store/ui.js b/uniapp/common/store/ui.js
index 0d87e8c..6e697a8 100644
--- a/uniapp/common/store/ui.js
+++ b/uniapp/common/store/ui.js
@@ -25,6 +25,8 @@ export const uiState = reactive({
aboutUs: '',
shareTitle: '',
subscribe: {},
+ // 余额支付密码开关(后台「支付配置」下发;开启后余额支付需验证 6 位支付密码)
+ payPasswordEnabled: false,
loaded: false,
})
@@ -41,6 +43,7 @@ export function applyUiConfig(cfg = {}) {
uiState.aboutUs = cfg.aboutUs || ''
uiState.shareTitle = cfg.shareTitle || cfg.siteName || cfg.appName || ''
uiState.subscribe = cfg.subscribe || {}
+ uiState.payPasswordEnabled = !!(cfg && cfg.payPasswordEnabled)
setRemoteUi(cfg)
}
diff --git a/uniapp/components/fc-pay-password/fc-pay-password.vue b/uniapp/components/fc-pay-password/fc-pay-password.vue
new file mode 100644
index 0000000..4a3d934
--- /dev/null
+++ b/uniapp/components/fc-pay-password/fc-pay-password.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+ {{ title }}
+
+ ×
+
+
+ {{ tip }}
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+ {{ confirmText }}
+
+
+
+
+
+
+
+
+
diff --git a/uniapp/pages.json b/uniapp/pages.json
index a4df566..c22ae74 100644
--- a/uniapp/pages.json
+++ b/uniapp/pages.json
@@ -131,6 +131,14 @@
"backgroundColor": "#f2fef7"
}
},
+ {
+ "path": "pages/member/flows",
+ "style": {
+ "navigationBarTitleText": "余额明细",
+ "navigationStyle": "custom",
+ "backgroundColor": "#f2fef7"
+ }
+ },
{
"path": "pages/member/coupons",
"style": {
diff --git a/uniapp/pages/detail/detail.vue b/uniapp/pages/detail/detail.vue
index a729bc9..d1cef90 100644
--- a/uniapp/pages/detail/detail.vue
+++ b/uniapp/pages/detail/detail.vue
@@ -388,6 +388,10 @@
+
+
+
@@ -491,6 +495,7 @@
import postApi from '@/common/api/post.js'
import favoriteApi from '@/common/api/favorite.js'
import reportApi from '@/common/api/report.js'
+import memberApi from '@/common/api/member.js'
import { REPORT_REASONS } from '@/common/constants/report.js'
import { iconUrl, categoryIconUrl, loadUiConfig, uiState } from '@/common/store/ui.js'
import { fromNow, assetUrl } from '@/common/utils/format.js'
@@ -498,10 +503,11 @@ import { pay } from '@/common/utils/pay.js'
import { useUserStore } from '@/common/store/index.js'
import config from '@/common/config/index.js'
import FcNavbar from '@/components/fc-navbar/fc-navbar.vue'
+import FcPayPassword from '@/components/fc-pay-password/fc-pay-password.vue'
import QRCode from '@/common/utils/qrcode-generator.js'
export default {
- components: { FcNavbar },
+ components: { FcNavbar, FcPayPassword },
data() {
return {
id: 0,
@@ -524,6 +530,7 @@ export default {
replySubmitting: false,
showPoster: false,
showPayModal: false,
+ pwdPopup: { show: false, title: '', tip: '' },
score: 5,
commentText: '',
submitting: false,
@@ -917,9 +924,33 @@ export default {
*/
async confirmPay() {
if (this.submitting) return
+ // 后台开启支付密码:余额支付需先验证(未设置则引导去余额中心设置)
+ if (this.payType === 'balance' && uiState.payPasswordEnabled) {
+ const assets = await this.fetchAssetsSafe()
+ if (!assets.hasPayPassword) {
+ uni.showModal({
+ title: '需要支付密码',
+ content: '开启支付密码验证后才能使用余额支付,请先到「余额中心」设置。',
+ confirmText: '去设置',
+ success: (r) => {
+ if (r.confirm) uni.navigateTo({ url: '/pages/member/recharge' })
+ },
+ })
+ return
+ }
+ this.pwdPopup = { show: true, title: '请输入支付密码', tip: `解锁该信息 · ¥${Number(this.payViewPrice).toFixed(2)}` }
+ return
+ }
+ await this.doPay()
+ },
+ /**
+ * 实际支付(confirmPay 校验通过 / 支付密码验证后调用)
+ * @param {string} [payPassword] 余额支付密码(开关开启时必传)
+ */
+ async doPay(payPassword) {
this.submitting = true
try {
- const res = await postApi.payView(this.id, { payType: this.payType })
+ const res = await postApi.payView(this.id, { payType: this.payType, payPassword })
if (res.order) {
await pay({ ...res.order, payType: this.payType })
await this.waitUnlock()
@@ -935,6 +966,24 @@ export default {
this.submitting = false
}
},
+ /**
+ * 支付密码弹窗确认(余额支付验证)
+ * @param {string} password
+ */
+ async onPwdConfirm(password) {
+ this.pwdPopup.show = false
+ await this.doPay(password)
+ },
+ /**
+ * 拉取资产(仅取支付密码状态,失败不阻断流程)
+ */
+ async fetchAssetsSafe() {
+ try {
+ return await memberApi.assets()
+ } catch (e) {
+ return {}
+ }
+ },
/**
* 微信支付结果轮询
*/
diff --git a/uniapp/pages/member/flows.vue b/uniapp/pages/member/flows.vue
new file mode 100644
index 0000000..d584ed2
--- /dev/null
+++ b/uniapp/pages/member/flows.vue
@@ -0,0 +1,225 @@
+
+
+
+
+
+
+
+ {{ t.name }}
+
+
+
+
+
+
+
+
+
+
+ {{ f.scene }}
+ {{ formatTime(f.createdAt) }}
+
+
+
+ {{ f.type === 'income' ? '+' : '-' }}{{ Number(f.amount).toFixed(2) }}
+
+ 余额 {{ Number(f.balance).toFixed(2) }}
+
+
+ 加载中…
+ 上拉加载更多
+ 没有更多了
+
+
+
+
+
+
+
+
diff --git a/uniapp/pages/member/index.vue b/uniapp/pages/member/index.vue
index 880a3d2..a19ba2f 100644
--- a/uniapp/pages/member/index.vue
+++ b/uniapp/pages/member/index.vue
@@ -289,20 +289,25 @@
+
+
+
@@ -130,6 +203,7 @@ export default {
padding-bottom: 60rpx;
}
.recharge__hero {
+ position: relative;
background: $fc-gradient-hero;
padding: 48rpx 40rpx 60rpx;
border-radius: 0 0 40rpx 40rpx;
@@ -137,6 +211,28 @@ export default {
flex-direction: column;
align-items: center;
}
+/* 右上角:余额明细入口(半透明白胶囊,微信零钱同款位置) */
+.recharge__hero-entry {
+ position: absolute;
+ top: 24rpx;
+ right: 28rpx;
+ display: flex;
+ align-items: center;
+ padding: 10rpx 22rpx;
+ border-radius: $fc-radius-pill;
+ background: rgba(255, 255, 255, 0.85);
+ font-size: $fc-font-xs;
+ color: $fc-text-secondary;
+ &:active {
+ opacity: 0.7;
+ }
+}
+.recharge__hero-entry-arrow {
+ margin-left: 4rpx;
+ font-size: 28rpx;
+ line-height: 1;
+ color: $fc-text-placeholder;
+}
.recharge__hero-label {
color: $fc-text-secondary;
font-weight: 500;
@@ -217,34 +313,6 @@ export default {
font-size: $fc-font-lg;
color: $fc-text;
}
-.recharge__pay {
- display: flex;
- margin-top: 24rpx;
-}
-.recharge__pay-type {
- flex: 1;
- height: 80rpx;
- border-radius: $fc-radius;
- border: 2rpx solid $fc-border;
- background: $fc-bg;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: $fc-font-sm;
- color: $fc-text-regular;
- &:first-child {
- margin-right: 16rpx;
- }
- &--active {
- border-color: $fc-primary-text;
- background: $fc-primary-lighter;
- color: $fc-primary-text;
- font-weight: 600;
- }
-}
-.recharge__pay-text {
- margin-left: 10rpx;
-}
.recharge__btn {
margin-top: 28rpx;
height: 90rpx;
@@ -265,4 +333,37 @@ export default {
color: $fc-text-placeholder;
line-height: $fc-leading;
}
+
+/* 支付密码行 */
+.recharge__row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+.recharge__row-left {
+ display: flex;
+ align-items: center;
+}
+.recharge__row-title {
+ margin-left: 10rpx;
+ font-size: $fc-font-base;
+ color: $fc-text;
+ font-weight: 500;
+}
+.recharge__row-right {
+ display: flex;
+ align-items: center;
+}
+.recharge__row-value {
+ font-size: $fc-font-sm;
+ color: $fc-text-secondary;
+}
+.recharge__row-arrow {
+ margin-left: 8rpx;
+ font-size: 36rpx;
+ color: $fc-text-placeholder;
+ line-height: 1;
+}
+
+/* 行标题(支付密码行) */
diff --git a/uniapp/pages/mine/index.vue b/uniapp/pages/mine/index.vue
index 27ec670..42348b0 100644
--- a/uniapp/pages/mine/index.vue
+++ b/uniapp/pages/mine/index.vue
@@ -176,7 +176,7 @@ export default {
{ iconKey: 'bar-fav-on', text: '我的收藏', url: '/pages/favorite/index' },
{ iconKey: 'mine-follow', text: '我的关注', url: '/pages/favorite/follows' },
{ iconKey: 'mine-order', text: '我的订单', url: '/pages/member/orders' },
- { iconKey: 'link-recharge', text: '余额充值', url: '/pages/member/recharge' },
+ { iconKey: 'link-recharge', text: '我的余额', url: '/pages/member/recharge' },
{ iconKey: 'mine-dist', text: '分销中心', url: '/pages/distribution/index' },
{ iconKey: 'shield', text: bizText, action: 'merchant' },
]
@@ -527,7 +527,7 @@ export default {
*/
go(url) {
if (!url) return
- const needAuth = ['/pages/post/mine', '/pages/favorite/index', '/pages/favorite/follows', '/pages/member/orders', '/pages/member/recharge', '/pages/distribution/index', '/pages/post/create', '/pages/post/drafts']
+ const needAuth = ['/pages/post/mine', '/pages/favorite/index', '/pages/favorite/follows', '/pages/member/orders', '/pages/member/recharge', '/pages/member/flows', '/pages/distribution/index', '/pages/post/create', '/pages/post/drafts']
if (needAuth.includes(url) && !this.isLogin) {
this.goLogin()
return
diff --git a/uniapp/pages/post/create.vue b/uniapp/pages/post/create.vue
index 8fc541d..56f7549 100644
--- a/uniapp/pages/post/create.vue
+++ b/uniapp/pages/post/create.vue
@@ -376,6 +376,10 @@
+
+
+
@@ -391,16 +395,18 @@
*/
import FcNavbar from '@/components/fc-navbar/fc-navbar.vue'
import FcAreaPicker from '@/components/fc-area-picker/fc-area-picker.vue'
+import FcPayPassword from '@/components/fc-pay-password/fc-pay-password.vue'
import homeApi from '@/common/api/home.js'
import postApi from '@/common/api/post.js'
+import memberApi from '@/common/api/member.js'
import { useUserStore } from '@/common/store/index.js'
-import { iconUrl, categoryIconUrl, loadUiConfig, requestSubscribe } from '@/common/store/ui.js'
+import { iconUrl, categoryIconUrl, loadUiConfig, requestSubscribe, uiState } from '@/common/store/ui.js'
import { pay } from '@/common/utils/pay.js'
import { assetUrl } from '@/common/utils/format.js'
import config from '@/common/config/index.js'
export default {
- components: { FcNavbar, FcAreaPicker },
+ components: { FcNavbar, FcAreaPicker, FcPayPassword },
data() {
return {
categories: [],
@@ -410,6 +416,7 @@ export default {
areaRows: [],
submitting: false,
uploading: false,
+ pwdPopup: { show: false, title: '', tip: '' },
draftTimer: null,
catDrawerVisible: false,
catDrawerAnim: false,
@@ -589,6 +596,13 @@ export default {
needPay() {
return this.publish.mode === 'paid' && this.form.plan !== 'quota'
},
+ /**
+ * 当前选中付费套餐的价格文本(仅用于支付密码弹窗提示)
+ */
+ selectedPlanPrice() {
+ const plan = this.plans.find((p) => p.key === this.form.plan)
+ return plan ? String(plan.price).replace(/[¥元]/g, '') : ''
+ },
/**
* 当前分类对应的常用计价单位
*/
@@ -1116,6 +1130,24 @@ export default {
if (this.form.phone && !/^1\d{10}$/.test(this.form.phone)) return '请输入正确的11位手机号'
return ''
},
+ /**
+ * 拉取资产(仅取支付密码状态,失败不阻断流程)
+ */
+ async fetchAssetsSafe() {
+ try {
+ return await memberApi.assets()
+ } catch (e) {
+ return {}
+ }
+ },
+ /**
+ * 支付密码弹窗确认(余额支付验证)
+ * @param {string} password
+ */
+ async onPwdConfirm(password) {
+ this.pwdPopup.show = false
+ await this.doSubmit(password)
+ },
/**
* 提交发布信息
*/
@@ -1131,6 +1163,32 @@ export default {
return
}
+ // 后台开启支付密码:余额支付发布需先验证(未设置则引导去余额中心设置)
+ if (this.needPay && this.form.payType === 'balance' && uiState.payPasswordEnabled) {
+ const assets = await this.fetchAssetsSafe()
+ if (!assets.hasPayPassword) {
+ uni.showModal({
+ title: '需要支付密码',
+ content: '开启支付密码验证后才能使用余额支付,请先到「余额中心」设置。',
+ confirmText: '去设置',
+ success: (r) => {
+ if (r.confirm) uni.navigateTo({ url: '/pages/member/recharge' })
+ },
+ })
+ return
+ }
+ const priceText = this.selectedPlanPrice ? ` · ¥${this.selectedPlanPrice}` : ''
+ this.pwdPopup = { show: true, title: '请输入支付密码', tip: `发布付费信息${priceText}` }
+ return
+ }
+
+ await this.doSubmit()
+ },
+ /**
+ * 实际提交(submit 校验通过 / 支付密码验证后调用)
+ * @param {string} [payPassword] 余额支付密码(开关开启时必传)
+ */
+ async doSubmit(payPassword) {
this.submitting = true
try {
// 订阅消息授权(审核结果);未配置模板时静默跳过
@@ -1141,6 +1199,7 @@ export default {
priceUnit: this.form.isNegotiable ? '面议' : (this.form.priceUnit || '元'),
tags: this.form.tags,
plan: this.form.plan === 'quota' ? 'free' : this.form.plan,
+ payPassword,
}
const res = await postApi.create(payload)