const Base = require('./base'); const jwt = require('jsonwebtoken'); const COS = require('cos-nodejs-sdk-v5'); const fs = require('fs'); const path = require('path'); const cosConfig = require('../config/cos.js'); const APP_REMARK = 'pap_mini_cytx'; module.exports = class extends Base { // POST /api/mp/login async loginAction() { const code = this.post('code'); if (!code) return this.json({ code: 1, msg: '缺少code参数' }); try { const wechatService = this.service('wechat'); const session = await wechatService.code2Session(code, APP_REMARK); const { openid, unionid } = session; if (!openid) return this.json({ code: 1, msg: '获取openid失败' }); const userModel = this.model('wechat_user'); let user = await userModel.findByOpenId(openid, APP_REMARK); if (think.isEmpty(user)) { const id = await userModel.createUser({ open_id: openid, union_id: unionid || '', app_remark: APP_REMARK, nickname: '微信用户', status: 1 }); user = await userModel.where({ id }).find(); } if (user.status !== 1) return this.json({ code: 1, msg: '账号已被停用' }); const token = jwt.sign( { id: user.id, open_id: user.open_id, type: 'mp' }, Base.JWT_SECRET, { expiresIn: 7 * 24 * 60 * 60 } ); let patient = null; if (user.patient_id) { patient = await this.model('patient') .field('id, patient_no, name, phone, status, auth_status') .where({ id: user.patient_id, is_deleted: 0 }).find(); if (think.isEmpty(patient)) patient = null; } return this.json({ code: 0, data: { token, userInfo: { id: user.id, nickname: user.nickname || '', avatar: user.avatar || '', phone: user.phone || '', patient_id: user.patient_id || null, patient }}}); } catch (error) { think.logger.error('login error:', error); return this.json({ code: 1, msg: error.message || '登录失败' }); } } // POST /api/mp/phoneLogin - H5 手机号验证码登录 async phoneLoginAction() { const { mobile, code } = this.post(); if (!mobile || !/^1[3-9]\d{9}$/.test(mobile)) { return this.json({ code: 1, msg: '请输入正确的手机号' }); } if (!code || !/^\d{6}$/.test(code)) { return this.json({ code: 1, msg: '请输入6位验证码' }); } const verifyResult = await this.verifySmsCode(mobile, 'login', code); if (!verifyResult.success) return this.json({ code: 1, msg: verifyResult.message }); try { const userModel = this.model('wechat_user'); // 查找已有的 H5 用户(open_id 以 h5_ 开头) let user = await userModel.where({ open_id: 'h5_' + mobile, app_remark: APP_REMARK, status: 1 }).find(); if (think.isEmpty(user)) { // 没有 H5 用户记录,创建一条新的 // 同时查找该手机号是否已有 patient(可能在小程序端已认证) let patientId = null; const patient = await this.model('patient').where({ phone: mobile, is_deleted: 0 }).find(); if (!think.isEmpty(patient)) patientId = patient.id; const id = await userModel.createUser({ open_id: 'h5_' + mobile, union_id: '', app_remark: APP_REMARK, nickname: '', phone: mobile, patient_id: patientId, status: 1 }); user = await userModel.where({ id }).find(); } if (user.status !== 1) return this.json({ code: 1, msg: '账号已被停用' }); const token = jwt.sign( { id: user.id, open_id: user.open_id || '', type: 'mp' }, Base.JWT_SECRET, { expiresIn: 7 * 24 * 60 * 60 } ); let patient = null; if (user.patient_id) { patient = await this.model('patient') .field('id, patient_no, name, phone, status, auth_status') .where({ id: user.patient_id, is_deleted: 0 }).find(); if (think.isEmpty(patient)) patient = null; } return this.json({ code: 0, data: { token, userInfo: { id: user.id, nickname: user.nickname || '', avatar: user.avatar || '', phone: user.phone || mobile, patient_id: user.patient_id || null, patient }}}); } catch (error) { think.logger.error('phoneLogin error:', error); return this.json({ code: 1, msg: error.message || '登录失败' }); } } // GET /api/mp/userinfo async userinfoAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); try { const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find(); if (think.isEmpty(user)) return this.json({ code: 1009, msg: '用户不存在' }); let patient = null; if (user.patient_id) { patient = await this.model('patient') .field('id, patient_no, name, phone, status, auth_status') .where({ id: user.patient_id, is_deleted: 0 }).find(); if (think.isEmpty(patient)) patient = null; } return this.json({ code: 0, data: { id: user.id, nickname: user.nickname || '', avatar: user.avatar || '', phone: user.phone || '', patient_id: user.patient_id || null, patient }}); } catch (error) { think.logger.error('userinfo error:', error); return this.json({ code: 1, msg: '获取用户信息失败' }); } } // POST /api/mp/upload async uploadAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); let file = this.file('file'); let filePath = file ? file.path : ''; if (!filePath) { await sleep(1000); file = this.file('file'); filePath = file ? file.path : ''; if (!filePath) return this.json({ code: 1, msg: '请选择要上传的文件' }); } try { const originalName = file.name; const mimeType = file.type; const now = new Date(); const dateFolder = now.getFullYear() + '/' + String(now.getMonth() + 1).padStart(2, '0') + '/' + String(now.getDate()).padStart(2, '0'); const ext = path.extname(originalName); const baseName = path.basename(originalName, ext); const fileName = baseName + '_' + Date.now() + ext; const cosKey = 'uploads/' + dateFolder + '/' + fileName; const cos = new COS({ SecretId: cosConfig.secretId, SecretKey: cosConfig.secretKey }); const uploadResult = await new Promise((resolve, reject) => { cos.putObject({ Bucket: cosConfig.bucket, Region: cosConfig.region, Key: cosKey, Body: fs.createReadStream(filePath), ContentType: mimeType }, (err, data) => { if (err) reject(err); else resolve(data); }); }); setTimeout(() => { try { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } catch (e) {} }, 1000); if (uploadResult.statusCode === 200) { const bucketUrl = 'https://' + uploadResult.Location; const cdnUrl = bucketUrl.replace(cosConfig.bucketUrl, cosConfig.cdnUrl); return this.json({ code: 0, data: { url: cdnUrl } }); } return this.json({ code: 1, msg: '文件上传失败' }); } catch (error) { think.logger.error('upload error:', error); if (file && file.path && fs.existsSync(file.path)) { try { fs.unlinkSync(file.path); } catch (e) {} } return this.json({ code: 1, msg: '文件上传失败: ' + error.message }); } } // POST /api/mp/sendSmsCode async sendSmsCodeAction() { const { mobile, bizType = 'real_name_auth' } = this.post(); // login 场景不需要登录态 if (bizType !== 'login') { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); } if (!mobile || !/^1[3-9]\d{9}$/.test(mobile)) { return this.json({ code: 1, msg: '请输入正确的手机号' }); } const result = await this.sendSmsCode(mobile, bizType); if (!result.success) return this.json({ code: 1, msg: result.message }); const smsConfig = require('../config/sms.js'); const data = smsConfig.enabled ? {} : { code: result.code }; return this.json({ code: 0, data, msg: result.message }); } // GET /api/mp/authInfo async authInfoAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, 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: 0, data: { authStatus: 0 } }); const patient = await this.model('patient').where({ id: user.patient_id, is_deleted: 0 }).find(); if (think.isEmpty(patient)) return this.json({ code: 0, data: { authStatus: 0 } }); return this.json({ code: 0, data: { authStatus: patient.auth_status || 0, idCardType: patient.id_card_type || 1, idCardFront: patient.id_card_front || '', idCardBack: patient.id_card_back || '', photo: patient.photo || '', realName: patient.name || '', idCard: patient.id_card || '', gender: patient.gender || '', birthday: patient.birth_date || '', issuingAuthority: patient.issuing_authority || '', validPeriod: patient.valid_period || '', phone: patient.phone || '', authTime: patient.auth_time || '' }}); } catch (error) { think.logger.error('authInfo error:', error); return this.json({ code: 1, msg: '获取认证信息失败' }); } } // POST /api/mp/authSubmit async authSubmitAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); const { idCardType, idCardFront, idCardBack, photo, realName, idCard, gender, birthday, issuingAuthority, validPeriod, mobile, code, confirmBind } = this.post(); if (!realName) return this.json({ code: 1, msg: '请输入证件姓名' }); if (!idCard) return this.json({ code: 1, msg: '请输入证件号码' }); const cardTypeInt = parseInt(idCardType) || 1; if (cardTypeInt === 2) { if (!photo) return this.json({ code: 1, msg: '请上传免冠照片' }); } else { if (!idCardFront) return this.json({ code: 1, msg: '请上传证件正面照片' }); if (!idCardBack) return this.json({ code: 1, msg: '请上传证件反面照片' }); } if (!mobile || !/^1[3-9]\d{9}$/.test(mobile)) return this.json({ code: 1, msg: '请输入正确的手机号' }); // 确认绑定时跳过验证码校验(第一次提交时已校验过) if (!confirmBind) { if (!code || !/^\d{6}$/.test(code)) return this.json({ code: 1, msg: '请输入6位验证码' }); const verifyResult = await this.verifySmsCode(mobile, 'real_name_auth', code); if (!verifyResult.success) return this.json({ code: 1, msg: verifyResult.message }); } const patientModel = this.model('patient'); const userModel = this.model('wechat_user'); const currentUser = await userModel.where({ id: mpUser.id }).find(); // 排除当前用户已绑定的 patient const excludeId = currentUser.patient_id || 0; // 查找身份证是否已存在 const idCardCondition = { id_card: idCard, is_deleted: 0 }; if (excludeId) idCardCondition.id = ['!=', excludeId]; const existByIdCard = await patientModel.where(idCardCondition).find(); // 查找手机号是否已存在 const phoneCondition = { phone: mobile, is_deleted: 0 }; if (excludeId) phoneCondition.id = ['!=', excludeId]; const existByPhone = await patientModel.where(phoneCondition).find(); if (!think.isEmpty(existByIdCard)) { if (existByIdCard.phone === mobile) { // 手机号+身份证都匹配 → 可绑定已有患者 if (!confirmBind) { const maskedName = existByIdCard.name.length > 1 ? existByIdCard.name[0] + '*'.repeat(existByIdCard.name.length - 1) : existByIdCard.name; const maskedPhone = '****' + existByIdCard.phone.slice(-4); return this.json({ code: 1010, data: { patientName: maskedName, patientPhone: maskedPhone }, msg: '该用户信息已存在' }); } // 用户确认绑定 const boundUser = await userModel.where({ patient_id: existByIdCard.id, id: ['!=', mpUser.id], open_id: ['NOT LIKE', 'h5_%'], status: 1 }).find(); if (!think.isEmpty(boundUser)) { return this.json({ code: 1, msg: '该患者信息已被其他微信账号绑定' }); } const now = think.datetime(new Date()); await userModel.where({ id: mpUser.id }).update({ patient_id: existByIdCard.id, update_time: now }); await patientModel.where({ id: existByIdCard.id }).update({ name: realName, phone: mobile, id_card_type: cardTypeInt, id_card_front: idCardFront || '', id_card_back: idCardBack || '', photo: photo || '', gender: gender || '', birth_date: birthday || null, issuing_authority: issuingAuthority || '', valid_period: validPeriod || '', auth_status: 1, auth_time: now, update_time: now }); return this.json({ code: 0, data: {}, msg: '实名认证成功' }); } // 身份证存在但手机号不同 return this.json({ code: 1, msg: '该证件号已被其他用户认证' }); } // 身份证不存在,但手机号已被其他患者使用 if (!think.isEmpty(existByPhone)) { return this.json({ code: 1, msg: '该手机号已被其他患者使用' }); } if (cardTypeInt === 1 || cardTypeInt === 3) { const faceidConfig = require('../config/faceid.js'); if (faceidConfig.enabled) { const verifyIdResult = await this._verifyIdCard(realName, idCard); if (!verifyIdResult.success) return this.json({ code: 1, msg: verifyIdResult.message }); } } const now = think.datetime(new Date()); try { if (currentUser.patient_id) { await patientModel.where({ id: currentUser.patient_id }).update({ name: realName, phone: mobile, id_card: idCard, id_card_type: cardTypeInt, id_card_front: idCardFront || '', id_card_back: idCardBack || '', photo: photo || '', gender: gender || '', birth_date: birthday || null, issuing_authority: issuingAuthority || '', valid_period: validPeriod || '', auth_status: 1, auth_time: now, update_time: now }); } else { const patientNo = patientModel.generatePatientNo(); const patientId = await patientModel.add({ patient_no: patientNo, name: realName, phone: mobile, id_card: idCard, id_card_type: cardTypeInt, id_card_front: idCardFront || '', id_card_back: idCardBack || '', photo: photo || '', gender: gender || '', birth_date: birthday || null, issuing_authority: issuingAuthority || '', valid_period: validPeriod || '', auth_status: 1, auth_time: now, status: -1, is_deleted: 0, create_time: now, update_time: now }); await userModel.where({ id: mpUser.id }).update({ patient_id: patientId, update_time: now }); } return this.json({ code: 0, data: {}, msg: '实名认证成功' }); } catch (error) { think.logger.error('authSubmit error:', error); return this.json({ code: 1, msg: '认证失败: ' + error.message }); } } // GET /api/mp/myInfo async myInfoAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, 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: 0, data: null }); const patient = await this.model('patient').where({ id: user.patient_id, is_deleted: 0 }).find(); if (think.isEmpty(patient)) return this.json({ code: 0, data: null }); const codes = [patient.province_code, patient.city_code, patient.district_code].filter(Boolean); const regionMap = {}; if (codes.length) { const regions = await this.model('sys_region').where({ code: ['in', codes] }).select(); regions.forEach(r => { regionMap[r.code] = r.name; }); } let documents = []; try { documents = JSON.parse(patient.documents || '[]'); } catch (e) { documents = []; } let samplePhotos = []; try { samplePhotos = JSON.parse(patient.sample_photos || '[]'); } catch (e) { samplePhotos = []; } let sampleTypes = []; try { sampleTypes = JSON.parse(patient.sample_types || '[]'); } catch (e) { sampleTypes = []; } // 如果是驳回状态,查最近一条驳回原因 let rejectReason = ''; if (patient.status === 2) { const audit = await this.model('patient_audit') .where({ patient_id: user.patient_id, action: 'reject' }) .order('id DESC') .find(); if (!think.isEmpty(audit)) rejectReason = audit.reason || ''; } return this.json({ code: 0, data: { name: patient.name || '', id_card: patient.id_card || '', phone: patient.phone || '', gender: patient.gender || '', birth_date: patient.birth_date || '', province_code: patient.province_code || '', city_code: patient.city_code || '', district_code: patient.district_code || '', province_name: regionMap[patient.province_code] || '', city_name: regionMap[patient.city_code] || '', district_name: regionMap[patient.district_code] || '', address: patient.address || '', hospital: patient.hospital || '', emergency_contact: patient.emergency_contact || '', emergency_phone: patient.emergency_phone || '', tag: patient.tag || '', documents, sample_types: sampleTypes, wax_return: patient.wax_return || 0, return_name: patient.return_name || '', return_phone: patient.return_phone || '', return_province_code: patient.return_province_code || '', return_city_code: patient.return_city_code || '', return_district_code: patient.return_district_code || '', return_address: patient.return_address || '', report_email: patient.report_email || '', sample_tracking_no: patient.sample_tracking_no || '', sample_photos: samplePhotos, sign_income: patient.sign_income || '', sign_privacy: patient.sign_privacy || '', sign_privacy_jhr: patient.sign_privacy_jhr || '', sign_promise: patient.sign_promise || '', income_amount: patient.income_amount || '', guardian_name: patient.guardian_name || '', guardian_id_card: patient.guardian_id_card || '', guardian_relation: patient.guardian_relation || '', status: patient.status, auth_status: patient.auth_status || 0, reject_reason: rejectReason }}); } catch (error) { think.logger.error('myInfo error:', error); return this.json({ code: 1, msg: '获取资料失败' }); } } // POST /api/mp/saveMyInfo async saveMyInfoAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); const { gender, province_code, city_code, district_code, address, hospital, emergency_contact, emergency_phone, documents, tag, 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_privacy_jhr, sign_promise, income_amount, guardian_name, guardian_id_card, guardian_relation, mp_env_version } = this.post(); 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: '请先完成实名认证' }); if (!province_code || !city_code || !district_code) return this.json({ code: 1, msg: '请选择省市区' }); if (!address) return this.json({ code: 1, msg: '请填写详细地址' }); if (!emergency_contact || !emergency_phone) return this.json({ code: 1, msg: '请填写紧急联系人信息' }); const now = think.datetime(new Date()); try { // 更新小程序版本标识(用于订阅消息推送到对应版本) if (mp_env_version) { await this.model('wechat_user').where({ id: mpUser.id }).update({ mp_env_version }); } await this.model('patient').where({ id: user.patient_id }).update({ gender: gender || '', province_code: province_code || '', city_code: city_code || '', district_code: district_code || '', address: address || '', hospital: hospital || '', emergency_contact: emergency_contact || '', emergency_phone: emergency_phone || '', tag: tag || '', documents: JSON.stringify(documents || []), sample_types: JSON.stringify(sample_types || []), wax_return: (sample_types && sample_types.length && wax_return) ? 1 : 0, return_name: (sample_types && sample_types.length && wax_return) ? (return_name || '') : '', return_phone: (sample_types && sample_types.length && wax_return) ? (return_phone || '') : '', return_province_code: (sample_types && sample_types.length && wax_return) ? (return_province_code || '') : '', return_city_code: (sample_types && sample_types.length && wax_return) ? (return_city_code || '') : '', return_district_code: (sample_types && sample_types.length && wax_return) ? (return_district_code || '') : '', return_address: (sample_types && sample_types.length && wax_return) ? (return_address || '') : '', report_email: (sample_types && sample_types.length) ? (report_email || '') : '', sample_tracking_no: (sample_types && sample_types.length) ? (sample_tracking_no || '') : '', sample_photos: (sample_types && sample_types.length) ? JSON.stringify(sample_photos || []) : '[]', sign_income: sign_income || '', sign_privacy: sign_privacy || '', sign_privacy_jhr: sign_privacy_jhr || '', sign_promise: sign_promise || '', income_amount: income_amount || null, guardian_name: guardian_name || '', guardian_id_card: guardian_id_card || '', guardian_relation: guardian_relation || '', status: 0, update_time: now }); // 记录提交审核日志 await this.model('patient_audit').add({ patient_id: user.patient_id, action: 'submit', operator_id: 0, operator_name: '用户自助提交' }); return this.json({ code: 0, data: {}, msg: '提交成功' }); } catch (error) { think.logger.error('saveMyInfo error:', error); return this.json({ code: 1, msg: '保存失败: ' + error.message }); } } // POST /api/mp/changePhone async changePhoneAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); const { mobile, code } = this.post(); if (!mobile || !/^1[3-9]\d{9}$/.test(mobile)) return this.json({ code: 1, msg: '请输入正确的手机号' }); if (!code || !/^\d{6}$/.test(code)) return this.json({ code: 1, msg: '请输入6位验证码' }); const verifyResult = await this.verifySmsCode(mobile, 'change_phone', code); if (!verifyResult.success) return this.json({ code: 1, msg: verifyResult.message }); const now = think.datetime(new Date()); const userModel = this.model('wechat_user'); try { await userModel.where({ id: mpUser.id }).update({ phone: mobile, update_time: now }); const user = await userModel.where({ id: mpUser.id }).find(); if (user.patient_id) { await this.model('patient').where({ id: user.patient_id }).update({ phone: mobile, update_time: now }); } return this.json({ code: 0, data: {}, msg: '手机号修改成功' }); } catch (error) { think.logger.error('changePhone error:', error); return this.json({ code: 1, msg: '修改失败: ' + error.message }); } } // POST /api/mp/sign - 签署协议,生成合成图返回URL(不写库) async signAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); const { type, signImage, amount, guardianName, guardianIdCard, guardianRelation } = this.post(); const validTypes = ['income', 'privacy', 'privacy_jhr', 'promise']; if (!validTypes.includes(type)) return this.json({ code: 1, msg: '签署类型错误' }); if (!signImage) return this.json({ code: 1, msg: '请先签名' }); if (type === 'income' && (!amount || Number(amount) <= 0)) { return this.json({ code: 1, msg: '请填写有效的收入金额' }); } if (type === 'privacy_jhr') { if (!guardianName) return this.json({ code: 1, msg: '请输入监护人姓名' }); if (!guardianIdCard) return this.json({ code: 1, msg: '请输入监护人身份证号' }); if (!guardianRelation) 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').field('name, id_card').where({ id: user.patient_id, is_deleted: 0 }).find(); if (think.isEmpty(patient)) return this.json({ code: 1, msg: '患者信息不存在' }); // 获取协议内容 const contentKey = 'sign_' + type; const doc = await this.model('content').getByKey(contentKey); if (think.isEmpty(doc)) return this.json({ code: 1, msg: '协议内容未配置' }); const signTime = think.datetime(new Date()); // 调用截图服务生成合成图 const screenshotService = this.service('screenshot'); const generateParams = { title: doc.title, content: doc.content, signImageUrl: signImage, signerName: patient.name, signerIdCard: patient.id_card, signTime, amount: type === 'income' ? amount : null }; // 监护人类型传递额外字段 if (type === 'privacy_jhr') { generateParams.guardianName = guardianName; generateParams.guardianIdCard = guardianIdCard; generateParams.guardianRelation = guardianRelation; } const url = await screenshotService.generate(generateParams); return this.json({ code: 0, data: { url }, msg: '签署成功' }); } catch (error) { think.logger.error('sign error:', error); return this.json({ code: 1, msg: '签署失败: ' + error.message }); } } // POST /api/mp/updateAvatar - 更新头像 async updateAvatarAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); const { avatar } = this.post(); if (!avatar) return this.json({ code: 1, msg: '请上传头像' }); const now = think.datetime(new Date()); try { await this.model('wechat_user').where({ id: mpUser.id }).update({ avatar, update_time: now }); return this.json({ code: 0, data: { avatar }, msg: '头像更新成功' }); } catch (error) { think.logger.error('updateAvatar error:', error); return this.json({ code: 1, msg: '更新失败: ' + error.message }); } } // GET /api/mp/messages - 消息列表 async messagesAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); const { page = 1, pageSize = 20 } = this.get(); 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: 0, data: { data: [], count: 0, totalPages: 0, currentPage: 1 } }); const list = await this.model('message') .where({ patient_id: user.patient_id }) .order('id DESC') .page(page, pageSize) .countSelect(); return this.json({ code: 0, data: list }); } catch (error) { think.logger.error('messages error:', error); return this.json({ code: 1, msg: '获取消息失败' }); } } // GET /api/mp/messageDetail - 消息详情(同时标记已读) async messageDetailAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, msg: '请先登录' }); const { id } = this.get(); if (!id) 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 msg = await this.model('message').where({ id, patient_id: user.patient_id }).find(); if (think.isEmpty(msg)) return this.json({ code: 1, msg: '消息不存在' }); // 标记已读 if (!msg.is_read) { await this.model('message').where({ id }).update({ is_read: 1 }); } // 查患者姓名 const patient = await this.model('patient').field('name').where({ id: user.patient_id }).find(); msg.patient_name = patient ? patient.name : ''; return this.json({ code: 0, data: msg }); } catch (error) { think.logger.error('messageDetail error:', error); return this.json({ code: 1, msg: '获取消息详情失败' }); } } // GET /api/mp/unreadCount - 未读消息数 async unreadCountAction() { const mpUser = this.mpUser; if (!mpUser) return this.json({ code: 1009, 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: 0, data: { count: 0 } }); const count = await this.model('message').where({ patient_id: user.patient_id, is_read: 0 }).count(); return this.json({ code: 0, data: { count } }); } catch (error) { think.logger.error('unreadCount error:', error); return this.json({ code: 0, data: { count: 0 } }); } } // GET /api/mp/subscribeConfig - 获取订阅消息模板配置 async subscribeConfigAction() { try { const wechatService = this.service('wechat'); const templates = await wechatService.getSubscribeTemplates(APP_REMARK); return this.json({ code: 0, data: templates }); } catch (error) { think.logger.error('subscribeConfig error:', error); return this.json({ code: 0, data: {} }); } } /** * 批量重新生成声明与承诺签署图并更新数据库 * GET /api/mp/regenerateSign */ async regenerateSignAction() { return false; // 先关闭接口,确认后再删除这行 try { const patients = await this.model('patient') .field('id, name, sign_promise') .where({ is_deleted: 0, sign_promise: ['!=', ''],id: ['in', [61,62]] }) .select(); if (!patients.length) return this.json({ code: 1, msg: '没有需要处理的患者' }); const doc = await this.model('content').getByKey('sign_promise'); if (think.isEmpty(doc)) return this.json({ code: 1, msg: '协议内容未配置' }); const screenshotService = this.service('screenshot'); const results = []; let successCount = 0; let failCount = 0; for (const patient of patients) { try { const newUrl = await screenshotService.regenerate({ originalImageUrl: patient.sign_promise, title: doc.title, content: doc.content }); await this.model('patient').where({ id: patient.id }).update({ sign_promise: newUrl }); successCount++; results.push({ id: patient.id, name: patient.name, status: 'ok' }); } catch (e) { failCount++; results.push({ id: patient.id, name: patient.name, status: 'fail', error: e.message }); think.logger.error(`regenerateSign patient ${patient.id} error:`, e); } } return this.json({ code: 0, data: { total: patients.length, success: successCount, fail: failCount, results }, msg: `处理完成:成功${successCount},失败${failCount}` }); } catch (error) { think.logger.error('regenerateSign error:', error); return this.json({ code: 1, msg: '执行失败: ' + error.message }); } } /** * POST /api/mp/regenerateSignByUrl * Temp no-login endpoint. Rebuild sign_income/sign_privacy/sign_promise with a fresh signature image. * Body: { id, url } */ async regenerateSignByUrlAction() { return false; // 先关闭接口,确认后再删除这行 const id = parseInt(this.post('id') || this.get('id'), 10); const signImageUrl = this.post('url') || this.post('signImageUrl') || this.get('url') || this.get('signImageUrl'); if (![61, 62].includes(id)) { return this.json({ code: 1, msg: '只允许处理患者 61、62' }); } if (!signImageUrl || !/^https?:\/\//i.test(signImageUrl)) { return this.json({ code: 1, msg: 'url 参数错误' }); } try { const patient = await this.model('patient') .field('id,name,id_card,sign_income,sign_privacy,sign_promise,income_amount,create_time,update_time') .where({ id, is_deleted: 0 }) .find(); if (think.isEmpty(patient)) { return this.json({ code: 1, msg: '患者不存在' }); } if (!patient.name || !patient.id_card) { return this.json({ code: 1, msg: '患者姓名或身份证缺失' }); } const screenshotService = this.service('screenshot'); const signTime = patient.update_time || patient.create_time || think.datetime(new Date()); const tasks = [ { field: 'sign_income', key: 'sign_income', amount: patient.income_amount }, { field: 'sign_privacy', key: 'sign_privacy' }, { field: 'sign_promise', key: 'sign_promise' } ]; const updates = {}; const results = []; for (const item of tasks) { const doc = await this.model('content').getByKey(item.key); if (think.isEmpty(doc)) { throw new Error(`${item.key} 协议内容未配置`); } const newUrl = await screenshotService.generate({ title: doc.title, content: doc.content, signImageUrl, signerName: patient.name, signerIdCard: patient.id_card, signTime, amount: item.amount || null }); updates[item.field] = newUrl; results.push({ field: item.field, oldUrl: patient[item.field] || '', newUrl }); } await this.model('patient').where({ id }).update(updates); return this.json({ code: 0, data: { id, results }, msg: '重生成成功' }); } catch (error) { think.logger.error('regenerateSignByUrl error:', error); return this.json({ code: 1, msg: '重生成失败: ' + error.message }); } } // @private async _verifyIdCard(name, idCard) { const faceidConfig = require('../config/faceid.js'); try { const tencentcloud = require('tencentcloud-sdk-nodejs'); const FaceidClient = tencentcloud.faceid.v20180301.Client; const client = new FaceidClient({ credential: { secretId: faceidConfig.secretId, secretKey: faceidConfig.secretKey }, region: faceidConfig.region, profile: { httpProfile: { endpoint: 'faceid.tencentcloudapi.com' } } }); const result = await client.IdCardVerification({ IdCard: idCard, Name: name }); if (result.Result === '0') return { success: true, message: '身份核验通过' }; if (result.Result === '1') return { success: false, message: '姓名与身份证号不匹配' }; if (result.Result === '2') return { success: false, message: '身份证号格式错误' }; if (result.Result === '3') return { success: false, message: '姓名格式错误' }; return { success: false, message: result.Description || '身份核验失败' }; } catch (error) { think.logger.error('[FaceId] verify error:', error); return { success: false, message: error.message || '身份核验服务异常' }; } } };