From 5fcf74deb05dd36aec60e40193d73faeeeef8d3a Mon Sep 17 00:00:00 2001 From: MiFaZhan Date: Thu, 29 Oct 2026 15:15:12 +0800 Subject: [PATCH] =?UTF-8?q?sync:=20=E5=AF=B9=E9=BD=90=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=20v1.0.2=20=E7=94=9F=E4=BA=A7=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=B9=B6=E8=84=B1=E6=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- app/Dockerfile | 1 + app/manifest.config.ts | 4 +- app/package.json | 9 +- app/patches/@wot-ui__ui@2.1.0.patch | 87 ++ app/pnpm-lock.yaml | 12 +- app/src/App.ku.vue | 7 +- app/src/App.vue | 5 +- app/src/api/card.ts | 29 +- app/src/api/diagnosis.ts | 2 +- app/src/api/executionLog.ts | 57 ++ app/src/api/login.ts | 28 + app/src/api/types/card.ts | 19 +- app/src/api/types/customer.ts | 7 + app/src/api/types/diagnosis.ts | 6 + app/src/api/types/login.ts | 18 +- app/src/api/types/warning.ts | 6 +- app/src/api/types/workOrder.ts | 33 + app/src/api/workOrder.ts | 6 + app/src/components/Navbar/index.vue | 7 +- .../components/WorkOrderAttachmentPreview.vue | 38 + .../components/WorkOrderAttachments.test.ts | 33 + app/src/components/WorkOrderAttachments.vue | 162 ++++ app/src/constant/RouterPath.ts | 82 +- app/src/constant/release.ts | 33 + app/src/http/interceptor.test.ts | 39 + app/src/http/tools/enum.ts | 2 +- app/src/manifest.json | 4 +- app/src/pages.json | 32 +- .../business/components/BusinessSection.vue | 6 +- app/src/pages/business/data.test.ts | 9 + app/src/pages/business/data.ts | 35 +- app/src/pages/business/index.test.ts | 125 +++ app/src/pages/business/index.vue | 79 +- app/src/pages/business/views/addon/index.vue | 192 ++-- .../pages/business/views/allocate/index.vue | 864 +++++++++-------- .../business/views/allocate/selection.test.ts | 74 ++ .../business/views/allocate/selection.ts | 22 + .../business/views/allocate/submit.test.ts | 81 ++ app/src/pages/business/views/api/diagnosis.ts | 44 - app/src/pages/business/views/api/workOrder.ts | 36 +- .../business/views/balanceRecharge/index.vue | 84 +- .../pages/business/views/cardRenew/index.vue | 394 ++++---- .../business/views/customer/index.test.ts | 117 +++ .../pages/business/views/customer/index.vue | 315 +++---- .../views/customerDetail/data.test.ts | 41 + .../business/views/customerDetail/data.ts | 16 + .../business/views/customerDetail/index.vue | 222 ++--- .../diagnosis/DiagnosisFlowChart.test.ts | 52 ++ .../views/diagnosis/DiagnosisFlowChart.vue | 70 ++ .../business/views/diagnosis/display.test.ts | 113 +++ .../pages/business/views/diagnosis/display.ts | 133 +++ .../pages/business/views/diagnosis/index.vue | 876 +++++++++++------- app/src/pages/business/views/order/index.vue | 27 +- .../pages/business/views/warning/index.vue | 223 +---- .../business/views/workOrder/index.test.ts | 22 + .../pages/business/views/workOrder/index.vue | 247 +++-- .../views/workOrderCreate/index.test.ts | 117 +++ .../business/views/workOrderCreate/index.vue | 267 +++--- .../views/workOrderDetail/index.test.ts | 316 +++++++ .../business/views/workOrderDetail/index.vue | 602 ++++++++++-- app/src/pages/card/action.test.ts | 23 + app/src/pages/card/action.ts | 11 +- app/src/pages/card/components/CardList.vue | 257 ++--- app/src/pages/card/components/CardToolbar.vue | 2 +- app/src/pages/card/index.vue | 171 +--- app/src/pages/card/views/detail/index.vue | 130 ++- app/src/pages/card/views/warning/index.vue | 211 +---- .../pages/home/components/PublicCardQuery.vue | 129 +-- .../pages/home/components/QuickBusiness.vue | 47 - app/src/pages/home/index.vue | 40 +- app/src/pages/login/forgot-password/index.vue | 8 +- app/src/pages/login/index.test.ts | 125 +++ app/src/pages/login/index.vue | 414 +++++---- .../pages/message/components/MessageList.vue | 184 +--- .../message/components/MessageToolbar.vue | 121 +-- app/src/pages/message/data.ts | 46 +- app/src/pages/message/index.test.ts | 82 ++ app/src/pages/message/index.vue | 619 +++++-------- app/src/pages/message/pagination.test.ts | 25 + app/src/pages/message/pagination.ts | 22 + .../pages/message/views/cardDetail/index.vue | 246 +++-- .../message/views/systemDetail/index.vue | 246 +++-- .../pages/user/components/UserEntryGroup.vue | 41 + .../pages/user/components/UserProfileCard.vue | 60 +- .../user/components/UserServiceCells.vue | 267 +----- .../pages/user/components/UserStatsCard.vue | 17 +- .../user/components/UserSupportCells.vue | 148 +-- app/src/pages/user/index.test.ts | 92 ++ app/src/pages/user/index.vue | 91 +- app/src/pages/user/views/about/index.vue | 2 +- app/src/pages/user/views/agreement/index.vue | 18 +- app/src/pages/user/views/executionLog/data.ts | 32 + .../user/views/executionLog/index.test.ts | 235 +++++ .../pages/user/views/executionLog/index.vue | 223 +++++ .../user/views/executionLogDetail/index.vue | 203 ++++ app/src/pages/user/views/manual/index.vue | 53 ++ .../user/views/notification/data.test.ts | 25 + .../user/views/notification/index.test.ts | 22 + .../pages/user/views/notification/index.vue | 398 +------- app/src/pages/user/views/profile/index.vue | 649 ++++++------- app/src/router/interceptor.ts | 8 + app/src/store/index.ts | 4 +- app/src/store/message.test.ts | 54 ++ app/src/store/message.ts | 30 +- app/src/store/user.test.ts | 36 + app/src/store/user.ts | 23 +- app/src/tabbar/config.ts | 24 +- app/src/tabbar/index.vue | 2 +- app/src/types/components.d.ts | 7 +- app/src/types/uni-pages.d.ts | 3 + app/src/uni.scss | 2 +- app/src/utils/allocateStatus.ts | 33 + app/src/utils/autoCreateRoutePath.ts | 4 +- app/src/utils/cardRecognition.ts | 24 +- app/src/utils/index.ts | 107 --- app/src/utils/lifecycleBatch.ts | 27 +- app/src/utils/loginNavigation.test.ts | 40 + app/src/utils/loginNavigation.ts | 16 + app/src/utils/permission.ts | 9 + app/src/utils/userIdentity.test.ts | 18 + app/src/utils/userIdentity.ts | 9 + app/src/utils/workOrderAttachment.test.ts | 41 + app/src/utils/workOrderAttachment.ts | 30 + .../utils/workOrderAttachmentDownload.test.ts | 53 ++ app/src/utils/workOrderAttachmentDownload.ts | 20 + app/tsconfig.json | 1 + app/vite.config.ts | 3 + app/vitest.config.ts | 14 + server/api/app/app.go | 4 + server/api/app/v1/allocate_result.go | 21 + server/api/app/v1/auth.go | 9 + server/api/app/v1/customer.go | 5 +- server/api/app/v1/execution_log.go | 28 + server/api/app/v1/notification.go | 24 + server/api/app/v1/work_order.go | 6 + server/api/common/v1/upload.go | 2 +- server/api/gen/testdemo/v1/testdemo.go | 2 +- server/api/gen/testtree/v1/testtree.go | 4 +- server/api/gen_codes/v1/gen_table.go | 2 +- server/api/monitor/v1/cache.go | 2 +- server/api/system/v1/auth.go | 2 +- server/api/system/v1/captcha.go | 2 +- server/api/system/v1/client.go | 2 +- server/api/system/v1/config.go | 2 +- server/api/system/v1/dept.go | 2 +- server/api/system/v1/dict.go | 2 +- server/api/system/v1/job.go | 2 +- server/api/system/v1/logininfor.go | 2 +- server/api/system/v1/menu.go | 2 +- server/api/system/v1/notice.go | 2 +- server/api/system/v1/online.go | 2 +- server/api/system/v1/oper_log.go | 2 +- server/api/system/v1/oss.go | 2 +- server/api/system/v1/oss_config.go | 2 +- server/api/system/v1/post.go | 2 +- server/api/system/v1/role.go | 2 +- server/api/system/v1/social.go | 2 +- server/api/system/v1/sysnoticeuser.go | 2 +- server/api/system/v1/tenant.go | 2 +- server/api/system/v1/user.go | 2 +- .../{config.yaml => config.example.yaml} | 49 +- server/config/config_default.yaml | 21 +- server/go.mod | 2 +- .../hack/{config.yaml => config.example.yaml} | 0 server/internal/cmd/cmd.go | 2 +- server/internal/cmd/global.go | 2 +- server/internal/cmd/http.go | 4 + server/internal/cmd/initfuunc.go | 2 +- server/internal/cmd/monitor.go | 4 +- server/internal/cmd/shutdown.go | 4 +- server/internal/consts/batch.go | 1 + server/internal/consts/bcache.go | 2 +- server/internal/consts/code.go | 2 +- server/internal/consts/config.go | 2 +- server/internal/consts/consts.go | 2 +- server/internal/consts/event.go | 2 +- server/internal/consts/http.go | 2 +- server/internal/consts/mcache.go | 2 +- server/internal/consts/queue.go | 6 +- server/internal/consts/system.go | 2 +- server/internal/consts/websocket.go | 2 +- .../controller/app/allocate_result.go | 39 + server/internal/controller/app/app_v1_auth.go | 8 + .../controller/app/app_v1_customer.go | 4 +- .../controller/app/app_v1_notification.go | 29 + .../controller/app/app_v1_work_order.go | 7 + .../internal/controller/app/execution_log.go | 31 + server/internal/controller/gen/test_demo.go | 2 +- server/internal/controller/gen/test_tree.go | 2 +- .../controller/system/sys_notice_user.go | 2 +- .../controller/system/system_v1_dict.go | 1 + server/internal/dao/internal/batch_main.go | 10 +- server/internal/library/addons/addons.go | 2 +- .../library/allocaterequest/request.go | 35 + .../library/allocaterequest/request_test.go | 41 + .../internal/library/allocaterequest/store.go | 25 + .../internal/library/allocationunits/units.go | 141 +++ .../library/allocationunits/units_test.go | 55 ++ server/internal/library/batchimport/async.go | 9 + server/internal/library/batchimport/runner.go | 15 +- server/internal/library/batchimport/types.go | 3 + .../library/batchmetadata/metadata.go | 52 ++ .../library/batchmetadata/metadata_test.go | 52 ++ server/internal/library/bcache/bcache.go | 2 +- .../library/bcache/bcache_system_auth.go | 4 +- server/internal/library/cache/cache.go | 2 +- server/internal/library/cache/file/file.go | 2 +- server/internal/library/contexts/contexts.go | 4 +- server/internal/library/event/event.go | 2 +- server/internal/library/mcache/mcache.go | 2 +- .../library/mcache/mcache_access_list.go | 26 +- .../library/mcache/mcache_access_list_test.go | 19 + .../library/mcache/mcache_system_config.go | 4 +- .../library/mcache/mcache_system_dept.go | 4 +- .../library/mcache/mcache_system_user.go | 4 +- server/internal/library/rabbitmq/publisher.go | 20 + server/internal/library/websocket/client.go | 2 +- .../library/websocket/client_manager.go | 2 +- .../internal/library/websocket/controller.go | 2 +- server/internal/library/websocket/init.go | 4 +- server/internal/library/websocket/model.go | 2 +- server/internal/library/websocket/router.go | 2 +- server/internal/library/worker/options.go | 2 +- server/internal/library/worker/work_proces.go | 2 +- server/internal/library/worker/worker.go | 2 +- .../library/xgen/gen_consts/gen_codes.go | 2 +- .../library/xgen/gen_consts/gen_config.go | 2 +- .../internal/library/xgen/gen_dao/gen_dao.go | 2 +- .../library/xgen/gen_model/gen_codes.go | 2 +- .../library/xgen/gen_model/gen_config.go | 2 +- .../library/xgen/gen_model/gen_view.go | 2 +- .../library/xgen/gen_view/gen_column.go | 4 +- .../xgen/gen_view/gen_column_default.go | 2 +- .../library/xgen/gen_view/gen_column_map.go | 2 +- .../library/xgen/gen_view/gen_curd.go | 6 +- .../library/xgen/gen_view/gen_curd_input.go | 4 +- .../library/xgen/gen_view/gen_curd_logic.go | 2 +- .../library/xgen/gen_view/gen_curd_model.go | 4 +- .../xgen/gen_view/gen_curd_web_edit.go | 2 +- .../xgen/gen_view/gen_curd_web_index.go | 2 +- .../xgen/gen_view/gen_curd_web_model.go | 2 +- .../xgen/gen_view/gen_curd_web_view.go | 2 +- .../library/xgen/gen_view/gen_util.go | 2 +- .../library/xgen/gen_view/gohtml/consts.go | 2 +- .../library/xgen/gen_view/gohtml/doc.go | 2 +- .../library/xgen/gen_view/gohtml/element.go | 2 +- .../library/xgen/gen_view/gohtml/formatter.go | 2 +- .../xgen/gen_view/gohtml/html_document.go | 2 +- .../library/xgen/gen_view/gohtml/parser.go | 2 +- .../xgen/gen_view/gohtml/tag_element.go | 2 +- .../xgen/gen_view/gohtml/text_element.go | 2 +- .../library/xgen/gen_view/gohtml/utils.go | 2 +- .../library/xgen/gen_view/gohtml/writer.go | 2 +- server/internal/library/xgen/xgen_curd.go | 6 +- server/internal/library/xgen/xgen_init.go | 2 +- server/internal/library/xgorm/dao.go | 4 +- server/internal/library/xgorm/dao_tree.go | 4 +- .../library/xgorm/gorm_model/gorm_tree.go | 2 +- .../library/xgorm/handler/filter_auth.go | 2 +- .../library/xgorm/handler/force_cache.go | 2 +- .../internal/library/xgorm/handler/handler.go | 2 +- .../internal/library/xgorm/handler/tenant.go | 2 +- server/internal/library/xgorm/hook/member.go | 2 +- server/internal/logic/app/app_access.go | 4 + server/internal/logic/app/app_access_test.go | 12 + server/internal/logic/app/app_auth.go | 226 ++++- server/internal/logic/app/app_auth_test.go | 79 +- .../internal/logic/app/app_card_allocate.go | 241 ++--- .../logic/app/app_card_allocate_async.go | 241 +++++ .../logic/app/app_card_allocate_async_test.go | 77 ++ .../logic/app/app_card_allocate_queue_test.go | 261 ++++++ .../logic/app/app_card_allocate_request.go | 58 ++ server/internal/logic/app/app_card_change.go | 3 + server/internal/logic/app/app_card_channel.go | 194 ++-- server/internal/logic/app/app_card_query.go | 19 +- server/internal/logic/app/app_card_renew.go | 3 +- .../logic/app/app_card_renew_batch.go | 11 +- server/internal/logic/app/app_card_restart.go | 73 +- .../logic/app/app_card_restart_test.go | 16 + server/internal/logic/app/app_customer.go | 186 +++- .../logic/app/app_customer_scope_test.go | 148 +++ .../internal/logic/app/app_customer_test.go | 125 +++ server/internal/logic/app/app_diagnosis.go | 795 ++++++++++++---- .../logic/app/app_diagnosis_contract_test.go | 103 ++ .../app/app_diagnosis_enterprise_test.go | 54 ++ .../logic/app/app_diagnosis_legacy_card.go | 44 + .../app/app_diagnosis_legacy_card_test.go | 115 +++ .../logic/app/app_diagnosis_network_block.go | 39 + .../app/app_diagnosis_network_block_test.go | 43 + .../logic/app/app_diagnosis_rate_limit.go | 381 ++++++++ .../internal/logic/app/app_diagnosis_test.go | 274 +++++- .../logic/app/app_diagnosis_traffic.go | 85 +- .../logic/app/app_diagnosis_traffic_test.go | 111 +++ .../logic/app/app_lifecycle_metadata_test.go | 33 + server/internal/logic/app/app_message.go | 6 +- server/internal/logic/app/app_notification.go | 137 +++ .../logic/app/app_notification_test.go | 111 +++ server/internal/logic/app/app_onelink_mock.go | 2 +- .../internal/logic/app/app_payment_wechat.go | 4 +- .../logic/app/app_upstream_session.go | 56 +- .../logic/app/app_upstream_session_test.go | 132 +++ server/internal/logic/app/app_user.go | 37 + server/internal/logic/app/app_warning.go | 32 +- .../logic/app/app_warning_summary_test.go | 53 ++ server/internal/logic/app/app_work_order.go | 259 ++++-- .../logic/app/app_work_order_action.go | 378 ++++++++ .../logic/app/app_work_order_action_test.go | 246 +++++ .../logic/app/app_work_order_attachment.go | 59 ++ .../app/app_work_order_attachment_test.go | 73 ++ .../logic/app/app_work_order_contact_test.go | 44 + .../logic/app/app_work_order_handler_test.go | 114 +++ .../logic/app/app_worker_batch_async.go | 4 +- server/internal/logic/app/main_test.go | 20 + server/internal/logic/gen/test_demo.go | 2 +- server/internal/logic/gen/test_tree.go | 2 +- .../internal/logic/gen_codes/gen_selects.go | 2 +- .../internal/logic/gen_codes/sys_gen_table.go | 2 +- server/internal/logic/info/info.go | 2 +- server/internal/logic/memorydb/memorydb.go | 2 +- .../internal/logic/middleware/middleware.go | 2 +- .../internal/logic/monitor/monitor_server.go | 8 +- server/internal/logic/system/sys_auth.go | 14 +- server/internal/logic/system/sys_captcha.go | 4 +- server/internal/logic/system/sys_client.go | 2 +- server/internal/logic/system/sys_config.go | 2 +- server/internal/logic/system/sys_dept.go | 2 +- server/internal/logic/system/sys_dept_test.go | 91 +- server/internal/logic/system/sys_dict_data.go | 2 +- server/internal/logic/system/sys_dict_type.go | 4 +- server/internal/logic/system/sys_job.go | 2 +- .../internal/logic/system/sys_logininfor.go | 2 +- server/internal/logic/system/sys_menu.go | 8 +- server/internal/logic/system/sys_notice.go | 2 +- .../internal/logic/system/sys_notice_user.go | 2 +- server/internal/logic/system/sys_oper_log.go | 4 +- server/internal/logic/system/sys_oss.go | 25 +- .../internal/logic/system/sys_oss_config.go | 2 +- .../logic/system/sys_oss_path_test.go | 58 +- server/internal/logic/system/sys_post.go | 2 +- server/internal/logic/system/sys_qrcode.go | 6 +- server/internal/logic/system/sys_role.go | 4 +- server/internal/logic/system/sys_social.go | 2 +- server/internal/logic/system/sys_tenant.go | 8 +- .../logic/system/sys_tenant_package.go | 2 +- server/internal/logic/system/sys_user.go | 4 +- .../internal/logic/system/sys_user_online.go | 2 +- server/internal/logic/system/sys_user_test.go | 58 +- server/internal/logic/system/task_test.go | 2 +- server/internal/model/app_auth.go | 20 +- server/internal/model/app_card.go | 16 +- server/internal/model/app_customer.go | 53 +- server/internal/model/app_diagnosis.go | 15 +- server/internal/model/app_execution_log.go | 53 ++ server/internal/model/app_notification.go | 9 + server/internal/model/app_user.go | 1 + server/internal/model/app_warning.go | 12 +- server/internal/model/app_work_order.go | 35 +- server/internal/model/context.go | 2 +- server/internal/model/do/batch_main.go | 8 +- server/internal/model/entity/batch_main.go | 8 +- server/internal/model/genin/test_demo.go | 2 +- server/internal/model/genin/test_tree.go | 4 +- server/internal/model/monitor_server.go | 2 +- server/internal/model/request/common.go | 2 +- server/internal/model/response/common.go | 2 +- server/internal/model/sys_auth.go | 2 +- server/internal/model/sys_client.go | 2 +- server/internal/model/sys_config.go | 2 +- server/internal/model/sys_dept.go | 2 +- server/internal/model/sys_dict.go | 2 +- server/internal/model/sys_gen_table.go | 2 +- server/internal/model/sys_job.go | 2 +- server/internal/model/sys_logininfor.go | 2 +- server/internal/model/sys_menu.go | 2 +- server/internal/model/sys_notice.go | 2 +- server/internal/model/sys_notice_user.go | 4 +- server/internal/model/sys_oper_log.go | 2 +- server/internal/model/sys_oss.go | 2 +- server/internal/model/sys_oss_config.go | 2 +- server/internal/model/sys_post.go | 2 +- server/internal/model/sys_role.go | 2 +- server/internal/model/sys_social.go | 2 +- server/internal/model/sys_tenant.go | 2 +- server/internal/model/sys_tenant_package.go | 2 +- server/internal/model/sys_user.go | 2 +- server/internal/model/sys_user_online.go | 2 +- server/internal/model/work_order_queue.go | 7 + server/internal/packed/packed.go | 2 +- server/internal/packed/response/response.go | 2 +- server/internal/queues/base.go | 2 +- .../queues/httpqueues/sys_opt_log_queue.go | 2 +- server/internal/router/app.go | 19 + server/internal/router/common.go | 2 +- server/internal/router/gen.go | 2 +- server/internal/router/gen_codes.go | 2 +- server/internal/router/genrouter/init.go | 2 +- server/internal/router/genrouter/test_demo.go | 2 +- server/internal/router/genrouter/test_tree.go | 4 +- server/internal/router/monitor.go | 2 +- server/internal/router/router.go | 2 +- server/internal/router/system.go | 2 +- .../service/allocatedispatch/service.go | 144 +++ .../service/allocatedispatch/service_test.go | 77 ++ server/internal/service/app.go | 7 +- .../internal/service/executionlog/service.go | 303 ++++++ .../service/executionlog/service_test.go | 251 +++++ server/internal/tasks/clear_data.go | 2 +- server/main.go | 2 +- .../generate/default/curd/api.go.template | 2 +- .../default/curd/controller.go.template | 2 +- .../generate/default/curd/input.go.template | 2 +- .../generate/default/curd/logic.go.template | 2 +- .../generate/default/curd/router.go.template | 2 +- .../20261009_batch_execution_metadata.sql | 12 + server/storage/data/xiu_admin.sql | 40 +- server/utility/convert/convert.go | 2 +- server/utility/excel/excel.go | 2 +- server/utility/gfredis/gfredis.go | 2 +- server/utility/tree/tree.go | 2 +- server/utility/tree/tree_node.go | 2 +- server/utility/utility.go | 2 +- server/utility/version/version.go | 14 +- 423 files changed, 15001 insertions(+), 6382 deletions(-) create mode 100644 app/patches/@wot-ui__ui@2.1.0.patch create mode 100644 app/src/api/executionLog.ts create mode 100644 app/src/components/WorkOrderAttachmentPreview.vue create mode 100644 app/src/components/WorkOrderAttachments.test.ts create mode 100644 app/src/components/WorkOrderAttachments.vue create mode 100644 app/src/constant/release.ts create mode 100644 app/src/http/interceptor.test.ts create mode 100644 app/src/pages/business/index.test.ts create mode 100644 app/src/pages/business/views/allocate/selection.test.ts create mode 100644 app/src/pages/business/views/allocate/selection.ts create mode 100644 app/src/pages/business/views/allocate/submit.test.ts delete mode 100644 app/src/pages/business/views/api/diagnosis.ts create mode 100644 app/src/pages/business/views/customer/index.test.ts create mode 100644 app/src/pages/business/views/customerDetail/data.test.ts create mode 100644 app/src/pages/business/views/customerDetail/data.ts create mode 100644 app/src/pages/business/views/diagnosis/DiagnosisFlowChart.test.ts create mode 100644 app/src/pages/business/views/diagnosis/DiagnosisFlowChart.vue create mode 100644 app/src/pages/business/views/diagnosis/display.test.ts create mode 100644 app/src/pages/business/views/diagnosis/display.ts create mode 100644 app/src/pages/business/views/workOrder/index.test.ts create mode 100644 app/src/pages/business/views/workOrderCreate/index.test.ts create mode 100644 app/src/pages/business/views/workOrderDetail/index.test.ts create mode 100644 app/src/pages/card/action.test.ts create mode 100644 app/src/pages/login/index.test.ts create mode 100644 app/src/pages/message/index.test.ts create mode 100644 app/src/pages/message/pagination.test.ts create mode 100644 app/src/pages/message/pagination.ts create mode 100644 app/src/pages/user/components/UserEntryGroup.vue create mode 100644 app/src/pages/user/index.test.ts create mode 100644 app/src/pages/user/views/executionLog/data.ts create mode 100644 app/src/pages/user/views/executionLog/index.test.ts create mode 100644 app/src/pages/user/views/executionLog/index.vue create mode 100644 app/src/pages/user/views/executionLogDetail/index.vue create mode 100644 app/src/pages/user/views/manual/index.vue create mode 100644 app/src/pages/user/views/notification/data.test.ts create mode 100644 app/src/pages/user/views/notification/index.test.ts create mode 100644 app/src/store/message.test.ts create mode 100644 app/src/store/user.test.ts create mode 100644 app/src/utils/allocateStatus.ts create mode 100644 app/src/utils/loginNavigation.test.ts create mode 100644 app/src/utils/loginNavigation.ts create mode 100644 app/src/utils/userIdentity.test.ts create mode 100644 app/src/utils/workOrderAttachment.test.ts create mode 100644 app/src/utils/workOrderAttachment.ts create mode 100644 app/src/utils/workOrderAttachmentDownload.test.ts create mode 100644 app/src/utils/workOrderAttachmentDownload.ts create mode 100644 app/vitest.config.ts create mode 100644 server/api/app/v1/allocate_result.go create mode 100644 server/api/app/v1/execution_log.go create mode 100644 server/api/app/v1/notification.go rename server/config/{config.yaml => config.example.yaml} (89%) rename server/hack/{config.yaml => config.example.yaml} (100%) create mode 100644 server/internal/controller/app/allocate_result.go create mode 100644 server/internal/controller/app/app_v1_notification.go create mode 100644 server/internal/controller/app/execution_log.go create mode 100644 server/internal/library/allocaterequest/request.go create mode 100644 server/internal/library/allocaterequest/request_test.go create mode 100644 server/internal/library/allocaterequest/store.go create mode 100644 server/internal/library/allocationunits/units.go create mode 100644 server/internal/library/allocationunits/units_test.go create mode 100644 server/internal/library/batchmetadata/metadata.go create mode 100644 server/internal/library/batchmetadata/metadata_test.go create mode 100644 server/internal/library/mcache/mcache_access_list_test.go create mode 100644 server/internal/logic/app/app_card_allocate_async.go create mode 100644 server/internal/logic/app/app_card_allocate_async_test.go create mode 100644 server/internal/logic/app/app_card_allocate_queue_test.go create mode 100644 server/internal/logic/app/app_card_allocate_request.go create mode 100644 server/internal/logic/app/app_customer_scope_test.go create mode 100644 server/internal/logic/app/app_customer_test.go create mode 100644 server/internal/logic/app/app_diagnosis_contract_test.go create mode 100644 server/internal/logic/app/app_diagnosis_enterprise_test.go create mode 100644 server/internal/logic/app/app_diagnosis_legacy_card.go create mode 100644 server/internal/logic/app/app_diagnosis_legacy_card_test.go create mode 100644 server/internal/logic/app/app_diagnosis_network_block.go create mode 100644 server/internal/logic/app/app_diagnosis_network_block_test.go create mode 100644 server/internal/logic/app/app_diagnosis_rate_limit.go create mode 100644 server/internal/logic/app/app_diagnosis_traffic_test.go create mode 100644 server/internal/logic/app/app_lifecycle_metadata_test.go create mode 100644 server/internal/logic/app/app_notification.go create mode 100644 server/internal/logic/app/app_notification_test.go create mode 100644 server/internal/logic/app/app_upstream_session_test.go create mode 100644 server/internal/logic/app/app_warning_summary_test.go create mode 100644 server/internal/logic/app/app_work_order_action.go create mode 100644 server/internal/logic/app/app_work_order_action_test.go create mode 100644 server/internal/logic/app/app_work_order_attachment.go create mode 100644 server/internal/logic/app/app_work_order_attachment_test.go create mode 100644 server/internal/logic/app/app_work_order_contact_test.go create mode 100644 server/internal/logic/app/app_work_order_handler_test.go create mode 100644 server/internal/logic/app/main_test.go create mode 100644 server/internal/model/app_execution_log.go create mode 100644 server/internal/model/app_notification.go create mode 100644 server/internal/model/work_order_queue.go create mode 100644 server/internal/service/allocatedispatch/service.go create mode 100644 server/internal/service/allocatedispatch/service_test.go create mode 100644 server/internal/service/executionlog/service.go create mode 100644 server/internal/service/executionlog/service_test.go create mode 100644 server/resource/migrations/20261009_batch_execution_metadata.sql diff --git a/README.md b/README.md index f3c8598..9b45d40 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ iotLCM 物联网卡全生命周期管理平台的**小程序端 + 用户端服 ## 快速开始 ### 后端 +将 `server/config/config.example.yaml` 复制为 `server/config/config.yaml` 并填写自己的凭据;使用代码生成前同样将 `server/hack/config.example.yaml` 复制为 `server/hack/config.yaml`。 + ```bash cd server @@ -51,7 +53,7 @@ pnpm dev:mp-weixin # 微信小程序,产物用微信开发者工具打开 | 位置 | 说明 | | --- | --- | -| `server/config/config.yaml` | 后端配置(**公开版凭据为占位符**) | +| `server/config/config.example.yaml` | 后端配置(**公开版凭据为占位符**) | | `app/env/.env*` | 前端环境变量(**公开版 AppID 为占位符**) | 后端配置占位: diff --git a/app/Dockerfile b/app/Dockerfile index ada19b1..d69440e 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -11,6 +11,7 @@ FROM ${BASE_REGISTRY}/node:24-alpine AS builder WORKDIR /build/app COPY package.json pnpm-lock.yaml .npmrc ./ +COPY patches ./patches RUN npm config set registry https://registry.npmmirror.com/ && \ npm install -g pnpm@10.10.0 --registry=https://registry.npmmirror.com/ && \ diff --git a/app/manifest.config.ts b/app/manifest.config.ts index 607a04b..b4c4a31 100644 --- a/app/manifest.config.ts +++ b/app/manifest.config.ts @@ -25,8 +25,8 @@ export default defineManifestConfig({ 'name': VITE_APP_TITLE, 'appid': VITE_UNI_APPID, 'description': '', - 'versionName': '1.0.0', - 'versionCode': '100', + 'versionName': '1.0.2', + 'versionCode': '102', 'transformPx': false, 'locale': VITE_FALLBACK_LOCALE, // 'zh-Hans' 'h5': { diff --git a/app/package.json b/app/package.json index 9067152..3351d1b 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { "name": "iotlcm-app-ui", "type": "module", - "version": "1.0.0", + "version": "1.0.2", "unibest": { "platforms": [ "h5", @@ -25,7 +25,7 @@ "author": { "name": "feige996", "zhName": "菲鸽", - "email": "1020103647@example.com", + "email": "xxx@example.com", "github": "https://github.com/feige996", "gitee": "https://gitee.com/feige996" }, @@ -112,6 +112,7 @@ "upload:changeset": "pnpm changeset && pnpm changeset version", "upload:mp": "node ./scripts/upload-weixin.js", "type-check": "vue-tsc --noEmit", + "test:run": "vitest run", "lint": "eslint", "lint:fix": "eslint --fix", "format": "prettier . --write", @@ -202,11 +203,15 @@ "vite": "5.2.8", "vite-plugin-restart": "^1.0.0", "vite-plugin-vue-devtools": "^8.0.5", + "vitest": "3.2.4", "vue-tsc": "^3.0.6" }, "pnpm": { "overrides": { "unconfig": "7.3.2" + }, + "patchedDependencies": { + "@wot-ui/ui@2.1.0": "patches/@wot-ui__ui@2.1.0.patch" } }, "overrides": { diff --git a/app/patches/@wot-ui__ui@2.1.0.patch b/app/patches/@wot-ui__ui@2.1.0.patch new file mode 100644 index 0000000..a64506c --- /dev/null +++ b/app/patches/@wot-ui__ui@2.1.0.patch @@ -0,0 +1,87 @@ +diff --git a/components/wd-button/wd-button.vue b/components/wd-button/wd-button.vue +index 8b2610d90c8ebe78e2726179bb83f2b688ec0d96..c25b26965ccfb9470cc3c2ab9e3b2a1576c3220d 100644 +--- a/components/wd-button/wd-button.vue ++++ b/components/wd-button/wd-button.vue +@@ -18,7 +18,7 @@ + ]" + :hover-start-time="hoverStartTime" + :hover-stay-time="hoverStayTime" +- :open-type="openTypeValue" ++ :open-type="openTypeValue as UniHelper.ButtonOpenType" + :send-message-title="sendMessageTitle" + :send-message-path="sendMessagePath" + :send-message-img="sendMessageImg" +diff --git a/components/wd-fab/types.ts b/components/wd-fab/types.ts +index 50ca94f1a95a54c95fe68d9ed3535b45f1e36bcc..57b5ceb1143a0e9385cc280fe8054ed87f255807 100644 +--- a/components/wd-fab/types.ts ++++ b/components/wd-fab/types.ts +@@ -87,7 +87,7 @@ export const fabProps = { + */ + gap: { + type: Object as PropType, +- default: () => ({}) ++ default: (): FabGap => ({}) + }, + /** + * 点击时是否展开菜单 +diff --git a/components/wd-swiper/wd-swiper.vue b/components/wd-swiper/wd-swiper.vue +index c662e565171dad9c350b7215dddd3e8fc11320ab..54a5260a40ed1d6554a7e1b2158f94d8598b1c10 100644 +--- a/components/wd-swiper/wd-swiper.vue ++++ b/components/wd-swiper/wd-swiper.vue +@@ -158,9 +158,9 @@ const swiperIndicator = computed(() => { + indicatorPosition: indicatorPosition || 'bottom' + } + if (isObj(indicator)) { +- swiperIndicator.type = indicator.type || 'dots' +- swiperIndicator.minShowNum = indicator.minShowNum || 2 +- swiperIndicator.showControls = indicator.showControls || false ++ swiperIndicator.type = (indicator as Partial).type || 'dots' ++ swiperIndicator.minShowNum = (indicator as Partial).minShowNum || 2 ++ swiperIndicator.showControls = (indicator as Partial).showControls || false + } + return swiperIndicator + }) +diff --git a/components/wd-tour/wd-tour.vue b/components/wd-tour/wd-tour.vue +index d1c6e895a819a4e15178930c3869a7034d741887..2eacba3bb4b20621d94465d17a94d88ccea6195e 100644 +--- a/components/wd-tour/wd-tour.vue ++++ b/components/wd-tour/wd-tour.vue +@@ -69,7 +69,7 @@ export default { + + + diff --git a/app/src/components/WorkOrderAttachments.test.ts b/app/src/components/WorkOrderAttachments.test.ts new file mode 100644 index 0000000..afffaf9 --- /dev/null +++ b/app/src/components/WorkOrderAttachments.test.ts @@ -0,0 +1,33 @@ +import { readFileSync } from 'node:fs' +import { createContext, runInContext } from 'node:vm' +import { ModuleKind, ScriptTarget, transpileModule } from 'typescript' +import { describe, expect, it, vi } from 'vitest' +import { workOrderAttachmentName } from '@/utils/workOrderAttachment' + +const source = readFileSync(new URL('./WorkOrderAttachments.vue', import.meta.url), 'utf8') + +describe('工单上传原始文件名', () => { + it('微信上传携带原名,并保留URL字符串协议', async () => { + const emit = vi.fn() + const upload = vi.fn((options) => { + options.success({ statusCode: 200, data: JSON.stringify({ code: 0, data: { url: 'https://files/random.pdf', originalName: '说明.pdf' } }) }) + }) + const context = createContext({ + exports: {}, + require: (name: string) => name === '@/utils/workOrderAttachment' ? { workOrderAttachmentName } : name === '@/utils' ? { getEnvBaseUrl: () => 'https://api/api/v1' } : {}, + defineProps: () => ({ modelValue: [], disabled: false }), + defineEmits: () => emit, + ref: (value: unknown) => ({ value }), + uni: { uploadFile: upload, showToast: vi.fn() }, + }) + const script = source.match(/]*>([\s\S]*?)<\/script>/)?.[1] + if (!script) + throw new Error('未找到附件上传脚本') + runInContext(transpileModule(script, { compilerOptions: { module: ModuleKind.CommonJS, target: ScriptTarget.ES2020 } }).outputText, context) + await runInContext('upload("wxfile://random.pdf", "说明.pdf", 20)', context) + expect(upload).toHaveBeenCalledWith(expect.objectContaining({ url: '/work-order/upload', formData: { fileType: 'file', originalName: '说明.pdf' } })) + expect(emit).toHaveBeenCalledWith('update:modelValue', ['https://files/random.pdf']) + expect(runInContext('nameOf("https://files/random.pdf")', context)).toBe('说明.pdf') + expect(source).toContain('form.append(\'originalName\', name)') + }) +}) diff --git a/app/src/components/WorkOrderAttachments.vue b/app/src/components/WorkOrderAttachments.vue new file mode 100644 index 0000000..5ea52bf --- /dev/null +++ b/app/src/components/WorkOrderAttachments.vue @@ -0,0 +1,162 @@ + + + diff --git a/app/src/constant/RouterPath.ts b/app/src/constant/RouterPath.ts index d632ec8..ca795e3 100644 --- a/app/src/constant/RouterPath.ts +++ b/app/src/constant/RouterPath.ts @@ -6,76 +6,82 @@ * 页面路径常量 */ const RouterPath = { - /** 山东平台物联卡 */ - Home: '/pages/home/index', + /** 示例企业物联卡 */ + 'Home': '/pages/home/index', /** 办理 */ - Business: '/pages/business/index', + 'Business': '/pages/business/index', /** 卡片 */ - Card: '/pages/card/index', - /** 山东平台物联卡 */ - Login: '/pages/login/index', + 'Card': '/pages/card/index', + /** 示例企业物联卡 */ + 'Login': '/pages/login/index', /** 消息 */ - Message: '/pages/message/index', + 'Message': '/pages/message/index', /** 我的 */ - User: '/pages/user/index', + 'User': '/pages/user/index', /** 找回密码 */ 'LoginForgot-password': '/pages/login/forgot-password/index', /** 注册账号 */ - LoginRegister: '/pages/login/register/index', + 'LoginRegister': '/pages/login/register/index', /** 加油包 */ - BusinessViewsAddon: '/pages/business/views/addon/index', + 'BusinessViewsAddon': '/pages/business/views/addon/index', /** 批量卡片划拨 */ - BusinessViewsAllocate: '/pages/business/views/allocate/index', + 'BusinessViewsAllocate': '/pages/business/views/allocate/index', /** 余额充值 */ - BusinessViewsBalanceRecharge: '/pages/business/views/balanceRecharge/index', + 'BusinessViewsBalanceRecharge': '/pages/business/views/balanceRecharge/index', /** 单卡套餐续费 */ - BusinessViewsCardRenew: '/pages/business/views/cardRenew/index', + 'BusinessViewsCardRenew': '/pages/business/views/cardRenew/index', /** 套餐变更 */ - BusinessViewsChange: '/pages/business/views/change/index', + 'BusinessViewsChange': '/pages/business/views/change/index', /** 客户管理 */ - BusinessViewsCustomer: '/pages/business/views/customer/index', + 'BusinessViewsCustomer': '/pages/business/views/customer/index', /** 客户详情 */ - BusinessViewsCustomerDetail: '/pages/business/views/customerDetail/index', + 'BusinessViewsCustomerDetail': '/pages/business/views/customerDetail/index', /** 卡片排查 */ - BusinessViewsDiagnosis: '/pages/business/views/diagnosis/index', + 'BusinessViewsDiagnosis': '/pages/business/views/diagnosis/index', /** 我的订单 */ - BusinessViewsOrder: '/pages/business/views/order/index', + 'BusinessViewsOrder': '/pages/business/views/order/index', /** 批量套餐续费 */ - BusinessViewsRenew: '/pages/business/views/renew/index', + 'BusinessViewsRenew': '/pages/business/views/renew/index', /** 预警配置 */ - BusinessViewsWarning: '/pages/business/views/warning/index', + 'BusinessViewsWarning': '/pages/business/views/warning/index', /** 工单列表 */ - BusinessViewsWorkOrder: '/pages/business/views/workOrder/index', + 'BusinessViewsWorkOrder': '/pages/business/views/workOrder/index', /** 提交工单 */ - BusinessViewsWorkOrderCreate: '/pages/business/views/workOrderCreate/index', + 'BusinessViewsWorkOrderCreate': '/pages/business/views/workOrderCreate/index', /** 工单详情 */ - BusinessViewsWorkOrderDetail: '/pages/business/views/workOrderDetail/index', + 'BusinessViewsWorkOrderDetail': '/pages/business/views/workOrderDetail/index', /** 卡片详情 */ - CardViewsDetail: '/pages/card/views/detail/index', + 'CardViewsDetail': '/pages/card/views/detail/index', /** 全部设备 */ - CardViewsManagement: '/pages/card/views/management/index', + 'CardViewsManagement': '/pages/card/views/management/index', /** 卡片详情 */ - CardViewsPackageDetail: '/pages/card/views/packageDetail/index', + 'CardViewsPackageDetail': '/pages/card/views/packageDetail/index', /** 预警配置 */ - CardViewsWarning: '/pages/card/views/warning/index', + 'CardViewsWarning': '/pages/card/views/warning/index', /** 消息详情 */ - MessageViewsCardDetail: '/pages/message/views/cardDetail/index', + 'MessageViewsCardDetail': '/pages/message/views/cardDetail/index', /** 消息详情 */ - MessageViewsSystemDetail: '/pages/message/views/systemDetail/index', + 'MessageViewsSystemDetail': '/pages/message/views/systemDetail/index', /** 关于我们 */ - UserViewsAbout: '/pages/user/views/about/index', + 'UserViewsAbout': '/pages/user/views/about/index', /** 协议与隐私 */ - UserViewsAgreement: '/pages/user/views/agreement/index', + 'UserViewsAgreement': '/pages/user/views/agreement/index', /** 联系客服 */ - UserViewsContact: '/pages/user/views/contact/index', + 'UserViewsContact': '/pages/user/views/contact/index', + /** 批次执行日志 */ + 'UserViewsExecutionLog': '/pages/user/views/executionLog/index', + /** 执行日志详情 */ + 'UserViewsExecutionLogDetail': '/pages/user/views/executionLogDetail/index', /** 帮助与反馈 */ - UserViewsFeedback: '/pages/user/views/feedback/index', - /** 微信提醒 */ - UserViewsNotification: '/pages/user/views/notification/index', - /** 账号资料 */ - UserViewsProfile: '/pages/user/views/profile/index', + 'UserViewsFeedback': '/pages/user/views/feedback/index', + /** 使用手册 */ + 'UserViewsManual': '/pages/user/views/manual/index', + /** 消息提醒 */ + 'UserViewsNotification': '/pages/user/views/notification/index', + /** 个人资料 */ + 'UserViewsProfile': '/pages/user/views/profile/index', /** 实名信息 */ - UserViewsRealName: '/pages/user/views/realName/index', + 'UserViewsRealName': '/pages/user/views/realName/index', } export default RouterPath diff --git a/app/src/constant/release.ts b/app/src/constant/release.ts new file mode 100644 index 0000000..c6fc6b3 --- /dev/null +++ b/app/src/constant/release.ts @@ -0,0 +1,33 @@ +/** + * v1.0.2 front-end release scope (legacy export names retained). + * + * Business, customer management and authorized allocation are now available. + * Other unreleased lifecycle actions remain hidden. The API layer is unchanged; + * these switches keep every front-end entry point consistent. + */ +export const V101_RELEASE = { + showWechatLogin: false, + showRegister: false, + showBindCard: false, + showCardMoreActions: false, + showBusinessTab: true, + showUserBusinessEntries: false, + showDiagnosisFlowForNonAdmin: false, +} as const + +/** Routes that must remain unreachable from direct front-end navigation. */ +export const V101_HIDDEN_ROUTE_PATHS = new Set([ + '/pages/business/views/addon/index', + '/pages/business/views/balanceRecharge/index', + '/pages/business/views/cardRenew/index', + '/pages/business/views/change/index', + '/pages/business/views/order/index', + '/pages/business/views/renew/index', + '/pages/business/views/warning/index', + '/pages/card/views/packageDetail/index', + '/pages/card/views/warning/index', +]) + +export function isV101HiddenRoute(path: string) { + return V101_HIDDEN_ROUTE_PATHS.has(path) +} diff --git a/app/src/http/interceptor.test.ts b/app/src/http/interceptor.test.ts new file mode 100644 index 0000000..0b19569 --- /dev/null +++ b/app/src/http/interceptor.test.ts @@ -0,0 +1,39 @@ +import { readFileSync } from 'node:fs' +import { afterEach, describe, expect, it, vi } from 'vitest' + +const config = vi.hoisted(() => ({ baseUrl: '/app/api/v1' })) + +vi.mock('@/utils', () => ({ getEnvBaseUrl: () => config.baseUrl })) +vi.mock('@/store', () => ({ + useTokenStore: () => ({ updateNowTime: () => ({ validToken: 'test-token' }) }), +})) + +afterEach(() => { + vi.unstubAllGlobals() + vi.resetModules() +}) + +describe('工单附件上传地址', () => { + it.each([ + '/app/api/v1', + 'http://localhost:8010/api/v1', + 'https://example.com/app/api/v1', + ])('基础地址 %s 只拼接一次,并保留鉴权', async (baseUrl) => { + config.baseUrl = baseUrl + const addInterceptor = vi.fn() + vi.stubGlobal('uni', { addInterceptor }) + const { requestInterceptor } = await import('./interceptor') + requestInterceptor.install() + const uploadInterceptor = addInterceptor.mock.calls.find(([name]) => name === 'uploadFile')?.[1] + expect(uploadInterceptor).toBeDefined() + const options = uploadInterceptor.invoke({ url: '/work-order/upload' }) + expect(options.url).toBe(`${baseUrl}/work-order/upload`) + expect(options.header.Authorization).toBe('Bearer test-token') + }) + + it('原生上传交由拦截器拼接,H5 fetch 保留基础地址', () => { + const source = readFileSync(new URL('../components/WorkOrderAttachments.vue', import.meta.url), 'utf8') + expect(source).toMatch(/uni\.uploadFile\(\{\s*url: '\/work-order\/upload'/) + expect(source).toMatch(/fetch\(`\$\{getEnvBaseUrl\(\)\}\/work-order\/upload`/) + }) +}) diff --git a/app/src/http/tools/enum.ts b/app/src/http/tools/enum.ts index 8ea91ff..72cfce0 100644 --- a/app/src/http/tools/enum.ts +++ b/app/src/http/tools/enum.ts @@ -28,7 +28,7 @@ const networkErrorPattern = /request:fail|timeout|failed to fetch|networkerror|err_connection|econnrefused|econnreset|econnaborted|enotfound|net::|socket|connect|refused|abort|not in domain list|url not in domain list|ssl|tls|certificate|网络|超时/i const technicalDetailPattern - = /https?:\/\/|\/api\/|接口[::]|errMsg|statusCode|StatusCode|request:|XMLHttpRequest|TypeError:|Error:/i + = /https?:\/\/|\/api\/|接口[::]|errMsg|statusCode|request:|XMLHttpRequest|TypeError:|Error:/i /** * 根据状态码,生成对应的错误信息 diff --git a/app/src/manifest.json b/app/src/manifest.json index 05eeedf..365cb58 100644 --- a/app/src/manifest.json +++ b/app/src/manifest.json @@ -2,8 +2,8 @@ "name": "山东平台物联卡", "appid": "__UNI__YOURAPPID", "description": "", - "versionName": "1.0.0", - "versionCode": "100", + "versionName": "1.0.2", + "versionCode": "102", "transformPx": false, "app-plus": { "usingComponents": true, diff --git a/app/src/pages.json b/app/src/pages.json index 34fd6e3..386f33b 100644 --- a/app/src/pages.json +++ b/app/src/pages.json @@ -21,7 +21,7 @@ "type": "home", "style": { "navigationStyle": "default", - "navigationBarTitleText": "山东平台物联卡", + "navigationBarTitleText": "示例企业物联卡", "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black" }, @@ -49,7 +49,7 @@ "type": "page", "name": "Login", "style": { - "navigationBarTitleText": "山东平台物联卡", + "navigationBarTitleText": "示例企业物联卡", "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black", "disableScroll": true @@ -180,7 +180,8 @@ "path": "workOrder/index", "type": "page", "style": { - "navigationBarTitleText": "工单列表" + "navigationBarTitleText": "工单列表", + "enablePullDownRefresh": true } }, { @@ -290,7 +291,7 @@ "path": "notification/index", "type": "page", "style": { - "navigationBarTitleText": "微信提醒" + "navigationBarTitleText": "消息提醒" } }, { @@ -298,7 +299,7 @@ "type": "page", "excludeLoginPath": true, "style": { - "navigationBarTitleText": "账号资料" + "navigationBarTitleText": "个人资料" } }, { @@ -307,6 +308,27 @@ "style": { "navigationBarTitleText": "实名信息" } + }, + { + "path": "manual/index", + "type": "page", + "style": { + "navigationBarTitleText": "使用手册" + } + }, + { + "path": "executionLog/index", + "type": "page", + "style": { + "navigationBarTitleText": "批次执行日志" + } + }, + { + "path": "executionLogDetail/index", + "type": "page", + "style": { + "navigationBarTitleText": "执行日志详情" + } } ] } diff --git a/app/src/pages/business/components/BusinessSection.vue b/app/src/pages/business/components/BusinessSection.vue index c74d6a0..0c70d84 100644 --- a/app/src/pages/business/components/BusinessSection.vue +++ b/app/src/pages/business/components/BusinessSection.vue @@ -3,6 +3,7 @@ import type { BusinessServiceItem, BusinessServiceSection, BusinessTone } from ' const props = defineProps<{ section: BusinessServiceSection + busy?: boolean }>() const emit = defineEmits<{ @@ -19,12 +20,13 @@ const toneClass: Record = { } function handleClick(item: BusinessServiceItem) { - emit('action', item) + if (!props.busy) + emit('action', item) } function visibleRoleTag(item: BusinessServiceItem) { if (item.agentOnly) - return '代理商' + return '企业 / 代理' if (item.terminalOnly) return '用户' if (item.terminalHidden) diff --git a/app/src/pages/business/data.test.ts b/app/src/pages/business/data.test.ts index 0cce706..ad4c030 100644 --- a/app/src/pages/business/data.test.ts +++ b/app/src/pages/business/data.test.ts @@ -21,6 +21,15 @@ const expectedSubViews = [ ] describe('getUserBusinessSections', () => { + it('按服务、资源管理和执行记录划分入口且不重复', () => { + const sectionItems = (title: string) => businessSections.find(section => section.title === title)?.items.map(item => item.action) + expect(sectionItems('服务记录')).toEqual(['order', 'workOrder']) + expect(sectionItems('卡片与客户')).toEqual(['cardList', 'customer']) + expect(sectionItems('执行记录')).toEqual(['executionLog']) + const titles = businessSections.flatMap(section => section.items.map(item => item.title)) + expect(new Set(titles).size).toBe(titles.length) + }) + it('只保留用户侧一级业务入口', () => { const sections = getUserBusinessSections(businessSections) const titles = sections.flatMap(section => section.items.map(item => item.title)) diff --git a/app/src/pages/business/data.ts b/app/src/pages/business/data.ts index 482f7b1..c05a290 100644 --- a/app/src/pages/business/data.ts +++ b/app/src/pages/business/data.ts @@ -1,6 +1,6 @@ import type { IUserInfoRes } from '@/api/types/login' import type { AppPermissionAction } from '@/utils/permission' -import { hasUserPermission } from '@/utils/permission' +import { hasAccessCode, hasUserPermission } from '@/utils/permission' import { getAccountWorkspace, isTerminalUserInfo } from '@/utils/userIdentity' export type BusinessTone = 'blue' | 'green' | 'orange' | 'primary' | 'purple' | 'red' @@ -18,11 +18,13 @@ export interface BusinessServiceItem { | 'cardList' | 'change' | 'customer' + | 'executionLog' | 'diagnosis' | 'order' | 'renew' | 'warning' | 'workOrder' + accessCode?: string agentOnly?: boolean terminalOnly?: boolean terminalHidden?: boolean @@ -113,7 +115,7 @@ export const businessSections: BusinessServiceSection[] = [ }, { title: '预警配置', - desc: '流量阈值与微信订阅', + desc: '流量阈值与站内消息提醒', tag: '预警', icon: 'i-carbon-warning-alt', tone: 'orange', @@ -134,7 +136,7 @@ export const businessSections: BusinessServiceSection[] = [ }, { title: '服务记录', - desc: '订单与工单', + desc: '订单查询与问题反馈', type: 'grid', items: [ { @@ -154,6 +156,13 @@ export const businessSections: BusinessServiceSection[] = [ action: 'workOrder', permissionAction: ['workOrder', 'workOrderCreate'], }, + ], + }, + { + title: '卡片与客户', + desc: '卡片信息与客户管理', + type: 'list', + items: [ { title: '我的卡片', desc: '查看卡信息、套餐和流量', @@ -164,7 +173,7 @@ export const businessSections: BusinessServiceSection[] = [ }, { title: '客户管理', - desc: '直属客户与客户详情', + desc: '授权范围内的客户与详情', tag: '客户', icon: 'i-carbon-user-multiple', tone: 'green', @@ -174,6 +183,22 @@ export const businessSections: BusinessServiceSection[] = [ }, ], }, + { + title: '执行记录', + desc: '批次进度与执行结果', + type: 'list', + items: [ + { + title: '批次执行日志', + desc: '查看批次进度与执行结果', + tag: '日志', + icon: 'i-carbon-document', + tone: 'blue', + action: 'executionLog', + accessCode: 'cpm:common:batchExecutionLog:list', + }, + ], + }, ] /** ==================== 方法 ==================== */ @@ -191,6 +216,8 @@ export function getUserBusinessSections( .map(section => ({ ...section, items: section.items.filter((item) => { + if (item.accessCode && !hasAccessCode(userInfo?.accessCodes, item.accessCode)) + return false if (item.agentOnly && !isAgentUser) return false if (item.terminalOnly && !isTerminalUser) diff --git a/app/src/pages/business/index.test.ts b/app/src/pages/business/index.test.ts new file mode 100644 index 0000000..ca424a5 --- /dev/null +++ b/app/src/pages/business/index.test.ts @@ -0,0 +1,125 @@ +import type { IUserInfoRes } from '@/api/types/login' +import { readFileSync } from 'node:fs' +import { createContext, runInContext } from 'node:vm' +import { ModuleKind, transpileModule } from 'typescript' +import { describe, expect, it, vi } from 'vitest' +import RouterPath from '@/constant/RouterPath' +import * as release from '@/constant/release' +import { customTabbarList } from '@/tabbar/config' +import * as permission from '@/utils/permission' +import * as data from './data' + +function createHarness(userInfo: Partial) { + const navigateTo = vi.fn((options: { complete?: () => void }) => options.complete?.()) + const switchTab = vi.fn((options: { complete?: () => void }) => options.complete?.()) + const fetchUserInfo = vi.fn(async () => userInfo) + const tokenStore = { hasLogin: true, updateNowTime: () => tokenStore } + const context = createContext({ + exports: {}, + require: (name: string) => { + if (name === '@/constant/RouterPath') + return { default: RouterPath } + if (name === '@/constant/release') + return release + if (name === '@/utils/permission') + return permission + if (name === '@/store/user') + return { useUserStore: () => ({ userInfo, fetchUserInfo }) } + if (name === '@/store/token') + return { useTokenStore: () => tokenStore } + if (name === './data') + return data + if (name === './components/BusinessSection.vue') + return {} + throw new Error(`Unexpected import: ${name}`) + }, + ref: (value: unknown) => ({ value }), + computed: (getter: () => unknown) => ({ get value() { return getter() } }), + definePage: () => undefined, + onShow: () => undefined, + uni: { navigateTo, switchTab, showToast: vi.fn() }, + Promise, + }) + const source = readFileSync(new URL('./index.vue', import.meta.url), 'utf8') + const script = source.match(/]*>([\s\S]*?)<\/script>/)?.[1] + if (!script) + throw new Error('Business page script not found') + runInContext(transpileModule(script, { compilerOptions: { module: ModuleKind.CommonJS } }).outputText, context) + return { + titles: () => Array.from(runInContext('sections.value.flatMap(section => section.items.map(item => item.title))', context)) as string[], + action: (title: string) => runInContext(`handleBusinessAction(sections.value.flatMap(section => section.items).find(item => item.title === ${JSON.stringify(title)}))`, context) as Promise, + fetchUserInfo, + navigateTo, + switchTab, + } +} + +function visibleTitles(userInfo: Partial) { + return createHarness(userInfo).titles() +} + +describe('办理入口发布范围', () => { + it('沿用紧凑办理布局,不展示大块消息提醒入口', () => { + const source = readFileSync(new URL('./index.vue', import.meta.url), 'utf8') + const template = source.split('')[0] + expect(template).toContain('assist-card') + expect(template).toContain(':busy="actionPending"') + expect(template).not.toContain('查看业务消息') + expect(template).not.toContain('微信提醒设置') + expect(template).not.toContain('业务办理') + }) + + it('身份标签放在业务标题旁,保持单行且不挤占右侧区域', () => { + const source = readFileSync(new URL('./components/BusinessSection.vue', import.meta.url), 'utf8') + expect(source).toContain('biz-title-row') + expect(source).toContain('app-tag app-tag--info biz-role-tag') + expect(source).toContain('white-space: nowrap') + expect(source).not.toContain('title-width="75%"') + }) + + it('同一办理入口连续点击只跳转一次', async () => { + const harness = createHarness({ userId: 1, username: 'admin' }) + await Promise.all([harness.action('我的卡片'), harness.action('我的卡片')]) + expect(harness.fetchUserInfo).toHaveBeenCalledTimes(1) + expect(harness.switchTab).toHaveBeenCalledTimes(1) + expect(harness.switchTab.mock.calls[0][0]).toMatchObject({ url: RouterPath.Card }) + }) + + it('权限更新后不再跳转已失效的客户入口', async () => { + const user = { userId: 12, userType: 'sys_user', appAccess: { configured: true, features: ['customer:list'] } } + const harness = createHarness(user) + harness.fetchUserInfo.mockImplementationOnce(async () => { + user.appAccess.features = [] + return user + }) + await harness.action('客户管理') + expect(harness.navigateTo).not.toHaveBeenCalled() + }) + + it('工单入口按列表或提交权限开放,路由可达', () => { + for (const feature of ['workorder:list', 'workorder:create']) + expect(visibleTitles({ userId: 12, appAccess: { configured: true, features: [feature] } })).toContain('工单服务') + expect(visibleTitles({ userId: 12, appAccess: { configured: true, features: [] } })).not.toContain('工单服务') + for (const route of [RouterPath.BusinessViewsWorkOrder, RouterPath.BusinessViewsWorkOrderCreate, RouterPath.BusinessViewsWorkOrderDetail]) + expect(release.isV101HiddenRoute(route)).toBe(false) + expect(release.isV101HiddenRoute(RouterPath.BusinessViewsRenew)).toBe(true) + }) + it('批次日志入口只对拥有原有日志权限的账号开放', () => { + expect(visibleTitles({ userId: 12, userType: 'sys_user', accessCodes: ['cpm:common:batchExecutionLog:list'] })).toContain('批次执行日志') + expect(visibleTitles({ userId: 13, userType: 'sys_user', accessCodes: [] })).not.toContain('批次执行日志') + }) + it('恢复首页、设备、办理、我的顺序', () => { + expect(customTabbarList.map(item => item.text)).toEqual(['首页', '设备', '办理', '我的']) + expect(release.isV101HiddenRoute('/pages/business/index')).toBe(false) + }) + + it('管理员只能看到已开放业务,不展示仍受发布限制的操作', () => { + expect(visibleTitles({ userId: 1, username: 'admin' })).toEqual(['卡片排查', '卡片划拨', '工单服务', '我的卡片', '客户管理']) + }) + + it('客户入口继续受账号身份和权限限制', () => { + expect(visibleTitles({ userId: 12, userType: 'sys_user', appAccess: { configured: true, features: ['customer:list'] } })).toContain('客户管理') + expect(visibleTitles({ userId: 13, userType: 'sys_user', appAccess: { configured: true, features: [] } })).not.toContain('客户管理') + expect(visibleTitles({ userId: 14, userType: 'app_user' })).not.toContain('客户管理') + }) +}) diff --git a/app/src/pages/business/index.vue b/app/src/pages/business/index.vue index 1a0f480..259cdf5 100644 --- a/app/src/pages/business/index.vue +++ b/app/src/pages/business/index.vue @@ -1,10 +1,11 @@