feat: 新增会员浏览/拨打额度并优化无图展示逻辑
新增会员每日浏览完整信息和拨打电话的额度配置,调整付费查看接口描述为解锁完整信息,优化帖子无图时的兜底展示逻辑,修复图片加载路径问题,删除废弃工具脚本。
这个提交包含在:
@@ -922,23 +922,7 @@ class Post extends V1
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情页联系方式可见性(避免「已开通会员仍显示已受保护」)
|
||||
*
|
||||
* 可见规则(与 payView 计费口径一致):
|
||||
* ① 会员有效期内 → 免费看
|
||||
* ② 已付费解锁 fc_pay_view → 看(余额 / 微信回调写入)
|
||||
* ③ 本人发布的信息 → 看
|
||||
* 其它(游客 / 非会员未付费)→ 仍返回脱敏串
|
||||
*
|
||||
* 出参新增:unlocked(bool)、wechat(明文,未解锁为空串)。
|
||||
* 注意:只加在详情接口,列表(首页/收藏等)仍保持脱敏,避免手机号被批量抓取。
|
||||
*
|
||||
* @param array $data buildPost() 结果
|
||||
* @param array $post fc_post 行
|
||||
* @return array
|
||||
*/
|
||||
/**
|
||||
* 判断当前全站是否处于「联系方式完全免费公开」模式
|
||||
* 判断当前全站是否处于「完整信息免费公开」模式
|
||||
* 规则:fee.viewMode === 'free' 或 fee.payView <= 0 时均为免费公开
|
||||
*
|
||||
* @return bool
|
||||
@@ -951,13 +935,17 @@ class Post extends V1
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情页联系方式与内容访问控制(支持完全免费公开模式与商业付费/会员解锁模式自由切换)
|
||||
* 详情页完整信息访问控制(免费公开 / 付费解锁 / 会员每日额度 三态自由切换)
|
||||
*
|
||||
* 可见规则:
|
||||
* ① 全站处于「完全免费公开」模式(fee.viewMode=free 或 fee.payView<=0)→ unlocked=true,直接明文返回
|
||||
* ② 本人发布的信息或已付费解锁记录(fc_pay_view)→ unlocked=true,返回真实联系方式明文
|
||||
* ③ 会员享有每日免费查看额度(freeView=0 代表会员不限次数免费看),未点击解锁时不泄露真实号码
|
||||
* ④ 未解锁状态下,对标题与详细正文中的手机号执行掩码遮蔽,封堵正文直接贴电话的白嫖漏洞
|
||||
* 联系方式规则:任何模式下界面都不展示手机号(明文与脱敏串均不返回),仅保留拨打按钮,
|
||||
* 由 POST /post/:id/dial 校验拨打额度后调起系统拨号。
|
||||
*
|
||||
* 完整信息(正文/农产品规格)规则:
|
||||
* ① 免费公开模式(fee.viewMode=free 或 fee.payView<=0)→ 直接放行
|
||||
* ② 本人发布 或 已付费解锁(fc_pay_view,永久)→ 放行
|
||||
* ③ 会员:每日免费浏览额度(fc_plan.view_quota,0=不限),同一条当日只计 1 次
|
||||
* ④ 额度用尽 / 非会员未解锁 → content 仅返回摘要,contentLocked=true
|
||||
* 防白嫖:标题与正文中出现的手机号始终动态遮蔽。
|
||||
*
|
||||
* @param array $data buildPost() 结果
|
||||
* @param array $post fc_post 行
|
||||
@@ -965,70 +953,57 @@ class Post extends V1
|
||||
*/
|
||||
protected function contactAccess($data, $post)
|
||||
{
|
||||
$data['unlocked'] = false;
|
||||
$data['wechat'] = '';
|
||||
|
||||
$me = $this->user();
|
||||
$ext = $me ? $this->ext($me['id']) : null;
|
||||
$now = time();
|
||||
$userId = $me ? (int)$me['id'] : 0;
|
||||
|
||||
$isMember = false;
|
||||
$quota = $this->memberFreeViewQuota($ext);
|
||||
$freeViewUnlimited = ($quota === 0);
|
||||
$used = 0;
|
||||
$freeViewLeft = 0;
|
||||
$payViewPrice = max(0, round((float)$this->cfg('fee.payView', 2), 2));
|
||||
$isFreeView = $this->isFreeViewMode();
|
||||
|
||||
$isOwner = false;
|
||||
$paid = false;
|
||||
|
||||
if ($me && $ext) {
|
||||
$isMember = (int)$ext['is_member'] === 1
|
||||
&& !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now;
|
||||
$isOwner = (int)$post['user_id'] === (int)$me['id'];
|
||||
$paid = (bool)Db::name('fc_pay_view')
|
||||
->where('user_id', $me['id'])
|
||||
->where('post_id', $post['id'])
|
||||
->find();
|
||||
}
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$used = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
|
||||
if ($isMember) {
|
||||
$freeViewLeft = $freeViewUnlimited ? 999 : max(0, $quota - $used);
|
||||
} else {
|
||||
$freeViewLeft = 0;
|
||||
$isFreeView = $this->isFreeViewMode();
|
||||
$payViewPrice = max(0, round((float)$this->cfg('fee.payView', 2), 2));
|
||||
$isOwner = $userId > 0 && (int)$post['user_id'] === $userId;
|
||||
|
||||
// ---- 已付费解锁记录(永久解锁,任何模式不占每日额度) ----
|
||||
$paid = false;
|
||||
if ($userId > 0) {
|
||||
$paid = (bool)Db::name('fc_pay_view')
|
||||
->where('user_id', $userId)
|
||||
->where('post_id', (int)$post['id'])
|
||||
->find();
|
||||
}
|
||||
|
||||
// ---- 完整信息解锁判定 ----
|
||||
$unlocked = false;
|
||||
$viewState = null; // 会员付费模式下的浏览额度状态(含本次消耗后的最新值)
|
||||
if ($isFreeView || $isOwner || $paid) {
|
||||
$unlocked = true;
|
||||
} elseif ($isMember) {
|
||||
// 会员每日浏览额度(view_quota=0 代表不限),同一条当日只计 1 次
|
||||
$viewState = $this->viewQuotaState($userId, $ext);
|
||||
if ($this->hasViewedToday($userId, (int)$post['id'])) {
|
||||
$unlocked = true; // 今日已浏览过该条,不重复扣额度
|
||||
} elseif ($viewState['unlimited'] || $viewState['left'] > 0) {
|
||||
$this->recordViewLog($userId, (int)$post['id']);
|
||||
$viewState['used'] += 1;
|
||||
$viewState['left'] = $viewState['unlimited'] ? 999 : max(0, $viewState['total'] - $viewState['used']);
|
||||
$unlocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
$data['viewMode'] = $isFreeView ? 'free' : 'paid';
|
||||
$data['isMember'] = $isMember;
|
||||
$data['freeViewQuota'] = $quota;
|
||||
$data['freeViewUnlimited'] = $freeViewUnlimited;
|
||||
$data['freeViewUsed'] = $used;
|
||||
$data['freeViewLeft'] = $freeViewLeft;
|
||||
$data['payViewPrice'] = $isFreeView ? 0 : $payViewPrice;
|
||||
$contentLocked = !$unlocked;
|
||||
|
||||
// 每日电话拨打次数上限与剩余额度(详情预览页隐藏明文手机号,仅保留免责声明弹窗拨打)
|
||||
$callState = $this->callQuotaState($me ? (int)$me['id'] : 0, $isMember);
|
||||
$rawPhone = (string)$post['phone'];
|
||||
$contactName = (string)(isset($post['contact']) ? $post['contact'] : '');
|
||||
$maskedPhone = $this->maskPhone($rawPhone);
|
||||
// ---- 正文与农产品规格锁定 ----
|
||||
if ($contentLocked) {
|
||||
$data['content'] = $this->contentPreview((string)$data['content'], 100);
|
||||
unset($data['plantArea'], $data['harvestTime'], $data['outputRange']);
|
||||
}
|
||||
|
||||
$data['hasPhone'] = $rawPhone !== '';
|
||||
$data['dialDisplay'] = $this->buildDialDisplay($contactName, $rawPhone);
|
||||
$data['callQuotaTotal'] = $callState['total'];
|
||||
$data['callQuotaUsed'] = $callState['used'];
|
||||
$data['callQuotaLeft'] = $callState['left'];
|
||||
$data['canDial'] = $isOwner || $paid || $callState['unlimited'] || $callState['left'] > 0;
|
||||
|
||||
// 界面上始终隐藏明文手机号(仅返回脱敏号码),通过 POST /post/:id/dial 校验额度并调起系统拨号
|
||||
$data['unlocked'] = ($isFreeView || $isOwner || $paid);
|
||||
$data['phone'] = $maskedPhone;
|
||||
$data['contact'] = $maskedPhone;
|
||||
$data['contactName'] = $contactName;
|
||||
$data['wechat'] = '';
|
||||
|
||||
// 标题与详细描述中的任何手机号执行动态安全遮蔽,防止正文直接贴电话
|
||||
// 标题与正文中出现的手机号始终遮蔽,防止正文贴电话绕过限制
|
||||
if (!empty($data['content'])) {
|
||||
$data['content'] = $this->maskTextPhones($data['content']);
|
||||
}
|
||||
@@ -1036,39 +1011,138 @@ class Post extends V1
|
||||
$data['title'] = $this->maskTextPhones($data['title']);
|
||||
}
|
||||
|
||||
// ---- 拨打额度状态 ----
|
||||
$callState = $this->callQuotaState($userId, $isMember);
|
||||
|
||||
$data['unlocked'] = $unlocked;
|
||||
$data['contentLocked'] = $contentLocked;
|
||||
$data['viewMode'] = $isFreeView ? 'free' : 'paid';
|
||||
$data['payViewPrice'] = $isFreeView ? 0 : $payViewPrice;
|
||||
$data['isMember'] = $isMember;
|
||||
|
||||
// 会员每日浏览额度(免费公开模式不展示、不计数)
|
||||
$data['viewQuotaTotal'] = $viewState ? $viewState['total'] : 0;
|
||||
$data['viewQuotaUsed'] = $viewState ? $viewState['used'] : 0;
|
||||
$data['viewQuotaLeft'] = $viewState ? $viewState['left'] : 0;
|
||||
$data['viewQuotaUnlimited'] = $viewState ? $viewState['unlimited'] : false;
|
||||
|
||||
$data['hasPhone'] = (string)$post['phone'] !== '';
|
||||
// 任何模式均不返回手机号(明文与脱敏串都不给),界面只保留拨打按钮,
|
||||
// 由 POST /post/:id/dial 校验拨打额度后调起系统拨号
|
||||
$data['phone'] = '';
|
||||
$data['contact'] = (string)(isset($post['contact']) ? $post['contact'] : '');
|
||||
$data['contactName'] = (string)(isset($post['contact']) ? $post['contact'] : '');
|
||||
$data['callQuotaTotal'] = $callState['total'];
|
||||
$data['callQuotaUsed'] = $callState['used'];
|
||||
$data['callQuotaLeft'] = $callState['left'];
|
||||
$data['canDial'] = $isOwner || $paid || $callState['unlimited'] || $callState['left'] > 0;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造免责声明拨打弹窗展示文案(如:夏178****8426)
|
||||
* 计算会员今日的完整信息浏览额度状态(真源:fc_plan.view_quota;0 代表不限)
|
||||
* 是否今日已浏览过某条 → hasViewedToday 单独判断
|
||||
*
|
||||
* @param string $contactName 联系人姓名
|
||||
* @param string $phone 手机号
|
||||
* @param int $userId 用户ID
|
||||
* @param array $ext fc_user_ext 行
|
||||
* @return array{total:int,used:int,left:int,unlimited:bool}
|
||||
*/
|
||||
protected function viewQuotaState($userId, $ext = null)
|
||||
{
|
||||
$benefit = \app\common\library\FcConfig::resolveMemberBenefit($ext);
|
||||
$total = isset($benefit['viewQuota']) ? max(0, (int)$benefit['viewQuota']) : 20;
|
||||
$unlimited = ($total === 0);
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$used = (int)Db::name('fc_view_log')
|
||||
->where('user_id', (int)$userId)
|
||||
->where('view_date', $today)
|
||||
->count();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'used' => $used,
|
||||
'left' => $unlimited ? 999 : max(0, $total - $used),
|
||||
'unlimited' => $unlimited,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一条完整信息浏览日志(同一条当日幂等,重复浏览不产生新记录)
|
||||
*
|
||||
* @param int $userId 用户ID
|
||||
* @param int $postId 信息ID
|
||||
* @return bool 是否成功写入新记录
|
||||
*/
|
||||
protected function recordViewLog($userId, $postId)
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
$exist = Db::name('fc_view_log')
|
||||
->where('user_id', (int)$userId)
|
||||
->where('post_id', (int)$postId)
|
||||
->where('view_date', $today)
|
||||
->find();
|
||||
if ($exist) {
|
||||
return false;
|
||||
}
|
||||
Db::name('fc_view_log')->insert([
|
||||
'user_id' => (int)$userId,
|
||||
'post_id' => (int)$postId,
|
||||
'view_date' => $today,
|
||||
'createtime' => time(),
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户今日是否已浏览过指定信息
|
||||
*
|
||||
* @param int $userId 用户ID
|
||||
* @param int $postId 信息ID
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasViewedToday($userId, $postId)
|
||||
{
|
||||
return (bool)Db::name('fc_view_log')
|
||||
->where('user_id', (int)$userId)
|
||||
->where('post_id', (int)$postId)
|
||||
->where('view_date', date('Y-m-d'))
|
||||
->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取正文摘要(锁定态展示,保留首行主信息)
|
||||
*
|
||||
* @param string $content 正文
|
||||
* @param int $length 摘要长度(字符)
|
||||
* @return string
|
||||
*/
|
||||
protected function buildDialDisplay($contactName, $phone)
|
||||
protected function contentPreview($content, $length = 100)
|
||||
{
|
||||
$masked = $this->maskPhone($phone);
|
||||
$name = trim((string)$contactName);
|
||||
if ($name === '') {
|
||||
return $masked;
|
||||
$text = trim((string)$content);
|
||||
if ($text === '' || mb_strlen($text, 'UTF-8') <= $length) {
|
||||
return $text;
|
||||
}
|
||||
$surname = mb_substr($name, 0, 1, 'UTF-8');
|
||||
return $surname . $masked;
|
||||
return mb_substr($text, 0, $length, 'UTF-8') . '…';
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算指定用户今日的电话拨打额度状态
|
||||
*
|
||||
* 会员:每日拨打次数真源 = 所属 fc_plan 套餐 call_quota(0 代表不限)
|
||||
* 非会员:全局配置 call.normalDailyQuota(0 代表不限)
|
||||
* @param int $userId 用户ID(0表示游客)
|
||||
* @param bool $isMember 是否为有效期内VIP会员
|
||||
* @return array{total:int,used:int,left:int,unlimited:bool}
|
||||
*/
|
||||
protected function callQuotaState($userId, $isMember)
|
||||
{
|
||||
$normalQuota = max(0, (int)$this->cfg('call.normalDailyQuota', 5));
|
||||
$vipQuota = max(0, (int)$this->cfg('call.vipDailyQuota', 30));
|
||||
$total = $isMember ? $vipQuota : $normalQuota;
|
||||
if ($isMember) {
|
||||
$benefit = \app\common\library\FcConfig::resolveMemberBenefit();
|
||||
$total = isset($benefit['callQuota']) ? max(0, (int)$benefit['callQuota']) : 30;
|
||||
} else {
|
||||
$total = max(0, (int)$this->cfg('call.normalDailyQuota', 5));
|
||||
}
|
||||
$unlimited = ($total === 0);
|
||||
|
||||
$used = 0;
|
||||
@@ -1091,7 +1165,7 @@ class Post extends V1
|
||||
/**
|
||||
* P6-B 信息详情页申请拨打电话(校验每日拨打次数上限并记录拨打日志)
|
||||
* POST /api/v1/post/:id/dial
|
||||
* → {phoneNumber, dialDisplay, callQuotaTotal, callQuotaUsed, callQuotaLeft}
|
||||
* → {phoneNumber, callQuotaTotal, callQuotaUsed, callQuotaLeft}
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
@@ -1125,8 +1199,8 @@ class Post extends V1
|
||||
// 非本人且未单条付费解锁时,校验每日拨打次数上限
|
||||
if (!$isOwner && !$paid && !$state['unlimited'] && $state['left'] <= 0) {
|
||||
$msg = $isMember
|
||||
? ('您今日的 VIP 拨打次数已达上限(' . $state['total'] . '次/日)')
|
||||
: ('您今日的免费拨打次数已达上限(' . $state['total'] . '次/日),开通 VIP 会员可享每日更多拨打次数');
|
||||
? ('您今日的拨打次数已达上限(' . $state['total'] . '次/日),明日再来或升级更高套餐')
|
||||
: ('您今日的免费拨打次数已达上限(' . $state['total'] . '次/日),开通会员可享更多拨打次数');
|
||||
$this->fail($msg, 403);
|
||||
}
|
||||
|
||||
@@ -1145,7 +1219,6 @@ class Post extends V1
|
||||
|
||||
$this->ok([
|
||||
'phoneNumber' => $phone,
|
||||
'dialDisplay' => $this->buildDialDisplay(isset($post['contact']) ? $post['contact'] : '', $phone),
|
||||
'callQuotaTotal' => $state['total'],
|
||||
'callQuotaUsed' => $newUsed,
|
||||
'callQuotaLeft' => $newLeft,
|
||||
@@ -1153,56 +1226,11 @@ class Post extends V1
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员每日免费查看条数(从用户所属 fc_plan 套餐或默认套餐读取;0 代表会员无限次免费看)
|
||||
*
|
||||
* @param array|null $ext fc_user_ext 行
|
||||
* @return int
|
||||
*/
|
||||
protected function memberFreeViewQuota($ext = null)
|
||||
{
|
||||
$benefit = \app\common\library\FcConfig::resolveMemberBenefit($ext);
|
||||
return isset($benefit['freeView']) ? max(0, (int)$benefit['freeView']) : 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消耗一次会员免费查看额度(跨天自动重置;quota=0 代表会员不限次数无限看;需在事务内调用)
|
||||
*
|
||||
* @param int $userId
|
||||
* @param int $quota
|
||||
* @return bool 是否成功占用
|
||||
*/
|
||||
protected function consumeFreeView($userId, $quota)
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
$ext = Db::name('fc_user_ext')->where('user_id', (int)$userId)->lock(true)->find();
|
||||
if (!$ext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((string)$ext['free_view_date'] !== $today) {
|
||||
Db::name('fc_user_ext')->where('user_id', (int)$userId)->update([
|
||||
'free_view_date' => $today,
|
||||
'free_view_used' => 1,
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($quota > 0 && (int)$ext['free_view_used'] >= $quota) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Db::name('fc_user_ext')->where('user_id', (int)$userId)->setInc('free_view_used', 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* P6 付费查看联系方式
|
||||
* P6 付费解锁完整信息(单条永久解锁)
|
||||
* POST /api/v1/post/:id/pay-view {payType}
|
||||
* → {unlocked:true,contact:{phone,wechat}}(免费公开/余额/会员免费:立即解锁)
|
||||
* → {order:{orderNo,amount,payType,status:'pending'}}(微信支付:前端再调 POST /pay/prepay 调起收银台)
|
||||
* 规则 BR-05;金额取 fee.payView;全站免费模式、会员额度内或已解锁免费
|
||||
* → {unlocked:true}(余额:立即解锁)
|
||||
* → {order:{orderNo,amount,payType,status:'pending'}}(微信支付:前端再调 POST /pay/prepay 调起收银台,回调写解锁记录)
|
||||
* 适用:付费模式下非会员,或会员每日浏览额度用尽后的单条解锁;免费公开模式无需解锁
|
||||
*/
|
||||
public function payView()
|
||||
{
|
||||
@@ -1210,10 +1238,7 @@ class Post extends V1
|
||||
$id = $this->routeId();
|
||||
|
||||
$payType = (string)$this->input('payType', '');
|
||||
if ($payType === '') {
|
||||
$payType = 'free';
|
||||
}
|
||||
if (!in_array($payType, ['wxpay', 'balance', 'free'], true)) {
|
||||
if (!in_array($payType, ['wxpay', 'balance'], true)) {
|
||||
$this->fail('支付方式不支持');
|
||||
}
|
||||
|
||||
@@ -1222,128 +1247,94 @@ class Post extends V1
|
||||
$this->fail('信息不存在', 404);
|
||||
}
|
||||
|
||||
$phone = (string)$post['phone'];
|
||||
$wechat = $post['wechat'] !== '' ? (string)$post['wechat'] : ('tongcheng_' . $id);
|
||||
$contactName = (string)(isset($post['contact']) ? $post['contact'] : '');
|
||||
|
||||
$contact = [
|
||||
'name' => $contactName,
|
||||
'phone' => $phone,
|
||||
'wechat' => $wechat,
|
||||
];
|
||||
|
||||
$amount = max(0, round((float)$this->cfg('fee.payView', 2), 2));
|
||||
$isFreeView = $this->isFreeViewMode();
|
||||
$now = time();
|
||||
if ($isFreeView) {
|
||||
$this->ok(['unlocked' => true, 'freeMode' => true]);
|
||||
}
|
||||
|
||||
$ext = $this->ext($user['id']);
|
||||
$isMember = (int)$ext['is_member'] === 1
|
||||
&& !empty($ext['member_expiretime']) && (int)$ext['member_expiretime'] > $now;
|
||||
$quota = $this->memberFreeViewQuota($ext);
|
||||
$freeViewUnlimited = ($quota === 0);
|
||||
$isOwner = (int)$post['user_id'] === (int)$user['id'];
|
||||
if ($isOwner) {
|
||||
$this->ok(['unlocked' => true, 'freeMode' => true]);
|
||||
}
|
||||
|
||||
// 已解锁直接返回(严格返回纯净扁平字段,杜绝 JSON 嵌套导致前端格式错乱)
|
||||
// 已解锁直接返回
|
||||
$exist = Db::name('fc_pay_view')
|
||||
->where('user_id', $user['id'])
|
||||
->where('post_id', $id)
|
||||
->find();
|
||||
if ($exist) {
|
||||
$today = date('Y-m-d');
|
||||
$usedToday = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
|
||||
$this->ok([
|
||||
'unlocked' => true,
|
||||
'phone' => $phone,
|
||||
'contact' => $contact,
|
||||
'contactName' => $contactName,
|
||||
'wechat' => $wechat,
|
||||
'contactInfo' => $contact,
|
||||
'freeViewUnlimited' => $freeViewUnlimited,
|
||||
'freeViewLeft' => $isMember ? ($freeViewUnlimited ? 999 : max(0, $quota - $usedToday)) : 0,
|
||||
]);
|
||||
$this->ok(['unlocked' => true]);
|
||||
}
|
||||
|
||||
$amount = max(0, round((float)$this->cfg('fee.payView', 2), 2));
|
||||
$now = time();
|
||||
$orderId = 0;
|
||||
$pending = null;
|
||||
|
||||
// 后台开启支付密码时,余额支付需校验(全站免费或会员免费额度未用完时走免费,不要求密码)
|
||||
if (!$isFreeView && $payType === 'balance' && !$isMember) {
|
||||
// 后台开启支付密码时,余额支付需校验
|
||||
if ($payType === 'balance') {
|
||||
$this->verifyPayPassword($user);
|
||||
}
|
||||
|
||||
try {
|
||||
Db::transaction(function () use (&$orderId, &$pending, $isFreeView, $isMember, $quota, $payType, $amount, $user, $id, $now) {
|
||||
if ($isFreeView) {
|
||||
// 全站免费浏览模式(或解锁单价设为 0 元):直接免单解锁,避免走 0 元支付死锁
|
||||
$amount = 0;
|
||||
$payType = 'free';
|
||||
} else {
|
||||
// 会员每日免费查看额度(quota=0 代表会员不限次数;当天用完后按正常付费)
|
||||
$usedFree = $isMember ? $this->consumeFreeView((int)$user['id'], $quota) : false;
|
||||
|
||||
if ($usedFree) {
|
||||
$amount = 0;
|
||||
$payType = 'free';
|
||||
} elseif ($payType === 'free') {
|
||||
// 用户希望使用会员免费特权,但额度已耗尽或非会员
|
||||
throw new \RuntimeException($isMember ? '今日会员免费查看额度已用完,请选择支付方式付费查看' : '该内容需付费查看,请选择支付方式');
|
||||
} elseif ($payType === 'balance') {
|
||||
// 条件扣款:并发下不会扣成负数
|
||||
$affected = Db::name('user')
|
||||
->where('id', (int)$user['id'])
|
||||
->where('money', '>=', $amount)
|
||||
->setDec('money', $amount);
|
||||
if (!$affected) {
|
||||
throw new \RuntimeException('余额不足,请充值或使用微信支付');
|
||||
}
|
||||
$balance = (float)Db::name('user')->where('id', (int)$user['id'])->value('money');
|
||||
// 余额支付为「即时支付」:同步落一笔已支付订单,保证「我的订单」与微信支付口径一致
|
||||
$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 解锁(回调可独立完成)
|
||||
$orderNo = $this->orderNo('VO');
|
||||
Db::name('fc_order')->insert([
|
||||
'order_no' => $orderNo,
|
||||
'user_id' => $user['id'],
|
||||
'type' => 'payview',
|
||||
'title' => '付费查看 #' . $id,
|
||||
'amount' => $amount,
|
||||
'pay_type' => 'wxpay',
|
||||
'status' => 'pending',
|
||||
'paytime' => null,
|
||||
'related_id' => $id,
|
||||
'createtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$pending = [
|
||||
'orderNo' => $orderNo,
|
||||
'amount' => $amount,
|
||||
'payType' => 'wxpay',
|
||||
'status' => 'pending',
|
||||
];
|
||||
return;
|
||||
Db::transaction(function () use (&$orderId, &$pending, $payType, $amount, $user, $id, $now) {
|
||||
if ($payType === 'balance') {
|
||||
// 条件扣款:并发下不会扣成负数
|
||||
$affected = Db::name('user')
|
||||
->where('id', (int)$user['id'])
|
||||
->where('money', '>=', $amount)
|
||||
->setDec('money', $amount);
|
||||
if (!$affected) {
|
||||
throw new \RuntimeException('余额不足,请充值或使用微信支付');
|
||||
}
|
||||
$balance = (float)Db::name('user')->where('id', (int)$user['id'])->value('money');
|
||||
// 余额支付为「即时支付」:同步落一笔已支付订单,保证「我的订单」与微信支付口径一致
|
||||
$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 解锁(回调可独立完成)
|
||||
$orderNo = $this->orderNo('VO');
|
||||
Db::name('fc_order')->insert([
|
||||
'order_no' => $orderNo,
|
||||
'user_id' => $user['id'],
|
||||
'type' => 'payview',
|
||||
'title' => '解锁信息 #' . $id,
|
||||
'amount' => $amount,
|
||||
'pay_type' => 'wxpay',
|
||||
'status' => 'pending',
|
||||
'paytime' => null,
|
||||
'related_id' => $id,
|
||||
'createtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$pending = [
|
||||
'orderNo' => $orderNo,
|
||||
'amount' => $amount,
|
||||
'payType' => 'wxpay',
|
||||
'status' => 'pending',
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
Db::name('fc_pay_view')->insert([
|
||||
@@ -1375,20 +1366,8 @@ class Post extends V1
|
||||
}
|
||||
}
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$extNow = $this->ext($user['id']);
|
||||
$usedNow = ((string)$extNow['free_view_date'] === $today) ? (int)$extNow['free_view_used'] : 0;
|
||||
|
||||
$this->ok([
|
||||
'unlocked' => true,
|
||||
'phone' => $phone,
|
||||
'contact' => $contact,
|
||||
'contactName' => $contactName,
|
||||
'wechat' => $wechat,
|
||||
'contactInfo' => $contact,
|
||||
'freeViewUnlimited' => $freeViewUnlimited,
|
||||
'freeViewLeft' => $isMember ? ($freeViewUnlimited ? 999 : max(0, $quota - $usedNow)) : 0,
|
||||
'usedFree' => isset($usedFree) ? $usedFree : false,
|
||||
'unlocked' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户