diff --git a/AGENTS.md b/AGENTS.md index 08bdd318..5d421941 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,12 +18,40 @@ If you're using Codex or another agent-capable tool, additional project-scoped h ## Subagents -- ALWAYS wait for all subagents to complete before yielding. +- ALWAYS wait for every spawned subagent to reach a terminal status before yielding, acting on partial results, or spawning followups. + - On Codex, this means calling the `wait` tool with the subagent's thread id (requires `multi_agent_v2`). Do NOT infer completion from elapsed time. + - On Claude Code / OpenCode, this means awaiting the Task/agent tool result before continuing. +- NEVER cancel or re-spawn a subagent that hasn't finished. If a subagent appears stuck, raise the wait timeout (Codex default 30s, max 1h) before judging it broken. - Spawn subagents automatically when: - Parallelizable work (e.g., install + verify, npm test + typecheck, multiple tasks from plan) - - Long-running or blocking tasks where a worker can run independently. + - Long-running or blocking tasks where a worker can run independently - Isolation for risky changes or checks +### Codex-only — `spawn_agent` parameters + +When calling `spawn_agent`, ALWAYS pass `fork_turns="none"`. Without it the child inherits the parent transcript and sees your prior `spawn_agent(...)` records, then applies the "wait for spawned subagents" rule to itself — causing `wait_agent` self-deadlock. + +```text +spawn_agent(agent_type="trellis-implement", message="...", fork_turns="none") +``` + +### Codex-only — multi-subagent close-loop + +When `wait` returns a `completed` notification, treat it as an event signal — not as "all done". Run this loop: + +1. Maintain an `expected_agents` set of dispatched sub-agent thread IDs. +2. After each `wait` update: + 1. Call `list_agents` to inspect ALL live agents' status. + 2. For each agent now in a terminal state: + - Verify its promised deliverable exists (e.g. `{task_dir}/research/*.md`). + - Read or summarize as needed. + - `close_agent` to release the slot. + - Remove from `expected_agents`. + 3. If `expected_agents` still contains running agents → keep waiting. + 4. If `expected_agents` is empty → continue main flow. +3. Never `wait` on an agent that has already reported `completed`. +4. If a `completed` agent is missing its deliverable, treat it as failed — surface that in your report instead of re-waiting. + Managed by Trellis. Edits outside this block are preserved; edits inside may be overwritten by a future `trellis update`. diff --git a/admin/src/api/tcm.ts b/admin/src/api/tcm.ts index 8a31caa9..47ef6cf6 100644 --- a/admin/src/api/tcm.ts +++ b/admin/src/api/tcm.ts @@ -480,6 +480,11 @@ export function prescriptionOrderLogs(params: { id: number }) { return request.get({ url: '/tcm.prescriptionOrder/logs', params }) } +/** 修改订单金额 */ +export function prescriptionOrderUpdateAmount(params: { id: number; amount: number }) { + return request.post({ url: '/tcm.prescriptionOrder/updateAmount', params }) +} + // ========== 处方库 ========== // 处方库列表 diff --git a/admin/src/views/consumer/prescription/index.vue b/admin/src/views/consumer/prescription/index.vue index 8900af65..12c67ac0 100644 --- a/admin/src/views/consumer/prescription/index.vue +++ b/admin/src/views/consumer/prescription/index.vue @@ -2138,6 +2138,22 @@ const rxOutPelletText = computed(() => { function computeOutPellet(v: any): string { if (!v) return '' + if ((v.prescription_type || '浓缩水丸') === '浓缩水丸') { + const days = Number(v.medication_days ?? v.usage_days ?? v.dose_count) + const times = Number(v.times_per_day) + const doseG = Number(v.dosage_amount) + const bags = Number(v.dosage_bag_count) > 0 ? Number(v.dosage_bag_count) : 1 + if ( + Number.isFinite(days) && + Number.isFinite(times) && + Number.isFinite(doseG) && + days > 0 && + times > 0 && + doseG > 0 + ) { + return formatNum(days * times * doseG * bags) + } + } const explicit = v.out_pellet || v.total_weight if (explicit && Number(explicit) > 0) return formatNum(Number(explicit)) const list = slipHerbsList.value @@ -3150,9 +3166,14 @@ const handleEdit = async (row: any) => { // 提交表单 const handleSubmit = async () => { if (!formRef.value) return - - await formRef.value.validate() - + + try { + await formRef.value.validate() + } catch { + // Element Plus:校验失败会 reject,表单项已展示错误,勿再向外抛成 Uncaught + return + } + // 验证药材 if (!editForm.herbs || editForm.herbs.length === 0) { feedback.msgError('请至少添加一味药材') diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index 4f7879fd..bf34cdf9 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -667,7 +667,19 @@ -
总金额
+
+
总金额
+ + 修改 + +
¥{{ detailData.amount }}
@@ -1223,6 +1235,48 @@ + + + +
¥{{ formatMoney(detailData?.amount) }}
+
+ + + +
+ 已完成或已取消订单不可修改。提交成功后会刷新详情与操作日志。 +
+
+ +
+ | null>(null) const logisticsTracePhoneTail = ref('') const detailLogs = ref([]) +const updateAmountVisible = ref(false) +const updateAmountSaving = ref(false) +const updateAmountFormRef = ref() +const updateAmountForm = reactive({ + id: 0, + amount: undefined as number | undefined +}) +const updateAmountRules: FormRules = { + amount: [ + { required: true, message: '请输入订单金额', trigger: 'blur' }, + { + validator: (_rule, value, callback) => { + const num = Number(value) + if (!Number.isFinite(num) || num <= 0) { + callback(new Error('订单金额必须大于0')) + return + } + callback() + }, + trigger: 'blur' + } + ] +} function canViewPrescriptionOrderLogs() { const p = userStore.perms || [] @@ -3423,6 +3507,7 @@ function logActionText(act: string) { revoke_pay_audit: '撤回支付审核', gancao_submit: '甘草下单', patch_rx_patient: '处方患者信息', + update_amount: '修改订单金额', complete: '完成订单' } return m[act] || act @@ -3507,6 +3592,24 @@ function resetPatchRxPatientForm() { patchRxPatientForm.phone = '' } +function resetUpdateAmountDialog() { + updateAmountForm.id = 0 + updateAmountForm.amount = undefined + updateAmountFormRef.value?.clearValidate() +} + +function openUpdateAmount() { + const row = detailData.value + if (!canUpdateAmount(row)) { + feedback.msgWarning('当前订单状态不允许修改金额') + return + } + updateAmountForm.id = Number(row?.id) || 0 + updateAmountForm.amount = Number(row?.amount) || undefined + updateAmountVisible.value = true + nextTick(() => updateAmountFormRef.value?.clearValidate()) +} + async function refreshCurrentPrescriptionOrderDetail() { const id = detailData.value?.id if (!id) return @@ -3546,6 +3649,37 @@ async function submitPatchRxPatient() { } } +async function submitUpdateAmount() { + const form = updateAmountFormRef.value + if (!form) return + try { + await form.validate() + } catch { + return + } + if (!canUpdateAmount(detailData.value)) { + feedback.msgWarning('当前订单状态不允许修改金额') + return + } + const orderId = updateAmountForm.id + updateAmountSaving.value = true + try { + await prescriptionOrderUpdateAmount({ + id: orderId, + amount: Number(updateAmountForm.amount) + }) + feedback.msgSuccess('修改成功') + updateAmountVisible.value = false + await refreshCurrentPrescriptionOrderDetail() + await fetchLogs(orderId) + getLists() + } catch { + /* 拦截器已提示 */ + } finally { + updateAmountSaving.value = false + } +} + async function openDetail(id: number) { // 修复 Bug:显式彻底清空缓存,防止前一次弹窗的数据残留 detailData.value = null @@ -4567,13 +4701,14 @@ const slipPerBagDosageText = computed(() => { const slipPillGrams = computed(() => { const d = prescriptionViewData.value if (!d || (d.prescription_type || '浓缩水丸') !== '浓缩水丸') return null - /** 用药疗程以业务订单 medication_days 为准,缺省时回退处方 usage_days / 剂数 */ + /** 用药疗程以业务订单 medication_days 为准,缺省时回退处方 usage_days / 剂数;出丸 = 每袋用量×袋数×每天次数×服用天数 */ const days = Number(d.medication_days ?? d.usage_days ?? d.dose_count) const times = Number(d.times_per_day) const doseG = Number(d.dosage_amount) + const bags = Number(d.dosage_bag_count) > 0 ? Number(d.dosage_bag_count) : 1 if (!Number.isFinite(days) || !Number.isFinite(times) || !Number.isFinite(doseG)) return null if (days <= 0 || times <= 0 || doseG <= 0) return null - return days * times * doseG + return days * times * doseG * bags }) /* ============================================================ @@ -4684,7 +4819,7 @@ const rxUsageText = computed(() => { return seg.join(', ') }) -/** 出丸:优先用 slipPillGrams(按医嘱天数·次数·每包克数),否则按药材总量×剂数 */ +/** 出丸:优先用 slipPillGrams(每袋用量×袋数×每天次数×服用天数),否则按药材总量×剂数 */ const rxOutPelletGrams = computed(() => { const pill = slipPillGrams.value if (typeof pill === 'number' && isFinite(pill) && pill > 0) return formatNum(pill) diff --git a/admin/src/views/consumer/prescription/order_list_h5.vue b/admin/src/views/consumer/prescription/order_list_h5.vue index 4f5127be..d03b4b45 100644 --- a/admin/src/views/consumer/prescription/order_list_h5.vue +++ b/admin/src/views/consumer/prescription/order_list_h5.vue @@ -677,7 +677,19 @@ -
总金额
+
+
总金额
+ + 修改 + +
¥{{ detailData.amount }}
@@ -1221,6 +1233,47 @@ + + + +
¥{{ formatMoney(detailData?.amount) }}
+
+ + + +
+ 已完成或已取消订单不可修改。提交成功后会刷新详情与操作日志。 +
+
+ +
+ ) { else if (cmd === 'withdraw') confirmWithdraw(row as any) } +function canUpdateAmount(row: { id?: number; fulfillment_status?: number } | null | undefined) { + if (!row?.id) return false + const fs = Number(row.fulfillment_status) + return fs !== 3 && fs !== 4 +} + const detailVisible = ref(false) const detailLoading = ref(false) const detailData = ref | null>(null) @@ -3344,6 +3404,29 @@ const logisticsTracePayload = ref | null>(null) const logisticsTracePhoneTail = ref('') const detailLogs = ref([]) +const updateAmountVisible = ref(false) +const updateAmountSaving = ref(false) +const updateAmountFormRef = ref() +const updateAmountForm = reactive({ + id: 0, + amount: undefined as number | undefined +}) +const updateAmountRules: FormRules = { + amount: [ + { required: true, message: '请输入订单金额', trigger: 'blur' }, + { + validator: (_rule, value, callback) => { + const num = Number(value) + if (!Number.isFinite(num) || num <= 0) { + callback(new Error('订单金额必须大于0')) + return + } + callback() + }, + trigger: 'blur' + } + ] +} function canViewPrescriptionOrderLogs() { const p = userStore.perms || [] @@ -3379,6 +3462,7 @@ function logActionText(act: string) { revoke_pay_audit: '撤回支付审核', gancao_submit: '甘草下单', patch_rx_patient: '处方患者信息', + update_amount: '修改订单金额', complete: '完成订单' } return m[act] || act @@ -3463,6 +3547,24 @@ function resetPatchRxPatientForm() { patchRxPatientForm.phone = '' } +function resetUpdateAmountDialog() { + updateAmountForm.id = 0 + updateAmountForm.amount = undefined + updateAmountFormRef.value?.clearValidate() +} + +function openUpdateAmount() { + const row = detailData.value + if (!canUpdateAmount(row)) { + feedback.msgWarning('当前订单状态不允许修改金额') + return + } + updateAmountForm.id = Number(row?.id) || 0 + updateAmountForm.amount = Number(row?.amount) || undefined + updateAmountVisible.value = true + nextTick(() => updateAmountFormRef.value?.clearValidate()) +} + async function refreshCurrentPrescriptionOrderDetail() { const id = detailData.value?.id if (!id) return @@ -3502,6 +3604,37 @@ async function submitPatchRxPatient() { } } +async function submitUpdateAmount() { + const form = updateAmountFormRef.value + if (!form) return + try { + await form.validate() + } catch { + return + } + if (!canUpdateAmount(detailData.value)) { + feedback.msgWarning('当前订单状态不允许修改金额') + return + } + const orderId = updateAmountForm.id + updateAmountSaving.value = true + try { + await prescriptionOrderUpdateAmount({ + id: orderId, + amount: Number(updateAmountForm.amount) + }) + feedback.msgSuccess('修改成功') + updateAmountVisible.value = false + await refreshCurrentPrescriptionOrderDetail() + await fetchLogs(orderId) + getLists() + } catch { + /* 拦截器已提示 */ + } finally { + updateAmountSaving.value = false + } +} + async function openDetail(id: number) { // 修复 Bug:显式彻底清空缓存,防止前一次弹窗的数据残留 detailData.value = null @@ -4520,13 +4653,14 @@ const slipPerBagDosageText = computed(() => { const slipPillGrams = computed(() => { const d = prescriptionViewData.value if (!d || (d.prescription_type || '浓缩水丸') !== '浓缩水丸') return null - /** 用药疗程以业务订单 medication_days 为准,缺省时回退处方 usage_days / 剂数 */ + /** 用药疗程以业务订单 medication_days 为准,缺省时回退处方 usage_days / 剂数;出丸 = 每袋用量×袋数×每天次数×服用天数 */ const days = Number(d.medication_days ?? d.usage_days ?? d.dose_count) const times = Number(d.times_per_day) const doseG = Number(d.dosage_amount) + const bags = Number(d.dosage_bag_count) > 0 ? Number(d.dosage_bag_count) : 1 if (!Number.isFinite(days) || !Number.isFinite(times) || !Number.isFinite(doseG)) return null if (days <= 0 || times <= 0 || doseG <= 0) return null - return days * times * doseG + return days * times * doseG * bags }) /* ============================================================ @@ -4637,7 +4771,7 @@ const rxUsageText = computed(() => { return seg.join(', ') }) -/** 出丸:优先用 slipPillGrams(按医嘱天数·次数·每包克数),否则按药材总量×剂数 */ +/** 出丸:优先用 slipPillGrams(每袋用量×袋数×每天次数×服用天数),否则按药材总量×剂数 */ const rxOutPelletGrams = computed(() => { const pill = slipPillGrams.value if (typeof pill === 'number' && isFinite(pill) && pill > 0) return formatNum(pill) diff --git a/server/app/adminapi/controller/tcm/PrescriptionOrderController.php b/server/app/adminapi/controller/tcm/PrescriptionOrderController.php index 5450e0b6..c833475b 100644 --- a/server/app/adminapi/controller/tcm/PrescriptionOrderController.php +++ b/server/app/adminapi/controller/tcm/PrescriptionOrderController.php @@ -105,6 +105,17 @@ class PrescriptionOrderController extends BaseAdminController return $this->success('保存成功', $result); } + public function updateAmount() + { + $params = (new PrescriptionOrderValidate())->post()->goCheck('updateAmount'); + $result = PrescriptionOrderLogic::updateAmount($params, $this->adminId, $this->adminInfo); + if ($result === false) { + return $this->fail(PrescriptionOrderLogic::getError()); + } + + return $this->success('修改成功'); + } + /** * 修改关联处方的患者姓名与手机号(订单详情场景) */ diff --git a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php index cf54b27c..b1d129a0 100644 --- a/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php +++ b/server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php @@ -2642,6 +2642,60 @@ class PrescriptionOrderLogic ]; } + /** + * 修改订单金额 + */ + public static function updateAmount(array $params, int $adminId, array $adminInfo): bool + { + self::$error = ''; + $id = (int) $params['id']; + $newAmount = round((float) $params['amount'], 2); + + // 查询订单 + $order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find(); + if (!$order) { + self::setError('订单不存在'); + return false; + } + + // 数据权限检查 + if (!self::canAccessOrder($order, $adminId, $adminInfo)) { + self::setError('无权限操作此订单'); + return false; + } + + // 状态检查:已完成(3)和已取消(4)不允许修改 + if (in_array((int) $order->fulfillment_status, [3, 4], true)) { + self::setError('已完成或已取消的订单不允许修改金额'); + return false; + } + + $oldAmount = round((float) $order->amount, 2); + $oldAmountCents = (int) round($oldAmount * 100); + $newAmountCents = (int) round($newAmount * 100); + + // 金额未变化 + if ($newAmountCents === $oldAmountCents) { + self::setError('金额未发生变化'); + return false; + } + + // 更新金额 + $order->amount = $newAmount; + try { + $order->save(); + } catch (\Throwable $e) { + self::setError($e->getMessage()); + return false; + } + + // 记录日志 + $summary = sprintf('将订单金额从 ¥%.2f 修改为 ¥%.2f', $oldAmount, $newAmount); + self::writeLog($id, $adminId, $adminInfo, 'update_amount', $summary); + + return true; + } + private static function writeLog(int $orderId, int $adminId, array $adminInfo, string $action, string $summary): void { $adminName = $adminInfo['name'] ?? ''; diff --git a/server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php b/server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php index 56459913..d22a37b5 100644 --- a/server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php +++ b/server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php @@ -77,5 +77,13 @@ class PrescriptionOrderValidate extends BaseValidate 'submitGancaoRecipel' => ['id'], 'previewGancaoRecipel' => ['id'], 'patchPrescriptionPatient' => ['id', 'patient_name', 'phone'], + 'updateAmount' => ['id', 'amount'], ]; + + public function updateAmount(): PrescriptionOrderValidate + { + return $this->only(['id', 'amount']) + ->append('id', 'require|integer|gt:0') + ->append('amount', 'require|float|gt:0'); + } } diff --git a/server/sql/1.9.20260508/add_update_amount_menu.sql b/server/sql/1.9.20260508/add_update_amount_menu.sql new file mode 100644 index 00000000..620957bd --- /dev/null +++ b/server/sql/1.9.20260508/add_update_amount_menu.sql @@ -0,0 +1,17 @@ +-- 处方业务订单:修改订单金额 +-- 执行前确认表前缀为 zyt_;若已有相同 perms 则跳过。 +-- 需在「角色权限」中为有权限的角色勾选本按钮权限。 + +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', '修改订单金额', '', 60, + 'tcm.prescriptionOrder/updateAmount', '', '', + '', '', 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/updateAmount');