280 lines
11 KiB
PHP
280 lines
11 KiB
PHP
<?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 传入的履约状态列表(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("<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;
|
||
}
|
||
}
|