表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>, 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} */ 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, ]; } }