Commit e626a715 authored by 刘小敏's avatar 刘小敏

feat(agent): 新增代理商佣金结算与详情查看功能;代理商佣金结算脚本;

parent f325cc9b
......@@ -81,6 +81,13 @@ class Goods
{
if (isset($this->commissionRules[$agentLevel]) && isset($this->commissionRules[$agentLevel][$commissionLevel])) {
$commissionRule = $this->commissionRules[$agentLevel][$commissionLevel];
custom_log(json_encode([
'unique_tag'=>'记录一下佣金规则',
'agentLevel' => $agentLevel,
'commissionLevel' => $commissionLevel,
'rule' => $commissionRule,
'allRules' => $this->commissionRules,
], JSON_UNESCAPED_UNICODE), 0, 'commission');
return $commissionRule;
}
return false;
......
......@@ -73,10 +73,22 @@ class Agent extends Backend
if (empty($params)) {
$this->error(__('Parameter %s can not be empty', ''));
}
$oldRate = $row->commission_rate;
$result = false;
\think\Db::startTrans();
try {
$result = $row->allowField(true)->save($params);
// 佣金比例变更时记录日志
$newRate = isset($params['commission_rate']) ? round(floatval($params['commission_rate']), 2) : null;
if ($newRate !== null && $newRate != $oldRate) {
\app\admin\model\shopro\agent\AgentRateLog::create([
'agent_id' => $row->id,
'old_rate' => $oldRate,
'new_rate' => $newRate,
'operator_id' => $this->auth->id ?? 0,
'remark' => '后台编辑',
]);
}
\think\Db::commit();
} catch (\Exception $e) {
\think\Db::rollback();
......@@ -115,9 +127,32 @@ class Agent extends Backend
}
$ids = is_array($ids) ? $ids : explode(',', $ids);
// 查询旧佣金比例
$oldAgents = $this->model->where('id', 'in', $ids)->column('commission_rate', 'id');
$count = $this->model->where('id', 'in', $ids)->update(['commission_rate' => $rate]);
if ($count !== false) {
// 记录变更日志
$operatorId = $this->auth->id ?? 0;
$logData = [];
foreach ($ids as $agentId) {
$oldRate = isset($oldAgents[$agentId]) ? $oldAgents[$agentId] : null;
if ($oldRate !== null && $oldRate != $rate) {
$logData[] = [
'agent_id' => $agentId,
'old_rate' => $oldRate,
'new_rate' => $rate,
'operator_id' => $operatorId,
'remark' => '批量修改',
'createtime' => time(),
];
}
}
if (!empty($logData)) {
\app\admin\model\shopro\agent\AgentRateLog::insertAll($logData);
}
$this->success("已成功更新 {$count} 条记录的佣金比例为 {$rate}%");
} else {
$this->error('更新失败');
......@@ -125,6 +160,95 @@ class Agent extends Backend
}
/**
* 详情(弹窗)+ 佣金流水 Ajax 分页
*/
public function detail($ids = null)
{
// 容错:多层兜底获取代理商 ID
// Fast.api.open 以 iframe 方式打开,URL 格式可能是:
// ?ids=1&dialog=1 (新 JS,query 参数)
// /ids/1 (旧 JS,路径参数)
// /ids/1.html (旧 JS + url_html_suffix=html)
// 1) 方法参数(ThinkPHP 路由自动注入)
if (empty($ids)) {
// 2) param() — 兼容 query string (?ids=1) 和 url_common_param
$ids = $this->request->param('ids');
}
if (empty($ids)) {
// 3) get() — 直接读 GET 参数
$ids = $this->request->get('ids');
}
if (empty($ids)) {
// 4) pathinfo 正则解析 /ids/数字 或 /ids/数字.html
$pathinfo = $this->request->pathinfo();
if (preg_match('#/ids/(\d+)[.\b]#', $pathinfo, $m)) {
$ids = $m[1];
}
}
// 临时调试日志(问题定位后可删除)
\think\Log::write('[detail] ids来源检测: method=' . var_export(func_get_arg(0), true) .
' param=' . var_export($this->request->param('ids'), true) .
' get=' . var_export($this->request->get('ids'), true) .
' pathinfo=' . $this->request->pathinfo() .
' 最终ids=' . var_export($ids, true), 'notice');
if (empty($ids)) {
if ($this->request->isAjax()) {
return json(['code' => 0, 'msg' => '缺少代理商ID', 'total' => 0, 'rows' => []]);
}
$this->error(__('Parameter %s can not be empty', 'ids'));
}
$row = $this->model->with('user')->find($ids);
if (!$row) {
if ($this->request->isAjax()) {
return json(['code' => 0, 'msg' => '代理商不存在', 'total' => 0, 'rows' => []]);
}
$this->error(__('No Results were found'));
}
$commissionModel = new \app\admin\model\shopro\agent\AgentOrderCommission;
// Ajax 分页获取佣金流水
if ($this->request->isAjax()) {
$where = ['agent_id' => $row->id];
$offset = $this->request->request('offset', 0, 'intval');
$limit = $this->request->request('limit', 10, 'intval');
$sort = $this->request->request('sort', 'id');
$order = $this->request->request('order', 'desc');
// 白名单校验排序字段,防止 SQL 注入
$allowSort = ['id', 'commission_amount', 'createtime', 'order_createtime'];
if (!in_array($sort, $allowSort)) {
$sort = 'id';
}
if (!in_array(strtolower($order), ['asc', 'desc'])) {
$order = 'desc';
}
$list = $commissionModel
->where($where)
->order($sort, $order)
->limit($offset, $limit)
->select();
$total = $commissionModel->where($where)->count();
return json([
'code' => 1,
'total' => $total,
'rows' => $list,
]);
}
// 弹窗视图
$this->view->assign('row', $row);
$this->view->assign('statusList', $commissionModel->statusList());
return $this->view->fetch();
}
/**
* 查看
*/
public function index()
......
......@@ -4,9 +4,9 @@ return [
'User_id' => '用户ID',
'Province_id' => '代理省份ID',
'Province_name' => '省份名称',
'Real_name' => '真实姓名(快照)',
'Id_card' => '身份证号(脱敏)',
'Id_card_images' => '身份证照片(JSON)',
'Real_name' => '真实姓名',
'Id_card' => '身份证号',
'Id_card_images' => '身份证照片(JSON)',
'Commission_rate' => '佣金比例(%)',
'Total_commission' => '累计佣金',
'Status' => '状态',
......@@ -24,4 +24,25 @@ return [
'Remark' => '备注',
'Batch_commission_rate' => '批量修改佣金比例',
'Please_input_commission_rate' => '请输入佣金比例(0-100)',
'Agent_info' => '代理商资料',
'Agent_id' => '代理商ID',
'User_name' => '用户名',
'Commission_flow_logs' => '代理商佣金流水',
'Order_sn' => '订单号',
'Order_createtime' => '订单创建时间',
'Reward_amount' => '奖励金额(5%)',
'Aftersale_order_sn' => '售后订单号',
'Order_status' => '订单状态',
'Agent_user_id' => '代理商用户ID',
'Buyer_user_id' => '购买人用户ID',
'Receive_province_id' => '收货省份ID',
'Receive_province_name' => '收货省份名称',
'Goods_id' => '商品ID',
'Goods_title' => '商品标题',
'Goods_pay_price' => '商品实付金额',
'Commission_rate_snapshot' => '佣金比例(快照)',
'Commission_amount' => '佣金金额',
'Commission_status' => '结算状态',
'Order_confirm_time' => '确认收货时间',
'Detail' => '详情',
];
<?php
namespace app\admin\model\shopro\agent;
use app\admin\model\shopro\Common;
class AgentOrderCommission extends Common
{
protected $name = 'shopro_agent_order_commission';
// 状态
const STATUS_PENDING = 0; // 待结算
const STATUS_SETTLED = 1; // 已结算
const STATUS_CANCEL = -1; // 已取消
const STATUS_REFUND = -2; // 已退款
protected $type = [
'goods_pay_price' => 'decimal',
'commission_rate_snapshot' => 'decimal',
'commission_amount' => 'decimal',
];
public function statusList()
{
return [
self::STATUS_PENDING => '待结算',
self::STATUS_SETTLED => '已结算',
self::STATUS_CANCEL => '已取消',
self::STATUS_REFUND => '已退款',
];
}
public function getStatusTextAttr($value, $data)
{
$value = $value ?? ($data['status'] ?? null);
$map = $this->statusList();
return $map[$value] ?? '-';
}
// 奖励金额 = 订单金额 * 5%
public function getRewardAmountAttr($value, $data)
{
$price = $data['goods_pay_price'] ?? 0;
return round($price * 0.05, 2);
}
}
<?php
namespace app\admin\model\shopro\agent;
use app\admin\model\shopro\Common;
class AgentRateLog extends Common
{
protected $name = 'shopro_agent_rate_log';
protected $autoWriteTimestamp = 'timestamp';
protected $createTime = 'createtime';
protected $updateTime = false;
}
<!-- 代理商详情弹窗 -->
<div class="panel panel-default" style="border:none;box-shadow:none;margin-bottom:0;">
<div class="panel-body" style="padding-top:0;">
<!-- 顶部:代理商资料 -->
<div class="panel panel-default">
<div class="panel-heading">{:__('Agent_info')}</div>
<div class="panel-body">
<table class="table table-bordered table-striped" style="margin-bottom:0;">
<colgroup>
<col width="15%"><col width="35%"><col width="15%"><col width="35%">
</colgroup>
<tbody>
<tr>
<td><strong>{:__('Agent_id')}</strong></td>
<td>{$row.id}</td>
<td><strong>{:__('User_id')}</strong></td>
<td>{$row.user_id}</td>
</tr>
<tr>
<td><strong>{:__('User_name')}</strong></td>
<td>{$row->user->nickname ?? '-'}</td>
<td><strong>{:__('Province_name')}</strong></td>
<td>{$row.province_name}</td>
</tr>
<tr>
<td><strong>{:__(' ')}</strong></td>
<td>{$row.real_name}</td>
<td><strong>{:__('Id_card')}</strong></td>
<td>{$row.id_card}</td>
</tr>
<tr>
<td><strong>{:__('Commission_rate')}</strong></td>
<td>{$row.commission_rate}%</td>
<td><strong>{:__('Total_commission')}</strong></td>
<td>¥{$row.total_commission}</td>
</tr>
<tr>
<td><strong>{:__('Status')}</strong></td>
<td>{$statusList[$row.status] ?? $row.status}</td>
<td><strong>{:__('Createtime')}</strong></td>
<td>{$row.createtime|default='-'}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 底部:代理商佣金流水 -->
<div class="panel panel-default">
<div class="panel-heading">{:__('Commission_flow_logs')}</div>
<div class="panel-body">
<table id="commission-table" class="table table-striped table-bordered table-hover table-nowrap"
width="100%">
</table>
</div>
</div>
</div>
</div>
<script type="text/javascript">
require(['jquery', 'bootstrap', 'table'], function ($, undefined, Table) {
var commissionTable = $("#commission-table");
// AJAX 请求地址
var detailUrl = "{:url('shopro/agent/agent/detail', ['ids' => $row['id'] ?? 0])}";
// 自定义 AJAX:发送分页参数,调用 params.success 返回数据
var commissionAjax = function (params) {
$.ajax({
url: detailUrl,
data: {
offset: params.data.offset || 0,
limit: params.data.limit || 10
},
type: 'get',
dataType: 'json',
success: function (res) {
params.success({
total: res.total || 0,
rows: res.rows || []
});
},
error: function (xhr, status, error) {
console.error('[佣金流水] AJAX 请求失败:', status, error, xhr.responseText);
params.success({ total: 0, rows: [] });
}
});
};
commissionTable.bootstrapTable({
url: detailUrl,
sidePagination: 'server',
pagination: true,
pageSize: 10,
pageList: [10, 20, 50],
showRefresh: false,
showToggle: false,
showColumns: false,
method: 'get',
queryParams: function (params) {
return {
offset: params.offset || 0,
limit: params.limit || 10,
sort: params.sort || '',
order: params.order || ''
};
},
responseHandler: function (res) {
return {
total: res.total || 0,
rows: res.rows || []
};
},
ajax: commissionAjax,
columns: [
[
{ field: 'id', title: 'ID', width: 60, align: 'center', sortable: true },
{ field: 'order_id', title: '{:__("Order_id")}', width: 70, align: 'center' },
{ field: 'order_sn', title: '{:__("Order_sn")}', width: 160, align: 'center' },
{ field: 'order_createtime', title: '{:__("Order_createtime")}', width: 150, align: 'center',
formatter: function (value) {
if (!value) return '-';
var d = new Date(parseInt(value) * 1000);
return d.getFullYear() + '-' +
('0' + (d.getMonth() + 1)).slice(-2) + '-' +
('0' + d.getDate()).slice(-2) + ' ' +
('0' + d.getHours()).slice(-2) + ':' +
('0' + d.getMinutes()).slice(-2) + ':' +
('0' + d.getSeconds()).slice(-2);
}
},
{ field: 'reward_amount', title: '{:__("Reward_amount")}', width: 100, align: 'center',
formatter: function (value, row) {
var price = row.goods_pay_price || 0;
var reward = (price * 0.05).toFixed(2);
return '<span style="color:#f39c12;">¥' + reward + '</span>';
}
},
{ field: 'aftersale_order_sn', title: '{:__("Aftersale_order_sn")}', width: 160, align: 'center',
formatter: function (value) { return value || '-'; }
},
{ field: 'order_status', title: '{:__("Order_status")}', width: 90, align: 'center',
formatter: function (value) { return value || '-'; }
},
{ field: 'agent_id', title: '{:__("Agent_id")}', width: 70, align: 'center' },
{ field: 'agent_user_id', title: '{:__("Agent_user_id")}', width: 80, align: 'center' },
{ field: 'buyer_user_id', title: '{:__("Buyer_user_id")}', width: 80, align: 'center' },
{ field: 'receive_province_name', title: '{:__("Receive_province_name")}', width: 100, align: 'center',
formatter: function (value) { return value || '-'; }
},
{ field: 'goods_title', title: '{:__("Goods_title")}', width: 150, align: 'left',
formatter: function (value) { return value || '-'; }
},
{ field: 'goods_pay_price', title: '{:__("Goods_pay_price")}', width: 100, align: 'center',
formatter: function (value) {
return value ? '¥' + parseFloat(value).toFixed(2) : '-';
}
},
{ field: 'commission_rate_snapshot', title: '{:__("Commission_rate_snapshot")}', width: 100, align: 'center',
formatter: function (value) {
return value !== null && value !== undefined ? value + '%' : '-';
}
},
{ field: 'commission_amount', title: '{:__("Commission_amount")}', width: 100, align: 'center',
formatter: function (value) {
return value ? '¥' + parseFloat(value).toFixed(2) : '-';
}
},
{ field: 'status', title: '{:__("Commission_status")}', width: 80, align: 'center',
formatter: function (value) {
var statusList = {:json_encode($statusList ?? [])};
return statusList[value] || '-';
}
},
{ field: 'order_confirm_time', title: '{:__("Order_confirm_time")}', width: 150, align: 'center',
formatter: function (value) {
if (!value) return '-';
var d = new Date(parseInt(value) * 1000);
return d.getFullYear() + '-' +
('0' + (d.getMonth() + 1)).slice(-2) + '-' +
('0' + d.getDate()).slice(-2) + ' ' +
('0' + d.getHours()).slice(-2) + ':' +
('0' + d.getMinutes()).slice(-2) + ':' +
('0' + d.getSeconds()).slice(-2);
}
},
{ field: 'createtime', title: '{:__("Createtime")}', width: 150, align: 'center',
formatter: function (value) {
if (!value) return '-';
var d = new Date(parseInt(value) * 1000);
return d.getFullYear() + '-' +
('0' + (d.getMonth() + 1)).slice(-2) + '-' +
('0' + d.getDate()).slice(-2) + ' ' +
('0' + d.getHours()).slice(-2) + ':' +
('0' + d.getMinutes()).slice(-2) + ':' +
('0' + d.getSeconds()).slice(-2);
}
}
]
]
});
});
</script>
......@@ -25,5 +25,6 @@ return [
'app\command\TrackFailedRetry',
'app\command\DataStatsCenter',
'app\command\TestLogDb',
'app\command\AgentOrderCommission',
];
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
use think\Db;
/**
* 分销订单佣金结算定时任务
*
* 遍历已完成的分销订单,为每笔订单查找最近的两级代理商,
* 并根据各代理商的 commission_rate 发放佣金奖励,写入 fa_shopro_agent_order_commission 表。
*
* 定时执行建议:每小时执行一次(默认查36小时内订单)
* 命令:php think shopro:agent-order-commission
*
* 回溯补填(脚本异常停了N天后使用):
* php think shopro:agent-order-commission --days=3 # 回溯3天
* php think shopro:agent-order-commission --start="2026-06-28 00:00:00" --end="2026-07-01 00:00:00"
*
* 注意:每笔订单写入前有防重复检查,回溯范围扩大不会导致重复发佣。
*/
class AgentOrderCommission extends Command
{
/**
* 控制台输出对象
* @var Output
*/
protected $output = null;
/**
* 查询时间范围(小时)
*/
const LOOKBACK_HOURS = 36;
/**
* 分页大小(每页订单数)
*/
const PAGE_SIZE = 100;
/**
* 配置命令信息
*/
protected function configure()
{
$this->setName('shopro:agent-order-commission')
->setDescription('分销订单佣金结算:遍历已完成订单,按代理商commission_rate发放佣金')
->addOption('days', 'd', Option::VALUE_OPTIONAL, '回溯天数(覆盖默认36h窗口),例: --days=3', null)
->addOption('start', 's', Option::VALUE_OPTIONAL, '起始时间,格式: Y-m-d H:i:s 或时间戳,例: --start="2026-06-28 00:00:00"', null)
->addOption('end', 'e', Option::VALUE_OPTIONAL, '截止时间,格式: Y-m-d H:i:s 或时间戳,例: --end="2026-07-01 00:00:00"', null);
}
/**
* 自定义日志记录方法
*
* @param string $level 日志级别: info/warning/error
* @param string $message 日志内容
*/
protected function customLog($level, $message)
{
$timestamp = date('Y-m-d H:i:s');
$formattedMessage = "[{$timestamp}] [{$level}] {$message}";
if ($this->output) {
switch ($level) {
case 'error':
$this->output->writeln("<error>{$formattedMessage}</error>");
break;
case 'warning':
$this->output->writeln("<comment>{$formattedMessage}</comment>");
break;
default:
$this->output->writeln("<info>{$formattedMessage}</info>");
break;
}
}
custom_log($message, 0, 'agent_commission', $level);
}
/**
* 命令执行入口方法
*
* @param Input $input
* @param Output $output
*/
protected function execute(Input $input, Output $output)
{
$this->output = $output;
$this->customLog('info', '========== 开始执行分销订单佣金结算任务 ==========');
// 建议:为 fa_shopro_agent_order_commission 添加唯一索引防止并发重复
// ALTER TABLE fa_shopro_agent_order_commission ADD UNIQUE INDEX uk_order_agent (order_id, agent_id);
try {
// 结算时间范围(通过命令行参数或默认36h窗口)
$endTime = time();
$startTime = null;
$startOption = $input->getOption('start');
$endOption = $input->getOption('end');
$daysOption = $input->getOption('days');
if ($startOption) {
// 精确指定起始时间
$startTime = $this->parseTime($startOption);
} elseif ($daysOption && intval($daysOption) > 0) {
// 按天数回溯
$startTime = $endTime - (intval($daysOption) * 86400);
} else {
// 默认:36小时窗口
$startTime = $endTime - (self::LOOKBACK_HOURS * 3600);
}
if ($endOption) {
$endTime = $this->parseTime($endOption);
}
// 时间范围校验
if ($startTime >= $endTime) {
$this->customLog('error', "时间范围无效: 起始时间 >= 截止时间");
return;
}
$diffHours = round(($endTime - $startTime) / 3600, 1);
$this->customLog('info', "查询时间范围: " . date('Y-m-d H:i:s', $startTime) . " ~ " . date('Y-m-d H:i:s', $endTime) . " (跨度: {$diffHours}小时)");
// 1. 分页查询36小时内状态变更为 completed 的订单
$page = 1;
$totalProcessed = 0;
$successCount = 0;
$skipCount = 0;
$errorCount = 0;
while (true) {
$orders = $this->getCompletedOrders($startTime, $endTime, $page, self::PAGE_SIZE);
if (empty($orders)) {
$this->customLog('info', "第{$page}页无数据,分页遍历结束");
break;
}
$pageTotal = count($orders);
$totalProcessed += $pageTotal;
$this->customLog('info', "第{$page}页: 获取 {$pageTotal} 笔订单");
if ($page === 1 && $pageTotal === 0) {
$this->customLog('info', '无待处理订单,任务结束');
return;
}
// 遍历订单,查找两级代理商并结算佣金
foreach ($orders as $order) {
try {
$result = $this->processOrder($order);
if ($result === true) {
$successCount++;
} elseif ($result === 'skip') {
$skipCount++;
}
} catch (\Exception $e) {
$errorCount++;
$this->customLog('error', "订单[{$order['order_sn']}]处理异常: " . $e->getMessage());
}
}
// 如果本页数量少于分页大小,说明已是最后一页
if ($pageTotal < self::PAGE_SIZE) {
$this->customLog('info', "第{$page}页数据不足分页大小,已是最后一页");
break;
}
$page++;
}
$this->customLog('info', "共处理 {$totalProcessed} 笔订单 - 成功: {$successCount}, 跳过: {$skipCount}, 异常: {$errorCount}");
$this->customLog('info', '========== 分销订单佣金结算任务执行完成 ==========');
} catch (\Exception $e) {
$this->customLog('error', '[分销订单佣金结算任务执行失败]');
if (function_exists('format_log_error_custom_name')) {
format_log_error_custom_name($e, '[分销订单佣金结算任务失败]', '', 'agent_commission');
} else {
$this->customLog('error', $e->getMessage() . "\n" . $e->getTraceAsString());
}
}
}
/**
* 解析时间参数,支持时间戳数值和日期字符串两种格式
*
* @param string $value 时间值(时间戳或日期字符串)
* @return int 时间戳
* @throws \InvalidArgumentException
*/
protected function parseTime($value)
{
// 纯数字 → 当作时间戳
if (is_numeric($value) && strlen($value) >= 10) {
return intval($value);
}
// 日期字符串 → strtotime 解析
$timestamp = strtotime($value);
if ($timestamp === false) {
throw new \InvalidArgumentException("无法解析时间参数: {$value},请使用 Y-m-d H:i:s 格式或时间戳");
}
return $timestamp;
}
/**
* 分页获取36小时内状态变更为 completed 的订单列表
*
* 注意:使用 updatetime 而非 createtime 过滤。
* 订单可能在创建很久之后才变为 completed,用 createtime 会导致遗漏。
*
* @param int $startTime 开始时间戳
* @param int $endTime 结束时间戳
* @param int $page 页码(从1开始)
* @param int $pageSize 每页数量
* @return array
*/
protected function getCompletedOrders($startTime, $endTime, $page = 1, $pageSize = 100)
{
$orders = Db::name('shopro_order')
->alias('o')
->field('o.id, o.order_sn, o.user_id, o.pay_fee, o.order_amount, o.goods_amount, o.status, o.createtime, o.updatetime, o.ext')
->where('o.status', 'completed')
->where('o.updatetime', '>=', $startTime)
->where('o.updatetime', '<=', $endTime)
->where('o.pay_fee', '>', 0)
// 排除已删除的订单
->whereNull('o.deletetime')
->order('o.id', 'asc')
->page($page, $pageSize)
->select();
return $orders ?: [];
}
/**
* 处理单笔订单的佣金结算
*
* @param array $order 订单数据
* @return string|bool 'skip'=跳过, true=成功
*/
protected function processOrder($order)
{
$orderId = $order['id'];
$orderSn = $order['order_sn'];
$buyerUserId = $order['user_id'];
$payFee = floatval($order['pay_fee']);
// 获取订单商品信息
$orderItems = $this->getOrderItems($orderId);
// 查找两级代理商
$agents = $this->findAgentChain($buyerUserId, 2);
if (empty($agents)) {
$this->customLog('info', "订单[{$orderSn}]跳过: 未找到有效的上级代理商, 买家[{$buyerUserId}]");
return 'skip';
}
$processedCount = 0;
foreach ($agents as $agentInfo) {
$agentUserId = $agentInfo['user_id'];
$agentLevel = $agentInfo['level']; // 1=一级, 2=二级
// 从 fa_shopro_agent 表获取该代理商的佣金比例及记录ID
$agentRecord = Db::name('shopro_agent')
->where('user_id', $agentUserId)
->field('id, user_id, commission_rate')
->find();
if (!$agentRecord) {
$this->customLog('warning', "订单[{$orderSn}]代理商user_id[{$agentUserId}]在shopro_agent表中不存在, 跳过");
continue;
}
$recordAgentId = $agentRecord['id'];
$commissionRate = floatval($agentRecord['commission_rate']);
if ($commissionRate <= 0) {
$this->customLog('warning', "订单[{$orderSn}]代理商[{$recordAgentId}]commission_rate为0或未设置, 跳过");
continue;
}
// 计算佣金:订单支付金额 × commission_rate%
$commissionAmount = round($payFee * ($commissionRate / 100), 2);
if ($commissionAmount <= 0) {
$this->customLog('warning', "订单[{$orderSn}]代理商[{$recordAgentId}]计算佣金为0, 跳过");
continue;
}
// 获取订单关联的商品信息(取第一个商品作为代表)
$goodsInfo = !empty($orderItems) ? $orderItems[0] : [];
$goodsPayPrice = !empty($orderItems) ? array_sum(array_column($orderItems, 'pay_fee')) : $payFee;
// 获取收货地址中的省份信息
$provinceInfo = $this->getOrderProvince($order['ext'] ?? '');
// 构建佣金记录数据
$data = [
'agent_id' => $recordAgentId,
'agent_user_id' => $agentRecord['user_id'] ?? $agentUserId,
'order_id' => $orderId,
'order_sn' => $orderSn,
'buyer_user_id' => $buyerUserId,
'receive_province_id' => $provinceInfo['province_id'] ?? 0,
'receive_province_name' => $provinceInfo['province_name'] ?? '',
'goods_id' => $goodsInfo['goods_id'] ?? 0,
'goods_title' => $goodsInfo['goods_title'] ?? '',
'goods_pay_price' => $goodsPayPrice,
'commission_rate_snapshot' => $commissionRate,
'commission_amount' => $commissionAmount,
'order_status' => 'completed',
'aftersale_order_sn' => '',
'order_confirm_time' => $order['updatetime'],
'order_createtime' => $order['createtime'],
'status' => 0, // 0=待结算
'createtime' => time(),
'updatetime' => time(),
];
// 事务包裹:检查 + 写入,防止并发时重复插入
$inserted = false;
Db::startTrans();
try {
// 在事务内再次检查(利用事务的隔离性增强并发安全)
$exists = Db::name('shopro_agent_order_commission')
->where('order_id', $orderId)
->where('agent_id', $recordAgentId)
->where('status', 'in', [0, 1])
->lock(true) // 行级锁,进一步阻止并发
->find();
if ($exists) {
Db::rollback();
$this->customLog('info', "订单[{$orderSn}]代理商[{$recordAgentId}]第{$agentLevel}级已存在佣金记录(事务内), 跳过");
continue;
}
// 写入佣金表
Db::name('shopro_agent_order_commission')->insert($data);
Db::commit();
$inserted = true;
} catch (\PDOException $e) {
Db::rollback();
// 捕获唯一索引冲突(如有索引),说明真的重复了
if (stripos($e->getMessage(), 'Duplicate entry') !== false) {
$this->customLog('info', "订单[{$orderSn}]代理商[{$recordAgentId}]唯一索引冲突,已存在记录, 跳过");
} else {
$this->customLog('error', "订单[{$orderSn}]写入异常: " . $e->getMessage());
}
continue;
} catch (\Exception $e) {
Db::rollback();
$this->customLog('error', "订单[{$orderSn}]事务异常: " . $e->getMessage());
continue;
}
if ($inserted) {
$this->customLog('info', "订单[{$orderSn}]第{$agentLevel}级代理商[{$recordAgentId}]佣金: ¥{$commissionAmount} (订单金额: ¥{$payFee})");
$processedCount++;
}
}
return $processedCount > 0 ? true : 'skip';
}
/**
* 获取订单商品列表
*
* @param int $orderId 订单ID
* @return array
*/
protected function getOrderItems($orderId)
{
return Db::name('shopro_order_item')
->where('order_id', $orderId)
->field('id, goods_id, goods_title, goods_price, goods_num, pay_fee')
->select() ?: [];
}
/**
* 递归查找用户的最近两级代理商
*
* 从买家开始向上追溯 parent_user_id,检查每一级是否为有效代理商,
* 最多向上追溯两级。仅收集状态为 normal 或 freeze 的代理商。
*
* @param int $buyerUserId 买家用户ID
* @param int $maxLevel 最大追溯层级(默认2级)
* @return array [['user_id' => xxx, 'level' => 1], ...]
*/
protected function findAgentChain($buyerUserId, $maxLevel = 2)
{
$agents = [];
$currentUserId = $buyerUserId;
$currentLevel = 1;
while ($currentLevel <= $maxLevel) {
// 查找当前用户的上级
$user = Db::name('user')
->where('id', $currentUserId)
->field('id, parent_user_id')
->find();
if (!$user || empty($user['parent_user_id']) || $user['parent_user_id'] <= 0) {
break;
}
$parentUserId = $user['parent_user_id'];
// 检查上级是否为有效代理商
$agent = Db::name('shopro_commission_agent')
->where('user_id', $parentUserId)
->where('status', 'in', ['normal', 'freeze'])
->field('user_id, level')
->find();
if ($agent) {
$agents[] = [
'user_id' => $agent['user_id'],
'agent_user_id' => $agent['user_id'],
'level' => $currentLevel,
];
}
// 继续向上追溯(即使上级不是代理商,也继续向上找)
$currentUserId = $parentUserId;
$currentLevel++;
}
return $agents;
}
/**
* 获取订单的收货省份信息
*
* @param string $ext 订单 ext JSON 字段值
* @return array|null
*/
protected function getOrderProvince($ext)
{
if (empty($ext)) {
return null;
}
$ext = json_decode($ext, true);
if (is_array($ext) && isset($ext['address'])) {
$address = $ext['address'];
if (isset($address['province_id'])) {
return [
'province_id' => $address['province_id'],
'province_name' => $address['province_name'] ?? '',
];
}
}
return null;
}
}
......@@ -7,7 +7,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
extend: {
index_url: 'shopro/agent/agent/index' + location.search,
// add_url: 'shopro/agent/agent/add',
edit_url: 'shopro/agent/agent/edit',
// edit_url: 'shopro/agent/agent/edit',
// del_url: 'shopro/agent/agent/del',
multi_url: 'shopro/agent/agent/multi',
import_url: 'shopro/agent/agent/import',
......@@ -38,7 +38,31 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
{field: 'apply_id', title: __('Apply_id')},
{field: 'createtime', title: __('Createtime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'updatetime', title: __('Updatetime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
{field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, buttons: [
{
name: 'detail',
text: __('Detail'),
title: __('Detail'),
icon: 'fa fa-list',
classname: 'btn btn-info btn-xs btn-detail'
},
{
name: 'edit',
text: __('Edit'),
title: __('Edit'),
icon: 'fa fa-pencil',
classname: 'btn btn-primary btn-xs btn-editone',
url: 'shopro/agent/agent/edit'
},
{
name: 'del',
text: __('Delete'),
title: __('Delete'),
icon: 'fa fa-trash',
classname: 'btn btn-danger btn-xs btn-delone',
url: 'shopro/agent/agent/del'
}
], formatter: Table.api.formatter.operate}
]
]
});
......@@ -46,6 +70,17 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
// 为表格绑定事件
Table.api.bindevent(table);
// 弹窗查看详情
$(document).on('click', '.btn-detail', function () {
var id = $(this).closest('tr').data('id') || Table.api.selectedids(table);
if (!id) return;
Fast.api.open('shopro/agent/agent/detail?ids=' + id, __('Detail'), {
callback: function () {
table.bootstrapTable('refresh');
}
});
});
// 批量修改佣金比例按钮
$(document).on('click', '.btn-commission-rate', function () {
var ids = Table.api.selectedids(table);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment