#feature: add test mail generator
This commit is contained in:
@@ -0,0 +1,564 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Clients\CrmClient;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 进产诊断服务
|
||||
*
|
||||
* 复刻 agent-be 中以下三处进产/放行判断逻辑,给出失败原因:
|
||||
* - App\Services\AgentCase\ConfirmProduction::canProduction
|
||||
* - App\Services\AgentBusinessDocument\ConfirmProduction::canProduction
|
||||
* - App\Services\AgentSaleDocument\ConfirmPermit::canPermit
|
||||
*
|
||||
* 通过 agentslave 库直查所需表,并通过 CrmClient 调用 CRM 接口判断一级代理账期。
|
||||
*/
|
||||
class ProductionDiagnosisService
|
||||
{
|
||||
public const TYPE_CASE = 'case';
|
||||
public const TYPE_BUSINESS = 'business_document';
|
||||
public const TYPE_SALE = 'sale_document';
|
||||
|
||||
/** @var string agent-be 数据库连接名 */
|
||||
private string $connection = 'agentslave';
|
||||
|
||||
public function __construct(private readonly CrmClient $crm)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单次诊断
|
||||
*/
|
||||
public function diagnose(string $type, string $code): array
|
||||
{
|
||||
$code = trim($code);
|
||||
$entity = $this->findEntity($type, $code);
|
||||
|
||||
if (!$entity) {
|
||||
return [
|
||||
'type' => $type,
|
||||
'type_label' => $this->typeLabel($type),
|
||||
'code' => $code,
|
||||
'found' => false,
|
||||
'message' => '未在 '.$this->tableFor($type).' 表中找到对应记录',
|
||||
];
|
||||
}
|
||||
|
||||
$operatorCode = (string) $entity->agent_code;
|
||||
$operatorAgent = $this->findAgent($operatorCode);
|
||||
$checks = [];
|
||||
|
||||
$checks['status'] = $this->checkStatus($type, $entity);
|
||||
|
||||
if ($type === self::TYPE_CASE) {
|
||||
$checks['need_pfp'] = $this->checkNeedPfp($entity);
|
||||
}
|
||||
|
||||
$checks['owner_agent'] = $this->checkOwnerAgent($type, $entity, $operatorCode);
|
||||
$checks['credit'] = $this->checkCredit($entity, $operatorAgent, $operatorCode);
|
||||
|
||||
$canProduce = collect($checks)->every(fn ($c) => ($c['pass'] ?? false) === true);
|
||||
|
||||
return [
|
||||
'type' => $type,
|
||||
'type_label' => $this->typeLabel($type),
|
||||
'code' => $code,
|
||||
'found' => true,
|
||||
'entity' => $this->normalizeEntity($type, $entity),
|
||||
'operating_agent_code' => $operatorCode,
|
||||
'operating_agent' => $operatorAgent ? [
|
||||
'code' => (string) $operatorAgent->code,
|
||||
'name' => (string) ($operatorAgent->name ?? ''),
|
||||
'level' => isset($operatorAgent->level) ? (int) $operatorAgent->level : null,
|
||||
] : null,
|
||||
'checks' => array_values($checks),
|
||||
'can_production' => $canProduce,
|
||||
];
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 实体查找
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
private function findEntity(string $type, string $code): ?object
|
||||
{
|
||||
return match ($type) {
|
||||
self::TYPE_CASE => DB::connection($this->connection)
|
||||
->table('cases')
|
||||
->where('case_code', $code)
|
||||
->where('deleted', 0)
|
||||
->first(),
|
||||
self::TYPE_BUSINESS => DB::connection($this->connection)
|
||||
->table('business_documents')
|
||||
->where('code', $code)
|
||||
->where('deleted', 0)
|
||||
->first(),
|
||||
self::TYPE_SALE => DB::connection($this->connection)
|
||||
->table('sale_documents')
|
||||
->where('code', $code)
|
||||
->where('deleted', 0)
|
||||
->first(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function findAgent(string $agentCode): ?object
|
||||
{
|
||||
if ($agentCode === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DB::connection($this->connection)
|
||||
->table('agents')
|
||||
->where('code', $agentCode)
|
||||
->where('deleted', 0)
|
||||
->first();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 各项检查
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 状态检查 - 三类单据的「期望状态」不同
|
||||
*/
|
||||
private function checkStatus(string $type, object $entity): array
|
||||
{
|
||||
[$expectedStatus, $expectedLabel] = $this->expectedStatusFor($type);
|
||||
$actualStatus = (int) $entity->status;
|
||||
$pass = $actualStatus === $expectedStatus;
|
||||
|
||||
return [
|
||||
'key' => 'status',
|
||||
'label' => '状态检查',
|
||||
'pass' => $pass,
|
||||
'expected' => sprintf('%s (%d)', $expectedLabel, $expectedStatus),
|
||||
'actual' => sprintf('%s (%d)', $this->statusLabel($type, $actualStatus), $actualStatus),
|
||||
'detail' => $pass
|
||||
? '单据状态符合进产条件'
|
||||
: '单据状态不在「'.$expectedLabel.'」,无法进产',
|
||||
'hint' => $pass ? null : '需等待单据流转至「'.$expectedLabel.'」后才能进产',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 病例放行检查 - case.is_need_pfp > 0
|
||||
*/
|
||||
private function checkNeedPfp(object $caseEntity): array
|
||||
{
|
||||
$isNeedPfp = (int) ($caseEntity->is_need_pfp ?? 0);
|
||||
$isPfp = (int) ($caseEntity->is_pfp ?? 0);
|
||||
$pass = $isNeedPfp > 0;
|
||||
|
||||
$detail = $pass
|
||||
? '病例 is_need_pfp = '.$isNeedPfp.',命中放行节点'
|
||||
: '病例 is_need_pfp = 0,不需要放行(非欠款病例,不会触发进产流程)';
|
||||
|
||||
return [
|
||||
'key' => 'need_pfp',
|
||||
'label' => '放行节点检查 (is_need_pfp)',
|
||||
'pass' => $pass,
|
||||
'expected' => 'is_need_pfp > 0',
|
||||
'actual' => 'is_need_pfp = '.$isNeedPfp.', is_pfp = '.$isPfp,
|
||||
'detail' => $detail,
|
||||
'hint' => $pass ? null : '检查 stuck_payment_reason 配置以及病例账期推算逻辑',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 归属代理检查 - 必须满足:
|
||||
* 1) entity.agent_code === operatorCode
|
||||
* 2) 结算代理表中存在 (code = entity.code, agent_code = operatorCode, deleted = 0)
|
||||
*/
|
||||
private function checkOwnerAgent(string $type, object $entity, string $operatorCode): array
|
||||
{
|
||||
$entityAgentCode = (string) $entity->agent_code;
|
||||
$settlementTable = $this->settlementTableFor($type);
|
||||
$entityCode = $this->primaryCodeOf($type, $entity);
|
||||
|
||||
$exists = DB::connection($this->connection)
|
||||
->table($settlementTable)
|
||||
->where('code', $entityCode)
|
||||
->where('agent_code', $operatorCode)
|
||||
->where('deleted', 0)
|
||||
->exists();
|
||||
|
||||
$pass = $entityAgentCode === $operatorCode && $entityAgentCode !== '' && $exists;
|
||||
|
||||
$detail = match (true) {
|
||||
$entityAgentCode === '' => '归属代理 agent_code 为空,无法定位操作代理',
|
||||
$entityAgentCode !== $operatorCode => '当前操作代理 '.$operatorCode.' 与单据归属代理 '.$entityAgentCode.' 不一致',
|
||||
!$exists => '结算代理表 '.$settlementTable.' 中未找到 code='.$entityCode.', agent_code='.$operatorCode.' 的有效记录',
|
||||
default => '操作代理为归属代理,且在结算代理表中存在有效记录',
|
||||
};
|
||||
|
||||
return [
|
||||
'key' => 'owner_agent',
|
||||
'label' => '归属代理权限',
|
||||
'pass' => $pass,
|
||||
'expected' => '操作代理 = 单据 agent_code,且在 '.$settlementTable.' 中 deleted=0 存在记录',
|
||||
'actual' => sprintf(
|
||||
'单据 agent_code=%s,结算代理表存在=%s',
|
||||
$entityAgentCode === '' ? '(空)' : $entityAgentCode,
|
||||
$exists ? '是' : '否'
|
||||
),
|
||||
'detail' => $detail,
|
||||
'hint' => $pass ? null : '检查 '.$settlementTable.' 的记录是否被软删或代理归属是否被调整',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 账期检查 - 完整复现 AgentCredit::getLastCreditAgentCode 逻辑
|
||||
*
|
||||
* 通过 agent_agents 取链路(root → ... → operator),逐级查 contracts 表
|
||||
* 一级代理账期通过 CRM 接口 /api/group/detail/{code} 取得
|
||||
*/
|
||||
private function checkCredit(object $entity, ?object $operatorAgent, string $operatorCode): array
|
||||
{
|
||||
$productCode = (string) ($entity->product_code ?? '');
|
||||
|
||||
if (!$operatorAgent) {
|
||||
return [
|
||||
'key' => 'credit',
|
||||
'label' => '账期检查',
|
||||
'pass' => false,
|
||||
'expected' => '最后一级有账期的代理 = 单据 agent_code',
|
||||
'actual' => '未找到归属代理 '.$operatorCode.' 的代理记录',
|
||||
'detail' => 'agents 表中查不到该代理,无法计算账期链路',
|
||||
'hint' => '确认 agents 表中是否存在该 code 且 deleted=0',
|
||||
];
|
||||
}
|
||||
|
||||
if ($productCode === '') {
|
||||
return [
|
||||
'key' => 'credit',
|
||||
'label' => '账期检查',
|
||||
'pass' => false,
|
||||
'expected' => '存在 product_code 才能判断账期',
|
||||
'actual' => 'product_code 为空',
|
||||
'detail' => '单据未关联 product_code,账期无法判断',
|
||||
'hint' => '检查单据数据是否完整',
|
||||
];
|
||||
}
|
||||
|
||||
$relation = DB::connection($this->connection)
|
||||
->table('agent_agents')
|
||||
->where('agent_code', $operatorCode)
|
||||
->where('deleted', 0)
|
||||
->first();
|
||||
|
||||
if (!$relation) {
|
||||
return [
|
||||
'key' => 'credit',
|
||||
'label' => '账期检查',
|
||||
'pass' => false,
|
||||
'expected' => '存在 agent_agents 链路',
|
||||
'actual' => 'agent_agents 中无 '.$operatorCode.' 的记录',
|
||||
'detail' => '无法构建代理链路,AgentCredit 直接返回空,账期判断必然失败',
|
||||
'hint' => '检查 agent_agents 数据是否同步',
|
||||
];
|
||||
}
|
||||
|
||||
$rootAgentCode = (string) $relation->root_agent_code;
|
||||
|
||||
// 取整条链路 root -> operator,按 lft 升序
|
||||
$chain = DB::connection($this->connection)
|
||||
->table('agent_agents')
|
||||
->where('root_agent_code', $rootAgentCode)
|
||||
->where('lft', '<=', (int) $relation->lft)
|
||||
->where('rgt', '>=', (int) $relation->rgt)
|
||||
->where('deleted', 0)
|
||||
->orderBy('lft')
|
||||
->get();
|
||||
|
||||
if ($chain->isEmpty()) {
|
||||
return [
|
||||
'key' => 'credit',
|
||||
'label' => '账期检查',
|
||||
'pass' => false,
|
||||
'expected' => '存在代理链路',
|
||||
'actual' => '代理链路为空',
|
||||
'detail' => '无法构建代理链路',
|
||||
'hint' => '检查 agent_agents 数据',
|
||||
];
|
||||
}
|
||||
|
||||
// 一级代理账期 - 调 CRM
|
||||
$firstAgentCreditMap = $this->crm->isConfigured()
|
||||
? $this->crm->firstAgentCreditMap($rootAgentCode)
|
||||
: null;
|
||||
|
||||
$crmConfigured = $this->crm->isConfigured();
|
||||
$firstAgentHasCredit = $firstAgentCreditMap !== null
|
||||
&& isset($firstAgentCreditMap[$productCode])
|
||||
&& $firstAgentCreditMap[$productCode] === true;
|
||||
|
||||
// 子级代理逐级取合同 is_credit
|
||||
$subChain = $chain->slice(1)->values();
|
||||
$chainEvaluation = $this->evaluateChain($rootAgentCode, $subChain, $productCode, $firstAgentHasCredit);
|
||||
|
||||
$lastCreditAgentCode = $chainEvaluation['last_credit_agent_code'];
|
||||
$pass = $lastCreditAgentCode !== '' && $lastCreditAgentCode === $operatorCode;
|
||||
|
||||
$detail = match (true) {
|
||||
!$crmConfigured => 'CRM 接口未配置(CRM_SERVICE_BASE_URI),一级代理账期视为「未知」,链路计算可能与生产不一致',
|
||||
$firstAgentCreditMap === null => '调用 CRM 一级代理详情失败,无法判断一级代理账期',
|
||||
!$firstAgentHasCredit => '一级代理 '.$rootAgentCode.' 在产品 '.$productCode.' 上无账期,AgentCredit 返回空,账期判断必然失败',
|
||||
$lastCreditAgentCode === '' => '账期链路计算结果为空',
|
||||
$pass => '最后一级有账期的代理 = 单据归属代理('.$operatorCode.')',
|
||||
default => '最后一级有账期的代理为 '.$lastCreditAgentCode.',与单据归属代理 '.$operatorCode.' 不一致',
|
||||
};
|
||||
|
||||
return [
|
||||
'key' => 'credit',
|
||||
'label' => '账期检查',
|
||||
'pass' => $pass,
|
||||
'expected' => '最后一级有账期的代理 = '.$operatorCode,
|
||||
'actual' => '最后一级有账期的代理 = '.($lastCreditAgentCode === '' ? '(空)' : $lastCreditAgentCode),
|
||||
'detail' => $detail,
|
||||
'hint' => $pass ? null : $this->creditHint($crmConfigured, $firstAgentCreditMap, $firstAgentHasCredit),
|
||||
'chain' => $chainEvaluation['chain'],
|
||||
'product_code' => $productCode,
|
||||
'root_agent_code' => $rootAgentCode,
|
||||
'crm_configured' => $crmConfigured,
|
||||
'first_agent_credit_resolved' => $firstAgentCreditMap !== null,
|
||||
'first_agent_has_credit' => $firstAgentHasCredit,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 复现 AgentCredit::getLastCreditAgentCode 中遍历子代理的部分
|
||||
*
|
||||
* @return array{last_credit_agent_code:string, chain:array<int,array<string,mixed>>}
|
||||
*/
|
||||
private function evaluateChain(string $rootAgentCode, \Illuminate\Support\Collection $subChain, string $productCode, bool $firstAgentHasCredit): array
|
||||
{
|
||||
$chainView = [];
|
||||
|
||||
// root 节点
|
||||
$rootAgent = $this->findAgent($rootAgentCode);
|
||||
$chainView[] = [
|
||||
'agent_code' => $rootAgentCode,
|
||||
'agent_name' => $rootAgent->name ?? null,
|
||||
'level' => $rootAgent ? (int) $rootAgent->level : null,
|
||||
'is_root' => true,
|
||||
'has_credit' => $firstAgentHasCredit,
|
||||
'credit_source' => 'crm:getAgentByCode',
|
||||
];
|
||||
|
||||
if (!$firstAgentHasCredit) {
|
||||
return [
|
||||
'last_credit_agent_code' => '',
|
||||
'chain' => $chainView,
|
||||
];
|
||||
}
|
||||
|
||||
$lastCreditAgentCode = $rootAgentCode;
|
||||
$broken = false;
|
||||
|
||||
foreach ($subChain as $node) {
|
||||
$agentCode = (string) $node->agent_code;
|
||||
$agent = $this->findAgent($agentCode);
|
||||
$hasCredit = $this->subAgentHasCredit($agentCode, $productCode);
|
||||
|
||||
$chainView[] = [
|
||||
'agent_code' => $agentCode,
|
||||
'agent_name' => $agent->name ?? null,
|
||||
'level' => $agent ? (int) $agent->level : null,
|
||||
'is_root' => false,
|
||||
'has_credit' => $hasCredit,
|
||||
'credit_source' => 'db:agent_contracts',
|
||||
'broken' => $broken,
|
||||
];
|
||||
|
||||
if ($broken) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$hasCredit) {
|
||||
$broken = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$lastCreditAgentCode = $agentCode;
|
||||
}
|
||||
|
||||
return [
|
||||
'last_credit_agent_code' => $lastCreditAgentCode,
|
||||
'chain' => $chainView,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 子代理在某产品上是否有账期 - 复现 AgentCredit::get
|
||||
*
|
||||
* agent_contracts JOIN contracts WHERE contracts.status = ENABLE
|
||||
* 然后取 product_code 对应的 is_credit > 0
|
||||
*/
|
||||
private function subAgentHasCredit(string $agentCode, string $productCode): bool
|
||||
{
|
||||
// ContractModel::STATUS_ENABLE = 1
|
||||
$contracts = DB::connection($this->connection)
|
||||
->table('agent_contracts as ac')
|
||||
->join('contracts as c', 'c.id', '=', 'ac.contract_id')
|
||||
->where('ac.agent_code', $agentCode)
|
||||
->where('ac.deleted', 0)
|
||||
->where('c.deleted', 0)
|
||||
->where('c.status', 1)
|
||||
->where('c.product_code', $productCode)
|
||||
->select('c.is_credit')
|
||||
->get();
|
||||
|
||||
if ($contracts->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 与 AgentCredit::get 一致:取该 product 下最后一个值(map 覆盖)
|
||||
$hasCredit = false;
|
||||
foreach ($contracts as $row) {
|
||||
$hasCredit = ((int) $row->is_credit) > 0;
|
||||
}
|
||||
|
||||
return $hasCredit;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 标签与映射
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
private function expectedStatusFor(string $type): array
|
||||
{
|
||||
return match ($type) {
|
||||
// CaseEnum::STATUS_3D_CONFIRMED
|
||||
self::TYPE_CASE => [12, '3D设计已确认'],
|
||||
// BusinessDocumentEnum::STATUS_TO_BE_PAYMENT
|
||||
self::TYPE_BUSINESS => [5, '异常暂停(款项待支付)'],
|
||||
// SaleDocumentEnum::STATUS_WAIT_PERMIT
|
||||
self::TYPE_SALE => [3, '待放行'],
|
||||
};
|
||||
}
|
||||
|
||||
private function statusLabel(string $type, int $status): string
|
||||
{
|
||||
$map = match ($type) {
|
||||
self::TYPE_CASE => [
|
||||
1 => '资料处理中',
|
||||
2 => '文字方案设计中',
|
||||
3 => '文字方案待确认',
|
||||
4 => '3D设计中',
|
||||
5 => '3D设计待确认',
|
||||
6 => '加工中',
|
||||
7 => '已发货',
|
||||
8 => '暂停',
|
||||
9 => '结束',
|
||||
10 => '不收治',
|
||||
11 => '文字方案已确认',
|
||||
12 => '3D设计已确认',
|
||||
20 => '目标位设计中',
|
||||
21 => '目标位待确认',
|
||||
22 => '目标位已确认',
|
||||
30 => '产品待确认',
|
||||
31 => '产品已确认',
|
||||
],
|
||||
self::TYPE_BUSINESS => [
|
||||
1 => '资料未收到',
|
||||
2 => '资料处理中',
|
||||
3 => '风险待确认',
|
||||
4 => '风险已确认',
|
||||
5 => '异常暂停(款项待支付)',
|
||||
9 => '加工中',
|
||||
10 => '已发货',
|
||||
11 => '暂停',
|
||||
12 => '终止',
|
||||
],
|
||||
self::TYPE_SALE => [
|
||||
1 => '新建',
|
||||
2 => '待付款',
|
||||
3 => '待放行',
|
||||
4 => '待发货',
|
||||
5 => '已发货',
|
||||
6 => '待审批',
|
||||
7 => '审批拒绝',
|
||||
8 => '部分发货',
|
||||
],
|
||||
};
|
||||
|
||||
return $map[$status] ?? '未知状态';
|
||||
}
|
||||
|
||||
private function typeLabel(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
self::TYPE_CASE => '病例',
|
||||
self::TYPE_BUSINESS => '业务单据',
|
||||
self::TYPE_SALE => '销售单据',
|
||||
};
|
||||
}
|
||||
|
||||
private function tableFor(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
self::TYPE_CASE => 'cases',
|
||||
self::TYPE_BUSINESS => 'business_documents',
|
||||
self::TYPE_SALE => 'sale_documents',
|
||||
};
|
||||
}
|
||||
|
||||
private function settlementTableFor(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
self::TYPE_CASE => 'settlement_agent_case',
|
||||
self::TYPE_BUSINESS => 'settlement_agent_business_order',
|
||||
self::TYPE_SALE => 'settlement_agent_sales_order',
|
||||
};
|
||||
}
|
||||
|
||||
private function primaryCodeOf(string $type, object $entity): string
|
||||
{
|
||||
return $type === self::TYPE_CASE ? (string) $entity->case_code : (string) $entity->code;
|
||||
}
|
||||
|
||||
private function normalizeEntity(string $type, object $entity): array
|
||||
{
|
||||
$base = [
|
||||
'status' => (int) $entity->status,
|
||||
'status_label' => $this->statusLabel($type, (int) $entity->status),
|
||||
'agent_code' => (string) ($entity->agent_code ?? ''),
|
||||
'settlement_agent_code' => (string) ($entity->settlement_agent_code ?? ''),
|
||||
'product_code' => (string) ($entity->product_code ?? ''),
|
||||
];
|
||||
|
||||
if ($type === self::TYPE_CASE) {
|
||||
return $base + [
|
||||
'code' => (string) $entity->case_code,
|
||||
'hospital_code' => (string) ($entity->hospital_code ?? ''),
|
||||
'doctor_code' => (string) ($entity->doctor_code ?? ''),
|
||||
'patient_name' => (string) ($entity->patient_name ?? ''),
|
||||
'is_need_pfp' => (int) ($entity->is_need_pfp ?? 0),
|
||||
'is_pfp' => (int) ($entity->is_pfp ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
return $base + [
|
||||
'code' => (string) $entity->code,
|
||||
'hospital_code' => (string) ($entity->hospital_code ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
private function creditHint(bool $crmConfigured, ?array $firstAgentCreditMap, bool $firstAgentHasCredit): string
|
||||
{
|
||||
if (!$crmConfigured) {
|
||||
return '配置 .env 中的 CRM_SERVICE_BASE_URI 后可获得准确的一级代理账期判断';
|
||||
}
|
||||
if ($firstAgentCreditMap === null) {
|
||||
return 'CRM 接口调用失败,可查看 laravel.log';
|
||||
}
|
||||
if (!$firstAgentHasCredit) {
|
||||
return '需在 CRM 「集团详情」productList 中确认该产品的 agentAccountingPeriod > 0';
|
||||
}
|
||||
|
||||
return '检查链路中各代理的 agent_contracts / contracts 是否启用了对应产品账期';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user