CaiWuXiTong/modules/EcommerceFinance/Repositories/PinduoduoSpreadBillRepository.php

214 lines
7.1 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.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,
];
}
}