CaiWuXiTong/modules/EcommerceFinance/Repositories/ShopInfoFlowMirrorRepository.php

329 lines
13 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 Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use RuntimeException;
/**
* 财务经营风险管控 › 流程统计:店铺申请流程(开店 / 变更 / 关店)只读镜像。
*
* 表单字段来自 jiansun.dbo.fys_shopInfo_flow(聚水潭侧申请单,含店铺/主体/变更明细),
* 审批状态与当前审批人来自中台库的 OA 流程表 dbo.flow_flow(按 flowId 关联)。
*/
class ShopInfoFlowMirrorRepository
{
public const FORM_DATASET = 'jiansun.dbo.fys_shopInfo_flow';
public const FLOW_DATASET = 'jiansun.dbo.flow_flow';
/** OA 流程类型(flowType)到页面文案。 */
public const FLOW_TYPES = [
8 => '店铺开店',
9 => '店铺变更',
10 => '店铺关店',
12 => '主体注销自动流程',
];
/** OA 审批状态(flow_flow.flowStatus),取值与文案取自 OA 前端。 */
public const APPROVAL_STATUSES = [
0 => '暂存',
1 => '流程中',
2 => '拒绝',
3 => '完成',
4 => '撤回',
5 => '待签署',
6 => '已签署',
7 => '待审批',
];
/** 当前节点等待天数分档:值为该档包含的最小天数(含)和最大天数(不含)。 */
public const WAITING_DAY_BUCKETS = [
'1天内' => [0, 1],
'1天' => [1, 2],
'2天' => [2, 3],
'3天' => [3, 4],
'4天' => [4, 5],
'5天' => [5, 6],
'6天' => [6, 7],
'7天' => [7, 8],
'8-14天' => [8, 15],
'15-30天' => [15, 31],
'31天以上' => [31, null],
];
/** 店铺档案状态(fys_shopInfo.shopState 口径)。 */
private const SHOP_STATES = [0 => '正常', 3 => '关闭中', 6 => '已关闭'];
public function __construct(
private readonly BusinessMirrorDatasetRegistry $datasets,
private readonly GenericBusinessMirrorRepository $mirror,
) {
}
public function ready(): bool
{
try {
return $this->mirror->ready($this->datasets->get(self::FORM_DATASET));
} catch (\Throwable) {
return false;
}
}
/**
* @param array<string,mixed> $filters
* @return array{data:list<array<string,mixed>>,total:int}
*/
public function paginate(int $page, int $perPage, array $filters = []): array
{
if (! $this->ready()) {
throw new RuntimeException('shop_info_flow_mirror_not_ready');
}
$page = max($page, 1);
$perPage = min(max($perPage, 1), 100);
$connection = $this->mirror->connection();
$form = $this->datasets->get(self::FORM_DATASET)->targetTable();
$flow = $this->datasets->get(self::FLOW_DATASET)->targetTable();
$query = $connection->table($form . ' as f')
->leftJoin($flow . ' as o', 'o.flowId', '=', 'f.flowId');
$this->applyFilters($query, $filters);
$total = (clone $query)->count();
$rows = $query
->select([
'f.Id as id', 'f.flowId as flow_id', 'f.flowType as flow_type', 'f.applyUser as apply_user',
'f.applyTime as apply_time', 'f.applyDepartment as apply_department', 'f.remark as remark',
'f.platform as platform', 'f.responsibleDepartment as responsible_department',
'f.shopHead as shop_head', 'f.customerManager as customer_manager', 'f.shopType as shop_type',
'f.shopState as shop_state', 'f.shopName as shop_name', 'f.erpShopName as erp_shop_name',
'f.trademarkNumber as trademark_number', 'f.unifiedSocialCreditCode as subject_code',
'f.informationChangeType as information_change_type', 'f.changeReason as change_reason',
'f.changeSuccessful as change_successful', 'f.closeReason as close_reason',
'f.expectedClosingTime as expected_closing_time', 'f.refundType as refund_type',
'f.refundAmount as refund_amount', 'f.refundIssue as refund_issue',
'o.flowStatus as approval_status_code', 'o.approver as current_approver',
'o.subject as flow_subject', 'o.updateTime as flow_updated_at',
])
->orderByDesc('f.applyTime')->orderByDesc('f.Id')
->forPage($page, $perPage)
->get()
->map(fn (object $row): array => $this->mapRow($row))
->all();
return ['data' => $rows, 'total' => $total];
}
/** @param array<string,mixed> $filters */
private function applyFilters(Builder $query, array $filters): void
{
foreach ([
'flow_id' => 'f.flowId',
'apply_user' => 'f.applyUser',
'erp_shop_name' => 'f.erpShopName',
'shop_name' => 'f.shopName',
'shop_head' => 'f.shopHead',
'current_approver' => 'o.approver',
'responsible_department' => 'f.responsibleDepartment',
'trademark_number' => 'f.trademarkNumber',
] as $key => $column) {
$value = trim((string) ($filters[$key] ?? ''));
if ($value !== '') {
$query->where($column, 'like', '%' . addcslashes($value, '%_\\') . '%');
}
}
foreach ([['from', '>='], ['to', '<=']] as [$suffix, $operator]) {
$date = trim((string) ($filters['apply_' . $suffix] ?? ''));
if (preg_match('/^\d{4}-\d{2}-\d{2}$/D', $date)) {
$query->whereDate('f.applyTime', $operator, $date);
}
}
// 多选筛选:流程类型 / 审批状态 / 平台,多个取值取并集。
$flowTypes = $this->filterValues($filters['flow_type'] ?? '');
if ($flowTypes !== []) {
$codes = [];
foreach ($flowTypes as $label) {
$code = array_search($label, self::FLOW_TYPES, true);
if ($code !== false) {
$codes[] = (int) $code;
}
}
$query->whereIn('f.flowType', $codes === [] ? [-1] : $codes);
}
$statuses = $this->filterValues($filters['approval_status'] ?? '');
if ($statuses !== []) {
$codes = [];
foreach ($statuses as $label) {
$code = array_search($label, self::APPROVAL_STATUSES, true);
if ($code !== false) {
$codes[] = (int) $code;
}
}
$query->whereIn('o.flowStatus', $codes === [] ? [-1] : $codes);
}
$waitingDays = array_values(array_intersect(
$this->filterValues($filters['node_waiting_days'] ?? ''),
array_keys(self::WAITING_DAY_BUCKETS),
));
if ($waitingDays !== []) {
$now = now();
$query->where(function (Builder $buckets) use ($waitingDays, $now): void {
foreach ($waitingDays as $label) {
[$minimumDays, $maximumDays] = self::WAITING_DAY_BUCKETS[$label];
$buckets->orWhere(function (Builder $bucket) use ($minimumDays, $maximumDays, $now): void {
$bucket->whereIn('o.flowStatus', [1, 7])
->where('o.updateTime', '<=', $now);
if ($minimumDays === 0) {
$bucket->where('o.updateTime', '>', $now->copy()->subDay());
} elseif ($maximumDays === null) {
$bucket->where('o.updateTime', '<=', $now->copy()->subDays($minimumDays));
} else {
$bucket->where('o.updateTime', '>', $now->copy()->subDays($maximumDays))
->where('o.updateTime', '<=', $now->copy()->subDays($minimumDays));
}
});
}
});
}
$platforms = $this->filterValues($filters['platform'] ?? '');
if ($platforms !== []) {
$query->whereIn('f.platform', $platforms);
}
}
/** @return list<string> */
private function filterValues(mixed $value): array
{
$values = [];
foreach ((array) $value as $item) {
if (! is_scalar($item)) {
continue;
}
$item = trim((string) $item);
if ($item !== '') {
$values[$item] = true;
}
}
return array_keys($values);
}
/** @return array<string,mixed> */
private function mapRow(object $row): array
{
$flowType = (int) ($row->flow_type ?? 0);
$statusCode = $row->approval_status_code !== null ? (int) $row->approval_status_code : null;
$approvalStatus = $statusCode === null
? ''
: (self::APPROVAL_STATUSES[$statusCode] ?? (string) $statusCode);
$currentApprover = trim((string) ($row->current_approver ?? ''));
$currentNode = match ($statusCode) {
0 => '暂存',
1, 7 => $currentApprover !== '' ? '审批节点(' . $currentApprover . ')' : '待分配审批人',
2 => '已拒绝',
3 => '已完成',
4 => '已撤回',
5 => '待签署',
6 => '已签署',
default => '',
};
$nodeEnteredAt = in_array($statusCode, [1, 7], true)
? trim((string) ($row->flow_updated_at ?? ''))
: '';
return [
'id' => $row->id,
'flow_id' => (string) $row->flow_id,
'flow_type' => self::FLOW_TYPES[$flowType] ?? ('其它(' . $flowType . ')'),
'flow_subject' => (string) ($row->flow_subject ?? ''),
'approval_status' => $approvalStatus,
'current_approver' => $currentApprover,
'current_node' => $currentNode,
'node_entered_at' => $nodeEnteredAt,
'node_waiting_duration' => $this->waitingDuration($nodeEnteredAt),
'node_waiting_days' => $this->waitingDays($nodeEnteredAt),
'apply_user' => (string) ($row->apply_user ?? ''),
'apply_time' => (string) ($row->apply_time ?? ''),
'apply_department' => (string) ($row->apply_department ?? ''),
'remark' => (string) ($row->remark ?? ''),
'platform' => (string) ($row->platform ?? ''),
'responsible_department' => (string) ($row->responsible_department ?? ''),
'shop_head' => (string) ($row->shop_head ?? ''),
'customer_manager' => (string) ($row->customer_manager ?? ''),
'shop_type' => (string) ($row->shop_type ?? ''),
'shop_state' => $row->shop_state === null ? '' : (self::SHOP_STATES[(int) $row->shop_state] ?? (string) $row->shop_state),
'shop_name' => (string) ($row->shop_name ?? ''),
'erp_shop_name' => (string) ($row->erp_shop_name ?? ''),
'trademark_number' => (string) ($row->trademark_number ?? ''),
'subject_code' => (string) ($row->subject_code ?? ''),
'information_change_type' => $row->information_change_type === null ? '' : (string) $row->information_change_type,
'change_reason' => (string) ($row->change_reason ?? ''),
'change_successful' => $row->change_successful === null ? null : (bool) $row->change_successful,
'close_reason' => (string) ($row->close_reason ?? ''),
'expected_closing_time' => (string) ($row->expected_closing_time ?? ''),
'refund_type' => (string) ($row->refund_type ?? ''),
'refund_amount' => $row->refund_amount === null ? null : (float) $row->refund_amount,
'refund_issue' => (string) ($row->refund_issue ?? ''),
'flow_updated_at' => (string) ($row->flow_updated_at ?? ''),
];
}
private function waitingDuration(string $enteredAt): string
{
if ($enteredAt === '') {
return '';
}
try {
$minutes = (int) max(0, Carbon::parse($enteredAt)->diffInMinutes(now(), true));
} catch (\Throwable) {
return '';
}
if ($minutes < 1) {
return '不到1分钟';
}
$days = intdiv($minutes, 1440);
$hours = intdiv($minutes % 1440, 60);
$remainingMinutes = $minutes % 60;
if ($days > 0) {
return $days . '天' . ($hours > 0 ? $hours . '小时' : '');
}
if ($hours > 0) {
return $hours . '小时' . ($remainingMinutes > 0 ? $remainingMinutes . '分钟' : '');
}
return $remainingMinutes . '分钟';
}
private function waitingDays(string $enteredAt): string
{
if ($enteredAt === '') {
return '';
}
try {
$days = intdiv((int) max(0, Carbon::parse($enteredAt)->diffInMinutes(now(), true)), 1440);
} catch (\Throwable) {
return '';
}
foreach (self::WAITING_DAY_BUCKETS as $label => [$minimumDays, $maximumDays]) {
if ($days >= $minimumDays && ($maximumDays === null || $days < $maximumDays)) {
return $label;
}
}
return '';
}
}