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

fix(agent): 修复分佣逻辑及用户注销处理

- 修复代理商查找时间条件(>改为<),确保申请成功后的订单才分佣 - 修复获取订单省份信息方式,从解析ext JSON改为查询地址表 - 用户注销时禁用代理和分销商状态并记录日志 - 佣金列表增加结算状态批量操作及状态按钮展示 - 调整佣金列表字段顺序,隐藏无用字段 - 增加批量操作失败提示及列表加载错误处理
parent 4ee671c3
......@@ -211,6 +211,7 @@ class UserAuth
/**
* 注销用户
* 逻辑删除账号,更改用户状态为"禁用"
* 前端好像没用这个接口-暂时标记为废弃-2026.07.08
*/
public function deactivate()
{
......@@ -231,13 +232,21 @@ class UserAuth
/**
* 注销用户
* 物理删除用户,慎用
* 小程序端注销账户用的这个接口2026.07.08
*/
public function logoff()
{
$user = auth_user();
$user = UserModel::get($user->id);
$oldStatus = $user->status;
$user->delete(); // 软删除
custom_log("用户自行注销: user_id={$user->id}, {$oldStatus} -> disabled", $user->id, 'user_status_change', 'info');
// 注销用户时,代理和分销商状态设置为禁用
Db::table('fa_shopro_agent')->where('user_id', $user->id)->update(['status' => 'forbidden']);
Db::table('fa_shopro_commission_agent')->where('user_id', $user->id)->update(['status' => 'forbidden']);
$this->logout();
}
......
......@@ -11,13 +11,17 @@ return [
'Goods_id' => '商品ID',
'Goods_title' => '商品标题',
'Goods_pay_price' => '商品实付金额',
'Commission_rate_snapshot' => '佣金比例(%快照)',
'Commission_rate_snapshot' => '佣金比例(%)',
'Commission_amount' => '佣金金额',
'Order_status' => '订单状态',
'Aftersale_order_sn' => '售后订单号',
'Order_confirm_time' => '订单确认收货时间',
'Order_createtime' => '订单创建时间',
'Status' => '结算状态:0待结算 1已结算 -1已取消 -2已退款',
'Status' => '结算状态',
'Createtime' => '创建时间',
'Updatetime' => '更新时间'
'Updatetime' => '更新时间',
'Set status to 0' => '设为待结算',
'Set status to 1' => '设为已结算',
'Set status to -1' => '设为已取消',
'Set status to -2' => '设为已退款',
];
......@@ -73,14 +73,14 @@
<button type="reset" class="btn btn-default btn-commission-reset"><i class="fa fa-undo"></i> 重置</button>
</form>
<!-- 批量操作按钮 -->
<div class="commission-batch-actions" style="margin-bottom:8px;">
<!-- <div class="commission-batch-actions" style="margin-bottom:8px;">
<button id="btn-batch-export" class="btn btn-success btn-xs" disabled>
<i class="fa fa-download"></i> 批量导出
</button>
<button id="btn-batch-status" class="btn btn-warning btn-xs" disabled>
<i class="fa fa-edit"></i> 批量修改结算状态
</button>
</div>
</div> -->
</div>
<table id="commission-table" class="table table-striped table-bordered table-hover table-nowrap"
width="100%"
......
......@@ -10,6 +10,14 @@
<!-- <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('shopro/agent/order/commission/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('shopro/agent/order/commission/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('shopro/agent/order/commission/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a> -->
<div class="dropdown btn-group {:$auth->check('shopro/agent/agent/batch_commission_status')?'':'hide'}">
<a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
<ul class="dropdown-menu text-left" role="menu">
{foreach name="statusList" item="vo"}
<li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:" data-params="status={$key}">{:__('Set status to ' . $key)}</a></li>
{/foreach}
</ul>
</div>
</div>
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
data-status-list='{:htmlentities(json_encode($statusList ?? []))}'
......
......@@ -269,7 +269,6 @@ class AgentOrderCommission extends Command
// 查找两级代理商,确保代理商申请成功后的订单才分佣
$agents = $this->findAgentChain($buyerUserId, $order['createtime'], 2);
if (empty($agents)) {
$this->customLog('info', "订单[{$orderSn}]跳过: 未找到有效的上级代理商, 买家[{$buyerUserId}]");
return 'skip';
......@@ -309,11 +308,11 @@ class AgentOrderCommission extends Command
}
// 获取订单关联的商品信息(取第一个商品作为代表)
$goodsInfo = !empty($orderItems) ? $orderItems[0] : [];
$goodsPayPrice = !empty($orderItems) ? array_sum(array_column($orderItems, 'pay_fee')) : $payFee;
// $goodsInfo = !empty($orderItems) ? $orderItems[0] : [];
// $goodsPayPrice = !empty($orderItems) ? array_sum(array_column($orderItems, 'pay_fee')) : $payFee;
// 获取收货地址中的省份信息
$provinceInfo = $this->getOrderProvince($order['ext'] ?? '');
$provinceInfo = $this->getOrderProvince($order ?? '');
// 构建佣金记录数据
$data = [
......@@ -431,9 +430,9 @@ class AgentOrderCommission extends Command
// 检查上级是否为有效代理商,确保代理商申请成功后的订单才分佣
$agent = Db::name('shopro_agent')
->where('user_id', $parentUserId)
->where('createtime', '>', $order_create_time)
->where('createtime', '<', $order_create_time)
->where('status', 'in', ['normal', 'freeze'])
->field('user_id, level')
->field('user_id')
->find();
if ($agent) {
......@@ -455,27 +454,29 @@ class AgentOrderCommission extends Command
/**
* 获取订单的收货省份信息
*
* @param string $ext 订单 ext JSON 字段值
* @return array|null
* @param array $order 订单信息
* @return array|null ['province_id' => int, 'province_name' => string] 或 null
*/
protected function getOrderProvince($ext)
protected function getOrderProvince($order)
{
if (empty($ext)) {
$orderId = $order['id'] ?? 0;
if ($orderId <= 0) {
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'] ?? '',
];
}
$address = Db::name('shopro_order_address')
->where('order_id', $orderId)
->field('province_id, province_name')
->find();
if (!$address) {
return null;
}
return null;
return [
'province_id' => $address['province_id'] ?? 0,
'province_name' => $address['province_name'] ?? '',
];
}
/**
......
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
/**
* 初始化所有现有商品参与分销
*
* 遍历 fa_shopro_goods 中所有未删除的商品,为每个商品在
* fa_shopro_commission_goods 表中插入一条分销记录(已存在的跳过)。
*
* 命令:php think shopro:init-commission-goods
*
* 使用场景:
* - 首次部署分销功能后,批量初始化存量商品
* - 可重复执行,已存在的记录不会重复写入(INSERT IGNORE)
*/
class InitCommissionGoods extends Command
{
/**
* 控制台输出对象
* @var Output
*/
protected $output = null;
/**
* 分页大小(每页商品数)
*/
const PAGE_SIZE = 100;
/**
* 配置命令信息
*/
protected function configure()
{
$this->setName('shopro:init-commission-goods')
->setDescription('一次性脚本,初始化所有现有商品参与分销:为每个商品在 fa_shopro_commission_goods 插入记录(已存在则跳过)');
}
/**
* 自定义日志记录方法
*
* @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, 'init_commission_goods', $level);
}
/**
* 命令执行入口方法
*
* @param Input $input
* @param Output $output
*/
protected function execute(Input $input, Output $output)
{
$this->output = $output;
$this->customLog('info', '========== 开始初始化商品参与分销 ==========');
try {
// 默认佣金配置(与后台"设置佣金"功能保持一致)
$defaultCommissionConfig = json_encode([
'status' => 1,
'level' => 2,
'self_buy' => 0,
'reward_type' => 'pay_price',
'reward_event'=> 'finish',
]);
$page = 1;
$totalProcessed = 0;
$insertCount = 0;
$skipCount = 0;
while (true) {
// 分页查询未删除的商品
$goodsList = Db::name('shopro_goods')
->whereNull('deletetime')
->field('id')
->order('id', 'asc')
->page($page, self::PAGE_SIZE)
->select();
if (empty($goodsList)) {
$this->customLog('info', "第{$page}页无数据,分页遍历结束");
break;
}
$pageTotal = count($goodsList);
$totalProcessed += $pageTotal;
$this->customLog('info', "第{$page}页: 获取 {$pageTotal} 个商品");
foreach ($goodsList as $goods) {
$goodsId = $goods['id'];
// INSERT IGNORE:利用主键唯一索引,已存在则跳过
$result = Db::execute(
"INSERT IGNORE INTO `fa_shopro_commission_goods`
(`goods_id`, `self_rules`, `commission_rules`, `status`, `commission_config`, `commission_order_status`)
VALUES (?, ?, ?, ?, ?, ?)",
[$goodsId, 0, '', 1, $defaultCommissionConfig, 1]
);
if ($result > 0) {
$insertCount++;
} else {
$skipCount++;
}
}
// 如果本页数量少于分页大小,说明已是最后一页
if ($pageTotal < self::PAGE_SIZE) {
$this->customLog('info', "第{$page}页数据不足分页大小,已是最后一页");
break;
}
$page++;
}
$this->customLog('info', "共处理 {$totalProcessed} 个商品 - 新增: {$insertCount}, 已存在跳过: {$skipCount}");
$this->customLog('info', '========== 初始化商品参与分销完成 ==========');
} catch (\Exception $e) {
$this->customLog('error', '[初始化商品参与分销失败]');
if (function_exists('format_log_error_custom_name')) {
format_log_error_custom_name($e, '[初始化商品参与分销失败]', '', 'init_commission_goods');
} else {
$this->customLog('error', $e->getMessage() . "\n" . $e->getTraceAsString());
}
}
}
}
......@@ -80,9 +80,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
console.log('---id:'+id);
if (!id) return;
Fast.api.open('shopro/agent/agent/detail?ids=' + id, __('Detail'), {
area: ['90%', '90%'],
callback: function () {
console.log('---66666');
table.bootstrapTable('refresh');
if (table.length && table.bootstrapTable) {
table.bootstrapTable('refresh');
}
}
});
});
......@@ -127,6 +129,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
success: function () {
Layer.close(index);
table.bootstrapTable('refresh');
},
error: function () {
Toastr.error('批量修改佣金比例失败');
}
});
}
......@@ -236,60 +241,80 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
singleSelect: false,
queryParams: buildQueryParams,
responseHandler: function (res) {
if (!res || res.code !== undefined && res.code == 0) {
Toastr.error((res && res.msg) || '佣金流水加载失败');
return { total: 0, rows: [] };
}
var data = res.data || res;
if (!data || (!data.total && !data.rows)) {
data = data || {};
}
return {
total: data.total || 0,
rows: data.rows || []
};
},
onLoadError: function (status, res) {
Toastr.error('佣金流水加载失败,请稍后重试');
},
columns: [
[
{checkbox: true},
{field: 'id', title: __('Id'), width: 60, align: 'center', sortable: true},
{field: 'status', title: __('Commission_status'), width: 80, align: 'center',
formatter: function (value) {
var text = statusList[value] || '-';
var cls = '';
switch (parseInt(value)) {
case 0: cls = 'btn-warning'; break; // 待结算
case 1: cls = 'btn-success'; break; // 已结算
case -1: cls = 'btn-default'; break; // 已取消
case -2: cls = 'btn-danger'; break; // 已退款
default: cls = 'btn-default'; break;
}
return '<button class="btn btn-xs ' + cls + '" style="pointer-events:none;">' + text + '</button>';
}
},
{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: Table.api.formatter.datetime},
{field: 'reward_amount', title: __('Reward_amount'), width: 100, align: 'center',
formatter: function (value, row) {
var price = row.goods_pay_price || 0;
return '<span style="color:#f39c12;">¥' + (price * 0.05).toFixed(2) + '</span>';
{field: 'commission_rate_snapshot', title: __('Commission_rate_snapshot'), width: 120, align: 'center',
formatter: function (value) {
return value !== null && value !== undefined ? value + '%' : '-';
}
},
{field: 'aftersale_order_sn', title: __('Aftersale_order_sn'), width: 160, align: 'center',
formatter: function (value) { return value || '-'; }
{field: 'commission_amount', title: __('Commission_amount'), width: 100, align: 'center',
formatter: function (value) {
var price = value ? '¥' + parseFloat(value).toFixed(2) : '-';
return '<span style="color:#f39c12;">' + price + '</span>';
}
},
// {field: 'reward_amount', title: __('Reward_amount'), width: 100, align: 'center',
// formatter: function (value, row) {
// var price = row.goods_pay_price || 0;
// return '<span style="color:#f39c12;">¥' + (price * 0.05).toFixed(2) + '</span>';
// }
// },
{field: 'order_status', title: __('Order_status'), width: 90, align: 'center',
formatter: function (value) { return orderStatusList[value] || 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',
{field: 'agent_id', title: __('Agent_id'), width: 120, align: 'center'},
{field: 'agent_user_id', title: __('Agent_user_id'), width: 120, align: 'center'},
{field: 'buyer_user_id', title: __('Buyer_user_id'), width: 120, align: 'center'},
{field: 'receive_province_name', title: __('Receive_province_name'), width: 120, align: 'center',
formatter: function (value) { return value || '-'; }
},
{field: 'goods_title', title: __('Goods_title'), width: 150, align: 'left',
// {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: 'aftersale_order_sn', title: __('Aftersale_order_sn'), width: 160, align: 'center',
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) {
return statusList[value] || '-';
}
},
{field: 'order_confirm_time', title: __('Order_confirm_time'), width: 150, align: 'center', formatter: Table.api.formatter.datetime},
{field: 'createtime', title: __('Createtime'), width: 150, align: 'center', formatter: Table.api.formatter.datetime}
]
......@@ -373,6 +398,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
success: function () {
Layer.close(index);
$table.bootstrapTable('refresh');
},
error: function () {
Toastr.error('批量修改结算状态失败');
}
});
}
......
......@@ -32,23 +32,39 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
[
{checkbox: true},
{field: 'id', title: __('Id')},
{field: 'status', title: __('Status'), formatter: function (value) {
var text = statusList[value] || '-';
var cls = '';
switch (parseInt(value)) {
case 0: cls = 'btn-warning'; break; // 待结算
case 1: cls = 'btn-success'; break; // 已结算
case -1: cls = 'btn-default'; break; // 已取消
case -2: cls = 'btn-danger'; break; // 已退款
default: cls = 'btn-default'; break;
}
return '<button class="btn btn-xs ' + cls + '" style="pointer-events:none;">' + text + '</button>';
}},
{field: 'agent_id', title: __('Agent_id')},
{field: 'agent_user_id', title: __('Agent_user_id')},
{field: 'order_id', title: __('Order_id')},
{field: 'order_sn', title: __('Order_sn'), operate: 'LIKE'},
{field: 'buyer_user_id', title: __('Buyer_user_id')},
{field: 'commission_rate_snapshot', title: __('Commission_rate_snapshot'), operate:'BETWEEN'},
{field: 'commission_amount', title: __('Commission_amount'), operate:'BETWEEN',
formatter: function (value) {
var price = value ? '¥' + parseFloat(value).toFixed(2) : '-';
return '<span style="color:#f39c12;">' + price + '</span>';
}
},
{field: 'receive_province_id', title: __('Receive_province_id')},
{field: 'receive_province_name', title: __('Receive_province_name'), operate: 'LIKE'},
{field: 'goods_id', title: __('Goods_id')},
{field: 'goods_title', title: __('Goods_title'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
{field: 'goods_pay_price', title: __('Goods_pay_price'), operate:'BETWEEN'},
{field: 'commission_rate_snapshot', title: __('Commission_rate_snapshot'), operate:'BETWEEN'},
{field: 'commission_amount', title: __('Commission_amount'), operate:'BETWEEN'},
// {field: 'goods_id', title: __('Goods_id')},
// {field: 'goods_title', title: __('Goods_title'), operate: 'LIKE', table: table, class: 'autocontent', formatter: Table.api.formatter.content},
// {field: 'goods_pay_price', title: __('Goods_pay_price'), operate:'BETWEEN'},
{field: 'order_status', title: __('Order_status'), operate: 'LIKE', formatter: function (value) { return orderStatusList[value] || value || '-'; }},
{field: 'aftersale_order_sn', title: __('Aftersale_order_sn'), operate: 'LIKE'},
{field: 'aftersale_order_sn', title: __('Aftersale_order_sn'), operate: 'LIKE', formatter: function (value) { return value || '-'; }},
{field: 'order_confirm_time', title: __('Order_confirm_time'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'order_createtime', title: __('Order_createtime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime},
{field: 'status', title: __('Status'), formatter: function (value) { return statusList[value] || value || '-'; }},
{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}
......
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