1. 新增会员权益配置控制器 Memberbenefit 和 Publish,拆分运营配置模块 2. 新增套餐表字段 free_view_quota、post_discount,支持按套餐配置免费看和发帖折扣 3. 优化前端页面,新增套餐编辑表单、区域选择器、发布配置交互 4. 新增调试支持,支持 mock openid、本地 IP 绕过权限,便于自动化测试 5. 新增数据库工具脚本,用于检查配置、修复数据、验证会员功能 6. 优化会员接口,返回套餐维度的权益信息,支持无限次免费看场景
247 行
8.5 KiB
PHP
247 行
8.5 KiB
PHP
<?php
|
||
|
||
namespace app\common\library;
|
||
|
||
use think\Db;
|
||
|
||
/**
|
||
* 同城分类信息 · 全民分销核心业务服务
|
||
*
|
||
* 负责分销拓客关系链绑定(1级/2级关系)与订单支付后自动拆单分佣结算
|
||
*/
|
||
class DistributionService
|
||
{
|
||
/**
|
||
* 绑定分销上下级关系
|
||
*
|
||
* @param int $userId 新用户ID
|
||
* @param int $inviterId 推荐人用户ID(可选)
|
||
* @param string $inviteCode 推荐人邀请码(可选)
|
||
* @return bool 是否成功建立或已存在关系
|
||
*/
|
||
public static function bindRelation($userId, $inviterId = 0, $inviteCode = '')
|
||
{
|
||
$userId = (int)$userId;
|
||
if ($userId <= 0) {
|
||
return false;
|
||
}
|
||
|
||
// 幂等防御:若用户已有推荐人或已有上级关系记录,则保持原有归属(首绑锁定)
|
||
$ext = Db::name('fc_user_ext')->where('user_id', $userId)->find();
|
||
if ($ext && !empty($ext['inviter_id']) && (int)$ext['inviter_id'] > 0) {
|
||
return true;
|
||
}
|
||
$hasRelation = Db::name('fc_dist_relation')->where('user_id', $userId)->count();
|
||
if ($hasRelation > 0) {
|
||
return true;
|
||
}
|
||
|
||
$parent1Id = 0;
|
||
// 优先通过邀请码查询推荐人
|
||
$inviteCode = trim((string)$inviteCode);
|
||
if ($inviteCode !== '') {
|
||
$parentExt = Db::name('fc_user_ext')->where('invite_code', $inviteCode)->find();
|
||
if ($parentExt && (int)$parentExt['user_id'] > 0) {
|
||
$parent1Id = (int)$parentExt['user_id'];
|
||
}
|
||
}
|
||
|
||
// 次选通过直接传入的推荐人用户ID定位
|
||
if ($parent1Id <= 0 && (int)$inviterId > 0) {
|
||
$parentUser = Db::name('user')->where('id', (int)$inviterId)->find();
|
||
if ($parentUser) {
|
||
$parent1Id = (int)$parentUser['id'];
|
||
}
|
||
}
|
||
|
||
// 推荐人不存在,或推荐人是用户本人,则不建立关系
|
||
if ($parent1Id <= 0 || $parent1Id === $userId) {
|
||
return false;
|
||
}
|
||
|
||
$now = time();
|
||
|
||
// 更新用户扩展表的直接推荐人ID
|
||
Db::name('fc_user_ext')->where('user_id', $userId)->update([
|
||
'inviter_id' => $parent1Id,
|
||
'updatetime' => $now,
|
||
]);
|
||
|
||
// 1. 建立 1 级分销关系(直推)
|
||
Db::name('fc_dist_relation')->insert([
|
||
'user_id' => $userId,
|
||
'parent_id' => $parent1Id,
|
||
'level' => 1,
|
||
'createtime' => $now,
|
||
]);
|
||
|
||
// 2. 递归追溯 2 级分销关系(间推:查找 1 级推荐人的 1 级推荐人)
|
||
$parent2Row = Db::name('fc_dist_relation')
|
||
->where('user_id', $parent1Id)
|
||
->where('level', 1)
|
||
->find();
|
||
|
||
if ($parent2Row && (int)$parent2Row['parent_id'] > 0) {
|
||
$parent2Id = (int)$parent2Row['parent_id'];
|
||
// 避免循环推荐(2级推荐人不能为用户本人和1级推荐人)
|
||
if ($parent2Id !== $userId && $parent2Id !== $parent1Id) {
|
||
Db::name('fc_dist_relation')->insert([
|
||
'user_id' => $userId,
|
||
'parent_id' => $parent2Id,
|
||
'level' => 2,
|
||
'createtime' => $now,
|
||
]);
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 订单支付成功后执行自动拆单分佣结算
|
||
*
|
||
* 适用于各类消费型订单(发布 post、会员开通 member、付费查看 payview 等)
|
||
*
|
||
* @param array $order 订单数据数组,必须包含 id, order_no, user_id, amount, type
|
||
* @return array 结算明细数组
|
||
*/
|
||
public static function settleOrder(array $order)
|
||
{
|
||
$orderId = (int)(isset($order['id']) ? $order['id'] : 0);
|
||
$orderNo = isset($order['order_no']) ? (string)$order['order_no'] : '';
|
||
$payerId = (int)(isset($order['user_id']) ? $order['user_id'] : 0);
|
||
$amount = round((float)(isset($order['amount']) ? $order['amount'] : 0), 2);
|
||
$type = isset($order['type']) ? (string)$order['type'] : 'post';
|
||
|
||
// 充值订单不分佣(避免双重套现),金额必须大于 0
|
||
if ($amount <= 0 || $payerId <= 0 || $type === 'recharge') {
|
||
return [];
|
||
}
|
||
|
||
// 读取分销全局配置
|
||
$enabled = self::getConfig('dist.enabled', 1);
|
||
if (!$enabled) {
|
||
return [];
|
||
}
|
||
|
||
// 幂等防御:已对此订单结算过佣金则直接跳过,防止并发重复发放
|
||
$alreadySettled = Db::name('fc_commission')
|
||
->where('order_no', $orderNo)
|
||
->find();
|
||
if ($alreadySettled) {
|
||
return [];
|
||
}
|
||
|
||
$maxLevel = (int)self::getConfig('dist.level', 2);
|
||
$rate1 = (float)self::getConfig('dist.level1Rate', 20); // 默认 20%
|
||
$rate2 = (float)self::getConfig('dist.level2Rate', 10); // 默认 10%
|
||
$settleDays = self::settleDays();
|
||
|
||
// 查询该付款用户的所有上级推广员(按层级正序)
|
||
$relations = Db::name('fc_dist_relation')
|
||
->where('user_id', $payerId)
|
||
->where('level', '<=', $maxLevel)
|
||
->order('level asc')
|
||
->select();
|
||
|
||
if (empty($relations)) {
|
||
return [];
|
||
}
|
||
|
||
$now = time();
|
||
$settledList = [];
|
||
|
||
foreach ($relations as $rel) {
|
||
$level = (int)$rel['level'];
|
||
$parentId = (int)$rel['parent_id'];
|
||
if ($parentId <= 0) {
|
||
continue;
|
||
}
|
||
|
||
// 获取该层级的有效比例
|
||
$rate = ($level === 1) ? $rate1 : (($level === 2) ? $rate2 : 0);
|
||
if ($rate <= 0) {
|
||
continue;
|
||
}
|
||
|
||
$commissionAmount = round($amount * ($rate / 100), 2);
|
||
if ($commissionAmount <= 0) {
|
||
continue;
|
||
}
|
||
|
||
// 写入分销佣金明细表(结算周期内为「未结算」,到期由 fc:settle 置为已结算)
|
||
$commId = Db::name('fc_commission')->insertGetId([
|
||
'user_id' => $parentId,
|
||
'from_user_id' => $payerId,
|
||
'order_id' => $orderId,
|
||
'order_no' => $orderNo,
|
||
'order_type' => $type,
|
||
'amount' => $commissionAmount,
|
||
'rate' => $rate,
|
||
'level' => $level,
|
||
'status' => $settleDays > 0 ? 0 : 1,
|
||
'settle_due' => $now + $settleDays * 86400,
|
||
'settletime' => $settleDays > 0 ? null : $now,
|
||
'createtime' => $now,
|
||
]);
|
||
|
||
// 发送分销佣金入账站内信通知
|
||
$levelText = ($level === 1) ? '直接下线' : '间接下线';
|
||
$noticeContent = '您的 ' . $levelText . ' 产生了一笔消费(¥' . $amount . '),为您带来分销佣金 ¥' . $commissionAmount . ',已计入可提现资产。';
|
||
Db::name('fc_message')->insert([
|
||
'user_id' => $parentId,
|
||
'type' => 'interact',
|
||
'title' => '分销佣金到账提醒',
|
||
'content' => $noticeContent,
|
||
'isread' => 0,
|
||
'createtime' => $now,
|
||
]);
|
||
|
||
$settledList[] = [
|
||
'commissionId' => $commId,
|
||
'userId' => $parentId,
|
||
'level' => $level,
|
||
'amount' => $commissionAmount,
|
||
];
|
||
}
|
||
|
||
return $settledList;
|
||
}
|
||
|
||
/**
|
||
* 结算周期天数:解析 dist.settleCycle(支持 T+7 / T+1 / T+0 或纯数字 7 / 0),默认 7 天
|
||
*
|
||
* @return int
|
||
*/
|
||
public static function settleDays()
|
||
{
|
||
$cycle = trim((string)self::getConfig('dist.settleCycle', 'T+7'));
|
||
if (preg_match('/T\s*\+\s*(\d+)/i', $cycle, $m)) {
|
||
return max(0, (int)$m[1]);
|
||
}
|
||
if (is_numeric($cycle)) {
|
||
return max(0, (int)$cycle);
|
||
}
|
||
|
||
return 7;
|
||
}
|
||
|
||
/**
|
||
* 读取业务配置项
|
||
*
|
||
* @param string $key 配置键名
|
||
* @param mixed $default 默认回退值
|
||
* @return mixed
|
||
*/
|
||
protected static function getConfig($key, $default = null)
|
||
{
|
||
$row = Db::name('fc_config')->where('key', $key)->find();
|
||
if (!$row || !isset($row['value']) || $row['value'] === '') {
|
||
return $default;
|
||
}
|
||
$val = $row['value'];
|
||
$decoded = json_decode($val, true);
|
||
return (json_last_error() === JSON_ERROR_NONE) ? $decoded : $val;
|
||
}
|
||
}
|