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()} - -
-
- {:__('说明')}: - {:__('本页是「发布模式 + 发布价格/条数」的唯一入口;保存后会同步「会员套餐」的月/年卡价格与含发布条数,因此用户端发布页、会员中心与后端计费口径一致。')} -
- {:__('「会员单条发布价格」= 未购买套餐时按条计费的价格;「按月/按年」= 会员套餐价格,并赠送对应条数的发布额度(额度随会员有效期,过期自动清零)。')} -
- {:__('计费顺序:套餐额度 → 免费发布条数 → 单条付费;免费发布条数达到后即按「会员单条发布价格」计费,金额一律由服务端计算。')} -
- -
-
- {:token()} - - - - - - - - - - - - - - - - - - - - - - - -
{:__('发布模式')} - - -
{:__('免费发布条数')} -
- - {:__('条')} -
-
- -
- {:__('免费发布条数:免费发布模式下前 N 条免费,达到条数后按「会员单条发布价格」计费(计费标准与付费发布一致);付费发布模式下同样保留这 N 条免费额度(避免一开付费就是硬付费墙)。填 0 = 不限制(免费到底)。')} -
{:__('会员单条发布价格')} -
- ¥ - - {:__('元/条')} -
- {:__('未开通套餐的用户按此价格付费发布(金额由服务端计算,前端不可传价)')} -
{:__('会员按月发布')} -
- ¥ - - {:__('元/月')} -
-
- - {:__('条/月')} -
- {:__('价格与条数会同步到「会员套餐 → 月卡(M1)」;购买后立即赠送该条数的发布额度')} -
{:__('会员按年发布')} -
- ¥ - - {:__('元/年')} -
-
- - {:__('条/年')} -
- {:__('价格与条数会同步到「会员套餐 → 年卡(M12)」;购买后立即赠送该条数的发布额度')} -
- -
-
- -
-
-
-
- -
-
- {:__('当前会员套餐')} - {:__('(月/年卡的价格与含发布条数由本页同步,其余字段在「交易管理 → 会员套餐」维护)')} -
- - - - - - - - - - - - - {foreach $plans as $p} - - - - - - - - - {/foreach} - -
{:__('套餐编码')}{:__('套餐名称')}{:__('售价')}{:__('有效天数')}{:__('含发布条数')}{:__('是否启用')}
{$p.plan_code}{$p.name}¥{$p.price}{$p.days}{$p.post_quota}{if $p.enabled}{:__('是')}{else/}{:__('否')}{/if}
-
-
-
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 @@ + + + + + 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 @@ + + + + + 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)