CaiWuXiTong/app/Business/TableRegistry.php
renshen 885961084e 流程统计:接入店铺申请流程(开店/变更/关店)共享镜像
数据源:jiansun.dbo.fys_shopInfo_flow(申请单明细,4572 行,含店铺/平台/负责人/利润中心/商标/变更原因/关店原因等)
+ 中台库 dbo.flow_flow(按 flowId 关联审批状态与当前审批人),两个数据集已登记并完成首次全量。

- 新增 ShopInfoFlowMirrorRepository:左连接两套镜像,支持单据编号/申请人/申请日期/当前审批人/
  ERP店铺名称/店铺运营负责人/利润中心/商标号等文本筛选,流程类型、审批状态、平台为可搜索多选。
- 审批状态文案取自 OA 前端字典:0暂存/1流程中/2拒绝/3完成/4撤回/5待签署/6已签署/7待审批;
  流程类型:8店铺开店/9店铺变更/10店铺关店/12主体注销自动流程。
- 页面按目录表渲染(查询条件、筛选字段、导出),列与外部店铺申请页对齐。
- TableRegistry 登记为只读上游页,镜像未就绪时页面给提示而不是报错。
2026-10-08 14:38:16 +08:00

444 lines
19 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 App\Business;
use App\Models\Business\BankAccount;
use App\Models\Business\BusinessModel;
use App\Models\Business\CompanyCancellation;
use App\Models\Business\CompanyChange;
use App\Models\Business\CompanyInformation;
use App\Models\Business\CompanyRegistration;
use App\Models\Business\CompanyStoreData;
use App\Models\Business\ExpressReceiving;
use App\Models\Business\FundForecast;
use App\Models\Business\InvoiceBasic;
use App\Models\Business\InstallationDetail;
use App\Models\Business\StoreChange;
use App\Models\Business\StoreChangeLog;
use App\Models\Business\StoreClosing;
use App\Models\Business\StoreInformation;
use App\Models\Business\ShopInfoFlow;
use App\Models\Business\StoreInvoice;
use App\Models\Business\StoreOpening;
use App\Models\Business\StoreReceivingAccount;
use App\Models\Business\SwActualStatistics;
use App\Models\Business\SwPlanning;
use App\Models\Business\TaxData;
use App\Models\Business\Trademark;
use App\Utilities\BusinessNavigation;
use Illuminate\Support\Arr;
/**
* Central registry that maps a navigation (section, page[, tab]) tuple to the
* corresponding Eloquent model, field metadata, and Excel sheet name. This
* drives the dynamic CRUD views so we don't have to hand-write 11 controllers
* and 11 blade templates.
*/
class TableRegistry
{
/**
* Field type hints used by the Blade form renderer.
* text|number|date|boolean|select|textarea|currency
*/
private const FIELD_TYPES = [
'change_date' => 'date',
'opened_at' => 'date',
'establishment_date' => 'date',
'registration_date' => 'date',
'expiry_date' => 'date',
'invoice_date' => 'date',
'trigger_store_change' => 'boolean',
'trigger_company_register' => 'boolean',
'invoice_type' => 'select',
'account_type' => 'select',
'receiving_method' => 'select',
'platform' => 'select',
'receiving_type' => 'select',
'notes' => 'textarea',
'business_scope' => 'textarea',
'shareholders' => 'textarea',
'subscribed_capital' => 'number',
];
private const SELECT_OPTIONS = [
'invoice_type' => ['普票', '专票'],
'account_type' => ['基本户', '一般户', '个人账户', '个人微信', '个人支付宝'],
'receiving_method' => ['支付宝', '银行卡'],
'platform' => ['京东', '抖音', '拼多多', '淘宝', '天猫', '淘工厂', '唯品会', '得物'],
'receiving_type' => ['对公', '个人'],
];
private const COLUMN_LABELS = [
'unified_social_credit_code' => '统一社会信用代码',
'company_name' => '公司名称',
'subscribed_capital' => '认缴注册资本(万元)',
'company_type' => '类型',
'legal_representative' => '法定代表人',
'establishment_date' => '成立日期',
'registration_status' => '注册状态',
'registered_address' => '注册地址(住所)',
'business_scope' => '经营范围',
'basic_deposit_account_number' => '基本存款账户编号',
'company_status' => '公司状态',
'financial_officer' => '财务负责人',
'supervisor' => '监事',
'taxpayer_category' => '纳税类别及类型',
'shareholders' => '股东',
'notes' => '备注',
'registration_type' => '注册类型',
'change_type' => '变更类型',
'change_status' => '变更状态',
'change_date' => '变更日期',
'cancellation_status' => '注销状态',
'trademark_name' => '商标名称',
'trademark_type' => '商标类型',
'trademark_status' => '商标状态',
'registration_date' => '注册日期',
'expiry_date' => '到期日期',
'opened_at' => '开店时间',
'erp_store_name' => 'ERP系统店铺名称',
'trademark_number' => '商标号',
'platform' => '平台',
'profit_center' => '店铺所属利润中心',
'store_manager' => '店铺负责人',
'store_status' => '店铺状态',
'store_type' => '店铺类型',
'channel_mode' => '渠道模式',
'store_name' => '店铺名称',
'store_platform' => '店铺平台',
'opening_status' => '开店状态',
'closing_status' => '关店状态',
'store_entity' => '店铺主体',
'receiving_account_info' => '收款账户信息',
'historical_entity' => '历史主体',
];
/**
* Currency columns (rendered with two decimals and right-aligned).
*/
private const CURRENCY_HINTS = [
'real_ship_revenue', 'zhongcai_revenue', 'refund_amount', 'net_revenue',
'tax_revenue', 'tax_refund', 'tax_net_revenue', 'revenue_diff', 'final_tax_revenue',
'product_cost', 'logistics_cost', 'platform_invoice', 'labor_cost',
'other_reimbursement', 'profit',
'provided_revenue', 'declared_revenue',
'goods_special_amount', 'goods_special_tax', 'goods_normal_amount', 'goods_normal_tax',
'goods_unbilled_amount', 'goods_unbilled_tax', 'goods_subtotal_amount', 'goods_subtotal_tax',
'service_special_amount', 'service_special_tax', 'service_normal_amount', 'service_normal_tax',
'service_unbilled_amount', 'service_unbilled_tax', 'service_subtotal_amount', 'service_subtotal_tax',
'total_revenue', 'total_tax',
'input_goods_amount', 'input_goods_tax', 'input_service_amount', 'input_service_tax',
'input_other_amount', 'input_other_tax', 'input_subtotal_amount', 'input_subtotal_tax',
'extra_credit', 'retained_credit', 'vat_payable', 'city_tax', 'education_surcharge',
'local_education_surcharge', 'stamp_tax', 'union_fee', 'individual_tax', 'income_tax',
'garbage_fee', 'other_penalty', 'total_tax_paid',
'store_revenue', 'company_revenue',
'invoice_limit', 'output_amount', 'input_amount',
'invoice_amount',
];
/**
* Map (section_slug / page_slug) -> Model class.
* Tabs reuse the same model but filter by an extra column.
*/
private static function routeMap(): array
{
return [
'company-management/basic-information' => CompanyInformation::class,
'company-management/subject-operating-report' => StoreInformation::class,
'company-management/registration' => CompanyRegistration::class,
'company-management/changes' => CompanyChange::class,
'company-management/cancellation' => CompanyCancellation::class,
'company-management/tax-data' => TaxData::class,
'company-management/store-data' => CompanyStoreData::class,
'trademark-management/registration' => Trademark::class,
'trademark-management/changes' => Trademark::class,
'trademark-management/renewal' => Trademark::class,
'trademark-management/information' => Trademark::class,
'store-management/basic-information' => StoreInformation::class,
'store-management/store-accounts' => StoreInformation::class,
'store-management/store-report' => StoreInformation::class,
'store-management/opening-process' => StoreOpening::class,
'store-management/changes' => StoreChange::class,
'store-management/closing-process' => StoreClosing::class,
'financial-sw/planning' => SwPlanning::class,
'financial-sw/actual-statistics' => SwActualStatistics::class,
'store-management/change-log' => StoreChangeLog::class,
'invoice-management/basic-information' => InvoiceBasic::class,
'invoice-management/store-backend-invoicing' => StoreInvoice::class,
'fund-management/fund-forecast' => FundForecast::class,
'fund-management/store-receiving-accounts' => StoreReceivingAccount::class,
'fund-management/bank-accounts' => BankAccount::class,
'fund-management/shared-company-accounts' => BankAccount::class, // 重用 BankAccount,仅类型过滤
'billing-transactions/store-receiving-balance' => StoreReceivingAccount::class,
'financial-operations/installation-details' => InstallationDetail::class,
'financial-risk-control/process-statistics' => ShopInfoFlow::class,
];
}
/**
* Resolve the model class for a given navigation triple.
* Returns null if no model is registered (page is purely descriptive).
*/
public static function modelClassFor(string $section, string $page, ?string $tab = null): ?string
{
$key = "{$section}/{$page}";
$map = self::routeMap();
// For store-receiving-accounts tabs, we route by platform (tab slug).
if ($key === 'fund-management/store-receiving-accounts') {
return StoreReceivingAccount::class;
}
return $map[$key] ?? null;
}
/**
* Build the descriptor used by the Blade view to render the table.
*/
public static function describe(string $section, string $page, ?string $tab = null): ?TableDescriptor
{
if ($section === 'fund-management' && $page === 'store-receiving-accounts') {
return new TableDescriptor(
model: StoreReceivingAccount::class,
table: (new StoreReceivingAccount())->getTable(),
fields: self::storeReceivingAccountFields($tab),
tab: $tab,
tabFilterColumn: $tab ? 'store_platform' : null,
readOnly: false,
);
}
$modelClass = self::modelClassFor($section, $page, $tab);
if (! $modelClass) {
return null;
}
$instance = new $modelClass();
$labels = method_exists($modelClass, 'columnLabels')
? $modelClass::columnLabels()
: [];
$fieldTypes = method_exists($modelClass, 'fieldTypes')
? $modelClass::fieldTypes()
: [];
$columns = self::isUpstreamReadOnly($section, $page)
? $instance->getFillable()
: $instance->getConnection()->getSchemaBuilder()->getColumnListing($instance->getTable());
// Drop meta columns from the rendered table.
$visible = array_values(array_filter($columns, function (string $col) {
return ! in_array($col, ['id', 'company_id', 'created_at', 'updated_at', 'deleted_at'], true);
}));
$fields = [];
foreach ($visible as $col) {
$fields[] = [
'name' => $col,
'label' => $labels[$col] ?? self::COLUMN_LABELS[$col] ?? ucfirst(str_replace('_', ' ', $col)),
'type' => $fieldTypes[$col] ?? self::fieldType($col),
'options' => self::selectOptions($col),
'currency' => in_array($col, self::CURRENCY_HINTS, true),
];
}
return new TableDescriptor(
model: $modelClass,
table: $instance->getTable(),
fields: $fields,
tab: $tab,
tabFilterColumn: $tab ? self::tabFilterColumn($section, $page) : null,
readOnly: self::isUpstreamReadOnly($section, $page),
);
}
private static function isUpstreamReadOnly(string $section, string $page): bool
{
return in_array("{$section}/{$page}", [
'company-management/basic-information',
'company-management/subject-operating-report',
'store-management/basic-information',
'store-management/store-accounts',
'store-management/store-report',
'trademark-management/information',
'financial-operations/installation-details',
'financial-risk-control/process-statistics',
], true);
}
private static function fieldType(string $column): string
{
return self::FIELD_TYPES[$column] ?? (
str_contains($column, '_budget') || str_contains($column, '_actual')
? 'currency'
: (in_array($column, self::CURRENCY_HINTS, true) ? 'currency' : 'text')
);
}
private static function selectOptions(string $column): ?array
{
return self::SELECT_OPTIONS[$column] ?? null;
}
/**
* For pages with tabs, the column used to filter by tab slug.
*/
private static function tabFilterColumn(string $section, string $page): ?string
{
$key = "{$section}/{$page}";
return match ($key) {
'fund-management/store-receiving-accounts' => 'store_platform',
default => null,
};
}
/**
* For "shared-company-accounts" page we only show bank accounts that are
* marked as shared (account_type == '基本户' by convention).
*/
public static function fixedFilters(string $section, string $page): array
{
$key = "{$section}/{$page}";
return match ($key) {
'fund-management/shared-company-accounts' => ['account_type' => '基本户'],
default => [],
};
}
public static function importIdentityFields(string $section, string $page): array
{
return match ("{$section}/{$page}") {
'company-management/basic-information' => ['unified_social_credit_code'],
'trademark-management/information',
'trademark-management/registration',
'trademark-management/changes',
'trademark-management/renewal' => ['trademark_name'],
'store-management/basic-information' => ['erp_store_name', 'platform'],
default => [],
};
}
/**
* Resolve a tab slug to its display title from the navigation config.
*/
public static function tabTitle(string $section, string $page, string $tabSlug): ?string
{
foreach (BusinessNavigation::all() as $navSection) {
if ($navSection['slug'] !== $section) {
continue;
}
foreach ($navSection['pages'] ?? [] as $navPage) {
if (($navPage['slug'] ?? null) !== $page) {
continue;
}
foreach ($navPage['tabs'] ?? [] as $tab) {
if (($tab['slug'] ?? null) === $tabSlug) {
return $tab['title'] ?? $tabSlug;
}
}
}
}
return null;
}
/**
* Map tab slug -> filter value for the tab filter column.
* For example the "taobao-alipay" tab maps to "淘宝" platform.
*/
public static function tabValue(string $section, string $page, string $tabSlug): ?string
{
$key = "{$section}/{$page}";
if ($key === 'fund-management/store-receiving-accounts') {
return match ($tabSlug) {
'taobao' => '淘宝',
'jd' => '京东',
'douyin' => '抖音',
'pinduoduo' => '拼多多',
default => null,
};
}
return null;
}
/** @return list<string> */
public static function tabValues(string $section, string $page, string $tabSlug): array
{
if ($section !== 'fund-management' || $page !== 'store-receiving-accounts') {
$value = self::tabValue($section, $page, $tabSlug);
return $value === null ? [] : [$value];
}
return match ($tabSlug) {
'taobao' => ['淘宝', '阿里巴巴', '天猫', '淘宝分销', '唯品会'],
'jd' => ['京东', '京喜'],
'douyin' => ['抖音'],
'pinduoduo' => ['拼多多'],
default => [],
};
}
/** @return list<array{name:string,label:string,type:string,options?:list<string>,currency:bool}> */
private static function storeReceivingAccountFields(?string $tab): array
{
$text = static fn (string $name, string $label): array => [
'name' => $name, 'label' => $label, 'type' => 'text', 'options' => null, 'currency' => false,
];
$select = static fn (string $name, string $label, array $options): array => [
'name' => $name, 'label' => $label, 'type' => 'select', 'options' => $options, 'currency' => false,
];
$fields = [];
if ($tab === 'taobao' || $tab === 'jd') {
$fields[] = $select('store_platform', '类型', self::tabValues('fund-management', 'store-receiving-accounts', $tab));
}
$fields[] = $text('erp_store_name', 'ERP店铺名称');
$fields[] = $text('shop_name', '店铺名称');
if ($tab === 'jd') {
$fields[] = $text('store_id', '店铺id');
}
$fields[] = $select('store_status', '店铺状态', ['正常', '关店中', '已关店']);
$fields[] = $text('store_entity_name', '店铺主体');
$fields[] = $text('original_store_entity', '原店铺主体');
if ($tab === 'taobao') {
$fields[] = $select('account_status', '状态', ['已维护', '待维护']);
$fields[] = $text('login_account', '登录账号');
$fields[] = $text('login_password', '登录密码');
$fields[] = $text('payment_password', '支付密码');
$fields[] = $select('withdrawal_path', '提现路径', ['公户', '网商银行', '支付宝']);
$fields[] = $text('withdrawal_account_name', '提现账户名称');
$fields[] = $text('alipay_or_bank_account', '支付宝账号');
} elseif ($tab === 'jd') {
$fields[] = $text('login_account', '登录账号');
$fields[] = $text('login_password', '登录密码');
$fields[] = $text('payment_verification_method', '支付验证方式');
$fields[] = $text('financial_account', '金融账号');
$fields[] = $text('financial_password', '金融密码');
$fields[] = $text('payment_verification_method_secondary', '支付验证方式');
$fields[] = $select('account_status', '状态', ['已维护', '待维护']);
$fields[] = $select('withdrawal_path', '提现路径', ['公户', '网商银行', '支付宝']);
$fields[] = $text('withdrawal_account_name', '提现账户名称');
$fields[] = $text('bank_card_last_four', '银行卡账号(后四位)');
} else {
$fields[] = $select('account_status', '状态', ['已维护', '待维护']);
$fields[] = $text('login_account', '登录账号');
$fields[] = $text('login_password', '登录密码');
$fields[] = $text('payment_password', '支付密码');
$fields[] = $select('withdrawal_path', '提现路径', ['公户', '网商银行', '支付宝']);
$fields[] = $text('withdrawal_account_name', '提现账户名称');
$fields[] = $text('bank_card_last_four', '银行卡账号(后四位)');
}
return $fields;
}
}