新增
This commit is contained in:
@@ -1,469 +1,469 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\model\Order;
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
use app\common\model\tcm\PrescriptionOrderLog;
|
||||
use app\common\model\tcm\PrescriptionOrderPayOrder;
|
||||
use app\common\service\ExpressTrackingService;
|
||||
use think\facade\Config;
|
||||
use think\facade\Log;
|
||||
use think\Response;
|
||||
|
||||
/**
|
||||
* 甘草订单状态回调控制器
|
||||
*
|
||||
* 回调地址在【中药处方下单】时通过 callback_url 字段传入。
|
||||
* 当订单状态发生变化后,甘草会 POST 回调此地址。
|
||||
* 必须在 5 秒内返回纯文本 "ok",否则甘草视为失败并最多重试 10 次(间隔=失败次数×5分钟)。
|
||||
*
|
||||
* @see https://apidoc.igancao.com/service-doc/scm-outer-recipel.html#订单状态回调
|
||||
*/
|
||||
class GancaoCallbackController extends BaseApiController
|
||||
{
|
||||
public array $notNeedLogin = ['orderStatus'];
|
||||
/**
|
||||
* 甘草 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
|
||||
{
|
||||
$rawBody = (string) file_get_contents('php://input');
|
||||
$headers = $this->request->header();
|
||||
|
||||
$accessAppkey = (string) $this->pickHeader($headers, ['access-appkey', 'accessappkey', 'x-access-appkey']);
|
||||
$accessNonce = (string) $this->pickHeader($headers, ['access-nonce', 'accessnonce', 'x-access-nonce']);
|
||||
$accessTimestamp = (string) $this->pickHeader($headers, ['access-timestamp', 'accesstimestamp', 'x-access-timestamp']);
|
||||
$accessSign = (string) $this->pickHeader($headers, ['access-sign', 'accesssign', 'x-access-sign']);
|
||||
|
||||
Log::info(sprintf(
|
||||
'Gancao callback received | appkey=%s | nonce=%s | ts=%s | sign=%s | body=%s | headers=%s',
|
||||
$accessAppkey !== '' ? $accessAppkey : '(empty)',
|
||||
$accessNonce !== '' ? $accessNonce : '(empty)',
|
||||
$accessTimestamp !== '' ? $accessTimestamp : '(empty)',
|
||||
$accessSign !== '' ? $accessSign : '(empty)',
|
||||
$rawBody,
|
||||
json_encode($headers, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
));
|
||||
|
||||
try {
|
||||
if (!$this->verifySign($accessAppkey, $accessNonce, $accessTimestamp, $accessSign, $rawBody)) {
|
||||
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->ok();
|
||||
}
|
||||
|
||||
$this->handleCallback($data);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gancao callback exception', [
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->ok();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 签名验证 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* 兼容多种 header key 命名(ThinkPHP 默认都会统一成小写-连字符,但不同反向代理/php-fpm 下可能变体)
|
||||
*
|
||||
* @param array<string, string|array<int, string>> $headers
|
||||
* @param array<int, string> $candidates 按优先级排列的 header key
|
||||
*/
|
||||
private function pickHeader(array $headers, array $candidates): string
|
||||
{
|
||||
foreach ($candidates as $key) {
|
||||
if (!isset($headers[$key])) {
|
||||
continue;
|
||||
}
|
||||
$v = $headers[$key];
|
||||
if (is_array($v)) {
|
||||
$v = reset($v);
|
||||
}
|
||||
$v = trim((string) $v);
|
||||
if ($v !== '') {
|
||||
return $v;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* md5(access-appkey + secret-key + access-nonce + access-timestamp + $sBody)
|
||||
*
|
||||
* 注意:回调签名使用的是「回调通知账号」—— callback_appkey / callback_secret,
|
||||
* 与下单使用的 biz_ak / biz_sk 是不同的两套凭证。
|
||||
*/
|
||||
private function verifySign(string $appkey, string $nonce, string $timestamp, string $sign, string $body): bool
|
||||
{
|
||||
$config = Config::get('gancao_scm', []);
|
||||
$cfgAppkey = (string) ($config['callback_appkey'] ?? '');
|
||||
$secretKey = (string) ($config['callback_secret'] ?? '');
|
||||
|
||||
if ($appkey === '' || $sign === '') {
|
||||
Log::warning(sprintf(
|
||||
'Gancao callback missing header | appkey=%s | sign=%s',
|
||||
$appkey !== '' ? $appkey : '(empty)',
|
||||
$sign !== '' ? $sign : '(empty)'
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($appkey !== $cfgAppkey) {
|
||||
Log::warning(sprintf(
|
||||
'Gancao callback appkey mismatch | received=%s | expected(config.callback_appkey)=%s',
|
||||
$appkey,
|
||||
$cfgAppkey !== '' ? $cfgAppkey : '(empty, check GANCAO_SCM_CALLBACK_APPKEY in .env)'
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
$expected = md5($appkey . $secretKey . $nonce . $timestamp . $body);
|
||||
if (!hash_equals($expected, $sign)) {
|
||||
Log::warning(sprintf(
|
||||
'Gancao callback sign mismatch | received=%s | expected=%s | nonce=%s | ts=%s',
|
||||
$sign,
|
||||
$expected,
|
||||
$nonce,
|
||||
$timestamp
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 回调数据处理 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private function handleCallback(array $data): void
|
||||
{
|
||||
$recipelOrderNo = (string) ($data['recipel_order_no'] ?? '');
|
||||
$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 = $this->findOrder($recipelOrderNo, $appOrderNo);
|
||||
if (!$order) {
|
||||
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,
|
||||
'recipel_order_no' => $recipelOrderNo,
|
||||
'state' => $state,
|
||||
'ext' => $ext,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过甘草处方单号或应用商订单号查找本地订单
|
||||
*/
|
||||
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 完成 - 终态。fulfillment 见 resolveFulfilmentOnGancaoState30(与 zyt_order 已付/关联合计对比业务订单 amount)
|
||||
* 90 拦截 - 可恢复
|
||||
* 91 主动撤单 - 终态(退费)
|
||||
* 92 驳回 - 终态(无法制作并退费)
|
||||
*/
|
||||
/**
|
||||
* 甘草 state=30:返回 fulfillment_status 3=已完成 或 6=已签收
|
||||
* 1) 已支付金额(zyt_order.status=2 的 amount 合计)与业务订单 amount 一致 → 3
|
||||
* 2) 否则已关联订单金额合计(全部关联单 amount)与业务订单 amount 一致 → 3
|
||||
* 3) 否则 → 6(含:已支付与总金额不一致且关联合计也不一致)
|
||||
* 无关联 zyt_order:仅甘草完成则 3
|
||||
*/
|
||||
private function resolveFulfilmentOnGancaoState30(PrescriptionOrder $order): int
|
||||
{
|
||||
$poId = (int) $order->id;
|
||||
if ($poId <= 0) {
|
||||
return 3;
|
||||
}
|
||||
$payIds = PrescriptionOrderPayOrder::where('prescription_order_id', $poId)
|
||||
->column('pay_order_id');
|
||||
$payIds = array_values(array_filter(
|
||||
array_map('intval', is_array($payIds) ? $payIds : []),
|
||||
static fn (int $id): bool => $id > 0
|
||||
));
|
||||
if ($payIds === []) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
$orderAmt = round((float) ($order->amount ?? 0), 2);
|
||||
$sumAll = round(
|
||||
(float) Order::whereIn('id', $payIds)->whereNull('delete_time')->sum('amount'),
|
||||
2
|
||||
);
|
||||
$sumPaid = round(
|
||||
(float) Order::whereIn('id', $payIds)
|
||||
->whereNull('delete_time')
|
||||
->where('status', 2)
|
||||
->sum('amount'),
|
||||
2
|
||||
);
|
||||
|
||||
if (abs($sumPaid - $orderAmt) <= 0.02) {
|
||||
return 3;
|
||||
}
|
||||
if (abs($sumAll - $orderAmt) <= 0.02) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
Log::warning('Gancao 完成回调:已支付(status=2)与关联合计均未与业务订单金额对齐,标已签收(6)', [
|
||||
'prescription_order_id' => $poId,
|
||||
'order_no' => (string) ($order->order_no ?? ''),
|
||||
'tcm_order_amount' => $orderAmt,
|
||||
'sum_paid_status2' => $sumPaid,
|
||||
'sum_linked_all' => $sumAll,
|
||||
'linked_pay_order_ids' => $payIds,
|
||||
]);
|
||||
|
||||
return 6;
|
||||
}
|
||||
|
||||
private function updateOrderStatus(PrescriptionOrder $order, int $state, array $ext): void
|
||||
{
|
||||
$order->gancao_order_state = $state;
|
||||
|
||||
switch ($state) {
|
||||
case 10:
|
||||
case 11:
|
||||
break;
|
||||
|
||||
case 110:
|
||||
$this->handleProduction($order, $ext);
|
||||
break;
|
||||
|
||||
case 20:
|
||||
$this->handleShipping($order, $ext);
|
||||
break;
|
||||
|
||||
case 30:
|
||||
$this->handleShipping($order, $ext);
|
||||
if ((int) $order->fulfillment_status !== 4) {
|
||||
$order->fulfillment_status = $this->resolveFulfilmentOnGancaoState30($order);
|
||||
}
|
||||
break;
|
||||
|
||||
case 90:
|
||||
$order->gancao_remark = '甘草订单被拦截(可恢复)';
|
||||
break;
|
||||
|
||||
case 91:
|
||||
if ((int) $order->fulfillment_status !== 3) {
|
||||
$order->fulfillment_status = 4; // 已取消
|
||||
}
|
||||
$order->gancao_remark = '甘草主动撤单(已退费)';
|
||||
break;
|
||||
|
||||
case 92:
|
||||
if ((int) $order->fulfillment_status !== 3) {
|
||||
$order->fulfillment_status = 4; // 已取消
|
||||
}
|
||||
$order->gancao_remark = '甘草驳回(无法制作并退费)';
|
||||
break;
|
||||
}
|
||||
|
||||
$savedOk = false;
|
||||
try {
|
||||
$order->save();
|
||||
$savedOk = true;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gancao callback save failed', [
|
||||
'order_id' => $order->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($savedOk && in_array((int) $order->fulfillment_status, [5, 6], true)) {
|
||||
ExpressTrackingService::applyAssistantReleaseForShippedPrescriptionOrder($order, [
|
||||
'tracking_number' => (string) ($order->tracking_number ?? ''),
|
||||
'source' => 'gancao_callback',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 !== '' && trim((string) ($order->tracking_number ?? '')) === '') {
|
||||
$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
|
||||
{
|
||||
$stateName = self::STATE_MAP[$state] ?? "未知状态({$state})";
|
||||
$summary = "甘草回调:{$stateName}";
|
||||
|
||||
if (isset($ext['flow_name'])) {
|
||||
$summary .= " | 流程:{$ext['flow_name']}";
|
||||
}
|
||||
if (isset($ext['supplier'])) {
|
||||
$summary .= " | 药房:{$ext['supplier']}";
|
||||
}
|
||||
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()]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 响应 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private function ok(): Response
|
||||
{
|
||||
return response('ok', 200, [], 'html');
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\model\Order;
|
||||
use app\common\model\tcm\PrescriptionOrder;
|
||||
use app\common\model\tcm\PrescriptionOrderLog;
|
||||
use app\common\model\tcm\PrescriptionOrderPayOrder;
|
||||
use app\common\service\ExpressTrackingService;
|
||||
use think\facade\Config;
|
||||
use think\facade\Log;
|
||||
use think\Response;
|
||||
|
||||
/**
|
||||
* 甘草订单状态回调控制器
|
||||
*
|
||||
* 回调地址在【中药处方下单】时通过 callback_url 字段传入。
|
||||
* 当订单状态发生变化后,甘草会 POST 回调此地址。
|
||||
* 必须在 5 秒内返回纯文本 "ok",否则甘草视为失败并最多重试 10 次(间隔=失败次数×5分钟)。
|
||||
*
|
||||
* @see https://apidoc.igancao.com/service-doc/scm-outer-recipel.html#订单状态回调
|
||||
*/
|
||||
class GancaoCallbackController extends BaseApiController
|
||||
{
|
||||
public array $notNeedLogin = ['orderStatus'];
|
||||
/**
|
||||
* 甘草 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
|
||||
{
|
||||
$rawBody = (string) file_get_contents('php://input');
|
||||
$headers = $this->request->header();
|
||||
|
||||
$accessAppkey = (string) $this->pickHeader($headers, ['access-appkey', 'accessappkey', 'x-access-appkey']);
|
||||
$accessNonce = (string) $this->pickHeader($headers, ['access-nonce', 'accessnonce', 'x-access-nonce']);
|
||||
$accessTimestamp = (string) $this->pickHeader($headers, ['access-timestamp', 'accesstimestamp', 'x-access-timestamp']);
|
||||
$accessSign = (string) $this->pickHeader($headers, ['access-sign', 'accesssign', 'x-access-sign']);
|
||||
|
||||
Log::info(sprintf(
|
||||
'Gancao callback received | appkey=%s | nonce=%s | ts=%s | sign=%s | body=%s | headers=%s',
|
||||
$accessAppkey !== '' ? $accessAppkey : '(empty)',
|
||||
$accessNonce !== '' ? $accessNonce : '(empty)',
|
||||
$accessTimestamp !== '' ? $accessTimestamp : '(empty)',
|
||||
$accessSign !== '' ? $accessSign : '(empty)',
|
||||
$rawBody,
|
||||
json_encode($headers, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
));
|
||||
|
||||
try {
|
||||
if (!$this->verifySign($accessAppkey, $accessNonce, $accessTimestamp, $accessSign, $rawBody)) {
|
||||
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->ok();
|
||||
}
|
||||
|
||||
$this->handleCallback($data);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gancao callback exception', [
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->ok();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 签名验证 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* 兼容多种 header key 命名(ThinkPHP 默认都会统一成小写-连字符,但不同反向代理/php-fpm 下可能变体)
|
||||
*
|
||||
* @param array<string, string|array<int, string>> $headers
|
||||
* @param array<int, string> $candidates 按优先级排列的 header key
|
||||
*/
|
||||
private function pickHeader(array $headers, array $candidates): string
|
||||
{
|
||||
foreach ($candidates as $key) {
|
||||
if (!isset($headers[$key])) {
|
||||
continue;
|
||||
}
|
||||
$v = $headers[$key];
|
||||
if (is_array($v)) {
|
||||
$v = reset($v);
|
||||
}
|
||||
$v = trim((string) $v);
|
||||
if ($v !== '') {
|
||||
return $v;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* md5(access-appkey + secret-key + access-nonce + access-timestamp + $sBody)
|
||||
*
|
||||
* 注意:回调签名使用的是「回调通知账号」—— callback_appkey / callback_secret,
|
||||
* 与下单使用的 biz_ak / biz_sk 是不同的两套凭证。
|
||||
*/
|
||||
private function verifySign(string $appkey, string $nonce, string $timestamp, string $sign, string $body): bool
|
||||
{
|
||||
$config = Config::get('gancao_scm', []);
|
||||
$cfgAppkey = (string) ($config['callback_appkey'] ?? '');
|
||||
$secretKey = (string) ($config['callback_secret'] ?? '');
|
||||
|
||||
if ($appkey === '' || $sign === '') {
|
||||
Log::warning(sprintf(
|
||||
'Gancao callback missing header | appkey=%s | sign=%s',
|
||||
$appkey !== '' ? $appkey : '(empty)',
|
||||
$sign !== '' ? $sign : '(empty)'
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($appkey !== $cfgAppkey) {
|
||||
Log::warning(sprintf(
|
||||
'Gancao callback appkey mismatch | received=%s | expected(config.callback_appkey)=%s',
|
||||
$appkey,
|
||||
$cfgAppkey !== '' ? $cfgAppkey : '(empty, check GANCAO_SCM_CALLBACK_APPKEY in .env)'
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
$expected = md5($appkey . $secretKey . $nonce . $timestamp . $body);
|
||||
if (!hash_equals($expected, $sign)) {
|
||||
Log::warning(sprintf(
|
||||
'Gancao callback sign mismatch | received=%s | expected=%s | nonce=%s | ts=%s',
|
||||
$sign,
|
||||
$expected,
|
||||
$nonce,
|
||||
$timestamp
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 回调数据处理 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private function handleCallback(array $data): void
|
||||
{
|
||||
$recipelOrderNo = (string) ($data['recipel_order_no'] ?? '');
|
||||
$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 = $this->findOrder($recipelOrderNo, $appOrderNo);
|
||||
if (!$order) {
|
||||
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,
|
||||
'recipel_order_no' => $recipelOrderNo,
|
||||
'state' => $state,
|
||||
'ext' => $ext,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过甘草处方单号或应用商订单号查找本地订单
|
||||
*/
|
||||
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 完成 - 终态。fulfillment 见 resolveFulfilmentOnGancaoState30(与 zyt_order 已付/关联合计对比业务订单 amount)
|
||||
* 90 拦截 - 可恢复
|
||||
* 91 主动撤单 - 终态(退费)
|
||||
* 92 驳回 - 终态(无法制作并退费)
|
||||
*/
|
||||
/**
|
||||
* 甘草 state=30:返回 fulfillment_status 3=已完成 或 6=已签收
|
||||
* 1) 已支付金额(zyt_order.status=2 的 amount 合计)与业务订单 amount 一致 → 3
|
||||
* 2) 否则已关联订单金额合计(全部关联单 amount)与业务订单 amount 一致 → 3
|
||||
* 3) 否则 → 6(含:已支付与总金额不一致且关联合计也不一致)
|
||||
* 无关联 zyt_order:仅甘草完成则 3
|
||||
*/
|
||||
private function resolveFulfilmentOnGancaoState30(PrescriptionOrder $order): int
|
||||
{
|
||||
$poId = (int) $order->id;
|
||||
if ($poId <= 0) {
|
||||
return 3;
|
||||
}
|
||||
$payIds = PrescriptionOrderPayOrder::where('prescription_order_id', $poId)
|
||||
->column('pay_order_id');
|
||||
$payIds = array_values(array_filter(
|
||||
array_map('intval', is_array($payIds) ? $payIds : []),
|
||||
static fn (int $id): bool => $id > 0
|
||||
));
|
||||
if ($payIds === []) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
$orderAmt = round((float) ($order->amount ?? 0), 2);
|
||||
$sumAll = round(
|
||||
(float) Order::whereIn('id', $payIds)->whereNull('delete_time')->sum('amount'),
|
||||
2
|
||||
);
|
||||
$sumPaid = round(
|
||||
(float) Order::whereIn('id', $payIds)
|
||||
->whereNull('delete_time')
|
||||
->where('status', 2)
|
||||
->sum('amount'),
|
||||
2
|
||||
);
|
||||
|
||||
if (abs($sumPaid - $orderAmt) <= 0.02) {
|
||||
return 3;
|
||||
}
|
||||
if (abs($sumAll - $orderAmt) <= 0.02) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
Log::warning('Gancao 完成回调:已支付(status=2)与关联合计均未与业务订单金额对齐,标已签收(6)', [
|
||||
'prescription_order_id' => $poId,
|
||||
'order_no' => (string) ($order->order_no ?? ''),
|
||||
'tcm_order_amount' => $orderAmt,
|
||||
'sum_paid_status2' => $sumPaid,
|
||||
'sum_linked_all' => $sumAll,
|
||||
'linked_pay_order_ids' => $payIds,
|
||||
]);
|
||||
|
||||
return 6;
|
||||
}
|
||||
|
||||
private function updateOrderStatus(PrescriptionOrder $order, int $state, array $ext): void
|
||||
{
|
||||
$order->gancao_order_state = $state;
|
||||
|
||||
switch ($state) {
|
||||
case 10:
|
||||
case 11:
|
||||
break;
|
||||
|
||||
case 110:
|
||||
$this->handleProduction($order, $ext);
|
||||
break;
|
||||
|
||||
case 20:
|
||||
$this->handleShipping($order, $ext);
|
||||
break;
|
||||
|
||||
case 30:
|
||||
$this->handleShipping($order, $ext);
|
||||
if ((int) $order->fulfillment_status !== 4) {
|
||||
$order->fulfillment_status = $this->resolveFulfilmentOnGancaoState30($order);
|
||||
}
|
||||
break;
|
||||
|
||||
case 90:
|
||||
$order->gancao_remark = '甘草订单被拦截(可恢复)';
|
||||
break;
|
||||
|
||||
case 91:
|
||||
if ((int) $order->fulfillment_status !== 3) {
|
||||
$order->fulfillment_status = 4; // 已取消
|
||||
}
|
||||
$order->gancao_remark = '甘草主动撤单(已退费)';
|
||||
break;
|
||||
|
||||
case 92:
|
||||
if ((int) $order->fulfillment_status !== 3) {
|
||||
$order->fulfillment_status = 4; // 已取消
|
||||
}
|
||||
$order->gancao_remark = '甘草驳回(无法制作并退费)';
|
||||
break;
|
||||
}
|
||||
|
||||
$savedOk = false;
|
||||
try {
|
||||
$order->save();
|
||||
$savedOk = true;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Gancao callback save failed', [
|
||||
'order_id' => $order->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($savedOk && in_array((int) $order->fulfillment_status, [5, 6], true)) {
|
||||
ExpressTrackingService::applyAssistantReleaseForShippedPrescriptionOrder($order, [
|
||||
'tracking_number' => (string) ($order->tracking_number ?? ''),
|
||||
'source' => 'gancao_callback',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 !== '' && trim((string) ($order->tracking_number ?? '')) === '') {
|
||||
$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
|
||||
{
|
||||
$stateName = self::STATE_MAP[$state] ?? "未知状态({$state})";
|
||||
$summary = "甘草回调:{$stateName}";
|
||||
|
||||
if (isset($ext['flow_name'])) {
|
||||
$summary .= " | 流程:{$ext['flow_name']}";
|
||||
}
|
||||
if (isset($ext['supplier'])) {
|
||||
$summary .= " | 药房:{$ext['supplier']}";
|
||||
}
|
||||
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()]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 响应 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private function ok(): Response
|
||||
{
|
||||
return response('ok', 200, [], 'html');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,425 +1,425 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\logic\tcm;
|
||||
|
||||
use app\common\model\tcm\BloodRecord;
|
||||
use app\common\model\tcm\DailyGamify;
|
||||
use app\common\model\tcm\DietRecord;
|
||||
use app\common\model\tcm\ExerciseRecord;
|
||||
|
||||
/**
|
||||
* 稳糖分 / 勋章 / 浇水领奖
|
||||
*/
|
||||
class DailyGamifyLogic
|
||||
{
|
||||
protected static string $error = '';
|
||||
|
||||
/** @var array<string,array{name:string,points:int}> */
|
||||
protected static array $taskDefs = [
|
||||
'glucose' => ['name' => '测血糖', 'points' => 10],
|
||||
'bp' => ['name' => '测血压', 'points' => 10],
|
||||
'diet' => ['name' => '饮食', 'points' => 10],
|
||||
'exercise' => ['name' => '运动', 'points' => 10],
|
||||
];
|
||||
|
||||
public static function getError(): string
|
||||
{
|
||||
return self::$error;
|
||||
}
|
||||
|
||||
protected static function setError(string $msg): bool
|
||||
{
|
||||
self::$error = $msg;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static function assertOwned(int $userId, int $diagnosisId): bool
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return self::setError('请先登录') ? false : false;
|
||||
}
|
||||
if ($diagnosisId <= 0) {
|
||||
return self::setError('诊单ID不能为空') ? false : false;
|
||||
}
|
||||
$owned = \think\facade\Db::name('diagnosis_view_records')
|
||||
->where('user_id', $userId)
|
||||
->where('diagnosis_id', $diagnosisId)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
if (!$owned) {
|
||||
return self::setError('无权操作该诊单') ? false : false;
|
||||
}
|
||||
$diagnosis = \app\common\model\tcm\Diagnosis::where('id', $diagnosisId)
|
||||
->where('delete_time', null)
|
||||
->field('show_card')
|
||||
->find();
|
||||
if (!$diagnosis || (int) ($diagnosis['show_card'] ?? 1) !== 1) {
|
||||
return self::setError('该就诊卡已在统计端隐藏') ? false : false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function todayRange(): array
|
||||
{
|
||||
return [
|
||||
strtotime(date('Y-m-d 00:00:00')),
|
||||
strtotime(date('Y-m-d 23:59:59')),
|
||||
];
|
||||
}
|
||||
|
||||
protected static function hasValue($v): bool
|
||||
{
|
||||
if ($v === null || $v === '' || $v === '0' || $v === 0) {
|
||||
return false;
|
||||
}
|
||||
if (is_string($v)) {
|
||||
return trim($v) !== '';
|
||||
}
|
||||
return is_numeric($v) && (float) $v > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 今日任务是否已完成(依据真实业务记录)
|
||||
*/
|
||||
public static function evaluateTaskCompletion(int $diagnosisId): array
|
||||
{
|
||||
[$start, $end] = self::todayRange();
|
||||
|
||||
$blood = BloodRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('source', 1)
|
||||
->where('record_date', '>=', $start)
|
||||
->where('record_date', '<=', $end)
|
||||
->find();
|
||||
|
||||
$glucoseDone = false;
|
||||
$bpDone = false;
|
||||
if ($blood) {
|
||||
$glucoseDone = self::hasValue($blood['fasting_blood_sugar'] ?? null)
|
||||
|| self::hasValue($blood['postprandial_blood_sugar'] ?? null)
|
||||
|| self::hasValue($blood['other_blood_sugar'] ?? null);
|
||||
$bpDone = self::hasValue($blood['systolic_pressure'] ?? null)
|
||||
|| self::hasValue($blood['diastolic_pressure'] ?? null);
|
||||
}
|
||||
|
||||
$diet = DietRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $start)
|
||||
->where('record_date', '<=', $end)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$dietDone = false;
|
||||
if ($diet) {
|
||||
$dietDone = self::hasValue($diet['breakfast_foods'] ?? null)
|
||||
|| self::hasValue($diet['lunch_foods'] ?? null)
|
||||
|| self::hasValue($diet['dinner_foods'] ?? null);
|
||||
}
|
||||
|
||||
$exercise = ExerciseRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $start)
|
||||
->where('record_date', '<=', $end)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$exerciseDone = false;
|
||||
if ($exercise) {
|
||||
$exerciseDone = self::hasValue($exercise['exercise_type'] ?? null)
|
||||
|| self::hasValue($exercise['duration'] ?? null);
|
||||
}
|
||||
|
||||
return [
|
||||
'glucose' => $glucoseDone,
|
||||
'bp' => $bpDone,
|
||||
'diet' => $dietDone,
|
||||
'exercise' => $exerciseDone,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<string,bool>> $taskAwards
|
||||
* @return array<int,array{id:string,name:string,points:int,completed:bool,claimed:bool}>
|
||||
*/
|
||||
public static function buildTodayTasks(int $diagnosisId, array $taskAwards): array
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
$awards = isset($taskAwards[$today]) && is_array($taskAwards[$today]) ? $taskAwards[$today] : [];
|
||||
$completion = self::evaluateTaskCompletion($diagnosisId);
|
||||
$list = [];
|
||||
|
||||
foreach (self::$taskDefs as $id => $def) {
|
||||
$list[] = [
|
||||
'id' => $id,
|
||||
'name' => $def['name'],
|
||||
'points' => $def['points'],
|
||||
'completed' => !empty($completion[$id]),
|
||||
'claimed' => self::isTaskClaimed($id, $awards, $completion),
|
||||
];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务是否已领取(兼容旧版 blood 合并任务)
|
||||
*
|
||||
* @param array<string,bool> $awards
|
||||
* @param array<string,bool> $completion
|
||||
*/
|
||||
protected static function isTaskClaimed(string $id, array $awards, array $completion = []): bool
|
||||
{
|
||||
if (!empty($awards[$id])) {
|
||||
return true;
|
||||
}
|
||||
// 旧版 blood 一次性领取:对应分项当日已有记录则视为已领,避免拆分后重复领奖/轮换引导
|
||||
if (!empty($awards['blood'])) {
|
||||
if ($id === 'glucose' && !empty($completion['glucose'])) {
|
||||
return true;
|
||||
}
|
||||
if ($id === 'bp' && !empty($completion['bp'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 与前端 tongji/utils/treeLevels.js 保持一致 */
|
||||
protected const TREE_MAX_LEVEL = 9;
|
||||
protected const TREE_XP_PER_LEVEL = 50;
|
||||
|
||||
protected static function treeMeta(int $points): array
|
||||
{
|
||||
$points = max(0, (int) $points);
|
||||
$level = min(self::TREE_MAX_LEVEL, (int) floor($points / self::TREE_XP_PER_LEVEL));
|
||||
$names = ['种子眠', '破土芽', '展两叶', '小树苗', '青枝繁', '拔节高', '稳糖冠', '初绽香', '漫开花', '圆满树'];
|
||||
$xpIn = $level >= self::TREE_MAX_LEVEL ? self::TREE_XP_PER_LEVEL : ($points % self::TREE_XP_PER_LEVEL);
|
||||
$progress = $level >= self::TREE_MAX_LEVEL
|
||||
? 100
|
||||
: (int) round(($xpIn / self::TREE_XP_PER_LEVEL) * 100);
|
||||
$nextName = $level < self::TREE_MAX_LEVEL ? ($names[$level + 1] ?? '') : '';
|
||||
$pointsToNext = $level >= self::TREE_MAX_LEVEL
|
||||
? 0
|
||||
: (self::TREE_XP_PER_LEVEL - $xpIn);
|
||||
|
||||
return [
|
||||
'tree_level' => $level,
|
||||
'tree_progress' => $progress,
|
||||
'tree_level_name' => $names[$level] ?? '种子眠',
|
||||
'tree_xp_in_level' => $xpIn,
|
||||
'tree_xp_need' => self::TREE_XP_PER_LEVEL,
|
||||
'tree_points_next' => $pointsToNext,
|
||||
'tree_next_name' => $nextName,
|
||||
'tree_is_max' => $level >= self::TREE_MAX_LEVEL,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取稳糖乐园状态(含今日任务)
|
||||
*/
|
||||
public static function getState(int $userId, int $diagnosisId): array|false
|
||||
{
|
||||
self::$error = '';
|
||||
if (!self::assertOwned($userId, $diagnosisId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
||||
->where('user_id', $userId)
|
||||
->find();
|
||||
|
||||
$points = $row ? (int) $row['points'] : 0;
|
||||
$badges = $row ? self::decodeJsonArray((string) ($row['badges'] ?? '')) : [];
|
||||
$taskAwards = $row ? self::decodeJsonObject((string) ($row['task_awards'] ?? '')) : [];
|
||||
|
||||
$todayTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
$claimable = 0;
|
||||
foreach ($todayTasks as $t) {
|
||||
if ($t['completed'] && !$t['claimed']) {
|
||||
$claimable += (int) $t['points'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge([
|
||||
'points' => $points,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
'today_tasks' => $todayTasks,
|
||||
'claimable_points' => $claimable,
|
||||
], self::treeMeta($points));
|
||||
}
|
||||
|
||||
/**
|
||||
* 浇水:领取今日已完成且未领取的任务积分
|
||||
*/
|
||||
public static function waterTree(int $userId, int $diagnosisId): array|false
|
||||
{
|
||||
self::$error = '';
|
||||
if (!self::assertOwned($userId, $diagnosisId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
||||
->where('user_id', $userId)
|
||||
->find();
|
||||
|
||||
$points = $row ? (int) $row['points'] : 0;
|
||||
$badges = $row ? self::decodeJsonArray((string) ($row['badges'] ?? '')) : [];
|
||||
$taskAwards = $row ? self::decodeJsonObject((string) ($row['task_awards'] ?? '')) : [];
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$todayTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
$addedPoints = 0;
|
||||
$claimedIds = [];
|
||||
$pending = [];
|
||||
|
||||
if (!isset($taskAwards[$today]) || !is_array($taskAwards[$today])) {
|
||||
$taskAwards[$today] = [];
|
||||
}
|
||||
|
||||
foreach ($todayTasks as $task) {
|
||||
if ($task['completed'] && !$task['claimed']) {
|
||||
$id = (string) $task['id'];
|
||||
$taskAwards[$today][$id] = true;
|
||||
$addedPoints += (int) $task['points'];
|
||||
$claimedIds[] = $id;
|
||||
} elseif (!$task['completed']) {
|
||||
$pending[] = [
|
||||
'id' => $task['id'],
|
||||
'name' => $task['name'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($addedPoints <= 0) {
|
||||
$claimable = 0;
|
||||
foreach ($todayTasks as $t) {
|
||||
if ($t['completed'] && !$t['claimed']) {
|
||||
$claimable += (int) $t['points'];
|
||||
}
|
||||
}
|
||||
$refreshedTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
return [
|
||||
'added_points' => 0,
|
||||
'claimed_tasks' => [],
|
||||
'claimable_points' => $claimable,
|
||||
'pending_tasks' => $pending,
|
||||
'points' => $points,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
'today_tasks' => $refreshedTasks,
|
||||
'message' => $claimable > 0 ? '请先点击浇水领取积分' : (count($pending) ? '请先完成今日任务再浇水' : '今日奖励已全部领取'),
|
||||
] + self::treeMeta($points);
|
||||
}
|
||||
|
||||
$newPoints = $points + $addedPoints;
|
||||
$saved = self::saveState($userId, $diagnosisId, $newPoints, $badges, $taskAwards);
|
||||
if ($saved === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$refreshed = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
|
||||
return [
|
||||
'added_points' => $addedPoints,
|
||||
'claimed_tasks' => $claimedIds,
|
||||
'claimable_points' => 0,
|
||||
'pending_tasks' => $pending,
|
||||
'points' => $newPoints,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
'today_tasks' => $refreshed,
|
||||
'message' => "浇水成功,获得 {$addedPoints} 稳糖积分",
|
||||
] + self::treeMeta($newPoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $badges
|
||||
* @param array<string,array<string,bool>> $taskAwards
|
||||
*/
|
||||
public static function saveState(int $userId, int $diagnosisId, int $points, array $badges, array $taskAwards): array|false
|
||||
{
|
||||
self::$error = '';
|
||||
if (!self::assertOwned($userId, $diagnosisId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$points = max(0, (int) $points);
|
||||
$badges = array_values(array_unique(array_filter(array_map('strval', $badges))));
|
||||
if (!is_array($taskAwards)) {
|
||||
$taskAwards = [];
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
||||
->where('user_id', $userId)
|
||||
->find();
|
||||
|
||||
$data = [
|
||||
'points' => $points,
|
||||
'badges' => json_encode($badges, JSON_UNESCAPED_UNICODE),
|
||||
'task_awards' => json_encode($taskAwards, JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => $now,
|
||||
];
|
||||
|
||||
if ($row) {
|
||||
DailyGamify::where('id', (int) $row['id'])->update($data);
|
||||
} else {
|
||||
$data['diagnosis_id'] = $diagnosisId;
|
||||
$data['user_id'] = $userId;
|
||||
$data['create_time'] = $now;
|
||||
DailyGamify::create($data);
|
||||
}
|
||||
|
||||
return [
|
||||
'points' => $points,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地缓存迁到服务端:取 points/badges/task_awards 的较大合并
|
||||
*/
|
||||
public static function mergeFromClient(int $userId, int $diagnosisId, int $points, array $badges, array $taskAwards): array|false
|
||||
{
|
||||
$server = self::getState($userId, $diagnosisId);
|
||||
if ($server === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mergedPoints = max((int) $server['points'], max(0, $points));
|
||||
$mergedBadges = array_values(array_unique(array_merge($server['badges'], $badges)));
|
||||
$mergedAwards = $server['task_awards'];
|
||||
foreach ($taskAwards as $date => $tasks) {
|
||||
if (!is_array($tasks)) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($mergedAwards[$date]) || !is_array($mergedAwards[$date])) {
|
||||
$mergedAwards[$date] = [];
|
||||
}
|
||||
foreach ($tasks as $taskId => $flag) {
|
||||
if ($flag) {
|
||||
$mergedAwards[$date][(string) $taskId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::saveState($userId, $diagnosisId, $mergedPoints, $mergedBadges, $mergedAwards);
|
||||
}
|
||||
|
||||
protected static function decodeJsonArray(string $json): array
|
||||
{
|
||||
if ($json === '') {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
return is_array($data) ? array_values(array_map('strval', $data)) : [];
|
||||
}
|
||||
|
||||
protected static function decodeJsonObject(string $json): array
|
||||
{
|
||||
if ($json === '') {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
}
|
||||
<?php
|
||||
|
||||
namespace app\api\logic\tcm;
|
||||
|
||||
use app\common\model\tcm\BloodRecord;
|
||||
use app\common\model\tcm\DailyGamify;
|
||||
use app\common\model\tcm\DietRecord;
|
||||
use app\common\model\tcm\ExerciseRecord;
|
||||
|
||||
/**
|
||||
* 稳糖分 / 勋章 / 浇水领奖
|
||||
*/
|
||||
class DailyGamifyLogic
|
||||
{
|
||||
protected static string $error = '';
|
||||
|
||||
/** @var array<string,array{name:string,points:int}> */
|
||||
protected static array $taskDefs = [
|
||||
'glucose' => ['name' => '测血糖', 'points' => 10],
|
||||
'bp' => ['name' => '测血压', 'points' => 10],
|
||||
'diet' => ['name' => '饮食', 'points' => 10],
|
||||
'exercise' => ['name' => '运动', 'points' => 10],
|
||||
];
|
||||
|
||||
public static function getError(): string
|
||||
{
|
||||
return self::$error;
|
||||
}
|
||||
|
||||
protected static function setError(string $msg): bool
|
||||
{
|
||||
self::$error = $msg;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static function assertOwned(int $userId, int $diagnosisId): bool
|
||||
{
|
||||
if ($userId <= 0) {
|
||||
return self::setError('请先登录') ? false : false;
|
||||
}
|
||||
if ($diagnosisId <= 0) {
|
||||
return self::setError('诊单ID不能为空') ? false : false;
|
||||
}
|
||||
$owned = \think\facade\Db::name('diagnosis_view_records')
|
||||
->where('user_id', $userId)
|
||||
->where('diagnosis_id', $diagnosisId)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
if (!$owned) {
|
||||
return self::setError('无权操作该诊单') ? false : false;
|
||||
}
|
||||
$diagnosis = \app\common\model\tcm\Diagnosis::where('id', $diagnosisId)
|
||||
->where('delete_time', null)
|
||||
->field('show_card')
|
||||
->find();
|
||||
if (!$diagnosis || (int) ($diagnosis['show_card'] ?? 1) !== 1) {
|
||||
return self::setError('该就诊卡已在统计端隐藏') ? false : false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function todayRange(): array
|
||||
{
|
||||
return [
|
||||
strtotime(date('Y-m-d 00:00:00')),
|
||||
strtotime(date('Y-m-d 23:59:59')),
|
||||
];
|
||||
}
|
||||
|
||||
protected static function hasValue($v): bool
|
||||
{
|
||||
if ($v === null || $v === '' || $v === '0' || $v === 0) {
|
||||
return false;
|
||||
}
|
||||
if (is_string($v)) {
|
||||
return trim($v) !== '';
|
||||
}
|
||||
return is_numeric($v) && (float) $v > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 今日任务是否已完成(依据真实业务记录)
|
||||
*/
|
||||
public static function evaluateTaskCompletion(int $diagnosisId): array
|
||||
{
|
||||
[$start, $end] = self::todayRange();
|
||||
|
||||
$blood = BloodRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('source', 1)
|
||||
->where('record_date', '>=', $start)
|
||||
->where('record_date', '<=', $end)
|
||||
->find();
|
||||
|
||||
$glucoseDone = false;
|
||||
$bpDone = false;
|
||||
if ($blood) {
|
||||
$glucoseDone = self::hasValue($blood['fasting_blood_sugar'] ?? null)
|
||||
|| self::hasValue($blood['postprandial_blood_sugar'] ?? null)
|
||||
|| self::hasValue($blood['other_blood_sugar'] ?? null);
|
||||
$bpDone = self::hasValue($blood['systolic_pressure'] ?? null)
|
||||
|| self::hasValue($blood['diastolic_pressure'] ?? null);
|
||||
}
|
||||
|
||||
$diet = DietRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $start)
|
||||
->where('record_date', '<=', $end)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$dietDone = false;
|
||||
if ($diet) {
|
||||
$dietDone = self::hasValue($diet['breakfast_foods'] ?? null)
|
||||
|| self::hasValue($diet['lunch_foods'] ?? null)
|
||||
|| self::hasValue($diet['dinner_foods'] ?? null);
|
||||
}
|
||||
|
||||
$exercise = ExerciseRecord::where('diagnosis_id', $diagnosisId)
|
||||
->where('record_date', '>=', $start)
|
||||
->where('record_date', '<=', $end)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$exerciseDone = false;
|
||||
if ($exercise) {
|
||||
$exerciseDone = self::hasValue($exercise['exercise_type'] ?? null)
|
||||
|| self::hasValue($exercise['duration'] ?? null);
|
||||
}
|
||||
|
||||
return [
|
||||
'glucose' => $glucoseDone,
|
||||
'bp' => $bpDone,
|
||||
'diet' => $dietDone,
|
||||
'exercise' => $exerciseDone,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<string,bool>> $taskAwards
|
||||
* @return array<int,array{id:string,name:string,points:int,completed:bool,claimed:bool}>
|
||||
*/
|
||||
public static function buildTodayTasks(int $diagnosisId, array $taskAwards): array
|
||||
{
|
||||
$today = date('Y-m-d');
|
||||
$awards = isset($taskAwards[$today]) && is_array($taskAwards[$today]) ? $taskAwards[$today] : [];
|
||||
$completion = self::evaluateTaskCompletion($diagnosisId);
|
||||
$list = [];
|
||||
|
||||
foreach (self::$taskDefs as $id => $def) {
|
||||
$list[] = [
|
||||
'id' => $id,
|
||||
'name' => $def['name'],
|
||||
'points' => $def['points'],
|
||||
'completed' => !empty($completion[$id]),
|
||||
'claimed' => self::isTaskClaimed($id, $awards, $completion),
|
||||
];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务是否已领取(兼容旧版 blood 合并任务)
|
||||
*
|
||||
* @param array<string,bool> $awards
|
||||
* @param array<string,bool> $completion
|
||||
*/
|
||||
protected static function isTaskClaimed(string $id, array $awards, array $completion = []): bool
|
||||
{
|
||||
if (!empty($awards[$id])) {
|
||||
return true;
|
||||
}
|
||||
// 旧版 blood 一次性领取:对应分项当日已有记录则视为已领,避免拆分后重复领奖/轮换引导
|
||||
if (!empty($awards['blood'])) {
|
||||
if ($id === 'glucose' && !empty($completion['glucose'])) {
|
||||
return true;
|
||||
}
|
||||
if ($id === 'bp' && !empty($completion['bp'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 与前端 tongji/utils/treeLevels.js 保持一致 */
|
||||
protected const TREE_MAX_LEVEL = 9;
|
||||
protected const TREE_XP_PER_LEVEL = 50;
|
||||
|
||||
protected static function treeMeta(int $points): array
|
||||
{
|
||||
$points = max(0, (int) $points);
|
||||
$level = min(self::TREE_MAX_LEVEL, (int) floor($points / self::TREE_XP_PER_LEVEL));
|
||||
$names = ['种子眠', '破土芽', '展两叶', '小树苗', '青枝繁', '拔节高', '稳糖冠', '初绽香', '漫开花', '圆满树'];
|
||||
$xpIn = $level >= self::TREE_MAX_LEVEL ? self::TREE_XP_PER_LEVEL : ($points % self::TREE_XP_PER_LEVEL);
|
||||
$progress = $level >= self::TREE_MAX_LEVEL
|
||||
? 100
|
||||
: (int) round(($xpIn / self::TREE_XP_PER_LEVEL) * 100);
|
||||
$nextName = $level < self::TREE_MAX_LEVEL ? ($names[$level + 1] ?? '') : '';
|
||||
$pointsToNext = $level >= self::TREE_MAX_LEVEL
|
||||
? 0
|
||||
: (self::TREE_XP_PER_LEVEL - $xpIn);
|
||||
|
||||
return [
|
||||
'tree_level' => $level,
|
||||
'tree_progress' => $progress,
|
||||
'tree_level_name' => $names[$level] ?? '种子眠',
|
||||
'tree_xp_in_level' => $xpIn,
|
||||
'tree_xp_need' => self::TREE_XP_PER_LEVEL,
|
||||
'tree_points_next' => $pointsToNext,
|
||||
'tree_next_name' => $nextName,
|
||||
'tree_is_max' => $level >= self::TREE_MAX_LEVEL,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取稳糖乐园状态(含今日任务)
|
||||
*/
|
||||
public static function getState(int $userId, int $diagnosisId): array|false
|
||||
{
|
||||
self::$error = '';
|
||||
if (!self::assertOwned($userId, $diagnosisId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
||||
->where('user_id', $userId)
|
||||
->find();
|
||||
|
||||
$points = $row ? (int) $row['points'] : 0;
|
||||
$badges = $row ? self::decodeJsonArray((string) ($row['badges'] ?? '')) : [];
|
||||
$taskAwards = $row ? self::decodeJsonObject((string) ($row['task_awards'] ?? '')) : [];
|
||||
|
||||
$todayTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
$claimable = 0;
|
||||
foreach ($todayTasks as $t) {
|
||||
if ($t['completed'] && !$t['claimed']) {
|
||||
$claimable += (int) $t['points'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge([
|
||||
'points' => $points,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
'today_tasks' => $todayTasks,
|
||||
'claimable_points' => $claimable,
|
||||
], self::treeMeta($points));
|
||||
}
|
||||
|
||||
/**
|
||||
* 浇水:领取今日已完成且未领取的任务积分
|
||||
*/
|
||||
public static function waterTree(int $userId, int $diagnosisId): array|false
|
||||
{
|
||||
self::$error = '';
|
||||
if (!self::assertOwned($userId, $diagnosisId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
||||
->where('user_id', $userId)
|
||||
->find();
|
||||
|
||||
$points = $row ? (int) $row['points'] : 0;
|
||||
$badges = $row ? self::decodeJsonArray((string) ($row['badges'] ?? '')) : [];
|
||||
$taskAwards = $row ? self::decodeJsonObject((string) ($row['task_awards'] ?? '')) : [];
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$todayTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
$addedPoints = 0;
|
||||
$claimedIds = [];
|
||||
$pending = [];
|
||||
|
||||
if (!isset($taskAwards[$today]) || !is_array($taskAwards[$today])) {
|
||||
$taskAwards[$today] = [];
|
||||
}
|
||||
|
||||
foreach ($todayTasks as $task) {
|
||||
if ($task['completed'] && !$task['claimed']) {
|
||||
$id = (string) $task['id'];
|
||||
$taskAwards[$today][$id] = true;
|
||||
$addedPoints += (int) $task['points'];
|
||||
$claimedIds[] = $id;
|
||||
} elseif (!$task['completed']) {
|
||||
$pending[] = [
|
||||
'id' => $task['id'],
|
||||
'name' => $task['name'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($addedPoints <= 0) {
|
||||
$claimable = 0;
|
||||
foreach ($todayTasks as $t) {
|
||||
if ($t['completed'] && !$t['claimed']) {
|
||||
$claimable += (int) $t['points'];
|
||||
}
|
||||
}
|
||||
$refreshedTasks = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
return [
|
||||
'added_points' => 0,
|
||||
'claimed_tasks' => [],
|
||||
'claimable_points' => $claimable,
|
||||
'pending_tasks' => $pending,
|
||||
'points' => $points,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
'today_tasks' => $refreshedTasks,
|
||||
'message' => $claimable > 0 ? '请先点击浇水领取积分' : (count($pending) ? '请先完成今日任务再浇水' : '今日奖励已全部领取'),
|
||||
] + self::treeMeta($points);
|
||||
}
|
||||
|
||||
$newPoints = $points + $addedPoints;
|
||||
$saved = self::saveState($userId, $diagnosisId, $newPoints, $badges, $taskAwards);
|
||||
if ($saved === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$refreshed = self::buildTodayTasks($diagnosisId, $taskAwards);
|
||||
|
||||
return [
|
||||
'added_points' => $addedPoints,
|
||||
'claimed_tasks' => $claimedIds,
|
||||
'claimable_points' => 0,
|
||||
'pending_tasks' => $pending,
|
||||
'points' => $newPoints,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
'today_tasks' => $refreshed,
|
||||
'message' => "浇水成功,获得 {$addedPoints} 稳糖积分",
|
||||
] + self::treeMeta($newPoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $badges
|
||||
* @param array<string,array<string,bool>> $taskAwards
|
||||
*/
|
||||
public static function saveState(int $userId, int $diagnosisId, int $points, array $badges, array $taskAwards): array|false
|
||||
{
|
||||
self::$error = '';
|
||||
if (!self::assertOwned($userId, $diagnosisId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$points = max(0, (int) $points);
|
||||
$badges = array_values(array_unique(array_filter(array_map('strval', $badges))));
|
||||
if (!is_array($taskAwards)) {
|
||||
$taskAwards = [];
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$row = DailyGamify::where('diagnosis_id', $diagnosisId)
|
||||
->where('user_id', $userId)
|
||||
->find();
|
||||
|
||||
$data = [
|
||||
'points' => $points,
|
||||
'badges' => json_encode($badges, JSON_UNESCAPED_UNICODE),
|
||||
'task_awards' => json_encode($taskAwards, JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => $now,
|
||||
];
|
||||
|
||||
if ($row) {
|
||||
DailyGamify::where('id', (int) $row['id'])->update($data);
|
||||
} else {
|
||||
$data['diagnosis_id'] = $diagnosisId;
|
||||
$data['user_id'] = $userId;
|
||||
$data['create_time'] = $now;
|
||||
DailyGamify::create($data);
|
||||
}
|
||||
|
||||
return [
|
||||
'points' => $points,
|
||||
'badges' => $badges,
|
||||
'task_awards' => $taskAwards,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地缓存迁到服务端:取 points/badges/task_awards 的较大合并
|
||||
*/
|
||||
public static function mergeFromClient(int $userId, int $diagnosisId, int $points, array $badges, array $taskAwards): array|false
|
||||
{
|
||||
$server = self::getState($userId, $diagnosisId);
|
||||
if ($server === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mergedPoints = max((int) $server['points'], max(0, $points));
|
||||
$mergedBadges = array_values(array_unique(array_merge($server['badges'], $badges)));
|
||||
$mergedAwards = $server['task_awards'];
|
||||
foreach ($taskAwards as $date => $tasks) {
|
||||
if (!is_array($tasks)) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($mergedAwards[$date]) || !is_array($mergedAwards[$date])) {
|
||||
$mergedAwards[$date] = [];
|
||||
}
|
||||
foreach ($tasks as $taskId => $flag) {
|
||||
if ($flag) {
|
||||
$mergedAwards[$date][(string) $taskId] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::saveState($userId, $diagnosisId, $mergedPoints, $mergedBadges, $mergedAwards);
|
||||
}
|
||||
|
||||
protected static function decodeJsonArray(string $json): array
|
||||
{
|
||||
if ($json === '') {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
return is_array($data) ? array_values(array_map('strval', $data)) : [];
|
||||
}
|
||||
|
||||
protected static function decodeJsonObject(string $json): array
|
||||
{
|
||||
if ($json === '') {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode($json, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user