25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

601 lines
24 KiB

  1. const Base = require('../base.js');
  2. const dayjs = require('dayjs');
  3. module.exports = class extends Base {
  4. // 患者列表页面
  5. async indexAction() {
  6. this.assign('currentPage', 'patient');
  7. this.assign('pageTitle', '患者管理');
  8. this.assign('breadcrumb', [{ name: '患者管理' }]);
  9. this.assign('adminUser', this.adminUser || {});
  10. this.assign('canAdd', this.isSuperAdmin || (this.userPermissions || []).includes('patient:add'));
  11. this.assign('canEdit', this.isSuperAdmin || (this.userPermissions || []).includes('patient:edit'));
  12. this.assign('canExport', this.isSuperAdmin || (this.userPermissions || []).includes('patient:export'));
  13. this.assign('canAudit', this.isSuperAdmin || (this.userPermissions || []).includes('patient:audit'));
  14. this.assign('canView', this.isSuperAdmin || (this.userPermissions || []).includes('patient:view'));
  15. this.assign('canDelete', this.isSuperAdmin || (this.userPermissions || []).includes('patient:delete'));
  16. return this.display();
  17. }
  18. // 获取患者列表接口
  19. async listAction() {
  20. const { keyword, tag, status, startDate, endDate, province_code, city_code, district_code, page = 1, pageSize = 10 } = this.get();
  21. const model = this.model('patient');
  22. const list = await model.getList({ keyword, tag, status, startDate, endDate, province_code, city_code, district_code, page, pageSize });
  23. // 收集所有省市区 code 批量查询名称
  24. const allCodes = new Set();
  25. list.data.forEach(item => {
  26. if (item.province_code) allCodes.add(item.province_code);
  27. if (item.city_code) allCodes.add(item.city_code);
  28. if (item.district_code) allCodes.add(item.district_code);
  29. });
  30. const regionMap = {};
  31. if (allCodes.size) {
  32. const regions = await this.model('sys_region')
  33. .where({ code: ['in', [...allCodes]] })
  34. .select();
  35. regions.forEach(r => { regionMap[r.code] = r.name; });
  36. }
  37. // 脱敏 + 拼接地区
  38. list.data.forEach(item => {
  39. if (item.id_card && item.id_card.length === 18) {
  40. item.id_card_mask = item.id_card.slice(0, 3) + '***********' + item.id_card.slice(-4);
  41. } else {
  42. item.id_card_mask = item.id_card || '';
  43. }
  44. if (item.phone && item.phone.length === 11) {
  45. item.phone_mask = item.phone.slice(0, 3) + '****' + item.phone.slice(-4);
  46. } else {
  47. item.phone_mask = item.phone || '';
  48. }
  49. const pName = regionMap[item.province_code] || '';
  50. const cName = regionMap[item.city_code] || '';
  51. const dName = regionMap[item.district_code] || '';
  52. item.region_name = [pName, cName, dName].filter(Boolean).join(' ');
  53. });
  54. const counts = await model.getStatusCounts({ keyword, tag, startDate, endDate, province_code, city_code, district_code });
  55. return this.success({ ...list, counts });
  56. }
  57. // 患者详情页面
  58. async detailAction() {
  59. const { id } = this.get();
  60. if (!id) return this.redirect('/admin/patient.html');
  61. this.assign('currentPage', 'patient');
  62. this.assign('pageTitle', '患者详情');
  63. this.assign('breadcrumb', [
  64. { name: '患者管理', url: '/admin/patient.html' },
  65. { name: '患者详情' }
  66. ]);
  67. this.assign('patientId', id);
  68. this.assign('adminUser', this.adminUser || {});
  69. this.assign('canAudit', this.isSuperAdmin || (this.userPermissions || []).includes('patient:audit'));
  70. return this.display();
  71. }
  72. // 获取患者详情接口(不脱敏)
  73. async infoAction() {
  74. const { id } = this.get();
  75. if (!id) return this.fail('参数错误');
  76. const patient = await this.model('patient')
  77. .where({ id, is_deleted: 0 })
  78. .find();
  79. if (think.isEmpty(patient)) {
  80. return this.fail('患者不存在');
  81. }
  82. // 解析 JSON 字段
  83. try { patient.documents = JSON.parse(patient.documents || '[]'); } catch (e) { patient.documents = []; }
  84. try { patient.sample_types = JSON.parse(patient.sample_types || '[]'); } catch (e) { patient.sample_types = []; }
  85. try { patient.sample_photos = JSON.parse(patient.sample_photos || '[]'); } catch (e) { patient.sample_photos = []; }
  86. // 查询省市区名称(包含寄回地址)
  87. const allCodes = [
  88. patient.province_code, patient.city_code, patient.district_code,
  89. patient.return_province_code, patient.return_city_code, patient.return_district_code,
  90. patient.hospital_province_code, patient.hospital_city_code, patient.hospital_district_code
  91. ].filter(Boolean);
  92. if (allCodes.length) {
  93. const regions = await this.model('sys_region')
  94. .where({ code: ['in', allCodes] })
  95. .select();
  96. const regionMap = {};
  97. regions.forEach(r => { regionMap[r.code] = r.name; });
  98. patient.province_name = regionMap[patient.province_code] || '';
  99. patient.city_name = regionMap[patient.city_code] || '';
  100. patient.district_name = regionMap[patient.district_code] || '';
  101. patient.return_province_name = regionMap[patient.return_province_code] || '';
  102. patient.return_city_name = regionMap[patient.return_city_code] || '';
  103. patient.return_district_name = regionMap[patient.return_district_code] || '';
  104. patient.hospital_province_name = regionMap[patient.hospital_province_code] || '';
  105. patient.hospital_city_name = regionMap[patient.hospital_city_code] || '';
  106. patient.hospital_district_name = regionMap[patient.hospital_district_code] || '';
  107. }
  108. // 获取审核记录
  109. const audits = await this.model('patient_audit')
  110. .where({ patient_id: id })
  111. .order('id DESC')
  112. .select();
  113. return this.success({ patient, audits });
  114. }
  115. // 新增患者
  116. async addAction() {
  117. const data = this.post();
  118. const { name, phone, id_card, gender, birth_date, province_code, city_code, district_code, address, hospital, hospital_province_code, hospital_city_code, hospital_district_code, emergency_contact, emergency_phone, tag, documents, 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, sign_income, sign_privacy, sign_promise, sign_privacy_jhr, income_amount, guardian_name, guardian_id_card, guardian_relation } = data;
  119. if (!name || !phone || !id_card || !gender || !birth_date) {
  120. return this.fail('请填写完整信息');
  121. }
  122. if (!province_code || !city_code || !district_code) {
  123. return this.fail('请选择省市区');
  124. }
  125. if (!address) {
  126. return this.fail('请填写详细地址');
  127. }
  128. if (!/^1\d{10}$/.test(phone)) {
  129. return this.fail('手机号格式不正确');
  130. }
  131. if (!/^\d{17}[\dXx]$/.test(id_card)) {
  132. return this.fail('身份证号格式不正确');
  133. }
  134. const model = this.model('patient');
  135. // 唯一性校验
  136. const existByIdCardAndPhone = await model.where({ id_card, phone, is_deleted: 0 }).find();
  137. if (!think.isEmpty(existByIdCardAndPhone)) {
  138. return this.fail('该患者已存在(身份证+手机号匹配)');
  139. }
  140. const existByPhone = await model.where({ phone, is_deleted: 0 }).find();
  141. if (!think.isEmpty(existByPhone)) {
  142. return this.fail('该手机号已被其他患者使用');
  143. }
  144. const existByIdCard = await model.where({ id_card, is_deleted: 0 }).find();
  145. if (!think.isEmpty(existByIdCard)) {
  146. return this.fail('该身份证号已被其他患者使用');
  147. }
  148. const patientNo = model.generatePatientNo();
  149. const id = await model.add({
  150. patient_no: patientNo,
  151. name,
  152. phone,
  153. id_card,
  154. gender,
  155. birth_date,
  156. province_code: province_code || '',
  157. city_code: city_code || '',
  158. district_code: district_code || '',
  159. address: address || '',
  160. hospital: hospital || '',
  161. hospital_province_code: hospital_province_code || '',
  162. hospital_city_code: hospital_city_code || '',
  163. hospital_district_code: hospital_district_code || '',
  164. emergency_contact: emergency_contact || '',
  165. emergency_phone: emergency_phone || '',
  166. tag: tag || '',
  167. documents: JSON.stringify(documents || []),
  168. sample_types: JSON.stringify(sample_types || []),
  169. wax_return: wax_return ? 1 : 0,
  170. return_name: return_name || '',
  171. return_phone: return_phone || '',
  172. return_province_code: return_province_code || '',
  173. return_city_code: return_city_code || '',
  174. return_district_code: return_district_code || '',
  175. return_address: return_address || '',
  176. report_email: report_email || '',
  177. sample_tracking_no: sample_tracking_no || '',
  178. sample_photos: JSON.stringify(sample_photos || []),
  179. sign_income: sign_income || '',
  180. sign_privacy: sign_privacy || '',
  181. sign_promise: sign_promise || '',
  182. sign_privacy_jhr: sign_privacy_jhr || '',
  183. income_amount: income_amount || '',
  184. guardian_name: guardian_name || '',
  185. guardian_id_card: guardian_id_card || '',
  186. guardian_relation: guardian_relation || '',
  187. status: 1,
  188. create_by: this.adminUser?.id || 0
  189. });
  190. // 记录审核日志(后台新增直接审核通过)
  191. await this.model('patient_audit').add({
  192. patient_id: id,
  193. action: 'approve',
  194. operator_id: this.adminUser?.id || 0,
  195. operator_name: this.adminUser?.nickname || this.adminUser?.username || ''
  196. });
  197. await this.log('add', '患者管理', `新增患者「${name}」编号:${patientNo}`);
  198. return this.success({ id, patient_no: patientNo });
  199. }
  200. // 审核通过
  201. async approveAction() {
  202. const { id } = this.post();
  203. if (!id) return this.fail('参数错误');
  204. const patient = await this.model('patient')
  205. .where({ id, is_deleted: 0 })
  206. .find();
  207. if (think.isEmpty(patient)) return this.fail('患者不存在');
  208. if (patient.status === 1) return this.fail('该患者已审核通过');
  209. await this.model('patient').where({ id }).update({
  210. status: 1,
  211. update_by: this.adminUser?.id || 0
  212. });
  213. await this.model('patient_audit').add({
  214. patient_id: id,
  215. action: 'approve',
  216. operator_id: this.adminUser?.id || 0,
  217. operator_name: this.adminUser?.nickname || this.adminUser?.username || ''
  218. });
  219. // 发送消息通知
  220. await this.model('message').add({
  221. patient_id: id,
  222. type: 1,
  223. title: '审核通过',
  224. content: '您提交的个人资料已审核通过。',
  225. reason: ''
  226. });
  227. // 发送订阅消息
  228. await this._sendAuditSubscribeMessage(id, patient.name, '审核通过');
  229. // 发送短信通知
  230. await this._sendAuditSms(patient.phone, 'approved');
  231. await this.log('edit', '患者管理', `审核通过患者(ID:${id})`);
  232. return this.success();
  233. }
  234. // 驳回
  235. async rejectAction() {
  236. const { id, reason } = this.post();
  237. if (!id) return this.fail('参数错误');
  238. if (!reason) return this.fail('请填写驳回原因');
  239. const patient = await this.model('patient')
  240. .where({ id, is_deleted: 0 })
  241. .find();
  242. if (think.isEmpty(patient)) return this.fail('患者不存在');
  243. if (patient.status === 2) return this.fail('该患者已被驳回');
  244. await this.model('patient').where({ id }).update({
  245. status: 2,
  246. update_by: this.adminUser?.id || 0
  247. });
  248. await this.model('patient_audit').add({
  249. patient_id: id,
  250. action: 'reject',
  251. reason,
  252. operator_id: this.adminUser?.id || 0,
  253. operator_name: this.adminUser?.nickname || this.adminUser?.username || ''
  254. });
  255. // 发送消息通知
  256. await this.model('message').add({
  257. patient_id: id,
  258. type: 2,
  259. title: '审核未通过',
  260. content: '您提交的个人资料未通过审核,请根据以下原因修改后重新提交。',
  261. reason: reason
  262. });
  263. // 发送订阅消息
  264. await this._sendAuditSubscribeMessage(id, patient.name, '审核驳回');
  265. // 发送短信通知
  266. await this._sendAuditSms(patient.phone, 'rejected', reason);
  267. await this.log('edit', '患者管理', `驳回患者(ID:${id}),原因:${reason}`);
  268. return this.success();
  269. }
  270. // 删除患者(软删除,支持批量)
  271. async deleteAction() {
  272. const { ids } = this.post();
  273. if (!ids || !ids.length) return this.fail('参数错误');
  274. const model = this.model('patient');
  275. const patients = await model.where({ id: ['in', ids], is_deleted: 0 }).select();
  276. if (!patients.length) return this.fail('患者不存在');
  277. await model.where({ id: ['in', ids] }).update({
  278. is_deleted: 1,
  279. update_by: this.adminUser?.id || 0
  280. });
  281. // 清除 wechat_user 表中的患者关联,避免用户重新认证时走 update 逻辑
  282. await this.model('wechat_user')
  283. .where({ patient_id: ['in', ids] })
  284. .update({ patient_id: 0 });
  285. const names = patients.map(p => p.name).join('、');
  286. await this.log('delete', '患者管理', `删除患者「${names}」共${patients.length}条`);
  287. return this.success();
  288. }
  289. // 编辑患者
  290. async editAction() {
  291. const data = this.post();
  292. const { id, name, phone, id_card, gender, birth_date, province_code, city_code, district_code, address, hospital, hospital_province_code, hospital_city_code, hospital_district_code, emergency_contact, emergency_phone, tag, documents, 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, sign_income, sign_privacy, sign_promise, sign_privacy_jhr, income_amount, guardian_name, guardian_id_card, guardian_relation } = data;
  293. if (!id) return this.fail('参数错误');
  294. if (!name || !phone || !id_card || !gender || !birth_date) return this.fail('请填写完整信息');
  295. if (!province_code || !city_code || !district_code) return this.fail('请选择省市区');
  296. if (!address) return this.fail('请填写详细地址');
  297. if (!/^1\d{10}$/.test(phone)) return this.fail('手机号格式不正确');
  298. if (!/^\d{17}[\dXx]$/.test(id_card)) return this.fail('身份证号格式不正确');
  299. const patient = await this.model('patient').where({ id, is_deleted: 0 }).find();
  300. if (think.isEmpty(patient)) return this.fail('患者不存在');
  301. // 唯一性校验(排除自身)
  302. const existByPhone = await this.model('patient').where({ phone, id: ['!=', id], is_deleted: 0 }).find();
  303. if (!think.isEmpty(existByPhone)) return this.fail('该手机号已被其他患者使用');
  304. const existByIdCard = await this.model('patient').where({ id_card, id: ['!=', id], is_deleted: 0 }).find();
  305. if (!think.isEmpty(existByIdCard)) return this.fail('该身份证号已被其他患者使用');
  306. await this.model('patient').where({ id }).update({
  307. name, phone, id_card, gender, birth_date,
  308. province_code, city_code, district_code,
  309. address: address || '',
  310. hospital: hospital || '',
  311. hospital_province_code: hospital_province_code || '',
  312. hospital_city_code: hospital_city_code || '',
  313. hospital_district_code: hospital_district_code || '',
  314. emergency_contact: emergency_contact || '',
  315. emergency_phone: emergency_phone || '',
  316. tag: tag || '',
  317. documents: JSON.stringify(documents || []),
  318. sample_types: JSON.stringify(sample_types || []),
  319. wax_return: wax_return ? 1 : 0,
  320. return_name: return_name || '',
  321. return_phone: return_phone || '',
  322. return_province_code: return_province_code || '',
  323. return_city_code: return_city_code || '',
  324. return_district_code: return_district_code || '',
  325. return_address: return_address || '',
  326. report_email: report_email || '',
  327. sample_tracking_no: sample_tracking_no || '',
  328. sample_photos: JSON.stringify(sample_photos || []),
  329. sign_income: sign_income || '',
  330. sign_privacy: sign_privacy || '',
  331. sign_promise: sign_promise || '',
  332. sign_privacy_jhr: sign_privacy_jhr || '',
  333. income_amount: income_amount || '',
  334. guardian_name: guardian_name || '',
  335. guardian_id_card: guardian_id_card || '',
  336. guardian_relation: guardian_relation || '',
  337. update_by: this.adminUser?.id || 0
  338. });
  339. await this.log('edit', '患者管理', `编辑患者「${name}」(ID:${id})`);
  340. return this.success();
  341. }
  342. // 导出患者数据(CSV,不脱敏)
  343. async exportAction() {
  344. const { keyword, tag, status, startDate, endDate, province_code, city_code, district_code } = this.get();
  345. const model = this.model('patient');
  346. const list = await model.getAll({ keyword, tag, status, startDate, endDate, province_code, city_code, district_code });
  347. // 批量查省市区名称(包含寄回地址)
  348. const allCodes = new Set();
  349. list.forEach(item => {
  350. if (item.province_code) allCodes.add(item.province_code);
  351. if (item.city_code) allCodes.add(item.city_code);
  352. if (item.district_code) allCodes.add(item.district_code);
  353. if (item.return_province_code) allCodes.add(item.return_province_code);
  354. if (item.return_city_code) allCodes.add(item.return_city_code);
  355. if (item.return_district_code) allCodes.add(item.return_district_code);
  356. if (item.hospital_province_code) allCodes.add(item.hospital_province_code);
  357. if (item.hospital_city_code) allCodes.add(item.hospital_city_code);
  358. if (item.hospital_district_code) allCodes.add(item.hospital_district_code);
  359. });
  360. const regionMap = {};
  361. if (allCodes.size) {
  362. const regions = await this.model('sys_region')
  363. .where({ code: ['in', [...allCodes]] })
  364. .select();
  365. regions.forEach(r => { regionMap[r.code] = r.name; });
  366. }
  367. // 批量查最近一条审核记录(审核日期、驳回原因)
  368. const patientIds = list.map(item => item.id);
  369. const auditMap = {};
  370. if (patientIds.length) {
  371. const audits = await this.model('patient_audit')
  372. .where({ patient_id: ['in', patientIds], action: ['in', ['approve', 'reject']] })
  373. .order('id DESC')
  374. .select();
  375. audits.forEach(a => {
  376. if (!auditMap[a.patient_id]) auditMap[a.patient_id] = a;
  377. });
  378. }
  379. const statusMap = { '-1': '待提交', 0: '待审核', 1: '审核通过', 2: '已驳回' };
  380. const header = [
  381. 'ID', '姓名', '性别', '身份证', '手机号', '省份', '城市', '详细地址',
  382. '医院名称', '医院省份', '医院城市', '医院区县', '紧急联系人', '紧急联系电话', '瘤种',
  383. '送检样本类型', '是否需寄回', '收件人', '收件电话', '收件地址',
  384. '报告接收邮箱', '送检物流单号',
  385. '提交时间', '审核状态', '审核日期', '审核驳回原因'
  386. ];
  387. const ExcelJS = require('exceljs');
  388. const workbook = new ExcelJS.Workbook();
  389. const sheet = workbook.addWorksheet('患者信息');
  390. // 表头
  391. sheet.addRow(header);
  392. // 表头样式:绿色背景、白色加粗字体、冻结首行
  393. const headerRow = sheet.getRow(1);
  394. const colCount = header.length;
  395. for (let i = 1; i <= colCount; i++) {
  396. const cell = headerRow.getCell(i);
  397. cell.font = { bold: true, color: { argb: 'FFFFFFFF' } };
  398. cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4CAF50' } };
  399. cell.alignment = { vertical: 'middle', horizontal: 'center' };
  400. }
  401. sheet.views = [{ state: 'frozen', ySplit: 1 }];
  402. // 边框样式
  403. const thinBorder = {
  404. top: { style: 'thin' },
  405. left: { style: 'thin' },
  406. bottom: { style: 'thin' },
  407. right: { style: 'thin' }
  408. };
  409. // 表头加边框
  410. for (let i = 1; i <= colCount; i++) {
  411. headerRow.getCell(i).border = thinBorder;
  412. }
  413. // 数据行
  414. list.forEach(item => {
  415. const audit = auditMap[item.id];
  416. let sampleTypes = '';
  417. try { sampleTypes = JSON.parse(item.sample_types || '[]').join('、'); } catch (e) { sampleTypes = ''; }
  418. const returnAddr = item.return_address
  419. ? [regionMap[item.return_province_code] || '', regionMap[item.return_city_code] || '', regionMap[item.return_district_code] || '', item.return_address].filter(Boolean).join('')
  420. : '';
  421. const row = sheet.addRow([
  422. item.patient_no,
  423. item.name,
  424. item.gender,
  425. item.id_card,
  426. item.phone,
  427. regionMap[item.province_code] || '',
  428. regionMap[item.city_code] || '',
  429. item.address || '',
  430. item.hospital || '',
  431. regionMap[item.hospital_province_code] || '',
  432. regionMap[item.hospital_city_code] || '',
  433. regionMap[item.hospital_district_code] || '',
  434. item.emergency_contact || '',
  435. item.emergency_phone || '',
  436. item.tag || '',
  437. sampleTypes,
  438. item.wax_return ? '是' : '否',
  439. item.return_name || '',
  440. item.return_phone || '',
  441. returnAddr,
  442. item.report_email || '',
  443. item.sample_tracking_no || '',
  444. item.create_time || '',
  445. statusMap[item.status] || '',
  446. audit ? (audit.create_time || '') : '',
  447. (audit && audit.action === 'reject') ? (audit.reason || '') : ''
  448. ]);
  449. // 数据行加边框
  450. for (let i = 1; i <= colCount; i++) {
  451. row.getCell(i).border = thinBorder;
  452. }
  453. });
  454. // 自动列宽
  455. sheet.columns.forEach(col => {
  456. let maxLen = 10;
  457. col.eachCell({ includeEmpty: true }, cell => {
  458. const len = String(cell.value || '').length;
  459. if (len > maxLen) maxLen = len;
  460. });
  461. col.width = Math.min(maxLen + 4, 40);
  462. });
  463. const buffer = await workbook.xlsx.writeBuffer();
  464. const ts = dayjs().format('YYYYMMDDHHmmss');
  465. const fileName = encodeURIComponent(`患者信息_${ts}.xlsx`);
  466. this.ctx.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
  467. this.ctx.set('Content-Disposition', `attachment; filename*=UTF-8''${fileName}`);
  468. this.ctx.body = Buffer.from(buffer);
  469. await this.log('export', '患者管理', `导出患者数据 ${list.length} 条`);
  470. }
  471. // @private 发送审核结果订阅消息
  472. async _sendAuditSubscribeMessage(patientId, patientName, result) {
  473. try {
  474. const APP_REMARK = 'pap_mini_cytx';
  475. // 查找该患者关联的微信用户
  476. const wechatUser = await this.model('wechat_user')
  477. .where({ patient_id: patientId, app_remark: APP_REMARK, status: 1 })
  478. .find();
  479. if (think.isEmpty(wechatUser) || !wechatUser.open_id) return;
  480. const wechatService = this.service('wechat');
  481. const templates = await wechatService.getSubscribeTemplates(APP_REMARK);
  482. const templateId = templates.audit_result;
  483. if (!templateId) return;
  484. // 映射小程序版本: __wxConfig.envVersion -> miniprogram_state
  485. // develop -> developer, trial -> trial, release -> formal
  486. const envMap = { develop: 'developer', trial: 'trial', release: 'formal' };
  487. const miniprogramState = envMap[wechatUser.mp_env_version] || 'formal';
  488. const dayjs = require('dayjs');
  489. await wechatService.sendSubscribeMessage({
  490. remark: APP_REMARK,
  491. openid: wechatUser.open_id,
  492. templateId,
  493. page: 'pages/profile/profile',
  494. miniprogramState,
  495. data: {
  496. thing2: { value: '肠愈同行患者关爱' },
  497. thing14: { value: patientName || '用户' },
  498. phrase1: { value: result },
  499. time13: { value: dayjs().format('YYYY-MM-DD HH:mm:ss') }
  500. }
  501. });
  502. } catch (error) {
  503. // 订阅消息发送失败不影响审核流程
  504. think.logger.error('[Subscribe] 审核消息发送失败:', error);
  505. }
  506. }
  507. // @private 发送审核结果短信通知
  508. async _sendAuditSms(phone, type, reason) {
  509. if (!phone) return;
  510. try {
  511. const smsConfig = require('../../config/sms.js');
  512. const templates = smsConfig.templates;
  513. think.logger.info(`[SMS] _sendAuditSms 开始 - phone: ${phone}, type: ${type}, reason: ${reason || '(无)'}`);
  514. think.logger.info(`[SMS] 模板配置 - auditApproved: ${templates.auditApproved || '(空)'}, auditRejected: ${templates.auditRejected || '(空)'}`);
  515. if (type === 'approved' && templates.auditApproved) {
  516. await this.sendNotifySms(phone, templates.auditApproved, [], 'audit_approved');
  517. } else if (type === 'rejected' && templates.auditRejected) {
  518. // 驳回原因截取前15字符(短信模板变量有长度限制)
  519. const shortReason = (reason || '资料不符合要求').slice(0, 15);
  520. await this.sendNotifySms(phone, templates.auditRejected, [shortReason], 'audit_rejected');
  521. }
  522. } catch (error) {
  523. think.logger.error('[SMS] 审核短信发送失败:', error);
  524. }
  525. }
  526. };