From abcecf66e7aa5c6109cdd68aaba1c5510d569849 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 11 Apr 2026 18:06:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- COMPLETION_REQUEST_FEATURE.md | 93 ++ DIAGNOSIS_LIST_SORTING.md | 90 ++ IM_CHAT_MESSAGES_PERFORMANCE_FIX.md | 255 +++++ QYWX_CUSTOMER_SYNC_FIX.md | 164 +++ QYWX_CUSTOMER_SYNC_GUIDE.md | 246 ++++ QYWX_IP_WHITELIST_GUIDE.md | 88 ++ QYWX_SYNC_FIX.md | 108 ++ add_completion_request_field.sql | 8 + admin/src/api/qywx.ts | 26 + admin/src/api/tcm.ts | 10 +- admin/src/components/chat-dialog/index.vue | 5 +- admin/src/utils/im-business-message-parse.ts | 53 +- .../src/views/consumer/prescription/index.vue | 13 +- .../consumer/prescription/order_list.vue | 1012 ++++++++++++++--- admin/src/views/doctor/tongji.vue | 411 ++++--- admin/src/views/fans/qywx.vue | 347 ++++++ admin/src/views/order/index.vue | 7 +- .../diagnosis/components/CallRecordPanel.vue | 6 + .../components/ImChatRecordPanel.vue | 4 +- admin/src/views/tcm/diagnosis/edit.vue | 4 +- .../controller/qywx/CustomerController.php | 67 ++ .../controller/tcm/DiagnosisController.php | 3 + .../tcm/PrescriptionOrderController.php | 16 +- .../adminapi/lists/doctor/StatisticsLists.php | 178 ++- .../app/adminapi/lists/qywx/CustomerLists.php | 56 + .../app/adminapi/lists/tcm/DiagnosisLists.php | 10 +- .../adminapi/lists/tcm/PrescriptionLists.php | 32 +- .../app/adminapi/logic/order/OrderLogic.php | 4 +- .../app/adminapi/logic/qywx/CustomerLogic.php | 265 +++++ .../logic/tcm/PrescriptionOrderLogic.php | 209 +++- .../validate/qywx/CustomerValidate.php | 34 + .../tcm/PrescriptionOrderValidate.php | 1 + server/app/command/QywxCheckPermissions.php | 93 ++ server/app/command/QywxSyncCustomer.php | 78 ++ .../app/common/model/QywxExternalContact.php | 19 + server/app/common/model/QywxSyncSettings.php | 13 + .../app/common/service/TencentImService.php | 3 +- .../service/wechat/WechatWorkService.php | 218 ++++ server/config/console.php | 2 + server/config/project.php | 8 +- .../create_qywx_external_contact.sql | 39 + .../{403-BHu_NRKY.js => 403-W3h9vBWH.js} | 2 +- .../{404-BBXckn-U.js => 404-fyEy0uE4.js} | 2 +- ...-vue-CmwKjXis.js => icons-vue-DsycZ37q.js} | 2 +- ....js => AppointmentRecordPanel-CRrNJPLw.js} | 2 +- ...WYN0kADd.js => AssignLogPanel-BvvcR7Kn.js} | 2 +- ...e_type_script_setup_true_lang-BVePoT2B.js} | 2 +- ...s => AssistantWatchCallDialog-XgMsg-3o.js} | 2 +- .../admin/assets/BloodRecordList-D1yJdGJX.js | 1 - .../admin/assets/BloodRecordList-Ge4RKsxL.js | 1 + .../admin/assets/CallRecordPanel-D1yS2Ki-.css | 1 + .../admin/assets/CallRecordPanel-D7pIqxrZ.js | 1 - .../admin/assets/CallRecordPanel-DJE0QP6H.css | 1 - .../admin/assets/CallRecordPanel-DuxHYL8j.js | 1 + ...DHZq06xe.js => CaseRecordList-ByVU3KnB.js} | 2 +- .../assets/ImChatRecordPanel-BOq3xG8P.js | 1 + .../assets/ImChatRecordPanel-BXaTtVuW.css | 1 - .../assets/ImChatRecordPanel-DjYHf6nj.css | 1 + .../assets/ImChatRecordPanel-EilmbQmo.js | 1 - ...DSlfmFi0.js => account-adjust-DCiuEpbA.js} | 2 +- ...e_type_script_setup_true_lang-YggogXkm.js} | 2 +- .../{add-DtNvxGNR.js => add-ClJguCdG.js} | 2 +- ...dd-nav-DQqbCWHS.js => add-nav-DZekUqzo.js} | 2 +- ...e_type_script_setup_true_lang-DbPUenSv.js} | 2 +- .../{admin-DXvawCY2.js => admin-CN6Ez_uX.js} | 2 +- ...nt-oB8TToVN.js => appointment-BjmU0niv.js} | 2 +- ...rticle-Bv_mVWDC.js => article-CKzso6NG.js} | 2 +- .../{attr-BsBAoMXy.js => attr-8WivR0K2.js} | 2 +- .../{attr-B13ES8hy.js => attr-BeHm41Vt.js} | 2 +- .../{attr-DwBTqcrJ.js => attr-C9UKhhe2.js} | 2 +- .../{attr-BtnxfkmR.js => attr-C9YfsCUl.js} | 2 +- .../{attr-fL60dHSY.js => attr-CBvYrA2k.js} | 2 +- .../{attr-Clcusz13.js => attr-CDT3OH0D.js} | 2 +- .../{attr-CspKhujp.js => attr-Crmga-B3.js} | 2 +- .../{attr-Utc2v47Q.js => attr-CyMGui12.js} | 2 +- .../{attr-DKU9lp3G.js => attr-iMxub4S5.js} | 2 +- .../admin/assets/attr-setting-BVUuiBvp.js | 1 - .../admin/assets/attr-setting-Da7VRM8O.js | 1 + ...e_type_script_setup_true_lang-C6lYcUFf.js} | 2 +- ...e_type_script_setup_true_lang-6feMPF8O.js} | 2 +- ...e_type_script_setup_true_lang-CHvdnOTe.js} | 2 +- ...e_type_script_setup_true_lang-ChVRECak.js} | 2 +- ...e_type_script_setup_true_lang-CxDRo6h6.js} | 2 +- ...e_type_script_setup_true_lang-D1CFAoKr.js} | 2 +- ...e_type_script_setup_true_lang-DaJjxgj9.js} | 2 +- ...e_type_script_setup_true_lang-DwghrNvi.js} | 2 +- .../{auth-DWy8rJZf.js => auth-DOJs-CkZ.js} | 2 +- ...e_type_script_setup_true_lang-BkYBxDg5.js} | 2 +- ...Y2g2wG7.js => balance_details-pvUHrx2P.js} | 2 +- ...T8x_GH.js => bind-work-wechat-BnTB_0jT.js} | 2 +- .../{cache-B15yiUhW.js => cache-BKANaUkB.js} | 2 +- ...Hwiu73k.js => change-password-DzXQZ0Fz.js} | 2 +- .../{code-Bh1_JFed.js => code-Cp_4CaAu.js} | 2 +- ...w-B43yG_TY.js => code-preview-BYcKeRdR.js} | 2 +- ...e_type_script_setup_true_lang-DhUvDxvC.js} | 2 +- ...{config-CplXNvuf.js => config-BiXQPwq8.js} | 2 +- ...sumer-BwnTVJYH.js => consumer-D59xnCrW.js} | 2 +- ...ontent-W2lzHLKG.js => content--_otKNcP.js} | 2 +- ...ontent-DQBRTRo-.js => content-BBNLXtq1.js} | 2 +- ...ontent-ClfMjdEi.js => content-BKHxVwNW.js} | 2 +- ...ontent-yIlo53vS.js => content-Bi5OgnKP.js} | 2 +- ...ontent-CHnmxWDT.js => content-BkT0XQYO.js} | 2 +- ...ontent-DkJGWyJ7.js => content-Bz9BRoD3.js} | 2 +- ...ontent-BllhP0mX.js => content-CVQDRtl6.js} | 2 +- ...ontent-EVcCkwrA.js => content-CzET6ZOY.js} | 2 +- ...ontent-Cx3G8QSp.js => content-DDwZ9510.js} | 2 +- ...ontent-DtUi7GyP.js => content-DPHBLW5G.js} | 2 +- ...ontent-zwo6EyJA.js => content-DVruHPIi.js} | 2 +- ...ontent-DoSkp8Rl.js => content-DkpUAg9J.js} | 2 +- ...ontent-CouZzFRM.js => content-cWeDbF1c.js} | 2 +- ...e_type_script_setup_true_lang-BfxsednI.js} | 2 +- ...e_type_script_setup_true_lang-C7hhBdwq.js} | 2 +- ...e_type_script_setup_true_lang-CucsgKGv.js} | 2 +- ...e_type_script_setup_true_lang-DHTycTmO.js} | 2 +- ...e_type_script_setup_true_lang-GEFFCTZx.js} | 2 +- ...ble-A0HpJ7Mm.js => data-table-CVToWtGV.js} | 2 +- ...e_type_script_setup_true_lang-CCKhdE3_.js} | 2 +- ...ion-DM5uWpUh.js => decoration-Bm7sdIO5.js} | 2 +- ...BfhW5Pfn.js => decoration-img-C_6VQE85.js} | 2 +- ...-CtZiwc94.js => default_reply-DETQlHCh.js} | 2 +- ...ent-D1C6B5Rb.js => department-CPN-GQBQ.js} | 2 +- ...{detail-R33HH-pF.js => detail-CjhL1xRr.js} | 2 +- ...{detail-OXnGdhXW.js => detail-DkGJp6bn.js} | 2 +- ...e_type_script_setup_true_lang-KAUod-LS.js} | 2 +- .../{dict-DsF-lFVQ.js => dict-_OsdLnQ0.js} | 2 +- ...{doctor-ERC0u9ds.js => doctor-B1l88RG7.js} | 2 +- .../{edit-Clxpwe1d.js => edit--Q-eJJiP.js} | 2 +- .../{edit-mIbm_DQM.js => edit-9b3NnDCR.js} | 2 +- server/public/admin/assets/edit-B4katxjg.css | 1 + .../{edit-BmJVtpqm.js => edit-B9_OPaOY.js} | 2 +- .../{edit-DOfiJnb0.js => edit-BYcKv2Nr.js} | 2 +- .../{edit-Di3fESiq.js => edit-BcNbHG9E.js} | 2 +- .../{edit-CB24ZT7f.js => edit-CLnbAjNe.js} | 2 +- server/public/admin/assets/edit-CTGLLvCK.css | 1 - .../{edit-WbtKJoSY.js => edit-CU1b3WT8.js} | 2 +- .../{edit-BuZP-n_w.js => edit-CWPwekag.js} | 2 +- .../{edit-VZjHtNEm.js => edit-CaOp3WqK.js} | 2 +- .../{edit-EtjsXbWm.js => edit-Cuo5Xix3.js} | 2 +- .../{edit-1w4qctHu.js => edit-D-7kihnF.js} | 2 +- .../{edit-CbK9A2PJ.js => edit-DOLeB9bU.js} | 2 +- .../{edit-O1DAOQSJ.js => edit-DcbXwFsK.js} | 2 +- .../{edit-Zw9_Kv3n.js => edit-DiwjWVeq.js} | 2 +- .../{edit-XHsmOjza.js => edit-DlyMF4P1.js} | 2 +- .../{edit-Cq_bDKDi.js => edit-FOPggzAA.js} | 2 +- .../{edit-DWHm4v8w.js => edit-ZHONAKYO.js} | 2 +- .../{edit-CeM1_STE.js => edit-da7f1plM.js} | 2 +- .../{edit-BSr-JJy7.js => edit-exJlNE2k.js} | 2 +- ...e_type_script_setup_true_lang-4M1uawlp.js} | 2 +- ...e_type_script_setup_true_lang-AZqplQcV.js} | 2 +- ...e_type_script_setup_true_lang-C6c6C0k_.js} | 2 +- ...e_type_script_setup_true_lang-CT55bh5g.js} | 2 +- ...e_type_script_setup_true_lang-CcPfqlP3.js} | 2 +- ...e_type_script_setup_true_lang-Ckd6_StS.js} | 2 +- ...e_type_script_setup_true_lang-CzIPtSHb.js} | 2 +- ...e_type_script_setup_true_lang-DEb_eTOG.js} | 2 +- ...e_type_script_setup_true_lang-DprRMcLt.js} | 2 +- ...e_type_script_setup_true_lang-DurmHjST.js} | 2 +- ...e_name_articleColumnEdit_lang-BvJY6-ul.js} | 2 +- ...E3_bqN8U.css => element-plus-CDEmIEUa.css} | 2 +- .../admin/assets/element-plus-D_tJqBgC.js | 6 - .../admin/assets/element-plus-Dmpg6ayE.js | 6 + ...nt-Dfo0VQfJ.js => environment-DdFNqDX0.js} | 2 +- .../{error-DTOfRDY0.js => error-t_EkCG7t.js} | 2 +- .../{fans-B1cWXwKm.js => fans-DyPtWpdL.js} | 2 +- .../{file-D-YnPjFu.js => file-BppN_Z6J.js} | 2 +- .../{file-CCsyMC54.js => file-CSSg91XZ.js} | 2 +- ...{filing-SsBi5ffK.js => filing-CQaJcxa_.js} | 2 +- ...inance-D2z713BN.js => finance-D5VZU_9K.js} | 2 +- ...y-XqLdO7zL.js => follow_reply-DVUfSvOm.js} | 2 +- ...e_type_script_setup_true_lang-BPq3_KjI.js} | 2 +- .../assets/{h5-BXRcO42Y.js => h5-C2AYlkv7.js} | 2 +- .../assets/{h5-CElwROQj.js => h5-UKjKCMut.js} | 2 +- .../{icon-CYScamKK.js => icon-BjJcIm_n.js} | 2 +- .../im-business-message-parse-CV1kBVNe.js | 1 - .../im-business-message-parse-ClZDqEhB.js | 1 + server/public/admin/assets/index-6vM5hgd3.css | 1 + .../{index-C7-SaQaE.js => index-BAzumEn_.js} | 2 +- server/public/admin/assets/index-BGEYAs46.css | 1 + .../{index-B5YfXeiX.js => index-BKLtguGT.js} | 2 +- .../{index-R422JBri.js => index-BNNrWZS2.js} | 2 +- server/public/admin/assets/index-BNrzv8cF.css | 1 - server/public/admin/assets/index-BO22pDeE.css | 1 + .../{index-DcSJzola.js => index-BRE4E6Vm.js} | 2 +- .../{index-CNV8v40p.js => index-BcjJEs4G.js} | 2 +- server/public/admin/assets/index-Bdvhi5Z1.js | 1 + .../{index-CuzrQAfl.js => index-Bi1KXaIZ.js} | 2 +- .../{index-DvmRDAPB.js => index-Bjxj32-X.js} | 2 +- .../{index-Dk9Aujqe.js => index-BpcBhjuy.js} | 2 +- .../{index-CK8AfBoJ.js => index-BslvW0Xb.js} | 2 +- .../{index-CX4LZY3k.js => index-ByoNSxjv.js} | 2 +- server/public/admin/assets/index-C11IbIoh.css | 1 + server/public/admin/assets/index-C2QZXI4o.js | 2 - server/public/admin/assets/index-CC2dh0em.js | 2 + server/public/admin/assets/index-CDhcw7Zl.css | 1 - server/public/admin/assets/index-CLJhkNLF.js | 2 - server/public/admin/assets/index-CV8PkUyQ.js | 1 - .../{index-CnDGJDlm.js => index-CWW8cL6Z.js} | 2 +- .../{index--UPAVlPP.js => index-CYtIBlBr.js} | 2 +- ...{index-Bw6nwob3.css => index-Ca9-_Wc4.css} | 2 +- .../{index-dPKfVNOt.js => index-Cd8YnHKB.js} | 2 +- server/public/admin/assets/index-CiOQZMXH.js | 1 - .../{index-C8aSG10T.js => index-Cj_fKeap.js} | 2 +- .../{index-DRfpjfDk.js => index-ClInsgAk.js} | 2 +- .../{index-BMxItfrv.js => index-CtFRZE9b.js} | 2 +- server/public/admin/assets/index-CyOxl4QQ.css | 1 - .../{index-LhIzvuH6.js => index-D5I36WUQ.js} | 2 +- server/public/admin/assets/index-D6n3beXq.css | 1 + .../{index-BjP7uRVt.js => index-D8qLD5yB.js} | 2 +- .../{index-zdk8FXaA.js => index-D8wUL01m.js} | 6 +- server/public/admin/assets/index-DC1d-X1P.js | 2 + .../{index-fYTl8bPL.js => index-DGzewPWT.js} | 2 +- server/public/admin/assets/index-DLTwVBhz.js | 1 - .../{index-CeMAytUF.js => index-DOD1_T7u.js} | 2 +- server/public/admin/assets/index-DSta_kR_.css | 1 - server/public/admin/assets/index-DWk_b8Nv.js | 2 + server/public/admin/assets/index-Da4T11FW.css | 1 - server/public/admin/assets/index-De1i6-_G.js | 2 - .../{index-B0-CMmaf.js => index-DrHgxz5E.js} | 2 +- server/public/admin/assets/index-Dsqo5NKa.js | 1 + .../{index-CtI-RQmj.js => index-DvDZD3BW.js} | 2 +- .../{index-B9vchYqI.js => index-Dyw4Bs8T.js} | 2 +- server/public/admin/assets/index-H2pYAx4C.js | 1 + .../{index-BE87uaYN.js => index-JC3O30UN.js} | 2 +- .../{index-Dn-Ofp4z.js => index-R0RdFdal.js} | 2 +- .../{index-DmM2qFKf.js => index-SNmtJ_Bs.js} | 2 +- .../{index-g_OimFHv.js => index-_E7jYQiT.js} | 2 +- .../{index-DBOpb5OQ.js => index-i7zXlMKf.js} | 2 +- .../{index-uns5GlJh.js => index-mRhHxr_h.js} | 2 +- .../{index-DSLJKRNq.js => index-nuHiAmJ7.js} | 2 +- server/public/admin/assets/index-p0SHiF4t.js | 1 - server/public/admin/assets/index-uzqiWbfO.js | 1 + .../{index-0ZxTs5_c.js => index-vaRb-9O-.js} | 2 +- ...e_type_script_setup_true_lang-BfmZk1Ml.js} | 2 +- ...e_type_script_setup_true_lang-BzILDzNF.js} | 2 +- ...e_type_script_setup_true_lang-DyQAa4vY.js} | 2 +- ...e_type_script_setup_true_lang-HG7uja_e.js} | 2 +- ...e_type_script_setup_true_lang-pXicZ9qy.js} | 2 +- ...e_vue_type_style_index_0_lang-D1-Jsym2.js} | 2 +- .../public/admin/assets/index_h5-CiRK5Z-j.js | 1 - .../public/admin/assets/index_h5-DZ8wSxRI.js | 1 + ...on-vJXluvYc.js => information-DmpFe_sL.js} | 2 +- ...ournal-DtHD3Y25.js => journal-Ck7vlvaC.js} | 2 +- ...-0VwNDhOW.js => keyword_reply-DHvG632T.js} | 2 +- .../{link-BAtxiWYr.js => link-DuSWz-nG.js} | 2 +- server/public/admin/assets/list-BcENeHib.js | 2 - server/public/admin/assets/list-BnBvuH7l.js | 2 + server/public/admin/assets/list-CgT7ux9e.css | 1 - server/public/admin/assets/list-DMH7gqqN.css | 1 + .../{list-AyLjHngq.js => list-IJDD4v4W.js} | 2 +- .../{login-C8d0fPHk.js => login-qPar3k-n.js} | 2 +- ...C4WHTu0t.js => login_register-CWNes9cm.js} | 2 +- ...icine-JFvY0I_c.js => medicine-DCa5VHZd.js} | 2 +- ...icine-L99e6FLc.js => medicine-YapJuv_G.js} | 2 +- .../{menu-BTLLBcs4.js => menu-BCs92u6p.js} | 2 +- .../{menu-DwaaS5Nh.js => menu-DNa47EbZ.js} | 2 +- .../{menu-DOvFmi4u.js => menu-mDEmyn7p.js} | 2 +- ...u-set-DQ2tFagX.js => menu-set-C-xj8VxZ.js} | 2 +- ...e_type_script_setup_true_lang-C-PhiS38.js} | 2 +- ...essage-g-iRPj9v.js => message-CutWFsId.js} | 2 +- ...e-BAZO-N9A.js => mobile-style-Wp5PUtZ9.js} | 2 +- ...e_type_script_setup_true_lang-B3uTNLek.js} | 2 +- ...a-attr-DSrpPY-K.js => oa-attr-BG9cPDs4.js} | 2 +- ...m-CNpBGj0l.js => oa-menu-form-CQEO1eiP.js} | 2 +- ...1tb5v.js => oa-menu-form-edit-M-NipbyZ.js} | 2 +- ...e_type_script_setup_true_lang-BcOAscR0.js} | 2 +- ...e_type_script_setup_true_lang-ByPCfX_H.js} | 2 +- ...phone-dLzkAioi.js => oa-phone-Ch4qvxEk.js} | 2 +- ...g-C1VY4L3U.js => open_setting-udbGKJN_.js} | 2 +- .../{order-DQ_203rf.js => order-D8LIXVNB.js} | 2 +- .../admin/assets/order_list-CoRfQWe5.css | 1 + .../admin/assets/order_list-yQNZfjah.js | 1 + ...rflow-CEjTKXoN.js => overflow-JSSQqshi.js} | 2 +- ...{paiban-GtunX9Lr.js => paiban-D7Qzys7x.js} | 2 +- ...l-Cx0Ylzv5.js => patient-call-nKsjAgq4.js} | 2 +- .../{pay-BLgf3CmD.js => pay-v9y4qZZ2.js} | 2 +- .../assets/{pc-BwEgUHoi.js => pc-BWruUdu5.js} | 2 +- ...ils-BnmW2P2t.js => pc_details-CT6yJ3_6.js} | 2 +- .../{perm-BpSYNrQi.js => perm-BfOX4s25.js} | 2 +- ...{picker-Dd64qXUf.js => picker-B5ZiOH7-.js} | 2 +- ...{picker-C8lFmPfc.js => picker-Df7KkdWk.js} | 2 +- ...e_type_script_setup_true_lang-JVrY6m1N.js} | 2 +- ...-BsRT9icJ.js => popover_input-q1ECJaJ_.js} | 2 +- .../{post-D1yUIiVo.js => post-DVV_yBbz.js} | 2 +- ...15k.js => prescription-drawer-CPZFLH2u.js} | 2 +- .../assets/prescription-drawer-_wsVAGl0.css | 1 + .../assets/prescription-drawer-uMED861l.css | 1 - ...review-CbVixrpC.js => preview-CGS5X6P8.js} | 2 +- ...-pc-jc_ygjl9.js => preview-pc-riMgcvDF.js} | 2 +- .../public/admin/assets/progress-CQXIT7YT.css | 1 + .../public/admin/assets/progress-CyS-BQry.js | 1 - .../public/admin/assets/progress-IhkG0rt9.css | 1 - .../public/admin/assets/progress-mXZLUVHP.js | 1 + ...tocol-Dc8d97Dt.js => protocol-BU2sV6Rz.js} | 2 +- server/public/admin/assets/qywx-B2_YcuPk.css | 1 + server/public/admin/assets/qywx-CdbIpoXq.js | 1 + ...XWARmP5.js => recharge_record-D8KzKP8y.js} | 2 +- ...log-BDRRZ4tr.js => refund-log-lqdg3tzR.js} | 2 +- ...e_type_script_setup_true_lang-gLb3pbTv.js} | 2 +- ...-chg3Fcic.js => refund_record-BM0eqNDQ.js} | 2 +- ...-sYkGw7Dk.js => relations-add-Bg4Fxdpi.js} | 2 +- ...e_type_script_setup_true_lang-C6N6sEvb.js} | 2 +- ...text-DTLQ4KLU.js => rich_text-BkZKCgNi.js} | 2 +- .../{role-CxO4EoT5.js => role-BIezdAsx.js} | 2 +- ...{roster-DCc8KY4o.js => roster-_tBANg8j.js} | 2 +- ...etting-BtYclTQc.js => setting-INJkhGl8.js} | 2 +- .../{setup-C9K64vIE.js => setup-D592UMoK.js} | 2 +- ...ics-CQ1h3Jks.js => statistics--YHyEZxF.js} | 2 +- .../{style-CQ0lZeOe.js => style-C5m2K9Dk.js} | 2 +- ...{tabbar-BIAKYnk5.js => tabbar-BgeBV1-s.js} | 2 +- server/public/admin/assets/tcm-BCaX4wIV.js | 1 + server/public/admin/assets/tcm-CgwtzlaG.js | 1 - ...r-2i2vE-oC.js => theme-picker-BnVPZoF_.js} | 2 +- server/public/admin/assets/tongji-B8Bl5suj.js | 1 - .../public/admin/assets/tongji-C0VnzFxy.css | 1 + server/public/admin/assets/tongji-CDlB6kN_.js | 1 + .../public/admin/assets/tongji-qftPQn42.css | 1 - ...{upload-Cl5ZdHRx.js => upload-CwmjoJh6.js} | 2 +- ...K1FBl8D4.js => useDictOptions-7gUIpUOu.js} | 2 +- ...nuOa-7i4hWT0q.js => useMenuOa-By63luzh.js} | 2 +- .../{user-Xpz693Jr.js => user-C24uvj2m.js} | 2 +- .../{weapp-PpT_XwjK.js => weapp-BXdMcuzU.js} | 2 +- .../{weapp-CaROY3Vi.js => weapp-l1PoIc7w.js} | 2 +- ...ebsite-D0DEd3BF.js => website-BQObevlt.js} | 2 +- .../{wx_oa-C9TfIkMb.js => wx_oa-DAhGf1fY.js} | 2 +- server/public/admin/index.html | 10 +- 325 files changed, 4199 insertions(+), 633 deletions(-) create mode 100644 COMPLETION_REQUEST_FEATURE.md create mode 100644 DIAGNOSIS_LIST_SORTING.md create mode 100644 IM_CHAT_MESSAGES_PERFORMANCE_FIX.md create mode 100644 QYWX_CUSTOMER_SYNC_FIX.md create mode 100644 QYWX_CUSTOMER_SYNC_GUIDE.md create mode 100644 QYWX_IP_WHITELIST_GUIDE.md create mode 100644 QYWX_SYNC_FIX.md create mode 100644 add_completion_request_field.sql create mode 100644 admin/src/api/qywx.ts create mode 100644 admin/src/views/fans/qywx.vue create mode 100644 server/app/adminapi/controller/qywx/CustomerController.php create mode 100644 server/app/adminapi/lists/qywx/CustomerLists.php create mode 100644 server/app/adminapi/logic/qywx/CustomerLogic.php create mode 100644 server/app/adminapi/validate/qywx/CustomerValidate.php create mode 100644 server/app/command/QywxCheckPermissions.php create mode 100644 server/app/command/QywxSyncCustomer.php create mode 100644 server/app/common/model/QywxExternalContact.php create mode 100644 server/app/common/model/QywxSyncSettings.php create mode 100644 server/app/common/service/wechat/WechatWorkService.php create mode 100644 server/database/migrations/create_qywx_external_contact.sql rename server/public/admin/assets/{403-BHu_NRKY.js => 403-W3h9vBWH.js} (85%) rename server/public/admin/assets/{404-BBXckn-U.js => 404-fyEy0uE4.js} (78%) rename server/public/admin/assets/@element-plus/{icons-vue-CmwKjXis.js => icons-vue-DsycZ37q.js} (99%) rename server/public/admin/assets/{AppointmentRecordPanel-CAXpSxRa.js => AppointmentRecordPanel-CRrNJPLw.js} (95%) rename server/public/admin/assets/{AssignLogPanel-WYN0kADd.js => AssignLogPanel-BvvcR7Kn.js} (81%) rename server/public/admin/assets/{AssignLogPanel.vue_vue_type_script_setup_true_lang-BocBQBMI.js => AssignLogPanel.vue_vue_type_script_setup_true_lang-BVePoT2B.js} (89%) rename server/public/admin/assets/{AssistantWatchCallDialog-C-CVHpHq.js => AssistantWatchCallDialog-XgMsg-3o.js} (94%) delete mode 100644 server/public/admin/assets/BloodRecordList-D1yJdGJX.js create mode 100644 server/public/admin/assets/BloodRecordList-Ge4RKsxL.js create mode 100644 server/public/admin/assets/CallRecordPanel-D1yS2Ki-.css delete mode 100644 server/public/admin/assets/CallRecordPanel-D7pIqxrZ.js delete mode 100644 server/public/admin/assets/CallRecordPanel-DJE0QP6H.css create mode 100644 server/public/admin/assets/CallRecordPanel-DuxHYL8j.js rename server/public/admin/assets/{CaseRecordList-DHZq06xe.js => CaseRecordList-ByVU3KnB.js} (78%) create mode 100644 server/public/admin/assets/ImChatRecordPanel-BOq3xG8P.js delete mode 100644 server/public/admin/assets/ImChatRecordPanel-BXaTtVuW.css create mode 100644 server/public/admin/assets/ImChatRecordPanel-DjYHf6nj.css delete mode 100644 server/public/admin/assets/ImChatRecordPanel-EilmbQmo.js rename server/public/admin/assets/{account-adjust-DSlfmFi0.js => account-adjust-DCiuEpbA.js} (86%) rename server/public/admin/assets/{account-adjust.vue_vue_type_script_setup_true_lang-DtjEjIXx.js => account-adjust.vue_vue_type_script_setup_true_lang-YggogXkm.js} (91%) rename server/public/admin/assets/{add-DtNvxGNR.js => add-ClJguCdG.js} (86%) rename server/public/admin/assets/{add-nav-DQqbCWHS.js => add-nav-DZekUqzo.js} (66%) rename server/public/admin/assets/{add-nav.vue_vue_type_script_setup_true_lang-Clj0RWzm.js => add-nav.vue_vue_type_script_setup_true_lang-DbPUenSv.js} (90%) rename server/public/admin/assets/{admin-DXvawCY2.js => admin-CN6Ez_uX.js} (84%) rename server/public/admin/assets/{appointment-oB8TToVN.js => appointment-BjmU0niv.js} (91%) rename server/public/admin/assets/{article-Bv_mVWDC.js => article-CKzso6NG.js} (93%) rename server/public/admin/assets/{attr-BsBAoMXy.js => attr-8WivR0K2.js} (63%) rename server/public/admin/assets/{attr-B13ES8hy.js => attr-BeHm41Vt.js} (61%) rename server/public/admin/assets/{attr-DwBTqcrJ.js => attr-C9UKhhe2.js} (64%) rename server/public/admin/assets/{attr-BtnxfkmR.js => attr-C9YfsCUl.js} (73%) rename server/public/admin/assets/{attr-fL60dHSY.js => attr-CBvYrA2k.js} (90%) rename server/public/admin/assets/{attr-Clcusz13.js => attr-CDT3OH0D.js} (65%) rename server/public/admin/assets/{attr-CspKhujp.js => attr-Crmga-B3.js} (64%) rename server/public/admin/assets/{attr-Utc2v47Q.js => attr-CyMGui12.js} (63%) rename server/public/admin/assets/{attr-DKU9lp3G.js => attr-iMxub4S5.js} (63%) delete mode 100644 server/public/admin/assets/attr-setting-BVUuiBvp.js create mode 100644 server/public/admin/assets/attr-setting-Da7VRM8O.js rename server/public/admin/assets/{attr-setting.vue_vue_type_script_setup_true_lang-rBfblQSf.js => attr-setting.vue_vue_type_script_setup_true_lang-C6lYcUFf.js} (89%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-DCUgal-n.js => attr.vue_vue_type_script_setup_true_lang-6feMPF8O.js} (92%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-DGq0Bw05.js => attr.vue_vue_type_script_setup_true_lang-CHvdnOTe.js} (93%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-CIwerCM7.js => attr.vue_vue_type_script_setup_true_lang-ChVRECak.js} (92%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-CI94iDCk.js => attr.vue_vue_type_script_setup_true_lang-CxDRo6h6.js} (93%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-BQH83vDf.js => attr.vue_vue_type_script_setup_true_lang-D1CFAoKr.js} (93%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-DZmBI0zi.js => attr.vue_vue_type_script_setup_true_lang-DaJjxgj9.js} (94%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-BGlorjx9.js => attr.vue_vue_type_script_setup_true_lang-DwghrNvi.js} (93%) rename server/public/admin/assets/{auth-DWy8rJZf.js => auth-DOJs-CkZ.js} (71%) rename server/public/admin/assets/{auth.vue_vue_type_script_setup_true_lang-BnGNjPtr.js => auth.vue_vue_type_script_setup_true_lang-BkYBxDg5.js} (92%) rename server/public/admin/assets/{balance_details-BY2g2wG7.js => balance_details-pvUHrx2P.js} (90%) rename server/public/admin/assets/{bind-work-wechat-QOT8x_GH.js => bind-work-wechat-BnTB_0jT.js} (95%) rename server/public/admin/assets/{cache-B15yiUhW.js => cache-BKANaUkB.js} (91%) rename server/public/admin/assets/{change-password-sHwiu73k.js => change-password-DzXQZ0Fz.js} (91%) rename server/public/admin/assets/{code-Bh1_JFed.js => code-Cp_4CaAu.js} (92%) rename server/public/admin/assets/{code-preview-B43yG_TY.js => code-preview-BYcKeRdR.js} (81%) rename server/public/admin/assets/{code-preview.vue_vue_type_script_setup_true_lang-B_7q71qf.js => code-preview.vue_vue_type_script_setup_true_lang-DhUvDxvC.js} (91%) rename server/public/admin/assets/{config-CplXNvuf.js => config-BiXQPwq8.js} (92%) rename server/public/admin/assets/{consumer-BwnTVJYH.js => consumer-D59xnCrW.js} (81%) rename server/public/admin/assets/{content-W2lzHLKG.js => content--_otKNcP.js} (90%) rename server/public/admin/assets/{content-DQBRTRo-.js => content-BBNLXtq1.js} (90%) rename server/public/admin/assets/{content-ClfMjdEi.js => content-BKHxVwNW.js} (81%) rename server/public/admin/assets/{content-yIlo53vS.js => content-Bi5OgnKP.js} (81%) rename server/public/admin/assets/{content-CHnmxWDT.js => content-BkT0XQYO.js} (88%) rename server/public/admin/assets/{content-DkJGWyJ7.js => content-Bz9BRoD3.js} (89%) rename server/public/admin/assets/{content-BllhP0mX.js => content-CVQDRtl6.js} (88%) rename server/public/admin/assets/{content-EVcCkwrA.js => content-CzET6ZOY.js} (86%) rename server/public/admin/assets/{content-Cx3G8QSp.js => content-DDwZ9510.js} (90%) rename server/public/admin/assets/{content-DtUi7GyP.js => content-DPHBLW5G.js} (81%) rename server/public/admin/assets/{content-zwo6EyJA.js => content-DVruHPIi.js} (81%) rename server/public/admin/assets/{content-DoSkp8Rl.js => content-DkpUAg9J.js} (90%) rename server/public/admin/assets/{content-CouZzFRM.js => content-cWeDbF1c.js} (66%) rename server/public/admin/assets/{content.vue_vue_type_script_setup_true_lang-CFUjbrbQ.js => content.vue_vue_type_script_setup_true_lang-BfxsednI.js} (91%) rename server/public/admin/assets/{content.vue_vue_type_script_setup_true_lang-BfxH3-y0.js => content.vue_vue_type_script_setup_true_lang-C7hhBdwq.js} (86%) rename server/public/admin/assets/{content.vue_vue_type_script_setup_true_lang-B8njkPyY.js => content.vue_vue_type_script_setup_true_lang-CucsgKGv.js} (90%) rename server/public/admin/assets/{content.vue_vue_type_script_setup_true_lang-DmXwhNrJ.js => content.vue_vue_type_script_setup_true_lang-DHTycTmO.js} (91%) rename server/public/admin/assets/{content.vue_vue_type_script_setup_true_lang-DEKh7b2S.js => content.vue_vue_type_script_setup_true_lang-GEFFCTZx.js} (93%) rename server/public/admin/assets/{data-table-A0HpJ7Mm.js => data-table-CVToWtGV.js} (78%) rename server/public/admin/assets/{data-table.vue_vue_type_script_setup_true_lang-DjVSIGkj.js => data-table.vue_vue_type_script_setup_true_lang-CCKhdE3_.js} (87%) rename server/public/admin/assets/{decoration-DM5uWpUh.js => decoration-Bm7sdIO5.js} (87%) rename server/public/admin/assets/{decoration-img-BfhW5Pfn.js => decoration-img-C_6VQE85.js} (90%) rename server/public/admin/assets/{default_reply-CtZiwc94.js => default_reply-DETQlHCh.js} (89%) rename server/public/admin/assets/{department-D1C6B5Rb.js => department-CPN-GQBQ.js} (85%) rename server/public/admin/assets/{detail-R33HH-pF.js => detail-CjhL1xRr.js} (90%) rename server/public/admin/assets/{detail-OXnGdhXW.js => detail-DkGJp6bn.js} (77%) rename server/public/admin/assets/{detail.vue_vue_type_script_setup_true_lang-HyVErxzo.js => detail.vue_vue_type_script_setup_true_lang-KAUod-LS.js} (94%) rename server/public/admin/assets/{dict-DsF-lFVQ.js => dict-_OsdLnQ0.js} (92%) rename server/public/admin/assets/{doctor-ERC0u9ds.js => doctor-B1l88RG7.js} (93%) rename server/public/admin/assets/{edit-Clxpwe1d.js => edit--Q-eJJiP.js} (72%) rename server/public/admin/assets/{edit-mIbm_DQM.js => edit-9b3NnDCR.js} (87%) create mode 100644 server/public/admin/assets/edit-B4katxjg.css rename server/public/admin/assets/{edit-BmJVtpqm.js => edit-B9_OPaOY.js} (72%) rename server/public/admin/assets/{edit-DOfiJnb0.js => edit-BYcKv2Nr.js} (72%) rename server/public/admin/assets/{edit-Di3fESiq.js => edit-BcNbHG9E.js} (93%) rename server/public/admin/assets/{edit-CB24ZT7f.js => edit-CLnbAjNe.js} (81%) delete mode 100644 server/public/admin/assets/edit-CTGLLvCK.css rename server/public/admin/assets/{edit-WbtKJoSY.js => edit-CU1b3WT8.js} (72%) rename server/public/admin/assets/{edit-BuZP-n_w.js => edit-CWPwekag.js} (78%) rename server/public/admin/assets/{edit-VZjHtNEm.js => edit-CaOp3WqK.js} (72%) rename server/public/admin/assets/{edit-EtjsXbWm.js => edit-Cuo5Xix3.js} (80%) rename server/public/admin/assets/{edit-1w4qctHu.js => edit-D-7kihnF.js} (94%) rename server/public/admin/assets/{edit-CbK9A2PJ.js => edit-DOLeB9bU.js} (72%) rename server/public/admin/assets/{edit-O1DAOQSJ.js => edit-DcbXwFsK.js} (78%) rename server/public/admin/assets/{edit-Zw9_Kv3n.js => edit-DiwjWVeq.js} (70%) rename server/public/admin/assets/{edit-XHsmOjza.js => edit-DlyMF4P1.js} (96%) rename server/public/admin/assets/{edit-Cq_bDKDi.js => edit-FOPggzAA.js} (93%) rename server/public/admin/assets/{edit-DWHm4v8w.js => edit-ZHONAKYO.js} (92%) rename server/public/admin/assets/{edit-CeM1_STE.js => edit-da7f1plM.js} (61%) rename server/public/admin/assets/{edit-BSr-JJy7.js => edit-exJlNE2k.js} (94%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-B4JYne-z.js => edit.vue_vue_type_script_setup_true_lang-4M1uawlp.js} (93%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-DWJr43-F.js => edit.vue_vue_type_script_setup_true_lang-AZqplQcV.js} (94%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-DCwkgbiT.js => edit.vue_vue_type_script_setup_true_lang-C6c6C0k_.js} (95%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-D_9_6gwT.js => edit.vue_vue_type_script_setup_true_lang-CT55bh5g.js} (95%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-eHjRizsD.js => edit.vue_vue_type_script_setup_true_lang-CcPfqlP3.js} (94%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-BQLLHkNg.js => edit.vue_vue_type_script_setup_true_lang-Ckd6_StS.js} (89%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-Bn25lr6h.js => edit.vue_vue_type_script_setup_true_lang-CzIPtSHb.js} (92%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-CqqEYZQs.js => edit.vue_vue_type_script_setup_true_lang-DEb_eTOG.js} (97%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-zg7lp-Gj.js => edit.vue_vue_type_script_setup_true_lang-DprRMcLt.js} (91%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-WtchkpFu.js => edit.vue_vue_type_script_setup_true_lang-DurmHjST.js} (92%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_name_articleColumnEdit_lang-DHR2scKm.js => edit.vue_vue_type_script_setup_true_name_articleColumnEdit_lang-BvJY6-ul.js} (90%) rename server/public/admin/assets/{element-plus-E3_bqN8U.css => element-plus-CDEmIEUa.css} (90%) delete mode 100644 server/public/admin/assets/element-plus-D_tJqBgC.js create mode 100644 server/public/admin/assets/element-plus-Dmpg6ayE.js rename server/public/admin/assets/{environment-Dfo0VQfJ.js => environment-DdFNqDX0.js} (93%) rename server/public/admin/assets/{error-DTOfRDY0.js => error-t_EkCG7t.js} (90%) rename server/public/admin/assets/{fans-B1cWXwKm.js => fans-DyPtWpdL.js} (89%) rename server/public/admin/assets/{file-D-YnPjFu.js => file-BppN_Z6J.js} (86%) rename server/public/admin/assets/{file-CCsyMC54.js => file-CSSg91XZ.js} (95%) rename server/public/admin/assets/{filing-SsBi5ffK.js => filing-CQaJcxa_.js} (88%) rename server/public/admin/assets/{finance-D2z713BN.js => finance-D5VZU_9K.js} (90%) rename server/public/admin/assets/{follow_reply-XqLdO7zL.js => follow_reply-DVUfSvOm.js} (88%) rename server/public/admin/assets/{footer.vue_vue_type_script_setup_true_lang-Bodxu_Kw.js => footer.vue_vue_type_script_setup_true_lang-BPq3_KjI.js} (89%) rename server/public/admin/assets/{h5-BXRcO42Y.js => h5-C2AYlkv7.js} (91%) rename server/public/admin/assets/{h5-CElwROQj.js => h5-UKjKCMut.js} (98%) rename server/public/admin/assets/{icon-CYScamKK.js => icon-BjJcIm_n.js} (92%) delete mode 100644 server/public/admin/assets/im-business-message-parse-CV1kBVNe.js create mode 100644 server/public/admin/assets/im-business-message-parse-ClZDqEhB.js create mode 100644 server/public/admin/assets/index-6vM5hgd3.css rename server/public/admin/assets/{index-C7-SaQaE.js => index-BAzumEn_.js} (93%) create mode 100644 server/public/admin/assets/index-BGEYAs46.css rename server/public/admin/assets/{index-B5YfXeiX.js => index-BKLtguGT.js} (94%) rename server/public/admin/assets/{index-R422JBri.js => index-BNNrWZS2.js} (86%) delete mode 100644 server/public/admin/assets/index-BNrzv8cF.css create mode 100644 server/public/admin/assets/index-BO22pDeE.css rename server/public/admin/assets/{index-DcSJzola.js => index-BRE4E6Vm.js} (94%) rename server/public/admin/assets/{index-CNV8v40p.js => index-BcjJEs4G.js} (78%) create mode 100644 server/public/admin/assets/index-Bdvhi5Z1.js rename server/public/admin/assets/{index-CuzrQAfl.js => index-Bi1KXaIZ.js} (90%) rename server/public/admin/assets/{index-DvmRDAPB.js => index-Bjxj32-X.js} (86%) rename server/public/admin/assets/{index-Dk9Aujqe.js => index-BpcBhjuy.js} (92%) rename server/public/admin/assets/{index-CK8AfBoJ.js => index-BslvW0Xb.js} (88%) rename server/public/admin/assets/{index-CX4LZY3k.js => index-ByoNSxjv.js} (88%) create mode 100644 server/public/admin/assets/index-C11IbIoh.css delete mode 100644 server/public/admin/assets/index-C2QZXI4o.js create mode 100644 server/public/admin/assets/index-CC2dh0em.js delete mode 100644 server/public/admin/assets/index-CDhcw7Zl.css delete mode 100644 server/public/admin/assets/index-CLJhkNLF.js delete mode 100644 server/public/admin/assets/index-CV8PkUyQ.js rename server/public/admin/assets/{index-CnDGJDlm.js => index-CWW8cL6Z.js} (91%) rename server/public/admin/assets/{index--UPAVlPP.js => index-CYtIBlBr.js} (93%) rename server/public/admin/assets/{index-Bw6nwob3.css => index-Ca9-_Wc4.css} (66%) rename server/public/admin/assets/{index-dPKfVNOt.js => index-Cd8YnHKB.js} (93%) delete mode 100644 server/public/admin/assets/index-CiOQZMXH.js rename server/public/admin/assets/{index-C8aSG10T.js => index-Cj_fKeap.js} (63%) rename server/public/admin/assets/{index-DRfpjfDk.js => index-ClInsgAk.js} (85%) rename server/public/admin/assets/{index-BMxItfrv.js => index-CtFRZE9b.js} (92%) delete mode 100644 server/public/admin/assets/index-CyOxl4QQ.css rename server/public/admin/assets/{index-LhIzvuH6.js => index-D5I36WUQ.js} (92%) create mode 100644 server/public/admin/assets/index-D6n3beXq.css rename server/public/admin/assets/{index-BjP7uRVt.js => index-D8qLD5yB.js} (91%) rename server/public/admin/assets/{index-zdk8FXaA.js => index-D8wUL01m.js} (71%) create mode 100644 server/public/admin/assets/index-DC1d-X1P.js rename server/public/admin/assets/{index-fYTl8bPL.js => index-DGzewPWT.js} (90%) delete mode 100644 server/public/admin/assets/index-DLTwVBhz.js rename server/public/admin/assets/{index-CeMAytUF.js => index-DOD1_T7u.js} (90%) delete mode 100644 server/public/admin/assets/index-DSta_kR_.css create mode 100644 server/public/admin/assets/index-DWk_b8Nv.js delete mode 100644 server/public/admin/assets/index-Da4T11FW.css delete mode 100644 server/public/admin/assets/index-De1i6-_G.js rename server/public/admin/assets/{index-B0-CMmaf.js => index-DrHgxz5E.js} (91%) create mode 100644 server/public/admin/assets/index-Dsqo5NKa.js rename server/public/admin/assets/{index-CtI-RQmj.js => index-DvDZD3BW.js} (86%) rename server/public/admin/assets/{index-B9vchYqI.js => index-Dyw4Bs8T.js} (74%) create mode 100644 server/public/admin/assets/index-H2pYAx4C.js rename server/public/admin/assets/{index-BE87uaYN.js => index-JC3O30UN.js} (83%) rename server/public/admin/assets/{index-Dn-Ofp4z.js => index-R0RdFdal.js} (97%) rename server/public/admin/assets/{index-DmM2qFKf.js => index-SNmtJ_Bs.js} (87%) rename server/public/admin/assets/{index-g_OimFHv.js => index-_E7jYQiT.js} (86%) rename server/public/admin/assets/{index-DBOpb5OQ.js => index-i7zXlMKf.js} (97%) rename server/public/admin/assets/{index-uns5GlJh.js => index-mRhHxr_h.js} (90%) rename server/public/admin/assets/{index-DSLJKRNq.js => index-nuHiAmJ7.js} (90%) delete mode 100644 server/public/admin/assets/index-p0SHiF4t.js create mode 100644 server/public/admin/assets/index-uzqiWbfO.js rename server/public/admin/assets/{index-0ZxTs5_c.js => index-vaRb-9O-.js} (83%) rename server/public/admin/assets/{index.vue_vue_type_script_setup_true_lang-DoJzuhty.js => index.vue_vue_type_script_setup_true_lang-BfmZk1Ml.js} (96%) rename server/public/admin/assets/{index.vue_vue_type_script_setup_true_lang-Bn4hGFEd.js => index.vue_vue_type_script_setup_true_lang-BzILDzNF.js} (92%) rename server/public/admin/assets/{index.vue_vue_type_script_setup_true_lang-DFNL-1hU.js => index.vue_vue_type_script_setup_true_lang-DyQAa4vY.js} (97%) rename server/public/admin/assets/{index.vue_vue_type_script_setup_true_lang-oQIsvp3z.js => index.vue_vue_type_script_setup_true_lang-HG7uja_e.js} (93%) rename server/public/admin/assets/{index.vue_vue_type_script_setup_true_lang-B2L9Hbb9.js => index.vue_vue_type_script_setup_true_lang-pXicZ9qy.js} (93%) rename server/public/admin/assets/{index.vue_vue_type_style_index_0_lang-BF0GJ75W.js => index.vue_vue_type_style_index_0_lang-D1-Jsym2.js} (96%) delete mode 100644 server/public/admin/assets/index_h5-CiRK5Z-j.js create mode 100644 server/public/admin/assets/index_h5-DZ8wSxRI.js rename server/public/admin/assets/{information-vJXluvYc.js => information-DmpFe_sL.js} (91%) rename server/public/admin/assets/{journal-DtHD3Y25.js => journal-Ck7vlvaC.js} (89%) rename server/public/admin/assets/{keyword_reply-0VwNDhOW.js => keyword_reply-DHvG632T.js} (89%) rename server/public/admin/assets/{link-BAtxiWYr.js => link-DuSWz-nG.js} (75%) delete mode 100644 server/public/admin/assets/list-BcENeHib.js create mode 100644 server/public/admin/assets/list-BnBvuH7l.js delete mode 100644 server/public/admin/assets/list-CgT7ux9e.css create mode 100644 server/public/admin/assets/list-DMH7gqqN.css rename server/public/admin/assets/{list-AyLjHngq.js => list-IJDD4v4W.js} (87%) rename server/public/admin/assets/{login-C8d0fPHk.js => login-qPar3k-n.js} (96%) rename server/public/admin/assets/{login_register-C4WHTu0t.js => login_register-CWNes9cm.js} (94%) rename server/public/admin/assets/{medicine-JFvY0I_c.js => medicine-DCa5VHZd.js} (81%) rename server/public/admin/assets/{medicine-L99e6FLc.js => medicine-YapJuv_G.js} (97%) rename server/public/admin/assets/{menu-BTLLBcs4.js => menu-BCs92u6p.js} (86%) rename server/public/admin/assets/{menu-DwaaS5Nh.js => menu-DNa47EbZ.js} (76%) rename server/public/admin/assets/{menu-DOvFmi4u.js => menu-mDEmyn7p.js} (90%) rename server/public/admin/assets/{menu-set-DQ2tFagX.js => menu-set-C-xj8VxZ.js} (66%) rename server/public/admin/assets/{menu-set.vue_vue_type_script_setup_true_lang-DmGNkHc9.js => menu-set.vue_vue_type_script_setup_true_lang-C-PhiS38.js} (92%) rename server/public/admin/assets/{message-g-iRPj9v.js => message-CutWFsId.js} (87%) rename server/public/admin/assets/{mobile-style-BAZO-N9A.js => mobile-style-Wp5PUtZ9.js} (82%) rename server/public/admin/assets/{mobile-style.vue_vue_type_script_setup_true_lang-B0js45YA.js => mobile-style.vue_vue_type_script_setup_true_lang-B3uTNLek.js} (95%) rename server/public/admin/assets/{oa-attr-DSrpPY-K.js => oa-attr-BG9cPDs4.js} (82%) rename server/public/admin/assets/{oa-menu-form-CNpBGj0l.js => oa-menu-form-CQEO1eiP.js} (82%) rename server/public/admin/assets/{oa-menu-form-edit-Lkm1tb5v.js => oa-menu-form-edit-M-NipbyZ.js} (75%) rename server/public/admin/assets/{oa-menu-form-edit.vue_vue_type_script_setup_true_lang-DFePDpZ1.js => oa-menu-form-edit.vue_vue_type_script_setup_true_lang-BcOAscR0.js} (90%) rename server/public/admin/assets/{oa-menu-form.vue_vue_type_script_setup_true_lang-pBFWnWXf.js => oa-menu-form.vue_vue_type_script_setup_true_lang-ByPCfX_H.js} (95%) rename server/public/admin/assets/{oa-phone-dLzkAioi.js => oa-phone-Ch4qvxEk.js} (90%) rename server/public/admin/assets/{open_setting-C1VY4L3U.js => open_setting-udbGKJN_.js} (92%) rename server/public/admin/assets/{order-DQ_203rf.js => order-D8LIXVNB.js} (95%) create mode 100644 server/public/admin/assets/order_list-CoRfQWe5.css create mode 100644 server/public/admin/assets/order_list-yQNZfjah.js rename server/public/admin/assets/{overflow-CEjTKXoN.js => overflow-JSSQqshi.js} (88%) rename server/public/admin/assets/{paiban-GtunX9Lr.js => paiban-D7Qzys7x.js} (93%) rename server/public/admin/assets/{patient-call-Cx0Ylzv5.js => patient-call-nKsjAgq4.js} (87%) rename server/public/admin/assets/{pay-BLgf3CmD.js => pay-v9y4qZZ2.js} (85%) rename server/public/admin/assets/{pc-BwEgUHoi.js => pc-BWruUdu5.js} (89%) rename server/public/admin/assets/{pc_details-BnmW2P2t.js => pc_details-CT6yJ3_6.js} (58%) rename server/public/admin/assets/{perm-BpSYNrQi.js => perm-BfOX4s25.js} (67%) rename server/public/admin/assets/{picker-Dd64qXUf.js => picker-B5ZiOH7-.js} (94%) rename server/public/admin/assets/{picker-C8lFmPfc.js => picker-Df7KkdWk.js} (96%) rename server/public/admin/assets/{picker.vue_vue_type_script_setup_true_lang-C_uXS3WY.js => picker.vue_vue_type_script_setup_true_lang-JVrY6m1N.js} (98%) rename server/public/admin/assets/{popover_input-BsRT9icJ.js => popover_input-q1ECJaJ_.js} (89%) rename server/public/admin/assets/{post-D1yUIiVo.js => post-DVV_yBbz.js} (86%) rename server/public/admin/assets/{prescription-drawer-C-6CV15k.js => prescription-drawer-CPZFLH2u.js} (97%) create mode 100644 server/public/admin/assets/prescription-drawer-_wsVAGl0.css delete mode 100644 server/public/admin/assets/prescription-drawer-uMED861l.css rename server/public/admin/assets/{preview-CbVixrpC.js => preview-CGS5X6P8.js} (52%) rename server/public/admin/assets/{preview-pc-jc_ygjl9.js => preview-pc-riMgcvDF.js} (55%) create mode 100644 server/public/admin/assets/progress-CQXIT7YT.css delete mode 100644 server/public/admin/assets/progress-CyS-BQry.js delete mode 100644 server/public/admin/assets/progress-IhkG0rt9.css create mode 100644 server/public/admin/assets/progress-mXZLUVHP.js rename server/public/admin/assets/{protocol-Dc8d97Dt.js => protocol-BU2sV6Rz.js} (84%) create mode 100644 server/public/admin/assets/qywx-B2_YcuPk.css create mode 100644 server/public/admin/assets/qywx-CdbIpoXq.js rename server/public/admin/assets/{recharge_record-3XWARmP5.js => recharge_record-D8KzKP8y.js} (90%) rename server/public/admin/assets/{refund-log-BDRRZ4tr.js => refund-log-lqdg3tzR.js} (81%) rename server/public/admin/assets/{refund-log.vue_vue_type_script_setup_true_lang-Dm_bEIMO.js => refund-log.vue_vue_type_script_setup_true_lang-gLb3pbTv.js} (92%) rename server/public/admin/assets/{refund_record-chg3Fcic.js => refund_record-BM0eqNDQ.js} (92%) rename server/public/admin/assets/{relations-add-sYkGw7Dk.js => relations-add-Bg4Fxdpi.js} (74%) rename server/public/admin/assets/{relations-add.vue_vue_type_script_setup_true_lang-CAA2u7e9.js => relations-add.vue_vue_type_script_setup_true_lang-C6N6sEvb.js} (91%) rename server/public/admin/assets/{rich_text-DTLQ4KLU.js => rich_text-BkZKCgNi.js} (77%) rename server/public/admin/assets/{role-CxO4EoT5.js => role-BIezdAsx.js} (83%) rename server/public/admin/assets/{roster-DCc8KY4o.js => roster-_tBANg8j.js} (91%) rename server/public/admin/assets/{setting-BtYclTQc.js => setting-INJkhGl8.js} (80%) rename server/public/admin/assets/{setup-C9K64vIE.js => setup-D592UMoK.js} (79%) rename server/public/admin/assets/{statistics-CQ1h3Jks.js => statistics--YHyEZxF.js} (88%) rename server/public/admin/assets/{style-CQ0lZeOe.js => style-C5m2K9Dk.js} (81%) rename server/public/admin/assets/{tabbar-BIAKYnk5.js => tabbar-BgeBV1-s.js} (76%) create mode 100644 server/public/admin/assets/tcm-BCaX4wIV.js delete mode 100644 server/public/admin/assets/tcm-CgwtzlaG.js rename server/public/admin/assets/{theme-picker-2i2vE-oC.js => theme-picker-BnVPZoF_.js} (87%) delete mode 100644 server/public/admin/assets/tongji-B8Bl5suj.js create mode 100644 server/public/admin/assets/tongji-C0VnzFxy.css create mode 100644 server/public/admin/assets/tongji-CDlB6kN_.js delete mode 100644 server/public/admin/assets/tongji-qftPQn42.css rename server/public/admin/assets/{upload-Cl5ZdHRx.js => upload-CwmjoJh6.js} (90%) rename server/public/admin/assets/{useDictOptions-K1FBl8D4.js => useDictOptions-7gUIpUOu.js} (84%) rename server/public/admin/assets/{useMenuOa-7i4hWT0q.js => useMenuOa-By63luzh.js} (92%) rename server/public/admin/assets/{user-Xpz693Jr.js => user-C24uvj2m.js} (82%) rename server/public/admin/assets/{weapp-PpT_XwjK.js => weapp-BXdMcuzU.js} (91%) rename server/public/admin/assets/{weapp-CaROY3Vi.js => weapp-l1PoIc7w.js} (68%) rename server/public/admin/assets/{website-D0DEd3BF.js => website-BQObevlt.js} (91%) rename server/public/admin/assets/{wx_oa-C9TfIkMb.js => wx_oa-DAhGf1fY.js} (93%) diff --git a/COMPLETION_REQUEST_FEATURE.md b/COMPLETION_REQUEST_FEATURE.md new file mode 100644 index 00000000..d30a055b --- /dev/null +++ b/COMPLETION_REQUEST_FEATURE.md @@ -0,0 +1,93 @@ +# 完单申请功能实现总结 + +## 功能概述 + +在补齐支付单时,用户可以选择是否申请完成订单。如果勾选了完单申请,审核人员在进行支付审核时可以看到该申请信息。 + +## 数据库修改 + +### 新增字段(zyt_tcm_prescription_order表) + +```sql +-- 执行 add_completion_request_field.sql +ALTER TABLE `zyt_tcm_prescription_order` +ADD COLUMN `completion_request` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '完单申请:0=未申请,1=已申请' AFTER `payment_slip_audit_remark`, +ADD COLUMN `completion_request_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '完单申请时间' AFTER `completion_request`, +ADD COLUMN `completion_request_by` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '完单申请人ID' AFTER `completion_request_time`, +ADD COLUMN `completion_request_by_name` varchar(64) NOT NULL DEFAULT '' COMMENT '完单申请人姓名' AFTER `completion_request_by`; +``` + +## 前端修改 + +### 1. 补齐支付单对话框(order_list.vue) + +- 添加"完单申请"单选项(不申请/申请完成订单) +- 添加提示文字说明 +- 表单数据中添加 `completion_request` 字段(默认值0) + +### 2. 提交逻辑 + +- 手动创建支付单时,将 `completion_request` 字段传递给后端 +- 关联已有支付单时,同样传递 `completion_request` 字段 + +### 3. 审核对话框 + +- 如果订单有完单申请(`completion_request=1`),显示警告提示框 +- 显示申请人姓名和申请时间 +- 审核人员可以清楚看到该订单已申请完成 + +## 后端修改 + +### 1. 新增支付单接口(addPayOrder) + +**文件**: `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` + +- 接收 `completion_request` 参数 +- 如果值为1,记录完单申请信息: + - `completion_request = 1` + - `completion_request_time = 当前时间戳` + - `completion_request_by = 操作人ID` + - `completion_request_by_name = 操作人姓名` +- 操作日志中记录"并申请完成订单" + +### 2. 关联支付单接口(linkPayOrder) + +**文件**: `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` + +- 接收 `completion_request` 参数 +- 如果值为1,记录完单申请信息(同上) +- 操作日志中记录"并申请完成订单" + +## 使用流程 + +1. **补齐支付单** + - 用户在"已发货"状态下点击"新增支付单" + - 选择手动创建或关联已有支付单 + - 勾选"申请完成订单"选项 + - 提交后,订单记录完单申请信息 + +2. **支付审核** + - 审核人员点击"支付审核" + - 如果该订单有完单申请,对话框顶部显示黄色警告框 + - 显示申请人和申请时间 + - 审核人员可以根据完单申请决定是否通过 + +3. **完成订单** + - 支付审核通过后,订单状态变为"已发货" + - 满足完成条件(已发货+支付审核通过)时,可以点击"完成订单" + +## 注意事项 + +1. 完单申请只是一个提示信息,不会自动完成订单 +2. 审核人员仍需手动点击"完成订单"按钮 +3. 完单申请信息在订单详情中可见 +4. 操作日志会记录完单申请的操作 + +## 测试要点 + +- [ ] 手动创建支付单时勾选完单申请 +- [ ] 关联已有支付单时勾选完单申请 +- [ ] 审核对话框正确显示完单申请信息 +- [ ] 申请人姓名和时间正确显示 +- [ ] 操作日志正确记录 +- [ ] 不勾选完单申请时,审核对话框不显示提示 diff --git a/DIAGNOSIS_LIST_SORTING.md b/DIAGNOSIS_LIST_SORTING.md new file mode 100644 index 00000000..4f77d046 --- /dev/null +++ b/DIAGNOSIS_LIST_SORTING.md @@ -0,0 +1,90 @@ +# 诊单列表排序规则修改 + +## 修改内容 + +修改了诊单列表的排序逻辑,按照新的业务规则对数据进行排序。 + +## 排序规则 + +### 优先级顺序 +1. **已过号** (status=4) - 最优先显示 +2. **已预约** (status=1) - 第二优先 +3. **已完成** (status=3) - 第三优先 +4. **其他状态** - 最后显示 + +### 同状态内排序 +在同一状态内,按以下规则升序排列: +- 挂号日期 (appointment_date) +- 挂号时间 (appointment_time) +- 诊单ID (id) 降序 + +## 技术实现 + +### 排序SQL逻辑 + +```php +// 获取最早的挂号状态(用于排序优先级) +$minAptStatusExpr = '(SELECT apt.status FROM ' . $aptTbl . ' apt + WHERE apt.patient_id = ' . $diagTbl . '.id + AND apt.status IN (1,3,4)' . $minAptDateCond . ' + ORDER BY CASE apt.status + WHEN 4 THEN 1 + WHEN 1 THEN 2 + WHEN 3 THEN 3 + ELSE 4 + END, + apt.appointment_date ASC, + apt.appointment_time ASC + LIMIT 1)'; + +// 获取最早的挂号时间(用于同状态内排序) +$minAptExpr = '(SELECT MIN(CONCAT(apt.appointment_date, \' \', + IFNULL(NULLIF(TRIM(apt.appointment_time), \'\'), \'00:00:00\'))) + FROM ' . $aptTbl . ' apt + WHERE apt.patient_id = ' . $diagTbl . '.id + AND apt.status IN (1,3,4)' . $minAptDateCond . ')'; + +// 最终排序 +->orderRaw('CASE IFNULL(' . $minAptStatusExpr . ', 999) + WHEN 4 THEN 1 + WHEN 1 THEN 2 + WHEN 3 THEN 3 + ELSE 4 +END ASC, +IFNULL(' . $minAptExpr . ", '9999-12-31 23:59:59') ASC, +{$diagTbl}.id DESC") +``` + +### 关键点 + +1. **子查询获取状态**: 使用子查询获取每个诊单最早的挂号状态 +2. **CASE表达式**: 将状态映射为排序优先级数字 (1-4) +3. **IFNULL处理**: 对于没有挂号的诊单,使用默认值 999 和 '9999-12-31 23:59:59' +4. **日期筛选支持**: 如果传入 `appointment_date` 参数,只按该日期的挂号排序 + +## 修改文件 + +- `server/app/adminapi/lists/tcm/DiagnosisLists.php` + +## 验证步骤 + +1. 清除缓存: `php think clear` ✅ +2. 访问诊单列表页面 +3. 验证排序顺序: + - 已过号的诊单显示在最前面 + - 然后是已预约的诊单 + - 最后是已完成的诊单 +4. 验证同状态内按挂号时间升序排列 + +## 业务场景 + +此排序规则适用于以下场景: +- 医生/医助需要优先处理已过号的患者(可能需要重新安排) +- 然后关注即将到来的预约 +- 最后查看已完成的历史记录 + +## 注意事项 + +- 排序只考虑状态为 1(已预约)、3(已完成)、4(已过号) 的挂号记录 +- 没有挂号记录的诊单会排在最后 +- 如果一个诊单有多条挂号记录,取最早的一条进行排序 diff --git a/IM_CHAT_MESSAGES_PERFORMANCE_FIX.md b/IM_CHAT_MESSAGES_PERFORMANCE_FIX.md new file mode 100644 index 00000000..ce46431d --- /dev/null +++ b/IM_CHAT_MESSAGES_PERFORMANCE_FIX.md @@ -0,0 +1,255 @@ +# IM 聊天记录接口性能优化 + +## 问题描述 + +接口 `tcm.diagnosis/getImChatMessages?diagnosis_id=377` 响应超时(30秒),错误信息: +``` +Maximum execution time of 30 seconds exceeded +``` + +错误发生在 `TencentImService.php` 第 346 行的 `curl_exec($ch)` 调用。 + +## 根本原因 + +1. **查询范围过大** + - 原代码调用 `collectAllDoctorImPeerAccounts()` 获取所有医生和医助账号 + - 如果系统有 50+ 个医生/医助,就需要对每个账号调用腾讯云 IM API + - 每个账号可能需要多次分页请求(每次最多 100 条消息) + +2. **API 调用次数过多** + - 假设有 50 个医生,每个医生有 300 条消息 + - 总 API 调用次数 = 50 × (300/100) = 150 次 + - 每次调用耗时 0.5-1 秒,总耗时 75-150 秒 + +3. **超时设置不合理** + - PHP 默认执行时间限制:30 秒 + - 单个 HTTP 请求超时:30 秒 + - 实际需要的时间远超这些限制 + +## 优化方案 + +### 1. 只查询指派的医助(核心优化) + +修改 `getImChatMessagesForDiagnosis` 方法: + +**优化前:** +```php +// 查询所有医生/医助账号(可能有几十个) +$doctorAccounts = self::collectAllDoctorImPeerAccounts(); +$assistantId = isset($diag['assistant_id']) ? (int)$diag['assistant_id'] : 0; +if ($assistantId > 0) { + $doctorAccounts[] = 'doctor_' . $assistantId; +} +``` + +**优化后:** +```php +// 只查询指派给该诊单的医助 +$doctorAccounts = []; +$assistantId = isset($diag['assistant_id']) ? (int)$diag['assistant_id'] : 0; +if ($assistantId > 0) { + $doctorAccounts[] = 'doctor_' . $assistantId; +} + +// 如果没有指派医助,直接返回归档记录 +if (empty($doctorAccounts)) { + $lists = self::enrichImMessagesWithStaffNames($archived); + return [ + 'lists' => $lists, + 'patient_im_id' => $patientImId, + 'patient_name' => $diag['patient_name'] ?? '', + 'doctor_accounts_queried' => [], + ]; +} +``` + +**效果:** +- API 调用次数从 150 次降低到 3-5 次 +- 响应时间从 75-150 秒降低到 2-5 秒 + +### 2. 增加 HTTP 请求超时时间 + +修改 `TencentImService::adminGetRoamMsg` 方法: + +```php +// 从 30 秒增加到 60 秒 +$result = $this->httpPost($url, json_encode($data), 60); +``` + +### 3. 增加 PHP 执行时间限制 + +修改 `DiagnosisController::getImChatMessages` 方法: + +```php +public function getImChatMessages() +{ + // 增加执行时间限制到 120 秒 + set_time_limit(120); + + // ... 其他代码 +} +``` + +### 4. 新增优化版拉取方法 + +添加 `pullLiveImChatMessagesForDiagnosisOptimized` 方法: + +```php +/** + * 优化版:只拉取指定医生账号的消息(避免查询所有医生导致超时) + */ +private static function pullLiveImChatMessagesForDiagnosisOptimized($diag, array $doctorAccounts): array +{ + // 只查询指定的医生账号,大大减少API调用次数 + // ... +} +``` + +## 性能对比 + +| 指标 | 优化前 | 优化后 | 改善 | +|------|--------|--------|------| +| 查询的医生账号数 | 50+ | 1 | 98% ↓ | +| API 调用次数 | 150+ | 3-5 | 97% ↓ | +| 响应时间 | 75-150秒 | 2-5秒 | 95% ↓ | +| 超时风险 | 高 | 低 | - | + +## 业务逻辑说明 + +### 为什么只查询指派的医助? + +1. **业务场景** + - 每个诊单都会指派一个医助(`assistant_id`) + - 患者主要与指派的医助沟通 + - 其他医生/医助不会与该患者聊天 + +2. **数据准确性** + - 只显示与该诊单相关的聊天记录 + - 避免显示无关的聊天记录 + - 提高数据查询的精准度 + +3. **归档机制** + - 系统有定时任务将 IM 消息归档到本地数据库 + - 归档数据突破腾讯云 7 天限制 + - 即使不查询所有医生,历史记录也不会丢失 + +### 如果需要查询所有医生怎么办? + +如果确实需要查询所有医生的聊天记录(例如管理员查看),可以: + +1. **使用定时任务归档** + ```bash + php think tcm:sync-im-chat-archive --days=7 --limit=100 + ``` + +2. **异步查询** + - 将查询任务放入队列 + - 后台慢慢处理 + - 完成后通知用户 + +3. **分页查询** + - 前端分批请求不同医生的记录 + - 每次只查询 1-2 个医生 + - 避免单次请求超时 + +## 测试建议 + +1. **测试正常场景** + ``` + GET /adminapi/tcm.diagnosis/getImChatMessages?diagnosis_id=377 + ``` + - 应该在 5 秒内返回 + - 返回指派医助的聊天记录 + +2. **测试无指派医助场景** + - 创建一个未指派医助的诊单 + - 应该返回归档记录(如果有) + - 不应该报错 + +3. **测试大量消息场景** + - 找一个有 500+ 条消息的诊单 + - 应该能正常返回 + - 响应时间应该在 10 秒内 + +## 后续优化建议 + +### 1. 添加缓存 + +```php +public static function getImChatMessagesForDiagnosis(int $diagnosisId) +{ + // 缓存 5 分钟 + $cacheKey = 'im_chat_messages_' . $diagnosisId; + $cached = Cache::get($cacheKey); + if ($cached) { + return $cached; + } + + // ... 查询逻辑 + + Cache::set($cacheKey, $result, 300); + return $result; +} +``` + +### 2. 使用 Redis 队列 + +```php +// 将耗时的查询放入队列 +Queue::push(SyncImChatMessagesJob::class, [ + 'diagnosis_id' => $diagnosisId +]); + +// 前端轮询查询结果 +``` + +### 3. WebSocket 实时推送 + +```php +// 使用 WebSocket 实时推送新消息 +// 避免每次都查询腾讯云 API +``` + +### 4. 增量同步 + +```php +// 只同步最近 1 小时的新消息 +// 历史消息从归档表读取 +$minTime = time() - 3600; +$result = $svc->adminGetRoamMsg($operator, $peer, 100, $minTime); +``` + +## 相关文件 + +- `server/app/adminapi/logic/tcm/DiagnosisLogic.php` - 主要逻辑 +- `server/app/common/service/TencentImService.php` - IM 服务 +- `server/app/adminapi/controller/tcm/DiagnosisController.php` - 控制器 + +## 监控建议 + +1. **添加性能日志** + ```php + $startTime = microtime(true); + // ... 查询逻辑 + $duration = microtime(true) - $startTime; + Log::info('IM消息查询耗时', [ + 'diagnosis_id' => $diagnosisId, + 'duration' => $duration, + 'doctor_accounts' => count($doctorAccounts), + 'message_count' => count($merged), + ]); + ``` + +2. **设置告警** + - 响应时间 > 10 秒:警告 + - 响应时间 > 30 秒:严重 + - API 调用失败率 > 5%:严重 + +3. **定期检查** + - 每周检查平均响应时间 + - 每月检查 API 调用次数 + - 及时发现性能退化 + +--- + +最后更新:2024-04-11 diff --git a/QYWX_CUSTOMER_SYNC_FIX.md b/QYWX_CUSTOMER_SYNC_FIX.md new file mode 100644 index 00000000..9f48241a --- /dev/null +++ b/QYWX_CUSTOMER_SYNC_FIX.md @@ -0,0 +1,164 @@ +# 企业微信客户同步问题修复指南 + +## 问题描述 + +同步企业微信客户时,能成功获取企业成员列表(207人),但无法获取任何客户数据,所有API调用都返回错误码 `48002: "api forbidden"`。 + +## 根本原因 + +企业微信应用未开启"客户联系"API权限。错误码 `48002` 表示API接口被禁用或未授权。 + +## 解决方案 + +### 方案一:开启API权限(推荐) + +1. 登录企业微信管理后台 + - 访问:https://work.weixin.qq.com/ + - 使用管理员账号登录 + +2. 进入应用管理 + - 点击"应用管理" + - 找到您正在使用的应用(使用 `external_pay_secret` 的应用) + +3. 开启客户联系权限 + - 在应用详情页面,找到"客户联系"权限设置 + - 开启以下权限: + * ✓ 客户联系 - 获取客户列表 + * ✓ 客户联系 - 获取客户详情 + * ✓ 客户联系 - 获取客户数据统计(可选) + +4. 配置可信IP + - 在应用设置中找到"企业可信IP" + - 添加您的服务器IP地址 + - 保存配置 + +5. 等待生效 + - 权限修改后可能需要几分钟生效 + - 建议等待5-10分钟后再测试 + +### 方案二:升级企业微信版本 + +如果您的企业微信版本不支持客户联系API: + +1. 检查当前版本是否支持"客户联系"功能 +2. 如需要,升级到支持该功能的版本 +3. 部分功能可能需要付费版本 + +### 方案三:使用替代方案 + +如果无法开启API权限: + +1. 使用企业微信后台手动导出客户数据 +2. 通过CSV导入到系统 +3. 或使用企业微信的webhook回调功能(需要配置) + +## 诊断工具 + +### 1. 运行权限检查命令 + +```bash +php think qywx:check-permissions +``` + +这个命令会: +- 检查应用配置是否正确 +- 测试获取部门成员权限 +- 测试获取客户列表权限 +- 显示详细的错误信息和解决建议 + +### 2. 查看日志 + +日志文件位置:`runtime/log/` + +关键日志信息: +``` +企业微信-获取成员客户列表响应 userId=XXX: {"errcode":48002,"errmsg":"api forbidden"...} +``` + +如果看到 `errcode: 48002`,说明是权限问题。 + +## 代码改进 + +已对代码进行以下改进: + +1. **更好的错误处理** + - `getExternalContactList()` 方法现在会检测 `48002` 错误 + - 权限错误时返回 `false` 而不是空数组 + - 同步逻辑会捕获权限错误并抛出明确的异常 + +2. **新增诊断方法** + - `WechatWorkService::checkAppPermissions()` - 检查应用权限 + - `QywxCheckPermissions` 命令 - 完整的权限诊断工具 + +3. **改进的日志记录** + - 权限错误会记录更详细的信息 + - 包含解决方案提示 + +## 测试步骤 + +1. 确认已开启API权限 +2. 运行诊断命令: + ```bash + php think qywx:check-permissions + ``` + +3. 如果诊断通过,运行同步: + ```bash + php think qywx:sync-customer + ``` + +4. 检查同步结果: + - 查看日志文件 + - 检查数据库表 `la_qywx_external_contact` + - 在管理后台查看客户列表 + +## 常见问题 + +### Q1: 权限已开启,但仍然报错48002 +A: +- 等待5-10分钟让权限生效 +- 清除企业微信access_token缓存:删除缓存键 `qywx_access_token` +- 检查服务器IP是否在可信IP列表中 + +### Q2: 找不到"客户联系"权限选项 +A: +- 您的企业微信版本可能不支持此功能 +- 需要升级到企业版或专业版 +- 联系企业微信客服确认 + +### Q3: 部分成员能获取客户,部分不能 +A: +- 检查成员是否有"客户联系"权限 +- 在企业微信后台为成员分配权限 +- 确认成员确实有添加客户 + +### Q4: 同步速度很慢 +A: +- 这是正常的,因为需要逐个成员获取客户列表 +- 207个成员 × 每个成员的客户数 = 大量API调用 +- 建议设置定时任务,每小时或每天同步一次 + +## 相关文件 + +- `server/app/adminapi/logic/qywx/CustomerLogic.php` - 同步逻辑 +- `server/app/common/service/wechat/WechatWorkService.php` - API服务 +- `server/app/command/QywxSyncCustomer.php` - 同步命令 +- `server/app/command/QywxCheckPermissions.php` - 诊断命令(新增) +- `server/config/project.php` - 配置文件 + +## 联系支持 + +如果按照以上步骤仍无法解决: + +1. 收集以下信息: + - 诊断命令的完整输出 + - 最近的日志文件 + - 企业微信应用的权限截图 + +2. 联系企业微信技术支持 + - 官方文档:https://developer.work.weixin.qq.com/ + - 开发者社区:https://developers.weixin.qq.com/community/business + +--- + +最后更新:2024-04-11 diff --git a/QYWX_CUSTOMER_SYNC_GUIDE.md b/QYWX_CUSTOMER_SYNC_GUIDE.md new file mode 100644 index 00000000..68322fa5 --- /dev/null +++ b/QYWX_CUSTOMER_SYNC_GUIDE.md @@ -0,0 +1,246 @@ +# 企业微信客户同步功能说明 + +## 功能概述 + +自动从企业微信同步外部联系人(客户)信息,包括客户基本信息、跟进人等,用于患者信息匹配和自动关联。 + +同步流程: +1. 获取企业成员列表(从指定部门开始,递归获取所有子部门成员) +2. 遍历每个成员,获取其客户列表 +3. 获取客户详情并保存到数据库 +4. 自动去重,避免重复处理同一客户 + +## 功能特性 + +1. **手动同步**:点击"立即同步"按钮手动触发同步 +2. **自动同步**:配置定时自动同步,支持多种时间间隔 +3. **客户管理**:查看客户列表、搜索、查看详情 +4. **统计信息**:显示总客户数、今日新增、最后同步时间等 + +## 安装步骤 + +### 1. 数据库迁移 + +执行SQL文件创建数据表: + +```bash +mysql -u用户名 -p密码 数据库名 < server/database/migrations/create_qywx_external_contact.sql +``` + +### 2. 配置企业微信 + +在 `server/.env` 文件中配置企业微信信息: + +```env +# 企业微信配置 +WECHAT_WORK_CORP_ID=你的企业ID +WECHAT_WORK_EXTERNAL_PAY_SECRET=你的应用Secret +``` + +在 `server/config/project.php` 文件中配置同步部门(可选,默认为1即根部门): + +```php +// 企业微信客户同步配置 +// 从哪个部门开始同步(1=根部门,会递归获取所有子部门成员) +'qywx_sync_department_id' => 1, +``` + +获取方式: +- 企业ID:企业微信管理后台 → 我的企业 → 企业信息 → 企业ID +- Secret:企业微信管理后台 → 应用管理 → 选择应用 → Secret +- 部门ID:企业微信管理后台 → 通讯录 → 选择部门 → 查看部门ID(默认根部门ID为1) + +### 3. 配置权限 + +需要在企业微信应用中开启以下权限: +- 通讯录管理 → 成员信息读取(用于获取企业成员列表) +- 客户联系 → 客户基础信息 +- 客户联系 → 客户标签 +- 客户联系 → 客户联系人 + +注意:Secret必须是对应应用的Secret,且该应用需要有上述权限。 + +### 4. 配置IP白名单(重要!) + +企业微信API有IP白名单限制,必须将服务器IP添加到白名单: + +1. 获取服务器公网IP:`curl ifconfig.me` +2. 登录企业微信管理后台 → 应用管理 → 选择应用 +3. 找到"企业可信IP"设置,添加服务器IP +4. 保存并等待几分钟生效 + +详细说明请查看:[QYWX_IP_WHITELIST_GUIDE.md](./QYWX_IP_WHITELIST_GUIDE.md) + +### 5. 配置定时任务(可选) + +如果需要自动同步,配置Linux crontab: + +```bash +# 编辑crontab +crontab -e + +# 添加以下行(每小时执行一次) +0 * * * * cd /path/to/your/project/server && php think qywx:sync-customer >> /dev/null 2>&1 +``` + +或者手动强制同步(忽略自动同步设置): +```bash +php think qywx:sync-customer --force +``` + +## 使用说明 + +### 前端页面 + +访问路径:`/fans/qywx` + +功能: +- 查看客户列表 +- 搜索客户(按名称、跟进人) +- 查看客户详情 +- 手动同步 +- 配置自动同步 + +### 同步设置 + +1. 点击"同步设置"按钮 +2. 开启"自动同步"开关 +3. 选择同步间隔(1小时、2小时、4小时、6小时、12小时、24小时) +4. 点击"保存" + +### API接口 + +#### 1. 获取客户列表 +``` +GET /qywx.customer/lists +参数: +- name: 客户名称(可选) +- follow_user: 跟进人(可选) +- page: 页码 +- limit: 每页数量 +``` + +#### 2. 同步客户 +``` +POST /qywx.customer/sync +返回: +- sync_count: 同步数量 +- new_count: 新增数量 +- update_count: 更新数量 +``` + +#### 3. 获取统计信息 +``` +GET /qywx.customer/stats +返回: +- total: 总客户数 +- today: 今日新增 +- lastSync: 最后同步时间 +- syncStatus: 同步状态 +``` + +#### 4. 获取同步设置 +``` +GET /qywx.customer/getSyncSettings +返回: +- auto_sync: 是否自动同步 +- interval: 同步间隔(秒) +- last_sync_time: 最后同步时间 +``` + +#### 5. 保存同步设置 +``` +POST /qywx.customer/saveSyncSettings +参数: +- auto_sync: 是否自动同步(boolean) +- interval: 同步间隔(秒,3600-86400) +``` + +## 数据表结构 + +### zyt_qywx_external_contact(外部联系人表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int | 主键ID | +| external_userid | varchar(64) | 外部联系人ID(唯一) | +| name | varchar(64) | 客户名称 | +| avatar | varchar(255) | 头像URL | +| type | tinyint | 类型:1=微信用户,2=企业微信用户 | +| gender | tinyint | 性别:0=未知,1=男,2=女 | +| unionid | varchar(64) | 微信unionid | +| position | varchar(64) | 职位 | +| corp_name | varchar(128) | 企业名称 | +| corp_full_name | varchar(255) | 企业全称 | +| external_profile | text | 扩展信息JSON | +| follow_users | text | 跟进人列表JSON | +| create_time | int | 添加时间 | +| update_time | int | 更新时间 | +| delete_time | int | 删除时间 | + +### zyt_qywx_sync_settings(同步设置表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int | 主键ID | +| setting_key | varchar(64) | 设置键(唯一) | +| setting_value | text | 设置值JSON | +| create_time | int | 创建时间 | +| update_time | int | 更新时间 | + +## 注意事项 + +1. **API调用限制**:企业微信API有调用频率限制,建议同步间隔不要太短 +2. **Secret安全**:请妥善保管企业微信Secret,不要泄露 +3. **权限配置**:确保应用有足够的权限访问客户信息 +4. **错误处理**:同步失败会记录日志,可在系统日志中查看详细错误信息 +5. **数据更新**:同步会更新已存在的客户信息,不会删除已有数据 + +## 故障排查 + +### 1. 错误码 60020 - IP白名单限制 + +错误信息:`not allow to access from your ip` + +解决方法: +- 将服务器IP添加到企业微信应用的"企业可信IP"白名单 +- 详细说明:[QYWX_IP_WHITELIST_GUIDE.md](./QYWX_IP_WHITELIST_GUIDE.md) + +### 2. 同步失败 + +检查: +- 企业微信配置是否正确(corp_id、secret) +- 应用权限是否开启 +- IP是否在白名单中(最常见问题) +- 网络是否正常 +- 查看系统日志:`server/runtime/log/` + +### 3. 获取不到客户 + +检查: +- 应用是否有客户联系权限和通讯录读取权限 +- 是否有外部联系人 +- Secret是否正确(需要使用对应应用的Secret) +- 企业成员是否有添加客户 +- 部门ID配置是否正确(默认1为根部门) + +### 4. missing field `userid` 错误 + +这个错误已修复。新版本会先获取企业成员列表,然后遍历每个成员获取其客户。如果仍然出现此错误,请检查: +- 应用是否有"通讯录管理-成员信息读取"权限 +- 配置的Secret是否正确 + +### 5. 定时任务不执行 + +检查: +- crontab是否配置正确 +- PHP路径是否正确 +- 项目路径是否正确 +- 是否开启了自动同步 + +## 参考文档 + +- [企业微信API文档](https://developer.work.weixin.qq.com/document/) +- [获取部门成员](https://developer.work.weixin.qq.com/document/path/90200) +- [获取成员客户列表](https://developer.work.weixin.qq.com/document/path/92113) +- [获取客户详情](https://developer.work.weixin.qq.com/document/path/92114) diff --git a/QYWX_IP_WHITELIST_GUIDE.md b/QYWX_IP_WHITELIST_GUIDE.md new file mode 100644 index 00000000..5527fabf --- /dev/null +++ b/QYWX_IP_WHITELIST_GUIDE.md @@ -0,0 +1,88 @@ +# 企业微信IP白名单配置指南 + +## 问题现象 + +同步企业微信客户时报错: +``` +errcode: 60020 +errmsg: not allow to access from your ip +``` + +## 问题原因 + +企业微信API有IP白名单限制,只有在白名单中的IP才能调用API。 + +## 解决方法 + +### 1. 获取服务器IP地址 + +在服务器上运行以下命令获取公网IP: + +```bash +curl ifconfig.me +``` + +或者查看日志中的IP地址,例如: +``` +from ip: 8.219.70.152 +``` + +### 2. 配置企业微信IP白名单 + +1. 登录企业微信管理后台:https://work.weixin.qq.com/ +2. 进入"应用管理" +3. 选择你配置的应用(使用该应用的Secret) +4. 找到"企业可信IP"设置 +5. 点击"配置"或"修改" +6. 添加服务器的公网IP地址 +7. 保存配置 + +### 3. 验证配置 + +配置完成后,等待几分钟让配置生效,然后重新运行同步命令: + +```bash +php think qywx:sync-customer --force +``` + +## 注意事项 + +1. **IP地址格式**: + - 单个IP:`8.219.70.152` + - IP段:`8.219.70.0/24` + +2. **多个IP**:如果有多个服务器,需要添加所有服务器的IP + +3. **动态IP**:如果服务器IP会变化,建议: + - 使用固定IP的服务器 + - 或配置IP段 + - 或使用代理服务器 + +4. **测试环境**:开发环境和生产环境可能有不同的IP,都需要添加 + +## 常见问题 + +### Q: 添加IP后还是报错? +A: 等待5-10分钟让配置生效,清除缓存后重试: +```bash +php think clear +php think qywx:sync-customer --force +``` + +### Q: 不知道服务器IP? +A: 在服务器上运行: +```bash +curl ifconfig.me +# 或 +curl ip.sb +# 或 +curl ipinfo.io/ip +``` + +### Q: 本地开发如何测试? +A: 将本地公网IP也添加到白名单,或使用内网穿透工具(如ngrok) + +## 相关文档 + +- [企业微信API - IP白名单说明](https://developer.work.weixin.qq.com/document/path/90238) +- [企业微信错误码查询](https://open.work.weixin.qq.com/devtool/query) diff --git a/QYWX_SYNC_FIX.md b/QYWX_SYNC_FIX.md new file mode 100644 index 00000000..0dbdf12e --- /dev/null +++ b/QYWX_SYNC_FIX.md @@ -0,0 +1,108 @@ +# 企业微信客户同步功能修复 + +## 问题描述 + +调用企业微信API获取客户列表时报错: +``` +missing field `userid`. invalid Request Parameter +``` + +## 问题原因 + +根据企业微信API文档,获取客户列表接口 `/cgi-bin/externalcontact/list` 需要传入 `userid` 参数(企业成员ID)。之前的实现直接调用该接口,没有传入必需的 `userid` 参数。 + +## 解决方案 + +修改同步逻辑,采用正确的流程: + +1. 先调用 `/cgi-bin/user/list` 获取企业成员列表 +2. 遍历每个成员,调用 `/cgi-bin/externalcontact/list?userid=xxx` 获取该成员的客户列表 +3. 对每个客户调用 `/cgi-bin/externalcontact/get` 获取详情 +4. 保存到数据库,自动去重 + +## 修改文件 + +### 1. server/app/common/service/wechat/WechatWorkService.php + +新增方法: +- `getDepartmentUserList()` - 获取部门成员列表 + +修改方法: +- `getExternalContactList($userId)` - 添加 `$userId` 参数,获取指定成员的客户列表 + +### 2. server/app/adminapi/logic/qywx/CustomerLogic.php + +修改 `syncCustomers()` 方法: +- 先获取企业成员列表 +- 遍历成员获取其客户 +- 使用 `$processedCustomers` 数组避免重复处理同一客户 + +### 3. server/config/project.php + +新增配置项: +```php +// 企业微信客户同步配置 +// 从哪个部门开始同步(1=根部门,会递归获取所有子部门成员) +'qywx_sync_department_id' => 1, +``` + +### 4. QYWX_CUSTOMER_SYNC_GUIDE.md + +更新文档: +- 添加同步流程说明 +- 添加部门ID配置说明 +- 添加通讯录权限要求 +- 添加 `missing field userid` 错误排查说明 + +## 权限要求 + +企业微信应用需要开启以下权限: +- 通讯录管理 → 成员信息读取(新增) +- 客户联系 → 客户基础信息 +- 客户联系 → 客户标签 +- 客户联系 → 客户联系人 + +## 配置说明 + +### 必需配置(server/.env) +```env +WECHAT_WORK_CORP_ID=你的企业ID +WECHAT_WORK_EXTERNAL_PAY_SECRET=你的应用Secret +``` + +### 可选配置(server/config/project.php) +```php +'qywx_sync_department_id' => 1, // 默认1(根部门),会递归获取所有子部门成员 +``` + +## 使用方法 + +1. 确保配置正确 +2. 清除缓存:`php think clear` +3. 在前端页面点击"立即同步"按钮 +4. 或运行命令:`php think qywx:sync-customer` + +## 测试验证 + +修复后,同步功能应该能够: +- 成功获取企业成员列表 +- 遍历每个成员获取其客户 +- 正确保存客户信息到数据库 +- 显示同步统计(总数、新增、更新) + +注意:首次使用需要配置IP白名单,否则会报错 60020。 + +## 常见问题 + +### 错误码 60020 - IP白名单限制 + +如果遇到 `not allow to access from your ip` 错误,需要: +1. 获取服务器公网IP:`curl ifconfig.me` +2. 在企业微信管理后台添加IP到白名单 +3. 详细说明:[QYWX_IP_WHITELIST_GUIDE.md](./QYWX_IP_WHITELIST_GUIDE.md) + +## 参考文档 + +- [获取部门成员](https://developer.work.weixin.qq.com/document/path/90200) +- [获取成员客户列表](https://developer.work.weixin.qq.com/document/path/92113) +- [获取客户详情](https://developer.work.weixin.qq.com/document/path/92114) diff --git a/add_completion_request_field.sql b/add_completion_request_field.sql new file mode 100644 index 00000000..be0586a5 --- /dev/null +++ b/add_completion_request_field.sql @@ -0,0 +1,8 @@ +-- 为处方业务订单表添加完单申请字段 +-- 用于记录补齐支付单时是否申请完成订单 + +ALTER TABLE `zyt_tcm_prescription_order` +ADD COLUMN `completion_request` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '完单申请:0=未申请,1=已申请' AFTER `payment_slip_audit_remark`, +ADD COLUMN `completion_request_time` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '完单申请时间' AFTER `completion_request`, +ADD COLUMN `completion_request_by` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '完单申请人ID' AFTER `completion_request_time`, +ADD COLUMN `completion_request_by_name` varchar(64) NOT NULL DEFAULT '' COMMENT '完单申请人姓名' AFTER `completion_request_by`; diff --git a/admin/src/api/qywx.ts b/admin/src/api/qywx.ts new file mode 100644 index 00000000..55d6f4b7 --- /dev/null +++ b/admin/src/api/qywx.ts @@ -0,0 +1,26 @@ +import request from '@/utils/request' + +// 获取企业微信客户列表 +export function qywxCustomerLists(params: any) { + return request.get({ url: '/qywx.customer/lists', params }) +} + +// 同步企业微信客户 +export function qywxCustomerSync() { + return request.post({ url: '/qywx.customer/sync' }) +} + +// 获取统计信息 +export function qywxCustomerStats() { + return request.get({ url: '/qywx.customer/stats' }) +} + +// 获取同步设置 +export function qywxSyncSettingsGet() { + return request.get({ url: '/qywx.customer/getSyncSettings' }) +} + +// 保存同步设置 +export function qywxSyncSettingsSave(params: any) { + return request.post({ url: '/qywx.customer/saveSyncSettings', params }) +} diff --git a/admin/src/api/tcm.ts b/admin/src/api/tcm.ts index 8666f6a5..24c6490e 100644 --- a/admin/src/api/tcm.ts +++ b/admin/src/api/tcm.ts @@ -243,7 +243,7 @@ export function prescriptionOrderLists(params: any) { return request.get({ url: '/tcm.prescriptionOrder/lists', params }) } -/** 诊单下可关联的已支付 zyt_order(已占用且未撤回的会排除;编辑时传 prescription_order_id 保留当前单已选) */ +/** 诊单下可关联的支付单(待支付/已支付的 zyt_order,已占用且未撤回的会排除;编辑时传 prescription_order_id 保留当前单已选) */ export function prescriptionOrderPaidPayOrders(params: { diagnosis_id: number prescription_order_id?: number @@ -313,6 +313,14 @@ export function prescriptionOrderAddPayOrder(params: { return request.post({ url: '/tcm.prescriptionOrder/addPayOrder', params }) } +/** 为「已发货」订单关联已有支付单并重置支付审核为待审核 */ +export function prescriptionOrderLinkPayOrder(params: { + id: number + pay_order_id: number +}) { + return request.post({ url: '/tcm.prescriptionOrder/linkPayOrder', params }) +} + /** 将「已发货」且支付审核通过的订单标记为「已完成」 */ export function prescriptionOrderComplete(params: { id: number }) { return request.post({ url: '/tcm.prescriptionOrder/complete', params }) diff --git a/admin/src/components/chat-dialog/index.vue b/admin/src/components/chat-dialog/index.vue index 0ee0a4dd..c94a18d5 100644 --- a/admin/src/components/chat-dialog/index.vue +++ b/admin/src/components/chat-dialog/index.vue @@ -11,7 +11,7 @@ class="chat-window-header" @mousedown="onHeaderMouseDown" > - 与 {{ patientName }} 聊天 + 与 {{ patientName }} 通讯
@@ -74,7 +74,8 @@
): FriendlyParse { - const callType = inner.call_type + const callType = inner.call_type ?? inner.callType ?? 2 const callTypeLabel = - callType === 2 ? '视频通话' : callType === 1 ? '语音通话' : '通话' - const cmd = String(inner.cmd || '') + callType === 2 ? '视频' : callType === 1 ? '语音通话' : '通话' + + let cmd = String(inner.cmd || inner.action || '').toLowerCase() + if (!cmd) { + const at = Number(inner.actionType ?? inner.action_type ?? inner.call_action) + if (at === 1) cmd = 'invite' + else if (at === 2) cmd = 'cancel' + else if (at === 3) cmd = 'accept' + else if (at === 4) cmd = 'reject' + else if (at === 5) cmd = 'timeout' + else if (at === 6) cmd = 'hangup' + else if (at === 7) cmd = 'linebusy' + } + const cmdMap: Record = { hangup: '已结束', invite: '发起通话', linebusy: '对方忙线', - cancel: '已取消', - reject: '已拒绝', + cancel: '已取消呼叫', + reject: '已拒绝接听', accept: '已接听', timeout: '无人接听' } const action = cmdMap[cmd] || (cmd ? `状态:${cmd}` : '') - const main = action ? `${callTypeLabel} · ${action}` : callTypeLabel + + // 如果有通话时长,追加显示 + const duration = Number(inner.duration ?? inner.call_duration) + const durationText = (duration > 0 && (action === '已结束' || action === '已挂断')) ? ` (${Math.floor(duration / 60)}分${duration % 60}秒)` : '' + + const main = action ? `${callTypeLabel} · ${action}${durationText}` : callTypeLabel const parts: string[] = [] - if (inner.inviter) parts.push(`发起方 ${inner.inviter}`) - if (inner.invitee) parts.push(`对方 ${inner.invitee}`) - if (inner.groupID) parts.push(`群组 ${inner.groupID}`) + //if (inner.inviter) parts.push(`发起方: ${inner.inviter}`) + if (inner.invitee) parts.push(`对方: ${inner.invitee}`) + if (inner.groupID) parts.push(`群组: ${inner.groupID}`) + + // 强制输出兜底调试信息(仅未识别到 action 时触发) + if (!action) { + const dbg = JSON.stringify(inner).replace(/"/g, '') + parts.push(`(原始参数: ${dbg})`) + } + return { main, sub: parts.length ? parts.join(',') : undefined, @@ -83,19 +107,18 @@ export function parseImBusinessPayload(raw: string): FriendlyParse | null { return { main: '问诊已完成', sub: formatBizTime(o.time), tag: '诊室' } } - if (o.businessID === 1 || o.businessID === '1') { + if (o.businessID === 1 || o.businessID === '1' || o.businessID === 'av_call') { let inner: unknown = o.data if (typeof inner === 'string') { try { inner = JSON.parse(inner) } catch { - return { main: '通话信令', sub: '内层数据解析失败', tag: '通话' } + // fallback } } - if (inner && typeof inner === 'object') { - return parseRtcInner(inner as Record) - } - return null + // 深远兼容:合并外层字段和 data 层字段,避免各端版本取参有遗漏 + const merged = { ...o, ...(typeof inner === 'object' && inner ? inner : {}) } + return parseRtcInner(merged as Record) } if (o.businessID === 'rtc_call' || o.cmd) { diff --git a/admin/src/views/consumer/prescription/index.vue b/admin/src/views/consumer/prescription/index.vue index 6cfe555e..2115ff1f 100644 --- a/admin/src/views/consumer/prescription/index.vue +++ b/admin/src/views/consumer/prescription/index.vue @@ -160,6 +160,11 @@
{{ row.prescription_date || '—' }}
+ + + @@ -941,7 +1214,9 @@ import { prescriptionOrderAddPayOrder, prescriptionOrderComplete, prescriptionOrderRevokeRxAudit, - prescriptionOrderRevokePayAudit + prescriptionOrderRevokePayAudit, + prescriptionOrderLinkPayOrder, + prescriptionDetail } from '@/api/tcm' import { usePaging } from '@/hooks/usePaging' import feedback from '@/utils/feedback' @@ -1023,7 +1298,7 @@ function auditTagType(s: number | undefined): 'success' | 'warning' | 'danger' | function fulfillmentText(s: number | undefined) { const m: Record = { 1: '待双审通过', - 2: '履约中', + 2: '待发货', 3: '已完成', 4: '已取消', 5: '已发货' @@ -1108,13 +1383,13 @@ function canWithdrawRow(row: { fulfillment_status?: number }) { return Number(row.fulfillment_status) === 1 } -function canShipRow(row: { fulfillment_status?: number; tracking_number?: unknown }) { - // 履约中(2) 且有快递单号时,可确认发货 - return Number(row.fulfillment_status) === 2 && String(row.tracking_number || '').trim() !== '' +function canShipRow(row: { fulfillment_status?: number }) { + // 履约中(2) 可发货 + return Number(row.fulfillment_status) === 2 } function canAddPayOrderRow(row: { fulfillment_status?: number }) { - // 已发货(5) 状态可新增支付单 + // 已发货(5) 状态可补齐支付单 return Number(row.fulfillment_status) === 5 } @@ -1124,9 +1399,8 @@ function canCompleteRow(row: { fulfillment_status?: number; payment_slip_audit_s } function canQuickTrackRow(row: { fulfillment_status?: number }) { - // 待双审通过(1)、履约中(2)、已发货(5) 均可修改快递单号 - const fs = Number(row.fulfillment_status) - return fs === 1 || fs === 2 || fs === 5 + // 已发货(5) 后如需修改快递信息可单独使用此功能 + return Number(row.fulfillment_status) === 5 } function orderStatusText(s: number | undefined) { @@ -1134,7 +1408,8 @@ function orderStatusText(s: number | undefined) { 1: '待支付', 2: '已支付', 3: '已取消', - 4: '已退款' + 4: '已退款', + 5:'待审核' } return m[Number(s)] ?? '—' } @@ -1143,6 +1418,55 @@ const detailVisible = ref(false) const detailLoading = ref(false) const detailData = ref | null>(null) +// --- 订单可视化审批履约流程逻辑 --- +const workflowActiveStep = computed(() => { + if (!detailData.value) return 0 + const fs = Number(detailData.value.fulfillment_status) + const rx = Number(detailData.value.prescription_audit_status) + const pay = Number(detailData.value.payment_slip_audit_status) + + if (fs === 3) return 5 // 订单完成 + if (fs === 5) return 4 // 已发货 + + if (fs === 4) { // 已取消 + if (rx === 2) return 1 + if (pay === 2) return 2 + return 3 + } + + if (fs === 2) return 3 // 履约中 + + // fs === 1 (待双审通过) + if (rx === 0 || rx === 2) return 1 + if (pay === 0 || pay === 2) return 2 + return 3 +}) + +const workflowRxStatus = computed(() => { + const rx = Number(detailData.value?.prescription_audit_status) + if (rx === 1) return 'success' + if (rx === 2) return 'error' + if (workflowActiveStep.value === 1) return 'process' + return 'wait' +}) + +const workflowPayStatus = computed(() => { + const pay = Number(detailData.value?.payment_slip_audit_status) + if (pay === 1) return 'success' + if (pay === 2) return 'error' + if (workflowActiveStep.value === 2) return 'process' + return 'wait' +}) + +const workflowFulfillStatus = computed(() => { + const fs = Number(detailData.value?.fulfillment_status) + if (fs === 3 || fs === 5) return 'success' + if (fs === 4) return 'error' + if (workflowActiveStep.value === 3) return 'process' + return 'wait' +}) +// --- end 可视化流程逻辑 --- + const detailLinkedPayOrders = computed(() => { const d = detailData.value if (!d) return [] @@ -1150,6 +1474,29 @@ const detailLinkedPayOrders = computed(() => { return Array.isArray(arr) ? arr : [] }) +const detailUnlinkedPayOrders = ref([]) + +async function loadDetailUnlinkedPayOrders(diagnosisId: number, prescriptionOrderId: number, linkedIds: number[]) { + if (!diagnosisId) { + detailUnlinkedPayOrders.value = [] + return + } + try { + const res: any = await prescriptionOrderPaidPayOrders({ + diagnosis_id: diagnosisId, + prescription_order_id: prescriptionOrderId + }) + const lists = res?.lists ?? res?.data?.lists ?? [] + // 过滤掉已关联的支付单 + const filtered = Array.isArray(lists) + ? lists.filter((item: any) => !linkedIds.includes(item.id)) + : [] + detailUnlinkedPayOrders.value = filtered + } catch { + detailUnlinkedPayOrders.value = [] + } +} + function normalizeSlipHerbs(raw: unknown): Array<{ name: string; dosage: number }> { if (!raw) return [] if (Array.isArray(raw)) { @@ -1224,7 +1571,7 @@ function logActionText(act: string) { fill_tracking: '填快递单', ship: '确认发货', withdraw: '撤销', - add_pay_order: '新增支付单', + add_pay_order: '补齐支付单', complete: '完成订单' } return m[act] || act @@ -1277,6 +1624,7 @@ async function fetchLogisticsTrace() { async function openDetail(id: number) { // 修复 Bug:显式彻底清空缓存,防止前一次弹窗的数据残留 detailData.value = null + detailUnlinkedPayOrders.value = [] logisticsTracePayload.value = null detailLogisticsExpress.value = 'auto' detailLogs.value = [] @@ -1293,6 +1641,13 @@ async function openDetail(id: number) { fetchLogisticsTrace() } fetchLogs(id) + + // 加载未关联的支付单 + const diagId = d.diagnosis_id + const linkedIds = d.pay_order_ids || [] + if (diagId) { + void loadDetailUnlinkedPayOrders(diagId, id, linkedIds) + } } } catch (e: any) { feedback.msgError(e?.message || '加载失败') @@ -1500,6 +1855,7 @@ async function submitEdit() { const auditVisible = ref(false) const auditKind = ref<'prescription' | 'payment'>('prescription') const auditTargetId = ref(0) +const auditRowData = ref(null) const auditRemark = ref('') const auditSubmitting = ref(false) @@ -1510,6 +1866,7 @@ const auditTitle = computed(() => function openAudit(row: { id: number }, kind: 'prescription' | 'payment') { auditKind.value = kind auditTargetId.value = row.id + auditRowData.value = row auditRemark.value = '' auditVisible.value = true } @@ -1694,18 +2051,24 @@ async function submitShip() { } } -// ─── 新增支付单(已发货状态)──────────────────────────────────────────── +// ─── 补齐支付单(已发货状态)──────────────────────────────────────────── const addPayOrderVisible = ref(false) const addPayOrderSaving = ref(false) const addPayOrderRowId = ref(0) const addPayOrderFormRef = ref() +const addPayOrderAvailableLoading = ref(false) +const addPayOrderAvailableList = ref([]) const addPayOrderForm = reactive({ + add_mode: 'create' as 'create' | 'link', order_type: 3, pay_amount: undefined as number | undefined, - pay_remark: '' + pay_remark: '', + link_pay_order_ids: [] as number[], + completion_request: 0 }) const addPayOrderRules: FormRules = { + add_mode: [{ required: true, message: '请选择添加方式', trigger: 'change' }], order_type: [{ required: true, message: '请选择费用类别', trigger: 'change' }], pay_amount: [ { required: true, message: '请输入支付金额', trigger: 'blur' }, @@ -1720,15 +2083,63 @@ const addPayOrderRules: FormRules = { }, trigger: 'blur' } + ], + link_pay_order_ids: [ + { + required: true, + message: '请至少选择一个支付单', + trigger: 'change', + validator: (_rule, v, cb) => { + if (!Array.isArray(v) || v.length === 0) { + cb(new Error('请至少选择一个支付单')) + } else { + cb() + } + } + } ] } -function openAddPayOrder(row: { id: number }) { +async function loadAddPayOrderAvailable(diagnosisId: number, prescriptionOrderId: number, currentLinkedIds: number[] = []) { + if (!diagnosisId) { + addPayOrderAvailableList.value = [] + return + } + try { + addPayOrderAvailableLoading.value = true + const res: any = await prescriptionOrderPaidPayOrders({ + diagnosis_id: diagnosisId, + prescription_order_id: prescriptionOrderId + }) + const lists = res?.lists ?? res?.data?.lists ?? [] + // 过滤掉当前订单已关联的支付单 + const filtered = Array.isArray(lists) + ? lists.filter((item: any) => !currentLinkedIds.includes(item.id)) + : [] + addPayOrderAvailableList.value = filtered + } catch { + addPayOrderAvailableList.value = [] + } finally { + addPayOrderAvailableLoading.value = false + } +} + +function openAddPayOrder(row: { id: number; diagnosis_id?: number; pay_order_ids?: number[] }) { addPayOrderRowId.value = row.id + addPayOrderForm.add_mode = 'create' addPayOrderForm.order_type = 3 addPayOrderForm.pay_amount = undefined addPayOrderForm.pay_remark = '' + addPayOrderForm.link_pay_order_ids = [] + addPayOrderForm.completion_request = 0 addPayOrderVisible.value = true + + // 加载可用支付单列表,排除当前订单已关联的 + const diagId = row.diagnosis_id || detailData.value?.diagnosis_id + const currentLinkedIds = row.pay_order_ids || detailData.value?.pay_order_ids || [] + if (diagId) { + void loadAddPayOrderAvailable(diagId, row.id, currentLinkedIds) + } } async function submitAddPayOrder() { @@ -1736,13 +2147,32 @@ async function submitAddPayOrder() { await addPayOrderFormRef.value.validate() addPayOrderSaving.value = true try { - await prescriptionOrderAddPayOrder({ - id: addPayOrderRowId.value, - order_type: addPayOrderForm.order_type, - pay_amount: addPayOrderForm.pay_amount!, - pay_remark: addPayOrderForm.pay_remark || '' - }) - feedback.msgSuccess('支付单已新增,请等待审核') + if (addPayOrderForm.add_mode === 'create') { + // 手动创建新支付单 + await prescriptionOrderAddPayOrder({ + id: addPayOrderRowId.value, + order_type: addPayOrderForm.order_type, + pay_amount: addPayOrderForm.pay_amount!, + pay_remark: addPayOrderForm.pay_remark || '', + completion_request: addPayOrderForm.completion_request + }) + feedback.msgSuccess('支付单已新增,请等待审核') + } else { + // 关联已有支付单(支持多个) + const payOrderIds = addPayOrderForm.link_pay_order_ids + if (payOrderIds.length === 0) { + feedback.msgError('请至少选择一个支付单') + return + } + + // 批量关联支付单 + await prescriptionOrderLinkPayOrder({ + id: addPayOrderRowId.value, + pay_order_ids: payOrderIds, + completion_request: addPayOrderForm.completion_request + }) + feedback.msgSuccess(`已关联 ${payOrderIds.length} 个支付单,请等待审核`) + } addPayOrderVisible.value = false getLists() // 若详情 Drawer 打开,同步刷新 @@ -1834,6 +2264,72 @@ async function confirmRevokePayAudit(row: { id: number }) { } } } + +// ─── 查看处方详情(处方单样式)──────────────────────────────────────────── +/** 处方笺抬头/页脚(与诊间打印一致) */ +const SLIP_HOSPITAL_NAME = '成都双流甄养堂互联网医院' +const SLIP_COMPANY_LINE = '成都双流甄养堂互联网医院有限公司 联系方式:4001667339' +const SLIP_ADDRESS_LINE = '地址:四川省成都市双流区黄甲街道黄龙大道二段280号' + +const prescriptionViewVisible = ref(false) +const prescriptionViewLoading = ref(false) +const prescriptionViewData = ref(null) + +// 处方单辅助函数 +const slipHerbsList = computed(() => { + const h = prescriptionViewData.value?.herbs + return Array.isArray(h) ? h : [] +}) + +const slipDietaryText = computed(() => { + const d = prescriptionViewData.value?.dietary_taboo + if (Array.isArray(d)) return d.filter(Boolean).join('、') + if (typeof d === 'string' && d.trim()) return d.trim() + return '' +}) + +function formatAuditTime(ts: number | null | undefined) { + if (!ts) return '' + const d = new Date(ts * 1000) + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}` +} + +function formatVoidTime(ts: number | null | undefined) { + if (!ts) return '—' + return formatAuditTime(ts) +} + +function slipGenderLabel(g: unknown) { + if (g === 1 || g === '1') return '男' + if (g === 0 || g === '0') return '女' + return '—' +} + +function slipAgeText(age: unknown) { + if (age === null || age === undefined || age === '') return '—' + return `${age}岁` +} + +async function openPrescriptionView(row: any) { + if (!row.prescription_id) { + feedback.msgWarning('该订单未关联处方') + return + } + + prescriptionViewVisible.value = true + prescriptionViewLoading.value = true + prescriptionViewData.value = null + + try { + const res: any = await prescriptionDetail({ id: row.prescription_id }) + prescriptionViewData.value = res?.data ?? res ?? null + } catch (e: any) { + feedback.msgError(e?.message || '加载处方详情失败') + prescriptionViewVisible.value = false + } finally { + prescriptionViewLoading.value = false + } +} diff --git a/admin/src/views/doctor/tongji.vue b/admin/src/views/doctor/tongji.vue index d486d1af..10955978 100644 --- a/admin/src/views/doctor/tongji.vue +++ b/admin/src/views/doctor/tongji.vue @@ -1,7 +1,12 @@ @@ -213,6 +192,17 @@ const getStatistics = async () => { const res = await getDoctorStatistics(params) statisticsList.value = res.lists || [] + + // 为每个医生添加必要的属性 + statisticsList.value.forEach(item => { + // 确保数据结构完整 + if (!item.dept_status_details) { + item.dept_status_details = [] + } + if (!item.channel_status_details) { + item.channel_status_details = [] + } + }) } catch (error: any) { console.error('获取统计数据错误:', error) ElMessage.error(error.msg || '获取统计数据失败') @@ -234,23 +224,38 @@ const handleReset = () => { getStatistics() } -const getCompletionRateClass = (rate: number) => { - if (rate >= 80) return 'text-green-600 font-semibold' - if (rate >= 60) return 'text-blue-600 font-semibold' - if (rate >= 40) return 'text-orange-600 font-semibold' - return 'text-red-600 font-semibold' +// 计算部门总数量 +const getDeptTotalCount = (dept: any) => { + if (!dept.statuses) return 0 + return Object.values(dept.statuses).reduce((total: number, count: number) => total + count, 0) } -// 解析部门统计字符串为数组 -const parseDeptStats = (stats: string) => { - if (!stats || stats === '未分配') return [] - return stats.split(' ').filter(item => item.trim()) +// 计算渠道总数量 +const getChannelTotalCount = (channel: any) => { + if (!channel.statuses) return 0 + return Object.values(channel.statuses).reduce((total: number, count: number) => total + count, 0) } -// 解析渠道统计字符串为数组 -const parseChannelStats = (stats: string) => { - if (!stats || stats === '未设置') return [] - return stats.split(' ').filter(item => item.trim()) +// 获取状态名称 +const getStatusName = (status: string | number) => { + const statusMap: Record = { + 1: '已挂号', + 2: '已取消', + 3: '已完成', + 4: '已过号' + } + return statusMap[status] || `状态${status}` +} + +// 获取状态标签类型 +const getStatusType = (status: string | number) => { + const typeMap: Record = { + 1: 'info', + 2: 'danger', + 3: 'success', + 4: 'warning' + } + return typeMap[status] || 'default' } onMounted(() => { @@ -262,23 +267,155 @@ onMounted(() => { diff --git a/admin/src/views/fans/qywx.vue b/admin/src/views/fans/qywx.vue new file mode 100644 index 00000000..2d8c6d91 --- /dev/null +++ b/admin/src/views/fans/qywx.vue @@ -0,0 +1,347 @@ + + + + + diff --git a/admin/src/views/order/index.vue b/admin/src/views/order/index.vue index c3b2cb3b..35151d54 100644 --- a/admin/src/views/order/index.vue +++ b/admin/src/views/order/index.vue @@ -36,6 +36,7 @@ + @@ -808,7 +809,8 @@ const getStatusText = (status: number) => { 1: '待支付', 2: '已支付', 3: '已取消', - 4: '已退款' + 4: '已退款', + 5: '待审核' } return statusMap[status] || '未知' } @@ -819,7 +821,8 @@ const getStatusTag = (status: number): 'warning' | 'success' | 'info' | 'danger' 1: 'warning', 2: 'success', 3: 'info', - 4: 'danger' + 4: 'danger', + 5: 'warning' } return tagMap[status] || 'info' } diff --git a/admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue b/admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue index 0e292908..f083d9c7 100644 --- a/admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue +++ b/admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue @@ -25,6 +25,12 @@ {{ row.call_type === 1 ? '语音' : '视频' }} + + + - + @@ -72,14 +72,14 @@ - + - + @@ -97,9 +97,9 @@ - + - +