快递应付 → 快递报价 下新增「快递入驻」页:生成入驻二维码发给快递方,
对方免登录在手机上填写主体、联系人、e签宝签署人、结算账户、报价并上传资质,
内部对接人初审、管理员财务复审后生成首版报价,并自动生成报价确认单 PDF
推送到快递填写的 e签宝账号,签署归档后开通。
- 新增三张表 business_courier_onboardings / _files / _events(纯新增,不改旧表)
- 免登录填报页 /onboarding/courier/{token}:40 位随机 token,可暂存、提交、退回后补件,
链接有效期可延长、可作废;银行账号加密存储;附件存 uploads 盘
- 新增 COURIER_ONBOARDING_PUBLIC_URL 配置二维码里的站点地址(留空用当前访问地址)
- 内置纯 PHP 二维码生成(App\Support\QrCode,SVG 输出,无新增依赖)
- 生成二维码时必须定好系统快递名称(唯一,快递方不可见);财务复审沿用该名称,快递所填报价写成报价管理的 CourierQuoteVersion,
复用 CourierQuoteSignatureService 生成报价确认单并推送 e签宝(失败可重推/同步状态);初审人默认不能复审(COURIER_ONBOARDING_ALLOW_SAME_REVIEWER=true 可在测试环境放开)
- 支持公司 / 个人两种入驻:个人填姓名+身份证号(加密)、传身份证和银行卡,报价确认单按个人身份在 e签宝签署
- 扫码先选入驻方式;证件改为 营业执照 / 法人身份证正反面 / 银行卡正反面 各一张(个人营业执照选填),手机端上传前自动压缩,超限回到填报页提示
- 报价支持 4 种计费方式(面单+费率 / 首重+续重 / 按重量段一口价 / 小重量一口价+超出另算,费率可分段)+「其他」上传报价表;附加规则:计泡、起算重量、取整、每票加收(CourierQuoteStructure)
- 不再要求上传盖章报价单,至少填写一行在线报价
- 开通:报价确认单签署归档、或匹配到已签署协议、或填写线下签署说明
- 生成二维码前必填「快递公司代码」(自动大写、不可重复),入驻信息按代码对齐快递资料
- 公司入驻对公(营业执照主体)+ 对私(法定代表人)两个打款账户必填并自动核对;个人入驻对私必填、对公选填
- 变价:报价管理里后台改价 → 审核 → 自动推送快递 e签宝 → 回签;回签单导出 xlsx/zip;误发版本可作废
- 短期(临时)变价:变价单/报价版本加选填「结束日期」(end_date);按日期取价,期间用临时价,到期次日自动恢复原长期报价,
不用重签;报价管理显示 生效中(临时)/ 暂停中 / 已到期,确认单写明执行期间和到期后恢复的版本,审核提醒期间重叠
- 快递资料分四类:快递资料 / 网点 / 打款信息 / 操作日志,支持查询、导出、重置、编辑;
财务系统入驻的快递直接作为快递资料行,合同起止取协议签署里已签署的合作合同;
点开一行在下方看该快递的网点 / 打款信息(完整账号)/ 操作日志
- 编辑存财务系统覆盖值(business_courier_profiles,不回写工作平台),改动记操作日志(business_courier_profile_events)
- 打款信息新增 / 变更(business_courier_payment_accounts,账号加密)保存后跳到协议签署,自动预填收款账户证明 / 收款账户变更,
按签署状态显示 待推送协议 / 签署中 / 已生效;入口在快递资料明细和「打款信息」页签(表格每个账户可直接「变更」)
- ERP 开通:入驻开通后标「待 ERP 开通」,可手动补录(钉钉「新建快递」流程同步待接入)
- 新增 CourierOnboardingWorkflowTest(8 个用例),更新快递报价目录顺序断言
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gzfsnV3fJ89CoUy1D9V9g
126 lines
6.6 KiB
PHP
126 lines
6.6 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Business;
|
||
|
||
use App\Abstracts\Http\Controller;
|
||
use App\Business\CourierProfileData;
|
||
use App\Models\Business\CourierPaymentAccount;
|
||
use App\Models\Business\CourierProfile;
|
||
use Illuminate\Http\RedirectResponse;
|
||
use Illuminate\Http\Request;
|
||
|
||
/**
|
||
* 快递资料在财务系统的维护:编辑主信息(存覆盖值,不回写工作平台),打款信息新增/变更(保存后跳协议签署推送)。
|
||
*/
|
||
class CourierProfiles extends Controller
|
||
{
|
||
public function __construct(private readonly CourierProfileData $profiles)
|
||
{
|
||
}
|
||
|
||
public function update(Request $request, string $expcode): RedirectResponse
|
||
{
|
||
$row = $this->profiles->currentRow($expcode);
|
||
abort_if($row === null, 404);
|
||
|
||
$rules = [];
|
||
foreach (CourierProfile::EDITABLE as $field => $label) {
|
||
$rules[$field] = match (true) {
|
||
in_array($field, CourierProfile::BOOLEAN_FIELDS, true) => ['nullable', 'in:0,1'],
|
||
in_array($field, CourierProfile::DATE_FIELDS, true) => ['nullable', 'date'],
|
||
$field === 'expname' => ['required', 'string', 'max:100'],
|
||
default => ['nullable', 'string', 'max:255'],
|
||
};
|
||
}
|
||
$data = $request->validate($rules, ['expname.required' => '快递公司名称不能为空。'], CourierProfile::EDITABLE);
|
||
$count = $this->profiles->update($expcode, $row->expname ?? null, $row, $data, (int) user_id());
|
||
|
||
return redirect($this->pageUrl($expcode, 'logs'))
|
||
->with('success', $count > 0 ? "已保存 {$count} 项修改(财务系统维护,不回写工作平台),修改记录见下方操作日志。" : '没有修改任何字段。');
|
||
}
|
||
|
||
/** 打款信息页签里发起:快递公司代码在表单里填。 */
|
||
public function storePaymentAccountForCode(Request $request): RedirectResponse
|
||
{
|
||
$data = $request->validate(
|
||
['expcode' => ['required', 'string', 'max:64', 'regex:/^[0-9A-Za-z_\-]+$/']],
|
||
['expcode.required' => '请填写快递公司代码。', 'expcode.regex' => '快递公司代码只能是字母、数字、下划线或短横线。'],
|
||
);
|
||
|
||
return $this->storePaymentAccount($request, trim($data['expcode']));
|
||
}
|
||
|
||
public function storePaymentAccount(Request $request, string $expcode): RedirectResponse
|
||
{
|
||
$row = $this->profiles->currentRow($expcode);
|
||
if ($row === null) {
|
||
throw \Illuminate\Validation\ValidationException::withMessages(['expcode' => "快递资料里没有快递公司代码「{$expcode}」,请核对。"]);
|
||
}
|
||
|
||
$change = $request->input('action') === CourierPaymentAccount::ACTION_CHANGE;
|
||
$number = ['required', 'string', 'max:64', 'regex:/^[0-9A-Za-z\- ]+$/'];
|
||
$data = $request->validate([
|
||
'action' => ['required', 'in:add,change'],
|
||
'account_type' => ['required', 'in:' . implode(',', CourierPaymentAccount::ACCOUNT_TYPES)],
|
||
'account_name' => ['required', 'string', 'max:255'],
|
||
'bank_name' => ['required', 'string', 'max:255'],
|
||
'account_number' => $number,
|
||
'effective_date' => ['required', 'date'],
|
||
'old_account_name' => [$change ? 'required' : 'nullable', 'string', 'max:255'],
|
||
'old_account_number' => $change ? $number : ['nullable', 'string', 'max:64'],
|
||
'old_stop_date' => [$change ? 'required' : 'nullable', 'date'],
|
||
'change_date' => [$change ? 'required' : 'nullable', 'date'],
|
||
'note' => ['nullable', 'string', 'max:500'],
|
||
], [
|
||
'account_number.regex' => '银行账号只能包含数字、字母和短横线。',
|
||
'old_account_number.regex' => '原账号只能包含数字、字母和短横线。',
|
||
], [
|
||
'account_type' => '账户类型', 'account_name' => $change ? '新开户名称' : '开户名称', 'bank_name' => $change ? '新开户银行' : '开户银行',
|
||
'account_number' => $change ? '新银行账号' : '银行账号', 'effective_date' => $change ? '新账户启用日期' : '生效日期',
|
||
'old_account_name' => '原开户名称', 'old_account_number' => '原账号', 'old_stop_date' => '原账户停止使用日期', 'change_date' => '账户变更日期',
|
||
]);
|
||
|
||
$strip = static fn (?string $value): ?string => $value === null ? null : (string) preg_replace('/\s+/u', '', $value);
|
||
$record = CourierPaymentAccount::query()->create([
|
||
'company_id' => company_id(),
|
||
'expcode' => $expcode,
|
||
'courier_name' => $row->expname ?? null,
|
||
'action' => $data['action'],
|
||
'account_type' => $data['account_type'],
|
||
'account_name' => trim($data['account_name']),
|
||
'bank_name' => trim($data['bank_name']),
|
||
'account_number' => $strip($data['account_number']),
|
||
'effective_date' => $data['effective_date'],
|
||
'old_account_name' => $change ? trim((string) $data['old_account_name']) : null,
|
||
'old_account_number' => $change ? $strip($data['old_account_number']) : null,
|
||
'old_stop_date' => $change ? $data['old_stop_date'] : null,
|
||
'change_date' => $change ? $data['change_date'] : null,
|
||
'note' => $data['note'] ?? null,
|
||
'created_by' => (int) user_id(),
|
||
'updated_by' => (int) user_id(),
|
||
]);
|
||
|
||
$summary = $change
|
||
? "打款信息变更:{$record->old_account_name} → {$record->account_name}({$record->account_type})"
|
||
: "打款信息新增:{$record->account_name}({$record->account_type},{$record->bank_name})";
|
||
$this->profiles->log($expcode, $change ? 'account_change' : 'account_add', $summary . ',待推送' . ($change ? '收款账户变更' : '收款账户证明') . '协议', null, (int) user_id());
|
||
|
||
return redirect()->route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
'agreement_view' => 'create',
|
||
'payment_account' => $record->id,
|
||
])->with('success', $summary . '已保存。请核对下方预填的协议内容,补齐签署人信息后推送给快递签署;签署完成后这条打款信息显示为「已生效」。');
|
||
}
|
||
|
||
private function pageUrl(string $expcode, string $detailTab): string
|
||
{
|
||
return route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'courier-information',
|
||
'selected' => $expcode,
|
||
'detail_tab' => $detailTab,
|
||
]) . '#courier-detail';
|
||
}
|
||
}
|