feat: 完善会员资金明细与退款转账逻辑,新增支付密码组件与资金流水界面

这个提交包含在:
2026-10-24 17:55:36 +08:00
父节点 6b86177b5f
当前提交 b6c53abb84
共有 30 个文件被更改,包括 1222 次插入 和 537 次删除

查看文件

@@ -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 解锁(回调可独立完成)