CaiWuXiTong/modules/EcommerceFinance/Repositories/TmallTaobaoSpreadBillRepository.php

245 lines
8.7 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 Modules\EcommerceFinance\Repositories;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
/**
* 淘系(天猫+淘宝)支付宝账户流水 - 子功能页面数据仓库
*
* 数据来源:上游 JianSun.dbo.tmall_spread_bills
* 表1列:店铺名称 | 记账日期 | 交易日期 | 金额 | 余额 | 备注 | finType | 收支类型 | 交易类型
*
* 字段映射(上游表 -> 表1 列):
* shopName -> 店铺名称
* uniTransTime -> 记账日期(datetime)
* transTime -> 交易日期(date)
* amount(分) -> 金额(元)—— 上游以「分」存储,返回时除以 100
* balance(分) -> 余额(元)—— 上游以「分」存储,返回时除以 100
* comment -> 备注
* finType -> finType(CREDIT/DEBIT 原值)
* finType 派生 -> 收支类型(CREDIT=支出, DEBIT=收入)
* bizType/tradeType -> 交易类型(CASH_DEDUCT=扣款, INTEL_CHARGE_POST=充值 等)
*
* 金额单位换算:上游 amount/balance 均以「分」存储,前端期望「元」,
* 因此 normalize() 中统一除以 100,summary() 也基于「元」单位汇总。
*/
class TmallTaobaoSpreadBillRepository
{
private const TABLE = '[dbo].[tmall_spread_bills]';
private const SOURCE = 'jiansun';
/** finType -> 中文收支类型(CREDIT=支出, DEBIT=收入,与天猫平台口径一致)*/
private const FIN_TYPE_MAP = [
'CREDIT' => '支出',
'DEBIT' => '收入',
];
/** bizType -> 中文交易类型(天猫平台 bizType 枚举)*/
private const BIZ_TYPE_MAP = [
'CASH_DEDUCT' => '扣款',
'INTEL_CHARGE_POST' => '充值',
'TRANSFER_IN' => '转入',
'TRANSFER_OUT' => '转出',
'REFUND' => '退款',
'WITHDRAW' => '提现',
];
/** tradeType 兜底映射 */
private const TRADE_TYPE_MAP = [
'DEDUCT' => '扣款',
'CHARGE' => '充值',
'TRANSFER_IN' => '转入',
'TRANSFER_OUT' => '转出',
'REFUND' => '退款',
'WITHDRAW' => '提现',
];
private UpstreamReadOnlyQuery $query;
public function __construct(UpstreamReadOnlyQueryFactory $factory)
{
$this->query = $factory->forSource(self::SOURCE);
}
/**
* 店铺下拉选项(全部去重)
*
* @return string[]
*/
public function storeOptions(): array
{
$rows = $this->query->select(
'SELECT DISTINCT shopName FROM ' . self::TABLE . ' WHERE shopName IS NOT NULL ORDER BY shopName'
);
return array_map(static fn (array $r) => (string) $r['shopName'], $rows);
}
/**
* 分页查询淘系支付宝账户流水,返回表1 9 列结构。
*
* SQL Server 2012 兼容:用 ROW_NUMBER() OVER 做分页,避免 OFFSET/FETCH。
*
* @param string|null $flowType 收支类型筛选:income=收入,expense=支出
* @return array{rows: array<int,array<string,mixed>>, total: int}
*/
public function paginate(
?string $storeName = null,
?string $fromDate = null, // Y-m-d(按交易日期 transTime 过滤)
?string $toDate = null, // Y-m-d
?string $flowType = null, // 'income' | 'expense' | null
int $page = 1,
int $pageSize = 25
): array {
$page = max(1, $page);
$pageSize = max(1, min(200, $pageSize));
[$whereSql, $params] = $this->buildWhere($storeName, $fromDate, $toDate, $flowType);
// 总数
$total = (int) $this->query->scalar(
'SELECT COUNT(*) FROM ' . self::TABLE . $whereSql,
$params
);
if ($total === 0) {
return ['rows' => [], 'total' => 0];
}
// 分页(按记账日期 uniTransTime DESC, id DESC 排序,与列表展示方向一致)
$offsetStart = ($page - 1) * $pageSize + 1;
$offsetEnd = $page * $pageSize;
$pagedSql = 'SELECT * FROM ('
. 'SELECT src.id, src.shopName, src.uniTransTime, src.transTime, '
. 'src.amount, src.balance, src.comment, src.finType, '
. 'src.bizType, src.tradeType, '
. 'ROW_NUMBER() OVER (ORDER BY src.uniTransTime DESC, src.id DESC) AS rn '
. 'FROM ' . self::TABLE . ' src' . $whereSql
. ') AS t WHERE t.rn BETWEEN ? AND ? ORDER BY t.rn';
$rawRows = $this->query->select(
$pagedSql,
array_merge($params, [$offsetStart, $offsetEnd])
);
return [
'rows' => array_map(fn (array $r) => $this->normalize($r), $rawRows),
'total' => $total,
];
}
/**
* 当前筛选条件下的汇总(金额单位:元)。
*
* @param string|null $flowType 'income'/'expense'/null
* @return array{income_total: float, expense_total: float, net: float, count: int}
*/
public function summary(
?string $storeName = null,
?string $fromDate = null,
?string $toDate = null,
?string $flowType = null
): array {
[$whereSql, $params] = $this->buildWhere($storeName, $fromDate, $toDate, $flowType);
// amount 单位为「分」,汇总时除以 100 转为「元」
$row = $this->query->first(
'SELECT '
. 'COALESCE(SUM(CASE WHEN finType = \'DEBIT\' THEN amount ELSE 0 END), 0) AS income_total_raw, '
. 'COALESCE(SUM(CASE WHEN finType = \'CREDIT\' THEN amount ELSE 0 END), 0) AS expense_total_raw, '
. 'COUNT(*) AS cnt '
. 'FROM ' . self::TABLE . $whereSql,
$params
);
// 分 -> 元
$income = ((float) ($row['income_total_raw'] ?? 0)) / 100;
$expense = ((float) ($row['expense_total_raw'] ?? 0)) / 100;
return [
'income_total' => $income,
'expense_total' => $expense,
'net' => $income - $expense,
'count' => (int) ($row['cnt'] ?? 0),
];
}
/**
* 构造 WHERE 子句。
*
* @param string|null $flowType 'income'/'expense'/null
* @return array{0:string,1:array<int,mixed>}
*/
private function buildWhere(
?string $storeName,
?string $fromDate,
?string $toDate,
?string $flowType = null
): array {
$conditions = [];
$params = [];
if ($storeName !== null && $storeName !== '') {
$conditions[] = 'shopName = ?';
$params[] = $storeName;
}
// 日期按交易日期 transTime 过滤;Y-m-d 校验后字符串比较 date 即可
if ($fromDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fromDate) === 1) {
$conditions[] = 'transTime >= ?';
$params[] = $fromDate;
}
if ($toDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $toDate) === 1) {
$conditions[] = 'transTime <= ?';
$params[] = $toDate;
}
// 收支类型筛选:income -> finType='DEBIT'(收入),expense -> finType='CREDIT'(支出)
if ($flowType === 'income') {
$conditions[] = 'finType = \'DEBIT\'';
} elseif ($flowType === 'expense') {
$conditions[] = 'finType = \'CREDIT\'';
}
$whereSql = $conditions === [] ? '' : ' WHERE ' . implode(' AND ', $conditions);
return [$whereSql, $params];
}
/**
* 把上游行归一化为表1 9 列结构(前端可直接消费)。
* 金额单位从「分」转为「元」。
*/
private function normalize(array $row): array
{
$finType = (string) ($row['finType'] ?? '');
$bizType = (string) ($row['bizType'] ?? '');
$tradeType = (string) ($row['tradeType'] ?? '');
// 交易类型:优先 bizType,其次 tradeType,最后返回原始 bizType
$transactionType = self::BIZ_TYPE_MAP[$bizType]
?? self::TRADE_TYPE_MAP[$tradeType]
?? $bizType;
return [
// 表1 9 列
'store_name' => (string) ($row['shopName'] ?? ''),
'bookkeeping_time' => (string) ($row['uniTransTime'] ?? ''),
'transaction_date' => (string) ($row['transTime'] ?? ''),
'amount' => ((float) ($row['amount'] ?? 0)) / 100,
'balance' => ((float) ($row['balance'] ?? 0)) / 100,
'comment' => (string) ($row['comment'] ?? ''),
'fin_type' => $finType,
'income_expense_type' => self::FIN_TYPE_MAP[$finType] ?? '',
'transaction_type' => $transactionType,
// 原始字段保留
'biz_type_raw' => $bizType,
'trade_type_raw' => $tradeType,
];
}
}