Files
webman-buildadmin/app/admin/controller/Channel.php

840 lines
31 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\admin\controller;
use Throwable;
use app\common\controller\Backend;
use support\think\Db;
use support\Response;
use Webman\Http\Request as WebmanRequest;
/**
* 渠道管理
*/
class Channel extends Backend
{
/**
* 预览接口与手动结算共用「手动结算」按钮权限(避免额外菜单节点)
*/
protected array $noNeedPermission = ['manualSettlePreview'];
/**
* Channel模型对象
* @var object|null
* @phpstan-var \app\common\model\Channel|null
*/
protected ?object $model = null;
protected array|string $preExcludeFields = ['id', 'user_count', 'profit_amount', 'create_time', 'update_time'];
protected array $withJoinTable = ['adminGroup', 'admin'];
protected string|array $quickSearchField = ['id', 'code', 'name'];
protected bool $modelSceneValidate = true;
private array $currentChannelIds = [];
protected function initController(WebmanRequest $request): ?Response
{
$this->model = new \app\common\model\Channel();
$this->currentChannelIds = $this->getCurrentChannelIds();
return null;
}
/**
* 渠道-管理员树(父级=渠道,子级=管理员,仅可选择子级)
*/
public function adminTree(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) return $response;
$query = Db::name('channel')
->field(['id', 'name', 'admin_group_id'])
->order('id', 'asc');
if (!$this->auth->isSuperAdmin()) {
$query = $query->where('id', 'in', $this->currentChannelIds ?: [0]);
}
$channels = $query->select()->toArray();
$groupChildrenCache = [];
$getGroupChildren = function ($groupId) use (&$getGroupChildren, &$groupChildrenCache) {
if ($groupId === null || $groupId === '') return [];
if (array_key_exists($groupId, $groupChildrenCache)) return $groupChildrenCache[$groupId];
$children = Db::name('admin_group')
->where('pid', $groupId)
->where('status', 1)
->column('id');
$all = [];
foreach ($children as $cid) {
$all[] = $cid;
foreach ($getGroupChildren($cid) as $cc) {
$all[] = $cc;
}
}
$groupChildrenCache[$groupId] = $all;
return $all;
};
$tree = [];
foreach ($channels as $ch) {
$groupId = $ch['admin_group_id'] ?? null;
$groupIds = [];
if ($groupId !== null && $groupId !== '') {
$groupIds[] = $groupId;
foreach ($getGroupChildren($groupId) as $gid) {
$groupIds[] = $gid;
}
}
$adminIds = [];
if ($groupIds) {
$adminIds = Db::name('admin_group_access')
->where('group_id', 'in', array_unique($groupIds))
->column('uid');
}
$adminIds = array_values(array_unique($adminIds));
$admins = [];
if ($adminIds) {
$admins = Db::name('admin')
->field(['id', 'username'])
->where('id', 'in', $adminIds)
->order('id', 'asc')
->select()
->toArray();
}
$children = [];
foreach ($admins as $a) {
$children[] = [
'value' => (string) $a['id'],
'label' => $a['username'],
'channel_id' => $ch['id'],
'is_leaf' => true,
];
}
$tree[] = [
'value' => 'channel_' . $ch['id'],
'label' => $ch['name'],
'disabled' => true,
'children' => $children,
];
}
return $this->success('', [
'list' => $tree,
]);
}
/**
* 添加重写管理员只选顶级组admin_group_id 后端自动写入)
* @throws Throwable
*/
protected function _add(): Response
{
if ($this->request && $this->request->method() === 'POST') {
$data = $this->request->post();
if (!$data) {
return $this->error(__('Parameter %s can not be empty', ['']));
}
$data = $this->applyInputFilter($data);
$data = $this->excludeFields($data);
$data = $this->normalizeAgentModeFields($data);
$bizErr = $this->validateAndNormalizeBusinessFields($data);
if ($bizErr !== null) {
return $this->error($bizErr);
}
unset($data['invite_code']);
$adminId = $data['admin_id'] ?? null;
if ($adminId === null || $adminId === '') {
return $this->error(__('Parameter %s can not be empty', ['admin_id']));
}
if (array_key_exists('admin_group_id', $data)) {
unset($data['admin_group_id']);
}
$topGroupId = Db::name('admin_group_access')
->alias('aga')
->join('admin_group ag', 'aga.group_id = ag.id')
->where('aga.uid', $adminId)
->where('ag.pid', 0)
->value('ag.id');
if ($topGroupId === null || $topGroupId === '') {
return $this->error(__('Record not found'));
}
$data['admin_group_id'] = $topGroupId;
if (!$this->auth->isSuperAdmin()) {
$data['admin_id'] = $this->auth->id;
}
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
$data[$this->dataLimitField] = $this->auth->id;
}
$result = false;
$this->model->startTrans();
try {
if ($this->modelValidate) {
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
if (class_exists($validate)) {
$validate = new $validate();
if ($this->modelSceneValidate) {
$validate->scene('add');
}
$validate->check($data);
}
}
$result = $this->model->save($data);
$this->model->commit();
} catch (Throwable $e) {
$this->model->rollback();
return $this->error($e->getMessage());
}
if ($result !== false) {
return $this->success(__('Added successfully'));
}
return $this->error(__('No rows were added'));
}
return $this->error(__('Parameter error'));
}
/**
* 编辑重写管理员只选顶级组admin_group_id 后端自动写入)
* @throws Throwable
*/
protected function _edit(): Response
{
$pk = $this->model->getPk();
$id = $this->request ? ($this->request->post($pk) ?? $this->request->get($pk)) : null;
$row = $this->model->find($id);
if (!$row) {
return $this->error(__('Record not found'));
}
if (!$this->auth->isSuperAdmin() && !in_array($row['id'], $this->currentChannelIds, true)) {
return $this->error(__('You have no permission'));
}
$dataLimitAdminIds = $this->getDataLimitAdminIds();
if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) {
return $this->error(__('You have no permission'));
}
if ($this->request && $this->request->method() === 'POST') {
$data = $this->request->post();
if (!$data) {
return $this->error(__('Parameter %s can not be empty', ['']));
}
$data = $this->applyInputFilter($data);
$data = $this->excludeFields($data);
$data = $this->normalizeAgentModeFields($data);
$bizErr = $this->validateAndNormalizeBusinessFields($data);
if ($bizErr !== null) {
return $this->error($bizErr);
}
unset($data['invite_code']);
if (array_key_exists('admin_group_id', $data)) {
unset($data['admin_group_id']);
}
$nextAdminId = array_key_exists('admin_id', $data) ? $data['admin_id'] : ($row['admin_id'] ?? null);
if ($nextAdminId !== null && $nextAdminId !== '') {
$topGroupId = Db::name('admin_group_access')
->alias('aga')
->join('admin_group ag', 'aga.group_id = ag.id')
->where('aga.uid', $nextAdminId)
->where('ag.pid', 0)
->value('ag.id');
if ($topGroupId === null || $topGroupId === '') {
return $this->error(__('Record not found'));
}
$data['admin_group_id'] = $topGroupId;
}
if (!$this->auth->isSuperAdmin()) {
$data['admin_id'] = $this->auth->id;
}
$result = false;
$this->model->startTrans();
try {
if ($this->modelValidate) {
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
if (class_exists($validate)) {
$validate = new $validate();
if ($this->modelSceneValidate) {
$validate->scene('edit');
}
$data[$pk] = $row[$pk];
$validate->check($data);
}
}
$result = $row->save($data);
$this->model->commit();
} catch (Throwable $e) {
$this->model->rollback();
return $this->error($e->getMessage());
}
if ($result !== false) {
return $this->success(__('Update successful'));
}
return $this->error(__('No rows updated'));
}
return $this->success('', [
'row' => $row
]);
}
/**
* 查看
* @throws Throwable
*/
protected function _index(): Response
{
if ($this->request && $this->request->get('select')) {
return $this->select($this->request);
}
list($where, $alias, $limit, $order) = $this->queryBuilder();
if (!$this->auth->isSuperAdmin()) {
$where[] = [$alias['channel'] . '.id', 'in', $this->currentChannelIds ?: [0]];
}
$res = $this->model
->withJoin($this->withJoinTable, $this->withJoinType)
->with($this->withJoinTable)
->visible(['adminGroup' => ['name'], 'admin' => ['username']])
->alias($alias)
->where($where)
->order($order)
->paginate($limit);
return $this->success('', [
'list' => $res->items(),
'total' => $res->total(),
'remark' => get_route_remark(),
]);
}
/**
* 手动结算预览:区间=上次结算周期结束~当前时间;金额来自已结算注单汇总(服务端计算)
*/
public function manualSettlePreview(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
if (!$this->auth->check('channel/manualSettle')) {
return $this->error(__('You have no permission'));
}
$id = (int) ($request->get('id', 0));
if ($id <= 0) {
return $this->error(__('Parameter error'));
}
$row = $this->model->find($id);
if (!$row) {
return $this->error(__('Record not found'));
}
if (!$this->auth->isSuperAdmin() && !in_array((int) $row['id'], $this->currentChannelIds, true)) {
return $this->error(__('You have no permission'));
}
$payload = $this->buildManualSettlePayload($row->toArray());
if (is_string($payload)) {
return $this->error($payload);
}
return $this->success('', $payload);
}
/**
* 手动结算(渠道维度):仅接收备注;周期与金额全部由服务端按注单汇总计算,并写入结算周期与佣金记录
*/
public function manualSettle(WebmanRequest $request): Response
{
$response = $this->initializeBackend($request);
if ($response !== null) {
return $response;
}
$id = (int) ($request->post('id', $request->get('id', 0)));
if ($id <= 0) {
return $this->error(__('Parameter error'));
}
$row = $this->model->find($id);
if (!$row) {
return $this->error(__('Record not found'));
}
if (!$this->auth->isSuperAdmin() && !in_array((int) $row['id'], $this->currentChannelIds, true)) {
return $this->error(__('You have no permission'));
}
$remark = (string) $request->post('remark', '');
$payload = $this->buildManualSettlePayload($row->toArray());
if (is_string($payload)) {
return $this->error($payload);
}
$settlementNo = $payload['settlement_no'];
if (Db::name('agent_settlement_period')->where('settlement_no', $settlementNo)->value('id')) {
return $this->error('结算单号已存在,请稍后重试');
}
$adminId = $row['admin_id'] ?? null;
if ($adminId === null || $adminId === '' || (int) $adminId <= 0) {
return $this->error('渠道未绑定代理管理员,无法生成佣金记录');
}
$now = time();
Db::startTrans();
try {
$periodId = (int) Db::name('agent_settlement_period')->insertGetId([
'settlement_no' => $settlementNo,
'period_start_at' => $payload['period_start_ts'],
'period_end_at' => $payload['period_end_ts'],
'total_bet_amount' => $payload['total_bet_amount'],
'total_payout_amount' => $payload['total_payout_amount'],
'platform_profit_amount' => $payload['platform_profit_amount'],
'status' => 2,
'remark' => trim($remark) !== '' ? $remark : ('手动结算-渠道#' . $row['id'] . '-' . $row['name']),
'create_time' => $now,
'update_time' => $now,
]);
Db::name('agent_commission_record')->insert([
'settlement_period_id' => $periodId,
'channel_id' => (int) $row['id'],
'admin_id' => (int) $adminId,
'commission_rate' => $payload['commission_rate'],
'calc_base_amount' => $payload['calc_base_amount'],
'commission_amount' => $payload['commission_amount'],
'status' => 0,
'settled_at' => null,
'remark' => trim($remark) !== '' ? $remark : ('手动结算佣金-CH' . $row['id']),
'create_time' => $now,
'update_time' => $now,
]);
Db::name('channel')->where('id', $row['id'])->update([
'update_time' => $now,
]);
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return $this->error($e->getMessage());
}
return $this->success('手动结算已完成,已生成结算周期与佣金记录');
}
/**
* @return array|string 成功返回预览数据数组,失败返回错误文案
*/
private function buildManualSettlePayload(array $row): array|string
{
$channelId = (int) ($row['id'] ?? 0);
if ($channelId <= 0) {
return '渠道数据异常';
}
$endTs = time();
$lastEnd = $this->getLastSettlementEndForChannel($channelId);
$channelCreateTs = (int) ($row['create_time'] ?? 0);
if ($lastEnd === null) {
$periodStartTs = $channelCreateTs > 0 ? $channelCreateTs : 0;
} else {
$periodStartTs = (int) $lastEnd;
}
if ($periodStartTs >= $endTs) {
return '结算区间无效(开始时间不早于当前)';
}
$stats = $this->aggregateBetOrderForChannel($channelId, $periodStartTs, $lastEnd !== null, $endTs);
$totalBet = $stats['total_bet'];
$totalPayout = $stats['total_payout'];
$profit = bcsub($totalBet, $totalPayout, 4);
$mode = (string) ($row['agent_mode'] ?? 'turnover');
$commission = $this->computeCommissionAmounts($row, $totalBet, $profit, $mode);
if (is_string($commission)) {
return $commission;
}
$settlementNo = $this->generateAgentSettlementNo('M', $channelId, $endTs);
return [
'settlement_no' => $settlementNo,
'period_start_ts' => $periodStartTs,
'period_end_ts' => $endTs,
'period_start_at' => date('Y-m-d H:i:s', $periodStartTs),
'period_end_at' => date('Y-m-d H:i:s', $endTs),
'total_bet_amount' => $totalBet,
'total_payout_amount' => $totalPayout,
'platform_profit_amount' => $profit,
'commission_rate' => $commission['commission_rate'],
'calc_base_amount' => $commission['calc_base_amount'],
'commission_amount' => $commission['commission_amount'],
'agent_mode' => $mode,
];
}
/**
* 生成代理结算周期单号:仅大写字母与数字、无分隔符;首字符 M=手动结算A=自动结算(定时任务等复用)
*/
private function generateAgentSettlementNo(string $sourceFlag, int $channelId, int $endTs): string
{
$flag = strtoupper(trim($sourceFlag));
if ($flag !== 'M' && $flag !== 'A') {
$flag = 'M';
}
$channelPart = str_pad((string) max(0, $channelId), 6, '0', STR_PAD_LEFT);
$timePart = str_pad((string) max(0, $endTs), 10, '0', STR_PAD_LEFT);
$base = $flag . $channelPart . $timePart;
for ($i = 0; $i < 8; $i++) {
$randPart = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8));
$no = $base . $randPart;
if (!Db::name('agent_settlement_period')->where('settlement_no', $no)->value('id')) {
return $no;
}
}
return $base . strtoupper(substr(bin2hex(random_bytes(8)), 0, 16));
}
private function getLastSettlementEndForChannel(int $channelId): ?int
{
$row = Db::name('agent_commission_record')
->alias('acr')
->join('agent_settlement_period asp', 'acr.settlement_period_id = asp.id')
->where('acr.channel_id', $channelId)
->field('MAX(asp.period_end_at) AS m')
->find();
if (!$row) {
return null;
}
$m = $row['m'] ?? null;
if ($m === null || $m === '') {
return null;
}
return (int) $m;
}
/**
* @return array{total_bet:string,total_payout:string}
*/
private function aggregateBetOrderForChannel(int $channelId, int $periodStartTs, bool $hasPriorSettlement, int $endTs): array
{
$query = Db::name('bet_order')
->where('channel_id', $channelId)
->where('status', 2)
->where('create_time', '<=', $endTs);
if ($hasPriorSettlement) {
$query->where('create_time', '>', $periodStartTs);
} else {
$query->where('create_time', '>=', $periodStartTs);
}
$row = $query->field('SUM(total_amount) AS tb, SUM(win_amount) AS tw, SUM(jackpot_extra_amount) AS tj')->find();
$tb = $row && $row['tb'] !== null && $row['tb'] !== '' ? (string) $row['tb'] : '0.0000';
$tw = $row && $row['tw'] !== null && $row['tw'] !== '' ? (string) $row['tw'] : '0.0000';
$tj = $row && $row['tj'] !== null && $row['tj'] !== '' ? (string) $row['tj'] : '0.0000';
$totalPayout = bcadd($tw, $tj, 4);
return [
'total_bet' => number_format((float) $tb, 4, '.', ''),
'total_payout' => number_format((float) $totalPayout, 4, '.', ''),
];
}
/**
* @return array{commission_rate:string,calc_base_amount:string,commission_amount:string}|string
*/
private function computeCommissionAmounts(array $row, string $totalBet, string $platformProfit, string $mode): array|string
{
if ($mode === 'turnover') {
$ratePercent = $row['turnover_share_rate'] ?? null;
if ($ratePercent === null || $ratePercent === '') {
return '普通返水代理未配置返水分红比例';
}
$rateDec = bcdiv((string) $ratePercent, '100', 6);
$amount = bcmul($totalBet, $rateDec, 4);
return [
'commission_rate' => $rateDec,
'calc_base_amount' => $totalBet,
'commission_amount' => $amount,
];
}
if ($mode === 'affiliate') {
$fee = $row['affiliate_fee_rate'] ?? null;
$rulesRaw = $row['affiliate_ladder_rules'] ?? null;
if ($fee === null || $fee === '') {
return '联营代理未配置成本扣除比例';
}
$rules = $this->normalizeLadderRulesForSettlement($rulesRaw);
if ($rules === []) {
return '联营阶梯规则无效或为空';
}
if (bccomp($platformProfit, '0', 4) <= 0) {
return [
'commission_rate' => '0.000000',
'calc_base_amount' => '0.0000',
'commission_amount' => '0.0000',
];
}
$afterFee = bcmul($platformProfit, bcsub('1', (string) $fee, 8), 4);
if (bccomp($afterFee, '0', 4) <= 0) {
return [
'commission_rate' => '0.000000',
'calc_base_amount' => '0.0000',
'commission_amount' => '0.0000',
];
}
$playerLoss = $platformProfit;
$share = $this->pickAffiliateShareRateFromLadder($rules, $playerLoss);
$rateDec = number_format($share, 6, '.', '');
$amount = bcmul($afterFee, $rateDec, 4);
return [
'commission_rate' => $rateDec,
'calc_base_amount' => $afterFee,
'commission_amount' => $amount,
];
}
return '未知的代理模式';
}
/**
* @return array<int, array{minLoss: string, shareRate: string}>
*/
private function normalizeLadderRulesForSettlement(mixed $rulesRaw): array
{
if ($rulesRaw === null || $rulesRaw === '') {
return [];
}
if (is_string($rulesRaw)) {
$decoded = json_decode($rulesRaw, true);
$rulesRaw = is_array($decoded) ? $decoded : [];
}
if (!is_array($rulesRaw)) {
return [];
}
$out = [];
foreach ($rulesRaw as $rule) {
if (!is_array($rule)) {
continue;
}
$minLoss = $rule['minLoss'] ?? ($rule['min_loss'] ?? null);
$shareRate = $rule['shareRate'] ?? ($rule['share_rate'] ?? null);
if ($minLoss === null || $shareRate === null || !is_numeric((string) $minLoss) || !is_numeric((string) $shareRate)) {
continue;
}
$out[] = [
'minLoss' => number_format((float) $minLoss, 4, '.', ''),
'shareRate' => number_format((float) $shareRate, 6, '.', ''),
];
}
usort($out, function ($a, $b) {
return bccomp($a['minLoss'], $b['minLoss'], 4);
});
return $out;
}
/**
* @param array<int, array{minLoss: string, shareRate: string}> $rules
*/
private function pickAffiliateShareRateFromLadder(array $rules, string $playerLoss): float
{
$chosen = (float) $rules[0]['shareRate'];
foreach ($rules as $rule) {
if (bccomp($playerLoss, $rule['minLoss'], 4) >= 0) {
$chosen = (float) $rule['shareRate'];
}
}
return $chosen;
}
private function getCurrentChannelIds(): array
{
if ($this->auth->isSuperAdmin()) {
return Db::name('channel')->column('id');
}
$admin = Db::name('admin')
->field(['id', 'channel_id'])
->where('id', $this->auth->id)
->find();
$ids = [];
if ($admin && !empty($admin['channel_id'])) {
$ids[] = $admin['channel_id'];
}
$byAdmin = Db::name('channel')->where('admin_id', $this->auth->id)->column('id');
return array_values(array_unique(array_merge($ids, $byAdmin)));
}
private function normalizeAgentModeFields(array $data): array
{
$mode = $data['agent_mode'] ?? null;
if (empty($data['settle_cycle'])) {
$data['settle_cycle'] = 'weekly';
}
if (empty($data['settle_weekday'])) {
$data['settle_weekday'] = 1;
}
if (empty($data['settle_time'])) {
$data['settle_time'] = '02:00:00';
}
if ($mode === 'turnover') {
$data['affiliate_share_rate'] = null;
$data['affiliate_fee_rate'] = null;
$data['carryover_balance'] = 0;
$data['affiliate_contract_no'] = null;
$data['affiliate_contract_name'] = null;
$data['affiliate_ladder_rules'] = null;
$data['affiliate_effective_start_at'] = null;
$data['affiliate_effective_end_at'] = null;
return $data;
}
if ($mode === 'affiliate') {
$data['turnover_share_rate'] = null;
}
return $data;
}
private function validateAndNormalizeBusinessFields(array &$data): ?string
{
$cycle = isset($data['settle_cycle']) ? trim((string) $data['settle_cycle']) : 'weekly';
if (!in_array($cycle, ['daily', 'weekly', 'monthly'], true)) {
return '结算周期不合法';
}
$data['settle_cycle'] = $cycle;
$settleTime = isset($data['settle_time']) ? trim((string) $data['settle_time']) : '02:00:00';
if (!preg_match('/^\d{2}:\d{2}:\d{2}$/', $settleTime)) {
return '结算时间格式不正确HH:mm:ss';
}
$data['settle_time'] = $settleTime;
if ($cycle === 'weekly') {
$weekday = isset($data['settle_weekday']) ? (int) $data['settle_weekday'] : 1;
if ($weekday < 1 || $weekday > 7) {
return '周结必须选择周一到周日';
}
$data['settle_weekday'] = $weekday;
} else {
$data['settle_weekday'] = 1;
}
if ($cycle === 'monthly') {
$monthday = isset($data['settle_monthday']) ? (int) $data['settle_monthday'] : 1;
if ($monthday < 1 || $monthday > 31) {
return '月结日期必须在1到31之间';
}
$data['settle_monthday'] = $monthday;
} else {
$data['settle_monthday'] = 1;
}
$mode = isset($data['agent_mode']) ? (string) $data['agent_mode'] : '';
if ($mode === 'turnover') {
if (isset($data['turnover_share_rate']) && $data['turnover_share_rate'] !== '' && $data['turnover_share_rate'] !== null) {
$num = (float) $data['turnover_share_rate'];
if ($num < 0 || $num > 100) {
return '返水分红比例必须在0到100之间';
}
}
return null;
}
if ($mode === 'affiliate') {
foreach (['affiliate_share_rate' => '联营占成比例', 'affiliate_fee_rate' => '联营成本扣除比例'] as $field => $label) {
if (!isset($data[$field]) || $data[$field] === '' || $data[$field] === null) {
return $label . '不能为空';
}
$num = (float) $data[$field];
if ($num < 0 || $num > 1) {
return $label . '必须在0到1之间';
}
}
$ladderErr = $this->validateLadderRulesField($data);
if ($ladderErr !== null) {
return $ladderErr;
}
}
return null;
}
private function validateLadderRulesField(array &$data): ?string
{
$rulesRaw = $data['affiliate_ladder_rules'] ?? null;
if ($rulesRaw === null || $rulesRaw === '') {
return '联营阶梯规则不能为空';
}
if (is_string($rulesRaw)) {
$decoded = json_decode($rulesRaw, true);
if (!is_array($decoded)) {
return '联营阶梯规则必须是有效JSON数组';
}
$rulesRaw = $decoded;
}
if (!is_array($rulesRaw) || $rulesRaw === []) {
return '联营阶梯规则至少需要一条';
}
$normalized = [];
$prevMinLoss = null;
foreach ($rulesRaw as $idx => $rule) {
if (!is_array($rule)) {
return '联营阶梯规则第' . ($idx + 1) . '行格式错误';
}
$minLoss = $rule['minLoss'] ?? ($rule['min_loss'] ?? null);
$shareRate = $rule['shareRate'] ?? ($rule['share_rate'] ?? null);
if ($minLoss === null || $minLoss === '' || !is_numeric((string) $minLoss)) {
return '联营阶梯规则第' . ($idx + 1) . '行起始客损格式错误';
}
if ($shareRate === null || $shareRate === '' || !is_numeric((string) $shareRate)) {
return '联营阶梯规则第' . ($idx + 1) . '行占成比例格式错误';
}
$minLossNum = (float) $minLoss;
$shareRateNum = (float) $shareRate;
if ($minLossNum < 0) {
return '联营阶梯规则第' . ($idx + 1) . '行起始客损不能为负';
}
if ($shareRateNum < 0 || $shareRateNum > 1) {
return '联营阶梯规则第' . ($idx + 1) . '行占成比例必须在0到1之间';
}
if ($prevMinLoss !== null && $minLossNum <= $prevMinLoss) {
return '联营阶梯规则需按起始客损递增';
}
$prevMinLoss = $minLossNum;
$normalized[] = [
'minLoss' => number_format($minLossNum, 4, '.', ''),
'shareRate' => number_format($shareRateNum, 6, '.', ''),
];
}
$data['affiliate_ladder_rules'] = $normalized;
return null;
}
}