You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

793 line
36 KiB

  1. const Base = require('./base');
  2. const jwt = require('jsonwebtoken');
  3. const COS = require('cos-nodejs-sdk-v5');
  4. const fs = require('fs');
  5. const path = require('path');
  6. const cosConfig = require('../config/cos.js');
  7. const APP_REMARK = 'pap_mini_cytx';
  8. module.exports = class extends Base {
  9. // POST /api/mp/login
  10. async loginAction() {
  11. const code = this.post('code');
  12. if (!code) return this.json({ code: 1, msg: '缺少code参数' });
  13. try {
  14. const wechatService = this.service('wechat');
  15. const session = await wechatService.code2Session(code, APP_REMARK);
  16. const { openid, unionid } = session;
  17. if (!openid) return this.json({ code: 1, msg: '获取openid失败' });
  18. const userModel = this.model('wechat_user');
  19. let user = await userModel.findByOpenId(openid, APP_REMARK);
  20. if (think.isEmpty(user)) {
  21. const id = await userModel.createUser({
  22. open_id: openid, union_id: unionid || '', app_remark: APP_REMARK,
  23. nickname: '微信用户', status: 1
  24. });
  25. user = await userModel.where({ id }).find();
  26. }
  27. if (user.status !== 1) return this.json({ code: 1, msg: '账号已被停用' });
  28. const token = jwt.sign(
  29. { id: user.id, open_id: user.open_id, type: 'mp' },
  30. Base.JWT_SECRET, { expiresIn: 7 * 24 * 60 * 60 }
  31. );
  32. let patient = null;
  33. if (user.patient_id) {
  34. patient = await this.model('patient')
  35. .field('id, patient_no, name, phone, status, auth_status')
  36. .where({ id: user.patient_id, is_deleted: 0 }).find();
  37. if (think.isEmpty(patient)) patient = null;
  38. }
  39. return this.json({ code: 0, data: { token, userInfo: {
  40. id: user.id, nickname: user.nickname || '', avatar: user.avatar || '',
  41. phone: user.phone || '', patient_id: user.patient_id || null, patient
  42. }}});
  43. } catch (error) {
  44. think.logger.error('login error:', error);
  45. return this.json({ code: 1, msg: error.message || '登录失败' });
  46. }
  47. }
  48. // POST /api/mp/phoneLogin - H5 手机号验证码登录
  49. async phoneLoginAction() {
  50. const { mobile, code } = this.post();
  51. if (!mobile || !/^1[3-9]\d{9}$/.test(mobile)) {
  52. return this.json({ code: 1, msg: '请输入正确的手机号' });
  53. }
  54. if (!code || !/^\d{6}$/.test(code)) {
  55. return this.json({ code: 1, msg: '请输入6位验证码' });
  56. }
  57. const verifyResult = await this.verifySmsCode(mobile, 'login', code);
  58. if (!verifyResult.success) return this.json({ code: 1, msg: verifyResult.message });
  59. try {
  60. const userModel = this.model('wechat_user');
  61. // 查找已有的 H5 用户(open_id 以 h5_ 开头)
  62. let user = await userModel.where({
  63. open_id: 'h5_' + mobile, app_remark: APP_REMARK, status: 1
  64. }).find();
  65. if (think.isEmpty(user)) {
  66. // 没有 H5 用户记录,创建一条新的
  67. // 同时查找该手机号是否已有 patient(可能在小程序端已认证)
  68. let patientId = null;
  69. const patient = await this.model('patient').where({ phone: mobile, is_deleted: 0 }).find();
  70. if (!think.isEmpty(patient)) patientId = patient.id;
  71. const id = await userModel.createUser({
  72. open_id: 'h5_' + mobile, union_id: '', app_remark: APP_REMARK,
  73. nickname: '', phone: mobile, patient_id: patientId, status: 1
  74. });
  75. user = await userModel.where({ id }).find();
  76. }
  77. if (user.status !== 1) return this.json({ code: 1, msg: '账号已被停用' });
  78. const token = jwt.sign(
  79. { id: user.id, open_id: user.open_id || '', type: 'mp' },
  80. Base.JWT_SECRET, { expiresIn: 7 * 24 * 60 * 60 }
  81. );
  82. let patient = null;
  83. if (user.patient_id) {
  84. patient = await this.model('patient')
  85. .field('id, patient_no, name, phone, status, auth_status')
  86. .where({ id: user.patient_id, is_deleted: 0 }).find();
  87. if (think.isEmpty(patient)) patient = null;
  88. }
  89. return this.json({ code: 0, data: { token, userInfo: {
  90. id: user.id, nickname: user.nickname || '', avatar: user.avatar || '',
  91. phone: user.phone || mobile, patient_id: user.patient_id || null, patient
  92. }}});
  93. } catch (error) {
  94. think.logger.error('phoneLogin error:', error);
  95. return this.json({ code: 1, msg: error.message || '登录失败' });
  96. }
  97. }
  98. // GET /api/mp/userinfo
  99. async userinfoAction() {
  100. const mpUser = this.mpUser;
  101. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  102. try {
  103. const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find();
  104. if (think.isEmpty(user)) return this.json({ code: 1009, msg: '用户不存在' });
  105. let patient = null;
  106. if (user.patient_id) {
  107. patient = await this.model('patient')
  108. .field('id, patient_no, name, phone, status, auth_status')
  109. .where({ id: user.patient_id, is_deleted: 0 }).find();
  110. if (think.isEmpty(patient)) patient = null;
  111. }
  112. return this.json({ code: 0, data: {
  113. id: user.id, nickname: user.nickname || '', avatar: user.avatar || '',
  114. phone: user.phone || '', patient_id: user.patient_id || null, patient
  115. }});
  116. } catch (error) {
  117. think.logger.error('userinfo error:', error);
  118. return this.json({ code: 1, msg: '获取用户信息失败' });
  119. }
  120. }
  121. // POST /api/mp/upload
  122. async uploadAction() {
  123. const mpUser = this.mpUser;
  124. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  125. const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
  126. let file = this.file('file');
  127. let filePath = file ? file.path : '';
  128. if (!filePath) {
  129. await sleep(1000);
  130. file = this.file('file');
  131. filePath = file ? file.path : '';
  132. if (!filePath) return this.json({ code: 1, msg: '请选择要上传的文件' });
  133. }
  134. try {
  135. const originalName = file.name;
  136. const mimeType = file.type;
  137. const now = new Date();
  138. const dateFolder = now.getFullYear() + '/' + String(now.getMonth() + 1).padStart(2, '0') + '/' + String(now.getDate()).padStart(2, '0');
  139. const ext = path.extname(originalName);
  140. const baseName = path.basename(originalName, ext);
  141. const fileName = baseName + '_' + Date.now() + ext;
  142. const cosKey = 'uploads/' + dateFolder + '/' + fileName;
  143. const cos = new COS({ SecretId: cosConfig.secretId, SecretKey: cosConfig.secretKey });
  144. const uploadResult = await new Promise((resolve, reject) => {
  145. cos.putObject({
  146. Bucket: cosConfig.bucket, Region: cosConfig.region, Key: cosKey,
  147. Body: fs.createReadStream(filePath), ContentType: mimeType
  148. }, (err, data) => { if (err) reject(err); else resolve(data); });
  149. });
  150. setTimeout(() => { try { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } catch (e) {} }, 1000);
  151. if (uploadResult.statusCode === 200) {
  152. const bucketUrl = 'https://' + uploadResult.Location;
  153. const cdnUrl = bucketUrl.replace(cosConfig.bucketUrl, cosConfig.cdnUrl);
  154. return this.json({ code: 0, data: { url: cdnUrl } });
  155. }
  156. return this.json({ code: 1, msg: '文件上传失败' });
  157. } catch (error) {
  158. think.logger.error('upload error:', error);
  159. if (file && file.path && fs.existsSync(file.path)) { try { fs.unlinkSync(file.path); } catch (e) {} }
  160. return this.json({ code: 1, msg: '文件上传失败: ' + error.message });
  161. }
  162. }
  163. // POST /api/mp/sendSmsCode
  164. async sendSmsCodeAction() {
  165. const { mobile, bizType = 'real_name_auth' } = this.post();
  166. // login 场景不需要登录态
  167. if (bizType !== 'login') {
  168. const mpUser = this.mpUser;
  169. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  170. }
  171. if (!mobile || !/^1[3-9]\d{9}$/.test(mobile)) {
  172. return this.json({ code: 1, msg: '请输入正确的手机号' });
  173. }
  174. const result = await this.sendSmsCode(mobile, bizType);
  175. if (!result.success) return this.json({ code: 1, msg: result.message });
  176. const smsConfig = require('../config/sms.js');
  177. const data = smsConfig.enabled ? {} : { code: result.code };
  178. return this.json({ code: 0, data, msg: result.message });
  179. }
  180. // GET /api/mp/authInfo
  181. async authInfoAction() {
  182. const mpUser = this.mpUser;
  183. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  184. try {
  185. const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find();
  186. if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 0, data: { authStatus: 0 } });
  187. const patient = await this.model('patient').where({ id: user.patient_id, is_deleted: 0 }).find();
  188. if (think.isEmpty(patient)) return this.json({ code: 0, data: { authStatus: 0 } });
  189. return this.json({ code: 0, data: {
  190. authStatus: patient.auth_status || 0,
  191. idCardType: patient.id_card_type || 1,
  192. idCardFront: patient.id_card_front || '',
  193. idCardBack: patient.id_card_back || '',
  194. photo: patient.photo || '',
  195. realName: patient.name || '',
  196. idCard: patient.id_card || '',
  197. gender: patient.gender || '',
  198. birthday: patient.birth_date || '',
  199. issuingAuthority: patient.issuing_authority || '',
  200. validPeriod: patient.valid_period || '',
  201. phone: patient.phone || '',
  202. authTime: patient.auth_time || ''
  203. }});
  204. } catch (error) {
  205. think.logger.error('authInfo error:', error);
  206. return this.json({ code: 1, msg: '获取认证信息失败' });
  207. }
  208. }
  209. // POST /api/mp/authSubmit
  210. async authSubmitAction() {
  211. const mpUser = this.mpUser;
  212. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  213. const { idCardType, idCardFront, idCardBack, photo, realName, idCard, gender, birthday, issuingAuthority, validPeriod, mobile, code, confirmBind } = this.post();
  214. if (!realName) return this.json({ code: 1, msg: '请输入证件姓名' });
  215. if (!idCard) return this.json({ code: 1, msg: '请输入证件号码' });
  216. const cardTypeInt = parseInt(idCardType) || 1;
  217. if (cardTypeInt === 2) {
  218. if (!photo) return this.json({ code: 1, msg: '请上传免冠照片' });
  219. } else {
  220. if (!idCardFront) return this.json({ code: 1, msg: '请上传证件正面照片' });
  221. if (!idCardBack) return this.json({ code: 1, msg: '请上传证件反面照片' });
  222. }
  223. if (!mobile || !/^1[3-9]\d{9}$/.test(mobile)) return this.json({ code: 1, msg: '请输入正确的手机号' });
  224. // 确认绑定时跳过验证码校验(第一次提交时已校验过)
  225. if (!confirmBind) {
  226. if (!code || !/^\d{6}$/.test(code)) return this.json({ code: 1, msg: '请输入6位验证码' });
  227. const verifyResult = await this.verifySmsCode(mobile, 'real_name_auth', code);
  228. if (!verifyResult.success) return this.json({ code: 1, msg: verifyResult.message });
  229. }
  230. const patientModel = this.model('patient');
  231. const userModel = this.model('wechat_user');
  232. const currentUser = await userModel.where({ id: mpUser.id }).find();
  233. // 排除当前用户已绑定的 patient
  234. const excludeId = currentUser.patient_id || 0;
  235. // 查找身份证是否已存在
  236. const idCardCondition = { id_card: idCard, is_deleted: 0 };
  237. if (excludeId) idCardCondition.id = ['!=', excludeId];
  238. const existByIdCard = await patientModel.where(idCardCondition).find();
  239. // 查找手机号是否已存在
  240. const phoneCondition = { phone: mobile, is_deleted: 0 };
  241. if (excludeId) phoneCondition.id = ['!=', excludeId];
  242. const existByPhone = await patientModel.where(phoneCondition).find();
  243. if (!think.isEmpty(existByIdCard)) {
  244. if (existByIdCard.phone === mobile) {
  245. // 手机号+身份证都匹配 → 可绑定已有患者
  246. if (!confirmBind) {
  247. const maskedName = existByIdCard.name.length > 1
  248. ? existByIdCard.name[0] + '*'.repeat(existByIdCard.name.length - 1)
  249. : existByIdCard.name;
  250. const maskedPhone = '****' + existByIdCard.phone.slice(-4);
  251. return this.json({
  252. code: 1010,
  253. data: { patientName: maskedName, patientPhone: maskedPhone },
  254. msg: '该用户信息已存在'
  255. });
  256. }
  257. // 用户确认绑定
  258. const boundUser = await userModel.where({
  259. patient_id: existByIdCard.id,
  260. id: ['!=', mpUser.id],
  261. open_id: ['NOT LIKE', 'h5_%'],
  262. status: 1
  263. }).find();
  264. if (!think.isEmpty(boundUser)) {
  265. return this.json({ code: 1, msg: '该患者信息已被其他微信账号绑定' });
  266. }
  267. const now = think.datetime(new Date());
  268. await userModel.where({ id: mpUser.id }).update({ patient_id: existByIdCard.id, update_time: now });
  269. await patientModel.where({ id: existByIdCard.id }).update({
  270. name: realName, phone: mobile, id_card_type: cardTypeInt,
  271. id_card_front: idCardFront || '', id_card_back: idCardBack || '', photo: photo || '',
  272. gender: gender || '', birth_date: birthday || null,
  273. issuing_authority: issuingAuthority || '', valid_period: validPeriod || '',
  274. auth_status: 1, auth_time: now, update_time: now
  275. });
  276. return this.json({ code: 0, data: {}, msg: '实名认证成功' });
  277. }
  278. // 身份证存在但手机号不同
  279. return this.json({ code: 1, msg: '该证件号已被其他用户认证' });
  280. }
  281. // 身份证不存在,但手机号已被其他患者使用
  282. if (!think.isEmpty(existByPhone)) {
  283. return this.json({ code: 1, msg: '该手机号已被其他患者使用' });
  284. }
  285. if (cardTypeInt === 1 || cardTypeInt === 3) {
  286. const faceidConfig = require('../config/faceid.js');
  287. if (faceidConfig.enabled) {
  288. const verifyIdResult = await this._verifyIdCard(realName, idCard);
  289. if (!verifyIdResult.success) return this.json({ code: 1, msg: verifyIdResult.message });
  290. }
  291. }
  292. const now = think.datetime(new Date());
  293. try {
  294. if (currentUser.patient_id) {
  295. await patientModel.where({ id: currentUser.patient_id }).update({
  296. name: realName, phone: mobile, id_card: idCard, id_card_type: cardTypeInt,
  297. id_card_front: idCardFront || '', id_card_back: idCardBack || '', photo: photo || '',
  298. gender: gender || '', birth_date: birthday || null,
  299. issuing_authority: issuingAuthority || '', valid_period: validPeriod || '',
  300. auth_status: 1, auth_time: now, update_time: now
  301. });
  302. } else {
  303. const patientNo = patientModel.generatePatientNo();
  304. const patientId = await patientModel.add({
  305. patient_no: patientNo, name: realName, phone: mobile, id_card: idCard,
  306. id_card_type: cardTypeInt, id_card_front: idCardFront || '', id_card_back: idCardBack || '',
  307. photo: photo || '', gender: gender || '', birth_date: birthday || null,
  308. issuing_authority: issuingAuthority || '', valid_period: validPeriod || '',
  309. auth_status: 1, auth_time: now, status: -1, is_deleted: 0, create_time: now, update_time: now
  310. });
  311. await userModel.where({ id: mpUser.id }).update({ patient_id: patientId, update_time: now });
  312. }
  313. return this.json({ code: 0, data: {}, msg: '实名认证成功' });
  314. } catch (error) {
  315. think.logger.error('authSubmit error:', error);
  316. return this.json({ code: 1, msg: '认证失败: ' + error.message });
  317. }
  318. }
  319. // GET /api/mp/myInfo
  320. async myInfoAction() {
  321. const mpUser = this.mpUser;
  322. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  323. try {
  324. const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find();
  325. if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 0, data: null });
  326. const patient = await this.model('patient').where({ id: user.patient_id, is_deleted: 0 }).find();
  327. if (think.isEmpty(patient)) return this.json({ code: 0, data: null });
  328. const codes = [patient.province_code, patient.city_code, patient.district_code].filter(Boolean);
  329. const regionMap = {};
  330. if (codes.length) {
  331. const regions = await this.model('sys_region').where({ code: ['in', codes] }).select();
  332. regions.forEach(r => { regionMap[r.code] = r.name; });
  333. }
  334. let documents = [];
  335. try { documents = JSON.parse(patient.documents || '[]'); } catch (e) { documents = []; }
  336. let samplePhotos = [];
  337. try { samplePhotos = JSON.parse(patient.sample_photos || '[]'); } catch (e) { samplePhotos = []; }
  338. let sampleTypes = [];
  339. try { sampleTypes = JSON.parse(patient.sample_types || '[]'); } catch (e) { sampleTypes = []; }
  340. // 如果是驳回状态,查最近一条驳回原因
  341. let rejectReason = '';
  342. if (patient.status === 2) {
  343. const audit = await this.model('patient_audit')
  344. .where({ patient_id: user.patient_id, action: 'reject' })
  345. .order('id DESC')
  346. .find();
  347. if (!think.isEmpty(audit)) rejectReason = audit.reason || '';
  348. }
  349. return this.json({ code: 0, data: {
  350. name: patient.name || '', id_card: patient.id_card || '', phone: patient.phone || '',
  351. gender: patient.gender || '', birth_date: patient.birth_date || '',
  352. province_code: patient.province_code || '', city_code: patient.city_code || '',
  353. district_code: patient.district_code || '',
  354. province_name: regionMap[patient.province_code] || '',
  355. city_name: regionMap[patient.city_code] || '',
  356. district_name: regionMap[patient.district_code] || '',
  357. address: patient.address || '',
  358. hospital: patient.hospital || '',
  359. emergency_contact: patient.emergency_contact || '',
  360. emergency_phone: patient.emergency_phone || '',
  361. tag: patient.tag || '', documents,
  362. sample_types: sampleTypes,
  363. wax_return: patient.wax_return || 0,
  364. return_name: patient.return_name || '',
  365. return_phone: patient.return_phone || '',
  366. return_province_code: patient.return_province_code || '',
  367. return_city_code: patient.return_city_code || '',
  368. return_district_code: patient.return_district_code || '',
  369. return_address: patient.return_address || '',
  370. report_email: patient.report_email || '',
  371. sample_tracking_no: patient.sample_tracking_no || '',
  372. sample_photos: samplePhotos,
  373. sign_income: patient.sign_income || '',
  374. sign_privacy: patient.sign_privacy || '',
  375. sign_privacy_jhr: patient.sign_privacy_jhr || '',
  376. sign_promise: patient.sign_promise || '',
  377. income_amount: patient.income_amount || '',
  378. guardian_name: patient.guardian_name || '',
  379. guardian_id_card: patient.guardian_id_card || '',
  380. guardian_relation: patient.guardian_relation || '',
  381. status: patient.status, auth_status: patient.auth_status || 0,
  382. reject_reason: rejectReason
  383. }});
  384. } catch (error) {
  385. think.logger.error('myInfo error:', error);
  386. return this.json({ code: 1, msg: '获取资料失败' });
  387. }
  388. }
  389. // POST /api/mp/saveMyInfo
  390. async saveMyInfoAction() {
  391. const mpUser = this.mpUser;
  392. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  393. 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();
  394. const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find();
  395. if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 1, msg: '请先完成实名认证' });
  396. if (!province_code || !city_code || !district_code) return this.json({ code: 1, msg: '请选择省市区' });
  397. if (!address) return this.json({ code: 1, msg: '请填写详细地址' });
  398. if (!emergency_contact || !emergency_phone) return this.json({ code: 1, msg: '请填写紧急联系人信息' });
  399. const now = think.datetime(new Date());
  400. try {
  401. // 更新小程序版本标识(用于订阅消息推送到对应版本)
  402. if (mp_env_version) {
  403. await this.model('wechat_user').where({ id: mpUser.id }).update({ mp_env_version });
  404. }
  405. await this.model('patient').where({ id: user.patient_id }).update({
  406. gender: gender || '', province_code: province_code || '', city_code: city_code || '',
  407. district_code: district_code || '', address: address || '',
  408. hospital: hospital || '',
  409. emergency_contact: emergency_contact || '', emergency_phone: emergency_phone || '',
  410. tag: tag || '',
  411. documents: JSON.stringify(documents || []),
  412. sample_types: JSON.stringify(sample_types || []),
  413. wax_return: (sample_types && sample_types.length && wax_return) ? 1 : 0,
  414. return_name: (sample_types && sample_types.length && wax_return) ? (return_name || '') : '',
  415. return_phone: (sample_types && sample_types.length && wax_return) ? (return_phone || '') : '',
  416. return_province_code: (sample_types && sample_types.length && wax_return) ? (return_province_code || '') : '',
  417. return_city_code: (sample_types && sample_types.length && wax_return) ? (return_city_code || '') : '',
  418. return_district_code: (sample_types && sample_types.length && wax_return) ? (return_district_code || '') : '',
  419. return_address: (sample_types && sample_types.length && wax_return) ? (return_address || '') : '',
  420. report_email: (sample_types && sample_types.length) ? (report_email || '') : '',
  421. sample_tracking_no: (sample_types && sample_types.length) ? (sample_tracking_no || '') : '',
  422. sample_photos: (sample_types && sample_types.length) ? JSON.stringify(sample_photos || []) : '[]',
  423. sign_income: sign_income || '',
  424. sign_privacy: sign_privacy || '',
  425. sign_privacy_jhr: sign_privacy_jhr || '',
  426. sign_promise: sign_promise || '',
  427. income_amount: income_amount || null,
  428. guardian_name: guardian_name || '',
  429. guardian_id_card: guardian_id_card || '',
  430. guardian_relation: guardian_relation || '',
  431. status: 0,
  432. update_time: now
  433. });
  434. // 记录提交审核日志
  435. await this.model('patient_audit').add({
  436. patient_id: user.patient_id,
  437. action: 'submit',
  438. operator_id: 0,
  439. operator_name: '用户自助提交'
  440. });
  441. return this.json({ code: 0, data: {}, msg: '提交成功' });
  442. } catch (error) {
  443. think.logger.error('saveMyInfo error:', error);
  444. return this.json({ code: 1, msg: '保存失败: ' + error.message });
  445. }
  446. }
  447. // POST /api/mp/changePhone
  448. async changePhoneAction() {
  449. const mpUser = this.mpUser;
  450. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  451. const { mobile, code } = this.post();
  452. if (!mobile || !/^1[3-9]\d{9}$/.test(mobile)) return this.json({ code: 1, msg: '请输入正确的手机号' });
  453. if (!code || !/^\d{6}$/.test(code)) return this.json({ code: 1, msg: '请输入6位验证码' });
  454. const verifyResult = await this.verifySmsCode(mobile, 'change_phone', code);
  455. if (!verifyResult.success) return this.json({ code: 1, msg: verifyResult.message });
  456. const now = think.datetime(new Date());
  457. const userModel = this.model('wechat_user');
  458. try {
  459. await userModel.where({ id: mpUser.id }).update({ phone: mobile, update_time: now });
  460. const user = await userModel.where({ id: mpUser.id }).find();
  461. if (user.patient_id) {
  462. await this.model('patient').where({ id: user.patient_id }).update({ phone: mobile, update_time: now });
  463. }
  464. return this.json({ code: 0, data: {}, msg: '手机号修改成功' });
  465. } catch (error) {
  466. think.logger.error('changePhone error:', error);
  467. return this.json({ code: 1, msg: '修改失败: ' + error.message });
  468. }
  469. }
  470. // POST /api/mp/sign - 签署协议,生成合成图返回URL(不写库)
  471. async signAction() {
  472. const mpUser = this.mpUser;
  473. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  474. const { type, signImage, amount, guardianName, guardianIdCard, guardianRelation } = this.post();
  475. const validTypes = ['income', 'privacy', 'privacy_jhr', 'promise'];
  476. if (!validTypes.includes(type)) return this.json({ code: 1, msg: '签署类型错误' });
  477. if (!signImage) return this.json({ code: 1, msg: '请先签名' });
  478. if (type === 'income' && (!amount || Number(amount) <= 0)) {
  479. return this.json({ code: 1, msg: '请填写有效的收入金额' });
  480. }
  481. if (type === 'privacy_jhr') {
  482. if (!guardianName) return this.json({ code: 1, msg: '请输入监护人姓名' });
  483. if (!guardianIdCard) return this.json({ code: 1, msg: '请输入监护人身份证号' });
  484. if (!guardianRelation) return this.json({ code: 1, msg: '请输入与患者关系' });
  485. }
  486. try {
  487. // 获取患者姓名
  488. const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find();
  489. if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 1, msg: '请先完成实名认证' });
  490. const patient = await this.model('patient').field('name, id_card').where({ id: user.patient_id, is_deleted: 0 }).find();
  491. if (think.isEmpty(patient)) return this.json({ code: 1, msg: '患者信息不存在' });
  492. // 获取协议内容
  493. const contentKey = 'sign_' + type;
  494. const doc = await this.model('content').getByKey(contentKey);
  495. if (think.isEmpty(doc)) return this.json({ code: 1, msg: '协议内容未配置' });
  496. const signTime = think.datetime(new Date());
  497. // 调用截图服务生成合成图
  498. const screenshotService = this.service('screenshot');
  499. const generateParams = {
  500. title: doc.title,
  501. content: doc.content,
  502. signImageUrl: signImage,
  503. signerName: patient.name,
  504. signerIdCard: patient.id_card,
  505. signTime,
  506. amount: type === 'income' ? amount : null
  507. };
  508. // 监护人类型传递额外字段
  509. if (type === 'privacy_jhr') {
  510. generateParams.guardianName = guardianName;
  511. generateParams.guardianIdCard = guardianIdCard;
  512. generateParams.guardianRelation = guardianRelation;
  513. }
  514. const url = await screenshotService.generate(generateParams);
  515. return this.json({ code: 0, data: { url }, msg: '签署成功' });
  516. } catch (error) {
  517. think.logger.error('sign error:', error);
  518. return this.json({ code: 1, msg: '签署失败: ' + error.message });
  519. }
  520. }
  521. // POST /api/mp/updateAvatar - 更新头像
  522. async updateAvatarAction() {
  523. const mpUser = this.mpUser;
  524. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  525. const { avatar } = this.post();
  526. if (!avatar) return this.json({ code: 1, msg: '请上传头像' });
  527. const now = think.datetime(new Date());
  528. try {
  529. await this.model('wechat_user').where({ id: mpUser.id }).update({ avatar, update_time: now });
  530. return this.json({ code: 0, data: { avatar }, msg: '头像更新成功' });
  531. } catch (error) {
  532. think.logger.error('updateAvatar error:', error);
  533. return this.json({ code: 1, msg: '更新失败: ' + error.message });
  534. }
  535. }
  536. // GET /api/mp/messages - 消息列表
  537. async messagesAction() {
  538. const mpUser = this.mpUser;
  539. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  540. const { page = 1, pageSize = 20 } = this.get();
  541. try {
  542. const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find();
  543. if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 0, data: { data: [], count: 0, totalPages: 0, currentPage: 1 } });
  544. const list = await this.model('message')
  545. .where({ patient_id: user.patient_id })
  546. .order('id DESC')
  547. .page(page, pageSize)
  548. .countSelect();
  549. return this.json({ code: 0, data: list });
  550. } catch (error) {
  551. think.logger.error('messages error:', error);
  552. return this.json({ code: 1, msg: '获取消息失败' });
  553. }
  554. }
  555. // GET /api/mp/messageDetail - 消息详情(同时标记已读)
  556. async messageDetailAction() {
  557. const mpUser = this.mpUser;
  558. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  559. const { id } = this.get();
  560. if (!id) return this.json({ code: 1, msg: '参数错误' });
  561. try {
  562. const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find();
  563. if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 1, msg: '无权访问' });
  564. const msg = await this.model('message').where({ id, patient_id: user.patient_id }).find();
  565. if (think.isEmpty(msg)) return this.json({ code: 1, msg: '消息不存在' });
  566. // 标记已读
  567. if (!msg.is_read) {
  568. await this.model('message').where({ id }).update({ is_read: 1 });
  569. }
  570. // 查患者姓名
  571. const patient = await this.model('patient').field('name').where({ id: user.patient_id }).find();
  572. msg.patient_name = patient ? patient.name : '';
  573. return this.json({ code: 0, data: msg });
  574. } catch (error) {
  575. think.logger.error('messageDetail error:', error);
  576. return this.json({ code: 1, msg: '获取消息详情失败' });
  577. }
  578. }
  579. // GET /api/mp/unreadCount - 未读消息数
  580. async unreadCountAction() {
  581. const mpUser = this.mpUser;
  582. if (!mpUser) return this.json({ code: 1009, msg: '请先登录' });
  583. try {
  584. const user = await this.model('wechat_user').where({ id: mpUser.id, status: 1 }).find();
  585. if (think.isEmpty(user) || !user.patient_id) return this.json({ code: 0, data: { count: 0 } });
  586. const count = await this.model('message').where({ patient_id: user.patient_id, is_read: 0 }).count();
  587. return this.json({ code: 0, data: { count } });
  588. } catch (error) {
  589. think.logger.error('unreadCount error:', error);
  590. return this.json({ code: 0, data: { count: 0 } });
  591. }
  592. }
  593. // GET /api/mp/subscribeConfig - 获取订阅消息模板配置
  594. async subscribeConfigAction() {
  595. try {
  596. const wechatService = this.service('wechat');
  597. const templates = await wechatService.getSubscribeTemplates(APP_REMARK);
  598. return this.json({ code: 0, data: templates });
  599. } catch (error) {
  600. think.logger.error('subscribeConfig error:', error);
  601. return this.json({ code: 0, data: {} });
  602. }
  603. }
  604. /**
  605. * 批量重新生成声明与承诺签署图并更新数据库
  606. * GET /api/mp/regenerateSign
  607. */
  608. async regenerateSignAction() {
  609. return false; // 先关闭接口,确认后再删除这行
  610. try {
  611. const patients = await this.model('patient')
  612. .field('id, name, sign_promise')
  613. .where({ is_deleted: 0, sign_promise: ['!=', ''],id: ['in', [61,62]] })
  614. .select();
  615. if (!patients.length) return this.json({ code: 1, msg: '没有需要处理的患者' });
  616. const doc = await this.model('content').getByKey('sign_promise');
  617. if (think.isEmpty(doc)) return this.json({ code: 1, msg: '协议内容未配置' });
  618. const screenshotService = this.service('screenshot');
  619. const results = [];
  620. let successCount = 0;
  621. let failCount = 0;
  622. for (const patient of patients) {
  623. try {
  624. const newUrl = await screenshotService.regenerate({
  625. originalImageUrl: patient.sign_promise,
  626. title: doc.title,
  627. content: doc.content
  628. });
  629. await this.model('patient').where({ id: patient.id }).update({ sign_promise: newUrl });
  630. successCount++;
  631. results.push({ id: patient.id, name: patient.name, status: 'ok' });
  632. } catch (e) {
  633. failCount++;
  634. results.push({ id: patient.id, name: patient.name, status: 'fail', error: e.message });
  635. think.logger.error(`regenerateSign patient ${patient.id} error:`, e);
  636. }
  637. }
  638. return this.json({
  639. code: 0,
  640. data: { total: patients.length, success: successCount, fail: failCount, results },
  641. msg: `处理完成:成功${successCount},失败${failCount}`
  642. });
  643. } catch (error) {
  644. think.logger.error('regenerateSign error:', error);
  645. return this.json({ code: 1, msg: '执行失败: ' + error.message });
  646. }
  647. }
  648. /**
  649. * POST /api/mp/regenerateSignByUrl
  650. * Temp no-login endpoint. Rebuild sign_income/sign_privacy/sign_promise with a fresh signature image.
  651. * Body: { id, url }
  652. */
  653. async regenerateSignByUrlAction() {
  654. return false; // 先关闭接口,确认后再删除这行
  655. const id = parseInt(this.post('id') || this.get('id'), 10);
  656. const signImageUrl = this.post('url') || this.post('signImageUrl') || this.get('url') || this.get('signImageUrl');
  657. if (![61, 62].includes(id)) {
  658. return this.json({ code: 1, msg: '只允许处理患者 61、62' });
  659. }
  660. if (!signImageUrl || !/^https?:\/\//i.test(signImageUrl)) {
  661. return this.json({ code: 1, msg: 'url 参数错误' });
  662. }
  663. try {
  664. const patient = await this.model('patient')
  665. .field('id,name,id_card,sign_income,sign_privacy,sign_promise,income_amount,create_time,update_time')
  666. .where({ id, is_deleted: 0 })
  667. .find();
  668. if (think.isEmpty(patient)) {
  669. return this.json({ code: 1, msg: '患者不存在' });
  670. }
  671. if (!patient.name || !patient.id_card) {
  672. return this.json({ code: 1, msg: '患者姓名或身份证缺失' });
  673. }
  674. const screenshotService = this.service('screenshot');
  675. const signTime = patient.update_time || patient.create_time || think.datetime(new Date());
  676. const tasks = [
  677. { field: 'sign_income', key: 'sign_income', amount: patient.income_amount },
  678. { field: 'sign_privacy', key: 'sign_privacy' },
  679. { field: 'sign_promise', key: 'sign_promise' }
  680. ];
  681. const updates = {};
  682. const results = [];
  683. for (const item of tasks) {
  684. const doc = await this.model('content').getByKey(item.key);
  685. if (think.isEmpty(doc)) {
  686. throw new Error(`${item.key} 协议内容未配置`);
  687. }
  688. const newUrl = await screenshotService.generate({
  689. title: doc.title,
  690. content: doc.content,
  691. signImageUrl,
  692. signerName: patient.name,
  693. signerIdCard: patient.id_card,
  694. signTime,
  695. amount: item.amount || null
  696. });
  697. updates[item.field] = newUrl;
  698. results.push({
  699. field: item.field,
  700. oldUrl: patient[item.field] || '',
  701. newUrl
  702. });
  703. }
  704. await this.model('patient').where({ id }).update(updates);
  705. return this.json({
  706. code: 0,
  707. data: { id, results },
  708. msg: '重生成成功'
  709. });
  710. } catch (error) {
  711. think.logger.error('regenerateSignByUrl error:', error);
  712. return this.json({ code: 1, msg: '重生成失败: ' + error.message });
  713. }
  714. }
  715. // @private
  716. async _verifyIdCard(name, idCard) {
  717. const faceidConfig = require('../config/faceid.js');
  718. try {
  719. const tencentcloud = require('tencentcloud-sdk-nodejs');
  720. const FaceidClient = tencentcloud.faceid.v20180301.Client;
  721. const client = new FaceidClient({
  722. credential: { secretId: faceidConfig.secretId, secretKey: faceidConfig.secretKey },
  723. region: faceidConfig.region,
  724. profile: { httpProfile: { endpoint: 'faceid.tencentcloudapi.com' } }
  725. });
  726. const result = await client.IdCardVerification({ IdCard: idCard, Name: name });
  727. if (result.Result === '0') return { success: true, message: '身份核验通过' };
  728. if (result.Result === '1') return { success: false, message: '姓名与身份证号不匹配' };
  729. if (result.Result === '2') return { success: false, message: '身份证号格式错误' };
  730. if (result.Result === '3') return { success: false, message: '姓名格式错误' };
  731. return { success: false, message: result.Description || '身份核验失败' };
  732. } catch (error) {
  733. think.logger.error('[FaceId] verify error:', error);
  734. return { success: false, message: error.message || '身份核验服务异常' };
  735. }
  736. }
  737. };