更新
This commit is contained in:
@@ -221,7 +221,7 @@ export function generateOrderQrcode(params: any) {
|
|||||||
// ========== IM / 企业微信聊天记录 ==========
|
// ========== IM / 企业微信聊天记录 ==========
|
||||||
|
|
||||||
/** 腾讯云 IM 单聊漫游消息(诊单维度:患者 patient_* 与医生 doctor_*) */
|
/** 腾讯云 IM 单聊漫游消息(诊单维度:患者 patient_* 与医生 doctor_*) */
|
||||||
export function getImChatMessages(params: { diagnosis_id: number }) {
|
export function getImChatMessages(params: { diagnosis_id: number; only_archived?: 0 | 1 }) {
|
||||||
return request.get({ url: '/tcm.diagnosis/getImChatMessages', params })
|
return request.get({ url: '/tcm.diagnosis/getImChatMessages', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1448,6 +1448,7 @@ const createOrderRules: FormRules = {
|
|||||||
patient_id: [{ required: true, message: '请选择诊单患者', trigger: 'change' }],
|
patient_id: [{ required: true, message: '请选择诊单患者', trigger: 'change' }],
|
||||||
recipient_name: [{ required: true, message: '请输入收货人', trigger: 'blur' }],
|
recipient_name: [{ required: true, message: '请输入收货人', trigger: 'blur' }],
|
||||||
recipient_phone: [{ required: true, message: '请输入收货手机', trigger: 'blur' }],
|
recipient_phone: [{ required: true, message: '请输入收货手机', trigger: 'blur' }],
|
||||||
|
region: [{ required: true, message: '请选择省/市/区', trigger: 'change', type: 'array' }],
|
||||||
shipping_address: [{ required: true, message: '请输入收货地址', trigger: 'blur' }],
|
shipping_address: [{ required: true, message: '请输入收货地址', trigger: 'blur' }],
|
||||||
fee_type: [{ required: true, message: '请选择费用类别', trigger: 'change' }],
|
fee_type: [{ required: true, message: '请选择费用类别', trigger: 'change' }],
|
||||||
amount: [
|
amount: [
|
||||||
|
|||||||
@@ -2243,6 +2243,7 @@ async function loadEditPaidOrders(diagnosisId: number, prescriptionOrderId?: num
|
|||||||
const editRules: FormRules = {
|
const editRules: FormRules = {
|
||||||
recipient_name: [{ required: true, message: '请输入收货人', trigger: 'blur' }],
|
recipient_name: [{ required: true, message: '请输入收货人', trigger: 'blur' }],
|
||||||
recipient_phone: [{ required: true, message: '请输入手机', trigger: 'blur' }],
|
recipient_phone: [{ required: true, message: '请输入手机', trigger: 'blur' }],
|
||||||
|
region: [{ required: true, message: '请选择省/市/区', trigger: 'change', type: 'array' }],
|
||||||
shipping_address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
|
shipping_address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
|
||||||
fee_type: [{ required: true, message: '请选择费用类别', trigger: 'change' }],
|
fee_type: [{ required: true, message: '请选择费用类别', trigger: 'change' }],
|
||||||
amount: [
|
amount: [
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<div class="toolbar mb-3">
|
<div class="toolbar mb-3">
|
||||||
<el-button type="primary" link :loading="loading" @click="load">
|
<el-button type="primary" link :loading="loading" @click="refreshLive">
|
||||||
<el-icon class="mr-1"><Refresh /></el-icon>
|
<el-icon class="mr-1"><Refresh /></el-icon>
|
||||||
刷新
|
刷新(同步云端最新)
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -19,44 +19,42 @@
|
|||||||
<template v-if="!loading && rows.length">
|
<template v-if="!loading && rows.length">
|
||||||
<div class="chat-list">
|
<div class="chat-list">
|
||||||
<div
|
<div
|
||||||
v-for="row in rows"
|
v-for="item in rows"
|
||||||
:key="row.msg_id"
|
:key="item.raw.msg_id"
|
||||||
class="chat-row"
|
class="chat-row"
|
||||||
:class="row.is_from_doctor ? 'from-doctor' : 'from-patient'"
|
:class="item.raw.is_from_doctor ? 'from-doctor' : 'from-patient'"
|
||||||
>
|
>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<span class="name">{{ senderLabel(row) }}</span>
|
<span class="name">{{ senderLabel(item.raw) }}</span>
|
||||||
<span class="time">{{ formatTime(row.time) }}</span>
|
<span class="time">{{ formatTime(item.raw.time) }}</span>
|
||||||
<el-tag v-if="typeLabel(row)" size="small" type="info" class="ml-2">
|
<el-tag v-if="item.tag" size="small" type="info" class="ml-2">
|
||||||
{{ typeLabel(row) }}
|
{{ item.tag }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<template v-if="row.msg_type === 'image' && row.image_url">
|
<template v-if="item.raw.msg_type === 'image' && item.raw.image_url">
|
||||||
<el-image
|
<el-image
|
||||||
:src="row.image_url"
|
:src="item.raw.image_url"
|
||||||
:preview-src-list="[row.image_url]"
|
:preview-src-list="[item.raw.image_url]"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
class="chat-img"
|
class="chat-img"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="(row.msg_type === 'file' || row.msg_type === 'sound' || row.msg_type === 'video') && row.file_url">
|
<template v-else-if="(item.raw.msg_type === 'file' || item.raw.msg_type === 'sound' || item.raw.msg_type === 'video') && item.raw.file_url">
|
||||||
<el-link :href="row.file_url" target="_blank" type="primary">
|
<el-link :href="item.raw.file_url" target="_blank" type="primary">
|
||||||
{{ row.file_name || '打开文件' }}
|
{{ item.raw.file_name || '打开文件' }}
|
||||||
</el-link>
|
</el-link>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-for="fr in [parseImFriendly(row)]" :key="row.msg_id + '-body'">
|
<div v-if="item.friendly" class="friendly-text">
|
||||||
<div v-if="fr" class="friendly-text">
|
<div class="friendly-main">{{ item.friendly.main }}</div>
|
||||||
<div class="friendly-main">{{ fr.main }}</div>
|
<div v-if="item.friendly.sub" class="friendly-sub">{{ item.friendly.sub }}</div>
|
||||||
<div v-if="fr.sub" class="friendly-sub">{{ fr.sub }}</div>
|
</div>
|
||||||
</div>
|
<div v-else-if="item.raw.msg_type === 'text' && item.raw.text" class="text-content">
|
||||||
<div v-else-if="row.msg_type === 'text' && row.text" class="text-content">
|
{{ item.raw.text }}
|
||||||
{{ row.text }}
|
</div>
|
||||||
</div>
|
<div v-else-if="item.raw.text" class="text-content">{{ item.raw.text }}</div>
|
||||||
<div v-else-if="row.text" class="text-content">{{ row.text }}</div>
|
<span v-else class="muted">(无法展示该消息类型)</span>
|
||||||
<span v-else class="muted">(无法展示该消息类型)</span>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -80,12 +78,43 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rows = shallowRef<any[]>([])
|
const rawRows = shallowRef<any[]>([])
|
||||||
const patientImId = ref('')
|
const patientImId = ref('')
|
||||||
const patientName = ref('')
|
const patientName = ref('')
|
||||||
|
|
||||||
const patientImHint = computed(() => patientImId.value || 'patient_*')
|
const patientImHint = computed(() => patientImId.value || 'patient_*')
|
||||||
|
|
||||||
|
interface EnrichedRow {
|
||||||
|
raw: any
|
||||||
|
friendly: FriendlyParse | null
|
||||||
|
tag: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MSG_TYPE_LABELS: Record<string, string> = {
|
||||||
|
text: '',
|
||||||
|
image: '图片',
|
||||||
|
file: '文件',
|
||||||
|
sound: '语音',
|
||||||
|
video: '视频',
|
||||||
|
location: '位置',
|
||||||
|
custom: '自定义',
|
||||||
|
face: '表情',
|
||||||
|
other: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = computed<EnrichedRow[]>(() =>
|
||||||
|
rawRows.value.map((row) => {
|
||||||
|
const fr = parseImFriendly(row)
|
||||||
|
let tag = ''
|
||||||
|
if (fr?.tag) {
|
||||||
|
tag = fr.tag
|
||||||
|
} else {
|
||||||
|
tag = MSG_TYPE_LABELS[row.msg_type] || (row.msg_type !== 'other' ? row.msg_type : '')
|
||||||
|
}
|
||||||
|
return { raw: row, friendly: fr, tag }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
function formatTime(ts: number | undefined) {
|
function formatTime(ts: number | undefined) {
|
||||||
if (ts == null || !ts) return '—'
|
if (ts == null || !ts) return '—'
|
||||||
const sec = ts > 1e12 ? Math.floor(ts / 1000) : ts
|
const sec = ts > 1e12 ? Math.floor(ts / 1000) : ts
|
||||||
@@ -103,23 +132,6 @@ function parseImFriendly(row: any): FriendlyParse | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function typeLabel(row: any) {
|
|
||||||
const fr = parseImFriendly(row)
|
|
||||||
if (fr?.tag) return fr.tag
|
|
||||||
const m: Record<string, string> = {
|
|
||||||
text: '',
|
|
||||||
image: '图片',
|
|
||||||
file: '文件',
|
|
||||||
sound: '语音',
|
|
||||||
video: '视频',
|
|
||||||
location: '位置',
|
|
||||||
custom: '自定义',
|
|
||||||
face: '表情',
|
|
||||||
other: ''
|
|
||||||
}
|
|
||||||
return m[row.msg_type] || (row.msg_type !== 'other' ? row.msg_type : '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function senderLabel(row: any) {
|
function senderLabel(row: any) {
|
||||||
if (row.is_from_doctor) {
|
if (row.is_from_doctor) {
|
||||||
return row.from_staff_name ? `医生(${row.from_staff_name})` : '医生/员工'
|
return row.from_staff_name ? `医生(${row.from_staff_name})` : '医生/员工'
|
||||||
@@ -127,35 +139,42 @@ function senderLabel(row: any) {
|
|||||||
return patientName.value ? `患者(${patientName.value})` : '患者'
|
return patientName.value ? `患者(${patientName.value})` : '患者'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load(onlyArchived = false) {
|
||||||
if (!props.diagnosisId) return
|
if (!props.diagnosisId) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = (await getImChatMessages({ diagnosis_id: props.diagnosisId })) as {
|
const res = (await getImChatMessages({
|
||||||
|
diagnosis_id: props.diagnosisId,
|
||||||
|
only_archived: onlyArchived ? 1 : 0
|
||||||
|
})) as {
|
||||||
lists?: any[]
|
lists?: any[]
|
||||||
patient_im_id?: string
|
patient_im_id?: string
|
||||||
patient_name?: string
|
patient_name?: string
|
||||||
}
|
}
|
||||||
rows.value = res?.lists || []
|
rawRows.value = res?.lists || []
|
||||||
patientImId.value = res?.patient_im_id || ''
|
patientImId.value = res?.patient_im_id || ''
|
||||||
patientName.value = res?.patient_name || ''
|
patientName.value = res?.patient_name || ''
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
rows.value = []
|
rawRows.value = []
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshLive() {
|
||||||
|
load(false)
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.diagnosisId,
|
() => props.diagnosisId,
|
||||||
() => {
|
() => {
|
||||||
load()
|
load(true)
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
defineExpose({ refresh: load })
|
defineExpose({ refresh: refreshLive })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -303,12 +303,13 @@ class DiagnosisController extends BaseAdminController
|
|||||||
{
|
{
|
||||||
// 增加执行时间限制到 120 秒
|
// 增加执行时间限制到 120 秒
|
||||||
set_time_limit(120);
|
set_time_limit(120);
|
||||||
|
|
||||||
$diagnosisId = (int)$this->request->get('diagnosis_id', 0);
|
$diagnosisId = (int)$this->request->get('diagnosis_id', 0);
|
||||||
if ($diagnosisId <= 0) {
|
if ($diagnosisId <= 0) {
|
||||||
return $this->fail('诊单ID不能为空');
|
return $this->fail('诊单ID不能为空');
|
||||||
}
|
}
|
||||||
$result = DiagnosisLogic::getImChatMessagesForDiagnosis($diagnosisId);
|
$onlyArchived = (int)$this->request->get('only_archived', 0) === 1;
|
||||||
|
$result = DiagnosisLogic::getImChatMessagesForDiagnosis($diagnosisId, $onlyArchived);
|
||||||
if ($result === false) {
|
if ($result === false) {
|
||||||
return $this->fail(DiagnosisLogic::getError());
|
return $this->fail(DiagnosisLogic::getError());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -736,8 +736,11 @@ class DiagnosisLogic extends BaseLogic
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @notes 拉取与本诊单相关的 IM 单聊记录:本地归档 + 腾讯云漫游合并(归档突破云端约 7 天限制)
|
* @notes 拉取与本诊单相关的 IM 单聊记录:本地归档 + 腾讯云漫游合并(归档突破云端约 7 天限制)
|
||||||
|
*
|
||||||
|
* @param int $diagnosisId
|
||||||
|
* @param bool $onlyArchived 只读取本地归档,不请求腾讯云(用于首次打开快速展示)
|
||||||
*/
|
*/
|
||||||
public static function getImChatMessagesForDiagnosis(int $diagnosisId)
|
public static function getImChatMessagesForDiagnosis(int $diagnosisId, bool $onlyArchived = false)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$diag = Diagnosis::where('id', $diagnosisId)->where('delete_time', null)->find();
|
$diag = Diagnosis::where('id', $diagnosisId)->where('delete_time', null)->find();
|
||||||
@@ -753,6 +756,17 @@ class DiagnosisLogic extends BaseLogic
|
|||||||
$patientImId = 'patient_' . $patientId;
|
$patientImId = 'patient_' . $patientId;
|
||||||
$archived = self::loadArchivedImChatRows($diagnosisId);
|
$archived = self::loadArchivedImChatRows($diagnosisId);
|
||||||
|
|
||||||
|
if ($onlyArchived) {
|
||||||
|
$lists = self::enrichImMessagesWithStaffNames($archived);
|
||||||
|
return [
|
||||||
|
'lists' => $lists,
|
||||||
|
'patient_im_id' => $patientImId,
|
||||||
|
'patient_name' => $diag['patient_name'] ?? '',
|
||||||
|
'doctor_accounts_queried' => [],
|
||||||
|
'only_archived' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$config = self::getTrtcConfig();
|
$config = self::getTrtcConfig();
|
||||||
if (!$config) {
|
if (!$config) {
|
||||||
if (empty($archived)) {
|
if (empty($archived)) {
|
||||||
@@ -780,10 +794,19 @@ class DiagnosisLogic extends BaseLogic
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$live = self::pullLiveImChatMessagesForDiagnosis($diag);
|
$live = self::pullLiveImChatMessagesForDiagnosis($diag, $doctorAccounts);
|
||||||
$merged = self::mergeImMessagesByMsgId($archived, $live);
|
$merged = self::mergeImMessagesByMsgId($archived, $live);
|
||||||
$merged = self::enrichImMessagesWithStaffNames($merged);
|
$merged = self::enrichImMessagesWithStaffNames($merged);
|
||||||
|
|
||||||
|
// 首次云端拉取后异步落库,下一次即可直接读归档,无需再全量扫描医生账号
|
||||||
|
if (!empty($live)) {
|
||||||
|
try {
|
||||||
|
self::persistImChatArchiveRows($diagnosisId, $patientId, $live);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\think\facade\Log::warning('archive im chat on-the-fly failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'lists' => $merged,
|
'lists' => $merged,
|
||||||
'patient_im_id' => $patientImId,
|
'patient_im_id' => $patientImId,
|
||||||
@@ -814,7 +837,13 @@ class DiagnosisLogic extends BaseLogic
|
|||||||
$out['error'] = '诊单不存在';
|
$out['error'] = '诊单不存在';
|
||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
$live = self::pullLiveImChatMessagesForDiagnosis($diag);
|
$accounts = self::collectAllDoctorImPeerAccounts();
|
||||||
|
$assistantId = isset($diag['assistant_id']) ? (int)$diag['assistant_id'] : 0;
|
||||||
|
if ($assistantId > 0) {
|
||||||
|
$accounts[] = 'doctor_' . $assistantId;
|
||||||
|
$accounts = array_values(array_unique($accounts));
|
||||||
|
}
|
||||||
|
$live = self::pullLiveImChatMessagesForDiagnosis($diag, $accounts);
|
||||||
if (empty($live)) {
|
if (empty($live)) {
|
||||||
$out['skipped_live_empty'] = true;
|
$out['skipped_live_empty'] = true;
|
||||||
return $out;
|
return $out;
|
||||||
@@ -926,19 +955,16 @@ class DiagnosisLogic extends BaseLogic
|
|||||||
* @param Diagnosis|array<string, mixed> $diag
|
* @param Diagnosis|array<string, mixed> $diag
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
private static function pullLiveImChatMessagesForDiagnosis($diag): array
|
/**
|
||||||
|
* @param array<int, string> $doctorAccounts 已筛选的医生 IM 账号列表
|
||||||
|
*/
|
||||||
|
private static function pullLiveImChatMessagesForDiagnosis($diag, array $doctorAccounts = []): array
|
||||||
{
|
{
|
||||||
$patientId = (int)$diag['patient_id'];
|
$patientId = (int)$diag['patient_id'];
|
||||||
if ($patientId <= 0) {
|
if ($patientId <= 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
$patientImId = 'patient_' . $patientId;
|
$patientImId = 'patient_' . $patientId;
|
||||||
$doctorAccounts = self::collectAllDoctorImPeerAccounts();
|
|
||||||
$assistantId = isset($diag['assistant_id']) ? (int)$diag['assistant_id'] : 0;
|
|
||||||
if ($assistantId > 0) {
|
|
||||||
$doctorAccounts[] = 'doctor_' . $assistantId;
|
|
||||||
}
|
|
||||||
$doctorAccounts = array_values(array_unique($doctorAccounts));
|
|
||||||
if (empty($doctorAccounts)) {
|
if (empty($doctorAccounts)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user