This commit is contained in:
Your Name
2026-08-03 09:51:53 +08:00
parent 81d6a38e26
commit 22a371a733
10 changed files with 215 additions and 96 deletions
@@ -131,6 +131,26 @@ class PrescriptionOrderController extends BaseAdminController
return $this->success('保存成功', $result);
}
/**
* 仅修改承运商与快递单号,不受订单履约状态或远端药房快照锁限制。
*/
public function ddcode()
{
$params = (new PrescriptionOrderValidate())->post()->goCheck('ddcode');
$result = PrescriptionOrderLogic::ddcode(
(int) $params['id'],
(string) ($params['express_company'] ?? 'auto'),
(string) $params['tracking_number'],
$this->adminId,
$this->adminInfo
);
if ($result === false) {
return $this->fail(PrescriptionOrderLogic::getError());
}
return $this->success('快递单号已保存', $result);
}
public function updateAmount()
{
$params = (new PrescriptionOrderValidate())->post()->goCheck('updateAmount');
@@ -1904,21 +1904,72 @@ class PrescriptionOrderLogic
}
/**
* 甘草已提交后仅更新物流字段(tracking_number、express_company),忽略金额/地址等其它请求参数
* 仅修改承运商与快递单号。物流信息不属于药房下单快照,因此允许在所有履约状态下修正
*
* @param array<string,mixed> $params
* @return array<string,mixed>|false
*/
private static function editGancaoLogisticsOnly(
PrescriptionOrder $order,
array $params,
public static function ddcode(
int $id,
string $expressCompany,
string $trackingNumber,
int $adminId,
array $adminInfo
) {
$order->tracking_number = mb_substr(trim((string) ($params['tracking_number'] ?? '')), 0, 80);
if (array_key_exists('express_company', $params)) {
$order->express_company = self::normalizeExpressCompany($params['express_company']);
self::setError('');
try {
return LockedPharmacySnapshotMutation::execute(
$id,
static fn () => self::ddcodeLocked(
$id,
$expressCompany,
$trackingNumber,
$adminId,
$adminInfo
),
false
);
} catch (\Throwable $exception) {
if (self::getError() === '') {
self::setError($exception->getMessage());
}
return false;
}
}
/** @return array<string,mixed>|false */
private static function ddcodeLocked(
int $id,
string $expressCompany,
string $trackingNumber,
int $adminId,
array $adminInfo
) {
self::$error = '';
$trackingNumber = mb_substr(trim($trackingNumber), 0, 80);
if ($trackingNumber === '') {
self::$error = '快递单号不能为空';
return false;
}
$order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find();
if (!$order) {
self::$error = '订单不存在';
return false;
}
if (!self::canAccessOrder($order, $adminId, $adminInfo)) {
self::$error = '无权限操作';
return false;
}
$oldTrackingNumber = trim((string) ($order->tracking_number ?? ''));
$oldExpressCompany = self::normalizeExpressCompany((string) ($order->express_company ?? 'auto'));
$newExpressCompany = self::normalizeExpressCompany($expressCompany);
$order->tracking_number = $trackingNumber;
$order->express_company = $newExpressCompany;
try {
$order->save();
@@ -1932,8 +1983,15 @@ class PrescriptionOrderLogic
(int) $order->id,
$adminId,
$adminInfo,
'edit',
'甘草订单已提交,仅更新快递信息(单号/承运商)'
'fill_tracking',
sprintf(
'修改快递信息:单号「%s」→「%s」,承运商「%s」→「%s」',
$oldTrackingNumber !== '' ? $oldTrackingNumber : '空',
$trackingNumber,
$oldExpressCompany,
$newExpressCompany
),
true
);
$out = $order->toArray();
@@ -5370,7 +5428,14 @@ class PrescriptionOrderLogic
];
}
private static function writeLog(int $orderId, int $adminId, array $adminInfo, string $action, string $summary): void
private static function writeLog(
int $orderId,
int $adminId,
array $adminInfo,
string $action,
string $summary,
bool $strict = false
): void
{
$adminName = $adminInfo['name'] ?? '';
if ($adminName === '' && $adminId > 0) {
@@ -5388,7 +5453,10 @@ class PrescriptionOrderLogic
try {
$log->save();
} catch (\Throwable $e) {
// 忽略日志写入错误
if ($strict) {
throw new \RuntimeException('操作日志写入失败,快递信息未保存', 0, $e);
}
// 非关键日志沿用历史容错行为
}
}
@@ -59,6 +59,7 @@ class PrescriptionOrderValidate extends BaseValidate
'action.require' => '请选择审核操作',
'patient_name.require' => '请输入患者姓名',
'phone.require' => '请输入手机号',
'tracking_number.require' => '请输入快递单号',
'phone_tail.regex' => '手机后四位仅支持数字',
'reason.require' => '请填写退款原因',
];
@@ -75,6 +76,7 @@ class PrescriptionOrderValidate extends BaseValidate
'is_follow_up', 'prev_staff', 'service_channel', 'service_package',
'tracking_number', 'express_company', 'fee_type', 'amount', 'remark_extra', 'remark_assistant', 'pay_order_ids',
],
'ddcode' => ['id', 'tracking_number', 'express_company'],
'logisticsTrace' => ['id', 'phone_tail'],
'logisticsJdUpdate' => ['id'],
'auditPrescription' => ['id', 'action', 'remark'],
@@ -106,6 +108,14 @@ class PrescriptionOrderValidate extends BaseValidate
->append('amount', 'require|float|egt:0');
}
public function ddcode(): PrescriptionOrderValidate
{
return $this->only(['id', 'tracking_number', 'express_company'])
->append('id', 'require|integer|gt:0')
->append('tracking_number', 'require|max:80')
->append('express_company', 'max:20');
}
public function patchPrescriptionUsage(): PrescriptionOrderValidate
{
return $this->only(['id', 'times_per_day', 'usage_days', 'medication_days', 'aux_times_per_day', 'aux_usage_days'])
@@ -59,6 +59,17 @@ FROM DUAL
WHERE @po_menu_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/edit');
INSERT INTO zyt_system_menu (
pid, type, name, icon, sort, perms, paths, component,
selected, params, is_cache, is_show, is_disable, create_time, update_time
)
SELECT
@po_menu_id, 'A', '修改快递单号', '', 31, 'tcm.prescriptionOrder/ddcode', '', '',
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
FROM DUAL
WHERE @po_menu_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/ddcode');
INSERT INTO zyt_system_menu (
pid, type, name, icon, sort, perms, paths, component,
selected, params, is_cache, is_show, is_disable, create_time, update_time
@@ -0,0 +1,23 @@
-- 处方业务订单:独立修改快递单号权限。
-- 执行后需在「角色权限」中为允许修改物流信息的角色勾选本按钮权限。
SET @po_menu_id := (
SELECT id
FROM zyt_system_menu
WHERE perms = 'tcm.prescriptionOrder/lists'
LIMIT 1
);
INSERT INTO zyt_system_menu (
pid, type, name, icon, sort, perms, paths, component,
selected, params, is_cache, is_show, is_disable, create_time, update_time
)
SELECT
@po_menu_id, 'A', '修改快递单号', '', 31,
'tcm.prescriptionOrder/ddcode', '', '',
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
FROM DUAL
WHERE @po_menu_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.prescriptionOrder/ddcode'
);
@@ -23,6 +23,10 @@ $assertTrue(
str_contains($controllerSource, 'confirmGancaoSubmission'),
'admin API must expose an actionable Gancao reconciliation endpoint'
);
$assertTrue(
str_contains($controllerSource, 'public function ddcode()'),
'admin API must expose a dedicated tracking correction endpoint'
);
$assertTrue(
str_contains($migrationSource, '`lease_expires_at`'),
'pharmacy submission claims must persist an explicit lease expiry'
+47 -3
View File
@@ -799,14 +799,14 @@ $assertTrue(
'remote payload must reload the canonical order after acquiring the durable claim'
);
$assertTrue(
substr_count($prescriptionOrderLogicSource, 'LockedPharmacySnapshotMutation::execute(') >= 7,
'order edit, patient, ship, withdraw, audit revoke, ship-mode and usage mutations must run inside locked snapshot transactions'
substr_count($prescriptionOrderLogicSource, 'LockedPharmacySnapshotMutation::execute(') >= 8,
'order edit, logistics, patient, ship, withdraw, audit revoke, ship-mode and usage mutations must run inside locked snapshot transactions'
);
$assertTrue(
substr_count($prescriptionLogicSource, 'LockedPharmacySnapshotMutation::executeForPrescription(') >= 4,
'prescription edit, patient patch, delete and void must lock every linked order and claim before saving'
);
foreach (['patchPrescriptionPatient', 'edit', 'ship', 'withdraw', 'revokeRxAudit', 'setShipMode', 'patchPrescriptionUsage'] as $method) {
foreach (['patchPrescriptionPatient', 'edit', 'ddcode', 'ship', 'withdraw', 'revokeRxAudit', 'setShipMode', 'patchPrescriptionUsage'] as $method) {
$start = (int) strpos($prescriptionOrderLogicSource, 'public static function ' . $method . '(');
$end = (int) strpos($prescriptionOrderLogicSource, 'private static function ' . $method . 'Locked(', $start);
$wrapperSource = substr($prescriptionOrderLogicSource, $start, $end - $start);
@@ -844,6 +844,35 @@ $assertTrue(
!str_contains($shipLockedSource, '->ship_mode ='),
'shipping logistics must never overwrite the pharmacy target snapshot'
);
$ddcodeStart = (int) strpos($prescriptionOrderLogicSource, 'public static function ddcode(');
$ddcodeLockedStart = (int) strpos($prescriptionOrderLogicSource, 'private static function ddcodeLocked(', $ddcodeStart);
$ddcodeEnd = (int) strpos($prescriptionOrderLogicSource, 'public static function ship(', $ddcodeLockedStart);
$ddcodeWrapperSource = substr($prescriptionOrderLogicSource, $ddcodeStart, $ddcodeLockedStart - $ddcodeStart);
$ddcodeLockedSource = substr($prescriptionOrderLogicSource, $ddcodeLockedStart, $ddcodeEnd - $ddcodeLockedStart);
$assertTrue(
(bool) preg_match(
'/LockedPharmacySnapshotMutation::execute\(.*?self::ddcodeLocked\(.*?\),\s*false\s*\)/s',
$ddcodeWrapperSource
),
'tracking correction must keep row locks and transactions while bypassing only the remote snapshot mutability assertion'
);
preg_match_all('/\$order->([a-z_]+)\s*=/', $ddcodeLockedSource, $ddcodeAssignedFieldMatches);
$assertSame(
['tracking_number', 'express_company'],
array_values(array_unique($ddcodeAssignedFieldMatches[1] ?? [])),
'tracking correction must write only the tracking number and carrier fields'
);
$assertTrue(
!str_contains($ddcodeLockedSource, '$order->fulfillment_status')
&& !str_contains($ddcodeLockedSource, 'assertRemoteSnapshotMutable'),
'tracking correction must be available in every fulfillment state and for remotely submitted orders'
);
$assertTrue(
str_contains($ddcodeLockedSource, "'fill_tracking'")
&& str_contains($ddcodeLockedSource, "'修改快递信息:单号")
&& str_contains($ddcodeLockedSource, "\n true\n );"),
'tracking correction must append a strict operation log containing old and new logistics values'
);
$assertTrue(
substr_count($prescriptionLogicSource, 'remoteSnapshotLockErrorForPrescription') >= 2,
'consumer prescription edit and patient patch must enforce the linked order snapshot lock'
@@ -1459,6 +1488,9 @@ $assertTrue(
$tcmApi = (string) file_get_contents(dirname(__DIR__, 3) . '/admin/src/api/tcm.ts');
$desktopOrderPage = (string) file_get_contents(dirname(__DIR__, 3) . '/admin/src/views/consumer/prescription/order_list.vue');
$mobileOrderPage = (string) file_get_contents(dirname(__DIR__, 3) . '/admin/src/views/consumer/prescription/order_list_h5.vue');
$ddcodeMigration = (string) file_get_contents(
dirname(__DIR__, 2) . '/sql/1.9.20260803/add_prescription_order_ddcode_menu.sql'
);
$gancaoReconcileComponent = (string) file_get_contents(
dirname(__DIR__, 3) . '/admin/src/views/consumer/prescription/components/GancaoSubmissionReconcileButton.vue'
);
@@ -1486,6 +1518,18 @@ $assertTrue(
str_contains($tcmApi, "url: '/tcm.prescriptionOrder/uploadToPharmacy'"),
'unified pharmacy API client must use the independently registered upload URI'
);
$assertTrue(
str_contains($tcmApi, "url: '/tcm.prescriptionOrder/ddcode'")
&& str_contains($ddcodeMigration, "'tcm.prescriptionOrder/ddcode'"),
'tracking correction API and its dedicated permission must be registered together'
);
$assertTrue(
str_contains($desktopOrderPage, 'await prescriptionOrderDdcode({')
&& str_contains($mobileOrderPage, 'await prescriptionOrderDdcode({')
&& (bool) preg_match('/function canQuickTrackRow\([^)]*\)\s*\{\s*return true\s*\}/', $desktopOrderPage)
&& (bool) preg_match('/function canQuickTrackRow\([^)]*\)\s*\{\s*return true\s*\}/', $mobileOrderPage),
'desktop and mobile tracking correction must use the dedicated API in every fulfillment state'
);
$assertTrue(
str_contains($desktopOrderPage, "v-perms=\"['tcm.prescriptionOrder/uploadToPharmacy']\""),
'unified pharmacy buttons must require the new upload permission'