```
feat(split): 添加分账手动指定接收方功能 - 实现SplitManualService服务类,支持手动覆盖分账明细 - 新增SplitManual和SplitManualLog数据模型 - 修改SplitCalculator优先使用手动覆盖数据进行分账计算 - 添加分账记录手动指定接收方的数据库表结构 - 集成手动分账到汇付分账流程中,支持失败重试机制 - 添加商品详情内容更新SQL脚本用于汇付审核 ```
这个提交包含在:
@@ -21,10 +21,22 @@ class SplitCalculator
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算订单分账明细
|
||||
* 计算订单分账明细(优先手动覆盖)
|
||||
* @return array [['role_type','role_id','receiver_id','receiver_account','receiver_type','receiver_name','amount','description']]
|
||||
*/
|
||||
public function calculate($orderId)
|
||||
{
|
||||
$manualItems = (new SplitManualService())->getCalculateItems($orderId);
|
||||
if (!empty($manualItems)) {
|
||||
return $manualItems;
|
||||
}
|
||||
return $this->calculateAuto($orderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按佣金/规则自动计算分账明细
|
||||
*/
|
||||
public function calculateAuto($orderId)
|
||||
{
|
||||
$order = Order::get($orderId);
|
||||
if (!$order || !in_array($order['pay_type'], ['wechat', 'huifu_wechat'])) {
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\split;
|
||||
|
||||
use addons\shopro\model\Order;
|
||||
use addons\shopro\model\split\Receiver;
|
||||
use addons\shopro\model\split\SplitManual;
|
||||
use addons\shopro\model\split\SplitManualLog;
|
||||
use addons\shopro\model\split\SplitOrder;
|
||||
use think\Db;
|
||||
|
||||
class SplitManualService
|
||||
{
|
||||
protected $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = new Config();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存在手动覆盖
|
||||
*/
|
||||
public function hasOverride($orderId)
|
||||
{
|
||||
return (bool) SplitManual::where('order_id', intval($orderId))->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 供 SplitCalculator 使用的明细
|
||||
* @return array
|
||||
*/
|
||||
public function getCalculateItems($orderId)
|
||||
{
|
||||
$manual = SplitManual::where('order_id', intval($orderId))->find();
|
||||
if (!$manual) {
|
||||
return [];
|
||||
}
|
||||
$items = $manual['items'] ?? [];
|
||||
if (!is_array($items) || empty($items)) {
|
||||
return [];
|
||||
}
|
||||
return $this->normalizeCalculateItems($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存手动覆盖
|
||||
* @param int $orderId
|
||||
* @param array $items [{receiver_id, role_type, amount, description?}]
|
||||
* @param int $adminId
|
||||
* @param string $adminName
|
||||
* @param string $remark
|
||||
* @return SplitManual
|
||||
*/
|
||||
public function save($orderId, array $items, $adminId = 0, $adminName = '', $remark = '')
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
$this->assertEditableSplitOrder($orderId);
|
||||
$normalized = $this->validateAndNormalizeItems($orderId, $items);
|
||||
|
||||
$existing = SplitManual::where('order_id', $orderId)->find();
|
||||
$beforeItems = $existing ? ($existing['items'] ?? []) : [];
|
||||
$action = $existing ? 'update' : 'create';
|
||||
|
||||
$originalItems = [];
|
||||
if (!$existing) {
|
||||
$calculator = new SplitCalculator();
|
||||
$originalItems = $calculator->calculateAuto($orderId);
|
||||
$originalItems = $this->enrichItemsForStorage($originalItems);
|
||||
} else {
|
||||
$originalItems = $existing['original_items'] ?? [];
|
||||
}
|
||||
|
||||
$storageItems = $this->enrichItemsForStorage($normalized);
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
if ($existing) {
|
||||
$existing->items = $storageItems;
|
||||
$existing->admin_id = intval($adminId);
|
||||
$existing->remark = $remark;
|
||||
$existing->save();
|
||||
$manual = $existing;
|
||||
} else {
|
||||
$manual = SplitManual::create([
|
||||
'order_id' => $orderId,
|
||||
'items' => $storageItems,
|
||||
'original_items' => $originalItems,
|
||||
'admin_id' => intval($adminId),
|
||||
'remark' => $remark,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->appendLog($orderId, $action, $beforeItems, $storageItems, $adminId, $adminName, $remark);
|
||||
Db::commit();
|
||||
return $manual;
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除手动覆盖
|
||||
*/
|
||||
public function clear($orderId, $adminId = 0, $adminName = '', $remark = '')
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
$this->assertEditableSplitOrder($orderId);
|
||||
|
||||
$existing = SplitManual::where('order_id', $orderId)->find();
|
||||
if (!$existing) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$beforeItems = $existing['items'] ?? [];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$existing->delete();
|
||||
$this->appendLog($orderId, 'clear', $beforeItems, [], $adminId, $adminName, $remark);
|
||||
Db::commit();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存并重试前标记 pending 日志
|
||||
* @return int log id
|
||||
*/
|
||||
public function markPendingRetryLog($orderId, $adminId = 0, $adminName = '', $remark = '')
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
$manual = SplitManual::where('order_id', $orderId)->find();
|
||||
if (!$manual) {
|
||||
throw new \RuntimeException('请先保存手动指定接收方');
|
||||
}
|
||||
|
||||
$order = Order::get($orderId);
|
||||
$orderSn = $order ? (string) $order['order_sn'] : '';
|
||||
|
||||
$log = SplitManualLog::create([
|
||||
'order_id' => $orderId,
|
||||
'order_sn' => $orderSn,
|
||||
'action' => 'retry',
|
||||
'before_items' => $manual['items'] ?? [],
|
||||
'after_items' => $manual['items'] ?? [],
|
||||
'admin_id' => intval($adminId),
|
||||
'admin_name' => $adminName,
|
||||
'remark' => $remark,
|
||||
'retry_result' => 'pending',
|
||||
'createtime' => time(),
|
||||
]);
|
||||
|
||||
return intval($log->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试完成后更新日志
|
||||
*/
|
||||
public function finishPendingRetryLog($orderId, $success, $message = '', $splitOrderId = null)
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
$log = SplitManualLog::where('order_id', $orderId)
|
||||
->where('action', 'retry')
|
||||
->where('retry_result', 'pending')
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
if (!$log) {
|
||||
return;
|
||||
}
|
||||
|
||||
$log->retry_result = $success ? 'success' : 'fail';
|
||||
$log->retry_message = $message;
|
||||
if ($splitOrderId) {
|
||||
$log->retry_split_order_id = intval($splitOrderId);
|
||||
} else {
|
||||
$latest = SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->order('id', 'desc')->find();
|
||||
if ($latest) {
|
||||
$log->retry_split_order_id = intval($latest['id']);
|
||||
}
|
||||
}
|
||||
$log->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 变更历史
|
||||
*/
|
||||
public function getHistory($orderId, $offset = 0, $limit = 20)
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
$query = SplitManualLog::where('order_id', $orderId);
|
||||
$total = $query->count();
|
||||
$rows = SplitManualLog::where('order_id', $orderId)
|
||||
->order('id', 'desc')
|
||||
->limit($offset, $limit)
|
||||
->select();
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
$data = is_array($row) ? $row : $row->toArray();
|
||||
$data['action_text'] = $this->actionLabel($data['action'] ?? '');
|
||||
$data['retry_result_text'] = $this->retryResultLabel($data['retry_result'] ?? '');
|
||||
$data['createtime_text'] = !empty($data['createtime'])
|
||||
? date('Y-m-d H:i:s', $data['createtime'])
|
||||
: '-';
|
||||
$list[] = $data;
|
||||
}
|
||||
|
||||
return ['total' => $total, 'rows' => $list];
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇付接收方下拉选项
|
||||
*/
|
||||
public function listHuifuReceiverOptions($keyword = '')
|
||||
{
|
||||
$query = Receiver::where('status', 'normal')
|
||||
->where(function ($q) {
|
||||
$q->where('channel', 'huifu')->whereOr('channel', null)->whereOr('channel', '');
|
||||
})
|
||||
->where('receiver_account', '<>', '');
|
||||
|
||||
$keyword = trim((string) $keyword);
|
||||
if ($keyword !== '') {
|
||||
$query->where(function ($q) use ($keyword) {
|
||||
$like = '%' . $keyword . '%';
|
||||
$q->where('receiver_name', 'like', $like)
|
||||
->whereOr('receiver_account', 'like', $like);
|
||||
});
|
||||
}
|
||||
|
||||
$rows = $query->order('id', 'desc')->limit(200)->select();
|
||||
$options = [];
|
||||
foreach ($rows as $row) {
|
||||
$data = is_array($row) ? $row : $row->toArray();
|
||||
$account = trim((string) ($data['receiver_account'] ?? ''));
|
||||
if ($account === '') {
|
||||
continue;
|
||||
}
|
||||
$options[] = [
|
||||
'id' => intval($data['id']),
|
||||
'role_type' => $data['role_type'] ?? '',
|
||||
'role_id' => intval($data['role_id'] ?? 0),
|
||||
'receiver_account' => $account,
|
||||
'receiver_name' => trim((string) ($data['receiver_name'] ?? '')),
|
||||
'receiver_type' => $data['receiver_type'] ?? 'HUIFU_ID',
|
||||
'label' => $this->formatReceiverOptionLabel($data),
|
||||
];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载指定失败单的编辑上下文
|
||||
*/
|
||||
public function getManualContext($splitOrderId)
|
||||
{
|
||||
$splitOrder = SplitOrder::get(intval($splitOrderId));
|
||||
if (!$splitOrder) {
|
||||
throw new \RuntimeException('分账记录不存在');
|
||||
}
|
||||
$this->assertEditableSplitRow($splitOrder);
|
||||
|
||||
$orderId = intval($splitOrder['order_id']);
|
||||
$order = Order::get($orderId);
|
||||
$manual = SplitManual::where('order_id', $orderId)->find();
|
||||
|
||||
$calculator = new SplitCalculator();
|
||||
$autoItems = $calculator->calculateAuto($orderId);
|
||||
$autoItems = $this->enrichItemsForStorage($autoItems);
|
||||
|
||||
$currentItems = $manual ? ($manual['items'] ?? []) : $autoItems;
|
||||
$originalItems = $manual ? ($manual['original_items'] ?? []) : $autoItems;
|
||||
|
||||
$confirmBase = null;
|
||||
if (class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService') && $order) {
|
||||
try {
|
||||
$svc = new \addons\shopro_huifu\library\HuifuSplitService();
|
||||
$confirmBase = $svc->previewConfirmPayFee($order['order_sn'], $order['pay_fee']);
|
||||
} catch (\Throwable $e) {
|
||||
$confirmBase = floatval($order['pay_fee'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'split_order_id' => intval($splitOrder['id']),
|
||||
'order_id' => $orderId,
|
||||
'order_sn' => $splitOrder['order_sn'] ?? ($order['order_sn'] ?? ''),
|
||||
'pay_fee' => floatval($order['pay_fee'] ?? 0),
|
||||
'confirm_base' => floatval($confirmBase ?? ($order['pay_fee'] ?? 0)),
|
||||
'max_rate' => $this->config->getMaxRate(),
|
||||
'max_split_amount'=> round(floatval($order['pay_fee'] ?? 0) * $this->config->getMaxRate() / 100, 2),
|
||||
'has_manual' => (bool) $manual,
|
||||
'current_items' => $currentItems,
|
||||
'original_items' => $originalItems,
|
||||
'auto_items' => $autoItems,
|
||||
'receiver_options'=> $this->listHuifuReceiverOptions(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function appendLog($orderId, $action, $beforeItems, $afterItems, $adminId, $adminName, $remark)
|
||||
{
|
||||
$order = Order::get($orderId);
|
||||
SplitManualLog::create([
|
||||
'order_id' => $orderId,
|
||||
'order_sn' => $order ? (string) $order['order_sn'] : '',
|
||||
'action' => $action,
|
||||
'before_items' => is_array($beforeItems) ? $beforeItems : [],
|
||||
'after_items' => is_array($afterItems) ? $afterItems : [],
|
||||
'admin_id' => intval($adminId),
|
||||
'admin_name' => $adminName,
|
||||
'remark' => $remark,
|
||||
'createtime' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertEditableSplitOrder($orderId)
|
||||
{
|
||||
$row = SplitOrder::where('order_id', intval($orderId))
|
||||
->where('channel', 'huifu')
|
||||
->where('status', 4)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
if (!$row) {
|
||||
throw new \RuntimeException('仅失败状态的汇付分账记录可指定接收方');
|
||||
}
|
||||
}
|
||||
|
||||
protected function assertEditableSplitRow($splitOrder)
|
||||
{
|
||||
$data = is_array($splitOrder) ? $splitOrder : $splitOrder->toArray();
|
||||
if (($data['channel'] ?? '') !== 'huifu') {
|
||||
throw new \RuntimeException('仅汇付通道分账记录可指定接收方');
|
||||
}
|
||||
if (intval($data['status'] ?? 0) !== 4) {
|
||||
throw new \RuntimeException('仅失败状态的分账记录可指定接收方');
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateAndNormalizeItems($orderId, array $items)
|
||||
{
|
||||
if (empty($items)) {
|
||||
throw new \RuntimeException('请至少添加一行分账明细');
|
||||
}
|
||||
|
||||
$order = Order::get($orderId);
|
||||
if (!$order) {
|
||||
throw new \RuntimeException('订单不存在');
|
||||
}
|
||||
|
||||
$payFee = floatval($order['pay_fee']);
|
||||
$maxSplit = round($payFee * $this->config->getMaxRate() / 100, 2);
|
||||
$total = 0.0;
|
||||
$normalized = [];
|
||||
$paymentHuifuId = $this->getPaymentHuifuId();
|
||||
|
||||
foreach ($items as $item) {
|
||||
$receiverId = intval($item['receiver_id'] ?? 0);
|
||||
$amount = round(floatval($item['amount'] ?? 0), 2);
|
||||
if ($receiverId <= 0) {
|
||||
throw new \RuntimeException('请选择有效的分账接收方');
|
||||
}
|
||||
if ($amount <= 0) {
|
||||
throw new \RuntimeException('分账金额须大于 0');
|
||||
}
|
||||
|
||||
$receiver = Receiver::where('id', $receiverId)->where('status', 'normal')->find();
|
||||
if (!$receiver) {
|
||||
throw new \RuntimeException('接收方不存在或已禁用');
|
||||
}
|
||||
$account = trim((string) ($receiver['receiver_account'] ?? ''));
|
||||
if ($account === '') {
|
||||
throw new \RuntimeException('接收方未配置 huifu_id');
|
||||
}
|
||||
if ($paymentHuifuId !== '' && $account === $paymentHuifuId) {
|
||||
throw new \RuntimeException('不得仅分给收款主商户(' . $paymentHuifuId . ')');
|
||||
}
|
||||
|
||||
$total += $amount;
|
||||
$normalized[] = [
|
||||
'receiver_id' => $receiverId,
|
||||
'role_type' => $item['role_type'] ?? ($receiver['role_type'] ?? 'promoter'),
|
||||
'role_id' => intval($receiver['role_id'] ?? 0),
|
||||
'receiver_account' => $account,
|
||||
'receiver_type' => $receiver['receiver_type'] ?? 'HUIFU_ID',
|
||||
'receiver_name' => trim((string) ($receiver['receiver_name'] ?? '')),
|
||||
'amount' => $amount,
|
||||
'description' => trim((string) ($item['description'] ?? '手动指定')),
|
||||
];
|
||||
}
|
||||
|
||||
if ($total > $maxSplit + 0.001) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
'分账合计 ¥%s 超过上限 ¥%s(订单金额 × %s%%)',
|
||||
number_format($total, 2, '.', ''),
|
||||
number_format($maxSplit, 2, '.', ''),
|
||||
$this->config->getMaxRate()
|
||||
));
|
||||
}
|
||||
|
||||
$confirmBase = $payFee;
|
||||
if (class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService')) {
|
||||
try {
|
||||
$svc = new \addons\shopro_huifu\library\HuifuSplitService();
|
||||
$confirmBase = floatval($svc->previewConfirmPayFee($order['order_sn'], $payFee));
|
||||
} catch (\Throwable $e) {
|
||||
// use pay_fee
|
||||
}
|
||||
}
|
||||
if ($total > $confirmBase + 0.001) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
'分账合计 ¥%s 超过汇付确认基数 ¥%s',
|
||||
number_format($total, 2, '.', ''),
|
||||
number_format($confirmBase, 2, '.', '')
|
||||
));
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
protected function normalizeCalculateItems(array $items)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($items as $item) {
|
||||
$amount = round(floatval($item['amount'] ?? 0), 2);
|
||||
if ($amount <= 0) {
|
||||
continue;
|
||||
}
|
||||
$account = trim((string) ($item['receiver_account'] ?? ''));
|
||||
if ($account === '') {
|
||||
$receiverId = intval($item['receiver_id'] ?? 0);
|
||||
if ($receiverId > 0) {
|
||||
$receiver = Receiver::get($receiverId);
|
||||
if ($receiver) {
|
||||
$account = trim((string) ($receiver['receiver_account'] ?? ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($account === '') {
|
||||
continue;
|
||||
}
|
||||
$result[] = [
|
||||
'role_type' => $item['role_type'] ?? 'promoter',
|
||||
'role_id' => intval($item['role_id'] ?? 0),
|
||||
'receiver_id' => intval($item['receiver_id'] ?? 0),
|
||||
'receiver_account' => $account,
|
||||
'receiver_type' => $item['receiver_type'] ?? 'HUIFU_ID',
|
||||
'receiver_name' => trim((string) ($item['receiver_name'] ?? '')),
|
||||
'amount' => $amount,
|
||||
'description' => trim((string) ($item['description'] ?? '手动指定')),
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function enrichItemsForStorage(array $items)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($items as $item) {
|
||||
$receiverId = intval($item['receiver_id'] ?? 0);
|
||||
$account = trim((string) ($item['receiver_account'] ?? ''));
|
||||
$name = trim((string) ($item['receiver_name'] ?? ''));
|
||||
$roleType = $item['role_type'] ?? 'promoter';
|
||||
$roleId = intval($item['role_id'] ?? 0);
|
||||
|
||||
if ($receiverId > 0) {
|
||||
$receiver = Receiver::get($receiverId);
|
||||
if ($receiver) {
|
||||
if ($account === '') {
|
||||
$account = trim((string) ($receiver['receiver_account'] ?? ''));
|
||||
}
|
||||
if ($name === '') {
|
||||
$name = trim((string) ($receiver['receiver_name'] ?? ''));
|
||||
}
|
||||
if (!$roleType) {
|
||||
$roleType = $receiver['role_type'] ?? 'promoter';
|
||||
}
|
||||
if (!$roleId) {
|
||||
$roleId = intval($receiver['role_id'] ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'receiver_id' => $receiverId,
|
||||
'role_type' => $roleType,
|
||||
'role_id' => $roleId,
|
||||
'receiver_account' => $account,
|
||||
'receiver_type' => $item['receiver_type'] ?? 'HUIFU_ID',
|
||||
'receiver_name' => $name,
|
||||
'amount' => round(floatval($item['amount'] ?? 0), 2),
|
||||
'description' => trim((string) ($item['description'] ?? '')),
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getPaymentHuifuId()
|
||||
{
|
||||
if (!class_exists('\\addons\\shopro_huifu\\library\\HuifuConfig')) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return trim((string) \addons\shopro_huifu\library\HuifuConfig::getInstance()->get('huifu_id'));
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
protected function formatReceiverOptionLabel(array $data)
|
||||
{
|
||||
$name = trim((string) ($data['receiver_name'] ?? ''));
|
||||
$account = trim((string) ($data['receiver_account'] ?? ''));
|
||||
$roleType = $data['role_type'] ?? '';
|
||||
$roleMap = [
|
||||
'platform' => '平台',
|
||||
'promoter' => '推广员',
|
||||
'regional_agent' => '区域公司',
|
||||
];
|
||||
$roleLabel = $roleMap[$roleType] ?? $roleType;
|
||||
$parts = array_filter([$roleLabel, $name ?: ('#' . ($data['role_id'] ?? 0)), $account]);
|
||||
return implode(' · ', $parts);
|
||||
}
|
||||
|
||||
protected function actionLabel($action)
|
||||
{
|
||||
$map = [
|
||||
'create' => '首次指定',
|
||||
'update' => '修改',
|
||||
'clear' => '清除',
|
||||
'retry' => '保存并重试',
|
||||
];
|
||||
return $map[$action] ?? $action;
|
||||
}
|
||||
|
||||
protected function retryResultLabel($result)
|
||||
{
|
||||
if ($result === null || $result === '') {
|
||||
return '';
|
||||
}
|
||||
$map = [
|
||||
'pending' => '重试中',
|
||||
'success' => '成功',
|
||||
'fail' => '失败',
|
||||
];
|
||||
return $map[$result] ?? $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\model\split;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class SplitManual extends Model
|
||||
{
|
||||
protected $name = 'shopro_split_manual';
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createtime';
|
||||
protected $updateTime = 'updatetime';
|
||||
|
||||
public function getItemsAttr($value)
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($value, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
public function setItemsAttr($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getOriginalItemsAttr($value)
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($value, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
public function setOriginalItemsAttr($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\model\split;
|
||||
|
||||
use think\Model;
|
||||
|
||||
class SplitManualLog extends Model
|
||||
{
|
||||
protected $name = 'shopro_split_manual_log';
|
||||
protected $autoWriteTimestamp = false;
|
||||
protected $createTime = 'createtime';
|
||||
protected $updateTime = false;
|
||||
|
||||
public function getBeforeItemsAttr($value)
|
||||
{
|
||||
return $this->decodeItems($value);
|
||||
}
|
||||
|
||||
public function setBeforeItemsAttr($value)
|
||||
{
|
||||
return $this->encodeItems($value);
|
||||
}
|
||||
|
||||
public function getAfterItemsAttr($value)
|
||||
{
|
||||
return $this->decodeItems($value);
|
||||
}
|
||||
|
||||
public function setAfterItemsAttr($value)
|
||||
{
|
||||
return $this->encodeItems($value);
|
||||
}
|
||||
|
||||
protected function decodeItems($value)
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($value, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
protected function encodeItems($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
-- 汇付运营媒介审核:商品详情补充文字说明(保留原有图文,仅在 content 前追加)
|
||||
-- 执行前请备份 fa_shopro_goods、fa_shopro_richtext、fa_shopro_config
|
||||
-- 执行后:清除 Shopro 缓存 + 小程序重新发布
|
||||
|
||||
SET @now = UNIX_TIMESTAMP();
|
||||
|
||||
-- ========== 1. 退换货政策富文本(设置页可查看) ==========
|
||||
INSERT INTO `fa_shopro_richtext` (`title`, `content`, `createtime`, `updatetime`, `deletetime`)
|
||||
SELECT
|
||||
'退换货政策',
|
||||
'<p><strong>适用范围</strong></p><p>本政策适用于「雄戈小酒」微信小程序商城购买的实体商品(酒类)。</p><p><strong>签收与验货</strong></p><p>请您在签收时当面验货。如外包装严重破损、漏液或数量不符,请当场拍照/录像并拒收或立即联系客服,我们将协助处理。</p><p><strong>退换货条件</strong></p><p>1. 未开封、不影响二次销售,且在签收后 7 日内可申请退货(具体以订单状态及商品属性为准);</p><p>2. 因运输造成的破损、漏液,请保留完整外包装及物流面单,联系客服办理补发或退款;</p><p>3. 定制类、已开封或已饮用的酒类商品,除质量问题外不支持无理由退换。</p><p><strong>售后申请路径(小程序)</strong></p><p>① 底部「我的」→ 订单卡片「售后」→ 售后列表;</p><p>② 「我的」→「我的订单」→ 进入订单详情 →「申请售后」;</p><p>③ 「我的」→「设置」→「退换货政策」(本页)。</p><p><strong>处理时效</strong></p><p>客服会在 1 个工作日内响应;审核通过后,退款按原支付渠道退回,到账时间以支付机构为准。</p><p><strong>客服</strong></p><p>商品页「客服」按钮或「我的」联系在线客服,服务时间 9:00–21:00。</p>',
|
||||
@now,
|
||||
@now,
|
||||
NULL
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `fa_shopro_richtext`
|
||||
WHERE `title` = '退换货政策' AND `deletetime` IS NULL
|
||||
);
|
||||
|
||||
SET @return_policy_id = (
|
||||
SELECT `id` FROM `fa_shopro_richtext`
|
||||
WHERE `title` = '退换货政策' AND `deletetime` IS NULL
|
||||
ORDER BY `id` DESC LIMIT 1
|
||||
);
|
||||
|
||||
UPDATE `fa_shopro_config`
|
||||
SET `value` = JSON_SET(CAST(`value` AS JSON), '$.return_policy', CAST(@return_policy_id AS UNSIGNED))
|
||||
WHERE `name` = 'shopro' AND `group` = 'basic' AND JSON_VALID(`value`) = 1 AND @return_policy_id IS NOT NULL;
|
||||
|
||||
-- ========== 2. 详情顶部公共说明(不含重复标记) ==========
|
||||
SET @intro_tail = '<p><strong>发货说明</strong></p><p>下单后 48 小时内安排发货(节假日顺延),默认快递配送。签收前请验货,如有破损请保留外包装并联系客服。</p><p><strong>退换货与售后查看路径</strong></p><p>① 小程序底部「我的」→ 订单区「售后」;② 「我的」→「我的订单」→ 订单详情 →「申请售后」;③ 「我的」→「设置」→「退换货政策」查看全文。</p><p><strong>客服咨询</strong></p><p>商品页点击「客服」,或在「我的」联系在线客服(9:00–21:00)。</p><p><strong>温馨提示</strong></p><p>饮酒有害健康,请勿酒后驾驶;未成年人请勿购买及饮酒。</p><p>——以下为商品原有图文详情——</p>';
|
||||
|
||||
-- ========== 3. 196 雄戈小酒 礼盒装 ==========
|
||||
UPDATE `fa_shopro_goods`
|
||||
SET `content` = CONCAT(
|
||||
'<!-- huifu-goods-intro -->',
|
||||
'<p><strong>商品介绍</strong></p><p>「雄戈小酒」礼盒装,贵州雄戈酒业有限公司出品,适合商务馈赠、亲友聚会与节日送礼。礼盒内含多瓶小规格酱香型白酒,包装精致,便于携带与陈列。</p>',
|
||||
'<p><strong>产品规格</strong></p><p>净含量:120ml×5;酒精度:49%vol;香型:酱香型白酒;产地:贵州省。</p>',
|
||||
'<p><strong>饮用建议</strong></p><p>开瓶后请尽快饮用;常温避光保存。建议小杯慢饮,搭配合适餐食,切勿过量饮酒。</p>',
|
||||
@intro_tail,
|
||||
IFNULL(`content`, '')
|
||||
),
|
||||
`updatetime` = @now
|
||||
WHERE `id` = 196
|
||||
AND `deletetime` IS NULL
|
||||
AND (`content` IS NULL OR `content` NOT LIKE '%huifu-goods-intro%');
|
||||
|
||||
-- ========== 4. 199 雄戈小酒 商务宴请装 ==========
|
||||
UPDATE `fa_shopro_goods`
|
||||
SET `content` = CONCAT(
|
||||
'<!-- huifu-goods-intro -->',
|
||||
'<p><strong>商品介绍</strong></p><p>「雄戈小酒」商务宴请装,面向聚会、宴请、团队答谢等场景。手提礼袋包装,单份数量充足,方便席面分饮与携带。</p>',
|
||||
'<p><strong>产品规格</strong></p><p>净含量:120ml×10;酒精度:49%vol;香型:酱香型白酒;产地:贵州省。</p>',
|
||||
'<p><strong>饮用建议</strong></p><p>开瓶后请尽快饮用;常温避光保存。理性饮酒,请勿酒后驾车。</p>',
|
||||
@intro_tail,
|
||||
IFNULL(`content`, '')
|
||||
),
|
||||
`updatetime` = @now
|
||||
WHERE `id` = 199
|
||||
AND `deletetime` IS NULL
|
||||
AND (`content` IS NULL OR `content` NOT LIKE '%huifu-goods-intro%');
|
||||
|
||||
-- ========== 5. 201 销售礼盒装(原 content 为空,仅追加文字,不插入新图片) ==========
|
||||
UPDATE `fa_shopro_goods`
|
||||
SET `content` = CONCAT(
|
||||
'<!-- huifu-goods-intro -->',
|
||||
'<p><strong>商品介绍</strong></p><p>「雄戈小酒」销售礼盒装,以自然之味、养自然之身为理念,选用优质酱香型白酒小瓶组合,适合渠道团购、企业福利与高端礼赠。详情页下方轮播图可查看礼盒外观与实物展示。</p>',
|
||||
'<p><strong>产品说明</strong></p><p>具体净含量、酒精度及礼盒配置以页面标题、规格及客服说明为准;下单前如有疑问请联系在线客服确认。</p>',
|
||||
'<p><strong>适用场景</strong></p><p>商务拜访、节日送礼、客户答谢、品鉴分享等。</p>',
|
||||
@intro_tail,
|
||||
IFNULL(`content`, '')
|
||||
),
|
||||
`updatetime` = @now
|
||||
WHERE `id` = 201
|
||||
AND `deletetime` IS NULL
|
||||
AND (`content` IS NULL OR `content` NOT LIKE '%huifu-goods-intro%');
|
||||
|
||||
-- 可选:197/198 若仍展示可取消注释
|
||||
-- UPDATE `fa_shopro_goods` SET `content` = CONCAT('<!-- huifu-goods-intro -->', ... , @intro_tail, IFNULL(`content`, '')) WHERE `id` IN (197, 198) AND ...
|
||||
@@ -0,0 +1,37 @@
|
||||
-- 失败分账手动指定接收方
|
||||
-- 执行前请将 fa_ 替换为实际表前缀
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `fa_shopro_split_manual` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`order_id` int(11) NOT NULL COMMENT '订单ID',
|
||||
`items` text NOT NULL COMMENT '当前手动明细JSON',
|
||||
`original_items` text COMMENT '首次改人时自动计算快照JSON',
|
||||
`admin_id` int(11) NOT NULL DEFAULT 0 COMMENT '最后操作管理员',
|
||||
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||
`createtime` int(10) NOT NULL,
|
||||
`updatetime` int(10) NOT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE KEY `order_id` (`order_id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分账手动覆盖(当前值)';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `fa_shopro_split_manual_log` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`order_id` int(11) NOT NULL COMMENT '订单ID',
|
||||
`order_sn` varchar(50) NOT NULL DEFAULT '' COMMENT '订单号',
|
||||
`action` enum('create','update','clear','retry') NOT NULL DEFAULT 'update' COMMENT '操作类型',
|
||||
`before_items` text COMMENT '变更前明细JSON',
|
||||
`after_items` text COMMENT '变更后明细JSON',
|
||||
`admin_id` int(11) NOT NULL DEFAULT 0,
|
||||
`admin_name` varchar(64) DEFAULT NULL COMMENT '操作人姓名',
|
||||
`remark` varchar(255) DEFAULT NULL,
|
||||
`retry_split_order_id` int(11) DEFAULT NULL COMMENT '关联分账单ID',
|
||||
`retry_result` enum('pending','success','fail') DEFAULT NULL COMMENT '重试结果',
|
||||
`retry_message` text COMMENT '重试失败原因',
|
||||
`createtime` int(10) NOT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `order_id` (`order_id`) USING BTREE,
|
||||
KEY `createtime` (`createtime`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分账手动覆盖变更历史';
|
||||
|
||||
ALTER TABLE `fa_shopro_split_order`
|
||||
ADD COLUMN `split_source` varchar(16) NOT NULL DEFAULT 'auto' COMMENT 'auto=自动计算,manual=手动指定' AFTER `status`;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 分账记录:手动指定接收方权限节点
|
||||
SET @now = UNIX_TIMESTAMP();
|
||||
SET @ord = (SELECT id FROM `fa_auth_rule` WHERE `name` = 'shopro/split/order' LIMIT 1);
|
||||
|
||||
INSERT IGNORE INTO `fa_auth_rule` (`type`,`pid`,`name`,`title`,`icon`,`url`,`condition`,`remark`,`ismenu`,`extend`,`createtime`,`updatetime`,`weigh`,`status`) VALUES
|
||||
('file', @ord, 'shopro/split/order/manualReceivers', '指定接收方-加载', 'fa fa-circle-o', '', '', '', 0, '', @now, @now, 0, 'normal'),
|
||||
('file', @ord, 'shopro/split/order/saveManual', '指定接收方-保存', 'fa fa-circle-o', '', '', '', 0, '', @now, @now, 0, 'normal'),
|
||||
('file', @ord, 'shopro/split/order/saveManualAndRetry', '指定接收方-保存并重试', 'fa fa-circle-o', '', '', '', 0, '', @now, @now, 0, 'normal'),
|
||||
('file', @ord, 'shopro/split/order/clearManual', '指定接收方-清除', 'fa fa-circle-o', '', '', '', 0, '', @now, @now, 0, 'normal'),
|
||||
('file', @ord, 'shopro/split/order/manualHistory', '指定接收方-变更历史', 'fa fa-circle-o', '', '', '', 0, '', @now, @now, 0, 'normal');
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace addons\shopro_huifu\library;
|
||||
|
||||
use addons\shopro\library\split\SplitCalculator;
|
||||
use addons\shopro\library\split\SplitManualService;
|
||||
use addons\shopro\library\split\SplitOrderHelper;
|
||||
use addons\shopro\library\split\Config as SplitConfig;
|
||||
use addons\shopro\model\Order;
|
||||
@@ -34,6 +35,9 @@ class HuifuSplitService
|
||||
}
|
||||
SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->where('status', 'in', [1, 4])->delete();
|
||||
|
||||
$manualService = new SplitManualService();
|
||||
$isManual = $manualService->hasOverride($orderId);
|
||||
|
||||
$calculator = new SplitCalculator();
|
||||
$items = $calculator->calculate($orderId);
|
||||
if (empty($items)) {
|
||||
@@ -44,6 +48,7 @@ class HuifuSplitService
|
||||
}
|
||||
$msg = $calculator->formatEmptySplitFailureMessage($orderId);
|
||||
$this->markFailed($orderId, 'HFPS' . date('YmdHis') . $orderId, $order, [], $msg);
|
||||
$manualService->finishPendingRetryLog($orderId, false, $msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -71,6 +76,7 @@ class HuifuSplitService
|
||||
$blockReason = $this->getDelayTransBlockReason($order['order_sn'], $orderId);
|
||||
if ($blockReason !== null) {
|
||||
$this->markFailed($orderId, $outOrderNo, $order, $items, $blockReason);
|
||||
$manualService->finishPendingRetryLog($orderId, false, $blockReason);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -85,6 +91,7 @@ class HuifuSplitService
|
||||
'total_amount' => $order['pay_fee'],
|
||||
'split_amount' => $splitAmount,
|
||||
'status' => 1,
|
||||
'split_source' => $isManual ? 'manual' : 'auto',
|
||||
]);
|
||||
|
||||
foreach ($items as $item) {
|
||||
@@ -111,10 +118,12 @@ class HuifuSplitService
|
||||
'confirm_status' => 'S',
|
||||
]);
|
||||
Db::commit();
|
||||
$manualService->finishPendingRetryLog($orderId, true, '', $splitOrder->id);
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
$this->markFailed($orderId, $outOrderNo, $order, $items, $e->getMessage());
|
||||
$manualService->finishPendingRetryLog($orderId, false, $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -506,6 +515,7 @@ class HuifuSplitService
|
||||
{
|
||||
$splitOrder = SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->find();
|
||||
$splitConfig = new SplitConfig();
|
||||
$isManual = (new SplitManualService())->hasOverride($orderId);
|
||||
if (!$splitOrder) {
|
||||
$splitOrder = SplitOrder::create([
|
||||
'order_id' => $orderId,
|
||||
@@ -516,6 +526,7 @@ class HuifuSplitService
|
||||
'total_amount' => $order['pay_fee'],
|
||||
'split_amount' => array_sum(array_column($items, 'amount')),
|
||||
'status' => 4,
|
||||
'split_source' => $isManual ? 'manual' : 'auto',
|
||||
'wx_result' => $message,
|
||||
'retry_count' => 1,
|
||||
'next_retry_time'=> time() + $splitConfig->getRetryInterval(),
|
||||
|
||||
在新工单中引用
屏蔽一个用户