fix(资金流水): 读取侧只展示现金渠道,交易类型下拉改由后端下发

- CapitalFlow 新增 searchCashOnlyAttr 搜索器:列表、导出、账单统计统一
  whereNotIn('pay_type', ['', 'yue', 'admin']),历史遗留的站内结算流水不再展示,
  与写入侧 PayServices::isCashChannel() 口径对齐。
- CapitalFlowServices::getList()/getFlowRecord() 注入 cash_only,trading_type
  文案改为按值映射(不再依赖数组下标,历史下线类型回退「其他」)。
- 新增 type_options(全部/订单支付/订单退款/购买会员/线下收银)供前端下拉,
  同时保留 status 全量文案兼容旧前端。
- 后台资金流水页:筛选改用 trading_type + typeOptions,支付方式直接取
  pay_type_name,移除本地硬编码的支付方式表。
- 同步 .crmeb 售后文档的资金流水读取侧口径说明。
This commit is contained in:
吴昊天 2026-10-24 11:15:36 +08:00
parent 90b1877806
commit aa82159d43
4 changed files with 42 additions and 33 deletions

View File

@ -72,7 +72,7 @@ AfterSaleServices::refund
- Handler 中的资金/资源副作用一律经 `RefundEffectContextInterface::once()` 登记去重,禁止直接裸写。
- 中台 `refund_status`(0 无 / 1 申请中 / 2 已退)由 Adapter `resolveRefundStatus` 推导,退款完成时 `syncStatus` 同步。
**资金流水(`eb_capital_flow`)只记真实资金进出**:判定统一入口为 `PayServices::isCashChannel()`(第三方在线渠道 + 线下支付;余额 `yue`、后台代付、0 元单一律 false)。`CapitalFlowServices::setFlow()` 内部已做拦截,退款链路在调用前还要排除 `refund_type=balance` / `refund_method=balance` 的站内结转,即「余额支付订单退款」「原路退款但改退余额」都不产生现金流出,不记流水。
**资金流水(`eb_capital_flow`)只记真实资金进出**:判定统一入口为 `PayServices::isCashChannel()`(第三方在线渠道 + 线下支付;余额 `yue`、后台代付、0 元单一律 false)。`CapitalFlowServices::setFlow()` 内部已做拦截,退款链路在调用前还要排除 `refund_type=balance` / `refund_method=balance` 的站内结转,即「余额支付订单退款」「原路退款但改退余额」都不产生现金流出,不记流水。读取侧口径一致:`CapitalFlow::searchCashOnlyAttr` 在列表、导出、账单统计中统一排除 `pay_type` 为 `yue` / `admin` / 空的历史流水,保证后台资金流水页面只出现现金渠道记录。
> **现状说明**:`OrderRefundCreateAfter / OrderRefundRefuse / OrderRefundCancelAfter` 的模块监听器已注册但服务层触发点待接线(见 [订单生命周期·事件地图](order-lifecycle.md#三订单事件地图)),相关后置逻辑目前挂在 `AfterSaleServices` 内部回调(`lifecycle->afterApply/afterAudit`)。

View File

@ -59,4 +59,18 @@ class CapitalFlow extends BaseModel
{
if (!empty($value)) $query->whereIn('id', $value);
}
/**
* 仅现金渠道搜索器
* 资金流水只统计真实资金进出:余额支付(yue)、后台代付(admin)、0 元单(空) 属于站内结算,
* 即使历史数据中存在也不在资金流水、账单中展示。
* @param $query
* @param $value
*/
public function searchCashOnlyAttr($query, $value)
{
if ($value) {
$query->whereNotIn('pay_type', ['', 'yue', 'admin']);
}
}
}

View File

@ -128,12 +128,25 @@ class CapitalFlowServices extends BaseService
{
$export = $where['export'] ?? 0;
unset($where['export']);
// 资金流水只展示现金渠道:余额支付、后台代付、0 元单等站内结算即使有历史数据也不展示
$where['cash_only'] = 1;
[$page, $limit] = $this->getPageValue();
$status = ['全部', '支付订单', '订单退款', '充值订单', '充值退款', '抽奖红包', '佣金提现', '购买会员', '线下收银'];
// 全量类型文案(用于列表/导出展示,历史数据可能包含已下线的类型)
$typeLabels = [0 => '全部', 1 => '订单支付', 2 => '订单退款', 3 => '充值订单', 4 => '充值退款', 5 => '抽奖红包', 6 => '佣金提现', 7 => '购买会员', 8 => '线下收银'];
// 筛选下拉只保留资金流水页面实际使用的现金交易类型;value 与 trading_type 一一对应,不依赖数组下标
$type_options = [
['label' => '全部', 'value' => 0],
['label' => '订单支付', 'value' => 1],
['label' => '订单退款', 'value' => 2],
['label' => '购买会员', 'value' => 7],
['label' => '线下收银', 'value' => 8],
];
// 兼容旧前端:status 为下标即 trading_type 的全量类型列表;新前端改用 type_options
$status = ['全部', '订单支付', '订单退款', '充值订单', '充值退款', '抽奖红包', '佣金提现', '购买会员', '线下收银'];
$list = $this->dao->getList($where, $page, $limit);
foreach ($list as &$item) {
$item['add_time'] = date('Y-m-d H:i:s', $item['add_time']);
$item['trading_type'] = $status[$item['trading_type']];
$item['trading_type'] = $typeLabels[$item['trading_type']] ?? '其他';
$item['pay_type_name'] = PayServices::getPayType()[$item['pay_type'] != 'routine' ? $item['pay_type'] : 'weixin'] ?? '其他方式';
}
$count = $this->dao->count($where);
@ -143,7 +156,7 @@ class CapitalFlowServices extends BaseService
$fileName = '账单导出' . date('YmdHis') . rand(1000, 9999);
return compact('list', 'fileKey', 'header', 'fileName');
} else {
return compact('list', 'count', 'status');
return compact('list', 'count', 'status', 'type_options');
}
}
@ -170,6 +183,8 @@ class CapitalFlowServices extends BaseService
*/
public function getFlowRecord($where)
{
// 账单统计与流水列表口径保持一致,只统计现金渠道
$where['cash_only'] = 1;
[$page, $limit] = $this->getPageValue();
$data = $this->dao->getRecordList($where, $page, $limit);
$i = 1;

View File

@ -23,8 +23,8 @@
></date-range-picker>
</el-form-item>
<el-form-item label="交易类型:">
<el-select v-model="formValidate.status" @change="selChange" class="form_content_width">
<el-option :label="item" :value="index" v-for="(item, index) in withdrawal" :key="index"></el-option>
<el-select v-model="formValidate.trading_type" @change="selChange" class="form_content_width">
<el-option :label="item.label" :value="item.value" v-for="item in typeOptions" :key="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="流水搜索:">
@ -73,9 +73,7 @@
</el-table-column>
<el-table-column label="支付方式" min-width="90">
<template #default="scope">
<div v-for="item in payment" :key="item.value">
<span v-if="scope.row.pay_type == item.value"> {{ item.title }} </span>
</div>
<span>{{ scope.row.pay_type_name || '--' }}</span>
</template>
</el-table-column>
<el-table-column label="备注" min-width="90">
@ -129,29 +127,8 @@ const modals = ref(false);
const total = ref(0);
const loading = ref(false);
const tabList = ref([]);
const withdrawal = ref([]);
const payment = [
{
title: '全部',
value: '',
},
{
title: '微信',
value: 'weixin',
},
{
title: '支付宝',
value: 'alipay',
},
{
title: '银行卡',
value: 'bank',
},
{
title: '线下支付',
value: 'offline',
},
];
// 交易类型筛选:后端返回 [{label, value}],value 直接对应 trading_type
const typeOptions = ref([]);
const formValidate = ref({
trading_type: 0,
time: '',
@ -214,7 +191,10 @@ function getList() {
let data = res.data;
tabList.value = data.list;
total.value = data.count;
withdrawal.value = data.status;
typeOptions.value =
data.type_options && data.type_options.length
? data.type_options
: (data.status || []).map((label, value) => ({ label, value }));
loading.value = false;
})
.catch((res) => {