Merge remote-tracking branch 'origin/main' into feature/WuYongJie-marketing-account-jd-ledgers
# Conflicts: # resources/views/business/pages/show.blade.php
This commit is contained in:
commit
8ceae88719
181
app/Business/BusinessTableImporter.php
Normal file
181
app/Business/BusinessTableImporter.php
Normal file
@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\Business;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||||
|
||||
class BusinessTableImporter
|
||||
{
|
||||
public function import(
|
||||
UploadedFile $file,
|
||||
TableDescriptor $descriptor,
|
||||
string $section,
|
||||
string $page,
|
||||
?string $tab = null,
|
||||
): array {
|
||||
$spreadsheet = IOFactory::load($file->getRealPath());
|
||||
[$rows, $headerRow, $columnMap] = $this->bestMatchingSheet($spreadsheet, $descriptor);
|
||||
|
||||
if ($rows === null || $columnMap === []) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => '未找到可识别的表头,请使用当前页面显示的中文表头。',
|
||||
]);
|
||||
}
|
||||
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$model = $descriptor->model;
|
||||
$identityFields = TableRegistry::importIdentityFields($section, $page);
|
||||
|
||||
DB::transaction(function () use (
|
||||
$rows,
|
||||
$headerRow,
|
||||
$columnMap,
|
||||
$descriptor,
|
||||
$section,
|
||||
$page,
|
||||
$tab,
|
||||
$model,
|
||||
$identityFields,
|
||||
&$created,
|
||||
&$updated,
|
||||
) {
|
||||
foreach (array_slice($rows, $headerRow + 1) as $offset => $row) {
|
||||
$data = [];
|
||||
|
||||
foreach ($columnMap as $columnIndex => $field) {
|
||||
$data[$field['name']] = $this->normalizeValue(
|
||||
$row[$columnIndex] ?? null,
|
||||
$field['type'],
|
||||
$headerRow + $offset + 2,
|
||||
$field['label'],
|
||||
);
|
||||
}
|
||||
|
||||
if (collect($data)->every(fn ($value) => $value === null || $value === '')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($descriptor->tabFilterColumn && $tab) {
|
||||
$tabValue = TableRegistry::tabValue($section, $page, $tab);
|
||||
|
||||
if ($tabValue !== null) {
|
||||
$data[$descriptor->tabFilterColumn] = $tabValue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (TableRegistry::fixedFilters($section, $page) as $column => $value) {
|
||||
$data[$column] = $value;
|
||||
}
|
||||
|
||||
$data['company_id'] = company_id() ?: 1;
|
||||
$identity = collect($identityFields)
|
||||
->filter(fn ($field) => array_key_exists($field, $data) && $data[$field] !== null && $data[$field] !== '')
|
||||
->mapWithKeys(fn ($field) => [$field => $data[$field]])
|
||||
->all();
|
||||
|
||||
$record = count($identity) === count($identityFields) && $identity !== []
|
||||
? $model::query()->where($identity)->first()
|
||||
: null;
|
||||
|
||||
if ($record) {
|
||||
$record->update($data);
|
||||
$updated++;
|
||||
} else {
|
||||
$model::create($data);
|
||||
$created++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($created + $updated === 0) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => '表格中没有可导入的数据行。',
|
||||
]);
|
||||
}
|
||||
|
||||
return compact('created', 'updated');
|
||||
}
|
||||
|
||||
private function bestMatchingSheet($spreadsheet, TableDescriptor $descriptor): array
|
||||
{
|
||||
$fieldLookup = [];
|
||||
|
||||
foreach ($descriptor->fields as $field) {
|
||||
$fieldLookup[$this->normalizeHeader($field['name'])] = $field;
|
||||
$fieldLookup[$this->normalizeHeader($field['label'])] = $field;
|
||||
}
|
||||
|
||||
$best = [null, null, []];
|
||||
$bestScore = 0;
|
||||
|
||||
foreach ($spreadsheet->getWorksheetIterator() as $worksheet) {
|
||||
$rows = $worksheet->toArray(null, true, false, false);
|
||||
|
||||
foreach (array_slice($rows, 0, 20, true) as $rowIndex => $row) {
|
||||
$columnMap = [];
|
||||
|
||||
foreach ($row as $columnIndex => $heading) {
|
||||
$key = $this->normalizeHeader($heading);
|
||||
|
||||
if ($key !== '' && isset($fieldLookup[$key])) {
|
||||
$columnMap[$columnIndex] = $fieldLookup[$key];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($columnMap) > $bestScore) {
|
||||
$best = [$rows, $rowIndex, $columnMap];
|
||||
$bestScore = count($columnMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $best;
|
||||
}
|
||||
|
||||
private function normalizeHeader($value): string
|
||||
{
|
||||
return mb_strtolower(preg_replace('/[\s\x{FEFF}_\-()()]+/u', '', trim((string) $value)));
|
||||
}
|
||||
|
||||
private function normalizeValue($value, string $type, int $rowNumber, string $label)
|
||||
{
|
||||
if ($value === null || trim((string) $value) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($type === 'date') {
|
||||
try {
|
||||
return is_numeric($value)
|
||||
? ExcelDate::excelToDateTimeObject($value)->format('Y-m-d')
|
||||
: \Illuminate\Support\Carbon::parse($value)->format('Y-m-d');
|
||||
} catch (\Throwable $e) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => "第 {$rowNumber} 行“{$label}”不是有效日期。",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($type, ['number', 'currency'], true)) {
|
||||
$number = str_replace([',', ',', '¥', '¥'], '', (string) $value);
|
||||
|
||||
if (! is_numeric($number)) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => "第 {$rowNumber} 行“{$label}”不是有效数字。",
|
||||
]);
|
||||
}
|
||||
|
||||
return (float) $number;
|
||||
}
|
||||
|
||||
if ($type === 'boolean') {
|
||||
return in_array(mb_strtolower(trim((string) $value)), ['1', '是', 'true', 'yes', 'y'], true) ? 1 : 0;
|
||||
}
|
||||
|
||||
return trim((string) $value);
|
||||
}
|
||||
}
|
||||
19
app/Business/TableDescriptor.php
Normal file
19
app/Business/TableDescriptor.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Business;
|
||||
|
||||
/**
|
||||
* Immutable descriptor used by the Blade views to render a CRUD table.
|
||||
*/
|
||||
class TableDescriptor
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $model,
|
||||
public readonly string $table,
|
||||
public readonly array $fields,
|
||||
public readonly ?string $tab = null,
|
||||
public readonly ?string $tabFilterColumn = null,
|
||||
public readonly bool $readOnly = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
347
app/Business/TableRegistry.php
Normal file
347
app/Business/TableRegistry.php
Normal file
@ -0,0 +1,347 @@
|
||||
<?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\StoreChange;
|
||||
use App\Models\Business\StoreChangeLog;
|
||||
use App\Models\Business\StoreClosing;
|
||||
use App\Models\Business\StoreInformation;
|
||||
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/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/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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$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',
|
||||
'store-management/basic-information',
|
||||
'trademark-management/information',
|
||||
], 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-alipay' => '淘宝',
|
||||
'taobao-wechat' => '淘宝',
|
||||
'pinduoduo' => '拼多多',
|
||||
'jd-wallet' => '京东',
|
||||
'douyin' => '抖音',
|
||||
'dewu' => '得物',
|
||||
'vipshop' => '唯品会',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
208
app/Business/UpstreamBusinessInformation.php
Normal file
208
app/Business/UpstreamBusinessInformation.php
Normal file
@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace App\Business;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||||
|
||||
class UpstreamBusinessInformation
|
||||
{
|
||||
private const SOURCE = 'operations_replica';
|
||||
|
||||
public function __construct(private readonly UpstreamQueryCacheFactory $caches)
|
||||
{
|
||||
}
|
||||
|
||||
public function paginate(string $section, string $page, int $perPage = 50): LengthAwarePaginator
|
||||
{
|
||||
$currentPage = max((int) request('page', 1), 1);
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$key = "{$section}/{$page}";
|
||||
|
||||
$payload = match ($key) {
|
||||
'company-management/basic-information' => $this->companyPage($currentPage, $perPage),
|
||||
'store-management/basic-information' => $this->storePage($currentPage, $perPage),
|
||||
'trademark-management/information' => $this->trademarkPage($currentPage, $perPage),
|
||||
default => ['data' => [], 'total' => 0],
|
||||
};
|
||||
|
||||
return new LengthAwarePaginator(
|
||||
array_map(static fn (array $row) => (object) $row, $payload['data']),
|
||||
(int) $payload['total'],
|
||||
$perPage,
|
||||
$currentPage,
|
||||
[
|
||||
'path' => request()->url(),
|
||||
'query' => request()->query(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function companyPage(int $page, int $perPage): array
|
||||
{
|
||||
return $this->remember('company-information.read', $page, $perPage, function (UpstreamReadOnlyQuery $query) use ($page, $perPage) {
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$rows = $query->select(<<<SQL
|
||||
SELECT
|
||||
Id AS id,
|
||||
unifiedSocialCreditCode AS unified_social_credit_code,
|
||||
businessLicenseName AS company_name,
|
||||
registeredCapital AS subscribed_capital,
|
||||
businessLicenseType AS company_type,
|
||||
IncorporationDate AS establishment_date,
|
||||
legalRepresentative AS legal_representative,
|
||||
registeredAddress AS registered_address,
|
||||
businessScope AS business_scope,
|
||||
accountSerialNumber AS basic_deposit_account_number,
|
||||
companyStatus AS company_status,
|
||||
financialManager AS financial_officer,
|
||||
supervisor AS supervisor,
|
||||
taxCategoryAndType AS taxpayer_category,
|
||||
NULL AS shareholders
|
||||
FROM rpt.fys_companyInfo
|
||||
ORDER BY Id DESC
|
||||
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY
|
||||
SQL);
|
||||
|
||||
return [
|
||||
'data' => array_map(fn (array $row) => $this->mapCompany($row), $rows),
|
||||
'total' => (int) $query->scalar('SELECT COUNT(*) FROM rpt.fys_companyInfo'),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function storePage(int $page, int $perPage): array
|
||||
{
|
||||
return $this->remember('store-information.read', $page, $perPage, function (UpstreamReadOnlyQuery $query) use ($page, $perPage) {
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$rows = $query->select(<<<SQL
|
||||
SELECT
|
||||
s.Id AS id,
|
||||
s.openTime AS opened_at,
|
||||
s.erpShopName AS erp_store_name,
|
||||
s.unifiedSocialCreditCode AS unified_social_credit_code,
|
||||
c.businessLicenseName AS company_name,
|
||||
s.trademarkNumber AS trademark_number,
|
||||
NULL AS trademark_name,
|
||||
s.platform AS platform,
|
||||
s.responsibleDepartment AS profit_center,
|
||||
s.shopHead AS store_manager,
|
||||
s.shopState AS store_status,
|
||||
s.shopType AS store_type,
|
||||
s.channelType AS channel_mode
|
||||
FROM rpt.fys_shopInfo AS s
|
||||
OUTER APPLY (
|
||||
SELECT TOP 1 company.businessLicenseName
|
||||
FROM rpt.fys_companyInfo AS company
|
||||
WHERE company.unifiedSocialCreditCode = NULLIF(s.unifiedSocialCreditCode, '')
|
||||
ORDER BY company.Id DESC
|
||||
) AS c
|
||||
ORDER BY s.Id DESC
|
||||
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY
|
||||
SQL);
|
||||
|
||||
return [
|
||||
'data' => array_map(fn (array $row) => $this->mapStore($row), $rows),
|
||||
'total' => (int) $query->scalar('SELECT COUNT(*) FROM rpt.fys_shopInfo'),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function trademarkPage(int $page, int $perPage): array
|
||||
{
|
||||
return $this->remember('trademark-information.read', $page, $perPage, function (UpstreamReadOnlyQuery $query) use ($page, $perPage) {
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$rows = $query->select(<<<SQL
|
||||
SELECT
|
||||
Id AS id,
|
||||
trademarkName AS trademark_name,
|
||||
internationalClassification AS trademark_type,
|
||||
trademarkStatus AS trademark_status,
|
||||
registrationDate AS registration_date,
|
||||
termOfValidity AS expiry_date,
|
||||
remark AS notes
|
||||
FROM rpt.fys_TrademarkInfo
|
||||
WHERE isDelete = 0
|
||||
ORDER BY Id DESC
|
||||
OFFSET {$offset} ROWS FETCH NEXT {$perPage} ROWS ONLY
|
||||
SQL);
|
||||
|
||||
return [
|
||||
'data' => array_map(fn (array $row) => $this->mapTrademark($row), $rows),
|
||||
'total' => (int) $query->scalar('SELECT COUNT(*) FROM rpt.fys_TrademarkInfo WHERE isDelete = 0'),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function remember(string $scope, int $page, int $perPage, callable $resolver): array
|
||||
{
|
||||
$cache = $this->caches->forSource(self::SOURCE);
|
||||
|
||||
return $cache->remember($scope, compact('page', 'perPage'), function () use ($resolver) {
|
||||
return $resolver(new UpstreamReadOnlyQuery(self::SOURCE));
|
||||
});
|
||||
}
|
||||
|
||||
private function mapCompany(array $row): array
|
||||
{
|
||||
$row = $this->toUtf8($row);
|
||||
$row['establishment_date'] = $this->date($row['establishment_date'] ?? null);
|
||||
$row['company_status'] = match ((string) ($row['company_status'] ?? '')) {
|
||||
'0' => '正常',
|
||||
'1' => '已注销',
|
||||
default => $row['company_status'] ?? null,
|
||||
};
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function mapStore(array $row): array
|
||||
{
|
||||
$row = $this->toUtf8($row);
|
||||
$row['opened_at'] = $this->date($row['opened_at'] ?? null);
|
||||
$row['store_status'] = match ((string) ($row['store_status'] ?? '')) {
|
||||
'0' => '正常',
|
||||
'3' => '关闭中',
|
||||
'6' => '已关闭',
|
||||
default => $row['store_status'] ?? null,
|
||||
};
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function mapTrademark(array $row): array
|
||||
{
|
||||
$row = $this->toUtf8($row);
|
||||
$row['registration_date'] = $this->date($row['registration_date'] ?? null);
|
||||
$row['expiry_date'] = $this->date($row['expiry_date'] ?? null);
|
||||
$row['trademark_status'] = match ((string) ($row['trademark_status'] ?? '')) {
|
||||
'0' => '未启用',
|
||||
'1' => '有效',
|
||||
default => $row['trademark_status'] ?? null,
|
||||
};
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function toUtf8(array $row): array
|
||||
{
|
||||
foreach ($row as &$value) {
|
||||
if (is_string($value) && ! mb_check_encoding($value, 'UTF-8')) {
|
||||
$value = mb_convert_encoding($value, 'UTF-8', 'GB18030');
|
||||
}
|
||||
}
|
||||
unset($value);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function date($value): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return substr((string) $value, 0, 10);
|
||||
}
|
||||
}
|
||||
@ -2,9 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers\Business;
|
||||
|
||||
use App\Business\BusinessTableImporter;
|
||||
use App\Business\TableRegistry;
|
||||
use App\Business\UpstreamBusinessInformation;
|
||||
use App\Models\Banking\MarketingAccountBill;
|
||||
use App\Models\StoreReceivables\StoreZhongcaiReconciliation;
|
||||
use App\Utilities\SidebarPermissions;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
@ -36,6 +40,8 @@ class Pages extends Controller
|
||||
private readonly SupplierPayableReportRepository $supplierPayableReports,
|
||||
private readonly UpstreamQueryCacheFactory $upstreamCaches,
|
||||
) {
|
||||
$this->middleware('auth');
|
||||
$this->middleware('company.identify');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,6 +51,7 @@ class Pages extends Controller
|
||||
'taobao-alipay', 'taobao-wechat', 'pinduoduo',
|
||||
'jd-wallet', 'douyin', 'dewu', 'vipshop',
|
||||
];
|
||||
|
||||
public function home()
|
||||
{
|
||||
$section = collect(SidebarPermissions::navigationFor(user()))->first();
|
||||
@ -76,12 +83,7 @@ class Pages extends Controller
|
||||
if ($pages->isEmpty()) {
|
||||
abort_if($page !== null || $tab !== null, 404);
|
||||
|
||||
return view('business.pages.show', [
|
||||
'section' => $sectionConfig,
|
||||
'page' => null,
|
||||
'tabs' => collect(),
|
||||
'activeTab' => null,
|
||||
]);
|
||||
return view('business.pages.show', $this->viewData($sectionConfig));
|
||||
}
|
||||
|
||||
if ($page === null) {
|
||||
@ -97,7 +99,6 @@ class Pages extends Controller
|
||||
|
||||
abort_if(empty($pageConfig), 404);
|
||||
|
||||
// 如果该 page 是一个 children-only 的父级分组,访问它时跳到第一个子页面
|
||||
if (! empty($pageConfig['children']) && empty($pageConfig['tabs'])) {
|
||||
return $this->redirectToPage($sectionConfig, $pageConfig['children'][0]);
|
||||
}
|
||||
@ -107,18 +108,13 @@ class Pages extends Controller
|
||||
if ($tabs->isEmpty()) {
|
||||
abort_if($tab !== null, 404);
|
||||
|
||||
$content = $this->resolveContent($page, null);
|
||||
|
||||
return view('business.pages.show', [
|
||||
'section' => $sectionConfig,
|
||||
'page' => $pageConfig,
|
||||
'tabs' => $tabs,
|
||||
'activeTab' => null,
|
||||
'content' => $content,
|
||||
'edgeBanding' => $this->edgeBandingContent($page),
|
||||
'inventoryData' => $this->inventoryContent($page),
|
||||
'rawBoardData' => $this->rawBoardContent($page),
|
||||
]);
|
||||
return view('business.pages.show', $this->viewData(
|
||||
$sectionConfig,
|
||||
$pageConfig,
|
||||
$tabs,
|
||||
null,
|
||||
$this->resolveContent($page, null),
|
||||
));
|
||||
}
|
||||
|
||||
if ($tab === null) {
|
||||
@ -129,23 +125,94 @@ class Pages extends Controller
|
||||
|
||||
abort_if(empty($activeTab), 404);
|
||||
|
||||
$content = $this->resolveContent($page, $tab);
|
||||
|
||||
return view('business.pages.show', [
|
||||
'section' => $sectionConfig,
|
||||
'page' => $pageConfig,
|
||||
'tabs' => $tabs,
|
||||
'activeTab' => $activeTab,
|
||||
'content' => $content,
|
||||
'edgeBanding' => $this->edgeBandingContent($page),
|
||||
'inventoryData' => $this->inventoryContent($page),
|
||||
'rawBoardData' => $this->rawBoardContent($page),
|
||||
]);
|
||||
return view('business.pages.show', $this->viewData(
|
||||
$sectionConfig,
|
||||
$pageConfig,
|
||||
$tabs,
|
||||
$activeTab,
|
||||
$this->resolveContent($page, $tab),
|
||||
));
|
||||
}
|
||||
|
||||
protected function viewData(
|
||||
array $section,
|
||||
?array $page = null,
|
||||
$tabs = null,
|
||||
?array $activeTab = null,
|
||||
?array $content = null,
|
||||
): array {
|
||||
$descriptor = null;
|
||||
$rows = collect();
|
||||
$tabValue = null;
|
||||
$tabColumn = null;
|
||||
|
||||
if ($page && $content === null) {
|
||||
$descriptor = TableRegistry::describe(
|
||||
$section['slug'],
|
||||
$page['slug'],
|
||||
$activeTab['slug'] ?? null,
|
||||
);
|
||||
|
||||
if ($descriptor) {
|
||||
if ($descriptor->readOnly) {
|
||||
$rows = app(UpstreamBusinessInformation::class)->paginate(
|
||||
$section['slug'],
|
||||
$page['slug'],
|
||||
50,
|
||||
);
|
||||
|
||||
return [
|
||||
'section' => $section,
|
||||
'page' => $page,
|
||||
'tabs' => $tabs ?? collect(),
|
||||
'activeTab' => $activeTab,
|
||||
'content' => $content,
|
||||
'descriptor' => $descriptor,
|
||||
'rows' => $rows,
|
||||
'tabColumn' => null,
|
||||
'tabValue' => null,
|
||||
'edgeBanding' => $this->edgeBandingContent($page['slug']),
|
||||
'inventoryData' => $this->inventoryContent($page['slug']),
|
||||
'rawBoardData' => $this->rawBoardContent($page['slug']),
|
||||
];
|
||||
}
|
||||
|
||||
$query = ($descriptor->model)::query();
|
||||
|
||||
if ($descriptor->tabFilterColumn && $activeTab) {
|
||||
$tabValue = TableRegistry::tabValue($section['slug'], $page['slug'], $activeTab['slug']);
|
||||
|
||||
if ($tabValue !== null) {
|
||||
$query->where($descriptor->tabFilterColumn, $tabValue);
|
||||
}
|
||||
|
||||
$tabColumn = $descriptor->tabFilterColumn;
|
||||
}
|
||||
|
||||
foreach (TableRegistry::fixedFilters($section['slug'], $page['slug']) as $column => $value) {
|
||||
$query->where($column, $value);
|
||||
}
|
||||
|
||||
$rows = $query->orderByDesc('id')->paginate(50)->withQueryString();
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'section' => $section,
|
||||
'page' => $page,
|
||||
'tabs' => $tabs ?? collect(),
|
||||
'activeTab' => $activeTab,
|
||||
'content' => $content,
|
||||
'descriptor' => $descriptor,
|
||||
'rows' => $rows,
|
||||
'tabColumn' => $tabColumn,
|
||||
'tabValue' => $tabValue,
|
||||
'edgeBanding' => $page ? $this->edgeBandingContent($page['slug']) : null,
|
||||
'inventoryData' => $page ? $this->inventoryContent($page['slug']) : null,
|
||||
'rawBoardData' => $page ? $this->rawBoardContent($page['slug']) : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据页面和标签加载对应的业务内容数据
|
||||
*/
|
||||
protected function resolveContent(string $page, ?string $tab): ?array
|
||||
{
|
||||
return match ($page) {
|
||||
@ -609,7 +676,7 @@ class Pages extends Controller
|
||||
/**
|
||||
* 种菜应收对账 - 加载对账数据(21字段)
|
||||
*/
|
||||
protected function contentForZhongcaiReconciliation(): ?array
|
||||
protected function contentForZhongcaiReconciliation(): array
|
||||
{
|
||||
return [
|
||||
'view' => 'store_receivables.zhongcai_reconciliations.partials.embedded-table',
|
||||
@ -619,13 +686,9 @@ class Pages extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 营销账户账单 - 加载指定平台的数据
|
||||
* 淘系支付宝、拼多多使用独立的流水账单表,其他平台使用通用汇总账单表
|
||||
*/
|
||||
protected function contentForMarketingAccountBills(string $tab): ?array
|
||||
protected function contentForMarketingAccountBills(?string $tab): ?array
|
||||
{
|
||||
if (!in_array($tab, self::MARKETING_PLATFORMS, true)) {
|
||||
if (! in_array($tab, self::MARKETING_PLATFORMS, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -779,7 +842,6 @@ class Pages extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
// 其他平台 - 使用通用汇总账单表(15字段)
|
||||
return [
|
||||
'view' => 'banking.marketing_account_bills.partials.embedded-table',
|
||||
'platform' => $tab,
|
||||
@ -790,6 +852,86 @@ class Pages extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
public function store(Request $request, string $section, string $page, ?string $tab = null)
|
||||
{
|
||||
$descriptor = TableRegistry::describe($section, $page, $tab);
|
||||
abort_if(! $descriptor, 404);
|
||||
|
||||
$data = $request->validate($this->validationRules($descriptor));
|
||||
|
||||
if ($descriptor->tabFilterColumn && $tab && ($tabValue = TableRegistry::tabValue($section, $page, $tab))) {
|
||||
$data[$descriptor->tabFilterColumn] = $tabValue;
|
||||
}
|
||||
|
||||
foreach (TableRegistry::fixedFilters($section, $page) as $column => $value) {
|
||||
$data[$column] = $value;
|
||||
}
|
||||
|
||||
($descriptor->model)::create($data);
|
||||
|
||||
return $this->redirectBack($section, $page, $tab);
|
||||
}
|
||||
|
||||
public function import(
|
||||
Request $request,
|
||||
BusinessTableImporter $importer,
|
||||
string $section,
|
||||
string $page,
|
||||
?string $tab = null,
|
||||
) {
|
||||
$descriptor = TableRegistry::describe($section, $page, $tab);
|
||||
abort_if(! $descriptor, 404);
|
||||
|
||||
$request->validate([
|
||||
'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:10240'],
|
||||
]);
|
||||
|
||||
$result = $importer->import($request->file('import'), $descriptor, $section, $page, $tab);
|
||||
|
||||
return $this->redirectBack($section, $page, $tab)->with(
|
||||
'success',
|
||||
"导入完成:新增 {$result['created']} 条,更新 {$result['updated']} 条。",
|
||||
);
|
||||
}
|
||||
|
||||
public function update(Request $request, string $section, string $page, $id, ?string $tab = null)
|
||||
{
|
||||
$descriptor = TableRegistry::describe($section, $page, $tab);
|
||||
abort_if(! $descriptor, 404);
|
||||
|
||||
$row = ($descriptor->model)::findOrFail($id);
|
||||
$row->update($request->validate($this->validationRules($descriptor)));
|
||||
|
||||
return $this->redirectBack($section, $page, $tab);
|
||||
}
|
||||
|
||||
public function destroy(string $section, string $page, $id, ?string $tab = null)
|
||||
{
|
||||
$descriptor = TableRegistry::describe($section, $page, $tab);
|
||||
abort_if(! $descriptor, 404);
|
||||
|
||||
($descriptor->model)::findOrFail($id)->delete();
|
||||
|
||||
return $this->redirectBack($section, $page, $tab);
|
||||
}
|
||||
|
||||
protected function validationRules($descriptor): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
foreach ($descriptor->fields as $field) {
|
||||
$rules[$field['name']] = match ($field['type']) {
|
||||
'number', 'currency' => ['nullable', 'numeric'],
|
||||
'boolean' => ['nullable', 'boolean'],
|
||||
'date' => ['nullable', 'date'],
|
||||
'select' => ['nullable', 'string', 'max:64'],
|
||||
default => ['nullable', 'string', 'max:255'],
|
||||
};
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
protected function redirectToPage(array $section, array $page)
|
||||
{
|
||||
$parameters = [
|
||||
@ -816,4 +958,19 @@ class Pages extends Controller
|
||||
|
||||
return checkdate($month, $day, $year) ? $date : '';
|
||||
}
|
||||
|
||||
protected function redirectBack(string $section, string $page, ?string $tab)
|
||||
{
|
||||
$parameters = [
|
||||
'company_id' => company_id() ?: 1,
|
||||
'section' => $section,
|
||||
'page' => $page,
|
||||
];
|
||||
|
||||
if ($tab) {
|
||||
$parameters['tab'] = $tab;
|
||||
}
|
||||
|
||||
return redirect()->route('business.pages.show', $parameters)->with('success', true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ class ShowInAdmin
|
||||
$menu = $event->menu;
|
||||
|
||||
foreach (SidebarPermissions::navigationFor(user()) as $sectionIndex => $section) {
|
||||
$pages = $section['pages'] ?? [];
|
||||
$pages = $this->sortByTitleLength($section['pages'] ?? []);
|
||||
$order = ($sectionIndex + 1) * 10;
|
||||
|
||||
if (empty($pages)) {
|
||||
@ -44,7 +44,7 @@ class ShowInAdmin
|
||||
$parameters['tab'] = $page['tabs'][0]['slug'];
|
||||
}
|
||||
|
||||
$children = $page['children'] ?? [];
|
||||
$children = $this->sortByTitleLength($page['children'] ?? []);
|
||||
|
||||
if (! empty($children)) {
|
||||
// 父级 page 渲染为 dropdown:标题可点击跳到第一个子页面,子项作为嵌套菜单
|
||||
@ -103,4 +103,24 @@ class ShowInAdmin
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function sortByTitleLength(array $items): array
|
||||
{
|
||||
$indexed = [];
|
||||
|
||||
foreach (array_values($items) as $index => $item) {
|
||||
$indexed[] = [
|
||||
'item' => $item,
|
||||
'index' => $index,
|
||||
'length' => mb_strlen((string) ($item['title'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
usort($indexed, static function (array $left, array $right): int {
|
||||
return ($left['length'] <=> $right['length'])
|
||||
?: ($left['index'] <=> $right['index']);
|
||||
});
|
||||
|
||||
return array_column($indexed, 'item');
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,4 +13,18 @@ class BankAccount extends Model
|
||||
protected $casts = [
|
||||
'company_id' => 'integer',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'account_name' => '账户名称',
|
||||
'account_type' => '账户类型',
|
||||
'bank_account_number' => '银行账号',
|
||||
'opening_bank' => '开户行',
|
||||
'ukey_password_maker' => 'UKey密码(制单)',
|
||||
'login_password_maker' => '登录密码(制单)',
|
||||
'ukey_password_reviewer' => 'UKey密码(审核)',
|
||||
'login_password_reviewer' => '登录密码(审核)',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
68
app/Models/Business/BusinessModel.php
Normal file
68
app/Models/Business/BusinessModel.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Business;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Base class for all "business finance" tables that mirror the Excel sheets
|
||||
* the user supplied (店铺后台税务数据、财务SW规划表、实际SW统计表 etc.).
|
||||
*
|
||||
* Each table is scoped to a company (multi-tenant) and the column list drives
|
||||
* the dynamic Blade views / forms so we don't have to hand-write 11 sets of
|
||||
* controllers and templates.
|
||||
*/
|
||||
abstract class BusinessModel extends Model
|
||||
{
|
||||
public $timestamps = true;
|
||||
|
||||
protected $guarded = ['id', 'company_id', 'created_at', 'updated_at'];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::addGlobalScope('company', function ($builder) {
|
||||
if (company_id()) {
|
||||
$builder->where($builder->getModel()->getTable() . '.company_id', company_id());
|
||||
}
|
||||
});
|
||||
|
||||
static::creating(function (self $model) {
|
||||
if (empty($model->company_id)) {
|
||||
$model->company_id = company_id();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordered columns (id, then declared columns, then timestamps).
|
||||
* Used by the Blade views to render the table headers and form fields.
|
||||
*/
|
||||
public static function declaredColumns(): array
|
||||
{
|
||||
static $cache = [];
|
||||
|
||||
$key = static::class;
|
||||
if (isset($cache[$key])) {
|
||||
return $cache[$key];
|
||||
}
|
||||
|
||||
/** @var \Illuminate\Database\Schema\SQLiteBuilder $schema */
|
||||
$schema = (new static())->getConnection()->getSchemaBuilder();
|
||||
$columns = $schema->getColumnListing((new static())->getTable());
|
||||
|
||||
// Re-order: primary key first, then declared columns, then timestamps.
|
||||
$ordered = array_values(array_filter($columns, function (string $col) {
|
||||
return ! in_array($col, ['id', 'company_id', 'created_at', 'updated_at'], true);
|
||||
}));
|
||||
|
||||
return $cache[$key] = ['id' => 'id'] + array_combine($ordered, $ordered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column human-readable labels (overridable per model).
|
||||
*/
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
54
app/Models/Business/CompanyInformation.php
Normal file
54
app/Models/Business/CompanyInformation.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Business;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
|
||||
class CompanyInformation extends Model
|
||||
{
|
||||
protected $table = 'business_company_information';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'unified_social_credit_code',
|
||||
'company_name',
|
||||
'subscribed_capital',
|
||||
'company_type',
|
||||
'establishment_date',
|
||||
'legal_representative',
|
||||
'registered_address',
|
||||
'business_scope',
|
||||
'basic_deposit_account_number',
|
||||
'company_status',
|
||||
'financial_officer',
|
||||
'supervisor',
|
||||
'taxpayer_category',
|
||||
'shareholders',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'company_id' => 'integer',
|
||||
'subscribed_capital' => 'decimal:2',
|
||||
'establishment_date' => 'date:Y-m-d',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'unified_social_credit_code' => '统一社会信用代码',
|
||||
'company_name' => '公司名称',
|
||||
'subscribed_capital' => '认缴注册资本(万元)',
|
||||
'company_type' => '类型',
|
||||
'establishment_date' => '成立日期',
|
||||
'legal_representative' => '法定代表人',
|
||||
'registered_address' => '注册地址(住所)',
|
||||
'business_scope' => '经营范围',
|
||||
'basic_deposit_account_number' => '基本存款账户编号',
|
||||
'company_status' => '公司状态',
|
||||
'financial_officer' => '财务负责人',
|
||||
'supervisor' => '监事',
|
||||
'taxpayer_category' => '纳税类别及类型',
|
||||
'shareholders' => '股东',
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Models/Business/CompanyStore.php
Normal file
28
app/Models/Business/CompanyStore.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Business;
|
||||
|
||||
class CompanyStore extends BusinessModel
|
||||
{
|
||||
protected $table = 'biz_company_stores';
|
||||
|
||||
protected $casts = [
|
||||
'store_revenue' => 'decimal:2',
|
||||
'company_revenue' => 'decimal:2',
|
||||
'trigger_store_change' => 'boolean',
|
||||
'trigger_company_register' => 'boolean',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'company_name' => '公司名称',
|
||||
'store_name' => '店铺名称',
|
||||
'store_revenue' => '店铺业绩',
|
||||
'company_revenue' => '公司业绩',
|
||||
'trigger_store_change' => '是否触发店铺变更流程',
|
||||
'trigger_company_register' => '是否触发公司注册流程',
|
||||
'period' => '期间',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -15,4 +15,16 @@ class CompanyStoreData extends Model
|
||||
'trigger_store_change' => 'boolean',
|
||||
'trigger_company_registration' => 'boolean',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'company_name' => '公司名称',
|
||||
'store_name' => '店铺名称',
|
||||
'store_performance' => '店铺业绩',
|
||||
'company_performance' => '公司业绩',
|
||||
'trigger_store_change' => '是否触发店铺变更流程',
|
||||
'trigger_company_registration' => '是否触发公司注册流程',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
19
app/Models/Business/ExpressReceiving.php
Normal file
19
app/Models/Business/ExpressReceiving.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Business;
|
||||
|
||||
class ExpressReceiving extends BusinessModel
|
||||
{
|
||||
protected $table = 'ak_biz_express_receiving';
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'express_name' => '快递名称',
|
||||
'receiving_type' => '收款类型',
|
||||
'receiving_name' => '收款名称',
|
||||
'account_number' => '收款账号',
|
||||
'bank_name' => '开户行',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -13,4 +13,53 @@ class FundForecast extends Model
|
||||
protected $casts = [
|
||||
'company_id' => 'integer',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'budget_month' => '预算月份',
|
||||
'actual_month' => '实际月份',
|
||||
'store_balance_budget' => '店铺余额(预算)',
|
||||
'store_balance_actual' => '店铺余额(实际)',
|
||||
'bank_balance_budget' => '银行余额(预算)',
|
||||
'bank_balance_actual' => '银行余额(实际)',
|
||||
'other_balance_budget' => '其他余额(预算)',
|
||||
'other_balance_actual' => '其他余额(实际)',
|
||||
'expected_monthly_return_budget' => '预计回款(预算)',
|
||||
'expected_monthly_return_actual' => '预计回款(实际)',
|
||||
'income_total_budget' => '收入合计(预算)',
|
||||
'income_total_actual' => '收入合计(实际)',
|
||||
'prior_unpaid_budget' => '往期未付(预算)',
|
||||
'prior_unpaid_actual' => '往期未付(实际)',
|
||||
'advance_budget' => '借支(预算)',
|
||||
'advance_actual' => '借支(实际)',
|
||||
'supplier_budget' => '供应商(预算)',
|
||||
'supplier_actual' => '供应商(实际)',
|
||||
'spot_settlement_budget' => '现结(预算)',
|
||||
'spot_settlement_actual' => '现结(实际)',
|
||||
'courier_budget' => '快递(预算)',
|
||||
'courier_actual' => '快递(实际)',
|
||||
'paper_factory_budget' => '纸厂压贴(预算)',
|
||||
'paper_factory_actual' => '纸厂压贴(实际)',
|
||||
'edge_band_budget' => '封边条(预算)',
|
||||
'edge_band_actual' => '封边条(实际)',
|
||||
'board_budget' => '板材(预算)',
|
||||
'board_actual' => '板材(实际)',
|
||||
'salary_budget' => '工资(预算)',
|
||||
'salary_actual' => '工资(实际)',
|
||||
'daily_expense_budget' => '日常费用(预算)',
|
||||
'daily_expense_actual' => '日常费用(实际)',
|
||||
'planting_budget' => '种菜(预算)',
|
||||
'planting_actual' => '种菜(实际)',
|
||||
'red_packet_budget' => '红包(预算)',
|
||||
'red_packet_actual' => '红包(实际)',
|
||||
'corporate_deduction_budget' => '对公扣款(预算)',
|
||||
'corporate_deduction_actual' => '对公扣款(实际)',
|
||||
'expense_total_budget' => '支出合计(预算)',
|
||||
'expense_total_actual' => '支出合计(实际)',
|
||||
'bank_reservation' => '银行预留',
|
||||
'net_balance_budget' => '净结余(预算)',
|
||||
'net_balance_actual' => '净结余(实际)',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
27
app/Models/Business/InvoiceBasic.php
Normal file
27
app/Models/Business/InvoiceBasic.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Business;
|
||||
|
||||
class InvoiceBasic extends BusinessModel
|
||||
{
|
||||
protected $table = 'ak_biz_invoice_basic';
|
||||
|
||||
protected $casts = [
|
||||
'invoice_limit' => 'decimal:2',
|
||||
'output_amount' => 'decimal:2',
|
||||
'input_amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'store_name' => '店铺名称',
|
||||
'store_entity' => '店铺主体',
|
||||
'invoice_issuer' => '开票员',
|
||||
'invoice_limit' => '开票额度',
|
||||
'output_amount' => '销项金额',
|
||||
'input_amount' => '进项金额',
|
||||
'period' => '期间',
|
||||
];
|
||||
}
|
||||
}
|
||||
57
app/Models/Business/StoreInformation.php
Normal file
57
app/Models/Business/StoreInformation.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Business;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
|
||||
class StoreInformation extends Model
|
||||
{
|
||||
protected $table = 'business_store_information';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'opened_at',
|
||||
'erp_store_name',
|
||||
'unified_social_credit_code',
|
||||
'company_name',
|
||||
'trademark_number',
|
||||
'trademark_name',
|
||||
'platform',
|
||||
'profit_center',
|
||||
'store_manager',
|
||||
'store_status',
|
||||
'store_type',
|
||||
'channel_mode',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'company_id' => 'integer',
|
||||
'opened_at' => 'date:Y-m-d',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'opened_at' => '开店时间',
|
||||
'erp_store_name' => 'ERP系统店铺名称',
|
||||
'unified_social_credit_code' => '公司社会统一信用码',
|
||||
'company_name' => '公司名称',
|
||||
'trademark_number' => '商标号',
|
||||
'trademark_name' => '商标名称',
|
||||
'platform' => '平台',
|
||||
'profit_center' => '店铺所属利润中心',
|
||||
'store_manager' => '店铺负责人',
|
||||
'store_status' => '店铺状态',
|
||||
'store_type' => '店铺类型',
|
||||
'channel_mode' => '渠道模式',
|
||||
];
|
||||
}
|
||||
|
||||
public static function fieldTypes(): array
|
||||
{
|
||||
return [
|
||||
'opened_at' => 'date',
|
||||
'platform' => 'text',
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Models/Business/StoreInvoice.php
Normal file
28
app/Models/Business/StoreInvoice.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Business;
|
||||
|
||||
class StoreInvoice extends BusinessModel
|
||||
{
|
||||
protected $table = 'ak_biz_store_invoice';
|
||||
|
||||
protected $casts = [
|
||||
'invoice_amount' => 'decimal:2',
|
||||
'invoice_date' => 'date',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'store' => '店铺',
|
||||
'seller_name' => '销售方名称',
|
||||
'seller_tax_no' => '销售方税号',
|
||||
'buyer_name' => '购买方名称',
|
||||
'buyer_tax_no' => '购买方税号',
|
||||
'invoice_amount' => '开票金额',
|
||||
'invoice_category' => '类目',
|
||||
'invoice_type' => '开票类型',
|
||||
'invoice_date' => '开票日期',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -13,4 +13,20 @@ class StoreReceivingAccount extends Model
|
||||
protected $casts = [
|
||||
'company_id' => 'integer',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'store_platform' => '店铺平台',
|
||||
'store_name' => '店铺名称',
|
||||
'wallet_number' => '钱包号',
|
||||
'store_entity_name' => '店铺主体名称',
|
||||
'login_account' => '登录账号',
|
||||
'login_password' => '登录密码',
|
||||
'receiving_method' => '收款方式',
|
||||
'receiving_account_name' => '收款账户名称',
|
||||
'alipay_or_bank_account' => '支付宝或银行卡账号',
|
||||
'opening_bank' => '开户行',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
107
app/Models/Business/SwActual.php
Normal file
107
app/Models/Business/SwActual.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Business;
|
||||
|
||||
class SwActual extends BusinessModel
|
||||
{
|
||||
protected $table = 'biz_sw_actual';
|
||||
|
||||
protected $casts = [
|
||||
'provided_revenue' => 'decimal:2',
|
||||
'declared_revenue' => 'decimal:2',
|
||||
'goods_special_amount' => 'decimal:2',
|
||||
'goods_special_tax' => 'decimal:2',
|
||||
'goods_normal_amount' => 'decimal:2',
|
||||
'goods_normal_tax' => 'decimal:2',
|
||||
'goods_unbilled_amount' => 'decimal:2',
|
||||
'goods_unbilled_tax' => 'decimal:2',
|
||||
'goods_subtotal_amount' => 'decimal:2',
|
||||
'goods_subtotal_tax' => 'decimal:2',
|
||||
'service_special_amount' => 'decimal:2',
|
||||
'service_special_tax' => 'decimal:2',
|
||||
'service_normal_amount' => 'decimal:2',
|
||||
'service_normal_tax' => 'decimal:2',
|
||||
'service_unbilled_amount' => 'decimal:2',
|
||||
'service_unbilled_tax' => 'decimal:2',
|
||||
'service_subtotal_amount' => 'decimal:2',
|
||||
'service_subtotal_tax' => 'decimal:2',
|
||||
'total_revenue' => 'decimal:2',
|
||||
'total_tax' => 'decimal:2',
|
||||
'input_goods_amount' => 'decimal:2',
|
||||
'input_goods_tax' => 'decimal:2',
|
||||
'input_service_amount' => 'decimal:2',
|
||||
'input_service_tax' => 'decimal:2',
|
||||
'input_other_amount' => 'decimal:2',
|
||||
'input_other_tax' => 'decimal:2',
|
||||
'input_subtotal_amount' => 'decimal:2',
|
||||
'input_subtotal_tax' => 'decimal:2',
|
||||
'extra_credit' => 'decimal:2',
|
||||
'retained_credit' => 'decimal:2',
|
||||
'vat_payable' => 'decimal:2',
|
||||
'city_tax' => 'decimal:2',
|
||||
'education_surcharge' => 'decimal:2',
|
||||
'local_education_surcharge' => 'decimal:2',
|
||||
'stamp_tax' => 'decimal:2',
|
||||
'union_fee' => 'decimal:2',
|
||||
'individual_tax' => 'decimal:2',
|
||||
'income_tax' => 'decimal:2',
|
||||
'garbage_fee' => 'decimal:2',
|
||||
'other_penalty' => 'decimal:2',
|
||||
'total_tax_paid' => 'decimal:2',
|
||||
'tax_burden_rate' => 'decimal:4',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'region' => '区域',
|
||||
'company_type' => '公司类型',
|
||||
'contact_person' => '对接人',
|
||||
'bookkeeping_company' => '代账公司',
|
||||
'period' => '期间',
|
||||
'company_name' => '公司名称',
|
||||
'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' => '税金合计',
|
||||
'tax_burden_rate' => '税负',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -13,4 +13,58 @@ class SwActualStatistics extends Model
|
||||
protected $casts = [
|
||||
'company_id' => 'integer',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'region' => '区域',
|
||||
'company_type' => '公司类型',
|
||||
'contact_person' => '对接人',
|
||||
'bookkeeping_company' => '代账公司',
|
||||
'period' => '期间',
|
||||
'company_name' => '公司名称',
|
||||
'provided_performance' => '提供业绩',
|
||||
'actual_declared_performance' => '实际申报业绩',
|
||||
'goods_special_invoice_amount' => '商品收入(专票金额)',
|
||||
'goods_special_invoice_tax' => '商品收入(专票税额)',
|
||||
'goods_general_invoice_amount' => '商品收入(普票金额)',
|
||||
'goods_general_invoice_tax' => '商品收入(普票税额)',
|
||||
'goods_uninvoiced_amount' => '商品收入(未开票金额)',
|
||||
'goods_uninvoiced_tax' => '商品收入(未开票税额)',
|
||||
'goods_income_subtotal' => '商品收入小计',
|
||||
'goods_tax_subtotal' => '商品税金小计',
|
||||
'service_special_invoice_amount' => '服务收入(专票金额)',
|
||||
'service_special_invoice_tax' => '服务收入(专票税额)',
|
||||
'service_general_invoice_amount' => '服务收入(普票金额)',
|
||||
'service_general_invoice_tax' => '服务收入(普票税额)',
|
||||
'service_uninvoiced_amount' => '服务收入(未开票金额)',
|
||||
'service_uninvoiced_tax' => '服务收入(未开票税额)',
|
||||
'service_income_subtotal' => '服务收入小计',
|
||||
'service_tax_subtotal' => '服务税金小计',
|
||||
'total_income' => '合计收入',
|
||||
'total_tax' => '合计税金',
|
||||
'input_goods_amount' => '进项商品金额',
|
||||
'input_goods_tax' => '进项商品税额',
|
||||
'input_service_amount' => '进项服务费金额',
|
||||
'input_service_tax' => '进项服务费税额',
|
||||
'input_other_amount' => '进项其他金额',
|
||||
'input_other_tax' => '进项其他税额',
|
||||
'input_total_amount' => '进项金额合计',
|
||||
'input_total_tax' => '进项税额合计',
|
||||
'additional_deduction' => '加计抵减',
|
||||
'retained_tax' => '留抵税金',
|
||||
'monthly_vat' => '本月应交增值税',
|
||||
'urban_maintenance_tax' => '城建税金',
|
||||
'education_surcharge' => '教育费附加',
|
||||
'local_education_surcharge' => '地方教育费附加',
|
||||
'stamp_tax' => '印花税',
|
||||
'union_fund' => '工会经费',
|
||||
'personal_income_tax' => '个税',
|
||||
'corporate_income_tax' => '所得税',
|
||||
'garbage_fee' => '垃圾处理费',
|
||||
'other_late_fee' => '其他(滞纳金)',
|
||||
'total_paid_tax' => '税金合计',
|
||||
'tax_burden' => '税负',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,4 +13,18 @@ class SwPlanning extends Model
|
||||
protected $casts = [
|
||||
'company_id' => 'integer',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'company_name' => '公司名称',
|
||||
'final_tax_performance' => '最终税务业绩',
|
||||
'product_cost' => '产品成本',
|
||||
'logistics_cost' => '物流成本',
|
||||
'platform_invoice' => '平台开票',
|
||||
'labor_cost' => '人工成本',
|
||||
'other_expenses' => '其他费用',
|
||||
'profit' => '利润',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,4 +13,20 @@ class TaxData extends Model
|
||||
protected $casts = [
|
||||
'company_id' => 'integer',
|
||||
];
|
||||
|
||||
public static function columnLabels(): array
|
||||
{
|
||||
return [
|
||||
'company_name' => '公司名称',
|
||||
'real_shipping_performance' => '公司真实发货业绩',
|
||||
'planting_performance' => '公司种菜业绩',
|
||||
'refund_amount' => '退款金额',
|
||||
'net_performance_income' => '业绩净收入',
|
||||
'tax_performance' => '税务业绩',
|
||||
'tax_refund' => '税务退款',
|
||||
'tax_net_income' => '税务净收入',
|
||||
'performance_difference' => '业绩差额',
|
||||
'final_tax_performance' => '最终报税业绩',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ return [
|
||||
'title' => '公司管理',
|
||||
'icon' => 'business',
|
||||
'pages' => [
|
||||
['slug' => 'basic-information', 'title' => '公司信息'],
|
||||
['slug' => 'registration', 'title' => '公司注册'],
|
||||
['slug' => 'changes', 'title' => '公司变更'],
|
||||
['slug' => 'cancellation', 'title' => '公司注销'],
|
||||
@ -19,7 +20,7 @@ return [
|
||||
'icon' => 'verified',
|
||||
'pages' => [
|
||||
['slug' => 'registration', 'title' => '商标注册'],
|
||||
['slug' => 'changes', 'title' => '商标表更'],
|
||||
['slug' => 'changes', 'title' => '商标变更'],
|
||||
['slug' => 'renewal', 'title' => '商标续展'],
|
||||
['slug' => 'information', 'title' => '商标信息'],
|
||||
],
|
||||
@ -29,6 +30,7 @@ return [
|
||||
'title' => '店铺管理',
|
||||
'icon' => 'storefront',
|
||||
'pages' => [
|
||||
['slug' => 'basic-information', 'title' => '店铺基础信息'],
|
||||
['slug' => 'opening-process', 'title' => '开店流程'],
|
||||
['slug' => 'changes', 'title' => '店铺变更'],
|
||||
['slug' => 'closing-process', 'title' => '关店流程'],
|
||||
|
||||
@ -28,6 +28,8 @@ return [
|
||||
// 只限制冷查询和后台刷新;共享快照命中不占槽位。
|
||||
'max_concurrency' => (int) env('UPSTREAM_MAX_CONCURRENCY', 4),
|
||||
'slot_wait_ms' => (int) env('UPSTREAM_SLOT_WAIT_MS', 1500),
|
||||
// 开发机可覆盖系统数据库中记录的 ODBC 驱动名。
|
||||
'odbc_driver' => env('UPSTREAM_ODBC_DRIVER'),
|
||||
],
|
||||
|
||||
'stored_configuration' => [
|
||||
|
||||
@ -0,0 +1,276 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// 1. 店铺后台税务数据
|
||||
Schema::create('ak_biz_tax_data', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('company_name')->default('')->comment('公司名称');
|
||||
$table->decimal('real_ship_revenue', 18, 2)->default(0)->comment('公司真实发货业绩');
|
||||
$table->decimal('zhongcai_revenue', 18, 2)->default(0)->comment('公司种菜业绩');
|
||||
$table->decimal('refund_amount', 18, 2)->default(0)->comment('退款金额');
|
||||
$table->decimal('net_revenue', 18, 2)->default(0)->comment('业绩净收入');
|
||||
$table->decimal('tax_revenue', 18, 2)->default(0)->comment('税务业绩');
|
||||
$table->decimal('tax_refund', 18, 2)->default(0)->comment('税务退款');
|
||||
$table->decimal('tax_net_revenue', 18, 2)->default(0)->comment('税务净收入');
|
||||
$table->decimal('revenue_diff', 18, 2)->default(0)->comment('业绩差额');
|
||||
$table->decimal('final_tax_revenue', 18, 2)->default(0)->comment('最终报税业绩');
|
||||
$table->string('period')->default('')->comment('期间,例如 2026-07');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 2. 财务SW规划表
|
||||
Schema::create('ak_biz_sw_planning', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('company_name')->default('');
|
||||
$table->decimal('final_tax_revenue', 18, 2)->default(0)->comment('最终报税业绩');
|
||||
$table->decimal('product_cost', 18, 2)->default(0)->comment('商品成本');
|
||||
$table->decimal('logistics_cost', 18, 2)->default(0)->comment('物流成本');
|
||||
$table->decimal('platform_invoice', 18, 2)->default(0)->comment('平台来票');
|
||||
$table->decimal('labor_cost', 18, 2)->default(0)->comment('人工');
|
||||
$table->decimal('other_reimbursement', 18, 2)->default(0)->comment('其他报销');
|
||||
$table->decimal('profit', 18, 2)->default(0)->comment('利润');
|
||||
$table->string('period')->default('');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 3. 实际SW统计表(多级表头,扁平化存储)
|
||||
Schema::create('biz_sw_actual', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('region')->default('')->comment('区域');
|
||||
$table->string('company_type')->default('')->comment('公司类型');
|
||||
$table->string('contact_person')->default('')->comment('对接人');
|
||||
$table->string('bookkeeping_company')->default('')->comment('代账公司');
|
||||
$table->string('period')->default('')->comment('期间');
|
||||
$table->string('company_name')->default('')->comment('公司名称');
|
||||
$table->decimal('provided_revenue', 18, 2)->default(0)->comment('提供业绩');
|
||||
$table->decimal('declared_revenue', 18, 2)->default(0)->comment('实际申报业绩');
|
||||
// 商品收入 13%
|
||||
$table->decimal('goods_special_amount', 18, 2)->default(0)->comment('商品专票金额');
|
||||
$table->decimal('goods_special_tax', 18, 2)->default(0)->comment('商品专票税额');
|
||||
$table->decimal('goods_normal_amount', 18, 2)->default(0)->comment('商品普票金额');
|
||||
$table->decimal('goods_normal_tax', 18, 2)->default(0)->comment('商品普票税额');
|
||||
$table->decimal('goods_unbilled_amount', 18, 2)->default(0)->comment('商品未开票金额');
|
||||
$table->decimal('goods_unbilled_tax', 18, 2)->default(0)->comment('商品未开票税额');
|
||||
$table->decimal('goods_subtotal_amount', 18, 2)->default(0)->comment('商品收入小计');
|
||||
$table->decimal('goods_subtotal_tax', 18, 2)->default(0)->comment('商品税金小计');
|
||||
// 服务收入 6%
|
||||
$table->decimal('service_special_amount', 18, 2)->default(0)->comment('服务专票金额');
|
||||
$table->decimal('service_special_tax', 18, 2)->default(0)->comment('服务专票税额');
|
||||
$table->decimal('service_normal_amount', 18, 2)->default(0)->comment('服务普票金额');
|
||||
$table->decimal('service_normal_tax', 18, 2)->default(0)->comment('服务普票税额');
|
||||
$table->decimal('service_unbilled_amount', 18, 2)->default(0)->comment('服务未开票金额');
|
||||
$table->decimal('service_unbilled_tax', 18, 2)->default(0)->comment('服务未开票税额');
|
||||
$table->decimal('service_subtotal_amount', 18, 2)->default(0)->comment('服务收入小计');
|
||||
$table->decimal('service_subtotal_tax', 18, 2)->default(0)->comment('服务税金小计');
|
||||
$table->decimal('total_revenue', 18, 2)->default(0)->comment('合计收入');
|
||||
$table->decimal('total_tax', 18, 2)->default(0)->comment('合计税金');
|
||||
// 进项发票
|
||||
$table->decimal('input_goods_amount', 18, 2)->default(0)->comment('进项商品金额');
|
||||
$table->decimal('input_goods_tax', 18, 2)->default(0)->comment('进项商品税额');
|
||||
$table->decimal('input_service_amount', 18, 2)->default(0)->comment('进项服务费金额');
|
||||
$table->decimal('input_service_tax', 18, 2)->default(0)->comment('进项服务费税额');
|
||||
$table->decimal('input_other_amount', 18, 2)->default(0)->comment('进项其他金额');
|
||||
$table->decimal('input_other_tax', 18, 2)->default(0)->comment('进项其他税额');
|
||||
$table->decimal('input_subtotal_amount', 18, 2)->default(0)->comment('进项金额合计');
|
||||
$table->decimal('input_subtotal_tax', 18, 2)->default(0)->comment('进项税额合计');
|
||||
$table->decimal('extra_credit', 18, 2)->default(0)->comment('加计抵减');
|
||||
$table->decimal('retained_credit', 18, 2)->default(0)->comment('留抵税金');
|
||||
$table->decimal('vat_payable', 18, 2)->default(0)->comment('本月应交增值税');
|
||||
$table->decimal('city_tax', 18, 2)->default(0)->comment('城建税金');
|
||||
$table->decimal('education_surcharge', 18, 2)->default(0)->comment('教育费附加');
|
||||
$table->decimal('local_education_surcharge', 18, 2)->default(0)->comment('地方教育费附加');
|
||||
$table->decimal('stamp_tax', 18, 2)->default(0)->comment('印花税');
|
||||
$table->decimal('union_fee', 18, 2)->default(0)->comment('工会经费');
|
||||
$table->decimal('individual_tax', 18, 2)->default(0)->comment('个税');
|
||||
$table->decimal('income_tax', 18, 2)->default(0)->comment('所得税');
|
||||
$table->decimal('garbage_fee', 18, 2)->default(0)->comment('垃圾处理费');
|
||||
$table->decimal('other_penalty', 18, 2)->default(0)->comment('其他(滞纳金)');
|
||||
$table->decimal('total_tax_paid', 18, 2)->default(0)->comment('税金合计');
|
||||
$table->decimal('tax_burden_rate', 8, 4)->default(0)->comment('税负');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 4. 公司店铺数据
|
||||
Schema::create('biz_company_stores', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('company_name')->default('')->comment('公司名称');
|
||||
$table->string('store_name')->default('')->comment('店铺名称');
|
||||
$table->decimal('store_revenue', 18, 2)->default(0)->comment('店铺业绩');
|
||||
$table->decimal('company_revenue', 18, 2)->default(0)->comment('公司业绩');
|
||||
$table->boolean('trigger_store_change')->default(false)->comment('是否触发店铺变更流程');
|
||||
$table->boolean('trigger_company_register')->default(false)->comment('是否触发公司注册流程');
|
||||
$table->string('period')->default('');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 5. 店铺变更统计表
|
||||
Schema::create('ak_biz_store_changes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('store_name')->default('')->comment('店铺名称');
|
||||
$table->string('store_entity')->default('')->comment('店铺主体');
|
||||
$table->date('change_date')->nullable()->comment('变更时间');
|
||||
$table->string('receiving_account')->default('')->comment('收款账户信息');
|
||||
$table->string('previous_entity')->default('')->comment('历史主体');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 6. 发票基础信息+发票数据报表
|
||||
Schema::create('ak_biz_invoice_basic', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('store_name')->default('')->comment('店铺名称');
|
||||
$table->string('store_entity')->default('')->comment('店铺主体');
|
||||
$table->string('invoice_issuer')->default('')->comment('开票员');
|
||||
$table->decimal('invoice_limit', 18, 2)->default(0)->comment('开票额度');
|
||||
$table->decimal('output_amount', 18, 2)->default(0)->comment('销项金额');
|
||||
$table->decimal('input_amount', 18, 2)->default(0)->comment('进项金额');
|
||||
$table->string('period')->default('');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 7. 店铺后台开票
|
||||
Schema::create('ak_biz_store_invoice', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('store')->default('')->comment('店铺');
|
||||
$table->string('seller_name')->default('')->comment('销售方名称');
|
||||
$table->string('seller_tax_no')->default('')->comment('销售方税号');
|
||||
$table->string('buyer_name')->default('')->comment('购买方名称');
|
||||
$table->string('buyer_tax_no')->default('')->comment('购买方税号');
|
||||
$table->decimal('invoice_amount', 18, 2)->default(0)->comment('开票金额');
|
||||
$table->string('invoice_category')->default('')->comment('类目');
|
||||
$table->string('invoice_type')->default('')->comment('开票类型,普票/专票');
|
||||
$table->date('invoice_date')->nullable()->comment('开票日期');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 8. 资金预测表(月度资金收支预算表)
|
||||
Schema::create('ak_biz_fund_forecast', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('period')->default('')->comment('预算填报月份');
|
||||
$table->string('actual_period')->default('')->comment('实际填报月份');
|
||||
// 收入明细
|
||||
$table->decimal('store_balance_budget', 18, 2)->default(0)->comment('店铺余额预算');
|
||||
$table->decimal('store_balance_actual', 18, 2)->default(0)->comment('店铺余额实际');
|
||||
$table->decimal('bank_balance_budget', 18, 2)->default(0)->comment('银行余额预算');
|
||||
$table->decimal('bank_balance_actual', 18, 2)->default(0)->comment('银行余额实际');
|
||||
$table->decimal('other_balance_budget', 18, 2)->default(0)->comment('其他账户余额预算');
|
||||
$table->decimal('other_balance_actual', 18, 2)->default(0)->comment('其他账户余额实际');
|
||||
$table->decimal('expected_inbound_budget', 18, 2)->default(0)->comment('预计本月回款预算');
|
||||
$table->decimal('expected_inbound_actual', 18, 2)->default(0)->comment('预计本月回款实际');
|
||||
$table->decimal('income_total_budget', 18, 2)->default(0)->comment('收入合计预算');
|
||||
$table->decimal('income_total_actual', 18, 2)->default(0)->comment('收入合计实际');
|
||||
// 支出明细
|
||||
$table->decimal('previous_unpaid_budget', 18, 2)->default(0)->comment('往期未付预算');
|
||||
$table->decimal('previous_unpaid_actual', 18, 2)->default(0)->comment('往期未付实际');
|
||||
$table->decimal('advance_budget', 18, 2)->default(0)->comment('借支预算');
|
||||
$table->decimal('advance_actual', 18, 2)->default(0)->comment('借支实际');
|
||||
$table->decimal('supplier_budget', 18, 2)->default(0)->comment('供应商预算');
|
||||
$table->decimal('supplier_actual', 18, 2)->default(0)->comment('供应商实际');
|
||||
$table->decimal('cash_budget', 18, 2)->default(0)->comment('现结预算');
|
||||
$table->decimal('cash_actual', 18, 2)->default(0)->comment('现结实际');
|
||||
$table->decimal('express_budget', 18, 2)->default(0)->comment('快递预算');
|
||||
$table->decimal('express_actual', 18, 2)->default(0)->comment('快递实际');
|
||||
$table->decimal('paper_press_budget', 18, 2)->default(0)->comment('纸厂压贴预算');
|
||||
$table->decimal('paper_press_actual', 18, 2)->default(0)->comment('纸厂压贴实际');
|
||||
$table->decimal('edge_banding_budget', 18, 2)->default(0)->comment('封边条预算');
|
||||
$table->decimal('edge_banding_actual', 18, 2)->default(0)->comment('封边条实际');
|
||||
$table->decimal('board_budget', 18, 2)->default(0)->comment('板材、三合板预算');
|
||||
$table->decimal('board_actual', 18, 2)->default(0)->comment('板材、三合板实际');
|
||||
$table->decimal('salary_budget', 18, 2)->default(0)->comment('工资预算');
|
||||
$table->decimal('salary_actual', 18, 2)->default(0)->comment('工资实际');
|
||||
$table->decimal('daily_expense_budget', 18, 2)->default(0)->comment('日常费用预算');
|
||||
$table->decimal('daily_expense_actual', 18, 2)->default(0)->comment('日常费用实际');
|
||||
$table->decimal('zhongcai_budget', 18, 2)->default(0)->comment('种菜预算');
|
||||
$table->decimal('zhongcai_actual', 18, 2)->default(0)->comment('种菜实际');
|
||||
$table->decimal('red_packet_budget', 18, 2)->default(0)->comment('红包预算');
|
||||
$table->decimal('red_packet_actual', 18, 2)->default(0)->comment('红包实际');
|
||||
$table->decimal('corporate_deduction_budget', 18, 2)->default(0)->comment('对公扣款预算');
|
||||
$table->decimal('corporate_deduction_actual', 18, 2)->default(0)->comment('对公扣款实际');
|
||||
$table->decimal('expense_total_budget', 18, 2)->default(0)->comment('支出合计预算');
|
||||
$table->decimal('expense_total_actual', 18, 2)->default(0)->comment('支出合计实际');
|
||||
// 资金结余
|
||||
$table->decimal('reserved_balance_budget', 18, 2)->default(0)->comment('银行账户、店铺预留预算');
|
||||
$table->decimal('reserved_balance_actual', 18, 2)->default(0)->comment('银行账户、店铺预留实际');
|
||||
$table->decimal('net_balance_budget', 18, 2)->default(0)->comment('本月净结余预算');
|
||||
$table->decimal('net_balance_actual', 18, 2)->default(0)->comment('本月净结余实际');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 9. 店铺收款账户信息
|
||||
Schema::create('ak_biz_store_receiving_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('platform')->default('')->comment('店铺平台');
|
||||
$table->string('store_name')->default('')->comment('店铺名称');
|
||||
$table->string('wallet_no')->default('')->comment('钱包号');
|
||||
$table->string('store_entity')->default('')->comment('店铺主体名称');
|
||||
$table->string('login_account')->default('')->comment('登录账号');
|
||||
$table->string('login_password')->default('')->comment('登录密码');
|
||||
$table->string('receiving_method')->default('')->comment('收款方式');
|
||||
$table->string('receiving_account_name')->default('')->comment('收款账户名称');
|
||||
$table->string('account_number')->default('')->comment('支付宝账号/银行账号');
|
||||
$table->string('bank_name')->default('')->comment('开户银行');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 10. 银行账户信息
|
||||
Schema::create('ak_biz_bank_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('account_name')->default('')->comment('账户名称');
|
||||
$table->string('account_type')->default('')->comment('账户类型:基本户/一般户/个人账户/个人微信/个人支付宝');
|
||||
$table->string('bank_account')->default('')->comment('银行账号');
|
||||
$table->string('bank_name')->default('')->comment('开户行');
|
||||
$table->string('maker_ukey_password')->default('')->comment('ukey密码(制单)');
|
||||
$table->string('maker_login_password')->default('')->comment('登录密码(制单)');
|
||||
$table->string('auditor_ukey_password')->default('')->comment('ukey密码(审核)');
|
||||
$table->string('auditor_login_password')->default('')->comment('登录密码(审核)');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 11. 快递收款系统
|
||||
Schema::create('ak_biz_express_receiving', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('company_id')->constrained('companies')->cascadeOnDelete();
|
||||
$table->string('express_name')->default('')->comment('快递名称');
|
||||
$table->string('receiving_type')->default('')->comment('收款类型:对公/个人');
|
||||
$table->string('receiving_name')->default('')->comment('收款名称');
|
||||
$table->string('account_number')->default('')->comment('收款账号');
|
||||
$table->string('bank_name')->default('')->comment('开户行');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach ([
|
||||
'ak_biz_tax_data',
|
||||
'ak_biz_sw_planning',
|
||||
'biz_sw_actual',
|
||||
'biz_company_stores',
|
||||
'ak_biz_store_changes',
|
||||
'ak_biz_invoice_basic',
|
||||
'ak_biz_store_invoice',
|
||||
'ak_biz_fund_forecast',
|
||||
'ak_biz_store_receiving_accounts',
|
||||
'ak_biz_bank_accounts',
|
||||
'ak_biz_express_receiving',
|
||||
] as $table) {
|
||||
Schema::dropIfExists($table);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('business_company_information', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('company_id')->default(1)->index();
|
||||
$table->string('unified_social_credit_code')->nullable()->comment('统一社会信用代码');
|
||||
$table->string('company_name')->nullable()->comment('公司名称');
|
||||
$table->decimal('subscribed_capital', 15, 2)->nullable()->comment('认缴注册资本(万元)');
|
||||
$table->string('company_type')->nullable()->comment('类型');
|
||||
$table->date('establishment_date')->nullable()->comment('成立日期');
|
||||
$table->string('legal_representative')->nullable()->comment('法定代表人');
|
||||
$table->string('registered_address')->nullable()->comment('注册地址(住所)');
|
||||
$table->text('business_scope')->nullable()->comment('经营范围');
|
||||
$table->string('basic_deposit_account_number')->nullable()->comment('基本存款账户编号');
|
||||
$table->string('company_status')->nullable()->comment('公司状态');
|
||||
$table->string('financial_officer')->nullable()->comment('财务负责人');
|
||||
$table->string('supervisor')->nullable()->comment('监事');
|
||||
$table->string('taxpayer_category')->nullable()->comment('纳税类别及类型');
|
||||
$table->text('shareholders')->nullable()->comment('股东');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::create('business_store_information', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('company_id')->default(1)->index();
|
||||
$table->date('opened_at')->nullable()->comment('开店时间');
|
||||
$table->string('erp_store_name')->nullable()->comment('ERP系统店铺名称');
|
||||
$table->string('unified_social_credit_code')->nullable()->comment('公司社会统一信用码');
|
||||
$table->string('company_name')->nullable()->comment('公司名称');
|
||||
$table->string('trademark_number')->nullable()->comment('商标号');
|
||||
$table->string('trademark_name')->nullable()->comment('商标名称');
|
||||
$table->string('platform')->nullable()->comment('平台');
|
||||
$table->string('profit_center')->nullable()->comment('店铺所属利润中心');
|
||||
$table->string('store_manager')->nullable()->comment('店铺负责人');
|
||||
$table->string('store_status')->nullable()->comment('店铺状态');
|
||||
$table->string('store_type')->nullable()->comment('店铺类型');
|
||||
$table->string('channel_mode')->nullable()->comment('渠道模式');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('business_store_information');
|
||||
Schema::dropIfExists('business_company_information');
|
||||
}
|
||||
};
|
||||
@ -145,7 +145,7 @@ PHP;
|
||||
$db = trim((string) ($connection['database'] ?? ''));
|
||||
$user = trim((string) ($connection['username'] ?? ''));
|
||||
$pass = (string) ($connection['password'] ?? '');
|
||||
$driver = trim((string) ($connection['odbc_driver'] ?? 'FreeTDS'), '{} ');
|
||||
$driver = self::resolveDriver((string) ($connection['odbc_driver'] ?? 'FreeTDS'));
|
||||
$tdsVersion = trim((string) ($connection['tds_version'] ?? '7.4'));
|
||||
$encryption = strtolower(trim((string) ($connection['encryption'] ?? 'off')));
|
||||
$connectTimeout = min(max((int) ($connection['connect_timeout'] ?? 5), 1), 30);
|
||||
@ -167,17 +167,30 @@ PHP;
|
||||
|
||||
$applicationIntent = $applicationIntent === '' ? '' : 'ReadOnly';
|
||||
|
||||
$dsn = sprintf(
|
||||
'odbc:Driver={%s};Server=%s;Port=%d;Database=%s;UID=%s;PWD=%s;TDS_Version=%s;ClientCharset=UTF-8;Connect Timeout=%d',
|
||||
str_replace('}', '}}', $driver),
|
||||
self::quoteOdbcValue($host),
|
||||
$port,
|
||||
self::quoteOdbcValue($db),
|
||||
self::quoteOdbcValue($user),
|
||||
self::quoteOdbcValue($pass),
|
||||
$tdsVersion,
|
||||
$connectTimeout
|
||||
);
|
||||
if (strcasecmp($driver, 'FreeTDS') === 0) {
|
||||
$dsn = sprintf(
|
||||
'odbc:Driver={%s};Server=%s;Port=%d;Database=%s;UID=%s;PWD=%s;TDS_Version=%s;ClientCharset=UTF-8;Connect Timeout=%d',
|
||||
str_replace('}', '}}', $driver),
|
||||
self::quoteOdbcValue($host),
|
||||
$port,
|
||||
self::quoteOdbcValue($db),
|
||||
self::quoteOdbcValue($user),
|
||||
self::quoteOdbcValue($pass),
|
||||
$tdsVersion,
|
||||
$connectTimeout
|
||||
);
|
||||
} else {
|
||||
$dsn = sprintf(
|
||||
'odbc:Driver={%s};Server=%s;Database=%s;UID=%s;PWD=%s;Connection Timeout=%d',
|
||||
str_replace('}', '}}', $driver),
|
||||
self::quoteOdbcValue($host . ',' . $port),
|
||||
self::quoteOdbcValue($db),
|
||||
self::quoteOdbcValue($user),
|
||||
self::quoteOdbcValue($pass),
|
||||
$connectTimeout
|
||||
);
|
||||
}
|
||||
|
||||
$dsn .= self::encryptionAttributes($driver, $encryption);
|
||||
|
||||
if ($applicationIntent !== '') {
|
||||
@ -238,4 +251,21 @@ PHP;
|
||||
? $attributes
|
||||
: $attributes . ';TrustServerCertificate=yes';
|
||||
}
|
||||
|
||||
private static function resolveDriver(string $configured): string
|
||||
{
|
||||
$override = trim((string) config('upstreams.runtime.odbc_driver', ''), '{} ');
|
||||
|
||||
if ($override !== '') {
|
||||
return $override;
|
||||
}
|
||||
|
||||
$configured = trim($configured, '{} ');
|
||||
|
||||
if (PHP_OS_FAMILY === 'Windows' && strcasecmp($configured, 'FreeTDS') === 0) {
|
||||
return 'SQL Server';
|
||||
}
|
||||
|
||||
return $configured;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<x-layouts.admin>
|
||||
<x-slot name="title">
|
||||
{{ $page['title'] ?? $section['title'] }}
|
||||
{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@ -102,12 +102,272 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.business-blank-page {
|
||||
min-height: 34rem;
|
||||
margin-top: 1.5rem;
|
||||
.biz-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #eef0f4;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 6px 18px rgba(31, 41, 55, 0.04);
|
||||
}
|
||||
|
||||
.biz-alert {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 0.5rem;
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.biz-alert-error {
|
||||
border-color: #fecaca;
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.biz-import-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.biz-import-input {
|
||||
max-width: 15rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.biz-card + .biz-card {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.biz-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.biz-card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.biz-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.biz-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.biz-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.biz-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #f9fafb;
|
||||
color: #4b5563;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.biz-table tbody td {
|
||||
padding: 0.65rem 1rem;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
color: #1f2937;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.biz-table tbody tr:hover td {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.biz-table .num {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.biz-table .actions-cell {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.biz-empty {
|
||||
padding: 4rem 1rem;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.biz-empty .material-icons-outlined {
|
||||
font-size: 3rem;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.biz-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.4rem 0.85rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
transition: background 150ms ease, color 150ms ease;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.biz-btn-primary {
|
||||
background: #4f46e5;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.biz-btn-primary:hover {
|
||||
background: #4338ca;
|
||||
}
|
||||
|
||||
.biz-btn-ghost {
|
||||
background: transparent;
|
||||
color: #4b5563;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.biz-btn-ghost:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.biz-btn-danger-ghost {
|
||||
background: transparent;
|
||||
color: #dc2626;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.biz-btn-danger-ghost:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.biz-badge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.biz-badge-yes { background: #dcfce7; color: #15803d; }
|
||||
.biz-badge-no { background: #f3f4f6; color: #6b7280; }
|
||||
|
||||
/* Modal */
|
||||
.biz-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
display: none;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 4rem 1rem;
|
||||
z-index: 60;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.biz-modal.is-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.biz-modal-card {
|
||||
background: #ffffff;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.25);
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.biz-modal-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.biz-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.biz-form-grid .full {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.biz-field-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.biz-field-input,
|
||||
.biz-field-select,
|
||||
.biz-field-textarea {
|
||||
width: 100%;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0.65rem;
|
||||
font-size: 0.875rem;
|
||||
color: #111827;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.biz-field-input:focus,
|
||||
.biz-field-select:focus,
|
||||
.biz-field-textarea:focus {
|
||||
border-color: #6366f1;
|
||||
outline: 2px solid rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
|
||||
.biz-modal-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.biz-pagination {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
color: #4b5563;
|
||||
font-size: 0.85rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.biz-pagination a,
|
||||
.biz-pagination span {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.biz-pagination a:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.business-content-section {
|
||||
@ -134,6 +394,13 @@
|
||||
'my-6',
|
||||
'business-page--marketing-account-bills' => $isMarketingAccountBillsPage,
|
||||
])>
|
||||
@if (is_string(session('success')))
|
||||
<div class="biz-alert" role="status">{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
@if ($errors->has('import'))
|
||||
<div class="biz-alert biz-alert-error" role="alert">{{ $errors->first('import') }}</div>
|
||||
@endif
|
||||
@if ($tabs->isNotEmpty())
|
||||
<nav class="business-tab-bar" aria-label="{{ $page['title'] }}">
|
||||
@foreach ($tabs as $tab)
|
||||
@ -192,8 +459,258 @@
|
||||
</section>
|
||||
@elseif ($pageView)
|
||||
@include($pageView)
|
||||
@elseif ($descriptor)
|
||||
<div class="biz-card mt-6">
|
||||
<div class="biz-card-head">
|
||||
<div class="biz-card-title">
|
||||
<span class="material-icons-outlined" aria-hidden="true">table_view</span>
|
||||
<span>{{ $activeTab['title'] ?? $page['title'] }} · 数据列表</span>
|
||||
@if ($descriptor->readOnly)
|
||||
<span class="biz-badge biz-badge-yes">工作平台数据库</span>
|
||||
@endif
|
||||
</div>
|
||||
@unless ($descriptor->readOnly)
|
||||
<div class="biz-card-actions">
|
||||
<form
|
||||
class="biz-import-form"
|
||||
method="POST"
|
||||
enctype="multipart/form-data"
|
||||
action="{{ route('business.pages.import', [
|
||||
'section' => $section['slug'],
|
||||
'page' => $page['slug'],
|
||||
'tab' => $activeTab['slug'] ?? null,
|
||||
]) }}"
|
||||
>
|
||||
@csrf
|
||||
<input class="biz-import-input" type="file" name="import" accept=".xlsx,.xls,.csv" required>
|
||||
<button type="submit" class="biz-btn biz-btn-ghost" title="导入 Excel 或 CSV">
|
||||
<span class="material-icons-outlined" style="font-size:1.1rem">upload_file</span>
|
||||
<span>导入</span>
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="biz-btn biz-btn-primary" onclick="bizOpenCreateModal()">
|
||||
<span class="material-icons-outlined" style="font-size:1.1rem">add</span>
|
||||
<span>新增</span>
|
||||
</button>
|
||||
</div>
|
||||
@endunless
|
||||
</div>
|
||||
|
||||
<div class="biz-table-wrap">
|
||||
<table class="biz-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
@foreach ($descriptor->fields as $field)
|
||||
<th class="@if ($field['currency']) num @endif">{{ $field['label'] }}</th>
|
||||
@endforeach
|
||||
@unless ($descriptor->readOnly)
|
||||
<th class="actions-cell">操作</th>
|
||||
@endunless
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($rows as $row)
|
||||
<tr>
|
||||
<td>{{ $row->id }}</td>
|
||||
@foreach ($descriptor->fields as $field)
|
||||
@php
|
||||
$val = $row->{$field['name']};
|
||||
@endphp
|
||||
<td class="@if ($field['currency']) num @endif">
|
||||
@if ($field['type'] === 'boolean')
|
||||
@if ($val)
|
||||
<span class="biz-badge biz-badge-yes">是</span>
|
||||
@else
|
||||
<span class="biz-badge biz-badge-no">否</span>
|
||||
@endif
|
||||
@elseif ($field['type'] === 'date' && $val)
|
||||
{{ \Illuminate\Support\Carbon::parse($val)->format('Y-m-d') }}
|
||||
@elseif ($field['currency'])
|
||||
{{ number_format((float) $val, 2) }}
|
||||
@else
|
||||
{{ $val }}
|
||||
@endif
|
||||
</td>
|
||||
@endforeach
|
||||
@unless ($descriptor->readOnly)
|
||||
<td class="actions-cell">
|
||||
<button type="button" class="biz-btn biz-btn-ghost" title="编辑" onclick='bizOpenEditModal({{ json_encode($row->only(array_merge(['id'], array_map(fn($f) => $f['name'], $descriptor->fields)))) }})'>
|
||||
<span class="material-icons-outlined" style="font-size:1.1rem">edit</span>
|
||||
</button>
|
||||
<form method="POST" action="{{ route('business.pages.destroy', [
|
||||
'section' => $section['slug'],
|
||||
'page' => $page['slug'],
|
||||
'id' => $row->id,
|
||||
'tab' => $activeTab['slug'] ?? null,
|
||||
]) }}" style="display:inline" onsubmit="return confirm('确认删除这条记录?')">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="biz-btn biz-btn-danger-ghost">
|
||||
<span class="material-icons-outlined" style="font-size:1.1rem">delete</span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
@endunless
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ count($descriptor->fields) + ($descriptor->readOnly ? 1 : 2) }}">
|
||||
<div class="biz-empty">
|
||||
<span class="material-icons-outlined" aria-hidden="true">inbox</span>
|
||||
<div>{{ $descriptor->readOnly ? '暂无工作平台数据' : '暂无数据,点击右上角"新增"开始录入' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="biz-pagination">
|
||||
<span>共 {{ $rows->total() }} 条 · 第 {{ $rows->currentPage() }} / {{ $rows->lastPage() }} 页</span>
|
||||
<span>
|
||||
@if ($rows->onFirstPage())
|
||||
<span>‹ 上一页</span>
|
||||
@else
|
||||
<a href="{{ $rows->previousPageUrl() }}">‹ 上一页</a>
|
||||
@endif
|
||||
@if ($rows->hasMorePages())
|
||||
<a href="{{ $rows->nextPageUrl() }}">下一页 ›</a>
|
||||
@else
|
||||
<span>下一页 ›</span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@unless ($descriptor->readOnly)
|
||||
{{-- Generic create/edit modal --}}
|
||||
<div id="biz-modal" class="biz-modal" onclick="if(event.target===this)bizCloseModal()">
|
||||
<div class="biz-modal-card">
|
||||
<div class="biz-modal-title" id="biz-modal-title">新增记录</div>
|
||||
<form id="biz-form" method="POST" action="">
|
||||
@csrf
|
||||
<input type="hidden" name="_method" value="POST" id="biz-form-method">
|
||||
<div class="biz-form-grid">
|
||||
@foreach ($descriptor->fields as $field)
|
||||
<div class="{{ $field['type'] === 'textarea' ? 'full' : '' }}">
|
||||
<label class="biz-field-label">{{ $field['label'] }}</label>
|
||||
@switch($field['type'])
|
||||
@case('boolean')
|
||||
<select class="biz-field-select" name="{{ $field['name'] }}" data-default="">
|
||||
<option value="0">否</option>
|
||||
<option value="1">是</option>
|
||||
</select>
|
||||
@break
|
||||
@case('select')
|
||||
<select class="biz-field-select" name="{{ $field['name'] }}" data-default="">
|
||||
<option value="">— 请选择 —</option>
|
||||
@foreach ($field['options'] ?? [] as $opt)
|
||||
<option value="{{ $opt }}">{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@break
|
||||
@case('date')
|
||||
<input type="date" class="biz-field-input" name="{{ $field['name'] }}" data-default="">
|
||||
@break
|
||||
@case('currency')
|
||||
@case('number')
|
||||
<input type="number" step="0.01" class="biz-field-input" name="{{ $field['name'] }}" data-default="">
|
||||
@break
|
||||
@case('textarea')
|
||||
<textarea class="biz-field-textarea" name="{{ $field['name'] }}" rows="3" data-default=""></textarea>
|
||||
@break
|
||||
@default
|
||||
<input type="text" class="biz-field-input" name="{{ $field['name'] }}" data-default="">
|
||||
@endswitch
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="biz-modal-foot">
|
||||
<button type="button" class="biz-btn biz-btn-ghost" onclick="bizCloseModal()">取消</button>
|
||||
<button type="submit" class="biz-btn biz-btn-primary">
|
||||
<span class="material-icons-outlined" style="font-size:1.1rem">save</span>
|
||||
<span>保存</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__bizDescriptor = {
|
||||
section: {{ json_encode($section['slug']) }},
|
||||
page: {{ json_encode($page['slug']) }},
|
||||
tab: {{ json_encode($activeTab['slug'] ?? null) }},
|
||||
storeUrl: "{{ route('business.pages.store', ['section' => $section['slug'], 'page' => $page['slug'], 'tab' => $activeTab['slug'] ?? null]) }}",
|
||||
updateBase: "{{ route('business.pages.update', ['section' => $section['slug'], 'page' => $page['slug'], 'id' => '__ID__', 'tab' => $activeTab['slug'] ?? null]) }}".replace('__ID__', ''),
|
||||
fields: {{ json_encode($descriptor->fields) }}
|
||||
};
|
||||
|
||||
function bizOpenModal() {
|
||||
var m = document.getElementById('biz-modal');
|
||||
m.classList.add('is-open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function bizCloseModal() {
|
||||
var m = document.getElementById('biz-modal');
|
||||
m.classList.remove('is-open');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function bizResetForm() {
|
||||
var form = document.getElementById('biz-form');
|
||||
form.querySelectorAll('[data-default]').forEach(function (el) {
|
||||
el.value = el.getAttribute('data-default') || '';
|
||||
});
|
||||
}
|
||||
|
||||
function bizOpenCreateModal() {
|
||||
document.getElementById('biz-modal-title').textContent = '新增记录';
|
||||
var form = document.getElementById('biz-form');
|
||||
form.action = window.__bizDescriptor.storeUrl;
|
||||
document.getElementById('biz-form-method').value = 'POST';
|
||||
bizResetForm();
|
||||
bizOpenModal();
|
||||
}
|
||||
|
||||
function bizOpenEditModal(rowData) {
|
||||
document.getElementById('biz-modal-title').textContent = '编辑记录 # ' + rowData.id;
|
||||
var form = document.getElementById('biz-form');
|
||||
form.action = window.__bizDescriptor.updateBase + rowData.id;
|
||||
document.getElementById('biz-form-method').value = 'PATCH';
|
||||
|
||||
form.querySelectorAll('[data-default]').forEach(function (el) {
|
||||
var name = el.getAttribute('name');
|
||||
var val = rowData[name] === null ? '' : rowData[name];
|
||||
// Format dates to YYYY-MM-DD for the date input
|
||||
if (el.type === 'date' && val) {
|
||||
val = String(val).substr(0, 10);
|
||||
}
|
||||
// Booleans -> "1" / "0"
|
||||
if (typeof val === 'boolean') {
|
||||
val = val ? '1' : '0';
|
||||
}
|
||||
el.value = val;
|
||||
});
|
||||
|
||||
bizOpenModal();
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') bizCloseModal();
|
||||
});
|
||||
</script>
|
||||
@endunless
|
||||
@else
|
||||
<section class="business-blank-page" aria-label="{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}"></section>
|
||||
<section class="biz-card mt-6" aria-label="{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}">
|
||||
<div class="biz-empty">
|
||||
<span class="material-icons-outlined" aria-hidden="true">description</span>
|
||||
<div>该页面暂未配置数据表,敬请期待。</div>
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
@ -64,6 +64,19 @@ Route::prefix('workspace-data/supplier-payables')
|
||||
Route::get('workspace/{section}/{page?}/{tab?}', 'Business\Pages@show')
|
||||
->name('business.pages.show');
|
||||
|
||||
Route::post('workspace/{section}/{page}/import/{tab?}', 'Business\Pages@import')
|
||||
->middleware('import')
|
||||
->name('business.pages.import');
|
||||
|
||||
Route::post('workspace/{section}/{page}/{tab?}', 'Business\Pages@store')
|
||||
->name('business.pages.store');
|
||||
|
||||
Route::patch('workspace/{section}/{page}/{id}/{tab?}', 'Business\Pages@update')
|
||||
->name('business.pages.update');
|
||||
|
||||
Route::delete('workspace/{section}/{page}/{id}/{tab?}', 'Business\Pages@destroy')
|
||||
->name('business.pages.destroy');
|
||||
|
||||
Route::group(['as' => 'system-settings.', 'prefix' => 'system-settings'], function () {
|
||||
Route::get('users', 'SystemSettings\Users@index')->name('users.index');
|
||||
Route::get('users/create', 'SystemSettings\Users@create')->name('users.create');
|
||||
|
||||
177
tests/Feature/Business/ManagementCrudTest.php
Normal file
177
tests/Feature/Business/ManagementCrudTest.php
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Business\TableRegistry;
|
||||
use App\Business\UpstreamBusinessInformation;
|
||||
use App\Models\Auth\Permission;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Mockery;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class ManagementCrudTest extends FeatureTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
}
|
||||
|
||||
public function testCompanyInformationUsesWorkbookHeadersAndReadOnlyUpstreamRows(): void
|
||||
{
|
||||
$this->mockUpstreamPage((object) [
|
||||
'id' => 1045,
|
||||
'unified_social_credit_code' => '91320404MADJRTTA0U',
|
||||
'company_name' => '测试上游公司',
|
||||
'subscribed_capital' => '10.00',
|
||||
'company_type' => '店铺-普通公司',
|
||||
'establishment_date' => '2024-04-28',
|
||||
'legal_representative' => '测试法人',
|
||||
'registered_address' => '测试地址',
|
||||
'business_scope' => '测试经营范围',
|
||||
'basic_deposit_account_number' => 'J0001',
|
||||
'company_status' => '正常',
|
||||
'financial_officer' => '测试财务',
|
||||
'supervisor' => '',
|
||||
'taxpayer_category' => '小规模纳税人',
|
||||
'shareholders' => null,
|
||||
]);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'company-management',
|
||||
'page' => 'basic-information',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('公司信息')
|
||||
->assertSeeText('认缴注册资本(万元)')
|
||||
->assertSeeText('基本存款账户编号')
|
||||
->assertSeeText('纳税类别及类型')
|
||||
->assertSeeText('测试上游公司')
|
||||
->assertSeeText('工作平台数据库')
|
||||
->assertDontSeeText('导入')
|
||||
->assertDontSeeText('新增');
|
||||
}
|
||||
|
||||
public function testStoreInformationKeepsTwelveHeadersAndShowsReadOnlyUpstreamRows(): void
|
||||
{
|
||||
$this->mockUpstreamPage((object) [
|
||||
'id' => 2047,
|
||||
'opened_at' => '2026-07-18',
|
||||
'erp_store_name' => '测试上游店铺',
|
||||
'unified_social_credit_code' => '91320412MA216EYU49',
|
||||
'company_name' => '测试关联公司',
|
||||
'trademark_number' => '35774784',
|
||||
'trademark_name' => null,
|
||||
'platform' => '天猫',
|
||||
'profit_center' => '运营中心A',
|
||||
'store_manager' => '测试负责人',
|
||||
'store_status' => '正常',
|
||||
'store_type' => '旗舰店',
|
||||
'channel_mode' => '直销',
|
||||
]);
|
||||
|
||||
$response = $this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'store-management',
|
||||
'page' => 'basic-information',
|
||||
]));
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSeeText('ERP系统店铺名称')
|
||||
->assertSeeText('公司社会统一信用码')
|
||||
->assertSeeText('渠道模式')
|
||||
->assertSeeText('测试上游店铺')
|
||||
->assertSeeText('测试关联公司')
|
||||
->assertSeeText('工作平台数据库')
|
||||
->assertDontSeeText('导入')
|
||||
->assertDontSeeText('新增');
|
||||
|
||||
preg_match_all('/<th(?:\s|>)/', $response->getContent(), $headers);
|
||||
$this->assertCount(13, $headers[0]);
|
||||
}
|
||||
|
||||
public function testTrademarkInformationShowsReadOnlyUpstreamRows(): void
|
||||
{
|
||||
$this->mockUpstreamPage((object) [
|
||||
'id' => 600,
|
||||
'trademark_name' => '测试上游商标',
|
||||
'trademark_type' => '20',
|
||||
'trademark_status' => '有效',
|
||||
'registration_date' => '2025-12-21',
|
||||
'expiry_date' => '2035-12-20',
|
||||
'notes' => null,
|
||||
]);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'trademark-management',
|
||||
'page' => 'information',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('商标名称')
|
||||
->assertSeeText('注册日期')
|
||||
->assertSeeText('测试上游商标')
|
||||
->assertSeeText('工作平台数据库')
|
||||
->assertDontSeeText('导入')
|
||||
->assertDontSeeText('新增');
|
||||
}
|
||||
|
||||
public function testTargetManagementSectionsOnlyUseChineseTableHeaders(): void
|
||||
{
|
||||
$pages = [
|
||||
['company-management', 'basic-information'],
|
||||
['company-management', 'registration'],
|
||||
['company-management', 'changes'],
|
||||
['company-management', 'cancellation'],
|
||||
['company-management', 'tax-data'],
|
||||
['company-management', 'store-data'],
|
||||
['trademark-management', 'registration'],
|
||||
['trademark-management', 'changes'],
|
||||
['trademark-management', 'renewal'],
|
||||
['trademark-management', 'information'],
|
||||
['store-management', 'basic-information'],
|
||||
['store-management', 'opening-process'],
|
||||
['store-management', 'changes'],
|
||||
['store-management', 'closing-process'],
|
||||
['store-management', 'change-log'],
|
||||
['fund-management', 'store-receiving-accounts', 'taobao-alipay'],
|
||||
['fund-management', 'bank-accounts'],
|
||||
['fund-management', 'shared-company-accounts'],
|
||||
['fund-management', 'fund-forecast'],
|
||||
['financial-sw', 'planning'],
|
||||
['financial-sw', 'actual-statistics'],
|
||||
];
|
||||
|
||||
foreach ($pages as $route) {
|
||||
[$section, $page] = $route;
|
||||
$tab = $route[2] ?? null;
|
||||
$descriptor = TableRegistry::describe($section, $page, $tab);
|
||||
|
||||
$this->assertNotNull($descriptor, "{$section}/{$page} 缺少表格描述配置");
|
||||
|
||||
if ($page === 'store-receiving-accounts') {
|
||||
$this->assertSame('store_platform', $descriptor->tabFilterColumn);
|
||||
}
|
||||
|
||||
foreach ($descriptor->fields as $field) {
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/\p{Han}/u',
|
||||
$field['label'],
|
||||
"{$section}/{$page} 的 {$field['name']} 仍在使用英文表头 {$field['label']}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function mockUpstreamPage(object $row): void
|
||||
{
|
||||
$paginator = new LengthAwarePaginator([$row], 1, 50, 1, ['path' => '/']);
|
||||
$service = Mockery::mock(UpstreamBusinessInformation::class);
|
||||
$service->shouldReceive('paginate')->once()->andReturn($paginator);
|
||||
$this->app->instance(UpstreamBusinessInformation::class, $service);
|
||||
}
|
||||
}
|
||||
@ -36,9 +36,10 @@ class NavigationTest extends FeatureTestCase
|
||||
])->assertOk();
|
||||
|
||||
$navigation = BusinessNavigation::all();
|
||||
$companyRegistration = collect($navigation[0]['pages'])->firstWhere('slug', 'registration');
|
||||
|
||||
$this->assertSame('企业档案', $navigation[0]['title']);
|
||||
$this->assertSame('企业设立', $navigation[0]['pages'][0]['title']);
|
||||
$this->assertSame('企业设立', $companyRegistration['title']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user