From 6cb49bed3833f7a9220b8ec907ca41ca9cd7536d Mon Sep 17 00:00:00 2001 From: leiyun Date: Wed, 3 Jun 2026 23:15:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/message.sql | 2 +- sql/sample_edit_request.sql | 24 +++ sql/subscribe.sql | 4 +- src/config/router.js | 4 + src/controller/admin/patient.js | 282 +++++++++++++++++++++++++++++- src/controller/mp.js | 119 ++++++++++++- src/model/patient_sample_log.js | 19 +++ view/admin/patient_detail.html | 294 ++++++++++++++++++++++++++++++-- view/admin/patient_index.html | 9 +- 9 files changed, 734 insertions(+), 23 deletions(-) create mode 100644 sql/sample_edit_request.sql create mode 100644 src/model/patient_sample_log.js diff --git a/sql/message.sql b/sql/message.sql index 0423092..a45f1c5 100644 --- a/sql/message.sql +++ b/sql/message.sql @@ -2,7 +2,7 @@ CREATE TABLE `message` ( `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, `patient_id` INT UNSIGNED NOT NULL COMMENT '患者ID', - `type` TINYINT NOT NULL DEFAULT 0 COMMENT '类型:1=审核通过 2=审核拒绝 3=回寄信息', + `type` TINYINT NOT NULL DEFAULT 0 COMMENT '类型:1=审核通过 2=审核拒绝 3=回寄信息 4=送检修改申请审核结果', `title` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '标题', `content` TEXT COMMENT '详细内容', `reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '驳回原因', diff --git a/sql/sample_edit_request.sql b/sql/sample_edit_request.sql new file mode 100644 index 0000000..1f99aa3 --- /dev/null +++ b/sql/sample_edit_request.sql @@ -0,0 +1,24 @@ +-- 送检信息修改申请与送检流转日志 +ALTER TABLE `patient` + MODIFY COLUMN `sample_info_status` tinyint(1) DEFAULT 0 COMMENT '送检信息状态: 0可修改 1已生效 2已寄回 3修改申请待审核', + ADD COLUMN `sample_edit_reason` varchar(500) DEFAULT '' COMMENT '送检信息修改申请原因' AFTER `return_time`, + ADD COLUMN `sample_edit_reject_reason` varchar(500) DEFAULT '' COMMENT '送检信息修改申请驳回原因' AFTER `sample_edit_reason`, + ADD COLUMN `sample_edit_apply_time` datetime DEFAULT NULL COMMENT '送检信息修改申请时间' AFTER `sample_edit_reject_reason`, + ADD COLUMN `sample_edit_audit_time` datetime DEFAULT NULL COMMENT '送检信息修改申请审核时间' AFTER `sample_edit_apply_time`; + +CREATE TABLE IF NOT EXISTS `patient_sample_log` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `patient_id` int unsigned NOT NULL COMMENT '患者ID', + `action` varchar(50) NOT NULL DEFAULT '' COMMENT '操作类型', + `title` varchar(100) NOT NULL DEFAULT '' COMMENT '日志标题', + `content` text COMMENT '日志内容', + `reason` varchar(500) NOT NULL DEFAULT '' COMMENT '原因', + `operator_type` varchar(20) NOT NULL DEFAULT '' COMMENT '操作人类型: patient/admin/system', + `operator_id` int unsigned NOT NULL DEFAULT 0 COMMENT '操作人ID', + `operator_name` varchar(100) NOT NULL DEFAULT '' COMMENT '操作人名称', + `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_patient_id` (`patient_id`), + KEY `idx_action` (`action`), + KEY `idx_create_time` (`create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='患者送检流转日志'; diff --git a/sql/subscribe.sql b/sql/subscribe.sql index f8376e2..6890ac0 100644 --- a/sql/subscribe.sql +++ b/sql/subscribe.sql @@ -12,4 +12,6 @@ ALTER TABLE `wechat_user` ADD COLUMN `mp_env_version` varchar(20) DEFAULT 'release' COMMENT '小程序版本: develop/trial/release' AFTER `patient_id`; -- 更新 pap_mini_cytx 应用的模板配置 -UPDATE `sys_wechat_app` SET `subscribe_templates` = '{"audit_result":"OcMLIGS8pMnpGzZmPkdMEOmmWmvvvU_nN-kmiK5sUGE"}' WHERE `remark` = 'pap_mini_cytx'; +UPDATE `sys_wechat_app` +SET `subscribe_templates` = '{"audit_result":"OcMLIGS8pMnpGzZmPkdMEOmmWmvvvU_nN-kmiK5sUGE","sample_edit_audit":"OcMLIGS8pMnpGzZmPkdMELDgFefqOeQ-1jsit8Ldw2w"}' +WHERE `remark` = 'pap_mini_cytx'; diff --git a/src/config/router.js b/src/config/router.js index d9aaceb..cc22f24 100644 --- a/src/config/router.js +++ b/src/config/router.js @@ -54,6 +54,9 @@ module.exports = [ ['/admin/patient/saveSampleReceiverConfig', 'admin/patient/saveSampleReceiverConfig', 'post'], ['/admin/patient/resetSampleInfo', 'admin/patient/resetSampleInfo', 'post'], ['/admin/patient/saveReturnTrackingNo', 'admin/patient/saveReturnTrackingNo', 'post'], + ['/admin/patient/approveSampleEdit', 'admin/patient/approveSampleEdit', 'post'], + ['/admin/patient/rejectSampleEdit', 'admin/patient/rejectSampleEdit', 'post'], + ['/admin/patient/editSampleInfo', 'admin/patient/editSampleInfo', 'post'], ['/admin/patient/export', 'admin/patient/export'], // 公共接口 ['/common/regions', 'common/regions'], @@ -83,6 +86,7 @@ module.exports = [ ['/api/mp/subscribeConfig', 'mp/subscribeConfig'], ['/api/mp/sampleInfo', 'mp/sampleInfo'], ['/api/mp/saveSampleInfo', 'mp/saveSampleInfo', 'post'], + ['/api/mp/applySampleInfoEdit', 'mp/applySampleInfoEdit', 'post'], ['/api/mp/regenerateSign', 'mp/regenerateSign'], ['/api/mp/regenerateSignByUrl', 'mp/regenerateSignByUrl', 'post'], diff --git a/src/controller/admin/patient.js b/src/controller/admin/patient.js index d143f90..627a80e 100644 --- a/src/controller/admin/patient.js +++ b/src/controller/admin/patient.js @@ -1,13 +1,119 @@ const Base = require('../base.js'); const dayjs = require('dayjs'); +const APP_REMARK = 'pap_mini_cytx'; const AUDIT_APPROVED_CONTENT = '您提交的肠愈同行患者关爱项目资料已审核通过,请于7天内在小程序-个人中心-送检信息中提交相关信息。'; +const SAMPLE_EDIT_AUDIT_TEMPLATE_ID = 'OcMLIGS8pMnpGzZmPkdMELDgFefqOeQ-1jsit8Ldw2w'; module.exports = class extends Base { _canEditPatient() { return this.isSuperAdmin || (this.userPermissions || []).includes('patient:edit'); } + _adminOperator() { + const user = this.adminUser || {}; + return { + operator_id: user.id || 0, + operator_name: user.nickname || user.username || '' + }; + } + + _parseArrayValue(value) { + if (Array.isArray(value)) return value; + if (!value) return []; + try { return JSON.parse(value || '[]') || []; } catch (e) { return []; } + } + + _hasSampleInfo(patient = {}) { + const sampleTypes = this._parseArrayValue(patient.sample_types); + const samplePhotos = this._parseArrayValue(patient.sample_photos); + return !!( + sampleTypes.length || + samplePhotos.length || + patient.report_email || + patient.sample_tracking_no || + patient.return_name || + patient.return_phone || + patient.return_address + ); + } + + _sampleInfoStatusText(patient = {}) { + const status = Number(patient.sample_info_status) || 0; + if (status === 3) return '修改申请待审核'; + if (status === 2) return '已寄回'; + if (status === 1) return '已生效'; + return this._hasSampleInfo(patient) ? '可修改' : '未提交'; + } + + _sampleInfoStatusType(patient = {}) { + const status = Number(patient.sample_info_status) || 0; + if (status === 3) return 'warning'; + if (status === 2) return 'primary'; + if (status === 1) return 'success'; + return this._hasSampleInfo(patient) ? 'warning' : 'info'; + } + + _buildFlowLogs(audits = [], sampleLogs = []) { + const auditLogs = audits.map(item => { + const action = item.action || ''; + let title = '资料流转'; + let content = ''; + if (action === 'submit') { + title = '提交患者资料'; + content = `${item.operator_name || '系统'} 提交了患者资料,等待审核`; + } else if (action === 'approve') { + title = '患者资料审核通过'; + content = `${item.operator_name || '系统'} 审核通过`; + } else if (action === 'reject') { + title = '患者资料审核驳回'; + content = `${item.operator_name || '系统'} 驳回了资料`; + } + return { + id: item.id, + source: 'audit', + action, + title, + content, + reason: item.reason || '', + operator_name: item.operator_name || '', + operator_type: 'admin', + create_time: item.create_time + }; + }); + const logs = sampleLogs.map(item => ({ + id: item.id, + source: 'sample', + action: item.action || '', + title: item.title || '送检信息', + content: item.content || '', + reason: item.reason || '', + operator_name: item.operator_name || '', + operator_type: item.operator_type || '', + create_time: item.create_time + })); + return auditLogs.concat(logs).sort((a, b) => { + const ta = new Date(a.create_time || 0).getTime(); + const tb = new Date(b.create_time || 0).getTime(); + if (tb !== ta) return tb - ta; + return (b.id || 0) - (a.id || 0); + }); + } + + async _addSampleLog(patientId, action, title, content, reason = '') { + const operator = this._adminOperator(); + return this.model('patient_sample_log').addLog({ + patient_id: patientId, + action, + title, + content, + reason, + operator_type: 'admin', + operator_id: operator.operator_id, + operator_name: operator.operator_name + }); + } + // 患者列表页面 async indexAction() { this.assign('currentPage', 'patient'); @@ -60,6 +166,8 @@ module.exports = class extends Base { const cName = regionMap[item.city_code] || ''; const dName = regionMap[item.district_code] || ''; item.region_name = [pName, cName, dName].filter(Boolean).join(' '); + item.sample_info_status_text = this._sampleInfoStatusText(item); + item.sample_info_status_type = this._sampleInfoStatusType(item); }); const counts = await model.getStatusCounts({ keyword, tag, startDate, endDate, province_code, city_code, district_code }); @@ -125,13 +233,21 @@ module.exports = class extends Base { patient.hospital_district_name = regionMap[patient.hospital_district_code] || ''; } - // 获取审核记录 + patient.sample_info_status_text = this._sampleInfoStatusText(patient); + patient.sample_info_status_type = this._sampleInfoStatusType(patient); + + // 获取审核记录和送检流转日志 const audits = await this.model('patient_audit') .where({ patient_id: id }) .order('id DESC') .select(); + const sampleLogs = await this.model('patient_sample_log') + .where({ patient_id: id }) + .order('id DESC') + .select(); + const flowLogs = this._buildFlowLogs(audits, sampleLogs); - return this.success({ patient, audits }); + return this.success({ patient, audits: flowLogs, flowLogs }); } async sampleReceiverConfigAction() { @@ -372,9 +488,14 @@ module.exports = class extends Base { await this.model('patient').where({ id }).update({ sample_info_status: 0, + sample_edit_reason: '', + sample_edit_reject_reason: '', + sample_edit_apply_time: null, + sample_edit_audit_time: null, update_by: this.adminUser?.id || 0 }); + await this._addSampleLog(id, 'sample_reset_edit', '重置送检信息编辑', '后台重置送检信息为可修改'); await this.log('edit', '患者管理', `重置送检信息编辑权限(ID:${id})`); return this.success(); } @@ -409,10 +530,119 @@ module.exports = class extends Base { is_read: 0 }); + await this._addSampleLog(id, 'sample_return', Number(patient.sample_info_status) === 2 ? '修改回寄物流单号' : '填写回寄物流单号', `回寄物流单号:${trackingNo}`); await this.log('edit', '患者管理', `填写回寄物流单号(ID:${id}):${trackingNo}`); return this.success(); } + async approveSampleEditAction() { + if (!this._canEditPatient()) return this.fail('暂无权限'); + const { id } = this.post(); + if (!id) return this.fail('参数错误'); + + const patient = await this.model('patient').where({ id, is_deleted: 0 }).find(); + if (think.isEmpty(patient)) return this.fail('患者不存在'); + if (Number(patient.sample_info_status) !== 3) return this.fail('当前没有待审核的送检修改申请'); + + const reason = patient.sample_edit_reason || ''; + await this.model('patient').where({ id }).update({ + sample_info_status: 0, + sample_edit_reason: '', + sample_edit_reject_reason: '', + sample_edit_apply_time: null, + sample_edit_audit_time: null, + update_by: this.adminUser?.id || 0 + }); + + await this._addSampleLog(id, 'sample_approve_edit', '送检修改申请通过', '后台通过送检信息修改申请,患者可重新编辑送检信息', reason); + await this._addSampleEditMessage(id, true); + await this._sendSampleEditSubscribeMessage(id, patient.name, '审核通过', '请重新提交送检信息'); + await this.log('edit', '患者管理', `通过送检信息修改申请(ID:${id})`); + return this.success(); + } + + async rejectSampleEditAction() { + if (!this._canEditPatient()) return this.fail('暂无权限'); + const { id, reason } = this.post(); + if (!id) return this.fail('参数错误'); + if (!reason || !reason.trim()) return this.fail('请填写驳回原因'); + + const patient = await this.model('patient').where({ id, is_deleted: 0 }).find(); + if (think.isEmpty(patient)) return this.fail('患者不存在'); + if (Number(patient.sample_info_status) !== 3) return this.fail('当前没有待审核的送检修改申请'); + + const rejectReason = reason.trim(); + const now = think.datetime(new Date()); + await this.model('patient').where({ id }).update({ + sample_info_status: 1, + sample_edit_reject_reason: rejectReason, + sample_edit_audit_time: now, + update_by: this.adminUser?.id || 0 + }); + + await this._addSampleLog(id, 'sample_reject_edit', '送检修改申请驳回', '后台驳回送检信息修改申请', rejectReason); + await this._addSampleEditMessage(id, false, rejectReason); + await this._sendSampleEditSubscribeMessage(id, patient.name, '审核拒绝', '修改申请未通过,请查看详情'); + await this.log('edit', '患者管理', `驳回送检信息修改申请(ID:${id}),原因:${rejectReason}`); + return this.success(); + } + + async editSampleInfoAction() { + if (!this._canEditPatient()) return this.fail('暂无权限'); + const data = this.post(); + const { id, sample_types, wax_return, return_name, return_phone, return_province_code, return_city_code, return_district_code, return_address, report_email, sample_tracking_no, sample_photos } = data; + if (!id) return this.fail('参数错误'); + + const patient = await this.model('patient').where({ id, is_deleted: 0 }).find(); + if (think.isEmpty(patient)) return this.fail('患者不存在'); + + const finalSampleTypes = this._parseArrayValue(sample_types); + const finalSamplePhotos = this._parseArrayValue(sample_photos); + const hasSample = finalSampleTypes.length > 0; + const needReturn = hasSample && Number(wax_return) === 1; + const finalReportEmail = hasSample ? (report_email || '').trim() : ''; + + if (needReturn) { + if (!return_name || !return_name.trim()) return this.fail('请填写回寄收件人'); + if (!return_phone || !return_phone.trim()) return this.fail('请填写回寄收件电话'); + if (!return_province_code || !return_city_code || !return_district_code) return this.fail('请选择回寄收件地址'); + if (!return_address || !return_address.trim()) return this.fail('请填写回寄详细地址'); + } + if (finalReportEmail) { + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(finalReportEmail)) return this.fail('邮箱格式不正确'); + const dup = await this.model('patient') + .where({ report_email: finalReportEmail, is_deleted: 0, id: ['!=', id] }) + .find(); + if (!think.isEmpty(dup)) return this.fail('该邮箱已被其他患者使用'); + } + + await this.model('patient').where({ id }).update({ + sample_types: JSON.stringify(finalSampleTypes), + wax_return: needReturn ? 1 : 0, + return_name: needReturn ? (return_name || '').trim() : '', + return_phone: needReturn ? (return_phone || '').trim() : '', + return_province_code: needReturn ? (return_province_code || '') : '', + return_city_code: needReturn ? (return_city_code || '') : '', + return_district_code: needReturn ? (return_district_code || '') : '', + return_address: needReturn ? (return_address || '').trim() : '', + report_email: hasSample ? finalReportEmail : '', + sample_tracking_no: hasSample ? (sample_tracking_no || '').trim() : '', + sample_photos: hasSample ? JSON.stringify(finalSamplePhotos) : '[]', + sample_info_status: 1, + return_tracking_no: '', + return_time: null, + sample_edit_reason: '', + sample_edit_reject_reason: '', + sample_edit_apply_time: null, + sample_edit_audit_time: null, + update_by: this.adminUser?.id || 0 + }); + + await this._addSampleLog(id, 'sample_admin_edit', '后台编辑送检信息', '后台直接编辑送检信息,状态已生效'); + await this.log('edit', '患者管理', `后台编辑送检信息(ID:${id})`); + return this.success(); + } + // 编辑患者 async editAction() { const data = this.post(); @@ -617,7 +847,6 @@ module.exports = class extends Base { // @private 发送审核结果订阅消息 async _sendAuditSubscribeMessage(patientId, patientName, result) { try { - const APP_REMARK = 'pap_mini_cytx'; // 查找该患者关联的微信用户 const wechatUser = await this.model('wechat_user') .where({ patient_id: patientId, app_remark: APP_REMARK, status: 1 }) @@ -654,6 +883,53 @@ module.exports = class extends Base { } } + async _addSampleEditMessage(patientId, approved, reason = '') { + await this.model('message').add({ + patient_id: patientId, + type: 4, + title: approved ? '送检信息修改申请已通过' : '送检信息修改申请未通过', + content: approved + ? '您的送检信息修改申请已通过,请前往送检信息页面重新提交。' + : '您的送检信息修改申请未通过,请查看原因。', + reason: approved ? '' : reason, + is_read: 0 + }); + } + + // @private 发送送检修改申请审核结果订阅消息 + async _sendSampleEditSubscribeMessage(patientId, patientName, result, remark) { + try { + const wechatUser = await this.model('wechat_user') + .where({ patient_id: patientId, app_remark: APP_REMARK, status: 1 }) + .find(); + if (think.isEmpty(wechatUser) || !wechatUser.open_id) return; + + const wechatService = this.service('wechat'); + const templates = await wechatService.getSubscribeTemplates(APP_REMARK); + const templateId = templates.sample_edit_audit || SAMPLE_EDIT_AUDIT_TEMPLATE_ID; + if (!templateId) return; + + const envMap = { develop: 'developer', trial: 'trial', release: 'formal' }; + const miniprogramState = envMap[wechatUser.mp_env_version] || 'formal'; + const limitThing = value => String(value || '').slice(0, 20); + await wechatService.sendSubscribeMessage({ + remark: APP_REMARK, + openid: wechatUser.open_id, + templateId, + page: 'pages/profile/profile', + miniprogramState, + data: { + thing4: { value: limitThing(remark || '送检信息修改申请已处理') }, + phrase1: { value: result }, + thing16: { value: limitThing(patientName || '患者') }, + time13: { value: dayjs().format('YYYY-MM-DD HH:mm:ss') } + } + }); + } catch (error) { + think.logger.error('[Subscribe] 送检修改申请审核消息发送失败:', error); + } + } + // @private 发送审核结果短信通知 async _sendAuditSms(phone, type, reason) { if (!phone) return; diff --git a/src/controller/mp.js b/src/controller/mp.js index a661dcd..3a4304e 100644 --- a/src/controller/mp.js +++ b/src/controller/mp.js @@ -7,8 +7,29 @@ const cosConfig = require('../config/cos.js'); const APP_REMARK = 'pap_mini_cytx'; const AUDIT_APPROVED_CONTENT = '您提交的肠愈同行患者关爱项目资料已审核通过,请于7天内在小程序-个人中心-送检信息中提交相关信息。'; +const SAMPLE_EDIT_AUDIT_TEMPLATE_ID = 'OcMLIGS8pMnpGzZmPkdMELDgFefqOeQ-1jsit8Ldw2w'; module.exports = class extends Base { + _parseArrayValue(value) { + if (Array.isArray(value)) return value; + if (!value) return []; + try { return JSON.parse(value || '[]') || []; } catch (e) { return []; } + } + + _hasSampleInfo(patient = {}) { + const sampleTypes = this._parseArrayValue(patient.sample_types); + const samplePhotos = this._parseArrayValue(patient.sample_photos); + return !!( + sampleTypes.length || + samplePhotos.length || + patient.report_email || + patient.sample_tracking_no || + patient.return_name || + patient.return_phone || + patient.return_address + ); + } + // POST /api/mp/login async loginAction() { const code = this.post('code'); @@ -672,10 +693,11 @@ module.exports = class extends Base { try { const wechatService = this.service('wechat'); const templates = await wechatService.getSubscribeTemplates(APP_REMARK); + if (!templates.sample_edit_audit) templates.sample_edit_audit = SAMPLE_EDIT_AUDIT_TEMPLATE_ID; return this.json({ code: 0, data: templates }); } catch (error) { think.logger.error('subscribeConfig error:', error); - return this.json({ code: 0, data: {} }); + return this.json({ code: 0, data: { sample_edit_audit: SAMPLE_EDIT_AUDIT_TEMPLATE_ID } }); } } @@ -729,6 +751,10 @@ module.exports = class extends Base { sample_info_status: Number(patient.sample_info_status) || 0, return_tracking_no: patient.return_tracking_no || '', return_time: patient.return_time || '', + sample_edit_reason: patient.sample_edit_reason || '', + sample_edit_reject_reason: patient.sample_edit_reject_reason || '', + sample_edit_apply_time: patient.sample_edit_apply_time || '', + sample_edit_audit_time: patient.sample_edit_audit_time || '', sample_receiver_info: sampleReceiverInfo }}); } catch (error) { @@ -768,16 +794,30 @@ module.exports = class extends Base { } if (sampleInfoStatus === 1) { const contactPhone = sampleReceiverInfo.contact_phone || ''; + const msg = '送检信息已生效,如需修改请点击页面的【申请修改送检信息】按钮申请,通过后可重新提交送检信息。' + + (contactPhone ? `详情咨询${contactPhone}。` : ''); return this.json({ code: 1010, data: { sample_info_status: 1, sample_receiver_info: sampleReceiverInfo }, - msg: contactPhone - ? `送检信息已生效,如需修改请联系平台【${contactPhone}】重置修改权限。` - : '送检信息已生效,如需修改请联系平台重置修改权限。' + msg + }); + } + if (sampleInfoStatus === 3) { + return this.json({ + code: 1010, + data: { + sample_info_status: 3, + sample_edit_reason: patient.sample_edit_reason || '', + sample_edit_apply_time: patient.sample_edit_apply_time || '', + sample_receiver_info: sampleReceiverInfo + }, + msg: '送检信息修改申请正在审核中,请等待平台处理。' }); } const hasSample = sample_types && sample_types.length > 0; + const hadSampleInfo = this._hasSampleInfo(patient); + const now = think.datetime(new Date()); // 邮箱格式与重复校验 if (hasSample && report_email) { @@ -802,7 +842,22 @@ module.exports = class extends Base { report_email: hasSample ? (report_email || '') : '', sample_tracking_no: hasSample ? (sample_tracking_no || '') : '', sample_photos: hasSample ? JSON.stringify(sample_photos || []) : '[]', - sample_info_status: 1 + sample_info_status: 1, + sample_edit_reason: '', + sample_edit_reject_reason: '', + sample_edit_apply_time: null, + sample_edit_audit_time: null, + update_time: now + }); + + await this.model('patient_sample_log').addLog({ + patient_id: user.patient_id, + action: hadSampleInfo ? 'sample_update' : 'sample_submit', + title: hadSampleInfo ? '修改送检信息' : '提交送检信息', + content: hadSampleInfo ? '患者修改并提交送检信息' : '患者提交送检信息', + operator_type: 'patient', + operator_id: user.id || 0, + operator_name: patient.name || '患者' }); return this.json({ code: 0, data: {}, msg: '提交成功' }); @@ -812,6 +867,60 @@ module.exports = class extends Base { } } + /** + * 申请修改送检信息 + * POST /api/mp/applySampleInfoEdit + */ + async applySampleInfoEditAction() { + const mpUser = this.mpUser; + if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); + const { reason, mp_env_version } = this.post(); + if (!reason || !reason.trim()) return this.json({ code: 1, msg: '请填写申请原因' }); + + try { + const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find(); + if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 1, msg: '请先完成实名认证' }); + const patient = await this.model('patient').where({ id: user.patient_id, is_deleted: 0 }).find(); + if (think.isEmpty(patient)) return this.json({ code: 1, msg: '患者信息不存在' }); + if (patient.status !== 1) return this.json({ code: 1, msg: '请先通过审核' }); + + const sampleInfoStatus = Number(patient.sample_info_status) || 0; + if (sampleInfoStatus === 3) return this.json({ code: 1, msg: '修改申请正在审核中,请勿重复提交' }); + if (sampleInfoStatus === 2) return this.json({ code: 1, msg: '样品已经寄回,暂不支持申请修改送检信息' }); + if (sampleInfoStatus !== 1) return this.json({ code: 1, msg: '当前送检信息无需申请修改' }); + + const now = think.datetime(new Date()); + const applyReason = reason.trim(); + if (mp_env_version) { + await this.model('wechat_user').where({ id: user.id }).update({ mp_env_version }); + } + await this.model('patient').where({ id: user.patient_id }).update({ + sample_info_status: 3, + sample_edit_reason: applyReason, + sample_edit_reject_reason: '', + sample_edit_apply_time: now, + sample_edit_audit_time: null, + update_time: now + }); + + await this.model('patient_sample_log').addLog({ + patient_id: user.patient_id, + action: 'sample_apply_edit', + title: '申请修改送检信息', + content: '患者申请修改送检信息', + reason: applyReason, + operator_type: 'patient', + operator_id: user.id || 0, + operator_name: patient.name || '患者' + }); + + return this.json({ code: 0, data: { sample_info_status: 3 }, msg: '申请已提交,请等待平台审核' }); + } catch (error) { + think.logger.error('applySampleInfoEdit error:', error); + return this.json({ code: 1, msg: '提交失败: ' + error.message }); + } + } + /** * 批量重新生成声明与承诺签署图并更新数据库 * GET /api/mp/regenerateSign diff --git a/src/model/patient_sample_log.js b/src/model/patient_sample_log.js new file mode 100644 index 0000000..133e8e9 --- /dev/null +++ b/src/model/patient_sample_log.js @@ -0,0 +1,19 @@ +module.exports = class extends think.Model { + get tableName() { + return 'patient_sample_log'; + } + + async addLog(data = {}) { + return this.add({ + patient_id: data.patient_id || 0, + action: data.action || '', + title: data.title || '', + content: data.content || '', + reason: data.reason || '', + operator_type: data.operator_type || '', + operator_id: data.operator_id || 0, + operator_name: data.operator_name || '', + create_time: data.create_time || think.datetime(new Date()) + }); + } +}; diff --git a/view/admin/patient_detail.html b/view/admin/patient_detail.html index 4598539..4b2b9cf 100644 --- a/view/admin/patient_detail.html +++ b/view/admin/patient_detail.html @@ -20,8 +20,12 @@ .timeline-list li::before { content: ''; position: absolute; left: -6px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: var(--el-color-primary); } .timeline-list li .time { font-size: 12px; color: #909399; margin-bottom: 4px; } .timeline-list li .desc { font-size: 14px; color: #303133; } + .timeline-list li .subdesc { font-size: 13px; color: #606266; margin-top: 4px; } .timeline-list li .reason { font-size: 13px; color: #F56C6C; margin-top: 4px; } .fixed-bottom-bar { position: fixed; bottom: 0; left: 220px; right: 0; height: 64px; background: #fff; box-shadow: 0 -2px 8px rgba(0,0,0,0.08); display: flex; align-items: center; justify-content: center; gap: 16px; z-index: 10; padding: 0 24px; } + .sample-photo-edit { position: relative; width: 80px; height: 80px; } + .sample-photo-edit .el-image { width: 80px; height: 80px; border-radius: 6px; border: 1px solid #eee; } + .sample-photo-edit .del { position: absolute; top: -6px; right: -6px; cursor: pointer; background: rgba(0,0,0,0.6); color: #fff; border-radius: 50%; width: 20px; height: 20px; display:flex; align-items:center; justify-content:center; font-size:12px; z-index:10; line-height:1; } {% endblock %} @@ -172,6 +176,10 @@
物流单号:${ patient.sample_tracking_no }
回寄单号:${ patient.return_tracking_no }
回寄时间:${ patient.return_time }
+
申请原因:${ patient.sample_edit_reason }
+
申请时间:${ patient.sample_edit_apply_time }
+
驳回原因:${ patient.sample_edit_reject_reason }
+
处理时间:${ patient.sample_edit_audit_time }
送检单照片
@@ -184,19 +192,18 @@
- +
-

审核记录

+

流转日志

- +
@@ -205,6 +212,9 @@ 返 回 审核通过 驳 回 + 编辑送检信息 + 通过送检修改申请 + 驳回送检修改申请 重置送检信息编辑 ${ patient.sample_info_status == 2 ? '修改回寄物流单号' : '填写回寄物流单号' } @@ -239,6 +249,75 @@ + + + + + + + +
+ 取消 + 确认驳回 +
+
+ + + + + + + ${ st.name } + + + + + + + + + + + +
+ 取消 + 保存并生效 +
+
+ {% endblock %} {% block js %} @@ -269,6 +348,8 @@ var app = createApp({ return_address: '', report_email: '', sample_tracking_no: '', sample_photos: [], sample_info_status: 0, return_tracking_no: '', return_time: '', + sample_edit_reason: '', sample_edit_reject_reason: '', + sample_edit_apply_time: '', sample_edit_audit_time: '', sign_income: '', sign_privacy: '', sign_promise: '', emergency_contact: '', emergency_phone: '', status: -1, create_time: '' @@ -280,7 +361,27 @@ var app = createApp({ var returnTrackingVisible = ref(false); var returnTrackingSaving = ref(false); var returnTrackingNo = ref(''); - var selectedReasons = ref([]); var commonReasons = [ + var uploadHeaders = {}; + var sampleRejectVisible = ref(false); + var sampleRejectSaving = ref(false); + var sampleRejectReason = ref(''); + var sampleEditVisible = ref(false); + var sampleEditSaving = ref(false); + var sampleTypeList = ref([]); + var regionTree = ref([]); + var sampleEditForm = reactive({ + sample_types: [], + wax_return: 0, + return_name: '', + return_phone: '', + returnRegionCodes: [], + return_address: '', + report_email: '', + sample_tracking_no: '', + sample_photos: [] + }); + var selectedReasons = ref([]); + var commonReasons = [ '身份证照片模糊,请重新上传', '病历资料不完整,请补充', '检查报告缺失,请上传', @@ -305,6 +406,20 @@ var app = createApp({ } } + async function loadSampleTypes() { + try { + var res = await fetch('/common/sampleTypes').then(function(r) { return r.json(); }); + if (res.code === 0) sampleTypeList.value = (res.data && res.data.list) || []; + } catch(e) {} + } + + async function loadRegions() { + try { + var res = await fetch('/common/regions').then(function(r) { return r.json(); }); + if (res.code === 0) regionTree.value = res.data || []; + } catch(e) {} + } + function goBack() { window.location.href = '/admin/patient.html'; } @@ -349,13 +464,31 @@ var app = createApp({ return [patient.id_card_front, patient.id_card_back, patient.photo].filter(Boolean); }); + var sampleEditShowWaxReturn = Vue.computed(function() { + if (!sampleEditForm.sample_types || !sampleEditForm.sample_types.length) return false; + return sampleEditForm.sample_types.some(function(name) { + var st = sampleTypeList.value.find(function(item) { return item.name === name; }); + return st && st.need_return; + }); + }); + + var sampleEditNeedReturnNames = Vue.computed(function() { + if (!sampleEditForm.sample_types || !sampleEditForm.sample_types.length) return ''; + return sampleEditForm.sample_types.filter(function(name) { + var st = sampleTypeList.value.find(function(item) { return item.name === name; }); + return st && st.need_return; + }).join('、'); + }); + function sampleInfoStatusText(status) { + if (Number(status) === 3) return '修改申请待审核'; if (Number(status) === 2) return '已寄回'; if (Number(status) === 1) return '已生效'; return '可修改'; } function sampleInfoStatusType(status) { + if (Number(status) === 3) return 'warning'; if (Number(status) === 2) return 'primary'; if (Number(status) === 1) return 'success'; return 'warning'; @@ -466,16 +599,155 @@ var app = createApp({ } } - onMounted(function() { loadDetail(); }); + async function approveSampleEdit() { + try { + await ElementPlus.ElMessageBox.confirm( + '确认通过该患者的送检信息修改申请吗?通过后患者可在小程序重新编辑并提交送检信息。', + '通过送检修改申请', + { confirmButtonText: '确认通过', cancelButtonText: '取消', type: 'success' } + ); + var res = await fetch('/admin/patient/approveSampleEdit', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: patient.id }) + }).then(function(r) { return r.json(); }); + if (res.code === 0) { + ElementPlus.ElMessage.success('已通过申请'); + loadDetail(); + } else { + ElementPlus.ElMessage.error(res.msg || '操作失败'); + } + } catch(e) {} + } + + function showRejectSampleEditDialog() { + sampleRejectReason.value = ''; + sampleRejectVisible.value = true; + } + + async function rejectSampleEdit() { + if (!sampleRejectReason.value.trim()) { + ElementPlus.ElMessage.warning('请填写驳回原因'); + return; + } + sampleRejectSaving.value = true; + try { + var res = await fetch('/admin/patient/rejectSampleEdit', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: patient.id, reason: sampleRejectReason.value.trim() }) + }).then(function(r) { return r.json(); }); + if (res.code === 0) { + ElementPlus.ElMessage.success('已驳回申请'); + sampleRejectVisible.value = false; + loadDetail(); + } else { + ElementPlus.ElMessage.error(res.msg || '操作失败'); + } + } finally { + sampleRejectSaving.value = false; + } + } + + function onSampleEditTypesChange() { + if (!sampleEditShowWaxReturn.value) { + sampleEditForm.wax_return = 0; + sampleEditForm.return_name = ''; + sampleEditForm.return_phone = ''; + sampleEditForm.returnRegionCodes = []; + sampleEditForm.return_address = ''; + } + if (!sampleEditForm.sample_types || !sampleEditForm.sample_types.length) { + sampleEditForm.report_email = ''; + sampleEditForm.sample_tracking_no = ''; + sampleEditForm.sample_photos = []; + } + } + + function showSampleEditDialog() { + Object.assign(sampleEditForm, { + sample_types: (patient.sample_types || []).slice(), + wax_return: patient.wax_return ? 1 : 0, + return_name: patient.return_name || '', + return_phone: patient.return_phone || '', + returnRegionCodes: [patient.return_province_code, patient.return_city_code, patient.return_district_code].filter(Boolean), + return_address: patient.return_address || '', + report_email: patient.report_email || '', + sample_tracking_no: patient.sample_tracking_no || '', + sample_photos: (patient.sample_photos || []).slice() + }); + sampleEditVisible.value = true; + } + + function onSampleEditPhotoUpload(res) { + if (res.code === 0 && res.data && res.data.url) { + sampleEditForm.sample_photos.push(res.data.url); + } else { + ElementPlus.ElMessage.error(res.msg || '上传失败'); + } + } + + async function saveSampleEdit() { + var regionCodes = sampleEditForm.returnRegionCodes || []; + if (sampleEditShowWaxReturn.value && sampleEditForm.wax_return === 1) { + if (!sampleEditForm.return_name.trim()) return ElementPlus.ElMessage.warning('请填写回寄收件人'); + if (!sampleEditForm.return_phone.trim()) return ElementPlus.ElMessage.warning('请填写回寄电话'); + if (regionCodes.length !== 3) return ElementPlus.ElMessage.warning('请选择回寄地区'); + if (!sampleEditForm.return_address.trim()) return ElementPlus.ElMessage.warning('请填写回寄详细地址'); + } + if (sampleEditForm.report_email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(sampleEditForm.report_email)) { + return ElementPlus.ElMessage.warning('邮箱格式不正确'); + } + sampleEditSaving.value = true; + try { + var res = await fetch('/admin/patient/editSampleInfo', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: patient.id, + sample_types: sampleEditForm.sample_types || [], + wax_return: sampleEditShowWaxReturn.value ? sampleEditForm.wax_return : 0, + return_name: sampleEditForm.return_name.trim(), + return_phone: sampleEditForm.return_phone.trim(), + return_province_code: regionCodes[0] || '', + return_city_code: regionCodes[1] || '', + return_district_code: regionCodes[2] || '', + return_address: sampleEditForm.return_address.trim(), + report_email: sampleEditForm.report_email.trim(), + sample_tracking_no: sampleEditForm.sample_tracking_no.trim(), + sample_photos: sampleEditForm.sample_photos || [] + }) + }).then(function(r) { return r.json(); }); + if (res.code === 0) { + ElementPlus.ElMessage.success('送检信息已保存并生效'); + sampleEditVisible.value = false; + loadDetail(); + } else { + ElementPlus.ElMessage.error(res.msg || '保存失败'); + } + } finally { + sampleEditSaving.value = false; + } + } + + onMounted(function() { + loadDetail(); + loadSampleTypes(); + loadRegions(); + }); return { loading, patient, audits, canAudit, canEdit, returnTrackingVisible, returnTrackingSaving, returnTrackingNo, + sampleRejectVisible, sampleRejectSaving, sampleRejectReason, + sampleEditVisible, sampleEditSaving, sampleEditForm, sampleTypeList, regionTree, uploadHeaders, rejectVisible, rejectSaving, rejectReason, selectedReasons, commonReasons, hospitalRegionText, goBack, downloadSign, isImageUrl, signDocs, signImageList, - authImageList, sampleInfoStatusText, sampleInfoStatusType, + authImageList, sampleInfoStatusText, sampleInfoStatusType, sampleEditShowWaxReturn, sampleEditNeedReturnNames, handleApprove, showRejectDialog, toggleReason, doReject, resetSampleInfo, - showReturnTrackingDialog, saveReturnTrackingNo + showReturnTrackingDialog, saveReturnTrackingNo, + approveSampleEdit, showRejectSampleEditDialog, rejectSampleEdit, + showSampleEditDialog, onSampleEditTypesChange, onSampleEditPhotoUpload, saveSampleEdit }; } }); diff --git a/view/admin/patient_index.html b/view/admin/patient_index.html index fcd99aa..47bd9ab 100644 --- a/view/admin/patient_index.html +++ b/view/admin/patient_index.html @@ -90,11 +90,16 @@ 已驳回 + + + @@ -348,7 +353,7 @@ const app = createApp({ const loading = ref(false); const tableData = ref([]); const pagination = reactive({ page: 1, pageSize: 10, total: 0 }); - const counts = reactive({ all: 0, pending: 0, approved: 0, rejected: 0 }); + const counts = reactive({ all: 0, draft: 0, pending: 0, approved: 0, rejected: 0 }); const uploadHeaders = {}; // 新增弹窗