代办任务
This commit is contained in:
@@ -11,7 +11,8 @@
|
|||||||
"Bash(php think *)",
|
"Bash(php think *)",
|
||||||
"Bash(python3 -c ' *)",
|
"Bash(python3 -c ' *)",
|
||||||
"Bash(python3 *)",
|
"Bash(python3 *)",
|
||||||
"Bash(awk '/<template>/,/<\\\\/template>/{print NR\": \"$0; if\\(/<\\\\/template>/\\) exit}' D:/web/zyt/admin/src/views/consumer/prescription/order_list.vue)"
|
"Bash(awk '/<template>/,/<\\\\/template>/{print NR\": \"$0; if\\(/<\\\\/template>/\\) exit}' D:/web/zyt/admin/src/views/consumer/prescription/order_list.vue)",
|
||||||
|
"Bash(npx eslint *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,3 +26,5 @@ bin-release/
|
|||||||
/.cursor
|
/.cursor
|
||||||
/.codex
|
/.codex
|
||||||
/.agents
|
/.agents
|
||||||
|
/.claude
|
||||||
|
/.claude
|
||||||
|
|||||||
+48
-7
@@ -6,7 +6,11 @@ export function tcmDiagnosisLists(params: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 医助理诊单统计(按部门、按人)
|
// 医助理诊单统计(按部门、按人)
|
||||||
export function assistantDiagnosisStats(params?: { start_time?: string; end_time?: string; days?: number }) {
|
export function assistantDiagnosisStats(params?: {
|
||||||
|
start_time?: string
|
||||||
|
end_time?: string
|
||||||
|
days?: number
|
||||||
|
}) {
|
||||||
return request.get({ url: '/tcm.diagnosis/assistantDiagnosisStats', params })
|
return request.get({ url: '/tcm.diagnosis/assistantDiagnosisStats', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +302,11 @@ export function prescriptionVoid(params: { id: number }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 处方审核:action approve | reject(驳回同时作废处方) */
|
/** 处方审核:action approve | reject(驳回同时作废处方) */
|
||||||
export function prescriptionAudit(params: { id: number; action: 'approve' | 'reject'; remark?: string }) {
|
export function prescriptionAudit(params: {
|
||||||
|
id: number
|
||||||
|
action: 'approve' | 'reject'
|
||||||
|
remark?: string
|
||||||
|
}) {
|
||||||
return request.post({ url: '/tcm.prescription/audit', params })
|
return request.post({ url: '/tcm.prescription/audit', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,10 +409,7 @@ export function prescriptionOrderAddPayOrder(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 为「已发货」订单关联已有支付单并重置支付审核为待审核 */
|
/** 为「已发货」订单关联已有支付单并重置支付审核为待审核 */
|
||||||
export function prescriptionOrderLinkPayOrder(params: {
|
export function prescriptionOrderLinkPayOrder(params: { id: number; pay_order_id: number }) {
|
||||||
id: number
|
|
||||||
pay_order_id: number
|
|
||||||
}) {
|
|
||||||
return request.post({ url: '/tcm.prescriptionOrder/linkPayOrder', params })
|
return request.post({ url: '/tcm.prescriptionOrder/linkPayOrder', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +424,11 @@ export function prescriptionOrderSubmitGancaoRecipel(params: { id: number }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 甘草药管家:预下单测试(仅 CTM_PREVIEW,不提交订单) */
|
/** 甘草药管家:预下单测试(仅 CTM_PREVIEW,不提交订单) */
|
||||||
export function prescriptionOrderPreviewGancaoRecipel(params: { id: number; dose_count?: number; medication_days?: number }) {
|
export function prescriptionOrderPreviewGancaoRecipel(params: {
|
||||||
|
id: number
|
||||||
|
dose_count?: number
|
||||||
|
medication_days?: number
|
||||||
|
}) {
|
||||||
return request.post({ url: '/tcm.prescriptionOrder/previewGancaoRecipel', params })
|
return request.post({ url: '/tcm.prescriptionOrder/previewGancaoRecipel', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,3 +463,35 @@ export function prescriptionLibraryDelete(params: { id: number }) {
|
|||||||
export function prescriptionLibraryDetail(params: { id: number }) {
|
export function prescriptionLibraryDetail(params: { id: number }) {
|
||||||
return request.get({ url: '/tcm.prescriptionLibrary/detail', params })
|
return request.get({ url: '/tcm.prescriptionLibrary/detail', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 诊单待办事项(T5) ==========
|
||||||
|
|
||||||
|
/** 待办列表(按 diagnosis_id) */
|
||||||
|
export function diagnosisTodoLists(params: {
|
||||||
|
diagnosis_id: number
|
||||||
|
page_no?: number
|
||||||
|
page_size?: number
|
||||||
|
status?: number
|
||||||
|
creator_id?: number
|
||||||
|
}) {
|
||||||
|
return request.get({ url: '/tcm.diagnosisTodo/lists', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增待办(remind_time 为 unix 秒级时间戳) */
|
||||||
|
export function diagnosisTodoAdd(params: {
|
||||||
|
diagnosis_id: number
|
||||||
|
content: string
|
||||||
|
remind_time: number
|
||||||
|
}) {
|
||||||
|
return request.post({ url: '/tcm.diagnosisTodo/add', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取消待办(仅 status=0 / 创建人或超管) */
|
||||||
|
export function diagnosisTodoCancel(params: { id: number }) {
|
||||||
|
return request.post({ url: '/tcm.diagnosisTodo/cancel', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 待办详情 */
|
||||||
|
export function diagnosisTodoDetail(params: { id: number }) {
|
||||||
|
return request.get({ url: '/tcm.diagnosisTodo/detail', params })
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
<template>
|
||||||
|
<div class="diagnosis-todo-list">
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!props.diagnosisId"
|
||||||
|
@click="handleAdd"
|
||||||
|
v-perms="['tcm.diagnosisTodo/add']"
|
||||||
|
>
|
||||||
|
+ 新增待办
|
||||||
|
</el-button>
|
||||||
|
<el-radio-group v-model="filterStatus" class="ml-3" @change="handleFilterChange">
|
||||||
|
<el-radio-button :value="''">全部</el-radio-button>
|
||||||
|
<el-radio-button :value="0">待执行</el-radio-button>
|
||||||
|
<el-radio-button :value="1">已发送</el-radio-button>
|
||||||
|
<el-radio-button :value="3">失败</el-radio-button>
|
||||||
|
<el-radio-button :value="2">已取消</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
<el-button class="ml-3" @click="fetchList">刷新</el-button>
|
||||||
|
<span class="hint">
|
||||||
|
推送由后端 cron 分钟级触发,需服务器配置
|
||||||
|
<code>tcm:diagnosis-todo-notify</code>。
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="pager.loading"
|
||||||
|
:data="pager.lists"
|
||||||
|
border
|
||||||
|
class="todo-table"
|
||||||
|
empty-text="暂无待办"
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<el-table-column label="提醒时间" prop="remind_time_text" width="160" />
|
||||||
|
<el-table-column label="内容" prop="content" min-width="240" show-overflow-tooltip />
|
||||||
|
<el-table-column label="状态" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusTagType(row.status)" effect="light" size="small">
|
||||||
|
{{ row.status_text }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建人" prop="creator_name" width="120">
|
||||||
|
<template #default="{ row }">{{ row.creator_name || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="推送时间" prop="notified_at_text" width="160">
|
||||||
|
<template #default="{ row }">{{ row.notified_at_text || '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="失败原因" prop="error" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.status === 3" class="text-danger">{{ row.error || '未知错误' }}</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
v-if="row.status === 0 && row.can_cancel"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleCancel(row)"
|
||||||
|
v-perms="['tcm.diagnosisTodo/cancel']"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</el-button>
|
||||||
|
<span v-else class="text-muted">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-wrap">
|
||||||
|
<pagination v-model="pager" @change="fetchList" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增弹框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="addDialogVisible"
|
||||||
|
title="新增待办"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form ref="addFormRef" :model="addForm" :rules="addRules" label-width="100px">
|
||||||
|
<el-form-item label="提醒时间" prop="remindAt">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="addForm.remindAt"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="选择提醒时间"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
value-format="x"
|
||||||
|
:disabled-date="disablePastDate"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="提醒内容" prop="content">
|
||||||
|
<el-input
|
||||||
|
v-model="addForm.content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
:maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="例:3 天后回访患者复测餐后血糖"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="提醒人">
|
||||||
|
<span class="text-muted">企微推送给当前登录账号(创建人本人)</span>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="addDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="addSubmitting" @click="handleAddSubmit">
|
||||||
|
提交
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { diagnosisTodoLists, diagnosisTodoAdd, diagnosisTodoCancel } from '@/api/tcm'
|
||||||
|
import feedback from '@/utils/feedback'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
diagnosisId: number | null | undefined
|
||||||
|
patientId?: number | null
|
||||||
|
patientName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
// pager 与项目内 <pagination v-model="pager" /> 约定的字段保持一致
|
||||||
|
const pager = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 15,
|
||||||
|
count: 0,
|
||||||
|
loading: false,
|
||||||
|
lists: [] as any[]
|
||||||
|
})
|
||||||
|
|
||||||
|
const filterStatus = ref<number | ''>('')
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
if (!props.diagnosisId || Number(props.diagnosisId) <= 0) {
|
||||||
|
pager.lists = []
|
||||||
|
pager.count = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pager.loading = true
|
||||||
|
try {
|
||||||
|
const params: {
|
||||||
|
page_no: number
|
||||||
|
page_size: number
|
||||||
|
diagnosis_id: number
|
||||||
|
status?: number
|
||||||
|
} = {
|
||||||
|
page_no: pager.page,
|
||||||
|
page_size: pager.size,
|
||||||
|
diagnosis_id: Number(props.diagnosisId)
|
||||||
|
}
|
||||||
|
if (filterStatus.value !== '' && filterStatus.value !== undefined) {
|
||||||
|
params.status = Number(filterStatus.value)
|
||||||
|
}
|
||||||
|
const res: any = await diagnosisTodoLists(params)
|
||||||
|
pager.count = Number(res?.count ?? 0)
|
||||||
|
pager.lists = Array.isArray(res?.lists) ? res.lists : []
|
||||||
|
} catch {
|
||||||
|
// request 拦截器已有错误 toast
|
||||||
|
pager.lists = []
|
||||||
|
pager.count = 0
|
||||||
|
} finally {
|
||||||
|
pager.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.diagnosisId,
|
||||||
|
() => {
|
||||||
|
pager.page = 1
|
||||||
|
fetchList()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleFilterChange = () => {
|
||||||
|
pager.page = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusTagType = (status: number): 'info' | 'success' | 'warning' | 'danger' => {
|
||||||
|
switch (Number(status)) {
|
||||||
|
case 0:
|
||||||
|
return 'info'
|
||||||
|
case 1:
|
||||||
|
return 'success'
|
||||||
|
case 2:
|
||||||
|
return 'warning'
|
||||||
|
case 3:
|
||||||
|
return 'danger'
|
||||||
|
default:
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 新增 ==========
|
||||||
|
const addDialogVisible = ref(false)
|
||||||
|
const addSubmitting = ref(false)
|
||||||
|
const addFormRef = ref<FormInstance>()
|
||||||
|
const addForm = reactive({
|
||||||
|
remindAt: '' as string | number,
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
const addRules: FormRules = {
|
||||||
|
remindAt: [{ required: true, message: '请选择提醒时间', trigger: 'change' }],
|
||||||
|
content: [
|
||||||
|
{ required: true, message: '请输入提醒内容', trigger: 'blur' },
|
||||||
|
{ max: 500, message: '内容最多 500 字', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const disablePastDate = (date: Date) => {
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
return date.getTime() < today.getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
if (!props.diagnosisId) {
|
||||||
|
feedback.msgWarning('诊单未保存,无法添加待办')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addForm.remindAt = ''
|
||||||
|
addForm.content = ''
|
||||||
|
addDialogVisible.value = true
|
||||||
|
nextTick(() => addFormRef.value?.clearValidate())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddSubmit = async () => {
|
||||||
|
if (!addFormRef.value) return
|
||||||
|
const valid = await addFormRef.value.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
const remindMs = Number(addForm.remindAt)
|
||||||
|
if (!Number.isFinite(remindMs) || remindMs <= Date.now() + 30 * 1000) {
|
||||||
|
feedback.msgWarning('提醒时间必须晚于当前时间至少 30 秒')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubmitting.value = true
|
||||||
|
try {
|
||||||
|
await diagnosisTodoAdd({
|
||||||
|
diagnosis_id: Number(props.diagnosisId),
|
||||||
|
content: addForm.content.trim(),
|
||||||
|
remind_time: Math.floor(remindMs / 1000)
|
||||||
|
})
|
||||||
|
feedback.msgSuccess('已创建')
|
||||||
|
addDialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} catch {
|
||||||
|
// request 拦截器已有错误 toast
|
||||||
|
} finally {
|
||||||
|
addSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 取消 ==========
|
||||||
|
const handleCancel = async (row: any) => {
|
||||||
|
try {
|
||||||
|
await feedback.confirm(`确认取消「${row.remind_time_text} · ${truncate(row.content, 30)}」吗?`)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await diagnosisTodoCancel({ id: Number(row.id) })
|
||||||
|
feedback.msgSuccess('已取消')
|
||||||
|
fetchList()
|
||||||
|
} catch {
|
||||||
|
// request 拦截器已有错误 toast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const truncate = (text: string, len: number) => {
|
||||||
|
if (!text) return ''
|
||||||
|
return text.length > len ? text.slice(0, len) + '...' : text
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ refresh: fetchList })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.diagnosis-todo-list {
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 12px;
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -594,7 +594,24 @@
|
|||||||
/>
|
/>
|
||||||
<el-empty v-else description="请先保存诊单后再添加运动打卡记录" />
|
<el-empty v-else description="请先保存诊单后再添加运动打卡记录" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 待办事项标签页(T5:企微推送给创建人本人) -->
|
||||||
|
<el-tab-pane
|
||||||
|
label="待办事项"
|
||||||
|
name="todo"
|
||||||
|
:disabled="!formData.id"
|
||||||
|
lazy
|
||||||
|
v-if="hasPermission(['tcm.diagnosisTodo/lists'])"
|
||||||
|
>
|
||||||
|
<diagnosis-todo-list
|
||||||
|
v-if="formData.id"
|
||||||
|
:diagnosis-id="Number(formData.id)"
|
||||||
|
:patient-id="Number(formData.patient_id)"
|
||||||
|
:patient-name="formData.patient_name"
|
||||||
|
/>
|
||||||
|
<el-empty v-else description="请先保存诊单后再添加待办事项" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
<!-- 病历记录标签页(开方后自动显示) -->
|
<!-- 病历记录标签页(开方后自动显示) -->
|
||||||
<el-tab-pane label="处方" name="bingli" :disabled="!formData.id" v-if="hasPermission(['tcm.diagnosis/chufang'])">
|
<el-tab-pane label="处方" name="bingli" :disabled="!formData.id" v-if="hasPermission(['tcm.diagnosis/chufang'])">
|
||||||
<div v-if="formData.id" class="bingli-tab-content">
|
<div v-if="formData.id" class="bingli-tab-content">
|
||||||
@@ -705,6 +722,7 @@ import { QuestionFilled } from '@element-plus/icons-vue'
|
|||||||
import BloodRecordList from './components/BloodRecordList.vue'
|
import BloodRecordList from './components/BloodRecordList.vue'
|
||||||
import DietRecordList from './components/DietRecordList.vue'
|
import DietRecordList from './components/DietRecordList.vue'
|
||||||
import ExerciseRecordList from './components/ExerciseRecordList.vue'
|
import ExerciseRecordList from './components/ExerciseRecordList.vue'
|
||||||
|
import DiagnosisTodoList from './components/DiagnosisTodoList.vue'
|
||||||
import CaseRecordList from './components/CaseRecordList.vue'
|
import CaseRecordList from './components/CaseRecordList.vue'
|
||||||
import CallRecordPanel from './components/CallRecordPanel.vue'
|
import CallRecordPanel from './components/CallRecordPanel.vue'
|
||||||
import ImChatRecordPanel from './components/ImChatRecordPanel.vue'
|
import ImChatRecordPanel from './components/ImChatRecordPanel.vue'
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||||
|
// | 开源版本可自由商用,可去除界面版权logo
|
||||||
|
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||||
|
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||||
|
// | 访问官网:https://www.likeadmin.cn
|
||||||
|
// | likeadmin团队 版权所有 拥有最终解释权
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | author: likeadminTeam
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\adminapi\controller\tcm;
|
||||||
|
|
||||||
|
use app\adminapi\controller\BaseAdminController;
|
||||||
|
use app\adminapi\lists\tcm\DiagnosisTodoLists;
|
||||||
|
use app\adminapi\logic\tcm\DiagnosisTodoLogic;
|
||||||
|
use app\adminapi\validate\tcm\DiagnosisTodoValidate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊单待办事项控制器
|
||||||
|
*
|
||||||
|
* 职责:仅 lists / add / cancel / detail;编辑能力故意不开放(要改 = 取消重建)
|
||||||
|
*
|
||||||
|
* @package app\adminapi\controller\tcm
|
||||||
|
*/
|
||||||
|
class DiagnosisTodoController extends BaseAdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @notes 待办列表(按 diagnosis_id)
|
||||||
|
*/
|
||||||
|
public function lists()
|
||||||
|
{
|
||||||
|
(new DiagnosisTodoValidate())->goCheck('list');
|
||||||
|
|
||||||
|
return $this->dataLists(new DiagnosisTodoLists());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 新增待办
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
$params = (new DiagnosisTodoValidate())->post()->goCheck('add');
|
||||||
|
$result = DiagnosisTodoLogic::add($params, $this->adminId, $this->adminInfo);
|
||||||
|
if (!$result) {
|
||||||
|
return $this->fail(DiagnosisTodoLogic::getError() ?: '创建失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('创建成功', [], 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 取消待办(仅 status=0 可取消,仅创建人/超管)
|
||||||
|
*/
|
||||||
|
public function cancel()
|
||||||
|
{
|
||||||
|
$params = (new DiagnosisTodoValidate())->post()->goCheck('cancel');
|
||||||
|
$result = DiagnosisTodoLogic::cancel((int) $params['id'], $this->adminId);
|
||||||
|
if (!$result) {
|
||||||
|
return $this->fail(DiagnosisTodoLogic::getError() ?: '取消失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success('已取消', [], 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 待办详情
|
||||||
|
*/
|
||||||
|
public function detail()
|
||||||
|
{
|
||||||
|
$params = (new DiagnosisTodoValidate())->goCheck('detail');
|
||||||
|
$result = DiagnosisTodoLogic::detail((int) $params['id'], $this->adminId);
|
||||||
|
|
||||||
|
return $this->data($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||||
|
// | 开源版本可自由商用,可去除界面版权logo
|
||||||
|
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||||
|
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||||
|
// | 访问官网:https://www.likeadmin.cn
|
||||||
|
// | likeadmin团队 版权所有 拥有最终解释权
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | author: likeadminTeam
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\adminapi\lists\tcm;
|
||||||
|
|
||||||
|
use app\adminapi\lists\BaseAdminDataLists;
|
||||||
|
use app\adminapi\logic\tcm\DiagnosisTodoLogic;
|
||||||
|
use app\common\lists\ListsSearchInterface;
|
||||||
|
use app\common\model\tcm\DiagnosisTodo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊单待办事项列表
|
||||||
|
*
|
||||||
|
* 默认按 remind_time desc 排序;按 diagnosis_id 过滤。
|
||||||
|
*
|
||||||
|
* @package app\adminapi\lists\tcm
|
||||||
|
*/
|
||||||
|
class DiagnosisTodoLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||||
|
{
|
||||||
|
public function setSearch(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'=' => ['diagnosis_id', 'status', 'creator_id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lists(): array
|
||||||
|
{
|
||||||
|
$lists = DiagnosisTodo::where($this->searchWhere)
|
||||||
|
->field([
|
||||||
|
'id', 'diagnosis_id', 'patient_id', 'content', 'remind_time',
|
||||||
|
'status', 'creator_id', 'creator_name', 'notified_at', 'error',
|
||||||
|
'cancelled_at', 'cancelled_by', 'create_time', 'update_time',
|
||||||
|
])
|
||||||
|
->append([
|
||||||
|
'status_text',
|
||||||
|
'remind_time_text',
|
||||||
|
'notified_at_text',
|
||||||
|
'cancelled_at_text',
|
||||||
|
])
|
||||||
|
->order('remind_time', 'desc')
|
||||||
|
->order('id', 'desc')
|
||||||
|
->limit($this->limitOffset, $this->limitLength)
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
// 业务态:是否能被「我」取消(前端按钮显隐用)
|
||||||
|
$adminId = (int) $this->adminId;
|
||||||
|
foreach ($lists as &$item) {
|
||||||
|
$item['can_cancel'] = DiagnosisTodoLogic::canCancel($item, $adminId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return DiagnosisTodo::where($this->searchWhere)->count();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||||
|
// | 开源版本可自由商用,可去除界面版权logo
|
||||||
|
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||||
|
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||||
|
// | 访问官网:https://www.likeadmin.cn
|
||||||
|
// | likeadmin团队 版权所有 拥有最终解释权
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | author: likeadminTeam
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\adminapi\logic\tcm;
|
||||||
|
|
||||||
|
use app\common\logic\BaseLogic;
|
||||||
|
use app\common\model\auth\Admin;
|
||||||
|
use app\common\model\auth\AdminRole;
|
||||||
|
use app\common\model\tcm\Diagnosis;
|
||||||
|
use app\common\model\tcm\DiagnosisTodo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊单待办事项逻辑
|
||||||
|
*
|
||||||
|
* 状态机:
|
||||||
|
* add() → status=0 (待执行)
|
||||||
|
* cron → status=1 (已发送) / status=3 (失败)
|
||||||
|
* cancel() → status=2 (已取消,仅 status=0 可取消)
|
||||||
|
*
|
||||||
|
* 鉴权:
|
||||||
|
* add:诊单需存在;数据权限交由路由层「tcm.diagnosisTodo/add」节点 + 前端 tab disabled 控制。
|
||||||
|
* cancel:仅创建人本人或超级管理员 (role_id=1) 可取消,且仅 status=0 时。
|
||||||
|
*
|
||||||
|
* @package app\adminapi\logic\tcm
|
||||||
|
*/
|
||||||
|
class DiagnosisTodoLogic extends BaseLogic
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @notes 新增待办
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $params 已通过 sceneAdd 验证
|
||||||
|
* @param int $adminId 当前 admin id
|
||||||
|
* @param array<string,mixed> $adminInfo request->adminInfo
|
||||||
|
*/
|
||||||
|
public static function add(array $params, int $adminId, array $adminInfo): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$diagnosisId = (int) ($params['diagnosis_id'] ?? 0);
|
||||||
|
$diagnosis = Diagnosis::findOrEmpty($diagnosisId);
|
||||||
|
if ($diagnosis->isEmpty()) {
|
||||||
|
self::setError('诊单不存在');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$remindTime = (int) ($params['remind_time'] ?? 0);
|
||||||
|
if ($remindTime <= time()) {
|
||||||
|
self::setError('提醒时间必须晚于当前时间');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$creatorName = trim((string) ($adminInfo['name'] ?? ''));
|
||||||
|
if ($creatorName === '' && $adminId > 0) {
|
||||||
|
$creatorName = (string) Admin::where('id', $adminId)->value('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
DiagnosisTodo::create([
|
||||||
|
'diagnosis_id' => $diagnosisId,
|
||||||
|
'patient_id' => (int) ($diagnosis->getAttr('patient_id') ?? 0),
|
||||||
|
'content' => trim((string) ($params['content'] ?? '')),
|
||||||
|
'remind_time' => $remindTime,
|
||||||
|
'status' => DiagnosisTodo::STATUS_PENDING,
|
||||||
|
'creator_id' => $adminId,
|
||||||
|
'creator_name' => $creatorName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
self::setError($e->getMessage());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 取消待办(人工)
|
||||||
|
*/
|
||||||
|
public static function cancel(int $id, int $adminId): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$todo = DiagnosisTodo::findOrEmpty($id);
|
||||||
|
if ($todo->isEmpty()) {
|
||||||
|
self::setError('待办不存在');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = (int) $todo->getAttr('status');
|
||||||
|
if ($status !== DiagnosisTodo::STATUS_PENDING) {
|
||||||
|
self::setError('该待办已不是「待执行」状态,无法取消');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$creatorId = (int) $todo->getAttr('creator_id');
|
||||||
|
$isSuper = self::isSuperAdmin($adminId);
|
||||||
|
if ($creatorId !== $adminId && !$isSuper) {
|
||||||
|
self::setError('仅创建人或超级管理员可取消');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$todo->save([
|
||||||
|
'status' => DiagnosisTodo::STATUS_CANCELLED,
|
||||||
|
'cancelled_at' => time(),
|
||||||
|
'cancelled_by' => $adminId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
self::setError($e->getMessage());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 详情
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function detail(int $id, int $adminId = 0): array
|
||||||
|
{
|
||||||
|
$todo = DiagnosisTodo::findOrEmpty($id);
|
||||||
|
if ($todo->isEmpty()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$arr = $todo->append([
|
||||||
|
'status_text',
|
||||||
|
'remind_time_text',
|
||||||
|
'notified_at_text',
|
||||||
|
'cancelled_at_text',
|
||||||
|
])->toArray();
|
||||||
|
|
||||||
|
// 业务态:是否能被「我」取消(前端按钮显隐用)
|
||||||
|
$arr['can_cancel'] = self::canCancel($todo->toArray(), $adminId);
|
||||||
|
|
||||||
|
return $arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否超管:role_id=1
|
||||||
|
*/
|
||||||
|
public static function isSuperAdmin(int $adminId): bool
|
||||||
|
{
|
||||||
|
if ($adminId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$roleIds = AdminRole::where('admin_id', $adminId)->column('role_id');
|
||||||
|
|
||||||
|
return in_array(1, array_map('intval', $roleIds), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务判定:当前 admin 能否取消该待办
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $todoRow
|
||||||
|
*/
|
||||||
|
public static function canCancel(array $todoRow, int $adminId): bool
|
||||||
|
{
|
||||||
|
if ((int) ($todoRow['status'] ?? -1) !== DiagnosisTodo::STATUS_PENDING) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($adminId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((int) ($todoRow['creator_id'] ?? 0) === $adminId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::isSuperAdmin($adminId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||||
|
// | 开源版本可自由商用,可去除界面版权logo
|
||||||
|
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||||
|
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||||
|
// | 访问官网:https://www.likeadmin.cn
|
||||||
|
// | likeadmin团队 版权所有 拥有最终解释权
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | author: likeadminTeam
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\adminapi\validate\tcm;
|
||||||
|
|
||||||
|
use app\common\model\tcm\Diagnosis;
|
||||||
|
use app\common\model\tcm\DiagnosisTodo;
|
||||||
|
use app\common\validate\BaseValidate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊单待办事项验证
|
||||||
|
* Class DiagnosisTodoValidate
|
||||||
|
* @package app\adminapi\validate\tcm
|
||||||
|
*/
|
||||||
|
class DiagnosisTodoValidate extends BaseValidate
|
||||||
|
{
|
||||||
|
protected $rule = [
|
||||||
|
'id' => 'require|number',
|
||||||
|
'diagnosis_id' => 'require|number|gt:0|checkDiagnosis',
|
||||||
|
'content' => 'require|length:1,500',
|
||||||
|
'remind_time' => 'require|number|checkRemindFuture',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $message = [
|
||||||
|
'id.require' => '参数缺失',
|
||||||
|
'diagnosis_id.require' => '诊单 id 不能为空',
|
||||||
|
'diagnosis_id.gt' => '诊单 id 非法',
|
||||||
|
'content.require' => '请输入提醒内容',
|
||||||
|
'content.length' => '提醒内容长度需在 1-500 字',
|
||||||
|
'remind_time.require' => '请选择提醒时间',
|
||||||
|
'remind_time.number' => '提醒时间格式错误',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function sceneAdd()
|
||||||
|
{
|
||||||
|
return $this->only(['diagnosis_id', 'content', 'remind_time']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneCancel()
|
||||||
|
{
|
||||||
|
return $this->only(['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneDetail()
|
||||||
|
{
|
||||||
|
return $this->only(['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sceneList()
|
||||||
|
{
|
||||||
|
return $this->only(['diagnosis_id'])
|
||||||
|
->append('diagnosis_id', 'require|number|gt:0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊单存在校验
|
||||||
|
*/
|
||||||
|
protected function checkDiagnosis($value): bool|string
|
||||||
|
{
|
||||||
|
$diagnosis = Diagnosis::findOrEmpty((int) $value);
|
||||||
|
if ($diagnosis->isEmpty()) {
|
||||||
|
return '诊单不存在';
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提醒时间必须在当前时间之后(至少 30 秒),避免立刻就过期
|
||||||
|
*/
|
||||||
|
protected function checkRemindFuture($value): bool|string
|
||||||
|
{
|
||||||
|
$ts = (int) $value;
|
||||||
|
if ($ts <= time() + 30) {
|
||||||
|
return '提醒时间必须晚于当前时间';
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\command;
|
||||||
|
|
||||||
|
use app\common\model\auth\Admin;
|
||||||
|
use app\common\model\tcm\Diagnosis;
|
||||||
|
use app\common\model\tcm\DiagnosisTodo;
|
||||||
|
use app\common\service\wechat\WechatWorkAppMessageService;
|
||||||
|
use think\console\Command;
|
||||||
|
use think\console\Input;
|
||||||
|
use think\console\Output;
|
||||||
|
use think\facade\Db;
|
||||||
|
use think\facade\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊单待办事项 - 企业微信定时推送
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* php think tcm:diagnosis-todo-notify
|
||||||
|
*
|
||||||
|
* 推荐 cron(每分钟执行):
|
||||||
|
* * * * * * cd /path/to/server && php think tcm:diagnosis-todo-notify >> /var/log/zyt-todo.log 2>&1
|
||||||
|
*
|
||||||
|
* 行为:
|
||||||
|
* 1. 取出 status=0 且 remind_time<=now 的待办,按 remind_time asc,限 200 条
|
||||||
|
* 2. 每条用乐观锁 (update set status=1 where id=? and status=0) 抢占,避免并发重复推送
|
||||||
|
* 3. 找创建人 admin.work_wechat_userid,未绑定 → status=3 + error
|
||||||
|
* 4. 调 WechatWorkAppMessageService::sendTextToUser,失败 → status=3 + error(终态,不重试)
|
||||||
|
* 5. 整批不中断;输出 处理 N / 成功 X / 失败 Y / 跳过未绑定 Z
|
||||||
|
*/
|
||||||
|
class DiagnosisTodoNotify extends Command
|
||||||
|
{
|
||||||
|
/** 单次扫描最大条数 */
|
||||||
|
private const BATCH_LIMIT = 200;
|
||||||
|
|
||||||
|
protected function configure()
|
||||||
|
{
|
||||||
|
$this->setName('tcm:diagnosis-todo-notify')
|
||||||
|
->setDescription('诊单待办事项:扫描到点的待执行项并向创建人发送企业微信消息');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(Input $input, Output $output): int
|
||||||
|
{
|
||||||
|
$startTs = microtime(true);
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$output->writeln('[' . date('Y-m-d H:i:s', $now) . '] 开始扫描诊单待办事项...');
|
||||||
|
|
||||||
|
$totalProcessed = 0;
|
||||||
|
$totalSent = 0;
|
||||||
|
$totalFailed = 0;
|
||||||
|
$totalSkipped = 0; // 被并发抢占
|
||||||
|
$totalUnbound = 0; // 创建人未绑定企微(计入 failed)
|
||||||
|
|
||||||
|
try {
|
||||||
|
$todoTable = (new DiagnosisTodo())->getTable();
|
||||||
|
|
||||||
|
$rows = Db::name(self::stripTablePrefix($todoTable))
|
||||||
|
->where('status', DiagnosisTodo::STATUS_PENDING)
|
||||||
|
->where('remind_time', '<=', $now)
|
||||||
|
->whereNull('delete_time')
|
||||||
|
->order('remind_time', 'asc')
|
||||||
|
->limit(self::BATCH_LIMIT)
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
$output->writeln('待处理数量:' . count($rows));
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$totalProcessed++;
|
||||||
|
$todoId = (int) ($row['id'] ?? 0);
|
||||||
|
if ($todoId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 乐观锁:抢占成功时影响 1 行;并发场景下另一个进程已抢走则影响 0 行 → 跳过
|
||||||
|
$affected = Db::name(self::stripTablePrefix($todoTable))
|
||||||
|
->where('id', $todoId)
|
||||||
|
->where('status', DiagnosisTodo::STATUS_PENDING)
|
||||||
|
->update([
|
||||||
|
'status' => DiagnosisTodo::STATUS_SENT,
|
||||||
|
'notified_at' => $now,
|
||||||
|
'update_time' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($affected <= 0) {
|
||||||
|
$totalSkipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取创建人 work_wechat_userid
|
||||||
|
$creatorId = (int) ($row['creator_id'] ?? 0);
|
||||||
|
$wxId = '';
|
||||||
|
if ($creatorId > 0) {
|
||||||
|
$wxId = (string) Admin::where('id', $creatorId)->value('work_wechat_userid');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($wxId === '') {
|
||||||
|
$this->markFailed($todoTable, $todoId, '创建人未绑定企业微信 userid');
|
||||||
|
$totalUnbound++;
|
||||||
|
$totalFailed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拼推送文本
|
||||||
|
$text = $this->buildText($row);
|
||||||
|
|
||||||
|
$res = WechatWorkAppMessageService::sendTextToUser($wxId, $text);
|
||||||
|
if (!($res['ok'] ?? false)) {
|
||||||
|
$errMsg = (string) ($res['message'] ?? '企业微信推送失败');
|
||||||
|
$this->markFailed($todoTable, $todoId, $errMsg);
|
||||||
|
$totalFailed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalSent++;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('诊单待办推送异常 todo_id=' . $todoId . ' msg=' . $e->getMessage());
|
||||||
|
try {
|
||||||
|
$this->markFailed($todoTable, $todoId, '系统异常: ' . $e->getMessage());
|
||||||
|
} catch (\Throwable $ee) {
|
||||||
|
Log::error('回写失败状态出错 todo_id=' . $todoId . ' msg=' . $ee->getMessage());
|
||||||
|
}
|
||||||
|
$totalFailed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('诊单待办批处理异常: ' . $e->getMessage());
|
||||||
|
$output->error('批处理异常: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$duration = round(microtime(true) - $startTs, 3);
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'处理完成。总数: %d, 成功: %d, 失败: %d (其中未绑定企微: %d), 并发跳过: %d, 耗时: %ss',
|
||||||
|
$totalProcessed,
|
||||||
|
$totalSent,
|
||||||
|
$totalFailed,
|
||||||
|
$totalUnbound,
|
||||||
|
$totalSkipped,
|
||||||
|
$duration
|
||||||
|
));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接推送文本
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $row 待办记录原始行
|
||||||
|
*/
|
||||||
|
private function buildText(array $row): string
|
||||||
|
{
|
||||||
|
$patientName = '';
|
||||||
|
$diagnosisId = (int) ($row['diagnosis_id'] ?? 0);
|
||||||
|
if ($diagnosisId > 0) {
|
||||||
|
$patientName = (string) Diagnosis::where('id', $diagnosisId)->value('patient_name');
|
||||||
|
}
|
||||||
|
|
||||||
|
$remindAt = (int) ($row['remind_time'] ?? 0);
|
||||||
|
$content = trim((string) ($row['content'] ?? ''));
|
||||||
|
|
||||||
|
$lines = [
|
||||||
|
'【患者跟踪提醒】',
|
||||||
|
'患者:' . ($patientName !== '' ? $patientName : '-'),
|
||||||
|
'时间:' . ($remindAt > 0 ? date('Y-m-d H:i', $remindAt) : '-'),
|
||||||
|
'内容:' . ($content !== '' ? $content : '-'),
|
||||||
|
'— 二中心跟踪系统',
|
||||||
|
];
|
||||||
|
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记一条待办为「发送失败」,写入 error 字段
|
||||||
|
*/
|
||||||
|
private function markFailed(string $todoTable, int $todoId, string $errMsg): void
|
||||||
|
{
|
||||||
|
$errMsg = mb_substr($errMsg, 0, 500);
|
||||||
|
Db::name(self::stripTablePrefix($todoTable))
|
||||||
|
->where('id', $todoId)
|
||||||
|
->update([
|
||||||
|
'status' => DiagnosisTodo::STATUS_FAILED,
|
||||||
|
'error' => $errMsg,
|
||||||
|
'update_time' => time(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Db::name() 接收的是不带前缀的表名,BloodRecord 等 Model::getTable() 返回的是带前缀的全名。
|
||||||
|
* 这里去掉首段 `<prefix>_`。注意:项目实际前缀是 `zyt_`,但 think-orm 的 Db::name()
|
||||||
|
* 内部会用 config('database.prefix') 自动拼回,所以这里只需剥离一次即可。
|
||||||
|
*/
|
||||||
|
private static function stripTablePrefix(string $tableWithPrefix): string
|
||||||
|
{
|
||||||
|
$prefix = (string) config('database.connections.mysql.prefix', '');
|
||||||
|
if ($prefix !== '' && str_starts_with($tableWithPrefix, $prefix)) {
|
||||||
|
return substr($tableWithPrefix, strlen($prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tableWithPrefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | likeadmin快速开发前后端分离管理后台(PHP版)
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
|
||||||
|
// | 开源版本可自由商用,可去除界面版权logo
|
||||||
|
// | gitee下载:https://gitee.com/likeshop_gitee/likeadmin
|
||||||
|
// | github下载:https://github.com/likeshop-github/likeadmin
|
||||||
|
// | 访问官网:https://www.likeadmin.cn
|
||||||
|
// | likeadmin团队 版权所有 拥有最终解释权
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | author: likeadminTeam
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace app\common\model\tcm;
|
||||||
|
|
||||||
|
use app\common\model\BaseModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 诊单待办事项模型
|
||||||
|
*
|
||||||
|
* 状态机:0=待执行 → 1=已发送 / 2=已取消 / 3=发送失败(终态,不重试)
|
||||||
|
*
|
||||||
|
* @package app\common\model\tcm
|
||||||
|
*/
|
||||||
|
class DiagnosisTodo extends BaseModel
|
||||||
|
{
|
||||||
|
protected $name = 'tcm_diagnosis_todo';
|
||||||
|
|
||||||
|
// 自动时间戳类型设置为整型
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'create_time';
|
||||||
|
protected $updateTime = 'update_time';
|
||||||
|
protected $deleteTime = 'delete_time';
|
||||||
|
|
||||||
|
// 取出字段保持整型,前端按需要再格式化
|
||||||
|
protected $dateFormat = false;
|
||||||
|
|
||||||
|
/** 状态:待执行 */
|
||||||
|
public const STATUS_PENDING = 0;
|
||||||
|
/** 状态:已发送(成功) */
|
||||||
|
public const STATUS_SENT = 1;
|
||||||
|
/** 状态:已取消(人工) */
|
||||||
|
public const STATUS_CANCELLED = 2;
|
||||||
|
/** 状态:发送失败(终态,不重试) */
|
||||||
|
public const STATUS_FAILED = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态文本映射
|
||||||
|
*
|
||||||
|
* @var array<int,string>
|
||||||
|
*/
|
||||||
|
public const STATUS_TEXT_MAP = [
|
||||||
|
self::STATUS_PENDING => '待执行',
|
||||||
|
self::STATUS_SENT => '已发送',
|
||||||
|
self::STATUS_CANCELLED => '已取消',
|
||||||
|
self::STATUS_FAILED => '发送失败',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 状态文本访问器
|
||||||
|
*/
|
||||||
|
public function getStatusTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$status = (int) ($data['status'] ?? 0);
|
||||||
|
|
||||||
|
return self::STATUS_TEXT_MAP[$status] ?? '未知';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 提醒时间格式化(Y-m-d H:i)
|
||||||
|
*/
|
||||||
|
public function getRemindTimeTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$ts = (int) ($data['remind_time'] ?? 0);
|
||||||
|
|
||||||
|
return $ts > 0 ? date('Y-m-d H:i', $ts) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 推送时间格式化
|
||||||
|
*/
|
||||||
|
public function getNotifiedAtTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$ts = (int) ($data['notified_at'] ?? 0);
|
||||||
|
|
||||||
|
return $ts > 0 ? date('Y-m-d H:i', $ts) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 取消时间格式化
|
||||||
|
*/
|
||||||
|
public function getCancelledAtTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$ts = (int) ($data['cancelled_at'] ?? 0);
|
||||||
|
|
||||||
|
return $ts > 0 ? date('Y-m-d H:i', $ts) : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,5 +34,7 @@ return [
|
|||||||
'gancao:sync-logistics' => 'app\\command\\GancaoSyncLogisticsRoute',
|
'gancao:sync-logistics' => 'app\\command\\GancaoSyncLogisticsRoute',
|
||||||
// 迁移诊单图片到医生备注表
|
// 迁移诊单图片到医生备注表
|
||||||
'migrate:images-to-doctor-note' => 'app\\command\\MigrateImagesToDoctorNote',
|
'migrate:images-to-doctor-note' => 'app\\command\\MigrateImagesToDoctorNote',
|
||||||
|
// 诊单待办事项:扫描到点的待执行项并向创建人发送企业微信消息
|
||||||
|
'tcm:diagnosis-todo-notify' => 'app\\command\\DiagnosisTodoNotify',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 诊单待办事项:菜单按钮权限注册(T5 - 2026-05-05)
|
||||||
|
# 挂在「中医诊单」(perms=tcm.diagnosis/lists) 菜单下,按钮型权限节点:
|
||||||
|
# tcm.diagnosisTodo/lists 待办列表
|
||||||
|
# tcm.diagnosisTodo/add 新增待办
|
||||||
|
# tcm.diagnosisTodo/cancel 取消待办
|
||||||
|
# detail 不单独注册(前端不主动调,由列表/详情接口已覆盖)
|
||||||
|
|
||||||
|
SET @diag_menu_id := (SELECT id FROM zyt_system_menu WHERE perms = 'tcm.diagnosis/lists' LIMIT 1);
|
||||||
|
|
||||||
|
INSERT INTO zyt_system_menu (
|
||||||
|
pid, type, name, icon, sort, perms, paths, component,
|
||||||
|
selected, params, is_cache, is_show, is_disable, create_time, update_time
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
@diag_menu_id, 'A', '待办列表', '', 60,
|
||||||
|
'tcm.diagnosisTodo/lists', '', '',
|
||||||
|
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||||
|
FROM DUAL
|
||||||
|
WHERE @diag_menu_id IS NOT NULL
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.diagnosisTodo/lists');
|
||||||
|
|
||||||
|
INSERT INTO zyt_system_menu (
|
||||||
|
pid, type, name, icon, sort, perms, paths, component,
|
||||||
|
selected, params, is_cache, is_show, is_disable, create_time, update_time
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
@diag_menu_id, 'A', '新增待办', '', 61,
|
||||||
|
'tcm.diagnosisTodo/add', '', '',
|
||||||
|
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||||
|
FROM DUAL
|
||||||
|
WHERE @diag_menu_id IS NOT NULL
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.diagnosisTodo/add');
|
||||||
|
|
||||||
|
INSERT INTO zyt_system_menu (
|
||||||
|
pid, type, name, icon, sort, perms, paths, component,
|
||||||
|
selected, params, is_cache, is_show, is_disable, create_time, update_time
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
@diag_menu_id, 'A', '取消待办', '', 62,
|
||||||
|
'tcm.diagnosisTodo/cancel', '', '',
|
||||||
|
'', '', 0, 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
|
||||||
|
FROM DUAL
|
||||||
|
WHERE @diag_menu_id IS NOT NULL
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM zyt_system_menu WHERE perms = 'tcm.diagnosisTodo/cancel');
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- 诊单待办事项(T5 - 2026-05-05)
|
||||||
|
-- 用途:医助在诊单上挂「将来要提醒自己的事」,到点由企微推送给创建人本人。
|
||||||
|
-- 设计:状态机 0=待执行 → 1=已发送(成功)/ 2=已取消(人工) / 3=发送失败(终态,不重试)
|
||||||
|
-- 索引说明:
|
||||||
|
-- idx_diagnosis_id —— 编辑页/列表按诊单查询
|
||||||
|
-- idx_creator_id —— 反查某 admin 创建过的待办(取消权限校验等)
|
||||||
|
-- idx_status_remind_time —— 复合索引,cron 扫表「status=0 AND remind_time<=now」核心索引
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `zyt_tcm_diagnosis_todo` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
`diagnosis_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '所属诊单 id(zyt_tcm_diagnosis.id)',
|
||||||
|
`patient_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '冗余患者 id,便于反查',
|
||||||
|
`content` varchar(500) NOT NULL DEFAULT '' COMMENT '提醒内容(企微推送正文)',
|
||||||
|
`remind_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '提醒时间戳(秒);cron 扫描 remind_time<=now',
|
||||||
|
`status` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '状态:0=待执行 1=已发送 2=已取消 3=发送失败',
|
||||||
|
`creator_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建人 admin.id(推送目标)',
|
||||||
|
`creator_name` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人姓名快照',
|
||||||
|
`notified_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '实际推送时间(成功时写入)',
|
||||||
|
`error` varchar(500) NOT NULL DEFAULT '' COMMENT '失败原因(status=3 时写入)',
|
||||||
|
`cancelled_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '取消时间',
|
||||||
|
`cancelled_by` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '取消人 admin.id',
|
||||||
|
`create_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '创建时间',
|
||||||
|
`update_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '更新时间',
|
||||||
|
`delete_time` int(11) unsigned NULL DEFAULT NULL COMMENT '软删除时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_diagnosis_id` (`diagnosis_id`),
|
||||||
|
KEY `idx_creator_id` (`creator_id`),
|
||||||
|
KEY `idx_status_remind_time` (`status`, `remind_time`)
|
||||||
|
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '诊单待办事项(企微定时推送给创建人)';
|
||||||
Reference in New Issue
Block a user