CRMEB/backend/app/services/statistic/CapitalFlowServices.php
吴昊天 51f76ec771 fix(资金流水): 只记录真实资金进出,余额结算与退回余额不再记账
- PayServices 新增 isCashChannel():仅第三方在线渠道 + 线下支付为真;
  余额支付、后台代付、空支付方式(0 元单)一律为假。isRealPaymentChannel()
  保留余额支付语义(站内有结算无资金流),内部委托 isCashChannel 判定现金渠道。
- CapitalFlowServices::setFlow() 作为唯一出口统一拦截非现金渠道与未知业务类型,
  避免各业务分支各自判断导致漏过滤;nickname/phone 缺省兼容,返回是否写入。
- 退款链路:refund_type=balance(含原路退款改退余额)属于站内结转,
  RefundExecutorServices::writeFlow() 与 RefundEffectServices 均跳过记账;
  其余退款同样走 isCashChannel 判定,余额支付订单退款不再产生流水。
- 补充 .crmeb 售后文档的资金流水口径说明。
2026-10-23 14:17:15 +08:00

198 lines
7.8 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
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\services\statistic;
use app\dao\system\statistics\CapitalFlowDao;
use crmeb\basic\BaseService;
use app\services\pay\PayServices;
use crmeb\exceptions\AdminException;
class CapitalFlowServices extends BaseService
{
/**
* @var CapitalFlowDao
*/
protected $dao;
/**
* @param CapitalFlowDao $dao
*/
public function __construct(CapitalFlowDao $dao)
{
$this->dao = $dao;
}
/**
* 添加资金流水
* 资金流水记录的是平台真实资金进出(第三方在线收款、线下收银、充值及对应退款),
* 余额支付、后台代付、0 元单等站内虚拟结算不产生现金流动,一律不记账。
* 该判定放在此处作为唯一出口,避免各业务分支各自判断导致漏过滤。
* @param array $orderInfo 业务订单信息
* @param string $type 流水类型
* @return bool 是否写入流水
*/
public function setFlow($orderInfo, $type = '')
{
$data['flow_id'] = 'ZJ' . date('Ymdhis', time()) . rand('1000', '9999');
switch ($type) {
case 'order':
$data['order_id'] = $orderInfo['order_id'];
$data['uid'] = $orderInfo['uid'];
$data['price'] = $orderInfo['pay_price'];
$data['trading_type'] = 1;
$data['pay_type'] = $orderInfo['pay_type'];
break;
case 'refund':
$data['order_id'] = $orderInfo['order_id'];
$data['uid'] = $orderInfo['uid'];
$data['price'] = bcmul('-1', $orderInfo['refund_price'], 2);
$data['trading_type'] = 2;
$data['pay_type'] = $orderInfo['pay_type'];
break;
case 'recharge':
$data['order_id'] = $orderInfo['order_id'];
$data['uid'] = $orderInfo['uid'];
$data['price'] = $orderInfo['price'];
$data['trading_type'] = 3;
$data['pay_type'] = $orderInfo['recharge_type'];
break;
case 'refund_recharge':
$data['order_id'] = $orderInfo['order_id'];
$data['uid'] = $orderInfo['uid'];
$data['price'] = bcmul('-1', $orderInfo['price'], 2);
$data['trading_type'] = 4;
$data['pay_type'] = $orderInfo['recharge_type'];
break;
case 'luck':
$data['order_id'] = $orderInfo['order_id'];
$data['uid'] = $orderInfo['uid'];
$data['price'] = $orderInfo['price'];
$data['trading_type'] = 5;
$data['pay_type'] = $orderInfo['pay_type'];
break;
case 'extract':
$data['order_id'] = $orderInfo['order_id'];
$data['uid'] = $orderInfo['uid'];
$data['price'] = $orderInfo['price'];
$data['trading_type'] = 6;
$data['pay_type'] = $orderInfo['pay_type'];
break;
case 'pay_member':
$data['order_id'] = $orderInfo['order_id'];
$data['uid'] = $orderInfo['uid'];
$data['price'] = $orderInfo['pay_price'];
$data['trading_type'] = 7;
$data['pay_type'] = $orderInfo['pay_type'];
break;
case 'offline_scan':
$data['order_id'] = $orderInfo['order_id'];
$data['uid'] = $orderInfo['uid'];
$data['price'] = $orderInfo['pay_price'];
$data['trading_type'] = 8;
$data['pay_type'] = $orderInfo['pay_type'];
break;
default:
// 未知业务类型不记账,避免写入无金额无单号的空流水
return false;
}
// 非现金渠道(余额支付/后台代付/0 元单等)无真实资金流,不记流水
/** @var PayServices $payServices */
$payServices = app()->make(PayServices::class);
if (!$payServices->isCashChannel((string)($data['pay_type'] ?? ''))) {
return false;
}
$data['nickname'] = $orderInfo['nickname'] ?? '';
$data['phone'] = $orderInfo['phone'] ?? '';
$data['add_time'] = time();
return (bool)$this->dao->save($data);
}
/**
* 获取资金流水
* @param $where
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getFlowList($where)
{
$export = $where['export'] ?? 0;
unset($where['export']);
[$page, $limit] = $this->getPageValue();
$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['pay_type_name'] = PayServices::getPayType()[$item['pay_type'] != 'routine' ? $item['pay_type'] : 'weixin'] ?? '其他方式';
}
$count = $this->dao->count($where);
if ($export) {
$fileKey = ['flow_id', 'order_id', 'nickname', 'phone', 'price', 'trading_type', 'pay_type_name', 'add_time', 'mark'];
$header = ['交易单号', '关联订单', '用户', '电话', '金额', '订单类型', '支付类型', '交易时间', '备注'];
$fileName = '账单导出' . date('YmdHis') . rand(1000, 9999);
return compact('list', 'fileKey', 'header', 'fileName');
} else {
return compact('list', 'count', 'status');
}
}
/**
* 添加备注
* @param $id
* @param $data
* @return bool
*/
public function setMark($id, $data)
{
$res = $this->dao->update($id, $data);
if ($res) {
return true;
} else {
throw new AdminException('备注失败');
}
}
/**
* 获取账单记录
* @param $where
* @return array
*/
public function getFlowRecord($where)
{
[$page, $limit] = $this->getPageValue();
$data = $this->dao->getRecordList($where, $page, $limit);
$i = 1;
foreach ($data['list'] as &$item) {
$item['id'] = $i;
$i++;
$item['entry_price'] = bcadd($item['income_price'], $item['exp_price'], 2);
switch ($where['type']) {
case "day" :
$item['title'] = "日账单";
$item['add_time'] = date('Y-m-d', $item['add_time']);
break;
case "week" :
$item['title'] = "周账单";
$item['add_time'] = '第' . $item['day'] . '周(' . date('m', $item['add_time']) . '月)';
break;
case "month" :
$item['title'] = "月账单";
$item['add_time'] = date('Y-m', $item['add_time']);
break;
}
}
return $data;
}
}