setName('tcm:backfill-internal-cost') ->setDescription('批量用甘草预报价回填处方业务订单 internal_cost') ->addOption('dry-run', null, Option::VALUE_NONE, '只演练不写库') ->addOption('limit', 'l', Option::VALUE_OPTIONAL, '最多处理条数', 100) ->addOption('order-id', null, Option::VALUE_OPTIONAL, '仅处理指定 prescription_order.id', null) ->addOption('force', 'f', Option::VALUE_NONE, '覆盖已有 internal_cost(默认只填 NULL)') ->addOption('null-or-zero', null, Option::VALUE_NONE, '与默认一致且同时处理 internal_cost=0 的行(仍可用 --force 覆盖任意值)') ->addOption('min-id', null, Option::VALUE_OPTIONAL, '仅 id>=该值', null) ->addOption('max-id', null, Option::VALUE_OPTIONAL, '仅 id<=该值', null) ->addOption( 'fulfillment-status', null, Option::VALUE_OPTIONAL, '仅处理指定履约状态,逗号分隔数字,如 5,6,3=已发货/已签收/已完成;不传则不限', null ) ->addOption('sleep-ms', null, Option::VALUE_OPTIONAL, '每条成功调用后休眠毫秒,减轻开放平台压力', 250) ->addOption('detail', 'd', Option::VALUE_NONE, '打印每条明细'); } /** * @return array{admin_id:int, root:int, name:string, role_id:int[]} */ private static function cliAdminInfo(): array { return [ 'admin_id' => 0, 'root' => 1, 'name' => 'CLI-internal-cost', 'role_id' => [], ]; } /** * @param array $fee */ private static function totalFeeFromPreview(array $fee): float { $m = (float) ($fee['m_cost'] ?? 0); $p = (float) ($fee['proces_cost'] ?? 0); $l = (float) ($fee['lis_cost'] ?? 0); return round($m + $p + $l, 2); } /** * 解析 CLI 传入的履约状态列表(1–12,与 PrescriptionOrderLogic::fulfillmentStatusLabel 一致) * * @return int[] 去重后的状态码;$raw 非空但解析不到合法值时返回 null 表示调用方应报错 */ private static function parseFulfillmentStatusOption(?string $raw): ?array { if ($raw === null) { return []; } $raw = trim($raw); if ($raw === '') { return []; } $seen = []; foreach (explode(',', $raw) as $part) { $n = (int) trim($part); if ($n >= 1 && $n <= 12) { $seen[$n] = true; } } $ids = array_keys($seen); sort($ids); return $ids !== [] ? $ids : null; } protected function execute(Input $input, Output $output): int { $dryRun = (bool) $input->getOption('dry-run'); $verbose = (bool) $input->getOption('detail'); $force = (bool) $input->getOption('force'); $nullOrZero = (bool) $input->getOption('null-or-zero'); $limit = max(1, (int) $input->getOption('limit')); $sleepMs = max(0, (int) $input->getOption('sleep-ms')); $onlyOrderId = $input->getOption('order-id'); $onlyOrderId = $onlyOrderId !== null && $onlyOrderId !== '' ? (int) $onlyOrderId : null; $minId = $input->getOption('min-id'); $minId = $minId !== null && $minId !== '' ? (int) $minId : null; $maxId = $input->getOption('max-id'); $maxId = $maxId !== null && $maxId !== '' ? (int) $maxId : null; $fsRaw = $input->getOption('fulfillment-status'); $fsRaw = $fsRaw !== null ? (string) $fsRaw : null; $fulfillmentStatuses = self::parseFulfillmentStatusOption($fsRaw); if ($fulfillmentStatuses === null) { $output->error('--fulfillment-status 格式无效,请传入 1–12 的数字,英文逗号分隔,例如:5,6,3'); return 1; } $adminInfo = self::cliAdminInfo(); $adminId = 0; $output->writeln('========================================'); $output->writeln('批量回填 internal_cost(甘草 CTM_PREVIEW)'); $output->writeln('========================================'); if (!GancaoScmRecipelService::isConfigured()) { $output->error('甘草 SCM 未配置:' . GancaoScmRecipelService::whyNotConfigured()); return 1; } $q = PrescriptionOrder::whereNull('delete_time')->where('prescription_id', '>', 0)->order('id', 'asc'); if ($fulfillmentStatuses !== []) { $q->whereIn('fulfillment_status', $fulfillmentStatuses); } if ($onlyOrderId !== null && $onlyOrderId > 0) { $q->where('id', $onlyOrderId); } else { if (!$force) { if ($nullOrZero) { $q->whereRaw('(internal_cost IS NULL OR internal_cost = 0)'); } else { $q->whereNull('internal_cost'); } } if ($minId !== null && $minId > 0) { $q->where('id', '>=', $minId); } if ($maxId !== null && $maxId > 0) { $q->where('id', '<=', $maxId); } $q->limit($limit); } $rows = $q->field(['id', 'order_no', 'dose_count', 'medication_days', 'internal_cost', 'fulfillment_status'])->select()->toArray(); if ($rows === []) { $output->warning('没有符合条件的订单。'); return 0; } $fsLabel = ''; if ($fulfillmentStatuses !== []) { $parts = []; foreach ($fulfillmentStatuses as $code) { $parts[] = (string) $code . '=' . PrescriptionOrderLogic::fulfillmentStatusLabel((int) $code); } $fsLabel = ' fulfillment-status=[' . implode(',', $parts) . ']'; } $output->writeln(sprintf( '待处理 %d 条(dry-run=%s force=%s sleep-ms=%d%s)', count($rows), $dryRun ? 'yes' : 'no', $force ? 'yes' : 'no', $sleepMs, $fsLabel )); $ok = 0; $fail = 0; $skipped = 0; $reasons = []; foreach ($rows as $row) { $id = (int) ($row['id'] ?? 0); if ($id <= 0) { continue; } if (!$force && !$nullOrZero && $row['internal_cost'] !== null && $row['internal_cost'] !== '') { $skipped++; continue; } $params = []; $dc = (int) ($row['dose_count'] ?? 0); if ($dc > 0) { $params['dose_count'] = $dc; } $md = $row['medication_days'] ?? null; if ($md !== null && $md !== '' && (int) $md > 0) { $params['medication_days'] = (int) $md; } $ret = PrescriptionOrderLogic::previewGancaoRecipel($id, $adminId, $adminInfo, $params); if ($ret === false) { $fail++; $err = PrescriptionOrderLogic::getError(); $reasons[$err] = ($reasons[$err] ?? 0) + 1; if ($verbose) { $output->writeln("#{$id} 失败:{$err}"); } continue; } $fee = is_array($ret['fee'] ?? null) ? $ret['fee'] : []; $total = self::totalFeeFromPreview($fee); if ($verbose) { $ono = (string) ($row['order_no'] ?? ''); $fs = (int) ($row['fulfillment_status'] ?? 0); $fst = PrescriptionOrderLogic::fulfillmentStatusLabel($fs); $output->writeln(sprintf('#%d %s [%s] → internal_cost=%.2f', $id, $ono, $fst, $total)); } if (!$dryRun) { PrescriptionOrder::where('id', $id)->whereNull('delete_time')->update([ 'internal_cost' => $total, ]); $log = new PrescriptionOrderLog(); $log->prescription_order_id = $id; $log->admin_id = 0; $log->admin_name = 'CLI'; $log->action = 'cli_backfill_internal_cost'; $log->summary = mb_substr(sprintf('甘草预报价回填 internal_cost=%.2f', $total), 0, 500); $log->create_time = time(); try { $log->save(); } catch (\Throwable $e) { // 忽略 } } $ok++; if ($sleepMs > 0 && !$dryRun) { usleep($sleepMs * 1000); } } $output->writeln(''); $output->writeln(sprintf('完成:成功 %d,失败 %d,跳过 %d', $ok, $fail, $skipped)); if ($reasons !== []) { $output->writeln('失败原因统计:'); foreach ($reasons as $msg => $cnt) { $output->writeln(' [' . $cnt . 'x] ' . $msg); } } return $fail > 0 ? 2 : 0; } }