'待快递填写', self::STATUS_SUBMITTED => '待初审', self::STATUS_RETURNED => '已退回补件', self::STATUS_FIRST_APPROVED => '待财务复审', self::STATUS_APPROVED => '待签署开通', self::STATUS_ACTIVATED => '已开通', self::STATUS_REJECTED => '不予合作', self::STATUS_CANCELLED => '已作废', ]; /** * 生成二维码时我方必填的快递资料(快递方看不到),直接对齐到快递资料。 * 字段 => [名称, 可选值(null 为自由填写)] */ public const PROFILE_FIELDS = [ 'courier_category' => ['快递类型', ['内采快递', '外采快递']], 'cooperation_mode' => ['快递合作方式', ['保丢保损', '保丢不保损', '不保丢不保损']], 'courier_attribute' => ['快递属性', ['简尚', '工厂']], 'deduction_method' => ['扣款方式', ['运费', '损耗+运费', '无']], 'waybill_type' => ['单号类型', ['系统对接', '手填单号']], 'courier_email' => ['快递邮箱', null], 'need_logistics_check' => ['是否需要核对物流点', ['是', '否']], 'shipping_mode' => ['快递发货方式', ['一单一货', '一单多货']], 'workplatform_account' => ['是否已有工作平台账号', ['已有', '新开']], 'network_code' => ['快递网点代码', null], 'network_region' => ['网点所在地区', null], ]; /** * 校验规则:生成二维码时全部必填;入驻详情里修改时,提交了哪项就校验哪项(不能清空)。 * * @return array> */ public static function profileRules(bool $onlyPresent = false): array { $rules = []; foreach (self::PROFILE_FIELDS as $field => [$label, $options]) { $base = $onlyPresent ? ['sometimes', 'required'] : ['required']; $rules[$field] = [...$base, ...match (true) { $options !== null => ['in:' . implode(',', $options)], $field === 'courier_email' => ['email', 'max:191'], default => ['string', 'max:' . ($field === 'network_code' ? 64 : 191)], }]; } return $rules; } /** @return array 必填提示 */ public static function profileMessages(): array { $messages = []; foreach (self::PROFILE_FIELDS as $field => [$label, $options]) { $messages[$field . '.required'] = ($options !== null ? '请选择' : '请填写') . $label . '。'; } return $messages; } /** @return array 字段名称,用于校验提示 */ public static function profileAttributes(): array { return array_map(static fn (array $config): string => $config[0], self::PROFILE_FIELDS); } /** 快递方可以继续编辑资料的状态。 */ public const COURIER_EDITABLE = [self::STATUS_INVITED, self::STATUS_RETURNED]; public const ENTITY_COMPANY = 'company'; public const ENTITY_PERSON = 'person'; public const ENTITY_TYPES = [ self::ENTITY_COMPANY => '公司 / 个体户', self::ENTITY_PERSON => '个人', ]; public const SERVICE_TYPES = ['快递', '快运', '大件专线', '其他']; public const INVOICE_TYPES = ['增值税专用发票', '增值税普通发票', '不开票']; public const SETTLEMENT_CYCLES = ['月结', '半月结', '周结', '其他']; public const QUOTE_MODES = \App\Support\CourierQuoteStructure::MODES; public const VOLUMETRIC_RULES = ['不计泡', '计泡']; public const PROVINCES = [ '江苏', '浙江', '上海', '安徽', '北京', '天津', '河北', '山西', '山东', '河南', '湖北', '湖南', '江西', '福建', '广东', '广西', '海南', '重庆', '四川', '贵州', '云南', '陕西', '甘肃', '宁夏', '青海', '辽宁', '吉林', '黑龙江', '内蒙古', '新疆', '西藏', ]; protected $table = 'business_courier_onboardings'; protected $fillable = [ 'company_id', 'number', 'token', 'status', 'invite_label', 'expires_at', 'entity_type', 'id_number', 'company_full_name', 'unified_social_credit_code', 'legal_representative', 'business_address', 'carrier_brand', 'network_name', 'service_type', 'contact_name', 'contact_phone', 'finance_contact_name', 'finance_contact_phone', 'pickup_warehouses', 'esign_operator_name', 'esign_account', 'bank_account_name', 'bank_name', 'bank_account_number', 'invoice_type', 'tax_rate', 'settlement_cycle', 'private_account_name', 'private_bank_name', 'private_account_number', 'workplatform_expcode', 'erp_account_id', 'erp_opened_at', 'erp_opened_source', 'dingtalk_instance_id', 'courier_category', 'cooperation_mode', 'courier_attribute', 'deduction_method', 'waybill_type', 'courier_email', 'need_logistics_check', 'shipping_mode', 'workplatform_account', 'network_code', 'network_region', 'quote_mode', 'volumetric_rule', 'volumetric_divisor', 'quote_effective_date', 'quote_structure', 'quote_rows', 'min_weight', 'weight_rounding', 'per_ticket_surcharge', 'quote_note', 'remark', 'courier_name', 'reconciler', 'return_reason', 'submitted_at', 'first_reviewed_by', 'first_reviewed_at', 'final_reviewed_by', 'final_reviewed_at', 'quote_change_id', 'quote_version_id', 'activated_by', 'activated_at', 'created_by', 'updated_by', ]; protected $casts = [ 'company_id' => 'integer', 'expires_at' => 'datetime', 'submitted_at' => 'datetime', 'first_reviewed_at' => 'datetime', 'final_reviewed_at' => 'datetime', 'activated_at' => 'datetime', 'quote_effective_date' => 'date', 'quote_rows' => 'array', 'quote_structure' => 'array', 'min_weight' => 'float', 'per_ticket_surcharge' => 'float', 'bank_account_number' => 'encrypted', 'private_account_number' => 'encrypted', 'erp_opened_at' => 'datetime', 'id_number' => 'encrypted', 'tax_rate' => 'float', 'volumetric_divisor' => 'integer', 'first_reviewed_by' => 'integer', 'final_reviewed_by' => 'integer', 'activated_by' => 'integer', 'created_by' => 'integer', 'deleted_at' => 'datetime', ]; protected $hidden = ['token', 'bank_account_number', 'private_account_number', 'id_number']; public function files(): HasMany { return $this->hasMany(CourierOnboardingFile::class, 'onboarding_id')->orderBy('id'); } public function events(): HasMany { return $this->hasMany(CourierOnboardingEvent::class, 'onboarding_id')->orderBy('id'); } public function quoteVersion(): BelongsTo { return $this->belongsTo(CourierQuoteVersion::class, 'quote_version_id'); } public function isPerson(): bool { return $this->entity_type === self::ENTITY_PERSON; } public function entityLabel(): string { return $this->isPerson() ? '个人' : '公司'; } /** 主体名称:公司为公司全称,个人为姓名。 */ public function subjectLabel(): string { return $this->isPerson() ? '姓名' : '公司全称'; } public function maskedIdNumber(): string { $value = (string) ($this->id_number ?? ''); return strlen($value) < 8 ? $value : substr($value, 0, 3) . str_repeat('*', strlen($value) - 7) . substr($value, -4); } public function statusLabel(): string { return self::STATUS_LABELS[$this->status] ?? $this->status; } public function courierCanEdit(): bool { return in_array($this->status, self::COURIER_EDITABLE, true) && ! $this->linkExpired(); } public function linkExpired(): bool { return $this->expires_at !== null && $this->expires_at->isPast(); } public function displayName(): string { foreach ([$this->courier_name, $this->network_name, $this->company_full_name, $this->invite_label] as $name) { if (filled($name)) { return (string) $name; } } return $this->number; } /** e签宝通知方式:1 短信(手机号账号),2 邮件(邮箱账号)。 */ public function esignNoticeType(): string { return str_contains((string) $this->esign_account, '@') ? '2' : '1'; } public function quoteSignature(): ?CourierQuoteSignature { return $this->quoteVersion?->signature; } public function quoteModeLabel(): string { return \App\Support\CourierQuoteStructure::MODES[$this->quote_mode] ?? ''; } /** * @return array{mode:string,flat_bands:list,then_mode:string,first_weight:?float,rate_tiers:list} */ public function quoteStructure(): array { $structure = is_array($this->quote_structure) ? $this->quote_structure : []; return [ 'mode' => (string) ($structure['mode'] ?? $this->quote_mode ?? 'waybill_rate'), 'flat_bands' => array_values($structure['flat_bands'] ?? []), 'then_mode' => (string) ($structure['then_mode'] ?? ''), 'first_weight' => isset($structure['first_weight']) ? (float) $structure['first_weight'] : null, 'rate_tiers' => array_values($structure['rate_tiers'] ?? []), ]; } /** @return list */ public function quoteColumns(): array { return \App\Support\CourierQuoteStructure::columns($this->quoteStructure()); } /** 附加计费规则的文字(计泡、起算重量、取整、每票加收)。 */ public function quoteRuleLines(): array { $kg = static fn ($value): string => \App\Support\CourierQuoteStructure::kg((float) $value); return array_values(array_filter([ '计费方式:' . \App\Support\CourierQuoteStructure::describe($this->quoteStructure()), $this->volumetric_rule === '计泡' ? '计泡:长*宽*高/' . ($this->volumetric_divisor ?: 6000) . ',取实重与体积重较大值' : ($this->volumetric_rule ? '不计泡' : null), $this->min_weight ? '起算重量:不足' . $kg($this->min_weight) . 'kg按' . $kg($this->min_weight) . 'kg计' : null, $this->weight_rounding && $this->weight_rounding !== 'none' ? '重量取整:' . (\App\Support\CourierQuoteStructure::ROUNDING[$this->weight_rounding] ?? $this->weight_rounding) : null, $this->per_ticket_surcharge ? '每票加收:' . $kg($this->per_ticket_surcharge) . '元' : null, ])); } /** ERP 开通信息来源:钉钉「新建快递」流程同步 / 手动补录 / 工作平台老数据。 */ public const ERP_SOURCES = [ 'dingtalk' => '钉钉流程同步', 'manual' => '手动补录', 'workplatform' => '工作平台', ]; /** 开通后等 ERP 专员在钉钉发起「新建快递」流程、开通发货系统账号。 */ public function erpPending(): bool { return $this->status === self::STATUS_ACTIVATED && $this->erp_opened_at === null; } public function erpSourceLabel(): string { return self::ERP_SOURCES[$this->erp_opened_source] ?? ''; } /** 户名比对:去掉空格,全角括号转半角,英文统一大写。 */ public static function normalizeName(?string $name): string { $name = str_replace(['(', ')', ' '], ['(', ')', ''], (string) $name); return mb_strtoupper((string) preg_replace('/\s+/u', '', $name)); } /** 对私收款人应为谁:公司入驻是法定代表人,个人入驻是本人。 */ public function expectedPrivatePayee(): ?string { return $this->isPerson() ? $this->company_full_name : $this->legal_representative; } /** * 打款账户核对结果:对公户名 = 营业执照公司全称,对私收款人 = 法定代表人(个人为本人)。 * * @return array{corporate:?bool,private:?bool} */ public function accountChecks(): array { $same = static fn (?string $a, ?string $b): ?bool => blank($a) || blank($b) ? null : self::normalizeName($a) === self::normalizeName($b); return [ 'corporate' => $this->isPerson() ? null : $same($this->bank_account_name, $this->company_full_name), 'private' => $same($this->private_account_name, $this->expectedPrivatePayee()), ]; } public function maskedBankAccount(): string { $value = (string) ($this->bank_account_number ?? ''); if (mb_strlen($value) <= 8) { return $value; } return mb_substr($value, 0, 4) . str_repeat('*', max(mb_strlen($value) - 8, 4)) . mb_substr($value, -4); } /** * @return list,base:?float,rates:list}> */ public function quoteRowList(): array { return array_values(array_filter( is_array($this->quote_rows) ? $this->quote_rows : [], static fn ($row): bool => is_array($row) && filled($row['region'] ?? null), )); } }