214 lines
7.1 KiB
PHP
214 lines
7.1 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Repositories;
|
||
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||
|
||
/**
|
||
* 拼多多账户流水 - 子功能页面数据仓库
|
||
*
|
||
* 数据来源:上游 JianSun.dbo.pdd_spread_bills
|
||
* 字段映射(上游表 -> 表1 列):
|
||
* shopName -> 店铺名称
|
||
* amount -> 金额(元)
|
||
* balance -> 余额(元)
|
||
* summary -> 摘要
|
||
* flowType(1=收入, 2=支出) -> 收支类型
|
||
* toAdvertiserFundType -> 资金类型(1=现金, 3=红包/券)
|
||
* createTime -> 费用时间
|
||
*
|
||
* 提供两个能力:
|
||
* - paginate():分页查询流水,返回表1 7 列结构
|
||
* - summary():当前筛选条件下的汇总(收入合计、支出合计、净额、笔数)
|
||
*/
|
||
class PinduoduoSpreadBillRepository
|
||
{
|
||
private const TABLE = '[dbo].[pdd_spread_bills]';
|
||
private const SOURCE = 'jiansun';
|
||
|
||
/** flowType -> 中文收支类型 */
|
||
private const FLOW_TYPE_MAP = [
|
||
1 => '收入',
|
||
2 => '支出',
|
||
];
|
||
|
||
/** toAdvertiserFundType -> 中文资金类型 */
|
||
private const FUND_TYPE_MAP = [
|
||
1 => '现金',
|
||
3 => '红包/券',
|
||
];
|
||
|
||
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 7 列结构。
|
||
*
|
||
* 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
|
||
?string $toDate = null, // Y-m-d
|
||
?string $flowType = 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];
|
||
}
|
||
|
||
// 分页(按 createTime DESC, id DESC 排序,与列表展示方向一致)
|
||
$offsetStart = ($page - 1) * $pageSize + 1;
|
||
$offsetEnd = $page * $pageSize;
|
||
|
||
$pagedSql = 'SELECT * FROM ('
|
||
. 'SELECT src.id, src.shopName, src.amount, src.balance, src.summary, '
|
||
. 'src.flowType, src.toAdvertiserFundType, src.createTime, '
|
||
. 'ROW_NUMBER() OVER (ORDER BY src.createTime 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=支出
|
||
* @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);
|
||
|
||
$row = $this->query->first(
|
||
'SELECT '
|
||
. 'COALESCE(SUM(CASE WHEN flowType = 1 THEN amount ELSE 0 END), 0) AS income_total, '
|
||
. 'COALESCE(SUM(CASE WHEN flowType = 2 THEN amount ELSE 0 END), 0) AS expense_total, '
|
||
. 'COUNT(*) AS cnt '
|
||
. 'FROM ' . self::TABLE . $whereSql,
|
||
$params
|
||
);
|
||
|
||
$income = (float) ($row['income_total'] ?? 0);
|
||
$expense = (float) ($row['expense_total'] ?? 0);
|
||
|
||
return [
|
||
'income_total' => $income,
|
||
'expense_total' => $expense,
|
||
'net' => $income - $expense,
|
||
'count' => (int) ($row['cnt'] ?? 0),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 构造 WHERE 子句(不带前导空格时给前面加空格,避免拼到 FROM 后粘连)。
|
||
*
|
||
* @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;
|
||
}
|
||
|
||
// 日期按 Y-m-d 校验;SQL Server 用字符串比较 datetime 即可
|
||
if ($fromDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fromDate) === 1) {
|
||
$conditions[] = 'createTime >= ?';
|
||
$params[] = $fromDate . ' 00:00:00';
|
||
}
|
||
|
||
if ($toDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $toDate) === 1) {
|
||
$conditions[] = 'createTime <= ?';
|
||
$params[] = $toDate . ' 23:59:59';
|
||
}
|
||
|
||
if ($flowType === 'income') {
|
||
$conditions[] = 'flowType = 1';
|
||
} elseif ($flowType === 'expense') {
|
||
$conditions[] = 'flowType = 2';
|
||
}
|
||
|
||
$whereSql = $conditions === [] ? '' : ' WHERE ' . implode(' AND ', $conditions);
|
||
|
||
return [$whereSql, $params];
|
||
}
|
||
|
||
/**
|
||
* 把上游行归一化为表1 7 列结构(前端可直接消费)。
|
||
*/
|
||
private function normalize(array $row): array
|
||
{
|
||
$flowType = isset($row['flowType']) ? (int) $row['flowType'] : null;
|
||
$fundType = isset($row['toAdvertiserFundType']) ? (int) $row['toAdvertiserFundType'] : null;
|
||
|
||
return [
|
||
'store_name' => (string) ($row['shopName'] ?? ''),
|
||
'amount' => (float) ($row['amount'] ?? 0),
|
||
'balance' => (float) ($row['balance'] ?? 0),
|
||
'summary' => (string) ($row['summary'] ?? ''),
|
||
'income_expense_type' => self::FLOW_TYPE_MAP[$flowType] ?? '',
|
||
'fund_type' => self::FUND_TYPE_MAP[$fundType] ?? '',
|
||
'cost_time' => (string) ($row['createTime'] ?? ''),
|
||
// 原始字段保留,前端可选展示
|
||
'flow_type_raw' => $flowType,
|
||
'fund_type_raw' => $fundType,
|
||
];
|
||
}
|
||
}
|