Files
zyt/server/app/command/TcmBackfillPrescriptionOrderInternalCost.php
2026-05-14 11:00:56 +08:00

280 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\command;
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderLog;
use app\common\service\gancao\GancaoScmRecipelService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
/**
* 历史 internal_cost 回填:对每条业务订单调用甘草预下单(CTM_PREVIEW),
* 将返回 fee 的「药材成本 + 制作费 + 物流费」合计写入 zyt_tcm_prescription_order.internal_cost。
*
* php think tcm:backfill-internal-cost --dry-run
* php think tcm:backfill-internal-cost --limit=30 --sleep-ms=400
* php think tcm:backfill-internal-cost --order-id=123
* php think tcm:backfill-internal-cost --force
* php think tcm:backfill-internal-cost --null-or-zero --min-id=1000
* php think tcm:backfill-internal-cost --fulfillment-status=5,6,3
*
* 履约状态代码(fulfillment_status):1待双审通过 2待发货 3已完成 4已取消 5已发货 6已签收
* 7进行中 8暂不制药 9拒收 10退款 11保留药方 12制药缓发
*
* 说明:使用 root 上下文仅用于绕过后台「谁能看哪张处方/订单」校验;不落管理员登录态。
* 甘草未配置、药材无法匹配、规则拦截、缺药等会跳过并在末尾汇总。
*/
class TcmBackfillPrescriptionOrderInternalCost extends Command
{
protected function configure(): void
{
$this->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<string, mixed> $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 传入的履约状态列表(112,与 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("<error>#{$id} 失败:{$err}</error>");
}
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;
}
}