更新
This commit is contained in:
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\BaseController;
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
use app\common\model\tcm\PrescriptionOrderLog;
|
||||
use think\facade\Config;
|
||||
@@ -13,337 +12,347 @@ use think\Response;
|
||||
|
||||
/**
|
||||
* 甘草订单状态回调控制器
|
||||
*
|
||||
*
|
||||
* 回调地址在【中药处方下单】时通过 callback_url 字段传入。
|
||||
* 当订单状态发生变化后,甘草会 POST 回调此地址。
|
||||
* 必须在 5 秒内返回纯文本 "ok",否则甘草视为失败并最多重试 10 次(间隔=失败次数×5分钟)。
|
||||
*
|
||||
* @see https://apidoc.igancao.com/service-doc/scm-outer-recipel.html#订单状态回调
|
||||
*/
|
||||
class GancaoCallbackController extends BaseController
|
||||
class GancaoCallbackController extends BaseApiController
|
||||
{
|
||||
public array $notNeedLogin = ['orderStatus'];
|
||||
/**
|
||||
* 甘草订单状态回调接口
|
||||
*
|
||||
* 回调地址在【中药处方下单】时用 callback_url 字段传入
|
||||
* 当订单状态发生变化后会触发业务回调
|
||||
*
|
||||
* @return Response
|
||||
* 甘草 state → 中文名称映射
|
||||
*/
|
||||
private const STATE_MAP = [
|
||||
10 => '系统审核中',
|
||||
11 => '系统审核通过',
|
||||
110 => '订单药房流转制作中',
|
||||
20 => '物流中',
|
||||
30 => '完成',
|
||||
90 => '拦截',
|
||||
91 => '主动撤单',
|
||||
92 => '驳回',
|
||||
];
|
||||
|
||||
/**
|
||||
* 物流商名称 → express_company 编码映射
|
||||
*/
|
||||
private const EXPRESS_MAP = [
|
||||
'顺丰' => 'sf',
|
||||
'京东' => 'jd',
|
||||
'极兔' => 'jt',
|
||||
'圆通' => 'yt',
|
||||
'中通' => 'zt',
|
||||
'韵达' => 'yd',
|
||||
'申通' => 'st',
|
||||
'邮政' => 'yz',
|
||||
'EMS' => 'ems',
|
||||
];
|
||||
|
||||
/**
|
||||
* 甘草订单状态回调入口
|
||||
*/
|
||||
public function orderStatus(): Response
|
||||
{
|
||||
try {
|
||||
// 获取原始请求体
|
||||
$rawBody = file_get_contents('php://input');
|
||||
|
||||
// 获取请求头
|
||||
$headers = $this->request->header();
|
||||
$accessAppkey = $headers['access-appkey'] ?? '';
|
||||
$accessNonce = $headers['access-nonce'] ?? '';
|
||||
$accessTimestamp = $headers['access-timestamp'] ?? '';
|
||||
$accessSign = $headers['access-sign'] ?? '';
|
||||
|
||||
// 记录回调请求
|
||||
|
||||
$headers = $this->request->header();
|
||||
$accessAppkey = (string) ($headers['access-appkey'] ?? '');
|
||||
$accessNonce = (string) ($headers['access-nonce'] ?? '');
|
||||
$accessTimestamp = (string) ($headers['access-timestamp'] ?? '');
|
||||
$accessSign = (string) ($headers['access-sign'] ?? '');
|
||||
|
||||
Log::info('Gancao callback received', [
|
||||
'headers' => [
|
||||
'access-appkey' => $accessAppkey,
|
||||
'access-nonce' => $accessNonce,
|
||||
'access-timestamp' => $accessTimestamp,
|
||||
'access-sign' => $accessSign,
|
||||
],
|
||||
'body' => $rawBody,
|
||||
'appkey' => $accessAppkey,
|
||||
'nonce' => $accessNonce,
|
||||
'ts' => $accessTimestamp,
|
||||
'sign' => $accessSign,
|
||||
'body' => $rawBody,
|
||||
]);
|
||||
|
||||
// 验证签名
|
||||
|
||||
if (!$this->verifySign($accessAppkey, $accessNonce, $accessTimestamp, $accessSign, $rawBody)) {
|
||||
Log::warning('Gancao callback sign verification failed', [
|
||||
'access-appkey' => $accessAppkey,
|
||||
'access-sign' => $accessSign,
|
||||
]);
|
||||
return $this->response('sign verification failed', 403);
|
||||
Log::warning('Gancao callback sign verification failed');
|
||||
return $this->ok();
|
||||
}
|
||||
|
||||
// 解析回调数据
|
||||
|
||||
$data = json_decode($rawBody, true);
|
||||
if (!is_array($data)) {
|
||||
Log::error('Gancao callback invalid json', ['body' => $rawBody]);
|
||||
return $this->response('invalid json', 400);
|
||||
return $this->ok();
|
||||
}
|
||||
|
||||
// 处理回调数据
|
||||
|
||||
$this->handleCallback($data);
|
||||
|
||||
// 必须在5秒内返回 "ok",否则会认为回调失败
|
||||
return $this->response('ok');
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gancao callback exception', [
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
|
||||
// 即使出错也要返回 ok,避免甘草重试
|
||||
return $this->response('ok');
|
||||
}
|
||||
|
||||
return $this->ok();
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 签名验证 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* 验证回调签名
|
||||
*
|
||||
* 签名算法:md5(access-appkey + secret-key + access-nonce + access-timestamp + $sBody)
|
||||
*
|
||||
* @param string $appkey
|
||||
* @param string $nonce
|
||||
* @param string $timestamp
|
||||
* @param string $sign
|
||||
* @param string $body
|
||||
* @return bool
|
||||
* md5(access-appkey + secret-key + access-nonce + access-timestamp + $sBody)
|
||||
*/
|
||||
private function verifySign(string $appkey, string $nonce, string $timestamp, string $sign, string $body): bool
|
||||
{
|
||||
$config = Config::get('gancao_scm', []);
|
||||
|
||||
// 获取配置的 appkey 和 secret-key
|
||||
$configAppkey = (string) ($config['callback_appkey'] ?? $config['biz_ak'] ?? '');
|
||||
$secretKey = (string) ($config['callback_secret_key'] ?? $config['biz_sk'] ?? '');
|
||||
|
||||
// 验证 appkey
|
||||
if ($appkey !== $configAppkey) {
|
||||
Log::warning('Gancao callback appkey mismatch', [
|
||||
'received' => $appkey,
|
||||
'expected' => $configAppkey,
|
||||
]);
|
||||
if ($sign === '' || $appkey === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算签名
|
||||
$expectedSign = md5($appkey . $secretKey . $nonce . $timestamp . $body);
|
||||
|
||||
// 验证签名
|
||||
if ($sign !== $expectedSign) {
|
||||
Log::warning('Gancao callback sign mismatch', [
|
||||
'received' => $sign,
|
||||
'expected' => $expectedSign,
|
||||
]);
|
||||
|
||||
$config = Config::get('gancao_scm', []);
|
||||
$cfgAppkey = (string) ($config['biz_ak'] ?? '');
|
||||
$secretKey = (string) ($config['biz_sk'] ?? '');
|
||||
|
||||
if ($appkey !== $cfgAppkey) {
|
||||
Log::warning('Gancao callback appkey mismatch', compact('appkey', 'cfgAppkey'));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$expected = md5($appkey . $secretKey . $nonce . $timestamp . $body);
|
||||
if (!hash_equals($expected, $sign)) {
|
||||
Log::warning('Gancao callback sign mismatch', compact('sign', 'expected'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理回调数据
|
||||
*
|
||||
* @param array<string,mixed> $data
|
||||
* @return void
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 回调数据处理 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private function handleCallback(array $data): void
|
||||
{
|
||||
$recipelOrderNo = (string) ($data['recipel_order_no'] ?? '');
|
||||
$state = (int) ($data['state'] ?? 0);
|
||||
$ext = $data['ext'] ?? [];
|
||||
|
||||
if ($recipelOrderNo === '') {
|
||||
Log::warning('Gancao callback missing recipel_order_no', ['data' => $data]);
|
||||
$appOrderNo = (string) ($data['app_order_no'] ?? '');
|
||||
$state = (int) ($data['state'] ?? 0);
|
||||
$ext = is_array($data['ext'] ?? null) ? $data['ext'] : [];
|
||||
|
||||
if ($recipelOrderNo === '' && $appOrderNo === '') {
|
||||
Log::warning('Gancao callback missing order no', ['data' => $data]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找订单
|
||||
$order = PrescriptionOrder::where('gancao_reciperl_order_no', $recipelOrderNo)
|
||||
->whereNull('delete_time')
|
||||
->find();
|
||||
|
||||
|
||||
$order = $this->findOrder($recipelOrderNo, $appOrderNo);
|
||||
if (!$order) {
|
||||
Log::warning('Gancao callback order not found', [
|
||||
'recipel_order_no' => $recipelOrderNo,
|
||||
]);
|
||||
Log::warning('Gancao callback order not found', compact('recipelOrderNo', 'appOrderNo'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
|
||||
$this->updateOrderStatus($order, $state, $ext);
|
||||
|
||||
// 记录日志
|
||||
$this->writeCallbackLog($order, $state, $ext);
|
||||
|
||||
|
||||
Log::info('Gancao callback processed', [
|
||||
'order_id' => $order->id,
|
||||
'order_id' => $order->id,
|
||||
'recipel_order_no' => $recipelOrderNo,
|
||||
'state' => $state,
|
||||
'ext' => $ext,
|
||||
'state' => $state,
|
||||
'ext' => $ext,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新订单状态
|
||||
*
|
||||
* 状态说明:
|
||||
* - 10: 系统审核中
|
||||
* - 11: 系统审核通过
|
||||
* - 110: 订单药房流转制作中
|
||||
* - 20: 物流中
|
||||
* - 30: 完成(终态)
|
||||
* - 90: 拦截(终止流转:可恢复)
|
||||
* - 91: 主动撤单(退费:终态)
|
||||
* - 92: 驳回(无法制作并退费:终态)
|
||||
*
|
||||
* @param PrescriptionOrder $order
|
||||
* @param int $state
|
||||
* @param array<string,mixed> $ext
|
||||
* @return void
|
||||
* 通过甘草处方单号或应用商订单号查找本地订单
|
||||
*/
|
||||
private function findOrder(string $recipelOrderNo, string $appOrderNo): ?PrescriptionOrder
|
||||
{
|
||||
if ($recipelOrderNo !== '') {
|
||||
$order = PrescriptionOrder::where('gancao_reciperl_order_no', $recipelOrderNo)
|
||||
->whereNull('delete_time')
|
||||
->find();
|
||||
if ($order) {
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
|
||||
if ($appOrderNo !== '') {
|
||||
return PrescriptionOrder::where('order_no', $appOrderNo)
|
||||
->whereNull('delete_time')
|
||||
->find() ?: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 订单状态更新 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* state 说明:
|
||||
* 10 系统审核中
|
||||
* 11 系统审核通过
|
||||
* 110 订单药房流转制作中(ext: flow_name, supplier)
|
||||
* 20 物流中(ext: shipping_name, nu, supplier)
|
||||
* 30 完成 - 终态(ext: shipping_name, nu, supplier)
|
||||
* 90 拦截 - 可恢复
|
||||
* 91 主动撤单 - 终态(退费)
|
||||
* 92 驳回 - 终态(无法制作并退费)
|
||||
*/
|
||||
private function updateOrderStatus(PrescriptionOrder $order, int $state, array $ext): void
|
||||
{
|
||||
// 保存甘草订单状态
|
||||
$order->gancao_order_state = $state;
|
||||
|
||||
// 根据状态更新订单履约状态
|
||||
|
||||
switch ($state) {
|
||||
case 10: // 系统审核中
|
||||
case 11: // 系统审核通过
|
||||
// 保持当前状态
|
||||
case 10:
|
||||
case 11:
|
||||
break;
|
||||
|
||||
case 110: // 订单药房流转制作中
|
||||
$flowName = (string) ($ext['flow_name'] ?? '');
|
||||
$supplier = (string) ($ext['supplier'] ?? '');
|
||||
|
||||
// 记录流程信息
|
||||
$order->gancao_flow_name = mb_substr($flowName, 0, 100);
|
||||
$order->gancao_supplier = mb_substr($supplier, 0, 100);
|
||||
|
||||
// 如果是发货流程,更新履约状态
|
||||
if (str_contains($flowName, '发货') || str_contains($flowName, '寄出')) {
|
||||
if ((int) $order->fulfillment_status === 2) {
|
||||
$order->fulfillment_status = 5; // 已发货
|
||||
}
|
||||
}
|
||||
|
||||
case 110:
|
||||
$this->handleProduction($order, $ext);
|
||||
break;
|
||||
|
||||
case 20: // 物流中
|
||||
$shippingName = (string) ($ext['shipping_name'] ?? '');
|
||||
$nu = (string) ($ext['nu'] ?? '');
|
||||
$supplier = (string) ($ext['supplier'] ?? '');
|
||||
|
||||
// 更新物流信息
|
||||
if ($nu !== '') {
|
||||
$order->tracking_number = mb_substr($nu, 0, 100);
|
||||
}
|
||||
if ($shippingName !== '') {
|
||||
// 转换快递公司名称
|
||||
$expressMap = [
|
||||
'顺丰' => 'sf',
|
||||
'京东' => 'jd',
|
||||
'极兔' => 'jt',
|
||||
];
|
||||
foreach ($expressMap as $name => $code) {
|
||||
if (str_contains($shippingName, $name)) {
|
||||
$order->express_company = $code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新履约状态为已发货
|
||||
if ((int) $order->fulfillment_status === 2) {
|
||||
$order->fulfillment_status = 5;
|
||||
}
|
||||
|
||||
case 20:
|
||||
$this->handleShipping($order, $ext);
|
||||
break;
|
||||
|
||||
case 30: // 完成
|
||||
// 更新履约状态为已完成
|
||||
|
||||
case 30:
|
||||
$this->handleShipping($order, $ext);
|
||||
if ((int) $order->fulfillment_status !== 4) {
|
||||
$order->fulfillment_status = 3;
|
||||
$order->fulfillment_status = 3; // 已完成
|
||||
}
|
||||
break;
|
||||
|
||||
case 90: // 拦截
|
||||
// 记录拦截原因
|
||||
$order->gancao_remark = '订单被拦截';
|
||||
|
||||
case 90:
|
||||
$order->gancao_remark = '甘草订单被拦截(可恢复)';
|
||||
break;
|
||||
|
||||
case 91: // 主动撤单
|
||||
// 更新履约状态为已取消
|
||||
|
||||
case 91:
|
||||
if ((int) $order->fulfillment_status !== 3) {
|
||||
$order->fulfillment_status = 4;
|
||||
$order->fulfillment_status = 4; // 已取消
|
||||
}
|
||||
$order->gancao_remark = '主动撤单(已退费)';
|
||||
$order->gancao_remark = '甘草主动撤单(已退费)';
|
||||
break;
|
||||
|
||||
case 92: // 驳回
|
||||
// 更新履约状态为已取消
|
||||
|
||||
case 92:
|
||||
if ((int) $order->fulfillment_status !== 3) {
|
||||
$order->fulfillment_status = 4;
|
||||
$order->fulfillment_status = 4; // 已取消
|
||||
}
|
||||
$order->gancao_remark = '订单被驳回(无法制作并退费)';
|
||||
$order->gancao_remark = '甘草驳回(无法制作并退费)';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gancao callback update order failed', [
|
||||
Log::error('Gancao callback save failed', [
|
||||
'order_id' => $order->id,
|
||||
'error' => $e->getMessage(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 记录回调日志
|
||||
*
|
||||
* @param PrescriptionOrder $order
|
||||
* @param int $state
|
||||
* @param array<string,mixed> $ext
|
||||
* @return void
|
||||
* state=110:药房流转制作中
|
||||
*/
|
||||
private function handleProduction(PrescriptionOrder $order, array $ext): void
|
||||
{
|
||||
$flowName = (string) ($ext['flow_name'] ?? '');
|
||||
$supplier = (string) ($ext['supplier'] ?? '');
|
||||
|
||||
if ($flowName !== '') {
|
||||
$order->gancao_flow_name = mb_substr($flowName, 0, 100);
|
||||
}
|
||||
if ($supplier !== '') {
|
||||
$order->gancao_supplier = mb_substr($supplier, 0, 100);
|
||||
}
|
||||
|
||||
$fs = (int) $order->fulfillment_status;
|
||||
if ($fs === 2 && (str_contains($flowName, '发货') || str_contains($flowName, '寄出'))) {
|
||||
$order->fulfillment_status = 5; // 已发货
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* state=20/30:物流中 / 已完成 — 回写快递单号与快递公司
|
||||
*/
|
||||
private function handleShipping(PrescriptionOrder $order, array $ext): void
|
||||
{
|
||||
$shippingName = (string) ($ext['shipping_name'] ?? '');
|
||||
$nu = (string) ($ext['nu'] ?? '');
|
||||
$supplier = (string) ($ext['supplier'] ?? '');
|
||||
|
||||
if ($nu !== '') {
|
||||
$order->tracking_number = mb_substr($nu, 0, 80);
|
||||
}
|
||||
if ($shippingName !== '') {
|
||||
$order->gancao_shipping_name = mb_substr($shippingName, 0, 50);
|
||||
$order->express_company = $this->resolveExpressCode($shippingName);
|
||||
}
|
||||
if ($supplier !== '') {
|
||||
$order->gancao_supplier = mb_substr($supplier, 0, 100);
|
||||
}
|
||||
|
||||
$fs = (int) $order->fulfillment_status;
|
||||
if (in_array($fs, [1, 2], true)) {
|
||||
$order->fulfillment_status = 5; // 已发货
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将甘草返回的物流商名称解析为系统内 express_company 短码
|
||||
*/
|
||||
private function resolveExpressCode(string $shippingName): string
|
||||
{
|
||||
foreach (self::EXPRESS_MAP as $keyword => $code) {
|
||||
if (str_contains($shippingName, $keyword)) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
return 'auto';
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 操作日志 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private function writeCallbackLog(PrescriptionOrder $order, int $state, array $ext): void
|
||||
{
|
||||
$stateMap = [
|
||||
10 => '系统审核中',
|
||||
11 => '系统审核通过',
|
||||
110 => '订单药房流转制作中',
|
||||
20 => '物流中',
|
||||
30 => '完成',
|
||||
90 => '拦截',
|
||||
91 => '主动撤单',
|
||||
92 => '驳回',
|
||||
];
|
||||
|
||||
$stateName = $stateMap[$state] ?? "状态{$state}";
|
||||
|
||||
$summary = "甘草订单状态更新:{$stateName}";
|
||||
|
||||
// 添加扩展信息
|
||||
$stateName = self::STATE_MAP[$state] ?? "未知状态({$state})";
|
||||
$summary = "甘草回调:{$stateName}";
|
||||
|
||||
if (isset($ext['flow_name'])) {
|
||||
$summary .= ",流程:{$ext['flow_name']}";
|
||||
$summary .= " | 流程:{$ext['flow_name']}";
|
||||
}
|
||||
if (isset($ext['shipping_name']) && isset($ext['nu'])) {
|
||||
$summary .= ",物流:{$ext['shipping_name']} {$ext['nu']}";
|
||||
if (isset($ext['supplier'])) {
|
||||
$summary .= " | 药房:{$ext['supplier']}";
|
||||
}
|
||||
|
||||
$log = new PrescriptionOrderLog();
|
||||
$log->prescription_order_id = (int) $order->id;
|
||||
$log->admin_id = 0; // 系统回调
|
||||
$log->admin_name = '甘草系统';
|
||||
$log->action = 'gancao_callback';
|
||||
$log->summary = mb_substr($summary, 0, 500);
|
||||
$log->create_time = time();
|
||||
|
||||
if (isset($ext['shipping_name'])) {
|
||||
$summary .= " | 物流:{$ext['shipping_name']}";
|
||||
}
|
||||
if (isset($ext['nu'])) {
|
||||
$summary .= " | 单号:{$ext['nu']}";
|
||||
}
|
||||
|
||||
try {
|
||||
$log = new PrescriptionOrderLog();
|
||||
$log->prescription_order_id = (int) $order->id;
|
||||
$log->admin_id = 0;
|
||||
$log->admin_name = '甘草系统';
|
||||
$log->action = 'gancao_callback';
|
||||
$log->summary = mb_substr($summary, 0, 500);
|
||||
$log->create_time = time();
|
||||
$log->save();
|
||||
} catch (\Throwable $e) {
|
||||
// 忽略日志写入错误
|
||||
Log::warning('Gancao callback log write failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回响应
|
||||
*
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @return Response
|
||||
*/
|
||||
private function response(string $message, int $code = 200): Response
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 响应 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private function ok(): Response
|
||||
{
|
||||
return response($message, $code, [], 'html');
|
||||
return response('ok', 200, [], 'html');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user