From c15c348b7a5fe162bc0b55a707c200b3cd9472aa Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 21 Apr 2026 09:49:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ADD_DOSE_UNIT_TO_ORDER_SUMMARY.md | 75 -- ADD_MISSING_FIELDS.sql | 26 - APPOINTMENT_BUTTON_DISPLAY_FIX.md | 208 --- APPOINTMENT_INDEX_OPTIMIZATION.md | 251 ---- BUG_FIXES_SUMMARY.md | 176 --- CHECK_DOSAGE_FIELDS.md | 91 -- COMPLETION_REPORT.md | 594 --------- COMPLETION_REQUEST_FEATURE.md | 93 -- DEBUG_GUIDE.md | 277 ---- DELIVERY_PACKAGE.md | 361 ----- DIAGNOSIS_IMAGE_URL_PREFIX_FIX.md | 259 ---- DIAGNOSIS_LIST_SORTING.md | 90 -- DOCTOR_STATISTICS_GUIDE.md | 121 -- DYNAMIC_DOSE_COUNT_LABEL.md | 204 --- EXPRESS_TRACKING_AUTO_LOAD.md | 299 ----- EXPRESS_TRACKING_COMMAND_FIX.md | 199 --- EXPRESS_TRACKING_DATABASE_QUERY.md | 328 ----- EXPRESS_TRACKING_FIXES.md | 365 ----- EXPRESS_TRACKING_QUICKSTART.md | 335 ----- EXPRESS_TRACKING_SYNC_COMPLETE.md | 244 ---- EXPRESS_TRACKING_SYSTEM.md | 540 -------- FINAL_FIX.sql | 28 - FINAL_IMPLEMENTATION_SUMMARY.md | 321 ----- FIRST_LOGIN_PASSWORD_CHANGE.md | 142 -- GANCAO_API_FIX.md | 161 --- GANCAO_ARRAY_TO_STRING_FIX.md | 278 ---- GANCAO_CALLBACK_SETUP.md | 355 ----- GANCAO_FINAL_FIX.md | 318 ----- GANCAO_FIX_SUMMARY.md | 286 ---- GANCAO_PREVIEW_FEATURE.md | 212 --- GANCAO_SSL_FIX.md | 284 ---- GANCAO_UPLOAD_BUTTON_CONDITION.md | 169 --- IM_CHAT_MESSAGES_PERFORMANCE_FIX.md | 255 ---- INSTALLATION_CHECKLIST.md | 361 ----- INTERNAL_COST_PERMISSION_CONTROL.md | 142 -- JTEXPRESS_SUPPORT_ADDED.md | 251 ---- KUAIDI100_ACCOUNT_ISSUE.md | 281 ---- KUAIDI100_TROUBLESHOOTING.md | 251 ---- LOCAL_RECORDING_DEBUG_GUIDE.md | 358 ----- LOGISTICS_INTEGRATION_GUIDE.md | 266 ---- LOGISTICS_UPDATE_README.md | 308 ----- MANUAL_INSTALL_SQL.md | 101 -- MEDICINE_LIBRARY_GUIDE.md | 239 ---- ORDER_DETAIL_ADDRESS_DISPLAY_FIX.md | 167 --- ORDER_LIST_REGION_UPDATE.md | 191 --- ORDER_STATUS_TABS_UI.md | 299 ----- ORDER_SUPERVISOR_ROLE_CONFIG_FIX.md | 308 ----- PAYMENT_AUDIT_PERMISSION_FIX.md | 84 -- PHARMACIST_ROLE_PERMISSIONS.md | 358 ----- PINYIN_ABBREVIATION_GUIDE.md | 275 ---- PRESCRIPTION_APPROVED_LOCK.md | 384 ------ PRESCRIPTION_BUTTON_DISPLAY_CHECK.md | 204 --- PRESCRIPTION_DOSAGE_COMPLETE_SUMMARY.md | 283 ---- ...CRIPTION_DOSAGE_DATABASE_IMPLEMENTATION.md | 366 ----- PRESCRIPTION_DOSAGE_FEATURE.md | 307 ----- PRESCRIPTION_DUPLICATE_REQUEST_FIX.md | 321 ----- PRESCRIPTION_FIELDS_FIX_SUMMARY.md | 251 ---- PRESCRIPTION_GET_BY_APPOINTMENT_PERMISSION.md | 228 ---- PRESCRIPTION_IMPORT_FEATURE.md | 234 ---- PRESCRIPTION_LIBRARY_DEMO.md | 222 ---- PRESCRIPTION_LIBRARY_FILES.md | 244 ---- PRESCRIPTION_LIBRARY_README.md | 180 --- PRESCRIPTION_LIBRARY_SUMMARY.md | 236 ---- PRESCRIPTION_MANAGEMENT.md | 126 -- PRESCRIPTION_ORDER_MIN_AMOUNT_VALIDATION.md | 318 ----- PRESCRIPTION_ORDER_WITHDRAW_RESET_AUDIT.md | 387 ------ PRESCRIPTION_TEST_GUIDE.md | 255 ---- PRESCRIPTION_TIMES_PER_DAY_FEATURE.md | 199 --- PRESCRIPTION_VIEW_PERMISSION_FIX.md | 129 -- QUICK_FIX_REFERENCE.md | 83 -- QUICK_FIX_SQL.sql | 23 - QUICK_START_FIRST_LOGIN.md | 80 -- QUICK_START_PRESCRIPTION_LIBRARY.md | 135 -- QYWX_CUSTOMER_SYNC_FIX.md | 164 --- QYWX_CUSTOMER_SYNC_GUIDE.md | 246 ---- QYWX_IP_WHITELIST_GUIDE.md | 88 -- QYWX_SYNC_FIX.md | 108 -- README_PRESCRIPTION_LIBRARY.md | 377 ------ RECORDING_FIX_SUMMARY.md | 144 -- REGION_API_ENV_VARIABLE_UPDATE.md | 288 ---- REGION_API_PRODUCTION_FIX.md | 243 ---- REGION_DATABASE_SAVE_FIX.md | 170 --- REGION_DATA_SETUP.md | 72 - REGION_FIELD_SUBMISSION_FIX.md | 153 --- REGION_SELECTOR_AND_DOSAGE_FIX.md | 199 --- REGION_SELECTOR_FEATURE.md | 357 ----- REVOKE_PRESCRIPTION_AUDIT_FIX.md | 226 ---- SECURITY_FIX_SUMMARY.md | 278 ---- SERVICE_PACKAGE_MULTI_SELECT.md | 293 ---- SHIPPING_REGION_FEATURE.md | 179 --- TEST_CHECKLIST.md | 192 --- TEST_PRESCRIPTION_LIBRARY.sql | 32 - TRTC_COS_STORAGE_MIGRATION.md | 348 ----- UPDATE_OLD_DATA.sql | 18 - add_bags_per_dose_field.sql | 3 - add_completion_request_field.sql | 8 - add_dose_unit_to_prescription_order.sql | 13 - add_gancao_callback_fields.sql | 12 - add_is_paw_field.sql | 5 - add_prescription_complete_fields.sql | 20 - add_prescription_dosage_fields.sql | 15 - add_shipping_region_fields.sql | 5 - admin/src/api/qywx-msg.ts | 184 +++ admin/src/api/tcm.ts | 5 +- .../views/chat/components/MessageBubble.vue | 307 +++++ admin/src/views/chat/components/SendPanel.vue | 471 +++++++ admin/src/views/chat/index.vue | 1173 +++++++++++++++++ .../src/views/consumer/prescription/index.vue | 4 +- .../consumer/prescription/order_list.vue | 176 ++- check_and_add_indexes.sql | 208 --- clear_cache.php | 41 - deploy_gancao_fix.sh | 113 -- diagnose_gancao_payload.php | 216 --- install_appointment_system.bat | 123 -- install_appointment_system.sh | 100 -- install_doctor_fields.bat | 98 -- install_doctor_fields.sh | 100 -- install_express_tracking.bat | 74 -- install_express_tracking.sh | 142 -- install_first_login_password.bat | 59 - install_first_login_password.sh | 55 - install_medicine_library.bat | 55 - install_medicine_library.sh | 55 - install_medicine_library.sql | 28 - install_prescription_library.bat | 60 - install_prescription_library.sh | 58 - install_prescription_shared.bat | 48 - install_prescription_shared.sh | 46 - install_video_call.bat | 179 --- install_video_call.sh | 138 -- optimize_appointment_indexes.sql | 76 -- .../controller/qywx/MessageController.php | 171 +++ .../tcm/PrescriptionOrderController.php | 1 + .../adminapi/lists/qywx/MsgArchiveLists.php | 159 +++ .../adminapi/lists/qywx/MsgSendTaskLists.php | 33 + .../adminapi/lists/qywx/MsgSessionLists.php | 109 ++ .../app/adminapi/logic/order/OrderLogic.php | 66 +- .../app/adminapi/logic/qywx/CustomerLogic.php | 31 +- .../app/adminapi/logic/qywx/MessageLogic.php | 291 ++++ .../logic/tcm/PrescriptionOrderLogic.php | 15 +- .../validate/qywx/MessageValidate.php | 37 + .../QywxExternalContactCallbackController.php | 44 +- server/app/command/QywxSyncMsgArchive.php | 115 ++ server/app/common/model/QywxMsgArchive.php | 46 + .../app/common/model/QywxMsgArchiveCursor.php | 13 + .../app/common/model/QywxMsgArchiveMedia.php | 15 + server/app/common/model/QywxMsgSendTask.php | 78 ++ server/app/common/model/QywxMsgSession.php | 14 + .../service/gancao/GancaoOpenApiTransport.php | 33 +- .../service/wechat/QywxMsgArchiveService.php | 516 ++++++++ .../service/wechat/WeComFinanceSdkClient.php | 529 ++++++++ .../service/wechat/WechatWorkService.php | 295 ++++- server/config/console.php | 2 + server/config/gancao_scm.php | 8 +- server/config/pay.php | 30 + server/config/project.php | 10 +- .../2026_04_20_create_qywx_msg_tables.sql | 105 ++ server/docs/qywx-im-deploy.md | 98 ++ .../{403-zYjP504q.js => 403-BWTSUfPO.js} | 2 +- .../{404-TIw8IuSl.js => 404-D83BldNg.js} | 2 +- ...-vue-jYCthPJY.js => icons-vue-l4gXVnmh.js} | 2 +- ....js => AppointmentRecordPanel-k-5vKMwK.js} | 2 +- ...CLUec2y6.js => AssignLogPanel-rAkQNyue.js} | 2 +- ...e_type_script_setup_true_lang-Dqu6qTHC.js} | 2 +- ...s => AssistantWatchCallDialog-D9lbdcoy.js} | 2 +- .../admin/assets/BloodRecordList-BZHp6lEo.js | 1 - .../admin/assets/BloodRecordList-CtBVm9Ip.js | 1 + .../admin/assets/CallRecordPanel-CmEgumhU.js | 1 + .../admin/assets/CallRecordPanel-qwv_1cNT.js | 1 - ...CM6Ngqwu.js => CaseRecordList-4Y4NF549.js} | 2 +- .../admin/assets/DietRecordList-BYmazEi2.js | 1 - .../admin/assets/DietRecordList-nS0ETSeF.js | 1 + .../assets/ExerciseRecordList-DiUvrfKf.js | 1 - .../assets/ExerciseRecordList-q1jhPP2F.js | 1 + .../assets/ImChatRecordPanel-CYw3Z4Ae.js | 1 + .../assets/ImChatRecordPanel-CkTDQwi-.js | 1 - .../admin/assets/MessageBubble-BwtdPM6K.css | 1 + .../admin/assets/MessageBubble-D8ipRsoE.js | 1 + .../public/admin/assets/SendPanel-CzJ2-W1X.js | 1 + .../admin/assets/SendPanel-DaGPWlQG.css | 1 + ...CKBT5Xk2.js => account-adjust-BV6Zwjav.js} | 2 +- ...e_type_script_setup_true_lang-BE3d2FB8.js} | 2 +- server/public/admin/assets/add-CRBC7FnZ.js | 1 + ...dd-nav-BwW8qh13.js => add-nav-CFbcQL7v.js} | 2 +- ...e_type_script_setup_true_lang-0VBUUpOA.js} | 2 +- server/public/admin/assets/add-zh8ACc0D.js | 1 - .../{admin-CKTixM2o.js => admin-DYennBuK.js} | 2 +- ...nt-CZWO4BPv.js => appointment-D9CSjS-B.js} | 2 +- ...rticle-DaMXj9hD.js => article-CVr2sQde.js} | 2 +- .../{attr-DpFSzX3Z.js => attr-0ndVXc_Y.js} | 2 +- .../{attr-OD0JhdIL.js => attr-BEpmh30I.js} | 2 +- .../{attr-1JMm7RD0.js => attr-C5dyU7KF.js} | 2 +- .../{attr-CzVBY5p6.js => attr-CXz4t9Mx.js} | 2 +- .../{attr-BARw9FRL.js => attr-DPgJhc_6.js} | 2 +- .../{attr-CECezgG5.js => attr-DZ6wkRAQ.js} | 2 +- .../{attr-DnMgNi3H.js => attr-REs4PTFC.js} | 2 +- .../{attr-B6ib2xhu.js => attr-e7GV3wI1.js} | 2 +- .../{attr-K3_g27L5.js => attr-rLcHMC7u.js} | 2 +- .../admin/assets/attr-setting-BfYPJgum.js | 1 - .../admin/assets/attr-setting-DVe1ZAii.js | 1 + ...e_type_script_setup_true_lang-Dgf-3PiC.js} | 2 +- ...e_type_script_setup_true_lang-BXMBQSFw.js} | 2 +- ...e_type_script_setup_true_lang-B__HpciW.js} | 2 +- ...e_type_script_setup_true_lang-Be2nmFCm.js} | 2 +- ...e_type_script_setup_true_lang-CW9idRxL.js} | 2 +- ...e_type_script_setup_true_lang-DNaDJS2k.js} | 2 +- ...e_type_script_setup_true_lang-Dp2PD0F6.js} | 2 +- ...e_type_script_setup_true_lang-DsRNG8AO.js} | 2 +- .../{auth-BAxiFNDK.js => auth-BDPcFaVz.js} | 2 +- ...ue_type_script_setup_true_lang-BflKoHU-.js | 1 - ...ue_type_script_setup_true_lang-DzYDHGm2.js | 1 + ...T6sXoX7.js => balance_details-tamuym7v.js} | 2 +- ...yqyyO1.js => bind-work-wechat-BELmCfra.js} | 2 +- .../{cache-CT_cccn9.js => cache-BDAyY3Gj.js} | 2 +- ...-YGCRF0.js => change-password-CrMDFfBc.js} | 2 +- .../{code-djxUtM4W.js => code-DuM6GmrV.js} | 2 +- ...w-BZSnDZgg.js => code-preview-Bmxa3pX9.js} | 2 +- ...e_type_script_setup_true_lang-gujKCv5T.js} | 2 +- ...{config-BvvCw9Na.js => config-BNyY-XxC.js} | 2 +- ...sumer-BTOcQp9Y.js => consumer-CQsI7n_P.js} | 2 +- ...ontent-Bwko0s3j.js => content-B5VuK7RB.js} | 2 +- ...ontent-BCOjmpzp.js => content-BDvcF9yO.js} | 2 +- ...ontent-Dp0yC2y8.js => content-BP4W-Rpp.js} | 2 +- ...ontent-3OpUaWtf.js => content-BqaFBvyA.js} | 2 +- ...ontent-Dtm7T4Lc.js => content-CG8HWzH9.js} | 2 +- ...ontent-C66DFEKF.js => content-CH7bY9Dg.js} | 2 +- ...ontent-cW6CVBV3.js => content-ClvUnAmZ.js} | 2 +- ...ontent-C-CnUgTr.js => content-CuMx5H4u.js} | 2 +- ...ontent-B9B8Apcq.js => content-D03hw7kt.js} | 2 +- ...ontent-DCwdWmv8.js => content-D10MyPmw.js} | 2 +- ...ontent-BhKrWJjv.js => content-D_nxNe3i.js} | 2 +- ...ontent-IzFO2da1.js => content-Hk52LDuL.js} | 2 +- ...ontent-CMRzAq2S.js => content-RLeeKghP.js} | 2 +- ...e_type_script_setup_true_lang-BRZYAhOX.js} | 2 +- ...e_type_script_setup_true_lang-Bqha-Grv.js} | 2 +- ...e_type_script_setup_true_lang-ByPSPrxz.js} | 2 +- ...e_type_script_setup_true_lang-CXXpePlK.js} | 2 +- ...e_type_script_setup_true_lang-DKaKmkDy.js} | 2 +- ...ble-BAxiPkev.js => data-table-07BfDyei.js} | 2 +- ...e_type_script_setup_true_lang-CcuLG-P3.js} | 2 +- ...ion-CXCDHeoq.js => decoration-DorZNrhC.js} | 2 +- ...DGr8eZBc.js => decoration-img-CKm1nxte.js} | 2 +- ...-CpRKTwhJ.js => default_reply-HgkNk_GE.js} | 2 +- ...ent-X4a55vKC.js => department-CdvmEXUZ.js} | 2 +- .../admin/assets/dept-tongji-BJC41MI1.js | 1 - .../admin/assets/dept-tongji-dcqynytH.js | 1 + ...{detail-B3n80IMC.js => detail-1wKi8CjE.js} | 2 +- ...{detail-ahQwEslI.js => detail-Gob8S3N6.js} | 2 +- ...e_type_script_setup_true_lang-CbQ-RTGD.js} | 2 +- .../{dict-cDrXJHQI.js => dict-Cc6F9r_C.js} | 2 +- ...{doctor-BeQqnq72.js => doctor-tJ7-dSBn.js} | 2 +- .../{edit-DaoYyYX9.js => edit-BHuRgMuf.js} | 2 +- .../{edit-C31rXLP5.js => edit-BIXH8xA5.js} | 2 +- .../{edit-aWTEHcPz.js => edit-BPqc5hru.js} | 2 +- .../{edit-DXS_TUId.js => edit-Bf84NgSl.js} | 2 +- .../{edit-BRx2sjUc.js => edit-BftCJT3z.js} | 2 +- .../{edit-BGelpCMV.js => edit-BrsmiNL6.js} | 2 +- .../{edit-CNfOdXw8.js => edit-BvduTqa9.js} | 2 +- .../{edit-Ce7wWSXo.js => edit-C80pkS3o.js} | 2 +- .../{edit-BAC1UYnW.js => edit-CBf6Hbf6.js} | 2 +- .../{edit-DGLvfwM3.js => edit-C_AgzCEg.js} | 2 +- .../{edit-CdZvEGPM.js => edit-CdqeMDSm.js} | 2 +- server/public/admin/assets/edit-CmZX_H0n.js | 1 - .../{edit-0Bt6-x-R.js => edit-CpF1mUvw.js} | 2 +- .../{edit-Ny9sXl-t.js => edit-CpaDSabc.js} | 2 +- server/public/admin/assets/edit-CpzmvM52.js | 1 + .../{edit-Bb9aZM6p.js => edit-DXPcoSF6.js} | 2 +- .../{edit-Q1mruGZq.js => edit-DYkO8I0k.js} | 2 +- .../{edit-BsoVdXqZ.js => edit-Do2l1Ya2.js} | 2 +- .../{edit-SJsL_xQb.js => edit-_FiQl505.js} | 2 +- .../{edit-CuR81NYk.js => edit-wliBrY8U.js} | 2 +- ...ue_type_script_setup_true_lang-2dTJQFQG.js | 1 + ...e_type_script_setup_true_lang-6r00PcnX.js} | 2 +- ...ue_type_script_setup_true_lang-BO9SbXln.js | 1 - ...e_type_script_setup_true_lang-CArqreN2.js} | 2 +- ...e_type_script_setup_true_lang-CF18SWBm.js} | 2 +- ...e_type_script_setup_true_lang-CVN9nZkD.js} | 2 +- ...e_type_script_setup_true_lang-Co88Jf3S.js} | 2 +- ...e_type_script_setup_true_lang-DpWVXJKY.js} | 2 +- ...e_type_script_setup_true_lang-Dy8C_4D8.js} | 2 +- ...e_type_script_setup_true_lang-Dywa3Hkm.js} | 2 +- ...e_type_script_setup_true_lang-gdixNGAM.js} | 2 +- ...e_name_articleColumnEdit_lang-CMQB-azs.js} | 2 +- ...s-6h1hTCrQ.js => element-plus-osWj3D3p.js} | 4 +- ...CYlJIMXk.css => element-plus-zNHqZp1Y.css} | 2 +- ...nt-DYX6YpWS.js => environment-fWztxACp.js} | 2 +- .../{error-BlU2BJ1p.js => error-Bx-pGU7q.js} | 2 +- .../{fans-CJtkF_LI.js => fans-cGgRMGM6.js} | 2 +- .../{file-Bl00fTDO.js => file-B0mYsl3b.js} | 2 +- .../{file-rhlCNPS0.js => file-BArbnVF4.js} | 2 +- ...{filing-CR17vjat.js => filing-hSVHUC2T.js} | 2 +- ...inance-C9v2y7Wt.js => finance-_Zy0NXqv.js} | 2 +- ...y-BpWSjbb2.js => follow_reply-Cq1QQpyh.js} | 2 +- ...e_type_script_setup_true_lang-C6pBtIyu.js} | 2 +- server/public/admin/assets/h5-6qbQ2CqL.js | 1 - .../assets/{h5-Di3_YAUv.js => h5-QqVM-oUm.js} | 2 +- server/public/admin/assets/h5-Zkr20Wt1.js | 1 + server/public/admin/assets/icon-CH7xzNdJ.js | 1 + server/public/admin/assets/icon-lYw5pLdE.js | 1 - server/public/admin/assets/index-55neFmjq.js | 2 - .../{index-DsSo_l55.js => index-9YImSnPn.js} | 2 +- .../{index-CCPwBqOQ.js => index-B0S-7rum.js} | 4 +- server/public/admin/assets/index-B1WRZwLH.js | 1 - server/public/admin/assets/index-B687Zw9l.js | 1 - .../{index-Ba5lM5yG.js => index-B6HDh3IU.js} | 2 +- .../{index-WiUh_w_G.js => index-B8cCNf64.js} | 2 +- server/public/admin/assets/index-B9vGwckb.js | 1 + server/public/admin/assets/index-BNTs4h_w.js | 1 + server/public/admin/assets/index-B_4R8cs4.js | 1 - .../{index-CNsnDN_w.js => index-Bd6RIUBW.js} | 2 +- server/public/admin/assets/index-BeI253Mt.js | 1 - .../{index-BArasJZ4.js => index-BjJKERC9.js} | 2 +- .../{index-BtynGDk7.js => index-BmXwPo0P.js} | 2 +- .../{index-BQIXL4qS.js => index-BxFitu_z.js} | 2 +- server/public/admin/assets/index-C11IbIoh.css | 1 - .../{index-Cl6K8CxX.js => index-C4Spimk6.js} | 2 +- .../{index-xkDxadic.js => index-C9b4Sag5.js} | 2 +- server/public/admin/assets/index-CA6za4YE.js | 1 - .../{index-BKDIL42L.js => index-CF_6tyUi.js} | 2 +- .../{index-CzFmbFBQ.js => index-CJKithYP.js} | 8 +- server/public/admin/assets/index-CMsGEvq_.css | 1 + .../{index-Bc1ewzr4.js => index-CQN7LSKe.js} | 2 +- server/public/admin/assets/index-Cim68r_S.js | 1 - .../{index-DpHv3G3S.js => index-CtPLEPGx.js} | 4 +- server/public/admin/assets/index-CtbhtIVL.js | 1 + server/public/admin/assets/index-CvLd_f9k.js | 1 + .../{index-D3ARV5b5.js => index-CyZZ81gQ.js} | 2 +- .../{index-yL8Xu_Q0.js => index-D-mHvQrV.js} | 2 +- server/public/admin/assets/index-D6IS__U-.js | 1 - .../{index-DLYusrMX.js => index-D7PWi55Q.js} | 2 +- .../{index-ChRu_rHZ.js => index-D7q8mwqM.js} | 2 +- server/public/admin/assets/index-DCrjgDth.js | 1 + .../{index-B397Wtat.js => index-DKaVkNDv.js} | 2 +- server/public/admin/assets/index-DVD5zwi1.js | 1 + .../{index-D3sMviNE.js => index-DXw-6Yt-.js} | 2 +- .../{index-CSs1DpQp.js => index-DbmS5QgH.js} | 2 +- server/public/admin/assets/index-Dm_WDutq.js | 1 + .../{index-D-TEjYz0.js => index-DpXgp0Jt.js} | 2 +- .../{index-DuHVr3-G.js => index-Ds6BcjTF.js} | 2 +- .../{index-CN39xsBn.js => index-NdNAeLzU.js} | 2 +- .../{index-DzjjpCg7.js => index-OOfj3k4H.js} | 2 +- .../{index-vledklw3.js => index-P75bTQgB.js} | 2 +- server/public/admin/assets/index-T5JxWg7E.js | 1 + server/public/admin/assets/index-WtXZKhZE.js | 1 + .../{index-BhIRJ4cH.js => index-b-zSWJml.js} | 2 +- .../{index-BFSqYBoL.js => index-bTzYy0gE.js} | 2 +- .../{index-CZCTajfc.js => index-iiFE26st.js} | 2 +- server/public/admin/assets/index-kH50yP0I.css | 1 + .../{index-fuo-gblw.js => index-kgKqtVrf.js} | 2 +- server/public/admin/assets/index-wAn-JtEX.js | 1 - server/public/admin/assets/index-xZXgRhuH.js | 2 + ...e_type_script_setup_true_lang-8jySDNH4.js} | 2 +- ...ue_type_script_setup_true_lang-BCTRSL78.js | 1 + ...e_type_script_setup_true_lang-BQXRM6_y.js} | 2 +- ...e_type_script_setup_true_lang-BRLwtJdB.js} | 2 +- ...ue_type_script_setup_true_lang-CGaXpMKH.js | 1 + ...ue_type_script_setup_true_lang-CLVBLrJv.js | 1 - ...ue_type_script_setup_true_lang-CLhPF1Ro.js | 1 - ...e_vue_type_style_index_0_lang-fYb7GwIh.js} | 2 +- ...ex_h5-PvEzFrU0.js => index_h5-CBPir90H.js} | 2 +- ...on-Uy3QgFMN.js => information-C_QP_KIG.js} | 2 +- .../public/admin/assets/journal-DPtWvD5V.js | 1 - .../public/admin/assets/journal-DVuou61R.js | 1 + ...-ChykV12D.js => keyword_reply-Jqw6pBWX.js} | 2 +- .../{link-CNpYvS0r.js => link-BwGD6shj.js} | 2 +- server/public/admin/assets/list-C8-XRWwZ.js | 2 + .../{list-B6s-9S91.js => list-CcIJIJLw.js} | 2 +- server/public/admin/assets/list-DIgPcNqy.js | 2 - .../{login-2R15073S.js => login-CMahIaRF.js} | 2 +- ...DSJ61FsQ.js => login_register-g74lF521.js} | 2 +- ...icine-DE-G9qYM.js => medicine-BYiH1_mI.js} | 2 +- .../public/admin/assets/medicine-BZuJMV9y.js | 1 - .../public/admin/assets/medicine-CBhmFlH1.js | 1 + .../{menu-NiEDDTAv.js => menu-C1llu_vR.js} | 2 +- .../{menu-BZ--F8lp.js => menu-CsOXi1od.js} | 2 +- .../{menu-DDPrAoWK.js => menu-lw9nZKd4.js} | 2 +- ...u-set-CMNjldC9.js => menu-set-DAdFhbM1.js} | 2 +- ...e_type_script_setup_true_lang-CDWwwb7g.js} | 2 +- ...essage-Clv9-mDb.js => message-CA2KRakP.js} | 2 +- ...e-kE7LFseL.js => mobile-style-BqJrKhzf.js} | 2 +- ...e_type_script_setup_true_lang-B-psZ_BW.js} | 2 +- ...a-attr-Cx-u1iTH.js => oa-attr-V_nEftdE.js} | 2 +- ...m-CNVT-s29.js => oa-menu-form-C2SOCQz9.js} | 2 +- ...x8g4A.js => oa-menu-form-edit-CCcyJ48A.js} | 2 +- ...e_type_script_setup_true_lang-CutmIyGM.js} | 2 +- ...e_type_script_setup_true_lang-t8TGc66l.js} | 2 +- ...phone-BR1ilG8r.js => oa-phone-ymXPS_p3.js} | 2 +- ...g-CSvvD6eT.js => open_setting-CdaAz6ND.js} | 2 +- .../{order-DxpelCR7.js => order-D4R5pbKT.js} | 2 +- .../admin/assets/order_list-0q-jl-2e.js | 1 + .../admin/assets/order_list-C4RHWSZE.js | 1 - .../admin/assets/order_list-PeU-7ru6.css | 1 + .../admin/assets/order_list-oIBW1jaW.css | 1 - ...rflow-DGzQ9nnb.js => overflow-KG9kluXc.js} | 2 +- ...{paiban-WNftey0d.js => paiban-DcPNxbY2.js} | 2 +- ...l-CT43CxGX.js => patient-call-Bulmp9id.js} | 2 +- .../{pay-BvrJOYi_.js => pay-BpKQLm-K.js} | 2 +- .../assets/{pc-kHeZeZWL.js => pc-C99NfL7y.js} | 2 +- ...ils-B8IbDum5.js => pc_details-B8WTfIle.js} | 2 +- .../{perm-NyMnMPw8.js => perm-C0HOCe9H.js} | 2 +- ...{picker-B6Dcn_V9.js => picker-CGqg4FNQ.js} | 2 +- ...{picker-RR6i8slw.js => picker-Der0ecbS.js} | 2 +- ...e_type_script_setup_true_lang-BEKUabE6.js} | 2 +- ...-C_Pe8f_-.js => popover_input-DNlaRJ9I.js} | 2 +- .../{post-B4qJ4BpN.js => post-BQqHbmMX.js} | 2 +- .../assets/prescription-drawer-Cw-gT-d5.js | 1 - .../assets/prescription-drawer-D8grpu2V.js | 1 + ...review-Dc92lEWh.js => preview-OS53Y7GY.js} | 2 +- ...-pc-BypmRL9U.js => preview-pc-BrkLkBy1.js} | 2 +- .../public/admin/assets/progress--ShEYBLn.js | 1 + .../public/admin/assets/progress-CFj8jg19.js | 1 - ...tocol-DtYWAkXb.js => protocol-DGrcv2_q.js} | 2 +- .../{qywx-Cg8yhQdu.js => qywx-CMKyj4C_.js} | 2 +- ...37RhQCY.js => recharge_record-C8JuP6km.js} | 2 +- ...log-lvbGypOd.js => refund-log-D56Zs4id.js} | 2 +- ...ue_type_script_setup_true_lang-D1NQV8R4.js | 1 + ...ue_type_script_setup_true_lang-DSWt8Prm.js | 1 - ...-Cti6jshY.js => refund_record-C7As0lPb.js} | 2 +- ...-BiDwQMKw.js => relations-add-dz_U1WZU.js} | 2 +- ...e_type_script_setup_true_lang-UzBvSLWh.js} | 2 +- ...text-EA2HMRhm.js => rich_text-Bo82DhL1.js} | 2 +- .../{role-DKAG1xTY.js => role-Dws5Gwe2.js} | 2 +- server/public/admin/assets/roster-CW2pW3MB.js | 1 - server/public/admin/assets/roster-T-A5MIRv.js | 1 + ...etting-BHfqR8Kw.js => setting-B87_MWw-.js} | 2 +- .../{setup-DkheUSiG.js => setup-2QtUVxxN.js} | 2 +- ...ics-BbrZUaXA.js => statistics-CZBtLQOm.js} | 2 +- .../{style-CkQvp6mm.js => style-BNglbWSG.js} | 2 +- ...{tabbar-DI2X_lfY.js => tabbar-Br2P4F36.js} | 2 +- .../{tcm-DompPo1O.js => tcm-7RUvcI90.js} | 2 +- ...r-B1mJ6xL0.js => theme-picker-DrVRk6fl.js} | 2 +- server/public/admin/assets/tongji-92lXx4M2.js | 1 - server/public/admin/assets/tongji-DmIV70Ur.js | 1 + ...{upload-D39d6sTW.js => upload-rNvMbhk7.js} | 2 +- ...CA4QiMnI.js => useDictOptions-BNfwjYNp.js} | 2 +- ...nuOa-Do4LEKMJ.js => useMenuOa-CiMZMkmv.js} | 2 +- .../{user-D_WetR3i.js => user-CTlHIfqI.js} | 2 +- .../{weapp-CSuNOJ5M.js => weapp-0h-epSYv.js} | 2 +- .../{weapp-mZDhTWBI.js => weapp-Bnzi0iXW.js} | 2 +- ...ebsite-Bj2jy9Q5.js => website-BKXRAVIa.js} | 2 +- .../{wx_oa-BZiZBWa_.js => wx_oa-qW170Twq.js} | 2 +- server/public/admin/index.html | 8 +- server/server.zip | Bin 58901371 -> 58955589 bytes sync_tracking_numbers.bat | 35 - sync_tracking_numbers.sh | 32 - test_first_login.sql | 23 - test_gancao_config.php | 193 --- test_gancao_ssl.php | 174 --- test_slot_generation.php | 107 -- 医生资质图片功能说明.md | 362 ----- 医生资质图片功能部署指南.md | 0 医生资质图片数据流转说明.md | 277 ---- 处方单字段修改说明.md | 166 --- 测试医生资质图片功能.md | 267 ---- 454 files changed, 5391 insertions(+), 25651 deletions(-) delete mode 100644 ADD_DOSE_UNIT_TO_ORDER_SUMMARY.md delete mode 100644 ADD_MISSING_FIELDS.sql delete mode 100644 APPOINTMENT_BUTTON_DISPLAY_FIX.md delete mode 100644 APPOINTMENT_INDEX_OPTIMIZATION.md delete mode 100644 BUG_FIXES_SUMMARY.md delete mode 100644 CHECK_DOSAGE_FIELDS.md delete mode 100644 COMPLETION_REPORT.md delete mode 100644 COMPLETION_REQUEST_FEATURE.md delete mode 100644 DEBUG_GUIDE.md delete mode 100644 DELIVERY_PACKAGE.md delete mode 100644 DIAGNOSIS_IMAGE_URL_PREFIX_FIX.md delete mode 100644 DIAGNOSIS_LIST_SORTING.md delete mode 100644 DOCTOR_STATISTICS_GUIDE.md delete mode 100644 DYNAMIC_DOSE_COUNT_LABEL.md delete mode 100644 EXPRESS_TRACKING_AUTO_LOAD.md delete mode 100644 EXPRESS_TRACKING_COMMAND_FIX.md delete mode 100644 EXPRESS_TRACKING_DATABASE_QUERY.md delete mode 100644 EXPRESS_TRACKING_FIXES.md delete mode 100644 EXPRESS_TRACKING_QUICKSTART.md delete mode 100644 EXPRESS_TRACKING_SYNC_COMPLETE.md delete mode 100644 EXPRESS_TRACKING_SYSTEM.md delete mode 100644 FINAL_FIX.sql delete mode 100644 FINAL_IMPLEMENTATION_SUMMARY.md delete mode 100644 FIRST_LOGIN_PASSWORD_CHANGE.md delete mode 100644 GANCAO_API_FIX.md delete mode 100644 GANCAO_ARRAY_TO_STRING_FIX.md delete mode 100644 GANCAO_CALLBACK_SETUP.md delete mode 100644 GANCAO_FINAL_FIX.md delete mode 100644 GANCAO_FIX_SUMMARY.md delete mode 100644 GANCAO_PREVIEW_FEATURE.md delete mode 100644 GANCAO_SSL_FIX.md delete mode 100644 GANCAO_UPLOAD_BUTTON_CONDITION.md delete mode 100644 IM_CHAT_MESSAGES_PERFORMANCE_FIX.md delete mode 100644 INSTALLATION_CHECKLIST.md delete mode 100644 INTERNAL_COST_PERMISSION_CONTROL.md delete mode 100644 JTEXPRESS_SUPPORT_ADDED.md delete mode 100644 KUAIDI100_ACCOUNT_ISSUE.md delete mode 100644 KUAIDI100_TROUBLESHOOTING.md delete mode 100644 LOCAL_RECORDING_DEBUG_GUIDE.md delete mode 100644 LOGISTICS_INTEGRATION_GUIDE.md delete mode 100644 LOGISTICS_UPDATE_README.md delete mode 100644 MANUAL_INSTALL_SQL.md delete mode 100644 MEDICINE_LIBRARY_GUIDE.md delete mode 100644 ORDER_DETAIL_ADDRESS_DISPLAY_FIX.md delete mode 100644 ORDER_LIST_REGION_UPDATE.md delete mode 100644 ORDER_STATUS_TABS_UI.md delete mode 100644 ORDER_SUPERVISOR_ROLE_CONFIG_FIX.md delete mode 100644 PAYMENT_AUDIT_PERMISSION_FIX.md delete mode 100644 PHARMACIST_ROLE_PERMISSIONS.md delete mode 100644 PINYIN_ABBREVIATION_GUIDE.md delete mode 100644 PRESCRIPTION_APPROVED_LOCK.md delete mode 100644 PRESCRIPTION_BUTTON_DISPLAY_CHECK.md delete mode 100644 PRESCRIPTION_DOSAGE_COMPLETE_SUMMARY.md delete mode 100644 PRESCRIPTION_DOSAGE_DATABASE_IMPLEMENTATION.md delete mode 100644 PRESCRIPTION_DOSAGE_FEATURE.md delete mode 100644 PRESCRIPTION_DUPLICATE_REQUEST_FIX.md delete mode 100644 PRESCRIPTION_FIELDS_FIX_SUMMARY.md delete mode 100644 PRESCRIPTION_GET_BY_APPOINTMENT_PERMISSION.md delete mode 100644 PRESCRIPTION_IMPORT_FEATURE.md delete mode 100644 PRESCRIPTION_LIBRARY_DEMO.md delete mode 100644 PRESCRIPTION_LIBRARY_FILES.md delete mode 100644 PRESCRIPTION_LIBRARY_README.md delete mode 100644 PRESCRIPTION_LIBRARY_SUMMARY.md delete mode 100644 PRESCRIPTION_MANAGEMENT.md delete mode 100644 PRESCRIPTION_ORDER_MIN_AMOUNT_VALIDATION.md delete mode 100644 PRESCRIPTION_ORDER_WITHDRAW_RESET_AUDIT.md delete mode 100644 PRESCRIPTION_TEST_GUIDE.md delete mode 100644 PRESCRIPTION_TIMES_PER_DAY_FEATURE.md delete mode 100644 PRESCRIPTION_VIEW_PERMISSION_FIX.md delete mode 100644 QUICK_FIX_REFERENCE.md delete mode 100644 QUICK_FIX_SQL.sql delete mode 100644 QUICK_START_FIRST_LOGIN.md delete mode 100644 QUICK_START_PRESCRIPTION_LIBRARY.md delete mode 100644 QYWX_CUSTOMER_SYNC_FIX.md delete mode 100644 QYWX_CUSTOMER_SYNC_GUIDE.md delete mode 100644 QYWX_IP_WHITELIST_GUIDE.md delete mode 100644 QYWX_SYNC_FIX.md delete mode 100644 README_PRESCRIPTION_LIBRARY.md delete mode 100644 RECORDING_FIX_SUMMARY.md delete mode 100644 REGION_API_ENV_VARIABLE_UPDATE.md delete mode 100644 REGION_API_PRODUCTION_FIX.md delete mode 100644 REGION_DATABASE_SAVE_FIX.md delete mode 100644 REGION_DATA_SETUP.md delete mode 100644 REGION_FIELD_SUBMISSION_FIX.md delete mode 100644 REGION_SELECTOR_AND_DOSAGE_FIX.md delete mode 100644 REGION_SELECTOR_FEATURE.md delete mode 100644 REVOKE_PRESCRIPTION_AUDIT_FIX.md delete mode 100644 SECURITY_FIX_SUMMARY.md delete mode 100644 SERVICE_PACKAGE_MULTI_SELECT.md delete mode 100644 SHIPPING_REGION_FEATURE.md delete mode 100644 TEST_CHECKLIST.md delete mode 100644 TEST_PRESCRIPTION_LIBRARY.sql delete mode 100644 TRTC_COS_STORAGE_MIGRATION.md delete mode 100644 UPDATE_OLD_DATA.sql delete mode 100644 add_bags_per_dose_field.sql delete mode 100644 add_completion_request_field.sql delete mode 100644 add_dose_unit_to_prescription_order.sql delete mode 100644 add_gancao_callback_fields.sql delete mode 100644 add_is_paw_field.sql delete mode 100644 add_prescription_complete_fields.sql delete mode 100644 add_prescription_dosage_fields.sql delete mode 100644 add_shipping_region_fields.sql create mode 100644 admin/src/api/qywx-msg.ts create mode 100644 admin/src/views/chat/components/MessageBubble.vue create mode 100644 admin/src/views/chat/components/SendPanel.vue create mode 100644 admin/src/views/chat/index.vue delete mode 100644 check_and_add_indexes.sql delete mode 100644 clear_cache.php delete mode 100644 deploy_gancao_fix.sh delete mode 100644 diagnose_gancao_payload.php delete mode 100644 install_appointment_system.bat delete mode 100644 install_appointment_system.sh delete mode 100644 install_doctor_fields.bat delete mode 100644 install_doctor_fields.sh delete mode 100644 install_express_tracking.bat delete mode 100644 install_express_tracking.sh delete mode 100644 install_first_login_password.bat delete mode 100644 install_first_login_password.sh delete mode 100644 install_medicine_library.bat delete mode 100644 install_medicine_library.sh delete mode 100644 install_medicine_library.sql delete mode 100644 install_prescription_library.bat delete mode 100644 install_prescription_library.sh delete mode 100644 install_prescription_shared.bat delete mode 100644 install_prescription_shared.sh delete mode 100644 install_video_call.bat delete mode 100644 install_video_call.sh delete mode 100644 optimize_appointment_indexes.sql create mode 100644 server/app/adminapi/controller/qywx/MessageController.php create mode 100644 server/app/adminapi/lists/qywx/MsgArchiveLists.php create mode 100644 server/app/adminapi/lists/qywx/MsgSendTaskLists.php create mode 100644 server/app/adminapi/lists/qywx/MsgSessionLists.php create mode 100644 server/app/adminapi/logic/qywx/MessageLogic.php create mode 100644 server/app/adminapi/validate/qywx/MessageValidate.php create mode 100644 server/app/command/QywxSyncMsgArchive.php create mode 100644 server/app/common/model/QywxMsgArchive.php create mode 100644 server/app/common/model/QywxMsgArchiveCursor.php create mode 100644 server/app/common/model/QywxMsgArchiveMedia.php create mode 100644 server/app/common/model/QywxMsgSendTask.php create mode 100644 server/app/common/model/QywxMsgSession.php create mode 100644 server/app/common/service/wechat/QywxMsgArchiveService.php create mode 100644 server/app/common/service/wechat/WeComFinanceSdkClient.php create mode 100644 server/database/migrations/2026_04_20_create_qywx_msg_tables.sql create mode 100644 server/docs/qywx-im-deploy.md rename server/public/admin/assets/{403-zYjP504q.js => 403-BWTSUfPO.js} (85%) rename server/public/admin/assets/{404-TIw8IuSl.js => 404-D83BldNg.js} (78%) rename server/public/admin/assets/@element-plus/{icons-vue-jYCthPJY.js => icons-vue-l4gXVnmh.js} (99%) rename server/public/admin/assets/{AppointmentRecordPanel-DujKSFG9.js => AppointmentRecordPanel-k-5vKMwK.js} (88%) rename server/public/admin/assets/{AssignLogPanel-CLUec2y6.js => AssignLogPanel-rAkQNyue.js} (81%) rename server/public/admin/assets/{AssignLogPanel.vue_vue_type_script_setup_true_lang-B51nxqq-.js => AssignLogPanel.vue_vue_type_script_setup_true_lang-Dqu6qTHC.js} (89%) rename server/public/admin/assets/{AssistantWatchCallDialog-rSsi47oR.js => AssistantWatchCallDialog-D9lbdcoy.js} (83%) delete mode 100644 server/public/admin/assets/BloodRecordList-BZHp6lEo.js create mode 100644 server/public/admin/assets/BloodRecordList-CtBVm9Ip.js create mode 100644 server/public/admin/assets/CallRecordPanel-CmEgumhU.js delete mode 100644 server/public/admin/assets/CallRecordPanel-qwv_1cNT.js rename server/public/admin/assets/{CaseRecordList-CM6Ngqwu.js => CaseRecordList-4Y4NF549.js} (74%) delete mode 100644 server/public/admin/assets/DietRecordList-BYmazEi2.js create mode 100644 server/public/admin/assets/DietRecordList-nS0ETSeF.js delete mode 100644 server/public/admin/assets/ExerciseRecordList-DiUvrfKf.js create mode 100644 server/public/admin/assets/ExerciseRecordList-q1jhPP2F.js create mode 100644 server/public/admin/assets/ImChatRecordPanel-CYw3Z4Ae.js delete mode 100644 server/public/admin/assets/ImChatRecordPanel-CkTDQwi-.js create mode 100644 server/public/admin/assets/MessageBubble-BwtdPM6K.css create mode 100644 server/public/admin/assets/MessageBubble-D8ipRsoE.js create mode 100644 server/public/admin/assets/SendPanel-CzJ2-W1X.js create mode 100644 server/public/admin/assets/SendPanel-DaGPWlQG.css rename server/public/admin/assets/{account-adjust-CKBT5Xk2.js => account-adjust-BV6Zwjav.js} (81%) rename server/public/admin/assets/{account-adjust.vue_vue_type_script_setup_true_lang-_b_PJdrZ.js => account-adjust.vue_vue_type_script_setup_true_lang-BE3d2FB8.js} (91%) create mode 100644 server/public/admin/assets/add-CRBC7FnZ.js rename server/public/admin/assets/{add-nav-BwW8qh13.js => add-nav-CFbcQL7v.js} (66%) rename server/public/admin/assets/{add-nav.vue_vue_type_script_setup_true_lang-CH3jNlCp.js => add-nav.vue_vue_type_script_setup_true_lang-0VBUUpOA.js} (90%) delete mode 100644 server/public/admin/assets/add-zh8ACc0D.js rename server/public/admin/assets/{admin-CKTixM2o.js => admin-DYennBuK.js} (84%) rename server/public/admin/assets/{appointment-CZWO4BPv.js => appointment-D9CSjS-B.js} (79%) rename server/public/admin/assets/{article-DaMXj9hD.js => article-CVr2sQde.js} (93%) rename server/public/admin/assets/{attr-DpFSzX3Z.js => attr-0ndVXc_Y.js} (64%) rename server/public/admin/assets/{attr-OD0JhdIL.js => attr-BEpmh30I.js} (63%) rename server/public/admin/assets/{attr-1JMm7RD0.js => attr-C5dyU7KF.js} (64%) rename server/public/admin/assets/{attr-CzVBY5p6.js => attr-CXz4t9Mx.js} (65%) rename server/public/admin/assets/{attr-BARw9FRL.js => attr-DPgJhc_6.js} (73%) rename server/public/admin/assets/{attr-CECezgG5.js => attr-DZ6wkRAQ.js} (90%) rename server/public/admin/assets/{attr-DnMgNi3H.js => attr-REs4PTFC.js} (63%) rename server/public/admin/assets/{attr-B6ib2xhu.js => attr-e7GV3wI1.js} (61%) rename server/public/admin/assets/{attr-K3_g27L5.js => attr-rLcHMC7u.js} (63%) delete mode 100644 server/public/admin/assets/attr-setting-BfYPJgum.js create mode 100644 server/public/admin/assets/attr-setting-DVe1ZAii.js rename server/public/admin/assets/{attr-setting.vue_vue_type_script_setup_true_lang-Dcouxod6.js => attr-setting.vue_vue_type_script_setup_true_lang-Dgf-3PiC.js} (89%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-Cwa8U5_P.js => attr.vue_vue_type_script_setup_true_lang-BXMBQSFw.js} (93%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-CMmNjPvE.js => attr.vue_vue_type_script_setup_true_lang-B__HpciW.js} (93%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-ZHFXoyNu.js => attr.vue_vue_type_script_setup_true_lang-Be2nmFCm.js} (93%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-DxClgwIr.js => attr.vue_vue_type_script_setup_true_lang-CW9idRxL.js} (93%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-Ca8elUNx.js => attr.vue_vue_type_script_setup_true_lang-DNaDJS2k.js} (92%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-DdzChMj0.js => attr.vue_vue_type_script_setup_true_lang-Dp2PD0F6.js} (94%) rename server/public/admin/assets/{attr.vue_vue_type_script_setup_true_lang-Byi24hV9.js => attr.vue_vue_type_script_setup_true_lang-DsRNG8AO.js} (92%) rename server/public/admin/assets/{auth-BAxiFNDK.js => auth-BDPcFaVz.js} (71%) delete mode 100644 server/public/admin/assets/auth.vue_vue_type_script_setup_true_lang-BflKoHU-.js create mode 100644 server/public/admin/assets/auth.vue_vue_type_script_setup_true_lang-DzYDHGm2.js rename server/public/admin/assets/{balance_details-BT6sXoX7.js => balance_details-tamuym7v.js} (90%) rename server/public/admin/assets/{bind-work-wechat-CvyqyyO1.js => bind-work-wechat-BELmCfra.js} (95%) rename server/public/admin/assets/{cache-CT_cccn9.js => cache-BDAyY3Gj.js} (83%) rename server/public/admin/assets/{change-password-B-YGCRF0.js => change-password-CrMDFfBc.js} (91%) rename server/public/admin/assets/{code-djxUtM4W.js => code-DuM6GmrV.js} (92%) rename server/public/admin/assets/{code-preview-BZSnDZgg.js => code-preview-Bmxa3pX9.js} (81%) rename server/public/admin/assets/{code-preview.vue_vue_type_script_setup_true_lang-oLdzHDvj.js => code-preview.vue_vue_type_script_setup_true_lang-gujKCv5T.js} (83%) rename server/public/admin/assets/{config-BvvCw9Na.js => config-BNyY-XxC.js} (92%) rename server/public/admin/assets/{consumer-BTOcQp9Y.js => consumer-CQsI7n_P.js} (81%) rename server/public/admin/assets/{content-Bwko0s3j.js => content-B5VuK7RB.js} (81%) rename server/public/admin/assets/{content-BCOjmpzp.js => content-BDvcF9yO.js} (90%) rename server/public/admin/assets/{content-Dp0yC2y8.js => content-BP4W-Rpp.js} (81%) rename server/public/admin/assets/{content-3OpUaWtf.js => content-BqaFBvyA.js} (88%) rename server/public/admin/assets/{content-Dtm7T4Lc.js => content-CG8HWzH9.js} (90%) rename server/public/admin/assets/{content-C66DFEKF.js => content-CH7bY9Dg.js} (81%) rename server/public/admin/assets/{content-cW6CVBV3.js => content-ClvUnAmZ.js} (90%) rename server/public/admin/assets/{content-C-CnUgTr.js => content-CuMx5H4u.js} (86%) rename server/public/admin/assets/{content-B9B8Apcq.js => content-D03hw7kt.js} (90%) rename server/public/admin/assets/{content-DCwdWmv8.js => content-D10MyPmw.js} (81%) rename server/public/admin/assets/{content-BhKrWJjv.js => content-D_nxNe3i.js} (88%) rename server/public/admin/assets/{content-IzFO2da1.js => content-Hk52LDuL.js} (66%) rename server/public/admin/assets/{content-CMRzAq2S.js => content-RLeeKghP.js} (89%) rename server/public/admin/assets/{content.vue_vue_type_script_setup_true_lang-Br4ZeErc.js => content.vue_vue_type_script_setup_true_lang-BRZYAhOX.js} (91%) rename server/public/admin/assets/{content.vue_vue_type_script_setup_true_lang-BIu3Xfg2.js => content.vue_vue_type_script_setup_true_lang-Bqha-Grv.js} (93%) rename server/public/admin/assets/{content.vue_vue_type_script_setup_true_lang-BaYIBtwJ.js => content.vue_vue_type_script_setup_true_lang-ByPSPrxz.js} (86%) rename server/public/admin/assets/{content.vue_vue_type_script_setup_true_lang-DNDbw7P9.js => content.vue_vue_type_script_setup_true_lang-CXXpePlK.js} (90%) rename server/public/admin/assets/{content.vue_vue_type_script_setup_true_lang-CpnarxEs.js => content.vue_vue_type_script_setup_true_lang-DKaKmkDy.js} (91%) rename server/public/admin/assets/{data-table-BAxiPkev.js => data-table-07BfDyei.js} (78%) rename server/public/admin/assets/{data-table.vue_vue_type_script_setup_true_lang-DsG_kSqH.js => data-table.vue_vue_type_script_setup_true_lang-CcuLG-P3.js} (87%) rename server/public/admin/assets/{decoration-CXCDHeoq.js => decoration-DorZNrhC.js} (87%) rename server/public/admin/assets/{decoration-img-DGr8eZBc.js => decoration-img-CKm1nxte.js} (90%) rename server/public/admin/assets/{default_reply-CpRKTwhJ.js => default_reply-HgkNk_GE.js} (89%) rename server/public/admin/assets/{department-X4a55vKC.js => department-CdvmEXUZ.js} (85%) delete mode 100644 server/public/admin/assets/dept-tongji-BJC41MI1.js create mode 100644 server/public/admin/assets/dept-tongji-dcqynytH.js rename server/public/admin/assets/{detail-B3n80IMC.js => detail-1wKi8CjE.js} (77%) rename server/public/admin/assets/{detail-ahQwEslI.js => detail-Gob8S3N6.js} (90%) rename server/public/admin/assets/{detail.vue_vue_type_script_setup_true_lang-DkfoXse9.js => detail.vue_vue_type_script_setup_true_lang-CbQ-RTGD.js} (79%) rename server/public/admin/assets/{dict-cDrXJHQI.js => dict-Cc6F9r_C.js} (92%) rename server/public/admin/assets/{doctor-BeQqnq72.js => doctor-tJ7-dSBn.js} (94%) rename server/public/admin/assets/{edit-DaoYyYX9.js => edit-BHuRgMuf.js} (72%) rename server/public/admin/assets/{edit-C31rXLP5.js => edit-BIXH8xA5.js} (72%) rename server/public/admin/assets/{edit-aWTEHcPz.js => edit-BPqc5hru.js} (72%) rename server/public/admin/assets/{edit-DXS_TUId.js => edit-Bf84NgSl.js} (87%) rename server/public/admin/assets/{edit-BRx2sjUc.js => edit-BftCJT3z.js} (85%) rename server/public/admin/assets/{edit-BGelpCMV.js => edit-BrsmiNL6.js} (80%) rename server/public/admin/assets/{edit-CNfOdXw8.js => edit-BvduTqa9.js} (85%) rename server/public/admin/assets/{edit-Ce7wWSXo.js => edit-C80pkS3o.js} (91%) rename server/public/admin/assets/{edit-BAC1UYnW.js => edit-CBf6Hbf6.js} (72%) rename server/public/admin/assets/{edit-DGLvfwM3.js => edit-C_AgzCEg.js} (72%) rename server/public/admin/assets/{edit-CdZvEGPM.js => edit-CdqeMDSm.js} (70%) delete mode 100644 server/public/admin/assets/edit-CmZX_H0n.js rename server/public/admin/assets/{edit-0Bt6-x-R.js => edit-CpF1mUvw.js} (78%) rename server/public/admin/assets/{edit-Ny9sXl-t.js => edit-CpaDSabc.js} (61%) create mode 100644 server/public/admin/assets/edit-CpzmvM52.js rename server/public/admin/assets/{edit-Bb9aZM6p.js => edit-DXPcoSF6.js} (80%) rename server/public/admin/assets/{edit-Q1mruGZq.js => edit-DYkO8I0k.js} (94%) rename server/public/admin/assets/{edit-BsoVdXqZ.js => edit-Do2l1Ya2.js} (78%) rename server/public/admin/assets/{edit-SJsL_xQb.js => edit-_FiQl505.js} (72%) rename server/public/admin/assets/{edit-CuR81NYk.js => edit-wliBrY8U.js} (92%) create mode 100644 server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-2dTJQFQG.js rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-B2Vsfowc.js => edit.vue_vue_type_script_setup_true_lang-6r00PcnX.js} (95%) delete mode 100644 server/public/admin/assets/edit.vue_vue_type_script_setup_true_lang-BO9SbXln.js rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-TOVsd0sj.js => edit.vue_vue_type_script_setup_true_lang-CArqreN2.js} (92%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-3SkTRodn.js => edit.vue_vue_type_script_setup_true_lang-CF18SWBm.js} (72%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-BeTv28bC.js => edit.vue_vue_type_script_setup_true_lang-CVN9nZkD.js} (91%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-C6Je9ija.js => edit.vue_vue_type_script_setup_true_lang-Co88Jf3S.js} (89%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-rHcS-Mro.js => edit.vue_vue_type_script_setup_true_lang-DpWVXJKY.js} (97%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-DYg1k2cJ.js => edit.vue_vue_type_script_setup_true_lang-Dy8C_4D8.js} (93%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-CyG6K9Qp.js => edit.vue_vue_type_script_setup_true_lang-Dywa3Hkm.js} (95%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_lang-2RHdTm0o.js => edit.vue_vue_type_script_setup_true_lang-gdixNGAM.js} (84%) rename server/public/admin/assets/{edit.vue_vue_type_script_setup_true_name_articleColumnEdit_lang-BCe1nUqH.js => edit.vue_vue_type_script_setup_true_name_articleColumnEdit_lang-CMQB-azs.js} (90%) rename server/public/admin/assets/{element-plus-6h1hTCrQ.js => element-plus-osWj3D3p.js} (99%) rename server/public/admin/assets/{element-plus-CYlJIMXk.css => element-plus-zNHqZp1Y.css} (87%) rename server/public/admin/assets/{environment-DYX6YpWS.js => environment-fWztxACp.js} (84%) rename server/public/admin/assets/{error-BlU2BJ1p.js => error-Bx-pGU7q.js} (90%) rename server/public/admin/assets/{fans-CJtkF_LI.js => fans-cGgRMGM6.js} (89%) rename server/public/admin/assets/{file-Bl00fTDO.js => file-B0mYsl3b.js} (95%) rename server/public/admin/assets/{file-rhlCNPS0.js => file-BArbnVF4.js} (86%) rename server/public/admin/assets/{filing-CR17vjat.js => filing-hSVHUC2T.js} (88%) rename server/public/admin/assets/{finance-C9v2y7Wt.js => finance-_Zy0NXqv.js} (90%) rename server/public/admin/assets/{follow_reply-BpWSjbb2.js => follow_reply-Cq1QQpyh.js} (88%) rename server/public/admin/assets/{footer.vue_vue_type_script_setup_true_lang-Bb6zG7Ce.js => footer.vue_vue_type_script_setup_true_lang-C6pBtIyu.js} (89%) delete mode 100644 server/public/admin/assets/h5-6qbQ2CqL.js rename server/public/admin/assets/{h5-Di3_YAUv.js => h5-QqVM-oUm.js} (91%) create mode 100644 server/public/admin/assets/h5-Zkr20Wt1.js create mode 100644 server/public/admin/assets/icon-CH7xzNdJ.js delete mode 100644 server/public/admin/assets/icon-lYw5pLdE.js delete mode 100644 server/public/admin/assets/index-55neFmjq.js rename server/public/admin/assets/{index-DsSo_l55.js => index-9YImSnPn.js} (79%) rename server/public/admin/assets/{index-CCPwBqOQ.js => index-B0S-7rum.js} (87%) delete mode 100644 server/public/admin/assets/index-B1WRZwLH.js delete mode 100644 server/public/admin/assets/index-B687Zw9l.js rename server/public/admin/assets/{index-Ba5lM5yG.js => index-B6HDh3IU.js} (75%) rename server/public/admin/assets/{index-WiUh_w_G.js => index-B8cCNf64.js} (86%) create mode 100644 server/public/admin/assets/index-B9vGwckb.js create mode 100644 server/public/admin/assets/index-BNTs4h_w.js delete mode 100644 server/public/admin/assets/index-B_4R8cs4.js rename server/public/admin/assets/{index-CNsnDN_w.js => index-Bd6RIUBW.js} (88%) delete mode 100644 server/public/admin/assets/index-BeI253Mt.js rename server/public/admin/assets/{index-BArasJZ4.js => index-BjJKERC9.js} (86%) rename server/public/admin/assets/{index-BtynGDk7.js => index-BmXwPo0P.js} (78%) rename server/public/admin/assets/{index-BQIXL4qS.js => index-BxFitu_z.js} (90%) delete mode 100644 server/public/admin/assets/index-C11IbIoh.css rename server/public/admin/assets/{index-Cl6K8CxX.js => index-C4Spimk6.js} (63%) rename server/public/admin/assets/{index-xkDxadic.js => index-C9b4Sag5.js} (73%) delete mode 100644 server/public/admin/assets/index-CA6za4YE.js rename server/public/admin/assets/{index-BKDIL42L.js => index-CF_6tyUi.js} (86%) rename server/public/admin/assets/{index-CzFmbFBQ.js => index-CJKithYP.js} (63%) create mode 100644 server/public/admin/assets/index-CMsGEvq_.css rename server/public/admin/assets/{index-Bc1ewzr4.js => index-CQN7LSKe.js} (83%) delete mode 100644 server/public/admin/assets/index-Cim68r_S.js rename server/public/admin/assets/{index-DpHv3G3S.js => index-CtPLEPGx.js} (86%) create mode 100644 server/public/admin/assets/index-CtbhtIVL.js create mode 100644 server/public/admin/assets/index-CvLd_f9k.js rename server/public/admin/assets/{index-D3ARV5b5.js => index-CyZZ81gQ.js} (94%) rename server/public/admin/assets/{index-yL8Xu_Q0.js => index-D-mHvQrV.js} (88%) delete mode 100644 server/public/admin/assets/index-D6IS__U-.js rename server/public/admin/assets/{index-DLYusrMX.js => index-D7PWi55Q.js} (77%) rename server/public/admin/assets/{index-ChRu_rHZ.js => index-D7q8mwqM.js} (80%) create mode 100644 server/public/admin/assets/index-DCrjgDth.js rename server/public/admin/assets/{index-B397Wtat.js => index-DKaVkNDv.js} (86%) create mode 100644 server/public/admin/assets/index-DVD5zwi1.js rename server/public/admin/assets/{index-D3sMviNE.js => index-DXw-6Yt-.js} (93%) rename server/public/admin/assets/{index-CSs1DpQp.js => index-DbmS5QgH.js} (74%) create mode 100644 server/public/admin/assets/index-Dm_WDutq.js rename server/public/admin/assets/{index-D-TEjYz0.js => index-DpXgp0Jt.js} (91%) rename server/public/admin/assets/{index-DuHVr3-G.js => index-Ds6BcjTF.js} (87%) rename server/public/admin/assets/{index-CN39xsBn.js => index-NdNAeLzU.js} (86%) rename server/public/admin/assets/{index-DzjjpCg7.js => index-OOfj3k4H.js} (87%) rename server/public/admin/assets/{index-vledklw3.js => index-P75bTQgB.js} (90%) create mode 100644 server/public/admin/assets/index-T5JxWg7E.js create mode 100644 server/public/admin/assets/index-WtXZKhZE.js rename server/public/admin/assets/{index-BhIRJ4cH.js => index-b-zSWJml.js} (85%) rename server/public/admin/assets/{index-BFSqYBoL.js => index-bTzYy0gE.js} (83%) rename server/public/admin/assets/{index-CZCTajfc.js => index-iiFE26st.js} (79%) create mode 100644 server/public/admin/assets/index-kH50yP0I.css rename server/public/admin/assets/{index-fuo-gblw.js => index-kgKqtVrf.js} (80%) delete mode 100644 server/public/admin/assets/index-wAn-JtEX.js create mode 100644 server/public/admin/assets/index-xZXgRhuH.js rename server/public/admin/assets/{index.vue_vue_type_script_setup_true_lang-CU4vN34y.js => index.vue_vue_type_script_setup_true_lang-8jySDNH4.js} (94%) create mode 100644 server/public/admin/assets/index.vue_vue_type_script_setup_true_lang-BCTRSL78.js rename server/public/admin/assets/{index.vue_vue_type_script_setup_true_lang-DfgBCqWg.js => index.vue_vue_type_script_setup_true_lang-BQXRM6_y.js} (93%) rename server/public/admin/assets/{index.vue_vue_type_script_setup_true_lang-GkCR2Dtu.js => index.vue_vue_type_script_setup_true_lang-BRLwtJdB.js} (96%) create mode 100644 server/public/admin/assets/index.vue_vue_type_script_setup_true_lang-CGaXpMKH.js delete mode 100644 server/public/admin/assets/index.vue_vue_type_script_setup_true_lang-CLVBLrJv.js delete mode 100644 server/public/admin/assets/index.vue_vue_type_script_setup_true_lang-CLhPF1Ro.js rename server/public/admin/assets/{index.vue_vue_type_style_index_0_lang-CsknWLym.js => index.vue_vue_type_style_index_0_lang-fYb7GwIh.js} (96%) rename server/public/admin/assets/{index_h5-PvEzFrU0.js => index_h5-CBPir90H.js} (83%) rename server/public/admin/assets/{information-Uy3QgFMN.js => information-C_QP_KIG.js} (91%) delete mode 100644 server/public/admin/assets/journal-DPtWvD5V.js create mode 100644 server/public/admin/assets/journal-DVuou61R.js rename server/public/admin/assets/{keyword_reply-ChykV12D.js => keyword_reply-Jqw6pBWX.js} (89%) rename server/public/admin/assets/{link-CNpYvS0r.js => link-BwGD6shj.js} (75%) create mode 100644 server/public/admin/assets/list-C8-XRWwZ.js rename server/public/admin/assets/{list-B6s-9S91.js => list-CcIJIJLw.js} (80%) delete mode 100644 server/public/admin/assets/list-DIgPcNqy.js rename server/public/admin/assets/{login-2R15073S.js => login-CMahIaRF.js} (96%) rename server/public/admin/assets/{login_register-DSJ61FsQ.js => login_register-g74lF521.js} (94%) rename server/public/admin/assets/{medicine-DE-G9qYM.js => medicine-BYiH1_mI.js} (81%) delete mode 100644 server/public/admin/assets/medicine-BZuJMV9y.js create mode 100644 server/public/admin/assets/medicine-CBhmFlH1.js rename server/public/admin/assets/{menu-NiEDDTAv.js => menu-C1llu_vR.js} (90%) rename server/public/admin/assets/{menu-BZ--F8lp.js => menu-CsOXi1od.js} (76%) rename server/public/admin/assets/{menu-DDPrAoWK.js => menu-lw9nZKd4.js} (86%) rename server/public/admin/assets/{menu-set-CMNjldC9.js => menu-set-DAdFhbM1.js} (66%) rename server/public/admin/assets/{menu-set.vue_vue_type_script_setup_true_lang-6xcaC9K4.js => menu-set.vue_vue_type_script_setup_true_lang-CDWwwb7g.js} (92%) rename server/public/admin/assets/{message-Clv9-mDb.js => message-CA2KRakP.js} (87%) rename server/public/admin/assets/{mobile-style-kE7LFseL.js => mobile-style-BqJrKhzf.js} (82%) rename server/public/admin/assets/{mobile-style.vue_vue_type_script_setup_true_lang-BuTWRXe0.js => mobile-style.vue_vue_type_script_setup_true_lang-B-psZ_BW.js} (95%) rename server/public/admin/assets/{oa-attr-Cx-u1iTH.js => oa-attr-V_nEftdE.js} (82%) rename server/public/admin/assets/{oa-menu-form-CNVT-s29.js => oa-menu-form-C2SOCQz9.js} (82%) rename server/public/admin/assets/{oa-menu-form-edit-CKnx8g4A.js => oa-menu-form-edit-CCcyJ48A.js} (75%) rename server/public/admin/assets/{oa-menu-form-edit.vue_vue_type_script_setup_true_lang-Vw5NLqKa.js => oa-menu-form-edit.vue_vue_type_script_setup_true_lang-CutmIyGM.js} (90%) rename server/public/admin/assets/{oa-menu-form.vue_vue_type_script_setup_true_lang-Cj8uklbF.js => oa-menu-form.vue_vue_type_script_setup_true_lang-t8TGc66l.js} (95%) rename server/public/admin/assets/{oa-phone-BR1ilG8r.js => oa-phone-ymXPS_p3.js} (90%) rename server/public/admin/assets/{open_setting-CSvvD6eT.js => open_setting-CdaAz6ND.js} (92%) rename server/public/admin/assets/{order-DxpelCR7.js => order-D4R5pbKT.js} (95%) create mode 100644 server/public/admin/assets/order_list-0q-jl-2e.js delete mode 100644 server/public/admin/assets/order_list-C4RHWSZE.js create mode 100644 server/public/admin/assets/order_list-PeU-7ru6.css delete mode 100644 server/public/admin/assets/order_list-oIBW1jaW.css rename server/public/admin/assets/{overflow-DGzQ9nnb.js => overflow-KG9kluXc.js} (88%) rename server/public/admin/assets/{paiban-WNftey0d.js => paiban-DcPNxbY2.js} (78%) rename server/public/admin/assets/{patient-call-CT43CxGX.js => patient-call-Bulmp9id.js} (84%) rename server/public/admin/assets/{pay-BvrJOYi_.js => pay-BpKQLm-K.js} (85%) rename server/public/admin/assets/{pc-kHeZeZWL.js => pc-C99NfL7y.js} (89%) rename server/public/admin/assets/{pc_details-B8IbDum5.js => pc_details-B8WTfIle.js} (58%) rename server/public/admin/assets/{perm-NyMnMPw8.js => perm-C0HOCe9H.js} (67%) rename server/public/admin/assets/{picker-B6Dcn_V9.js => picker-CGqg4FNQ.js} (93%) rename server/public/admin/assets/{picker-RR6i8slw.js => picker-Der0ecbS.js} (94%) rename server/public/admin/assets/{picker.vue_vue_type_script_setup_true_lang-DdHcdKtJ.js => picker.vue_vue_type_script_setup_true_lang-BEKUabE6.js} (92%) rename server/public/admin/assets/{popover_input-C_Pe8f_-.js => popover_input-DNlaRJ9I.js} (89%) rename server/public/admin/assets/{post-B4qJ4BpN.js => post-BQqHbmMX.js} (86%) delete mode 100644 server/public/admin/assets/prescription-drawer-Cw-gT-d5.js create mode 100644 server/public/admin/assets/prescription-drawer-D8grpu2V.js rename server/public/admin/assets/{preview-Dc92lEWh.js => preview-OS53Y7GY.js} (52%) rename server/public/admin/assets/{preview-pc-BypmRL9U.js => preview-pc-BrkLkBy1.js} (52%) create mode 100644 server/public/admin/assets/progress--ShEYBLn.js delete mode 100644 server/public/admin/assets/progress-CFj8jg19.js rename server/public/admin/assets/{protocol-DtYWAkXb.js => protocol-DGrcv2_q.js} (84%) rename server/public/admin/assets/{qywx-Cg8yhQdu.js => qywx-CMKyj4C_.js} (74%) rename server/public/admin/assets/{recharge_record-B37RhQCY.js => recharge_record-C8JuP6km.js} (90%) rename server/public/admin/assets/{refund-log-lvbGypOd.js => refund-log-D56Zs4id.js} (81%) create mode 100644 server/public/admin/assets/refund-log.vue_vue_type_script_setup_true_lang-D1NQV8R4.js delete mode 100644 server/public/admin/assets/refund-log.vue_vue_type_script_setup_true_lang-DSWt8Prm.js rename server/public/admin/assets/{refund_record-Cti6jshY.js => refund_record-C7As0lPb.js} (82%) rename server/public/admin/assets/{relations-add-BiDwQMKw.js => relations-add-dz_U1WZU.js} (74%) rename server/public/admin/assets/{relations-add.vue_vue_type_script_setup_true_lang-DzcWYLbn.js => relations-add.vue_vue_type_script_setup_true_lang-UzBvSLWh.js} (91%) rename server/public/admin/assets/{rich_text-EA2HMRhm.js => rich_text-Bo82DhL1.js} (77%) rename server/public/admin/assets/{role-DKAG1xTY.js => role-Dws5Gwe2.js} (83%) delete mode 100644 server/public/admin/assets/roster-CW2pW3MB.js create mode 100644 server/public/admin/assets/roster-T-A5MIRv.js rename server/public/admin/assets/{setting-BHfqR8Kw.js => setting-B87_MWw-.js} (65%) rename server/public/admin/assets/{setup-DkheUSiG.js => setup-2QtUVxxN.js} (79%) rename server/public/admin/assets/{statistics-BbrZUaXA.js => statistics-CZBtLQOm.js} (88%) rename server/public/admin/assets/{style-CkQvp6mm.js => style-BNglbWSG.js} (81%) rename server/public/admin/assets/{tabbar-DI2X_lfY.js => tabbar-Br2P4F36.js} (76%) rename server/public/admin/assets/{tcm-DompPo1O.js => tcm-7RUvcI90.js} (98%) rename server/public/admin/assets/{theme-picker-B1mJ6xL0.js => theme-picker-DrVRk6fl.js} (87%) delete mode 100644 server/public/admin/assets/tongji-92lXx4M2.js create mode 100644 server/public/admin/assets/tongji-DmIV70Ur.js rename server/public/admin/assets/{upload-D39d6sTW.js => upload-rNvMbhk7.js} (90%) rename server/public/admin/assets/{useDictOptions-CA4QiMnI.js => useDictOptions-BNfwjYNp.js} (84%) rename server/public/admin/assets/{useMenuOa-Do4LEKMJ.js => useMenuOa-CiMZMkmv.js} (92%) rename server/public/admin/assets/{user-D_WetR3i.js => user-CTlHIfqI.js} (82%) rename server/public/admin/assets/{weapp-CSuNOJ5M.js => weapp-0h-epSYv.js} (91%) rename server/public/admin/assets/{weapp-mZDhTWBI.js => weapp-Bnzi0iXW.js} (68%) rename server/public/admin/assets/{website-Bj2jy9Q5.js => website-BKXRAVIa.js} (91%) rename server/public/admin/assets/{wx_oa-BZiZBWa_.js => wx_oa-qW170Twq.js} (93%) delete mode 100644 sync_tracking_numbers.bat delete mode 100644 sync_tracking_numbers.sh delete mode 100644 test_first_login.sql delete mode 100644 test_gancao_config.php delete mode 100644 test_gancao_ssl.php delete mode 100644 test_slot_generation.php delete mode 100644 医生资质图片功能说明.md delete mode 100644 医生资质图片功能部署指南.md delete mode 100644 医生资质图片数据流转说明.md delete mode 100644 处方单字段修改说明.md delete mode 100644 测试医生资质图片功能.md diff --git a/ADD_DOSE_UNIT_TO_ORDER_SUMMARY.md b/ADD_DOSE_UNIT_TO_ORDER_SUMMARY.md deleted file mode 100644 index bd9abfe7..00000000 --- a/ADD_DOSE_UNIT_TO_ORDER_SUMMARY.md +++ /dev/null @@ -1,75 +0,0 @@ -# 添加剂量单位和剂数字段到订单编辑表单 - -## 任务概述 -在处方订单编辑表单中添加 `dose_unit`(剂量单位)和 `dose_count`(剂数)字段,支持用户选择和编辑。 - -## 实现内容 - -### 1. 前端表单字段(admin/src/views/consumer/prescription/order_list.vue) - -#### 添加的表单项: -- **剂量单位** (`dose_unit`): 下拉选择框,7个选项 - - 剂、丸、袋、盒、瓶、膏、贴 - - 默认值:剂 - -- **剂数** (`dose_count`): 数字输入框 - - 最小值:1 - - 默认值:1 - -#### 修改位置: -- 表单定义:在 `medication_days` 字段后添加 -- 数据加载:`openEdit()` 函数中加载 `dose_count` 和 `dose_unit` -- 数据提交:`submitEdit()` 函数中包含 `dose_count` 和 `dose_unit` - -### 2. 后端逻辑(server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php) - -#### 修改的方法: -- `edit()` 方法添加字段处理: - ```php - $order->dose_unit = (string) ($params['dose_unit'] ?? '剂'); - $order->dose_count = isset($params['dose_count']) && (int)$params['dose_count'] > 0 ? (int)$params['dose_count'] : 1; - ``` - -### 3. 数据库迁移(add_dose_unit_to_prescription_order.sql) - -#### 添加的字段: -```sql -ALTER TABLE `zyt_tcm_prescription_order` -ADD COLUMN `dose_unit` varchar(20) NOT NULL DEFAULT '剂' COMMENT '剂量单位:剂、丸、袋、盒、瓶、膏、贴' AFTER `medication_days`, -ADD COLUMN `dose_count` int(11) NOT NULL DEFAULT 1 COMMENT '剂数' AFTER `dose_unit`; -``` - -## 字段说明 - -| 字段名 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| dose_unit | varchar(20) | 剂 | 剂量单位:剂、丸、袋、盒、瓶、膏、贴 | -| dose_count | int(11) | 1 | 剂数,最小值为1 | - -## 部署步骤 - -1. **执行数据库迁移**: - ```bash - mysql -u用户名 -p数据库名 < add_dose_unit_to_prescription_order.sql - ``` - -2. **部署后端代码**: - - 更新 `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -3. **部署前端代码**: - - 更新 `admin/src/views/consumer/prescription/order_list.vue` - - 重新构建前端:`npm run build` - -## 测试要点 - -- [ ] 创建新订单时,`dose_unit` 默认为"剂",`dose_count` 默认为1 -- [ ] 编辑订单时,能正确加载已保存的 `dose_unit` 和 `dose_count` 值 -- [ ] 修改 `dose_unit` 和 `dose_count` 后能正确保存 -- [ ] 验证 `dose_count` 不能小于1 -- [ ] 验证所有7个剂量单位选项都能正常选择和保存 - -## 相关文件 - -- `admin/src/views/consumer/prescription/order_list.vue` - 前端订单列表和编辑表单 -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 后端订单编辑逻辑 -- `add_dose_unit_to_prescription_order.sql` - 数据库迁移脚本 diff --git a/ADD_MISSING_FIELDS.sql b/ADD_MISSING_FIELDS.sql deleted file mode 100644 index b84f0c0d..00000000 --- a/ADD_MISSING_FIELDS.sql +++ /dev/null @@ -1,26 +0,0 @@ --- 添加缺失的字段 --- 请在数据库管理工具中执行以下SQL - --- 1. 添加 usage_days(服用天数)- 在 dose_unit 之后 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_days` int(11) NOT NULL DEFAULT 7 COMMENT '服用天数' AFTER `dose_unit`; - --- 2. 添加 usage_time(服用时间)- 在 usage_instruction 之后 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_time` varchar(50) NOT NULL DEFAULT '饭前' COMMENT '服用时间:饭前、饭后、饭中、空腹、睡前、晨起、随时' AFTER `usage_instruction`; - --- 3. 添加 usage_way(服用方式)- 在 usage_time 之后 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_way` varchar(50) NOT NULL DEFAULT '温水送服' COMMENT '服用方式:温水送服、开水冲服、黄酒送服等' AFTER `usage_time`; - --- 4. 添加 dietary_taboo(忌口)- 在 usage_way 之后 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `dietary_taboo` varchar(200) NOT NULL DEFAULT '' COMMENT '忌口(逗号分隔):辛辣食物、生冷食物、油腻食物等' AFTER `usage_way`; - --- 5. 添加 usage_notes(其他服用说明)- 在 dietary_taboo 之后 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_notes` varchar(200) NOT NULL DEFAULT '' COMMENT '其他服用说明' AFTER `dietary_taboo`; - --- 6. 添加 is_shared(是否共享)- 在 template_id 之后 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `is_shared` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否共享:0-不共享(仅自己可见) 1-共享(所有人可见)' AFTER `template_id`; - --- 执行完成后,查看表结构确认 -SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT, COLUMN_COMMENT -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = 'zyt' AND TABLE_NAME = 'zyt_tcm_prescription' -ORDER BY ORDINAL_POSITION; diff --git a/APPOINTMENT_BUTTON_DISPLAY_FIX.md b/APPOINTMENT_BUTTON_DISPLAY_FIX.md deleted file mode 100644 index 29423f8b..00000000 --- a/APPOINTMENT_BUTTON_DISPLAY_FIX.md +++ /dev/null @@ -1,208 +0,0 @@ -# 预约列表按钮显示优化 - -## 问题描述 - -在预约列表中,同样的情况下,有的预约显示"开方"按钮,有的不显示。 - -## 问题原因 - -操作列的按钮过多,导致"开方"按钮被挤出可视区域或被其他按钮遮挡。 - -原有按钮(从左到右): -1. 编辑患者 -2. 视频二维码 -3. 通话 -4. 完成 -5. 开方/查看 -6. 更多 - -当所有按钮都显示时,操作列宽度为 380px,可能不够容纳所有按钮。 - -## 解决方案 - -根据预约状态优化按钮显示: - -### 已完成状态(status = 3) - -只显示核心按钮: -- 编辑患者 -- 开方/查看 -- 更多 - -隐藏按钮: -- 视频二维码(已完成,不需要视频) -- 通话(已完成,不需要通话) -- 完成(已经完成) - -### 其他状态(待接诊、已过号、已取消) - -显示所有按钮: -- 编辑患者 -- 视频二维码 -- 通话 -- 完成 -- 开方/查看 -- 更多 - -## 修改内容 - -**文件**: `admin/src/views/tcm/appointment/list.vue` - -### 修改前 - -```vue - - - -``` - -### 修改后 - -```vue - - - -``` - -## 按钮显示规则 - -### 预约状态说明 - -| 状态值 | 状态名称 | 说明 | -|-------|---------|------| -| 1 | 待接诊 | 患者已挂号,等待医生接诊 | -| 2 | 已取消 | 挂号已取消 | -| 3 | 已完成 | 医生已完成接诊 | -| 4 | 已过号 | 患者未按时就诊,已过号 | - -### 按钮显示矩阵 - -| 按钮 | 待接诊(1) | 已取消(2) | 已完成(3) | 已过号(4) | -|-----|----------|----------|----------|----------| -| 编辑患者 | ✅ | ✅ | ✅ | ✅ | -| 视频二维码 | ✅ | ✅ | ❌ | ✅ | -| 通话 | ✅ | ✅ | ❌ | ✅ | -| 完成 | ✅ | ✅ | ❌ | ✅ | -| 开方/查看 | ✅ | ✅ | ✅ | ✅ | -| 更多 | ✅ | ✅ | ✅ | ✅ | - -## 优势 - -### 1. 确保核心按钮始终可见 - -- "开方/查看"按钮是核心功能,必须始终显示 -- 已完成状态下,减少不必要的按钮,确保核心按钮可见 - -### 2. 优化用户体验 - -- 已完成的预约不需要"视频二维码"、"通话"、"完成"按钮 -- 减少按钮数量,界面更简洁 - -### 3. 避免按钮被遮挡 - -- 减少按钮数量后,操作列宽度足够容纳所有按钮 -- 避免按钮被挤出可视区域 - -## 测试建议 - -### 测试用例 1: 待接诊状态 - -1. 找到一个待接诊的预约(status = 1) -2. 检查操作列是否显示所有按钮: - - ✅ 编辑患者 - - ✅ 视频二维码 - - ✅ 通话 - - ✅ 完成 - - ✅ 开方/查看 - - ✅ 更多 - -### 测试用例 2: 已完成状态 - -1. 找到一个已完成的预约(status = 3) -2. 检查操作列是否只显示核心按钮: - - ✅ 编辑患者 - - ❌ 视频二维码(不显示) - - ❌ 通话(不显示) - - ❌ 完成(不显示) - - ✅ 开方/查看 - - ✅ 更多 - -### 测试用例 3: 已完成且已开方 - -1. 找到一个已完成且已开方的预约 -2. 检查"开方/查看"按钮是否显示为"查看" -3. 点击"查看"按钮,应该可以查看处方(只读模式) - -### 测试用例 4: 已完成但未开方 - -1. 找到一个已完成但未开方的预约 -2. 检查"开方/查看"按钮是否显示为"开方" -3. 点击"开方"按钮,应该可以创建新处方 - -## 注意事项 - -### 1. 权限检查 - -所有按钮都有权限检查,如果用户没有相应权限,按钮会被隐藏: -- `tcm.diagnosis/edit` - 编辑患者 -- `tcm.diagnosis/videoQr` - 视频二维码 -- `doctor.appointment/prescription` - 通话 -- `doctor.appointment/complete` - 完成 -- `tcm.diagnosis/kaifang` - 开方/查看 - -### 2. 按钮文字动态变化 - -"开方/查看"按钮的文字根据处方状态动态变化: -- 未开方或待审核:显示"开方" -- 已审核通过且未作废:显示"查看" -- 已驳回:显示"开方" - -### 3. 操作列宽度 - -操作列宽度为 380px,足够容纳: -- 已完成状态:3个按钮(编辑患者、开方/查看、更多) -- 其他状态:6个按钮(编辑患者、视频二维码、通话、完成、开方/查看、更多) - -如果按钮仍然被遮挡,可以考虑: -- 增加操作列宽度(如 420px) -- 将更多按钮移到下拉菜单中 -- 使用图标按钮减少宽度 - -## 相关文档 - -- `PRESCRIPTION_BUTTON_DISPLAY_CHECK.md` - 处方按钮显示检查文档 -- `PRESCRIPTION_APPROVED_LOCK.md` - 处方审核通过后锁定编辑文档 - -## 总结 - -通过根据预约状态优化按钮显示,确保了: -- ✅ "开方/查看"按钮始终可见 -- ✅ 已完成状态下界面更简洁 -- ✅ 避免按钮被挤出可视区域 -- ✅ 优化用户体验 -- ✅ 保持核心功能的可访问性 diff --git a/APPOINTMENT_INDEX_OPTIMIZATION.md b/APPOINTMENT_INDEX_OPTIMIZATION.md deleted file mode 100644 index 90bebaca..00000000 --- a/APPOINTMENT_INDEX_OPTIMIZATION.md +++ /dev/null @@ -1,251 +0,0 @@ -# 预约列表查询性能优化 - 索引方案 - -## 问题分析 - -预约列表查询涉及多表关联和复杂条件筛选,主要性能瓶颈: - -1. 多表 LEFT JOIN(预约表、诊断表、管理员表) -2. 多条件筛选(状态、日期范围、患者姓名、医生姓名) -3. 权限过滤(医生角色、医助角色) -4. 复杂排序(状态 + 日期 + 时间) -5. 子查询(确认诊单、开方状态) - -## 索引优化方案 - -### 1. 预约表 (zyt_appointment) - -#### 单列索引 -```sql --- 医生ID索引(医生角色查询) -ALTER TABLE `zyt_appointment` ADD INDEX `idx_doctor_id` (`doctor_id`); - --- 患者ID索引(关联查询) -ALTER TABLE `zyt_appointment` ADD INDEX `idx_patient_id` (`patient_id`); - --- 状态索引(状态筛选) -ALTER TABLE `zyt_appointment` ADD INDEX `idx_status` (`status`); - --- 预约日期索引(日期范围查询) -ALTER TABLE `zyt_appointment` ADD INDEX `idx_appointment_date` (`appointment_date`); -``` - -#### 组合索引(重点优化) -```sql --- 状态+日期+时间(用于排序,覆盖 ORDER BY) -ALTER TABLE `zyt_appointment` ADD INDEX `idx_status_date_time` (`status`, `appointment_date`, `appointment_time`); - --- 医生+状态+日期(医生角色常用查询) -ALTER TABLE `zyt_appointment` ADD INDEX `idx_doctor_status_date` (`doctor_id`, `status`, `appointment_date`); -``` - -**优化效果**: -- `idx_status_date_time` 可以直接用于排序,避免 filesort -- `idx_doctor_status_date` 可以快速过滤医生的预约记录 - -### 2. 诊断表 (zyt_tcm_diagnosis) - -```sql --- 患者姓名索引(模糊搜索,前缀索引) -ALTER TABLE `zyt_tcm_diagnosis` ADD INDEX `idx_patient_name` (`patient_name`(20)); - --- 医助ID索引(医助角色查询) -ALTER TABLE `zyt_tcm_diagnosis` ADD INDEX `idx_assistant_id` (`assistant_id`); -``` - -**说明**: -- `patient_name` 使用前缀索引(20),节省空间且支持 LIKE 'xxx%' 查询 -- 不支持 LIKE '%xxx%' 的优化,但可以减少扫描行数 - -### 3. 管理员表 (zyt_admin) - -```sql --- 管理员姓名索引(医生姓名搜索) -ALTER TABLE `zyt_admin` ADD INDEX `idx_name` (`name`(20)); -``` - -### 4. 诊断查看记录表 (zyt_diagnosis_view_records) - -```sql --- 诊断ID+确认状态+删除时间(用于确认诊单查询) -ALTER TABLE `zyt_diagnosis_view_records` ADD INDEX `idx_diagnosis_confirmed` (`diagnosis_id`, `is_confirmed`, `delete_time`); -``` - -**优化效果**: -- 加速 `WHERE diagnosis_id IN (...) AND is_confirmed = 1 AND delete_time IS NULL` 查询 -- 覆盖索引,无需回表 - -### 5. 处方表 (zyt_prescription) - -```sql --- 诊断ID+删除时间(用于开方状态查询) -ALTER TABLE `zyt_prescription` ADD INDEX `idx_diagnosis_delete` (`diagnosis_id`, `delete_time`); -``` - -**优化效果**: -- 加速 `WHERE diagnosis_id IN (...) AND delete_time IS NULL` 查询 - -### 6. 管理员角色表 (zyt_admin_role) - -```sql --- 管理员ID+角色ID(用于权限判断) -ALTER TABLE `zyt_admin_role` ADD INDEX `idx_admin_role` (`admin_id`, `role_id`); -``` - -**优化效果**: -- 加速角色查询,快速判断用户权限 - -## 执行步骤 - -### 方式一:直接执行(推荐) - -```bash -# 连接数据库 -mysql -h 39.97.232.35 -u cs_zyt -p cs_zyt - -# 执行索引创建脚本 -source optimize_appointment_indexes.sql -``` - -### 方式二:安全执行(检查后添加) - -```bash -# 执行安全脚本(会先检查索引是否存在) -source check_and_add_indexes.sql -``` - -### 方式三:phpMyAdmin - -1. 登录 phpMyAdmin -2. 选择数据库 `cs_zyt` -3. 点击 SQL 标签 -4. 复制 `optimize_appointment_indexes.sql` 内容 -5. 点击执行 - -## 预期效果 - -### 优化前 -- 查询时间:500ms - 2000ms -- 扫描行数:全表扫描 -- 使用索引:PRIMARY 或无 - -### 优化后 -- 查询时间:50ms - 200ms(提升 5-10 倍) -- 扫描行数:索引范围扫描 -- 使用索引:组合索引 - -## 验证方法 - -### 1. 查看执行计划 - -```sql -EXPLAIN SELECT - a.*, - u.patient_name, - u.phone as patient_phone, - ad.name as doctor_name -FROM zyt_appointment a -LEFT JOIN zyt_tcm_diagnosis u ON a.patient_id = u.id -LEFT JOIN zyt_admin ad ON a.doctor_id = ad.id -WHERE a.doctor_id = 1 - AND a.status = 1 - AND a.appointment_date >= '2024-03-01' -ORDER BY a.status ASC, a.appointment_date ASC, a.appointment_time ASC -LIMIT 20; -``` - -**期望结果**: -- `type`: ref 或 range(不是 ALL) -- `key`: idx_doctor_status_date 或 idx_status_date_time -- `rows`: 较小的数字(不是全表行数) - -### 2. 查看索引使用情况 - -```sql --- 查看索引统计 -SHOW INDEX FROM zyt_appointment; - --- 查看索引使用情况(需要开启慢查询日志) -SELECT * FROM sys.schema_unused_indexes WHERE object_schema = 'cs_zyt'; -``` - -## 注意事项 - -1. **索引维护成本**:索引会增加 INSERT/UPDATE/DELETE 的开销,但对于查询为主的场景,收益远大于成本 - -2. **索引选择性**: - - 高选择性字段(如 ID、日期)适合建索引 - - 低选择性字段(如性别、状态)单独建索引效果不明显,但可以作为组合索引的一部分 - -3. **前缀索引**: - - 对于长字符串字段(如姓名),使用前缀索引可以节省空间 - - 前缀长度选择:能区分大部分数据即可(通常 10-20 个字符) - -4. **组合索引顺序**: - - 遵循"最左前缀"原则 - - 将选择性高的字段放在前面 - - 考虑查询条件的组合频率 - -5. **定期维护**: - ```sql - -- 优化表(重建索引) - OPTIMIZE TABLE zyt_appointment; - OPTIMIZE TABLE zyt_tcm_diagnosis; - ``` - -## 进一步优化建议 - -### 1. 查询优化 - -```php -// 避免 SELECT *,只查询需要的字段 -$query->field('a.id, a.patient_id, a.doctor_id, a.status, ...'); - -// 使用 whereIn 代替多个 OR -$query->whereIn('a.status', [1, 3]); -``` - -### 2. 分页优化 - -```php -// 使用延迟关联优化深分页 -$subQuery = Appointment::where($conditions) - ->field('id') - ->limit($offset, $limit) - ->buildSql(); - -$list = Appointment::alias('a') - ->join([$subQuery => 'sub'], 'a.id = sub.id') - ->with(['patient', 'doctor']) - ->select(); -``` - -### 3. 缓存优化 - -```php -// 缓存医生列表、角色信息等不常变化的数据 -$roleIds = Cache::remember('admin_role_' . $adminId, function() { - return AdminRole::where('admin_id', $adminId)->column('role_id'); -}, 3600); -``` - -## 监控指标 - -定期检查以下指标: - -1. 慢查询日志(> 1s 的查询) -2. 索引使用率 -3. 表大小和索引大小 -4. 查询响应时间 - -```sql --- 查看表和索引大小 -SELECT - table_name, - ROUND(data_length / 1024 / 1024, 2) AS data_mb, - ROUND(index_length / 1024 / 1024, 2) AS index_mb, - ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_mb -FROM information_schema.tables -WHERE table_schema = 'cs_zyt' - AND table_name IN ('zyt_appointment', 'zyt_tcm_diagnosis', 'zyt_admin') -ORDER BY (data_length + index_length) DESC; -``` diff --git a/BUG_FIXES_SUMMARY.md b/BUG_FIXES_SUMMARY.md deleted file mode 100644 index 09ab253a..00000000 --- a/BUG_FIXES_SUMMARY.md +++ /dev/null @@ -1,176 +0,0 @@ -# Bug Fixes Summary - -## 修复的问题 - -### 1. 处方查看权限问题 ✅ (已解决) - -**问题描述**: -- 医生A开了处方后,医生B点击"查看病历"时提示"无权限查看此处方" -- 其他医生无法查看患者的历史处方记录 - -**根本原因**: -- 权限检查逻辑已经正确实现,允许有 `tcm.diagnosis/kaifang` 权限的医生查看所有处方(只读模式) -- 问题可能是前端缓存或权限配置问题 - -**解决方案**: -- 确认 `PrescriptionLogic::canViewPrescription()` 方法已包含以下逻辑: - ```php - // 有开方权限的医生可以查看所有处方(只读) - $permissions = $adminInfo['permissions'] ?? []; - if (in_array('tcm.diagnosis/kaifang', $permissions, true)) { - return true; - } - ``` -- 权限层级(从高到低): - 1. 超级管理员 - 2. 创建人 - 3. 医助 - 4. 共享处方 - 5. 可见角色 - 6. 有开方权限的医生(`tcm.diagnosis/kaifang`) - -**文件**: `server/app/adminapi/logic/tcm/PrescriptionLogic.php` - ---- - -### 2. 处方组件重复请求问题 ✅ (已解决) - -**问题描述**: -- 点击"开处方"或"查看病历"按钮时,重复请求 `/tcm.prescription/detail` 接口 -- 快速连续点击导致多次调用 - -**根本原因**: -- 已经实现了 `isOpening` 标志防止重复调用 -- 防重复逻辑已经正确工作 - -**解决方案**: -- 确认 `open()` 和 `openById()` 方法已包含防重复逻辑: - ```typescript - const isOpening = ref(false) - - const open = async (data: any, options?: { templateId?: number; stationName?: string }) => { - if (isOpening.value) { - console.warn('处方正在打开中,请勿重复点击') - return - } - isOpening.value = true - try { - // ... 处理逻辑 - } finally { - setTimeout(() => { - isOpening.value = false - }, 500) - } - } - ``` - -**文件**: `admin/src/components/tcm-prescription/index.vue` - ---- - -### 3. 订单撤回后处方审核状态重置 ✅ (已修复) - -**问题描述**: -- 撤回订单后,关联的处方审核状态没有重置为"待审核" -- 缺少 `audit_by_name` 字段的清空 - -**解决方案**: -- 在 `withdraw()` 方法中添加了 `audit_by_name` 字段的清空: - ```php - Prescription::where('id', $prescriptionId) - ->whereNull('delete_time') - ->update([ - 'audit_status' => 0, // 0=待审核 - 'audit_time' => null, - 'audit_by' => null, - 'audit_by_name' => '', // 新增:清空审核人姓名 - 'audit_remark' => '', - 'update_time' => time(), - ]); - ``` - -**文件**: `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - ---- - -### 4. 物流轨迹自动加载 ✅ (已实现) - -**问题描述**: -- 用户希望打开订单详情时,如果有快递单号,自动加载物流轨迹 -- 不需要手动点击"查询轨迹"按钮 - -**解决方案**: -- 已经实现自动加载逻辑: - ```typescript - async function openDetail(id: number) { - // ... 加载订单详情 - if (d) { - detailLogisticsExpress.value = String(d.express_company || 'auto') || 'auto' - if (String(d.tracking_number || '').trim()) { - fetchLogisticsTrace() // 自动加载物流轨迹 - } - fetchLogs(id) - } - } - ``` -- 物流查询优先从数据库读取,如果数据库没有数据,再调用快递100 API -- 按钮文字改为"刷新轨迹",用于手动刷新最新物流信息 - -**文件**: `admin/src/views/consumer/prescription/order_list.vue` - ---- - -## 测试建议 - -### 1. 处方查看权限测试 -1. 使用医生A账号创建处方 -2. 使用医生B账号(有 `tcm.diagnosis/kaifang` 权限)点击"查看病历" -3. 确认可以正常查看处方详情(只读模式) -4. 确认医生B不能编辑医生A创建的处方 - -### 2. 重复请求测试 -1. 快速连续点击"开处方"按钮多次 -2. 检查浏览器开发者工具的Network面板 -3. 确认只发送一次 `/tcm.prescription/detail` 请求 -4. 确认控制台显示"处方正在打开中,请勿重复点击"警告 - -### 3. 订单撤回测试 -1. 创建一个处方业务订单(处方审核状态为"已通过") -2. 点击"撤回"按钮 -3. 确认订单状态变为"已取消" -4. 在处方列表中确认该处方的审核状态变为"待审核" -5. 确认审核人、审核时间、审核意见都已清空 - -### 4. 物流轨迹自动加载测试 -1. 创建一个订单并填写快递单号 -2. 点击"查看"按钮打开订单详情 -3. 确认物流轨迹自动加载(如果数据库有数据,显示数据来源为"database") -4. 点击"刷新轨迹"按钮,确认可以手动刷新物流信息 -5. 如果数据库没有数据,确认调用快递100 API(显示数据来源为"api") - ---- - -## 相关文档 - -- `PRESCRIPTION_VIEW_PERMISSION_FIX.md` - 处方查看权限修复文档 -- `PRESCRIPTION_DUPLICATE_REQUEST_FIX.md` - 处方重复请求修复文档 -- `PRESCRIPTION_ORDER_WITHDRAW_RESET_AUDIT.md` - 订单撤回重置审核状态文档 -- `EXPRESS_TRACKING_AUTO_LOAD.md` - 物流轨迹自动加载文档 -- `EXPRESS_TRACKING_DATABASE_QUERY.md` - 物流追踪数据库查询优化文档 - ---- - -## 总结 - -所有问题都已经解决或确认正常工作: - -1. ✅ 处方查看权限:已实现,有开方权限的医生可以查看所有处方 -2. ✅ 重复请求防护:已实现,使用 `isOpening` 标志防止重复调用 -3. ✅ 撤回重置审核:已修复,添加了 `audit_by_name` 字段的清空 -4. ✅ 物流自动加载:已实现,打开详情时自动加载物流轨迹 - -如果仍然遇到问题,请检查: -- 用户权限配置是否正确 -- 浏览器缓存是否需要清除 -- 数据库表结构是否完整 -- 快递100账号是否已充值 diff --git a/CHECK_DOSAGE_FIELDS.md b/CHECK_DOSAGE_FIELDS.md deleted file mode 100644 index dd4e10c8..00000000 --- a/CHECK_DOSAGE_FIELDS.md +++ /dev/null @@ -1,91 +0,0 @@ -# 检查处方用量字段是否已添加到数据库 - -## 问题描述 -处方的用量相关字段(`dosage_amount`、`dosage_unit`、`need_decoction`)没有保存到数据库。 - -## 检查步骤 - -### 1. 检查数据库表结构 - -执行以下SQL查询,检查字段是否存在: - -```sql -SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT, COLUMN_COMMENT -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'zyt_tcm_prescription' - AND COLUMN_NAME IN ('dosage_amount', 'dosage_unit', 'need_decoction') -ORDER BY ORDINAL_POSITION; -``` - -### 2. 如果字段不存在,执行迁移 - -如果上述查询返回空结果,说明字段还未添加,需要执行迁移文件: - -```bash -mysql -u用户名 -p数据库名 < add_prescription_dosage_fields.sql -``` - -或者直接在数据库中执行: - -```sql -ALTER TABLE `zyt_tcm_prescription` -ADD COLUMN `dosage_amount` decimal(10,2) DEFAULT NULL COMMENT '用量数值' AFTER `prescription_type`, -ADD COLUMN `dosage_unit` varchar(10) NOT NULL DEFAULT '' COMMENT '用量单位(g/ml)' AFTER `dosage_amount`, -ADD COLUMN `need_decoction` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否代煎(0否1是,仅饮片)' AFTER `dosage_unit`; -``` - -### 3. 验证字段已添加 - -再次执行步骤1的查询,应该看到3个字段: - -| COLUMN_NAME | DATA_TYPE | COLUMN_DEFAULT | COLUMN_COMMENT | -|-------------|-----------|----------------|----------------| -| dosage_amount | decimal | NULL | 用量数值 | -| dosage_unit | varchar | '' | 用量单位(g/ml) | -| need_decoction | tinyint | 0 | 是否代煎(0否1是,仅饮片) | - -## 代码检查 - -### 前端代码 ✅ -- `admin/src/components/tcm-prescription/index.vue` 已正确实现 -- 包含 `dosage_amount`、`dosage_unit`、`need_decoction` 字段 -- 有 `watch` 监听处方类型变化,自动设置单位 - -### 后端代码 ✅ -- `server/app/adminapi/logic/tcm/PrescriptionLogic.php` 的 `add()` 方法已包含这些字段 -- `server/app/adminapi/logic/tcm/PrescriptionLogic.php` 的 `edit()` 方法已包含这些字段 - -### 数据库模型 ✅ -- `server/app/common/model/tcm/Prescription.php` 已配置类型转换: - ```php - protected $type = [ - 'dosage_amount' => 'float', - 'need_decoction' => 'integer', - ]; - ``` - -## 结论 - -代码层面一切正常,问题很可能是**数据库迁移未执行**。 - -请执行上述步骤1检查数据库表结构,如果字段不存在,执行步骤2的迁移SQL。 - -## 测试步骤 - -迁移完成后,测试以下场景: - -1. **创建新处方** - - 选择"浓缩水丸",用量应为 1-10g 下拉选择 - - 选择"饮片",用量应为 50-250ml 下拉选择,并显示代煎选项 - - 选择其他类型,用量应为自由输入 - - 保存后检查数据库,确认字段已保存 - -2. **编辑已有处方** - - 打开已有处方 - - 修改用量和代煎选项 - - 保存后检查数据库,确认字段已更新 - -3. **查看处方详情** - - 在订单详情中查看处方 - - 确认用量和代煎信息正确显示 diff --git a/COMPLETION_REPORT.md b/COMPLETION_REPORT.md deleted file mode 100644 index e9677b39..00000000 --- a/COMPLETION_REPORT.md +++ /dev/null @@ -1,594 +0,0 @@ -# 处方库功能 - 完成报告 - -## 📋 项目信息 - -**项目名称:** 处方库管理系统 -**版本号:** v1.0.0 -**完成日期:** 2026-03-22 -**开发状态:** ✅ 已完成并测试通过 - ---- - -## ✅ 完成清单 - -### 1. 数据库设计 ✅ - -- [x] 设计数据库表结构 -- [x] 创建索引优化查询 -- [x] 支持软删除 -- [x] 创建SQL文件 - -**文件:** `server/database/migrations/create_prescription_library.sql` - -### 2. 后端开发 ✅ - -#### 数据模型层 -- [x] PrescriptionLibrary 模型 -- [x] 时间格式化 -- [x] 软删除支持 - -**文件:** `server/app/common/model/tcm/PrescriptionLibrary.php` - -#### 控制器层 -- [x] 列表接口 -- [x] 添加接口 -- [x] 编辑接口 -- [x] 删除接口 -- [x] 详情接口 - -**文件:** `server/app/adminapi/controller/tcm/PrescriptionLibraryController.php` - -#### 业务逻辑层 -- [x] 添加处方逻辑 -- [x] 编辑处方逻辑(含权限验证) -- [x] 删除处方逻辑(含权限验证) -- [x] 详情查询逻辑(含权限验证) -- [x] 异常处理 - -**文件:** `server/app/adminapi/logic/tcm/PrescriptionLibraryLogic.php` - -#### 列表逻辑层 -- [x] 搜索条件设置 -- [x] 权限过滤(只显示自己的或公开的) -- [x] 分页支持 -- [x] JSON数据解析 - -**文件:** `server/app/adminapi/lists/tcm/PrescriptionLibraryLists.php` - -#### 验证器层 -- [x] 添加场景验证 -- [x] 编辑场景验证 -- [x] 删除场景验证 -- [x] 详情场景验证 -- [x] 自定义错误消息 - -**文件:** `server/app/adminapi/validate/tcm/PrescriptionLibraryValidate.php` - -### 3. 前端开发 ✅ - -#### 页面组件 -- [x] 搜索表单 -- [x] 数据表格 -- [x] 新增/编辑弹窗 -- [x] 药材动态表格 -- [x] 权限控制 -- [x] 表单验证 -- [x] 加载状态 -- [x] 错误提示 - -**文件:** `admin/src/views/consumer/prescription/list.vue` - -#### API接口 -- [x] prescriptionLibraryLists -- [x] prescriptionLibraryAdd -- [x] prescriptionLibraryEdit -- [x] prescriptionLibraryDelete -- [x] prescriptionLibraryDetail - -**文件:** `admin/src/api/tcm.ts`(已更新) - -### 4. 安装脚本 ✅ - -- [x] Windows 安装脚本 -- [x] Linux/Mac 安装脚本 -- [x] 交互式配置 -- [x] 错误处理 - -**文件:** -- `install_prescription_library.bat` -- `install_prescription_library.sh` - -### 5. 测试文件 ✅ - -- [x] 表结构查询 -- [x] 测试数据插入 -- [x] 数据查询测试 -- [x] 统计查询 -- [x] 清理脚本 - -**文件:** `TEST_PRESCRIPTION_LIBRARY.sql` - -### 6. 文档编写 ✅ - -#### 使用文档 -- [x] 完整使用说明(README) -- [x] 快速开始指南 -- [x] 功能演示文档 -- [x] 常见问题解答 - -**文件:** -- `PRESCRIPTION_LIBRARY_README.md` -- `QUICK_START_PRESCRIPTION_LIBRARY.md` -- `PRESCRIPTION_LIBRARY_DEMO.md` - -#### 技术文档 -- [x] 开发总结 -- [x] 文件清单 -- [x] API接口文档 -- [x] 数据库设计文档 - -**文件:** -- `PRESCRIPTION_LIBRARY_SUMMARY.md` -- `PRESCRIPTION_LIBRARY_FILES.md` - -#### 安装文档 -- [x] 安装检查清单 -- [x] 环境要求 -- [x] 问题排查 -- [x] 验证步骤 - -**文件:** `INSTALLATION_CHECKLIST.md` - -#### 项目文档 -- [x] 交付文档 -- [x] 文档索引 -- [x] 完成报告 - -**文件:** -- `DELIVERY_PACKAGE.md` -- `README_PRESCRIPTION_LIBRARY.md` -- `COMPLETION_REPORT.md`(本文件) - ---- - -## 📊 统计数据 - -### 文件统计 - -| 类型 | 数量 | 说明 | -|------|------|------| -| PHP文件 | 6 | 后端代码 | -| Vue文件 | 1 | 前端页面 | -| TypeScript文件 | 1 | API接口(修改) | -| SQL文件 | 2 | 数据库+测试 | -| Shell脚本 | 2 | 安装脚本 | -| Markdown文档 | 9 | 各类文档 | -| **总计** | **21** | **所有文件** | - -### 代码统计 - -| 类型 | 行数 | 说明 | -|------|------|------| -| PHP代码 | ~450行 | 后端业务逻辑 | -| Vue代码 | ~350行 | 前端页面组件 | -| TypeScript | ~30行 | API接口(新增) | -| SQL语句 | ~120行 | 数据库+测试 | -| Shell脚本 | ~100行 | 安装脚本 | -| 文档内容 | ~2000行 | 各类文档 | -| **总计** | **~3050行** | **所有代码** | - ---- - -## 🎯 功能实现 - -### 核心功能(8项) - -1. ✅ **处方创建** - - 处方名称输入 - - 动态添加药材 - - 设置是否公开 - - 数据验证 - -2. ✅ **处方编辑** - - 修改处方信息 - - 修改药材配方 - - 权限验证 - - 数据验证 - -3. ✅ **处方删除** - - 软删除机制 - - 权限验证(仅创建者) - - 确认提示 - -4. ✅ **处方查看** - - 详情展示 - - 只读模式 - - 权限验证 - -5. ✅ **处方列表** - - 分页显示 - - 权限过滤 - - 数据格式化 - -6. ✅ **处方搜索** - - 按名称搜索 - - 模糊匹配 - - 实时查询 - -7. ✅ **处方筛选** - - 按是否公开筛选 - - 组合查询 - - 结果统计 - -8. ✅ **权限控制** - - 查看权限 - - 编辑权限 - - 删除权限 - - 公开/私有设置 - -### 技术特性(10项) - -1. ✅ **完全独立** - 不依赖其他模块 -2. ✅ **动态药材** - 支持任意数量药材 -3. ✅ **小数剂量** - 精确到0.1克 -4. ✅ **权限控制** - 完善的权限验证 -5. ✅ **软删除** - 数据可恢复 -6. ✅ **数据验证** - 前后端双重验证 -7. ✅ **异常处理** - 完善的错误处理 -8. ✅ **响应式设计** - 适配各种屏幕 -9. ✅ **搜索筛选** - 快速查找处方 -10. ✅ **分页显示** - 优化性能 - ---- - -## 🧪 测试结果 - -### 功能测试 ✅ - -| 测试项 | 结果 | 说明 | -|--------|------|------| -| 创建处方(私有) | ✅ 通过 | 功能正常 | -| 创建处方(公开) | ✅ 通过 | 功能正常 | -| 编辑处方 | ✅ 通过 | 功能正常 | -| 删除处方 | ✅ 通过 | 功能正常 | -| 查看处方 | ✅ 通过 | 功能正常 | -| 搜索处方 | ✅ 通过 | 功能正常 | -| 筛选处方 | ✅ 通过 | 功能正常 | -| 分页显示 | ✅ 通过 | 功能正常 | - -### 权限测试 ✅ - -| 测试项 | 结果 | 说明 | -|--------|------|------| -| 私有处方权限 | ✅ 通过 | 只有创建者可见 | -| 公开处方权限 | ✅ 通过 | 所有人可见 | -| 编辑权限验证 | ✅ 通过 | 权限控制正确 | -| 删除权限验证 | ✅ 通过 | 仅创建者可删除 | - -### 数据验证测试 ✅ - -| 测试项 | 结果 | 说明 | -|--------|------|------| -| 处方名称验证 | ✅ 通过 | 必填验证正常 | -| 药材列表验证 | ✅ 通过 | 至少一味药材 | -| 药材名称验证 | ✅ 通过 | 不能为空 | -| 药材剂量验证 | ✅ 通过 | 必须大于0 | - -### 代码质量测试 ✅ - -| 测试项 | 结果 | 说明 | -|--------|------|------| -| PHP语法检查 | ✅ 通过 | 无语法错误 | -| Vue语法检查 | ✅ 通过 | 无语法错误 | -| TypeScript检查 | ✅ 通过 | 无类型错误 | -| 代码规范检查 | ✅ 通过 | 符合规范 | - ---- - -## 📚 文档完成度 - -### 使用文档 ✅ - -- [x] 完整使用说明(4972字) -- [x] 快速开始指南(4195字) -- [x] 功能演示文档(5779字) -- [x] 常见问题解答 - -### 技术文档 ✅ - -- [x] 开发总结(6123字) -- [x] 文件清单(6250字) -- [x] API接口文档 -- [x] 数据库设计文档 - -### 安装文档 ✅ - -- [x] 安装检查清单(详细步骤) -- [x] 环境要求说明 -- [x] 问题排查指南 -- [x] 验证测试步骤 - -### 项目文档 ✅ - -- [x] 交付文档(完整) -- [x] 文档索引(9596字) -- [x] 完成报告(本文件) - -**文档总字数:** ~40,000字 - ---- - -## 🎨 界面设计 - -### 列表页面 ✅ - -- [x] 搜索表单区域 -- [x] 操作按钮区域 -- [x] 数据表格区域 -- [x] 分页组件 -- [x] 加载状态 -- [x] 空数据提示 - -### 编辑弹窗 ✅ - -- [x] 处方名称输入 -- [x] 药材动态表格 -- [x] 添加/删除药材按钮 -- [x] 是否公开开关 -- [x] 提示文字 -- [x] 表单验证提示 - -### 查看模式 ✅ - -- [x] 只读显示 -- [x] 禁用编辑 -- [x] 隐藏操作按钮 -- [x] 清晰的数据展示 - ---- - -## 🔐 安全性 - -### 数据安全 ✅ - -- [x] SQL注入防护(参数化查询) -- [x] XSS防护(数据转义) -- [x] CSRF防护(框架内置) -- [x] 权限验证(多层验证) - -### 业务安全 ✅ - -- [x] 创建者验证 -- [x] 公开/私有控制 -- [x] 软删除机制 -- [x] 数据验证 - ---- - -## 🚀 性能优化 - -### 数据库优化 ✅ - -- [x] 添加索引(creator_id, is_public, create_time) -- [x] 字段筛选(避免SELECT *) -- [x] 分页查询 -- [x] 软删除过滤 - -### 前端优化 ✅ - -- [x] 按需加载 -- [x] 防抖处理 -- [x] 加载状态提示 -- [x] 错误处理 - ---- - -## 📦 交付内容 - -### 代码文件(8个) - -1. ✅ create_prescription_library.sql -2. ✅ PrescriptionLibrary.php -3. ✅ PrescriptionLibraryController.php -4. ✅ PrescriptionLibraryLogic.php -5. ✅ PrescriptionLibraryLists.php -6. ✅ PrescriptionLibraryValidate.php -7. ✅ list.vue -8. ✅ tcm.ts(已更新) - -### 安装文件(3个) - -1. ✅ install_prescription_library.bat -2. ✅ install_prescription_library.sh -3. ✅ TEST_PRESCRIPTION_LIBRARY.sql - -### 文档文件(9个) - -1. ✅ PRESCRIPTION_LIBRARY_README.md -2. ✅ PRESCRIPTION_LIBRARY_DEMO.md -3. ✅ PRESCRIPTION_LIBRARY_SUMMARY.md -4. ✅ QUICK_START_PRESCRIPTION_LIBRARY.md -5. ✅ PRESCRIPTION_LIBRARY_FILES.md -6. ✅ INSTALLATION_CHECKLIST.md -7. ✅ DELIVERY_PACKAGE.md -8. ✅ README_PRESCRIPTION_LIBRARY.md -9. ✅ COMPLETION_REPORT.md - -**总计:** 20个文件 - ---- - -## ✨ 亮点特色 - -### 1. 完全独立 ⭐ -不依赖任何其他业务模块,可独立部署和维护。 - -### 2. 权限灵活 ⭐ -支持公开/私有两种模式,满足不同使用场景。 - -### 3. 操作简单 ⭐ -直观的界面设计,5分钟即可上手使用。 - -### 4. 文档完善 ⭐ -提供9份详细文档,覆盖使用、开发、安装等各个方面。 - -### 5. 代码质量高 ⭐ -无语法错误,符合编码规范,易于维护和扩展。 - -### 6. 安装便捷 ⭐ -提供自动安装脚本,一键完成安装。 - ---- - -## 🎯 验收标准 - -### 功能验收 ✅ - -- [x] 所有核心功能正常工作 -- [x] 权限控制正确 -- [x] 数据验证有效 -- [x] 搜索筛选正常 -- [x] 分页显示正常 - -### 代码验收 ✅ - -- [x] 无语法错误 -- [x] 代码规范 -- [x] 注释完整 -- [x] 异常处理完善 -- [x] 性能优化 - -### 文档验收 ✅ - -- [x] 使用文档完整 -- [x] 安装文档清晰 -- [x] API文档准确 -- [x] 示例代码可用 -- [x] 常见问题解答 - -### 测试验收 ✅ - -- [x] 功能测试通过 -- [x] 权限测试通过 -- [x] 数据验证测试通过 -- [x] 代码质量测试通过 -- [x] 性能测试通过 - ---- - -## 📈 项目进度 - -### 开发阶段 - -| 阶段 | 状态 | 完成时间 | -|------|------|----------| -| 需求分析 | ✅ 完成 | 2026-03-22 | -| 数据库设计 | ✅ 完成 | 2026-03-22 | -| 后端开发 | ✅ 完成 | 2026-03-22 | -| 前端开发 | ✅ 完成 | 2026-03-22 | -| 功能测试 | ✅ 完成 | 2026-03-22 | -| 文档编写 | ✅ 完成 | 2026-03-22 | -| 项目交付 | ✅ 完成 | 2026-03-22 | - -**总耗时:** 1天 -**完成度:** 100% - ---- - -## 🎉 项目总结 - -### 成功要素 - -1. **需求明确** - 功能定义清晰,目标明确 -2. **设计合理** - 数据库设计合理,代码结构清晰 -3. **开发高效** - 代码质量高,开发速度快 -4. **测试充分** - 功能测试、权限测试、代码测试全面 -5. **文档完善** - 提供详细的使用和技术文档 - -### 经验总结 - -1. **独立性设计** - 功能独立,不耦合其他模块 -2. **权限控制** - 完善的权限验证机制 -3. **用户体验** - 简洁直观的界面设计 -4. **文档先行** - 完善的文档提高使用效率 -5. **测试驱动** - 充分的测试保证质量 - ---- - -## 🔮 未来展望 - -### 短期计划(1-2周) - -1. 添加处方分类功能 -2. 添加处方标签功能 -3. 优化搜索功能 - -### 中期计划(1-2月) - -1. 添加使用统计 -2. 添加热门处方排行 -3. 支持处方导入导出 - -### 长期计划(3-6月) - -1. 添加处方评论功能 -2. 添加版本管理 -3. 添加处方推荐算法 -4. 移动端适配 - ---- - -## 📝 签字确认 - -### 开发团队确认 - -**开发人员:** _______________ -**日期:** 2026-03-22 -**签字:** _______________ - -### 测试团队确认 - -**测试人员:** _______________ -**日期:** _______________ -**签字:** _______________ - -### 项目管理确认 - -**项目经理:** _______________ -**日期:** _______________ -**签字:** _______________ - -### 客户确认 - -**客户代表:** _______________ -**日期:** _______________ -**签字:** _______________ - ---- - -## 🎊 致谢 - -感谢所有参与本项目的人员! - -特别感谢: -- 需求提供方 -- 开发团队 -- 测试团队 -- 文档编写团队 - ---- - -## 📞 联系方式 - -如有任何问题或建议,请联系: - -**技术支持:** [联系方式] -**项目经理:** [联系方式] - ---- - -**项目状态:** ✅ 已完成并交付 -**版本号:** v1.0.0 -**完成日期:** 2026-03-22 - -**祝使用愉快!** 🎉🎊🎈 diff --git a/COMPLETION_REQUEST_FEATURE.md b/COMPLETION_REQUEST_FEATURE.md deleted file mode 100644 index d30a055b..00000000 --- a/COMPLETION_REQUEST_FEATURE.md +++ /dev/null @@ -1,93 +0,0 @@ -# 完单申请功能实现总结 - -## 功能概述 - -在补齐支付单时,用户可以选择是否申请完成订单。如果勾选了完单申请,审核人员在进行支付审核时可以看到该申请信息。 - -## 数据库修改 - -### 新增字段(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/DEBUG_GUIDE.md b/DEBUG_GUIDE.md deleted file mode 100644 index bf8420a6..00000000 --- a/DEBUG_GUIDE.md +++ /dev/null @@ -1,277 +0,0 @@ -# 首次登录修改密码 - 调试指南 - -## 如何验证功能是否正常工作 - -### 1. 检查数据库 - -```sql --- 查看管理员的 is_paw 状态 -SELECT id, account, name, is_paw FROM la_admin WHERE id = 1; -``` - -确认 `is_paw` 字段存在且值为 0。 - -### 2. 检查后端 API - -#### 2.1 登录接口 - -打开浏览器开发者工具(F12),切换到 Network 标签。 - -登录后查看 `/adminapi/login/account` 接口的响应: - -```json -{ - "code": 1, - "msg": "success", - "data": { - "name": "管理员", - "avatar": "...", - "role_name": "超级管理员", - "token": "...", - "is_paw": 0 // ← 确认这个字段存在且为 0 - } -} -``` - -#### 2.2 获取用户信息接口 - -刷新页面后,查看 `/adminapi/auth.admin/mySelf` 接口的响应: - -```json -{ - "code": 1, - "msg": "success", - "data": { - "user": { - "id": 1, - "account": "admin", - "name": "管理员", - "is_paw": 0, // ← 确认这个字段存在且为 0 - // ... 其他字段 - }, - "menu": [...], - "permissions": [...] - } -} -``` - -### 3. 检查前端状态 - -在浏览器控制台(Console)中输入: - -```javascript -// 查看 userStore 的状态 -const userStore = window.$nuxt?.$store?.state?.user || - JSON.parse(localStorage.getItem('pinia-user') || '{}') -console.log('isPaw:', userStore.isPaw) -console.log('userInfo:', userStore.userInfo) -``` - -或者在 Vue DevTools 中查看 Pinia store 的状态。 - -### 4. 检查路由守卫 - -在 `admin/src/permission.ts` 文件中添加调试日志: - -```typescript -router.beforeEach(async (to, from, next) => { - const userStore = useUserStore() - - console.log('=== 路由守卫 ===') - console.log('目标路径:', to.path) - console.log('isPaw:', userStore.isPaw) - console.log('hasUserInfo:', Object.keys(userStore.userInfo).length !== 0) - - // ... 其他代码 -}) -``` - -刷新页面,在控制台查看输出。 - -### 5. 完整测试流程 - -#### 步骤 1:设置测试账号 - -```sql -UPDATE la_admin SET is_paw = 0 WHERE id = 1; -``` - -#### 步骤 2:清除浏览器缓存 - -- 清除 localStorage -- 清除 sessionStorage -- 清除 Cookies -- 或者使用无痕模式 - -#### 步骤 3:登录 - -1. 打开浏览器开发者工具(F12) -2. 切换到 Network 标签 -3. 登录系统 -4. 查看 `/adminapi/login/account` 接口响应,确认 `is_paw: 0` -5. 观察是否跳转到 `/change-password` - -#### 步骤 4:测试 URL 绕过 - -1. 在地址栏输入 `/workbench/index` -2. 按回车 -3. 观察是否被拦截并跳转回 `/change-password` - -#### 步骤 5:测试刷新 - -1. 在修改密码页面按 F5 刷新 -2. 查看 Network 标签,确认调用了 `/adminapi/auth.admin/mySelf` 接口 -3. 查看接口响应,确认 `is_paw: 0` -4. 观察是否仍然停留在 `/change-password` - -#### 步骤 6:修改密码 - -1. 输入新密码(至少 6 位) -2. 点击"确认修改" -3. 观察是否提示"密码修改成功,请重新登录" -4. 观察是否跳转到登录页 - -#### 步骤 7:验证数据库 - -```sql -SELECT id, account, is_paw FROM la_admin WHERE id = 1; --- 应该显示 is_paw = 1 -``` - -#### 步骤 8:使用新密码登录 - -1. 使用新密码登录 -2. 观察是否直接进入系统(不跳转到修改密码页面) - -## 常见问题排查 - -### 问题:登录后没有跳转到修改密码页面 - -**排查步骤:** - -1. 检查登录接口响应是否包含 `is_paw` 字段 - ```javascript - // 在 Network 标签查看 /adminapi/login/account 响应 - ``` - -2. 检查前端是否保存了 `isPaw` 状态 - ```javascript - // 在 Console 输入 - console.log(useUserStore().isPaw) - ``` - -3. 检查登录页面代码 - ```typescript - // admin/src/views/account/login.vue - const result: any = await userStore.login(formData) - if (result.is_paw === 0) { - router.push('/change-password') - } - ``` - -### 问题:刷新后进入系统 - -**排查步骤:** - -1. 检查 `getUserInfo` 接口是否返回 `is_paw` - ```javascript - // 在 Network 标签查看 /adminapi/auth.admin/mySelf 响应 - // 确认 data.user.is_paw 存在 - ``` - -2. 检查后端代码 - ```php - // server/app/adminapi/logic/auth/AdminLogic.php - // detail() 方法的 field 数组中是否包含 'is_paw' - ``` - -3. 检查前端是否更新状态 - ```typescript - // admin/src/stores/modules/user.ts - getUserInfo() { - return new Promise((resolve, reject) => { - getUserInfo().then((data) => { - this.isPaw = data.user.is_paw ?? 1 // 确认这行存在 - }) - }) - } - ``` - -4. 检查路由守卫逻辑 - ```typescript - // admin/src/permission.ts - // 确认在 getUserInfo() 之后检查 isPaw - await userStore.getUserInfo() - if (userStore.isPaw === 0) { - next({ path: changePasswordPath }) - return - } - ``` - -### 问题:可以通过 URL 绕过 - -**排查步骤:** - -1. 检查路由守卫是否正确配置 - ```typescript - // admin/src/permission.ts - // 确认路由守卫在所有路由跳转时都会执行 - ``` - -2. 在路由守卫中添加日志 - ```typescript - router.beforeEach(async (to, from, next) => { - console.log('路由守卫执行:', to.path, 'isPaw:', userStore.isPaw) - // ... - }) - ``` - -3. 检查是否有其他路由配置覆盖了守卫 - -## 调试技巧 - -### 1. 使用 Vue DevTools - -安装 Vue DevTools 浏览器插件,可以实时查看: -- Pinia store 状态 -- 路由信息 -- 组件状态 - -### 2. 添加断点 - -在关键位置添加 `debugger` 语句: - -```typescript -// admin/src/permission.ts -router.beforeEach(async (to, from, next) => { - debugger // 浏览器会在这里暂停 - const userStore = useUserStore() - // ... -}) -``` - -### 3. 查看完整的请求响应 - -在 Network 标签中: -- 点击请求 -- 查看 Response 标签 -- 确认返回的数据结构 - -### 4. 清除缓存测试 - -每次测试前清除缓存,确保测试环境干净: - -```javascript -// 在 Console 中执行 -localStorage.clear() -sessionStorage.clear() -location.reload() -``` - -## 成功标准 - -✅ 登录后自动跳转到修改密码页面 -✅ 无法通过输入 URL 绕过 -✅ 刷新后仍然在修改密码页面 -✅ 修改密码后 `is_paw` 更新为 1 -✅ 使用新密码登录后正常进入系统 diff --git a/DELIVERY_PACKAGE.md b/DELIVERY_PACKAGE.md deleted file mode 100644 index dfb271dd..00000000 --- a/DELIVERY_PACKAGE.md +++ /dev/null @@ -1,361 +0,0 @@ -# 处方库功能 - 交付文档 - -## 📦 项目交付信息 - -**项目名称:** 处方库管理系统 - -**版本号:** v1.0.0 - -**交付日期:** 2026-03-22 - -**开发状态:** ✅ 已完成并测试通过 - -## 📋 交付清单 - -### 1. 核心代码文件(15个) - -#### 后端代码(6个) -- ✅ `server/database/migrations/create_prescription_library.sql` - 数据库表结构 -- ✅ `server/app/common/model/tcm/PrescriptionLibrary.php` - 数据模型 -- ✅ `server/app/adminapi/controller/tcm/PrescriptionLibraryController.php` - 控制器 -- ✅ `server/app/adminapi/logic/tcm/PrescriptionLibraryLogic.php` - 业务逻辑 -- ✅ `server/app/adminapi/lists/tcm/PrescriptionLibraryLists.php` - 列表逻辑 -- ✅ `server/app/adminapi/validate/tcm/PrescriptionLibraryValidate.php` - 数据验证 - -#### 前端代码(2个) -- ✅ `admin/src/views/consumer/prescription/list.vue` - 处方库页面 -- ✅ `admin/src/api/tcm.ts` - API接口(已更新) - -#### 安装脚本(2个) -- ✅ `install_prescription_library.bat` - Windows安装脚本 -- ✅ `install_prescription_library.sh` - Linux/Mac安装脚本 - -#### 测试文件(1个) -- ✅ `TEST_PRESCRIPTION_LIBRARY.sql` - 测试SQL语句 - -#### 文档文件(7个) -- ✅ `PRESCRIPTION_LIBRARY_README.md` - 完整使用说明 -- ✅ `PRESCRIPTION_LIBRARY_DEMO.md` - 功能演示文档 -- ✅ `PRESCRIPTION_LIBRARY_SUMMARY.md` - 开发总结 -- ✅ `QUICK_START_PRESCRIPTION_LIBRARY.md` - 快速开始指南 -- ✅ `PRESCRIPTION_LIBRARY_FILES.md` - 文件清单 -- ✅ `INSTALLATION_CHECKLIST.md` - 安装检查清单 -- ✅ `DELIVERY_PACKAGE.md` - 本交付文档 - -**总计:** 18个文件 - -### 2. 代码统计 - -| 类型 | 文件数 | 代码行数 | -|------|--------|----------| -| PHP | 6 | ~450行 | -| Vue | 1 | ~350行 | -| TypeScript | 1 | ~30行(新增) | -| SQL | 2 | ~120行 | -| Shell | 2 | ~100行 | -| Markdown | 7 | ~1800行 | -| **总计** | **19** | **~2850行** | - -## 🎯 功能特性 - -### 核心功能 -1. ✅ 处方创建(处方名称 + 药材配方) -2. ✅ 处方编辑 -3. ✅ 处方删除 -4. ✅ 处方查看 -5. ✅ 处方列表展示 -6. ✅ 处方搜索(按名称) -7. ✅ 处方筛选(按是否公开) -8. ✅ 权限控制(公开/私有) - -### 技术特性 -1. ✅ 完全独立,不依赖其他模块 -2. ✅ 支持动态添加药材 -3. ✅ 支持小数剂量(精确到0.1克) -4. ✅ 完善的权限控制 -5. ✅ 软删除机制 -6. ✅ 数据验证 -7. ✅ 异常处理 -8. ✅ 响应式设计 - -## 🔧 技术栈 - -### 后端 -- **框架:** ThinkPHP 6.x -- **语言:** PHP 7.4+ -- **数据库:** MySQL 5.7+ -- **架构:** MVC + 业务逻辑层 - -### 前端 -- **框架:** Vue 3 -- **UI库:** Element Plus -- **语言:** TypeScript -- **构建工具:** Vite - -## 📊 数据库设计 - -### 表名:zyt_prescription_library - -| 字段名 | 类型 | 说明 | 索引 | -|--------|------|------|------| -| id | int(11) | 主键ID | PRIMARY | -| prescription_name | varchar(100) | 处方名称 | - | -| herbs | text | 药材列表JSON | - | -| is_public | tinyint(1) | 是否公开 | INDEX | -| creator_id | int(11) | 创建人ID | INDEX | -| creator_name | varchar(50) | 创建人姓名 | - | -| create_time | int(10) | 创建时间 | INDEX | -| update_time | int(10) | 更新时间 | - | -| delete_time | int(10) | 删除时间 | - | - -### 索引设计 -- PRIMARY KEY: id -- INDEX: creator_id -- INDEX: is_public -- INDEX: create_time - -## 🔌 API接口 - -### 接口列表 - -| 接口路径 | 方法 | 说明 | 权限 | -|---------|------|------|------| -| /tcm.prescriptionLibrary/lists | GET | 获取处方列表 | 需登录 | -| /tcm.prescriptionLibrary/add | POST | 添加处方 | 需登录 | -| /tcm.prescriptionLibrary/edit | POST | 编辑处方 | 需登录+权限 | -| /tcm.prescriptionLibrary/delete | POST | 删除处方 | 需登录+创建者 | -| /tcm.prescriptionLibrary/detail | GET | 获取处方详情 | 需登录+权限 | - -### 数据格式 - -#### 药材数据格式(JSON) -```json -[ - { - "name": "药材名称", - "dosage": 剂量(克) - } -] -``` - -#### 示例 -```json -[ - {"name": "熟地黄", "dosage": 24}, - {"name": "山茱萸", "dosage": 12}, - {"name": "山药", "dosage": 12} -] -``` - -## 🔐 权限设计 - -### 查看权限 -- 可以查看自己创建的所有处方 -- 可以查看其他人创建的公开处方 -- 不能查看其他人创建的私有处方 - -### 编辑权限 -- 可以编辑自己创建的处方 -- 可以编辑其他人创建的公开处方 -- 不能编辑其他人创建的私有处方 - -### 删除权限 -- 只能删除自己创建的处方 -- 不能删除其他人创建的处方 - -## 📖 使用文档 - -### 快速开始 -请参阅:`QUICK_START_PRESCRIPTION_LIBRARY.md` - -### 完整文档 -请参阅:`PRESCRIPTION_LIBRARY_README.md` - -### 功能演示 -请参阅:`PRESCRIPTION_LIBRARY_DEMO.md` - -### 安装指南 -请参阅:`INSTALLATION_CHECKLIST.md` - -## 🧪 测试报告 - -### 功能测试 -- ✅ 创建处方(私有) -- ✅ 创建处方(公开) -- ✅ 编辑处方 -- ✅ 删除处方 -- ✅ 查看处方 -- ✅ 搜索处方 -- ✅ 筛选处方 - -### 权限测试 -- ✅ 私有处方权限控制 -- ✅ 公开处方权限控制 -- ✅ 删除权限控制 -- ✅ 编辑权限控制 - -### 数据验证测试 -- ✅ 处方名称验证 -- ✅ 药材列表验证 -- ✅ 药材名称验证 -- ✅ 药材剂量验证 - -### 代码质量测试 -- ✅ 无语法错误 -- ✅ 无类型错误 -- ✅ 代码规范检查通过 - -## 🚀 部署说明 - -### 部署步骤 - -1. **备份数据库** - ```bash - mysqldump -u用户名 -p密码 数据库名 > backup.sql - ``` - -2. **上传代码文件** - - 上传所有后端PHP文件 - - 上传前端Vue文件 - -3. **安装数据库表** - ```bash - # Windows - install_prescription_library.bat - - # Linux/Mac - ./install_prescription_library.sh - ``` - -4. **配置菜单权限** - - 在后台添加菜单项 - - 配置访问权限 - -5. **重启服务** - ```bash - # 清除缓存 - php think clear - - # 重新编译前端 - npm run build - ``` - -6. **测试功能** - - 访问处方库页面 - - 测试各项功能 - -### 环境要求 -- PHP >= 7.4 -- MySQL >= 5.7 -- Node.js >= 14.x -- ThinkPHP 6.x -- Vue 3.x - -## 📝 注意事项 - -### 重要提示 -1. ⚠️ 安装前请务必备份数据库 -2. ⚠️ 确保数据库用户有创建表权限 -3. ⚠️ 安装后需要配置菜单权限 -4. ⚠️ 建议在测试环境先测试 - -### 已知限制 -1. 暂不支持处方分类 -2. 暂不支持批量导入 -3. 暂不支持处方导出 -4. 暂不支持处方评论 - -### 未来扩展 -1. 处方分类功能 -2. 处方标签功能 -3. 使用统计功能 -4. 导入导出功能 -5. 处方评论功能 -6. 版本管理功能 - -## 🆘 技术支持 - -### 常见问题 -请参阅:`QUICK_START_PRESCRIPTION_LIBRARY.md` 中的常见问题部分 - -### 问题反馈 -如遇到问题,请提供: -1. 错误信息截图 -2. 浏览器控制台日志 -3. 后端错误日志 -4. 操作步骤描述 - -### 联系方式 -请联系技术支持团队获取帮助。 - -## ✅ 验收标准 - -### 功能验收 -- [x] 所有核心功能正常工作 -- [x] 权限控制正确 -- [x] 数据验证有效 -- [x] 搜索筛选正常 - -### 代码验收 -- [x] 无语法错误 -- [x] 代码规范 -- [x] 注释完整 -- [x] 异常处理完善 - -### 文档验收 -- [x] 使用文档完整 -- [x] 安装文档清晰 -- [x] API文档准确 -- [x] 示例代码可用 - -### 测试验收 -- [x] 功能测试通过 -- [x] 权限测试通过 -- [x] 数据验证测试通过 -- [x] 代码质量测试通过 - -## 📜 版本历史 - -### v1.0.0 (2026-03-22) -- ✅ 初始版本发布 -- ✅ 实现核心功能 -- ✅ 完成文档编写 -- ✅ 通过测试验收 - -## 📄 许可证 - -本项目遵循项目主体的许可证协议。 - -## 🎉 交付确认 - -### 交付内容确认 -- [x] 所有代码文件已交付 -- [x] 所有文档文件已交付 -- [x] 安装脚本已交付 -- [x] 测试文件已交付 - -### 质量确认 -- [x] 代码质量符合标准 -- [x] 功能测试通过 -- [x] 文档完整准确 -- [x] 可以正常部署使用 - -### 交付签字 - -**开发人员:** _______________ **日期:** _______________ - -**测试人员:** _______________ **日期:** _______________ - -**项目经理:** _______________ **日期:** _______________ - -**客户确认:** _______________ **日期:** _______________ - ---- - -## 🌟 感谢使用 - -感谢选择处方库管理系统! - -如有任何问题或建议,欢迎随时联系我们。 - -**祝使用愉快!** 🎊 diff --git a/DIAGNOSIS_IMAGE_URL_PREFIX_FIX.md b/DIAGNOSIS_IMAGE_URL_PREFIX_FIX.md deleted file mode 100644 index b17ee92f..00000000 --- a/DIAGNOSIS_IMAGE_URL_PREFIX_FIX.md +++ /dev/null @@ -1,259 +0,0 @@ -# Diagnosis Image URL Prefix Fix (诊断图片URL前缀修复) - -## Issue -Image URLs in the diagnosis detail (tongue_images and report_files) were stored as relative paths without the domain prefix, causing issues when accessing them from external sources or different domains. - -## Solution -Added logic to automatically prepend the current domain to image URLs that don't already have an `http://` or `https://` prefix. - -## Changes Made - -### File: `server/app/adminapi/logic/tcm/DiagnosisLogic.php` - -#### 1. Updated tongue_images Processing -Added domain prefix logic after parsing the images array: - -```php -if (!empty($diagnosis['tongue_images'])) { - // 先尝试 JSON 解析(兼容旧数据) - $files = json_decode($diagnosis['tongue_images'], true); - if (is_array($files)) { - $diagnosis['tongue_images'] = $files; - } else { - // JSON 解析失败则按逗号分割 - $diagnosis['tongue_images'] = array_filter(explode(',', $diagnosis['tongue_images'])) ?: []; - } - - // 为没有 http 前缀的图片添加域名 - $domain = request()->domain(); - $diagnosis['tongue_images'] = array_map(function($url) use ($domain) { - if (empty($url)) { - return $url; - } - // 如果已经包含 http:// 或 https://,则跳过 - if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) { - return $url; - } - // 添加域名前缀 - return $domain . $url; - }, $diagnosis['tongue_images']); -} else { - $diagnosis['tongue_images'] = []; -} -``` - -#### 2. Updated report_files Processing -Applied the same logic to report files: - -```php -if (!empty($diagnosis['report_files'])) { - // 先尝试 JSON 解析(兼容旧数据) - $files = json_decode($diagnosis['report_files'], true); - if (is_array($files)) { - $diagnosis['report_files'] = $files; - } else { - // JSON 解析失败则按逗号分割 - $diagnosis['report_files'] = array_filter(explode(',', $diagnosis['report_files'])) ?: []; - } - - // 为没有 http 前缀的文件添加域名 - $domain = request()->domain(); - $diagnosis['report_files'] = array_map(function($url) use ($domain) { - if (empty($url)) { - return $url; - } - // 如果已经包含 http:// 或 https://,则跳过 - if (stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) { - return $url; - } - // 添加域名前缀 - return $domain . $url; - }, $diagnosis['report_files']); -} else { - $diagnosis['report_files'] = []; -} -``` - -## Logic Flow - -### URL Processing Logic: -1. Parse the images/files array (JSON or comma-separated) -2. Get current domain using `request()->domain()` -3. For each URL in the array: - - Check if URL is empty → skip - - Check if URL starts with `http://` or `https://` → skip (already has protocol) - - Otherwise → prepend domain to the URL - -### Examples: - -#### Relative Path (Needs Domain): -```php -Input: "/uploads/images/tongue/2024/01/image.jpg" -Domain: "https://admin.zhenyangtang.com.cn" -Output: "https://admin.zhenyangtang.com.cn/uploads/images/tongue/2024/01/image.jpg" -``` - -#### Absolute URL (Skip): -```php -Input: "https://cdn.example.com/images/tongue.jpg" -Output: "https://cdn.example.com/images/tongue.jpg" -``` - -#### HTTP URL (Skip): -```php -Input: "http://example.com/image.jpg" -Output: "http://example.com/image.jpg" -``` - -#### Empty URL (Skip): -```php -Input: "" -Output: "" -``` - -## Benefits - -### 1. Cross-Domain Compatibility -Images can now be accessed from different domains or external applications without path issues. - -### 2. API Response Consistency -API responses always return complete, accessible URLs regardless of how they were stored. - -### 3. Backward Compatibility -- Existing relative paths are automatically converted -- Existing absolute URLs are preserved -- No database migration required - -### 4. Flexible Storage -- Database can store either relative or absolute paths -- System handles both formats transparently -- Future-proof for CDN integration - -## Use Cases - -### 1. Mobile App Access -Mobile apps can directly use the returned URLs without needing to construct them. - -### 2. Third-Party Integration -External systems can access images using the complete URLs from API responses. - -### 3. CDN Migration -When migrating to CDN, images with absolute CDN URLs will work alongside local relative paths. - -### 4. Multi-Domain Setup -System works correctly across different domains (dev, staging, production). - -## Testing Steps - -### 1. Test Relative Path URLs -1. Create a diagnosis with tongue images stored as relative paths: - ``` - /uploads/images/tongue/2024/01/image.jpg - ``` -2. Fetch diagnosis detail via API -3. Verify response contains: - ```json - { - "tongue_images": [ - "https://admin.zhenyangtang.com.cn/uploads/images/tongue/2024/01/image.jpg" - ] - } - ``` - -### 2. Test Absolute URLs -1. Create a diagnosis with absolute URL: - ``` - https://cdn.example.com/images/tongue.jpg - ``` -2. Fetch diagnosis detail -3. Verify URL is unchanged: - ```json - { - "tongue_images": [ - "https://cdn.example.com/images/tongue.jpg" - ] - } - ``` - -### 3. Test Mixed URLs -1. Create a diagnosis with both relative and absolute URLs: - ``` - [ - "/uploads/local.jpg", - "https://cdn.example.com/remote.jpg" - ] - ``` -2. Fetch diagnosis detail -3. Verify correct processing: - ```json - { - "tongue_images": [ - "https://admin.zhenyangtang.com.cn/uploads/local.jpg", - "https://cdn.example.com/remote.jpg" - ] - } - ``` - -### 4. Test Empty/Null Values -1. Create a diagnosis with empty images -2. Fetch diagnosis detail -3. Verify empty array is returned: - ```json - { - "tongue_images": [] - } - ``` - -### 5. Test Report Files -Repeat all tests above for `report_files` field. - -## Technical Details - -### Domain Detection -Uses `request()->domain()` which returns the current request domain including protocol: -- Development: `http://localhost:8000` -- Production: `https://admin.zhenyangtang.com.cn` - -### Case-Insensitive Protocol Check -Uses `stripos()` for case-insensitive checking: -- Matches: `http://`, `HTTP://`, `Http://` -- Matches: `https://`, `HTTPS://`, `Https://` - -### Array Processing -Uses `array_map()` for efficient processing of all URLs in the array. - -## Related Files - -### Backend: -- `server/app/adminapi/logic/tcm/DiagnosisLogic.php` - - Updated `tongue_images` processing - - Updated `report_files` processing - -### Database: -- `tcm_diagnosis` table - - `tongue_images` column (stores JSON or comma-separated paths) - - `report_files` column (stores JSON or comma-separated paths) - -## Notes - -### Storage Format -The database continues to store paths as-is (relative or absolute). The conversion happens only when reading data for API responses. - -### Performance -The `array_map()` operation is efficient and adds minimal overhead. For typical diagnosis records with 1-5 images, the performance impact is negligible. - -### Future Enhancements -If needed, this logic could be extracted into a helper function for reuse across other file/image fields in the system. - -## Summary - -✅ Automatically adds domain prefix to relative image URLs -✅ Preserves absolute URLs (http/https) -✅ Handles both tongue_images and report_files -✅ Backward compatible with existing data -✅ Case-insensitive protocol detection -✅ Handles empty/null values gracefully -✅ No database migration required -✅ Works across different domains - -Image URLs in diagnosis details are now always complete and accessible, improving API usability and cross-domain compatibility! diff --git a/DIAGNOSIS_LIST_SORTING.md b/DIAGNOSIS_LIST_SORTING.md deleted file mode 100644 index 4f77d046..00000000 --- a/DIAGNOSIS_LIST_SORTING.md +++ /dev/null @@ -1,90 +0,0 @@ -# 诊单列表排序规则修改 - -## 修改内容 - -修改了诊单列表的排序逻辑,按照新的业务规则对数据进行排序。 - -## 排序规则 - -### 优先级顺序 -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/DOCTOR_STATISTICS_GUIDE.md b/DOCTOR_STATISTICS_GUIDE.md deleted file mode 100644 index b044d2cf..00000000 --- a/DOCTOR_STATISTICS_GUIDE.md +++ /dev/null @@ -1,121 +0,0 @@ -# 医生数据统计功能说明 - -## 功能概述 - -医生数据统计功能用于统计每个医生在不同时间范围内的诊单数据,包括: -- 已挂号数量(status=1) -- 已完成数量(status=3) -- 已过号数量(status=4) -- 已取消数量(status=2) -- 完成率计算 - -## 文件清单 - -### 前端文件 -1. `admin/src/views/doctor/tongji.vue` - 统计页面 -2. `admin/src/api/doctor.ts` - API接口(已更新) - -### 后端文件 -1. `server/app/adminapi/controller/doctor/StatisticsController.php` - 统计控制器 -2. `server/app/adminapi/lists/doctor/StatisticsLists.php` - 统计数据列表逻辑 - -## 功能特性 - -### 1. 时间范围筛选 -- 今天:查看当天的统计数据 -- 最近7天:查看最近一周的统计数据 -- 最近30天:查看最近一个月的统计数据 -- 自定义:选择任意日期范围进行查询 - -### 2. 医生筛选 -- 可以选择特定医生查看其统计数据 -- 也可以不选择医生,查看所有医生的统计数据 - -### 3. 统计指标 -- **总诊单数**:该时间范围内的所有诊单 -- **已挂号**:状态为"已预约"的诊单数量 -- **已完成**:状态为"已完成"的诊单数量 -- **已过号**:状态为"已过号"的诊单数量 -- **已取消**:状态为"已取消"的诊单数量 -- **完成率**:已完成数量 / 总诊单数 × 100% - -### 4. 完成率颜色标识 -- 绿色:完成率 ≥ 80% -- 蓝色:完成率 ≥ 60% -- 橙色:完成率 ≥ 40% -- 红色:完成率 < 40% - -## 使用方法 - -1. 访问医生统计页面(路由:`/doctor/tongji`) -2. 选择时间范围(今天/最近7天/最近30天/自定义) -3. 可选:选择特定医生 -4. 点击"查询"按钮获取统计数据 -5. 查看统计表格中的各项数据 - -## API接口 - -### 获取医生诊单统计 -- **接口地址**:`GET /doctor.statistics/lists` -- **请求参数**: - - `doctor_id`(可选):医生ID - - `time_type`(必填):时间类型(today/week/month/custom) - - `start_date`(可选):开始日期(time_type=custom时必填) - - `end_date`(可选):结束日期(time_type=custom时必填) - -- **返回数据**: -```json -{ - "code": 1, - "msg": "success", - "data": { - "lists": [ - { - "doctor_id": 1, - "doctor_name": "张医生", - "total_count": 100, - "registered_count": 20, - "completed_count": 60, - "missed_count": 10, - "cancelled_count": 10, - "completion_rate": 60.00, - "time_range": "今天 (2024-01-01)" - } - ] - } -} -``` - -## 数据库依赖 - -该功能依赖以下数据表: -- `la_doctor_appointment`:医生预约表 -- `la_admin`:管理员表(医生信息) -- `la_admin_role`:管理员角色关联表(用于识别医生角色) - -### 医生角色识别 -系统通过 `la_admin_role` 表来识别医生角色: -- 查询 `la_admin_role` 表中 `role_id=1` 的记录 -- 获取对应的 `admin_id` 列表 -- 这些 `admin_id` 对应的管理员即为医生 - -### 预约状态说明 -- 1:已预约(已挂号) -- 2:已取消 -- 3:已完成 -- 4:已过号 - -## 注意事项 - -1. 统计数据基于 `appointment_date` 字段进行时间范围筛选 -2. 只统计 `role_id=1` 的管理员(医生角色) -3. 只统计未删除且未禁用的医生 -4. 完成率保留两位小数 - -## 后续优化建议 - -1. 添加数据导出功能(Excel) -2. 添加图表展示(柱状图、折线图) -3. 添加同比、环比分析 -4. 添加医生排名功能 -5. 添加更多维度的统计(按科室、按时段等) diff --git a/DYNAMIC_DOSE_COUNT_LABEL.md b/DYNAMIC_DOSE_COUNT_LABEL.md deleted file mode 100644 index b71ac497..00000000 --- a/DYNAMIC_DOSE_COUNT_LABEL.md +++ /dev/null @@ -1,204 +0,0 @@ -# Dynamic Dose Count Label (动态剂数标签) - -## Feature -Made the dose count label dynamic based on the selected dose unit. When the user selects a different unit (剂/丸/袋/盒/瓶/膏/贴), the label automatically updates to match. - -## Changes Made - -### File: `admin/src/views/consumer/prescription/order_list.vue` - -#### 1. Added Computed Property -Added a computed property that generates the label dynamically: - -```typescript -// 动态剂数标签(根据剂量单位变化) -const doseCountLabel = computed(() => { - const unit = editForm.dose_unit || '剂' - return `${unit}数` -}) -``` - -#### 2. Updated Form Item Label -Changed from static label to dynamic computed property: - -**Before:** -```vue - - - -``` - -**After:** -```vue - - - -``` - -## How It Works - -### Label Generation Logic -```typescript -const unit = editForm.dose_unit || '剂' // Get current unit, default to '剂' -return `${unit}数` // Append '数' to create label -``` - -### Examples - -| Selected Unit | Label Display | -|---------------|---------------| -| 剂 | 剂数 | -| 丸 | 丸数 | -| 袋 | 袋数 | -| 盒 | 盒数 | -| 瓶 | 瓶数 | -| 膏 | 膏数 | -| 贴 | 贴数 | - -### Reactive Updates -The label updates automatically when: -1. User selects a different dose unit from the dropdown -2. Form is loaded with existing data -3. Form is reset or cleared - -## User Experience - -### Before: -- Label always shows "剂数" regardless of selected unit -- Confusing when unit is "贴" but label says "剂数" - -### After: -- Label dynamically matches the selected unit -- Shows "贴数" when unit is "贴" -- Shows "丸数" when unit is "丸" -- More intuitive and consistent - -## Implementation Details - -### Computed Property Benefits -1. **Reactive**: Automatically updates when `editForm.dose_unit` changes -2. **Efficient**: Only recalculates when dependency changes -3. **Clean**: No need for watchers or manual updates - -### Default Value -Uses `'剂'` as default when `dose_unit` is empty or undefined: -```typescript -const unit = editForm.dose_unit || '剂' -``` - -### Placeholder Update -Also updated the placeholder to match the label: -```vue -:placeholder="doseCountLabel" -``` - -This ensures consistency between the label and placeholder text. - -## Testing Steps - -### 1. Test Label Changes -1. Open order edit dialog -2. Check initial label (should be "剂数" if dose_unit is "剂") -3. Change dose unit to "贴" -4. Verify label changes to "贴数" -5. Try all other units and verify labels update correctly - -### 2. Test with Existing Data -1. Edit an order that has dose_unit = "丸" -2. Verify label shows "丸数" when dialog opens -3. Change to different unit -4. Verify label updates immediately - -### 3. Test Default Behavior -1. Create a new order (dose_unit is empty) -2. Verify label shows "剂数" (default) -3. Select a unit -4. Verify label updates - -### 4. Test Placeholder -1. Clear the dose_count field -2. Verify placeholder text matches the label -3. Change dose unit -4. Verify placeholder updates with label - -## Edge Cases Handled - -### Empty/Undefined Unit -```typescript -const unit = editForm.dose_unit || '剂' -``` -Falls back to '剂' if unit is empty, null, or undefined. - -### Form Reset -When form is reset, the computed property automatically recalculates based on the new (or default) value. - -### Multiple Edits -Label updates correctly even when editing multiple orders in sequence without closing the dialog. - -## Related Code - -### Form Structure -```vue - - - - - - - - - - - - - - - - - - -``` - -### Data Flow -``` -User selects dose_unit → editForm.dose_unit changes → -doseCountLabel computed property recalculates → -Label and placeholder update in UI -``` - -## Future Enhancements - -### Possible Improvements -1. **Validation Message**: Update validation messages to use dynamic unit -2. **Display in Detail View**: Show unit-specific label in order detail view -3. **Internationalization**: Support multiple languages for unit names -4. **Custom Units**: Allow admin to configure custom dose units - -### Example: Dynamic Validation -```typescript -const doseCountRules = computed(() => [ - { required: true, message: `请输入${editForm.dose_unit || '剂'}数`, trigger: 'blur' } -]) -``` - -## Summary - -✅ Added dynamic label that changes based on selected dose unit -✅ Label updates automatically when unit changes -✅ Placeholder also updates to match label -✅ Default to "剂数" when unit is empty -✅ Handles all 7 dose units (剂/丸/袋/盒/瓶/膏/贴) -✅ Reactive and efficient using computed property -✅ Better user experience with consistent terminology - -The dose count label now dynamically reflects the selected unit, making the form more intuitive and user-friendly! diff --git a/EXPRESS_TRACKING_AUTO_LOAD.md b/EXPRESS_TRACKING_AUTO_LOAD.md deleted file mode 100644 index a1428999..00000000 --- a/EXPRESS_TRACKING_AUTO_LOAD.md +++ /dev/null @@ -1,299 +0,0 @@ -# 物流轨迹自动加载优化 - -## 优化目标 - -将物流轨迹从"手动点击查询"改为"打开详情自动加载",提升用户体验: -1. 打开订单详情时自动显示物流轨迹 -2. 无需额外点击"查询轨迹"按钮 -3. 保留"刷新轨迹"按钮用于手动更新 - -## 用户体验对比 - -### 优化前 -``` -用户操作流程: -1. 点击订单查看详情 -2. 等待详情加载 -3. 找到"查询轨迹"按钮 -4. 点击"查询轨迹" -5. 等待轨迹加载 -6. 查看物流信息 - -总步骤:6步 -``` - -### 优化后 -``` -用户操作流程: -1. 点击订单查看详情 -2. 等待详情和轨迹自动加载 -3. 查看物流信息 - -总步骤:3步 -减少:50%操作步骤 -``` - -## 代码修改 - -### 1. 自动加载逻辑 - -**文件**:`admin/src/views/consumer/prescription/order_list.vue` - -在 `openDetail()` 函数中添加自动加载: - -```typescript -async function openDetail(id: number) { - detailVisible.value = true - detailData.value = null - logisticsTracePayload.value = null - detailLogisticsExpress.value = 'auto' - detailLoading.value = true - try { - const res: any = await prescriptionOrderDetail({ id }) - const d = res?.data ?? res ?? null - detailData.value = d - if (d) { - detailLogisticsExpress.value = String(d.express_company || 'auto') || 'auto' - - // 如果有快递单号,自动加载物流轨迹 - if (String(d.tracking_number || '').trim()) { - fetchLogisticsTrace() - } - } - } catch (e: any) { - feedback.msgError(e?.message || '加载失败') - detailVisible.value = false - } finally { - detailLoading.value = false - } -} -``` - -**关键点**: -- 检查订单是否有快递单号 -- 有快递单号时自动调用 `fetchLogisticsTrace()` -- 无快递单号时不调用,避免无效请求 - -### 2. UI调整 - -#### 按钮改为"刷新轨迹" - -```vue - - - 刷新轨迹 - -``` - -**改动**: -- 按钮文字:`查询轨迹` → `刷新轨迹` -- 按钮类型:`type="primary"` → `type="default"`(降低视觉优先级) -- 添加刷新图标:`` - -#### 添加加载状态提示 - -```vue -
- - 正在加载物流轨迹... -
-``` - -**作用**: -- 首次自动加载时显示加载提示 -- 已有数据时不显示(避免闪烁) -- 使用旋转动画图标 - -#### 导入图标 - -```typescript -import { Refresh, Loading } from '@element-plus/icons-vue' -``` - -## 工作流程 - -### 完整流程 - -``` -用户点击"查看详情" - ↓ -打开详情弹窗 - ↓ -加载订单详情 - ↓ -检查是否有快递单号? - ↓ 有 -自动调用 fetchLogisticsTrace() - ↓ -显示"正在加载物流轨迹..." - ↓ -查询数据库(优先) - ↓ -有数据? - ↓ 有 -显示物流轨迹(13条记录) - ↓ -显示数据来源和更新时间 -``` - -### 手动刷新流程 - -``` -用户点击"刷新轨迹"按钮 - ↓ -重新调用 fetchLogisticsTrace() - ↓ -查询数据库(优先) - ↓ -无数据或需要更新? - ↓ 是 -调用快递100 API - ↓ -更新数据库 - ↓ -显示最新轨迹 -``` - -## 性能优化 - -### 1. 并行加载 - -订单详情和物流轨迹是并行加载的: -- 订单详情:`prescriptionOrderDetail()` -- 物流轨迹:`fetchLogisticsTrace()`(在详情加载完成后立即触发) - -虽然不是完全并行,但由于物流查询从数据库读取(< 50ms),总体加载时间几乎不增加。 - -### 2. 条件加载 - -只有在有快递单号时才加载物流轨迹: -```typescript -if (String(d.tracking_number || '').trim()) { - fetchLogisticsTrace() -} -``` - -避免无效请求。 - -### 3. 数据库优先 - -物流查询优先从数据库读取: -- 数据库查询:< 50ms -- API查询:> 1000ms - -大部分情况下都能从数据库快速获取数据。 - -## 用户体验提升 - -### 1. 减少操作步骤 -- 从6步减少到3步 -- 减少50%操作 - -### 2. 信息即时可见 -- 打开详情即可看到物流信息 -- 无需额外操作 - -### 3. 保留手动控制 -- "刷新轨迹"按钮用于手动更新 -- 切换快递公司后可重新查询 - -### 4. 清晰的状态反馈 -- 加载中:显示"正在加载物流轨迹..." -- 加载完成:显示轨迹列表 -- 数据来源:显示"[数据库]"或"[API实时]" -- 更新时间:显示"最后更新:2026-04-07 17:14:16" - -## 边界情况处理 - -### 1. 无快递单号 -- 不调用物流查询 -- 不显示物流轨迹区域 - -### 2. 数据库无数据 -- 自动调用快递100 API -- 显示"[API实时]"标签 - -### 3. 查询失败 -- 显示错误提示 -- 保留"刷新轨迹"按钮供重试 - -### 4. 已签收订单 -- 显示完整历史轨迹 -- 显示"已签收"状态 -- 数据不会再自动更新(定时任务已停止) - -## 测试场景 - -### 场景1:有快递单号且数据库有数据 -``` -1. 打开订单详情 -2. 自动显示"正在加载物流轨迹..." -3. < 50ms 后显示13条轨迹 -4. 显示"[数据库]"标签 -5. 显示"最后更新:2026-04-07 17:14:16" -``` - -### 场景2:有快递单号但数据库无数据 -``` -1. 打开订单详情 -2. 自动显示"正在加载物流轨迹..." -3. 调用快递100 API(> 1s) -4. 显示轨迹 -5. 显示"[API实时]"标签 -6. 数据保存到数据库 -``` - -### 场景3:无快递单号 -``` -1. 打开订单详情 -2. 不显示物流轨迹区域 -3. 不发起任何物流查询请求 -``` - -### 场景4:手动刷新 -``` -1. 订单详情已打开,轨迹已显示 -2. 点击"刷新轨迹"按钮 -3. 重新查询(优先数据库) -4. 更新显示 -``` - -## 相关文件 - -### 修改的文件 -- `admin/src/views/consumer/prescription/order_list.vue` - 订单列表页面 - -### 相关文档 -- `EXPRESS_TRACKING_DATABASE_QUERY.md` - 数据库查询优化 -- `EXPRESS_TRACKING_SYNC_COMPLETE.md` - 同步完成报告 -- `EXPRESS_TRACKING_SYSTEM.md` - 系统架构 - -## 总结 - -✅ 打开详情自动加载物流轨迹 -✅ 减少50%用户操作步骤 -✅ 保留手动刷新功能 -✅ 添加加载状态提示 -✅ 优化按钮文字和样式 -✅ 条件加载,避免无效请求 -✅ 数据库优先,快速响应 - -**用户体验提升**: -- 操作步骤:6步 → 3步 -- 加载时间:< 50ms(数据库) -- 信息可见性:立即可见 -- 操作便捷性:无需额外点击 - -**下一步**: -1. 前端测试自动加载功能 -2. 验证各种边界情况 -3. 收集用户反馈 -4. 可选:添加"自动刷新"开关(用户可选择是否自动加载) diff --git a/EXPRESS_TRACKING_COMMAND_FIX.md b/EXPRESS_TRACKING_COMMAND_FIX.md deleted file mode 100644 index 0da9bb29..00000000 --- a/EXPRESS_TRACKING_COMMAND_FIX.md +++ /dev/null @@ -1,199 +0,0 @@ -# 物流追踪定时任务修复报告 - -## 问题诊断 - -### 1. PHP语法错误 -**问题**:`server/app/command/ExpressAutoUpdate.php` 文件中的crontab示例使用了 `*/10`,导致PHP解析器将其识别为代码而非注释。 - -**错误信息**: -``` -syntax error, unexpected token "*" -``` - -**原因**:注释中的 `*/10` 被PHP解析器误认为是多行注释的结束符。 - -**解决方案**:将crontab示例从 `*/10 * * * *` 改为 `0,10,20,30,40,50 * * * *`(效果相同,每10分钟执行)。 - -### 2. 命名空间路径错误 -**问题**:`server/config/console.php` 中的命名空间路径使用单反斜杠。 - -**错误配置**: -```php -'express:auto-update' => 'app\command\ExpressAutoUpdate', -``` - -**正确配置**: -```php -'express:auto-update' => 'app\\command\\ExpressAutoUpdate', -``` - -**原因**:PHP字符串中的反斜杠需要转义。 - -### 3. PHP版本检查 -**问题**:系统PHP版本为8.0.2,但Composer要求8.1.0+。 - -**临时解决方案**:注释掉 `server/vendor/composer/platform_check.php` 中的版本检查(仅用于测试)。 - -**生产环境建议**:升级PHP到8.1+版本。 - -## 已修复的文件 - -### 1. server/app/command/ExpressAutoUpdate.php -```php -/** - * 物流自动更新定时任务 - * - * 使用方法: - * php think express:auto-update - * - * 配置crontab(每10分钟执行一次): - * 0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1 - */ -``` - -### 2. server/config/console.php -```php -'commands' => [ - // ... 其他命令 - // 物流自动更新 - 'express:auto-update' => 'app\\command\\ExpressAutoUpdate', -], -``` - -### 3. server/vendor/composer/platform_check.php(临时) -```php -// Temporarily disabled for testing -// if (!(PHP_VERSION_ID >= 80100)) { -// $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; -// } -``` - -## 测试结果 - -### 命令执行测试 -```bash -$ php think express:auto-update -开始自动更新物流信息... -更新完成! -总数: 0 -成功: 0 -失败: 0 -耗时: 0.21秒 -``` - -✅ 命令执行成功,无语法错误。 -✅ 返回码为0,表示正常退出。 -✅ 总数为0是正常的,因为数据库中还没有需要更新的物流记录。 - -## 过滤逻辑验证 - -### 定时任务是否过滤已签收订单? - -**答案:是的,已正确过滤。** - -查看 `ExpressTrackingService::autoUpdateBatch()` 方法: - -```php -// 查询需要更新的记录 -// 条件: -// 1. auto_update = 1(启用自动更新) -// 2. next_update_time <= now(到达更新时间) -// 3. is_signed = 0(未签收) -// 4. current_state 不是终态(排除已签收、退签、拒签) -$list = ExpressTracking::where('auto_update', 1) - ->where('next_update_time', '<=', $now) - ->where('is_signed', 0) - ->whereNotIn('current_state', [ - ExpressTracking::STATE_SIGNED, // 3 - 已签收 - ExpressTracking::STATE_RETURN_SIGNED, // 4 - 退签 - ExpressTracking::STATE_REJECTED, // 5 - 拒签 - ]) - ->whereNull('delete_time') - ->limit($limit) - ->select(); -``` - -**过滤条件说明**: -1. `is_signed = 0` - 只查询未签收的记录 -2. `whereNotIn('current_state', [3, 4, 5])` - 排除所有终态(已签收、退签、拒签) -3. 双重保险,确保不会浪费查询次数 - -## 下一步操作 - -### 1. 执行数据库迁移 -```bash -# Windows -cd server -php think migrate:run - -# 或手动执行SQL -mysql -u root -p your_database < database/migrations/create_express_tracking_tables.sql -``` - -### 2. 集成到订单系统 -在订单创建/编辑时调用: -```php -use app\common\service\ExpressTrackingService; - -// 创建或更新物流追踪 -ExpressTrackingService::createOrUpdate([ - 'order_id' => $orderId, - 'order_type' => 'prescription', - 'tracking_number' => $trackingNumber, - 'express_company' => $expressCompany, - 'recipient_phone' => $phone, - 'recipient_name' => $name, - 'recipient_address' => $address, -]); -``` - -### 3. 配置定时任务 - -#### Windows(任务计划程序) -```batch -# 创建任务(每10分钟执行) -schtasks /create /tn "ExpressAutoUpdate" /tr "php D:\web\zyt\server\think express:auto-update" /sc minute /mo 10 -``` - -#### Linux(crontab) -```bash -# 编辑crontab -crontab -e - -# 添加以下行(每10分钟执行) -0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1 -``` - -### 4. 手动测试查询 -```bash -# 测试单次查询 -php think express:auto-update - -# 查看日志 -tail -f runtime/log/202X-XX-XX.log -``` - -## 注意事项 - -1. **PHP版本**:生产环境建议升级到PHP 8.1+ -2. **快递100账号**:确保账号已充值,余额充足 -3. **查询频率**:默认每10分钟查询一次,可根据需要调整 -4. **查询限制**:每次最多处理50条记录,避免超时 -5. **终态停止**:已签收/退签/拒签的订单会自动停止更新 - -## 相关文档 - -- [EXPRESS_TRACKING_SYSTEM.md](EXPRESS_TRACKING_SYSTEM.md) - 系统架构和设计 -- [EXPRESS_TRACKING_QUICKSTART.md](EXPRESS_TRACKING_QUICKSTART.md) - 快速开始指南 -- [EXPRESS_TRACKING_FIXES.md](EXPRESS_TRACKING_FIXES.md) - 修复记录 -- [KUAIDI100_TROUBLESHOOTING.md](KUAIDI100_TROUBLESHOOTING.md) - 快递100故障排查 - -## 总结 - -✅ 修复了PHP语法错误(crontab注释) -✅ 修复了命名空间路径错误(双反斜杠转义) -✅ 验证了过滤逻辑(已正确过滤已签收订单) -✅ 命令可以正常执行 -✅ 临时绕过PHP版本检查(仅测试用) - -系统已就绪,可以开始集成到订单系统并配置定时任务。 diff --git a/EXPRESS_TRACKING_DATABASE_QUERY.md b/EXPRESS_TRACKING_DATABASE_QUERY.md deleted file mode 100644 index f8a2a6da..00000000 --- a/EXPRESS_TRACKING_DATABASE_QUERY.md +++ /dev/null @@ -1,328 +0,0 @@ -# 物流追踪数据库查询优化 - -## 优化目标 - -将物流查询从"每次调用API"改为"优先查询数据库",实现: -1. 提高查询速度(数据库查询 < 50ms,API查询 > 1s) -2. 节省API调用次数(快递100按次收费) -3. 显示完整历史轨迹(数据库保存所有历史记录) -4. 降低API限流风险 - -## 实现方案 - -### 查询优先级 - -``` -用户点击"查询轨迹" - ↓ -1. 先查询数据库(zyt_express_tracking + zyt_express_trace) - ↓ - 有数据? - ↓ 是 - 返回数据库数据(标记 source: database) - ↓ 否 -2. 调用快递100 API - ↓ - 返回API数据(标记 source: api) -``` - -### 数据流转 - -``` -订单创建/编辑 - ↓ -ExpressTrackingService::createOrUpdate() - ↓ -创建追踪记录 + 立即查询一次 - ↓ -保存到数据库(tracking + traces) - ↓ -定时任务每10分钟自动更新 - ↓ -用户查询时直接从数据库读取 -``` - -## 代码修改 - -### 1. 后端服务层 - -**文件**:`server/app/common/service/ExpressTrackingService.php` - -新增方法: -```php -/** - * 根据快递单号获取追踪详情(包含轨迹) - */ -public static function getDetailByTrackingNumber(string $trackingNumber): ?array -{ - $tracking = ExpressTracking::with(['traces' => function($query) { - // 按时间倒序排列(最新的在前) - $query->order('trace_time_stamp', 'desc'); - }]) - ->where('tracking_number', $trackingNumber) - ->whereNull('delete_time') - ->find(); - - if (!$tracking) { - return null; - } - - $data = $tracking->toArray(); - $data['traces'] = $tracking->traces ? $tracking->traces->toArray() : []; - - return $data; -} -``` - -### 2. 后端逻辑层 - -**文件**:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -修改 `logisticsTrace()` 方法: -```php -// 优先从数据库查询物流追踪信息 -$trackingData = \app\common\service\ExpressTrackingService::getDetailByTrackingNumber($num); - -if ($trackingData && !empty($trackingData['traces'])) { - // 从数据库获取到数据,直接返回 - $payload = [ - 'carrier' => $trackingData['express_company'], - 'carrier_label' => $trackingData['express_company_name'], - 'kuaidi_com' => $trackingData['express_company'], - 'traces' => array_map(function($trace) { - return [ - 'time' => $trace['trace_time'], - 'ftime' => $trace['trace_time'], - 'context' => $trace['trace_context'], - 'location' => $trace['location'], - 'status' => $trace['status'], - 'statusCode' => $trace['status_code'], - ]; - }, $trackingData['traces']), - 'state' => $trackingData['current_state'], - 'state_text' => $trackingData['current_state_text'], - 'source' => 'database', - 'hint' => '', - 'official_url' => '', - 'last_query_time' => date('Y-m-d H:i:s', $trackingData['last_query_time']), - 'query_count' => $trackingData['query_count'], - ]; -} else { - // 数据库没有数据,调用快递100 API - $payload = ExpressTrackService::query($ec, $num, (string) ($row->recipient_phone ?? '')); - $payload['source'] = 'api'; -} -``` - -### 3. 前端显示 - -**文件**:`admin/src/views/consumer/prescription/order_list.vue` - -添加数据来源和更新时间显示: -```vue -
- 物流状态:{{ logisticsTracePayload.state_text }} - - ({{ logisticsTracePayload.carrier_label }}) - - - [{{ logisticsTracePayload.source === 'database' ? '数据库' : 'API实时' }}] - - - 最后更新:{{ logisticsTracePayload.last_query_time }} - -
-``` - -更新说明文字: -```vue -

- 优先从数据库查询历史轨迹(快速响应),数据库无记录时调用快递100实时查询。 - 支持顺丰、京东、极兔速递等快递公司。 - 顺丰查询建议填写正确收货手机后四位(本单收货手机已自动传入)。 -

-``` - -## 测试结果 - -### 数据库查询测试 - -```bash -$ php test_tracking_query.php - -======================================== -测试物流追踪查询(从数据库) -======================================== - -[1] 查询追踪主记录... -✓ 找到追踪记录 - 快递公司: 京东快递(单号识别) - 当前状态: 已签收 - 是否签收: 是 - 查询次数: 1 - 最后查询: 2026-04-07 17:14:16 - -[2] 查询轨迹明细... -✓ 找到 13 条轨迹记录 - -最新3条轨迹: - [2026-02-24 19:12:51] 您的快件已送达至【柜子】... - [2026-02-24 15:51:58] 您的快件正在派送中... - [2026-02-24 15:51:11] 您的快件已到达【郑州国企站】。 - -======================================== -测试完成! -======================================== -✓ 数据库查询正常 -✓ 数据格式兼容API -✓ 可以直接返回给前端 -``` - -### 性能对比 - -| 查询方式 | 响应时间 | API消耗 | 数据完整性 | -|---------|---------|---------|-----------| -| 数据库查询 | < 50ms | 0次 | 完整历史 | -| API查询 | > 1000ms | 1次 | 仅当前 | - -### 数据示例 - -**数据库返回**: -```json -{ - "carrier": "auto", - "carrier_label": "京东快递(单号识别)", - "traces": [ - { - "time": "2026-02-24 19:12:51", - "context": "您的快件已送达至【柜子】..." - }, - // ... 13条完整轨迹 - ], - "state": "3", - "state_text": "已签收", - "source": "database", - "last_query_time": "2026-04-07 17:14:16", - "query_count": "1" -} -``` - -## 工作流程 - -### 新订单流程 - -1. **订单创建/编辑** - ```php - ExpressTrackingService::createOrUpdate([ - 'order_id' => $orderId, - 'tracking_number' => $trackingNumber, - // ... - ]); - ``` - - 创建追踪记录 - - 立即查询一次API - - 保存到数据库 - -2. **定时任务自动更新** - ```bash - php think express:auto-update - ``` - - 每10分钟执行 - - 只更新未签收订单 - - 更新轨迹到数据库 - -3. **用户查询** - - 点击"查询轨迹" - - 优先从数据库读取 - - 秒级响应 - -### 现有订单同步 - -```bash -# 同步现有订单快递单号 -php think express:sync - -# 会自动: -# 1. 查询订单表中的快递单号 -# 2. 创建追踪记录 -# 3. 立即查询一次API -# 4. 保存到数据库 -``` - -## 优势分析 - -### 1. 性能提升 -- 数据库查询:< 50ms -- API查询:> 1000ms -- 提升 20倍以上 - -### 2. 成本节约 -- 每次用户查询不消耗API次数 -- 只有定时任务消耗API(每10分钟1次) -- 假设100个订单,每天查询10次: - - 优化前:100 × 10 = 1000次/天 - - 优化后:100 × 144 = 14400次/天(定时任务) + 0次(用户查询) - - 实际:只有未签收订单才会定时更新,已签收订单不消耗 - -### 3. 用户体验 -- 查询速度快(秒级响应) -- 显示完整历史轨迹 -- 显示数据来源和更新时间 -- 透明化数据状态 - -### 4. 数据完整性 -- 保存所有历史轨迹 -- 记录状态变更 -- 可追溯查询历史 -- 支持数据分析 - -## 注意事项 - -### 1. 数据同步 -- 新订单需要集成 `ExpressTrackingService::createOrUpdate()` -- 现有订单需要执行 `php think express:sync` - -### 2. 定时任务 -- 必须配置定时任务(每10分钟) -- 只更新未签收订单 -- 签收后自动停止更新 - -### 3. 数据时效性 -- 数据库数据最多延迟10分钟 -- 如需实时数据,可添加"强制刷新"按钮 -- 已签收订单不会再更新 - -### 4. 兼容性 -- 数据格式完全兼容原API -- 前端无需修改查询逻辑 -- 只需添加显示字段 - -## 相关文件 - -### 核心文件 -- `server/app/common/service/ExpressTrackingService.php` - 服务层 -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 逻辑层 -- `admin/src/views/consumer/prescription/order_list.vue` - 前端页面 - -### 测试脚本 -- `server/test_tracking_query.php` - 数据库查询测试 - -### 文档 -- `EXPRESS_TRACKING_SYSTEM.md` - 系统架构 -- `EXPRESS_TRACKING_SYNC_COMPLETE.md` - 同步完成报告 -- `EXPRESS_TRACKING_DATABASE_QUERY.md` - 本文档 - -## 总结 - -✅ 优先从数据库查询,提升20倍性能 -✅ 节省API调用次数,降低成本 -✅ 显示完整历史轨迹,提升用户体验 -✅ 数据格式兼容,无需修改前端逻辑 -✅ 添加数据来源和更新时间显示 -✅ 测试通过,13条轨迹记录正常显示 - -**下一步**: -1. 在订单创建/编辑时集成追踪服务 -2. 配置定时任务(每10分钟) -3. 前端测试查询功能 -4. 可选:添加"强制刷新"按钮(调用API实时更新) diff --git a/EXPRESS_TRACKING_FIXES.md b/EXPRESS_TRACKING_FIXES.md deleted file mode 100644 index 57ecf39b..00000000 --- a/EXPRESS_TRACKING_FIXES.md +++ /dev/null @@ -1,365 +0,0 @@ -# 物流追踪系统 - 问题修复 - -## 已修复的问题 - -### 1. 命令未注册错误 ✅ - -**问题:** -``` -[InvalidArgumentException] -There are no commands defined in the "express" namespace. -``` - -**原因:** -命令未在 `server/config/console.php` 中注册。 - -**解决方案:** -已在 `server/config/console.php` 中添加命令注册: - -```php -'commands' => [ - // ... 其他命令 - 'express:auto-update' => 'app\command\ExpressAutoUpdate', -], -``` - -**验证:** -```bash -cd server -php think express:auto-update -``` - ---- - -### 2. 已签收订单过滤优化 ✅ - -**问题:** -定时任务可能会处理已签收的快递,浪费查询次数。 - -**解决方案:** -优化了 `ExpressTrackingService::autoUpdateBatch()` 方法的查询条件: - -```php -$list = ExpressTracking::where('auto_update', 1) - ->where('next_update_time', '<=', $now) - ->where('is_signed', 0) // 【新增】未签收 - ->whereNotIn('current_state', [ // 【新增】排除终态 - ExpressTracking::STATE_SIGNED, // 已签收 - ExpressTracking::STATE_RETURN_SIGNED, // 退签 - ExpressTracking::STATE_REJECTED, // 拒签 - ]) - ->whereNull('delete_time') - ->limit($limit) - ->select(); -``` - -**过滤规则:** - -| 条件 | 说明 | -|------|------| -| `auto_update = 1` | 启用自动更新 | -| `next_update_time <= now` | 到达更新时间 | -| `is_signed = 0` | 未签收 | -| `current_state NOT IN (3,4,14)` | 排除终态(已签收、退签、拒签) | -| `delete_time IS NULL` | 未删除 | - -**会被处理的状态:** -- 0: 在途 -- 1: 揽收 -- 2: 疑难 -- 5: 派件中 -- 6: 退回 -- 7: 转投 -- 8: 清关 -- 10: 待清关 -- 11: 清关中 -- 12: 已清关 -- 13: 清关异常 - -**不会被处理的状态:** -- 3: 已签收 ❌ -- 4: 退签 ❌ -- 14: 拒签 ❌ - ---- - -## 自动停止更新机制 - -### 触发条件 - -当快递状态变为以下任一状态时,系统会自动停止更新: - -```php -// 在 ExpressTrackingService::queryAndUpdate() 中 -if (ExpressTracking::isFinalState($tracking->current_state)) { - $tracking->is_signed = 1; - $tracking->sign_time = $now; - $tracking->auto_update = 0; // 自动停止更新 -} -``` - -### 终态判断 - -```php -public static function isFinalState(string $state): bool -{ - return in_array($state, [ - self::STATE_SIGNED, // 3: 已签收 - self::STATE_RETURN_SIGNED, // 4: 退签 - self::STATE_REJECTED, // 14: 拒签 - ], true); -} -``` - ---- - -## 双重保护机制 - -系统采用双重保护,确保已签收的快递不会被重复查询: - -### 第一层:查询时过滤 - -在 `autoUpdateBatch()` 方法中,查询时就排除了终态订单: - -```php -->where('is_signed', 0) -->whereNotIn('current_state', ['3', '4', '14']) -``` - -### 第二层:更新时自动停止 - -在 `queryAndUpdate()` 方法中,当检测到签收时自动停止: - -```php -if (ExpressTracking::isFinalState($tracking->current_state)) { - $tracking->auto_update = 0; - $tracking->save(); -} -``` - ---- - -## 测试验证 - -### 1. 测试命令注册 - -```bash -cd server -php think list - -# 应该能看到: -# express -# express:auto-update 自动更新物流追踪信息 -``` - -### 2. 测试过滤逻辑 - -```bash -cd server -php test_express_auto_update.php - -# 查看输出,确认过滤规则正确 -``` - -### 3. 测试实际执行 - -```bash -cd server -php think express:auto-update - -# 应该看到: -# 开始自动更新物流信息... -# 更新完成! -# 总数: X -# 成功: X -# 失败: 0 -# 耗时: X秒 -``` - ---- - -## 性能优化 - -### 查询优化 - -通过添加过滤条件,减少了不必要的查询: - -**优化前:** -- 查询所有 `auto_update = 1` 的记录 -- 包括已签收的快递 -- 浪费查询次数 - -**优化后:** -- 只查询未签收且未到达终态的记录 -- 自动排除已签收的快递 -- 节省查询次数和API费用 - -### 索引建议 - -确保以下字段有索引: - -```sql --- 复合索引(最重要) -CREATE INDEX idx_auto_update_query -ON zyt_express_tracking(auto_update, next_update_time, is_signed, current_state); - --- 单字段索引 -CREATE INDEX idx_is_signed ON zyt_express_tracking(is_signed); -CREATE INDEX idx_current_state ON zyt_express_tracking(current_state); -``` - ---- - -## 监控建议 - -### 1. 监控自动更新执行情况 - -```bash -# 查看定时任务日志 -tail -f server/runtime/log/info.log | grep "Auto update" - -# 查看错误日志 -tail -f server/runtime/log/error.log | grep "express" -``` - -### 2. 统计查询情况 - -```sql --- 查看自动更新的记录数 -SELECT COUNT(*) as count -FROM zyt_express_tracking -WHERE auto_update = 1 -AND is_signed = 0 -AND current_state NOT IN ('3', '4', '14') -AND delete_time IS NULL; - --- 查看已签收的记录数 -SELECT COUNT(*) as count -FROM zyt_express_tracking -WHERE is_signed = 1; - --- 查看各状态分布 -SELECT - current_state, - current_state_text, - COUNT(*) as count, - SUM(auto_update) as auto_update_count -FROM zyt_express_tracking -WHERE delete_time IS NULL -GROUP BY current_state, current_state_text -ORDER BY count DESC; -``` - -### 3. 查询日志分析 - -```sql --- 查看最近的查询记录 -SELECT - tracking_number, - query_type, - is_success, - error_message, - FROM_UNIXTIME(query_time) as query_time -FROM zyt_express_query_log -ORDER BY query_time DESC -LIMIT 20; - --- 统计自动查询成功率 -SELECT - DATE(FROM_UNIXTIME(query_time)) as date, - COUNT(*) as total, - SUM(is_success) as success, - ROUND(SUM(is_success) / COUNT(*) * 100, 2) as success_rate -FROM zyt_express_query_log -WHERE query_type = 'auto' -GROUP BY date -ORDER BY date DESC -LIMIT 7; -``` - ---- - -## 常见问题 - -### Q1: 命令执行后显示 "总数: 0"? - -**A:** 这是正常的,说明: -1. 没有需要更新的快递记录 -2. 或所有快递都已签收 -3. 或还没到更新时间 - -**检查:** -```sql -SELECT - COUNT(*) as total, - SUM(CASE WHEN auto_update = 1 THEN 1 ELSE 0 END) as auto_update_enabled, - SUM(CASE WHEN is_signed = 1 THEN 1 ELSE 0 END) as signed, - SUM(CASE WHEN next_update_time <= UNIX_TIMESTAMP() THEN 1 ELSE 0 END) as need_update -FROM zyt_express_tracking -WHERE delete_time IS NULL; -``` - -### Q2: 已签收的快递还在被查询? - -**A:** 检查以下几点: -1. 确认代码已更新到最新版本 -2. 检查 `is_signed` 字段是否正确设置 -3. 检查 `auto_update` 字段是否为 0 - -**修复:** -```sql --- 手动修复已签收但未停止更新的记录 -UPDATE zyt_express_tracking -SET auto_update = 0 -WHERE current_state IN ('3', '4', '14') -AND auto_update = 1; -``` - -### Q3: 如何手动停止某个快递的自动更新? - -**A:** -```sql -UPDATE zyt_express_tracking -SET auto_update = 0 -WHERE tracking_number = 'JD0230761381812'; -``` - -或通过代码: -```php -$tracking = ExpressTracking::where('tracking_number', 'JD0230761381812')->find(); -$tracking->auto_update = 0; -$tracking->save(); -``` - ---- - -## 总结 - -### 已修复 - -1. ✅ 命令注册问题 -2. ✅ 已签收订单过滤 -3. ✅ 双重保护机制 -4. ✅ 性能优化 - -### 优势 - -1. **节省费用** - 不查询已签收的快递 -2. **提高效率** - 只处理需要更新的记录 -3. **自动停止** - 签收后自动停止更新 -4. **双重保护** - 查询和更新两层过滤 - -### 下一步 - -1. ✅ 执行 `php think express:auto-update` 测试 -2. ✅ 配置crontab定时任务 -3. ✅ 监控执行情况 -4. ✅ 查看查询日志 - ---- - -## 相关文档 - -- `EXPRESS_TRACKING_SYSTEM.md` - 完整系统文档 -- `EXPRESS_TRACKING_QUICKSTART.md` - 快速开始指南 -- `KUAIDI100_ACCOUNT_ISSUE.md` - 快递100账号问题 diff --git a/EXPRESS_TRACKING_QUICKSTART.md b/EXPRESS_TRACKING_QUICKSTART.md deleted file mode 100644 index a6a8f14d..00000000 --- a/EXPRESS_TRACKING_QUICKSTART.md +++ /dev/null @@ -1,335 +0,0 @@ -# 物流追踪系统 - 快速开始 - -## 5分钟快速部署 - -### 1. 安装数据库表 - -**Linux/Mac:** -```bash -chmod +x install_express_tracking.sh -./install_express_tracking.sh -``` - -**Windows:** -```cmd -install_express_tracking.bat -``` - -**或手动执行SQL:** -```bash -mysql -u root -p your_database < server/database/migrations/create_express_tracking_tables.sql -``` - -### 2. 注册命令 - -编辑 `server/config/console.php`,在 `commands` 数组中添加: - -```php -'commands' => [ - // ... 其他命令 - 'express:auto-update' => \app\command\ExpressAutoUpdate::class, -], -``` - -### 3. 测试命令 - -```bash -cd server -php think express:auto-update -``` - -应该看到: -``` -开始自动更新物流信息... -更新完成! -总数: 0 -成功: 0 -失败: 0 -耗时: 0.05秒 -``` - -### 4. 配置定时任务 - -**Linux/Mac (crontab):** -```bash -crontab -e - -# 添加以下行(每10分钟执行) -*/10 * * * * cd /path/to/your/server && php think express:auto-update >> /dev/null 2>&1 -``` - -**Windows (计划任务):** -```cmd -schtasks /create /tn "ExpressAutoUpdate" /tr "php D:\path\to\server\think express:auto-update" /sc minute /mo 10 -``` - ---- - -## 使用示例 - -### 示例1:订单发货时创建追踪 - -```php -use app\common\service\ExpressTrackingService; - -// 在订单发货时调用 -$tracking = ExpressTrackingService::createOrUpdate([ - 'order_id' => $order->id, - 'order_type' => 'prescription', - 'tracking_number' => 'JD0230761381812', - 'express_company' => 'jd', - 'recipient_phone' => $order->recipient_phone, - 'recipient_name' => $order->recipient_name, - 'recipient_address' => $order->shipping_address, -]); - -// 系统会: -// 1. 创建追踪记录 -// 2. 立即查询物流信息 -// 3. 存储到数据库 -// 4. 开始自动更新 -``` - -### 示例2:查询物流详情 - -```php -// 获取完整物流信息 -$detail = ExpressTrackingService::getDetail($trackingId); - -// 返回数据包含: -// - 基本信息(单号、快递公司、收件人等) -// - 当前状态(在途、派件、签收等) -// - 所有轨迹记录(时间、内容、位置) -// - 状态变更历史 -``` - -### 示例3:手动更新 - -```php -// 手动触发更新 -$result = ExpressTrackingService::queryAndUpdate($trackingId, false); - -// 返回最新的物流信息 -``` - ---- - -## 集成到现有系统 - -### 修改订单创建逻辑 - -在 `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` 的 `create()` 方法中添加: - -```php -public static function create(array $params, int $adminId, array $adminInfo) -{ - // ... 原有代码 - - $order->save(); - - // 【新增】创建物流追踪 - if (!empty($params['tracking_number'])) { - \app\common\service\ExpressTrackingService::createOrUpdate([ - 'order_id' => $order->id, - 'order_type' => 'prescription', - 'tracking_number' => $params['tracking_number'], - 'express_company' => $params['express_company'] ?? 'auto', - 'recipient_phone' => $order->recipient_phone, - 'recipient_name' => $order->recipient_name, - 'recipient_address' => $order->shipping_address, - ]); - } - - return $out; -} -``` - -### 修改订单编辑逻辑 - -在 `edit()` 方法中添加: - -```php -public static function edit(array $params, int $adminId, array $adminInfo) -{ - // ... 原有代码 - - $order->save(); - - // 【新增】更新物流追踪 - if (array_key_exists('tracking_number', $params) && !empty($params['tracking_number'])) { - \app\common\service\ExpressTrackingService::createOrUpdate([ - 'order_id' => $order->id, - 'order_type' => 'prescription', - 'tracking_number' => $params['tracking_number'], - 'express_company' => $order->express_company, - 'recipient_phone' => $order->recipient_phone, - 'recipient_name' => $order->recipient_name, - 'recipient_address' => $order->shipping_address, - ]); - } - - return $out; -} -``` - -### 修改物流查询接口 - -在 `logisticsTrace()` 方法开头添加: - -```php -public static function logisticsTrace(int $id, string $expressCompanyOverride, int $adminId, array $adminInfo): ?array -{ - // ... 权限检查等原有代码 - - // 【新增】优先从数据库获取 - $tracking = \app\common\model\ExpressTracking::where('order_id', $id) - ->where('order_type', 'prescription') - ->whereNull('delete_time') - ->find(); - - if ($tracking) { - // 如果距离上次查询超过5分钟,重新查询 - if (time() - $tracking->last_query_time > 300) { - \app\common\service\ExpressTrackingService::queryAndUpdate($tracking->id, false); - $tracking->refresh(); - } - - // 返回数据库中的数据 - $traces = $tracking->traces->map(function($trace) { - return [ - 'time' => $trace->trace_time, - 'context' => $trace->trace_context, - 'status' => $trace->status, - 'location' => $trace->location, - ]; - })->toArray(); - - $payload = [ - 'carrier' => $tracking->express_company, - 'carrier_label' => $tracking->express_company_name, - 'traces' => $traces, - 'state' => $tracking->current_state, - 'state_text' => $tracking->current_state_text, - 'source' => $tracking->data_source, - 'official_urls' => ExpressTrackService::officialUrls($tracking->tracking_number), - 'tracking_number' => $tracking->tracking_number, - 'is_signed' => $tracking->is_signed, - 'estimated_arrival_time' => $tracking->estimated_arrival_time, - ]; - - return $payload; - } - - // 如果数据库中没有,走原有逻辑 - // ... 原有查询代码 -} -``` - ---- - -## 验证安装 - -### 1. 检查数据库表 - -```sql -SHOW TABLES LIKE 'zyt_express%'; - --- 应该看到4个表: --- zyt_express_tracking --- zyt_express_trace --- zyt_express_state_log --- zyt_express_query_log -``` - -### 2. 测试创建追踪 - -```php -$tracking = ExpressTrackingService::createOrUpdate([ - 'order_id' => 1, - 'order_type' => 'test', - 'tracking_number' => 'JD0230761381812', - 'express_company' => 'jd', - 'recipient_phone' => '13800138000', - 'recipient_name' => '测试', - 'recipient_address' => '测试地址', -]); - -var_dump($tracking->id); // 应该返回ID -``` - -### 3. 检查定时任务 - -```bash -# 查看crontab -crontab -l | grep express - -# 手动执行一次 -cd server && php think express:auto-update - -# 查看日志 -tail -f runtime/log/info.log -``` - ---- - -## 常见问题 - -### Q: 定时任务不执行? - -**A:** 检查crontab配置和PHP路径 -```bash -# 查看crontab -crontab -l - -# 检查PHP路径 -which php - -# 查看cron日志 -tail -f /var/log/cron -``` - -### Q: 快递100返回"找不到对应公司"? - -**A:** 账号未充值,请先充值快递100账户 -- 登录:https://www.kuaidi100.com/ -- 充值后重试 - -### Q: 如何停止某个快递的自动更新? - -**A:** -```php -$tracking = ExpressTracking::find($id); -$tracking->auto_update = 0; -$tracking->save(); -``` - -### Q: 如何修改更新频率? - -**A:** -```php -// 修改单个追踪记录的更新间隔 -$tracking->update_interval = 3600; // 1小时 -$tracking->save(); - -// 或修改crontab执行频率 -crontab -e -# 改为每30分钟: */30 * * * * -``` - ---- - -## 下一步 - -1. ✅ 确认快递100已充值 -2. ✅ 集成到订单系统 -3. ✅ 配置定时任务 -4. ✅ 测试物流查询 -5. 📖 阅读完整文档:`EXPRESS_TRACKING_SYSTEM.md` - ---- - -## 技术支持 - -- 完整文档:`EXPRESS_TRACKING_SYSTEM.md` -- 快递100文档:https://api.kuaidi100.com/ -- 问题排查:`KUAIDI100_TROUBLESHOOTING.md` diff --git a/EXPRESS_TRACKING_SYNC_COMPLETE.md b/EXPRESS_TRACKING_SYNC_COMPLETE.md deleted file mode 100644 index f72c70e1..00000000 --- a/EXPRESS_TRACKING_SYNC_COMPLETE.md +++ /dev/null @@ -1,244 +0,0 @@ -# 物流追踪系统同步完成报告 - -## 问题分析 - -### 用户疑问 -"定时任务找不到快递单号,明明订单里有快递单号" - -### 根本原因 -定时任务查询的是 `zyt_express_tracking` 表(物流追踪表),而不是 `zyt_tcm_prescription_order` 表(订单表)。 - -这两个表是分离的: -- **订单表**:存储业务订单信息,包括快递单号 -- **物流追踪表**:专门存储物流追踪信息和轨迹历史 - -需要将订单表中的快递单号同步到物流追踪表,定时任务才能找到并更新。 - -## 解决方案 - -### 1. 创建同步命令 -创建了 `SyncTrackingNumbers` 命令,用于将订单表中的快递单号同步到物流追踪表。 - -**文件**:`server/app/command/SyncTrackingNumbers.php` - -**使用方法**: -```bash -php think express:sync -``` - -### 2. 注册命令 -在 `server/config/console.php` 中注册了两个命令: -```php -'commands' => [ - 'express:auto-update' => 'app\\command\\ExpressAutoUpdate', // 自动更新 - 'express:sync' => 'app\\command\\SyncTrackingNumbers', // 同步快递单号 -], -``` - -### 3. 执行同步 -```bash -$ php think express:sync -开始同步现有订单快递单号... -找到 1 个有快递单号的订单 -跳过: JD0230761381812 (已存在) - -======================================== -同步完成! -======================================== -总数: 1 -成功: 0 -跳过: 1 -失败: 0 -耗时: 0.28秒 -``` - -## 当前状态 - -### 物流追踪记录状态 -``` -快递单号: JD0230761381812 -快递公司: auto -当前状态: 3 - 已签收 -是否签收: 是 -自动更新: 禁用 -下次更新时间: 1970-01-01 08:00:00 (0) -当前时间: 2026-04-07 17:12:50 -是否应该更新: 否 -不更新原因: 自动更新未启用 已签收 状态为终态 -``` - -### 为什么定时任务总数为0? - -**答案**:这是正常的! - -这个快递单号已经是"已签收"状态(state=3),系统自动: -1. 停止了自动更新(`auto_update = 0`) -2. 标记为已签收(`is_signed = 1`) -3. 状态为终态(state = 3) - -**定时任务的过滤条件**: -```php -$list = ExpressTracking::where('auto_update', 1) // ✗ 当前为0 - ->where('next_update_time', '<=', $now) // ✗ 当前为0 - ->where('is_signed', 0) // ✗ 当前为1 - ->whereNotIn('current_state', [3, 4, 5]) // ✗ 当前为3 - ->whereNull('delete_time') - ->limit($limit) - ->select(); -``` - -所有条件都不满足,所以不会被查询到。这是设计的预期行为,避免浪费查询次数。 - -## 工作流程 - -### 新订单的物流追踪流程 - -1. **订单创建/编辑时** - ```php - ExpressTrackingService::createOrUpdate([ - 'order_id' => $orderId, - 'order_type' => 'prescription', - 'tracking_number' => $trackingNumber, - 'express_company' => $expressCompany, - 'recipient_phone' => $phone, - 'recipient_name' => $name, - 'recipient_address' => $address, - ]); - ``` - - 创建物流追踪记录 - - 立即查询一次物流信息 - - 设置 `auto_update = 1`(启用自动更新) - - 设置 `next_update_time = now`(立即更新) - -2. **定时任务自动更新** - ```bash - php think express:auto-update - ``` - - 每10分钟执行一次 - - 只查询未签收的订单 - - 更新物流信息和轨迹 - - 检测状态变更 - -3. **签收后自动停止** - - 检测到签收状态(state=3) - - 设置 `is_signed = 1` - - 设置 `auto_update = 0`(停止自动更新) - - 记录签收时间 - -### 现有订单的同步 - -对于已有的订单(如当前的 JD0230761381812),需要手动同步: - -```bash -# 同步现有订单快递单号 -php think express:sync - -# 同步后会立即查询一次物流信息 -# 如果已签收,会自动停止更新 -``` - -## 测试验证 - -### 1. 检查物流追踪记录 -```bash -php check_tracking_status.php -``` - -### 2. 测试同步命令 -```bash -php think express:sync -``` - -### 3. 测试自动更新 -```bash -php think express:auto-update -``` - -### 4. 查看日志 -```bash -tail -f runtime/log/202X-XX-XX.log -``` - -## 下一步操作 - -### 1. 集成到订单系统 - -在订单创建/编辑的控制器或逻辑层中添加: - -**文件**:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -```php -use app\common\service\ExpressTrackingService; - -// 在保存订单后 -if (!empty($params['tracking_number'])) { - ExpressTrackingService::createOrUpdate([ - 'order_id' => $orderId, - 'order_type' => 'prescription', - 'tracking_number' => $params['tracking_number'], - 'express_company' => $params['express_company'] ?? 'auto', - 'recipient_phone' => $params['recipient_phone'], - 'recipient_name' => $params['recipient_name'], - 'recipient_address' => $params['shipping_address'], - ]); -} -``` - -### 2. 配置定时任务 - -#### Windows(任务计划程序) -```batch -schtasks /create /tn "ExpressAutoUpdate" /tr "php D:\web\zyt\server\think express:auto-update" /sc minute /mo 10 -``` - -#### Linux(crontab) -```bash -crontab -e -# 添加以下行 -0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1 -``` - -### 3. 测试新订单 - -1. 创建一个新订单,填写快递单号 -2. 检查 `zyt_express_tracking` 表是否自动创建记录 -3. 等待10分钟,查看是否自动更新 -4. 查看 `zyt_express_trace` 表是否有轨迹记录 - -## 相关文件 - -### 核心文件 -- `server/app/common/service/ExpressTrackingService.php` - 物流追踪服务 -- `server/app/command/ExpressAutoUpdate.php` - 自动更新命令 -- `server/app/command/SyncTrackingNumbers.php` - 同步命令 -- `server/config/console.php` - 命令注册 - -### 数据库 -- `server/database/migrations/create_express_tracking_tables.sql` - 创建表 -- `server/database/migrations/sync_existing_tracking_numbers.sql` - 同步SQL - -### 工具脚本 -- `server/install_tracking_tables.php` - 安装表 -- `server/check_tracking_status.php` - 检查状态 - -### 文档 -- `EXPRESS_TRACKING_SYSTEM.md` - 系统架构 -- `EXPRESS_TRACKING_QUICKSTART.md` - 快速开始 -- `EXPRESS_TRACKING_FIXES.md` - 修复记录 -- `EXPRESS_TRACKING_COMMAND_FIX.md` - 命令修复 -- `EXPRESS_TRACKING_SYNC_COMPLETE.md` - 本文档 - -## 总结 - -✅ 问题已解决:订单表和物流追踪表已同步 -✅ 定时任务正常工作:正确过滤已签收订单 -✅ 自动停止机制:签收后自动停止更新,节省查询次数 -✅ 同步命令可用:`php think express:sync` -✅ 自动更新命令可用:`php think express:auto-update` - -**当前状态**:系统已就绪,等待新订单测试。现有订单(JD0230761381812)已签收,不会再更新。 - -**建议**: -1. 在订单创建/编辑时集成 `ExpressTrackingService::createOrUpdate()` -2. 配置定时任务(每10分钟执行一次) -3. 创建一个新的未签收订单进行完整测试 diff --git a/EXPRESS_TRACKING_SYSTEM.md b/EXPRESS_TRACKING_SYSTEM.md deleted file mode 100644 index 8305465d..00000000 --- a/EXPRESS_TRACKING_SYSTEM.md +++ /dev/null @@ -1,540 +0,0 @@ -# 物流追踪系统完整方案 - -## 系统架构 - -### 核心功能 - -1. **物流信息存储** - 将快递100查询结果存储到数据库 -2. **自动更新机制** - 定时自动查询更新物流状态 -3. **状态变更检测** - 监控物流状态变化 -4. **异常通知** - 签收、异常时自动通知 -5. **查询日志** - 记录所有查询请求和响应 - -### 数据库表结构 - -| 表名 | 说明 | 用途 | -|------|------|------| -| zyt_express_tracking | 物流追踪主表 | 存储快递单基本信息和当前状态 | -| zyt_express_trace | 物流轨迹明细表 | 存储每条物流轨迹记录 | -| zyt_express_state_log | 状态变更记录表 | 记录状态变化历史 | -| zyt_express_query_log | 查询日志表 | 记录所有查询请求 | - ---- - -## 安装步骤 - -### 1. 创建数据库表 - -```bash -# 执行SQL脚本 -mysql -u root -p your_database < server/database/migrations/create_express_tracking_tables.sql -``` - -或在数据库管理工具中执行 `create_express_tracking_tables.sql` 文件。 - -### 2. 注册定时任务命令 - -在 `server/config/console.php` 中添加: - -```php -'commands' => [ - // ... 其他命令 - 'express:auto-update' => \app\command\ExpressAutoUpdate::class, -], -``` - -### 3. 配置crontab定时任务 - -```bash -# 编辑crontab -crontab -e - -# 添加以下行(每10分钟执行一次) -*/10 * * * * cd /path/to/your/server && php think express:auto-update >> /dev/null 2>&1 -``` - -### 4. 测试定时任务 - -```bash -cd server -php think express:auto-update -``` - -应该看到输出: -``` -开始自动更新物流信息... -更新完成! -总数: 0 -成功: 0 -失败: 0 -耗时: 0.05秒 -``` - ---- - -## 使用方法 - -### 1. 创建物流追踪 - -当订单发货时,创建物流追踪记录: - -```php -use app\common\service\ExpressTrackingService; - -// 创建追踪记录 -$tracking = ExpressTrackingService::createOrUpdate([ - 'order_id' => 123, // 订单ID - 'order_type' => 'prescription', // 订单类型 - 'tracking_number' => 'JD0230761381812', // 快递单号 - 'express_company' => 'jd', // 快递公司 - 'recipient_phone' => '13800138000', // 收件人手机 - 'recipient_name' => '张三', // 收件人姓名 - 'recipient_address' => '北京市朝阳区xxx', // 收件地址 -]); - -// 系统会立即查询一次物流信息并存储 -``` - -### 2. 手动更新物流信息 - -```php -// 更新指定追踪记录 -$result = ExpressTrackingService::queryAndUpdate($trackingId, false); -``` - -### 3. 获取物流详情 - -```php -// 获取完整的物流信息(包含轨迹) -$detail = ExpressTrackingService::getDetail($trackingId); - -// 返回数据包含: -// - 基本信息 -// - 当前状态 -// - 所有轨迹记录 -// - 状态变更历史 -``` - -### 4. 自动更新配置 - -```php -// 修改更新间隔(秒) -$tracking->update_interval = 1800; // 30分钟 -$tracking->save(); - -// 停止自动更新 -$tracking->auto_update = 0; -$tracking->save(); - -// 启用自动更新 -$tracking->auto_update = 1; -$tracking->next_update_time = time(); // 立即更新 -$tracking->save(); -``` - ---- - -## 集成到现有订单系统 - -### 修改订单创建/编辑逻辑 - -在 `PrescriptionOrderLogic.php` 中: - -```php -public static function create(array $params, int $adminId, array $adminInfo) -{ - // ... 原有创建逻辑 - - $order->save(); - - // 创建物流追踪 - if (!empty($params['tracking_number'])) { - ExpressTrackingService::createOrUpdate([ - 'order_id' => $order->id, - 'order_type' => 'prescription', - 'tracking_number' => $params['tracking_number'], - 'express_company' => $params['express_company'] ?? 'auto', - 'recipient_phone' => $order->recipient_phone, - 'recipient_name' => $order->recipient_name, - 'recipient_address' => $order->shipping_address, - ]); - } - - return $out; -} - -public static function edit(array $params, int $adminId, array $adminInfo) -{ - // ... 原有编辑逻辑 - - // 更新物流追踪 - if (array_key_exists('tracking_number', $params)) { - if (!empty($params['tracking_number'])) { - ExpressTrackingService::createOrUpdate([ - 'order_id' => $order->id, - 'order_type' => 'prescription', - 'tracking_number' => $params['tracking_number'], - 'express_company' => $order->express_company, - 'recipient_phone' => $order->recipient_phone, - 'recipient_name' => $order->recipient_name, - 'recipient_address' => $order->shipping_address, - ]); - } - } - - return $out; -} -``` - -### 修改物流查询接口 - -在 `PrescriptionOrderLogic.php` 中: - -```php -public static function logisticsTrace(int $id, string $expressCompanyOverride, int $adminId, array $adminInfo): ?array -{ - // ... 原有逻辑 - - // 先从数据库获取 - $tracking = ExpressTracking::where('order_id', $id) - ->where('order_type', 'prescription') - ->whereNull('delete_time') - ->find(); - - if ($tracking) { - // 如果距离上次查询超过5分钟,重新查询 - if (time() - $tracking->last_query_time > 300) { - ExpressTrackingService::queryAndUpdate($tracking->id, false); - $tracking->refresh(); - } - - // 返回数据库中的数据 - $payload = [ - 'carrier' => $tracking->express_company, - 'carrier_label' => $tracking->express_company_name, - 'kuaidi_com' => $tracking->express_company, - 'traces' => $tracking->traces->map(function($trace) { - return [ - 'time' => $trace->trace_time, - 'context' => $trace->trace_context, - 'status' => $trace->status, - 'location' => $trace->location, - ]; - })->toArray(), - 'state' => $tracking->current_state, - 'state_text' => $tracking->current_state_text, - 'source' => $tracking->data_source, - 'hint' => '', - 'official_url' => ExpressTrackService::officialUrls($tracking->tracking_number)[$tracking->express_company] ?? '', - 'official_urls' => ExpressTrackService::officialUrls($tracking->tracking_number), - 'tracking_number' => $tracking->tracking_number, - 'express_company_used' => $tracking->express_company, - 'is_signed' => $tracking->is_signed, - 'sign_time' => $tracking->sign_time, - 'estimated_arrival_time' => $tracking->estimated_arrival_time, - ]; - - return $payload; - } - - // 如果数据库中没有,走原有逻辑 - // ... 原有查询逻辑 -} -``` - ---- - -## 自动更新机制 - -### 更新策略 - -1. **创建时立即查询** - 创建追踪记录时立即查询一次 -2. **定时自动更新** - 每10分钟检查一次需要更新的记录 -3. **智能更新间隔** - 默认30分钟更新一次 -4. **终态停止更新** - 签收、退签、拒签后停止自动更新 - -### 更新间隔建议 - -| 物流状态 | 更新间隔 | 说明 | -|---------|---------|------| -| 已下单/待揽收 | 30分钟 | 等待揽收 | -| 已揽收/在途 | 30分钟 | 运输中 | -| 派件中 | 15分钟 | 即将签收,加快更新 | -| 已签收 | 停止 | 终态,无需更新 | -| 疑难/异常 | 60分钟 | 降低频率 | - -### 定时任务配置 - -```bash -# 每10分钟执行一次(推荐) -*/10 * * * * cd /path/to/server && php think express:auto-update - -# 每5分钟执行一次(高频) -*/5 * * * * cd /path/to/server && php think express:auto-update - -# 每30分钟执行一次(低频) -*/30 * * * * cd /path/to/server && php think express:auto-update -``` - ---- - -## 状态变更通知 - -### 通知触发条件 - -1. **签收通知** - 快递签收时通知 -2. **异常通知** - 出现疑难、拒签等异常时通知 -3. **状态变更** - 每次状态变化都会记录 - -### 通知渠道(可扩展) - -#### 1. 企业微信通知 - -```php -// 在 ExpressTrackingService::sendNotification() 中实现 -use app\common\service\WechatWorkService; - -WechatWorkService::sendMessage([ - 'touser' => $userId, - 'msgtype' => 'text', - 'text' => [ - 'content' => "【物流通知】\n" . - "快递单号:{$tracking->tracking_number}\n" . - "状态变更:{$log->old_state_text} → {$log->new_state_text}\n" . - "最新动态:{$log->change_reason}" - ] -]); -``` - -#### 2. 短信通知 - -```php -// 对接短信服务商 -SmsService::send($tracking->recipient_phone, [ - 'template' => 'express_status_change', - 'params' => [ - 'tracking_number' => $tracking->tracking_number, - 'status' => $log->new_state_text, - ] -]); -``` - -#### 3. 系统内通知 - -```php -// 创建系统通知记录 -NoticeService::create([ - 'user_id' => $userId, - 'title' => '物流状态更新', - 'content' => "您的快递 {$tracking->tracking_number} 已{$log->new_state_text}", -]); -``` - ---- - -## 数据统计和分析 - -### 查询统计 - -```sql --- 查询成功率 -SELECT - DATE(FROM_UNIXTIME(query_time)) as date, - COUNT(*) as total, - SUM(is_success) as success, - ROUND(SUM(is_success) / COUNT(*) * 100, 2) as success_rate -FROM zyt_express_query_log -GROUP BY date -ORDER BY date DESC; - --- 平均响应时间 -SELECT - query_source, - AVG(response_time) as avg_response_time, - MAX(response_time) as max_response_time -FROM zyt_express_query_log -WHERE is_success = 1 -GROUP BY query_source; -``` - -### 物流状态分布 - -```sql --- 当前物流状态分布 -SELECT - current_state, - current_state_text, - COUNT(*) as count -FROM zyt_express_tracking -WHERE delete_time IS NULL -GROUP BY current_state, current_state_text -ORDER BY count DESC; - --- 签收率统计 -SELECT - DATE(FROM_UNIXTIME(sign_time)) as date, - COUNT(*) as signed_count -FROM zyt_express_tracking -WHERE is_signed = 1 -GROUP BY date -ORDER BY date DESC; -``` - -### 异常统计 - -```sql --- 异常快递统计 -SELECT - tracking_number, - express_company_name, - current_state_text, - latest_trace_context, - FROM_UNIXTIME(update_time) as update_time -FROM zyt_express_tracking -WHERE current_state IN ('2', '13', '14') -- 疑难、清关异常、拒签 -AND delete_time IS NULL -ORDER BY update_time DESC; -``` - ---- - -## 性能优化 - -### 1. 批量更新优化 - -```php -// 每次处理50条记录 -ExpressTrackingService::autoUpdateBatch(50); - -// 根据服务器性能调整 -// - 性能好:100条 -// - 性能一般:50条 -// - 性能差:20条 -``` - -### 2. 查询频率控制 - -```php -// 避免频繁查询同一单号 -if (time() - $tracking->last_query_time < 300) { - // 5分钟内不重复查询 - return $cachedResult; -} -``` - -### 3. 索引优化 - -数据库表已创建必要索引: -- `idx_order_id` - 订单ID索引 -- `idx_auto_update` - 自动更新索引 -- `idx_tracking_number` - 快递单号索引 - -### 4. 数据清理 - -```sql --- 清理90天前的查询日志 -DELETE FROM zyt_express_query_log -WHERE create_time < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 90 DAY)); - --- 清理已签收超过30天的轨迹明细 -DELETE t FROM zyt_express_trace t -INNER JOIN zyt_express_tracking tr ON t.tracking_id = tr.id -WHERE tr.is_signed = 1 -AND tr.sign_time < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY)); -``` - ---- - -## 监控和告警 - -### 1. 监控指标 - -- 自动更新成功率 -- 平均响应时间 -- 异常快递数量 -- 查询失败率 - -### 2. 告警规则 - -```php -// 在定时任务中添加告警逻辑 -$result = ExpressTrackingService::autoUpdateBatch(50); - -if ($result['failed'] > $result['success'] * 0.5) { - // 失败率超过50%,发送告警 - AlertService::send('物流自动更新失败率过高', $result); -} -``` - ---- - -## 常见问题 - -### Q1: 定时任务不执行? - -**检查步骤:** -1. 确认crontab已配置:`crontab -l` -2. 检查PHP路径:`which php` -3. 检查项目路径是否正确 -4. 查看cron日志:`tail -f /var/log/cron` - -### Q2: 更新频率太高,快递100余额消耗快? - -**解决方案:** -1. 增加更新间隔:`update_interval = 3600`(1小时) -2. 减少批量处理数量:`autoUpdateBatch(20)` -3. 签收后立即停止更新 -4. 使用智能更新策略 - -### Q3: 如何查看某个快递的完整历史? - -```php -$detail = ExpressTrackingService::getDetail($trackingId); - -// 包含: -// - traces: 所有轨迹记录 -// - state_logs: 状态变更历史 -``` - -### Q4: 如何手动触发更新? - -```php -// 方法1:通过服务类 -ExpressTrackingService::queryAndUpdate($trackingId, false); - -// 方法2:通过命令行 -php think express:auto-update -``` - ---- - -## 总结 - -### 优势 - -1. **数据持久化** - 所有物流信息存储在数据库,随时查询 -2. **自动更新** - 无需手动查询,系统自动追踪 -3. **状态监控** - 实时监控物流状态变化 -4. **异常通知** - 及时发现和处理异常 -5. **数据分析** - 支持物流数据统计分析 - -### 成本 - -1. **快递100费用** - 按查询次数计费 -2. **服务器资源** - 定时任务占用少量CPU和内存 -3. **数据库存储** - 需要额外的存储空间 - -### 建议 - -1. **充值快递100** - 确保有足够余额 -2. **合理设置更新间隔** - 平衡实时性和成本 -3. **定期清理数据** - 避免数据库膨胀 -4. **监控运行状态** - 及时发现问题 - ---- - -## 相关文档 - -- `LOGISTICS_INTEGRATION_GUIDE.md` - 物流集成指南 -- `KUAIDI100_ACCOUNT_ISSUE.md` - 快递100账号问题 -- `JTEXPRESS_SUPPORT_ADDED.md` - 极兔速递支持 diff --git a/FINAL_FIX.sql b/FINAL_FIX.sql deleted file mode 100644 index d80a36c1..00000000 --- a/FINAL_FIX.sql +++ /dev/null @@ -1,28 +0,0 @@ --- 最终修复SQL - 根据实际表结构添加字段 --- 请在数据库管理工具中执行 - --- 1. 在 dose_unit 后面添加 usage_days(服用天数) -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_days` int(11) NOT NULL DEFAULT 7 COMMENT '服用天数' AFTER `dose_unit`; - --- 2. 在 usage_instruction 后面添加 usage_time(服用时间) -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_time` varchar(50) NOT NULL DEFAULT '饭前' COMMENT '服用时间:饭前、饭后、饭中、空腹、睡前、晨起、随时' AFTER `usage_instruction`; - --- 3. 在 usage_time 后面添加 usage_way(服用方式) -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_way` varchar(50) NOT NULL DEFAULT '温水送服' COMMENT '服用方式:温水送服、开水冲服、黄酒送服等' AFTER `usage_time`; - --- 4. 在 usage_way 后面添加 dietary_taboo(忌口) -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `dietary_taboo` varchar(200) NOT NULL DEFAULT '' COMMENT '忌口(逗号分隔)' AFTER `usage_way`; - --- 5. 在 dietary_taboo 后面添加 usage_notes(其他说明) -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_notes` varchar(200) NOT NULL DEFAULT '' COMMENT '其他服用说明' AFTER `dietary_taboo`; - --- 6. 在 template_id 后面添加 is_shared(是否共享) -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `is_shared` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否共享:0-不共享 1-共享' AFTER `template_id`; - --- 验证字段是否添加成功 -SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT, COLUMN_COMMENT -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = 'zyt' - AND TABLE_NAME = 'zyt_tcm_prescription' - AND COLUMN_NAME IN ('usage_days', 'usage_time', 'usage_way', 'dietary_taboo', 'usage_notes', 'is_shared') -ORDER BY ORDINAL_POSITION; diff --git a/FINAL_IMPLEMENTATION_SUMMARY.md b/FINAL_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index a49474cf..00000000 --- a/FINAL_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,321 +0,0 @@ -# 首次登录强制修改密码 - 最终实现总结 - -## 功能概述 - -实现了一个安全的首次登录强制修改密码功能,当管理员的 `is_paw` 字段为 0 时,登录后必须修改密码才能使用系统。 - -## 核心修改 - -### 后端修改(3 个文件) - -#### 1. `server/app/adminapi/logic/LoginLogic.php` - -**修改内容:** -- 登录接口返回 `is_paw` 字段 -- 添加 `changeFirstPassword()` 方法处理密码修改 - -**关键代码:** -```php -// 登录时返回 is_paw -return [ - 'name' => $adminInfo['name'], - 'avatar' => $avatar, - 'role_name' => $adminInfo['role_name'], - 'token' => $adminInfo['token'], - 'is_paw' => $admin->is_paw ?? 1, -]; - -// 修改密码方法 -public function changeFirstPassword($token, $password) { - // 验证 token - // 更新密码和 is_paw 字段 - // 使 token 过期 -} -``` - -#### 2. `server/app/adminapi/controller/LoginController.php` - -**修改内容:** -- 添加 `changeFirstPassword` 接口 -- 将该接口添加到 `$notNeedLogin` 白名单 - -**关键代码:** -```php -public array $notNeedLogin = ['account', 'workWechatConfig', 'workWechatLogin', 'checkDbColumn', 'changeFirstPassword']; - -public function changeFirstPassword() { - // 验证参数 - // 调用 LoginLogic::changeFirstPassword() -} -``` - -#### 3. `server/app/adminapi/logic/auth/AdminLogic.php` - -**修改内容:** -- 在 `detail()` 方法的字段列表中添加 `is_paw` - -**关键代码:** -```php -$admin = Admin::field([ - 'id', 'account', 'name', 'disable', 'root', - 'multipoint_login', 'avatar', 'is_paw', // 添加此字段 - // ... 其他字段 -])->findOrEmpty($params['id'])->toArray(); -``` - -### 前端修改(5 个文件) - -#### 1. `admin/src/stores/modules/user.ts` - -**修改内容:** -- 添加 `isPaw` 状态 -- 登录时保存 `isPaw` -- 获取用户信息时更新 `isPaw`(解决刷新问题) - -**关键代码:** -```typescript -export interface UserState { - isPaw: number // 0=需要修改密码,1=正常 -} - -login(playload: any) { - this.isPaw = data.is_paw ?? 1 -} - -getUserInfo() { - this.isPaw = data.user.is_paw ?? 1 // 关键:刷新时从后端获取 -} -``` - -#### 2. `admin/src/permission.ts` - -**修改内容:** -- 添加路由守卫,在获取用户信息后检查 `isPaw` -- 强制跳转到修改密码页面 - -**关键代码:** -```typescript -router.beforeEach(async (to, from, next) => { - if (hasGetUserInfo) { - // 已获取用户信息,检查 isPaw - if (userStore.isPaw === 0) { - next({ path: changePasswordPath }) - return - } - } else { - // 首次获取用户信息 - await userStore.getUserInfo() - // 获取后检查 isPaw - if (userStore.isPaw === 0) { - next({ path: changePasswordPath }) - return - } - } -}) -``` - -#### 3. `admin/src/views/account/login.vue` - -**修改内容:** -- 登录成功后检查 `is_paw` 字段 -- 如果为 0,跳转到修改密码页面 - -**关键代码:** -```typescript -const result: any = await userStore.login(formData) -if (result.is_paw === 0) { - router.push('/change-password') - return -} -``` - -#### 4. `admin/src/views/account/change-password.vue` - -**修改内容:** -- 新建修改密码页面 -- 验证密码规则 -- 修改成功后更新状态并退出登录 - -#### 5. `admin/src/router/routes.ts` - -**修改内容:** -- 添加修改密码路由 - -**关键代码:** -```typescript -{ - path: '/change-password', - component: () => import('@/views/account/change-password.vue') -} -``` - -### 数据库修改 - -#### `add_is_paw_field.sql` - -```sql -ALTER TABLE `la_admin` -ADD COLUMN `is_paw` TINYINT(1) NOT NULL DEFAULT 1 -COMMENT '是否已修改初始密码:0=需要修改,1=正常' -AFTER `multipoint_login`; -``` - -## 安全机制 - -### 1. 强制跳转 -- 登录时检查 `is_paw`,为 0 则跳转到修改密码页面 -- 路由守卫拦截所有路由,强制跳转 - -### 2. URL 防护 -- 即使直接输入 URL,路由守卫也会拦截 -- 无法绕过修改密码页面 - -### 3. 刷新防护 -- 页面刷新时从后端重新获取 `is_paw` 状态 -- 确保状态始终与数据库一致 - -### 4. 多标签页防护 -- 所有标签页共享同一个 Vuex store -- 新打开的标签页也会被拦截 - -## 工作流程 - -``` -1. 管理员登录 (is_paw = 0) - ↓ -2. 后端返回 is_paw = 0 - ↓ -3. 前端保存 isPaw = 0 到 store - ↓ -4. 登录页面检查 isPaw,跳转到修改密码页面 - ↓ -5. 用户尝试访问其他页面(输入 URL 或刷新) - ↓ -6. 路由守卫检查 isPaw - ↓ -7. 如果 isPaw = 0,强制跳转回修改密码页面 - ↓ -8. 用户修改密码成功 - ↓ -9. 后端更新 is_paw = 1 - ↓ -10. 前端更新 isPaw = 1 - ↓ -11. 退出登录 - ↓ -12. 使用新密码重新登录 - ↓ -13. 路由守卫检查 isPaw = 1,允许正常访问 -``` - -## 关键技术点 - -### 1. 状态持久化问题 - -**问题:** 页面刷新后 Vuex store 中的 `isPaw` 状态丢失 - -**解决方案:** 在 `getUserInfo()` 方法中从后端重新获取 `is_paw` 状态 - -### 2. 路由守卫时机 - -**问题:** 在获取用户信息之前检查 `isPaw`,此时状态还是初始值 - -**解决方案:** 在获取用户信息之后再检查 `isPaw` - -### 3. 密码加密方式 - -**问题:** 使用错误的密码加密方式导致无法登录 - -**解决方案:** 使用系统的 `create_password()` 函数,而不是 `password_hash()` - -## 测试验证 - -### 必测场景 - -1. ✅ 登录后自动跳转到修改密码页面 -2. ✅ 直接输入 URL 无法绕过 -3. ✅ 刷新页面仍然在修改密码页面 -4. ✅ 修改密码成功后 `is_paw` 更新为 1 -5. ✅ 使用新密码登录后正常进入系统 -6. ✅ 多标签页测试 -7. ✅ 密码验证规则测试 -8. ✅ 企业微信登录测试 - -### 测试工具 - -- 浏览器开发者工具(Network、Console) -- Vue DevTools -- 数据库查询工具 - -## 文档清单 - -| 文档 | 说明 | -|------|------| -| `FIRST_LOGIN_PASSWORD_CHANGE.md` | 完整功能文档 | -| `QUICK_START_FIRST_LOGIN.md` | 快速开始指南 | -| `TEST_CHECKLIST.md` | 测试清单 | -| `SECURITY_FIX_SUMMARY.md` | 安全修复总结 | -| `DEBUG_GUIDE.md` | 调试指南 | -| `add_is_paw_field.sql` | 数据库字段添加 SQL | -| `test_first_login.sql` | 测试用 SQL | -| `install_first_login_password.sh` | Linux/Mac 安装脚本 | -| `install_first_login_password.bat` | Windows 安装脚本 | - -## 安装步骤 - -### 1. 执行数据库脚本 - -```bash -# Windows -install_first_login_password.bat - -# Linux/Mac -chmod +x install_first_login_password.sh -./install_first_login_password.sh -``` - -### 2. 设置测试账号 - -```sql -UPDATE la_admin SET is_paw = 0 WHERE id = 1; -``` - -### 3. 测试功能 - -按照 `TEST_CHECKLIST.md` 进行测试 - -## 常见问题 - -### Q1: 刷新后进入系统了 - -**A:** 检查后端 `AdminLogic::detail()` 方法是否返回 `is_paw` 字段,前端 `getUserInfo()` 是否更新状态。 - -### Q2: 可以通过 URL 绕过 - -**A:** 检查路由守卫是否正确配置,是否在获取用户信息后检查 `isPaw`。 - -### Q3: 修改密码后无法登录 - -**A:** 检查密码加密方式是否使用 `create_password()` 函数。 - -## 安全等级 - -- 修复前:⚠️ 低(可绕过) -- 修复后:✅ 高(无法绕过) - -## 维护建议 - -1. 定期审计管理员账号的 `is_paw` 状态 -2. 为新创建的管理员默认设置 `is_paw = 0` -3. 定期要求管理员修改密码 -4. 考虑添加密码强度验证 -5. 考虑添加密码历史记录 - -## 完成时间 - -2024-XX-XX - -## 版本 - -v1.0 - 初始实现 -v1.1 - 修复刷新绕过问题 diff --git a/FIRST_LOGIN_PASSWORD_CHANGE.md b/FIRST_LOGIN_PASSWORD_CHANGE.md deleted file mode 100644 index 960659e1..00000000 --- a/FIRST_LOGIN_PASSWORD_CHANGE.md +++ /dev/null @@ -1,142 +0,0 @@ -# 首次登录强制修改密码功能 - -## 功能说明 - -当管理员首次登录或需要重置密码时,系统会强制要求修改密码后才能正常使用系统。 - -## 安装步骤 - -### 方式一:使用安装脚本(推荐) - -#### Windows 系统 -```bash -install_first_login_password.bat -``` - -#### Linux/Mac 系统 -```bash -chmod +x install_first_login_password.sh -./install_first_login_password.sh -``` - -### 方式二:手动执行 SQL - -执行 `add_is_paw_field.sql` 文件中的 SQL 语句: - -```sql -ALTER TABLE `la_admin` -ADD COLUMN `is_paw` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否已修改初始密码:0=需要修改,1=正常' AFTER `multipoint_login`; -``` - -## 使用方法 - -### 设置管理员需要修改密码 - -如果需要强制某个管理员在下次登录时修改密码,执行以下 SQL: - -```sql -UPDATE la_admin SET is_paw = 0 WHERE id = <管理员ID>; -``` - -例如,强制 ID 为 1 的管理员修改密码: - -```sql -UPDATE la_admin SET is_paw = 0 WHERE id = 1; -``` - -### 批量设置所有管理员 - -如果需要强制所有管理员修改密码: - -```sql -UPDATE la_admin SET is_paw = 0; -``` - -## 工作流程 - -1. 管理员登录系统 -2. 系统检查 `is_paw` 字段 - - 如果 `is_paw = 0`:跳转到修改密码页面 - - 如果 `is_paw = 1`:正常进入系统 -3. 管理员在修改密码页面输入新密码 -4. 密码修改成功后,`is_paw` 自动更新为 1 -5. 系统要求重新登录 -6. 使用新密码登录后正常使用系统 - -## 安全机制 - -系统通过路由守卫确保安全性: - -1. **强制跳转**:当 `is_paw = 0` 时,用户访问任何页面都会被强制跳转到修改密码页面 -2. **URL 防护**:即使用户直接在浏览器输入其他页面 URL,也会被拦截并跳转到修改密码页面 -3. **状态持久化**:`is_paw` 状态存储在 Vuex store 中,页面刷新后仍然有效 -4. **防止绕过**:修改密码页面需要登录才能访问,但已修改密码的用户无法再次访问该页面 - -## 技术实现 - -### 后端修改 - -1. **LoginLogic.php** - 登录逻辑返回 `is_paw` 字段 -2. **LoginController.php** - 添加 `changeFirstPassword` 接口 -3. **数据库** - 添加 `is_paw` 字段 - -### 前端修改 - -1. **login.vue** - 登录成功后判断 `is_paw` 字段 -2. **change-password.vue** - 新增修改密码页面 -3. **routes.ts** - 添加修改密码路由 -4. **user.ts** - 添加修改密码 API -5. **user.ts (store)** - 存储 `isPaw` 状态 -6. **permission.ts** - 路由守卫,强制需要修改密码的用户只能访问修改密码页面 - -## 字段说明 - -| 字段名 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| is_paw | TINYINT(1) | 1 | 0=需要修改密码,1=正常 | - -## 注意事项 - -1. 修改密码后会自动退出登录,需要使用新密码重新登录 -2. 密码长度不能少于 6 位 -3. 两次输入的密码必须一致 -4. 该功能同时支持账号密码登录和企业微信登录 - -## 测试步骤 - -### 1. 安装功能 - -执行安装脚本或手动执行 SQL 添加 `is_paw` 字段。 - -### 2. 设置测试账号 - -```sql --- 设置 ID 为 1 的管理员需要修改密码 -UPDATE la_admin SET is_paw = 0 WHERE id = 1; -``` - -### 3. 测试登录流程 - -1. 使用该管理员账号登录 -2. 登录成功后应自动跳转到修改密码页面 -3. 输入新密码(至少 6 位) -4. 确认修改后应提示"密码修改成功,请重新登录" -5. 自动跳转到登录页面 -6. 使用新密码登录,应能正常进入系统 - -### 4. 验证数据库 - -```sql --- 查看管理员的 is_paw 状态,应该已更新为 1 -SELECT id, account, is_paw FROM la_admin WHERE id = 1; -``` - -### 5. 使用测试 SQL - -可以使用 `test_first_login.sql` 文件中的 SQL 语句进行测试。 - -## 安全建议 - -1. 建议为新创建的管理员账号设置 `is_paw = 0`,强制首次登录修改密码 -2. 定期要求管理员修改密码,可批量设置 `is_paw = 0` -3. 密码应包含字母、数字和特殊字符,提高安全性 diff --git a/GANCAO_API_FIX.md b/GANCAO_API_FIX.md deleted file mode 100644 index 1650eaac..00000000 --- a/GANCAO_API_FIX.md +++ /dev/null @@ -1,161 +0,0 @@ -# 甘草 API "Array to string conversion" 错误修复 - -## 问题描述 - -调用甘草处方下单接口时出现错误: -```json -{"code":2,"message":"Array to string conversion"} -``` - -## 根本原因 - -经过分析代码和 API 文档,发现以下几个问题: - -### 1. 变量作用域问题 -在 `PrescriptionOrderLogic::submitGancaoRecipel()` 方法中,`$appNo` 变量在返回语句中被引用,但实际定义在更早的位置(line 1638),然后又在 line 1658 被重新赋值。这可能导致变量引用错误。 - -### 2. 空字符串处理问题 -`buildSubmitPayload()` 方法中的 `cradle_store` 字段可能为空字符串,而甘草 API 可能不接受空字符串。 - -### 3. 患者年龄格式问题 -原代码使用 `sprintf('%d.00', $ageInt)` 格式化年龄,但 API 可能期望简单的整数字符串。 - -### 4. doct_advice 字段处理 -`doct_advice` 数组中的某些字段可能包含空字符串或未正确处理的值,导致 JSON 编码时出现问题。 - -## 修复内容 - -### 1. 修复 `GancaoScmRecipelService.php` - -#### 修改患者年龄格式 -```php -// 修改前 -$patientAge = sprintf('%d.00', $ageInt); - -// 修改后 -$patientAge = (string) $ageInt; -``` - -#### 确保 cradle_store 不为空 -```php -$preview['cradle_store'] = mb_substr(trim((string) ($c['cradle_store'] ?? '')), 0, 32); -if ($preview['cradle_store'] === '') { - $preview['cradle_store'] = 'default'; -} -``` - -#### 优化 doct_advice 字段处理 -```php -// 确保所有字段都是字符串,避免数组到字符串的转换错误 -$tabooStr = $taboo !== '' ? $taboo : '无'; -$usageTimeStr = $usageTime; -$usageBriefStr = $usageBrief !== '' ? $usageBrief : '遵医嘱'; -$othersStr = trim((string) ($rx['usage_notes'] ?? '')); -$notesDoctorStr = trim((string) ($order['remark_extra'] ?? '')); - -$preview['doct_advice'] = [ - 'taboo' => $tabooStr, - 'usage_time' => $usageTimeStr, - 'usage_brief' => $usageBriefStr, - 'others' => $othersStr !== '' ? $othersStr : '', - 'notes_doctor' => $notesDoctorStr !== '' ? $notesDoctorStr : '', -]; -``` - -#### 添加调试日志 -```php -public static function ctmSubmit(array $submitPayload): array -{ - $transport = self::transport(); - - // 记录请求负载以便调试 - Log::info('Gancao CTM_SUBMIT_RECIPEL payload', ['payload' => $submitPayload]); - - return $transport->post($submitPayload); -} -``` - -### 2. 修复 `PrescriptionOrderLogic.php` - -#### 修复变量引用问题 -```php -// 在返回语句中使用正确的变量引用 -$fee = $subBody['result']['fee'] ?? []; -$appNo = $submitPayload['app_order_no'] ?? ('PO' . (string) $order->id); - -return [ - 'recipel_order_no' => $gcNo, - 'app_order_no' => $appNo, - 'fee' => is_array($fee) ? $fee : [], -]; -``` - -#### 添加详细错误日志 -```php -use think\facade\Log; - -// 在 submitGancaoRecipel 方法中添加错误日志 -$subRet = GancaoScmRecipelService::ctmSubmit($submitPayload); -if ((int) ($subRet['state'] ?? 0) !== 1) { - $errMsg = (string) ($subRet['msg'] ?? ''); - $response = isset($subRet['response']) ? mb_substr((string) $subRet['response'], 0, 500) : ''; - self::$error = '甘草下单通信失败:' . $errMsg . ($response !== '' ? ';响应:' . $response : ''); - Log::error('Gancao CTM_SUBMIT failed', ['subRet' => $subRet, 'payload' => $submitPayload]); - - return false; -} -$subBody = $subRet['body'] ?? []; -if (!GancaoScmRecipelService::isApiSuccess($subBody)) { - self::$error = '甘草下单失败:' . GancaoScmRecipelService::apiStatusMessage($subBody); - Log::error('Gancao CTM_SUBMIT api error', ['body' => $subBody, 'payload' => $submitPayload]); - - return false; -} -``` - -## 测试建议 - -1. **检查日志文件**:修复后再次调用接口,查看 `runtime/log/` 目录下的日志文件,查找 `Gancao CTM_SUBMIT_RECIPEL payload` 日志,确认发送的数据格式是否正确。 - -2. **验证必填字段**:根据甘草 API 文档,确保以下字段都有有效值: - - `token`:有效的 API token - - `df_id`:剂型 ID(如 101 表示饮片) - - `amount`:剂量数量 - - `m_list`:药材列表(每个药材必须有 `id`、`quantity`、`brief` 字段) - - `express_to`:收货地址信息 - - `patient`:患者信息 - - `doctor`:医生信息 - - `callback_url`:回调地址(必须是 HTTPS) - -3. **检查配置**:确认 `.env` 文件中的甘草配置完整: - ```env - GANCAO_SCM_ENABLED=true - GANCAO_SCM_GATEWAY_URL=https://xxx - GANCAO_SCM_GATEWAY_AK=xxx - GANCAO_SCM_GATEWAY_SK=xxx - GANCAO_SCM_BIZ_AK=xxx - GANCAO_SCM_BIZ_SK=xxx - GANCAO_SCM_CALLBACK_URL=https://xxx - GANCAO_SCM_CRADLE_STORE=xxx - GANCAO_SCM_EXPRESS_TYPE=sf - ``` - -4. **测试流程**: - - 创建一个测试处方订单 - - 确保处方审核通过 - - 调用 `submitGancaoRecipel` 接口 - - 查看返回结果和日志 - -## 可能的其他原因 - -如果修复后仍然出现错误,可能是以下原因: - -1. **药材 ID 映射问题**:某些药材的甘草 ID(`gid`)未正确配置 -2. **剂型参数问题**:`df101ext` 等剂型扩展参数不符合 API 要求 -3. **地址解析问题**:收货地址无法正确解析为省市区 -4. **电话号码格式**:电话号码不是 11 位数字 - -## 参考文档 - -- 甘草 API 文档:https://apidoc.igancao.com/service-doc/scm-outer-recipel.html -- 特别关注「中药处方下单」和「中药处方下单参数预检查」章节 diff --git a/GANCAO_ARRAY_TO_STRING_FIX.md b/GANCAO_ARRAY_TO_STRING_FIX.md deleted file mode 100644 index 7d47d0fe..00000000 --- a/GANCAO_ARRAY_TO_STRING_FIX.md +++ /dev/null @@ -1,278 +0,0 @@ -# 甘草 API "Array to string conversion" 错误完整解决方案 - -## 问题现象 - -调用甘草处方下单接口时返回: -```json -{"code":2,"message":"Array to string conversion"} -``` - -## 根本原因分析 - -"Array to string conversion" 错误通常发生在以下情况: - -1. **配置文件中的值被错误地解析为数组** - - 例如:`.env` 中 `GANCAO_SCM_EXPRESS_TYPE=["sf"]` 会被解析为数组 - - 正确应该是:`GANCAO_SCM_EXPRESS_TYPE=sf` - -2. **PHP 尝试将数组转换为字符串** - - 在字符串拼接、JSON 编码或其他操作中 - -3. **配置读取问题** - - ThinkPHP 的配置解析可能将某些值错误地转换为数组 - -## 完整修复步骤 - -### 步骤 1: 检查配置文件 - -运行配置检查脚本: - -```bash -cd /path/to/your/project -php test_gancao_config.php -``` - -这个脚本会检查: -- 所有必需的配置项是否存在 -- 配置值的类型是否正确 -- 是否有数组值被错误地用作字符串 - -### 步骤 2: 修复 .env 配置 - -确保 `.env` 文件中的甘草配置格式正确: - -```env -# 甘草配置必须写在文件顶部,或者使用 [GANCAO_SCM] 分区 -# 不要写在 [trtc] 等其他分区内 - -GANCAO_SCM_ENABLED=true -GANCAO_SCM_GATEWAY_URL=https://your-gateway-url.com -GANCAO_SCM_GATEWAY_AK=your_gateway_ak -GANCAO_SCM_GATEWAY_SK=your_gateway_sk_16chars -GANCAO_SCM_BIZ_AK=your_biz_ak -GANCAO_SCM_BIZ_SK=your_biz_sk -GANCAO_SCM_CALLBACK_URL=https://your-domain.com/api/gancao/callback -GANCAO_SCM_EXPRESS_TYPE=sf -GANCAO_SCM_CRADLE_STORE=your_store_name -GANCAO_SCM_DF_ID=101 - -# 可选配置 -GANCAO_SCM_DEFAULT_IS_DECOCT=1 -GANCAO_SCM_DEFAULT_TIMES_PER_DAY=2 -GANCAO_SCM_DEFAULT_NUM_PER_PACK=2 -GANCAO_SCM_DEFAULT_DOSE_ML=150 -``` - -**重要注意事项:** - -1. ❌ **错误示例**(会导致数组转字符串错误): - ```env - GANCAO_SCM_EXPRESS_TYPE=["sf"] - GANCAO_SCM_CALLBACK_URL=["https://..."] - ``` - -2. ✅ **正确示例**: - ```env - GANCAO_SCM_EXPRESS_TYPE=sf - GANCAO_SCM_CALLBACK_URL=https://... - ``` - -3. 不要在值周围加引号(除非值本身包含空格) -4. 不要使用 JSON 格式 -5. 不要使用数组语法 `[]` - -### 步骤 3: 检查配置文件 - -如果使用了 `config/gancao_scm.php` 配置文件,确保格式正确: - -```php - env('gancao_scm.enabled', false), - 'gateway_url' => env('gancao_scm.gateway_url', ''), - 'gateway_ak' => env('gancao_scm.gateway_ak', ''), - 'gateway_sk' => env('gancao_scm.gateway_sk', ''), - 'biz_ak' => env('gancao_scm.biz_ak', ''), - 'biz_sk' => env('gancao_scm.biz_sk', ''), - 'callback_url' => env('gancao_scm.callback_url', ''), - 'express_type' => env('gancao_scm.express_type', 'sf'), // 确保是字符串 - 'cradle_store' => env('gancao_scm.cradle_store', ''), - 'df_id' => (int)env('gancao_scm.df_id', 101), - - // 可选配置 - 'default_is_decoct' => (int)env('gancao_scm.default_is_decoct', 1), - 'default_times_per_day' => (int)env('gancao_scm.default_times_per_day', 2), - 'default_num_per_pack' => (int)env('gancao_scm.default_num_per_pack', 2), - 'default_dose_ml' => (int)env('gancao_scm.default_dose_ml', 150), - - // 药材 ID 映射(这个可以是数组) - 'herb_id_map' => [ - // '药材名' => 甘草ID - ], -]; -``` - -### 步骤 4: 清除缓存 - -```bash -# 清除 ThinkPHP 缓存 -cd server -php think clear - -# 或者手动删除缓存目录 -rm -rf runtime/cache/* -rm -rf runtime/temp/* -``` - -### 步骤 5: 重启服务 - -```bash -# 如果使用 PHP-FPM -sudo systemctl restart php-fpm - -# 如果使用 Nginx -sudo systemctl restart nginx - -# 如果使用 Apache -sudo systemctl restart apache2 -``` - -### 步骤 6: 查看日志 - -修复后再次调用接口,查看日志文件: - -```bash -tail -f server/runtime/log/$(date +%Y%m%d).log -``` - -日志会显示: -- `Gancao CTM_SUBMIT_RECIPEL payload` - 发送的完整负载 -- `Gancao CTM_SUBMIT failed` - 如果通信失败 -- `Gancao CTM_SUBMIT api error` - 如果 API 返回错误 -- `submitGancaoRecipel exception` - 如果发生异常 - -## 代码修复说明 - -已修复的文件: - -### 1. `GancaoScmRecipelService.php` - -- ✅ 修复 `express_type` 可能是数组的问题 -- ✅ 修复 `callback_url` 可能是数组的问题 -- ✅ 修复患者年龄格式 -- ✅ 优化 `doct_advice` 字段处理 -- ✅ 添加调试日志 - -### 2. `PrescriptionOrderLogic.php` - -- ✅ 修复 `$appNo` 变量引用问题 -- ✅ 添加详细错误日志 -- ✅ 添加异常捕获 - -### 3. `PrescriptionOrderController.php` - -- ✅ 添加 try-catch 异常处理 -- ✅ 添加返回值类型检查 -- ✅ 添加详细日志记录 - -## 测试步骤 - -1. **运行配置检查**: - ```bash - php test_gancao_config.php - ``` - -2. **检查输出**: - - 所有配置项应该显示 ✓ - - 不应该有 ❌ 或 ⚠️ 标记 - - JSON 编码应该成功 - -3. **调用接口**: - ```bash - curl -X POST https://your-domain.com/api/tcm.prescriptionOrder/submitGancaoRecipel \ - -H "Content-Type: application/json" \ - -H "token: your_token" \ - -d '{"id": 123}' - ``` - -4. **查看日志**: - ```bash - tail -f server/runtime/log/$(date +%Y%m%d).log | grep -i gancao - ``` - -## 常见错误和解决方案 - -### 错误 1: "Array to string conversion" - -**原因**:配置值是数组而不是字符串 - -**解决**: -1. 运行 `php test_gancao_config.php` 找出哪个配置是数组 -2. 修改 `.env` 文件,移除 `[]` 和引号 -3. 清除缓存并重启服务 - -### 错误 2: "callback_url is invalid" - -**原因**:回调地址未配置或格式错误 - -**解决**: -1. 确保 `GANCAO_SCM_CALLBACK_URL` 配置正确 -2. 必须是 HTTPS 地址 -3. 格式:`https://your-domain.com/api/gancao/callback` - -### 错误 3: "express_type is invalid" - -**原因**:快递类型配置错误 - -**解决**: -1. 检查 `GANCAO_SCM_EXPRESS_TYPE` 配置 -2. 有效值:`sf`(顺丰)、`jd`(京东)、`jt`(极兔)、`auto`(自动) -3. 默认使用 `sf` - -### 错误 4: JSON 编码失败 - -**原因**:数据中包含无法编码的内容 - -**解决**: -1. 检查处方数据中是否有特殊字符 -2. 检查药材名称是否包含非 UTF-8 字符 -3. 查看日志中的完整错误信息 - -## 验证修复 - -修复成功的标志: - -1. ✅ 配置检查脚本全部通过 -2. ✅ 接口返回成功: - ```json - { - "code": 1, - "msg": "甘草药方上传成功", - "data": { - "recipel_order_no": "GC202604...", - "app_order_no": "PO123", - "fee": { - "total": 150.00, - "medicine": 120.00, - "process": 20.00, - "express": 10.00 - } - } - } - ``` -3. ✅ 日志中显示 `submitGancaoRecipel success` -4. ✅ 数据库中 `gancao_reciperl_order_no` 字段已更新 - -## 需要帮助? - -如果以上步骤都无法解决问题,请提供: - -1. 配置检查脚本的完整输出 -2. 错误日志的完整内容(`runtime/log/*.log`) -3. 接口返回的完整响应 -4. `.env` 文件中的甘草配置部分(隐藏敏感信息) - -## 参考文档 - -- 甘草 API 文档:https://apidoc.igancao.com/service-doc/scm-outer-recipel.html -- ThinkPHP 配置文档:https://www.kancloud.cn/manual/thinkphp6_0/1037488 diff --git a/GANCAO_CALLBACK_SETUP.md b/GANCAO_CALLBACK_SETUP.md deleted file mode 100644 index a81db000..00000000 --- a/GANCAO_CALLBACK_SETUP.md +++ /dev/null @@ -1,355 +0,0 @@ -# 甘草订单状态回调功能配置指南 - -## 功能说明 - -实现甘草订单状态回调功能,当甘草订单状态发生变化时(如审核通过、制作中、发货、完成等),甘草系统会主动回调我们的接口,更新订单状态。 - -## 已创建的文件 - -### 1. 回调控制器 -**文件:** `server/app/api/controller/GancaoCallbackController.php` - -**功能:** -- 接收甘草订单状态回调 -- 验证回调签名(防止伪造) -- 更新订单状态 -- 记录回调日志 - -**支持的订单状态:** -- `10` - 系统审核中 -- `11` - 系统审核通过 -- `110` - 订单药房流转制作中(包含:派单、审方、调配、复核、浸泡、煎药、包装、发货等流程) -- `20` - 物流中 -- `30` - 完成(终态) -- `90` - 拦截(终止流转:可恢复) -- `91` - 主动撤单(退费:终态) -- `92` - 驳回(无法制作并退费:终态) - -### 2. 数据库迁移文件 -**文件:** `add_gancao_callback_fields.sql` - -**新增字段:** -- `gancao_order_state` - 甘草订单状态 -- `gancao_flow_name` - 订单流程名称 -- `gancao_supplier` - 供应商/药房名称 -- `gancao_remark` - 订单备注 - -## 配置步骤 - -### 步骤 1: 执行数据库迁移 - -```bash -# 连接到数据库 -mysql -u root -p your_database - -# 执行 SQL 文件 -source /path/to/add_gancao_callback_fields.sql; - -# 或者直接执行 -mysql -u root -p your_database < add_gancao_callback_fields.sql -``` - -### 步骤 2: 添加路由 - -在 `server/route/api.php` 文件中添加回调路由: - -```php - (string) zyt_gancao_scm_env('CALLBACK_URL', ''), - - // 回调签名验证(如果甘草提供了独立的密钥) - 'callback_appkey' => (string) zyt_gancao_scm_env('CALLBACK_APPKEY', ''), - 'callback_secret_key' => (string) zyt_gancao_scm_env('CALLBACK_SECRET_KEY', ''), -]; -``` - -### 步骤 5: 测试回调接口 - -#### 方法 A:使用 curl 测试 - -```bash -# 模拟甘草回调请求 -curl -X POST https://your-domain.com/api/gancao/callback/order-status \ - -H "Content-Type: application/json" \ - -H "access-appkey: your_appkey" \ - -H "access-nonce: test1234" \ - -H "access-timestamp: $(date +%s)" \ - -H "access-sign: calculated_sign" \ - -d '{ - "recipel_order_no": "GC202604...", - "state": 11, - "ext": { - "flow_name": "审核通过" - } - }' -``` - -#### 方法 B:查看日志 - -```bash -# 查看回调日志 -tail -f server/runtime/log/$(date +%Y%m%d).log | grep -i "gancao callback" -``` - -### 步骤 6: 向甘草提供回调地址 - -联系甘草技术支持,提供以下信息: -1. 回调地址:`https://your-domain.com/api/gancao/callback/order-status` -2. 确认回调签名验证方式 -3. 测试回调是否正常 - -## 回调数据格式 - -### 请求头 - -``` -access-appkey: ak-xxxxx -access-nonce: random_string -access-timestamp: 1234567890 -access-sign: md5_hash -``` - -### 请求体 - -```json -{ - "recipel_order_no": "GC202604...", - "state": 110, - "ext": { - "flow_name": "煎药开始", - "supplier": "XX药房" - } -} -``` - -### 状态 110(制作中)的扩展信息 - -```json -{ - "recipel_order_no": "GC202604...", - "state": 110, - "ext": { - "flow_name": "派单|审方|调配|复核|浸泡|煎药开始|包装|发货|寄出", - "supplier": "XX药房" - } -} -``` - -### 状态 20(物流中)的扩展信息 - -```json -{ - "recipel_order_no": "GC202604...", - "state": 20, - "ext": { - "shipping_name": "顺丰速运", - "nu": "SF1234567890", - "supplier": "XX药房" - } -} -``` - -## 签名验证 - -### 签名算法 - -``` -access_sign = md5(access-appkey + secret-key + access-nonce + access-timestamp + request_body) -``` - -### 示例 - -``` -access-appkey: ak-36b05d5034f13a86b102c97e72f14 -secret-key: f8c1f3c58b55a61a4d41242016d314ea -access-nonce: 1jd4u8ii -access-timestamp: 1723014934 -Body: {"recipel_order_no":"1234","state":10,"ext":{"flow_name":"浸泡开始"}} - -计算: -md5("ak-36b05d5034f13a86b102c97e72f14" + "f8c1f3c58b55a61a4d41242016d314ea" + "1jd4u8ii" + "1723014934" + '{"recipel_order_no":"1234","state":10,"ext":{"flow_name":"浸泡开始"}}') -= 03b46e3b385d1dceb183cad08e44f31f -``` - -## 订单状态映射 - -### 甘草状态 → 系统履约状态 - -| 甘草状态 | 状态名称 | 系统履约状态 | 说明 | -|---------|---------|-------------|------| -| 10 | 系统审核中 | 保持不变 | 甘草内部审核 | -| 11 | 系统审核通过 | 保持不变 | 审核通过,准备制作 | -| 110 | 订单药房流转制作中 | 2(履约中) | 药房制作流程 | -| 110(发货) | 发货/寄出 | 5(已发货) | 药房已发货 | -| 20 | 物流中 | 5(已发货) | 快递运输中 | -| 30 | 完成 | 3(已完成) | 订单完成 | -| 90 | 拦截 | 保持不变 | 订单被拦截 | -| 91 | 主动撤单 | 4(已取消) | 撤单并退费 | -| 92 | 驳回 | 4(已取消) | 无法制作并退费 | - -## 日志记录 - -### 回调接收日志 - -``` -[info] Gancao callback received: { - "headers": {...}, - "body": {...} -} -``` - -### 回调处理日志 - -``` -[info] Gancao callback processed: { - "order_id": 123, - "recipel_order_no": "GC202604...", - "state": 110, - "ext": {...} -} -``` - -### 订单日志 - -在 `zyt_prescription_order_log` 表中记录: -- `admin_name`: "甘草系统" -- `action`: "gancao_callback" -- `summary`: "甘草订单状态更新:系统审核通过" - -## 错误处理 - -### 签名验证失败 - -``` -[warning] Gancao callback sign verification failed -``` - -**处理:** 返回 "ok",避免甘草重试 - -### 订单不存在 - -``` -[warning] Gancao callback order not found -``` - -**处理:** 返回 "ok",记录日志 - -### 更新失败 - -``` -[error] Gancao callback update order failed -``` - -**处理:** 返回 "ok",记录错误日志 - -## 重试机制 - -甘草的重试策略: -- 如果回调失败(未返回 "ok" 或超时),会进行重试 -- 最多重试 10 次 -- 重试间隔:失败次数 × 5 分钟 - -**建议:** -1. 接口必须在 5 秒内返回 "ok" -2. 使用异步处理复杂逻辑 -3. 即使处理失败也返回 "ok",避免重复回调 - -## 安全建议 - -1. **启用签名验证**:防止伪造回调 -2. **记录所有回调**:便于排查问题 -3. **限制访问频率**:防止恶意请求 -4. **使用 HTTPS**:保护数据传输安全 -5. **IP 白名单**:只允许甘草服务器 IP 访问(可选) - -## 监控和告警 - -### 监控指标 - -1. 回调接收数量 -2. 签名验证失败次数 -3. 订单更新失败次数 -4. 回调处理耗时 - -### 告警规则 - -1. 签名验证失败率 > 10% -2. 订单更新失败率 > 5% -3. 回调处理耗时 > 3 秒 - -## 测试清单 - -- [ ] 数据库字段已添加 -- [ ] 路由已配置 -- [ ] 回调 URL 已配置 -- [ ] 签名验证正常 -- [ ] 订单状态更新正常 -- [ ] 日志记录正常 -- [ ] 错误处理正常 -- [ ] 已向甘草提供回调地址 -- [ ] 已测试真实回调 - -## 故障排查 - -### 问题 1:收不到回调 - -**检查:** -1. 回调 URL 是否正确 -2. 服务器是否能从公网访问 -3. 防火墙是否开放端口 -4. 路由是否配置正确 - -### 问题 2:签名验证失败 - -**检查:** -1. appkey 是否正确 -2. secret-key 是否正确 -3. 签名算法是否正确 -4. 请求体是否被修改 - -### 问题 3:订单状态未更新 - -**检查:** -1. 订单是否存在 -2. 数据库字段是否添加 -3. 更新逻辑是否正确 -4. 查看错误日志 - -## 相关文档 - -- 甘草 API 文档:https://apidoc.igancao.com/service-doc/scm-outer-recipel.html#订单状态回调 -- ThinkPHP 路由文档:https://www.kancloud.cn/manual/thinkphp6_0/1037493 diff --git a/GANCAO_FINAL_FIX.md b/GANCAO_FINAL_FIX.md deleted file mode 100644 index 5097c9b7..00000000 --- a/GANCAO_FINAL_FIX.md +++ /dev/null @@ -1,318 +0,0 @@ -# 甘草 API "Array to string conversion" 最终修复 - -## 问题根源 - -错误发生在 `GancaoScmRecipelService::getToken()` 方法中: - -```php -// 第 126 行 -if ($code === '10103' || str_contains($apiMsg, '10103')) { -``` - -**原因:** 当甘草 API 返回错误时,`$body['status']['msg']` 可能是**数组**而不是字符串,导致: -1. `apiStatusMessage()` 方法尝试将数组转换为字符串 -2. `str_contains()` 函数接收到数组参数,触发 "Array to string conversion" 错误 - -## 已修复的问题 - -### 1. `apiStatusMessage()` 方法 - -**修复前:** -```php -$msg = (string) ($body['status']['msg'] ?? ''); -``` - -**修复后:** -```php -$msg = $body['status']['msg'] ?? ''; - -// 确保 msg 是字符串 -if (is_array($msg)) { - $msg = json_encode($msg, JSON_UNESCAPED_UNICODE); -} else { - $msg = (string) $msg; -} -``` - -### 2. `getToken()` 方法中的 `str_contains()` 调用 - -**修复前:** -```php -if ($code === '10103' || str_contains($apiMsg, '10103')) { -``` - -**修复后:** -```php -// 确保 $apiMsg 是字符串,避免 Array to string conversion 错误 -$apiMsgStr = is_string($apiMsg) ? $apiMsg : json_encode($apiMsg, JSON_UNESCAPED_UNICODE); - -if ($code === '10103' || str_contains($apiMsgStr, '10103')) { -``` - -### 3. 其他已修复的问题 - -#### SSL 连接问题(`GancaoOpenApiTransport.php`) -- ✅ 改用 HTTP/1.1 -- ✅ 强制使用 TLS 1.2 -- ✅ 降低 OpenSSL 安全级别 -- ✅ 增加连接超时 -- ✅ 添加 TCP keepalive - -#### 配置类型问题(`GancaoScmRecipelService.php`) -- ✅ 确保 `express_type` 是字符串 -- ✅ 确保 `callback_url` 是字符串 -- ✅ 修复患者年龄格式 -- ✅ 优化 `doct_advice` 字段处理 - -#### 错误处理(`PrescriptionOrderLogic.php` 和 `PrescriptionOrderController.php`) -- ✅ 添加详细错误日志 -- ✅ 添加异常捕获 -- ✅ 添加返回值类型检查 - -## 完整的修复文件列表 - -1. ✅ `server/app/common/service/gancao/GancaoScmRecipelService.php` -2. ✅ `server/app/common/service/gancao/GancaoOpenApiTransport.php` -3. ✅ `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` -4. ✅ `server/app/adminapi/controller/tcm/PrescriptionOrderController.php` - -## 部署步骤 - -### 步骤 1: 上传所有修复文件 - -```bash -# 上传修复后的文件 -scp server/app/common/service/gancao/GancaoScmRecipelService.php user@server:/path/to/server/app/common/service/gancao/ -scp server/app/common/service/gancao/GancaoOpenApiTransport.php user@server:/path/to/server/app/common/service/gancao/ -scp server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php user@server:/path/to/server/app/adminapi/logic/tcm/ -scp server/app/adminapi/controller/tcm/PrescriptionOrderController.php user@server:/path/to/server/app/adminapi/controller/tcm/ - -# 上传测试脚本 -scp test_gancao_config.php user@server:/path/to/ -scp test_gancao_ssl.php user@server:/path/to/ -scp diagnose_gancao_payload.php user@server:/path/to/ -``` - -### 步骤 2: 验证文件已上传 - -```bash -ssh user@server -cd /path/to/your/project - -# 检查文件修改时间 -ls -la server/app/common/service/gancao/GancaoScmRecipelService.php -ls -la server/app/common/service/gancao/GancaoOpenApiTransport.php -ls -la server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php -ls -la server/app/adminapi/controller/tcm/PrescriptionOrderController.php -``` - -### 步骤 3: 运行配置检查 - -```bash -php test_gancao_config.php -``` - -**期望输出:** 所有配置项显示 ✓ - -### 步骤 4: 运行 SSL 测试 - -```bash -php test_gancao_ssl.php -``` - -**期望输出:** 至少一种 SSL 配置测试成功,并且能成功获取 token - -### 步骤 5: 清除缓存 - -```bash -cd server -php think clear -rm -rf runtime/cache/* -rm -rf runtime/temp/* -``` - -### 步骤 6: 重启服务 - -```bash -# PHP-FPM -sudo systemctl restart php-fpm - -# Nginx -sudo systemctl restart nginx - -# 或者 Apache -sudo systemctl restart apache2 -``` - -### 步骤 7: 测试接口 - -**开启日志监控:** -```bash -tail -f server/runtime/log/$(date +%Y%m%d).log | grep -E "(Gancao|gancao|submitGancao)" -``` - -**在另一个终端调用接口:** -```bash -curl -X POST https://your-domain.com/api/tcm.prescriptionOrder/submitGancaoRecipel \ - -H "Content-Type: application/json" \ - -H "token: your_admin_token" \ - -d '{"id": 订单ID}' -``` - -## 预期结果 - -### 成功的情况 - -**接口响应:** -```json -{ - "code": 1, - "show": 0, - "msg": "甘草药方上传成功", - "data": { - "recipel_order_no": "GC202604...", - "app_order_no": "PO123", - "fee": { - "total": 150.00, - "medicine": 120.00, - "process": 20.00, - "express": 10.00 - } - } -} -``` - -**日志内容:** -``` -[info] Gancao MAKE_TOKEN request: {...} -[info] Gancao MAKE_TOKEN response: {...} -[info] Gancao CTM_SUBMIT_RECIPEL payload: {...} -[info] submitGancaoRecipel success: {...} -``` - -**数据库变化:** -- `prescription_order` 表中对应订单的 `gancao_reciperl_order_no` 字段已更新 -- `gancao_submit_time` 字段已更新为当前时间戳 - -### 失败的情况 - -如果仍然失败,日志会显示详细错误信息: - -**配置错误:** -``` -[error] Gancao MAKE_TOKEN api error: {"body": {...}, "apiMsg": "..."} -``` -→ 检查 `.env` 配置,特别是 AK/SK - -**SSL 连接错误:** -``` -[error] Gancao CTM_SUBMIT failed: {"subRet": {...}, "payload": {...}} -``` -→ 运行 `php test_gancao_ssl.php` 诊断 SSL 问题 - -**业务逻辑错误:** -``` -[error] submitGancaoRecipel exception: {"message": "...", "trace": "..."} -``` -→ 检查处方数据是否完整,药材 ID 是否正确映射 - -## 常见错误和解决方案 - -### 错误 1: "Array to string conversion" - -**已修复!** 如果仍然出现,请检查: -1. 文件是否正确上传 -2. 缓存是否已清除 -3. 服务是否已重启 - -### 错误 2: SSL 连接错误 - -**解决方案:** -1. 运行 `php test_gancao_ssl.php` -2. 查看哪种 SSL 配置成功 -3. 如果都失败,检查 OpenSSL 版本:`openssl version` -4. 联系甘草技术支持确认 SSL 要求 - -### 错误 3: "MAKE_TOKEN 失败:[10103]" - -**原因:** AK/SK 不匹配或密码计算错误 - -**解决方案:** -1. 检查 `.env` 中的配置: - ```env - GANCAO_SCM_BIZ_AK=xxx - GANCAO_SCM_BIZ_SK=xxx - ``` -2. 确认 BIZ AK/SK 与网关 AK/SK 是否相同 -3. 检查服务器时间是否正确:`date` -4. 联系甘草获取正确的 AK/SK - -### 错误 4: "药材无法匹配甘草药ID" - -**原因:** 药材 ID 映射缺失 - -**解决方案:** -1. 检查 `zyt_doctor_medicine` 表中药材的 `gid` 字段 -2. 或在 `.env` 配置 `herb_id_map`: - ```php - 'herb_id_map' => [ - '当归' => 1001, - '黄芪' => 1002, - ] - ``` - -## 验证修复成功 - -✅ **所有以下条件都满足,说明修复成功:** - -1. `php test_gancao_config.php` 全部通过 -2. `php test_gancao_ssl.php` 显示 "✓ 成功获取 token" -3. 接口返回 `{"code": 1, "msg": "甘草药方上传成功"}` -4. 日志中显示 `submitGancaoRecipel success` -5. 数据库中订单已更新 `gancao_reciperl_order_no` -6. 没有 "Array to string conversion" 错误 -7. 没有 SSL 连接错误 - -## 技术支持 - -如果以上步骤都无法解决问题,请收集以下信息: - -1. **配置检查输出:** - ```bash - php test_gancao_config.php > config_check.txt 2>&1 - ``` - -2. **SSL 测试输出:** - ```bash - php test_gancao_ssl.php > ssl_check.txt 2>&1 - ``` - -3. **错误日志:** - ```bash - tail -200 server/runtime/log/$(date +%Y%m%d).log > error.log - ``` - -4. **接口响应:** - ```bash - curl -X POST ... > response.json 2>&1 - ``` - -5. **系统信息:** - ```bash - php -v > system_info.txt - openssl version >> system_info.txt - curl --version >> system_info.txt - cat /etc/os-release >> system_info.txt - ``` - -将这些文件发送给技术支持进行分析。 - -## 总结 - -这次修复解决了三个主要问题: - -1. **Array to string conversion** - 甘草 API 返回的错误消息可能是数组 -2. **SSL 连接失败** - TLS 版本和 HTTP 版本不兼容 -3. **配置类型错误** - 某些配置值被错误地解析为数组 - -所有问题都已修复,现在应该可以正常调用甘草 API 了! diff --git a/GANCAO_FIX_SUMMARY.md b/GANCAO_FIX_SUMMARY.md deleted file mode 100644 index ac7f20d7..00000000 --- a/GANCAO_FIX_SUMMARY.md +++ /dev/null @@ -1,286 +0,0 @@ -# 甘草 API "Array to string conversion" 错误修复总结 - -## 问题描述 -在 Linux 服务器上调用甘草处方下单接口时,返回错误: -```json -{"code":2,"message":"Array to string conversion"} -``` - -## 已完成的修复 - -### 1. 代码层面修复 - -#### ✅ `GancaoScmRecipelService.php` -- 修复 `express_type` 字段:确保是字符串而不是数组 -- 修复 `callback_url` 字段:确保是字符串而不是数组 -- 修复患者年龄格式:从 `sprintf('%d.00', $ageInt)` 改为 `(string) $ageInt` -- 优化 `doct_advice` 字段处理:确保所有子字段都是字符串 -- 添加调试日志:记录发送的完整负载 - -#### ✅ `PrescriptionOrderLogic.php` -- 修复 `$appNo` 变量引用问题 -- 添加详细错误日志:记录失败时的完整请求和响应 -- 添加 `use think\facade\Log;` 导入 - -#### ✅ `PrescriptionOrderController.php` -- 添加 try-catch 异常处理 -- 添加返回值类型检查 -- 添加详细日志记录 - -### 2. 诊断工具 - -创建了三个诊断脚本: - -1. **`test_gancao_config.php`** - 配置检查脚本 - - 检查所有配置项是否存在 - - 检查配置值类型是否正确 - - 检查是否有数组值被错误地用作字符串 - - 测试 JSON 编码 - -2. **`diagnose_gancao_payload.php`** - 负载诊断脚本 - - 测试 `buildPreviewPayload` 方法 - - 测试 `buildSubmitPayload` 方法 - - 检查每个字段的类型 - - 测试 JSON 编码 - - 保存负载到文件以便检查 - -3. **文档**: - - `GANCAO_API_FIX.md` - 初步修复说明 - - `GANCAO_ARRAY_TO_STRING_FIX.md` - 完整解决方案 - - `GANCAO_FIX_SUMMARY.md` - 本文档 - -## 下一步操作 - -### 步骤 1: 上传修复后的代码到服务器 - -```bash -# 上传修改的文件 -scp server/app/common/service/gancao/GancaoScmRecipelService.php user@server:/path/to/server/app/common/service/gancao/ -scp server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php user@server:/path/to/server/app/adminapi/logic/tcm/ -scp server/app/adminapi/controller/tcm/PrescriptionOrderController.php user@server:/path/to/server/app/adminapi/controller/tcm/ - -# 上传诊断脚本 -scp test_gancao_config.php user@server:/path/to/ -scp diagnose_gancao_payload.php user@server:/path/to/ -``` - -### 步骤 2: 在服务器上运行配置检查 - -```bash -ssh user@server -cd /path/to/your/project - -# 运行配置检查 -php test_gancao_config.php -``` - -**期望输出**: -``` -=== 甘草配置检查 === - -1. 检查配置是否存在: - ✓ 配置已加载 - -2. 检查各项配置: - - enabled: ✓ boolean = true - - gateway_url: ✓ string = https://... - - gateway_ak: ✓ string = xxx - - gateway_sk: ✓ string = xxx - - biz_ak: ✓ string = xxx - - biz_sk: ✓ string = xxx - - callback_url: ✓ string = https://... - - express_type: ✓ string = sf - - cradle_store: ✓ string = xxx - - df_id: ✓ integer = 101 - -... - -=== 检查结果: ✓ 配置正常 === -``` - -**如果看到 ❌ 或 ⚠️**: -- 记录哪个字段有问题 -- 检查 `.env` 文件中该字段的配置 -- 修复后重新运行检查 - -### 步骤 3: 运行负载诊断 - -```bash -php diagnose_gancao_payload.php -``` - -这会: -- 测试负载构建过程 -- 检查每个字段的类型 -- 测试 JSON 编码 -- 生成一个 JSON 文件(`gancao_payload_YYYYMMDDHHMMSS.json`) - -检查生成的 JSON 文件,确保: -- 没有字段的值是 `"Array"` -- 所有字符串字段都是字符串 -- 所有数组字段都是数组 - -### 步骤 4: 清除缓存 - -```bash -cd server -php think clear - -# 或手动删除 -rm -rf runtime/cache/* -rm -rf runtime/temp/* -``` - -### 步骤 5: 重启服务 - -```bash -# PHP-FPM -sudo systemctl restart php-fpm - -# Nginx -sudo systemctl restart nginx -``` - -### 步骤 6: 测试接口 - -```bash -# 查看实时日志 -tail -f server/runtime/log/$(date +%Y%m%d).log | grep -i gancao -``` - -在另一个终端调用接口: -```bash -curl -X POST https://your-domain.com/api/tcm.prescriptionOrder/submitGancaoRecipel \ - -H "Content-Type: application/json" \ - -H "token: your_token" \ - -d '{"id": 订单ID}' -``` - -### 步骤 7: 检查日志 - -日志中应该看到: - -**成功的情况**: -``` -[info] Gancao CTM_SUBMIT_RECIPEL payload: {...} -[info] submitGancaoRecipel success: {...} -``` - -**失败的情况**: -``` -[error] Gancao CTM_SUBMIT failed: {...} -[error] submitGancaoRecipel exception: {...} -``` - -## 常见问题排查 - -### 问题 1: 配置检查显示某个字段是数组 - -**解决方案**: -1. 编辑 `.env` 文件 -2. 找到该字段的配置行 -3. 移除 `[]` 和多余的引号 -4. 示例: - ```env - # 错误 - GANCAO_SCM_EXPRESS_TYPE=["sf"] - - # 正确 - GANCAO_SCM_EXPRESS_TYPE=sf - ``` -5. 保存后清除缓存并重启服务 - -### 问题 2: JSON 中包含 "Array" 字符串 - -**解决方案**: -1. 运行 `diagnose_gancao_payload.php` 找出是哪个字段 -2. 检查该字段在代码中的处理 -3. 确保使用 `(string)` 强制转换 -4. 检查配置文件中该字段的值 - -### 问题 3: 仍然报 "Array to string conversion" - -**可能原因**: -1. 缓存未清除 -2. 服务未重启 -3. 上传的文件未生效 -4. 配置文件有语法错误 - -**排查步骤**: -1. 确认文件已上传:`ls -la server/app/common/service/gancao/GancaoScmRecipelService.php` -2. 检查文件修改时间:`stat server/app/common/service/gancao/GancaoScmRecipelService.php` -3. 清除所有缓存:`rm -rf server/runtime/*` -4. 重启所有服务 -5. 检查 PHP 错误日志:`tail -f /var/log/php-fpm/error.log` - -### 问题 4: 配置检查通过但接口仍然失败 - -**排查步骤**: -1. 检查日志中的完整错误信息 -2. 查看 `Gancao CTM_SUBMIT_RECIPEL payload` 日志,确认发送的数据 -3. 检查甘草 API 返回的错误信息 -4. 确认药材 ID 映射是否正确 -5. 确认处方数据是否完整 - -## 验证修复成功 - -修复成功的标志: - -1. ✅ `test_gancao_config.php` 全部通过 -2. ✅ `diagnose_gancao_payload.php` 全部通过 -3. ✅ 接口返回成功: - ```json - { - "code": 1, - "msg": "甘草药方上传成功", - "data": { - "recipel_order_no": "GC...", - "app_order_no": "PO...", - "fee": {...} - } - } - ``` -4. ✅ 日志显示 `submitGancaoRecipel success` -5. ✅ 数据库 `prescription_order` 表中 `gancao_reciperl_order_no` 字段已更新 - -## 需要进一步帮助 - -如果以上步骤都无法解决问题,请提供: - -1. **配置检查输出**: - ```bash - php test_gancao_config.php > config_check.txt 2>&1 - ``` - -2. **负载诊断输出**: - ```bash - php diagnose_gancao_payload.php > payload_check.txt 2>&1 - ``` - -3. **错误日志**: - ```bash - tail -100 server/runtime/log/$(date +%Y%m%d).log > error.log - ``` - -4. **接口响应**: - ```bash - curl -X POST ... > response.json 2>&1 - ``` - -5. **`.env` 配置**(隐藏敏感信息): - ```bash - grep GANCAO_SCM .env > gancao_config.txt - ``` - -将这些文件发送给技术支持进行分析。 - -## 总结 - -这次修复主要解决了配置值类型错误的问题。关键点: - -1. **配置必须是正确的类型**:字符串字段不能是数组 -2. **`.env` 格式很重要**:不要使用 JSON 语法或数组语法 -3. **缓存必须清除**:修改配置后必须清除缓存 -4. **日志很重要**:添加了详细日志帮助排查问题 - -希望这次修复能解决你的问题! diff --git a/GANCAO_PREVIEW_FEATURE.md b/GANCAO_PREVIEW_FEATURE.md deleted file mode 100644 index f8088222..00000000 --- a/GANCAO_PREVIEW_FEATURE.md +++ /dev/null @@ -1,212 +0,0 @@ -# 甘草预下单测试功能 - -## 功能概述 -在订单编辑表单中添加"甘草预下单测试"功能,允许用户在不真实提交订单的情况下,测试当前配置的甘草预下单价格。测试结果以抽屉形式展示,包含完整的价格、配送、规则检查和药材明细信息。 - -## 实现内容 - -### 1. 后端接口 - -#### 控制器方法 (`server/app/adminapi/controller/tcm/PrescriptionOrderController.php`) -```php -public function previewGancaoRecipel() -``` -- 调用验证器场景:`previewGancaoRecipel` -- 调用逻辑方法:`PrescriptionOrderLogic::previewGancaoRecipel()` -- 返回预览结果(包含价格信息) - -#### 逻辑方法 (`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php`) -```php -public static function previewGancaoRecipel(int $id, int $adminId, array $adminInfo) -``` -- 验证订单和处方存在性 -- 检查药材是否能匹配甘草药ID -- 获取甘草token -- 调用 `CTM_PREVIEW` 接口 -- 检查规则拦截和药材可用性 -- 返回完整的预览数据(不提交订单) - -#### 验证器 (`server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php`) -- 添加场景:`previewGancaoRecipel => ['id']` - -### 2. 前端实现 - -#### API接口 (`admin/src/api/tcm.ts`) -```typescript -export function prescriptionOrderPreviewGancaoRecipel(params: { id: number }) -``` - -#### UI组件 (`admin/src/views/consumer/prescription/order_list.vue`) - -**测试按钮位置**:在"剂数"和"剂量单位"字段后,"上次医生"字段前 - -**组件结构**: -```vue -测试价格 - - - - - - - -``` - -### 3. 抽屉展示内容 - -#### 价格信息卡片 -- **药材成本** (`m_cost`): 橙色显示,单位:元 -- **加工费** (`proces_cost`): 蓝色显示,单位:分转元 -- **物流费** (`lis_cost`): 绿色显示,单位:分转元 -- **总计**: 红色加粗显示,自动计算总和 - -#### 配送信息卡片 -- 调配中心名称 (`ds_name`) -- 调配中心ID (`ds_id`) -- 服用天数范围 (`take_days.min` - `take_days.max`) -- 默认服用天数 (`take_days.default`) - -#### 规则检查卡片 -- 显示所有规则检查结果 (`rule_check`) -- 根据 `type` 显示不同颜色: - - `type >= 2`: 错误(红色) - - `type === 1`: 警告(黄色) - - `type === 0`: 信息(蓝色) -- 显示规则代码 (`code`) 和消息 (`msg`) - -#### 药材明细表格 -- 序号 -- 药材名称 (`title`) -- 用量 (`quantity` + `unit`) -- 单价 (`price`,元/克,保留4位小数) -- 小计(单价 × 用量,橙色显示) -- 库存状态 (`is_available`,1=有货/0=缺货) -- 备注 (`brief`) - -#### 订单信息卡片 -- 订单号 (`order_no`):跨2列显示 -- 剂数 (`dose_count`):显示格式"X剂"(如:5剂) -- 单剂量:自动计算所有药材用量总和,显示格式"X克"(如:196克) - -## 接口返回数据结构 - -```json -{ - "code": 1, - "msg": "预下单成功", - "data": { - "success": true, - "fee": { - "m_cost": "112.88", - "proces_cost": 80, - "lis_cost": 0 - }, - "result": { - "aa_id": 73, - "ds_id": 135, - "ds_name": "郑州-甘草调配中心√", - "take_days": { - "min": 7, - "max": 35, - "default": 28 - }, - "rule_check": [ - { - "code": "pill_yield_notice", - "msg": "出丸量约98g±5%", - "type": 0 - } - ], - "fee": { - "m_cost": "112.88", - "proces_cost": 80, - "lis_cost": 0 - }, - "m_list": [ - { - "id": 420, - "title": "龙胆", - "price": 0.405028, - "unit": "克", - "quantity": "6", - "is_available": 1, - "brief": "" - } - ] - }, - "order_no": "PO20260414160525788214", - "dose_count": 5 - } -} -``` - -## UI设计特点 - -### 布局 -- 使用 `el-drawer` 抽屉组件,宽度 800px -- 内容分为5个卡片区域,使用 `el-card` 组件 -- 使用 `el-descriptions` 展示键值对信息 -- 使用 `el-table` 展示药材明细列表 - -### 颜色方案 -- 药材成本:橙色 (`text-orange-600`) -- 加工费:蓝色 (`text-blue-600`) -- 物流费:绿色 (`text-green-600`) -- 总计:红色加粗 (`text-red-600 font-bold`) -- 小计:橙色 (`text-orange-600`) - -### 交互 -- 点击"测试价格"按钮触发预下单 -- 按钮显示加载状态(loading) -- 成功后自动打开抽屉展示结果 -- 失败时显示错误提示消息 - -## 使用流程 - -1. 打开订单编辑对话框 -2. 配置剂数(`dose_count`)和剂量单位(`dose_unit`) -3. 点击"测试价格"按钮 -4. 系统调用甘草 `CTM_PREVIEW` 接口 -5. 自动打开抽屉展示完整的预下单结果 - -## 注意事项 - -- 预下单测试**不会**真实提交订单到甘草 -- 预下单测试**不会**扣费 -- 仅用于验证配置和查看价格 -- 需要先保存订单才能测试(`editForm.id` 必须存在) -- 测试结果会检查: - - 药材是否匹配甘草药ID - - 规则是否拦截 - - 药材是否缺货 -- 抽屉关闭时自动销毁内容(`destroy-on-close`) - -## 相关文件 - -### 后端 -- `server/app/adminapi/controller/tcm/PrescriptionOrderController.php` -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` -- `server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php` -- `server/app/common/service/gancao/GancaoScmRecipelService.php` - -### 前端 -- `admin/src/api/tcm.ts` -- `admin/src/views/consumer/prescription/order_list.vue` - -## 测试要点 - -- [ ] 点击"测试价格"按钮能正常调用接口 -- [ ] 成功时自动打开抽屉展示结果 -- [ ] 价格信息显示正确(药材成本、加工费、物流费、总计) -- [ ] 配送信息显示完整(调配中心、服用天数范围) -- [ ] 规则检查按类型显示不同颜色 -- [ ] 药材明细表格显示完整(名称、用量、单价、小计、库存状态) -- [ ] 订单信息显示正确(订单号、剂数) -- [ ] 失败时显示清晰的错误信息 -- [ ] 按钮加载状态正常显示 -- [ ] 价格单位正确(分转元) -- [ ] 仅在编辑已存在订单时显示测试按钮 -- [ ] 测试不会真实提交订单到甘草 -- [ ] 测试不会修改订单状态 -- [ ] 抽屉关闭后内容正确销毁 - diff --git a/GANCAO_SSL_FIX.md b/GANCAO_SSL_FIX.md deleted file mode 100644 index 1914c363..00000000 --- a/GANCAO_SSL_FIX.md +++ /dev/null @@ -1,284 +0,0 @@ -# 甘草 SSL 连接错误修复指南 - -## 错误信息 - -``` -甘草网关通信失败:通信失败:HTTP 200 curl#56 OpenSSL SSL_read: error:0A000126:SSL routines::unexpected eof while reading, errno 0 -``` - -## 问题分析 - -这个错误表示: -- ✅ TCP 连接成功(HTTP 200) -- ✅ SSL 握手开始 -- ❌ SSL 读取数据时连接意外断开 - -**常见原因:** -1. TLS 版本不兼容(服务器要求 TLS 1.2+,客户端使用旧版本) -2. SSL 加密套件不匹配 -3. HTTP 版本问题(HTTP/1.0 vs HTTP/1.1) -4. OpenSSL 安全级别过高 -5. 服务器端提前关闭连接 - -## 已实施的修复 - -### 修改 `GancaoOpenApiTransport.php` - -```php -// 1. 改用 HTTP/1.1(原来是 HTTP/1.0) -curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - -// 2. 增加连接超时 -curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 从 5 秒增加到 10 秒 - -// 3. 完全禁用 SSL 验证 -curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); // 从 2 改为 0 - -// 4. 强制使用 TLS 1.2 -curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2); - -// 5. 降低 OpenSSL 安全级别 -curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, 'DEFAULT@SECLEVEL=1'); - -// 6. 添加 TCP keepalive -curl_setopt($ch, CURLOPT_TCP_KEEPALIVE, 1); -curl_setopt($ch, CURLOPT_TCP_KEEPIDLE, 120); -curl_setopt($ch, CURLOPT_TCP_KEEPINTVL, 60); -``` - -## 测试步骤 - -### 步骤 1: 上传修复后的文件 - -```bash -scp server/app/common/service/gancao/GancaoOpenApiTransport.php user@server:/path/to/server/app/common/service/gancao/ -scp test_gancao_ssl.php user@server:/path/to/ -``` - -### 步骤 2: 运行 SSL 测试 - -```bash -ssh user@server -cd /path/to/your/project -php test_gancao_ssl.php -``` - -**期望输出:** -``` -=== 甘草 SSL 连接测试 === - -网关地址: https://xxx.com - -1. DNS 解析测试: - ✓ xxx.com -> 1.2.3.4 - -2. TCP 连接测试: - ✓ TCP 连接成功 - -3. SSL/TLS 支持检测: - - SSLv2: ✗ 不支持 - - SSLv3: ✗ 不支持 - - TLS 1.0: ✓ 支持 - - TLS 1.1: ✓ 支持 - - TLS 1.2: ✓ 支持 - - TLS 1.3: ✓ 支持 - -4. cURL SSL 测试: - 测试 HTTP/1.0 + TLS 1.2: - ❌ 失败: [56] OpenSSL SSL_read... - 测试 HTTP/1.1 + TLS 1.2: - ✓ 成功 (HTTP 200) - 测试 HTTP/1.1 + TLS 1.2 + SECLEVEL=1: - ✓ 成功 (HTTP 200) - -5. OpenSSL 版本信息: - 版本: OpenSSL 1.1.1... - -6. cURL 版本信息: - 版本: 7.x.x - SSL 版本: OpenSSL/1.1.1... - -7. 测试实际 API 调用 (MAKE_TOKEN): - ✓ 成功获取 token (长度: 64) -``` - -### 步骤 3: 清除缓存并重启 - -```bash -cd server -php think clear -rm -rf runtime/cache/* - -# 重启服务 -sudo systemctl restart php-fpm -sudo systemctl restart nginx -``` - -### 步骤 4: 测试接口 - -```bash -# 查看日志 -tail -f server/runtime/log/$(date +%Y%m%d).log | grep -i gancao -``` - -在另一个终端调用接口: -```bash -curl -X POST https://your-domain.com/api/tcm.prescriptionOrder/submitGancaoRecipel \ - -H "Content-Type: application/json" \ - -H "token: your_token" \ - -d '{"id": 订单ID}' -``` - -## 如果仍然失败 - -### 方案 A: 检查 OpenSSL 版本 - -```bash -openssl version -``` - -**要求:** OpenSSL 1.0.2 或更高版本 - -**如果版本过低,升级 OpenSSL:** - -```bash -# CentOS/RHEL -sudo yum update openssl - -# Ubuntu/Debian -sudo apt-get update -sudo apt-get install --only-upgrade openssl - -# 重新编译 PHP(如果需要) -``` - -### 方案 B: 修改 OpenSSL 配置 - -编辑 `/etc/ssl/openssl.cnf`: - -```ini -# 在文件开头添加 -openssl_conf = openssl_init - -[openssl_init] -ssl_conf = ssl_sect - -[ssl_sect] -system_default = system_default_sect - -[system_default_sect] -MinProtocol = TLSv1.2 -CipherString = DEFAULT@SECLEVEL=1 -``` - -重启服务: -```bash -sudo systemctl restart php-fpm -``` - -### 方案 C: 使用不同的 TLS 版本 - -如果 TLS 1.2 不工作,尝试其他版本。 - -修改 `GancaoOpenApiTransport.php`: - -```php -// 尝试 TLS 1.3 -curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_3); - -// 或者让 cURL 自动选择 -curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_DEFAULT); -``` - -### 方案 D: 禁用 HTTP/2 - -如果使用了 HTTP/2,尝试禁用: - -```php -curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); -``` - -### 方案 E: 增加调试信息 - -临时添加详细的 cURL 调试: - -```php -// 在 GancaoOpenApiTransport.php 的 post 方法中添加 -curl_setopt($ch, CURLOPT_VERBOSE, true); -$verbose = fopen('php://temp', 'w+'); -curl_setopt($ch, CURLOPT_STDERR, $verbose); - -// 在 curl_exec 之后添加 -rewind($verbose); -$verboseLog = stream_get_contents($verbose); -\think\facade\Log::info('cURL verbose output', ['log' => $verboseLog]); -``` - -查看详细日志: -```bash -tail -f server/runtime/log/$(date +%Y%m%d).log -``` - -### 方案 F: 联系甘草技术支持 - -提供以下信息: -1. `php test_gancao_ssl.php` 的完整输出 -2. `openssl version` 输出 -3. `php -v` 输出 -4. `curl --version` 输出 -5. 服务器操作系统版本 - -询问: -- 甘草网关支持的 TLS 版本 -- 推荐的 SSL 加密套件 -- 是否有特殊的 HTTP 头要求 -- 是否有 IP 白名单限制 - -## 常见问题 - -### Q1: 为什么 HTTP/1.0 会导致问题? - -A: 某些现代服务器不再支持 HTTP/1.0,或者在 HTTP/1.0 下有不同的 SSL 处理逻辑。HTTP/1.1 是更标准的选择。 - -### Q2: SECLEVEL=1 是什么? - -A: OpenSSL 1.1.0+ 引入了安全级别概念: -- SECLEVEL=2(默认):要求 112 位安全性 -- SECLEVEL=1:要求 80 位安全性(更宽松) -- SECLEVEL=0:允许所有加密套件(不推荐) - -降低安全级别可以兼容更多服务器,但会降低安全性。 - -### Q3: 为什么要禁用 SSL 验证? - -A: 在开发/测试环境中,禁用 SSL 验证可以避免证书问题。**生产环境建议启用验证**。 - -### Q4: TCP keepalive 有什么用? - -A: 保持 TCP 连接活跃,防止长时间传输时连接被中断。 - -## 验证修复成功 - -修复成功的标志: - -1. ✅ `php test_gancao_ssl.php` 显示 "✓ 成功获取 token" -2. ✅ 接口返回成功: - ```json - { - "code": 1, - "msg": "甘草药方上传成功", - "data": {...} - } - ``` -3. ✅ 日志中没有 SSL 错误 -4. ✅ 数据库中订单已更新 - -## 总结 - -SSL 连接问题通常是由于: -1. **TLS 版本不匹配** → 使用 TLS 1.2 -2. **HTTP 版本问题** → 使用 HTTP/1.1 -3. **OpenSSL 安全级别过高** → 降低到 SECLEVEL=1 -4. **连接超时** → 增加超时时间 - -修复后应该能正常连接甘草 API。如果仍有问题,运行 `test_gancao_ssl.php` 并根据输出进一步诊断。 diff --git a/GANCAO_UPLOAD_BUTTON_CONDITION.md b/GANCAO_UPLOAD_BUTTON_CONDITION.md deleted file mode 100644 index c760cf10..00000000 --- a/GANCAO_UPLOAD_BUTTON_CONDITION.md +++ /dev/null @@ -1,169 +0,0 @@ -# 甘草上传药方按钮显示条件 - -## 需求 -"上传药方"按钮应该只在特定条件下显示: -1. 处方审核已通过 -2. 还没有上传过甘草订单(gancao_reciperl_order_no 为空) - -## 实现 - -### 文件:`admin/src/views/consumer/prescription/order_list.vue` - -#### 1. 添加条件判断函数 - -```typescript -function canUploadGancaoRow(row: { - prescription_audit_status?: number - gancao_reciperl_order_no?: string -}) { - // 处方审核已通过(1) 且没有甘草订单号时才显示 - const rxStatus = Number(row.prescription_audit_status) - const gancaoOrderNo = String(row.gancao_reciperl_order_no || '').trim() - return rxStatus === 1 && gancaoOrderNo === '' -} -``` - -**逻辑说明**: -- `prescription_audit_status === 1`:处方审核状态为"已通过" -- `gancao_reciperl_order_no` 为空:还没有上传过甘草订单 - -#### 2. 更新按钮显示条件 - -```vue -上传药方 -``` - -## 显示逻辑 - -### 场景 1:处方未审核 -``` -prescription_audit_status: 0 (待审核) -gancao_reciperl_order_no: '' -→ 按钮隐藏 ❌ -``` - -### 场景 2:处方已驳回 -``` -prescription_audit_status: 2 (已驳回) -gancao_reciperl_order_no: '' -→ 按钮隐藏 ❌ -``` - -### 场景 3:处方已通过,未上传 -``` -prescription_audit_status: 1 (已通过) -gancao_reciperl_order_no: '' -→ 按钮显示 ✅ -``` - -### 场景 4:处方已通过,已上传 -``` -prescription_audit_status: 1 (已通过) -gancao_reciperl_order_no: 'GC202604150001' -→ 按钮隐藏 ❌ -``` - -## 审核状态说明 - -| 状态值 | 说明 | 是否显示按钮 | -|--------|------|-------------| -| 0 | 待审核 | ❌ | -| 1 | 已通过 | ✅ (如果未上传) | -| 2 | 已驳回 | ❌ | - -## 业务流程 - -``` -创建订单 - ↓ -处方待审核 (prescription_audit_status = 0) - ↓ 审核 -处方已通过 (prescription_audit_status = 1) - ↓ 显示"上传药方"按钮 -点击上传 - ↓ 调用甘草API -上传成功,保存订单号 (gancao_reciperl_order_no = 'GC...') - ↓ 按钮隐藏 -订单继续履约流程 -``` - -## 相关字段 - -### prescription_audit_status (处方审核状态) -- **类型**: tinyint(2) -- **值**: - - 0: 待审核 - - 1: 已通过 - - 2: 已驳回 - -### gancao_reciperl_order_no (甘草处方订单号) -- **类型**: varchar(32) -- **说明**: 上传甘草成功后返回的订单号 -- **示例**: `GC202604150001` -- **空值**: 表示还未上传 - -## 防止重复上传 - -通过检查 `gancao_reciperl_order_no` 字段,确保: -1. 每个订单只能上传一次 -2. 上传成功后按钮自动隐藏 -3. 避免重复提交导致的数据混乱 - -## 用户体验 - -### 上传前 -``` -操作列: -[详情/审核] [编辑] [上传药方] [确认发货] ... -``` - -### 上传后 -``` -操作列: -[详情/审核] [编辑] [确认发货] ... -``` -"上传药方"按钮消失,界面更简洁。 - -## 错误处理 - -如果用户尝试重复上传(通过其他方式),后端也有验证: - -```php -// server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php -if (!self::canUploadGancaoRecipel($item, $adminId, $adminInfo, $assistantByDiag)) { - self::$error = '须处方审核通过后方可上传甘草;已完成/已取消订单不可上传;同一订单仅可上传一次'; - return false; -} -``` - -## 测试清单 - -- [ ] 创建新订单,处方待审核,按钮不显示 -- [ ] 处方审核通过,按钮显示 -- [ ] 点击上传药方,上传成功 -- [ ] 刷新页面,按钮消失 -- [ ] 处方被驳回,按钮不显示 -- [ ] 已上传的订单,按钮不显示 - -## 相关文件 - -- `admin/src/views/consumer/prescription/order_list.vue` - 订单列表页面 -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 后端业务逻辑 -- `GANCAO_CALLBACK_SETUP.md` - 甘草回调功能文档 - -## 总结 - -通过添加 `canUploadGancaoRow()` 函数,实现了智能的按钮显示控制: -- ✅ 只在处方审核通过后显示 -- ✅ 上传成功后自动隐藏 -- ✅ 防止重复上传 -- ✅ 提升用户体验 - -这样可以避免用户误操作,确保业务流程的正确性。 diff --git a/IM_CHAT_MESSAGES_PERFORMANCE_FIX.md b/IM_CHAT_MESSAGES_PERFORMANCE_FIX.md deleted file mode 100644 index ce46431d..00000000 --- a/IM_CHAT_MESSAGES_PERFORMANCE_FIX.md +++ /dev/null @@ -1,255 +0,0 @@ -# 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/INSTALLATION_CHECKLIST.md b/INSTALLATION_CHECKLIST.md deleted file mode 100644 index d1ca2d98..00000000 --- a/INSTALLATION_CHECKLIST.md +++ /dev/null @@ -1,361 +0,0 @@ -# 处方库功能 - 安装检查清单 - -## 📋 安装前检查 - -### 环境要求 -- [ ] PHP >= 7.4 -- [ ] MySQL >= 5.7 -- [ ] Node.js >= 14.x(前端开发) -- [ ] 已安装并运行 ThinkPHP 框架 -- [ ] 已安装并运行 Vue 3 前端项目 - -### 权限检查 -- [ ] 数据库创建表权限 -- [ ] 文件读写权限 -- [ ] 后台管理员账号 - -## 🔧 安装步骤 - -### 步骤1:备份数据库 ✅ -```bash -# 备份当前数据库(推荐) -mysqldump -u用户名 -p密码 数据库名 > backup_$(date +%Y%m%d).sql -``` -- [ ] 已完成数据库备份 - -### 步骤2:安装数据库表 ✅ - -#### 方式A:自动安装(推荐) - -**Windows:** -```bash -双击运行 install_prescription_library.bat -``` - -**Linux/Mac:** -```bash -chmod +x install_prescription_library.sh -./install_prescription_library.sh -``` - -#### 方式B:手动安装 -```bash -mysql -h主机 -u用户名 -p密码 数据库名 < server/database/migrations/create_prescription_library.sql -``` - -- [ ] 数据库表安装成功 -- [ ] 验证表是否创建:`SHOW TABLES LIKE '%prescription_library%';` - -### 步骤3:验证后端文件 ✅ - -检查以下文件是否存在: - -**模型层:** -- [ ] server/app/common/model/tcm/PrescriptionLibrary.php - -**控制器层:** -- [ ] server/app/adminapi/controller/tcm/PrescriptionLibraryController.php - -**业务逻辑层:** -- [ ] server/app/adminapi/logic/tcm/PrescriptionLibraryLogic.php - -**列表逻辑层:** -- [ ] server/app/adminapi/lists/tcm/PrescriptionLibraryLists.php - -**验证器层:** -- [ ] server/app/adminapi/validate/tcm/PrescriptionLibraryValidate.php - -### 步骤4:验证前端文件 ✅ - -检查以下文件是否存在: - -**页面组件:** -- [ ] admin/src/views/consumer/prescription/list.vue - -**API接口:** -- [ ] admin/src/api/tcm.ts(已更新) - -### 步骤5:配置路由(如需要) ✅ - -ThinkPHP 通常自动识别路由,无需手动配置。 - -如需手动配置,在路由文件中添加: -```php -// 处方库路由 -Route::group('tcm.prescriptionLibrary', function () { - Route::get('lists', 'tcm.PrescriptionLibrary/lists'); - Route::post('add', 'tcm.PrescriptionLibrary/add'); - Route::post('edit', 'tcm.PrescriptionLibrary/edit'); - Route::post('delete', 'tcm.PrescriptionLibrary/delete'); - Route::get('detail', 'tcm.PrescriptionLibrary/detail'); -}); -``` - -- [ ] 路由配置完成(如需要) - -### 步骤6:配置菜单权限 ✅ - -在后台管理系统中添加菜单项: - -**菜单配置示例:** -``` -菜单名称:处方库 -菜单路径:/consumer/prescription/list -图标:el-icon-document -排序:100 -权限标识:tcm.prescriptionLibrary/lists -``` - -**权限节点:** -- [ ] tcm.prescriptionLibrary/lists(查看列表) -- [ ] tcm.prescriptionLibrary/add(添加处方) -- [ ] tcm.prescriptionLibrary/edit(编辑处方) -- [ ] tcm.prescriptionLibrary/delete(删除处方) -- [ ] tcm.prescriptionLibrary/detail(查看详情) - -- [ ] 菜单配置完成 -- [ ] 权限节点配置完成 - -### 步骤7:重启服务(如需要) ✅ - -**后端:** -```bash -# 清除缓存 -php think clear - -# 重启PHP-FPM(如使用) -sudo systemctl restart php-fpm -``` - -**前端:** -```bash -# 开发环境 -npm run dev - -# 生产环境 -npm run build -``` - -- [ ] 后端服务已重启 -- [ ] 前端已重新编译 - -## 🧪 功能测试 - -### 测试1:访问页面 ✅ -- [ ] 能够访问处方库页面 -- [ ] 页面正常显示,无报错 -- [ ] 列表为空或显示数据 - -### 测试2:创建处方 ✅ -- [ ] 点击"新增处方"按钮 -- [ ] 填写处方名称 -- [ ] 添加药材 -- [ ] 设置是否公开 -- [ ] 保存成功 - -### 测试3:查看处方 ✅ -- [ ] 点击"查看"按钮 -- [ ] 显示处方详情 -- [ ] 字段不可编辑 - -### 测试4:编辑处方 ✅ -- [ ] 点击"编辑"按钮 -- [ ] 修改处方信息 -- [ ] 保存成功 - -### 测试5:删除处方 ✅ -- [ ] 点击"删除"按钮 -- [ ] 确认删除 -- [ ] 删除成功 - -### 测试6:搜索功能 ✅ -- [ ] 输入处方名称搜索 -- [ ] 选择是否公开筛选 -- [ ] 显示正确结果 - -### 测试7:权限测试 ✅ -- [ ] 用户A创建私有处方 -- [ ] 用户B无法查看用户A的私有处方 -- [ ] 用户A创建公开处方 -- [ ] 用户B可以查看用户A的公开处方 -- [ ] 用户B无法删除用户A的处方 - -## 🔍 问题排查 - -### 问题1:无法访问页面 -**可能原因:** -- 路由未配置 -- 权限未分配 -- 前端路由错误 - -**解决方案:** -1. 检查路由配置 -2. 检查菜单权限 -3. 查看浏览器控制台错误 - -### 问题2:数据库表创建失败 -**可能原因:** -- 数据库连接失败 -- 权限不足 -- 表已存在 - -**解决方案:** -1. 检查数据库配置 -2. 检查用户权限 -3. 删除已存在的表后重试 - -### 问题3:API请求失败 -**可能原因:** -- 后端文件未上传 -- 路由配置错误 -- 权限验证失败 - -**解决方案:** -1. 检查后端文件是否存在 -2. 检查API路径是否正确 -3. 查看后端日志 - -### 问题4:前端页面报错 -**可能原因:** -- 前端文件未编译 -- API接口路径错误 -- 组件引用错误 - -**解决方案:** -1. 重新编译前端代码 -2. 检查API接口配置 -3. 查看浏览器控制台错误 - -## 📊 验证清单 - -### 数据库验证 -```sql --- 检查表是否存在 -SHOW TABLES LIKE '%prescription_library%'; - --- 检查表结构 -DESC zyt_prescription_library; - --- 检查索引 -SHOW INDEX FROM zyt_prescription_library; - --- 插入测试数据 -INSERT INTO `zyt_prescription_library` -(`prescription_name`, `herbs`, `is_public`, `creator_id`, `creator_name`, `create_time`, `update_time`) -VALUES -('测试处方', '[{"name":"测试药材","dosage":10}]', 1, 1, '测试医生', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); - --- 查询测试数据 -SELECT * FROM zyt_prescription_library WHERE prescription_name = '测试处方'; - --- 删除测试数据 -DELETE FROM zyt_prescription_library WHERE prescription_name = '测试处方'; -``` - -- [ ] 表结构正确 -- [ ] 索引创建成功 -- [ ] 数据插入成功 -- [ ] 数据查询成功 -- [ ] 数据删除成功 - -### API验证 -使用 Postman 或类似工具测试: - -**获取列表:** -``` -GET /api/tcm.prescriptionLibrary/lists -``` - -**添加处方:** -``` -POST /api/tcm.prescriptionLibrary/add -Body: { - "prescription_name": "测试处方", - "herbs": [{"name": "测试药材", "dosage": 10}], - "is_public": 1 -} -``` - -**编辑处方:** -``` -POST /api/tcm.prescriptionLibrary/edit -Body: { - "id": 1, - "prescription_name": "测试处方(已修改)", - "herbs": [{"name": "测试药材", "dosage": 15}], - "is_public": 0 -} -``` - -**删除处方:** -``` -POST /api/tcm.prescriptionLibrary/delete -Body: { - "id": 1 -} -``` - -**获取详情:** -``` -GET /api/tcm.prescriptionLibrary/detail?id=1 -``` - -- [ ] 列表接口正常 -- [ ] 添加接口正常 -- [ ] 编辑接口正常 -- [ ] 删除接口正常 -- [ ] 详情接口正常 - -## ✅ 安装完成确认 - -### 最终检查 -- [ ] 所有文件已上传 -- [ ] 数据库表已创建 -- [ ] 菜单权限已配置 -- [ ] 功能测试通过 -- [ ] API测试通过 -- [ ] 权限测试通过 - -### 文档确认 -- [ ] 已阅读 PRESCRIPTION_LIBRARY_README.md -- [ ] 已阅读 QUICK_START_PRESCRIPTION_LIBRARY.md -- [ ] 已了解功能使用方法 - -### 备份确认 -- [ ] 已备份数据库 -- [ ] 已备份原有代码 -- [ ] 已记录安装日期和版本 - -## 📝 安装记录 - -**安装日期:** _______________ - -**安装人员:** _______________ - -**版本号:** v1.0.0 - -**数据库名:** _______________ - -**备注:** -``` -_______________________________________________ -_______________________________________________ -_______________________________________________ -``` - -## 🎉 安装成功! - -恭喜!处方库功能已成功安装。 - -**下一步:** -1. 创建第一个处方 -2. 测试各项功能 -3. 培训使用人员 -4. 收集反馈意见 - -**技术支持:** -如有问题,请查看文档或联系技术支持团队。 - ---- - -**祝使用愉快!** 🚀 diff --git a/INTERNAL_COST_PERMISSION_CONTROL.md b/INTERNAL_COST_PERMISSION_CONTROL.md deleted file mode 100644 index 6ac24780..00000000 --- a/INTERNAL_COST_PERMISSION_CONTROL.md +++ /dev/null @@ -1,142 +0,0 @@ -# 内部成本字段权限控制 - -## 需求说明 - -在业务订单列表中添加内部成本字段的显示,但只有指定角色的用户才能看到该字段。 - -## 权限配置 - -文件:`server/config/project.php` - -```php -// 处方业务订单 internal_cost 等财务字段:以下角色 ID + root 可见 -'prescription_order_finance_roles' => [0, 3, 6], -``` - -- 角色 0:超级管理员 -- 角色 3:管理员 -- 角色 6:药师 - -## 后端权限控制 - -文件:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -后端已经实现了权限控制逻辑: - -```php -public static function canViewInternalCost(array $adminInfo): bool -{ - if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { - return true; - } - $allow = Config::get('project.prescription_order_finance_roles', [0, 3]); - $allow = array_map('intval', is_array($allow) ? $allow : []); - $myRoles = array_map('intval', $adminInfo['role_id'] ?? []); - - return count(array_intersect($myRoles, $allow)) > 0; -} - -public static function maskInternalCostIfNeeded(array &$row, array $adminInfo): void -{ - if (!self::canViewInternalCost($adminInfo)) { - unset($row['internal_cost']); - } -} -``` - -后端在返回数据前会自动移除 `internal_cost` 字段,如果用户没有权限。 - -## 前端权限控制 - -文件:`admin/src/views/consumer/prescription/order_list.vue` - -### 1. 添加权限检查函数 - -```typescript -import useUserStore from '@/stores/modules/user' - -const userStore = useUserStore() - -/** 与 server/config/project.php prescription_order_finance_roles 保持一致 */ -const FINANCE_ROLE_IDS = [0, 3, 6] - -/** 判断当前用户是否有查看财务字段(内部成本)的权限 */ -const canViewFinanceFields = () => { - const u = userStore.userInfo - if (!u) return false - if (Number(u.root) === 1) return true - const ids = Array.isArray(u.role_ids) ? u.role_ids.map((n: unknown) => Number(n)) : [] - return FINANCE_ROLE_IDS.some((rid) => ids.includes(rid)) -} -``` - -### 2. 在列表中添加权限控制 - -**表格列(只有有权限的用户才能看到整列):** - -```vue - - - -``` - -**金额信息弹出框(只有有权限的用户才能看到内部成本行):** - -```vue -
- 内部成本: - ¥{{ formatMoney(row.internal_cost) }} -
-``` - -## 权限控制层级 - -### 后端(第一层防护) - -1. 在 `PrescriptionOrderLogic::detail()` 和列表方法中调用 `maskInternalCostIfNeeded()` -2. 如果用户没有权限,直接从返回数据中移除 `internal_cost` 字段 -3. 前端收到的数据中不包含该字段 - -### 前端(第二层防护) - -1. 使用 `v-if="canViewFinanceFields()"` 控制整个表格列的显示 -2. 在弹出框中也使用 `canViewFinanceFields()` 控制内部成本行的显示 -3. 即使后端数据泄露,前端也不会显示 - -## 双重防护的优势 - -1. **后端防护**:确保数据安全,无权限用户无法获取敏感数据 -2. **前端防护**:优化用户体验,无权限用户不会看到空白或无意义的列 -3. **配置同步**:前端 `FINANCE_ROLE_IDS` 与后端配置保持一致 - -## 测试验证 - -### 有权限的用户(角色 0, 3, 6) - -1. 登录系统 -2. 进入"消费者处方订单"列表 -3. 可以看到"内部成本"列 -4. 点击金额信息图标,弹出框中显示内部成本 - -### 无权限的用户(其他角色) - -1. 登录系统 -2. 进入"消费者处方订单"列表 -3. 看不到"内部成本"列 -4. 点击金额信息图标,弹出框中不显示内部成本 - -## 注意事项 - -1. **配置同步**:修改后端配置后,需要同步修改前端 `FINANCE_ROLE_IDS` 常量 -2. **缓存清除**:修改配置后需要运行 `php think clear` 清除缓存 -3. **角色分配**:确保用户的角色ID正确分配在数据库中 -4. **超级管理员**:`root=1` 的用户始终有权限查看所有字段 - -## 相关文件 - -- `server/config/project.php` - 权限配置 -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 后端权限逻辑 -- `admin/src/views/consumer/prescription/order_list.vue` - 前端显示控制 -- `admin/src/stores/modules/user.ts` - 用户信息存储 diff --git a/JTEXPRESS_SUPPORT_ADDED.md b/JTEXPRESS_SUPPORT_ADDED.md deleted file mode 100644 index bf4ebcde..00000000 --- a/JTEXPRESS_SUPPORT_ADDED.md +++ /dev/null @@ -1,251 +0,0 @@ -# 极兔速递支持已添加 - -## 更新说明 - -系统已添加极兔速递(J&T Express)的物流查询支持。 - -### 单号示例 -- `JT5472795424064` - 极兔速递单号格式:JT + 13位数字 - ---- - -## 更新内容 - -### 1. 后端更新 - -**文件:** `server/app/common/service/ExpressTrackService.php` - -- 添加极兔速递常量 `KUAIDI_COM_JT = 'jtexpress'` -- 添加极兔单号识别规则:`/^JT\d{13}$/i` -- 添加极兔官网查询链接:`https://www.jtexpress.com.cn/index/query/gzquery.html?bills={单号}` -- 更新 `resolveCarrier()` 方法支持极兔识别 -- 更新 `officialUrls()` 方法返回极兔链接 - -**文件:** `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -- 更新 `normalizeExpressCompany()` 方法支持 `jt` 和 `jtexpress` - -### 2. 前端更新 - -**文件:** `admin/src/views/consumer/prescription/order_list.vue` - -- 添加极兔速递选项到快递公司下拉框 -- 添加"极兔速递查件"官网链接按钮 -- 更新 `expressCompanyLabel()` 函数支持极兔显示 -- 更新 `detailOfficialUrls` 计算属性包含极兔链接 -- 更新提示文本说明支持极兔速递 - ---- - -## 使用方法 - -### 方式一:自动识别(推荐) - -系统会自动识别以 `JT` 开头且后跟13位数字的单号为极兔速递。 - -**示例:** -- 单号:`JT5472795424064` -- 快递公司:选择"自动识别" -- 系统自动识别为:极兔速递 - -### 方式二:手动选择 - -在订单编辑或查询时,手动选择"极兔速递"。 - -**步骤:** -1. 打开业务订单详情 -2. 在"快递公司"下拉框中选择"极兔速递" -3. 填写快递单号 -4. 点击"查询物流" - ---- - -## 快递100 API 支持 - -极兔速递在快递100中的编码为 `jtexpress`,系统已配置。 - -### 查询参数示例 - -```json -{ - "com": "jtexpress", - "num": "JT5472795424064", - "resultv2": "1" -} -``` - -### 注意事项 - -1. **快递100配置** - - 确保已配置 `LOGISTICS_KUAIDI100_CUSTOMER` 和 `LOGISTICS_KUAIDI100_KEY` - - 确保 `LOGISTICS_KUAIDI100_DISABLE = false` - -2. **单号格式** - - 极兔单号通常为:JT + 13位数字 - - 示例:`JT5472795424064` - -3. **查询时机** - - 快递已揽收后才能查询到物流信息 - - 刚下单的快递可能需要等待1-2小时 - ---- - -## 官网查询 - -如果快递100查询失败,可以使用官网链接: - -**极兔速递官网:** https://www.jtexpress.com.cn/ - -**查询链接格式:** -``` -https://www.jtexpress.com.cn/index/query/gzquery.html?bills={单号} -``` - -**示例:** -``` -https://www.jtexpress.com.cn/index/query/gzquery.html?bills=JT5472795424064 -``` - ---- - -## 测试 - -### 测试脚本 - -运行测试脚本验证极兔速递支持: - -```bash -cd server -php test_jt_express.php -``` - -### 测试步骤 - -1. **创建测试订单** - - 快递公司:选择"极兔速递"或"自动识别" - - 快递单号:`JT5472795424064`(替换为真实单号) - -2. **查询物流** - - 打开订单详情 - - 点击"查询物流"按钮 - - 查看是否返回物流轨迹 - -3. **官网查询** - - 点击"极兔速递查件"链接 - - 确认能正常打开极兔官网查询页面 - ---- - -## 支持的快递公司列表 - -| 快递公司 | 代码 | 快递100编码 | 单号格式 | 自动识别 | -|---------|------|------------|---------|---------| -| 顺丰速运 | sf | shunfeng | SF + 数字 | ✅ | -| 京东快递 | jd | jd | JD/JDV/JDK/JDEX + 数字 | ✅ | -| 极兔速递 | jt | jtexpress | JT + 13位数字 | ✅ | - ---- - -## 扩展其他快递公司 - -如需添加更多快递公司(如中通、圆通、申通等),请参考以下步骤: - -### 1. 查询快递100编码 - -访问快递100官方文档查询快递公司编码: -https://api.kuaidi100.com/document/5f0ffb5ebc8da837cbd8aefc/5f0ffc3cbc8da837cbd8afc0 - -### 2. 修改后端代码 - -在 `ExpressTrackService.php` 中: - -```php -// 添加常量 -private const KUAIDI_COM_ZTO = 'zhongtong'; // 中通快递 - -// 添加识别规则 -if ($ec === 'zto' || $ec === 'zhongtong') { - return ['carrier' => 'zto', 'kuaidi_com' => self::KUAIDI_COM_ZTO, 'label' => '中通快递']; -} - -// 添加单号自动识别(可选) -if (preg_match('/^[0-9]{12}$/', $num)) { - return ['carrier' => 'zto', 'kuaidi_com' => self::KUAIDI_COM_ZTO, 'label' => '中通快递(单号识别)']; -} - -// 添加官网链接 -'zto' => 'https://www.zto.com/GuestService/Bill?txtBill=' . $enc, -``` - -### 3. 修改前端代码 - -在 `order_list.vue` 中: - -```vue - - - - -中通快递查件 - - -zto: `https://www.zto.com/GuestService/Bill?txtBill=${enc}` - - -if (s === 'zto' || s === 'zhongtong') return '中通快递' -``` - -### 4. 更新后端逻辑 - -在 `PrescriptionOrderLogic.php` 中: - -```php -if (!in_array($ec, ['sf', 'jd', 'jt', 'zto', 'auto'], true)) { - return 'auto'; -} -``` - ---- - -## 常见问题 - -### Q1: 极兔单号查询失败? - -**可能原因:** -1. 快递尚未揽收 -2. 单号格式错误 -3. 快递100配置问题 - -**解决方案:** -1. 确认单号格式:JT + 13位数字 -2. 等待快递揽收后再查询 -3. 使用"极兔速递查件"官网链接 - -### Q2: 自动识别不准确? - -**解决方案:** -手动选择快递公司,不使用"自动识别"。 - -### Q3: 如何添加更多快递公司? - -参考上面的"扩展其他快递公司"章节。 - ---- - -## 更新历史 - -### 2024-01-15 -- ✅ 添加极兔速递支持 -- ✅ 支持单号自动识别(JT + 13位数字) -- ✅ 添加极兔官网查询链接 -- ✅ 更新前端选项和显示 -- ✅ 创建测试脚本 - ---- - -## 相关文档 - -- `LOGISTICS_INTEGRATION_GUIDE.md` - 完整物流集成指南 -- `LOGISTICS_UPDATE_README.md` - 物流功能更新说明 -- `server/.env.logistics.example` - 配置示例 -- 快递100官方文档:https://api.kuaidi100.com/ diff --git a/KUAIDI100_ACCOUNT_ISSUE.md b/KUAIDI100_ACCOUNT_ISSUE.md deleted file mode 100644 index 26177936..00000000 --- a/KUAIDI100_ACCOUNT_ISSUE.md +++ /dev/null @@ -1,281 +0,0 @@ -# 快递100账号问题解决方案 - -## 问题确认 - -根据快递100官方文档,错误码 `400` 的含义是: - -``` -400 - 找不到对应公司 -原因:提交数据不完整或者账号未充值 -``` - -你的账号返回此错误,最可能的原因是:**账号未充值或余额不足** - ---- - -## 立即检查 - -### 1. 登录快递100后台 - -访问:https://www.kuaidi100.com/ - -使用你的账号登录(Customer: `CC25SV92****`) - -### 2. 检查账户余额 - -在后台查看: -- 当前余额 -- 可用查询次数 -- 套餐类型 -- 支持的快递公司列表 - -### 3. 检查充值记录 - -确认是否已经充值: -- 充值金额 -- 充值时间 -- 是否到账 - ---- - -## 解决方案 - -### 方案一:充值账户(推荐) - -1. **登录快递100后台** - - 网址:https://www.kuaidi100.com/ - - 使用你的账号登录 - -2. **进入充值页面** - - 找到"充值"或"套餐购买"入口 - - 选择合适的套餐 - -3. **选择套餐** - - 基础套餐:支持常用快递公司 - - 标准套餐:支持更多快递公司 - - 企业套餐:支持所有快递公司 - -4. **完成支付** - - 支付宝/微信/对公转账 - - 等待到账(通常即时到账) - -5. **测试查询** - ```bash - cd server - php debug_kuaidi100.php - ``` - -### 方案二:联系客服 - -如果确认已充值但仍然报错: - -**快递100客服:** -- 官网:https://www.kuaidi100.com/ -- 在线客服:工作日 9:00-18:00 -- 电话:查看官网联系方式 - -**提供信息:** -- Customer ID: `CC25SV92****` -- 错误信息:`找不到对应公司 (returnCode: 400)` -- 测试单号:`JD0230761381812` -- 快递公司:京东快递 - -### 方案三:使用官网查询(临时方案) - -在充值前,系统已提供官网查询链接: - -**京东快递:** -``` -https://www.jdl.com/#/trackQuery?waybillCode=JD0230761381812 -``` - -**极兔速递:** -``` -https://www.jtexpress.com.cn/index/query/gzquery.html?bills=JT5472795424064 -``` - -**顺丰速运:** -``` -https://www.sf-express.com/cn/sc/dynamic_function/waybill/#search/bill-number/SF1234567890 -``` - ---- - -## 快递100套餐说明 - -### 基础套餐 -- **价格**:约 ¥100-500/年 -- **查询次数**:1000-5000次 -- **支持快递**:10-20家常用快递 -- **适合**:小型企业、个人开发者 - -### 标准套餐 -- **价格**:约 ¥500-2000/年 -- **查询次数**:5000-20000次 -- **支持快递**:50+家快递公司 -- **适合**:中型企业 - -### 企业套餐 -- **价格**:约 ¥2000+/年 -- **查询次数**:20000+次 -- **支持快递**:100+家快递公司 -- **适合**:大型企业、电商平台 - -**注意:** 具体价格和套餐内容以快递100官网为准。 - ---- - -## 测试步骤 - -### 充值后测试 - -1. **测试配置** - ```bash - cd server - php check_logistics_config.php - ``` - -2. **测试API调用** - ```bash - php debug_kuaidi100.php - ``` - - 应该看到: - ```json - { - "message": "ok", - "state": "0", - "data": [...] - } - ``` - -3. **测试前端查询** - - 打开业务订单详情 - - 填写快递单号 - - 点击"查询物流" - - 应该能看到物流轨迹 - ---- - -## 常见问题 - -### Q1: 充值后多久生效? - -**A:** 通常即时生效。如果超过10分钟未生效,联系客服。 - -### Q2: 如何查看剩余查询次数? - -**A:** 登录快递100后台,在首页或账户信息页面查看。 - -### Q3: 查询次数用完了怎么办? - -**A:** -1. 续费充值 -2. 升级套餐 -3. 临时使用官网查询链接 - -### Q4: 可以按次付费吗? - -**A:** 快递100通常提供套餐制,具体咨询客服。 - -### Q5: 支持哪些快递公司? - -**A:** -- 基础套餐:顺丰、京东、中通、圆通、申通、韵达等常用快递 -- 标准套餐:50+家快递公司 -- 企业套餐:100+家快递公司 - -完整列表见:https://api.kuaidi100.com/document/5f0ffb5ebc8da837cbd8aefc/5f0ffc3cbc8da837cbd8afc0 - ---- - -## 系统当前状态 - -### ✅ 已完成的优化 - -1. **错误提示优化** - - 当快递100返回400错误时 - - 提示:"快递100暂不支持该快递公司或编码错误,请使用下方官网链接查询" - -2. **官网链接备用** - - 所有快递公司都提供官网查询链接 - - 用户可以直接点击跳转 - -3. **支持多家快递** - - 顺丰速运(shunfeng) - - 京东快递(jingdong) - - 极兔速递(jtexpress) - -4. **详细日志记录** - - 记录所有查询请求和响应 - - 方便排查问题 - -### 🔄 待完成(充值后) - -1. **实时物流查询** - - 快递100 API实时查询 - - 返回完整物流轨迹 - - 显示物流状态 - -2. **自动识别快递公司** - - 根据单号自动识别 - - 无需手动选择 - -3. **高级功能** - - 预计到达时间 - - 快递员信息 - - 路由信息 - ---- - -## 建议 - -### 短期(立即可用) - -**使用官网查询链接:** -- 功能完全可用 -- 无需额外费用 -- 只需点击跳转 - -### 中期(推荐) - -**充值快递100账户:** -- 统一的查询接口 -- 更好的用户体验 -- 自动化物流追踪 - -### 长期(可选) - -**对接多个物流API:** -- 快递100作为主要渠道 -- 各快递公司官方API作为备用 -- 提高查询成功率 - ---- - -## 总结 - -**问题原因:** 快递100账号未充值或余额不足 - -**立即行动:** -1. 登录快递100后台检查余额 -2. 充值账户 -3. 测试查询功能 - -**临时方案:** -- 使用系统提供的官网查询链接 -- 功能完全可用 - -**充值后:** -- 可以使用快递100实时查询 -- 获得更好的用户体验 -- 支持更多快递公司 - ---- - -## 相关文档 - -- 快递100官方文档:https://api.kuaidi100.com/document/5f0ffb5ebc8da837cbd8aefc -- 快递公司编码表:https://api.kuaidi100.com/document/5f0ffb5ebc8da837cbd8aefc/5f0ffc3cbc8da837cbd8afc0 -- `LOGISTICS_INTEGRATION_GUIDE.md` - 物流集成指南 -- `KUAIDI100_TROUBLESHOOTING.md` - 故障排查指南 diff --git a/KUAIDI100_TROUBLESHOOTING.md b/KUAIDI100_TROUBLESHOOTING.md deleted file mode 100644 index f7c404fa..00000000 --- a/KUAIDI100_TROUBLESHOOTING.md +++ /dev/null @@ -1,251 +0,0 @@ -# 快递100查询问题排查指南 - -## 问题现象 - -查询京东快递单号 `JD0230761381812` 时,快递100返回错误: -``` -"找不到对应公司" (returnCode: 400) -``` - -但在京东官网可以正常查询到物流信息。 - ---- - -## 问题原因 - -### 1. 快递100账号权限限制 - -快递100的不同套餐支持的快递公司数量不同: -- **基础版**:支持常用的10-20家快递公司 -- **标准版**:支持50+家快递公司 -- **企业版**:支持100+家快递公司 - -你的账号可能是基础版,不包含京东快递的查询权限。 - -### 2. 快递公司编码问题 - -快递100对某些快递公司有特殊的编码要求: -- 京东快递:`jingdong`(不是 `jd`) -- 极兔速递:`jtexpress`(不是 `jt`) -- 顺丰速运:`shunfeng`(不是 `sf`) - -### 3. 需要额外开通 - -某些快递公司需要在快递100后台单独开通才能查询。 - ---- - -## 解决方案 - -### 方案一:升级快递100套餐(推荐) - -1. 登录快递100后台:https://www.kuaidi100.com/ -2. 进入"套餐管理" -3. 升级到支持更多快递公司的套餐 -4. 或单独开通京东快递查询权限 - -### 方案二:使用官网查询(当前可用) - -系统已经提供了官网查询链接作为备用方案: - -**京东物流官网:** -``` -https://www.jdl.com/#/trackQuery?waybillCode=JD0230761381812 -``` - -**使用步骤:** -1. 在订单详情页面 -2. 点击"京东物流查件"链接 -3. 自动跳转到京东官网查询页面 -4. 查看完整的物流轨迹 - -### 方案三:联系快递100客服 - -如果确认套餐应该支持但仍然查询失败: - -1. 联系快递100客服 -2. 提供你的 Customer ID -3. 说明查询失败的快递公司 -4. 请求开通相应权限 - -**快递100客服:** -- 官网:https://www.kuaidi100.com/ -- 在线客服:工作日 9:00-18:00 - -### 方案四:使用自动识别 - -某些情况下,使用 `com=auto` 自动识别可能比指定快递公司更有效: - -```php -$paramArr = [ - 'com' => 'auto', // 自动识别 - 'num' => 'JD0230761381812', - 'resultv2' => '1', -]; -``` - ---- - -## 当前系统优化 - -系统已经做了以下优化: - -### 1. 改进错误提示 - -当快递100返回"找不到对应公司"时,系统会提示: -``` -"快递100暂不支持该快递公司或编码错误,请使用下方官网链接查询" -``` - -### 2. 提供官网链接 - -无论快递100是否可用,系统都会提供官网查询链接: -- 顺丰速运官网 -- 京东物流官网 -- 极兔速递官网 - -### 3. 记录详细日志 - -系统会记录快递100的错误信息,方便排查: -```php -Log::info('ExpressTrackService kuaidi100 business fail', [ - 'message' => '找不到对应公司', - 'returnCode' => '400', - 'num' => 'JD0230761381812', - 'com' => 'jingdong' -]); -``` - ---- - -## 测试步骤 - -### 1. 确认快递100配置 - -```bash -cd server -php check_logistics_config.php -``` - -应该显示: -``` -✅ 配置正确!快递100应该可以正常使用 -``` - -### 2. 测试API调用 - -```bash -php debug_kuaidi100.php -``` - -查看快递100的实际响应。 - -### 3. 测试自动识别 - -```bash -php test_auto_detect.php -``` - -尝试使用自动识别功能。 - -### 4. 前端测试 - -1. 打开业务订单详情 -2. 填写快递单号:`JD0230761381812` -3. 快递公司选择"京东快递" -4. 点击"查询物流" -5. 如果失败,点击"京东物流查件"链接 - ---- - -## 快递100支持的快递公司 - -### 常见快递公司编码 - -| 快递公司 | 快递100编码 | 是否需要手机号 | -|---------|------------|--------------| -| 顺丰速运 | shunfeng | 是(后4位) | -| 京东快递 | jingdong | 否 | -| 极兔速递 | jtexpress | 否 | -| 中通快递 | zhongtong | 否 | -| 圆通速递 | yuantong | 否 | -| 申通快递 | shentong | 否 | -| 韵达快递 | yunda | 否 | -| 百世快递 | huitongkuaidi | 否 | -| EMS | ems | 否 | -| 邮政包裹 | youzhengguonei | 否 | - -### 查询完整列表 - -访问快递100官方文档: -https://api.kuaidi100.com/document/5f0ffb5ebc8da837cbd8aefc/5f0ffc3cbc8da837cbd8afc0 - ---- - -## 常见错误码 - -| 错误码 | 说明 | 解决方案 | -|-------|------|---------| -| 400 | 找不到对应公司 | 检查快递公司编码或升级套餐 | -| 500 | 服务器错误 | 稍后重试 | -| 600 | 您不是合法的订阅者 | 检查 Customer 和 Key | -| 601 | SIGN签名错误 | 检查签名算法 | -| 700 | 订阅数据已达上限 | 升级套餐或等待重置 | -| 701 | 快递公司参数异常 | 检查 com 参数 | -| 702 | 快递单号参数异常 | 检查 num 参数 | -| 703 | 查询无结果 | 快递可能未揽收 | -| 704 | 快递公司识别失败 | 使用自动识别或指定公司 | - ---- - -## 建议 - -### 短期方案(立即可用) - -使用官网查询链接: -- 系统已经提供了所有快递公司的官网链接 -- 用户点击即可跳转查询 -- 无需额外配置 - -### 长期方案(推荐) - -1. **升级快递100套餐** - - 支持更多快递公司 - - 提供更好的用户体验 - - 统一的查询接口 - -2. **对接多个物流API** - - 快递100作为主要渠道 - - 各快递公司官方API作为备用 - - 自动切换,提高成功率 - -3. **缓存查询结果** - - 减少API调用次数 - - 降低费用 - - 提高响应速度 - ---- - -## 总结 - -当前问题是快递100账号不支持京东快递查询,但系统已经提供了官网查询链接作为备用方案。 - -**用户可以:** -1. 点击"京东物流查件"链接查询 -2. 联系快递100升级套餐 -3. 等待系统对接京东官方API - -**系统已优化:** -1. 改进错误提示 -2. 提供官网链接 -3. 记录详细日志 -4. 支持多种查询方式 - ---- - -## 相关文档 - -- `LOGISTICS_INTEGRATION_GUIDE.md` - 物流集成指南 -- `LOGISTICS_UPDATE_README.md` - 更新说明 -- `JTEXPRESS_SUPPORT_ADDED.md` - 极兔速递支持 -- 快递100官方文档:https://api.kuaidi100.com/ diff --git a/LOCAL_RECORDING_DEBUG_GUIDE.md b/LOCAL_RECORDING_DEBUG_GUIDE.md deleted file mode 100644 index f1e11114..00000000 --- a/LOCAL_RECORDING_DEBUG_GUIDE.md +++ /dev/null @@ -1,358 +0,0 @@ -# 本地录制调试指南(第二版) - -## 问题描述 -本地录制总是失败,`blob` 为空,导致无法保存通话录制文件。 - -## 根本原因分析 - -从实际日志分析发现: -1. **录制启动太晚**:原来的 1200ms 延迟导致录制在对方挂断后才启动 -2. **SDK 提前销毁轨道**:`stopLocalVideo/stopLocalAudio` 在 `exitRoom` 之前就销毁了视频轨道 -3. **时间窗口太短**:从对方离开到轨道销毁只有约 200-300ms - -关键时间线(问题场景): -``` -15:30:51.841 - 对方离开(USER_LEAVE 事件) -15:30:51.xxx - beginStopNow 被调用(但录制还未启动) -15:30:52.072 - stopLocalVideo/stopLocalAudio(轨道被销毁) -15:30:52.102 - exitRoom -15:30:52.374 - MediaRecorder 已启动(太晚了!) -15:30:52.xxx - 数据块数: 0(因为轨道已销毁) -``` - -## 已实施的修复(第二版) - -### 1. 大幅缩短录制启动延迟 -- **从 1200ms 缩短到 500ms** -- 使用 300ms 间隔重试(最多 10 次) -- 更快地检测和启动录制 - -```typescript -// 延迟 500ms 后开始尝试(给视频元素一点加载时间) -startLocalRecTimer = setTimeout(() => attemptStartRecording(0), 500) -``` - -### 2. 在通话结束事件时立即停止录制 -监听 TUICallEngine 事件,在检测到通话结束时立即停止: - -```typescript -const onEarlyStopRecording = () => { - console.log('[chat-dialog] 检测到通话结束事件,立即停止录制') - if (localCallRecorder.isRecording()) { - console.log('[chat-dialog] 录制进行中,触发 beginStopNow') - localCallRecorder.beginStopNow() - } -} -// 监听: CALL_END, USER_LEAVE, ON_CALL_NOT_CONNECTED -``` - -### 3. 缩短停止延迟 -- **从 300ms 缩短到 100ms** -- 减少等待时间,避免 SDK 提前销毁轨道 - -```typescript -// 缩短延迟到 100ms,避免 SDK 提前销毁轨道 -setTimeout(() => { - mr.stop() -}, 100) -``` - -### 4. 增强日志输出 -在关键位置添加了详细的 console.log: - -- **录制启动阶段**: - - 检查容器和通话状态 - - 视频元素数量和就绪状态 - - MediaRecorder 创建和启动状态 - - Canvas 和音频混音初始化 - -- **录制进行阶段**: - - 数据块收集(ondataavailable) - - MediaRecorder 状态变化 - -- **录制停止阶段**: - - beginStopNow 调用时机 - - 数据块数量 - - Blob 生成结果 - -### 5. 智能视频元素检测 -改进了视频元素查找逻辑: - -```typescript -// 1. 首先检查容器内的视频 -let videos = Array.from(root.querySelectorAll('video')) - -// 2. 如果没有,检查 body(TUICallKit Teleport) -if (videos.length === 0) { - const bodyVideos = Array.from(document.body.querySelectorAll('video')) - videos = bodyVideos.filter(v => { - const so = v.srcObject - if (!(so instanceof MediaStream)) return false - return so.getVideoTracks().some(t => t.readyState === 'live') - }) -} - -// 3. 检查视频是否有有效数据 -const hasReadyVideo = videos.some(v => - v.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA -) -``` - -## 调试步骤 - -### 1. 打开浏览器控制台 -在发起视频通话前,打开浏览器开发者工具(F12),切换到 Console 标签。 - -### 2. 发起视频通话 -点击视频通话按钮,观察控制台输出。 - -### 3. 关键日志检查点 - -#### 通话接通后(约 0.5-1 秒) -应该看到: -``` -[chat-dialog] 尝试启动本地录制(第 1 次) -[chat-dialog] 在 body 中找到活跃视频: 2 -[chat-dialog] 视频元素已就绪,立即启动录制 -[call-local-recorder] 使用 MIME 类型: video/webm;codecs=vp9,opus -[call-local-recorder] Canvas 视频轨道数: 1 -[call-local-recorder] 找到视频元素数量: 2 -[call-local-recorder] 输出流轨道: { video: 1, audio: 2 } -[call-local-recorder] MediaRecorder 创建成功 -[call-local-recorder] MediaRecorder.start() 调用成功 -[call-local-recorder] MediaRecorder 已启动 -[chat-dialog] ✅ 本地录制已成功启动 -``` - -#### 录制过程中(每 250ms) -应该看到: -``` -[call-local-recorder] 收到数据块,大小: 12345 (总计: 1 块) -[call-local-recorder] 收到数据块,大小: 15678 (总计: 2 块) -... -``` - -**重要**:如果没有看到数据块,说明录制有问题! - -#### 对方挂断或己方挂断时 -应该看到: -``` -[chat-dialog] 检测到通话结束事件,立即停止录制 -[chat-dialog] 录制进行中,触发 beginStopNow -[call-local-recorder] beginStopNow 被调用 -[call-local-recorder] 准备停止 MediaRecorder,当前状态: recording 已收集数据块: 15 -[call-local-recorder] 调用 requestData() -[call-local-recorder] 延迟后数据块数: 16 -[call-local-recorder] 调用 stop() -[call-local-recorder] MediaRecorder onstop 触发,数据块数: 16 -[call-local-recorder] 生成 Blob: 1234567 bytes, type: video/webm -[chat-dialog] localCallRecorder.stop() 返回: 1234567 bytes -[chat-dialog] 开始上传录制文件 -[chat-dialog] 上传成功,文件 URL: https://... -[chat-dialog] 关联录制文件到诊单 -本地录制已上传并关联到通话记录 -``` - -## 常见问题诊断 - -### 问题 1:录制启动太晚 -**症状**: -``` -[call-local-recorder] MediaRecorder 已启动 -(出现在 stopLocalVideo 之后) -``` - -**原因**: -- 视频元素加载慢 -- 重试次数不够 - -**解决**: -- 已缩短初始延迟到 500ms -- 增加重试次数到 10 次 -- 每次重试间隔 300ms - -### 问题 2:找不到视频元素 -**症状**: -``` -[chat-dialog] 未找到视频元素,300ms 后重试 -(重复多次) -``` - -**原因**: -- TUICallKit 将视频 Teleport 到 body -- 视频元素尚未渲染 - -**解决**: -- 代码已添加 body 视频检查 -- 添加了活跃视频流过滤 - -### 问题 3:视频元素未就绪 -**症状**: -``` -[chat-dialog] 视频元素未就绪,300ms 后重试 -``` - -**原因**: -- 视频流尚未加载 -- readyState < HAVE_CURRENT_DATA - -**解决**: -- 代码已添加就绪检查和重试 -- 最多重试 10 次 - -### 问题 4:没有收到数据块 -**症状**: -- 录制启动成功 -- 但从未看到 "收到数据块" 日志 -- 最终 `数据块数: 0` - -**可能原因**: -1. Canvas 绘制失败(跨域问题) -2. MediaRecorder 不支持当前 MIME 类型 -3. 视频流被中断或为空 - -**检查方法**: -```javascript -// 在控制台手动检查 -const videos = document.querySelectorAll('video') -videos.forEach((v, i) => { - console.log(`Video ${i}:`, { - readyState: v.readyState, - videoWidth: v.videoWidth, - videoHeight: v.videoHeight, - srcObject: v.srcObject, - videoTracks: v.srcObject?.getVideoTracks().map(t => ({ - id: t.id, - label: t.label, - readyState: t.readyState, - enabled: t.enabled - })), - audioTracks: v.srcObject?.getAudioTracks().map(t => ({ - id: t.id, - label: t.label, - readyState: t.readyState, - enabled: t.enabled - })) - }) -}) -``` - -### 问题 5:通话时间过短 -**症状**: -``` -[call-local-recorder] 生成 Blob: null -或 -[call-local-recorder] 生成 Blob: 1234 bytes(很小) -``` - -**原因**: -- 通话时长 < 1 秒 -- 录制启动后立即挂断 - -**解决**: -- 确保通话时长 > 2 秒 -- 测试时保持通话至少 5 秒 - -### 问题 6:SDK 提前销毁轨道 -**症状**: -``` -[call-local-recorder] MediaRecorder 错误 -或 -数据块数为 0 -``` - -**原因**: -- SDK exitRoom 在 MediaRecorder.stop() 之前执行 -- 视频轨道被销毁 - -**解决**: -- 已在 USER_LEAVE 事件时立即停止录制 -- 已在多个挂断入口添加钩子 -- 缩短停止延迟到 100ms - -## 测试建议 - -### 最小测试场景 -1. 发起视频通话 -2. 等待接通(看到对方画面) -3. **保持通话至少 5 秒**(重要!) -4. 正常挂断 -5. 检查控制台日志和录制结果 - -### 关键检查点 -- [ ] 录制在接通后 1 秒内启动 -- [ ] 看到 "✅ 本地录制已成功启动" -- [ ] 看到持续的 "收到数据块" 日志 -- [ ] 数据块数 > 0(至少 4-5 块) -- [ ] 生成的 Blob 大小 > 10KB -- [ ] 上传成功并关联到诊单 - -### 压力测试场景 -1. 快速挂断(2-3 秒) -2. 对方挂断 -3. 网络中断 -4. 切换会话后挂断 -5. 关闭聊天窗口 - -## 预期改进效果 - -### 修复前 -``` -通话接通 -> 等待 1200ms -> 尝试启动录制 -> 对方挂断 -> SDK 销毁轨道 -> 录制失败 -``` - -### 修复后 -``` -通话接通 -> 等待 500ms -> 快速启动录制 -> 开始收集数据 -> -检测到挂断 -> 立即停止录制 -> 生成 Blob -> 上传成功 -``` - -## 浏览器兼容性 - -### 支持的浏览器 -- Chrome/Edge 85+ -- Firefox 78+ -- Safari 14.1+ - -### MIME 类型优先级 -1. `video/webm;codecs=vp9,opus` (最佳) -2. `video/webm;codecs=vp8,opus` -3. `video/webm;codecs=vp9` -4. `video/webm` (兜底) - -### 检查浏览器支持 -```javascript -const mimes = [ - 'video/webm;codecs=vp9,opus', - 'video/webm;codecs=vp8,opus', - 'video/webm;codecs=vp9', - 'video/webm' -] -mimes.forEach(m => { - console.log(m, MediaRecorder.isTypeSupported(m)) -}) -``` - -## 性能优化建议 - -1. **Canvas 分辨率**:当前 1280x720,可根据需要调整 -2. **数据块间隔**:当前 250ms,可调整为 500ms 减少开销 -3. **音频混音**:如果不需要音频,可以禁用 AudioContext - -## 下一步优化(如果问题仍存在) - -1. **进一步缩短启动延迟**:从 500ms 减少到 300ms -2. **增加录制状态监控**:定期检查 MediaRecorder 状态 -3. **添加降级方案**:如果本地录制失败,提示用户依赖云端录制 -4. **添加手动录制控制**:让用户手动开始/停止录制 -5. **使用 MutationObserver**:监听视频元素的添加,立即启动录制 - -## 联系支持 - -如果按照本指南操作后问题仍未解决,请提供: -1. 完整的控制台日志(从通话开始到结束) -2. 浏览器版本和操作系统 -3. 通话时长(秒) -4. 是否看到视频画面 -5. 是否有任何错误提示 -6. 数据块数量(如果有) diff --git a/LOGISTICS_INTEGRATION_GUIDE.md b/LOGISTICS_INTEGRATION_GUIDE.md deleted file mode 100644 index c31aa523..00000000 --- a/LOGISTICS_INTEGRATION_GUIDE.md +++ /dev/null @@ -1,266 +0,0 @@ -# 物流查询集成指南 - -## 当前实现 - -系统已集成快递100 API,支持顺丰速运和京东快递的物流轨迹查询。 - -### 查询方式优先级 - -1. **快递100 API**(推荐)- 实时查询物流轨迹 -2. **官网链接跳转**(备用)- 未配置API时使用 - ---- - -## 快递100 API 配置(推荐) - -### 1. 注册快递100账号 - -访问:https://www.kuaidi100.com/ -- 注册企业账号 -- 申请API接口权限 -- 获取 `customer` 和 `key` - -### 2. 配置环境变量 - -在 `server/.env` 文件中添加: - -```env -# 快递100配置 -LOGISTICS_KUAIDI100_ENABLE=true -LOGISTICS_KUAIDI100_CUSTOMER=你的customer -LOGISTICS_KUAIDI100_KEY=你的key -LOGISTICS_KUAIDI100_QUERY_URL=https://poll.kuaidi100.com/poll/query.do -``` - -### 3. 配置文件 - -在 `server/config/logistics.php` 中: - -```php - [ - 'enable' => env('LOGISTICS_KUAIDI100_ENABLE', false), - 'customer' => env('LOGISTICS_KUAIDI100_CUSTOMER', ''), - 'key' => env('LOGISTICS_KUAIDI100_KEY', ''), - 'query_url' => env('LOGISTICS_KUAIDI100_QUERY_URL', 'https://poll.kuaidi100.com/poll/query.do'), - ], -]; -``` - -### 4. 支持的快递公司 - -- **顺丰速运** (sf / shunfeng) -- **京东快递** (jd) -- 更多快递公司可通过快递100文档查询编码 - ---- - -## 官网查询链接(备用方案) - -当未配置快递100 API时,系统会提供官网查询链接: - -### 顺丰速运 -- 链接格式:`https://www.sf-express.com/cn/sc/dynamic_function/waybill/#search/bill-number/{运单号}` -- 特点:需要输入收件人手机号后4位验证 - -### 京东物流 -- 链接格式:`https://www.jdl.com/#/trackQuery?waybillCode={运单号}` -- 特点:直接查询,无需验证 - ---- - -## 使用说明 - -### 前端查询界面 - -位置:`业务订单详情` → `物流轨迹` - -功能: -1. 选择快递公司(自动识别/顺丰/京东) -2. 点击"查询物流"按钮 -3. 查看实时物流轨迹 -4. 备用:点击"顺丰官网查件"或"京东物流查件" - -### 自动识别规则 - -系统会根据运单号自动识别快递公司: -- 顺丰:以 `SF` 开头 -- 京东:以 `JD`、`JDV`、`JDK`、`JDEX` 开头 - -### 物流状态说明 - -- **0** - 在途 -- **1** - 揽收 -- **2** - 疑难 -- **3** - 已签收 -- **4** - 退签 -- **5** - 派件中 -- **6** - 退回 -- **7** - 转投 -- **10** - 待清关 -- **11** - 清关中 -- **12** - 已清关 -- **13** - 清关异常 -- **14** - 收件人拒签 - ---- - -## 顺丰查询注意事项 - -### 为什么查不到顺丰快递? - -1. **手机号验证** - - 顺丰查询需要收件人手机号后4位 - - 系统已自动传入订单中的收件人手机号 - - 如果查询失败,请检查订单中的手机号是否正确 - -2. **运单号格式** - - 确保运单号正确无误 - - 顺丰运单号通常以 `SF` 开头 - -3. **快递100配置** - - 确认已正确配置 `LOGISTICS_KUAIDI100_*` 环境变量 - - 确认快递100账户余额充足 - -4. **官网查询** - - 如果API查询失败,可使用官网链接 - - 官网查询需要手动输入手机号后4位 - ---- - -## 扩展其他快递公司 - -### 1. 修改 ExpressTrackService.php - -在 `resolveCarrier` 方法中添加新的快递公司识别: - -```php -if ($ec === 'ems') { - return ['carrier' => 'ems', 'kuaidi_com' => 'ems', 'label' => 'EMS']; -} -``` - -### 2. 添加官网链接 - -在 `officialUrls` 方法中添加: - -```php -return [ - 'sf' => '...', - 'jd' => '...', - 'ems' => 'https://www.ems.com.cn/queryList?mailNum=' . $enc, -]; -``` - -### 3. 更新前端选项 - -在 `order_list.vue` 中添加选项: - -```vue - -``` - ---- - -## 故障排查 - -### 问题1:提示"未配置快递100查询密钥" - -**解决方案:** -1. 检查 `.env` 文件中的配置 -2. 确认 `LOGISTICS_KUAIDI100_ENABLE=true` -3. 重启服务器 - -### 问题2:查询返回"查询失败" - -**可能原因:** -1. 运单号错误 -2. 快递尚未揽收 -3. 快递100账户余额不足 -4. 手机号后4位不匹配(顺丰) - -**解决方案:** -1. 核对运单号 -2. 等待快递揽收后再查询 -3. 充值快递100账户 -4. 使用官网链接手动查询 - -### 问题3:顺丰查询失败但京东正常 - -**原因:** -顺丰查询需要手机号后4位验证 - -**解决方案:** -1. 确保订单中填写了正确的收件人手机号 -2. 使用官网链接,手动输入手机号后4位 - ---- - -## API 接口说明 - -### 查询物流轨迹 - -**接口:** `GET /tcm.prescriptionOrder/logisticsTrace` - -**参数:** -- `id` (必填): 业务订单ID -- `express_company` (可选): 快递公司 (auto/sf/jd) - -**返回示例:** - -```json -{ - "carrier": "sf", - "carrier_label": "顺丰速运", - "kuaidi_com": "shunfeng", - "traces": [ - { - "time": "2024-01-15 10:30:00", - "context": "快件已签收" - }, - { - "time": "2024-01-15 08:00:00", - "context": "派件中" - } - ], - "state": "3", - "state_text": "已签收", - "source": "kuaidi100", - "hint": "", - "official_url": "https://www.sf-express.com/...", - "official_urls": { - "sf": "https://www.sf-express.com/...", - "jd": "https://www.jdl.com/..." - }, - "tracking_number": "SF1234567890", - "express_company_used": "sf" -} -``` - ---- - -## 费用说明 - -### 快递100 API - -- 按查询次数计费 -- 具体价格请咨询快递100官方 -- 建议:配置后台缓存减少重复查询 - -### 官网查询 - -- 完全免费 -- 需要用户手动跳转 -- 顺丰需要手机号验证 - ---- - -## 更新日志 - -### 2024-01-15 -- 更新顺丰官网查询链接(新版) -- 更新京东物流查询链接 -- 优化手机号后4位自动传入逻辑 -- 完善错误提示信息 diff --git a/LOGISTICS_UPDATE_README.md b/LOGISTICS_UPDATE_README.md deleted file mode 100644 index 641bec19..00000000 --- a/LOGISTICS_UPDATE_README.md +++ /dev/null @@ -1,308 +0,0 @@ -# 物流查询功能更新说明 - -## 更新内容 - -### 1. 更新官网查询链接 - -#### 顺丰速运 -- **旧链接**:`https://ucmp.sf-express.com/cx/#/waybill/waybill-search?waybillNumber={单号}` -- **新链接**:`https://www.sf-express.com/cn/sc/dynamic_function/waybill/#search/bill-number/{单号}` -- **说明**:更新为顺丰官网最新版查询页面 - -#### 京东物流 -- **旧链接**:`https://www.jdl.com/express/tracking/?waybillCodes={单号}` -- **新链接**:`https://www.jdl.com/#/trackQuery?waybillCode={单号}` -- **说明**:更新为京东物流最新版查询页面 - -### 2. 更新文件列表 - -- `server/app/common/service/ExpressTrackService.php` - 后端服务类 -- `admin/src/views/consumer/prescription/order_list.vue` - 前端订单列表页面 - -### 3. 新增文档 - -- `LOGISTICS_INTEGRATION_GUIDE.md` - 完整的物流集成指南 -- `server/.env.logistics.example` - 环境变量配置示例 -- `server/test_logistics.php` - 物流查询测试脚本 - ---- - -## 快速开始 - -### 方案一:使用快递100 API(推荐) - -#### 1. 注册快递100账号 -访问:https://www.kuaidi100.com/ -- 注册企业账号 -- 申请API接口权限 -- 获取 Customer 和 Key - -#### 2. 配置环境变量 -在 `server/.env` 文件中添加: - -```env -LOGISTICS_KUAIDI100_CUSTOMER=你的customer -LOGISTICS_KUAIDI100_KEY=你的key -``` - -#### 3. 重启服务器 -```bash -# 重启PHP服务 -systemctl restart php-fpm - -# 或重启整个应用 -``` - -#### 4. 测试配置 -```bash -cd server -php test_logistics.php -``` - -### 方案二:使用官网链接(免费) - -无需配置,系统会自动提供官网查询链接: -- 顺丰速运:需要输入收件人手机号后4位 -- 京东物流:直接查询 - ---- - -## 使用说明 - -### 前端操作 - -1. 进入"业务订单列表" -2. 点击订单查看详情 -3. 在"物流轨迹"区域: - - 选择快递公司(自动识别/顺丰/京东) - - 点击"查询物流"按钮 - - 查看实时物流轨迹 -4. 备用方案: - - 点击"顺丰官网查件"或"京东物流查件" - - 跳转到官网手动查询 - -### 自动识别规则 - -系统会根据运单号自动识别快递公司: -- **顺丰**:以 `SF` 开头的运单号 -- **京东**:以 `JD`、`JDV`、`JDK`、`JDEX` 开头的运单号 - ---- - -## 顺丰查询问题解决 - -### 问题:查不到顺丰快递 - -#### 原因分析 -1. **手机号验证失败** - - 顺丰查询需要收件人手机号后4位 - - 系统会自动从订单中获取手机号 - - 如果手机号不正确,查询会失败 - -2. **运单号错误** - - 确保运单号完整且正确 - - 顺丰运单号通常以 `SF` 开头 - -3. **快递未揽收** - - 刚下单的快递可能还未揽收 - - 等待快递员揽收后再查询 - -4. **快递100配置问题** - - Customer 或 Key 配置错误 - - 账户余额不足 - -#### 解决方案 - -**方案1:检查订单手机号** -``` -1. 打开订单详情 -2. 确认"收件人手机号"字段填写正确 -3. 重新查询物流 -``` - -**方案2:使用官网查询** -``` -1. 点击"顺丰官网查件"链接 -2. 在官网页面手动输入手机号后4位 -3. 查看物流信息 -``` - -**方案3:检查快递100配置** -```bash -# 1. 查看配置 -cat server/.env | grep LOGISTICS_KUAIDI100 - -# 2. 测试配置 -cd server -php test_logistics.php - -# 3. 检查日志 -tail -f runtime/log/error.log -``` - ---- - -## 京东查询问题解决 - -### 问题:查不到京东快递 - -#### 原因分析 -1. 运单号错误 -2. 快递未揽收 -3. 快递100配置问题 - -#### 解决方案 - -**方案1:核对运单号** -- 确保运单号完整且正确 -- 京东运单号通常以 `JD` 开头 - -**方案2:使用官网查询** -- 点击"京东物流查件"链接 -- 京东查询无需手机号验证 - -**方案3:等待揽收** -- 刚下单的快递可能还未揽收 -- 等待1-2小时后再查询 - ---- - -## API 接口说明 - -### 查询物流轨迹 - -**接口地址:** `GET /tcm.prescriptionOrder/logisticsTrace` - -**请求参数:** -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| id | number | 是 | 业务订单ID | -| express_company | string | 否 | 快递公司 (auto/sf/jd) | - -**返回示例:** -```json -{ - "carrier": "sf", - "carrier_label": "顺丰速运", - "kuaidi_com": "shunfeng", - "traces": [ - { - "time": "2024-01-15 10:30:00", - "context": "快件已签收" - }, - { - "time": "2024-01-15 08:00:00", - "context": "派件中" - } - ], - "state": "3", - "state_text": "已签收", - "source": "kuaidi100", - "hint": "", - "official_url": "https://www.sf-express.com/...", - "official_urls": { - "sf": "https://www.sf-express.com/...", - "jd": "https://www.jdl.com/..." - }, - "tracking_number": "SF1234567890", - "express_company_used": "sf" -} -``` - -**物流状态码:** -| 状态码 | 说明 | -|--------|------| -| 0 | 在途 | -| 1 | 揽收 | -| 2 | 疑难 | -| 3 | 已签收 | -| 4 | 退签 | -| 5 | 派件中 | -| 6 | 退回 | -| 7 | 转投 | -| 10 | 待清关 | -| 11 | 清关中 | -| 12 | 已清关 | -| 13 | 清关异常 | -| 14 | 收件人拒签 | - ---- - -## 测试步骤 - -### 1. 测试配置 -```bash -cd server -php test_logistics.php -``` - -### 2. 测试官网链接 -1. 打开浏览器 -2. 访问生成的官网链接 -3. 确认能正常打开查询页面 - -### 3. 测试实时查询(需要配置快递100) -1. 在订单中填写真实的快递单号 -2. 点击"查询物流"按钮 -3. 查看是否返回物流轨迹 - -### 4. 测试手机号验证(顺丰) -1. 创建订单时填写正确的收件人手机号 -2. 查询顺丰快递 -3. 确认能正常返回结果 - ---- - -## 常见问题 - -### Q1: 为什么快递100查询失败? -**A:** 可能原因: -1. 未配置 Customer 和 Key -2. 账户余额不足 -3. 运单号错误或快递未揽收 -4. 网络连接问题 - -### Q2: 顺丰查询为什么需要手机号? -**A:** 顺丰为了保护用户隐私,查询时需要验证收件人手机号后4位。系统会自动从订单中获取手机号传给快递100。 - -### Q3: 可以添加其他快递公司吗? -**A:** 可以。参考 `LOGISTICS_INTEGRATION_GUIDE.md` 中的"扩展其他快递公司"章节。 - -### Q4: 快递100收费吗? -**A:** 是的,快递100按查询次数收费。具体价格请咨询快递100官方。 - -### Q5: 不配置快递100可以用吗? -**A:** 可以。系统会提供官网查询链接,用户可以跳转到官网手动查询。 - ---- - -## 技术支持 - -### 相关文档 -- `LOGISTICS_INTEGRATION_GUIDE.md` - 完整集成指南 -- `server/.env.logistics.example` - 配置示例 -- 快递100官方文档:https://api.kuaidi100.com/ - -### 日志查看 -```bash -# 查看错误日志 -tail -f server/runtime/log/error.log - -# 查看应用日志 -tail -f server/runtime/log/app.log -``` - -### 联系方式 -如有问题,请查看项目文档或联系技术支持。 - ---- - -## 更新历史 - -### 2024-01-15 -- 更新顺丰官网查询链接(新版) -- 更新京东物流查询链接 -- 新增完整的集成指南文档 -- 新增配置示例文件 -- 新增测试脚本 -- 优化错误提示信息 diff --git a/MANUAL_INSTALL_SQL.md b/MANUAL_INSTALL_SQL.md deleted file mode 100644 index 1eb97d2a..00000000 --- a/MANUAL_INSTALL_SQL.md +++ /dev/null @@ -1,101 +0,0 @@ -# 处方管理功能 - 手动安装SQL - -如果自动安装脚本无法运行,可以手动在数据库中执行以下SQL语句。 - -## 方法1:使用安装脚本(推荐) - -### Windows系统: -```bash -install_prescription_shared.bat -``` - -### Linux/Mac系统: -```bash -chmod +x install_prescription_shared.sh -./install_prescription_shared.sh -``` - -## 方法2:手动执行SQL - -打开你的数据库管理工具(如 phpMyAdmin、Navicat、MySQL Workbench等),选择数据库 `zyt`,然后执行以下SQL语句: - -```sql --- 处方增加共享字段 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `is_shared` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否共享:0-不共享(仅自己可见) 1-共享(所有人可见)' AFTER `template_id`; - --- 处方名称 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `prescription_name` varchar(100) NOT NULL DEFAULT '' COMMENT '处方名称' AFTER `sn`; - --- 处方类型 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `prescription_type` varchar(50) NOT NULL DEFAULT '浓缩水丸' COMMENT '处方类型:浓缩水丸、饮片、颗粒、丸剂、散剂等' AFTER `prescription_name`; - --- 舌象图片 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `tongue_image` varchar(255) NOT NULL DEFAULT '' COMMENT '舌象图片' AFTER `tongue`; - --- 脉象详情 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `pulse_condition` varchar(100) NOT NULL DEFAULT '' COMMENT '脉象详情' AFTER `pulse`; - --- 服用天数 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_days` int(11) NOT NULL DEFAULT 7 COMMENT '服用天数' AFTER `dose_unit`; - --- 服用时间 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_time` varchar(50) NOT NULL DEFAULT '饭前' COMMENT '服用时间:饭前、饭后、饭中、空腹、睡前、晨起、随时' AFTER `usage_days`; - --- 服用方式 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_way` varchar(50) NOT NULL DEFAULT '温水送服' COMMENT '服用方式:温水送服、开水冲服、黄酒送服等' AFTER `usage_time`; - --- 忌口 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `dietary_taboo` varchar(200) NOT NULL DEFAULT '' COMMENT '忌口(逗号分隔):辛辣食物、生冷食物、油腻食物等' AFTER `usage_way`; - --- 其他服用说明 -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_notes` varchar(200) NOT NULL DEFAULT '' COMMENT '其他服用说明' AFTER `dietary_taboo`; -``` - -## 方法3:使用命令行 - -```bash -# 进入项目根目录 -cd /path/to/your/project - -# 执行SQL文件 -mysql -h127.0.0.1 -P3306 -uroot -proot zyt < server/database/migrations/add_shared_to_prescription.sql -``` - -## 验证安装 - -执行以下SQL查询,检查字段是否已添加: - -```sql -SHOW COLUMNS FROM `zyt_tcm_prescription`; -``` - -应该能看到以下新增字段: -- prescription_name -- prescription_type -- is_shared -- usage_days -- usage_time -- usage_way -- dietary_taboo -- usage_notes -- tongue_image -- pulse_condition - -## 注意事项 - -1. 如果某个字段已存在,会报错但不影响其他字段的添加 -2. 请确保数据库名称为 `zyt`,如果不是请修改SQL中的表名前缀 -3. 执行前建议备份数据库 - -## 常见问题 - -### 问题1:字段已存在 -如果提示"Duplicate column name",说明该字段已经存在,可以忽略该错误。 - -### 问题2:表不存在 -如果提示"Table doesn't exist",请检查: -1. 数据库名称是否正确 -2. 表名前缀是否正确(默认是 zyt_) - -### 问题3:权限不足 -如果提示权限错误,请确保数据库用户有 ALTER TABLE 权限。 diff --git a/MEDICINE_LIBRARY_GUIDE.md b/MEDICINE_LIBRARY_GUIDE.md deleted file mode 100644 index 17931860..00000000 --- a/MEDICINE_LIBRARY_GUIDE.md +++ /dev/null @@ -1,239 +0,0 @@ -# 药品库系统安装指南 - -## 功能概述 - -药品库管理系统提供了完整的中药材管理功能,包括: - -- 药材名称管理 -- 供应商信息管理 -- 单位规格管理 -- 价格管理(结算价、零售价) -- 库存管理 -- 药品图片上传 -- 状态管理(启用/停用) -- 备注信息 - -## 安装步骤 - -### 1. 数据库安装 - -#### Windows 系统 - -双击运行 `install_medicine_library.bat` 文件,按提示输入数据库信息: - -```bash -install_medicine_library.bat -``` - -#### Linux/Mac 系统 - -执行以下命令: - -```bash -chmod +x install_medicine_library.sh -./install_medicine_library.sh -``` - -或者手动导入 SQL 文件: - -```bash -mysql -h localhost -u root -p your_database < install_medicine_library.sql -``` - -### 2. 后端文件说明 - -安装脚本会创建以下文件: - -``` -server/ -├── app/ -│ ├── adminapi/ -│ │ ├── controller/doctor/ -│ │ │ └── MedicineController.php # 药品控制器 -│ │ ├── logic/doctor/ -│ │ │ └── MedicineLogic.php # 药品业务逻辑 -│ │ ├── validate/doctor/ -│ │ │ └── MedicineValidate.php # 药品验证器 -│ │ └── lists/doctor/ -│ │ └── MedicineLists.php # 药品列表 -│ └── common/ -│ └── model/doctor/ -│ └── Medicine.php # 药品模型 -``` - -### 3. 前端文件说明 - -``` -admin/ -├── src/ -│ ├── api/ -│ │ └── medicine.ts # 药品API接口 -│ └── views/doctor/ -│ └── medicine.vue # 药品管理页面 -``` - -### 4. 数据库表结构 - -创建的数据表:`la_doctor_medicine` - -| 字段名 | 类型 | 说明 | -|--------|------|------| -| id | int | 主键ID | -| name | varchar(100) | 药材名称 | -| supplier | varchar(100) | 供应商名称 | -| unit | varchar(20) | 单位 | -| settlement_price | decimal(10,2) | 结算价 | -| retail_price | decimal(10,2) | 零售价 | -| stock | int | 库存数量 | -| image | varchar(255) | 药品图片 | -| status | tinyint(1) | 状态:0=停用,1=启用 | -| remark | varchar(500) | 备注 | -| create_time | int | 创建时间 | -| update_time | int | 更新时间 | -| delete_time | int | 删除时间 | - -## 功能使用 - -### 1. 访问药品库 - -登录后台管理系统后,访问:`/doctor/medicine` - -### 2. 添加药品 - -1. 点击右上角"添加药品"按钮 -2. 填写药品信息: - - 药材名称(必填) - - 供应商名称(必填) - - 单位(必填,可选择:克、千克、斤、盒、瓶、袋、包、片、粒、支) - - 结算价(必填) - - 零售价(必填) - - 库存数量 - - 药品图片(支持jpg、png格式,大小不超过2MB) - - 状态(启用/停用) - - 备注信息 -3. 点击"确定"保存 - -### 3. 编辑药品 - -1. 在列表中找到需要编辑的药品 -2. 点击"编辑"按钮 -3. 修改药品信息 -4. 点击"确定"保存 - -### 4. 删除药品 - -1. 在列表中找到需要删除的药品 -2. 点击"删除"按钮 -3. 确认删除操作 - -### 5. 查询药品 - -支持按以下条件查询: -- 药材名称(模糊搜索) -- 供应商名称(模糊搜索) - -### 6. 库存管理 - -- 库存为0时显示红色标签(危险) -- 库存小于10时显示黄色标签(警告) -- 库存充足时显示绿色标签(正常) - -## API 接口说明 - -### 1. 获取药品列表 - -``` -GET /api/doctor.medicine/lists -``` - -参数: -- page_no: 页码 -- page_size: 每页数量 -- name: 药材名称(可选) -- supplier: 供应商名称(可选) - -### 2. 添加药品 - -``` -POST /api/doctor.medicine/add -``` - -参数: -- name: 药材名称 -- supplier: 供应商名称 -- unit: 单位 -- settlement_price: 结算价 -- retail_price: 零售价 -- stock: 库存 -- image: 图片URL -- status: 状态 -- remark: 备注 - -### 3. 编辑药品 - -``` -POST /api/doctor.medicine/edit -``` - -参数:同添加接口,需额外传入 id - -### 4. 删除药品 - -``` -POST /api/doctor.medicine/delete -``` - -参数: -- id: 药品ID - -### 5. 药品详情 - -``` -GET /api/doctor.medicine/detail -``` - -参数: -- id: 药品ID - -## 示例数据 - -安装脚本会自动插入5条示例数据: - -1. 当归 - 同仁堂 -2. 黄芪 - 云南白药 -3. 枸杞子 - 宁夏枸杞 -4. 人参 - 长白山人参 -5. 甘草 - 内蒙古甘草 - -## 注意事项 - -1. 确保数据库连接信息正确 -2. 确保有足够的数据库权限 -3. 图片上传需要配置正确的上传接口 -4. 建议定期备份药品数据 -5. 价格字段保留两位小数 -6. 库存不能为负数 - -## 常见问题 - -### 1. 安装失败 - -- 检查数据库连接信息是否正确 -- 检查数据库用户是否有创建表的权限 -- 检查数据库是否已存在同名表 - -### 2. 图片上传失败 - -- 检查上传接口配置 -- 检查文件大小是否超过限制 -- 检查文件格式是否支持 - -### 3. 数据保存失败 - -- 检查必填字段是否填写完整 -- 检查价格格式是否正确 -- 检查网络连接是否正常 - -## 技术支持 - -如有问题,请联系技术支持团队。 diff --git a/ORDER_DETAIL_ADDRESS_DISPLAY_FIX.md b/ORDER_DETAIL_ADDRESS_DISPLAY_FIX.md deleted file mode 100644 index b62c76d8..00000000 --- a/ORDER_DETAIL_ADDRESS_DISPLAY_FIX.md +++ /dev/null @@ -1,167 +0,0 @@ -# Order Detail Address Display Fix (订单详情地址显示修复) - -## Issue -The order detail view was only showing the detailed address (`shipping_address`) without displaying the province, city, and district information. - -## Changes Made - -### 1. Added Computed Property for Full Address -**File**: `admin/src/views/consumer/prescription/order_list.vue` - -Added `detailFullAddress` computed property that combines all address components: - -```typescript -// 完整收货地址(省市区 + 详细地址) -const detailFullAddress = computed(() => { - const d = detailData.value - if (!d) return '—' - - const parts = [] - if (d.shipping_province) parts.push(d.shipping_province) - if (d.shipping_city) parts.push(d.shipping_city) - if (d.shipping_district) parts.push(d.shipping_district) - if (d.shipping_address) parts.push(d.shipping_address) - - return parts.length > 0 ? parts.join(' ') : '—' -}) -``` - -### 2. Updated Display Template -Changed the shipping address display from: -```vue - - {{ detailData.shipping_address }} - -``` - -To: -```vue - - {{ detailFullAddress }} - -``` - -## Display Format - -### Before: -``` -收货地址: 顶顶顶 -``` - -### After: -``` -收货地址: 北京市 北京市 丰台区 顶顶顶 -``` - -## Logic - -The `detailFullAddress` computed property: -1. Checks if `detailData` exists -2. Collects non-empty address components in order: - - `shipping_province` (省) - - `shipping_city` (市) - - `shipping_district` (区/县) - - `shipping_address` (详细地址) -3. Joins them with spaces -4. Returns '—' if no address components exist - -## Examples - -### Complete Address: -``` -Input: - shipping_province: "四川省" - shipping_city: "成都市" - shipping_district: "武侯区" - shipping_address: "天府大道中段666号" - -Output: - 四川省 成都市 武侯区 天府大道中段666号 -``` - -### Partial Address (Missing Province): -``` -Input: - shipping_province: null - shipping_city: "成都市" - shipping_district: "武侯区" - shipping_address: "天府大道中段666号" - -Output: - 成都市 武侯区 天府大道中段666号 -``` - -### Only Detailed Address: -``` -Input: - shipping_province: null - shipping_city: null - shipping_district: null - shipping_address: "天府大道中段666号" - -Output: - 天府大道中段666号 -``` - -### No Address: -``` -Input: - shipping_province: null - shipping_city: null - shipping_district: null - shipping_address: null - -Output: - — -``` - -## Testing Steps - -### 1. Test Complete Address Display -1. Open an order that has province/city/district data -2. Check the "收货地址" field in the detail view -3. Verify it shows: `省 市 区 详细地址` - -### 2. Test Legacy Orders (No Region Data) -1. Open an old order that only has `shipping_address` -2. Verify it still displays the detailed address correctly -3. Should show just the detailed address without extra spaces - -### 3. Test Empty Address -1. Create an order without any address information -2. Verify it displays "—" instead of empty string - -### 4. Test Different Address Combinations -Test various combinations: -- Full address (province + city + district + detail) -- City + district + detail (no province) -- District + detail (no province/city) -- Detail only (no region data) -- Empty (no data at all) - -## Related Files - -### Frontend: -- `admin/src/views/consumer/prescription/order_list.vue` - - Added `detailFullAddress` computed property - - Updated shipping address display template - -### Backend: -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - - Already returns `shipping_province`, `shipping_city`, `shipping_district` in detail response - -### Database: -- `tcm_prescription_order` table - - Columns: `shipping_province`, `shipping_city`, `shipping_district`, `shipping_address` - -## Benefits - -✅ **Complete Information**: Users can now see the full address including province/city/district -✅ **Better Readability**: Address components are clearly separated with spaces -✅ **Backward Compatible**: Works with legacy orders that only have detailed address -✅ **Graceful Handling**: Shows "—" when no address data exists -✅ **Flexible**: Handles partial address data (missing province, city, or district) - -## Summary - -The order detail view now displays the complete shipping address including province, city, district, and detailed address, providing users with full address information at a glance! diff --git a/ORDER_LIST_REGION_UPDATE.md b/ORDER_LIST_REGION_UPDATE.md deleted file mode 100644 index c82f3536..00000000 --- a/ORDER_LIST_REGION_UPDATE.md +++ /dev/null @@ -1,191 +0,0 @@ -# Order List Region Selector Update (订单列表省市区选择器更新) - -## Changes Made - -Updated the order list view (`order_list.vue`) to use the same API-based region data loading as the prescription index view. - -### 1. Replaced Static Region Data with API Loading -**File**: `admin/src/views/consumer/prescription/order_list.vue` - -#### Before: -- Static hardcoded region data with limited provinces/cities -- Only included: 四川省, 北京市, 上海市, 广东省 - -#### After: -- Dynamic API-based region data loading -- Fetches complete China region data from `/api-proxy/area/ChinaCitys.json` -- Includes all provinces, cities, and districts -- Falls back to basic data if API fails - -### 2. Added Data Transformation Function -Added `transformRegionData()` function to convert API format to el-cascader format: -- API format: `{province, citys: [{city, areas: [{area}]}]}` -- Cascader format: `{value, label, children: [{value, label, children: [{value, label}]}]}` - -### 3. Updated Cascader Component -Added missing props to the cascader: -- `emitPath: true` - Returns full path array instead of just last value -- `filterable` - Enables search functionality - -### 4. Updated onMounted Hook -Changed from: -```typescript -onMounted(() => { - getLists() -}) -``` - -To: -```typescript -onMounted(async () => { - await loadRegionData() - getLists() -}) -``` - -### 5. Added Region Fields to Quick Edit -Updated the quick tracking number edit function to include region fields: -```typescript -shipping_province: d.shipping_province || '', -shipping_city: d.shipping_city || '', -shipping_district: d.shipping_district || '', -``` - -This ensures region data is preserved when only updating tracking numbers. - -## Data Flow - -### Loading Region Data: -1. Component mounts -2. `loadRegionData()` fetches from `/api-proxy/area/ChinaCitys.json` -3. `transformRegionData()` converts API format to cascader format -4. `regionOptions` is populated with all China regions -5. If API fails, falls back to basic region data - -### Editing Order with Region: -1. User opens edit dialog -2. Region data is loaded from order: `[province, city, district]` -3. Cascader displays the selected region -4. User can change region selection -5. On save, region array is split into three fields: - - `shipping_province` - - `shipping_city` - - `shipping_district` -6. Backend saves the three separate fields - -### Quick Edit (Tracking Number): -1. User clicks quick edit for tracking number -2. System fetches full order details -3. Includes region fields in the update payload -4. Region data is preserved while updating tracking number - -## Code Structure - -### Region Data Loading: -```typescript -const regionOptions = ref([]) - -const transformRegionData = (data: any[]) => { - // Converts API format to cascader format -} - -const loadRegionData = async () => { - // Fetches from API and transforms data - // Falls back to basic data on error -} -``` - -### Region Field Handling in Edit: -```typescript -// Loading data into form: -const province = d.shipping_province || '' -const city = d.shipping_city || '' -const district = d.shipping_district || '' -if (province || city || district) { - editForm.region = [province, city, district].filter(v => v !== '') -} - -// Saving data from form: -if (editForm.region && editForm.region.length > 0) { - payload.shipping_province = editForm.region[0] || '' - payload.shipping_city = editForm.region[1] || '' - payload.shipping_district = editForm.region[2] || '' -} -``` - -## Testing Steps - -### 1. Test Region Data Loading -1. Open order list page -2. Check browser console for: - ``` - 开始加载省市区数据... - 响应状态: 200 - 加载的数据条数: XX - regionOptions 已设置,条数: XX - ``` -3. If you see errors, check that dev server was restarted - -### 2. Test Edit Order with Region -1. Click "编辑" on any order -2. Check that the region cascader shows the current region (if set) -3. Change the region selection -4. Save the order -5. Verify database is updated: - ```sql - SELECT shipping_province, shipping_city, shipping_district - FROM tcm_prescription_order - WHERE id = XX; - ``` - -### 3. Test Region Search -1. Open edit dialog -2. Click on region cascader -3. Type a province/city name in the search box -4. Verify search results appear -5. Select a region from search results - -### 4. Test Quick Edit Preserves Region -1. Find an order with region data set -2. Click "快递单号" quick edit button -3. Update tracking number -4. Save -5. Verify region data is still present in database - -### 5. Test Fallback Data -1. Stop the dev server -2. Open order list page -3. Verify fallback region data is used (四川省, 北京市) -4. Restart dev server -5. Refresh page -6. Verify full region data is loaded - -## Related Files - -### Frontend: -- `admin/src/views/consumer/prescription/order_list.vue` - - Added `loadRegionData()` and `transformRegionData()` functions - - Updated `onMounted()` to load region data - - Updated cascader component with `emitPath: true` and `filterable` - - Added region fields to quick edit function - - Region field handling already existed in main edit function - -### Backend: -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - - Already handles `shipping_province`, `shipping_city`, `shipping_district` in both `create()` and `edit()` methods - -### Configuration: -- `admin/vite.config.ts` - - Proxy configuration for `/api-proxy` already exists - -## Summary - -✅ Replaced static region data with API-based loading -✅ Added data transformation function -✅ Updated cascader with `emitPath: true` and `filterable` -✅ Updated `onMounted()` to load region data -✅ Added region fields to quick edit function -✅ Fallback data available if API fails -✅ Search functionality enabled - -The order list now uses the same comprehensive region data as the prescription index, providing access to all provinces, cities, and districts in China! diff --git a/ORDER_STATUS_TABS_UI.md b/ORDER_STATUS_TABS_UI.md deleted file mode 100644 index 8bb22581..00000000 --- a/ORDER_STATUS_TABS_UI.md +++ /dev/null @@ -1,299 +0,0 @@ -# Order Status Tabs UI (订单状态标签页UI) - -## Feature -Redesigned the fulfillment status filter from a dropdown select to clickable tabs for better user experience and faster filtering. - -## Changes Made - -### File: `admin/src/views/consumer/prescription/order_list.vue` - -#### 1. Replaced Dropdown with Tab UI -**Before:** -```vue - - - - - - - - - -``` - -**After:** -```vue - - -
-
-
- {{ tab.label }} - - {{ tab.count }} - -
-
-
-
-``` - -#### 2. Added Status Tabs Configuration -```typescript -// 状态标签页配置 -const statusTabs = ref([ - { label: '全部', value: '' as number | '', count: undefined }, - { label: '待双审通过', value: 1, count: undefined }, - { label: '履约中', value: 2, count: undefined }, - { label: '已发货', value: 5, count: undefined }, - { label: '已完成', value: 3, count: undefined }, - { label: '已取消', value: 4, count: undefined } -]) -``` - -#### 3. Added Click Handler -```typescript -// 处理状态标签点击 -const handleStatusTabClick = (value: number | '') => { - queryParams.fulfillment_status = value - resetPage() -} -``` - -## UI Design - -### Visual States - -**Active Tab:** -- Primary color background (`bg-primary`) -- White text -- Shadow effect -- Badge with semi-transparent white background - -**Inactive Tab:** -- Light gray background (`bg-gray-50`) -- Gray text -- Hover effect (lighter gray) -- Badge with gray background - -### Layout -- Horizontal flex layout with gap -- Wraps on smaller screens -- Smooth transitions (200ms) -- Rounded corners -- Consistent padding - -### Badge (Count Display) -- Small text size -- Rounded badge -- Shows count for each status (optional feature) -- Different styling for active/inactive states - -## User Experience Improvements - -### Before (Dropdown): -1. User clicks dropdown -2. Dropdown opens with options -3. User scrolls to find status -4. User clicks option -5. Dropdown closes -6. User clicks "查询" button - -**Total: 3-4 clicks + scrolling** - -### After (Tabs): -1. User clicks tab -2. Filter applies immediately - -**Total: 1 click** - -### Benefits: -- ✅ **Faster filtering**: One-click access to any status -- ✅ **Visual feedback**: Clear indication of current filter -- ✅ **Better discoverability**: All options visible at once -- ✅ **No extra clicks**: Auto-triggers search on click -- ✅ **Modern UI**: Follows common tab pattern -- ✅ **Mobile friendly**: Wraps on small screens - -## Features - -### 1. Active State Indication -The currently selected tab is highlighted with primary color, making it immediately clear which filter is active. - -### 2. Hover Effects -Inactive tabs show a subtle hover effect, indicating they are clickable. - -### 3. Count Badges (Optional) -Each tab can display a count badge showing the number of orders in that status. Currently set to `undefined` but can be populated with actual counts. - -### 4. Responsive Design -Tabs wrap to multiple lines on smaller screens using `flex-wrap`. - -### 5. Smooth Transitions -All state changes have smooth 200ms transitions for a polished feel. - -## Implementation Details - -### Tab Configuration -```typescript -{ - label: string, // Display text - value: number | '', // Filter value ('' for "All") - count: number | undefined // Optional count badge -} -``` - -### Click Handler Logic -```typescript -const handleStatusTabClick = (value: number | '') => { - queryParams.fulfillment_status = value // Update filter - resetPage() // Trigger search -} -``` - -### Styling Classes -- `bg-primary`: Primary brand color (active state) -- `bg-gray-50`: Light gray (inactive state) -- `hover:bg-gray-100`: Hover effect -- `shadow-md`: Shadow for active tab -- `transition-all duration-200`: Smooth transitions -- `select-none`: Prevent text selection on click - -## Future Enhancements - -### 1. Add Count Badges -Fetch and display order counts for each status: - -```typescript -// After fetching data -const updateStatusCounts = (data: any) => { - statusTabs.value[0].count = data.total - statusTabs.value[1].count = data.pending - statusTabs.value[2].count = data.processing - // ... etc -} -``` - -### 2. Add Icons -Add status-specific icons for better visual recognition: - -```vue - - - -``` - -### 3. Add Keyboard Navigation -Support arrow keys for tab navigation: - -```typescript -const handleKeydown = (e: KeyboardEvent) => { - if (e.key === 'ArrowLeft') { - // Navigate to previous tab - } else if (e.key === 'ArrowRight') { - // Navigate to next tab - } -} -``` - -### 4. Add Loading State -Show loading indicator on active tab while fetching: - -```vue - - - -``` - -## Testing Steps - -### 1. Test Tab Switching -1. Open order list page -2. Click on different status tabs -3. Verify: - - Active tab is highlighted - - Table updates with filtered results - - URL parameters update (if using router) - -### 2. Test Visual States -1. Hover over inactive tabs -2. Verify hover effect appears -3. Click a tab -4. Verify active state styling applies - -### 3. Test "全部" Tab -1. Click "全部" tab -2. Verify all orders are shown -3. Verify no status filter is applied - -### 4. Test Responsive Behavior -1. Resize browser window to small width -2. Verify tabs wrap to multiple lines -3. Verify all tabs remain accessible - -### 5. Test with Search -1. Enter order number in search -2. Click different status tabs -3. Verify both filters work together - -## Accessibility - -### Current Implementation: -- ✅ Keyboard accessible (clickable divs) -- ✅ Visual feedback on hover -- ✅ Clear active state indication -- ✅ Sufficient color contrast - -### Recommended Improvements: -- Add `role="tab"` and `aria-selected` attributes -- Add `tabindex="0"` for keyboard navigation -- Add keyboard event handlers (Enter/Space) -- Add `aria-label` for screen readers - -### Example: -```vue -
-``` - -## Related Files - -### Frontend: -- `admin/src/views/consumer/prescription/order_list.vue` - - Added status tabs UI - - Added `statusTabs` configuration - - Added `handleStatusTabClick` handler - - Removed dropdown select - -## Summary - -✅ Replaced dropdown with clickable tabs -✅ One-click filtering (no need to click "查询") -✅ Visual active state indication -✅ Hover effects for better UX -✅ Responsive design with flex-wrap -✅ Smooth transitions -✅ Support for count badges (optional) -✅ Cleaner, more modern UI - -The status filter is now much more intuitive and efficient, following modern UI patterns commonly seen in admin dashboards! diff --git a/ORDER_SUPERVISOR_ROLE_CONFIG_FIX.md b/ORDER_SUPERVISOR_ROLE_CONFIG_FIX.md deleted file mode 100644 index 319756ca..00000000 --- a/ORDER_SUPERVISOR_ROLE_CONFIG_FIX.md +++ /dev/null @@ -1,308 +0,0 @@ -# 订单管理员角色配置修复 - -## 问题描述 - -编辑业务订单时,关联支付单失败,提示"无权限关联所选支付单"。 - -**错误信息**: -```json -{ - "code": 0, - "show": 1, - "msg": "无权限关联所选支付单", - "data": [] -} -``` - -**接口**: `/adminapi/tcm.prescriptionOrder/edit` - -## 问题原因 - -`OrderLogic::isOrderListSupervisor()` 方法中硬编码了管理员角色 `[0, 3]`,没有读取配置文件中的 `order_edit_all_roles` 配置。 - -### 配置文件 - -**文件**: `server/config/project.php` - -```php -'order_edit_all_roles' => [0, 3, 6], // 0=超级管理员, 3=管理员, 6=药师 -``` - -### 代码问题 - -**文件**: `server/app/adminapi/logic/order/OrderLogic.php` - -**修改前**: -```php -public static function isOrderListSupervisor(int $adminId, array $adminInfo): bool -{ - if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { - return true; - } - $supervisorRoles = [0, 3]; // 硬编码,没有读取配置 - $myRoles = array_map('intval', $adminInfo['role_id'] ?? []); - - return count(array_intersect($myRoles, $supervisorRoles)) > 0; -} -``` - -**问题**: -- 硬编码了 `[0, 3]`,即使配置文件中设置了 `[0, 3, 6]`,角色 6(药师)也无法获得管理员权限 -- 导致药师无法关联其他人创建的支付单 - -## 解决方案 - -修改 `isOrderListSupervisor()` 方法,从配置文件读取管理员角色列表。 - -**修改后**: -```php -public static function isOrderListSupervisor(int $adminId, array $adminInfo): bool -{ - if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { - return true; - } - $supervisorRoles = config('project.order_edit_all_roles', [0, 3]); // 从配置读取 - $myRoles = array_map('intval', $adminInfo['role_id'] ?? []); - - return count(array_intersect($myRoles, $supervisorRoles)) > 0; -} -``` - -**改进点**: -- 从配置文件读取 `order_edit_all_roles` -- 支持动态配置管理员角色 -- 默认值为 `[0, 3]`,保持向后兼容 - -## 权限规则 - -### 支付单关联权限 - -在关联支付单到业务订单时,需要满足以下条件之一: - -1. **超级管理员** (`root = 1`) - - 可以关联所有支付单 - -2. **管理员角色** (`order_edit_all_roles` 配置的角色) - - 默认:`[0, 3]`(超级管理员、管理员) - - 可配置:`[0, 3, 6]`(超级管理员、管理员、药师) - - 可以关联所有支付单 - -3. **诊单医助** (`assistant_id` 匹配) - - 可以关联该诊单的所有支付单 - -4. **支付单创建人** (`creator_id` 匹配) - - 只能关联自己创建的支付单 - -### 权限检查逻辑 - -```php -public static function assertPayOrdersLinkable(array $ids, int $diagnosisId, int $adminId, array $adminInfo): ?string -{ - // ... 其他检查 ... - - $supervisor = self::isOrderListSupervisor($adminId, $adminInfo); - $assistantId = (int) Diagnosis::where('id', $diagnosisId)->value('assistant_id'); - $isDiagAssistant = $assistantId === $adminId; - - foreach ($orders as $o) { - // 检查权限 - if (! $supervisor && ! $isDiagAssistant && (int) $o->creator_id !== $adminId) { - return '无权限关联所选支付单'; - } - } - - return null; -} -``` - -## 配置说明 - -### 配置文件位置 - -`server/config/project.php` - -### 配置项 - -```php -return [ - // 订单管理全部权限角色(可以查看和编辑所有订单) - 'order_edit_all_roles' => [0, 3, 6], - - // 其他相关配置 - 'prescription_library_manage_all_roles' => [0, 3], // 处方库管理全部权限角色 - 'prescription_audit_roles' => [0, 3, 6], // 处方审核权限角色 - 'prescription_order_finance_roles' => [0, 3], // 处方订单财务权限角色 - 'prescription_order_payment_audit_roles' => [0, 3], // 处方订单支付审核权限角色 -]; -``` - -### 角色ID说明 - -| 角色ID | 角色名称 | 说明 | -|-------|---------|------| -| 0 | 超级管理员 | 拥有所有权限 | -| 1 | 医生 | 开方、诊断等医疗权限 | -| 2 | 医助 | 协助医生工作 | -| 3 | 管理员 | 管理订单、审核等权限 | -| 6 | 药师 | 审核处方、管理药品等权限 | - -## 使用场景 - -### 场景1:药师关联支付单 - -**配置前**: -```php -'order_edit_all_roles' => [0, 3], // 药师(角色6)无权限 -``` - -**问题**: -- 药师无法关联其他人创建的支付单 -- 提示"无权限关联所选支付单" - -**配置后**: -```php -'order_edit_all_roles' => [0, 3, 6], // 药师(角色6)有权限 -``` - -**效果**: -- 药师可以关联所有支付单 -- 可以正常编辑业务订单 - -### 场景2:医生关联支付单 - -**情况1:医生是支付单创建人** -- 可以关联自己创建的支付单 -- 不需要管理员权限 - -**情况2:医生不是支付单创建人** -- 如果医生是诊单医助,可以关联该诊单的所有支付单 -- 如果医生不是诊单医助,无法关联其他人创建的支付单 - -**情况3:医生是管理员角色** -- 如果医生同时拥有管理员角色(如角色3),可以关联所有支付单 - -### 场景3:管理员关联支付单 - -**配置**: -```php -'order_edit_all_roles' => [0, 3, 6], -``` - -**效果**: -- 超级管理员(角色0):可以关联所有支付单 -- 管理员(角色3):可以关联所有支付单 -- 药师(角色6):可以关联所有支付单 - -## 测试建议 - -### 测试用例 1: 药师关联支付单 - -**前提条件**: -- 配置 `order_edit_all_roles` 包含角色 6 -- 用户角色为药师(角色6) - -**测试步骤**: -1. 创建一个业务订单 -2. 选择其他人创建的支付单 -3. 点击保存 - -**预期结果**: -- 保存成功 -- 支付单成功关联到业务订单 - -### 测试用例 2: 医生关联自己的支付单 - -**前提条件**: -- 用户角色为医生(角色1) -- 支付单由该医生创建 - -**测试步骤**: -1. 创建一个业务订单 -2. 选择自己创建的支付单 -3. 点击保存 - -**预期结果**: -- 保存成功 -- 支付单成功关联到业务订单 - -### 测试用例 3: 医生关联其他人的支付单(无权限) - -**前提条件**: -- 用户角色为医生(角色1) -- 支付单由其他人创建 -- 医生不是诊单医助 - -**测试步骤**: -1. 创建一个业务订单 -2. 选择其他人创建的支付单 -3. 点击保存 - -**预期结果**: -- 保存失败 -- 提示"无权限关联所选支付单" - -### 测试用例 4: 诊单医助关联支付单 - -**前提条件**: -- 用户是该诊单的医助 -- 支付单由其他人创建 - -**测试步骤**: -1. 创建一个业务订单 -2. 选择该诊单的支付单(其他人创建) -3. 点击保存 - -**预期结果**: -- 保存成功 -- 支付单成功关联到业务订单 - -## 相关配置 - -### 其他权限配置 - -在 `server/config/project.php` 中,还有其他相关的权限配置: - -```php -return [ - // 订单管理全部权限角色 - 'order_edit_all_roles' => [0, 3, 6], - - // 处方库管理全部权限角色(可以查看所有处方) - 'prescription_library_manage_all_roles' => [0, 3], - - // 处方审核权限角色 - 'prescription_audit_roles' => [0, 3, 6], - - // 处方订单财务权限角色(可以查看内部成本) - 'prescription_order_finance_roles' => [0, 3], - - // 处方订单支付审核权限角色 - 'prescription_order_payment_audit_roles' => [0, 3], -]; -``` - -### 配置建议 - -根据实际业务需求配置角色权限: - -**推荐配置**: -```php -return [ - 'order_edit_all_roles' => [0, 3, 6], // 管理员和药师可以管理所有订单 - 'prescription_library_manage_all_roles' => [0, 3], // 管理员可以查看所有处方 - 'prescription_audit_roles' => [0, 3, 6], // 管理员和药师可以审核处方 - 'prescription_order_finance_roles' => [0, 3], // 管理员可以查看财务数据 - 'prescription_order_payment_audit_roles' => [0, 3], // 管理员可以审核支付单 -]; -``` - -## 总结 - -通过修改 `isOrderListSupervisor()` 方法,确保了: -- ✅ 从配置文件读取管理员角色列表 -- ✅ 支持动态配置管理员角色 -- ✅ 药师(角色6)可以关联所有支付单 -- ✅ 保持向后兼容(默认值为 `[0, 3]`) -- ✅ 统一权限管理,避免硬编码 - -现在,只需要在配置文件中添加角色ID,就可以赋予该角色管理员权限,无需修改代码。 diff --git a/PAYMENT_AUDIT_PERMISSION_FIX.md b/PAYMENT_AUDIT_PERMISSION_FIX.md deleted file mode 100644 index 6fb687de..00000000 --- a/PAYMENT_AUDIT_PERMISSION_FIX.md +++ /dev/null @@ -1,84 +0,0 @@ -# 支付单审核权限修复 - -## 问题描述 - -接口 `/adminapi/tcm.prescriptionOrder/auditPayment` 返回错误: -```json -{"code":0,"show":1,"msg":"无支付单审核权限","data":[]} -``` - -药师角色(角色ID=6)无法审核支付单。 - -## 问题原因 - -配置文件 `server/config/project.php` 中的 `prescription_order_payment_audit_roles` 已经包含角色 6,但由于 ThinkPHP 的配置缓存机制,旧的配置仍在使用。 - -## 解决方案 - -清除 ThinkPHP 缓存,使新配置生效: - -```bash -cd server -php think clear -``` - -## 权限配置确认 - -文件:`server/config/project.php` - -```php -// 业务订单「关联支付单」审核:以下角色 ID + root 可操作(可与财务角色一致) -'prescription_order_payment_audit_roles' => [0, 3, 6], -``` - -- 角色 0:超级管理员 -- 角色 3:管理员 -- 角色 6:药师 - -## 权限检查逻辑 - -文件:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -```php -public static function canAuditPaymentSlipOrder(array $adminInfo): bool -{ - if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { - return true; - } - $allow = Config::get('project.prescription_order_payment_audit_roles', [0, 3]); - - return self::roleIntersect($adminInfo, is_array($allow) ? $allow : []); -} -``` - -该方法从配置文件读取允许的角色列表,并检查当前用户的角色是否在列表中。 - -## 测试验证 - -1. 清除缓存后,药师角色(角色ID=6)应该能够: - - 调用 `/adminapi/tcm.prescriptionOrder/auditPayment` 接口 - - 审核通过或驳回支付单 - -2. 验证步骤: - - 使用药师账号登录 - - 打开业务订单详情 - - 点击"支付单审核"按钮 - - 确认可以正常审核 - -## 相关配置总结 - -药师角色(角色ID=6)的完整权限配置: - -```php -'order_edit_all_roles' => [0, 3, 6], // 订单管理全部权限 -'prescription_library_manage_all_roles' => [0, 3], // 处方库管理全部权限 -'prescription_audit_roles' => [0, 3, 6], // 消费者处方审核权限 -'prescription_order_finance_roles' => [0, 3], // 财务字段查看权限 -'prescription_order_payment_audit_roles' => [0, 3, 6], // 支付单审核权限 -``` - -## 注意事项 - -1. 每次修改 `server/config/project.php` 后,都需要清除缓存 -2. 生产环境部署时,确保运行 `php think clear` 命令 -3. 如果问题仍然存在,检查用户的角色ID是否正确分配 diff --git a/PHARMACIST_ROLE_PERMISSIONS.md b/PHARMACIST_ROLE_PERMISSIONS.md deleted file mode 100644 index 32068c37..00000000 --- a/PHARMACIST_ROLE_PERMISSIONS.md +++ /dev/null @@ -1,358 +0,0 @@ -# 药师角色权限配置 - -## 概述 - -为药师角色(角色ID = 6)配置完整的权限,使其能够: -- 审核处方 -- 审核支付单 -- 管理订单 -- 关联支付单 - -## 配置文件 - -**文件**: `server/config/project.php` - -### 完整配置 - -```php -return [ - // 订单管理全部权限角色 - 'order_edit_all_roles' => [0, 3, 6], - - // 处方库管理全部权限角色 - 'prescription_library_manage_all_roles' => [0, 3], - - // 消费者处方审核权限角色 - 'prescription_audit_roles' => [0, 3, 6], - - // 处方业务订单财务字段查看权限角色 - 'prescription_order_finance_roles' => [0, 3], - - // 业务订单支付单审核权限角色 - 'prescription_order_payment_audit_roles' => [0, 3, 6], -]; -``` - -## 权限说明 - -### 1. 订单管理权限 (`order_edit_all_roles`) - -**配置**: `[0, 3, 6]` - -**权限内容**: -- 查看所有订单(不限制创建人) -- 编辑所有订单 -- 关联所有支付单(不限制创建人) - -**适用场景**: -- 编辑业务订单 -- 关联支付单到业务订单 -- 查看订单列表 - -**角色**: -- 0: 超级管理员 -- 3: 管理员 -- 6: 药师 ✅ - -### 2. 处方库管理权限 (`prescription_library_manage_all_roles`) - -**配置**: `[0, 3]` - -**权限内容**: -- 查看所有处方库模板 -- 编辑所有处方库模板 -- 删除所有处方库模板 - -**适用场景**: -- 管理处方库 -- 查看处方模板 - -**角色**: -- 0: 超级管理员 -- 3: 管理员 - -**说明**: 药师不需要此权限,因为处方库主要由医生和管理员管理 - -### 3. 消费者处方审核权限 (`prescription_audit_roles`) - -**配置**: `[0, 3, 6]` - -**权限内容**: -- 审核待审核的处方 -- 通过或驳回处方 -- 驳回时会同时作废处方 - -**适用场景**: -- 消费者处方列表中的"审核"按钮 -- 处方审核流程 - -**角色**: -- 0: 超级管理员 -- 3: 管理员 -- 6: 药师 ✅ - -### 4. 财务字段查看权限 (`prescription_order_finance_roles`) - -**配置**: `[0, 3]` - -**权限内容**: -- 查看业务订单的 `internal_cost`(内部成本)字段 -- 查看其他财务相关字段 - -**适用场景**: -- 业务订单详情 -- 业务订单列表 - -**角色**: -- 0: 超级管理员 -- 3: 管理员 - -**说明**: 药师不需要此权限,财务数据仅管理员可见 - -### 5. 支付单审核权限 (`prescription_order_payment_audit_roles`) - -**配置**: `[0, 3, 6]` - -**权限内容**: -- 审核业务订单的关联支付单 -- 通过或驳回支付单审核 - -**适用场景**: -- 业务订单详情中的"支付审核"按钮 -- 支付单审核流程 - -**角色**: -- 0: 超级管理员 -- 3: 管理员 -- 6: 药师 ✅ - -## 角色权限矩阵 - -| 权限 | 超级管理员(0) | 医生(1) | 医助(2) | 管理员(3) | 药师(6) | -|-----|-------------|---------|---------|----------|---------| -| 订单管理全部权限 | ✅ | ❌ | ❌ | ✅ | ✅ | -| 处方库管理全部权限 | ✅ | ❌ | ❌ | ✅ | ❌ | -| 消费者处方审核 | ✅ | ❌ | ❌ | ✅ | ✅ | -| 财务字段查看 | ✅ | ❌ | ❌ | ✅ | ❌ | -| 支付单审核 | ✅ | ❌ | ❌ | ✅ | ✅ | - -## 修改记录 - -### 修改1: 订单管理权限 - -**文件**: `server/app/adminapi/logic/order/OrderLogic.php` - -**问题**: 硬编码了管理员角色 `[0, 3]`,没有读取配置 - -**修改前**: -```php -public static function isOrderListSupervisor(int $adminId, array $adminInfo): bool -{ - if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { - return true; - } - $supervisorRoles = [0, 3]; // 硬编码 - $myRoles = array_map('intval', $adminInfo['role_id'] ?? []); - - return count(array_intersect($myRoles, $supervisorRoles)) > 0; -} -``` - -**修改后**: -```php -public static function isOrderListSupervisor(int $adminId, array $adminInfo): bool -{ - if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { - return true; - } - $supervisorRoles = config('project.order_edit_all_roles', [0, 3]); // 从配置读取 - $myRoles = array_map('intval', $adminInfo['role_id'] ?? []); - - return count(array_intersect($myRoles, $supervisorRoles)) > 0; -} -``` - -### 修改2: 支付单审核权限 - -**文件**: `server/config/project.php` - -**修改前**: -```php -'prescription_order_payment_audit_roles' => [0, 3], -``` - -**修改后**: -```php -'prescription_order_payment_audit_roles' => [0, 3, 6], -``` - -## 使用场景 - -### 场景1: 药师审核处方 - -**流程**: -1. 医生创建处方(待审核状态) -2. 药师登录系统 -3. 进入消费者处方列表 -4. 点击"审核"按钮 -5. 选择"通过"或"驳回" -6. 填写审核意见 -7. 提交审核 - -**权限检查**: -- `prescription_audit_roles` 包含角色 6 ✅ -- 药师可以审核处方 - -### 场景2: 药师审核支付单 - -**流程**: -1. 管理员创建业务订单并关联支付单 -2. 药师登录系统 -3. 进入业务订单详情 -4. 点击"支付审核"按钮 -5. 选择"通过"或"驳回" -6. 填写审核意见 -7. 提交审核 - -**权限检查**: -- `prescription_order_payment_audit_roles` 包含角色 6 ✅ -- 药师可以审核支付单 - -### 场景3: 药师关联支付单 - -**流程**: -1. 药师创建业务订单 -2. 选择支付单(其他人创建的) -3. 点击保存 - -**权限检查**: -- `order_edit_all_roles` 包含角色 6 ✅ -- 药师可以关联所有支付单 - -### 场景4: 药师编辑订单 - -**流程**: -1. 药师查看业务订单列表 -2. 点击"编辑"按钮(其他人创建的订单) -3. 修改订单信息 -4. 点击保存 - -**权限检查**: -- `order_edit_all_roles` 包含角色 6 ✅ -- 药师可以编辑所有订单 - -## 测试建议 - -### 测试用例 1: 药师审核处方 - -**前提条件**: -- 用户角色为药师(角色6) -- 存在待审核的处方 - -**测试步骤**: -1. 登录药师账号 -2. 进入消费者处方列表 -3. 找到待审核的处方 -4. 点击"审核"按钮 -5. 选择"通过" -6. 点击确定 - -**预期结果**: -- 审核成功 -- 处方状态变为"已通过" - -### 测试用例 2: 药师审核支付单 - -**前提条件**: -- 用户角色为药师(角色6) -- 存在待审核的业务订单 - -**测试步骤**: -1. 登录药师账号 -2. 进入业务订单列表 -3. 点击"查看"按钮 -4. 点击"支付审核"按钮 -5. 选择"通过" -6. 点击确定 - -**预期结果**: -- 审核成功 -- 支付单审核状态变为"已通过" - -### 测试用例 3: 药师关联支付单 - -**前提条件**: -- 用户角色为药师(角色6) -- 存在其他人创建的支付单 - -**测试步骤**: -1. 登录药师账号 -2. 创建业务订单 -3. 选择其他人创建的支付单 -4. 点击保存 - -**预期结果**: -- 保存成功 -- 支付单成功关联到业务订单 - -### 测试用例 4: 药师编辑订单 - -**前提条件**: -- 用户角色为药师(角色6) -- 存在其他人创建的业务订单 - -**测试步骤**: -1. 登录药师账号 -2. 进入业务订单列表 -3. 点击"编辑"按钮(其他人创建的订单) -4. 修改订单信息 -5. 点击保存 - -**预期结果**: -- 保存成功 -- 订单信息更新 - -## 错误排查 - -### 错误1: "无权限关联所选支付单" - -**原因**: `order_edit_all_roles` 配置中没有包含角色 6 - -**解决方案**: -```php -'order_edit_all_roles' => [0, 3, 6], // 添加角色 6 -``` - -### 错误2: "无支付单审核权限" - -**原因**: `prescription_order_payment_audit_roles` 配置中没有包含角色 6 - -**解决方案**: -```php -'prescription_order_payment_audit_roles' => [0, 3, 6], // 添加角色 6 -``` - -### 错误3: "无处方审核权限" - -**原因**: `prescription_audit_roles` 配置中没有包含角色 6 - -**解决方案**: -```php -'prescription_audit_roles' => [0, 3, 6], // 添加角色 6 -``` - -## 相关文档 - -- `ORDER_SUPERVISOR_ROLE_CONFIG_FIX.md` - 订单管理员角色配置修复文档 -- `server/config/project.php` - 项目配置文件 - -## 总结 - -通过配置文件,为药师角色(角色6)赋予了以下权限: -- ✅ 订单管理全部权限(查看、编辑、关联支付单) -- ✅ 消费者处方审核权限(通过、驳回) -- ✅ 支付单审核权限(通过、驳回) -- ❌ 处方库管理权限(不需要) -- ❌ 财务字段查看权限(不需要) - -药师现在可以完整地参与处方审核和订单管理流程。 diff --git a/PINYIN_ABBREVIATION_GUIDE.md b/PINYIN_ABBREVIATION_GUIDE.md deleted file mode 100644 index 3d1f0310..00000000 --- a/PINYIN_ABBREVIATION_GUIDE.md +++ /dev/null @@ -1,275 +0,0 @@ -# 药材名称拼音首字母功能说明 - -## 概述 -系统使用 `overtrue/pinyin` 包自动生成中文药材名称的拼音首字母缩写,存储在 `name_pinyin_abbr` 字段中,用于快速检索。 - -## 核心实现 - -### 1. 依赖包 -```json -"overtrue/pinyin": "^6.0" -``` -已在 `server/composer.json` 中配置。 - -### 2. 服务类:`MedicineNameAbbrService` -**文件位置:** `server/app/common/service/doctor/MedicineNameAbbrService.php` - -```php -join(''); - - return strtolower($abbr); - } -} -``` - -**功能说明:** -- 输入:中文药材名称(如 "当归") -- 输出:拼音首字母小写连写(如 "dg") -- 自动处理:去除空格、转小写 -- 容错:如果 Pinyin 类不存在,返回空字符串 - -### 3. 使用示例 - -#### 示例 1:当归 -```php -MedicineNameAbbrService::build('当归'); -// 输出: "dg" -``` - -#### 示例 2:黄芪 -```php -MedicineNameAbbrService::build('黄芪'); -// 输出: "hq" -``` - -#### 示例 3:党参 -```php -MedicineNameAbbrService::build('党参'); -// 输出: "ds" -``` - -#### 示例 4:白术 -```php -MedicineNameAbbrService::build('白术'); -// 输出: "bs" -``` - -## 自动生成时机 - -### 1. 创建药材时 -**文件:** `server/app/adminapi/logic/doctor/MedicineLogic.php` - -```php -Medicine::create([ - 'name' => $params['name'], - 'name_pinyin_abbr' => MedicineNameAbbrService::build($params['name']), - // ... 其他字段 -]); -``` - -### 2. 编辑药材时 -```php -Medicine::update([ - 'id' => $params['id'], - 'name' => $params['name'], - 'name_pinyin_abbr' => MedicineNameAbbrService::build($params['name']), - // ... 其他字段 -]); -``` - -## 批量回填命令 - -### 命令:`sync_medicine_pinyin_abbr` -**文件:** `server/app/common/command/SyncMedicinePinyinAbbr.php` - -用于批量生成或更新已有药材的拼音首字母。 - -### 使用方法 - -#### 1. 仅更新空字段(默认) -```bash -cd server -php think sync_medicine_pinyin_abbr -``` -只处理 `name_pinyin_abbr` 为空的记录。 - -#### 2. 重算全部记录 -```bash -php think sync_medicine_pinyin_abbr --all -``` -重新计算所有药材的拼音首字母(覆盖现有值)。 - -#### 3. 预览模式(不写入数据库) -```bash -php think sync_medicine_pinyin_abbr --dry -``` -只统计将更新多少条,不实际写入。 - -#### 4. 组合使用 -```bash -php think sync_medicine_pinyin_abbr --all --dry -``` -预览全部重算的结果。 - -### 命令输出示例 -``` -模式: 仅空 abbr,待处理 150 条 -完成: 已更新 145 条,跳过 5 条。 -``` - -## 检索功能 - -### 列表搜索 -**文件:** `server/app/adminapi/lists/doctor/MedicineLists.php` - -```php -$query->where(function ($q) use ($kw, $keyword) { - $q->where('name_pinyin_abbr', 'like', '%' . $kw . '%') - ->whereOr('name', 'like', '%' . $keyword . '%'); -}); -``` - -**功能说明:** -- 用户输入 "dg" 可以搜索到 "当归" -- 用户输入 "当归" 也可以搜索到 -- 支持模糊匹配(LIKE 查询) - -### 搜索示例 - -| 用户输入 | 匹配药材 | -|---------|---------| -| dg | 当归 | -| hq | 黄芪 | -| ds | 党参、丹参 | -| bs | 白术、白芍 | -| 当归 | 当归 | - -## 数据库字段 - -### 表:`zyt_doctor_medicine` -```sql -`name_pinyin_abbr` varchar(64) DEFAULT NULL COMMENT '名称拼音首字母缩写(小写连写)' -``` - -### 索引建议 -如果药材数量很大(>10000条),建议添加索引: -```sql -ALTER TABLE `zyt_doctor_medicine` -ADD INDEX `idx_name_pinyin_abbr` (`name_pinyin_abbr`); -``` - -## 常见问题 - -### Q1: 如何安装 overtrue/pinyin? -```bash -cd server -composer require overtrue/pinyin -``` -(已在项目中安装,无需手动执行) - -### Q2: 多音字如何处理? -`overtrue/pinyin` 会自动选择最常用的读音。例如: -- "重" → "z" (重量) 或 "c" (重复) -- 默认使用最常见的读音 - -### Q3: 如何处理英文或数字? -- 英文字母:保留原样并转小写 -- 数字:保留原样 -- 示例:`"维生素B12"` → `"wssbB12"` → `"wssbb12"` - -### Q4: 为什么有些药材搜索不到? -可能原因: -1. `name_pinyin_abbr` 字段为空 → 运行批量回填命令 -2. 药材名称包含特殊字符 → 检查数据质量 -3. 索引未生效 → 检查数据库索引 - -### Q5: 如何在其他模块使用? -```php -use app\common\service\doctor\MedicineNameAbbrService; - -// 生成拼音首字母 -$abbr = MedicineNameAbbrService::build('当归'); -echo $abbr; // 输出: dg -``` - -## 性能优化建议 - -### 1. 批量处理 -命令使用 `chunk(200)` 分批处理,避免内存溢出: -```php -$makeQuery()->chunk(200, function ($rows) { - // 每次处理 200 条 -}); -``` - -### 2. 异步生成 -对于大量数据,建议: -- 使用队列异步处理 -- 在低峰期运行批量命令 -- 使用 `--dry` 先预览再执行 - -### 3. 缓存策略 -如果药材数据不常变动,可以: -- 缓存常用药材的拼音首字母 -- 使用 Redis 存储热门搜索词 - -## 相关文件 - -| 文件 | 说明 | -|------|------| -| `server/app/common/service/doctor/MedicineNameAbbrService.php` | 拼音首字母生成服务 | -| `server/app/common/command/SyncMedicinePinyinAbbr.php` | 批量回填命令 | -| `server/app/adminapi/logic/doctor/MedicineLogic.php` | 药材增删改逻辑 | -| `server/app/adminapi/lists/doctor/MedicineLists.php` | 药材列表搜索 | -| `server/app/common/model/doctor/Medicine.php` | 药材模型 | - -## 扩展应用 - -此功能可以扩展到其他需要中文检索的模块: - -### 1. 患者姓名检索 -```php -use app\common\service\doctor\MedicineNameAbbrService; - -$patientName = '张三'; -$abbr = MedicineNameAbbrService::build($patientName); // "zs" -``` - -### 2. 医生姓名检索 -```php -$doctorName = '李医生'; -$abbr = MedicineNameAbbrService::build($doctorName); // "lys" -``` - -### 3. 诊断名称检索 -```php -$diagnosis = '感冒'; -$abbr = MedicineNameAbbrService::build($diagnosis); // "gm" -``` - -## 总结 - -- **核心类:** `MedicineNameAbbrService::build()` -- **依赖包:** `overtrue/pinyin` -- **存储字段:** `name_pinyin_abbr` -- **批量命令:** `php think sync_medicine_pinyin_abbr` -- **应用场景:** 药材快速检索、模糊搜索 - -这个功能大大提升了中文药材名称的搜索体验,用户可以通过拼音首字母快速找到目标药材。 diff --git a/PRESCRIPTION_APPROVED_LOCK.md b/PRESCRIPTION_APPROVED_LOCK.md deleted file mode 100644 index 06916ec1..00000000 --- a/PRESCRIPTION_APPROVED_LOCK.md +++ /dev/null @@ -1,384 +0,0 @@ -# 处方审核通过后锁定编辑和作废 - -## 修改说明 - -处方审核通过后,禁止编辑和作废操作,确保已审核通过的处方不被修改,保证处方的完整性和可追溯性。 - -## 修改内容 - -### 1. 前端修改 - -**文件**: `admin/src/views/consumer/prescription/index.vue` - -#### 编辑按钮 - -**修改前**: -```vue - - 编辑 - -``` - -**修改后**: -```vue - - 编辑 - -``` - -**改进点**: -- 移除了业务订单驳回的例外情况 -- 只要处方审核通过且未作废,就不显示编辑按钮 -- 简化了逻辑,更加严格 - -#### 删除按钮 - -删除按钮已经使用了 `isApprovedActivePrescription(row)` 判断,无需修改: - -```vue - - 删除 - -``` - -#### 判断函数 - -```typescript -/** 已通过审核且未作废:仅可查看,不可编辑/删除 */ -function isApprovedActivePrescription(row: { audit_status?: number; void_status?: number }) { - return Number(row.audit_status) === 1 && Number(row.void_status) !== 1 -} -``` - -### 2. 后端修改 - -**文件**: `server/app/adminapi/logic/tcm/PrescriptionLogic.php` - -#### 编辑方法 - -**修改前**: -```php -// 已通过且未作废:不可编辑;例外:业务订单「处方审核」已驳回时允许医生改方,保存后业务订单侧审核会重置为待审 -if ((int) ($prescription->audit_status ?? 1) === 1 && (int) ($prescription->void_status ?? 0) === 0) { - if (!PrescriptionOrderLogic::allowDoctorEditApprovedPrescriptionDueToBusinessReject((int) $params['id'])) { - self::setError('该处方已通过审核,不可编辑'); - return false; - } -} -``` - -**修改后**: -```php -// 已通过且未作废:不可编辑 -if ((int) ($prescription->audit_status ?? 1) === 1 && (int) ($prescription->void_status ?? 0) === 0) { - self::setError('该处方已通过审核,不可编辑'); - return false; -} -``` - -**改进点**: -- 移除了业务订单驳回的例外情况 -- 简化了逻辑,更加严格 -- 不再调用 `PrescriptionOrderLogic::allowDoctorEditApprovedPrescriptionDueToBusinessReject()` - -#### 删除方法 - -删除方法已经有正确的检查,无需修改: - -```php -public static function delete(int $id): bool -{ - try { - $prescription = Prescription::find($id); - if (!$prescription) { - self::setError('处方不存在'); - return false; - } - - if ((int) ($prescription->audit_status ?? 1) === 1 && (int) ($prescription->void_status ?? 0) === 0) { - self::setError('该处方已通过审核,不可删除'); - return false; - } - - $prescription->delete(); - return true; - } catch (\Exception $e) { - self::setError($e->getMessage()); - return false; - } -} -``` - -#### 审核方法(作废) - -审核驳回会同时作废处方,已经有正确的状态检查: - -```php -public static function audit(int $id, string $action, string $remark, int $adminId, array $adminInfo): bool -{ - // ... - - // 只有待审核状态才能进行审核 - if ((int) ($row->audit_status ?? 1) !== 0) { - self::setError('当前状态不可审核'); - return false; - } - - // ... - - if ($action === 'reject') { - if (trim($remark) === '') { - self::setError('驳回时请填写审核意见'); - return false; - } - $row->audit_status = 2; - $row->audit_time = $now; - $row->audit_by = $adminId; - $row->audit_by_name = $name; - $row->audit_remark = $remark; - $row->void_status = 1; // 驳回时同时作废 - $row->void_time = $now; - $row->void_by = $adminId; - $row->void_by_name = $name; - - $ok = (bool) $row->save(); - if ($ok) { - self::$lastAuditWecomNotify = self::notifyCreatorAuditResult($row, 'reject', $remark, $adminInfo); - } - return $ok; - } - - // ... -} -``` - -## 权限规则 - -### 编辑权限 - -处方可以编辑的条件: -1. **待审核状态** (`audit_status = 0`) -2. **已驳回状态** (`audit_status = 2`),且满足以下条件: - - 该诊单当天没有其他已通过的处方 - - 编辑后会清除驳回状态,重新进入待审核 - -处方不可编辑的条件: -1. **已通过审核且未作废** (`audit_status = 1` && `void_status = 0`) - - 即使业务订单驳回了处方审核,也不允许编辑 - - 必须先撤回业务订单,处方审核状态会重置为待审核,然后才能编辑 - -### 删除权限 - -处方可以删除的条件: -1. **待审核状态** (`audit_status = 0`) -2. **已驳回状态** (`audit_status = 2`) -3. **已作废状态** (`void_status = 1`) - -处方不可删除的条件: -1. **已通过审核且未作废** (`audit_status = 1` && `void_status = 0`) - -### 作废权限(审核驳回) - -处方可以作废的条件: -1. **待审核状态** (`audit_status = 0`) -2. 有审核权限的用户 - -处方不可作废的条件: -1. **已通过审核** (`audit_status = 1`) -2. **已驳回** (`audit_status = 2`) - -## 业务流程 - -### 场景1:处方审核通过后需要修改 - -**旧流程**(已废弃): -1. 处方审核通过 -2. 创建业务订单 -3. 业务订单处方审核驳回 -4. 医生可以直接编辑已通过的处方 ❌ - -**新流程**(推荐): -1. 处方审核通过 -2. 创建业务订单 -3. 业务订单处方审核驳回 -4. 管理员撤回业务订单(处方审核状态重置为待审核) -5. 医生编辑处方 -6. 处方重新审核 -7. 创建新的业务订单 - -### 场景2:处方审核通过后发现错误 - -**解决方案**: -1. 如果已创建业务订单,先撤回业务订单 -2. 处方审核状态会自动重置为待审核 -3. 医生编辑处方 -4. 处方重新审核 -5. 创建新的业务订单 - -### 场景3:处方审核驳回后修改 - -**流程**: -1. 处方审核驳回(同时作废) -2. 医生编辑处方(编辑后会清除驳回和作废状态) -3. 处方重新进入待审核状态 -4. 重新审核 - -## 优势 - -### 1. 数据完整性 -- 已审核通过的处方不可修改,保证了处方的完整性 -- 避免审核后的处方被随意修改,导致审核记录与实际内容不符 - -### 2. 可追溯性 -- 每次修改都需要重新审核,留下完整的审核记录 -- 便于追溯处方的修改历史和审核历史 - -### 3. 流程规范 -- 强制执行"撤回 → 编辑 → 重新审核"的流程 -- 避免绕过审核流程直接修改处方 - -### 4. 责任明确 -- 审核人员对已审核的处方负责 -- 修改处方需要重新审核,责任链清晰 - -## 注意事项 - -### 1. 业务订单撤回 - -如果需要修改已审核通过的处方,必须先撤回业务订单: -- 撤回业务订单会自动重置处方审核状态为待审核 -- 撤回后才能编辑处方 -- 编辑后需要重新审核处方 -- 审核通过后可以创建新的业务订单 - -### 2. 处方驳回 - -处方驳回会同时作废处方: -- 驳回后处方状态变为"已驳回"(`audit_status = 2`) -- 同时处方会被作废(`void_status = 1`) -- 医生可以编辑已驳回的处方 -- 编辑后会清除驳回和作废状态,重新进入待审核 - -### 3. 权限配置 - -确保相关权限配置正确: -- `cf.prescription/edit` - 编辑处方权限 -- `cf.prescription/del` - 删除处方权限 -- `cf.prescription/audit` - 审核处方权限 -- `tcm.prescriptionOrder/withdraw` - 撤回业务订单权限 - -## 测试建议 - -### 1. 编辑权限测试 - -**测试用例 1: 待审核处方可以编辑** -- 创建处方(待审核状态) -- 点击"编辑"按钮 -- 预期:可以正常编辑 - -**测试用例 2: 已通过处方不可编辑** -- 创建处方并审核通过 -- 点击"编辑"按钮 -- 预期:按钮不显示或提示"该处方已通过审核,不可编辑" - -**测试用例 3: 业务订单驳回后不可直接编辑** -- 创建处方并审核通过 -- 创建业务订单 -- 业务订单处方审核驳回 -- 点击"编辑"按钮 -- 预期:按钮不显示或提示"该处方已通过审核,不可编辑" - -**测试用例 4: 撤回业务订单后可以编辑** -- 创建处方并审核通过 -- 创建业务订单 -- 撤回业务订单 -- 点击"编辑"按钮 -- 预期:可以正常编辑 - -**测试用例 5: 已驳回处方可以编辑** -- 创建处方 -- 审核驳回 -- 点击"编辑"按钮 -- 预期:可以正常编辑,编辑后清除驳回和作废状态 - -### 2. 删除权限测试 - -**测试用例 6: 待审核处方可以删除** -- 创建处方(待审核状态) -- 点击"删除"按钮 -- 预期:可以正常删除 - -**测试用例 7: 已通过处方不可删除** -- 创建处方并审核通过 -- 点击"删除"按钮 -- 预期:按钮不显示或提示"该处方已通过审核,不可删除" - -**测试用例 8: 已驳回处方可以删除** -- 创建处方 -- 审核驳回 -- 点击"删除"按钮 -- 预期:可以正常删除 - -### 3. 作废权限测试 - -**测试用例 9: 待审核处方可以驳回作废** -- 创建处方(待审核状态) -- 点击"审核"按钮,选择"驳回" -- 预期:处方被驳回并作废 - -**测试用例 10: 已通过处方不可驳回作废** -- 创建处方并审核通过 -- 点击"审核"按钮 -- 预期:按钮不显示或提示"当前状态不可审核" - -**测试用例 11: 已驳回处方不可再次驳回** -- 创建处方 -- 审核驳回 -- 再次点击"审核"按钮 -- 预期:按钮不显示或提示"当前状态不可审核" - -### 4. 业务流程测试 - -**测试用例 12: 完整的修改流程** -1. 创建处方并审核通过 -2. 创建业务订单 -3. 业务订单处方审核驳回 -4. 尝试编辑处方 → 预期:不可编辑 -5. 撤回业务订单 -6. 编辑处方 → 预期:可以编辑 -7. 处方重新审核通过 -8. 创建新的业务订单 → 预期:成功 - -## 相关文档 - -- `PRESCRIPTION_ORDER_WITHDRAW_RESET_AUDIT.md` - 订单撤回重置审核状态文档 -- `BUG_FIXES_SUMMARY.md` - Bug修复总结文档 - -## 总结 - -通过移除业务订单驳回的例外情况,确保了: -- ✅ 已审核通过的处方不可编辑 -- ✅ 已审核通过的处方不可删除 -- ✅ 已审核通过的处方不可作废(驳回) -- ✅ 强制执行"撤回 → 编辑 → 重新审核"的规范流程 -- ✅ 保证处方的完整性和可追溯性 -- ✅ 责任链清晰,审核记录完整 -- ✅ 前后端双重校验,防止绕过限制 diff --git a/PRESCRIPTION_BUTTON_DISPLAY_CHECK.md b/PRESCRIPTION_BUTTON_DISPLAY_CHECK.md deleted file mode 100644 index cad8914b..00000000 --- a/PRESCRIPTION_BUTTON_DISPLAY_CHECK.md +++ /dev/null @@ -1,204 +0,0 @@ -# 处方按钮显示检查 - -## 问题描述 - -用户反馈:处方审核通过后,"开方"按钮不显示了。 - -## 代码分析 - -### 前端代码 - -**文件**: `admin/src/views/tcm/appointment/list.vue` - -#### 按钮显示逻辑 - -```vue - - {{ prescriptionActionLabel(row) }} - -``` - -#### 按钮文字逻辑 - -```typescript -function prescriptionActionLabel(row: any) { - if ( - row?.prescription_audit_status === 1 && - Number(row?.prescription_void_status) !== 1 - ) { - return '查看' - } - return '开方' -} -``` - -**逻辑说明**: -- 如果处方审核通过(`prescription_audit_status === 1`)且未作废(`prescription_void_status !== 1`),按钮文字显示"查看" -- 否则,按钮文字显示"开方" - -### 后端代码 - -**文件**: `server/app/adminapi/lists/doctor/AppointmentLists.php` - -```php -$apptId = (int) ($item['id'] ?? 0); -$apptRx = $rxByAppointmentId[$apptId] ?? null; -$item['prescription_audit_status'] = $apptRx !== null ? (int) ($apptRx['audit_status'] ?? -1) : -1; -$item['prescription_void_status'] = $apptRx !== null ? (int) ($apptRx['void_status'] ?? 0) : 0; -``` - -**逻辑说明**: -- 如果预约有关联的处方,返回处方的审核状态和作废状态 -- 如果预约没有关联的处方,`prescription_audit_status` 为 `-1`,`prescription_void_status` 为 `0` - -## 可能的原因 - -### 1. 权限问题 - -按钮有权限检查 `v-perms="['tcm.diagnosis/kaifang']"`,如果用户没有这个权限,按钮会被隐藏。 - -**检查方法**: -1. 登录用户账号 -2. 检查用户角色是否有 `tcm.diagnosis/kaifang` 权限 -3. 在浏览器控制台查看按钮元素是否存在 - -### 2. 数据问题 - -后端返回的 `prescription_audit_status` 或 `prescription_void_status` 字段值不正确。 - -**检查方法**: -1. 打开浏览器开发者工具 -2. 查看预约列表接口的响应数据 -3. 检查 `prescription_audit_status` 和 `prescription_void_status` 字段的值 - -**预期值**: -- 未开方:`prescription_audit_status = -1`, `prescription_void_status = 0` -- 待审核:`prescription_audit_status = 0`, `prescription_void_status = 0` -- 已通过:`prescription_audit_status = 1`, `prescription_void_status = 0` -- 已驳回:`prescription_audit_status = 2`, `prescription_void_status = 1` - -### 3. 按钮文字问题 - -按钮确实显示了,但是文字从"开方"变成了"查看",用户可能误以为按钮消失了。 - -**检查方法**: -1. 查看预约列表 -2. 找到已审核通过的处方对应的预约 -3. 检查按钮文字是否显示为"查看" - -## 解决方案 - -### 方案1:确认权限配置 - -确保用户角色有 `tcm.diagnosis/kaifang` 权限: - -1. 进入后台管理 → 权限管理 → 角色管理 -2. 找到用户所属的角色 -3. 检查是否勾选了"中医诊断 → 开方"权限(`tcm.diagnosis/kaifang`) -4. 如果没有勾选,勾选后保存 - -### 方案2:检查数据返回 - -在浏览器开发者工具中检查接口返回: - -1. 打开浏览器开发者工具(F12) -2. 切换到 Network 标签 -3. 刷新预约列表页面 -4. 找到预约列表接口(通常是 `/doctor.appointment/lists`) -5. 查看响应数据中的 `prescription_audit_status` 和 `prescription_void_status` 字段 - -**示例数据**: -```json -{ - "id": 123, - "patient_name": "张三", - "prescription_audit_status": 1, // 1=已通过 - "prescription_void_status": 0, // 0=未作废 - "has_prescription": 1 -} -``` - -### 方案3:按钮文字说明 - -如果按钮文字从"开方"变成了"查看",这是正常的: - -- **未开方或待审核**:按钮显示"开方",点击后可以创建或编辑处方 -- **已审核通过**:按钮显示"查看",点击后只能查看处方(只读模式) - -这是为了区分"可编辑"和"只读"两种模式。 - -## 测试步骤 - -### 测试1:未开方的预约 - -1. 找到一个未开方的预约(`has_prescription = 0`) -2. 检查按钮是否显示"开方" -3. 点击按钮,应该可以创建新处方 - -### 测试2:待审核的处方 - -1. 创建一个处方(待审核状态) -2. 返回预约列表 -3. 检查按钮是否显示"开方" -4. 点击按钮,应该可以编辑处方 - -### 测试3:已审核通过的处方 - -1. 创建一个处方并审核通过 -2. 返回预约列表 -3. 检查按钮是否显示"查看" -4. 点击按钮,应该只能查看处方(只读模式) - -### 测试4:已驳回的处方 - -1. 创建一个处方并审核驳回 -2. 返回预约列表 -3. 检查按钮是否显示"开方" -4. 点击按钮,应该可以编辑处方 - -## 预期行为 - -| 处方状态 | `prescription_audit_status` | `prescription_void_status` | 按钮文字 | 点击行为 | -|---------|---------------------------|--------------------------|---------|---------| -| 未开方 | -1 | 0 | 开方 | 创建新处方 | -| 待审核 | 0 | 0 | 开方 | 编辑处方 | -| 已通过 | 1 | 0 | 查看 | 查看处方(只读) | -| 已驳回 | 2 | 1 | 开方 | 编辑处方 | - -## 常见问题 - -### Q1: 为什么审核通过后按钮文字变成"查看"? - -A: 这是为了区分"可编辑"和"只读"两种模式。已审核通过的处方不可编辑,只能查看。 - -### Q2: 如果需要修改已审核通过的处方怎么办? - -A: 需要先撤回业务订单(如果已创建),处方审核状态会自动重置为待审核,然后才能编辑。 - -### Q3: 按钮完全不显示怎么办? - -A: 检查用户是否有 `tcm.diagnosis/kaifang` 权限。如果没有,需要在角色管理中添加该权限。 - -### Q4: 点击"查看"按钮后能编辑吗? - -A: 不能。已审核通过的处方只能查看,不能编辑。如果需要编辑,需要先撤回业务订单。 - -## 总结 - -按钮的显示逻辑是正确的: -- ✅ 按钮始终显示(只要有 `tcm.diagnosis/kaifang` 权限) -- ✅ 按钮文字根据处方状态动态变化("开方" 或 "查看") -- ✅ 点击行为根据处方状态不同(可编辑 或 只读) - -如果用户反馈"按钮不显示",可能是以下原因: -1. 用户没有 `tcm.diagnosis/kaifang` 权限 -2. 按钮文字从"开方"变成了"查看",用户误以为按钮消失了 -3. 数据返回有问题,需要检查后端接口 - -建议先检查权限配置和数据返回,确认按钮是否真的不显示。 diff --git a/PRESCRIPTION_DOSAGE_COMPLETE_SUMMARY.md b/PRESCRIPTION_DOSAGE_COMPLETE_SUMMARY.md deleted file mode 100644 index 769fe3fd..00000000 --- a/PRESCRIPTION_DOSAGE_COMPLETE_SUMMARY.md +++ /dev/null @@ -1,283 +0,0 @@ -# 处方用量功能完整实现总结 - -## 概述 -在两个处方页面中实现了用量、单位和代煎功能: -1. **处方组件** (`admin/src/components/tcm-prescription/index.vue`) - 创建/编辑处方 -2. **处方列表** (`admin/src/views/consumer/prescription/index.vue`) - 查看/编辑处方 - -## 已实现的功能 - -### 1. 数据库层 ✅ -**文件**: `add_prescription_dosage_fields.sql` - -```sql -ALTER TABLE `zyt_tcm_prescription` -ADD COLUMN `dosage_amount` decimal(10,2) DEFAULT NULL COMMENT '用量数值', -ADD COLUMN `dosage_unit` varchar(10) NOT NULL DEFAULT '' COMMENT '用量单位(g/ml)', -ADD COLUMN `need_decoction` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否代煎(0否1是,仅饮片)'; -``` - -### 2. 后端层 ✅ - -#### 模型 (`server/app/common/model/tcm/Prescription.php`) -```php -protected $type = [ - 'dosage_amount' => 'float', - 'need_decoction' => 'integer', -]; -``` - -#### 业务逻辑 (`server/app/adminapi/logic/tcm/PrescriptionLogic.php`) -- ✅ `add()` 方法:保存新处方 -- ✅ `edit()` 方法:编辑处方 - -### 3. 前端层 ✅ - -#### 3.1 处方组件 (`admin/src/components/tcm-prescription/index.vue`) - -**数据结构**: -```typescript -interface PrescriptionForm { - prescription_type: string - dosage_amount: number | undefined - dosage_unit: string - need_decoction: boolean - // ... -} -``` - -**响应式数据**: -```typescript -const formData = reactive({ - prescription_type: '浓缩水丸', - dosage_amount: 1, - dosage_unit: 'g', - need_decoction: false, - // ... -}) -``` - -**自动切换逻辑**: -```typescript -watch(() => formData.prescription_type, (newType) => { - if (newType === '浓缩水丸') { - formData.dosage_unit = 'g' - formData.dosage_amount = 1 - formData.need_decoction = false - } else if (newType === '饮片') { - formData.dosage_unit = 'ml' - formData.dosage_amount = 50 - } else { - formData.dosage_unit = 'g' - formData.dosage_amount = undefined - formData.need_decoction = false - } -}) -``` - -#### 3.2 处方列表 (`admin/src/views/consumer/prescription/index.vue`) - -**数据结构**: -```typescript -const editForm = reactive({ - prescription_type: '浓缩水丸', - dosage_amount: 1 as number | undefined, - dosage_unit: 'g', - need_decoction: false, - // ... -}) -``` - -**自动切换逻辑**: -```typescript -watch(() => editForm.prescription_type, (newType) => { - if (newType === '浓缩水丸') { - editForm.dosage_unit = 'g' - editForm.dosage_amount = 1 - editForm.need_decoction = false - } else if (newType === '饮片') { - editForm.dosage_unit = 'ml' - editForm.dosage_amount = 50 - } else { - editForm.dosage_unit = 'g' - editForm.dosage_amount = undefined - editForm.need_decoction = false - } -}) -``` - -## UI 组件实现 - -### 浓缩水丸 (1-10g) -```vue - - - - - -``` - -### 饮片 (50-250ml) -```vue - - - - - - - - - - - 代煎 - 不代煎 - -``` - -### 其他类型 -```vue - -``` - -## 功能规则 - -### 1. 浓缩水丸 -- **单位**: g (克) -- **范围**: 1-10g -- **UI**: 下拉选择 -- **代煎**: 不显示 - -### 2. 饮片 -- **单位**: ml (毫升) -- **范围**: 50-250ml (步进50ml) -- **UI**: 下拉选择 -- **代煎**: 显示单选按钮(代煎/不代煎) - -### 3. 其他类型(颗粒、丸剂、散剂、膏方、汤剂) -- **单位**: g (克) -- **范围**: 不限制 -- **UI**: 数字输入框(支持小数) -- **代煎**: 不显示 - -## 数据流转示例 - -### 创建浓缩水丸处方 -``` -用户操作: -1. 选择"浓缩水丸" -2. 自动设置: 单位=g, 用量=1g -3. 选择用量: 5g -4. 保存 - -数据库存储: -prescription_type: "浓缩水丸" -dosage_amount: 5.00 -dosage_unit: "g" -need_decoction: 0 -``` - -### 创建饮片处方(代煎) -``` -用户操作: -1. 选择"饮片" -2. 自动设置: 单位=ml, 用量=50ml -3. 选择用量: 150ml -4. 选择"代煎" -5. 保存 - -数据库存储: -prescription_type: "饮片" -dosage_amount: 150.00 -dosage_unit: "ml" -need_decoction: 1 -``` - -## 部署清单 - -### 1. 数据库迁移 ⚠️ 必须执行 -```bash -mysql -u用户名 -p数据库名 < add_prescription_dosage_fields.sql -``` - -### 2. 后端文件 -- ✅ `server/app/common/model/tcm/Prescription.php` -- ✅ `server/app/adminapi/logic/tcm/PrescriptionLogic.php` - -### 3. 前端文件 -- ✅ `admin/src/components/tcm-prescription/index.vue` -- ✅ `admin/src/views/consumer/prescription/index.vue` - -## 测试清单 - -### 处方组件测试 -- [ ] 创建浓缩水丸处方,选择1-10g -- [ ] 创建饮片处方,选择50-250ml -- [ ] 饮片处方选择"代煎" -- [ ] 饮片处方选择"不代煎" -- [ ] 创建颗粒处方,输入自定义用量 -- [ ] 切换处方类型,验证用量自动调整 -- [ ] 保存处方,验证数据正确存储 - -### 处方列表测试 -- [ ] 编辑浓缩水丸处方,修改用量 -- [ ] 编辑饮片处方,修改用量和代煎选项 -- [ ] 编辑其他类型处方,输入自定义用量 -- [ ] 切换处方类型,验证用量自动调整 -- [ ] 保存修改,验证数据正确更新 -- [ ] 查看处方详情,验证显示正确 - -### 数据验证测试 -- [ ] 浓缩水丸用量范围:1-10g -- [ ] 饮片用量范围:50-250ml -- [ ] 饮片用量步进:50ml -- [ ] 代煎选项仅在饮片时显示 -- [ ] 切换类型时代煎选项自动隐藏 - -## 文件清单 - -### 数据库 -- `add_prescription_dosage_fields.sql` - 数据库迁移文件 - -### 后端 -- `server/app/common/model/tcm/Prescription.php` - 处方模型 -- `server/app/adminapi/logic/tcm/PrescriptionLogic.php` - 处方业务逻辑 - -### 前端 -- `admin/src/components/tcm-prescription/index.vue` - 处方组件 -- `admin/src/views/consumer/prescription/index.vue` - 处方列表 - -### 文档 -- `PRESCRIPTION_DOSAGE_FEATURE.md` - 功能说明 -- `PRESCRIPTION_DOSAGE_DATABASE_IMPLEMENTATION.md` - 数据库实现 -- `PRESCRIPTION_DOSAGE_COMPLETE_SUMMARY.md` - 本文档 - -## 注意事项 - -1. **数据库迁移必须先执行**,否则保存时会报错 -2. **旧数据兼容**:旧处方的用量字段为NULL,不影响显示 -3. **类型切换**:切换处方类型时,用量和单位会自动调整 -4. **代煎选项**:仅在选择"饮片"时显示 -5. **数据验证**:前端使用下拉选择确保数据有效性 - -## 后续优化建议 - -1. **后端验证**:添加用量范围的后端验证逻辑 -2. **打印显示**:在处方打印时显示用量和代煎信息 -3. **统计报表**:按处方类型和用量统计 -4. **价格联动**:根据用量自动计算处方价格 -5. **库存检查**:代煎选项关联药房库存 - -## 总结 - -✅ **数据库层**:3个字段已定义 -✅ **后端层**:模型和业务逻辑已实现 -✅ **前端层**:2个页面都已实现 -✅ **UI组件**:下拉选择器和单选按钮 -✅ **自动切换**:watch监听实现 -✅ **数据流转**:完整的保存和读取 - -**状态**: 功能完整实现,只需执行数据库迁移即可使用!🎉 diff --git a/PRESCRIPTION_DOSAGE_DATABASE_IMPLEMENTATION.md b/PRESCRIPTION_DOSAGE_DATABASE_IMPLEMENTATION.md deleted file mode 100644 index 1fd0165d..00000000 --- a/PRESCRIPTION_DOSAGE_DATABASE_IMPLEMENTATION.md +++ /dev/null @@ -1,366 +0,0 @@ -# 处方用量数据库实现完整指南 - -## 概述 -为处方系统添加用量、单位和代煎字段的完整数据库实现。 - -## 1. 数据库迁移 - -### 执行SQL文件 -**文件**: `add_prescription_dosage_fields.sql` - -```sql --- 添加处方用量相关字段 -ALTER TABLE `zyt_tcm_prescription` -ADD COLUMN `dosage_amount` decimal(10,2) DEFAULT NULL COMMENT '用量数值' AFTER `prescription_type`, -ADD COLUMN `dosage_unit` varchar(10) NOT NULL DEFAULT '' COMMENT '用量单位(g/ml)' AFTER `dosage_amount`, -ADD COLUMN `need_decoction` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否代煎(0否1是,仅饮片)' AFTER `dosage_unit`; -``` - -### 执行方式 -```bash -mysql -u用户名 -p数据库名 < add_prescription_dosage_fields.sql -``` - -### 字段说明 - -| 字段名 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| `dosage_amount` | decimal(10,2) | NULL | 用量数值,支持小数 | -| `dosage_unit` | varchar(10) | '' | 用量单位 (g/ml) | -| `need_decoction` | tinyint(1) | 0 | 是否代煎 (0=否, 1=是) | - -## 2. 后端实现 - -### 2.1 模型层 (Model) -**文件**: `server/app/common/model/tcm/Prescription.php` - -```php -class Prescription extends BaseModel -{ - // ... 其他配置 - - // 字段类型转换 - protected $type = [ - 'dosage_amount' => 'float', - 'need_decoction' => 'integer', - ]; -} -``` - -**作用**: 自动将数据库字段转换为正确的PHP类型。 - -### 2.2 业务逻辑层 (Logic) -**文件**: `server/app/adminapi/logic/tcm/PrescriptionLogic.php` - -#### add() 方法 - 添加处方 -```php -$data = [ - // ... 其他字段 - 'prescription_type' => $params['prescription_type'] ?? '浓缩水丸', - 'dosage_amount' => isset($params['dosage_amount']) ? (float)$params['dosage_amount'] : null, - 'dosage_unit' => $params['dosage_unit'] ?? '', - 'need_decoction' => (int)($params['need_decoction'] ?? 0), - // ... 其他字段 -]; -``` - -#### edit() 方法 - 编辑处方 -```php -$data = [ - // ... 其他字段 - 'prescription_type' => $params['prescription_type'] ?? $prescription->prescription_type, - 'dosage_amount' => isset($params['dosage_amount']) ? (float)$params['dosage_amount'] : $prescription->dosage_amount, - 'dosage_unit' => $params['dosage_unit'] ?? $prescription->dosage_unit, - 'need_decoction' => isset($params['need_decoction']) ? (int)$params['need_decoction'] : (int)($prescription->need_decoction ?? 0), - // ... 其他字段 -]; -``` - -## 3. 前端实现 - -### 3.1 TypeScript 接口 -**文件**: `admin/src/components/tcm-prescription/index.vue` - -```typescript -interface PrescriptionForm { - // ... 其他字段 - prescription_type: string - dosage_amount: number | undefined - dosage_unit: string - need_decoction: boolean - // ... 其他字段 -} -``` - -### 3.2 响应式数据 -```typescript -const formData = reactive({ - // ... 其他字段 - prescription_type: '浓缩水丸', - dosage_amount: 1, - dosage_unit: 'g', - need_decoction: false, - // ... 其他字段 -}) -``` - -### 3.3 自动切换逻辑 -```typescript -watch(() => formData.prescription_type, (newType) => { - if (newType === '浓缩水丸') { - formData.dosage_unit = 'g' - formData.dosage_amount = 1 - formData.need_decoction = false - } else if (newType === '饮片') { - formData.dosage_unit = 'ml' - formData.dosage_amount = 50 - } else { - formData.dosage_unit = 'g' - formData.dosage_amount = undefined - formData.need_decoction = false - } -}) -``` - -## 4. 数据流转 - -### 4.1 保存流程 -``` -前端表单 - ↓ -{ - prescription_type: "浓缩水丸", - dosage_amount: 5, - dosage_unit: "g", - need_decoction: false -} - ↓ -后端 PrescriptionLogic::add() - ↓ -数据库 zyt_tcm_prescription - ↓ -dosage_amount: 5.00 -dosage_unit: "g" -need_decoction: 0 -``` - -### 4.2 读取流程 -``` -数据库查询 - ↓ -Prescription Model (类型转换) - ↓ -{ - dosage_amount: 5.0, // float - dosage_unit: "g", // string - need_decoction: 0 // int -} - ↓ -前端接收 - ↓ -{ - dosage_amount: 5, - dosage_unit: "g", - need_decoction: false -} -``` - -## 5. 数据示例 - -### 5.1 浓缩水丸处方 -```json -{ - "prescription_type": "浓缩水丸", - "dosage_amount": 5.00, - "dosage_unit": "g", - "need_decoction": 0 -} -``` - -### 5.2 饮片处方(代煎) -```json -{ - "prescription_type": "饮片", - "dosage_amount": 150.00, - "dosage_unit": "ml", - "need_decoction": 1 -} -``` - -### 5.3 颗粒处方 -```json -{ - "prescription_type": "颗粒", - "dosage_amount": 15.50, - "dosage_unit": "g", - "need_decoction": 0 -} -``` - -## 6. 部署步骤 - -### 步骤 1: 备份数据库 -```bash -mysqldump -u用户名 -p数据库名 > backup_$(date +%Y%m%d_%H%M%S).sql -``` - -### 步骤 2: 执行数据库迁移 -```bash -mysql -u用户名 -p数据库名 < add_prescription_dosage_fields.sql -``` - -### 步骤 3: 验证字段添加 -```sql -SHOW COLUMNS FROM `zyt_tcm_prescription` LIKE 'dosage%'; -SHOW COLUMNS FROM `zyt_tcm_prescription` LIKE 'need_decoction'; -``` - -### 步骤 4: 部署后端代码 -```bash -# 上传修改的文件 -- server/app/common/model/tcm/Prescription.php -- server/app/adminapi/logic/tcm/PrescriptionLogic.php -``` - -### 步骤 5: 部署前端代码 -```bash -# 构建前端 -cd admin -npm run build - -# 上传构建产物 -# 上传 dist/ 目录到服务器 -``` - -### 步骤 6: 清除缓存(如需要) -```bash -cd server -php think clear -``` - -### 步骤 7: 测试功能 -1. 创建新处方,选择不同类型 -2. 验证用量字段正确保存 -3. 编辑处方,验证数据正确加载 -4. 查看处方详情,验证显示正确 - -## 7. 数据验证 - -### 7.1 前端验证 -```typescript -// 浓缩水丸:1-10g -if (formData.prescription_type === '浓缩水丸') { - if (formData.dosage_amount < 1 || formData.dosage_amount > 10) { - // 错误提示 - } -} - -// 饮片:50-250ml,步进50 -if (formData.prescription_type === '饮片') { - if (formData.dosage_amount % 50 !== 0) { - // 错误提示 - } -} -``` - -### 7.2 后端验证(建议添加) -```php -// 在 PrescriptionLogic::add() 中添加 -if ($params['prescription_type'] === '浓缩水丸') { - $amount = (float)($params['dosage_amount'] ?? 0); - if ($amount < 1 || $amount > 10) { - self::setError('浓缩水丸用量必须在1-10g之间'); - return null; - } -} - -if ($params['prescription_type'] === '饮片') { - $amount = (float)($params['dosage_amount'] ?? 0); - if ($amount < 50 || $amount > 250 || $amount % 50 !== 0) { - self::setError('饮片用量必须为50-250ml,且为50的倍数'); - return null; - } -} -``` - -## 8. 常见问题 - -### Q1: 字段已存在错误 -``` -ERROR 1060 (42S21): Duplicate column name 'dosage_amount' -``` -**解决**: 字段已存在,跳过此步骤或先删除字段再添加。 - -### Q2: 旧数据如何处理? -**方案**: 旧数据的 `dosage_amount` 为 NULL,前端显示为空,不影响功能。 - -### Q3: 如何批量更新旧数据? -```sql --- 为浓缩水丸类型设置默认用量 -UPDATE `zyt_tcm_prescription` -SET - `dosage_amount` = 1, - `dosage_unit` = 'g' -WHERE `prescription_type` = '浓缩水丸' - AND `dosage_amount` IS NULL; - --- 为饮片类型设置默认用量 -UPDATE `zyt_tcm_prescription` -SET - `dosage_amount` = 50, - `dosage_unit` = 'ml' -WHERE `prescription_type` = '饮片' - AND `dosage_amount` IS NULL; -``` - -## 9. 回滚方案 - -如果需要回滚,执行以下SQL: - -```sql --- 删除添加的字段 -ALTER TABLE `zyt_tcm_prescription` -DROP COLUMN `dosage_amount`, -DROP COLUMN `dosage_unit`, -DROP COLUMN `need_decoction`; -``` - -## 10. 相关文件清单 - -### 数据库 -- `add_prescription_dosage_fields.sql` - 数据库迁移文件 - -### 后端 -- `server/app/common/model/tcm/Prescription.php` - 处方模型 -- `server/app/adminapi/logic/tcm/PrescriptionLogic.php` - 处方业务逻辑 - -### 前端 -- `admin/src/components/tcm-prescription/index.vue` - 处方组件 - -### 文档 -- `PRESCRIPTION_DOSAGE_FEATURE.md` - 功能说明文档 -- `PRESCRIPTION_DOSAGE_DATABASE_IMPLEMENTATION.md` - 本文档 - -## 11. 测试清单 - -- [ ] 数据库字段添加成功 -- [ ] 创建浓缩水丸处方,用量1-10g -- [ ] 创建饮片处方,用量50-250ml -- [ ] 饮片处方代煎选项正常 -- [ ] 编辑处方,数据正确加载 -- [ ] 切换处方类型,用量自动调整 -- [ ] 保存处方,数据正确存储 -- [ ] 查看处方详情,显示正确 -- [ ] 旧数据兼容性测试 - -## 总结 - -此实现完整覆盖了从数据库到前端的所有层次: -1. ✅ 数据库字段添加 -2. ✅ 模型层类型转换 -3. ✅ 业务逻辑层数据处理 -4. ✅ 前端表单交互 -5. ✅ 自动切换逻辑 -6. ✅ 数据验证 - -所有代码已实现,只需执行数据库迁移即可使用! diff --git a/PRESCRIPTION_DOSAGE_FEATURE.md b/PRESCRIPTION_DOSAGE_FEATURE.md deleted file mode 100644 index b09fa50d..00000000 --- a/PRESCRIPTION_DOSAGE_FEATURE.md +++ /dev/null @@ -1,307 +0,0 @@ -# 处方用量功能实现 - -## 概述 -根据处方类型自动调整用量单位和范围,并为饮片类型添加代煎选项。 - -## 功能说明 - -### 1. 浓缩水丸 -- **单位**: g (克) -- **范围**: 1-10g -- **步进**: 1g -- **默认值**: 1g - -### 2. 饮片 -- **单位**: ml (毫升) -- **范围**: 50-250ml -- **步进**: 50ml (每50ml一个单位) -- **默认值**: 50ml -- **特殊选项**: 是否代煎(代煎/不代煎) - -### 3. 其他类型(颗粒、丸剂、散剂、膏方、汤剂) -- **单位**: g (克) -- **范围**: 不限制 -- **精度**: 小数点后2位 -- **默认值**: 无 - -## 前端实现 - -### 文件:`admin/src/components/tcm-prescription/index.vue` - -#### 1. TypeScript 接口 -```typescript -interface PrescriptionForm { - // ... 其他字段 - prescription_type: string // 处方类型 - dosage_amount: number | undefined // 用量数值 - dosage_unit: string // 用量单位 (g 或 ml) - need_decoction: boolean // 是否代煎(仅饮片) - // ... -} -``` - -#### 2. 响应式数据 -```typescript -const formData = reactive({ - // ... - prescription_type: '浓缩水丸', - dosage_amount: 1, - dosage_unit: 'g', - need_decoction: false, - // ... -}) -``` - -#### 3. 自动切换逻辑 -```typescript -watch(() => formData.prescription_type, (newType) => { - if (newType === '浓缩水丸') { - formData.dosage_unit = 'g' - formData.dosage_amount = 1 - formData.need_decoction = false - } else if (newType === '饮片') { - formData.dosage_unit = 'ml' - formData.dosage_amount = 50 - // need_decoction 保持用户选择 - } else { - formData.dosage_unit = 'g' - formData.dosage_amount = undefined - formData.need_decoction = false - } -}) -``` - -#### 4. UI 组件 -```vue - - - - - - - - - - {{ formData.dosage_unit }} - - - - - - - - 代煎 - 不代煎 - - - -``` - -## 用户交互流程 - -### 场景 1:选择浓缩水丸 -1. 用户选择"浓缩水丸" -2. 系统自动设置: - - 用量单位:g - - 默认用量:1g - - 用量范围:1-10g - - 隐藏代煎选项 - -### 场景 2:选择饮片 -1. 用户选择"饮片" -2. 系统自动设置: - - 用量单位:ml - - 默认用量:50ml - - 用量范围:50-250ml(步进50ml) - - 显示代煎选项(默认"不代煎") -3. 用户可选择: - - 用量:50ml, 100ml, 150ml, 200ml, 250ml - - 是否代煎:代煎 / 不代煎 - -### 场景 3:选择其他类型 -1. 用户选择"颗粒"、"丸剂"等 -2. 系统自动设置: - - 用量单位:g - - 默认用量:空 - - 用量范围:不限制(可输入小数) - - 隐藏代煎选项 - -## 数据示例 - -### 浓缩水丸处方 -```json -{ - "prescription_type": "浓缩水丸", - "dosage_amount": 5, - "dosage_unit": "g", - "need_decoction": false -} -``` - -### 饮片处方(代煎) -```json -{ - "prescription_type": "饮片", - "dosage_amount": 150, - "dosage_unit": "ml", - "need_decoction": true -} -``` - -### 饮片处方(不代煎) -```json -{ - "prescription_type": "饮片", - "dosage_amount": 200, - "dosage_unit": "ml", - "need_decoction": false -} -``` - -### 颗粒处方 -```json -{ - "prescription_type": "颗粒", - "dosage_amount": 15.5, - "dosage_unit": "g", - "need_decoction": false -} -``` - -## 后端存储建议 - -### 数据库字段 -建议在处方表中添加以下字段: - -```sql -ALTER TABLE `zyt_tcm_prescription` -ADD COLUMN `dosage_amount` decimal(10,2) DEFAULT NULL COMMENT '用量数值', -ADD COLUMN `dosage_unit` varchar(10) NOT NULL DEFAULT '' COMMENT '用量单位(g/ml)', -ADD COLUMN `need_decoction` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否代煎(0否1是,仅饮片)'; -``` - -### 后端验证逻辑 -```php -// 浓缩水丸验证 -if ($prescriptionType === '浓缩水丸') { - if ($dosageAmount < 1 || $dosageAmount > 10) { - throw new Exception('浓缩水丸用量必须在1-10g之间'); - } - if ($dosageUnit !== 'g') { - throw new Exception('浓缩水丸单位必须为g'); - } -} - -// 饮片验证 -if ($prescriptionType === '饮片') { - if ($dosageAmount < 50 || $dosageAmount > 250 || $dosageAmount % 50 !== 0) { - throw new Exception('饮片用量必须为50-250ml,且为50的倍数'); - } - if ($dosageUnit !== 'ml') { - throw new Exception('饮片单位必须为ml'); - } -} -``` - -## UI 布局 - -``` -┌─────────────────────────────────────────────────────────┐ -│ 处方类型: [浓缩水丸 ▼] 用量: [1] g │ -└─────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────┐ -│ 处方类型: [饮片 ▼] 用量: [150] ml │ -│ 是否代煎: ⦿ 代煎 ○ 不代煎 │ -└─────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────┐ -│ 处方类型: [颗粒 ▼] 用量: [15.5] g │ -└─────────────────────────────────────────────────────────┘ -``` - -## 业务规则 - -### 1. 浓缩水丸 -- 用于浓缩制剂,按克计量 -- 通常用量较小,1-10g范围合理 -- 不需要代煎 - -### 2. 饮片 -- 用于传统中药饮片 -- 按煎煮后的药液体积计量(ml) -- 每次50ml为一个标准单位 -- 可选择是否由药房代煎 -- 代煎:药房负责煎煮,患者直接服用 -- 不代煎:患者自行煎煮 - -### 3. 其他类型 -- 根据实际情况灵活设置用量 -- 支持小数点精度 - -## 注意事项 - -1. **单位自动切换**:切换处方类型时,单位和默认值会自动更新 -2. **代煎选项**:仅在选择"饮片"时显示 -3. **用量限制**:不同类型有不同的用量范围限制 -4. **数据验证**:前端和后端都应进行用量范围验证 -5. **用户体验**:使用 `el-input-number` 组件,支持键盘上下键调整 - -## 扩展建议 - -1. **价格联动**:根据用量自动计算处方价格 -2. **库存检查**:代煎选项可关联药房库存 -3. **历史记录**:记录患者常用的用量设置 -4. **模板保存**:将用量设置保存到处方模板 -5. **打印显示**:在处方打印时显示用量和代煎信息 - -## 相关文件 - -- `admin/src/components/tcm-prescription/index.vue` - 处方组件主文件 - -## 测试用例 - -### 测试 1:浓缩水丸用量 -- 选择"浓缩水丸" -- 验证用量范围:1-10g -- 验证步进:1g -- 验证代煎选项隐藏 - -### 测试 2:饮片用量 -- 选择"饮片" -- 验证用量范围:50-250ml -- 验证步进:50ml -- 验证代煎选项显示 -- 测试代煎/不代煎切换 - -### 测试 3:类型切换 -- 从"浓缩水丸"切换到"饮片" -- 验证单位从g变为ml -- 验证用量自动调整 -- 验证代煎选项出现 - -### 测试 4:数据保存 -- 填写完整处方信息 -- 保存处方 -- 验证用量数据正确存储 -- 验证代煎选项正确存储 diff --git a/PRESCRIPTION_DUPLICATE_REQUEST_FIX.md b/PRESCRIPTION_DUPLICATE_REQUEST_FIX.md deleted file mode 100644 index d6a1dd7f..00000000 --- a/PRESCRIPTION_DUPLICATE_REQUEST_FIX.md +++ /dev/null @@ -1,321 +0,0 @@ -# 处方组件重复请求修复 - -## 问题描述 - -在预约列表页面点击"开处方"或"查看病历"按钮时,会出现重复请求 `/tcm.prescription/detail` 接口的问题,导致错误。 - -## 问题原因 - -### 1. 快速连续点击 - -用户可能快速连续点击按钮,导致 `open()` 或 `openById()` 方法被多次调用。 - -### 2. 异步操作未加锁 - -`open()` 和 `openById()` 方法是异步的,但没有加锁机制,导致: -- 第一次点击开始执行 -- 第二次点击也开始执行 -- 两次请求同时发送 - -### 3. 代码流程 - -```javascript -const open = async (data: any) => { - // 没有防重复检查 - await loadDictOptions() - - if (appointmentId) { - const existing = await prescriptionGetByAppointment(...) // 请求1 - if (existing?.id) { - const detail = await prescriptionDetail(...) // 请求2 - } - } -} -``` - -## 解决方案 - -### 添加防重复打开标志 - -使用 `isOpening` 标志来防止重复调用: - -```typescript -// 防止重复打开 -const isOpening = ref(false) - -const open = async (data: any, options?: { templateId?: number; stationName?: string }) => { - // 防止重复打开 - if (isOpening.value) { - console.warn('处方正在打开中,请勿重复点击') - return - } - - isOpening.value = true - - try { - // 原有逻辑... - } finally { - // 延迟重置,避免快速连续点击 - setTimeout(() => { - isOpening.value = false - }, 500) - } -} -``` - -### 关键点 - -1. **检查标志**:方法开始时检查 `isOpening.value`,如果为 true 则直接返回 -2. **设置标志**:开始执行时设置 `isOpening.value = true` -3. **finally 重置**:无论成功还是失败,都在 finally 中重置标志 -4. **延迟重置**:使用 `setTimeout` 延迟 500ms 重置,防止快速连续点击 - -## 修改的文件 - -**文件**:`admin/src/components/tcm-prescription/index.vue` - -### 1. 添加防重复标志 - -```typescript -// 防止重复打开 -const isOpening = ref(false) -``` - -### 2. 修改 open 方法 - -```typescript -const open = async (data: any, options?: { templateId?: number; stationName?: string }) => { - // 防止重复打开 - if (isOpening.value) { - console.warn('处方正在打开中,请勿重复点击') - return - } - - isOpening.value = true - - try { - // 原有逻辑... - } finally { - setTimeout(() => { - isOpening.value = false - }, 500) - } -} -``` - -### 3. 修改 openById 方法 - -```typescript -const openById = async (prescriptionId: number) => { - // 防止重复打开 - if (isOpening.value) { - console.warn('处方正在打开中,请勿重复点击') - return - } - - isOpening.value = true - - try { - const res = await prescriptionDetail({ id: prescriptionId }) - savedPrescription.value = res - if (res?.case_record && Object.keys(res.case_record).length > 0) { - await loadDictOptions() - } - visible.value = true - } catch (e) { - feedback.msgError('加载处方失败') - } finally { - setTimeout(() => { - isOpening.value = false - }, 500) - } -} -``` - -## 工作流程 - -### 修复前 - -``` -用户点击"开处方"按钮 - ↓ -调用 open() 方法 - ↓ -用户再次点击(快速连续点击) - ↓ -再次调用 open() 方法 - ↓ -两次请求同时发送 - ↓ -❌ 重复请求错误 -``` - -### 修复后 - -``` -用户点击"开处方"按钮 - ↓ -调用 open() 方法 - ↓ -设置 isOpening = true - ↓ -用户再次点击(快速连续点击) - ↓ -检查 isOpening = true - ↓ -✓ 直接返回,不执行 - ↓ -第一次请求完成 - ↓ -500ms 后重置 isOpening = false -``` - -## 测试场景 - -### 场景1:快速连续点击 - -**操作**:快速连续点击"开处方"按钮 2-3 次 - -**预期结果**: -- 只发送一次请求 -- 控制台显示警告:"处方正在打开中,请勿重复点击" -- 处方正常打开 - ---- - -### 场景2:正常点击 - -**操作**:正常点击"开处方"按钮 - -**预期结果**: -- 发送请求 -- 处方正常打开 -- 无警告信息 - ---- - -### 场景3:间隔点击 - -**操作**:点击"开处方",等待 1 秒后再次点击 - -**预期结果**: -- 第一次点击正常打开 -- 第二次点击也正常打开(因为已过 500ms) - ---- - -### 场景4:查看已有处方 - -**操作**:点击"查看病历"按钮 - -**预期结果**: -- 调用 `prescriptionGetByAppointment` 查询 -- 如果有处方,调用 `prescriptionDetail` 获取详情 -- 只发送一次 detail 请求 - ---- - -### 场景5:请求失败 - -**操作**:点击"开处方",但请求失败 - -**预期结果**: -- 显示错误提示 -- 500ms 后 `isOpening` 重置 -- 可以再次点击 - -## 其他优化建议 - -### 1. 按钮禁用状态 - -可以在按钮上添加 loading 状态: - -```vue - - {{ prescriptionActionLabel(row) }} - -``` - -但这需要将 `isOpening` 暴露给父组件,可能不太合适。 - -### 2. 防抖处理 - -可以使用 lodash 的 debounce 函数: - -```typescript -import { debounce } from 'lodash-es' - -const debouncedOpen = debounce(open, 500, { leading: true, trailing: false }) -``` - -但当前的解决方案已经足够简单有效。 - -### 3. 请求取消 - -可以使用 AbortController 取消重复请求: - -```typescript -let abortController: AbortController | null = null - -const open = async (data: any) => { - if (abortController) { - abortController.abort() - } - abortController = new AbortController() - - // 在请求中使用 signal - await fetch(url, { signal: abortController.signal }) -} -``` - -但这需要修改 API 调用层,改动较大。 - -## 注意事项 - -### 1. 延迟时间 - -当前设置为 500ms,可以根据实际情况调整: -- 太短(< 300ms):可能无法有效防止快速连续点击 -- 太长(> 1000ms):用户体验不好,需要等待较长时间 - -### 2. 控制台警告 - -`console.warn` 只在开发环境有用,生产环境可以考虑: -- 移除警告 -- 或者改为用户提示:`feedback.msgWarning('请勿重复点击')` - -### 3. 多个入口 - -如果有多个地方调用 `open()` 方法,都会受到这个锁的保护。 - -## 相关文件 - -### 修改的文件 -- `admin/src/components/tcm-prescription/index.vue` - 处方组件 - -### 调用的文件 -- `admin/src/views/tcm/appointment/list.vue` - 预约列表 -- 其他可能调用处方组件的页面 - -## 总结 - -✅ 添加 `isOpening` 标志防止重复打开 -✅ 使用 try-finally 确保标志正确重置 -✅ 延迟 500ms 重置,防止快速连续点击 -✅ 同时修复 `open()` 和 `openById()` 方法 -✅ 添加控制台警告提示 - -**效果**: -- 防止重复请求 -- 避免接口错误 -- 提升用户体验 -- 代码简单易维护 - -**下一步**: -1. 测试快速连续点击 -2. 测试正常点击流程 -3. 测试请求失败情况 -4. 考虑是否需要用户提示 diff --git a/PRESCRIPTION_FIELDS_FIX_SUMMARY.md b/PRESCRIPTION_FIELDS_FIX_SUMMARY.md deleted file mode 100644 index eb3c0f80..00000000 --- a/PRESCRIPTION_FIELDS_FIX_SUMMARY.md +++ /dev/null @@ -1,251 +0,0 @@ -# 处方字段完整修复总结 - -## 问题描述 -前端添加了新字段(用量、代煎、每天几次),但后端没有相应处理,导致编辑保存时数据丢失。 - -## 修复内容 - -### 1. 数据库字段 ✅ - -**文件**: `add_prescription_complete_fields.sql` - -```sql --- 用量相关字段 -ALTER TABLE `zyt_tcm_prescription` -ADD COLUMN IF NOT EXISTS `dosage_amount` decimal(10,2) DEFAULT NULL COMMENT '用量数值', -ADD COLUMN IF NOT EXISTS `dosage_unit` varchar(10) NOT NULL DEFAULT '' COMMENT '用量单位(g/ml)', -ADD COLUMN IF NOT EXISTS `need_decoction` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否代煎(0否1是,仅饮片)'; - --- 每天几次字段 -ALTER TABLE `zyt_tcm_prescription` -ADD COLUMN IF NOT EXISTS `times_per_day` int(11) NOT NULL DEFAULT 2 COMMENT '每天服用次数'; -``` - -### 2. 后端修复 ✅ - -#### 文件: `server/app/adminapi/logic/tcm/PrescriptionLogic.php` - -**add() 方法** - 添加处方时保存新字段: -```php -'dosage_amount' => isset($params['dosage_amount']) ? (float)$params['dosage_amount'] : null, -'dosage_unit' => $params['dosage_unit'] ?? '', -'need_decoction' => (int)($params['need_decoction'] ?? 0), -'times_per_day' => (int)($params['times_per_day'] ?? 2), -``` - -**edit() 方法** - 编辑处方时更新新字段: -```php -'dosage_amount' => isset($params['dosage_amount']) ? (float)$params['dosage_amount'] : $prescription->dosage_amount, -'dosage_unit' => $params['dosage_unit'] ?? $prescription->dosage_unit, -'need_decoction' => isset($params['need_decoction']) ? (int)$params['need_decoction'] : (int)($prescription->need_decoction ?? 0), -'times_per_day' => (int)($params['times_per_day'] ?? $prescription->times_per_day ?? 2), -``` - -### 3. 前端修复 ✅ - -#### 文件: `admin/src/views/consumer/prescription/index.vue` - -**editForm 定义** - 添加新字段: -```typescript -const editForm = reactive({ - // ... - dosage_amount: 1 as number | undefined, - dosage_unit: 'g', - need_decoction: false, - times_per_day: 2, - // ... -}) -``` - -**handleEdit() 方法** - 加载数据时填充新字段: -```typescript -editForm.dosage_amount = row.dosage_amount ?? undefined -editForm.dosage_unit = row.dosage_unit || '' -editForm.need_decoction = row.need_decoction === 1 || row.need_decoction === true -editForm.times_per_day = row.times_per_day ?? 2 -``` - -**watch 监听** - 处方类型变化时自动调整: -```typescript -watch(() => editForm.prescription_type, (newType) => { - if (newType === '浓缩水丸') { - editForm.dosage_unit = 'g' - editForm.dosage_amount = 1 - editForm.need_decoction = false - } else if (newType === '饮片') { - editForm.dosage_unit = 'ml' - editForm.dosage_amount = 50 - } else { - editForm.dosage_unit = 'g' - editForm.dosage_amount = undefined - editForm.need_decoction = false - } -}) -``` - -## 字段说明 - -| 字段名 | 类型 | 默认值 | 说明 | -|--------|------|--------|------| -| `dosage_amount` | decimal(10,2) | NULL | 用量数值 | -| `dosage_unit` | varchar(10) | '' | 用量单位 (g/ml) | -| `need_decoction` | tinyint(1) | 0 | 是否代煎 (0=否, 1=是) | -| `times_per_day` | int(11) | 2 | 每天服用次数 | - -## 数据流转 - -### 创建处方 -``` -前端表单 - ↓ -{ - prescription_type: "浓缩水丸", - dosage_amount: 5, - dosage_unit: "g", - need_decoction: false, - times_per_day: 2 -} - ↓ -PrescriptionLogic::add() - ↓ -数据库保存 -``` - -### 编辑处方 -``` -数据库读取 - ↓ -{ - dosage_amount: 5.00, - dosage_unit: "g", - need_decoction: 0, - times_per_day: 2 -} - ↓ -前端 editForm 填充 - ↓ -用户修改 - ↓ -PrescriptionLogic::edit() - ↓ -数据库更新 -``` - -## 部署步骤 - -### 1. 备份数据库 ⚠️ -```bash -mysqldump -u用户名 -p数据库名 > backup_$(date +%Y%m%d_%H%M%S).sql -``` - -### 2. 执行数据库迁移 ⚠️ 必须 -```bash -mysql -u用户名 -p数据库名 < add_prescription_complete_fields.sql -``` - -### 3. 验证字段添加 -```sql -SHOW COLUMNS FROM `zyt_tcm_prescription` LIKE 'dosage%'; -SHOW COLUMNS FROM `zyt_tcm_prescription` LIKE 'need_decoction'; -SHOW COLUMNS FROM `zyt_tcm_prescription` LIKE 'times_per_day'; -``` - -### 4. 部署后端代码 -```bash -# 上传修改的文件 -- server/app/adminapi/logic/tcm/PrescriptionLogic.php -- server/app/common/model/tcm/Prescription.php -``` - -### 5. 部署前端代码 -```bash -cd admin -npm run build -# 上传 dist/ 目录 -``` - -### 6. 清除缓存(如需要) -```bash -cd server -php think clear -``` - -## 测试清单 - -### 创建处方测试 -- [ ] 创建浓缩水丸处方,设置用量5g,每天2次 -- [ ] 创建饮片处方,设置用量150ml,代煎,每天3次 -- [ ] 创建颗粒处方,设置用量15.5g,每天2次 -- [ ] 保存后查看数据库,验证字段正确存储 - -### 编辑处方测试 -- [ ] 编辑已有处方,修改用量 -- [ ] 编辑饮片处方,修改代煎选项 -- [ ] 编辑处方,修改每天几次 -- [ ] 切换处方类型,验证用量自动调整 -- [ ] 保存后查看数据库,验证字段正确更新 - -### 数据加载测试 -- [ ] 打开编辑对话框,验证用量正确显示 -- [ ] 验证代煎选项正确显示 -- [ ] 验证每天几次正确显示 -- [ ] 验证旧数据(无新字段)不报错 - -## 修复的问题 - -### 问题 1: 用量字段不保存 ✅ -**原因**: 后端 `add()` 和 `edit()` 方法没有处理 `dosage_amount`, `dosage_unit`, `need_decoction` 字段 -**修复**: 在两个方法中添加字段处理逻辑 - -### 问题 2: 每天几次不保存 ✅ -**原因**: -1. 数据库没有 `times_per_day` 字段 -2. 后端没有处理该字段 -3. 前端 editForm 定义错误(空字符串而非数字) - -**修复**: -1. 添加数据库字段 -2. 后端添加字段处理 -3. 前端修正默认值为数字 2 - -### 问题 3: 编辑时数据不加载 ✅ -**原因**: `handleEdit()` 方法没有加载新字段 -**修复**: 在加载数据时填充所有新字段 - -## 相关文件 - -### 数据库 -- `add_prescription_complete_fields.sql` - 完整字段迁移文件 - -### 后端 -- `server/app/adminapi/logic/tcm/PrescriptionLogic.php` - 处方业务逻辑 -- `server/app/common/model/tcm/Prescription.php` - 处方模型 - -### 前端 -- `admin/src/components/tcm-prescription/index.vue` - 处方组件 -- `admin/src/views/consumer/prescription/index.vue` - 处方列表 - -### 文档 -- `PRESCRIPTION_DOSAGE_FEATURE.md` - 功能说明 -- `PRESCRIPTION_DOSAGE_DATABASE_IMPLEMENTATION.md` - 数据库实现 -- `PRESCRIPTION_DOSAGE_COMPLETE_SUMMARY.md` - 完整总结 -- `PRESCRIPTION_FIELDS_FIX_SUMMARY.md` - 本文档 - -## 注意事项 - -1. **必须先执行数据库迁移**,否则保存时会报错 -2. **旧数据兼容**:旧处方的新字段为 NULL 或默认值,不影响显示 -3. **类型转换**: - - `dosage_amount`: float - - `need_decoction`: boolean (前端) → int (后端/数据库) - - `times_per_day`: int -4. **数据验证**:前端使用下拉和数字输入框确保数据有效性 - -## 总结 - -✅ **数据库**: 4个字段已添加 -✅ **后端**: add() 和 edit() 方法已更新 -✅ **前端**: editForm 定义和数据加载已修复 -✅ **数据流转**: 完整的保存和读取流程 - -**状态**: 所有问题已修复,功能完整可用!🎉 diff --git a/PRESCRIPTION_GET_BY_APPOINTMENT_PERMISSION.md b/PRESCRIPTION_GET_BY_APPOINTMENT_PERMISSION.md deleted file mode 100644 index 37ef9c3b..00000000 --- a/PRESCRIPTION_GET_BY_APPOINTMENT_PERMISSION.md +++ /dev/null @@ -1,228 +0,0 @@ -# 预约处方查询权限控制 - -## 修改说明 - -为 `getByAppointment` 接口添加了权限检查,确保用户只能查询自己有权限查看的处方。 - -## 修改内容 - -### 1. Controller 层修改 - -**文件**: `server/app/adminapi/controller/tcm/PrescriptionController.php` - -**修改前**: -```php -public function getByAppointment() -{ - $appointmentId = (int)($this->request->get('appointment_id') ?? 0); - if (!$appointmentId) { - return $this->fail('预约ID不能为空'); - } - $prescription = PrescriptionLogic::getByAppointment($appointmentId); - return $this->data($prescription ?? []); -} -``` - -**修改后**: -```php -public function getByAppointment() -{ - $appointmentId = (int)($this->request->get('appointment_id') ?? 0); - if (!$appointmentId) { - return $this->fail('预约ID不能为空'); - } - $prescription = PrescriptionLogic::getByAppointment($appointmentId, (int)$this->adminId, $this->adminInfo); - if ($prescription === null) { - $msg = PrescriptionLogic::getError(); - return $this->fail($msg !== '' ? $msg : '未找到处方或无权限查看'); - } - return $this->data($prescription); -} -``` - -**改进点**: -- 传入当前登录用户的 `adminId` 和 `adminInfo` -- 处理权限检查失败的情况,返回明确的错误信息 -- 区分"未找到处方"和"无权限查看"两种情况 - -### 2. Logic 层修改 - -**文件**: `server/app/adminapi/logic/tcm/PrescriptionLogic.php` - -**修改前**: -```php -/** - * 根据预约ID获取处方 - */ -public static function getByAppointment(int $appointmentId): ?array -{ - $row = Prescription::where('appointment_id', $appointmentId) - ->whereNull('delete_time') - ->order('id', 'desc') - ->find(); - return $row ? $row->toArray() : null; -} -``` - -**修改后**: -```php -/** - * 根据预约ID获取处方(带权限检查) - */ -public static function getByAppointment(int $appointmentId, int $viewerAdminId, array $viewerAdminInfo): ?array -{ - self::$error = ''; - $row = Prescription::where('appointment_id', $appointmentId) - ->whereNull('delete_time') - ->order('id', 'desc') - ->find(); - - if (!$row) { - return null; - } - - // 权限检查:只返回当前用户有权限查看的处方 - if (!self::canViewPrescription($row, $viewerAdminId, $viewerAdminInfo)) { - self::setError('无权限查看此处方'); - return null; - } - - return $row->toArray(); -} -``` - -**改进点**: -- 添加 `viewerAdminId` 和 `viewerAdminInfo` 参数 -- 使用 `canViewPrescription()` 方法进行权限检查 -- 设置错误信息,便于前端显示 - -## 权限规则 - -根据 `canViewPrescription()` 方法,以下用户可以查看处方: - -1. **超级管理员** - 可以查看所有处方 -2. **处方创建人** - 可以查看自己创建的处方 -3. **医助** - 可以查看自己协助的处方 -4. **共享处方** - 所有人可以查看标记为共享的处方 -5. **可见角色** - 处方指定的可见角色可以查看 -6. **有开方权限的医生** - 拥有 `tcm.diagnosis/kaifang` 权限的医生可以查看所有处方(只读) - -## 使用场景 - -这个接口主要用于以下场景: - -1. **查看病历按钮** - 在预约列表中点击"查看病历"时调用 -2. **处方历史查询** - 根据预约ID查询患者的历史处方 -3. **医生协作** - 其他医生查看患者的处方记录(需要有开方权限) - -## 前端调用示例 - -```typescript -// admin/src/views/tcm/appointment/list.vue -const handleViewCase = async (row: any) => { - if (!row.id) { - feedback.msgWarning('预约信息不完整') - return - } - try { - const prescription = await prescriptionGetByAppointment({ appointment_id: row.id }) - if (prescription?.id) { - prescriptionRef.value?.openById(prescription.id) - } else { - feedback.msgWarning('该预约暂无病历记录,请先开方') - } - } catch (error: any) { - // 权限不足或其他错误 - feedback.msgError(error?.msg || '获取病历失败') - } -} -``` - -## 错误处理 - -### 可能的错误信息 - -1. **"预约ID不能为空"** - 未传入 `appointment_id` 参数 -2. **"未找到处方或无权限查看"** - 处方不存在或当前用户无权限查看 -3. **"无权限查看此处方"** - 处方存在但当前用户无权限查看 - -### 前端处理建议 - -```typescript -try { - const prescription = await prescriptionGetByAppointment({ appointment_id: row.id }) - // 成功获取处方 -} catch (error: any) { - if (error?.msg?.includes('无权限')) { - feedback.msgWarning('您没有权限查看此处方') - } else if (error?.msg?.includes('未找到')) { - feedback.msgWarning('该预约暂无病历记录') - } else { - feedback.msgError(error?.msg || '获取病历失败') - } -} -``` - -## 测试建议 - -### 1. 权限测试 - -**测试用例 1: 创建人查看自己的处方** -- 医生A创建处方 -- 医生A查看该处方 -- 预期:成功返回处方数据 - -**测试用例 2: 其他医生查看处方(有开方权限)** -- 医生A创建处方 -- 医生B(有 `tcm.diagnosis/kaifang` 权限)查看该处方 -- 预期:成功返回处方数据(只读) - -**测试用例 3: 其他医生查看处方(无开方权限)** -- 医生A创建处方 -- 医生C(无 `tcm.diagnosis/kaifang` 权限)查看该处方 -- 预期:返回"无权限查看此处方"错误 - -**测试用例 4: 医助查看协助的处方** -- 医生A创建处方,医助B协助 -- 医助B查看该处方 -- 预期:成功返回处方数据 - -**测试用例 5: 查看共享处方** -- 医生A创建处方并标记为共享 -- 任何用户查看该处方 -- 预期:成功返回处方数据 - -### 2. 边界测试 - -**测试用例 6: 预约不存在** -- 传入不存在的 `appointment_id` -- 预期:返回"未找到处方或无权限查看" - -**测试用例 7: 预约存在但无处方** -- 传入存在的 `appointment_id`,但该预约没有创建处方 -- 预期:返回"未找到处方或无权限查看" - -**测试用例 8: 处方已删除** -- 传入已删除处方的 `appointment_id` -- 预期:返回"未找到处方或无权限查看" - -## 安全性说明 - -1. **防止越权访问** - 用户只能查看自己有权限的处方,防止查看其他医生的私有处方 -2. **数据隔离** - 通过权限检查实现数据隔离,保护患者隐私 -3. **审计追踪** - 所有查询操作都会记录当前用户信息,便于审计 -4. **错误信息脱敏** - 对于无权限的情况,不暴露处方是否存在的详细信息 - -## 相关文档 - -- `PRESCRIPTION_VIEW_PERMISSION_FIX.md` - 处方查看权限修复文档 -- `BUG_FIXES_SUMMARY.md` - Bug修复总结文档 - -## 总结 - -通过添加权限检查,确保了: -- ✅ 用户只能查询自己有权限查看的处方 -- ✅ 防止越权访问其他医生的私有处方 -- ✅ 支持医生协作(有开方权限的医生可以查看所有处方) -- ✅ 保护患者隐私和数据安全 -- ✅ 提供明确的错误信息,便于前端处理 diff --git a/PRESCRIPTION_IMPORT_FEATURE.md b/PRESCRIPTION_IMPORT_FEATURE.md deleted file mode 100644 index a1d76328..00000000 --- a/PRESCRIPTION_IMPORT_FEATURE.md +++ /dev/null @@ -1,234 +0,0 @@ -# 处方导入功能说明 - -## 功能概述 - -在中医处方单组件中新增了"从处方库导入"功能,允许医生快速从处方库中选择已保存的处方模板,一键导入药材配方。 - -## 使用位置 - -**组件路径:** `admin/src/components/tcm-prescription/index.vue` - -**功能位置:** 中药处方 (RP) 区域,"添加中药"按钮旁边 - -## 使用方法 - -### 1. 打开处方库 - -在编辑处方时,点击"从处方库导入"按钮,会弹出处方库选择对话框。 - -### 2. 搜索处方 - -- 在搜索框中输入处方名称 -- 按回车键或点击搜索按钮进行搜索 -- 支持模糊搜索 - -### 3. 选择处方 - -有两种方式导入处方: - -**方式一:点击"导入"按钮** -- 在列表中找到需要的处方 -- 点击右侧的"导入"按钮 - -**方式二:点击行** -- 直接点击处方所在的行 -- 自动导入该处方 - -### 4. 导入结果 - -- 导入成功后,药材列表会自动填充 -- 显示成功提示:已导入处方「处方名称」,共X味药材 -- 对话框自动关闭 - -## 功能特点 - -### 1. 实时搜索 -- 支持按处方名称搜索 -- 清空搜索框自动刷新列表 - -### 2. 分页显示 -- 默认每页显示15条 -- 支持切换每页10/15/20/50条 -- 显示总记录数 - -### 3. 详细信息 -列表显示以下信息: -- 处方名称 -- 药材数量(X味) -- 药材明细(药材名称 + 剂量) -- 创建人 - -### 4. 权限控制 -- 显示自己创建的所有处方 -- 显示其他人创建的公开处方 -- 不显示其他人的私有处方 - -### 5. 数据安全 -- 深拷贝药材数据,避免引用问题 -- 导入前验证药材数据是否存在 - -## 界面示例 - -``` -┌─────────────────────────────────────────────────────┐ -│ 从处方库导入 [×] │ -├─────────────────────────────────────────────────────┤ -│ [请输入处方名称搜索________________] [搜索] │ -│ │ -│ ┌─────────────────────────────────────────────────┐ │ -│ │ 处方名称 │ 药材数量 │ 药材明细 │ 创建人 │ 操作 │ │ -│ ├─────────────────────────────────────────────────┤ │ -│ │ 六味地黄丸│ 6味 │ 熟地黄24g、山茱萸12g...│ │ -│ │ │ │ │张医生│导入│ │ -│ ├─────────────────────────────────────────────────┤ │ -│ │ 补中益气汤│ 8味 │ 黄芪15g、党参10g... │ │ -│ │ │ │ │李医生│导入│ │ -│ └─────────────────────────────────────────────────┘ │ -│ │ -│ 共2条 [10] [15] [20] [50] [<] [1] [>] │ -└─────────────────────────────────────────────────────┘ -``` - -## 技术实现 - -### 1. API接口 - -使用处方库列表接口: -```typescript -prescriptionLibraryLists({ - page_no: 1, - page_size: 15, - prescription_name: '搜索关键词', - is_public: '' -}) -``` - -### 2. 数据结构 - -处方库返回的数据格式: -```typescript -{ - lists: [ - { - id: 1, - prescription_name: '六味地黄丸', - herbs: [ - { name: '熟地黄', dosage: 24 }, - { name: '山茱萸', dosage: 12 }, - // ... - ], - creator_name: '张医生', - is_public: 1 - } - ], - count: 10 -} -``` - -### 3. 导入逻辑 - -```typescript -const handleImportLibrary = (row: any) => { - // 1. 验证数据 - if (!row.herbs || row.herbs.length === 0) { - feedback.msgWarning('该处方没有药材信息') - return - } - - // 2. 深拷贝数据 - const importedHerbs = JSON.parse(JSON.stringify(row.herbs)) - - // 3. 替换当前药材列表 - formData.herbs = importedHerbs - - // 4. 提示成功 - feedback.msgSuccess(`已导入处方「${row.prescription_name}」`) - - // 5. 关闭对话框 - showLibraryDialog.value = false -} -``` - -## 注意事项 - -### 1. 数据覆盖 -- 导入处方会**替换**当前的药材列表 -- 如果已经添加了药材,导入后会被覆盖 -- 建议在开始编辑时就导入处方 - -### 2. 数据验证 -- 导入前会验证处方是否有药材数据 -- 如果处方为空,会提示错误 - -### 3. 权限限制 -- 只能看到自己的处方和公开的处方 -- 私有处方不会显示在列表中 - -### 4. 搜索范围 -- 搜索只针对处方名称 -- 不支持按药材名称搜索 - -## 使用场景 - -### 场景1:快速开方 -医生经常开"六味地黄丸"处方,可以: -1. 点击"从处方库导入" -2. 搜索"六味地黄丸" -3. 点击导入 -4. 药材自动填充,无需手动输入 - -### 场景2:参考经典处方 -医生想参考其他医生的处方: -1. 打开处方库 -2. 浏览公开的处方 -3. 选择合适的处方导入 -4. 根据患者情况调整剂量 - -### 场景3:标准化处方 -医院推广标准处方: -1. 管理员创建标准处方并设为公开 -2. 所有医生都能看到和使用 -3. 确保处方的一致性 - -## 优势 - -1. **提高效率** - 无需手动输入药材,节省时间 -2. **减少错误** - 避免手动输入导致的错误 -3. **标准化** - 使用标准处方模板,确保质量 -4. **知识共享** - 医生之间可以分享处方经验 -5. **快速学习** - 新医生可以参考资深医生的处方 - -## 后续优化建议 - -1. **批量导入** - 支持一次导入多个处方 -2. **处方预览** - 导入前预览处方详情 -3. **智能推荐** - 根据诊断自动推荐处方 -4. **使用统计** - 显示处方使用次数 -5. **收藏功能** - 收藏常用处方,快速访问 - -## 常见问题 - -### Q1: 导入后可以修改吗? -A: 可以。导入后可以自由添加、删除、修改药材。 - -### Q2: 导入会覆盖已有的药材吗? -A: 是的。导入会替换当前所有药材,建议在开始时就导入。 - -### Q3: 为什么看不到某个处方? -A: 可能是该处方设置为私有,只有创建者可见。 - -### Q4: 可以导入后再保存到处方库吗? -A: 可以。导入、修改后,可以通过"是否共享"选项保存为新的处方模板。 - -### Q5: 搜索不到处方怎么办? -A: 检查处方名称是否正确,或清空搜索框查看所有处方。 - -## 技术支持 - -如有问题或建议,请联系技术支持团队。 - ---- - -**版本:** v1.0.0 -**更新日期:** 2026-03-22 -**状态:** ✅ 已完成 diff --git a/PRESCRIPTION_LIBRARY_DEMO.md b/PRESCRIPTION_LIBRARY_DEMO.md deleted file mode 100644 index 75fb96f4..00000000 --- a/PRESCRIPTION_LIBRARY_DEMO.md +++ /dev/null @@ -1,222 +0,0 @@ -# 处方库功能演示 - -## 功能截图说明 - -### 1. 处方库列表页面 - -列表页面展示所有可访问的处方: -- 自己创建的处方(无论是否公开) -- 其他人创建的公开处方 - -**列表字段:** -- ID:处方唯一标识 -- 处方名称:如"六味地黄丸"、"补中益气汤" -- 药材数量:显示该处方包含多少味药材 -- 药材明细:显示所有药材及其剂量 -- 是否公开:标签显示"所有人可见"或"仅自己可见" -- 创建人:显示创建者姓名 -- 创建时间:处方创建的时间 - -**操作按钮:** -- 查看:查看处方详细信息(只读模式) -- 编辑:修改处方信息(需要权限) -- 删除:删除处方(仅创建者可操作) - -### 2. 搜索筛选功能 - -**搜索条件:** -- 处方名称:支持模糊搜索 -- 是否公开:可筛选"全部"、"仅自己可见"、"所有人可见" - -### 3. 新增/编辑处方弹窗 - -**表单字段:** - -1. **处方名称**(必填) - - 输入框,最多100个字符 - - 示例:六味地黄丸、补中益气汤、逍遥散 - -2. **药材配方**(必填) - - 动态表格,可添加多味药材 - - 每味药材包含: - - 药材名称(文本输入) - - 剂量/克(数字输入,支持小数) - - 支持添加和删除药材行 - -3. **是否公开** - - 开关按钮 - - 关闭:仅自己可见 - - 开启:所有人可见 - - 提示文字:勾选后,所有医生都可以查看和使用此处方模板 - -### 4. 查看处方(只读模式) - -点击"查看"按钮后,以只读模式展示处方详情: -- 所有字段不可编辑 -- 不显示"添加药材"和"删除"按钮 -- 底部不显示"确定"按钮 - -## 使用场景示例 - -### 场景1:创建常用处方模板 - -**背景:** -张医生经常开"六味地黄丸"处方,每次都要重新输入药材和剂量,比较麻烦。 - -**操作步骤:** -1. 进入处方库页面 -2. 点击"新增处方" -3. 输入处方名称:六味地黄丸 -4. 添加6味药材: - - 熟地黄 24g - - 山茱萸 12g - - 山药 12g - - 泽泻 9g - - 牡丹皮 9g - - 茯苓 9g -5. 设置为"仅自己可见" -6. 保存 - -**效果:** -以后张医生可以随时查看这个处方模板,快速参考药材配方。 - -### 场景2:分享经典处方 - -**背景:** -李主任想把一些经典处方分享给科室的其他医生使用。 - -**操作步骤:** -1. 创建处方时,将"是否公开"设置为"所有人可见" -2. 保存后,科室所有医生都能在处方库中看到这个处方 - -**效果:** -- 其他医生可以查看和参考这个处方 -- 但只有李主任可以修改或删除 -- 实现了知识共享 - -### 场景3:管理个人处方库 - -**背景:** -王医生积累了很多个人处方,需要分类管理。 - -**操作步骤:** -1. 使用搜索功能快速查找处方 -2. 通过"是否公开"筛选个人处方和公开处方 -3. 编辑或删除不再使用的处方 - -## 权限说明 - -### 查看权限 -- ✅ 可以查看自己创建的所有处方(无论是否公开) -- ✅ 可以查看其他人创建的公开处方 -- ❌ 不能查看其他人创建的私有处方 - -### 编辑权限 -- ✅ 可以编辑自己创建的处方 -- ✅ 可以编辑其他人创建的公开处方(协作编辑) -- ❌ 不能编辑其他人创建的私有处方 - -### 删除权限 -- ✅ 只能删除自己创建的处方 -- ❌ 不能删除其他人创建的处方(即使是公开的) - -## 数据示例 - -### 示例1:六味地黄丸 - -```json -{ - "prescription_name": "六味地黄丸", - "herbs": [ - {"name": "熟地黄", "dosage": 24}, - {"name": "山茱萸", "dosage": 12}, - {"name": "山药", "dosage": 12}, - {"name": "泽泻", "dosage": 9}, - {"name": "牡丹皮", "dosage": 9}, - {"name": "茯苓", "dosage": 9} - ], - "is_public": 1 -} -``` - -### 示例2:补中益气汤 - -```json -{ - "prescription_name": "补中益气汤", - "herbs": [ - {"name": "黄芪", "dosage": 15}, - {"name": "党参", "dosage": 10}, - {"name": "白术", "dosage": 10}, - {"name": "当归", "dosage": 10}, - {"name": "陈皮", "dosage": 6}, - {"name": "升麻", "dosage": 6}, - {"name": "柴胡", "dosage": 6}, - {"name": "甘草", "dosage": 5} - ], - "is_public": 1 -} -``` - -### 示例3:逍遥散 - -```json -{ - "prescription_name": "逍遥散", - "herbs": [ - {"name": "柴胡", "dosage": 10}, - {"name": "当归", "dosage": 10}, - {"name": "白芍", "dosage": 10}, - {"name": "白术", "dosage": 10}, - {"name": "茯苓", "dosage": 10}, - {"name": "薄荷", "dosage": 6}, - {"name": "生姜", "dosage": 6}, - {"name": "甘草", "dosage": 5} - ], - "is_public": 0 -} -``` - -## 技术特点 - -1. **独立性** - - 不依赖诊单、预约等其他功能 - - 可以单独使用和维护 - -2. **灵活性** - - 支持任意数量的药材 - - 剂量支持小数(精确到0.1克) - -3. **安全性** - - 完善的权限控制 - - 防止未授权访问和操作 - -4. **易用性** - - 简洁的界面设计 - - 直观的操作流程 - -## 未来扩展建议 - -1. **处方分类** - - 添加处方分类功能(如:补益类、清热类等) - - 支持按分类筛选 - -2. **处方标签** - - 支持为处方添加标签 - - 支持按标签搜索 - -3. **使用统计** - - 记录处方使用次数 - - 显示热门处方排行 - -4. **处方导入导出** - - 支持批量导入处方 - - 支持导出为Excel或PDF - -5. **处方评论** - - 允许医生对公开处方进行评论 - - 分享使用心得 - -6. **版本管理** - - 记录处方修改历史 - - 支持回退到历史版本 diff --git a/PRESCRIPTION_LIBRARY_FILES.md b/PRESCRIPTION_LIBRARY_FILES.md deleted file mode 100644 index 28c949f0..00000000 --- a/PRESCRIPTION_LIBRARY_FILES.md +++ /dev/null @@ -1,244 +0,0 @@ -# 处方库功能 - 文件清单 - -## 📁 所有文件列表 - -### 📄 文档文件(根目录) - -| 文件名 | 说明 | 用途 | -|--------|------|------| -| PRESCRIPTION_LIBRARY_README.md | 完整使用说明 | 详细的功能说明和使用指南 | -| PRESCRIPTION_LIBRARY_DEMO.md | 功能演示文档 | 使用场景和示例 | -| PRESCRIPTION_LIBRARY_SUMMARY.md | 开发总结 | 技术实现和开发总结 | -| QUICK_START_PRESCRIPTION_LIBRARY.md | 快速开始指南 | 5分钟快速上手 | -| PRESCRIPTION_LIBRARY_FILES.md | 本文件 | 文件清单 | -| TEST_PRESCRIPTION_LIBRARY.sql | 测试SQL | 测试数据和查询语句 | - -### 🔧 安装脚本(根目录) - -| 文件名 | 说明 | 平台 | -|--------|------|------| -| install_prescription_library.bat | Windows安装脚本 | Windows | -| install_prescription_library.sh | Linux/Mac安装脚本 | Linux/Mac | - -### 🗄️ 数据库文件 - -| 文件路径 | 说明 | -|----------|------| -| server/database/migrations/create_prescription_library.sql | 数据库表结构 | - -### 🔙 后端文件 - -#### 模型层 -| 文件路径 | 说明 | -|----------|------| -| server/app/common/model/tcm/PrescriptionLibrary.php | 处方库数据模型 | - -#### 控制器层 -| 文件路径 | 说明 | -|----------|------| -| server/app/adminapi/controller/tcm/PrescriptionLibraryController.php | 处方库控制器 | - -#### 业务逻辑层 -| 文件路径 | 说明 | -|----------|------| -| server/app/adminapi/logic/tcm/PrescriptionLibraryLogic.php | 处方库业务逻辑 | - -#### 列表逻辑层 -| 文件路径 | 说明 | -|----------|------| -| server/app/adminapi/lists/tcm/PrescriptionLibraryLists.php | 处方库列表逻辑 | - -#### 验证器层 -| 文件路径 | 说明 | -|----------|------| -| server/app/adminapi/validate/tcm/PrescriptionLibraryValidate.php | 处方库数据验证器 | - -### 🎨 前端文件 - -#### 页面组件 -| 文件路径 | 说明 | -|----------|------| -| admin/src/views/consumer/prescription/list.vue | 处方库页面组件 | - -#### API接口 -| 文件路径 | 说明 | 修改内容 | -|----------|------|----------| -| admin/src/api/tcm.ts | API接口定义 | 新增处方库相关接口 | - -## 📊 文件统计 - -### 按类型统计 -- 文档文件:6个 -- 安装脚本:2个 -- 数据库文件:1个 -- 后端PHP文件:5个 -- 前端Vue文件:1个 -- 前端API文件:1个(修改) - -**总计:** 16个文件(15个新建 + 1个修改) - -### 按目录统计 -``` -根目录/ -├── 文档文件 × 6 -├── 安装脚本 × 2 -└── 测试SQL × 1 - -server/ -├── database/migrations/ × 1 -├── app/common/model/tcm/ × 1 -└── app/adminapi/ - ├── controller/tcm/ × 1 - ├── logic/tcm/ × 1 - ├── lists/tcm/ × 1 - └── validate/tcm/ × 1 - -admin/ -└── src/ - ├── views/consumer/prescription/ × 1 - └── api/ × 1(修改) -``` - -## 🔍 文件依赖关系 - -``` -前端页面 (list.vue) - ↓ -前端API (tcm.ts) - ↓ -后端控制器 (PrescriptionLibraryController.php) - ↓ -后端验证器 (PrescriptionLibraryValidate.php) - ↓ -后端业务逻辑 (PrescriptionLibraryLogic.php) - ↓ -后端数据模型 (PrescriptionLibrary.php) - ↓ -数据库表 (zyt_prescription_library) -``` - -## 📝 代码行数统计 - -| 文件类型 | 文件数 | 预估代码行数 | -|---------|--------|-------------| -| 文档 (Markdown) | 6 | ~1500行 | -| SQL | 2 | ~100行 | -| PHP | 5 | ~400行 | -| Vue | 1 | ~350行 | -| TypeScript | 1 | ~30行(新增部分) | -| Shell脚本 | 2 | ~100行 | -| **总计** | **17** | **~2480行** | - -## ✅ 文件完整性检查 - -### 必需文件检查清单 - -#### 后端文件 -- [x] 数据库表结构文件 -- [x] 数据模型文件 -- [x] 控制器文件 -- [x] 业务逻辑文件 -- [x] 列表逻辑文件 -- [x] 验证器文件 - -#### 前端文件 -- [x] 页面组件文件 -- [x] API接口文件(已更新) - -#### 文档文件 -- [x] 使用说明文档 -- [x] 功能演示文档 -- [x] 开发总结文档 -- [x] 快速开始文档 -- [x] 文件清单文档 -- [x] 测试SQL文件 - -#### 安装文件 -- [x] Windows安装脚本 -- [x] Linux/Mac安装脚本 - -**结果:** ✅ 所有必需文件已创建完成 - -## 🎯 下一步操作 - -1. **安装数据库表** - - 运行安装脚本或手动执行SQL - -2. **配置菜单权限** - - 在后台管理系统中添加菜单项 - - 配置访问权限 - -3. **测试功能** - - 创建测试处方 - - 验证各项功能 - -4. **部署上线** - - 备份数据库 - - 部署代码 - - 验证生产环境 - -## 📦 打包清单 - -如需打包交付,建议包含以下文件: - -### 核心文件(必需) -``` -✅ server/database/migrations/create_prescription_library.sql -✅ server/app/common/model/tcm/PrescriptionLibrary.php -✅ server/app/adminapi/controller/tcm/PrescriptionLibraryController.php -✅ server/app/adminapi/logic/tcm/PrescriptionLibraryLogic.php -✅ server/app/adminapi/lists/tcm/PrescriptionLibraryLists.php -✅ server/app/adminapi/validate/tcm/PrescriptionLibraryValidate.php -✅ admin/src/views/consumer/prescription/list.vue -✅ admin/src/api/tcm.ts(修改部分) -``` - -### 文档文件(推荐) -``` -✅ PRESCRIPTION_LIBRARY_README.md -✅ QUICK_START_PRESCRIPTION_LIBRARY.md -✅ PRESCRIPTION_LIBRARY_DEMO.md -✅ PRESCRIPTION_LIBRARY_SUMMARY.md -``` - -### 安装文件(推荐) -``` -✅ install_prescription_library.bat -✅ install_prescription_library.sh -``` - -### 测试文件(可选) -``` -✅ TEST_PRESCRIPTION_LIBRARY.sql -``` - -## 🔐 文件权限建议 - -### Linux/Mac 系统 -```bash -# 安装脚本 -chmod +x install_prescription_library.sh - -# PHP文件 -chmod 644 server/app/**/*.php - -# Vue文件 -chmod 644 admin/src/**/*.vue - -# SQL文件 -chmod 644 server/database/migrations/*.sql -``` - -### Windows 系统 -默认权限即可,无需特殊设置。 - -## 📌 版本信息 - -- **版本号:** v1.0.0 -- **创建日期:** 2026-03-22 -- **最后更新:** 2026-03-22 -- **状态:** ✅ 已完成 - ---- - -**所有文件已创建完成,可以开始安装和使用!** 🎉 diff --git a/PRESCRIPTION_LIBRARY_README.md b/PRESCRIPTION_LIBRARY_README.md deleted file mode 100644 index 9bb72300..00000000 --- a/PRESCRIPTION_LIBRARY_README.md +++ /dev/null @@ -1,180 +0,0 @@ -# 处方库功能说明 - -## 功能概述 - -处方库是一个独立的处方模板管理系统,允许医生创建、管理和分享常用的中药处方配方。 - -## 主要特性 - -1. **处方管理** - - 创建处方模板,包含处方名称和药材配方 - - 每味药材包含名称和剂量(克) - - 支持编辑和删除处方 - -2. **权限控制** - - 仅自己可见:只有创建者可以查看和使用 - - 所有人可见:所有医生都可以查看和使用(但只有创建者可以删除) - -3. **独立功能** - - 不依赖其他功能模块 - - 不与诊单、预约等功能关联 - - 纯粹的处方模板库 - -## 安装步骤 - -### Windows 系统 - -1. 双击运行 `install_prescription_library.bat` -2. 按提示输入数据库配置信息 -3. 等待安装完成 - -### Linux/Mac 系统 - -1. 给脚本添加执行权限: - ```bash - chmod +x install_prescription_library.sh - ``` - -2. 运行安装脚本: - ```bash - ./install_prescription_library.sh - ``` - -3. 按提示输入数据库配置信息 - -### 手动安装 - -如果自动安装脚本无法运行,可以手动执行以下步骤: - -1. 使用数据库管理工具(如 phpMyAdmin、Navicat 等) -2. 打开 `server/database/migrations/create_prescription_library.sql` 文件 -3. 执行其中的 SQL 语句 - -## 数据库表结构 - -### zyt_prescription_library(处方库表) - -| 字段名 | 类型 | 说明 | -|--------|------|------| -| id | int | 主键ID | -| prescription_name | varchar(100) | 处方名称 | -| herbs | text | 药材列表JSON | -| is_public | tinyint(1) | 是否公开:0-仅自己可见 1-所有人可见 | -| creator_id | int | 创建人ID | -| creator_name | varchar(50) | 创建人姓名 | -| create_time | int | 创建时间 | -| update_time | int | 更新时间 | -| delete_time | int | 删除时间 | - -## 使用说明 - -### 创建处方 - -1. 点击"新增处方"按钮 -2. 输入处方名称(如:六味地黄丸、补中益气汤) -3. 点击"添加药材"按钮添加药材 -4. 输入药材名称和剂量(克) -5. 选择是否公开 -6. 点击"确定"保存 - -### 编辑处方 - -1. 在列表中找到要编辑的处方 -2. 点击"编辑"按钮 -3. 修改处方信息 -4. 点击"确定"保存 - -### 删除处方 - -1. 在列表中找到要删除的处方 -2. 点击"删除"按钮 -3. 确认删除操作 - -注意:只有创建者才能删除处方 - -### 查看处方 - -1. 在列表中找到要查看的处方 -2. 点击"查看"按钮 -3. 查看处方详细信息 - -## API 接口 - -### 后端路由 - -所有接口都在 `tcm.prescriptionLibrary` 控制器下: - -- `GET /tcm.prescriptionLibrary/lists` - 获取处方库列表 -- `POST /tcm.prescriptionLibrary/add` - 添加处方 -- `POST /tcm.prescriptionLibrary/edit` - 编辑处方 -- `POST /tcm.prescriptionLibrary/delete` - 删除处方 -- `GET /tcm.prescriptionLibrary/detail` - 获取处方详情 - -### 前端 API - -在 `admin/src/api/tcm.ts` 中: - -```typescript -// 处方库列表 -prescriptionLibraryLists(params) - -// 添加处方库 -prescriptionLibraryAdd(params) - -// 编辑处方库 -prescriptionLibraryEdit(params) - -// 删除处方库 -prescriptionLibraryDelete({ id }) - -// 处方库详情 -prescriptionLibraryDetail({ id }) -``` - -## 文件清单 - -### 后端文件 - -- `server/database/migrations/create_prescription_library.sql` - 数据库表结构 -- `server/app/common/model/tcm/PrescriptionLibrary.php` - 数据模型 -- `server/app/adminapi/controller/tcm/PrescriptionLibraryController.php` - 控制器 -- `server/app/adminapi/logic/tcm/PrescriptionLibraryLogic.php` - 业务逻辑 -- `server/app/adminapi/lists/tcm/PrescriptionLibraryLists.php` - 列表逻辑 -- `server/app/adminapi/validate/tcm/PrescriptionLibraryValidate.php` - 验证器 - -### 前端文件 - -- `admin/src/views/consumer/prescription/list.vue` - 处方库页面 -- `admin/src/api/tcm.ts` - API 接口(已更新) - -### 安装文件 - -- `install_prescription_library.sh` - Linux/Mac 安装脚本 -- `install_prescription_library.bat` - Windows 安装脚本 -- `PRESCRIPTION_LIBRARY_README.md` - 本说明文档 - -## 注意事项 - -1. 处方库功能完全独立,不影响现有的处方管理功能 -2. 药材数据以 JSON 格式存储在数据库中 -3. 权限控制确保只有创建者或公开的处方才能被访问 -4. 删除操作只有创建者才能执行 -5. 建议定期备份数据库 - -## 常见问题 - -### Q: 安装脚本执行失败? -A: 请检查: -- MySQL 是否已安装并正在运行 -- 数据库配置信息是否正确 -- 是否有足够的数据库权限 - -### Q: 无法看到其他人创建的处方? -A: 只有设置为"所有人可见"的处方才能被其他医生查看 - -### Q: 可以删除别人创建的处方吗? -A: 不可以,只有创建者才能删除自己创建的处方 - -## 技术支持 - -如有问题,请联系技术支持团队。 diff --git a/PRESCRIPTION_LIBRARY_SUMMARY.md b/PRESCRIPTION_LIBRARY_SUMMARY.md deleted file mode 100644 index ac4a187a..00000000 --- a/PRESCRIPTION_LIBRARY_SUMMARY.md +++ /dev/null @@ -1,236 +0,0 @@ -# 处方库功能开发总结 - -## 项目概述 - -已完成一个独立的处方库管理系统,用于管理中药处方模板。该功能完全独立,不与其他业务模块关联。 - -## 核心功能 - -### 1. 处方管理 -- ✅ 创建处方(处方名称 + 药材配方) -- ✅ 编辑处方 -- ✅ 删除处方 -- ✅ 查看处方详情 -- ✅ 处方列表展示 - -### 2. 药材管理 -- ✅ 动态添加药材 -- ✅ 设置药材名称和剂量(克) -- ✅ 删除药材 -- ✅ 支持小数剂量(精确到0.1克) - -### 3. 权限控制 -- ✅ 是否公开设置(仅自己可见/所有人可见) -- ✅ 查看权限控制 -- ✅ 编辑权限控制 -- ✅ 删除权限控制(仅创建者) - -### 4. 搜索筛选 -- ✅ 按处方名称搜索(模糊匹配) -- ✅ 按是否公开筛选 -- ✅ 分页显示 - -## 技术实现 - -### 后端(PHP + ThinkPHP) - -#### 数据库表 -- **表名:** `zyt_prescription_library` -- **字段:** - - id:主键 - - prescription_name:处方名称 - - herbs:药材列表(JSON格式) - - is_public:是否公开(0-私有,1-公开) - - creator_id:创建人ID - - creator_name:创建人姓名 - - create_time:创建时间 - - update_time:更新时间 - - delete_time:删除时间(软删除) - -#### 文件结构 -``` -server/ -├── database/migrations/ -│ └── create_prescription_library.sql # 数据库表结构 -├── app/common/model/tcm/ -│ └── PrescriptionLibrary.php # 数据模型 -├── app/adminapi/ -│ ├── controller/tcm/ -│ │ └── PrescriptionLibraryController.php # 控制器 -│ ├── logic/tcm/ -│ │ └── PrescriptionLibraryLogic.php # 业务逻辑 -│ ├── lists/tcm/ -│ │ └── PrescriptionLibraryLists.php # 列表逻辑 -│ └── validate/tcm/ -│ └── PrescriptionLibraryValidate.php # 数据验证 -``` - -#### API接口 -- `GET /tcm.prescriptionLibrary/lists` - 获取列表 -- `POST /tcm.prescriptionLibrary/add` - 添加处方 -- `POST /tcm.prescriptionLibrary/edit` - 编辑处方 -- `POST /tcm.prescriptionLibrary/delete` - 删除处方 -- `GET /tcm.prescriptionLibrary/detail` - 获取详情 - -### 前端(Vue 3 + Element Plus) - -#### 文件结构 -``` -admin/ -├── src/ -│ ├── views/consumer/prescription/ -│ │ └── list.vue # 处方库页面 -│ └── api/ -│ └── tcm.ts # API接口(已更新) -``` - -#### 主要组件 -- 搜索表单 -- 数据表格 -- 新增/编辑弹窗 -- 药材动态表格 - -## 安装部署 - -### 自动安装 -- **Windows:** `install_prescription_library.bat` -- **Linux/Mac:** `install_prescription_library.sh` - -### 手动安装 -执行 SQL 文件:`server/database/migrations/create_prescription_library.sql` - -## 文档清单 - -1. **PRESCRIPTION_LIBRARY_README.md** - 完整使用说明 -2. **PRESCRIPTION_LIBRARY_DEMO.md** - 功能演示和使用场景 -3. **PRESCRIPTION_LIBRARY_SUMMARY.md** - 本文档(开发总结) -4. **TEST_PRESCRIPTION_LIBRARY.sql** - 测试SQL语句 - -## 代码质量 - -### 后端 -- ✅ 遵循 PSR 编码规范 -- ✅ 完整的异常处理 -- ✅ 数据验证 -- ✅ 权限控制 -- ✅ 软删除支持 -- ✅ 无语法错误 - -### 前端 -- ✅ Vue 3 Composition API -- ✅ TypeScript 类型定义 -- ✅ 响应式设计 -- ✅ 表单验证 -- ✅ 用户友好的提示 -- ✅ 无语法错误 - -## 特色亮点 - -1. **完全独立** - - 不依赖其他业务模块 - - 可独立部署和维护 - -2. **权限灵活** - - 支持私有和公开两种模式 - - 精细的权限控制 - -3. **数据安全** - - 软删除机制 - - 权限验证 - - 数据验证 - -4. **用户体验** - - 直观的界面 - - 流畅的操作 - - 友好的提示 - -5. **扩展性强** - - 清晰的代码结构 - - 易于扩展新功能 - -## 测试建议 - -### 功能测试 -1. ✅ 创建处方(私有) -2. ✅ 创建处方(公开) -3. ✅ 编辑自己的处方 -4. ✅ 编辑公开的处方 -5. ✅ 删除自己的处方 -6. ✅ 尝试删除他人的处方(应失败) -7. ✅ 查看处方列表 -8. ✅ 搜索处方 -9. ✅ 筛选处方 - -### 权限测试 -1. ✅ 用户A创建私有处方,用户B不能查看 -2. ✅ 用户A创建公开处方,用户B可以查看 -3. ✅ 用户A创建处方,用户B不能删除 -4. ✅ 用户A创建公开处方,用户B可以编辑 - -### 数据验证测试 -1. ✅ 处方名称为空(应提示错误) -2. ✅ 药材列表为空(应提示错误) -3. ✅ 药材名称为空(应提示错误) -4. ✅ 药材剂量为0或负数(应提示错误) - -## 性能优化建议 - -1. **数据库索引** - - ✅ 已添加 creator_id 索引 - - ✅ 已添加 is_public 索引 - - ✅ 已添加 create_time 索引 - -2. **查询优化** - - ✅ 使用字段筛选,避免 SELECT * - - ✅ 分页查询 - - ✅ 软删除过滤 - -3. **前端优化** - - ✅ 按需加载 - - ✅ 防抖处理(搜索) - - ✅ 加载状态提示 - -## 后续优化方向 - -### 短期(1-2周) -1. 添加处方分类功能 -2. 添加处方标签 -3. 优化搜索功能(支持药材名称搜索) - -### 中期(1-2月) -1. 添加使用统计 -2. 添加热门处方排行 -3. 支持处方导入导出 - -### 长期(3-6月) -1. 添加处方评论功能 -2. 添加版本管理 -3. 添加处方推荐算法 -4. 移动端适配 - -## 维护说明 - -### 数据备份 -建议定期备份 `zyt_prescription_library` 表数据。 - -### 日志监控 -关注以下日志: -- 处方创建失败日志 -- 权限验证失败日志 -- 数据验证失败日志 - -### 性能监控 -关注以下指标: -- 列表查询响应时间 -- 数据库连接数 -- 并发用户数 - -## 联系方式 - -如有问题或建议,请联系开发团队。 - ---- - -**开发完成时间:** 2026-03-22 -**版本:** v1.0.0 -**状态:** ✅ 已完成并测试通过 diff --git a/PRESCRIPTION_MANAGEMENT.md b/PRESCRIPTION_MANAGEMENT.md deleted file mode 100644 index e4882e5f..00000000 --- a/PRESCRIPTION_MANAGEMENT.md +++ /dev/null @@ -1,126 +0,0 @@ -# 处方管理功能说明 - -## 功能概述 - -医生处方管理功能,支持创建、编辑、查看和删除处方,并支持处方共享功能。 - -## 主要功能 - -### 1. 处方列表 -- 查看所有处方(包括自己创建的和共享的) -- 按处方名称、患者姓名搜索 -- 按是否共享筛选 -- 显示处方基本信息:编号、名称、患者信息、药材数量、剂数、共享状态等 - -### 2. 新增处方 -- 处方名称:必填,用于标识处方 -- 患者信息:姓名、性别、年龄 -- 处方日期:默认当天 -- 临床诊断:必填,描述病情 -- 药材配方: - - 支持添加多味药材 - - 每味药材包含:名称、剂量(克) - - 可动态增删药材 -- 剂数和单位:默认7剂,支持多种单位(剂、丸、袋、盒等) -- 服用天数:默认7天,可自定义 -- 用法说明:默认"水煎服,一日二次" -- 服用说明:详细的服用注意事项,如饭前/饭后服用、忌口等 -- 医师姓名:必填 -- 是否共享: - - 不勾选(默认):仅自己可见 - - 勾选:所有医生都可以查看和使用 - -### 3. 编辑处方 -- 可编辑处方的所有信息 -- 权限控制: - - 创建者可以编辑自己的处方 - - 共享的处方所有人都可以编辑 - -### 4. 查看处方 -- 查看处方的详细信息 -- 只读模式,不可编辑 - -### 5. 删除处方 -- 删除不需要的处方 -- 需要确认操作 - -## 共享功能说明 - -### 共享规则 -1. **不共享(默认)**: - - 只有创建者自己可以看到 - - 其他医生无法查看和使用 - - 适用于个人专用处方 - -2. **共享**: - - 所有医生都可以查看 - - 所有医生都可以编辑 - - 适用于常用处方模板、标准处方等 - -### 使用场景 -- **个人处方**:医生为特定患者开具的处方,不需要共享 -- **处方模板**:常用的标准处方,可以共享给所有医生使用 -- **经验处方**:经过验证的有效处方,共享给团队学习和使用 - -## 安装步骤 - -### 1. 执行数据库迁移 - -**Windows系统:** -```bash -install_prescription_shared.bat -``` - -**Linux/Mac系统:** -```bash -chmod +x install_prescription_shared.sh -./install_prescription_shared.sh -``` - -### 2. 访问功能 -登录管理后台,进入"消费者管理" -> "处方管理" - -## 技术实现 - -### 前端 -- 文件位置:`admin/src/views/consumer/prescription/index.vue` -- 使用 Vue 3 + Element Plus -- 支持表格展示、表单编辑、弹窗操作 - -### 后端 -- 控制器:`server/app/adminapi/controller/tcm/PrescriptionController.php` -- 逻辑层:`server/app/adminapi/logic/tcm/PrescriptionLogic.php` -- 列表类:`server/app/adminapi/lists/tcm/PrescriptionLists.php` -- 验证器:`server/app/adminapi/validate/tcm/PrescriptionValidate.php` -- 模型:`server/app/common/model/tcm/Prescription.php` - -### API接口 -- `GET /tcm.prescription/lists` - 处方列表 -- `POST /tcm.prescription/add` - 添加处方 -- `POST /tcm.prescription/edit` - 编辑处方 -- `POST /tcm.prescription/delete` - 删除处方 -- `GET /tcm.prescription/detail` - 处方详情 - -### 数据库字段 -新增字段: -- `prescription_name` - 处方名称(varchar 100) -- `is_shared` - 是否共享(tinyint 1,0-不共享,1-共享) -- `usage_days` - 服用天数(int 11,默认7天) -- `usage_method` - 服用说明(varchar 200) - -## 注意事项 - -1. 处方名称和临床诊断为必填项 -2. 至少需要添加一味药材 -3. 药材剂量必须大于0 -4. 共享的处方所有人都可以编辑,请谨慎操作 -5. 删除操作不可恢复,请确认后再删除 - -## 后续优化建议 - -1. 添加处方模板功能,快速创建常用处方 -2. 支持处方打印和导出 -3. 添加处方审核流程 -4. 支持处方版本管理 -5. 添加处方使用统计 -6. 支持处方收藏功能 diff --git a/PRESCRIPTION_ORDER_MIN_AMOUNT_VALIDATION.md b/PRESCRIPTION_ORDER_MIN_AMOUNT_VALIDATION.md deleted file mode 100644 index 1745c131..00000000 --- a/PRESCRIPTION_ORDER_MIN_AMOUNT_VALIDATION.md +++ /dev/null @@ -1,318 +0,0 @@ -# 处方订单最小金额验证优化 - -## 需求说明 - -在创建处方订单时,如果关联的支付单总金额小于配置的最小金额(`PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT`),则不允许创建订单。 - -## 配置说明 - -**配置文件**:`server/.env` - -```env -PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT="100" -``` - -**说明**: -- 单位:元(人民币) -- 默认值:0(不校验) -- 当前配置:100元 - -## 验证规则 - -### 修改前(原逻辑) - -``` -1. 订单金额 >= 最小金额 ✓ -2. 每个关联支付单金额 >= 最小金额 ✓ -``` - -**问题**: -- 要求每个支付单都必须 >= 100元 -- 不允许多个小额支付单组合(如 60元 + 50元 = 110元) - -### 修改后(新逻辑) - -``` -1. 订单金额 >= 最小金额 ✓ -2. 必须关联至少一个支付单 ✓ -3. 关联支付单总金额 >= 最小金额 ✓ -``` - -**优势**: -- 允许多个小额支付单组合 -- 更灵活的支付方式 -- 总金额达标即可 - -## 代码修改 - -**文件**:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -**方法**:`assertDepositAndLinkPayRules()` - -### 修改内容 - -```php -public static function assertDepositAndLinkPayRules(float $orderAmount, array $payOrderIds): ?string -{ - $min = self::depositMinAmount(); - if ($min <= 0) { - return null; - } - - // 1. 检查订单金额 - if ($orderAmount < $min) { - return '订单金额须大于等于定金门槛 ¥' . $min . '(当前 ¥' . $orderAmount . ')'; - } - - // 2. 检查是否关联支付单 - if ($payOrderIds === []) { - return '未关联支付单,关联支付单总金额须大于等于定金门槛 ¥' . $min; - } - - // 3. 检查关联支付单的总金额 - $rows = Order::whereIn('id', $payOrderIds)->whereNull('delete_time')->column('amount', 'id'); - $totalAmount = 0.0; - foreach ($payOrderIds as $pid) { - $pid = (int) $pid; - if ($pid <= 0) { - continue; - } - $amt = round((float) ($rows[$pid] ?? 0), 2); - $totalAmount += $amt; - } - - if ($totalAmount < $min) { - return '关联支付单总金额须大于等于定金门槛 ¥' . $min . '(当前总金额 ¥' . $totalAmount . ')'; - } - - return null; -} -``` - -## 验证场景 - -### 场景1:订单金额不足 - -**配置**:最小金额 = 100元 - -**输入**: -- 订单金额:80元 -- 关联支付单:[100元] - -**结果**:❌ 失败 -**错误信息**:订单金额须大于等于定金门槛 ¥100(当前 ¥80) - ---- - -### 场景2:未关联支付单 - -**配置**:最小金额 = 100元 - -**输入**: -- 订单金额:150元 -- 关联支付单:[] - -**结果**:❌ 失败 -**错误信息**:未关联支付单,关联支付单总金额须大于等于定金门槛 ¥100 - ---- - -### 场景3:关联支付单总金额不足 - -**配置**:最小金额 = 100元 - -**输入**: -- 订单金额:150元 -- 关联支付单:[30元, 40元, 20元] -- 总金额:90元 - -**结果**:❌ 失败 -**错误信息**:关联支付单总金额须大于等于定金门槛 ¥100(当前总金额 ¥90) - ---- - -### 场景4:多个小额支付单组合达标 - -**配置**:最小金额 = 100元 - -**输入**: -- 订单金额:150元 -- 关联支付单:[60元, 50元] -- 总金额:110元 - -**结果**:✅ 成功 -**说明**:虽然单个支付单都小于100元,但总金额达标 - ---- - -### 场景5:单个大额支付单 - -**配置**:最小金额 = 100元 - -**输入**: -- 订单金额:150元 -- 关联支付单:[120元] -- 总金额:120元 - -**结果**:✅ 成功 - ---- - -### 场景6:配置为0(不校验) - -**配置**:最小金额 = 0元 - -**输入**: -- 订单金额:10元 -- 关联支付单:[] - -**结果**:✅ 成功 -**说明**:最小金额为0时,不进行任何校验 - ---- - -### 场景7:多个支付单,总金额刚好达标 - -**配置**:最小金额 = 100元 - -**输入**: -- 订单金额:150元 -- 关联支付单:[30元, 30元, 40元] -- 总金额:100元 - -**结果**:✅ 成功 -**说明**:总金额刚好等于最小金额,符合"大于等于"的要求 - -## 前端提示优化 - -**文件**:`admin/src/views/consumer/prescription/order_list.vue` - -**当前提示**: -``` -支付单审核时可对照此处关联的收款记录。 -列表仅展示金额大于等于定金门槛的收款单(门槛为 0 时展示全部)。 -``` - -**建议修改为**: -``` -支付单审核时可对照此处关联的收款记录。 -关联支付单总金额须大于等于定金门槛(当前门槛:¥100)。 -列表仅展示金额大于等于定金门槛的收款单(门槛为 0 时展示全部)。 -``` - -## 配置管理 - -### 查看当前配置 - -```bash -# 查看 .env 文件 -cat server/.env | grep PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT -``` - -### 修改配置 - -```bash -# 编辑 .env 文件 -vim server/.env - -# 修改为 200 元 -PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT="200" - -# 修改为 0(不校验) -PRESCRIPTION_ORDER_LINK_PAY_MIN_AMOUNT="0" -``` - -### 配置生效 - -修改 `.env` 文件后,配置会立即生效,无需重启服务。 - -## 业务逻辑说明 - -### 为什么需要最小金额限制? - -1. **定金机制**:确保客户已支付足够的定金 -2. **风险控制**:避免小额订单的履约风险 -3. **业务规范**:统一订单金额标准 - -### 为什么改为总金额验证? - -1. **灵活性**:允许客户分多次支付 -2. **用户体验**:不强制单次支付大额 -3. **实际需求**:多次小额支付也能达到定金要求 - -### 为什么必须关联支付单? - -1. **资金确认**:确保订单有对应的收款记录 -2. **审核依据**:支付单审核时需要对照 -3. **财务管理**:便于对账和统计 - -## 测试建议 - -### 1. 单元测试 - -```php -// 测试订单金额不足 -$result = PrescriptionOrderLogic::assertDepositAndLinkPayRules(80, [1]); -assert($result !== null); - -// 测试未关联支付单 -$result = PrescriptionOrderLogic::assertDepositAndLinkPayRules(150, []); -assert($result !== null); - -// 测试总金额不足 -$result = PrescriptionOrderLogic::assertDepositAndLinkPayRules(150, [1, 2]); // 假设总金额 < 100 -assert($result !== null); - -// 测试总金额达标 -$result = PrescriptionOrderLogic::assertDepositAndLinkPayRules(150, [3, 4]); // 假设总金额 >= 100 -assert($result === null); -``` - -### 2. 集成测试 - -1. 创建多个小额支付单(如 60元、50元) -2. 创建订单,关联这些支付单 -3. 验证是否能成功创建 -4. 验证错误提示是否正确 - -### 3. 边界测试 - -- 总金额刚好等于最小金额(100元) -- 总金额略小于最小金额(99.99元) -- 总金额略大于最小金额(100.01元) -- 最小金额为0 -- 最小金额为负数(应该按0处理) - -## 相关文件 - -### 配置文件 -- `server/.env` - 环境配置 -- `server/config/prescription_order.php` - 订单配置 - -### 后端文件 -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 订单逻辑 -- `server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php` - 订单验证 - -### 前端文件 -- `admin/src/views/consumer/prescription/order_list.vue` - 订单列表页面 - -## 总结 - -✅ 修改验证逻辑:从"每个支付单"改为"支付单总金额" -✅ 添加必须关联支付单的验证 -✅ 优化错误提示信息,显示当前总金额 -✅ 支持多个小额支付单组合 -✅ 保持订单金额验证不变 -✅ 配置为0时不进行校验 - -**优势**: -- 更灵活的支付方式 -- 更好的用户体验 -- 更清晰的错误提示 -- 更符合实际业务需求 - -**下一步**: -1. 测试各种场景 -2. 更新前端提示文字 -3. 编写单元测试 -4. 更新用户文档 diff --git a/PRESCRIPTION_ORDER_WITHDRAW_RESET_AUDIT.md b/PRESCRIPTION_ORDER_WITHDRAW_RESET_AUDIT.md deleted file mode 100644 index c8e59653..00000000 --- a/PRESCRIPTION_ORDER_WITHDRAW_RESET_AUDIT.md +++ /dev/null @@ -1,387 +0,0 @@ -# 订单撤回后重置处方审核状态 - -## 需求说明 - -当用户在订单列表中点击"撤回"按钮后,除了将订单状态改为"已取消",还需要将关联的处方审核状态重置为"待审核",以便重新审核。 - -## 业务场景 - -### 场景描述 - -1. 医生创建处方 -2. 处方审核通过(`audit_status = 1`) -3. 创建业务订单(`fulfillment_status = 1` 待双审通过) -4. 用户点击"撤回"订单 -5. **期望**:处方审核状态重置为"待审核"(`audit_status = 0`) - -### 为什么需要重置? - -1. **重新审核**:撤回订单可能是因为处方有问题,需要重新审核 -2. **状态一致性**:订单撤回后,处方应该回到初始状态 -3. **业务流程**:允许医生修改处方后重新提交审核 - -## 数据库表结构 - -### 处方表(zyt_tcm_prescription) - -```sql -`audit_status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '审核:0待审核 1已通过 2已驳回(同时作废)' -`audit_time` int(10) unsigned DEFAULT NULL COMMENT '审核时间' -`audit_by` int(11) unsigned DEFAULT NULL COMMENT '审核人ID' -`audit_remark` varchar(500) NOT NULL DEFAULT '' COMMENT '审核意见' -``` - -**审核状态**: -- 0 = 待审核 -- 1 = 已通过 -- 2 = 已驳回(同时作废) - -### 业务订单表(zyt_tcm_prescription_order) - -```sql -`prescription_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '消费者处方ID' -`fulfillment_status` tinyint(2) unsigned NOT NULL DEFAULT 1 COMMENT '1待双审通过 2履约中 3已完成 4已取消' -``` - -**履约状态**: -- 1 = 待双审通过 -- 2 = 履约中 -- 3 = 已完成 -- 4 = 已取消(撤回) - -## 代码修改 - -**文件**:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -**方法**:`withdraw()` - -### 修改内容 - -```php -public static function withdraw(int $id, int $adminId, array $adminInfo) -{ - self::$error = ''; - $order = PrescriptionOrder::where('id', $id)->whereNull('delete_time')->find(); - if (!$order) { - self::$error = '订单不存在'; - return false; - } - if (!self::canWithdrawOrder($order, $adminId, $adminInfo)) { - self::$error = '无权限撤回'; - return false; - } - if ((int) $order->fulfillment_status !== 1) { - self::$error = '仅「待双审通过」状态可撤回;双审通过后请走履约/完成流程'; - return false; - } - - $order->fulfillment_status = 4; - self::clearPayOrderLinks($order); - - try { - $order->save(); - - // 撤回订单后,将关联的处方审核状态改回"待审核" - $prescriptionId = (int) $order->prescription_id; - if ($prescriptionId > 0) { - Prescription::where('id', $prescriptionId) - ->whereNull('delete_time') - ->update([ - 'audit_status' => 0, // 0=待审核 - 'audit_time' => null, - 'audit_by' => null, - 'audit_remark' => '', - 'update_time' => time(), - ]); - } - } catch (\Throwable $e) { - self::$error = $e->getMessage(); - return false; - } - - $out = $order->toArray(); - self::maskInternalCostIfNeeded($out, $adminInfo); - self::attachLinkedPayOrders($out); - - return $out; -} -``` - -### 修改说明 - -1. **保存订单状态**:`$order->save()` -2. **获取处方ID**:`$prescriptionId = (int) $order->prescription_id` -3. **重置处方审核状态**: - - `audit_status = 0`(待审核) - - `audit_time = null`(清空审核时间) - - `audit_by = null`(清空审核人) - - `audit_remark = ''`(清空审核意见) - - `update_time = time()`(更新时间) - -## 工作流程 - -### 完整流程 - -``` -用户点击"撤回"按钮 - ↓ -调用 withdraw() 方法 - ↓ -验证权限和状态 - ↓ -更新订单状态为"已取消"(4) - ↓ -清除支付单关联 - ↓ -保存订单 - ↓ -获取关联的处方ID - ↓ -重置处方审核状态为"待审核"(0) - ↓ -清空审核时间、审核人、审核意见 - ↓ -返回成功 -``` - -### 前端显示 - -**订单列表页面**: -- 订单状态:已取消 -- 撤回按钮:消失(只有"待双审通过"状态才显示) - -**处方列表页面**: -- 审核状态:待审核 -- 可以重新审核 - -## 测试场景 - -### 场景1:正常撤回 - -**初始状态**: -- 处方审核状态:已通过(1) -- 订单履约状态:待双审通过(1) - -**操作**:点击"撤回"按钮 - -**预期结果**: -- 订单履约状态:已取消(4) -- 处方审核状态:待审核(0) -- 审核时间:null -- 审核人:null -- 审核意见:空 - ---- - -### 场景2:撤回后重新审核 - -**步骤**: -1. 撤回订单(处方状态变为"待审核") -2. 医生修改处方 -3. 重新提交审核 -4. 审核通过 -5. 创建新订单 - -**预期结果**:流程正常,可以重新审核和创建订单 - ---- - -### 场景3:无权限撤回 - -**初始状态**: -- 当前用户:非创建人、非医助 - -**操作**:点击"撤回"按钮 - -**预期结果**: -- 错误提示:"无权限撤回" -- 处方状态不变 - ---- - -### 场景4:非待双审状态撤回 - -**初始状态**: -- 订单履约状态:履约中(2)或已完成(3) - -**操作**:点击"撤回"按钮 - -**预期结果**: -- 错误提示:"仅「待双审通过」状态可撤回;双审通过后请走履约/完成流程" -- 处方状态不变 - ---- - -### 场景5:处方不存在 - -**初始状态**: -- 订单关联的处方已被删除 - -**操作**:点击"撤回"按钮 - -**预期结果**: -- 订单状态:已取消(4) -- 不会报错(因为有 `if ($prescriptionId > 0)` 判断) - -## 边界情况处理 - -### 1. 处方ID为0或null - -```php -if ($prescriptionId > 0) { - // 只有处方ID有效时才更新 -} -``` - -**处理**:跳过处方状态更新,不报错 - -### 2. 处方已被删除 - -```php -->whereNull('delete_time') -``` - -**处理**:不会更新已删除的处方 - -### 3. 数据库更新失败 - -```php -try { - $order->save(); - // 更新处方状态 -} catch (\Throwable $e) { - self::$error = $e->getMessage(); - return false; -} -``` - -**处理**:捕获异常,返回错误信息 - -### 4. 事务一致性 - -**问题**:订单保存成功,但处方更新失败怎么办? - -**当前实现**: -- 订单和处方更新在同一个 try-catch 块中 -- 如果处方更新失败,会抛出异常 -- 异常会导致整个方法返回 false - -**建议优化**:使用数据库事务 - -```php -Db::startTrans(); -try { - $order->save(); - - // 更新处方状态 - Prescription::where('id', $prescriptionId) - ->whereNull('delete_time') - ->update([...]); - - Db::commit(); -} catch (\Throwable $e) { - Db::rollback(); - self::$error = $e->getMessage(); - return false; -} -``` - -## 相关影响 - -### 1. 处方列表页面 - -**文件**:`admin/src/views/consumer/prescription/index.vue` - -**影响**: -- 撤回后,处方审核状态显示为"待审核" -- 可以重新点击"审核"按钮 - -### 2. 订单列表页面 - -**文件**:`admin/src/views/consumer/prescription/order_list.vue` - -**影响**: -- 撤回后,订单状态显示为"已取消" -- "撤回"按钮消失(只有"待双审通过"状态才显示) - -### 3. 业务流程 - -**影响**: -- 允许撤回后重新审核处方 -- 允许重新创建订单 -- 支付单关联被清除,可以重新关联 - -## 注意事项 - -### 1. 权限控制 - -只有以下用户可以撤回订单: -- 订单创建人 -- 诊单医助 -- 主管角色 - -### 2. 状态限制 - -只有"待双审通过"状态的订单可以撤回: -- 待双审通过(1):✅ 可以撤回 -- 履约中(2):❌ 不可撤回 -- 已完成(3):❌ 不可撤回 -- 已取消(4):❌ 不可撤回 - -### 3. 数据一致性 - -撤回操作会: -- 更新订单状态 -- 清除支付单关联 -- 重置处方审核状态 - -建议使用数据库事务确保一致性。 - -### 4. 审核历史 - -当前实现会清空审核历史(审核时间、审核人、审核意见)。 - -如果需要保留审核历史,可以考虑: -- 不清空这些字段 -- 或者创建审核历史表记录 - -## 相关文件 - -### 后端文件 -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 订单逻辑 -- `server/app/common/model/tcm/Prescription.php` - 处方模型 -- `server/app/common/model/tcm/PrescriptionOrder.php` - 订单模型 - -### 前端文件 -- `admin/src/views/consumer/prescription/order_list.vue` - 订单列表 -- `admin/src/views/consumer/prescription/index.vue` - 处方列表 - -### 数据库文件 -- `server/database/migrations/2026_03_30_prescription_visible_roles_audit.sql` - 处方审核字段 -- `server/database/migrations/2026_04_07_create_tcm_prescription_order.sql` - 订单表 - -## 总结 - -✅ 撤回订单时重置处方审核状态为"待审核" -✅ 清空审核时间、审核人、审核意见 -✅ 更新处方的 update_time -✅ 保持订单状态为"已取消" -✅ 清除支付单关联 -✅ 异常处理和错误提示 - -**优势**: -- 支持撤回后重新审核 -- 状态一致性 -- 业务流程完整 -- 用户体验友好 - -**建议优化**: -- 使用数据库事务确保一致性 -- 考虑保留审核历史记录 -- 添加操作日志 - -**下一步**: -1. 测试撤回功能 -2. 验证处方状态变化 -3. 测试重新审核流程 -4. 考虑添加事务支持 diff --git a/PRESCRIPTION_TEST_GUIDE.md b/PRESCRIPTION_TEST_GUIDE.md deleted file mode 100644 index 0e0fdea3..00000000 --- a/PRESCRIPTION_TEST_GUIDE.md +++ /dev/null @@ -1,255 +0,0 @@ -# 处方管理功能测试指南 - -## 测试前准备 - -### 1. 安装数据库字段 -运行安装脚本添加新字段: -```bash -# Windows -install_prescription_shared.bat - -# Linux/Mac -./install_prescription_shared.sh -``` - -### 2. 确认路由配置 -确保后端路由已配置处方管理相关接口。 - -## 测试用例 - -### 测试用例1:新增处方(不共享) - -**步骤:** -1. 登录管理后台 -2. 进入"消费者管理" -> "处方管理" -3. 点击"新增处方"按钮 -4. 填写表单: - - 处方名称:感冒清热方 - - 患者姓名:张三 - - 性别:男 - - 年龄:35 - - 处方日期:选择今天 - - 临床诊断:风寒感冒 - - 点击"添加药材",添加以下药材: - - 麻黄 9克 - - 桂枝 6克 - - 杏仁 9克 - - 甘草 3克 - - 剂数:7 - - 剂量单位:剂 - - 服用天数:7 - - 用法:水煎服,一日二次 - - 服用说明:饭前30分钟服用,温水送服;忌食辛辣、生冷食物 - - 医师姓名:李医生 - - 是否共享:不勾选 -5. 点击"确定" - -**预期结果:** -- 提示"添加成功" -- 列表中显示新增的处方 -- "是否共享"列显示"仅自己可见" - -### 测试用例2:新增处方(共享) - -**步骤:** -1. 点击"新增处方" -2. 填写表单(同测试用例1) -3. 勾选"是否共享"开关 -4. 点击"确定" - -**预期结果:** -- 提示"添加成功" -- "是否共享"列显示"所有人可见" - -### 测试用例3:查看处方 - -**步骤:** -1. 在列表中点击某个处方的"查看"按钮 -2. 查看处方详情 - -**预期结果:** -- 弹出详情对话框 -- 显示所有处方信息 -- 所有字段为只读状态 -- 没有"确定"按钮 - -### 测试用例4:编辑处方 - -**步骤:** -1. 在列表中点击某个处方的"编辑"按钮 -2. 修改处方名称为"感冒清热方(加强版)" -3. 添加一味药材:生姜 6克 -4. 点击"确定" - -**预期结果:** -- 提示"编辑成功" -- 列表中显示更新后的处方名称 -- 药材数量增加1 - -### 测试用例5:删除处方 - -**步骤:** -1. 在列表中点击某个处方的"删除"按钮 -2. 确认删除 - -**预期结果:** -- 提示"删除成功" -- 该处方从列表中消失 - -### 测试用例6:搜索功能 - -**步骤:** -1. 在"处方名称"输入框输入"感冒" -2. 点击"查询" - -**预期结果:** -- 只显示处方名称包含"感冒"的记录 - -**步骤:** -1. 在"是否共享"下拉框选择"所有人可见" -2. 点击"查询" - -**预期结果:** -- 只显示共享的处方 - -**步骤:** -1. 点击"重置"按钮 - -**预期结果:** -- 清空所有搜索条件 -- 显示所有处方 - -### 测试用例7:表单验证 - -**步骤:** -1. 点击"新增处方" -2. 不填写任何信息,直接点击"确定" - -**预期结果:** -- 显示必填项验证错误提示 - -**步骤:** -1. 填写基本信息,但不添加药材 -2. 点击"确定" - -**预期结果:** -- 提示"请至少添加一味药材" - -**步骤:** -1. 添加药材,但不填写药材名称 -2. 点击"确定" - -**预期结果:** -- 提示"第X味药材名称不能为空" - -**步骤:** -1. 填写药材名称,但剂量为0 -2. 点击"确定" - -**预期结果:** -- 提示"第X味药材剂量必须大于0" - -### 测试用例8:权限测试(多用户) - -**步骤:** -1. 用户A创建一个不共享的处方 -2. 用户B登录系统 -3. 查看处方列表 - -**预期结果:** -- 用户B看不到用户A创建的不共享处方 - -**步骤:** -1. 用户A创建一个共享的处方 -2. 用户B登录系统 -3. 查看处方列表 - -**预期结果:** -- 用户B可以看到用户A创建的共享处方 -- 用户B可以编辑该处方 - -### 测试用例9:分页功能 - -**步骤:** -1. 创建超过10条处方记录 -2. 查看列表底部的分页器 - -**预期结果:** -- 显示分页器 -- 可以切换页码 -- 每页显示正确数量的记录 - -## 常见问题排查 - -### 问题1:点击"新增处方"没有反应 -**排查:** -- 检查浏览器控制台是否有错误 -- 检查网络请求是否正常 - -### 问题2:提交表单后提示"处方名称不能为空" -**排查:** -- 确认数据库字段已添加 -- 检查后端验证规则 - -### 问题3:列表显示为空 -**排查:** -- 检查API接口是否正常 -- 检查数据库表是否有数据 -- 检查权限过滤逻辑 - -### 问题4:共享功能不生效 -**排查:** -- 确认is_shared字段已添加到数据库 -- 检查后端列表查询的权限逻辑 -- 确认用户ID获取正确 - -## 性能测试 - -### 测试场景1:大量数据加载 -1. 创建100条处方记录 -2. 测试列表加载速度 -3. 测试搜索响应速度 - -### 测试场景2:复杂处方 -1. 创建包含50味药材的处方 -2. 测试保存和加载速度 - -## 兼容性测试 - -### 浏览器兼容性 -- Chrome(推荐) -- Firefox -- Edge -- Safari - -### 分辨率测试 -- 1920x1080 -- 1366x768 -- 1280x720 - -## 测试报告模板 - -``` -测试日期:____年__月__日 -测试人员:________ -测试环境:________ - -| 测试用例 | 测试结果 | 备注 | -|---------|---------|------| -| 新增处方(不共享) | ☐ 通过 ☐ 失败 | | -| 新增处方(共享) | ☐ 通过 ☐ 失败 | | -| 查看处方 | ☐ 通过 ☐ 失败 | | -| 编辑处方 | ☐ 通过 ☐ 失败 | | -| 删除处方 | ☐ 通过 ☐ 失败 | | -| 搜索功能 | ☐ 通过 ☐ 失败 | | -| 表单验证 | ☐ 通过 ☐ 失败 | | -| 权限测试 | ☐ 通过 ☐ 失败 | | -| 分页功能 | ☐ 通过 ☐ 失败 | | - -总体评价:☐ 通过 ☐ 需要修复 - -问题列表: -1. -2. -3. -``` diff --git a/PRESCRIPTION_TIMES_PER_DAY_FEATURE.md b/PRESCRIPTION_TIMES_PER_DAY_FEATURE.md deleted file mode 100644 index 3af9b8b3..00000000 --- a/PRESCRIPTION_TIMES_PER_DAY_FEATURE.md +++ /dev/null @@ -1,199 +0,0 @@ -# 处方"每天几次"功能添加说明 - -## 功能描述 - -在中医处方单中添加"每天几次"字段,用于记录患者每天需要服用药物的次数。 - -## 修改内容 - -### 1. 表单界面(admin/src/components/tcm-prescription/index.vue) - -#### 添加表单字段(第 139-145 行) - -在"服用天数"字段后面添加"每天几次"输入框: - -```vue - - - - - - - - - - -``` - -**字段说明:** -- 字段名:`times_per_day` -- 类型:数字输入框 -- 最小值:1 -- 最大值:10 -- 默认值:2 - -### 2. TypeScript 类型定义(第 580-615 行) - -在 `PrescriptionForm` 接口中添加字段: - -```typescript -interface PrescriptionForm { - // ... 其他字段 - usage_days: number - times_per_day: number // 新增 - usage_instruction: string - // ... 其他字段 -} -``` - -### 3. 数据模型初始化(第 650-680 行) - -在 `formData` 中添加默认值: - -```typescript -const formData = reactive({ - // ... 其他字段 - usage_days: 7, - times_per_day: 2, // 新增,默认每天2次 - usage_instruction: '水煎服,一日二次', - // ... 其他字段 -}) -``` - -### 4. 处方预览显示(第 280-285 行) - -在处方打印预览中显示"每天几次"信息: - -```vue - -``` - -**显示效果:** -- 如果设置了 `times_per_day`:显示"服用7天,每天2次, 浓缩水丸 水煎服,一日二次" -- 如果未设置:显示"服用7天, 浓缩水丸 水煎服,一日二次" - -## 后端数据库支持 - -### 需要添加的数据库字段 - -如果后端数据库表中还没有 `times_per_day` 字段,需要执行以下 SQL: - -```sql --- 在处方表中添加 times_per_day 字段 -ALTER TABLE `zyt_prescription` -ADD COLUMN `times_per_day` INT(11) DEFAULT 2 COMMENT '每天服用次数' AFTER `usage_days`; - --- 如果需要更新现有数据的默认值 -UPDATE `zyt_prescription` SET `times_per_day` = 2 WHERE `times_per_day` IS NULL; -``` - -### 后端 API 修改 - -确保后端接口支持 `times_per_day` 字段: - -1. **创建/更新处方接口**:接收 `times_per_day` 参数 -2. **查询处方接口**:返回 `times_per_day` 字段 -3. **验证规则**:`times_per_day` 应该在 1-10 之间 - -## 使用说明 - -### 医生操作流程 - -1. 打开处方单编辑界面 -2. 填写患者信息和诊断信息 -3. 添加中药处方 -4. 在"服用天数"字段输入天数(如:7) -5. 在"每天几次"字段输入次数(如:2) -6. 系统会在处方预览中显示:"服用7天,每天2次" - -### 常见用法 - -- **每天2次**:早晚各一次(最常见) -- **每天3次**:早中晚各一次 -- **每天1次**:每日一次 -- **每天4次**:每6小时一次 - -## 测试要点 - -### 前端测试 - -1. ✅ 输入框显示正常 -2. ✅ 最小值限制(不能小于1) -3. ✅ 最大值限制(不能大于10) -4. ✅ 默认值为2 -5. ✅ 预览显示正确 -6. ✅ 保存后数据正确 - -### 后端测试 - -1. ✅ 创建处方时保存 `times_per_day` -2. ✅ 更新处方时更新 `times_per_day` -3. ✅ 查询处方时返回 `times_per_day` -4. ✅ 数据验证(1-10范围) - -### 集成测试 - -1. ✅ 创建处方 → 保存 → 查看预览 → 确认显示正确 -2. ✅ 编辑处方 → 修改次数 → 保存 → 确认更新成功 -3. ✅ 打印处方 → 确认打印内容包含"每天X次" - -## 界面效果 - -### 编辑界面 - -``` -┌─────────────────────────────────────────────────┐ -│ 处方类型: [浓缩水丸 ▼] │ -├─────────────────────────────────────────────────┤ -│ 服用天数: [ 7 ] 每天几次: [ 2 ] │ -├─────────────────────────────────────────────────┤ -│ 用法: 水煎服,一日二次 │ -└─────────────────────────────────────────────────┘ -``` - -### 预览界面 - -``` -服用7天,每天2次, 浓缩水丸 水煎服,一日二次 -服用时间: 饭前 服用方式: 温水送服 -``` - -## 注意事项 - -1. **默认值**:新建处方时默认为每天2次(最常见的用法) -2. **范围限制**:限制在1-10次之间,超出范围的输入会被自动调整 -3. **可选字段**:如果不填写,预览中不会显示"每天X次" -4. **与用法说明的关系**:`times_per_day` 是结构化数据,`usage_instruction` 是文字描述,两者可以互补 - -## 兼容性 - -- ✅ 兼容现有处方数据(旧数据没有此字段时不显示) -- ✅ 不影响现有功能 -- ✅ 向后兼容 - -## 后续优化建议 - -1. **智能提示**:根据处方类型自动推荐合适的服用次数 -2. **关联验证**:与"用法"字段进行一致性检查 -3. **统计分析**:统计不同处方类型的常用服用次数 -4. **模板支持**:在处方模板中包含 `times_per_day` 字段 - -## 完成状态 - -- ✅ 前端界面添加完成 -- ✅ 数据模型更新完成 -- ✅ 类型定义更新完成 -- ✅ 预览显示更新完成 -- ⏳ 等待后端数据库字段添加 -- ⏳ 等待后端 API 支持 - -## 相关文件 - -- `admin/src/components/tcm-prescription/index.vue` - 处方组件主文件 -- 后端需要修改的文件(待确认): - - 处方模型文件 - - 处方控制器文件 - - 数据库迁移文件 diff --git a/PRESCRIPTION_VIEW_PERMISSION_FIX.md b/PRESCRIPTION_VIEW_PERMISSION_FIX.md deleted file mode 100644 index 8d978164..00000000 --- a/PRESCRIPTION_VIEW_PERMISSION_FIX.md +++ /dev/null @@ -1,129 +0,0 @@ -# 业务订单中处方查看权限修复 - -## 问题描述 - -药师角色(角色ID=6)在查看业务订单详情时,提示"无权限查看此处方"。 - -虽然药师有业务订单管理权限(`order_edit_all_roles` 包含角色6),但在业务订单详情中需要显示关联的处方信息时,`PrescriptionLogic::canViewPrescription()` 方法会拒绝访问。 - -## 问题原因 - -`canViewPrescription` 方法的权限检查层级为: -1. 超级管理员 -2. 处方创建人 -3. 医助 -4. 共享处方 -5. 可见角色 -6. 有开方权限的医生(`tcm.diagnosis/kaifang`) - -药师角色通常没有 `tcm.diagnosis/kaifang` 权限,因此无法通过权限检查。 - -## 解决方案 - -在 `canViewPrescription` 方法中添加新的权限检查层级: - -**有业务订单管理权限的角色(如药师)可以查看处方(只读)** - -这样药师可以在业务订单中查看关联的处方信息,以便进行订单审核和管理。 - -## 代码修改 - -文件:`server/app/adminapi/logic/tcm/PrescriptionLogic.php` - -```php -public static function canViewPrescription($row, int $adminId, array $adminInfo): bool -{ - // 超级管理员 - if (!empty($adminInfo['root']) && (int) $adminInfo['root'] === 1) { - return true; - } - - // 创建人 - $creatorId = is_array($row) ? (int) ($row['creator_id'] ?? 0) : (int) $row->creator_id; - if ($creatorId === $adminId) { - return true; - } - - // 医助 - $assistantId = is_array($row) ? (int) ($row['assistant_id'] ?? 0) : (int) ($row->assistant_id ?? 0); - if ($assistantId > 0 && $assistantId === $adminId) { - return true; - } - - // 共享处方 - $isShared = is_array($row) ? (int) ($row['is_shared'] ?? 0) : (int) $row->is_shared; - if ($isShared === 1) { - return true; - } - - // 可见角色 - $vis = is_array($row) ? (string) ($row['visible_role_ids'] ?? '') : (string) ($row->visible_role_ids ?? ''); - $allowed = array_filter(array_map('intval', explode(',', $vis))); - $myRoles = array_map('intval', $adminInfo['role_id'] ?? []); - if (count(array_intersect($myRoles, $allowed)) > 0) { - return true; - } - - // 有开方权限的医生可以查看所有处方(只读) - $permissions = $adminInfo['permissions'] ?? []; - if (in_array('tcm.diagnosis/kaifang', $permissions, true)) { - return true; - } - - // 【新增】有业务订单管理权限的角色(如药师)可以查看处方(只读) - $orderEditAllRoles = Config::get('project.order_edit_all_roles', [0, 3]); - $myRoles = array_map('intval', $adminInfo['role_id'] ?? []); - $allowedRoles = array_map('intval', is_array($orderEditAllRoles) ? $orderEditAllRoles : []); - if (count(array_intersect($myRoles, $allowedRoles)) > 0) { - return true; - } - - return false; -} -``` - -## 权限层级(更新后) - -1. 超级管理员(root=1) -2. 处方创建人 -3. 医助 -4. 共享处方(is_shared=1) -5. 可见角色(visible_role_ids) -6. 有开方权限的医生(`tcm.diagnosis/kaifang`) -7. **有业务订单管理权限的角色(`order_edit_all_roles`)** ← 新增 - -## 配置文件 - -文件:`server/config/project.php` - -```php -// 医助创建订单权限:拥有以下角色ID的用户可修改任意订单,其他用户只能修改自己创建的订单 -'order_edit_all_roles' => [0, 3, 6], -``` - -- 角色 0:超级管理员 -- 角色 3:管理员 -- 角色 6:药师 - -## 使用场景 - -药师角色现在可以: -1. 查看业务订单列表 -2. 打开业务订单详情 -3. 查看订单中关联的处方信息(只读) -4. 进行处方审核 -5. 进行支付单审核 -6. 编辑业务订单信息 - -## 测试验证 - -1. 使用药师账号登录 -2. 进入"消费者处方订单"列表 -3. 点击任意订单的"详情"按钮 -4. 确认可以正常查看处方信息,不再提示"无权限查看此处方" - -## 注意事项 - -1. 此权限为只读权限,药师不能编辑处方内容 -2. 药师只能通过业务订单查看处方,不能直接在处方列表中查看其他医生的处方 -3. 修改代码后需要清除缓存:`php think clear` diff --git a/QUICK_FIX_REFERENCE.md b/QUICK_FIX_REFERENCE.md deleted file mode 100644 index b84371d3..00000000 --- a/QUICK_FIX_REFERENCE.md +++ /dev/null @@ -1,83 +0,0 @@ -# 本地录制修复 - 快速参考 - -## 🔍 问题 -本地录制总是失败,blob 为空 - -## 🎯 根本原因 -录制启动太晚(1200ms),在对方挂断后才启动,此时 SDK 已销毁视频轨道 - -## ✅ 解决方案 - -### 1. 缩短启动延迟 -``` -1200ms → 500ms -``` - -### 2. 快速重试 -``` -300ms 间隔,最多 10 次 -``` - -### 3. 立即停止录制 -``` -监听 USER_LEAVE 事件,立即调用 beginStopNow() -``` - -### 4. 缩短停止延迟 -``` -300ms → 100ms -``` - -## 📊 测试检查清单 - -### 启动阶段(接通后 1 秒内) -- [ ] 看到 "尝试启动本地录制" -- [ ] 看到 "视频元素已就绪" -- [ ] 看到 "✅ 本地录制已成功启动" - -### 录制阶段(每 250ms) -- [ ] 看到 "收到数据块,大小: XXX" -- [ ] 数据块数持续增加 - -### 停止阶段(挂断时) -- [ ] 看到 "检测到通话结束事件" -- [ ] 看到 "已收集数据块: X"(X > 0) -- [ ] 看到 "生成 Blob: XXX bytes"(不是 null) -- [ ] 看到 "本地录制已上传并关联到通话记录" - -## 🚨 常见问题 - -### 问题:数据块数为 0 -**原因**:录制启动太晚或视频流无效 -**检查**: -```javascript -// 控制台执行 -document.querySelectorAll('video').forEach((v, i) => { - console.log(`Video ${i}:`, v.readyState, v.srcObject) -}) -``` - -### 问题:Blob 为 null -**原因**:通话时间太短(< 2 秒) -**解决**:保持通话至少 5 秒 - -### 问题:录制启动失败 -**原因**:找不到视频元素 -**检查**:是否看到 "未找到视频元素" 日志 - -## 📝 修改的文件 -1. `admin/src/components/chat-dialog/index.vue` -2. `admin/src/utils/call-local-recorder.ts` - -## 🔗 详细文档 -- `LOCAL_RECORDING_DEBUG_GUIDE.md` - 完整调试指南 -- `RECORDING_FIX_SUMMARY.md` - 修复总结 -- `admin/test-local-recording.html` - 测试工具 - -## 💡 测试建议 -1. 清除浏览器缓存 -2. 打开控制台(F12) -3. 发起视频通话 -4. 保持通话 5 秒以上 -5. 正常挂断 -6. 检查日志和录制结果 diff --git a/QUICK_FIX_SQL.sql b/QUICK_FIX_SQL.sql deleted file mode 100644 index ca77340f..00000000 --- a/QUICK_FIX_SQL.sql +++ /dev/null @@ -1,23 +0,0 @@ --- 快速修复SQL - 只添加缺失的字段 --- 如果某个字段已存在会报错,但不影响其他字段的添加,可以忽略错误继续执行 - --- 检查当前表结构 --- SHOW COLUMNS FROM `zyt_tcm_prescription`; - --- 根据错误提示,prescription_type 已存在,所以跳过 --- 只添加缺失的字段: - --- 服用时间(如果不存在) -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_time` varchar(50) NOT NULL DEFAULT '饭前' COMMENT '服用时间:饭前、饭后、饭中、空腹、睡前、晨起、随时' AFTER `usage_instruction`; - --- 服用方式(如果不存在) -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_way` varchar(50) NOT NULL DEFAULT '温水送服' COMMENT '服用方式:温水送服、开水冲服、黄酒送服等' AFTER `usage_time`; - --- 忌口(如果不存在) -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `dietary_taboo` varchar(200) NOT NULL DEFAULT '' COMMENT '忌口(逗号分隔):辛辣食物、生冷食物、油腻食物等' AFTER `usage_way`; - --- 其他服用说明(如果不存在) -ALTER TABLE `zyt_tcm_prescription` ADD COLUMN `usage_notes` varchar(200) NOT NULL DEFAULT '' COMMENT '其他服用说明' AFTER `dietary_taboo`; - --- 完成后查看表结构确认 --- SHOW COLUMNS FROM `zyt_tcm_prescription`; diff --git a/QUICK_START_FIRST_LOGIN.md b/QUICK_START_FIRST_LOGIN.md deleted file mode 100644 index bbe991a5..00000000 --- a/QUICK_START_FIRST_LOGIN.md +++ /dev/null @@ -1,80 +0,0 @@ -# 首次登录修改密码 - 快速开始 - -## 一分钟快速安装 - -### Windows 用户 - -1. 双击运行 `install_first_login_password.bat` -2. 按提示输入数据库信息 -3. 完成! - -### Linux/Mac 用户 - -```bash -chmod +x install_first_login_password.sh -./install_first_login_password.sh -``` - -## 快速测试 - -### 1. 设置测试账号 - -```sql --- 设置管理员 ID 为 1 的账号需要修改密码 -UPDATE la_admin SET is_paw = 0 WHERE id = 1; -``` - -### 2. 登录测试 - -1. 使用该账号登录系统 -2. 登录后会自动跳转到修改密码页面 -3. 输入新密码(至少 6 位) -4. 点击"确认修改" -5. 系统提示"密码修改成功,请重新登录" -6. 使用新密码重新登录 - -### 3. 验证成功 - -```sql --- 查看 is_paw 字段,应该已经变成 1 -SELECT id, account, is_paw FROM la_admin WHERE id = 1; -``` - -## 常用操作 - -### 强制单个管理员修改密码 - -```sql -UPDATE la_admin SET is_paw = 0 WHERE id = <管理员ID>; -``` - -### 强制所有管理员修改密码 - -```sql -UPDATE la_admin SET is_paw = 0 WHERE delete_time = 0; -``` - -### 恢复正常状态 - -```sql -UPDATE la_admin SET is_paw = 1 WHERE id = <管理员ID>; -``` - -## 功能说明 - -- `is_paw = 0`:下次登录时强制修改密码 -- `is_paw = 1`:正常状态,无需修改密码 - -## 文件说明 - -| 文件 | 说明 | -|------|------| -| `add_is_paw_field.sql` | 数据库字段添加 SQL | -| `install_first_login_password.sh` | Linux/Mac 安装脚本 | -| `install_first_login_password.bat` | Windows 安装脚本 | -| `test_first_login.sql` | 测试用 SQL 语句 | -| `FIRST_LOGIN_PASSWORD_CHANGE.md` | 完整功能文档 | - -## 需要帮助? - -查看完整文档:`FIRST_LOGIN_PASSWORD_CHANGE.md` diff --git a/QUICK_START_PRESCRIPTION_LIBRARY.md b/QUICK_START_PRESCRIPTION_LIBRARY.md deleted file mode 100644 index 49d9f8f7..00000000 --- a/QUICK_START_PRESCRIPTION_LIBRARY.md +++ /dev/null @@ -1,135 +0,0 @@ -# 处方库功能 - 快速开始指南 - -## 🚀 5分钟快速上手 - -### 第一步:安装数据库表 - -#### Windows 用户 -双击运行 `install_prescription_library.bat` - -#### Linux/Mac 用户 -```bash -chmod +x install_prescription_library.sh -./install_prescription_library.sh -``` - -#### 手动安装 -使用数据库工具执行:`server/database/migrations/create_prescription_library.sql` - -### 第二步:访问功能 - -1. 登录后台管理系统 -2. 在菜单中找到"处方库"功能 -3. 开始使用! - -## 📝 快速创建第一个处方 - -### 示例:创建"六味地黄丸"处方 - -1. 点击"新增处方"按钮 - -2. 填写处方名称: - ``` - 六味地黄丸 - ``` - -3. 添加药材(点击6次"添加药材"按钮): - ``` - 熟地黄 24克 - 山茱萸 12克 - 山药 12克 - 泽泻 9克 - 牡丹皮 9克 - 茯苓 9克 - ``` - -4. 选择是否公开: - - 仅自己可见:只有你能看到 - - 所有人可见:所有医生都能看到 - -5. 点击"确定"保存 - -✅ 完成!你的第一个处方已创建成功! - -## 🎯 常用操作 - -### 查看处方 -- 在列表中点击"查看"按钮 -- 以只读模式查看处方详情 - -### 编辑处方 -- 在列表中点击"编辑"按钮 -- 修改处方信息后保存 - -### 删除处方 -- 在列表中点击"删除"按钮 -- 确认删除(只能删除自己创建的处方) - -### 搜索处方 -- 在搜索框输入处方名称 -- 选择是否公开筛选条件 -- 点击"查询"按钮 - -## 💡 使用技巧 - -### 技巧1:快速复制处方 -1. 查看已有处方 -2. 点击"编辑" -3. 修改处方名称 -4. 保存为新处方 - -### 技巧2:分享经典处方 -创建处方时,将"是否公开"设置为"所有人可见",其他医生就能看到并参考。 - -### 技巧3:管理个人处方库 -使用"是否公开"筛选,快速查看自己的私有处方。 - -## 📊 测试数据 - -想快速测试功能?执行以下SQL插入测试数据: - -```sql --- 插入3个经典处方 -INSERT INTO `zyt_prescription_library` -(`prescription_name`, `herbs`, `is_public`, `creator_id`, `creator_name`, `create_time`, `update_time`) -VALUES -('六味地黄丸', '[{"name":"熟地黄","dosage":24},{"name":"山茱萸","dosage":12},{"name":"山药","dosage":12},{"name":"泽泻","dosage":9},{"name":"牡丹皮","dosage":9},{"name":"茯苓","dosage":9}]', 1, 1, '测试医生', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), -('补中益气汤', '[{"name":"黄芪","dosage":15},{"name":"党参","dosage":10},{"name":"白术","dosage":10},{"name":"当归","dosage":10},{"name":"陈皮","dosage":6},{"name":"升麻","dosage":6},{"name":"柴胡","dosage":6},{"name":"甘草","dosage":5}]', 1, 1, '测试医生', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), -('逍遥散', '[{"name":"柴胡","dosage":10},{"name":"当归","dosage":10},{"name":"白芍","dosage":10},{"name":"白术","dosage":10},{"name":"茯苓","dosage":10},{"name":"薄荷","dosage":6},{"name":"生姜","dosage":6},{"name":"甘草","dosage":5}]', 0, 1, '测试医生', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); -``` - -## ❓ 常见问题 - -### Q1: 安装后看不到菜单? -A: 需要在系统菜单管理中添加处方库菜单项,或联系管理员配置权限。 - -### Q2: 无法删除处方? -A: 只有创建者才能删除处方。如果是其他人创建的,即使是公开的也不能删除。 - -### Q3: 其他人看不到我的处方? -A: 检查"是否公开"设置,确保设置为"所有人可见"。 - -### Q4: 药材剂量可以是小数吗? -A: 可以,支持精确到0.1克,如:3.5克。 - -### Q5: 可以导入Excel处方吗? -A: 当前版本暂不支持,需要手动录入。这是未来的扩展方向。 - -## 📚 更多文档 - -- **完整说明:** PRESCRIPTION_LIBRARY_README.md -- **功能演示:** PRESCRIPTION_LIBRARY_DEMO.md -- **开发总结:** PRESCRIPTION_LIBRARY_SUMMARY.md -- **测试SQL:** TEST_PRESCRIPTION_LIBRARY.sql - -## 🆘 需要帮助? - -如遇到问题,请: -1. 查看完整文档 -2. 检查数据库连接 -3. 查看浏览器控制台错误 -4. 联系技术支持 - ---- - -**祝使用愉快!** 🎉 diff --git a/QYWX_CUSTOMER_SYNC_FIX.md b/QYWX_CUSTOMER_SYNC_FIX.md deleted file mode 100644 index 9f48241a..00000000 --- a/QYWX_CUSTOMER_SYNC_FIX.md +++ /dev/null @@ -1,164 +0,0 @@ -# 企业微信客户同步问题修复指南 - -## 问题描述 - -同步企业微信客户时,能成功获取企业成员列表(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 deleted file mode 100644 index 68322fa5..00000000 --- a/QYWX_CUSTOMER_SYNC_GUIDE.md +++ /dev/null @@ -1,246 +0,0 @@ -# 企业微信客户同步功能说明 - -## 功能概述 - -自动从企业微信同步外部联系人(客户)信息,包括客户基本信息、跟进人等,用于患者信息匹配和自动关联。 - -同步流程: -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 deleted file mode 100644 index 5527fabf..00000000 --- a/QYWX_IP_WHITELIST_GUIDE.md +++ /dev/null @@ -1,88 +0,0 @@ -# 企业微信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 deleted file mode 100644 index 0dbdf12e..00000000 --- a/QYWX_SYNC_FIX.md +++ /dev/null @@ -1,108 +0,0 @@ -# 企业微信客户同步功能修复 - -## 问题描述 - -调用企业微信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/README_PRESCRIPTION_LIBRARY.md b/README_PRESCRIPTION_LIBRARY.md deleted file mode 100644 index 48b29a6b..00000000 --- a/README_PRESCRIPTION_LIBRARY.md +++ /dev/null @@ -1,377 +0,0 @@ -# 处方库功能 - 文档索引 - -## 📚 欢迎使用处方库管理系统 - -这是一个独立的中药处方模板管理系统,用于创建、管理和分享常用处方配方。 - -**版本:** v1.0.0 -**状态:** ✅ 已完成并测试通过 -**日期:** 2026-03-22 - ---- - -## 🚀 快速开始 - -### 第一次使用?从这里开始! - -1. **[快速开始指南](QUICK_START_PRESCRIPTION_LIBRARY.md)** ⭐ 推荐 - - 5分钟快速上手 - - 创建第一个处方 - - 常用操作说明 - -2. **[安装检查清单](INSTALLATION_CHECKLIST.md)** - - 详细的安装步骤 - - 环境检查 - - 问题排查 - ---- - -## 📖 完整文档 - -### 使用文档 - -| 文档名称 | 说明 | 适合人群 | -|---------|------|---------| -| [完整使用说明](PRESCRIPTION_LIBRARY_README.md) | 详细的功能说明和使用指南 | 所有用户 | -| [功能演示](PRESCRIPTION_LIBRARY_DEMO.md) | 使用场景和示例 | 新用户 | -| [快速开始](QUICK_START_PRESCRIPTION_LIBRARY.md) | 5分钟快速上手 | 新用户 ⭐ | - -### 技术文档 - -| 文档名称 | 说明 | 适合人群 | -|---------|------|---------| -| [开发总结](PRESCRIPTION_LIBRARY_SUMMARY.md) | 技术实现和开发总结 | 开发人员 | -| [文件清单](PRESCRIPTION_LIBRARY_FILES.md) | 所有文件列表和说明 | 开发人员 | -| [交付文档](DELIVERY_PACKAGE.md) | 项目交付信息 | 项目经理 | - -### 安装文档 - -| 文档名称 | 说明 | 适合人群 | -|---------|------|---------| -| [安装检查清单](INSTALLATION_CHECKLIST.md) | 详细的安装步骤和验证 | 运维人员 ⭐ | -| [测试SQL](TEST_PRESCRIPTION_LIBRARY.sql) | 测试数据和查询语句 | 测试人员 | - ---- - -## 🔧 安装文件 - -### 自动安装脚本 - -| 文件名 | 平台 | 使用方法 | -|--------|------|---------| -| `install_prescription_library.bat` | Windows | 双击运行 | -| `install_prescription_library.sh` | Linux/Mac | `chmod +x install_prescription_library.sh && ./install_prescription_library.sh` | - -### 手动安装 - -如果自动安装脚本无法运行,请执行: -```sql -server/database/migrations/create_prescription_library.sql -``` - ---- - -## 📂 文件结构 - -### 后端文件(PHP) -``` -server/ -├── database/migrations/ -│ └── create_prescription_library.sql # 数据库表结构 -├── app/common/model/tcm/ -│ └── PrescriptionLibrary.php # 数据模型 -└── app/adminapi/ - ├── controller/tcm/ - │ └── PrescriptionLibraryController.php # 控制器 - ├── logic/tcm/ - │ └── PrescriptionLibraryLogic.php # 业务逻辑 - ├── lists/tcm/ - │ └── PrescriptionLibraryLists.php # 列表逻辑 - └── validate/tcm/ - └── PrescriptionLibraryValidate.php # 数据验证 -``` - -### 前端文件(Vue) -``` -admin/ -└── src/ - ├── views/consumer/prescription/ - │ └── list.vue # 处方库页面 - └── api/ - └── tcm.ts # API接口(已更新) -``` - ---- - -## 🎯 核心功能 - -### ✅ 已实现功能 - -1. **处方管理** - - 创建处方(处方名称 + 药材配方) - - 编辑处方 - - 删除处方 - - 查看处方详情 - -2. **药材管理** - - 动态添加药材 - - 设置药材名称和剂量(克) - - 支持小数剂量(精确到0.1克) - -3. **权限控制** - - 是否公开设置(仅自己可见/所有人可见) - - 查看权限控制 - - 编辑权限控制 - - 删除权限控制(仅创建者) - -4. **搜索筛选** - - 按处方名称搜索(模糊匹配) - - 按是否公开筛选 - - 分页显示 - -### 🔮 未来扩展 - -1. 处方分类功能 -2. 处方标签功能 -3. 使用统计功能 -4. 导入导出功能 -5. 处方评论功能 -6. 版本管理功能 - ---- - -## 🔌 API接口 - -### 接口列表 - -| 接口路径 | 方法 | 说明 | -|---------|------|------| -| `/tcm.prescriptionLibrary/lists` | GET | 获取处方列表 | -| `/tcm.prescriptionLibrary/add` | POST | 添加处方 | -| `/tcm.prescriptionLibrary/edit` | POST | 编辑处方 | -| `/tcm.prescriptionLibrary/delete` | POST | 删除处方 | -| `/tcm.prescriptionLibrary/detail` | GET | 获取处方详情 | - -详细说明请参阅:[开发总结](PRESCRIPTION_LIBRARY_SUMMARY.md) - ---- - -## 💡 使用示例 - -### 示例1:六味地黄丸 - -```json -{ - "prescription_name": "六味地黄丸", - "herbs": [ - {"name": "熟地黄", "dosage": 24}, - {"name": "山茱萸", "dosage": 12}, - {"name": "山药", "dosage": 12}, - {"name": "泽泻", "dosage": 9}, - {"name": "牡丹皮", "dosage": 9}, - {"name": "茯苓", "dosage": 9} - ], - "is_public": 1 -} -``` - -### 示例2:补中益气汤 - -```json -{ - "prescription_name": "补中益气汤", - "herbs": [ - {"name": "黄芪", "dosage": 15}, - {"name": "党参", "dosage": 10}, - {"name": "白术", "dosage": 10}, - {"name": "当归", "dosage": 10}, - {"name": "陈皮", "dosage": 6}, - {"name": "升麻", "dosage": 6}, - {"name": "柴胡", "dosage": 6}, - {"name": "甘草", "dosage": 5} - ], - "is_public": 1 -} -``` - -更多示例请参阅:[功能演示](PRESCRIPTION_LIBRARY_DEMO.md) - ---- - -## ❓ 常见问题 - -### Q1: 如何安装? -A: 运行安装脚本或手动执行SQL文件。详见:[安装检查清单](INSTALLATION_CHECKLIST.md) - -### Q2: 如何创建处方? -A: 点击"新增处方"按钮,填写信息后保存。详见:[快速开始](QUICK_START_PRESCRIPTION_LIBRARY.md) - -### Q3: 如何分享处方? -A: 创建处方时,将"是否公开"设置为"所有人可见"。 - -### Q4: 如何删除处方? -A: 只有创建者才能删除处方,点击"删除"按钮即可。 - -### Q5: 药材剂量可以是小数吗? -A: 可以,支持精确到0.1克,如:3.5克。 - -更多问题请参阅:[完整使用说明](PRESCRIPTION_LIBRARY_README.md) - ---- - -## 🧪 测试 - -### 快速测试 - -1. 执行测试SQL插入测试数据: - ```bash - mysql -u用户名 -p密码 数据库名 < TEST_PRESCRIPTION_LIBRARY.sql - ``` - -2. 访问处方库页面,查看测试数据 - -3. 测试各项功能 - -详细测试步骤请参阅:[安装检查清单](INSTALLATION_CHECKLIST.md) - ---- - -## 📊 技术栈 - -### 后端 -- **框架:** ThinkPHP 6.x -- **语言:** PHP 7.4+ -- **数据库:** MySQL 5.7+ - -### 前端 -- **框架:** Vue 3 -- **UI库:** Element Plus -- **语言:** TypeScript - ---- - -## 🔐 权限说明 - -### 查看权限 -- ✅ 可以查看自己创建的所有处方 -- ✅ 可以查看其他人创建的公开处方 -- ❌ 不能查看其他人创建的私有处方 - -### 编辑权限 -- ✅ 可以编辑自己创建的处方 -- ✅ 可以编辑其他人创建的公开处方 -- ❌ 不能编辑其他人创建的私有处方 - -### 删除权限 -- ✅ 只能删除自己创建的处方 -- ❌ 不能删除其他人创建的处方 - ---- - -## 📝 更新日志 - -### v1.0.0 (2026-03-22) -- ✅ 初始版本发布 -- ✅ 实现核心功能 -- ✅ 完成文档编写 -- ✅ 通过测试验收 - ---- - -## 🆘 技术支持 - -### 获取帮助 - -1. **查看文档** - - 先查看相关文档 - - 查找常见问题 - -2. **问题反馈** - - 提供错误信息截图 - - 提供操作步骤描述 - - 提供浏览器控制台日志 - -3. **联系支持** - - 联系技术支持团队 - ---- - -## 📄 文档导航 - -### 按角色查看 - -#### 👨‍💼 管理员/使用者 -1. [快速开始指南](QUICK_START_PRESCRIPTION_LIBRARY.md) ⭐ -2. [完整使用说明](PRESCRIPTION_LIBRARY_README.md) -3. [功能演示](PRESCRIPTION_LIBRARY_DEMO.md) - -#### 👨‍💻 开发人员 -1. [开发总结](PRESCRIPTION_LIBRARY_SUMMARY.md) ⭐ -2. [文件清单](PRESCRIPTION_LIBRARY_FILES.md) -3. [交付文档](DELIVERY_PACKAGE.md) - -#### 🔧 运维人员 -1. [安装检查清单](INSTALLATION_CHECKLIST.md) ⭐ -2. [测试SQL](TEST_PRESCRIPTION_LIBRARY.sql) - -#### 📋 项目经理 -1. [交付文档](DELIVERY_PACKAGE.md) ⭐ -2. [开发总结](PRESCRIPTION_LIBRARY_SUMMARY.md) - -### 按任务查看 - -#### 🚀 安装部署 -1. [安装检查清单](INSTALLATION_CHECKLIST.md) -2. [快速开始指南](QUICK_START_PRESCRIPTION_LIBRARY.md) - -#### 📖 学习使用 -1. [快速开始指南](QUICK_START_PRESCRIPTION_LIBRARY.md) -2. [功能演示](PRESCRIPTION_LIBRARY_DEMO.md) -3. [完整使用说明](PRESCRIPTION_LIBRARY_README.md) - -#### 🔧 开发维护 -1. [开发总结](PRESCRIPTION_LIBRARY_SUMMARY.md) -2. [文件清单](PRESCRIPTION_LIBRARY_FILES.md) - -#### 📦 项目交付 -1. [交付文档](DELIVERY_PACKAGE.md) -2. [文件清单](PRESCRIPTION_LIBRARY_FILES.md) - ---- - -## ✅ 检查清单 - -### 安装前 -- [ ] 已阅读快速开始指南 -- [ ] 已备份数据库 -- [ ] 已检查环境要求 - -### 安装中 -- [ ] 已执行安装脚本 -- [ ] 已配置菜单权限 -- [ ] 已重启服务 - -### 安装后 -- [ ] 已测试基本功能 -- [ ] 已创建测试处方 -- [ ] 已验证权限控制 - ---- - -## 🎉 开始使用 - -准备好了吗?让我们开始吧! - -**推荐路径:** -1. 阅读 [快速开始指南](QUICK_START_PRESCRIPTION_LIBRARY.md) -2. 运行安装脚本 -3. 创建第一个处方 -4. 探索更多功能 - -**祝使用愉快!** 🚀 - ---- - -**版本:** v1.0.0 -**最后更新:** 2026-03-22 -**状态:** ✅ 已完成 diff --git a/RECORDING_FIX_SUMMARY.md b/RECORDING_FIX_SUMMARY.md deleted file mode 100644 index e84a3b50..00000000 --- a/RECORDING_FIX_SUMMARY.md +++ /dev/null @@ -1,144 +0,0 @@ -# 本地录制修复总结 - -## 问题根源 - -通过分析实际日志发现,本地录制失败的根本原因是: - -1. **录制启动太晚**:1200ms 延迟导致录制在对方挂断后才启动 -2. **SDK 提前销毁轨道**:`stopLocalVideo/stopLocalAudio` 在录制完成前就销毁了视频轨道 -3. **时间窗口极短**:从对方离开到轨道销毁只有 200-300ms - -## 关键时间线(问题场景) - -``` -15:30:51.841 - 对方离开(USER_LEAVE 事件) -15:30:51.xxx - beginStopNow 被调用(但录制还未启动!) -15:30:52.072 - stopLocalVideo/stopLocalAudio(轨道被销毁) -15:30:52.102 - exitRoom -15:30:52.374 - MediaRecorder 已启动(太晚了!) -15:30:52.xxx - 数据块数: 0(因为轨道已销毁) -``` - -## 修复方案 - -### 1. 大幅缩短录制启动延迟 -- **从 1200ms 缩短到 500ms** -- 使用 300ms 间隔快速重试(最多 10 次) -- 目标:在通话接通后 1 秒内启动录制 - -### 2. 在通话结束事件时立即停止录制 -- 监听 `TUICallEvent.USER_LEAVE`、`CALL_END`、`ON_CALL_NOT_CONNECTED` -- 检测到事件时立即调用 `beginStopNow()` -- 抢在 SDK 销毁轨道前完成录制 - -### 3. 缩短停止延迟 -- **从 300ms 缩短到 100ms** -- 减少等待时间,避免 SDK 提前销毁轨道 - -### 4. 智能视频元素检测 -- 首先检查容器内的视频 -- 如果没有,检查 body(TUICallKit Teleport) -- 过滤出有活跃视频流的元素 -- 验证 readyState >= HAVE_CURRENT_DATA - -## 修改的文件 - -1. **admin/src/components/chat-dialog/index.vue** - - 缩短录制启动延迟(1200ms → 500ms) - - 添加快速重试机制(300ms 间隔,最多 10 次) - - 在 `bindEngineRoomEvents` 中添加立即停止逻辑 - - 增强日志输出 - -2. **admin/src/utils/call-local-recorder.ts** - - 缩短停止延迟(300ms → 100ms) - - 增强日志输出 - - 添加详细的状态跟踪 - -3. **LOCAL_RECORDING_DEBUG_GUIDE.md** - - 完整的调试指南 - - 常见问题诊断 - - 测试建议 - -4. **admin/test-local-recording.html** - - 独立的测试工具 - - 浏览器兼容性检测 - - MediaRecorder 功能测试 - -## 测试步骤 - -### 1. 清除浏览器缓存 -确保使用最新的代码。 - -### 2. 打开控制台 -F12 打开开发者工具,切换到 Console 标签。 - -### 3. 发起视频通话 -点击视频通话按钮。 - -### 4. 观察关键日志 - -#### 接通后应该看到(1 秒内): -``` -[chat-dialog] 尝试启动本地录制(第 1 次) -[chat-dialog] 视频元素已就绪,立即启动录制 -[call-local-recorder] MediaRecorder.start() 调用成功 -[call-local-recorder] MediaRecorder 已启动 -[chat-dialog] ✅ 本地录制已成功启动 -``` - -#### 通话过程中应该看到(每 250ms): -``` -[call-local-recorder] 收到数据块,大小: 12345 (总计: 1 块) -[call-local-recorder] 收到数据块,大小: 15678 (总计: 2 块) -``` - -#### 挂断时应该看到: -``` -[chat-dialog] 检测到通话结束事件,立即停止录制 -[call-local-recorder] beginStopNow 被调用 -[call-local-recorder] 准备停止 MediaRecorder,当前状态: recording 已收集数据块: 15 -[call-local-recorder] 生成 Blob: 1234567 bytes -[chat-dialog] 开始上传录制文件 -本地录制已上传并关联到通话记录 -``` - -### 5. 验证结果 -- 检查诊单是否有录制文件 -- 播放录制文件验证内容 - -## 预期效果 - -### 修复前 -- 录制成功率:0% -- 数据块数:0 -- Blob 大小:null - -### 修复后 -- 录制成功率:>95%(通话时长 > 2 秒) -- 数据块数:>= 通话秒数 * 4 -- Blob 大小:>= 通话秒数 * 50KB - -## 注意事项 - -1. **最短通话时长**:建议至少 2 秒,否则可能来不及启动录制 -2. **浏览器兼容性**:Chrome/Edge 85+、Firefox 78+、Safari 14.1+ -3. **网络状况**:网络差时视频流可能不稳定,影响录制质量 -4. **权限问题**:确保浏览器已授予摄像头和麦克风权限 - -## 如果问题仍然存在 - -1. 查看完整的控制台日志 -2. 确认录制是否在接通后 1 秒内启动 -3. 确认是否有 "收到数据块" 日志 -4. 检查通话时长是否 > 2 秒 -5. 参考 `LOCAL_RECORDING_DEBUG_GUIDE.md` 进行详细诊断 - -## 进一步优化方向 - -如果修复后仍有问题,可以考虑: - -1. **进一步缩短启动延迟**:300ms 或更短 -2. **使用 MutationObserver**:监听视频元素添加,立即启动 -3. **添加录制状态监控**:定期检查 MediaRecorder 状态 -4. **添加降级方案**:本地录制失败时提示用户 -5. **添加手动控制**:让用户手动开始/停止录制 diff --git a/REGION_API_ENV_VARIABLE_UPDATE.md b/REGION_API_ENV_VARIABLE_UPDATE.md deleted file mode 100644 index 4d339344..00000000 --- a/REGION_API_ENV_VARIABLE_UPDATE.md +++ /dev/null @@ -1,288 +0,0 @@ -# Region API Environment Variable Update (省市区API环境变量更新) - -## Change Summary -Updated the region data API URL to use the `VITE_APP_BASE_URL` environment variable instead of a hardcoded URL, making it more flexible and maintainable across different environments. - -## Changes Made - -### 1. Updated prescription/index.vue -**File**: `admin/src/views/consumer/prescription/index.vue` - -**Before:** -```typescript -const apiUrl = import.meta.env.DEV - ? '/api-proxy/area/ChinaCitys.json' - : 'https://admin.zhenyangtang.com.cn/area/ChinaCitys.json' -``` - -**After:** -```typescript -const apiUrl = import.meta.env.DEV - ? '/api-proxy/area/ChinaCitys.json' - : `${import.meta.env.VITE_APP_BASE_URL}area/ChinaCitys.json` -``` - -### 2. Updated prescription/order_list.vue -**File**: `admin/src/views/consumer/prescription/order_list.vue` - -Applied the same change. - -## Environment Configuration - -### Production Environment -**File**: `admin/.env.production` -```env -VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn/' -``` - -### Development Environment -**File**: `admin/.env.development` (if exists) -```env -VITE_APP_BASE_URL='http://localhost:8080/' -``` - -## How It Works - -### URL Construction -The production URL is now constructed dynamically: -```typescript -`${import.meta.env.VITE_APP_BASE_URL}area/ChinaCitys.json` -``` - -With `VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn/'`, this becomes: -``` -https://cs.zhenyangtang.com.cn/area/ChinaCitys.json -``` - -### Development vs Production - -**Development Mode:** -- Uses proxy: `/api-proxy/area/ChinaCitys.json` -- Vite proxy rewrites to external URL -- Avoids CORS issues - -**Production Mode:** -- Uses: `${VITE_APP_BASE_URL}area/ChinaCitys.json` -- Resolves to: `https://cs.zhenyangtang.com.cn/area/ChinaCitys.json` -- Direct fetch from configured base URL - -## Benefits - -### 1. Environment Flexibility -- ✅ Different URLs for different environments (dev, staging, production) -- ✅ No code changes needed when switching environments -- ✅ Single source of truth for base URL - -### 2. Easier Deployment -- ✅ Change URL by updating `.env.production` file -- ✅ No need to modify source code -- ✅ Supports multiple deployment targets - -### 3. Consistency -- ✅ Uses same base URL as other API calls -- ✅ Maintains consistency across the application -- ✅ Easier to manage and maintain - -### 4. Configuration Management -- ✅ Centralized configuration in environment files -- ✅ Easy to override for different environments -- ✅ Follows best practices for environment-specific settings - -## Environment Files - -### Structure -``` -admin/ -├── .env.development # Development environment -├── .env.production # Production environment -├── .env.staging # Staging environment (optional) -└── .env.local # Local overrides (gitignored) -``` - -### Example Configurations - -**Development** (`.env.development`): -```env -VITE_APP_BASE_URL='http://localhost:8080/' -VITE_APP_REQUEST_TIMEOUT=60000 -``` - -**Staging** (`.env.staging`): -```env -VITE_APP_BASE_URL='https://staging.zhenyangtang.com.cn/' -VITE_APP_REQUEST_TIMEOUT=60000 -``` - -**Production** (`.env.production`): -```env -VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn/' -VITE_APP_REQUEST_TIMEOUT=60000 -``` - -## Testing Steps - -### 1. Verify Environment Variable -Check that the environment variable is loaded: -```typescript -console.log('Base URL:', import.meta.env.VITE_APP_BASE_URL) -``` - -Expected output in production: -``` -Base URL: https://cs.zhenyangtang.com.cn/ -``` - -### 2. Test Development Mode -1. Run development server: - ```bash - cd admin - npm run dev - ``` -2. Open browser console -3. Check constructed URL uses proxy -4. Verify region selector loads data - -### 3. Test Production Build -1. Build for production: - ```bash - cd admin - npm run build - ``` -2. Check built files use production URL -3. Serve and test: - ```bash - npm run preview - ``` -4. Verify region selector loads from: `https://cs.zhenyangtang.com.cn/area/ChinaCitys.json` - -### 4. Test Different Environments -Create a staging build: -```bash -npm run build -- --mode staging -``` - -Verify it uses the staging URL from `.env.staging`. - -## URL Resolution Examples - -### With Trailing Slash (Current) -```typescript -VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn/' -Result: https://cs.zhenyangtang.com.cn/area/ChinaCitys.json ✅ -``` - -### Without Trailing Slash -```typescript -VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn' -Result: https://cs.zhenyangtang.com.cnarea/ChinaCitys.json ❌ -``` - -**Important**: Ensure `VITE_APP_BASE_URL` always ends with `/` - -### Handling Missing Trailing Slash -If you want to be defensive, you could add: -```typescript -const baseUrl = import.meta.env.VITE_APP_BASE_URL.endsWith('/') - ? import.meta.env.VITE_APP_BASE_URL - : `${import.meta.env.VITE_APP_BASE_URL}/` - -const apiUrl = import.meta.env.DEV - ? '/api-proxy/area/ChinaCitys.json' - : `${baseUrl}area/ChinaCitys.json` -``` - -## Troubleshooting - -### Issue: Region selector not loading in production -**Check:** -1. Verify `.env.production` file exists -2. Check `VITE_APP_BASE_URL` is set correctly -3. Ensure URL ends with `/` -4. Verify file exists at: `${VITE_APP_BASE_URL}area/ChinaCitys.json` - -### Issue: 404 Not Found -**Solution:** -1. Check the constructed URL in browser Network tab -2. Verify the file is accessible at that URL -3. Check server configuration for the path - -### Issue: CORS Error -**Solution:** -1. Ensure server has proper CORS headers -2. Check `Access-Control-Allow-Origin` header -3. Verify the domain is allowed - -## Server Configuration - -### File Location -Ensure the file exists at: -``` -https://cs.zhenyangtang.com.cn/area/ChinaCitys.json -``` - -### Nginx Configuration Example -```nginx -location /area/ { - alias /path/to/public/area/; - add_header Access-Control-Allow-Origin *; - add_header Cache-Control "public, max-age=86400"; -} -``` - -### Apache Configuration Example -```apache - - Header set Access-Control-Allow-Origin "*" - Header set Cache-Control "public, max-age=86400" - -``` - -## Related Files - -### Frontend: -- `admin/src/views/consumer/prescription/index.vue` - - Updated to use `VITE_APP_BASE_URL` -- `admin/src/views/consumer/prescription/order_list.vue` - - Updated to use `VITE_APP_BASE_URL` - -### Configuration: -- `admin/.env.production` - - Contains `VITE_APP_BASE_URL='https://cs.zhenyangtang.com.cn/'` -- `admin/.env.development` - - Should contain development base URL -- `admin/vite.config.ts` - - Proxy configuration for development - -## Best Practices - -### 1. Always Use Environment Variables -✅ Do: `${import.meta.env.VITE_APP_BASE_URL}path/to/resource` -❌ Don't: `https://hardcoded-domain.com/path/to/resource` - -### 2. Ensure Trailing Slashes -✅ Do: `VITE_APP_BASE_URL='https://example.com/'` -❌ Don't: `VITE_APP_BASE_URL='https://example.com'` - -### 3. Use Consistent Naming -All Vite environment variables should start with `VITE_` to be exposed to the client. - -### 4. Document Environment Variables -Keep a `.env.example` file with all required variables: -```env -# Base URL for API and static resources -VITE_APP_BASE_URL='https://example.com/' - -# Request timeout in milliseconds -VITE_APP_REQUEST_TIMEOUT=60000 -``` - -## Summary - -✅ Replaced hardcoded URL with environment variable -✅ Uses `VITE_APP_BASE_URL` from `.env.production` -✅ More flexible for different environments -✅ Easier to deploy and maintain -✅ Consistent with other API configurations -✅ Applied to both prescription index and order list views - -The region API now uses the configured base URL, making it easier to manage across different environments! diff --git a/REGION_API_PRODUCTION_FIX.md b/REGION_API_PRODUCTION_FIX.md deleted file mode 100644 index 7b3795ed..00000000 --- a/REGION_API_PRODUCTION_FIX.md +++ /dev/null @@ -1,243 +0,0 @@ -# Region API Production URL Fix (省市区API生产环境URL修复) - -## Issue -The region data API was using a proxy path (`/api-proxy/area/ChinaCitys.json`) which only works in development mode. After building for production, the proxy is not available, causing the region selector to fail. - -## Root Cause -Vite's proxy configuration (`vite.config.ts`) only works during development (`npm run dev`). When the application is built for production (`npm run build`), the proxy is not included in the build output, and the relative path `/api-proxy/...` becomes invalid. - -## Solution -Use environment detection to switch between proxy URL (development) and full URL (production). - -## Changes Made - -### 1. Updated prescription/index.vue -**File**: `admin/src/views/consumer/prescription/index.vue` - -Changed from: -```typescript -const response = await fetch('/api-proxy/area/ChinaCitys.json') -``` - -To: -```typescript -// 开发环境使用代理,生产环境使用完整URL -const apiUrl = import.meta.env.DEV - ? '/api-proxy/area/ChinaCitys.json' - : 'https://admin.zhenyangtang.com.cn/area/ChinaCitys.json' - -const response = await fetch(apiUrl) -``` - -### 2. Updated prescription/order_list.vue -**File**: `admin/src/views/consumer/prescription/order_list.vue` - -Applied the same change to the order list view. - -## How It Works - -### Environment Detection -Uses Vite's built-in environment variable `import.meta.env.DEV`: -- **Development** (`npm run dev`): `import.meta.env.DEV = true` -- **Production** (`npm run build`): `import.meta.env.DEV = false` - -### URL Selection Logic -```typescript -const apiUrl = import.meta.env.DEV - ? '/api-proxy/area/ChinaCitys.json' // Development: Use proxy - : 'https://admin.zhenyangtang.com.cn/area/ChinaCitys.json' // Production: Use full URL -``` - -### Development Mode -- Uses proxy path: `/api-proxy/area/ChinaCitys.json` -- Vite proxy rewrites to: `https://admin.zhenyangtang.com.cn/area/ChinaCitys.json` -- Avoids CORS issues during development - -### Production Mode -- Uses full URL: `https://admin.zhenyangtang.com.cn/area/ChinaCitys.json` -- Direct fetch from the actual server -- No proxy needed - -## Benefits - -### 1. Works in Both Environments -- ✅ Development: Uses proxy for CORS-free development -- ✅ Production: Uses direct URL for deployed application - -### 2. No Build Configuration Changes -- No need to modify build scripts -- No environment-specific builds -- Single codebase for all environments - -### 3. Automatic Detection -- Automatically switches based on environment -- No manual configuration needed -- No risk of using wrong URL - -### 4. Maintains CORS Handling -- Development: Proxy handles CORS -- Production: Server should have proper CORS headers - -## Testing Steps - -### 1. Test Development Mode -1. Run development server: - ```bash - cd admin - npm run dev - ``` -2. Open browser console -3. Check for log: `开始加载省市区数据...` -4. Verify it uses proxy URL -5. Check region selector loads data - -### 2. Test Production Build -1. Build for production: - ```bash - cd admin - npm run build - ``` -2. Serve the built files: - ```bash - npm run preview - # or deploy to production server - ``` -3. Open browser console -4. Check for log: `开始加载省市区数据...` -5. Verify it uses full URL -6. Check region selector loads data - -### 3. Verify URL in Network Tab -**Development:** -- Request URL: `http://localhost:5173/api-proxy/area/ChinaCitys.json` -- Proxied to: `https://admin.zhenyangtang.com.cn/area/ChinaCitys.json` - -**Production:** -- Request URL: `https://admin.zhenyangtang.com.cn/area/ChinaCitys.json` -- Direct request (no proxy) - -### 4. Test Fallback Data -If API fails in either environment, verify fallback data is used: -```typescript -regionOptions.value = [ - { - name: '四川省', - children: [...] - }, - ... -] -``` - -## Alternative Solutions Considered - -### Option 1: Environment Variables (Not Chosen) -```typescript -const apiUrl = import.meta.env.VITE_REGION_API_URL || '/api-proxy/area/ChinaCitys.json' -``` -**Pros**: More flexible for different environments -**Cons**: Requires environment-specific configuration files - -### Option 2: Relative Path (Not Chosen) -```typescript -const apiUrl = '/area/ChinaCitys.json' -``` -**Pros**: Simple -**Cons**: Requires copying file to public folder, increases bundle size - -### Option 3: Backend API Endpoint (Not Chosen) -Create a backend endpoint to serve region data -**Pros**: More control, can cache, can update without frontend deploy -**Cons**: Requires backend changes, adds server load - -### Option 4: Current Solution (Chosen) ✅ -Use environment detection with conditional URL -**Pros**: -- Simple implementation -- No additional configuration -- Works in both environments -- No backend changes needed -**Cons**: -- Hardcoded production URL (acceptable for this use case) - -## Production Server Requirements - -### CORS Headers -The production server hosting `ChinaCitys.json` should have proper CORS headers: - -``` -Access-Control-Allow-Origin: * -# or specific domain: -Access-Control-Allow-Origin: https://your-admin-domain.com -``` - -### File Availability -Ensure the file is accessible at: -``` -https://admin.zhenyangtang.com.cn/area/ChinaCitys.json -``` - -### Alternative: Use Backend Proxy -If CORS is an issue in production, consider creating a backend endpoint: -```php -// server/app/adminapi/controller/RegionController.php -public function getChinaCitys() -{ - $url = 'https://admin.zhenyangtang.com.cn/area/ChinaCitys.json'; - $data = file_get_contents($url); - return json(['data' => json_decode($data, true)]); -} -``` - -Then update frontend to use: -```typescript -const apiUrl = import.meta.env.DEV - ? '/api-proxy/area/ChinaCitys.json' - : '/adminapi/region/getChinaCitys' // Backend endpoint -``` - -## Related Files - -### Frontend: -- `admin/src/views/consumer/prescription/index.vue` - - Updated `loadRegionData()` function -- `admin/src/views/consumer/prescription/order_list.vue` - - Updated `loadRegionData()` function - -### Configuration: -- `admin/vite.config.ts` - - Proxy configuration (development only) - -### Documentation: -- `REGION_DATA_SETUP.md` (should be updated to mention production URL) - -## Environment Variables Reference - -### Vite Built-in Variables: -- `import.meta.env.DEV` - Boolean, true in development -- `import.meta.env.PROD` - Boolean, true in production -- `import.meta.env.MODE` - String, 'development' or 'production' - -### Usage Example: -```typescript -if (import.meta.env.DEV) { - console.log('Running in development mode') -} - -if (import.meta.env.PROD) { - console.log('Running in production mode') -} - -console.log('Current mode:', import.meta.env.MODE) -``` - -## Summary - -✅ Fixed production build issue with region API -✅ Uses proxy in development for CORS-free development -✅ Uses direct URL in production for deployed application -✅ Automatic environment detection -✅ No build configuration changes needed -✅ Maintains fallback data for both environments -✅ Applied to both prescription index and order list views - -The region selector now works correctly in both development and production environments! diff --git a/REGION_DATABASE_SAVE_FIX.md b/REGION_DATABASE_SAVE_FIX.md deleted file mode 100644 index c78e0267..00000000 --- a/REGION_DATABASE_SAVE_FIX.md +++ /dev/null @@ -1,170 +0,0 @@ -# Region Database Save Fix (省市区数据库保存修复) - -## Issue -The region fields (province, city, district) were being sent in the API request payload but were not being saved to the database. - -## Root Cause Analysis - -### Frontend Issue -The frontend was sending the wrong parameter names: -- Sent: `province`, `city`, `district` -- Expected by backend: `shipping_province`, `shipping_city`, `shipping_district` - -### Backend Issue -The `create()` method in `PrescriptionOrderLogic.php` was not handling the province/city/district fields at all, even though the `edit()` method had the code to handle them. - -## Changes Made - -### 1. Frontend - Fixed Parameter Names -**File**: `admin/src/views/consumer/prescription/index.vue` - -Changed the payload to use the correct parameter names: - -```typescript -// Before: -payload.province = createOrderForm.region[0] -payload.city = createOrderForm.region[1] -payload.district = createOrderForm.region[2] - -// After: -payload.shipping_province = createOrderForm.region[0] -payload.shipping_city = createOrderForm.region[1] -payload.shipping_district = createOrderForm.region[2] -``` - -### 2. Backend - Added Region Fields to create() Method -**File**: `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -Added code to handle province/city/district fields in the `create()` method: - -```php -// 处理省市区字段 -if (isset($params['shipping_province'])) { - $order->shipping_province = (string) $params['shipping_province']; -} -if (isset($params['shipping_city'])) { - $order->shipping_city = (string) $params['shipping_city']; -} -if (isset($params['shipping_district'])) { - $order->shipping_district = (string) $params['shipping_district']; -} -``` - -This matches the existing code in the `edit()` method. - -## Data Flow (After Fix) - -### Create Order Flow: -1. User selects region: 北京市 → 北京市 → 丰台区 -2. Frontend stores: `createOrderForm.region = ['北京市', '北京市', '丰台区']` -3. Frontend sends payload: - ```json - { - "shipping_province": "北京市", - "shipping_city": "北京市", - "shipping_district": "丰台区", - ...other fields - } - ``` -4. Backend `create()` method processes: - ```php - $order->shipping_province = "北京市"; - $order->shipping_city = "北京市"; - $order->shipping_district = "丰台区"; - ``` -5. Database columns are populated: - ``` - shipping_province: 北京市 - shipping_city: 北京市 - shipping_district: 丰台区 - ``` - -## Testing Steps - -### 1. Test Create Order with Region -1. Open prescription list page -2. Click "创建订单" on any prescription -3. Fill in required fields: - - Select patient - - Enter recipient name and phone - - **Select region**: 北京市 → 北京市 → 丰台区 - - Enter detailed address -4. Click "创建业务订单" -5. Check browser Network tab: - - Verify payload includes: - ```json - { - "shipping_province": "北京市", - "shipping_city": "北京市", - "shipping_district": "丰台区" - } - ``` - -### 2. Verify Database -After creating the order, check the database: - -```sql -SELECT - id, - order_no, - shipping_province, - shipping_city, - shipping_district, - shipping_address -FROM tcm_prescription_order -ORDER BY id DESC -LIMIT 1; -``` - -Expected result: -``` -| id | order_no | shipping_province | shipping_city | shipping_district | shipping_address | -|----|----------|-------------------|---------------|-------------------|------------------| -| XX | POXXXXXX | 北京市 | 北京市 | 丰台区 | 顶顶顶 | -``` - -### 3. Test Different Regions -Test with various regions to ensure all work correctly: -- 四川省 → 成都市 → 武侯区 -- 上海市 → 上海市 → 浦东新区 -- 广东省 → 深圳市 → 南山区 - -### 4. Test Edit Order -1. Edit an existing order -2. Change the region -3. Save and verify the database is updated - -## Database Schema - -The database should have these columns (from `add_shipping_region_fields.sql`): - -```sql -ALTER TABLE `tcm_prescription_order` -ADD COLUMN `shipping_province` VARCHAR(50) DEFAULT NULL COMMENT '收货省份' AFTER `shipping_address`, -ADD COLUMN `shipping_city` VARCHAR(50) DEFAULT NULL COMMENT '收货城市' AFTER `shipping_province`, -ADD COLUMN `shipping_district` VARCHAR(50) DEFAULT NULL COMMENT '收货区县' AFTER `shipping_city`; -``` - -## Related Files - -### Frontend: -- `admin/src/views/consumer/prescription/index.vue` - - Changed parameter names in `submitCreateOrderFromPrescription()` - -### Backend: -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - - Added province/city/district handling in `create()` method - - Already had province/city/district handling in `edit()` method - -### Database: -- `add_shipping_region_fields.sql` (migration file) - -## Summary - -✅ Fixed frontend parameter names: `province` → `shipping_province`, etc. -✅ Added province/city/district handling to backend `create()` method -✅ Backend `edit()` method already had the correct code -✅ Database columns will now be populated correctly on order creation -✅ Database columns will be updated correctly on order edit - -The region fields are now properly saved to the database for both create and edit operations! diff --git a/REGION_DATA_SETUP.md b/REGION_DATA_SETUP.md deleted file mode 100644 index 83367146..00000000 --- a/REGION_DATA_SETUP.md +++ /dev/null @@ -1,72 +0,0 @@ -# 省市区数据加载说明 - -## 问题 -省市区下拉框显示为空 - -## 原因 -1. Vite代理配置需要重启开发服务器才能生效 -2. 如果代理加载失败,会使用后备数据 - -## 解决步骤 - -### 1. 重启开发服务器 -```bash -# 停止当前运行的开发服务器 (Ctrl+C) -# 然后重新启动 -cd admin -npm run dev -``` - -### 2. 检查控制台日志 -打开浏览器开发者工具的Console标签,应该能看到: -- "开始加载省市区数据..." -- "响应状态: 200" -- "加载的数据条数: XX" -- "regionOptions 已设置,条数: XX" - -### 3. 如果代理失败 -如果看到错误信息,会自动使用后备数据,包含: -- 四川省(成都市、绵阳市) -- 北京市 -- 上海市 -- 广东省(广州市、深圳市) - -## 配置说明 - -### Vite代理配置 (admin/vite.config.ts) -```typescript -server: { - proxy: { - '/api-proxy': { - target: 'https://admin.zhenyangtang.com.cn', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api-proxy/, '') - } - } -} -``` - -### 前端请求 (admin/src/views/consumer/prescription/index.vue) -```javascript -const response = await fetch('/api-proxy/area/ChinaCitys.json') -``` - -## 生产环境配置 - -生产环境需要在nginx配置代理: - -```nginx -location /api-proxy/ { - proxy_pass https://admin.zhenyangtang.com.cn/; - proxy_set_header Host admin.zhenyangtang.com.cn; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -} -``` - -## 测试 -1. 打开创建订单对话框 -2. 查看"省市区"下拉框 -3. 应该能看到省份列表 -4. 选择省份后能看到城市列表 -5. 选择城市后能看到区县列表 diff --git a/REGION_FIELD_SUBMISSION_FIX.md b/REGION_FIELD_SUBMISSION_FIX.md deleted file mode 100644 index abccae67..00000000 --- a/REGION_FIELD_SUBMISSION_FIX.md +++ /dev/null @@ -1,153 +0,0 @@ -# Region Field Submission Fix (省市区字段提交修复) - -## Issue -The region selector (省市区) in the create order form was not submitting the selected province, city, and district values to the database. - -## Root Cause -1. The cascader component had `emitPath: false`, which only returned the last selected value (district name) instead of the full path -2. The `submitCreateOrderFromPrescription()` function was not including the region fields in the payload sent to the backend - -## Changes Made - -### 1. Updated Cascader Configuration -**File**: `admin/src/views/consumer/prescription/index.vue` - -Changed `emitPath` from `false` to `true` to get the full path array: - -```typescript -// Before: -:props="{ value: 'name', label: 'name', children: 'children', emitPath: false, checkStrictly: false }" - -// After: -:props="{ value: 'name', label: 'name', children: 'children', emitPath: true, checkStrictly: false }" -``` - -**Effect**: Now `createOrderForm.region` will contain an array like `['四川省', '成都市', '锦江区']` instead of just `'锦江区'` - -### 2. Updated Form Submission -**File**: `admin/src/views/consumer/prescription/index.vue` - -Added code to extract province, city, and district from the region array and include them in the payload: - -```typescript -// 添加省市区字段 -if (createOrderForm.region && createOrderForm.region.length >= 3) { - payload.province = createOrderForm.region[0] - payload.city = createOrderForm.region[1] - payload.district = createOrderForm.region[2] -} -``` - -### 3. Updated handleRegionChange Comment -Added clarification that the value is now an array of the full path. - -## Data Flow - -### Before Fix: -1. User selects: 四川省 → 成都市 → 锦江区 -2. `createOrderForm.region` = `'锦江区'` (only last value) -3. Payload sent to backend: **No province/city/district fields** -4. Database: province, city, district columns remain empty - -### After Fix: -1. User selects: 四川省 → 成都市 → 锦江区 -2. `createOrderForm.region` = `['四川省', '成都市', '锦江区']` (full path) -3. Payload sent to backend: - ```json - { - "province": "四川省", - "city": "成都市", - "district": "锦江区", - ...other fields - } - ``` -4. Database: province, city, district columns are populated correctly - -## Testing Steps - -### 1. Test Region Selection and Submission -1. Open the prescription list page -2. Click "创建订单" button on any prescription -3. Fill in the required fields: - - Select a patient (患者) - - Enter recipient name (收货人) - - Enter recipient phone (收货手机) - - **Select province/city/district** (省市区): e.g., 四川省 → 成都市 → 锦江区 - - Enter detailed address (详细地址) -4. Click "创建业务订单" button -5. Check browser Network tab: - - Find the API request to create order - - Verify the payload includes: - ```json - { - "province": "四川省", - "city": "成都市", - "district": "锦江区" - } - ``` - -### 2. Verify Database Storage -After creating an order, check the database: - -```sql -SELECT id, order_no, province, city, district, shipping_address -FROM tcm_prescription_order -ORDER BY id DESC -LIMIT 1; -``` - -Expected result: -``` -| id | order_no | province | city | district | shipping_address | -|----|----------|----------|--------|----------|------------------| -| XX | XXXXXX | 四川省 | 成都市 | 锦江区 | 中关村... | -``` - -### 3. Test Different Regions -Test with various regions to ensure the data structure works correctly: -- 北京市 → 北京市 → 朝阳区 -- 上海市 → 上海市 → 浦东新区 -- 广东省 → 深圳市 → 南山区 - -### 4. Test Edge Cases -1. **No region selected**: Should still allow submission (if not required) -2. **Only province selected**: Should not submit incomplete data -3. **Clear selection**: Click the clear button (×) and verify region is cleared - -## Backend Verification - -The backend should already have the database columns from the migration `add_shipping_region_fields.sql`: -- `province` (varchar) -- `city` (varchar) -- `district` (varchar) - -If the backend validation or logic needs to be updated, check: -- `server/app/adminapi/validate/tcm/PrescriptionOrderValidate.php` -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -## Console Debugging - -If you need to debug, check the browser console for the log message: -``` -选择的省市区: ['四川省', '成都市', '锦江区'] -``` - -This confirms the cascader is emitting the correct data structure. - -## Related Files - -### Frontend: -- `admin/src/views/consumer/prescription/index.vue` (main changes) - -### Backend: -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` (should handle province/city/district) -- `add_shipping_region_fields.sql` (database migration) - -## Summary - -✅ Changed cascader `emitPath` to `true` to get full path array -✅ Added province/city/district extraction in form submission -✅ Payload now includes all three region fields -✅ Database columns will be populated correctly - -The region selector now properly submits province, city, and district values to the database. diff --git a/REGION_SELECTOR_AND_DOSAGE_FIX.md b/REGION_SELECTOR_AND_DOSAGE_FIX.md deleted file mode 100644 index 2cb8ca3a..00000000 --- a/REGION_SELECTOR_AND_DOSAGE_FIX.md +++ /dev/null @@ -1,199 +0,0 @@ -# Region Selector and Dosage Fields Fix - -## Issues Identified - -### 1. Region Selector Empty (省市区下拉为空) -**Problem**: The region cascader dropdown is empty when opening the create order form. - -**Root Cause**: The Vite proxy configuration is correct, but the development server needs to be restarted for the proxy to take effect. - -**Solution**: -- The proxy configuration in `admin/vite.config.ts` is already set up correctly: -```typescript -proxy: { - '/api-proxy': { - target: 'https://admin.zhenyangtang.com.cn', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api-proxy/, '') - } -} -``` -- The `transformRegionData()` function correctly converts the API format to el-cascader format -- **ACTION REQUIRED**: Restart the development server - -### 2. Dosage Dropdown Not Showing Selected Value on First Load -**Problem**: When editing a prescription, the dosage dropdown (用量) appears empty on first load, but shows correctly when reopening the dialog. - -**Root Cause**: Timing issue with the `isLoadingData` flag. The `nextTick()` callback was executing before all data was fully rendered. - -**Solution**: Changed from `nextTick()` to `setTimeout(..., 0)` to ensure all data is rendered before re-enabling the watch function. - -**Changed in**: `admin/src/views/consumer/prescription/index.vue` -```typescript -// Before: -nextTick(() => { - isLoadingData.value = false -}) - -// After: -setTimeout(() => { - isLoadingData.value = false -}, 0) -``` - -### 3. Dosage Fields Not Saving to Database -**Problem**: User reported that dosage fields (用量、单位、代煎、每贴出包数) are not being saved to the database. - -**Analysis**: -- ✅ Backend validation (`PrescriptionValidate.php`) already includes all dosage fields in `sceneAdd()` and `sceneEdit()` -- ✅ Frontend component (`tcm-prescription/index.vue`) includes all fields in `handleSave()` -- ✅ Frontend prescription list view (`prescription/index.vue`) includes all fields in form submission -- ✅ Watch function properly resets values when prescription type changes -- ✅ `isLoadingData` flag prevents watch from overwriting loaded data - -**Verification Needed**: -1. Check if database migration was executed: `add_prescription_dosage_fields.sql` -2. Check browser console for any API errors when saving -3. Verify that the API response shows the fields were saved - -## Files Modified - -### 1. admin/src/views/consumer/prescription/index.vue -- Changed `nextTick()` to `setTimeout(..., 0)` in `handleEdit()` function (line ~2187) - -## Files Already Correct (No Changes Needed) - -### 1. admin/vite.config.ts -- Proxy configuration is correct - -### 2. admin/src/views/consumer/prescription/index.vue -- `transformRegionData()` function correctly converts API format -- `loadRegionData()` function properly fetches and transforms data -- Watch function includes `isLoadingData` flag to prevent overwriting -- All dosage fields are included in form submission - -### 3. admin/src/components/tcm-prescription/index.vue -- All dosage fields are included in `handleSave()` -- Watch function properly handles prescription type changes -- bags_per_dose field is displayed for 饮片 type - -### 4. server/app/adminapi/validate/tcm/PrescriptionValidate.php -- All dosage fields are included in `sceneAdd()` and `sceneEdit()` - -## Testing Steps - -### Test 1: Region Selector -1. **Restart the development server**: - ```bash - # Stop current server (Ctrl+C) - cd admin - npm run dev - ``` -2. Open the create order form (创建订单) -3. Click on the "省市区" cascader -4. Verify that all provinces are displayed -5. Select a province and verify cities are displayed -6. Select a city and verify districts are displayed -7. Check browser console for any errors - -### Test 2: Dosage Dropdown Display -1. Create a new prescription with type "饮片" and dosage "100ml" -2. Save the prescription -3. **Refresh the page** (F5) -4. Edit the same prescription -5. Verify that the dosage dropdown shows "100ml" immediately on first load -6. Close and reopen the edit dialog -7. Verify it still shows "100ml" - -### Test 3: Dosage Fields Saving -1. Create a new prescription: - - Type: 饮片 - - Dosage: 150ml - - Decoction: 代煎 - - Bags per dose: 3包 -2. Save the prescription -3. Check browser Network tab for the API request payload -4. Verify the payload includes: - ```json - { - "dosage_amount": 150, - "dosage_unit": "ml", - "need_decoction": true, - "bags_per_dose": 3 - } - ``` -5. Refresh the page and edit the prescription -6. Verify all fields display the saved values - -### Test 4: Different Prescription Types -1. Test 浓缩水丸: - - Dosage: 1-10g dropdown - - No decoction option - - No bags_per_dose field -2. Test 饮片: - - Dosage: 50-250ml dropdown - - Decoction radio buttons (代煎/不代煎) - - Bags per dose: 1-9包 dropdown -3. Test 其他类型 (颗粒、丸剂、散剂、膏方、汤剂): - - Dosage: Free input with decimal support - - No decoction option - - No bags_per_dose field - -## Database Verification - -If dosage fields are still not saving, verify the database schema: - -```sql --- Check if columns exist -DESCRIBE tcm_prescription; - --- Should show these columns: --- dosage_amount (decimal or varchar) --- dosage_unit (varchar) --- need_decoction (tinyint) --- bags_per_dose (int) --- times_per_day (int) - --- If columns are missing, run the migration: -SOURCE add_prescription_dosage_fields.sql; -``` - -## Troubleshooting - -### Region Selector Still Empty After Restart -1. Check browser console for CORS errors -2. Verify the proxy is working: - ```bash - # In browser console: - fetch('/api-proxy/area/ChinaCitys.json').then(r => r.json()).then(console.log) - ``` -3. Check if the API endpoint is accessible: - ```bash - curl https://admin.zhenyangtang.com.cn/area/ChinaCitys.json - ``` - -### Dosage Dropdown Still Empty on First Load -1. Check browser console for errors -2. Verify `dosage_amount` is a number in the database (not string) -3. Add console.log in `handleEdit()` to debug: - ```typescript - console.log('dosage_amount from DB:', row.dosage_amount, typeof row.dosage_amount) - console.log('dosage_amount after conversion:', editForm.dosage_amount, typeof editForm.dosage_amount) - ``` - -### Dosage Fields Not Saving -1. Check browser Network tab for API errors -2. Verify database migration was executed -3. Check backend logs for validation errors -4. Verify the API endpoint is receiving the fields: - ```php - // In PrescriptionLogic.php - Log::write('Prescription params: ' . json_encode($params)); - ``` - -## Summary - -- ✅ Region selector: Proxy configured, needs server restart -- ✅ Dosage dropdown: Fixed timing issue with `setTimeout` -- ✅ Dosage fields saving: All code is correct, verify database migration -- ✅ bags_per_dose: Already implemented in both component and list view diff --git a/REGION_SELECTOR_FEATURE.md b/REGION_SELECTOR_FEATURE.md deleted file mode 100644 index b74e3a81..00000000 --- a/REGION_SELECTOR_FEATURE.md +++ /dev/null @@ -1,357 +0,0 @@ -# 省市区选择器功能说明 - -## 功能描述 - -在创建处方业务订单时,添加省市区级联选择器,方便用户选择收货地址的省市区信息。 - -## 修改内容 - -### 1. 表单界面(admin/src/views/consumer/prescription/index.vue) - -#### 添加省市区选择器(第 792-810 行) - -```vue - - - - - - - - - - - - - - - -``` - -**组件配置说明:** -- `v-model="createOrderForm.region"`:绑定选中的省市区数组 -- `:options="regionOptions"`:省市区数据源 -- `value: 'name'`:使用 name 字段作为值 -- `label: 'name'`:使用 name 字段作为显示文本 -- `emitPath: false`:只返回最后一级的值(区/县) -- `checkStrictly: false`:必须选择到最后一级 -- `clearable`:可清空 -- `filterable`:可搜索 - -### 2. 数据模型更新(第 1175-1195 行) - -```typescript -const createOrderForm = reactive({ - patient_id: '' as number | string, - recipient_name: '', - recipient_phone: '', - region: [] as string[], // 新增:省市区数组 - shipping_address: '', // 改为详细地址 - // ... 其他字段 -}) -``` - -### 3. 省市区数据(第 1195-1340 行) - -添加了常用省市区数据: -- 四川省(成都市、绵阳市) -- 北京市 -- 上海市 -- 广东省(广州市、深圳市) - -```typescript -const regionOptions = ref([ - { - name: '四川省', - children: [ - { - name: '成都市', - children: [ - { name: '锦江区' }, - { name: '青羊区' }, - // ... 更多区县 - ] - } - ] - }, - // ... 更多省份 -]) -``` - -### 4. 事件处理方法 - -```typescript -const handleRegionChange = (value: string[]) => { - console.log('选择的省市区:', value) -} -``` - -## 使用说明 - -### 用户操作流程 - -1. 点击"创建业务订单"按钮 -2. 选择诊单患者 -3. 填写收货人和收货手机 -4. **点击"省市区"选择器** -5. 依次选择:省 → 市 → 区/县 -6. 在"详细地址"中填写街道、门牌号等信息 -7. 填写其他订单信息 -8. 提交订单 - -### 界面效果 - -**编辑时:** -``` -┌─────────────────────────────────────────────────┐ -│ 收货人: [张三] 收货手机: [138****0000] │ -├─────────────────────────────────────────────────┤ -│ 省市区: [四川省 / 成都市 / 双流区 ▼] │ -├─────────────────────────────────────────────────┤ -│ 详细地址: │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ 黄甲街道黄龙大道二段280号 │ │ -│ └─────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────┘ -``` - -**完整地址组合:** -``` -四川省成都市双流区黄甲街道黄龙大道二段280号 -``` - -## 后端支持 - -### 数据库字段建议 - -如果需要单独存储省市区信息,可以添加以下字段: - -```sql --- 在 prescription_order 表中添加省市区字段 -ALTER TABLE `zyt_prescription_order` -ADD COLUMN `province` VARCHAR(50) DEFAULT '' COMMENT '省份' AFTER `recipient_phone`, -ADD COLUMN `city` VARCHAR(50) DEFAULT '' COMMENT '城市' AFTER `province`, -ADD COLUMN `district` VARCHAR(50) DEFAULT '' COMMENT '区/县' AFTER `city`; - --- 或者保持现有的 shipping_address 字段,前端组合完整地址 -``` - -### 后端 API 修改 - -#### 方案 A:前端组合完整地址(推荐) - -前端在提交时将省市区和详细地址组合: - -```typescript -const submitOrder = async () => { - const fullAddress = createOrderForm.region.join('') + createOrderForm.shipping_address - - await createPrescriptionOrder({ - ...createOrderForm, - shipping_address: fullAddress // 四川省成都市双流区黄甲街道... - }) -} -``` - -#### 方案 B:分别存储省市区 - -后端接收独立的省市区字段: - -```php -// 创建订单时 -$order->province = $params['province'] ?? ''; -$order->city = $params['city'] ?? ''; -$order->district = $params['district'] ?? ''; -$order->shipping_address = $params['shipping_address'] ?? ''; - -// 查询时返回 -return [ - 'province' => $order->province, - 'city' => $order->city, - 'district' => $order->district, - 'shipping_address' => $order->shipping_address, - 'full_address' => $order->province . $order->city . $order->district . $order->shipping_address -]; -``` - -## 扩展功能 - -### 1. 使用完整的省市区数据 - -当前只包含了部分常用省市区,实际项目中应该使用完整的数据。 - -**方案 A:使用 npm 包** - -```bash -npm install element-china-area-data -``` - -```typescript -import { regionData } from 'element-china-area-data' - -const regionOptions = ref(regionData) -``` - -**方案 B:从后端 API 获取** - -```typescript -import { getRegionTree } from '@/api/common' - -const regionOptions = ref([]) - -onMounted(async () => { - const res = await getRegionTree() - regionOptions.value = res.data -}) -``` - -### 2. 自动填充历史地址 - -根据患者历史订单自动填充地址: - -```typescript -const loadPatientLastAddress = async (patientId: number) => { - const res = await getPatientLastOrder(patientId) - if (res.data) { - createOrderForm.region = [res.data.province, res.data.city, res.data.district] - createOrderForm.shipping_address = res.data.detail_address - createOrderForm.recipient_name = res.data.recipient_name - createOrderForm.recipient_phone = res.data.recipient_phone - } -} -``` - -### 3. 地址簿功能 - -保存常用地址,快速选择: - -```vue - - - -``` - -### 4. 地址验证 - -验证地址的完整性和合法性: - -```typescript -const validateAddress = () => { - if (createOrderForm.region.length !== 3) { - feedback.msgError('请选择完整的省市区') - return false - } - if (!createOrderForm.shipping_address.trim()) { - feedback.msgError('请填写详细地址') - return false - } - return true -} -``` - -## 注意事项 - -1. **数据格式**: - - `region` 是数组:`['四川省', '成都市', '双流区']` - - 提交时需要组合成完整地址字符串 - -2. **必填验证**: - - 省市区选择器应该设置为必填 - - 详细地址也应该必填 - -3. **字符限制**: - - 详细地址限制 200 字符 - - 建议省市区 + 详细地址总长度不超过 250 字符 - -4. **兼容性**: - - 兼容现有的 `shipping_address` 字段 - - 旧数据可能没有省市区信息,需要做好兼容处理 - -5. **性能优化**: - - 省市区数据较大时,考虑懒加载 - - 使用虚拟滚动优化长列表 - -## 表单验证规则 - -更新验证规则: - -```typescript -const createOrderRules: FormRules = { - // ... 其他规则 - region: [ - { - required: true, - message: '请选择省市区', - trigger: 'change', - validator: (rule, value, callback) => { - if (!value || value.length !== 3) { - callback(new Error('请选择完整的省市区')) - } else { - callback() - } - } - } - ], - shipping_address: [ - { required: true, message: '请输入详细地址', trigger: 'blur' }, - { min: 5, message: '详细地址至少5个字符', trigger: 'blur' } - ] -} -``` - -## 测试要点 - -### 前端测试 - -1. ✅ 省市区选择器显示正常 -2. ✅ 可以正常选择省市区 -3. ✅ 搜索功能正常 -4. ✅ 清空功能正常 -5. ✅ 必填验证正常 -6. ✅ 数据绑定正确 - -### 集成测试 - -1. ✅ 创建订单 → 选择省市区 → 填写详细地址 → 提交成功 -2. ✅ 查看订单 → 地址显示完整 -3. ✅ 编辑订单 → 省市区回显正确 -4. ✅ 打印订单 → 地址格式正确 - -## 完成状态 - -- ✅ 前端界面添加完成 -- ✅ 数据模型更新完成 -- ✅ 省市区数据添加完成(简化版) -- ✅ 事件处理方法添加完成 -- ⏳ 等待完整省市区数据集成 -- ⏳ 等待后端 API 支持 -- ⏳ 等待表单验证规则更新 - -## 相关文件 - -- `admin/src/views/consumer/prescription/index.vue` - 处方订单列表页面 -- 后端需要修改的文件(待确认): - - 订单模型文件 - - 订单控制器文件 - - 数据库迁移文件(如果需要单独存储省市区) diff --git a/REVOKE_PRESCRIPTION_AUDIT_FIX.md b/REVOKE_PRESCRIPTION_AUDIT_FIX.md deleted file mode 100644 index 8602d400..00000000 --- a/REVOKE_PRESCRIPTION_AUDIT_FIX.md +++ /dev/null @@ -1,226 +0,0 @@ -# 撤回处方审核功能修复 - -## 问题描述 -撤回处方审核时,只更新了业务订单表(`zyt_tcm_prescription_order`)的 `prescription_audit_status`,但没有同步更新处方表(`zyt_tcm_prescription`)的 `audit_status`,导致处方表的审核状态不一致。 - -## 影响 -- 业务订单显示"待审核",但处方表仍显示"已通过" -- 处方列表页面显示的审核状态与实际不符 -- 可能导致业务流程混乱 - -## 修复方案 - -### 文件:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -在 `revokeRxAudit()` 方法中添加同步处方表审核状态的逻辑: - -```php -// 同步撤回处方表的审核状态 -$prescriptionId = (int) $order->prescription_id; -if ($prescriptionId > 0) { - try { - $prescription = Prescription::where('id', $prescriptionId)->whereNull('delete_time')->find(); - if ($prescription) { - $prescription->audit_status = 0; - $prescription->audit_remark = ''; - $prescription->audit_time = null; - $prescription->audit_by = null; - $prescription->audit_by_name = ''; - $prescription->save(); - } - } catch (\Throwable $e) { - // 记录日志但不影响主流程 - Log::error('撤回处方审核时同步处方表失败: ' . $e->getMessage(), [ - 'prescription_id' => $prescriptionId, - 'order_id' => $id - ]); - } -} -``` - -## 修复内容 - -### 1. 业务订单表更新(原有逻辑) -**表**: `zyt_tcm_prescription_order` - -```php -$order->prescription_audit_status = 0; // 重置为待审核 -$order->prescription_audit_remark = ''; // 清空审核意见 -``` - -### 2. 处方表同步更新(新增逻辑) -**表**: `zyt_tcm_prescription` - -```php -$prescription->audit_status = 0; // 重置为待审核 -$prescription->audit_remark = ''; // 清空审核意见 -$prescription->audit_time = null; // 清空审核时间 -$prescription->audit_by = null; // 清空审核人ID -$prescription->audit_by_name = ''; // 清空审核人姓名 -``` - -## 数据流转 - -### 撤回前 -``` -业务订单表 (zyt_tcm_prescription_order): - prescription_audit_status: 1 (已通过) - prescription_audit_remark: "审核通过" - -处方表 (zyt_tcm_prescription): - audit_status: 1 (已通过) - audit_remark: "审核通过" - audit_time: 1713168000 - audit_by: 5 - audit_by_name: "张医生" -``` - -### 撤回后 -``` -业务订单表 (zyt_tcm_prescription_order): - prescription_audit_status: 0 (待审核) - prescription_audit_remark: "" - -处方表 (zyt_tcm_prescription): - audit_status: 0 (待审核) - audit_remark: "" - audit_time: null - audit_by: null - audit_by_name: "" -``` - -## 审核状态说明 - -| 状态值 | 说明 | -|--------|------| -| 0 | 待审核 | -| 1 | 已通过 | -| 2 | 已驳回 | - -## 业务流程 - -``` -创建订单 - ↓ -处方待审核 (audit_status = 0) - ↓ 审核通过 -处方已通过 (audit_status = 1) - ↓ 撤回审核 -处方待审核 (audit_status = 0) ← 两个表都重置 - ↓ 重新审核 -处方已通过 (audit_status = 1) -``` - -## 错误处理 - -### 容错设计 -```php -try { - // 更新处方表 - $prescription->save(); -} catch (\Throwable $e) { - // 记录日志但不影响主流程 - Log::error('撤回处方审核时同步处方表失败: ' . $e->getMessage()); -} -``` - -**说明**: -- 即使处方表更新失败,业务订单表的撤回仍然成功 -- 错误会被记录到日志中,便于排查 -- 不会因为处方表问题导致整个撤回操作失败 - -## 相关表结构 - -### 业务订单表 (zyt_tcm_prescription_order) -```sql -prescription_id int(11) unsigned NOT NULL DEFAULT 0 COMMENT '消费者处方ID' -prescription_audit_status tinyint(2) unsigned NOT NULL DEFAULT 0 COMMENT '处方审核 0待审1通过2驳回' -prescription_audit_remark varchar(500) NOT NULL DEFAULT '' COMMENT '处方审核意见' -``` - -### 处方表 (zyt_tcm_prescription) -```sql -audit_status tinyint(1) NOT NULL DEFAULT 1 COMMENT '审核:0待审核 1已通过 2已驳回' -audit_time int(10) unsigned DEFAULT NULL COMMENT '审核时间' -audit_by int(11) unsigned DEFAULT NULL COMMENT '审核人ID' -audit_by_name varchar(50) NOT NULL DEFAULT '' COMMENT '审核人姓名' -audit_remark varchar(500) NOT NULL DEFAULT '' COMMENT '审核意见' -``` - -## 测试清单 - -### 正常流程测试 -- [ ] 创建订单,处方待审核 -- [ ] 审核通过处方 -- [ ] 验证业务订单表和处方表状态都为"已通过" -- [ ] 撤回处方审核 -- [ ] 验证业务订单表和处方表状态都为"待审核" -- [ ] 验证处方表的审核时间、审核人等字段已清空 - -### 边界情况测试 -- [ ] 撤回时处方不存在(已删除) -- [ ] 撤回时处方ID为0 -- [ ] 撤回时数据库连接失败 -- [ ] 验证错误被正确记录到日志 - -### 权限测试 -- [ ] 无审核权限的用户不能撤回 -- [ ] 有审核权限的用户可以撤回 -- [ ] 订单已结束时不能撤回 - -## 日志记录 - -### 成功日志 -``` -操作日志 (zyt_tcm_prescription_order_log): - action: "revoke_rx_audit" - summary: "撤回处方审核,状态重置为待审核" -``` - -### 错误日志 -``` -系统日志: - level: ERROR - message: "撤回处方审核时同步处方表失败: [错误信息]" - context: { - "prescription_id": 123, - "order_id": 456 - } -``` - -## 相关功能 - -### 1. 审核处方 -**方法**: `PrescriptionOrderLogic::auditPrescription()` -- 同时更新业务订单表和处方表的审核状态 - -### 2. 撤回处方审核 -**方法**: `PrescriptionOrderLogic::revokeRxAudit()` -- 同时重置业务订单表和处方表的审核状态(本次修复) - -### 3. 撤回支付单审核 -**方法**: `PrescriptionOrderLogic::revokePayAudit()` -- 只更新业务订单表的支付审核状态 - -## 注意事项 - -1. **数据一致性**:确保两个表的审核状态始终保持一致 -2. **容错处理**:处方表更新失败不影响主流程 -3. **日志记录**:所有错误都会被记录,便于排查 -4. **权限控制**:只有有审核权限的用户才能撤回 -5. **状态限制**:订单已结束时不能撤回 - -## 相关文件 - -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 业务逻辑 -- `server/app/adminapi/controller/tcm/PrescriptionOrderController.php` - 控制器 -- `admin/src/views/consumer/prescription/order_list.vue` - 前端页面 - -## 总结 - -✅ **修复前**:撤回审核只更新业务订单表 -✅ **修复后**:撤回审核同时更新业务订单表和处方表 -✅ **容错处理**:处方表更新失败不影响主流程 -✅ **日志记录**:错误会被记录到系统日志 - -这样可以确保数据一致性,避免审核状态不同步的问题。 diff --git a/SECURITY_FIX_SUMMARY.md b/SECURITY_FIX_SUMMARY.md deleted file mode 100644 index 1745d88f..00000000 --- a/SECURITY_FIX_SUMMARY.md +++ /dev/null @@ -1,278 +0,0 @@ -# 安全修复总结 - 防止绕过修改密码页面 - -## 问题描述 - -原始实现中存在两个安全漏洞: - -1. **URL 绕过**:用户登录后虽然会跳转到修改密码页面,但可以通过直接在浏览器地址栏输入其他页面 URL 来绕过修改密码页面 -2. **刷新绕过**:页面刷新后,`isPaw` 状态丢失(只存在内存中),导致用户可以直接进入系统 - -## 安全风险 - -- 用户可以使用初始密码继续使用系统 -- 无法强制用户修改弱密码 -- 违反安全策略要求 - -## 修复方案 - -### 1. 后端返回 is_paw 字段 - -在 `AdminLogic::detail()` 方法中添加 `is_paw` 字段: - -```php -$admin = Admin::field([ - 'id', 'account', 'name', 'disable', 'root', - 'multipoint_login', 'avatar', 'is_paw', // 添加此字段 - // ... 其他字段 -])->findOrEmpty($params['id'])->toArray(); -``` - -### 2. 状态管理 - -在 Vuex store 中添加 `isPaw` 状态: - -```typescript -export interface UserState { - token: string - userInfo: Record - routes: RouteRecordRaw[] - perms: string[] - isPaw: number // 0=需要修改密码,1=正常 -} -``` - -登录时保存 `isPaw` 状态: - -```typescript -login(playload: any) { - return new Promise((resolve, reject) => { - login({ account: account.trim(), password: password }) - .then((data) => { - this.token = data.token - this.isPaw = data.is_paw ?? 1 // 保存状态 - cache.set(TOKEN_KEY, data.token) - resolve(data) - }) - }) -} -``` - -### 2. 状态管理 - -在 Vuex store 中添加 `isPaw` 状态: - -```typescript -export interface UserState { - token: string - userInfo: Record - routes: RouteRecordRaw[] - perms: string[] - isPaw: number // 0=需要修改密码,1=正常 -} -``` - -登录时保存 `isPaw` 状态: - -```typescript -login(playload: any) { - return new Promise((resolve, reject) => { - login({ account: account.trim(), password: password }) - .then((data) => { - this.token = data.token - this.isPaw = data.is_paw ?? 1 // 保存状态 - cache.set(TOKEN_KEY, data.token) - resolve(data) - }) - }) -} -``` - -获取用户信息时更新 `isPaw` 状态(解决刷新问题): - -```typescript -getUserInfo() { - return new Promise((resolve, reject) => { - getUserInfo() - .then((data) => { - this.userInfo = data.user - this.perms = data.permissions - this.routes = filterAsyncRoutes(data.menu) - this.isPaw = data.user.is_paw ?? 1 // 从后端获取最新状态 - resolve(data) - }) - }) -} -``` - -### 3. 路由守卫 - -在 `permission.ts` 中添加路由守卫逻辑: - -```typescript -router.beforeEach(async (to, from, next) => { - const userStore = useUserStore() - - // 特殊处理:修改密码页面 - if (to.path === changePasswordPath) { - if (!userStore.token) { - // 未登录,跳转到登录页 - next({ path: loginPath }) - return - } - if (userStore.isPaw === 1) { - // 已经修改过密码,跳转到首页 - next({ path: defaultPath }) - return - } - // 需要修改密码,允许访问 - next() - return - } - - // 其他页面:检查是否需要修改密码 - if (userStore.token && userStore.isPaw === 0) { - // 需要修改密码,强制跳转到修改密码页面 - next({ path: changePasswordPath }) - return - } - - // ... 其他路由逻辑 -}) -``` - -### 3. 路由守卫 - -在 `permission.ts` 中添加路由守卫逻辑,关键是在获取用户信息后再检查 `isPaw`: - -```typescript -router.beforeEach(async (to, from, next) => { - const userStore = useUserStore() - - if (userStore.token) { - const hasGetUserInfo = Object.keys(userStore.userInfo).length !== 0 - - if (hasGetUserInfo) { - // 已经获取过用户信息,检查是否需要修改密码 - if (userStore.isPaw === 0) { - next({ path: changePasswordPath }) - return - } - // ... 其他逻辑 - } else { - // 首次获取用户信息 - await userStore.getUserInfo() - - // 获取用户信息后,检查是否需要修改密码 - if (userStore.isPaw === 0) { - next({ path: changePasswordPath }) - return - } - // ... 其他逻辑 - } - } -}) -``` - -### 4. 防护机制 - -### 4. 防护机制 - -#### 4.1 强制跳转 -- 当 `isPaw = 0` 时,访问任何页面都会被拦截 -- 自动跳转到修改密码页面 - -#### 4.2 URL 防护 -- 即使直接输入 URL,路由守卫也会拦截 -- 无法绕过修改密码页面 - -#### 4.3 状态验证 -- 修改密码页面需要登录才能访问 -- 已修改密码的用户无法再次访问该页面 - -#### 4.4 刷新防护 -- 页面刷新时重新从后端获取 `is_paw` 状态 -- 确保状态始终与数据库一致 - -#### 4.5 多标签页防护 -- 所有标签页共享同一个 store 状态 -- 新打开的标签页也会被拦截 - -## 修复后的安全流程 - -``` -用户登录 (is_paw = 0) - ↓ -路由守卫检查 isPaw - ↓ -强制跳转到修改密码页面 - ↓ -用户尝试访问其他页面(输入 URL) - ↓ -路由守卫再次检查 isPaw - ↓ -再次强制跳转到修改密码页面 - ↓ -用户修改密码成功 - ↓ -isPaw 更新为 1 - ↓ -退出登录 - ↓ -使用新密码重新登录 - ↓ -路由守卫检查 isPaw = 1 - ↓ -允许正常访问系统 -``` - -## 测试验证 - -### 测试场景 1:直接输入 URL -1. 使用 `is_paw = 0` 的账号登录 -2. 在地址栏输入 `/workbench/index` -3. 结果:自动跳转回 `/change-password` - -### 测试场景 2:多标签页 -1. 使用 `is_paw = 0` 的账号登录 -2. 打开新标签页访问其他页面 -3. 结果:新标签页也被跳转到修改密码页面 - -### 测试场景 3:页面刷新 -1. 在修改密码页面刷新浏览器 -2. 结果:仍然停留在修改密码页面 - -### 测试场景 4:已修改密码的用户 -1. 使用 `is_paw = 1` 的账号登录 -2. 尝试访问 `/change-password` -3. 结果:自动跳转到首页 - -## 相关文件 - -### 修改的文件 -- `admin/src/stores/modules/user.ts` - 添加 isPaw 状态管理 -- `admin/src/permission.ts` - 添加路由守卫逻辑 -- `admin/src/views/account/change-password.vue` - 更新 isPaw 状态 - -### 新增的文件 -- `TEST_CHECKLIST.md` - 测试清单 - -## 安全等级 - -修复前:⚠️ 低(可绕过) -修复后:✅ 高(无法绕过) - -## 建议 - -1. 定期审计管理员账号的 `is_paw` 状态 -2. 为新创建的管理员默认设置 `is_paw = 0` -3. 定期要求管理员修改密码(批量设置 `is_paw = 0`) -4. 考虑添加密码强度验证(大小写、数字、特殊字符) -5. 考虑添加密码历史记录,防止重复使用旧密码 - -## 完成时间 - -2024-XX-XX - -## 测试状态 - -✅ 已通过所有测试场景 diff --git a/SERVICE_PACKAGE_MULTI_SELECT.md b/SERVICE_PACKAGE_MULTI_SELECT.md deleted file mode 100644 index 8f3afaf4..00000000 --- a/SERVICE_PACKAGE_MULTI_SELECT.md +++ /dev/null @@ -1,293 +0,0 @@ -# Service Package Multi-Select Implementation (服务套餐多选实现) - -## Changes Made - -Updated the service package field from a simple text input to a multi-select dropdown with data loaded from the dictionary API. - -### 1. Added Dictionary API Import -**File**: `admin/src/views/consumer/prescription/order_list.vue` - -```typescript -import { getDictData } from '@/api/app' -``` - -### 2. Added Service Package Options State -```typescript -// 服务套餐选项 -const servicePackageOptions = ref>([]) -``` - -### 3. Added Loading Function -```typescript -// 加载服务套餐选项 -const loadServicePackageOptions = async () => { - try { - const data = await getDictData({ type: 'server_order' }) - servicePackageOptions.value = (data?.server_order || []).filter((item: any) => item.status !== 0) - console.log('服务套餐选项已加载:', servicePackageOptions.value.length) - } catch (error) { - console.error('加载服务套餐选项失败:', error) - servicePackageOptions.value = [] - } -} -``` - -### 4. Updated onMounted Hook -```typescript -onMounted(async () => { - await loadRegionData() - await loadServicePackageOptions() // Added - getLists() -}) -``` - -### 5. Changed Form Field Type -Changed `editForm.service_package` from string to array: -```typescript -// Before: -service_package: '', - -// After: -service_package: [] as string[], -``` - -### 6. Updated Edit Form UI -Changed from text input to multi-select: - -**Before:** -```vue - -``` - -**After:** -```vue - - - -``` - -### 7. Updated Data Loading Logic -Added logic to handle both array and string formats when loading data: - -```typescript -// 处理服务套餐:如果是字符串,转换为数组 -if (d.service_package) { - if (Array.isArray(d.service_package)) { - editForm.service_package = d.service_package - } else if (typeof d.service_package === 'string') { - editForm.service_package = d.service_package.split(',').filter(v => v.trim() !== '') - } else { - editForm.service_package = [] - } -} else { - editForm.service_package = [] -} -``` - -### 8. Updated Form Submission -Convert array back to comma-separated string for backend: - -```typescript -service_package: Array.isArray(editForm.service_package) ? editForm.service_package.join(',') : '', -``` - -### 9. Added Display Formatter -Added `formatServicePackage()` function to display service packages in detail view: - -```typescript -// 格式化服务套餐显示 -function formatServicePackage(value: any): string { - if (!value) return '—' - - let packages: string[] = [] - if (Array.isArray(value)) { - packages = value - } else if (typeof value === 'string') { - packages = value.split(',').filter(v => v.trim() !== '') - } - - if (packages.length === 0) return '—' - - // 将值转换为名称 - const names = packages.map(val => { - const option = servicePackageOptions.value.find(opt => opt.value === val) - return option ? option.name : val - }) - - return names.join('、') -} -``` - -### 10. Updated Detail Display -```vue - - {{ formatServicePackage(detailData.service_package) }} - -``` - -## Data Flow - -### Loading Options: -1. Component mounts -2. `loadServicePackageOptions()` calls API: `/adminapi/config/dict?type=server_order` -3. Filters out disabled options (status !== 0) -4. Stores in `servicePackageOptions` - -### Editing Order: -1. User opens edit dialog -2. Backend returns `service_package` as string: `"package1,package2,package3"` -3. Frontend splits string into array: `['package1', 'package2', 'package3']` -4. Multi-select displays selected options -5. User can add/remove selections -6. On save, array is joined back to string: `"package1,package2,package3"` -7. Backend receives comma-separated string - -### Displaying in Detail: -1. Backend returns `service_package` as string -2. `formatServicePackage()` splits string into array -3. Maps values to display names using `servicePackageOptions` -4. Joins names with Chinese comma: `"套餐A、套餐B、套餐C"` - -## UI Features - -### Multi-Select Features: -- **Multiple Selection**: Users can select multiple service packages -- **Collapse Tags**: Selected items are collapsed with a count badge when many are selected -- **Collapse Tags Tooltip**: Hover over collapsed tags to see all selections -- **Clearable**: Users can clear all selections with one click -- **Searchable**: Built-in search functionality (default for el-select) - -### Display Format: -- Detail view: `套餐A、套餐B、套餐C` (Chinese comma separator) -- Edit form: Tag-style display with collapse for many items -- Empty state: Shows `—` when no packages selected - -## Backend Compatibility - -### Database Storage: -The backend stores service packages as a comma-separated string in the database: -``` -service_package: "package1,package2,package3" -``` - -### API Response: -The backend returns the string as-is, and the frontend handles the conversion. - -### API Request: -The frontend sends the string format, maintaining backward compatibility. - -## Testing Steps - -### 1. Test Options Loading -1. Open order list page -2. Check browser console for: `服务套餐选项已加载: X` -3. Open edit dialog -4. Click on service package dropdown -5. Verify options are displayed - -### 2. Test Multi-Selection -1. Edit an order -2. Click service package dropdown -3. Select multiple packages -4. Verify tags appear below the dropdown -5. Select many packages to test collapse behavior -6. Hover over collapsed tags to see tooltip - -### 3. Test Saving -1. Select multiple service packages -2. Save the order -3. Check browser Network tab -4. Verify payload contains: `service_package: "value1,value2,value3"` -5. Verify database is updated - -### 4. Test Display -1. Open order detail view -2. Check "服务套餐" field -3. Verify it shows: `套餐A、套餐B、套餐C` -4. Test with orders that have: - - Multiple packages - - Single package - - No packages (should show `—`) - -### 5. Test Backward Compatibility -1. Edit an old order that has service_package as string -2. Verify it loads correctly into the multi-select -3. Verify you can modify and save - -### 6. Test Clear Functionality -1. Edit an order with service packages selected -2. Click the clear button (×) on the select -3. Verify all selections are cleared -4. Save and verify database is updated to empty string - -## Dictionary API Format - -The API endpoint `/adminapi/config/dict?type=server_order` should return: - -```json -{ - "server_order": [ - { - "name": "套餐A", - "value": "package_a", - "status": 1 - }, - { - "name": "套餐B", - "value": "package_b", - "status": 1 - }, - { - "name": "套餐C (已停用)", - "value": "package_c", - "status": 0 - } - ] -} -``` - -**Note**: Options with `status: 0` are filtered out and not displayed. - -## Related Files - -### Frontend: -- `admin/src/views/consumer/prescription/order_list.vue` - - Added `getDictData` import - - Added `servicePackageOptions` state - - Added `loadServicePackageOptions()` function - - Updated `editForm.service_package` to array type - - Updated edit form UI to multi-select - - Added data loading/saving conversion logic - - Added `formatServicePackage()` display function - - Updated detail display - -### Backend: -- `/adminapi/config/dict` endpoint (should already exist) -- Dictionary data for `type=server_order` - -## Summary - -✅ Changed from text input to multi-select dropdown -✅ Loads options from dictionary API -✅ Supports multiple selection with tag display -✅ Collapse tags for better UI when many items selected -✅ Converts between array (frontend) and string (backend) -✅ Displays formatted names in detail view -✅ Backward compatible with existing string data -✅ Filters out disabled options -✅ Clearable and searchable - -The service package field now provides a better user experience with predefined options and multi-selection capability! diff --git a/SHIPPING_REGION_FEATURE.md b/SHIPPING_REGION_FEATURE.md deleted file mode 100644 index 45911cc3..00000000 --- a/SHIPPING_REGION_FEATURE.md +++ /dev/null @@ -1,179 +0,0 @@ -# 收货地址省市区功能实现 - -## 概述 -为处方业务订单编辑表单添加省/市/区选择器,将地址信息结构化存储。 - -## 数据库变更 - -### 新增字段 -在 `zyt_tcm_prescription_order` 表中添加三个字段: - -```sql -ALTER TABLE `zyt_tcm_prescription_order` -ADD COLUMN `shipping_province` varchar(50) NOT NULL DEFAULT '' COMMENT '收货省份' AFTER `recipient_phone`, -ADD COLUMN `shipping_city` varchar(50) NOT NULL DEFAULT '' COMMENT '收货城市' AFTER `shipping_province`, -ADD COLUMN `shipping_district` varchar(50) NOT NULL DEFAULT '' COMMENT '收货区县' AFTER `shipping_city`; -``` - -**执行方式:** -```bash -mysql -u用户名 -p数据库名 < add_shipping_region_fields.sql -``` - -## 后端变更 - -### 文件:`server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - -在 `edit()` 方法中添加省市区字段处理: - -```php -// 处理省市区字段 -if (isset($params['shipping_province'])) { - $order->shipping_province = (string) $params['shipping_province']; -} -if (isset($params['shipping_city'])) { - $order->shipping_city = (string) $params['shipping_city']; -} -if (isset($params['shipping_district'])) { - $order->shipping_district = (string) $params['shipping_district']; -} -``` - -## 前端变更 - -### 文件:`admin/src/views/consumer/prescription/order_list.vue` - -#### 1. 数据结构 -在 `editForm` 中添加 `region` 字段: -```typescript -const editForm = reactive({ - // ... 其他字段 - region: [] as string[], // 新增 - shipping_address: '', - // ... -}) -``` - -#### 2. UI 组件 -在收货手机下方添加省市区选择器: -```vue - - - -``` - -#### 3. 数据提交 -在 `submitEdit()` 中将 region 数组拆分为三个字段: -```typescript -if (editForm.region && editForm.region.length > 0) { - payload.shipping_province = editForm.region[0] || '' - payload.shipping_city = editForm.region[1] || '' - payload.shipping_district = editForm.region[2] || '' -} else { - payload.shipping_province = '' - payload.shipping_city = '' - payload.shipping_district = '' -} -``` - -#### 4. 数据加载 -在 `openEdit()` 中从后端数据填充 region: -```typescript -const province = d.shipping_province || '' -const city = d.shipping_city || '' -const district = d.shipping_district || '' -if (province || city || district) { - editForm.region = [province, city, district].filter(v => v !== '') -} else { - editForm.region = [] -} -``` - -## 省市区数据 - -当前使用简化的示例数据,包含: -- 四川省(成都市、绵阳市) -- 北京市 -- 上海市 -- 广东省(广州市、深圳市) - -### 生产环境建议 - -**方案 1:使用完整数据集** -- 集成中国行政区划完整数据(约 3000+ 区县) -- 推荐数据源: - - [element-china-area-data](https://www.npmjs.com/package/element-china-area-data) - - [china-division](https://github.com/modood/Administrative-divisions-of-China) - -**方案 2:使用 API 服务** -- 调用第三方地址 API(如高德地图、腾讯地图) -- 优点:数据实时更新,减少前端体积 -- 缺点:依赖网络,需要 API Key - -## 使用流程 - -1. **执行数据库迁移** - ```bash - mysql -u用户名 -p数据库名 < add_shipping_region_fields.sql - ``` - -2. **重启后端服务**(如需要) - -3. **测试功能** - - 打开处方业务订单列表 - - 点击"编辑"按钮 - - 选择省/市/区 - - 填写详细地址 - - 保存并验证数据已正确存储 - -## 数据结构 - -### 前端存储格式 -```typescript -region: ['四川省', '成都市', '武侯区'] -``` - -### 后端存储格式 -```php -[ - 'shipping_province' => '四川省', - 'shipping_city' => '成都市', - 'shipping_district' => '武侯区', - 'shipping_address' => '天府大道中段123号' // 详细地址 -] -``` - -### 数据库存储 -``` -shipping_province: 四川省 -shipping_city: 成都市 -shipping_district: 武侯区 -shipping_address: 天府大道中段123号 -``` - -## 注意事项 - -1. **向后兼容**:旧数据的省市区字段为空字符串,不影响现有功能 -2. **可选字段**:省市区选择器可清空,允许用户只填写详细地址 -3. **数据验证**:前端使用 cascader 确保选择的层级关系正确 -4. **字段长度**:省市区字段各限制 50 字符 - -## 相关文件 - -- `add_shipping_region_fields.sql` - 数据库迁移文件 -- `admin/src/views/consumer/prescription/order_list.vue` - 前端订单列表页面 -- `server/app/adminapi/logic/tcm/PrescriptionOrderLogic.php` - 后端业务逻辑 - -## 后续优化建议 - -1. 在订单详情页面也显示省市区信息 -2. 在订单列表的收货人列中显示省市信息 -3. 添加按省市区筛选订单的功能 -4. 集成完整的中国行政区划数据 -5. 考虑添加邮政编码字段 diff --git a/TEST_CHECKLIST.md b/TEST_CHECKLIST.md deleted file mode 100644 index 6dc7c890..00000000 --- a/TEST_CHECKLIST.md +++ /dev/null @@ -1,192 +0,0 @@ -# 首次登录修改密码功能 - 测试清单 - -## 测试前准备 - -1. ✅ 执行安装脚本或 SQL,添加 `is_paw` 字段 -2. ✅ 设置测试账号:`UPDATE la_admin SET is_paw = 0 WHERE id = 1;` -3. ✅ 确认前后端代码已更新 - -## 测试场景 - -### 场景 1:正常登录流程 - -**步骤:** -1. 使用 `is_paw = 0` 的账号登录 -2. 观察是否自动跳转到修改密码页面 -3. 输入新密码(至少 6 位) -4. 点击"确认修改" -5. 观察是否提示"密码修改成功,请重新登录" -6. 使用新密码重新登录 -7. 观察是否能正常进入系统 - -**预期结果:** -- ✅ 登录后自动跳转到修改密码页面 -- ✅ 修改密码成功 -- ✅ 使用新密码能正常登录 -- ✅ 数据库中 `is_paw` 已更新为 1 - -### 场景 2:URL 防护测试(重要!) - -**步骤:** -1. 使用 `is_paw = 0` 的账号登录 -2. 登录后会跳转到修改密码页面 -3. 在浏览器地址栏直接输入其他页面 URL(如:`/workbench/index`) -4. 按回车访问 - -**预期结果:** -- ✅ 无论输入什么 URL,都会被强制跳转回修改密码页面 -- ✅ 无法绕过修改密码页面访问其他页面 - -### 场景 3:密码验证测试 - -**步骤:** -1. 在修改密码页面输入少于 6 位的密码 -2. 点击"确认修改" - -**预期结果:** -- ✅ 提示"密码长度不能少于6位" - -**步骤:** -1. 两次输入不同的密码 -2. 点击"确认修改" - -**预期结果:** -- ✅ 提示"两次输入的密码不一致" - -### 场景 4:已修改密码的用户 - -**步骤:** -1. 使用 `is_paw = 1` 的账号登录 -2. 观察是否直接进入系统 - -**预期结果:** -- ✅ 直接进入系统,不跳转到修改密码页面 - -**步骤:** -1. 在浏览器地址栏输入 `/change-password` -2. 按回车访问 - -**预期结果:** -- ✅ 自动跳转到首页,无法访问修改密码页面 - -### 场景 5:未登录用户 - -**步骤:** -1. 退出登录 -2. 在浏览器地址栏输入 `/change-password` -3. 按回车访问 - -**预期结果:** -- ✅ 自动跳转到登录页面 - -### 场景 6:企业微信登录 - -**步骤:** -1. 设置企业微信绑定的管理员 `is_paw = 0` -2. 使用企业微信登录 -3. 观察是否跳转到修改密码页面 - -**预期结果:** -- ✅ 登录后自动跳转到修改密码页面 -- ✅ 修改密码流程与账号密码登录一致 - -### 场景 7:页面刷新测试(重要!) - -**步骤:** -1. 使用 `is_paw = 0` 的账号登录 -2. 跳转到修改密码页面后,刷新浏览器(F5 或 Ctrl+R) -3. 观察页面状态 - -**预期结果:** -- ✅ 刷新后仍然停留在修改密码页面 -- ✅ 不会跳转到其他页面 - -**步骤(进阶测试):** -1. 使用 `is_paw = 0` 的账号登录 -2. 跳转到修改密码页面 -3. 在地址栏输入其他页面 URL(如 `/workbench/index`) -4. 按回车 -5. 观察是否跳转回修改密码页面 -6. 刷新浏览器 -7. 观察是否仍然在修改密码页面 - -**预期结果:** -- ✅ 输入其他 URL 后被拦截,跳转回修改密码页面 -- ✅ 刷新后仍然在修改密码页面 -- ✅ 无法通过任何方式绕过 - -### 场景 8:多标签页测试 - -**步骤:** -1. 使用 `is_paw = 0` 的账号登录 -2. 跳转到修改密码页面 -3. 打开新标签页,访问系统其他页面 - -**预期结果:** -- ✅ 新标签页也会被强制跳转到修改密码页面 - -## 数据库验证 - -### 修改前 -```sql -SELECT id, account, is_paw FROM la_admin WHERE id = 1; --- 应该显示 is_paw = 0 -``` - -### 修改后 -```sql -SELECT id, account, is_paw FROM la_admin WHERE id = 1; --- 应该显示 is_paw = 1 -``` - -## 常见问题排查 - -### 问题 1:登录后没有跳转到修改密码页面 - -**检查:** -- 数据库中 `is_paw` 字段是否为 0 -- 前端代码是否正确判断 `is_paw` 字段 -- 浏览器控制台是否有错误 - -### 问题 2:刷新后可以进入系统 - -**检查:** -- 后端 `AdminLogic::detail()` 方法是否返回 `is_paw` 字段 -- 前端 `getUserInfo()` 方法是否更新 `isPaw` 状态 -- 路由守卫是否在获取用户信息后检查 `isPaw` -- 浏览器控制台查看 API 返回的数据是否包含 `is_paw` - -### 问题 3:可以通过 URL 绕过修改密码页面 - -**检查:** -- `permission.ts` 路由守卫是否正确配置 -- `userStore.isPaw` 状态是否正确存储 -- 浏览器控制台查看路由守卫是否执行 - -### 问题 4:修改密码后 `is_paw` 没有更新 - -**检查:** -- 后端 `changeFirstPassword` 方法是否正确更新数据库 -- 查看数据库确认字段值 -- 检查后端日志是否有错误 - -### 问题 5:修改密码后无法登录 - -**检查:** -- 密码加密方式是否正确(应使用 `create_password` 函数) -- 检查后端日志 -- 尝试重置密码 - -## 测试完成标准 - -- ✅ 所有 8 个测试场景都通过 -- ✅ 数据库字段正确更新 -- ✅ 无法通过任何方式绕过修改密码页面 -- ✅ 密码验证规则正常工作 -- ✅ 修改密码后能正常登录使用系统 - -## 性能测试 - -- ✅ 路由守卫不影响正常用户的访问速度 -- ✅ 登录流程响应时间正常(< 2 秒) -- ✅ 修改密码操作响应时间正常(< 1 秒) diff --git a/TEST_PRESCRIPTION_LIBRARY.sql b/TEST_PRESCRIPTION_LIBRARY.sql deleted file mode 100644 index bc7f6a2e..00000000 --- a/TEST_PRESCRIPTION_LIBRARY.sql +++ /dev/null @@ -1,32 +0,0 @@ --- 测试处方库功能的SQL语句 - --- 1. 查看表结构 -DESC zyt_prescription_library; - --- 2. 插入测试数据 -INSERT INTO `zyt_prescription_library` -(`prescription_name`, `herbs`, `is_public`, `creator_id`, `creator_name`, `create_time`, `update_time`) -VALUES -('六味地黄丸', '[{"name":"熟地黄","dosage":24},{"name":"山茱萸","dosage":12},{"name":"山药","dosage":12},{"name":"泽泻","dosage":9},{"name":"牡丹皮","dosage":9},{"name":"茯苓","dosage":9}]', 1, 1, '测试医生', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), -('补中益气汤', '[{"name":"黄芪","dosage":15},{"name":"党参","dosage":10},{"name":"白术","dosage":10},{"name":"当归","dosage":10},{"name":"陈皮","dosage":6},{"name":"升麻","dosage":6},{"name":"柴胡","dosage":6},{"name":"甘草","dosage":5}]', 1, 1, '测试医生', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()), -('逍遥散', '[{"name":"柴胡","dosage":10},{"name":"当归","dosage":10},{"name":"白芍","dosage":10},{"name":"白术","dosage":10},{"name":"茯苓","dosage":10},{"name":"薄荷","dosage":6},{"name":"生姜","dosage":6},{"name":"甘草","dosage":5}]', 0, 1, '测试医生', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()); - --- 3. 查询所有处方 -SELECT * FROM zyt_prescription_library WHERE delete_time IS NULL; - --- 4. 查询公开的处方 -SELECT * FROM zyt_prescription_library WHERE is_public = 1 AND delete_time IS NULL; - --- 5. 查询某个医生创建的处方 -SELECT * FROM zyt_prescription_library WHERE creator_id = 1 AND delete_time IS NULL; - --- 6. 统计处方数量 -SELECT - COUNT(*) as total, - SUM(CASE WHEN is_public = 1 THEN 1 ELSE 0 END) as public_count, - SUM(CASE WHEN is_public = 0 THEN 1 ELSE 0 END) as private_count -FROM zyt_prescription_library -WHERE delete_time IS NULL; - --- 7. 清理测试数据(可选) --- DELETE FROM zyt_prescription_library WHERE creator_name = '测试医生'; diff --git a/TRTC_COS_STORAGE_MIGRATION.md b/TRTC_COS_STORAGE_MIGRATION.md deleted file mode 100644 index 8f22db93..00000000 --- a/TRTC_COS_STORAGE_MIGRATION.md +++ /dev/null @@ -1,348 +0,0 @@ -# TRTC 录制存储迁移:VOD → COS - -## 变更说明 - -将 TRTC 云端录制的存储方式从**腾讯云点播(VOD)**改为**对象存储(COS)**。 - -## 修改内容 - -### File: `server/app/common/service/TrtcCloudRecordingService.php` - -#### 1. 存储配置变更 - -**Before (VOD):** -```php -$tencentVod = new \TencentCloud\Trtc\V20190722\Models\TencentVod(); -$tencentVod->ExpireTime = 0; -$tencentVod->MediaType = 0; -$tencentVod->UserDefineRecordId = $prefix; -$tencentVod->SubAppId = $vodSubApp; - -$cloudVod = new \TencentCloud\Trtc\V20190722\Models\CloudVod(); -$cloudVod->TencentVod = $tencentVod; - -$storage = new \TencentCloud\Trtc\V20190722\Models\StorageParams(); -$storage->CloudVod = $cloudVod; -``` - -**After (COS):** -```php -$cloudStorage = new \TencentCloud\Trtc\V20190722\Models\CloudStorage(); -$cloudStorage->Vendor = 0; // 0=腾讯云 -$cloudStorage->Region = 'ap-guangzhou'; -$cloudStorage->Bucket = 'your-bucket-name'; -$cloudStorage->Prefix = 'trtc-recording/'; - -$storage = new \TencentCloud\Trtc\V20190722\Models\StorageParams(); -$storage->CloudStorage = $cloudStorage; -``` - -#### 2. 更新日志信息 -- 日志中标注存储类型为 COS -- 记录 bucket、region、prefix 等信息 -- 更新提示信息 - -#### 3. 更新路径清理函数 -- 允许路径中包含 `/` 字符(COS 路径分隔符) -- 从 `sanitizeVodUserDefineRecordId` 改为支持 COS 路径格式 - -## 配置要求 - -### 必需配置项 - -在 `config/trtc.php` 或 `.env` 中添加以下配置: - -```php -// config/trtc.php -return [ - // ... 其他配置 ... - - // COS 存储配置 - 'recording_cos_vendor' => 0, // 0=腾讯云 - 'recording_cos_region' => env('TRTC_RECORDING_COS_REGION', 'ap-guangzhou'), - 'recording_cos_bucket' => env('TRTC_RECORDING_COS_BUCKET', ''), - 'recording_cos_prefix' => env('TRTC_RECORDING_COS_PREFIX', 'trtc-recording/'), -]; -``` - -### .env 配置示例 - -```env -# TRTC 录制 COS 存储配置 -TRTC_RECORDING_COS_REGION=ap-guangzhou -TRTC_RECORDING_COS_BUCKET=your-bucket-1234567890 -TRTC_RECORDING_COS_PREFIX=trtc-recording/ -``` - -## COS Bucket 准备 - -### 1. 创建 COS 存储桶 - -1. 登录 [腾讯云 COS 控制台](https://console.cloud.tencent.com/cos) -2. 创建存储桶 - - 名称:例如 `trtc-recording-1234567890` - - 地域:选择与 TRTC 应用相同或就近的地域 - - 访问权限:私有读写(推荐) - -### 2. 配置 CORS(如需前端访问) - -如果需要前端直接访问录制文件,配置 CORS 规则: - -```json -{ - "CORSRules": [ - { - "AllowedOrigins": ["*"], - "AllowedMethods": ["GET", "HEAD"], - "AllowedHeaders": ["*"], - "ExposeHeaders": [], - "MaxAgeSeconds": 3600 - } - ] -} -``` - -### 3. 配置生命周期(可选) - -设置自动删除过期录制文件: - -- 规则名称:`auto-delete-old-recordings` -- 应用范围:前缀 `trtc-recording/` -- 删除策略:创建后 30 天删除 - -## 存储路径结构 - -### 默认路径格式 - -``` -bucket-name/ -└── trtc-recording/ - ├── room_123_20240101_120000.mp4 - ├── room_456_20240101_130000.mp4 - └── ... -``` - -### 自定义前缀路径 - -如果传入 `$vodUserDefineRecordId` 参数: - -``` -bucket-name/ -└── custom-prefix/ - ├── file1.mp4 - ├── file2.mp4 - └── ... -``` - -## 文件命名规则 - -COS 存储的文件名由 TRTC 自动生成,通常包含: -- 房间号 -- 时间戳 -- 用户 ID(单流录制) -- 文件格式(.mp4) - -示例: -``` -trtc-recording/sdkappid_roomid_userid_timestamp.mp4 -``` - -## 权限配置 - -### TRTC 服务需要的 COS 权限 - -确保 TRTC 服务有权限写入 COS: - -1. **方式一:使用主账号密钥**(不推荐生产环境) - - 使用主账号的 SecretId 和 SecretKey - -2. **方式二:使用子账号密钥**(推荐) - - 创建子账号 - - 授予 COS 写入权限策略: - ```json - { - "version": "2.0", - "statement": [ - { - "effect": "allow", - "action": [ - "cos:PutObject", - "cos:PostObject", - "cos:InitiateMultipartUpload", - "cos:UploadPart", - "cos:CompleteMultipartUpload" - ], - "resource": [ - "qcs::cos:ap-guangzhou:uid/1234567890:your-bucket-1234567890/*" - ] - } - ] - } - ``` - -3. **方式三:使用服务角色**(最佳实践) - - 为 TRTC 服务创建角色 - - 授予角色 COS 写入权限 - - TRTC 自动使用角色权限 - -## 对比:VOD vs COS - -| 特性 | VOD(点播) | COS(对象存储) | -|------|------------|----------------| -| **存储成本** | 较高 | 较低 | -| **功能** | 媒体处理、转码、播放 | 纯存储 | -| **访问方式** | 点播 API/播放器 | HTTP/HTTPS 直接访问 | -| **适用场景** | 需要转码、加密、播放统计 | 简单存储和下载 | -| **计费** | 存储+流量+转码 | 存储+流量 | -| **管理** | 媒资管理系统 | 文件管理 | - -## 迁移步骤 - -### 1. 准备 COS 存储桶 -按照上述"COS Bucket 准备"步骤创建并配置存储桶 - -### 2. 更新配置文件 -在 `.env` 中添加 COS 配置项 - -### 3. 部署代码 -部署更新后的 `TrtcCloudRecordingService.php` - -### 4. 测试录制 -1. 发起一次测试录制 -2. 检查 COS 控制台是否有文件上传 -3. 验证文件可以正常下载和播放 - -### 5. 监控日志 -查看日志确认录制状态: -``` -CreateCloudRecording mix (COS) -DescribeCloudRecording(合流校验-COS) -``` - -## 获取录制文件 - -### 方式一:COS 控制台 -1. 登录 COS 控制台 -2. 进入对应存储桶 -3. 浏览 `trtc-recording/` 目录 -4. 下载或获取文件 URL - -### 方式二:COS SDK -```php -use Qcloud\Cos\Client; - -$cosClient = new Client([ - 'region' => 'ap-guangzhou', - 'credentials' => [ - 'secretId' => 'your-secret-id', - 'secretKey' => 'your-secret-key', - ], -]); - -// 列出录制文件 -$result = $cosClient->listObjects([ - 'Bucket' => 'your-bucket-1234567890', - 'Prefix' => 'trtc-recording/', -]); - -foreach ($result['Contents'] as $file) { - echo $file['Key'] . "\n"; -} -``` - -### 方式三:生成临时访问 URL -```php -$url = $cosClient->getObjectUrl( - 'your-bucket-1234567890', - 'trtc-recording/file.mp4', - '+10 minutes' // URL 有效期 -); -``` - -## 回调通知 - -### COS 事件通知 -可以配置 COS 事件通知,在文件上传完成时触发回调: - -1. 在 COS 控制台配置事件通知 -2. 选择事件类型:`cos:ObjectCreated:*` -3. 配置回调 URL -4. 接收通知并处理 - -### 回调数据示例 -```json -{ - "Records": [ - { - "event": { - "eventName": "cos:ObjectCreated:Put", - "eventTime": "2024-01-01T12:00:00Z" - }, - "cos": { - "cosObject": { - "key": "trtc-recording/room_123_20240101_120000.mp4", - "size": 12345678, - "url": "https://bucket.cos.ap-guangzhou.myqcloud.com/..." - } - } - } - ] -} -``` - -## 故障排查 - -### 问题1:录制文件未上传到 COS -**检查项:** -- COS Bucket 名称是否正确 -- Region 是否匹配 -- TRTC 服务是否有 COS 写入权限 -- 查看日志中的错误信息 - -### 问题2:无法访问录制文件 -**检查项:** -- Bucket 访问权限设置 -- 文件路径是否正确 -- 是否需要签名 URL - -### 问题3:录制状态一直是 Idle -**检查项:** -- RoomIdType 是否与客户端一致 -- 房间内是否有用户推流 -- 网络连接是否正常 - -## 成本估算 - -### COS 存储成本(以广州地域为例) - -**存储费用:** -- 标准存储:0.118 元/GB/月 -- 低频存储:0.08 元/GB/月 - -**流量费用:** -- 外网下行流量:0.5 元/GB -- CDN 回源流量:0.15 元/GB - -**示例计算:** -- 每天录制 10 小时,每小时 500MB -- 月存储量:10 × 30 × 0.5 = 150GB -- 月存储费用:150 × 0.118 = 17.7 元 -- 月流量(假设下载 50GB):50 × 0.5 = 25 元 -- **月总费用:约 42.7 元** - -## 相关文档 - -- [TRTC 云端录制](https://cloud.tencent.com/document/product/647/16823) -- [COS 对象存储](https://cloud.tencent.com/document/product/436) -- [COS PHP SDK](https://cloud.tencent.com/document/product/436/12266) - -## 总结 - -✅ 存储方式从 VOD 改为 COS -✅ 降低存储成本 -✅ 简化文件管理 -✅ 支持自定义路径前缀 -✅ 更新日志和提示信息 -✅ 需要配置 COS Bucket 和权限 - -迁移到 COS 后,录制文件将直接存储到对象存储桶,成本更低,管理更简单! diff --git a/UPDATE_OLD_DATA.sql b/UPDATE_OLD_DATA.sql deleted file mode 100644 index 0c6d6f05..00000000 --- a/UPDATE_OLD_DATA.sql +++ /dev/null @@ -1,18 +0,0 @@ --- 更新旧数据,为新字段设置默认值 --- 这样可以确保所有记录都有完整的数据 - -UPDATE `zyt_tcm_prescription` -SET - `usage_days` = COALESCE(`usage_days`, 7), - `usage_time` = COALESCE(`usage_time`, '饭前'), - `usage_way` = COALESCE(`usage_way`, '温水送服'), - `dietary_taboo` = COALESCE(`dietary_taboo`, ''), - `usage_notes` = COALESCE(`usage_notes`, ''), - `is_shared` = COALESCE(`is_shared`, 0) -WHERE `delete_time` IS NULL; - --- 查看更新结果 -SELECT id, sn, prescription_name, usage_days, usage_time, usage_way, dietary_taboo, usage_notes, is_shared -FROM `zyt_tcm_prescription` -WHERE `delete_time` IS NULL -LIMIT 5; diff --git a/add_bags_per_dose_field.sql b/add_bags_per_dose_field.sql deleted file mode 100644 index d359451a..00000000 --- a/add_bags_per_dose_field.sql +++ /dev/null @@ -1,3 +0,0 @@ --- 添加每贴出包数字段到处方表 -ALTER TABLE `zyt_tcm_prescription` -ADD COLUMN `bags_per_dose` TINYINT UNSIGNED NULL DEFAULT 1 COMMENT '每贴出包数(饮片专用,1-9包)' AFTER `need_decoction`; diff --git a/add_completion_request_field.sql b/add_completion_request_field.sql deleted file mode 100644 index be0586a5..00000000 --- a/add_completion_request_field.sql +++ /dev/null @@ -1,8 +0,0 @@ --- 为处方业务订单表添加完单申请字段 --- 用于记录补齐支付单时是否申请完成订单 - -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/add_dose_unit_to_prescription_order.sql b/add_dose_unit_to_prescription_order.sql deleted file mode 100644 index b25bb9ae..00000000 --- a/add_dose_unit_to_prescription_order.sql +++ /dev/null @@ -1,13 +0,0 @@ --- 添加剂量单位和剂数字段到处方业务订单表 --- 用于记录订单的剂量单位和剂数信息 - -ALTER TABLE `zyt_tcm_prescription_order` -ADD COLUMN `dose_unit` varchar(20) NOT NULL DEFAULT '剂' COMMENT '剂量单位:剂、丸、袋、盒、瓶、膏、贴' AFTER `medication_days`, -ADD COLUMN `dose_count` int(11) NOT NULL DEFAULT 1 COMMENT '剂数' AFTER `dose_unit`; - --- 验证字段是否添加成功 -SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT, COLUMN_COMMENT -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'zyt_tcm_prescription_order' - AND COLUMN_NAME IN ('dose_unit', 'dose_count'); diff --git a/add_gancao_callback_fields.sql b/add_gancao_callback_fields.sql deleted file mode 100644 index 6522c2d1..00000000 --- a/add_gancao_callback_fields.sql +++ /dev/null @@ -1,12 +0,0 @@ --- 添加甘草订单回调相关字段 --- 在 zyt_prescription_order 表中添加 - -ALTER TABLE `zyt_prescription_order` -ADD COLUMN `gancao_order_state` INT(11) DEFAULT 0 COMMENT '甘草订单状态:10=审核中,11=审核通过,110=制作中,20=物流中,30=完成,90=拦截,91=撤单,92=驳回' AFTER `gancao_submit_time`, -ADD COLUMN `gancao_flow_name` VARCHAR(100) DEFAULT '' COMMENT '甘草订单流程名称(如:派单、审方、调配、复核、浸泡、煎药、包装、发货等)' AFTER `gancao_order_state`, -ADD COLUMN `gancao_supplier` VARCHAR(100) DEFAULT '' COMMENT '甘草供应商/药房名称' AFTER `gancao_flow_name`, -ADD COLUMN `gancao_remark` VARCHAR(500) DEFAULT '' COMMENT '甘草订单备注(拦截原因、驳回原因等)' AFTER `gancao_supplier`; - --- 添加索引 -ALTER TABLE `zyt_prescription_order` -ADD INDEX `idx_gancao_order_state` (`gancao_order_state`); diff --git a/add_is_paw_field.sql b/add_is_paw_field.sql deleted file mode 100644 index 5d03d94c..00000000 --- a/add_is_paw_field.sql +++ /dev/null @@ -1,5 +0,0 @@ --- 添加 is_paw 字段到 admin 表 --- is_paw: 0=需要修改密码,1=正常(默认值) - -ALTER TABLE `la_admin` -ADD COLUMN `is_paw` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否已修改初始密码:0=需要修改,1=正常' AFTER `multipoint_login`; diff --git a/add_prescription_complete_fields.sql b/add_prescription_complete_fields.sql deleted file mode 100644 index 23f63a19..00000000 --- a/add_prescription_complete_fields.sql +++ /dev/null @@ -1,20 +0,0 @@ --- 添加处方完整字段(用量、代煎、每天几次) --- 执行前请先备份数据库 - --- 1. 添加用量相关字段(如果不存在) -ALTER TABLE `zyt_tcm_prescription` -ADD COLUMN IF NOT EXISTS `dosage_amount` decimal(10,2) DEFAULT NULL COMMENT '用量数值' AFTER `prescription_type`, -ADD COLUMN IF NOT EXISTS `dosage_unit` varchar(10) NOT NULL DEFAULT '' COMMENT '用量单位(g/ml)' AFTER `dosage_amount`, -ADD COLUMN IF NOT EXISTS `need_decoction` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否代煎(0否1是,仅饮片)' AFTER `dosage_unit`; - --- 2. 添加每天几次字段(如果不存在) -ALTER TABLE `zyt_tcm_prescription` -ADD COLUMN IF NOT EXISTS `times_per_day` int(11) NOT NULL DEFAULT 2 COMMENT '每天服用次数' AFTER `usage_days`; - --- 验证字段是否添加成功 -SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT, COLUMN_COMMENT -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'zyt_tcm_prescription' - AND COLUMN_NAME IN ('dosage_amount', 'dosage_unit', 'need_decoction', 'times_per_day') -ORDER BY ORDINAL_POSITION; diff --git a/add_prescription_dosage_fields.sql b/add_prescription_dosage_fields.sql deleted file mode 100644 index cf91eb7c..00000000 --- a/add_prescription_dosage_fields.sql +++ /dev/null @@ -1,15 +0,0 @@ --- 添加处方用量相关字段 --- 用于存储处方类型对应的用量、单位和代煎信息 - -ALTER TABLE `zyt_tcm_prescription` -ADD COLUMN `dosage_amount` decimal(10,2) DEFAULT NULL COMMENT '用量数值' AFTER `prescription_type`, -ADD COLUMN `dosage_unit` varchar(10) NOT NULL DEFAULT '' COMMENT '用量单位(g/ml)' AFTER `dosage_amount`, -ADD COLUMN `need_decoction` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否代煎(0否1是,仅饮片)' AFTER `dosage_unit`; - --- 验证字段是否添加成功 -SELECT COLUMN_NAME, DATA_TYPE, COLUMN_DEFAULT, COLUMN_COMMENT -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'zyt_tcm_prescription' - AND COLUMN_NAME IN ('dosage_amount', 'dosage_unit', 'need_decoction') -ORDER BY ORDINAL_POSITION; diff --git a/add_shipping_region_fields.sql b/add_shipping_region_fields.sql deleted file mode 100644 index 0d95a8c4..00000000 --- a/add_shipping_region_fields.sql +++ /dev/null @@ -1,5 +0,0 @@ --- 添加省市区字段到处方业务订单表 -ALTER TABLE `zyt_tcm_prescription_order` -ADD COLUMN `shipping_province` varchar(50) NOT NULL DEFAULT '' COMMENT '收货省份' AFTER `recipient_phone`, -ADD COLUMN `shipping_city` varchar(50) NOT NULL DEFAULT '' COMMENT '收货城市' AFTER `shipping_province`, -ADD COLUMN `shipping_district` varchar(50) NOT NULL DEFAULT '' COMMENT '收货区县' AFTER `shipping_city`; diff --git a/admin/src/api/qywx-msg.ts b/admin/src/api/qywx-msg.ts new file mode 100644 index 00000000..c237727e --- /dev/null +++ b/admin/src/api/qywx-msg.ts @@ -0,0 +1,184 @@ +import request from '@/utils/request' + +/** 企业微信 员工↔客户 消息收发(企业群发 + 会话内容存档) */ + +/** 会话列表(按最近消息时间倒序) */ +export interface QywxMsgSession { + id: number + staff_userid: string + external_userid: string + roomid: string + session_type: number // 1=单聊 2=群聊 + last_msg_id: string + last_msg_seq: number + last_msg_time: number + last_msg_type: string + last_msg_summary: string + unread_staff: number + create_time: number + update_time: number + customer?: { + name?: string + avatar?: string + type?: number + gender?: number + corp_name?: string + unionid?: string + } | null + staff?: { + id?: number + name?: string + avatar?: string + } | null +} + +export interface QywxMsgArchiveItem { + id: number + msgid: string + seq: number + action: 'send' | 'recall' | 'switch' + from_user: string + from_is_staff: boolean + to_list: string[] + roomid: string + msgtype: string + content: string + media_id: string + md5sum: string + file_name: string + file_size: number + file_ext: string + play_length: number + send_time: number + from_profile?: Record | null + media?: Array<{ + id: number + sdkfileid: string + status: number + file_path: string + file_url: string + file_size: number + }> +} + +export interface QywxSendTask { + id: number + admin_id: number + sender_userid: string + external_userids: string[] + chat_type: number + msg_payload: any + msg_template_id: string + fail_list: any[] + status: number + error: string + create_time: number + update_time: number +} + +export interface QywxMsgAttachment { + msgtype: 'image' | 'video' | 'file' | 'link' | 'miniprogram' + image?: { media_id: string } + video?: { media_id: string } + file?: { media_id: string } + link?: { title: string; picurl?: string; desc?: string; url: string } + miniprogram?: { title: string; pic_media_id: string; appid: string; page: string } +} + +export interface QywxSendPayload { + text?: { content: string } + attachments?: QywxMsgAttachment[] +} + +/** 会话列表 */ +export function qywxMsgSessionLists(params: { + page_no?: number + page_size?: number + admin_id?: number + staff_userid?: string + external_userid?: string + keyword?: string + only_unread?: 0 | 1 +}) { + return request.get({ url: '/qywx.message/session_list', params }) +} + +/** 单会话消息历史 */ +export function qywxMsgArchiveLists(params: { + page_no?: number + page_size?: number + session_id?: number + staff_userid?: string + external_userid?: string + roomid?: string + before_time?: number + after_time?: number +}) { + return request.get({ url: '/qywx.message/archive_list', params }) +} + +/** 清零会话未读 */ +export function qywxMsgMarkRead(session_id: number) { + return request.post({ url: '/qywx.message/mark_read', params: { session_id } }) +} + +/** 创建企业群发任务(员工代发) */ +export function qywxMsgSend(params: { + sender_userid: string + external_userids: string[] + msg_payload: QywxSendPayload + chat_type?: 'single' | 'group' +}) { + return request.post({ url: '/qywx.message/send', params }) +} + +/** 群发任务列表 */ +export function qywxMsgSendTaskLists(params: { + page_no?: number + page_size?: number + sender_userid?: string + status?: number +}) { + return request.get({ url: '/qywx.message/send_task_list', params }) +} + +/** 群发任务送达详情 */ +export function qywxMsgSendTaskDetail(task_id: number, cursor = '') { + return request.get({ url: '/qywx.message/send_task_detail', params: { task_id, cursor } }) +} + +/** 可代发员工(已绑定企微 userid) */ +export function qywxMsgStaffList(keyword = '') { + return request.get({ url: '/qywx.message/staff_list', params: { keyword } }) +} + +/** 某员工的客户 */ +export function qywxMsgCustomerOfStaff(params: { + staff_userid: string + keyword?: string + limit?: number +}) { + return request.get({ url: '/qywx.message/customer_of_staff', params }) +} + +/** 会话存档状态(SDK / 私钥诊断) */ +export function qywxMsgArchiveStatus() { + return request.get({ url: '/qywx.message/archive_status' }) +} + +/** 手动触发一次会话存档拉取 */ +export function qywxMsgPullArchive(max_batches = 5, download = false) { + return request.post({ url: '/qywx.message/pull_archive', params: { max_batches, download } }) +} + +/** 上传媒体文件到企微,返回 media_id(用于 attachments) */ +export async function qywxMsgUploadMedia(file: File, type: 'image' | 'voice' | 'video' | 'file') { + const fd = new FormData() + fd.append('file', file) + fd.append('type', type) + return request.post({ + url: '/qywx.message/upload_to_qywx', + data: fd, + headers: { 'Content-Type': 'multipart/form-data' } + }) +} diff --git a/admin/src/api/tcm.ts b/admin/src/api/tcm.ts index 55a698a0..93b9a07b 100644 --- a/admin/src/api/tcm.ts +++ b/admin/src/api/tcm.ts @@ -307,7 +307,10 @@ export function prescriptionOrderLists(params: any) { return request.get({ url: '/tcm.prescriptionOrder/lists', params }) } -/** 诊单下可关联的支付单(待支付/已支付的 zyt_order,已占用且未撤回的会排除;编辑时传 prescription_order_id 保留当前单已选) */ +/** + * 诊单下可关联的支付单(已支付 zyt_order;已占用且未撤回的会排除;编辑时传 prescription_order_id 保留当前单已选)。 + * 服务端仅返回创建时间在 2026-04-20(含)之后的支付单;编辑时本单已关联的旧单仍会出现在列表中。 + */ export function prescriptionOrderPaidPayOrders(params: { diagnosis_id: number prescription_order_id?: number diff --git a/admin/src/views/chat/components/MessageBubble.vue b/admin/src/views/chat/components/MessageBubble.vue new file mode 100644 index 00000000..095ac2ab --- /dev/null +++ b/admin/src/views/chat/components/MessageBubble.vue @@ -0,0 +1,307 @@ + + + + + diff --git a/admin/src/views/chat/components/SendPanel.vue b/admin/src/views/chat/components/SendPanel.vue new file mode 100644 index 00000000..6f430809 --- /dev/null +++ b/admin/src/views/chat/components/SendPanel.vue @@ -0,0 +1,471 @@ + + + + + diff --git a/admin/src/views/chat/index.vue b/admin/src/views/chat/index.vue new file mode 100644 index 00000000..fa458425 --- /dev/null +++ b/admin/src/views/chat/index.vue @@ -0,0 +1,1173 @@ + + + + + diff --git a/admin/src/views/consumer/prescription/index.vue b/admin/src/views/consumer/prescription/index.vue index 182d7fb4..a5ae8a28 100644 --- a/admin/src/views/consumer/prescription/index.vue +++ b/admin/src/views/consumer/prescription/index.vue @@ -1544,7 +1544,9 @@ async function loadCreateOrderPaidOrders() { } try { createOrderPaidOrdersLoading.value = true - const res: any = await prescriptionOrderPaidPayOrders({ diagnosis_id: did }) + const res: any = await prescriptionOrderPaidPayOrders({ + diagnosis_id: did + }) const lists = res?.lists ?? res?.data?.lists ?? [] createOrderPaidOrders.value = Array.isArray(lists) ? lists : [] const dep = Number(res?.deposit_min_amount ?? res?.data?.deposit_min_amount) || 0 diff --git a/admin/src/views/consumer/prescription/order_list.vue b/admin/src/views/consumer/prescription/order_list.vue index 3626faad..af563687 100644 --- a/admin/src/views/consumer/prescription/order_list.vue +++ b/admin/src/views/consumer/prescription/order_list.vue @@ -444,10 +444,10 @@ {{ detailPrescription.prescription_date || '—' }} {{ detailPrescription.doctor_name || '—' }} {{ detailPrescription.prescription_type || '—' }} - + {{ detailPrescription.clinical_diagnosis || '—' }} - + {{ detailData.dose_count ?? detailPrescription.dose_count ?? '—' }} {{ detailData.dose_unit || '剂' }} · {{ detailPrescription.usage_instruction || detailPrescription.usage_method || '—' }} @@ -1357,35 +1357,60 @@ +
-
+

{{ SLIP_HOSPITAL_NAME }}处方笺

- 已作废 + 已作废 已驳回 已通过 待审核 - 已驳回 + 已驳回