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',
];
This diff is collapsed.
......@@ -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