CaiWuXiTong/modules/EcommerceFinance/Repositories/StoreAccountMirrorRepository.php
renshen 627fa966ef 店铺账号密码:新增授权状态、店铺状态两列(在账号分组名称后面)
两列都来自已有的共享镜像,按店铺名关联(下划线、空格统一成短横线):
- 授权状态:聚水潭店铺档案 jst_shopInfo 的授权到期时间;无到期时间为未授权,
  已过期为已过期,其余为已授权。
- 店铺状态:店铺档案 fys_shopInfo.shopState,0 正常 / 3 关闭中 / 6 已关闭,
  与店铺基础信息页口径一致。

两列同时做成下拉筛选(已授权/已过期/未授权、正常/关闭中/已关闭),并支持按业务
顺序排序;映射结果缓存 10 分钟,避免每次列表都全表读两张档案表。

线上命中率:账号表 966 个店铺,授权状态命中 963、店铺状态命中 948。
2026-10-08 11:39:34 +08:00

301 lines
13 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Illuminate\Support\Facades\Cache;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use RuntimeException;
class StoreAccountMirrorRepository
{
public const DATASET = 'database_4.external_store_accounts.accounts_v1';
/** 店铺档案(fys_shopInfo.shopState)到页面文案的映射。 */
private const STORE_STATUS_LABELS = [0 => '正常', 3 => '关闭中', 6 => '已关闭'];
/** 授权状态文案;顺序同时用于下拉和排序。 */
private const AUTHORIZATION_LABELS = ['已授权', '已过期', '未授权'];
/** 账号店铺名 -> 状态 的缓存键(两张档案表要全表读,缓存 10 分钟)。 */
private const STATUS_CACHE_KEY = 'store-accounts.statuses:v1';
public function __construct(
private readonly BusinessMirrorDatasetRegistry $datasets,
private readonly GenericBusinessMirrorRepository $mirror,
) {}
/** @return array{data:array<int,array<string,mixed>>,total:int} */
public function paginate(int $page, int $perPage, array $filters = []): array
{
$dataset = $this->datasets->get(self::DATASET);
if (! $this->mirror->ready($dataset)) {
throw new RuntimeException('store_account_mirror_not_ready');
}
$page = max($page, 1);
$perPage = min(max($perPage, 1), 100);
$query = $this->mirror->connection()->table($dataset->targetTable());
$columns = [
'store_name' => 'storeName', 'operation_owner' => 'operationOwner',
'operation_department_head' => 'operationDepartmentHead', 'operation_director' => 'operationDirector',
'profit_center' => 'profitCenter', 'account' => 'account', 'bound_phone' => 'boundPhone',
'verification_contact' => 'verificationContact', 'account_group_name' => 'accountGroupName',
];
foreach ($columns as $key => $column) {
$value = mb_substr(trim((string) ($filters[$key] ?? '')), 0, 191);
if ($value !== '') $query->where($column, 'like', '%' . addcslashes($value, '%_\\') . '%');
}
$accountType = trim((string) ($filters['account_type'] ?? ''));
if ($accountType !== '') $query->where('accountType', '=', $accountType === '主账号' ? 'MAIN' : ($accountType === '子账号' ? 'SUB' : $accountType));
$passwordState = trim((string) ($filters['password_configured'] ?? ''));
if (in_array($passwordState, ['已设置', '未设置', '状态未同步'], true)) {
if ($passwordState === '状态未同步') $query->whereNull('passwordConfigured');
else $query->where('passwordConfigured', $passwordState === '已设置');
}
foreach (['created_at' => 'createdAt', 'updated_at' => 'updatedAt'] as $key => $column) {
foreach (['_from' => '>=', '_to' => '<='] as $suffix => $operator) {
$date = trim((string) ($filters[$key . $suffix] ?? ''));
if (preg_match('/^\d{4}-\d{2}-\d{2}$/D', $date)) $query->whereDate($column, $operator, $date);
}
}
// 授权状态 / 店铺状态来自另两张镜像表(按店铺名关联),这里先在 PHP 里
// 把"账号店铺名 -> 状态"算出来,再用 whereIn 下推到 SQL,分页和总数不受影响。
$statuses = (trim((string) ($filters['authorization_status'] ?? '')) !== ''
|| trim((string) ($filters['store_status'] ?? '')) !== '')
? $this->statusesByStoreName()
: null;
if ($statuses !== null) {
foreach ([
'store_status' => $statuses['store'],
'authorization_status' => $statuses['authorization'],
] as $key => $byStore) {
$expected = trim((string) ($filters[$key] ?? ''));
if ($expected === '') {
continue;
}
$query->whereIn('storeName', $this->storeNamesWithStatus($byStore, $expected));
}
}
$total = (clone $query)->count();
$sortColumns = [
'store_name' => 'storeName', 'operation_owner' => 'operationOwner',
'operation_department_head' => 'operationDepartmentHead', 'operation_director' => 'operationDirector',
'profit_center' => 'profitCenter', 'account_type' => 'accountType', 'account' => 'account',
'password_configured' => 'passwordConfigured', 'bound_phone' => 'boundPhone',
'verification_contact' => 'verificationContact', 'account_group_name' => 'accountGroupName',
'created_at' => 'createdAt', 'updated_at' => 'updatedAt',
];
$sortColumn = $sortColumns[$filters['_sort'] ?? ''] ?? null;
$direction = ($filters['_direction'] ?? 'asc') === 'desc' ? 'desc' : 'asc';
if ($sortColumn !== null) {
$query->orderBy($sortColumn, $direction)->orderByDesc('id');
} elseif (in_array($filters['_sort'] ?? '', ['store_status', 'authorization_status'], true)) {
// 关联出来的状态列按业务顺序排(正常/关闭中/已关闭、已授权/已过期/未授权)。
$sortKey = $filters['_sort'] === 'store_status' ? 'store' : 'authorization';
$labels = $filters['_sort'] === 'store_status'
? array_values(self::STORE_STATUS_LABELS)
: self::AUTHORIZATION_LABELS;
$statuses ??= $this->statusesByStoreName();
[$sql, $bindings] = $this->statusOrderBy($statuses[$sortKey], $labels, $direction);
$query->orderByRaw($sql, $bindings)->orderByDesc('id');
} else {
$query->orderByDesc('id');
}
$statuses ??= $this->statusesByStoreName();
$rows = $query
->forPage($page, $perPage)
->get()
->map(fn (object $row): array => [
'id' => $row->id,
'store_name' => $row->storeName,
'operation_owner' => $row->operationOwner,
'operation_department_head' => $row->operationDepartmentHead,
'operation_director' => $row->operationDirector,
'profit_center' => $row->profitCenter,
'account_type' => $this->accountType($row->accountType),
'account' => $row->account,
// Only the safe boolean may enter the mirror, never a password.
'password_configured' => isset($row->passwordConfigured) ? (bool) $row->passwordConfigured : null,
'bound_phone' => $row->boundPhone,
'verification_contact' => $row->verificationContact,
'account_group_name' => $row->accountGroupName,
'authorization_status' => $statuses['authorization'][$row->storeName] ?? '',
'store_status' => $statuses['store'][$row->storeName] ?? '',
'created_at' => $row->createdAt,
'updated_at' => $row->updatedAt,
])
->all();
return ['data' => $rows, 'total' => $total];
}
/**
* 账号店铺名 -> 授权状态 / 店铺状态。
*
* - 授权状态:聚水潭店铺档案(jst_shopInfo)的 session_expired —— 无授权时间为未授权,
* 已过期;否则为已授权。
* - 店铺状态:店铺档案(fys_shopInfo)的 shopState —— 0 正常 / 3 关闭中 / 6 已关闭。
*
* 两张表按店铺名(把下划线、空格统一成短横线后)关联,实测账号表 966 个店铺里
* 授权命中 963、店铺状态命中 948。
*
* @return array{store:array<string,string>,authorization:array<string,string>}
*/
private function statusesByStoreName(): array
{
if (is_array($cached = Cache::get(self::STATUS_CACHE_KEY))) {
return $cached;
}
$storeStates = [];
foreach ($this->mirror->connection()->table('ops_fys_shop_info')
->select(['Id', 'erpShopName', 'shopState'])->orderBy('Id')->get() as $row) {
$key = $this->shopKey($row->erpShopName);
if ($key !== '') {
$storeStates[$key] = self::STORE_STATUS_LABELS[(int) $row->shopState] ?? '';
}
}
$authorizations = [];
$now = now()->getTimestamp();
foreach ($this->mirror->connection()->table('ops_jst_shop_info')
->select(['Id', 'shop_name', 'session_expired'])->orderBy('Id')->get() as $row) {
$key = $this->shopKey($row->shop_name);
if ($key === '') {
continue;
}
$expired = trim((string) $row->session_expired);
$timestamp = $expired === '' ? false : strtotime($expired);
$authorizations[$key] = $timestamp === false
? '未授权'
: ($timestamp >= $now ? '已授权' : '已过期');
}
$result = ['store' => [], 'authorization' => []];
foreach ($this->mirror->connection()->table($this->datasets->get(self::DATASET)->targetTable())
->select('storeName')->whereNotNull('storeName')->distinct()->pluck('storeName') as $name) {
$name = (string) $name;
$key = $this->shopKey($name);
$result['store'][$name] = $storeStates[$key] ?? '';
$result['authorization'][$name] = $authorizations[$key] ?? '';
}
Cache::put(self::STATUS_CACHE_KEY, $result, now()->addMinutes(10));
return $result;
}
/** @return list<string> 指定状态下的账号店铺名 */
private function storeNamesWithStatus(array $byStore, string $expected): array
{
$names = [];
foreach ($byStore as $name => $status) {
if ($status === $expected) {
$names[] = $name;
}
}
return $names;
}
/**
* 把状态列翻译成 SQL 排序:按给定文案顺序生成 CASE WHEN。
*
* @param array<string,string> $byStore
* @param list<string> $labels
* @return array{0:string,1:list<string>}
*/
private function statusOrderBy(array $byStore, array $labels, string $direction): array
{
$cases = [];
$bindings = [];
foreach ($labels as $index => $label) {
$names = $this->storeNamesWithStatus($byStore, $label);
if ($names === []) {
continue;
}
$cases[] = 'WHEN storeName IN (' . implode(', ', array_fill(0, count($names), '?')) . ') THEN ' . $index;
array_push($bindings, ...$names);
}
if ($cases === []) {
return ['1', []];
}
return ['CASE ' . implode(' ', $cases) . ' ELSE ' . count($labels) . ' END ' . $direction, $bindings];
}
/** 店铺名归一键:下划线、半/全角空格统一成短横线,便于跨表关联。 */
private function shopKey(mixed $name): string
{
return str_replace(['_', ' ', ' '], ['-', '', ''], trim((string) $name));
}
public function containsAccount(string $id): bool
{
$dataset = $this->datasets->get(self::DATASET);
if (! $this->mirror->ready($dataset)) {
throw new RuntimeException('store_account_mirror_not_ready');
}
return $this->mirror->connection()->table($dataset->targetTable())->where('id', $id)->exists();
}
public function containsAccounts(array $ids): bool
{
$ids = array_values(array_unique($ids));
if ($ids === []) {
return false;
}
$dataset = $this->datasets->get(self::DATASET);
if (! $this->mirror->ready($dataset)) {
throw new RuntimeException('store_account_mirror_not_ready');
}
return $this->mirror->connection()->table($dataset->targetTable())
->whereIn('id', $ids)->distinct()->count('id') === count($ids);
}
/**
* 店铺维度报表的归属字段覆盖表,以店铺名称(storeName)为键。
*
* @return array<string,array{operation_owner:string,operation_director:string,profit_center:string}>
*/
public function ownershipByStoreName(): array
{
$dataset = $this->datasets->get(self::DATASET);
if (! $this->mirror->ready($dataset)) {
throw new RuntimeException('store_account_mirror_not_ready');
}
$map = [];
foreach ($this->mirror->connection()->table($dataset->targetTable())
->select('storeName', 'operationOwner', 'operationDirector', 'profitCenter')
->get() as $row) {
$name = trim((string) $row->storeName);
if ($name === '') {
continue;
}
$map[$name] = [
'operation_owner' => trim((string) $row->operationOwner),
'operation_director' => trim((string) $row->operationDirector),
'profit_center' => trim((string) $row->profitCenter),
];
}
return $map;
}
private function accountType(mixed $value): ?string
{
$type = trim((string) $value);
return match (strtoupper($type)) {
'MAIN' => '主账号',
'SUB' => '子账号',
'', '/' => null,
default => $type,
};
}
}