feat(split): 添加分账手动指定接收方功能

- 实现SplitManualService服务类,支持手动覆盖分账明细
- 新增SplitManual和SplitManualLog数据模型
- 修改SplitCalculator优先使用手动覆盖数据进行分账计算
- 添加分账记录手动指定接收方的数据库表结构
- 集成手动分账到汇付分账流程中,支持失败重试机制
- 添加商品详情内容更新SQL脚本用于汇付审核
```
这个提交包含在:
可乐加冰
2026-10-22 22:38:35 +08:00
父节点 b5142ff780
当前提交 af08af8d47
共有 11 个文件被更改,包括 1301 次插入 和 11 次删除

查看文件

@@ -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(),

查看文件

@@ -6,6 +6,7 @@ use app\admin\controller\shopro\Base;
use addons\shopro\library\split\Config as SplitConfig;
use addons\shopro\library\split\SplitCalculator;
use addons\shopro\library\split\SplitExecutor;
use addons\shopro\library\split\SplitManualService;
use addons\shopro\library\split\ProfitSharingService;
use app\admin\model\shopro\order\Order as ShopOrder;
use app\admin\model\shopro\user\User as ShopUser;
@@ -183,12 +184,18 @@ class Order extends Base
$data['fail_reason_full'] = $this->enrichFailReasonWithDiagnose($data['fail_reason_full'], $status, $diag);
$data['fail_reason'] = $this->summarizeFailReason($data['fail_reason_full'], $diag, $status);
$data['split_source'] = $diag['final_source'];
$data['split_source_text'] = $this->formatSplitSourceText($diag);
$data['calc_source'] = $diag['final_source'];
$data['calc_source_text'] = $this->formatSplitSourceText($diag);
$data['split_diagnose'] = $diag['issues'];
$data['commission_total'] = $diag['commission_total'];
$data['rule_total'] = $diag['rule_total'];
$orderSplitSource = $data['split_source'] ?? 'auto';
$data['split_source'] = $orderSplitSource;
$data['split_source_text'] = $orderSplitSource === 'manual' ? '手动指定' : $data['calc_source_text'];
$data['has_manual_override'] = (new SplitManualService())->hasOverride($orderId);
$data['can_edit_manual'] = ($status === 4 && $channel === 'huifu');
unset($data['order'], $data['receivers']);
$rows[] = $data;
}
@@ -468,6 +475,143 @@ class Order extends Base
return $map[$type] ?? ($type ?: '接收方');
}
/**
* 加载指定接收方弹窗数据
*/
public function manualReceivers()
{
$splitOrderId = intval($this->request->get('split_order_id', 0));
if ($splitOrderId <= 0) {
$this->error('参数错误');
}
try {
$context = (new SplitManualService())->getManualContext($splitOrderId);
} catch (\Throwable $e) {
$this->error($e->getMessage());
}
$this->success('操作成功', null, $context);
}
/**
* 保存手动覆盖(不重试)
*/
public function saveManual()
{
$splitOrderId = intval($this->request->post('split_order_id', 0));
$items = $this->request->post('items/a', []);
$remark = trim((string) $this->request->post('remark', ''));
$row = $this->model->get($splitOrderId);
if (!$row) {
$this->error('分账记录不存在');
}
try {
(new SplitManualService())->save(
intval($row['order_id']),
$items,
intval($this->auth->id),
(string) ($this->auth->nickname ?? $this->auth->username ?? ''),
$remark
);
} catch (\Throwable $e) {
$this->error($e->getMessage());
}
$this->success('保存成功');
}
/**
* 保存手动覆盖并重新分账
*/
public function saveManualAndRetry()
{
$splitOrderId = intval($this->request->post('split_order_id', 0));
$items = $this->request->post('items/a', []);
$remark = trim((string) $this->request->post('remark', ''));
$row = $this->model->get($splitOrderId);
if (!$row) {
$this->error('分账记录不存在');
}
$orderId = intval($row['order_id']);
$adminId = intval($this->auth->id);
$adminName = (string) ($this->auth->nickname ?? $this->auth->username ?? '');
$service = new SplitManualService();
try {
$service->save($orderId, $items, $adminId, $adminName, $remark);
$service->markPendingRetryLog($orderId, $adminId, $adminName, $remark);
} catch (\Throwable $e) {
$this->error($e->getMessage());
}
try {
if (class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService')) {
$result = (new \addons\shopro_huifu\library\HuifuSplitService())->executeByOrderId($orderId);
} else {
$result = (new SplitExecutor())->retry($splitOrderId);
}
} catch (\Throwable $e) {
$service->finishPendingRetryLog($orderId, false, $e->getMessage());
$this->error('重试失败:' . $e->getMessage());
}
if ($result) {
$this->success('保存并重试成功');
}
$hint = $this->formatSplitWxResultHint($orderId, 'huifu');
$this->error('保存成功但分账失败' . ($hint ?: ''));
}
/**
* 清除手动覆盖
*/
public function clearManual()
{
$splitOrderId = intval($this->request->post('split_order_id', 0));
$remark = trim((string) $this->request->post('remark', ''));
$row = $this->model->get($splitOrderId);
if (!$row) {
$this->error('分账记录不存在');
}
try {
(new SplitManualService())->clear(
intval($row['order_id']),
intval($this->auth->id),
(string) ($this->auth->nickname ?? $this->auth->username ?? ''),
$remark
);
} catch (\Throwable $e) {
$this->error($e->getMessage());
}
$this->success('已清除手动指定');
}
/**
* 变更历史
*/
public function manualHistory()
{
$orderId = intval($this->request->get('order_id', 0));
if ($orderId <= 0) {
$splitOrderId = intval($this->request->get('split_order_id', 0));
if ($splitOrderId > 0) {
$row = $this->model->get($splitOrderId);
$orderId = $row ? intval($row['order_id']) : 0;
}
}
if ($orderId <= 0) {
$this->error('参数错误');
}
$offset = intval($this->request->get('offset', 0));
$limit = intval($this->request->get('limit', 20));
$limit = max(1, min(100, $limit));
$result = (new SplitManualService())->getHistory($orderId, $offset, $limit);
$this->success('操作成功', null, $result);
}
public function retry($ids = null)
{
$id = $ids ?: $this->request->post('id');

查看文件

@@ -108,6 +108,18 @@
}
.source-tag.commission { background: #ecf5ff; color: #409eff; }
.source-tag.rule { background: #fdf6ec; color: #e6a23c; }
.source-tag.manual { background: #f0ebfa; color: #7438D5; }
.manual-badge {
display: inline-block;
margin-left: 4px;
padding: 0 5px;
font-size: 10px;
line-height: 16px;
border-radius: 3px;
background: #fef0f0;
color: #f56c6c;
vertical-align: middle;
}
.source-detail-btn {
flex-shrink: 0;
font-size: 11px;
@@ -187,6 +199,57 @@
.diag-pop li { margin-bottom: 6px; }
.diag-pop li:last-child { margin-bottom: 0; }
.op-hint { font-size: 12px; color: #909399; margin-top: 10px; }
.manual-dialog .section-title {
font-size: 13px;
font-weight: 600;
color: #303133;
margin: 0 0 8px;
}
.manual-dialog .section-block {
margin-bottom: 16px;
padding: 12px;
background: #fafafa;
border-radius: 6px;
}
.manual-dialog .orig-item {
font-size: 12px;
color: #606266;
line-height: 1.7;
padding: 4px 0;
border-bottom: 1px dashed #ebeef5;
}
.manual-dialog .orig-item:last-child { border-bottom: none; }
.manual-dialog .sum-row {
margin-top: 10px;
font-size: 13px;
text-align: right;
color: #303133;
}
.manual-dialog .sum-row strong { color: #e1212b; }
.history-drawer .history-item {
padding: 12px 0;
border-bottom: 1px solid #ebeef5;
}
.history-drawer .history-item:last-child { border-bottom: none; }
.history-drawer .history-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.history-drawer .history-action {
font-weight: 600;
color: #303133;
font-size: 13px;
}
.history-drawer .history-time { font-size: 12px; color: #909399; }
.history-drawer .history-meta { font-size: 12px; color: #606266; margin-bottom: 6px; }
.history-drawer .items-diff { font-size: 12px; line-height: 1.65; color: #606266; }
.history-drawer .items-diff .arrow { color: #909399; margin: 0 4px; }
.history-drawer .retry-result { margin-top: 6px; font-size: 12px; }
.history-drawer .retry-result.success { color: #67c23a; }
.history-drawer .retry-result.fail { color: #f56c6c; }
.history-drawer .retry-result.pending { color: #409eff; }
</style>
<div id="splitOrderIndex" v-cloak>
<div class="page-card">
@@ -249,14 +312,15 @@
<span class="money-num highlight">¥{{ formatMoney(s.row.split_amount) }}</span>
</template>
</el-table-column>
<el-table-column label="分账来源" class-name="col-source" width="118" align="center">
<el-table-column label="分账来源" class-name="col-source" width="130" align="center">
<template slot-scope="s">
<div class="source-cell" v-if="s.row.split_source">
<span :class="'source-tag ' + (s.row.split_source === 'commission' ? 'commission' : 'rule')">
{{ s.row.split_source === 'commission' ? '按佣金' : '规则兜底' }}
<div class="source-cell" v-if="s.row.split_source === 'manual' || s.row.has_manual_override || s.row.calc_source">
<span class="source-tag manual" v-if="s.row.split_source === 'manual' || s.row.has_manual_override">手动</span>
<span v-else-if="s.row.calc_source" :class="'source-tag ' + (s.row.calc_source === 'commission' ? 'commission' : 'rule')">
{{ s.row.calc_source === 'commission' ? '按佣金' : '规则兜底' }}
</span>
<el-popover
v-if="s.row.split_source === 'rule' && s.row.split_diagnose && s.row.split_diagnose.length"
v-if="s.row.calc_source === 'rule' && s.row.split_diagnose && s.row.split_diagnose.length && s.row.split_source !== 'manual'"
placement="left"
width="300"
trigger="click">
@@ -310,8 +374,10 @@
<span v-else style="color:#909399;">—</span>
</template>
</el-table-column>
<el-table-column label="操作" width="128" fixed="right">
<el-table-column label="操作" width="200" fixed="right">
<template slot-scope="s">
<el-button type="text" size="small" v-if="s.row.can_edit_manual" @click="openManualDialog(s.row)">指定接收方</el-button>
<el-button type="text" size="small" v-if="s.row.can_edit_manual && (s.row.has_manual_override || s.row.split_source === 'manual')" @click="openHistoryDrawer(s.row)">变更历史</el-button>
<el-button type="text" size="small" v-if="s.row.status == 4" @click="retry(s.row)">重试</el-button>
<el-button type="text" size="small" @click="query(s.row.id)">同步</el-button>
</template>
@@ -329,8 +395,98 @@
@size-change="onSizeChange">
</el-pagination>
</div>
<p class="op-hint">「重试」仅对失败记录向通道重新发起;「同步」只更新状态,不会重复扣款。</p>
<p class="op-hint">失败汇付单可「指定接收方」改人改金额后重试;「同步」只更新状态,不会重复扣款。</p>
</div>
<!-- 指定接收方弹窗 -->
<el-dialog title="指定接收方" :visible.sync="manualDialogVisible" width="720px" :close-on-click-modal="false" custom-class="manual-dialog">
<div v-loading="manualLoading">
<p style="font-size:13px;color:#606266;margin:0 0 12px;">
订单 <strong>{{ manualContext.order_sn }}</strong>
· 确认基数 ¥{{ formatMoney(manualContext.confirm_base) }}
· 上限 ¥{{ formatMoney(manualContext.max_split_amount) }}
</p>
<div class="section-block" v-if="manualContext.original_items && manualContext.original_items.length">
<p class="section-title">原自动计算明细</p>
<div class="orig-item" v-for="(it, idx) in manualContext.original_items" :key="'orig'+idx">
{{ formatManualItemLine(it) }}
</div>
</div>
<p class="section-title">手动指定明细</p>
<el-table :data="manualForm.items" border size="small" style="width:100%;margin-bottom:10px;">
<el-table-column label="接收方" min-width="220">
<template slot-scope="s">
<el-select v-model="s.row.receiver_id" filterable placeholder="选择汇付接收方" size="small" style="width:100%;" @change="onReceiverChange(s.row)">
<el-option v-for="opt in manualContext.receiver_options" :key="opt.id" :label="opt.label" :value="opt.id"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="角色" width="100">
<template slot-scope="s">
<el-select v-model="s.row.role_type" size="small" style="width:100%;">
<el-option label="推广员" value="promoter"></el-option>
<el-option label="区域公司" value="regional_agent"></el-option>
<el-option label="平台" value="platform"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column label="金额" width="120">
<template slot-scope="s">
<el-input-number v-model="s.row.amount" :min="0.01" :precision="2" :step="0.01" size="small" controls-position="right" style="width:100%;"></el-input-number>
</template>
</el-table-column>
<el-table-column label="说明" width="120">
<template slot-scope="s">
<el-input v-model="s.row.description" size="small" placeholder="备注"></el-input>
</template>
</el-table-column>
<el-table-column label="" width="56" align="center">
<template slot-scope="s">
<el-button type="text" size="small" style="color:#f56c6c;" @click="removeManualRow(s.$index)">删</el-button>
</template>
</el-table-column>
</el-table>
<el-button size="small" @click="addManualRow">+ 添加一行</el-button>
<div class="sum-row">合计 <strong>¥{{ formatMoney(manualItemsTotal) }}</strong></div>
<el-input v-model="manualForm.remark" type="textarea" :rows="2" placeholder="操作备注(可选)" style="margin-top:12px;"></el-input>
</div>
<span slot="footer">
<el-button size="small" @click="manualDialogVisible = false">取消</el-button>
<el-button size="small" v-if="manualContext.has_manual" @click="clearManualOverride">清除手动指定</el-button>
<el-button size="small" type="primary" plain :loading="manualSaving" @click="saveManual(false)">仅保存</el-button>
<el-button size="small" type="primary" :loading="manualSaving" @click="saveManual(true)">保存并重试</el-button>
</span>
</el-dialog>
<!-- 变更历史抽屉 -->
<el-drawer title="变更历史" :visible.sync="historyDrawerVisible" size="480px" custom-class="history-drawer">
<div v-loading="historyLoading" style="padding:0 20px 20px;">
<div v-if="!historyList.length && !historyLoading" style="color:#909399;font-size:13px;text-align:center;padding:40px 0;">暂无变更记录</div>
<div class="history-item" v-for="h in historyList" :key="h.id">
<div class="history-head">
<span class="history-action">{{ h.action_text }}</span>
<span class="history-time">{{ h.createtime_text }}</span>
</div>
<div class="history-meta" v-if="h.admin_name">操作人:{{ h.admin_name }}</div>
<div class="history-meta" v-if="h.remark">备注:{{ h.remark }}</div>
<div class="items-diff">
<div v-if="h.before_items && h.before_items.length">
<div v-for="(it, i) in h.before_items" :key="'b'+i">{{ formatManualItemLine(it) }}</div>
</div>
<span v-else style="color:#c0c4cc;">(空)</span>
<span class="arrow">→</span>
<div v-if="h.after_items && h.after_items.length">
<div v-for="(it, i) in h.after_items" :key="'a'+i">{{ formatManualItemLine(it) }}</div>
</div>
<span v-else style="color:#c0c4cc;">(空)</span>
</div>
<div v-if="h.retry_result" :class="'retry-result ' + h.retry_result">
重试结果:{{ h.retry_result_text }}
<span v-if="h.retry_message"> — {{ h.retry_message }}</span>
</div>
</div>
</div>
</el-drawer>
</div>
<script src="__CDN__/assets/addons/shopro/libs/vue.js"></script>
<script src="__CDN__/assets/addons/shopro/libs/element/element.js"></script>

查看文件

@@ -18,9 +18,38 @@ define(['jquery', 'bootstrap', 'backend', 'form'], function ($, undefined, Backe
status: '',
channel: ''
},
splitHuifuOnly: !!(typeof Config !== 'undefined' && Config.splitHuifuOnly)
splitHuifuOnly: !!(typeof Config !== 'undefined' && Config.splitHuifuOnly),
manualDialogVisible: false,
manualLoading: false,
manualSaving: false,
manualRow: null,
manualContext: {
order_sn: '',
confirm_base: 0,
max_split_amount: 0,
has_manual: false,
original_items: [],
receiver_options: []
},
manualForm: {
items: [],
remark: ''
},
historyDrawerVisible: false,
historyLoading: false,
historyList: [],
historyRow: null
};
},
computed: {
manualItemsTotal() {
var sum = 0;
(this.manualForm.items || []).forEach(function (it) {
sum += parseFloat(it.amount) || 0;
});
return sum;
}
},
mounted() {
this.getList();
},
@@ -71,6 +100,18 @@ define(['jquery', 'bootstrap', 'backend', 'form'], function ($, undefined, Backe
var n = parseFloat(v);
return isNaN(n) ? '0.00' : n.toFixed(2);
},
roleTypeLabel(type) {
var map = { platform: '平台', promoter: '推广员', regional_agent: '区域公司' };
return map[type] || type || '接收方';
},
formatManualItemLine(it) {
if (!it) return '—';
var name = it.receiver_name || ('#' + (it.role_id || ''));
var account = it.receiver_account || '';
var amt = this.formatMoney(it.amount);
var role = this.roleTypeLabel(it.role_type);
return role + ' ' + name + (account ? ' (' + account + ')' : '') + ' ¥' + amt;
},
scrollCellWheel(e) {
var el = e.currentTarget;
if (!el || el.scrollWidth <= el.clientWidth) {
@@ -85,6 +126,155 @@ define(['jquery', 'bootstrap', 'backend', 'form'], function ($, undefined, Backe
if (!orderId) return;
Fast.api.open('shopro/order/order/detail?id=' + orderId, '订单详情');
},
openManualDialog(row) {
this.manualRow = row;
this.manualDialogVisible = true;
this.manualLoading = true;
this.manualForm.remark = '';
Fast.api.ajax({
url: 'shopro/split/order/manualReceivers',
type: 'GET',
data: { split_order_id: row.id }
}, (ret, res) => {
var ctx = (res && res.data) ? res.data : {};
this.manualContext = ctx;
this.manualForm.items = this.cloneManualItems(ctx.current_items || []);
if (!this.manualForm.items.length) {
this.addManualRow();
}
this.manualLoading = false;
return false;
}, () => {
this.manualLoading = false;
this.manualDialogVisible = false;
});
},
cloneManualItems(items) {
return (items || []).map(function (it) {
return {
receiver_id: it.receiver_id || 0,
role_type: it.role_type || 'promoter',
amount: parseFloat(it.amount) || 0,
description: it.description || '手动指定'
};
});
},
addManualRow() {
this.manualForm.items.push({
receiver_id: 0,
role_type: 'promoter',
amount: 0,
description: '手动指定'
});
},
removeManualRow(index) {
this.manualForm.items.splice(index, 1);
},
onReceiverChange(row) {
var opt = (this.manualContext.receiver_options || []).find(function (o) {
return o.id === row.receiver_id;
});
if (opt) {
row.role_type = opt.role_type || row.role_type;
}
},
validateManualForm() {
var items = this.manualForm.items || [];
if (!items.length) {
Layer.msg('请至少添加一行分账明细');
return false;
}
for (var i = 0; i < items.length; i++) {
if (!items[i].receiver_id) {
Layer.msg('第 ' + (i + 1) + ' 行请选择接收方');
return false;
}
if (!items[i].amount || parseFloat(items[i].amount) <= 0) {
Layer.msg('第 ' + (i + 1) + ' 行金额须大于 0');
return false;
}
}
var max = parseFloat(this.manualContext.max_split_amount) || 0;
if (max > 0 && this.manualItemsTotal > max + 0.001) {
Layer.msg('合计超过上限 ¥' + this.formatMoney(max));
return false;
}
var base = parseFloat(this.manualContext.confirm_base) || 0;
if (base > 0 && this.manualItemsTotal > base + 0.001) {
Layer.msg('合计超过确认基数 ¥' + this.formatMoney(base));
return false;
}
return true;
},
saveManual(andRetry) {
if (!this.manualRow || !this.validateManualForm()) {
return;
}
var url = andRetry
? 'shopro/split/order/saveManualAndRetry'
: 'shopro/split/order/saveManual';
var tip = andRetry
? '保存后将向汇付重新发起分账,是否继续?'
: '仅保存覆盖配置,不会立即重试,是否继续?';
var self = this;
Layer.confirm(tip, { title: '确认', btn: ['确定', '取消'] }, function () {
self.manualSaving = true;
Fast.api.ajax({
url: url,
type: 'POST',
data: {
split_order_id: self.manualRow.id,
items: self.manualForm.items,
remark: self.manualForm.remark
}
}, () => {
self.manualSaving = false;
self.manualDialogVisible = false;
self.getList();
}, () => {
self.manualSaving = false;
});
});
},
clearManualOverride() {
if (!this.manualRow) return;
var self = this;
Layer.confirm('清除后将恢复自动计算明细,是否继续?', { title: '清除手动指定', btn: ['清除', '取消'] }, function () {
self.manualSaving = true;
Fast.api.ajax({
url: 'shopro/split/order/clearManual',
type: 'POST',
data: {
split_order_id: self.manualRow.id,
remark: self.manualForm.remark
}
}, () => {
self.manualSaving = false;
self.manualDialogVisible = false;
self.getList();
}, () => {
self.manualSaving = false;
});
});
},
openHistoryDrawer(row) {
this.historyRow = row;
this.historyDrawerVisible = true;
this.historyLoading = true;
this.historyList = [];
Fast.api.ajax({
url: 'shopro/split/order/manualHistory',
type: 'GET',
data: { order_id: row.order_id, limit: 50 }
}, (ret, res) => {
var data = (res && res.data) ? res.data : {};
this.historyList = data.rows || [];
this.historyLoading = false;
return false;
}, () => {
this.historyLoading = false;
});
},
retry(row) {
var tip = this.splitHuifuOnly
? '将向汇付重新发起该笔分账。若订单资金仍被冻结,请先发货/确认收货或按文档处理 FREEZE。'