This commit is contained in:
Your Name
2026-09-09 12:18:17 +08:00
parent 0cea43b027
commit 40319eea41
76 changed files with 4821 additions and 268 deletions
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace app\common\enum;
/** 挂号的问诊方式,与诊单的初诊/复诊类型无关。 */
class AppointmentTypeEnum
{
public const VIDEO = 'video';
public const TEXT = 'text';
public static function isWritable($value): bool
{
return is_string($value) && in_array($value, [self::VIDEO, self::TEXT], true);
}
/** 只给未传字段的新客户端请求补默认值;显式传入空值仍需校验。 */
public static function withDefault(array $params): array
{
if (!array_key_exists('appointment_type', $params)) {
$params['appointment_type'] = self::VIDEO;
}
return $params;
}
/** 旧记录未填写类型时按视频展示,已记录的历史类型保持原值。 */
public static function normalizeStored($value): string
{
return $value === null || (is_string($value) && trim($value) === '')
? self::VIDEO
: (string) $value;
}
public static function description($value): string
{
return [
self::VIDEO => '视频问诊',
self::TEXT => '图文问诊',
'phone' => '电话问诊', // 仅兼容已存在的历史记录,不允许新写入。
][self::normalizeStored($value)] ?? '未知';
}
}
@@ -3,6 +3,7 @@
namespace app\common\model\doctor;
use app\common\model\BaseModel;
use app\common\enum\AppointmentTypeEnum;
/**
* 医生预约模型
@@ -58,12 +59,12 @@ class Appointment extends BaseModel
*/
public function getAppointmentTypeDescAttr($value, $data)
{
$typeMap = [
'video' => '视频问诊',
'text' => '图文问诊',
'phone' => '电话问诊',
];
return $typeMap[$data['appointment_type']] ?? '未知';
return AppointmentTypeEnum::description($data['appointment_type'] ?? null);
}
public function getAppointmentTypeAttr($value)
{
return AppointmentTypeEnum::normalizeStored($value);
}
/**
@@ -14,6 +14,9 @@ class PrescriptionOrder extends BaseModel
{
use SoftDelete;
/** 已取消 / 已退款的历史订单不再占用处方;部分退款仍沿用原履约状态。 */
public const RELEASED_PRESCRIPTION_STATUSES = [4, 10];
protected $name = 'tcm_prescription_order';
protected $deleteTime = 'delete_time';
@@ -261,8 +261,13 @@ class QywxPromotionMemberSchedulerService
return ['pool_id' => $poolId, 'next_member_id' => 0, 'queued' => false, 'blocked' => true];
}
$appliedUserIds = self::linkRangeUserIds($linkId);
$changed = !QywxPromotionMemberRange::same($range['userids'], $appliedUserIds);
$alreadyPending = in_array((int) ($sync['status'] ?? 0), [1, 3], true);
$departmentJson = (string) (Db::name('qywx_promotion_link')->where('id', $linkId)->value('range_department_json') ?? '[]');
$departmentIds = json_decode($departmentJson, true);
$changed = !QywxPromotionMemberRange::same($range['userids'], $appliedUserIds)
|| !empty($departmentIds);
// 活跃或超时的租约、尚未应用的版本都不能仅凭旧快照被重算成“已同步”。
$alreadyPending = in_array((int) ($sync['status'] ?? 0), [1, 2, 3], true)
|| (int) ($sync['desired_version'] ?? 0) > (int) ($sync['applied_version'] ?? 0);
$needsSync = $changed || $alreadyPending;
self::upsertSync($poolId, $linkId, $needsSync, $sync, $now);
@@ -99,33 +99,42 @@ class QywxPromotionRangeSyncService
$response = $this->api->getLink($remoteLinkId);
$remote = QywxCustomerAcquisitionLinkService::normaliseRemoteResponse($response, $remoteLinkId);
$actualUserIds = $remote['range_userids'];
if (!QywxPromotionMemberRange::same($actualUserIds, $desiredUserIds)) {
if (!QywxPromotionMemberRange::same($actualUserIds, $desiredUserIds)
|| $remote['range_department_ids'] !== []) {
throw new RuntimeException('企业微信返回的多人路由成员范围与方案可用医助不一致');
}
$url = $remote['url'];
$snapshot = json_encode($remote['snapshot'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Db::name('qywx_promotion_link')->where('id', (int) $link['id'])->update([
'wecom_url' => $url,
'remote_status' => 1,
'range_user_json' => json_encode($actualUserIds, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'range_department_json' => json_encode($remote['range_department_ids'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'remote_snapshot' => $snapshot === false ? null : $snapshot,
'last_sync_time' => time(),
'sync_error' => '',
'update_time' => time(),
]);
$freshVersion = (int) (Db::name('qywx_promotion_range_sync')
->where('pool_id', $poolId)->value('desired_version') ?? 0);
Db::name('qywx_promotion_range_sync')
->where('pool_id', $poolId)
->where('lock_token', $token)
->update([
'status' => $freshVersion === $desiredVersion ? 0 : 1,
$confirmed = Db::transaction(function () use (
$poolId, $token, $desiredVersion, $link, $url, $actualUserIds, $remote, $snapshot
): bool {
$fresh = Db::name('qywx_promotion_range_sync')->where('pool_id', $poolId)->lock(true)->find();
if (!$fresh || (int) ($fresh['status'] ?? 0) === 5) {
return false;
}
if ((string) ($fresh['lock_token'] ?? '') !== $token
|| (int) ($fresh['lock_until'] ?? 0) <= time()) {
// 过期工作不能覆盖新工作的确认结果。它可能较晚触达企微,需再推一次最新范围。
QywxPromotionMemberSchedulerService::requestPoolSync($poolId, (int) $fresh['promotion_link_id']);
return false;
}
Db::name('qywx_promotion_link')->where('id', (int) $link['id'])->update([
'wecom_url' => $url,
'remote_status' => 1,
'range_user_json' => json_encode($actualUserIds, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'range_department_json' => json_encode($remote['range_department_ids'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'remote_snapshot' => $snapshot === false ? null : $snapshot,
'last_sync_time' => time(),
'sync_error' => '',
'update_time' => time(),
]);
$isLatest = (int) ($fresh['desired_version'] ?? 0) === $desiredVersion;
Db::name('qywx_promotion_range_sync')->where('pool_id', $poolId)->update([
'status' => $isLatest ? 0 : 1,
'desired_member_id' => 0,
'applied_member_id' => 0,
'applied_version' => $desiredVersion,
'next_retry' => $freshVersion === $desiredVersion ? 0 : time(),
'next_retry' => $isLatest ? 0 : time(),
'attempts' => 0,
'lock_token' => '',
'lock_until' => 0,
@@ -133,13 +142,19 @@ class QywxPromotionRangeSyncService
'update_time' => time(),
]);
return ['status' => 'synced', 'pool_id' => $poolId, 'member_id' => 0];
return $isLatest;
});
return ['status' => $confirmed ? 'synced' : 'pending', 'pool_id' => $poolId, 'member_id' => 0];
} catch (\Throwable $e) {
$attempts = max(1, (int) ($claim['attempts'] ?? 0) + 1);
Db::name('qywx_promotion_range_sync')
->where('pool_id', $poolId)
->where('lock_token', $token)
->update([
Db::transaction(function () use ($poolId, $token, $attempts, $claim, $e): void {
$fresh = Db::name('qywx_promotion_range_sync')->where('pool_id', $poolId)->lock(true)->find();
if (!$fresh || (string) ($fresh['lock_token'] ?? '') !== $token
|| (int) ($fresh['status'] ?? 0) === 5) {
return;
}
Db::name('qywx_promotion_range_sync')->where('pool_id', $poolId)->update([
'status' => 3,
'next_retry' => time() + min(300, 15 * $attempts),
'lock_token' => '',
@@ -147,9 +162,10 @@ class QywxPromotionRangeSyncService
'last_error' => mb_substr($e->getMessage(), 0, 500),
'update_time' => time(),
]);
Db::name('qywx_promotion_link')
->where('id', (int) ($claim['promotion_link_id'] ?? 0))
->update(['sync_error' => mb_substr($e->getMessage(), 0, 500), 'update_time' => time()]);
Db::name('qywx_promotion_link')
->where('id', (int) ($claim['promotion_link_id'] ?? 0))
->update(['sync_error' => mb_substr($e->getMessage(), 0, 500), 'update_time' => time()]);
});
throw $e;
}