From 2ed5dfa6a10f03ca64cc9aede85f567b128c771b Mon Sep 17 00:00:00 2001 From: MiFaZhan Date: Mon, 26 Oct 2026 14:01:25 +0800 Subject: [PATCH 1/9] =?UTF-8?q?chore:=20=E5=88=9D=E5=A7=8B=E5=BC=80?= =?UTF-8?q?=E6=BA=90=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iotlcm-app:小程序端 + 用户端服务。 - app/:uni-app 小程序前端(基于 unibest,MIT) - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID - 内网后端地址 → admin.iotlcm.example.com - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明) - server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0) - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件) - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化 - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位) - 依赖号码尾部的断言同步更新 - 补 server/LICENSE(Apache-2.0) - 移除内部资产:CI 触发器 - 补根 README,LF 行尾 + .gitattributes 已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。 --- .gitattributes | 14 + .gitignore | 70 + AGENTS.md | 94 + LICENSE | 201 + README.md | 107 + app/.commitlintrc.cjs | 3 + app/.dockerignore | 11 + app/.editorconfig | 13 + app/.gitignore | 6 + app/.npmrc | 9 + app/.prettierignore | 9 + app/.prettierrc.json | 6 + app/Dockerfile | 129 + app/LICENSE | 21 + app/README.md | 66 + app/env/.env | 31 + app/env/.env.development | 10 + app/env/.env.production | 11 + app/env/.env.test | 10 + app/eslint.config.mjs | 60 + app/favicon.ico | Bin 0 -> 14575 bytes app/folder-alias.json | 5 + app/index.html | 28 + app/manifest.config.ts | 165 + app/openapi-ts-request.config.ts | 14 + app/package.json | 222 + app/pages.config.ts | 27 + app/pnpm-lock.yaml | 22285 ++++++++++++++++ app/project.config.json | 35 + app/project.private.config.json | 22 + app/public/favicon.png | Bin 0 -> 78475 bytes app/public/logo.png | Bin 0 -> 78475 bytes app/scripts/bump-version.js | 154 + app/scripts/create-base-files.js | 55 + app/scripts/open-dev-tools.js | 107 + app/scripts/postupgrade.js | 95 + app/scripts/upload-weixin.js | 306 + app/scripts/validate-message-page.mjs | 45 + app/scripts/validate-sub-pages.mjs | 62 + app/scripts/vite-plugin-eruda.js | 37 + app/src/App.ku.vue | 46 + app/src/App.vue | 49 + app/src/api/balance.ts | 61 + app/src/api/card.ts | 203 + app/src/api/customer.ts | 27 + app/src/api/diagnosis.ts | 44 + app/src/api/foo-alova.ts | 16 + app/src/api/foo.ts | 43 + app/src/api/login.ts | 152 + app/src/api/message.ts | 78 + app/src/api/order.ts | 21 + app/src/api/system-normalize.ts | 19 + app/src/api/system.test.ts | 24 + app/src/api/system.ts | 15 + app/src/api/types/balance.ts | 61 + app/src/api/types/card.ts | 394 + app/src/api/types/customer.ts | 53 + app/src/api/types/diagnosis.ts | 102 + app/src/api/types/login.ts | 246 + app/src/api/types/message.ts | 63 + app/src/api/types/order.ts | 72 + app/src/api/types/warning.ts | 127 + app/src/api/types/workOrder.ts | 62 + app/src/api/warning.ts | 81 + app/src/api/workOrder.ts | 35 + app/src/components/.gitkeep | 0 app/src/components/Navbar/index.vue | 113 + app/src/components/SideCaptcha.vue | 266 + app/src/constant/RouterPath.ts | 81 + app/src/env.d.ts | 39 + app/src/hooks/useRequest.ts | 54 + app/src/hooks/useScroll.md | 116 + app/src/hooks/useScroll.ts | 74 + app/src/hooks/useUpload.ts | 171 + app/src/http/README.md | 13 + app/src/http/alova.ts | 122 + app/src/http/error.ts | 7 + app/src/http/http.ts | 227 + app/src/http/interceptor.ts | 58 + app/src/http/tools/enum.ts | 129 + app/src/http/tools/queryString.ts | 29 + app/src/http/types.ts | 44 + app/src/http/vue-query.ts | 30 + app/src/layouts/blank.vue | 9 + app/src/layouts/default.vue | 5 + app/src/locale/README.md | 12 + app/src/locale/en.json | 11 + app/src/locale/index.ts | 81 + app/src/locale/zh-Hans.json | 11 + app/src/main.ts | 22 + app/src/manifest.json | 138 + app/src/pages.json | 345 + .../business/components/BusinessHero.vue | 39 + .../business/components/BusinessSection.vue | 216 + app/src/pages/business/data.test.ts | 200 + app/src/pages/business/data.ts | 214 + app/src/pages/business/index.vue | 189 + app/src/pages/business/views/addon/index.vue | 219 + .../pages/business/views/allocate/index.vue | 740 + app/src/pages/business/views/api/balance.ts | 61 + app/src/pages/business/views/api/customer.ts | 27 + app/src/pages/business/views/api/diagnosis.ts | 44 + app/src/pages/business/views/api/order.ts | 21 + app/src/pages/business/views/api/warning.ts | 81 + app/src/pages/business/views/api/workOrder.ts | 35 + .../business/views/balanceRecharge/index.vue | 171 + .../pages/business/views/cardRenew/index.vue | 487 + app/src/pages/business/views/change/index.vue | 645 + .../pages/business/views/customer/index.vue | 329 + .../business/views/customerDetail/index.vue | 251 + .../pages/business/views/diagnosis/index.vue | 1261 + app/src/pages/business/views/order/index.vue | 569 + app/src/pages/business/views/renew/index.vue | 729 + .../pages/business/views/warning/index.vue | 699 + .../pages/business/views/workOrder/index.vue | 204 + .../business/views/workOrderCreate/index.vue | 293 + .../business/views/workOrderDetail/index.vue | 222 + app/src/pages/card/action.ts | 134 + app/src/pages/card/components/CardList.vue | 1053 + app/src/pages/card/components/CardToolbar.vue | 41 + app/src/pages/card/components/EmptyState.vue | 28 + app/src/pages/card/components/FilterBar.vue | 114 + app/src/pages/card/components/PackageList.vue | 217 + app/src/pages/card/data.test.ts | 24 + app/src/pages/card/data.ts | 385 + app/src/pages/card/index.vue | 1399 + app/src/pages/card/views/api/warning.ts | 81 + app/src/pages/card/views/detail/index.vue | 1112 + app/src/pages/card/views/management/index.vue | 154 + .../pages/card/views/packageDetail/index.vue | 36 + app/src/pages/card/views/warning/index.vue | 587 + .../pages/home/components/CardOverview.vue | 344 + app/src/pages/home/components/FlowPool.vue | 110 + app/src/pages/home/components/FlowRanking.vue | 94 + .../pages/home/components/LifecycleStats.vue | 102 + .../pages/home/components/PublicCardQuery.vue | 511 + .../pages/home/components/QuickBusiness.vue | 365 + app/src/pages/home/index.vue | 298 + app/src/pages/login/forgot-password/index.vue | 129 + app/src/pages/login/index.vue | 1563 ++ app/src/pages/login/register/index.vue | 30 + .../pages/message/components/MessageList.vue | 212 + .../pages/message/components/MessageTabs.vue | 93 + .../message/components/MessageToolbar.vue | 142 + app/src/pages/message/data.test.ts | 29 + app/src/pages/message/data.ts | 302 + app/src/pages/message/index.vue | 588 + .../pages/message/views/cardDetail/index.vue | 254 + .../message/views/systemDetail/index.vue | 254 + .../pages/user/components/UserProfileCard.vue | 260 + .../user/components/UserServiceCells.vue | 421 + .../pages/user/components/UserStatsCard.vue | 211 + .../user/components/UserSupportCells.vue | 171 + app/src/pages/user/index.vue | 108 + app/src/pages/user/views/about/index.vue | 19 + app/src/pages/user/views/agreement/index.vue | 330 + app/src/pages/user/views/api/warning.ts | 8 + app/src/pages/user/views/contact/index.vue | 18 + app/src/pages/user/views/feedback/index.vue | 18 + .../pages/user/views/notification/index.vue | 403 + app/src/pages/user/views/profile/index.vue | 550 + app/src/pages/user/views/realName/index.vue | 67 + app/src/router/README.md | 55 + app/src/router/appPermission.ts | 26 + app/src/router/config.ts | 28 + app/src/router/interceptor.ts | 165 + app/src/router/permission.ts | 26 + app/src/service/index.ts | 6 + app/src/service/info.ts | 14 + app/src/service/listAll.ts | 18 + app/src/service/types.ts | 29 + app/src/static/app/icons/1024x1024.png | Bin 0 -> 59336 bytes app/src/static/app/icons/120x120.png | Bin 0 -> 3231 bytes app/src/static/app/icons/144x144.png | Bin 0 -> 3791 bytes app/src/static/app/icons/152x152.png | Bin 0 -> 3981 bytes app/src/static/app/icons/167x167.png | Bin 0 -> 4479 bytes app/src/static/app/icons/180x180.png | Bin 0 -> 4844 bytes app/src/static/app/icons/192x192.png | Bin 0 -> 5319 bytes app/src/static/app/icons/20x20.png | Bin 0 -> 574 bytes app/src/static/app/icons/29x29.png | Bin 0 -> 780 bytes app/src/static/app/icons/40x40.png | Bin 0 -> 985 bytes app/src/static/app/icons/58x58.png | Bin 0 -> 1483 bytes app/src/static/app/icons/60x60.png | Bin 0 -> 1521 bytes app/src/static/app/icons/72x72.png | Bin 0 -> 1802 bytes app/src/static/app/icons/76x76.png | Bin 0 -> 1972 bytes app/src/static/app/icons/80x80.png | Bin 0 -> 2060 bytes app/src/static/app/icons/87x87.png | Bin 0 -> 2289 bytes app/src/static/app/icons/96x96.png | Bin 0 -> 2396 bytes app/src/static/fonts/.gitkeep | 0 app/src/static/images/avatar.jpg | Bin 0 -> 58805 bytes app/src/static/images/default-avatar.png | Bin 0 -> 560 bytes app/src/static/images/shandong-yunze-logo.png | Bin 0 -> 78475 bytes app/src/static/logo.svg | 33 + app/src/static/my-icons/copyright.svg | 1 + app/src/static/my-icons/sparkle.svg | 4 + app/src/static/tabbar/example.png | Bin 0 -> 1371 bytes app/src/static/tabbar/exampleHL.png | Bin 0 -> 1398 bytes app/src/static/tabbar/home.png | Bin 0 -> 1346 bytes app/src/static/tabbar/homeHL.png | Bin 0 -> 1415 bytes app/src/static/tabbar/personal.png | Bin 0 -> 2457 bytes app/src/static/tabbar/personalHL.png | Bin 0 -> 2534 bytes app/src/static/tabbar/scan.png | Bin 0 -> 3962 bytes app/src/store/index.ts | 22 + app/src/store/lastQueriedCard.ts | 36 + app/src/store/message.ts | 107 + app/src/store/token.ts | 318 + app/src/store/user.ts | 142 + app/src/style/iconfont.css | 28 + app/src/style/index.scss | 635 + app/src/tabbar/README.md | 78 + app/src/tabbar/TabbarItem.vue | 102 + app/src/tabbar/config.ts | 144 + app/src/tabbar/i18n.ts | 30 + app/src/tabbar/index.vue | 162 + app/src/tabbar/store.ts | 105 + app/src/tabbar/types.ts | 39 + app/src/types/async-component.d.ts | 8 + app/src/types/async-import.d.ts | 13 + app/src/types/auto-import.d.ts | 193 + app/src/types/components.d.ts | 35 + app/src/types/uni-pages.d.ts | 63 + app/src/typings.d.ts | 171 + app/src/typings.ts | 21 + app/src/uni.scss | 77 + app/src/uni_modules/.gitkeep | 0 app/src/utils/autoCreateRoutePath.ts | 101 + app/src/utils/cardRecognition.ts | 72 + app/src/utils/clipboard.ts | 14 + app/src/utils/debounce.ts | 166 + app/src/utils/i18n.ts | 10 + app/src/utils/index.ts | 315 + app/src/utils/initNavbarDesc.ts | 22 + app/src/utils/lifecycleBatch.ts | 123 + app/src/utils/permission.test.ts | 383 + app/src/utils/permission.ts | 580 + app/src/utils/scanPageFilter.ts | 16 + app/src/utils/systemInfo.ts | 38 + app/src/utils/toLoginPage.ts | 44 + app/src/utils/updateManager.wx.ts | 29 + app/src/utils/uploadFile.ts | 327 + app/src/utils/userIdentity.ts | 103 + app/tsconfig.json | 57 + app/uno.config.ts | 202 + app/vite-plugins/README.md | 236 + app/vite-plugins/copy-native-resources.ts | 198 + app/vite-plugins/sync-manifest-plugins.ts | 68 + app/vite.config.ts | 341 + app/wot-ui-resolver.ts | 27 + server/.dockerignore | 12 + server/Dockerfile | 44 + server/LICENSE | 201 + server/Makefile | 7 + server/README.MD | 4 + server/api/app/app.go | 74 + server/api/app/v1/auth.go | 87 + server/api/app/v1/balance.go | 55 + server/api/app/v1/card.go | 199 + server/api/app/v1/customer.go | 26 + server/api/app/v1/diagnosis.go | 25 + server/api/app/v1/message.go | 33 + server/api/app/v1/open.go | 25 + server/api/app/v1/order.go | 26 + server/api/app/v1/user.go | 33 + server/api/app/v1/warning.go | 60 + server/api/app/v1/work_order.go | 44 + server/api/common/common.go | 16 + server/api/common/v1/upload.go | 29 + server/api/gen/testdemo/testdemo.go | 19 + server/api/gen/testdemo/v1/testdemo.go | 59 + server/api/gen/testtree/testtree.go | 20 + server/api/gen/testtree/v1/testtree.go | 67 + server/api/gen_codes/gen_codes.go | 25 + server/api/gen_codes/v1/gen_table.go | 112 + server/api/info/info.go | 15 + server/api/info/v1/info.go | 15 + server/api/monitor/monitor.go | 15 + server/api/monitor/v1/cache.go | 25 + server/api/system/system.go | 134 + server/api/system/v1/auth.go | 72 + server/api/system/v1/captcha.go | 18 + server/api/system/v1/client.go | 68 + server/api/system/v1/config.go | 59 + server/api/system/v1/dept.go | 70 + server/api/system/v1/dict.go | 108 + server/api/system/v1/job.go | 83 + server/api/system/v1/logininfor.go | 32 + server/api/system/v1/menu.go | 116 + server/api/system/v1/notice.go | 59 + server/api/system/v1/online.go | 33 + server/api/system/v1/oper_log.go | 25 + server/api/system/v1/oss.go | 43 + server/api/system/v1/oss_config.go | 54 + server/api/system/v1/post.go | 68 + server/api/system/v1/qrcode.go | 83 + server/api/system/v1/role.go | 69 + server/api/system/v1/social.go | 34 + server/api/system/v1/sysnoticeuser.go | 75 + server/api/system/v1/tenant.go | 135 + server/api/system/v1/user.go | 141 + server/config/config.yaml | 289 + server/config/config_default.yaml | 269 + server/go.mod | 118 + server/go.sum | 394 + server/hack/config.yaml | 18 + server/hack/hack-cli.mk | 15 + server/hack/hack.mk | 75 + server/internal/ai/chat_model.go | 112 + server/internal/ai/chat_model_test.go | 92 + server/internal/cmd/cmd.go | 10 + server/internal/cmd/global.go | 61 + server/internal/cmd/http.go | 54 + server/internal/cmd/initfuunc.go | 46 + server/internal/cmd/inithttp/inithttp.go | 59 + server/internal/cmd/monitor.go | 74 + server/internal/cmd/shutdown.go | 58 + server/internal/consts/app_register.go | 6 + server/internal/consts/batch.go | 151 + server/internal/consts/bcache.go | 13 + server/internal/consts/code.go | 41 + server/internal/consts/config.go | 21 + server/internal/consts/consts.go | 10 + server/internal/consts/db.go | 6 + server/internal/consts/event.go | 55 + server/internal/consts/http.go | 14 + server/internal/consts/mcache.go | 15 + server/internal/consts/queue.go | 20 + server/internal/consts/system.go | 150 + server/internal/consts/websocket.go | 23 + server/internal/controller/app/app_new.go | 9 + server/internal/controller/app/app_v1_auth.go | 81 + .../internal/controller/app/app_v1_balance.go | 89 + server/internal/controller/app/app_v1_card.go | 180 + .../controller/app/app_v1_customer.go | 24 + .../controller/app/app_v1_diagnosis.go | 24 + .../internal/controller/app/app_v1_message.go | 32 + server/internal/controller/app/app_v1_open.go | 24 + .../internal/controller/app/app_v1_order.go | 24 + server/internal/controller/app/app_v1_user.go | 32 + .../internal/controller/app/app_v1_warning.go | 56 + .../controller/app/app_v1_work_order.go | 40 + server/internal/controller/common/common.go | 5 + .../internal/controller/common/common_new.go | 15 + .../controller/common/common_v1_upload.go | 31 + server/internal/controller/gen/test_demo.go | 69 + server/internal/controller/gen/test_tree.go | 85 + .../controller/gen_codes/gen_codes.go | 5 + .../controller/gen_codes/gen_codes_new.go | 15 + .../gen_codes/gen_codes_v1_gen_table.go | 127 + server/internal/controller/info/info.go | 5 + server/internal/controller/info/info_new.go | 15 + .../internal/controller/info/info_v1_info.go | 15 + server/internal/controller/monitor/monitor.go | 5 + .../controller/monitor/monitor_new.go | 15 + .../controller/monitor/monitor_v1_cache.go | 57 + .../controller/system/sys_notice_user.go | 75 + server/internal/controller/system/system.go | 5 + .../internal/controller/system/system_new.go | 15 + .../controller/system/system_v1_auth.go | 94 + .../controller/system/system_v1_captcha.go | 17 + .../controller/system/system_v1_client.go | 75 + .../controller/system/system_v1_config.go | 63 + .../controller/system/system_v1_dept.go | 70 + .../controller/system/system_v1_dict.go | 123 + .../controller/system/system_v1_job.go | 118 + .../controller/system/system_v1_logininfor.go | 33 + .../controller/system/system_v1_menu.go | 62 + .../controller/system/system_v1_notice.go | 59 + .../controller/system/system_v1_online.go | 36 + .../controller/system/system_v1_oper_log.go | 25 + .../controller/system/system_v1_oss.go | 43 + .../controller/system/system_v1_oss_config.go | 61 + .../controller/system/system_v1_post.go | 67 + .../controller/system/system_v1_qrcode.go | 94 + .../controller/system/system_v1_role.go | 69 + .../controller/system/system_v1_social.go | 34 + .../controller/system/system_v1_tenant.go | 141 + .../controller/system/system_v1_user.go | 236 + server/internal/dao/batch_detail.go | 22 + server/internal/dao/batch_main.go | 22 + server/internal/dao/card_channel_assoc.go | 22 + server/internal/dao/gen_table.go | 22 + server/internal/dao/gen_table_column.go | 22 + server/internal/dao/internal/batch_detail.go | 109 + server/internal/dao/internal/batch_main.go | 135 + .../dao/internal/card_channel_assoc.go | 97 + server/internal/dao/internal/gen_table.go | 121 + .../internal/dao/internal/gen_table_column.go | 123 + server/internal/dao/internal/sys_client.go | 109 + server/internal/dao/internal/sys_config.go | 101 + server/internal/dao/internal/sys_dept.go | 125 + server/internal/dao/internal/sys_dict_data.go | 107 + server/internal/dao/internal/sys_dict_type.go | 99 + server/internal/dao/internal/sys_gen_table.go | 111 + server/internal/dao/internal/sys_job.go | 111 + .../internal/dao/internal/sys_logininfor.go | 101 + server/internal/dao/internal/sys_menu.go | 121 + server/internal/dao/internal/sys_notice.go | 107 + .../internal/dao/internal/sys_notice_user.go | 107 + server/internal/dao/internal/sys_oper_log.go | 113 + server/internal/dao/internal/sys_oss.go | 111 + .../internal/dao/internal/sys_oss_config.go | 117 + server/internal/dao/internal/sys_post.go | 109 + server/internal/dao/internal/sys_role.go | 115 + server/internal/dao/internal/sys_role_dept.go | 83 + server/internal/dao/internal/sys_role_menu.go | 85 + server/internal/dao/internal/sys_social.go | 137 + server/internal/dao/internal/sys_tenant.go | 125 + .../dao/internal/sys_tenant_package.go | 103 + server/internal/dao/internal/sys_user.go | 125 + .../internal/dao/internal/sys_user_online.go | 105 + server/internal/dao/internal/sys_user_post.go | 83 + server/internal/dao/internal/sys_user_role.go | 83 + .../internal/dao/internal/t_agent_balance.go | 111 + .../dao/internal/t_agent_enterprise.go | 109 + server/internal/dao/internal/t_api.go | 105 + server/internal/dao/internal/t_api_group.go | 99 + .../dao/internal/t_api_group_resource.go | 81 + .../internal/dao/internal/t_api_resource.go | 105 + .../internal/dao/internal/t_balance_detail.go | 117 + server/internal/dao/internal/t_brand.go | 113 + server/internal/dao/internal/t_category.go | 119 + .../internal/t_consume_account_month_bill.go | 133 + .../internal/dao/internal/t_credit_detail.go | 113 + .../dao/internal/t_cust_withdraw_account.go | 107 + .../dao/internal/t_dept_flow_daily.go | 103 + .../internal/dao/internal/t_division_code.go | 89 + .../dao/internal/t_fin_commission_detail.go | 115 + .../dao/internal/t_fin_withdrawable_change.go | 119 + .../internal/dao/internal/t_finance_apply.go | 125 + .../dao/internal/t_finance_audit_record.go | 105 + .../dao/internal/t_flow_package_config.go | 99 + server/internal/dao/internal/t_message_log.go | 107 + .../internal/dao/internal/t_message_type.go | 97 + server/internal/dao/internal/t_notify_log.go | 99 + server/internal/dao/internal/t_order.go | 183 + server/internal/dao/internal/t_order_item.go | 141 + .../internal/dao/internal/t_package_apply.go | 121 + .../dao/internal/t_package_audit_record.go | 105 + .../dao/internal/t_package_customer.go | 101 + .../dao/internal/t_package_terminal.go | 101 + server/internal/dao/internal/t_product.go | 189 + .../internal/t_profit_account_month_bill.go | 125 + .../dao/internal/t_resource_batch_download.go | 141 + .../dao/internal/t_sim_bind_package_logs.go | 127 + .../dao/internal/t_sim_card_bind_pkg.go | 127 + .../dao/internal/t_sim_card_cancellation.go | 191 + .../internal/dao/internal/t_sim_card_info.go | 189 + .../internal/dao/internal/t_sim_card_stock.go | 103 + .../dao/internal/t_sim_channel_template.go | 103 + .../internal/dao/internal/t_sim_channels.go | 127 + .../dao/internal/t_sim_flow_package_info.go | 143 + .../dao/internal/t_sim_flow_package_price.go | 111 + .../internal/dao/internal/t_sim_flow_pool.go | 119 + .../dao/internal/t_sim_package_category.go | 99 + server/internal/dao/internal/t_sim_pool.go | 117 + .../dao/internal/t_sim_pool_bind_package.go | 123 + .../dao/internal/t_sim_pool_bind_pkg_logs.go | 123 + .../dao/internal/t_sim_pool_warn_config.go | 109 + .../dao/internal/t_sim_pool_warn_log.go | 115 + .../internal/dao/internal/t_sim_terminal.go | 107 + .../dao/internal/t_sim_upstream_package.go | 101 + server/internal/dao/internal/t_terminal.go | 129 + .../dao/internal/t_tplatform_setting.go | 111 + .../internal/dao/internal/t_user_message.go | 97 + server/internal/dao/internal/t_warn_config.go | 123 + server/internal/dao/internal/t_warn_log.go | 125 + .../dao/internal/t_wo_communication.go | 105 + server/internal/dao/internal/t_wo_main.go | 137 + server/internal/dao/internal/t_wo_record.go | 107 + server/internal/dao/internal/test_demo.go | 107 + server/internal/dao/internal/test_tree.go | 105 + server/internal/dao/sys_client.go | 22 + server/internal/dao/sys_config.go | 22 + server/internal/dao/sys_dept.go | 22 + server/internal/dao/sys_dict_data.go | 22 + server/internal/dao/sys_dict_type.go | 22 + server/internal/dao/sys_gen_table.go | 22 + server/internal/dao/sys_job.go | 22 + server/internal/dao/sys_logininfor.go | 22 + server/internal/dao/sys_menu.go | 22 + server/internal/dao/sys_notice.go | 22 + server/internal/dao/sys_notice_user.go | 22 + server/internal/dao/sys_oper_log.go | 22 + server/internal/dao/sys_oss.go | 22 + server/internal/dao/sys_oss_config.go | 22 + server/internal/dao/sys_post.go | 22 + server/internal/dao/sys_role.go | 22 + server/internal/dao/sys_role_dept.go | 22 + server/internal/dao/sys_role_menu.go | 22 + server/internal/dao/sys_social.go | 22 + server/internal/dao/sys_tenant.go | 22 + server/internal/dao/sys_tenant_package.go | 22 + server/internal/dao/sys_user.go | 22 + server/internal/dao/sys_user_online.go | 22 + server/internal/dao/sys_user_post.go | 22 + server/internal/dao/sys_user_role.go | 22 + server/internal/dao/t_agent_balance.go | 22 + server/internal/dao/t_agent_enterprise.go | 22 + server/internal/dao/t_api.go | 22 + server/internal/dao/t_api_group.go | 22 + server/internal/dao/t_api_group_resource.go | 22 + server/internal/dao/t_api_resource.go | 22 + server/internal/dao/t_balance_detail.go | 22 + server/internal/dao/t_brand.go | 22 + server/internal/dao/t_category.go | 22 + .../dao/t_consume_account_month_bill.go | 22 + server/internal/dao/t_credit_detail.go | 22 + .../internal/dao/t_cust_withdraw_account.go | 22 + server/internal/dao/t_dept_flow_daily.go | 22 + server/internal/dao/t_division_code.go | 22 + .../internal/dao/t_fin_commission_detail.go | 22 + .../internal/dao/t_fin_withdrawable_change.go | 22 + server/internal/dao/t_finance_apply.go | 22 + server/internal/dao/t_finance_audit_record.go | 22 + server/internal/dao/t_flow_package_config.go | 22 + server/internal/dao/t_message_log.go | 22 + server/internal/dao/t_message_type.go | 22 + server/internal/dao/t_notify_log.go | 22 + server/internal/dao/t_order.go | 22 + server/internal/dao/t_order_item.go | 22 + server/internal/dao/t_package_apply.go | 22 + server/internal/dao/t_package_audit_record.go | 22 + server/internal/dao/t_package_customer.go | 22 + server/internal/dao/t_package_terminal.go | 22 + server/internal/dao/t_product.go | 22 + .../dao/t_profit_account_month_bill.go | 22 + .../internal/dao/t_resource_batch_download.go | 22 + .../internal/dao/t_sim_bind_package_logs.go | 22 + server/internal/dao/t_sim_card_bind_pkg.go | 22 + .../internal/dao/t_sim_card_cancellation.go | 22 + server/internal/dao/t_sim_card_info.go | 22 + server/internal/dao/t_sim_card_stock.go | 22 + server/internal/dao/t_sim_channel_template.go | 22 + server/internal/dao/t_sim_channels.go | 22 + .../internal/dao/t_sim_flow_package_info.go | 22 + .../internal/dao/t_sim_flow_package_price.go | 22 + server/internal/dao/t_sim_flow_pool.go | 22 + server/internal/dao/t_sim_package_category.go | 22 + server/internal/dao/t_sim_pool.go | 22 + .../internal/dao/t_sim_pool_bind_package.go | 22 + .../internal/dao/t_sim_pool_bind_pkg_logs.go | 22 + server/internal/dao/t_sim_pool_warn_config.go | 22 + server/internal/dao/t_sim_pool_warn_log.go | 22 + server/internal/dao/t_sim_terminal.go | 22 + server/internal/dao/t_sim_upstream_package.go | 22 + server/internal/dao/t_terminal.go | 22 + server/internal/dao/t_tplatform_setting.go | 22 + server/internal/dao/t_user_message.go | 22 + server/internal/dao/t_warn_config.go | 22 + server/internal/dao/t_warn_log.go | 22 + server/internal/dao/t_wo_communication.go | 22 + server/internal/dao/t_wo_main.go | 22 + server/internal/dao/t_wo_record.go | 22 + server/internal/dao/test_demo.go | 22 + server/internal/dao/test_tree.go | 22 + server/internal/library/addons/addons.go | 16 + .../library/appidentity/terminal_user.go | 190 + server/internal/library/batchimport/async.go | 168 + .../internal/library/batchimport/batch_no.go | 117 + .../library/batchimport/batch_no_test.go | 45 + server/internal/library/batchimport/runner.go | 596 + server/internal/library/batchimport/types.go | 82 + server/internal/library/bcache/bcache.go | 6 + .../library/bcache/bcache_system_auth.go | 77 + server/internal/library/cache/cache.go | 60 + server/internal/library/cache/file/file.go | 440 + server/internal/library/contexts/contexts.go | 198 + server/internal/library/event/event.go | 75 + server/internal/library/mcache/mcache.go | 46 + .../library/mcache/mcache_access_list.go | 75 + .../library/mcache/mcache_system_config.go | 77 + .../library/mcache/mcache_system_dept.go | 49 + .../library/mcache/mcache_system_user.go | 58 + server/internal/library/rabbitmq/publisher.go | 98 + server/internal/library/websocket/client.go | 156 + .../library/websocket/client_manager.go | 384 + .../internal/library/websocket/controller.go | 125 + server/internal/library/websocket/init.go | 63 + server/internal/library/websocket/model.go | 39 + server/internal/library/websocket/router.go | 62 + server/internal/library/worker/options.go | 225 + server/internal/library/worker/work_proces.go | 84 + server/internal/library/worker/worker.go | 306 + .../library/xgen/gen_consts/gen_codes.go | 86 + .../library/xgen/gen_consts/gen_config.go | 87 + .../internal/library/xgen/gen_dao/gen_dao.go | 65 + .../library/xgen/gen_model/gen_codes.go | 146 + .../library/xgen/gen_model/gen_config.go | 126 + .../library/xgen/gen_model/gen_view.go | 145 + .../library/xgen/gen_view/gen_column.go | 253 + .../xgen/gen_view/gen_column_default.go | 341 + .../library/xgen/gen_view/gen_column_map.go | 339 + .../library/xgen/gen_view/gen_curd.go | 993 + .../library/xgen/gen_view/gen_curd_input.go | 414 + .../library/xgen/gen_view/gen_curd_logic.go | 357 + .../library/xgen/gen_view/gen_curd_model.go | 903 + .../xgen/gen_view/gen_curd_web_edit.go | 342 + .../xgen/gen_view/gen_curd_web_index.go | 172 + .../xgen/gen_view/gen_curd_web_model.go | 97 + .../xgen/gen_view/gen_curd_web_view.go | 131 + .../library/xgen/gen_view/gen_util.go | 331 + .../library/xgen/gen_view/gohtml/consts.go | 12 + .../library/xgen/gen_view/gohtml/doc.go | 6 + .../library/xgen/gen_view/gohtml/element.go | 12 + .../library/xgen/gen_view/gohtml/formatter.go | 41 + .../xgen/gen_view/gohtml/html_document.go | 59 + .../library/xgen/gen_view/gohtml/parser.go | 133 + .../xgen/gen_view/gohtml/tag_element.go | 150 + .../xgen/gen_view/gohtml/text_element.go | 48 + .../library/xgen/gen_view/gohtml/utils.go | 106 + .../library/xgen/gen_view/gohtml/writer.go | 38 + server/internal/library/xgen/xgen_curd.go | 484 + server/internal/library/xgen/xgen_init.go | 19 + server/internal/library/xgorm/dao.go | 197 + server/internal/library/xgorm/dao_tree.go | 195 + .../library/xgorm/gorm_model/gorm_tree.go | 14 + .../library/xgorm/handler/filter_auth.go | 224 + .../library/xgorm/handler/force_cache.go | 13 + .../internal/library/xgorm/handler/handler.go | 40 + .../internal/library/xgorm/handler/tenant.go | 27 + server/internal/library/xgorm/hook/member.go | 118 + server/internal/logic/app/app_access.go | 393 + .../internal/logic/app/app_access_helpers.go | 43 + server/internal/logic/app/app_access_test.go | 111 + server/internal/logic/app/app_auth.go | 1368 + server/internal/logic/app/app_auth_test.go | 122 + server/internal/logic/app/app_balance.go | 379 + .../logic/app/app_balance_recharge.go | 320 + .../app/app_balance_recharge_complete.go | 356 + .../logic/app/app_balance_recharge_refund.go | 516 + server/internal/logic/app/app_balance_test.go | 29 + .../internal/logic/app/app_business_async.go | 77 + server/internal/logic/app/app_card.go | 69 + .../internal/logic/app/app_card_allocate.go | 941 + .../logic/app/app_card_allocate_processor.go | 166 + .../logic/app/app_card_allocate_result.go | 468 + server/internal/logic/app/app_card_bind.go | 187 + server/internal/logic/app/app_card_change.go | 1292 + server/internal/logic/app/app_card_channel.go | 282 + server/internal/logic/app/app_card_mapper.go | 538 + .../logic/app/app_card_mapper_test.go | 73 + server/internal/logic/app/app_card_package.go | 299 + server/internal/logic/app/app_card_query.go | 366 + .../logic/app/app_card_recognition.go | 521 + .../app/app_card_recognition_live_test.go | 40 + .../logic/app/app_card_recognition_test.go | 164 + server/internal/logic/app/app_card_renew.go | 1204 + .../logic/app/app_card_renew_batch.go | 1261 + .../logic/app/app_card_renew_complete.go | 306 + .../logic/app/app_card_renew_period_test.go | 142 + .../logic/app/app_card_renew_processor.go | 166 + .../logic/app/app_card_renew_resolver.go | 269 + .../logic/app/app_card_renew_result.go | 546 + .../logic/app/app_card_renew_result_test.go | 175 + server/internal/logic/app/app_card_restart.go | 1115 + .../logic/app/app_card_restart_test.go | 147 + server/internal/logic/app/app_card_session.go | 45 + server/internal/logic/app/app_card_utils.go | 254 + .../internal/logic/app/app_card_utils_test.go | 17 + server/internal/logic/app/app_customer.go | 755 + server/internal/logic/app/app_data_scope.go | 397 + server/internal/logic/app/app_diagnosis.go | 1538 ++ .../internal/logic/app/app_diagnosis_test.go | 228 + .../logic/app/app_diagnosis_traffic.go | 269 + server/internal/logic/app/app_message.go | 197 + server/internal/logic/app/app_onelink_mock.go | 543 + .../logic/app/app_onelink_provider.go | 80 + server/internal/logic/app/app_order.go | 343 + .../internal/logic/app/app_payment_wechat.go | 906 + .../internal/logic/app/app_register_config.go | 142 + .../logic/app/app_upstream_session.go | 225 + server/internal/logic/app/app_user.go | 589 + server/internal/logic/app/app_warning.go | 758 + server/internal/logic/app/app_warning_test.go | 107 + server/internal/logic/app/app_work_order.go | 448 + .../logic/app/app_worker_batch_async.go | 49 + server/internal/logic/gen/test_demo.go | 182 + server/internal/logic/gen/test_tree.go | 206 + .../internal/logic/gen_codes/gen_selects.go | 6 + .../internal/logic/gen_codes/sys_gen_table.go | 218 + server/internal/logic/info/info.go | 136 + server/internal/logic/logic.go | 16 + server/internal/logic/memorydb/memorydb.go | 123 + server/internal/logic/memorydb/sys_config.go | 204 + server/internal/logic/memorydb/sys_dept.go | 210 + .../internal/logic/memorydb/sys_dict_data.go | 205 + .../internal/logic/memorydb/sys_dict_type.go | 201 + server/internal/logic/memorydb/sys_menu.go | 232 + server/internal/logic/memorydb/sys_post.go | 205 + server/internal/logic/memorydb/sys_role.go | 228 + server/internal/logic/memorydb/sys_tenant.go | 216 + .../internal/logic/middleware/middleware.go | 378 + .../middleware/middleware_permission_test.go | 68 + .../internal/logic/monitor/monitor_server.go | 152 + server/internal/logic/system/sys_auth.go | 513 + server/internal/logic/system/sys_captcha.go | 61 + server/internal/logic/system/sys_client.go | 141 + server/internal/logic/system/sys_config.go | 191 + server/internal/logic/system/sys_dept.go | 422 + server/internal/logic/system/sys_dept_test.go | 47 + server/internal/logic/system/sys_dict_data.go | 225 + server/internal/logic/system/sys_dict_type.go | 185 + server/internal/logic/system/sys_job.go | 392 + .../internal/logic/system/sys_logininfor.go | 97 + server/internal/logic/system/sys_menu.go | 361 + server/internal/logic/system/sys_notice.go | 348 + .../internal/logic/system/sys_notice_user.go | 204 + server/internal/logic/system/sys_oper_log.go | 163 + server/internal/logic/system/sys_oss.go | 792 + .../internal/logic/system/sys_oss_config.go | 168 + .../logic/system/sys_oss_path_test.go | 87 + server/internal/logic/system/sys_post.go | 214 + server/internal/logic/system/sys_qrcode.go | 816 + server/internal/logic/system/sys_role.go | 355 + server/internal/logic/system/sys_social.go | 86 + server/internal/logic/system/sys_tenant.go | 639 + .../logic/system/sys_tenant_package.go | 140 + server/internal/logic/system/sys_user.go | 768 + .../internal/logic/system/sys_user_online.go | 113 + server/internal/logic/system/sys_user_test.go | 58 + server/internal/logic/system/task_test.go | 169 + server/internal/model/app_auth.go | 83 + server/internal/model/app_balance.go | 65 + server/internal/model/app_captcha.go | 16 + server/internal/model/app_card.go | 388 + server/internal/model/app_customer.go | 43 + server/internal/model/app_diagnosis.go | 118 + server/internal/model/app_message.go | 36 + server/internal/model/app_order.go | 72 + server/internal/model/app_user.go | 96 + server/internal/model/app_warning.go | 134 + server/internal/model/app_work_order.go | 58 + server/internal/model/bcache_model.go | 15 + server/internal/model/context.go | 26 + server/internal/model/do/batch_detail.go | 32 + server/internal/model/do/batch_main.go | 45 + .../internal/model/do/card_channel_assoc.go | 25 + server/internal/model/do/gen_table.go | 37 + server/internal/model/do/gen_table_column.go | 38 + server/internal/model/do/sys_client.go | 31 + server/internal/model/do/sys_config.go | 27 + server/internal/model/do/sys_dept.go | 39 + server/internal/model/do/sys_dict_data.go | 30 + server/internal/model/do/sys_dict_type.go | 26 + server/internal/model/do/sys_gen_table.go | 32 + server/internal/model/do/sys_job.go | 32 + server/internal/model/do/sys_logininfor.go | 27 + server/internal/model/do/sys_menu.go | 37 + server/internal/model/do/sys_notice.go | 30 + server/internal/model/do/sys_notice_user.go | 30 + server/internal/model/do/sys_oper_log.go | 33 + server/internal/model/do/sys_oss.go | 32 + server/internal/model/do/sys_oss_config.go | 35 + server/internal/model/do/sys_post.go | 31 + server/internal/model/do/sys_role.go | 34 + server/internal/model/do/sys_role_dept.go | 17 + server/internal/model/do/sys_role_menu.go | 18 + server/internal/model/do/sys_social.go | 45 + server/internal/model/do/sys_tenant.go | 39 + .../internal/model/do/sys_tenant_package.go | 28 + server/internal/model/do/sys_user.go | 39 + server/internal/model/do/sys_user_online.go | 29 + server/internal/model/do/sys_user_post.go | 17 + server/internal/model/do/sys_user_role.go | 17 + server/internal/model/do/t_agent_balance.go | 32 + .../internal/model/do/t_agent_enterprise.go | 31 + server/internal/model/do/t_api.go | 29 + server/internal/model/do/t_api_group.go | 26 + .../internal/model/do/t_api_group_resource.go | 16 + server/internal/model/do/t_api_resource.go | 29 + server/internal/model/do/t_balance_detail.go | 35 + server/internal/model/do/t_brand.go | 33 + server/internal/model/do/t_category.go | 36 + .../model/do/t_consume_account_month_bill.go | 43 + server/internal/model/do/t_credit_detail.go | 33 + .../model/do/t_cust_withdraw_account.go | 30 + server/internal/model/do/t_dept_flow_daily.go | 28 + server/internal/model/do/t_division_code.go | 20 + .../model/do/t_fin_commission_detail.go | 35 + .../model/do/t_fin_withdrawable_change.go | 36 + server/internal/model/do/t_finance_apply.go | 40 + .../model/do/t_finance_audit_record.go | 29 + .../model/do/t_flow_package_config.go | 27 + server/internal/model/do/t_message_log.go | 31 + server/internal/model/do/t_message_type.go | 25 + server/internal/model/do/t_notify_log.go | 26 + server/internal/model/do/t_order.go | 68 + server/internal/model/do/t_order_item.go | 47 + server/internal/model/do/t_package_apply.go | 37 + .../model/do/t_package_audit_record.go | 29 + .../internal/model/do/t_package_customer.go | 27 + .../internal/model/do/t_package_terminal.go | 27 + server/internal/model/do/t_product.go | 71 + .../model/do/t_profit_account_month_bill.go | 39 + .../model/do/t_resource_batch_download.go | 48 + .../model/do/t_sim_bind_package_logs.go | 40 + .../internal/model/do/t_sim_card_bind_pkg.go | 40 + .../model/do/t_sim_card_cancellation.go | 72 + server/internal/model/do/t_sim_card_info.go | 71 + server/internal/model/do/t_sim_card_stock.go | 28 + .../model/do/t_sim_channel_template.go | 28 + server/internal/model/do/t_sim_channels.go | 40 + .../model/do/t_sim_flow_package_info.go | 49 + .../model/do/t_sim_flow_package_price.go | 32 + server/internal/model/do/t_sim_flow_pool.go | 37 + .../model/do/t_sim_package_category.go | 26 + server/internal/model/do/t_sim_pool.go | 36 + .../model/do/t_sim_pool_bind_package.go | 38 + .../model/do/t_sim_pool_bind_pkg_logs.go | 38 + .../model/do/t_sim_pool_warn_config.go | 31 + .../internal/model/do/t_sim_pool_warn_log.go | 34 + server/internal/model/do/t_sim_terminal.go | 30 + .../model/do/t_sim_upstream_package.go | 27 + server/internal/model/do/t_terminal.go | 41 + .../internal/model/do/t_tplatform_setting.go | 32 + server/internal/model/do/t_user_message.go | 25 + server/internal/model/do/t_warn_config.go | 38 + server/internal/model/do/t_warn_log.go | 39 + .../internal/model/do/t_wo_communication.go | 30 + server/internal/model/do/t_wo_main.go | 45 + server/internal/model/do/t_wo_record.go | 30 + server/internal/model/do/test_demo.go | 30 + server/internal/model/do/test_tree.go | 29 + server/internal/model/entity/batch_detail.go | 30 + server/internal/model/entity/batch_main.go | 43 + .../model/entity/card_channel_assoc.go | 23 + server/internal/model/entity/gen_table.go | 35 + .../internal/model/entity/gen_table_column.go | 36 + server/internal/model/entity/sys_client.go | 29 + server/internal/model/entity/sys_config.go | 25 + server/internal/model/entity/sys_dept.go | 37 + server/internal/model/entity/sys_dict_data.go | 28 + server/internal/model/entity/sys_dict_type.go | 24 + server/internal/model/entity/sys_gen_table.go | 30 + server/internal/model/entity/sys_job.go | 30 + .../internal/model/entity/sys_logininfor.go | 25 + server/internal/model/entity/sys_menu.go | 35 + server/internal/model/entity/sys_notice.go | 28 + .../internal/model/entity/sys_notice_user.go | 28 + server/internal/model/entity/sys_oper_log.go | 31 + server/internal/model/entity/sys_oss.go | 30 + .../internal/model/entity/sys_oss_config.go | 33 + server/internal/model/entity/sys_post.go | 29 + server/internal/model/entity/sys_role.go | 32 + server/internal/model/entity/sys_role_dept.go | 12 + server/internal/model/entity/sys_role_menu.go | 13 + server/internal/model/entity/sys_social.go | 43 + server/internal/model/entity/sys_tenant.go | 37 + .../model/entity/sys_tenant_package.go | 26 + server/internal/model/entity/sys_user.go | 37 + .../internal/model/entity/sys_user_online.go | 27 + server/internal/model/entity/sys_user_post.go | 12 + server/internal/model/entity/sys_user_role.go | 12 + .../internal/model/entity/t_agent_balance.go | 30 + .../model/entity/t_agent_enterprise.go | 29 + server/internal/model/entity/t_api.go | 27 + server/internal/model/entity/t_api_group.go | 24 + .../model/entity/t_api_group_resource.go | 11 + .../internal/model/entity/t_api_resource.go | 27 + .../internal/model/entity/t_balance_detail.go | 33 + server/internal/model/entity/t_brand.go | 31 + server/internal/model/entity/t_category.go | 34 + .../entity/t_consume_account_month_bill.go | 41 + .../internal/model/entity/t_credit_detail.go | 31 + .../model/entity/t_cust_withdraw_account.go | 28 + .../model/entity/t_dept_flow_daily.go | 26 + .../internal/model/entity/t_division_code.go | 15 + .../model/entity/t_fin_commission_detail.go | 33 + .../model/entity/t_fin_withdrawable_change.go | 34 + .../internal/model/entity/t_finance_apply.go | 38 + .../model/entity/t_finance_audit_record.go | 27 + .../model/entity/t_flow_package_config.go | 25 + server/internal/model/entity/t_message_log.go | 29 + .../internal/model/entity/t_message_type.go | 23 + server/internal/model/entity/t_notify_log.go | 24 + server/internal/model/entity/t_order.go | 66 + server/internal/model/entity/t_order_item.go | 45 + .../internal/model/entity/t_package_apply.go | 35 + .../model/entity/t_package_audit_record.go | 27 + .../model/entity/t_package_customer.go | 25 + .../model/entity/t_package_terminal.go | 25 + server/internal/model/entity/t_product.go | 69 + .../entity/t_profit_account_month_bill.go | 37 + .../model/entity/t_resource_batch_download.go | 46 + .../model/entity/t_sim_bind_package_logs.go | 38 + .../model/entity/t_sim_card_bind_pkg.go | 38 + .../model/entity/t_sim_card_cancellation.go | 70 + .../internal/model/entity/t_sim_card_info.go | 69 + .../internal/model/entity/t_sim_card_stock.go | 26 + .../model/entity/t_sim_channel_template.go | 26 + .../internal/model/entity/t_sim_channels.go | 38 + .../model/entity/t_sim_flow_package_info.go | 47 + .../model/entity/t_sim_flow_package_price.go | 30 + .../internal/model/entity/t_sim_flow_pool.go | 35 + .../model/entity/t_sim_package_category.go | 24 + server/internal/model/entity/t_sim_pool.go | 34 + .../model/entity/t_sim_pool_bind_package.go | 36 + .../model/entity/t_sim_pool_bind_pkg_logs.go | 36 + .../model/entity/t_sim_pool_warn_config.go | 29 + .../model/entity/t_sim_pool_warn_log.go | 32 + .../internal/model/entity/t_sim_terminal.go | 28 + .../model/entity/t_sim_upstream_package.go | 25 + server/internal/model/entity/t_terminal.go | 39 + .../model/entity/t_tplatform_setting.go | 30 + .../internal/model/entity/t_user_message.go | 23 + server/internal/model/entity/t_warn_config.go | 36 + server/internal/model/entity/t_warn_log.go | 37 + .../model/entity/t_wo_communication.go | 28 + server/internal/model/entity/t_wo_main.go | 43 + server/internal/model/entity/t_wo_record.go | 28 + server/internal/model/entity/test_demo.go | 28 + server/internal/model/entity/test_tree.go | 27 + server/internal/model/genin/test_demo.go | 129 + server/internal/model/genin/test_tree.go | 172 + server/internal/model/info.go | 18 + server/internal/model/monitor_server.go | 36 + server/internal/model/mybytes.go | 52 + server/internal/model/qrcode.go | 85 + server/internal/model/request/common.go | 20 + server/internal/model/response/common.go | 12 + server/internal/model/sys_auth.go | 38 + server/internal/model/sys_client.go | 93 + server/internal/model/sys_config.go | 83 + server/internal/model/sys_dept.go | 102 + server/internal/model/sys_dict.go | 177 + server/internal/model/sys_gen_table.go | 127 + server/internal/model/sys_job.go | 81 + server/internal/model/sys_logininfor.go | 61 + server/internal/model/sys_menu.go | 111 + server/internal/model/sys_notice.go | 97 + server/internal/model/sys_notice_user.go | 201 + server/internal/model/sys_oper_log.go | 64 + server/internal/model/sys_oss.go | 112 + server/internal/model/sys_oss_config.go | 98 + server/internal/model/sys_post.go | 111 + server/internal/model/sys_role.go | 122 + server/internal/model/sys_social.go | 73 + server/internal/model/sys_tenant.go | 119 + server/internal/model/sys_tenant_package.go | 73 + server/internal/model/sys_user.go | 162 + server/internal/model/sys_user_online.go | 71 + server/internal/packed/packed.go | 6 + server/internal/packed/response/response.go | 60 + server/internal/queues/base.go | 57 + .../queues/httpqueues/sys_opt_log_queue.go | 52 + server/internal/router/app.go | 21 + server/internal/router/common.go | 27 + server/internal/router/gen.go | 18 + server/internal/router/gen_codes.go | 26 + server/internal/router/genrouter/init.go | 31 + server/internal/router/genrouter/test_demo.go | 13 + server/internal/router/genrouter/test_tree.go | 13 + server/internal/router/monitor.go | 28 + server/internal/router/router.go | 49 + server/internal/router/system.go | 28 + server/internal/service/app.go | 198 + server/internal/service/app_diagnosis.go | 26 + server/internal/service/gen.go | 75 + server/internal/service/gen_codes.go | 56 + server/internal/service/info.go | 38 + server/internal/service/memorydb.go | 198 + server/internal/service/middleware.go | 46 + server/internal/service/monitor.go | 44 + server/internal/service/system.go | 612 + server/internal/tasks/base.go | 17 + server/internal/tasks/clear_data.go | 32 + server/internal/tasks/task_log.go | 19 + server/internal/tasks/tasks.go | 386 + server/main.go | 42 + .../generate/default/curd/api.go.template | 101 + .../default/curd/controller.go.template | 119 + .../generate/default/curd/input.go.template | 168 + .../generate/default/curd/logic.go.template | 298 + .../generate/default/curd/router.go.template | 14 + .../generate/default/curd/source.sql.template | 82 + .../generate/default/curd/web.api.ts.template | 58 + .../default/curd/web.edit.vue.template | 135 + .../default/curd/web.index.vue.template | 235 + .../default/curd/web.model.ts.template | 57 + .../default/curd/web.view.vue.template | 41 + server/resource/public/public.txt | 0 .../20260709_fix_t_sim_terminal_unique.sql | 62 + server/storage/data/xiu_admin.sql | 1310 + server/utility/convert/convert.go | 92 + server/utility/excel/excel.go | 138 + server/utility/gfredis/conn.go | 11 + server/utility/gfredis/gfredis.go | 150 + server/utility/gfredis/hash.go | 51 + server/utility/gfredis/key.go | 16 + server/utility/gfredis/list.go | 37 + server/utility/gfredis/string.go | 42 + server/utility/gfredis/subpub.go | 18 + server/utility/gfredis/zset.go | 21 + server/utility/hash/crc16.go | 100 + server/utility/hash/sum16.go | 31 + server/utility/iot_excelize/batch_result.go | 92 + server/utility/iot_excelize/export_builder.go | 137 + server/utility/iot_excelize/import_reader.go | 202 + server/utility/iot_excelize/local_storage.go | 89 + server/utility/iot_excelize/schema.go | 111 + server/utility/iot_excelize/style.go | 53 + server/utility/iot_excelize/types.go | 49 + server/utility/iot_excelize/writer.go | 47 + server/utility/queue/memory_queue.go | 127 + server/utility/queue/queue.go | 180 + server/utility/queue/queue_test.go | 100 + server/utility/queue/redis_broadcast.go | 173 + server/utility/queue/redis_queue.go | 153 + server/utility/tree/tree.go | 109 + server/utility/tree/tree_node.go | 43 + server/utility/utility.go | 283 + server/utility/uuid32/uuid32.go | 15 + server/utility/version/version.go | 45 + 1013 files changed, 138466 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/.commitlintrc.cjs create mode 100644 app/.dockerignore create mode 100644 app/.editorconfig create mode 100644 app/.gitignore create mode 100644 app/.npmrc create mode 100644 app/.prettierignore create mode 100644 app/.prettierrc.json create mode 100644 app/Dockerfile create mode 100644 app/LICENSE create mode 100644 app/README.md create mode 100644 app/env/.env create mode 100644 app/env/.env.development create mode 100644 app/env/.env.production create mode 100644 app/env/.env.test create mode 100644 app/eslint.config.mjs create mode 100644 app/favicon.ico create mode 100644 app/folder-alias.json create mode 100644 app/index.html create mode 100644 app/manifest.config.ts create mode 100644 app/openapi-ts-request.config.ts create mode 100644 app/package.json create mode 100644 app/pages.config.ts create mode 100644 app/pnpm-lock.yaml create mode 100644 app/project.config.json create mode 100644 app/project.private.config.json create mode 100644 app/public/favicon.png create mode 100644 app/public/logo.png create mode 100644 app/scripts/bump-version.js create mode 100644 app/scripts/create-base-files.js create mode 100644 app/scripts/open-dev-tools.js create mode 100644 app/scripts/postupgrade.js create mode 100644 app/scripts/upload-weixin.js create mode 100644 app/scripts/validate-message-page.mjs create mode 100644 app/scripts/validate-sub-pages.mjs create mode 100644 app/scripts/vite-plugin-eruda.js create mode 100644 app/src/App.ku.vue create mode 100644 app/src/App.vue create mode 100644 app/src/api/balance.ts create mode 100644 app/src/api/card.ts create mode 100644 app/src/api/customer.ts create mode 100644 app/src/api/diagnosis.ts create mode 100644 app/src/api/foo-alova.ts create mode 100644 app/src/api/foo.ts create mode 100644 app/src/api/login.ts create mode 100644 app/src/api/message.ts create mode 100644 app/src/api/order.ts create mode 100644 app/src/api/system-normalize.ts create mode 100644 app/src/api/system.test.ts create mode 100644 app/src/api/system.ts create mode 100644 app/src/api/types/balance.ts create mode 100644 app/src/api/types/card.ts create mode 100644 app/src/api/types/customer.ts create mode 100644 app/src/api/types/diagnosis.ts create mode 100644 app/src/api/types/login.ts create mode 100644 app/src/api/types/message.ts create mode 100644 app/src/api/types/order.ts create mode 100644 app/src/api/types/warning.ts create mode 100644 app/src/api/types/workOrder.ts create mode 100644 app/src/api/warning.ts create mode 100644 app/src/api/workOrder.ts create mode 100644 app/src/components/.gitkeep create mode 100644 app/src/components/Navbar/index.vue create mode 100644 app/src/components/SideCaptcha.vue create mode 100644 app/src/constant/RouterPath.ts create mode 100644 app/src/env.d.ts create mode 100644 app/src/hooks/useRequest.ts create mode 100644 app/src/hooks/useScroll.md create mode 100644 app/src/hooks/useScroll.ts create mode 100644 app/src/hooks/useUpload.ts create mode 100644 app/src/http/README.md create mode 100644 app/src/http/alova.ts create mode 100644 app/src/http/error.ts create mode 100644 app/src/http/http.ts create mode 100644 app/src/http/interceptor.ts create mode 100644 app/src/http/tools/enum.ts create mode 100644 app/src/http/tools/queryString.ts create mode 100644 app/src/http/types.ts create mode 100644 app/src/http/vue-query.ts create mode 100644 app/src/layouts/blank.vue create mode 100644 app/src/layouts/default.vue create mode 100644 app/src/locale/README.md create mode 100644 app/src/locale/en.json create mode 100644 app/src/locale/index.ts create mode 100644 app/src/locale/zh-Hans.json create mode 100644 app/src/main.ts create mode 100644 app/src/manifest.json create mode 100644 app/src/pages.json create mode 100644 app/src/pages/business/components/BusinessHero.vue create mode 100644 app/src/pages/business/components/BusinessSection.vue create mode 100644 app/src/pages/business/data.test.ts create mode 100644 app/src/pages/business/data.ts create mode 100644 app/src/pages/business/index.vue create mode 100644 app/src/pages/business/views/addon/index.vue create mode 100644 app/src/pages/business/views/allocate/index.vue create mode 100644 app/src/pages/business/views/api/balance.ts create mode 100644 app/src/pages/business/views/api/customer.ts create mode 100644 app/src/pages/business/views/api/diagnosis.ts create mode 100644 app/src/pages/business/views/api/order.ts create mode 100644 app/src/pages/business/views/api/warning.ts create mode 100644 app/src/pages/business/views/api/workOrder.ts create mode 100644 app/src/pages/business/views/balanceRecharge/index.vue create mode 100644 app/src/pages/business/views/cardRenew/index.vue create mode 100644 app/src/pages/business/views/change/index.vue create mode 100644 app/src/pages/business/views/customer/index.vue create mode 100644 app/src/pages/business/views/customerDetail/index.vue create mode 100644 app/src/pages/business/views/diagnosis/index.vue create mode 100644 app/src/pages/business/views/order/index.vue create mode 100644 app/src/pages/business/views/renew/index.vue create mode 100644 app/src/pages/business/views/warning/index.vue create mode 100644 app/src/pages/business/views/workOrder/index.vue create mode 100644 app/src/pages/business/views/workOrderCreate/index.vue create mode 100644 app/src/pages/business/views/workOrderDetail/index.vue create mode 100644 app/src/pages/card/action.ts create mode 100644 app/src/pages/card/components/CardList.vue create mode 100644 app/src/pages/card/components/CardToolbar.vue create mode 100644 app/src/pages/card/components/EmptyState.vue create mode 100644 app/src/pages/card/components/FilterBar.vue create mode 100644 app/src/pages/card/components/PackageList.vue create mode 100644 app/src/pages/card/data.test.ts create mode 100644 app/src/pages/card/data.ts create mode 100644 app/src/pages/card/index.vue create mode 100644 app/src/pages/card/views/api/warning.ts create mode 100644 app/src/pages/card/views/detail/index.vue create mode 100644 app/src/pages/card/views/management/index.vue create mode 100644 app/src/pages/card/views/packageDetail/index.vue create mode 100644 app/src/pages/card/views/warning/index.vue create mode 100644 app/src/pages/home/components/CardOverview.vue create mode 100644 app/src/pages/home/components/FlowPool.vue create mode 100644 app/src/pages/home/components/FlowRanking.vue create mode 100644 app/src/pages/home/components/LifecycleStats.vue create mode 100644 app/src/pages/home/components/PublicCardQuery.vue create mode 100644 app/src/pages/home/components/QuickBusiness.vue create mode 100644 app/src/pages/home/index.vue create mode 100644 app/src/pages/login/forgot-password/index.vue create mode 100644 app/src/pages/login/index.vue create mode 100644 app/src/pages/login/register/index.vue create mode 100644 app/src/pages/message/components/MessageList.vue create mode 100644 app/src/pages/message/components/MessageTabs.vue create mode 100644 app/src/pages/message/components/MessageToolbar.vue create mode 100644 app/src/pages/message/data.test.ts create mode 100644 app/src/pages/message/data.ts create mode 100644 app/src/pages/message/index.vue create mode 100644 app/src/pages/message/views/cardDetail/index.vue create mode 100644 app/src/pages/message/views/systemDetail/index.vue create mode 100644 app/src/pages/user/components/UserProfileCard.vue create mode 100644 app/src/pages/user/components/UserServiceCells.vue create mode 100644 app/src/pages/user/components/UserStatsCard.vue create mode 100644 app/src/pages/user/components/UserSupportCells.vue create mode 100644 app/src/pages/user/index.vue create mode 100644 app/src/pages/user/views/about/index.vue create mode 100644 app/src/pages/user/views/agreement/index.vue create mode 100644 app/src/pages/user/views/api/warning.ts create mode 100644 app/src/pages/user/views/contact/index.vue create mode 100644 app/src/pages/user/views/feedback/index.vue create mode 100644 app/src/pages/user/views/notification/index.vue create mode 100644 app/src/pages/user/views/profile/index.vue create mode 100644 app/src/pages/user/views/realName/index.vue create mode 100644 app/src/router/README.md create mode 100644 app/src/router/appPermission.ts create mode 100644 app/src/router/config.ts create mode 100644 app/src/router/interceptor.ts create mode 100644 app/src/router/permission.ts create mode 100644 app/src/service/index.ts create mode 100644 app/src/service/info.ts create mode 100644 app/src/service/listAll.ts create mode 100644 app/src/service/types.ts create mode 100644 app/src/static/app/icons/1024x1024.png create mode 100644 app/src/static/app/icons/120x120.png create mode 100644 app/src/static/app/icons/144x144.png create mode 100644 app/src/static/app/icons/152x152.png create mode 100644 app/src/static/app/icons/167x167.png create mode 100644 app/src/static/app/icons/180x180.png create mode 100644 app/src/static/app/icons/192x192.png create mode 100644 app/src/static/app/icons/20x20.png create mode 100644 app/src/static/app/icons/29x29.png create mode 100644 app/src/static/app/icons/40x40.png create mode 100644 app/src/static/app/icons/58x58.png create mode 100644 app/src/static/app/icons/60x60.png create mode 100644 app/src/static/app/icons/72x72.png create mode 100644 app/src/static/app/icons/76x76.png create mode 100644 app/src/static/app/icons/80x80.png create mode 100644 app/src/static/app/icons/87x87.png create mode 100644 app/src/static/app/icons/96x96.png create mode 100644 app/src/static/fonts/.gitkeep create mode 100644 app/src/static/images/avatar.jpg create mode 100644 app/src/static/images/default-avatar.png create mode 100644 app/src/static/images/shandong-yunze-logo.png create mode 100644 app/src/static/logo.svg create mode 100644 app/src/static/my-icons/copyright.svg create mode 100644 app/src/static/my-icons/sparkle.svg create mode 100644 app/src/static/tabbar/example.png create mode 100644 app/src/static/tabbar/exampleHL.png create mode 100644 app/src/static/tabbar/home.png create mode 100644 app/src/static/tabbar/homeHL.png create mode 100644 app/src/static/tabbar/personal.png create mode 100644 app/src/static/tabbar/personalHL.png create mode 100644 app/src/static/tabbar/scan.png create mode 100644 app/src/store/index.ts create mode 100644 app/src/store/lastQueriedCard.ts create mode 100644 app/src/store/message.ts create mode 100644 app/src/store/token.ts create mode 100644 app/src/store/user.ts create mode 100644 app/src/style/iconfont.css create mode 100644 app/src/style/index.scss create mode 100644 app/src/tabbar/README.md create mode 100644 app/src/tabbar/TabbarItem.vue create mode 100644 app/src/tabbar/config.ts create mode 100644 app/src/tabbar/i18n.ts create mode 100644 app/src/tabbar/index.vue create mode 100644 app/src/tabbar/store.ts create mode 100644 app/src/tabbar/types.ts create mode 100644 app/src/types/async-component.d.ts create mode 100644 app/src/types/async-import.d.ts create mode 100644 app/src/types/auto-import.d.ts create mode 100644 app/src/types/components.d.ts create mode 100644 app/src/types/uni-pages.d.ts create mode 100644 app/src/typings.d.ts create mode 100644 app/src/typings.ts create mode 100644 app/src/uni.scss create mode 100644 app/src/uni_modules/.gitkeep create mode 100644 app/src/utils/autoCreateRoutePath.ts create mode 100644 app/src/utils/cardRecognition.ts create mode 100644 app/src/utils/clipboard.ts create mode 100644 app/src/utils/debounce.ts create mode 100644 app/src/utils/i18n.ts create mode 100644 app/src/utils/index.ts create mode 100644 app/src/utils/initNavbarDesc.ts create mode 100644 app/src/utils/lifecycleBatch.ts create mode 100644 app/src/utils/permission.test.ts create mode 100644 app/src/utils/permission.ts create mode 100644 app/src/utils/scanPageFilter.ts create mode 100644 app/src/utils/systemInfo.ts create mode 100644 app/src/utils/toLoginPage.ts create mode 100644 app/src/utils/updateManager.wx.ts create mode 100644 app/src/utils/uploadFile.ts create mode 100644 app/src/utils/userIdentity.ts create mode 100644 app/tsconfig.json create mode 100644 app/uno.config.ts create mode 100644 app/vite-plugins/README.md create mode 100644 app/vite-plugins/copy-native-resources.ts create mode 100644 app/vite-plugins/sync-manifest-plugins.ts create mode 100644 app/vite.config.ts create mode 100644 app/wot-ui-resolver.ts create mode 100644 server/.dockerignore create mode 100644 server/Dockerfile create mode 100644 server/LICENSE create mode 100644 server/Makefile create mode 100644 server/README.MD create mode 100644 server/api/app/app.go create mode 100644 server/api/app/v1/auth.go create mode 100644 server/api/app/v1/balance.go create mode 100644 server/api/app/v1/card.go create mode 100644 server/api/app/v1/customer.go create mode 100644 server/api/app/v1/diagnosis.go create mode 100644 server/api/app/v1/message.go create mode 100644 server/api/app/v1/open.go create mode 100644 server/api/app/v1/order.go create mode 100644 server/api/app/v1/user.go create mode 100644 server/api/app/v1/warning.go create mode 100644 server/api/app/v1/work_order.go create mode 100644 server/api/common/common.go create mode 100644 server/api/common/v1/upload.go create mode 100644 server/api/gen/testdemo/testdemo.go create mode 100644 server/api/gen/testdemo/v1/testdemo.go create mode 100644 server/api/gen/testtree/testtree.go create mode 100644 server/api/gen/testtree/v1/testtree.go create mode 100644 server/api/gen_codes/gen_codes.go create mode 100644 server/api/gen_codes/v1/gen_table.go create mode 100644 server/api/info/info.go create mode 100644 server/api/info/v1/info.go create mode 100644 server/api/monitor/monitor.go create mode 100644 server/api/monitor/v1/cache.go create mode 100644 server/api/system/system.go create mode 100644 server/api/system/v1/auth.go create mode 100644 server/api/system/v1/captcha.go create mode 100644 server/api/system/v1/client.go create mode 100644 server/api/system/v1/config.go create mode 100644 server/api/system/v1/dept.go create mode 100644 server/api/system/v1/dict.go create mode 100644 server/api/system/v1/job.go create mode 100644 server/api/system/v1/logininfor.go create mode 100644 server/api/system/v1/menu.go create mode 100644 server/api/system/v1/notice.go create mode 100644 server/api/system/v1/online.go create mode 100644 server/api/system/v1/oper_log.go create mode 100644 server/api/system/v1/oss.go create mode 100644 server/api/system/v1/oss_config.go create mode 100644 server/api/system/v1/post.go create mode 100644 server/api/system/v1/qrcode.go create mode 100644 server/api/system/v1/role.go create mode 100644 server/api/system/v1/social.go create mode 100644 server/api/system/v1/sysnoticeuser.go create mode 100644 server/api/system/v1/tenant.go create mode 100644 server/api/system/v1/user.go create mode 100644 server/config/config.yaml create mode 100644 server/config/config_default.yaml create mode 100644 server/go.mod create mode 100644 server/go.sum create mode 100644 server/hack/config.yaml create mode 100644 server/hack/hack-cli.mk create mode 100644 server/hack/hack.mk create mode 100644 server/internal/ai/chat_model.go create mode 100644 server/internal/ai/chat_model_test.go create mode 100644 server/internal/cmd/cmd.go create mode 100644 server/internal/cmd/global.go create mode 100644 server/internal/cmd/http.go create mode 100644 server/internal/cmd/initfuunc.go create mode 100644 server/internal/cmd/inithttp/inithttp.go create mode 100644 server/internal/cmd/monitor.go create mode 100644 server/internal/cmd/shutdown.go create mode 100644 server/internal/consts/app_register.go create mode 100644 server/internal/consts/batch.go create mode 100644 server/internal/consts/bcache.go create mode 100644 server/internal/consts/code.go create mode 100644 server/internal/consts/config.go create mode 100644 server/internal/consts/consts.go create mode 100644 server/internal/consts/db.go create mode 100644 server/internal/consts/event.go create mode 100644 server/internal/consts/http.go create mode 100644 server/internal/consts/mcache.go create mode 100644 server/internal/consts/queue.go create mode 100644 server/internal/consts/system.go create mode 100644 server/internal/consts/websocket.go create mode 100644 server/internal/controller/app/app_new.go create mode 100644 server/internal/controller/app/app_v1_auth.go create mode 100644 server/internal/controller/app/app_v1_balance.go create mode 100644 server/internal/controller/app/app_v1_card.go create mode 100644 server/internal/controller/app/app_v1_customer.go create mode 100644 server/internal/controller/app/app_v1_diagnosis.go create mode 100644 server/internal/controller/app/app_v1_message.go create mode 100644 server/internal/controller/app/app_v1_open.go create mode 100644 server/internal/controller/app/app_v1_order.go create mode 100644 server/internal/controller/app/app_v1_user.go create mode 100644 server/internal/controller/app/app_v1_warning.go create mode 100644 server/internal/controller/app/app_v1_work_order.go create mode 100644 server/internal/controller/common/common.go create mode 100644 server/internal/controller/common/common_new.go create mode 100644 server/internal/controller/common/common_v1_upload.go create mode 100644 server/internal/controller/gen/test_demo.go create mode 100644 server/internal/controller/gen/test_tree.go create mode 100644 server/internal/controller/gen_codes/gen_codes.go create mode 100644 server/internal/controller/gen_codes/gen_codes_new.go create mode 100644 server/internal/controller/gen_codes/gen_codes_v1_gen_table.go create mode 100644 server/internal/controller/info/info.go create mode 100644 server/internal/controller/info/info_new.go create mode 100644 server/internal/controller/info/info_v1_info.go create mode 100644 server/internal/controller/monitor/monitor.go create mode 100644 server/internal/controller/monitor/monitor_new.go create mode 100644 server/internal/controller/monitor/monitor_v1_cache.go create mode 100644 server/internal/controller/system/sys_notice_user.go create mode 100644 server/internal/controller/system/system.go create mode 100644 server/internal/controller/system/system_new.go create mode 100644 server/internal/controller/system/system_v1_auth.go create mode 100644 server/internal/controller/system/system_v1_captcha.go create mode 100644 server/internal/controller/system/system_v1_client.go create mode 100644 server/internal/controller/system/system_v1_config.go create mode 100644 server/internal/controller/system/system_v1_dept.go create mode 100644 server/internal/controller/system/system_v1_dict.go create mode 100644 server/internal/controller/system/system_v1_job.go create mode 100644 server/internal/controller/system/system_v1_logininfor.go create mode 100644 server/internal/controller/system/system_v1_menu.go create mode 100644 server/internal/controller/system/system_v1_notice.go create mode 100644 server/internal/controller/system/system_v1_online.go create mode 100644 server/internal/controller/system/system_v1_oper_log.go create mode 100644 server/internal/controller/system/system_v1_oss.go create mode 100644 server/internal/controller/system/system_v1_oss_config.go create mode 100644 server/internal/controller/system/system_v1_post.go create mode 100644 server/internal/controller/system/system_v1_qrcode.go create mode 100644 server/internal/controller/system/system_v1_role.go create mode 100644 server/internal/controller/system/system_v1_social.go create mode 100644 server/internal/controller/system/system_v1_tenant.go create mode 100644 server/internal/controller/system/system_v1_user.go create mode 100644 server/internal/dao/batch_detail.go create mode 100644 server/internal/dao/batch_main.go create mode 100644 server/internal/dao/card_channel_assoc.go create mode 100644 server/internal/dao/gen_table.go create mode 100644 server/internal/dao/gen_table_column.go create mode 100644 server/internal/dao/internal/batch_detail.go create mode 100644 server/internal/dao/internal/batch_main.go create mode 100644 server/internal/dao/internal/card_channel_assoc.go create mode 100644 server/internal/dao/internal/gen_table.go create mode 100644 server/internal/dao/internal/gen_table_column.go create mode 100644 server/internal/dao/internal/sys_client.go create mode 100644 server/internal/dao/internal/sys_config.go create mode 100644 server/internal/dao/internal/sys_dept.go create mode 100644 server/internal/dao/internal/sys_dict_data.go create mode 100644 server/internal/dao/internal/sys_dict_type.go create mode 100644 server/internal/dao/internal/sys_gen_table.go create mode 100644 server/internal/dao/internal/sys_job.go create mode 100644 server/internal/dao/internal/sys_logininfor.go create mode 100644 server/internal/dao/internal/sys_menu.go create mode 100644 server/internal/dao/internal/sys_notice.go create mode 100644 server/internal/dao/internal/sys_notice_user.go create mode 100644 server/internal/dao/internal/sys_oper_log.go create mode 100644 server/internal/dao/internal/sys_oss.go create mode 100644 server/internal/dao/internal/sys_oss_config.go create mode 100644 server/internal/dao/internal/sys_post.go create mode 100644 server/internal/dao/internal/sys_role.go create mode 100644 server/internal/dao/internal/sys_role_dept.go create mode 100644 server/internal/dao/internal/sys_role_menu.go create mode 100644 server/internal/dao/internal/sys_social.go create mode 100644 server/internal/dao/internal/sys_tenant.go create mode 100644 server/internal/dao/internal/sys_tenant_package.go create mode 100644 server/internal/dao/internal/sys_user.go create mode 100644 server/internal/dao/internal/sys_user_online.go create mode 100644 server/internal/dao/internal/sys_user_post.go create mode 100644 server/internal/dao/internal/sys_user_role.go create mode 100644 server/internal/dao/internal/t_agent_balance.go create mode 100644 server/internal/dao/internal/t_agent_enterprise.go create mode 100644 server/internal/dao/internal/t_api.go create mode 100644 server/internal/dao/internal/t_api_group.go create mode 100644 server/internal/dao/internal/t_api_group_resource.go create mode 100644 server/internal/dao/internal/t_api_resource.go create mode 100644 server/internal/dao/internal/t_balance_detail.go create mode 100644 server/internal/dao/internal/t_brand.go create mode 100644 server/internal/dao/internal/t_category.go create mode 100644 server/internal/dao/internal/t_consume_account_month_bill.go create mode 100644 server/internal/dao/internal/t_credit_detail.go create mode 100644 server/internal/dao/internal/t_cust_withdraw_account.go create mode 100644 server/internal/dao/internal/t_dept_flow_daily.go create mode 100644 server/internal/dao/internal/t_division_code.go create mode 100644 server/internal/dao/internal/t_fin_commission_detail.go create mode 100644 server/internal/dao/internal/t_fin_withdrawable_change.go create mode 100644 server/internal/dao/internal/t_finance_apply.go create mode 100644 server/internal/dao/internal/t_finance_audit_record.go create mode 100644 server/internal/dao/internal/t_flow_package_config.go create mode 100644 server/internal/dao/internal/t_message_log.go create mode 100644 server/internal/dao/internal/t_message_type.go create mode 100644 server/internal/dao/internal/t_notify_log.go create mode 100644 server/internal/dao/internal/t_order.go create mode 100644 server/internal/dao/internal/t_order_item.go create mode 100644 server/internal/dao/internal/t_package_apply.go create mode 100644 server/internal/dao/internal/t_package_audit_record.go create mode 100644 server/internal/dao/internal/t_package_customer.go create mode 100644 server/internal/dao/internal/t_package_terminal.go create mode 100644 server/internal/dao/internal/t_product.go create mode 100644 server/internal/dao/internal/t_profit_account_month_bill.go create mode 100644 server/internal/dao/internal/t_resource_batch_download.go create mode 100644 server/internal/dao/internal/t_sim_bind_package_logs.go create mode 100644 server/internal/dao/internal/t_sim_card_bind_pkg.go create mode 100644 server/internal/dao/internal/t_sim_card_cancellation.go create mode 100644 server/internal/dao/internal/t_sim_card_info.go create mode 100644 server/internal/dao/internal/t_sim_card_stock.go create mode 100644 server/internal/dao/internal/t_sim_channel_template.go create mode 100644 server/internal/dao/internal/t_sim_channels.go create mode 100644 server/internal/dao/internal/t_sim_flow_package_info.go create mode 100644 server/internal/dao/internal/t_sim_flow_package_price.go create mode 100644 server/internal/dao/internal/t_sim_flow_pool.go create mode 100644 server/internal/dao/internal/t_sim_package_category.go create mode 100644 server/internal/dao/internal/t_sim_pool.go create mode 100644 server/internal/dao/internal/t_sim_pool_bind_package.go create mode 100644 server/internal/dao/internal/t_sim_pool_bind_pkg_logs.go create mode 100644 server/internal/dao/internal/t_sim_pool_warn_config.go create mode 100644 server/internal/dao/internal/t_sim_pool_warn_log.go create mode 100644 server/internal/dao/internal/t_sim_terminal.go create mode 100644 server/internal/dao/internal/t_sim_upstream_package.go create mode 100644 server/internal/dao/internal/t_terminal.go create mode 100644 server/internal/dao/internal/t_tplatform_setting.go create mode 100644 server/internal/dao/internal/t_user_message.go create mode 100644 server/internal/dao/internal/t_warn_config.go create mode 100644 server/internal/dao/internal/t_warn_log.go create mode 100644 server/internal/dao/internal/t_wo_communication.go create mode 100644 server/internal/dao/internal/t_wo_main.go create mode 100644 server/internal/dao/internal/t_wo_record.go create mode 100644 server/internal/dao/internal/test_demo.go create mode 100644 server/internal/dao/internal/test_tree.go create mode 100644 server/internal/dao/sys_client.go create mode 100644 server/internal/dao/sys_config.go create mode 100644 server/internal/dao/sys_dept.go create mode 100644 server/internal/dao/sys_dict_data.go create mode 100644 server/internal/dao/sys_dict_type.go create mode 100644 server/internal/dao/sys_gen_table.go create mode 100644 server/internal/dao/sys_job.go create mode 100644 server/internal/dao/sys_logininfor.go create mode 100644 server/internal/dao/sys_menu.go create mode 100644 server/internal/dao/sys_notice.go create mode 100644 server/internal/dao/sys_notice_user.go create mode 100644 server/internal/dao/sys_oper_log.go create mode 100644 server/internal/dao/sys_oss.go create mode 100644 server/internal/dao/sys_oss_config.go create mode 100644 server/internal/dao/sys_post.go create mode 100644 server/internal/dao/sys_role.go create mode 100644 server/internal/dao/sys_role_dept.go create mode 100644 server/internal/dao/sys_role_menu.go create mode 100644 server/internal/dao/sys_social.go create mode 100644 server/internal/dao/sys_tenant.go create mode 100644 server/internal/dao/sys_tenant_package.go create mode 100644 server/internal/dao/sys_user.go create mode 100644 server/internal/dao/sys_user_online.go create mode 100644 server/internal/dao/sys_user_post.go create mode 100644 server/internal/dao/sys_user_role.go create mode 100644 server/internal/dao/t_agent_balance.go create mode 100644 server/internal/dao/t_agent_enterprise.go create mode 100644 server/internal/dao/t_api.go create mode 100644 server/internal/dao/t_api_group.go create mode 100644 server/internal/dao/t_api_group_resource.go create mode 100644 server/internal/dao/t_api_resource.go create mode 100644 server/internal/dao/t_balance_detail.go create mode 100644 server/internal/dao/t_brand.go create mode 100644 server/internal/dao/t_category.go create mode 100644 server/internal/dao/t_consume_account_month_bill.go create mode 100644 server/internal/dao/t_credit_detail.go create mode 100644 server/internal/dao/t_cust_withdraw_account.go create mode 100644 server/internal/dao/t_dept_flow_daily.go create mode 100644 server/internal/dao/t_division_code.go create mode 100644 server/internal/dao/t_fin_commission_detail.go create mode 100644 server/internal/dao/t_fin_withdrawable_change.go create mode 100644 server/internal/dao/t_finance_apply.go create mode 100644 server/internal/dao/t_finance_audit_record.go create mode 100644 server/internal/dao/t_flow_package_config.go create mode 100644 server/internal/dao/t_message_log.go create mode 100644 server/internal/dao/t_message_type.go create mode 100644 server/internal/dao/t_notify_log.go create mode 100644 server/internal/dao/t_order.go create mode 100644 server/internal/dao/t_order_item.go create mode 100644 server/internal/dao/t_package_apply.go create mode 100644 server/internal/dao/t_package_audit_record.go create mode 100644 server/internal/dao/t_package_customer.go create mode 100644 server/internal/dao/t_package_terminal.go create mode 100644 server/internal/dao/t_product.go create mode 100644 server/internal/dao/t_profit_account_month_bill.go create mode 100644 server/internal/dao/t_resource_batch_download.go create mode 100644 server/internal/dao/t_sim_bind_package_logs.go create mode 100644 server/internal/dao/t_sim_card_bind_pkg.go create mode 100644 server/internal/dao/t_sim_card_cancellation.go create mode 100644 server/internal/dao/t_sim_card_info.go create mode 100644 server/internal/dao/t_sim_card_stock.go create mode 100644 server/internal/dao/t_sim_channel_template.go create mode 100644 server/internal/dao/t_sim_channels.go create mode 100644 server/internal/dao/t_sim_flow_package_info.go create mode 100644 server/internal/dao/t_sim_flow_package_price.go create mode 100644 server/internal/dao/t_sim_flow_pool.go create mode 100644 server/internal/dao/t_sim_package_category.go create mode 100644 server/internal/dao/t_sim_pool.go create mode 100644 server/internal/dao/t_sim_pool_bind_package.go create mode 100644 server/internal/dao/t_sim_pool_bind_pkg_logs.go create mode 100644 server/internal/dao/t_sim_pool_warn_config.go create mode 100644 server/internal/dao/t_sim_pool_warn_log.go create mode 100644 server/internal/dao/t_sim_terminal.go create mode 100644 server/internal/dao/t_sim_upstream_package.go create mode 100644 server/internal/dao/t_terminal.go create mode 100644 server/internal/dao/t_tplatform_setting.go create mode 100644 server/internal/dao/t_user_message.go create mode 100644 server/internal/dao/t_warn_config.go create mode 100644 server/internal/dao/t_warn_log.go create mode 100644 server/internal/dao/t_wo_communication.go create mode 100644 server/internal/dao/t_wo_main.go create mode 100644 server/internal/dao/t_wo_record.go create mode 100644 server/internal/dao/test_demo.go create mode 100644 server/internal/dao/test_tree.go create mode 100644 server/internal/library/addons/addons.go create mode 100644 server/internal/library/appidentity/terminal_user.go create mode 100644 server/internal/library/batchimport/async.go create mode 100644 server/internal/library/batchimport/batch_no.go create mode 100644 server/internal/library/batchimport/batch_no_test.go create mode 100644 server/internal/library/batchimport/runner.go create mode 100644 server/internal/library/batchimport/types.go create mode 100644 server/internal/library/bcache/bcache.go create mode 100644 server/internal/library/bcache/bcache_system_auth.go create mode 100644 server/internal/library/cache/cache.go create mode 100644 server/internal/library/cache/file/file.go create mode 100644 server/internal/library/contexts/contexts.go create mode 100644 server/internal/library/event/event.go create mode 100644 server/internal/library/mcache/mcache.go create mode 100644 server/internal/library/mcache/mcache_access_list.go create mode 100644 server/internal/library/mcache/mcache_system_config.go create mode 100644 server/internal/library/mcache/mcache_system_dept.go create mode 100644 server/internal/library/mcache/mcache_system_user.go create mode 100644 server/internal/library/rabbitmq/publisher.go create mode 100644 server/internal/library/websocket/client.go create mode 100644 server/internal/library/websocket/client_manager.go create mode 100644 server/internal/library/websocket/controller.go create mode 100644 server/internal/library/websocket/init.go create mode 100644 server/internal/library/websocket/model.go create mode 100644 server/internal/library/websocket/router.go create mode 100644 server/internal/library/worker/options.go create mode 100644 server/internal/library/worker/work_proces.go create mode 100644 server/internal/library/worker/worker.go create mode 100644 server/internal/library/xgen/gen_consts/gen_codes.go create mode 100644 server/internal/library/xgen/gen_consts/gen_config.go create mode 100644 server/internal/library/xgen/gen_dao/gen_dao.go create mode 100644 server/internal/library/xgen/gen_model/gen_codes.go create mode 100644 server/internal/library/xgen/gen_model/gen_config.go create mode 100644 server/internal/library/xgen/gen_model/gen_view.go create mode 100644 server/internal/library/xgen/gen_view/gen_column.go create mode 100644 server/internal/library/xgen/gen_view/gen_column_default.go create mode 100644 server/internal/library/xgen/gen_view/gen_column_map.go create mode 100644 server/internal/library/xgen/gen_view/gen_curd.go create mode 100644 server/internal/library/xgen/gen_view/gen_curd_input.go create mode 100644 server/internal/library/xgen/gen_view/gen_curd_logic.go create mode 100644 server/internal/library/xgen/gen_view/gen_curd_model.go create mode 100644 server/internal/library/xgen/gen_view/gen_curd_web_edit.go create mode 100644 server/internal/library/xgen/gen_view/gen_curd_web_index.go create mode 100644 server/internal/library/xgen/gen_view/gen_curd_web_model.go create mode 100644 server/internal/library/xgen/gen_view/gen_curd_web_view.go create mode 100644 server/internal/library/xgen/gen_view/gen_util.go create mode 100644 server/internal/library/xgen/gen_view/gohtml/consts.go create mode 100644 server/internal/library/xgen/gen_view/gohtml/doc.go create mode 100644 server/internal/library/xgen/gen_view/gohtml/element.go create mode 100644 server/internal/library/xgen/gen_view/gohtml/formatter.go create mode 100644 server/internal/library/xgen/gen_view/gohtml/html_document.go create mode 100644 server/internal/library/xgen/gen_view/gohtml/parser.go create mode 100644 server/internal/library/xgen/gen_view/gohtml/tag_element.go create mode 100644 server/internal/library/xgen/gen_view/gohtml/text_element.go create mode 100644 server/internal/library/xgen/gen_view/gohtml/utils.go create mode 100644 server/internal/library/xgen/gen_view/gohtml/writer.go create mode 100644 server/internal/library/xgen/xgen_curd.go create mode 100644 server/internal/library/xgen/xgen_init.go create mode 100644 server/internal/library/xgorm/dao.go create mode 100644 server/internal/library/xgorm/dao_tree.go create mode 100644 server/internal/library/xgorm/gorm_model/gorm_tree.go create mode 100644 server/internal/library/xgorm/handler/filter_auth.go create mode 100644 server/internal/library/xgorm/handler/force_cache.go create mode 100644 server/internal/library/xgorm/handler/handler.go create mode 100644 server/internal/library/xgorm/handler/tenant.go create mode 100644 server/internal/library/xgorm/hook/member.go create mode 100644 server/internal/logic/app/app_access.go create mode 100644 server/internal/logic/app/app_access_helpers.go create mode 100644 server/internal/logic/app/app_access_test.go create mode 100644 server/internal/logic/app/app_auth.go create mode 100644 server/internal/logic/app/app_auth_test.go create mode 100644 server/internal/logic/app/app_balance.go create mode 100644 server/internal/logic/app/app_balance_recharge.go create mode 100644 server/internal/logic/app/app_balance_recharge_complete.go create mode 100644 server/internal/logic/app/app_balance_recharge_refund.go create mode 100644 server/internal/logic/app/app_balance_test.go create mode 100644 server/internal/logic/app/app_business_async.go create mode 100644 server/internal/logic/app/app_card.go create mode 100644 server/internal/logic/app/app_card_allocate.go create mode 100644 server/internal/logic/app/app_card_allocate_processor.go create mode 100644 server/internal/logic/app/app_card_allocate_result.go create mode 100644 server/internal/logic/app/app_card_bind.go create mode 100644 server/internal/logic/app/app_card_change.go create mode 100644 server/internal/logic/app/app_card_channel.go create mode 100644 server/internal/logic/app/app_card_mapper.go create mode 100644 server/internal/logic/app/app_card_mapper_test.go create mode 100644 server/internal/logic/app/app_card_package.go create mode 100644 server/internal/logic/app/app_card_query.go create mode 100644 server/internal/logic/app/app_card_recognition.go create mode 100644 server/internal/logic/app/app_card_recognition_live_test.go create mode 100644 server/internal/logic/app/app_card_recognition_test.go create mode 100644 server/internal/logic/app/app_card_renew.go create mode 100644 server/internal/logic/app/app_card_renew_batch.go create mode 100644 server/internal/logic/app/app_card_renew_complete.go create mode 100644 server/internal/logic/app/app_card_renew_period_test.go create mode 100644 server/internal/logic/app/app_card_renew_processor.go create mode 100644 server/internal/logic/app/app_card_renew_resolver.go create mode 100644 server/internal/logic/app/app_card_renew_result.go create mode 100644 server/internal/logic/app/app_card_renew_result_test.go create mode 100644 server/internal/logic/app/app_card_restart.go create mode 100644 server/internal/logic/app/app_card_restart_test.go create mode 100644 server/internal/logic/app/app_card_session.go create mode 100644 server/internal/logic/app/app_card_utils.go create mode 100644 server/internal/logic/app/app_card_utils_test.go create mode 100644 server/internal/logic/app/app_customer.go create mode 100644 server/internal/logic/app/app_data_scope.go create mode 100644 server/internal/logic/app/app_diagnosis.go create mode 100644 server/internal/logic/app/app_diagnosis_test.go create mode 100644 server/internal/logic/app/app_diagnosis_traffic.go create mode 100644 server/internal/logic/app/app_message.go create mode 100644 server/internal/logic/app/app_onelink_mock.go create mode 100644 server/internal/logic/app/app_onelink_provider.go create mode 100644 server/internal/logic/app/app_order.go create mode 100644 server/internal/logic/app/app_payment_wechat.go create mode 100644 server/internal/logic/app/app_register_config.go create mode 100644 server/internal/logic/app/app_upstream_session.go create mode 100644 server/internal/logic/app/app_user.go create mode 100644 server/internal/logic/app/app_warning.go create mode 100644 server/internal/logic/app/app_warning_test.go create mode 100644 server/internal/logic/app/app_work_order.go create mode 100644 server/internal/logic/app/app_worker_batch_async.go create mode 100644 server/internal/logic/gen/test_demo.go create mode 100644 server/internal/logic/gen/test_tree.go create mode 100644 server/internal/logic/gen_codes/gen_selects.go create mode 100644 server/internal/logic/gen_codes/sys_gen_table.go create mode 100644 server/internal/logic/info/info.go create mode 100644 server/internal/logic/logic.go create mode 100644 server/internal/logic/memorydb/memorydb.go create mode 100644 server/internal/logic/memorydb/sys_config.go create mode 100644 server/internal/logic/memorydb/sys_dept.go create mode 100644 server/internal/logic/memorydb/sys_dict_data.go create mode 100644 server/internal/logic/memorydb/sys_dict_type.go create mode 100644 server/internal/logic/memorydb/sys_menu.go create mode 100644 server/internal/logic/memorydb/sys_post.go create mode 100644 server/internal/logic/memorydb/sys_role.go create mode 100644 server/internal/logic/memorydb/sys_tenant.go create mode 100644 server/internal/logic/middleware/middleware.go create mode 100644 server/internal/logic/middleware/middleware_permission_test.go create mode 100644 server/internal/logic/monitor/monitor_server.go create mode 100644 server/internal/logic/system/sys_auth.go create mode 100644 server/internal/logic/system/sys_captcha.go create mode 100644 server/internal/logic/system/sys_client.go create mode 100644 server/internal/logic/system/sys_config.go create mode 100644 server/internal/logic/system/sys_dept.go create mode 100644 server/internal/logic/system/sys_dept_test.go create mode 100644 server/internal/logic/system/sys_dict_data.go create mode 100644 server/internal/logic/system/sys_dict_type.go create mode 100644 server/internal/logic/system/sys_job.go create mode 100644 server/internal/logic/system/sys_logininfor.go create mode 100644 server/internal/logic/system/sys_menu.go create mode 100644 server/internal/logic/system/sys_notice.go create mode 100644 server/internal/logic/system/sys_notice_user.go create mode 100644 server/internal/logic/system/sys_oper_log.go create mode 100644 server/internal/logic/system/sys_oss.go create mode 100644 server/internal/logic/system/sys_oss_config.go create mode 100644 server/internal/logic/system/sys_oss_path_test.go create mode 100644 server/internal/logic/system/sys_post.go create mode 100644 server/internal/logic/system/sys_qrcode.go create mode 100644 server/internal/logic/system/sys_role.go create mode 100644 server/internal/logic/system/sys_social.go create mode 100644 server/internal/logic/system/sys_tenant.go create mode 100644 server/internal/logic/system/sys_tenant_package.go create mode 100644 server/internal/logic/system/sys_user.go create mode 100644 server/internal/logic/system/sys_user_online.go create mode 100644 server/internal/logic/system/sys_user_test.go create mode 100644 server/internal/logic/system/task_test.go create mode 100644 server/internal/model/app_auth.go create mode 100644 server/internal/model/app_balance.go create mode 100644 server/internal/model/app_captcha.go create mode 100644 server/internal/model/app_card.go create mode 100644 server/internal/model/app_customer.go create mode 100644 server/internal/model/app_diagnosis.go create mode 100644 server/internal/model/app_message.go create mode 100644 server/internal/model/app_order.go create mode 100644 server/internal/model/app_user.go create mode 100644 server/internal/model/app_warning.go create mode 100644 server/internal/model/app_work_order.go create mode 100644 server/internal/model/bcache_model.go create mode 100644 server/internal/model/context.go create mode 100644 server/internal/model/do/batch_detail.go create mode 100644 server/internal/model/do/batch_main.go create mode 100644 server/internal/model/do/card_channel_assoc.go create mode 100644 server/internal/model/do/gen_table.go create mode 100644 server/internal/model/do/gen_table_column.go create mode 100644 server/internal/model/do/sys_client.go create mode 100644 server/internal/model/do/sys_config.go create mode 100644 server/internal/model/do/sys_dept.go create mode 100644 server/internal/model/do/sys_dict_data.go create mode 100644 server/internal/model/do/sys_dict_type.go create mode 100644 server/internal/model/do/sys_gen_table.go create mode 100644 server/internal/model/do/sys_job.go create mode 100644 server/internal/model/do/sys_logininfor.go create mode 100644 server/internal/model/do/sys_menu.go create mode 100644 server/internal/model/do/sys_notice.go create mode 100644 server/internal/model/do/sys_notice_user.go create mode 100644 server/internal/model/do/sys_oper_log.go create mode 100644 server/internal/model/do/sys_oss.go create mode 100644 server/internal/model/do/sys_oss_config.go create mode 100644 server/internal/model/do/sys_post.go create mode 100644 server/internal/model/do/sys_role.go create mode 100644 server/internal/model/do/sys_role_dept.go create mode 100644 server/internal/model/do/sys_role_menu.go create mode 100644 server/internal/model/do/sys_social.go create mode 100644 server/internal/model/do/sys_tenant.go create mode 100644 server/internal/model/do/sys_tenant_package.go create mode 100644 server/internal/model/do/sys_user.go create mode 100644 server/internal/model/do/sys_user_online.go create mode 100644 server/internal/model/do/sys_user_post.go create mode 100644 server/internal/model/do/sys_user_role.go create mode 100644 server/internal/model/do/t_agent_balance.go create mode 100644 server/internal/model/do/t_agent_enterprise.go create mode 100644 server/internal/model/do/t_api.go create mode 100644 server/internal/model/do/t_api_group.go create mode 100644 server/internal/model/do/t_api_group_resource.go create mode 100644 server/internal/model/do/t_api_resource.go create mode 100644 server/internal/model/do/t_balance_detail.go create mode 100644 server/internal/model/do/t_brand.go create mode 100644 server/internal/model/do/t_category.go create mode 100644 server/internal/model/do/t_consume_account_month_bill.go create mode 100644 server/internal/model/do/t_credit_detail.go create mode 100644 server/internal/model/do/t_cust_withdraw_account.go create mode 100644 server/internal/model/do/t_dept_flow_daily.go create mode 100644 server/internal/model/do/t_division_code.go create mode 100644 server/internal/model/do/t_fin_commission_detail.go create mode 100644 server/internal/model/do/t_fin_withdrawable_change.go create mode 100644 server/internal/model/do/t_finance_apply.go create mode 100644 server/internal/model/do/t_finance_audit_record.go create mode 100644 server/internal/model/do/t_flow_package_config.go create mode 100644 server/internal/model/do/t_message_log.go create mode 100644 server/internal/model/do/t_message_type.go create mode 100644 server/internal/model/do/t_notify_log.go create mode 100644 server/internal/model/do/t_order.go create mode 100644 server/internal/model/do/t_order_item.go create mode 100644 server/internal/model/do/t_package_apply.go create mode 100644 server/internal/model/do/t_package_audit_record.go create mode 100644 server/internal/model/do/t_package_customer.go create mode 100644 server/internal/model/do/t_package_terminal.go create mode 100644 server/internal/model/do/t_product.go create mode 100644 server/internal/model/do/t_profit_account_month_bill.go create mode 100644 server/internal/model/do/t_resource_batch_download.go create mode 100644 server/internal/model/do/t_sim_bind_package_logs.go create mode 100644 server/internal/model/do/t_sim_card_bind_pkg.go create mode 100644 server/internal/model/do/t_sim_card_cancellation.go create mode 100644 server/internal/model/do/t_sim_card_info.go create mode 100644 server/internal/model/do/t_sim_card_stock.go create mode 100644 server/internal/model/do/t_sim_channel_template.go create mode 100644 server/internal/model/do/t_sim_channels.go create mode 100644 server/internal/model/do/t_sim_flow_package_info.go create mode 100644 server/internal/model/do/t_sim_flow_package_price.go create mode 100644 server/internal/model/do/t_sim_flow_pool.go create mode 100644 server/internal/model/do/t_sim_package_category.go create mode 100644 server/internal/model/do/t_sim_pool.go create mode 100644 server/internal/model/do/t_sim_pool_bind_package.go create mode 100644 server/internal/model/do/t_sim_pool_bind_pkg_logs.go create mode 100644 server/internal/model/do/t_sim_pool_warn_config.go create mode 100644 server/internal/model/do/t_sim_pool_warn_log.go create mode 100644 server/internal/model/do/t_sim_terminal.go create mode 100644 server/internal/model/do/t_sim_upstream_package.go create mode 100644 server/internal/model/do/t_terminal.go create mode 100644 server/internal/model/do/t_tplatform_setting.go create mode 100644 server/internal/model/do/t_user_message.go create mode 100644 server/internal/model/do/t_warn_config.go create mode 100644 server/internal/model/do/t_warn_log.go create mode 100644 server/internal/model/do/t_wo_communication.go create mode 100644 server/internal/model/do/t_wo_main.go create mode 100644 server/internal/model/do/t_wo_record.go create mode 100644 server/internal/model/do/test_demo.go create mode 100644 server/internal/model/do/test_tree.go create mode 100644 server/internal/model/entity/batch_detail.go create mode 100644 server/internal/model/entity/batch_main.go create mode 100644 server/internal/model/entity/card_channel_assoc.go create mode 100644 server/internal/model/entity/gen_table.go create mode 100644 server/internal/model/entity/gen_table_column.go create mode 100644 server/internal/model/entity/sys_client.go create mode 100644 server/internal/model/entity/sys_config.go create mode 100644 server/internal/model/entity/sys_dept.go create mode 100644 server/internal/model/entity/sys_dict_data.go create mode 100644 server/internal/model/entity/sys_dict_type.go create mode 100644 server/internal/model/entity/sys_gen_table.go create mode 100644 server/internal/model/entity/sys_job.go create mode 100644 server/internal/model/entity/sys_logininfor.go create mode 100644 server/internal/model/entity/sys_menu.go create mode 100644 server/internal/model/entity/sys_notice.go create mode 100644 server/internal/model/entity/sys_notice_user.go create mode 100644 server/internal/model/entity/sys_oper_log.go create mode 100644 server/internal/model/entity/sys_oss.go create mode 100644 server/internal/model/entity/sys_oss_config.go create mode 100644 server/internal/model/entity/sys_post.go create mode 100644 server/internal/model/entity/sys_role.go create mode 100644 server/internal/model/entity/sys_role_dept.go create mode 100644 server/internal/model/entity/sys_role_menu.go create mode 100644 server/internal/model/entity/sys_social.go create mode 100644 server/internal/model/entity/sys_tenant.go create mode 100644 server/internal/model/entity/sys_tenant_package.go create mode 100644 server/internal/model/entity/sys_user.go create mode 100644 server/internal/model/entity/sys_user_online.go create mode 100644 server/internal/model/entity/sys_user_post.go create mode 100644 server/internal/model/entity/sys_user_role.go create mode 100644 server/internal/model/entity/t_agent_balance.go create mode 100644 server/internal/model/entity/t_agent_enterprise.go create mode 100644 server/internal/model/entity/t_api.go create mode 100644 server/internal/model/entity/t_api_group.go create mode 100644 server/internal/model/entity/t_api_group_resource.go create mode 100644 server/internal/model/entity/t_api_resource.go create mode 100644 server/internal/model/entity/t_balance_detail.go create mode 100644 server/internal/model/entity/t_brand.go create mode 100644 server/internal/model/entity/t_category.go create mode 100644 server/internal/model/entity/t_consume_account_month_bill.go create mode 100644 server/internal/model/entity/t_credit_detail.go create mode 100644 server/internal/model/entity/t_cust_withdraw_account.go create mode 100644 server/internal/model/entity/t_dept_flow_daily.go create mode 100644 server/internal/model/entity/t_division_code.go create mode 100644 server/internal/model/entity/t_fin_commission_detail.go create mode 100644 server/internal/model/entity/t_fin_withdrawable_change.go create mode 100644 server/internal/model/entity/t_finance_apply.go create mode 100644 server/internal/model/entity/t_finance_audit_record.go create mode 100644 server/internal/model/entity/t_flow_package_config.go create mode 100644 server/internal/model/entity/t_message_log.go create mode 100644 server/internal/model/entity/t_message_type.go create mode 100644 server/internal/model/entity/t_notify_log.go create mode 100644 server/internal/model/entity/t_order.go create mode 100644 server/internal/model/entity/t_order_item.go create mode 100644 server/internal/model/entity/t_package_apply.go create mode 100644 server/internal/model/entity/t_package_audit_record.go create mode 100644 server/internal/model/entity/t_package_customer.go create mode 100644 server/internal/model/entity/t_package_terminal.go create mode 100644 server/internal/model/entity/t_product.go create mode 100644 server/internal/model/entity/t_profit_account_month_bill.go create mode 100644 server/internal/model/entity/t_resource_batch_download.go create mode 100644 server/internal/model/entity/t_sim_bind_package_logs.go create mode 100644 server/internal/model/entity/t_sim_card_bind_pkg.go create mode 100644 server/internal/model/entity/t_sim_card_cancellation.go create mode 100644 server/internal/model/entity/t_sim_card_info.go create mode 100644 server/internal/model/entity/t_sim_card_stock.go create mode 100644 server/internal/model/entity/t_sim_channel_template.go create mode 100644 server/internal/model/entity/t_sim_channels.go create mode 100644 server/internal/model/entity/t_sim_flow_package_info.go create mode 100644 server/internal/model/entity/t_sim_flow_package_price.go create mode 100644 server/internal/model/entity/t_sim_flow_pool.go create mode 100644 server/internal/model/entity/t_sim_package_category.go create mode 100644 server/internal/model/entity/t_sim_pool.go create mode 100644 server/internal/model/entity/t_sim_pool_bind_package.go create mode 100644 server/internal/model/entity/t_sim_pool_bind_pkg_logs.go create mode 100644 server/internal/model/entity/t_sim_pool_warn_config.go create mode 100644 server/internal/model/entity/t_sim_pool_warn_log.go create mode 100644 server/internal/model/entity/t_sim_terminal.go create mode 100644 server/internal/model/entity/t_sim_upstream_package.go create mode 100644 server/internal/model/entity/t_terminal.go create mode 100644 server/internal/model/entity/t_tplatform_setting.go create mode 100644 server/internal/model/entity/t_user_message.go create mode 100644 server/internal/model/entity/t_warn_config.go create mode 100644 server/internal/model/entity/t_warn_log.go create mode 100644 server/internal/model/entity/t_wo_communication.go create mode 100644 server/internal/model/entity/t_wo_main.go create mode 100644 server/internal/model/entity/t_wo_record.go create mode 100644 server/internal/model/entity/test_demo.go create mode 100644 server/internal/model/entity/test_tree.go create mode 100644 server/internal/model/genin/test_demo.go create mode 100644 server/internal/model/genin/test_tree.go create mode 100644 server/internal/model/info.go create mode 100644 server/internal/model/monitor_server.go create mode 100644 server/internal/model/mybytes.go create mode 100644 server/internal/model/qrcode.go create mode 100644 server/internal/model/request/common.go create mode 100644 server/internal/model/response/common.go create mode 100644 server/internal/model/sys_auth.go create mode 100644 server/internal/model/sys_client.go create mode 100644 server/internal/model/sys_config.go create mode 100644 server/internal/model/sys_dept.go create mode 100644 server/internal/model/sys_dict.go create mode 100644 server/internal/model/sys_gen_table.go create mode 100644 server/internal/model/sys_job.go create mode 100644 server/internal/model/sys_logininfor.go create mode 100644 server/internal/model/sys_menu.go create mode 100644 server/internal/model/sys_notice.go create mode 100644 server/internal/model/sys_notice_user.go create mode 100644 server/internal/model/sys_oper_log.go create mode 100644 server/internal/model/sys_oss.go create mode 100644 server/internal/model/sys_oss_config.go create mode 100644 server/internal/model/sys_post.go create mode 100644 server/internal/model/sys_role.go create mode 100644 server/internal/model/sys_social.go create mode 100644 server/internal/model/sys_tenant.go create mode 100644 server/internal/model/sys_tenant_package.go create mode 100644 server/internal/model/sys_user.go create mode 100644 server/internal/model/sys_user_online.go create mode 100644 server/internal/packed/packed.go create mode 100644 server/internal/packed/response/response.go create mode 100644 server/internal/queues/base.go create mode 100644 server/internal/queues/httpqueues/sys_opt_log_queue.go create mode 100644 server/internal/router/app.go create mode 100644 server/internal/router/common.go create mode 100644 server/internal/router/gen.go create mode 100644 server/internal/router/gen_codes.go create mode 100644 server/internal/router/genrouter/init.go create mode 100644 server/internal/router/genrouter/test_demo.go create mode 100644 server/internal/router/genrouter/test_tree.go create mode 100644 server/internal/router/monitor.go create mode 100644 server/internal/router/router.go create mode 100644 server/internal/router/system.go create mode 100644 server/internal/service/app.go create mode 100644 server/internal/service/app_diagnosis.go create mode 100644 server/internal/service/gen.go create mode 100644 server/internal/service/gen_codes.go create mode 100644 server/internal/service/info.go create mode 100644 server/internal/service/memorydb.go create mode 100644 server/internal/service/middleware.go create mode 100644 server/internal/service/monitor.go create mode 100644 server/internal/service/system.go create mode 100644 server/internal/tasks/base.go create mode 100644 server/internal/tasks/clear_data.go create mode 100644 server/internal/tasks/task_log.go create mode 100644 server/internal/tasks/tasks.go create mode 100644 server/main.go create mode 100644 server/resource/generate/default/curd/api.go.template create mode 100644 server/resource/generate/default/curd/controller.go.template create mode 100644 server/resource/generate/default/curd/input.go.template create mode 100644 server/resource/generate/default/curd/logic.go.template create mode 100644 server/resource/generate/default/curd/router.go.template create mode 100644 server/resource/generate/default/curd/source.sql.template create mode 100644 server/resource/generate/default/curd/web.api.ts.template create mode 100644 server/resource/generate/default/curd/web.edit.vue.template create mode 100644 server/resource/generate/default/curd/web.index.vue.template create mode 100644 server/resource/generate/default/curd/web.model.ts.template create mode 100644 server/resource/generate/default/curd/web.view.vue.template create mode 100644 server/resource/public/public.txt create mode 100644 server/storage/data/20260709_fix_t_sim_terminal_unique.sql create mode 100644 server/storage/data/xiu_admin.sql create mode 100644 server/utility/convert/convert.go create mode 100644 server/utility/excel/excel.go create mode 100644 server/utility/gfredis/conn.go create mode 100644 server/utility/gfredis/gfredis.go create mode 100644 server/utility/gfredis/hash.go create mode 100644 server/utility/gfredis/key.go create mode 100644 server/utility/gfredis/list.go create mode 100644 server/utility/gfredis/string.go create mode 100644 server/utility/gfredis/subpub.go create mode 100644 server/utility/gfredis/zset.go create mode 100644 server/utility/hash/crc16.go create mode 100644 server/utility/hash/sum16.go create mode 100644 server/utility/iot_excelize/batch_result.go create mode 100644 server/utility/iot_excelize/export_builder.go create mode 100644 server/utility/iot_excelize/import_reader.go create mode 100644 server/utility/iot_excelize/local_storage.go create mode 100644 server/utility/iot_excelize/schema.go create mode 100644 server/utility/iot_excelize/style.go create mode 100644 server/utility/iot_excelize/types.go create mode 100644 server/utility/iot_excelize/writer.go create mode 100644 server/utility/queue/memory_queue.go create mode 100644 server/utility/queue/queue.go create mode 100644 server/utility/queue/queue_test.go create mode 100644 server/utility/queue/redis_broadcast.go create mode 100644 server/utility/queue/redis_queue.go create mode 100644 server/utility/tree/tree.go create mode 100644 server/utility/tree/tree_node.go create mode 100644 server/utility/utility.go create mode 100644 server/utility/uuid32/uuid32.go create mode 100644 server/utility/version/version.go diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..33f3c22 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# 统一使用 LF,避免跨平台协作时产生整文件 diff +* text=auto eol=lf + +# 二进制文件不做行尾转换 +*.png binary +*.jpg binary +*.gif binary +*.ico binary +*.woff binary +*.woff2 binary +*.ttf binary +*.pdf binary +*.exe binary +*.xlsx binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e68b2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +*.local + +# Editor directories and files +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.hbuilderx + +.stylelintcache +.eslintcache + +/docs/ + +src/types +# 单独把这个文件排除掉,用以解决部分电脑生成的 auto-import.d.ts 的API不完整导致类型提示报错问题 +!src/types/auto-import.d.ts +src/manifest.json +src/pages.json + +# 2025-10-15 by 菲鸽: lock 文件还是需要加入版本管理,今天又遇到版本不一致导致无法运行的问题了。 +# pnpm-lock.yaml +# package-lock.json + +# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作 +# `git rm -r --cached .` 然后提交 commit 即可。 + +# git rm -r --cached file1 file2 ## 针对某些文件 +# git rm -r --cached dir1 dir2 ## 针对某些文件夹 +# git rm -r --cached . ## 针对所有文件 + +# 更新 uni-app 官方版本 +# npx @dcloudio/uvm@latest +*.log +*.exe +*.tar +*.zip +.vscode +.cursor + +# Local upload files generated by the backend. +server/resource/upload/ +server/resource/certs/* +!server/resource/certs/ +server/upload/ +server/.env +server/xiujie-wechat/ + +# Deployment host runtime configuration. +deploy/.env + +# Local tooling / preview artifacts (not part of source) +.workbuddy/ +app/.card-dom.html +app/.preview-*.png +app/_tmp_* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..eeeca82 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,94 @@ +# 项目约束规范 +## xiuAdmin框架说明文档 + 先阅读 http://xiuadmin.com/sys/catalog.html +## 范围界定 +1. 本仓库包含两部分代码: + - 后端:基于 GoFrame v2 框架,存放于 `server/` 目录 + - 前端:Vue + Vben Admin 技术栈,存放于 `app/` 目录 +2. GoFrame 项目专属规范仅适用于 `server/` 下后端开发,或用户明确要求进行 GoFrame 开发的场景。 +3. 仅修改前端代码时,不得套用 GoFrame 后端开发规范。 +4. `vue/` 目录下前端开发需遵循 Vben Admin 5.x 开发规范,同步沿用 `vue/apps/web-antd` 中已落地的 Ant Design Vue 页面风格。 + +## GoFrame 后端开发规范 +1. 控制器、服务、中间件、路由、配置、ORM、自动生成代码均严格遵循 GoFrame v2 官方编码惯例。 +2. 禁止手动编辑自动生成的 DAO、DO、Entity 文件,统一使用项目内置代码生成流程更新。 +3. 新增逻辑前优先复用项目现有服务、工具函数与通用设计模式,避免重复新增抽象层。 +4. 数据库新增、更新操作必须使用自动生成的 DAO/DO 结构体;禁止将 `g.Map`、`map[string]interface{}` 作为 ORM 数据载体。 +5. 后端新增业务报错统一使用 `gerror` 组件封装并返回错误信息。 +6. 常规 ORM 操作交由 GoFrame 自动维护 `created_at`、`updated_at`、`deleted_at` 字段;逻辑删除统一调用 `Delete()` 方法。 + + +## 前端开发规范 +1. 必须使用中文进行对话以及编写文档。 +2. 编写代码、修改文档或做业务判断前,必须先查询是否有可使用的本地 SKILL、项目文档、知识库、已有实现或现成工具,再决定行动方案。 +3. 如果上下文过长、信息开始混乱或可能遗忘已确认内容,应提醒用户:建议压缩上下文。 +4. 用户指定修改某个部分时,只处理指定范围;未经用户同意,不修改其他未指定部分。 +5. 不要覆盖用户或其他工具已经产生的改动。若目标文件已有人工改动,应先理解并兼容现状。 +6. 编写界面前必须先确认 Wot UI、uni-app 内置组件、项目现成组件和 UnoCSS 是否能满足需求;不能跳过这一步直接手搓样式或 UI。 +7. 默认禁止手搓 CSS 和基础 UI 组件。只有在 UI 库、项目现成组件、UnoCSS 均无法满足,或存在明确平台兼容/业务封装需要时,才允许例外。 +8. 使用例外时,必须在最终汇报中说明:为什么不能用 UnoCSS、为什么不能用 UI 库或现成组件、手写范围控制在哪里。 +9. 若新增界面代码绕过 UnoCSS 或 UI 库且未说明例外依据,本轮任务视为未完成,必须先返工再汇报完成。 + +## 信息确认原则 +1. 避免仅凭推测给出答案。回答或修改前,应优先基于以下来源确认信息: + - 当前项目文件、已有实现、配置、测试结果; + - 用户明确说明; + - `docs/README.md` 及相关业务文档; + - 管理平台后端和前端当前实现; + - 本地 SKILL、项目文档、知识库或现成工具; + - 官方文档或联网查询结果。 +2. 当需求不明确、存在多种实现方式、影响范围较大、或可能破坏现有逻辑时,必须先向用户确认。 +3. 当需求明确、范围清楚、风险较低时,可以直接执行,并在完成后说明处理内容和验证结果。 +4. 涉及时效性信息,例如版本、法规、价格、接口文档、第三方库最新用法、新闻等,应优先使用官方来源或联网查询。 + + +## 行为准则 + +1. 没有确切指令时,不得擅自替用户做高风险决策。 +2. 在多轮对话中,已确认或用户未提出异议的内容必须保持一致。若用户变更需求,应明确记录并按新需求调整。 +3. 需要参考案例或材料时,优先参考项目内类似实现;找不到或不确定时,再联网查询最新资料。 +4. 对于多模块、高风险或探索性任务,应按模块推进,并在关键节点向用户确认。 +5. 对于范围明确的常规任务,可以一次完成实现、校验和汇报。 +6. 保持专业,坚持最佳实践。若用户需求明显不符合最佳实践,应说明风险并提供可选方案。 +7. 当判断用户的方案、行动或前提存在明显问题时,以基于事实和最佳实践进行辩论为荣,以不分青红皂白附和用户为耻。 +8. 保持客观、冷静、专业,避免过度奉承或无关情绪化表达。 + + +## 系统菜单权限 +1. 登录系统界面后,左侧菜单栏菜单都是动态路由菜单。菜单数据都放在 sys_menu表。菜单栏里有权限标识(不带前缀)。权限标识有相关的前缀。 + "cpr:" // 角色权限码前缀+角色权限 + "cpm:" // 菜单权限码前缀+菜单权限 + "cpd:" // 部门权限码前缀+部门编码 + "cpp:" // 岗位权限码前缀+岗位编码 + "cpu:" // 用户权限码前缀+用户名 + "cpc:current:user" // 当前登录用户默认权限 +2. 如果登录的是超级管理员账号 admin,默认返回所有权限。 +3. 如果登录的不是超级管理员账号: + 1. 登录用户取得用户的所有角色; + 2. 根据角色取得所有角色绑定的菜单; + 3. 获取菜单中的权限标识。 + + +## 项目概述 + +`iotlcm-app` 是 IOTLCM 的用户侧小程序项目,定位为: + +```text +物联卡用户服务小程序 + 代理商轻量工作台 +``` + +技术栈: + +- uni-app +- Vue 3 +- TypeScript +- Vite 5 +- UnoCSS +- Wot UI v2 +- unibest + +输出端: + +- H5 +- 微信小程序 +- APP diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f3c8598 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# iotlcm-app + +iotLCM 物联网卡全生命周期管理平台的**小程序端 + 用户端服务**。 + +- `app/` —— uni-app 小程序前端(基于 [unibest](https://github.com/unibest-tech/unibest),MIT) +- `server/` —— 用户端 GoFrame 服务(基于 XiuAdmin 框架,Apache-2.0) + +配套管理后台:[iotlcm-admin-svr](https://git.5iot.com/iotlcm-public/iotlcm-admin-svr) / [iotlcm-admin-ui](https://git.5iot.com/iotlcm-public/iotlcm-admin-ui) + +## 特性 + +**小程序端**(`app/`) + +- 公开查卡:输入卡号即可查状态、流量、到期时间 +- 卡片详情与用量曲线(日/月分库) +- 一键诊断:拍卡号或手输,问题排查直达 +- 账号体系:组织下个人账号、角色权限 + +**用户端服务**(`server/`) + +- 小程序端的业务后端:卡映射、诊断、订单、余额、消息、工单 +- 独立于管理后台的用户域数据与权限 +- 对接上游通道完成真实查卡与诊断 + +## 技术栈 + +- 前端:uni-app + Vue 3 + TypeScript + Vite 5 + UnoCSS + wot-ui + z-paging +- 后端:Go 1.25 + GoFrame v2 + MySQL + Redis + +## 快速开始 + +### 后端 + +```bash +cd server +go mod tidy +go run . +``` + +默认监听 `:8199`。 + +### 小程序前端 + +```bash +cd app +pnpm install +pnpm dev:mp-weixin # 微信小程序,产物用微信开发者工具打开 dist/dev/mp-weixin +``` + +## 配置 + +| 位置 | 说明 | +| --- | --- | +| `server/config/config.yaml` | 后端配置(**公开版凭据为占位符**) | +| `app/env/.env*` | 前端环境变量(**公开版 AppID 为占位符**) | + +后端配置占位: + +| 配置项 | 占位 | +| --- | --- | +| `database.default.link` | `mysql:${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)` | +| `redis.default.address` / `pass` | `${REDIS_ADDR}` / `${REDIS_PASSWORD}` | +| `jwt.signingKey` | `${JWT_SIGNING_KEY}` | + +前端配置占位: + +| 变量 | 占位 | +| --- | --- | +| `VITE_WX_APPID` | `YOUR_WX_APPID` | +| `VITE_UNI_APPID` | `__UNI__YOURAPPID` | +| `VITE_MP_SERVER_BASEURL` | `https://admin.iotlcm.example.com/app/api/v1` | + +## 目录结构 + +```text +iotlcm-app/ +├── app/ # uni-app 小程序前端 +│ ├── src/pages/ # 页面(首页、查卡、诊断、登录等) +│ ├── src/constant/ # 常量与路由定义 +│ ├── env/ # 环境变量 +│ └── src/static/logo.svg # 应用图标 +└── server/ # GoFrame 用户端服务 + ├── api/ # API 契约(app / common / gen) + ├── internal/ + │ ├── logic/app/ # 用户端业务逻辑 + │ ├── model/ consts/ # 模型与常量 + │ └── utility/ # 工具(含启动 banner) + ├── config/ # 配置 + └── storage/data/ # 框架基座初始化数据 +``` + +## 初始化数据 + +`server/storage/data/xiu_admin.sql` 是框架基座初始化数据(系统菜单、字典、角色、示例账号),是运行起点的必要输入。 + +> **数据库 schema 不随本仓发布** —— 生产库的完整 DDL 已过时,请按需自建表结构。 + +## 上游与许可 + +| 部分 | 上游 | 许可 | +| --- | --- | --- | +| `app/` | [unibest](https://github.com/unibest-tech/unibest) | MIT(见 [`app/LICENSE`](./app/LICENSE)) | +| `server/` | XiuAdmin(GoFrame 中后台框架基座) | Apache-2.0(见 [`server/LICENSE`](./server/LICENSE)) | + +## License + +根目录 [LICENSE](./LICENSE) 为 Apache-2.0。 diff --git a/app/.commitlintrc.cjs b/app/.commitlintrc.cjs new file mode 100644 index 0000000..98ee7df --- /dev/null +++ b/app/.commitlintrc.cjs @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], +} diff --git a/app/.dockerignore b/app/.dockerignore new file mode 100644 index 0000000..a67db47 --- /dev/null +++ b/app/.dockerignore @@ -0,0 +1,11 @@ +.git +.idea +.hbuilderx +node_modules/ +dist/ +unpackage/ +.vite/ +.stylelintcache +.eslintcache +*.log +*.local diff --git a/app/.editorconfig b/app/.editorconfig new file mode 100644 index 0000000..7f09864 --- /dev/null +++ b/app/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] # 表示所有文件适用 +charset = utf-8 # 设置文件字符集为 utf-8 +indent_style = space # 缩进风格(tab | space) +indent_size = 2 # 缩进大小 +end_of_line = lf # 控制换行类型(lf | cr | crlf) +trim_trailing_whitespace = true # 去除行首的任意空白字符 +insert_final_newline = true # 始终在文件末尾插入一个新行 + +[*.md] # 表示仅 md 文件适用以下规则 +max_line_length = off # 关闭最大行长度限制 +trim_trailing_whitespace = false # 关闭末尾空格修剪 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..6a8cecc --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,6 @@ +node_modules +private.*.key +private.key + +# Local Claude API credentials +scripts/claude-instances.local.ps1 diff --git a/app/.npmrc b/app/.npmrc new file mode 100644 index 0000000..f47ca59 --- /dev/null +++ b/app/.npmrc @@ -0,0 +1,9 @@ +# registry = https://registry.npmjs.org +registry = https://registry.npmmirror.com + +strict-peer-dependencies=false +auto-install-peers=true +shamefully-hoist=true +ignore-workspace-root-check=true +install-workspace-root=true +node-options=--max-old-space-size=8192 diff --git a/app/.prettierignore b/app/.prettierignore new file mode 100644 index 0000000..33fc9db --- /dev/null +++ b/app/.prettierignore @@ -0,0 +1,9 @@ +dist +node_modules +src/pages.json +src/manifest.json +src/service +auto-import.d.ts +uni-pages.d.ts +src/uni_modules +scripts diff --git a/app/.prettierrc.json b/app/.prettierrc.json new file mode 100644 index 0000000..0965e7e --- /dev/null +++ b/app/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..836bec5 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,129 @@ +# Build context: app/ +# Example: +# docker build -f Dockerfile -t iotlcm-app-web:latest . + +FROM crpi-svwhn8x8nvask31h.cn-beijing.personal.cr.aliyuncs.com/xjzlpublic/node:24-alpine AS builder + +WORKDIR /build/app + +COPY package.json pnpm-lock.yaml .npmrc ./ + +RUN npm config set registry https://registry.npmmirror.com/ && \ + npm install -g pnpm@10.10.0 --registry=https://registry.npmmirror.com/ && \ + pnpm config set registry https://registry.npmmirror.com/ && \ + pnpm install --frozen-lockfile --ignore-scripts + +COPY index.html vite.config.ts manifest.config.ts pages.config.ts tsconfig.json ./ +COPY uno.config.ts wot-ui-resolver.ts openapi-ts-request.config.ts ./ +COPY folder-alias.json favicon.ico ./ +COPY env ./env +COPY public ./public +COPY scripts ./scripts +COPY src ./src +COPY vite-plugins ./vite-plugins + +ARG VITE_SERVER_BASEURL=/app/api/v1 +ARG VITE_MP_SERVER_BASEURL=http://yznw2.xiujie.cn:8010/api/v1 +ARG VITE_APP_PUBLIC_BASE=/app/ + +RUN printf '%s\n' \ + "VITE_SERVER_BASEURL='${VITE_SERVER_BASEURL}'" \ + "VITE_MP_SERVER_BASEURL='${VITE_MP_SERVER_BASEURL}'" \ + "VITE_APP_PUBLIC_BASE='${VITE_APP_PUBLIC_BASE}'" \ + > env/.env.production.local + +RUN pnpm init-baseFiles +RUN CI=true pnpm build:h5:prod + +FROM crpi-svwhn8x8nvask31h.cn-beijing.personal.cr.aliyuncs.com/xjzlpublic/nginx:1.30.0-alpine AS runtime + +COPY --from=builder /build/app/dist/build/h5 /usr/share/nginx/html + +RUN printf '%s\n' \ + 'server {' \ + ' listen 80;' \ + ' server_name _;' \ + ' root /usr/share/nginx/html;' \ + ' index index.html;' \ + ' resolver 127.0.0.11 ipv6=off valid=30s;' \ + ' set $app_backend http://iotlcm-app-server:8010;' \ + '' \ + ' client_max_body_size 200m;' \ + '' \ + ' location = /app {' \ + ' return 301 /app/;' \ + ' }' \ + '' \ + ' location ^~ /app/api/ {' \ + ' rewrite ^/app/(.*)$ /$1 break;' \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Host $host;' \ + ' proxy_set_header X-Real-IP $remote_addr;' \ + ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ + ' proxy_set_header X-Forwarded-Proto $scheme;' \ + ' proxy_pass $app_backend;' \ + ' }' \ + '' \ + ' location ^~ /app/upload/ {' \ + ' rewrite ^/app/(.*)$ /$1 break;' \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Host $host;' \ + ' proxy_set_header X-Real-IP $remote_addr;' \ + ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ + ' proxy_set_header X-Forwarded-Proto $scheme;' \ + ' proxy_pass $app_backend;' \ + ' }' \ + '' \ + ' location ^~ /app/assets/ {' \ + ' rewrite ^/app/(.*)$ /$1 break;' \ + ' expires 30d;' \ + ' add_header Cache-Control "public, immutable";' \ + ' try_files $uri =404;' \ + ' }' \ + '' \ + ' location ^~ /app/ {' \ + ' rewrite ^/app/(.*)$ /$1 break;' \ + ' try_files $uri $uri/ /index.html;' \ + ' }' \ + '' \ + ' gzip on;' \ + ' gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml;' \ + '' \ + ' location / {' \ + ' try_files $uri $uri/ /index.html;' \ + ' }' \ + '' \ + ' location = /favicon.ico {' \ + ' alias /usr/share/nginx/html/static/images/shandong-yunze-logo.png;' \ + ' add_header Cache-Control "no-cache";' \ + ' }' \ + '' \ + ' location /api/ {' \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Host $host;' \ + ' proxy_set_header X-Real-IP $remote_addr;' \ + ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ + ' proxy_set_header X-Forwarded-Proto $scheme;' \ + ' proxy_pass $app_backend;' \ + ' }' \ + '' \ + ' location /upload/ {' \ + ' proxy_http_version 1.1;' \ + ' proxy_set_header Host $host;' \ + ' proxy_set_header X-Real-IP $remote_addr;' \ + ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \ + ' proxy_set_header X-Forwarded-Proto $scheme;' \ + ' proxy_pass $app_backend;' \ + ' }' \ + '' \ + ' location /assets/ {' \ + ' expires 30d;' \ + ' add_header Cache-Control "public, immutable";' \ + ' try_files $uri =404;' \ + ' }' \ + '}' \ + > /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/app/LICENSE b/app/LICENSE new file mode 100644 index 0000000..9e91d10 --- /dev/null +++ b/app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 菲鸽 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..d609221 --- /dev/null +++ b/app/README.md @@ -0,0 +1,66 @@ +

+ +

+ +

iotlcm-app-ui

+ +
+ +iotLCM 物联网卡全生命周期管理平台的**小程序端**。 + +基于 [unibest](https://github.com/unibest-tech/unibest)(MIT License)二次开发。 + +[![node version](https://img.shields.io/badge/node-%3E%3D18-green)](https://nodejs.org) + +
+ +## 技术栈 + +`uniapp` + `Vue3` + `TypeScript` + `Vite5` + `UnoCSS` + `wot-ui` + `z-paging` + +## 本地开发 + +```bash +pnpm install +pnpm dev:mp-weixin # 微信小程序 +pnpm dev:h5 # H5 +``` + +## 构建 + +```bash +pnpm build:mp-weixin # 微信小程序(产物在 dist/build/mp-weixin) +pnpm build:h5 +``` + +## 配置 + +环境变量文件在 `env/`: + +| 文件 | 用途 | +| --- | --- | +| `.env` | 通用配置(应用标题、端口、uni-app AppID、微信 AppID、公开路径) | +| `.env.development` | 开发环境(后端地址) | +| `.env.production` | 生产环境 | +| `.env.test` | 测试环境 | + +**公开版里的标识是占位符**: + +| 变量 | 占位 | 说明 | +| --- | --- | --- | +| `VITE_WX_APPID` | `YOUR_WX_APPID` | 微信小程序 AppID | +| `VITE_UNI_APPID` | `__UNI__YOURAPPID` | uni-app 应用标识 | +| `VITE_SERVER_BASEURL` | `http://localhost:8010/api/v1` | 后端地址 | +| `VITE_MP_SERVER_BASEURL` | `https://admin.iotlcm.example.com/app/api/v1` | 生产后端地址 | + +## 上游与许可 + +本项目基于 **[unibest](https://github.com/unibest-tech/unibest)**(MIT License)二次开发。 + +- 上游版权声明保留在 [`LICENSE`](./LICENSE) +- iotLCM 相关的业务页面、路由、配置为本项目新增 +- 上游仓库:[unibest-tech/unibest](https://github.com/unibest-tech/unibest) + +## License + +[MIT](./LICENSE) diff --git a/app/env/.env b/app/env/.env new file mode 100644 index 0000000..f353265 --- /dev/null +++ b/app/env/.env @@ -0,0 +1,31 @@ +VITE_APP_TITLE = '山东平台物联卡' +VITE_APP_PORT = 9000 + +VITE_UNI_APPID = '__UNI__YOURAPPID' +VITE_WX_APPID = 'YOUR_WX_APPID' + +# 微信开发者工具 CLI 路径,仅当默认安装路径不正确时配置(就是当 pnpm dev:mp 无法自动打开微信开发者工具时,才需要配置,通常是你更改了默认的安装位置导致的,一般出现在windows系统) +# macOS 示例: +# WECHAT_DEVTOOLS_CLI_PATH = '/Applications/wechatwebdevtools.app/Contents/MacOS/cli' +# Windows 示例: +# WECHAT_DEVTOOLS_CLI_PATH = 'C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat' + +# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base +# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router +# 比如你要部署到 https://unibest.tech/doc/ ,则配置为 /doc/ +VITE_APP_PUBLIC_BASE=/ + +# 默认后台请求地址 +# 不同命令会按 mode 叠加读取 .env.development / .env.test / .env.production。 +VITE_SERVER_BASEURL = 'http://localhost:8010/api/v1' +# 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api + +# 微信小程序专用后台请求地址,构建后会直接请求这个地址。 +# 不配置时会回退使用 VITE_SERVER_BASEURL。 +VITE_MP_SERVER_BASEURL = 'https://admin.iotlcm.example.com/app/api/v1' + +# 认证模式,'single' | 'double' ==> 单token | 双token +VITE_AUTH_MODE = 'single' + +# 原生插件资源复制开关,启用后 App 构建会把根目录 nativeplugins 复制到 dist +VITE_COPY_NATIVE_RES_ENABLE = true diff --git a/app/env/.env.development b/app/env/.env.development new file mode 100644 index 0000000..80a112a --- /dev/null +++ b/app/env/.env.development @@ -0,0 +1,10 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 +NODE_ENV = 'development' +# 是否去除console 和 debugger +VITE_DELETE_CONSOLE = false +# 是否开启sourcemap +VITE_SHOW_SOURCEMAP = false + +# development mode 后台请求地址 +VITE_SERVER_BASEURL = 'http://localhost:8010/api/v1' +VITE_MP_SERVER_BASEURL = 'https://admin.iotlcm.example.com/app/api/v1' diff --git a/app/env/.env.production b/app/env/.env.production new file mode 100644 index 0000000..493de00 --- /dev/null +++ b/app/env/.env.production @@ -0,0 +1,11 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 +NODE_ENV = 'production' +# 是否去除console 和 debugger +VITE_DELETE_CONSOLE = true +# 是否开启sourcemap +VITE_SHOW_SOURCEMAP = false + +# production mode 后台请求地址 +VITE_APP_PUBLIC_BASE = '/app/' +VITE_SERVER_BASEURL = '/app/api/v1' +VITE_MP_SERVER_BASEURL = 'https://admin.iotlcm.example.com/app/api/v1' diff --git a/app/env/.env.test b/app/env/.env.test new file mode 100644 index 0000000..a2627df --- /dev/null +++ b/app/env/.env.test @@ -0,0 +1,10 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 +NODE_ENV = 'development' +# 是否去除console 和 debugger +VITE_DELETE_CONSOLE = false +# 是否开启sourcemap +VITE_SHOW_SOURCEMAP = false + +# test mode 后台请求地址 +VITE_SERVER_BASEURL = 'http://localhost:8010/api/v1' +VITE_MP_SERVER_BASEURL = 'https://admin.iotlcm.example.com/app/api/v1' diff --git a/app/eslint.config.mjs b/app/eslint.config.mjs new file mode 100644 index 0000000..426985a --- /dev/null +++ b/app/eslint.config.mjs @@ -0,0 +1,60 @@ +import uniHelper from '@uni-helper/eslint-config' + +export default uniHelper({ + unocss: true, + vue: true, + markdown: false, + ignores: [ + // 忽略uni_modules目录 + '**/uni_modules/', + // 忽略原生插件目录 + '**/nativeplugins/', + 'dist', + // unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用 + 'auto-import.d.ts', + // vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore + 'uni-pages.d.ts', + // 插件生成的文件 + 'src/pages.json', + 'src/manifest.json', + // 忽略自动生成文件 + 'src/service/**', + ], + // https://eslint-config.antfu.me/rules + rules: { + 'no-useless-return': 'off', + 'no-console': 'off', + 'no-unused-vars': 'off', + 'vue/no-unused-refs': 'off', + 'unused-imports/no-unused-vars': 'off', + 'eslint-comments/no-unlimited-disable': 'off', + 'jsdoc/check-param-names': 'off', + 'jsdoc/require-returns-description': 'off', + 'ts/no-empty-object-type': 'off', + 'no-extend-native': 'off', + // uni 条件编译注释可能包裹 import,自动排序会破坏平台条件边界 + 'perfectionist/sort-imports': 'off', + 'vue/singleline-html-element-content-newline': [ + 'error', + { + externalIgnores: ['text'], + }, + ], + // vue SFC 调换顺序改这里 + 'vue/block-order': ['error', { + order: [['script', 'template'], 'style'], + }], + }, + formatters: { + /** + * Format CSS, LESS, SCSS files, also the ` diff --git a/app/src/App.vue b/app/src/App.vue new file mode 100644 index 0000000..5586e39 --- /dev/null +++ b/app/src/App.vue @@ -0,0 +1,49 @@ + + + diff --git a/app/src/api/balance.ts b/app/src/api/balance.ts new file mode 100644 index 0000000..b391bec --- /dev/null +++ b/app/src/api/balance.ts @@ -0,0 +1,61 @@ +import type { + BalanceRechargeMockPayParams, + BalanceRechargeRefundParams, + BalanceRechargeStatusParams, + BalanceRechargeStatusRes, + BalanceRechargeSubmitParams, + BalanceRechargeSubmitRes, +} from './types/balance' +import type { AppOrder } from './types/order' +import { http } from '@/http/http' + +export type { + BalanceRechargeMockPayParams, + BalanceRechargeRefundParams, + BalanceRechargeStatusParams, + BalanceRechargeStatusRes, + BalanceRechargeSubmitParams, + BalanceRechargeSubmitRes, +} from './types/balance' + +export function balanceRechargeSubmit(data: BalanceRechargeSubmitParams) { + return http.post( + '/balance/recharge-submit', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} + +export function balanceRechargeStatus(data: BalanceRechargeStatusParams) { + return http.get('/balance/recharge-status', data, undefined, { + hideErrorToast: true, + }) +} + +export function balanceRechargeMockPay(data: BalanceRechargeMockPayParams) { + return http.post( + '/balance/recharge-mock-pay', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} + +export function balanceRechargeRefund(data: BalanceRechargeRefundParams) { + return http.post( + '/balance/recharge-refund', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} diff --git a/app/src/api/card.ts b/app/src/api/card.ts new file mode 100644 index 0000000..3965e2b --- /dev/null +++ b/app/src/api/card.ts @@ -0,0 +1,203 @@ +import type { + AllocateBatchPrepareParams, + AllocateBatchRes, + AllocateBatchSubmitParams, + AllocateTargetListParams, + AllocateTargetListRes, + BindCardParams, + CardLifecycleParams, + CardLifecycleRes, + CardLifecycleStatusParams, + CardListParams, + CardListRes, + CardRecognitionRes, + CardQueryRes, + ChangePrepareParams, + ChangePrepareRes, + ChangeSubmitParams, + ChangeSubmitRes, + PackageDetailRes, + PackageListParams, + PackageListRes, + PublicPackageDetailParams, + PublicQueryCardParams, + RenewBatchPrepareParams, + RenewBatchSubmitParams, + RenewPrepareParams, + RenewPrepareRes, + RenewSubmitParams, + RenewSubmitRes, + RestartCardParams, + RestartCardRes, +} from './types/card' +import { http } from '@/http/http' +import { getEnvBaseUrl } from '@/utils/index' + +const CARD_REQUEST_OPTIONS = { + timeout: 15000, + hideErrorToast: true, +} as const + +export type { + AllocateBatchRes, + AllocateTarget, + AppCard, + AppPackage, + AppRenewOption, + CardLifecycleRes, + ChangePackageOption, + ChangePrepareRes, + ChangeSubmitRes, + RenewPackageOption, + RenewPrepareRes, + RenewSubmitRes, +} from './types/card' + +export function publicQueryCard(data: PublicQueryCardParams) { + return http.post('/card/public-query', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} + +export function recognizeCardImage(filePath: string) { + return new Promise((resolve, reject) => { + uni.uploadFile({ + url: `${getEnvBaseUrl()}/card/recognize`, + filePath, + name: 'file', + timeout: 30000, + success: (response) => { + let payload: { code?: number, message?: string, data?: CardRecognitionRes } + try { + payload = JSON.parse(String(response.data || '{}')) + } + catch { + reject({ message: '图片识别响应格式错误' }) + return + } + + if (response.statusCode < 200 || response.statusCode >= 300 || Number(payload.code) !== 0) { + reject({ message: payload.message || '图片识别失败' }) + return + } + resolve(payload.data || { + code: 'RECOGNITION_FAILED', + message: '图片识别结果为空', + cardType: 'iccid', + cardValue: '', + confidence: 0, + candidates: [], + }) + }, + fail: (error) => { + reject({ message: error.errMsg || '图片上传失败,请检查网络后重试' }) + }, + }) + }) +} + +export function getCardList(params: CardListParams) { + return http.get('/card/list', params, undefined, CARD_REQUEST_OPTIONS) +} + +export function getPackageList(params: PackageListParams) { + return http.get('/card/package-list', params, undefined, CARD_REQUEST_OPTIONS) +} + +export function publicPackageDetail(data: PublicPackageDetailParams) { + return http.post( + '/card/public-package-detail', + data, + undefined, + undefined, + CARD_REQUEST_OPTIONS, + ) +} + +export function getCardDetail(cardValue: string) { + return http.get('/card/detail', { cardValue }, undefined, CARD_REQUEST_OPTIONS) +} + +export function bindCard(data: BindCardParams) { + return http.post('/card/bind', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} + +export function restartCard(data: RestartCardParams) { + return http.post('/card/restart', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} + +export function stopCard(data: RestartCardParams) { + return http.post('/card/stop', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} + +export function startCard(data: RestartCardParams) { + return http.post('/card/start', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} + +export function startDataService(data: RestartCardParams) { + return lifecycleBatchSubmit({ cardValue: data.cardValue, operation: 'dataStart', remark: data.remark }) +} + +export function stopDataService(data: RestartCardParams) { + return lifecycleBatchSubmit({ cardValue: data.cardValue, operation: 'dataStop', remark: data.remark }) +} + +export function lifecycleBatchSubmit(data: CardLifecycleParams) { + return http.post( + '/card/lifecycle-batch-submit', + data, + undefined, + undefined, + CARD_REQUEST_OPTIONS, + ) +} + +export function lifecycleBatchStatus(data: CardLifecycleStatusParams) { + return http.get('/card/lifecycle-batch-status', data, undefined, CARD_REQUEST_OPTIONS) +} + +export function renewPrepare(data: RenewPrepareParams) { + return http.post('/card/renew-prepare', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} + +export function renewBatchPrepare(data: RenewBatchPrepareParams) { + return http.post('/card/renew-batch-prepare', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} + +export function renewSubmit(data: RenewSubmitParams) { + return http.post('/card/renew-submit', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} + +export function renewBatchSubmit(data: RenewBatchSubmitParams) { + return http.post('/card/renew-batch-submit', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} + +export function getAllocateTargets(params: AllocateTargetListParams) { + return http.get('/card/allocate-targets', params, undefined, CARD_REQUEST_OPTIONS) +} + +export function allocateBatchPrepare(data: AllocateBatchPrepareParams) { + return http.post( + '/card/allocate-batch-prepare', + data, + undefined, + undefined, + CARD_REQUEST_OPTIONS, + ) +} + +export function allocateBatchSubmit(data: AllocateBatchSubmitParams) { + return http.post( + '/card/allocate-batch-submit', + data, + undefined, + undefined, + CARD_REQUEST_OPTIONS, + ) +} + +export function changePrepare(data: ChangePrepareParams) { + return http.post('/card/change-prepare', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} + +export function changeSubmit(data: ChangeSubmitParams) { + return http.post('/card/change-submit', data, undefined, undefined, CARD_REQUEST_OPTIONS) +} diff --git a/app/src/api/customer.ts b/app/src/api/customer.ts new file mode 100644 index 0000000..8b751fd --- /dev/null +++ b/app/src/api/customer.ts @@ -0,0 +1,27 @@ +import type { + CustomerDetailParams, + CustomerDetailRes, + CustomerListParams, + CustomerListRes, +} from './types/customer' +import { http } from '@/http/http' + +export type { + AppCustomer, + AppCustomerDetail, + AppCustomerType, + CustomerDetailParams, + CustomerListParams, +} from './types/customer' + +export function getCustomerList(params: CustomerListParams) { + return http.get('/customer/list', params, undefined, { + hideErrorToast: true, + }) +} + +export function getCustomerDetail(params: CustomerDetailParams) { + return http.get('/customer/detail', params, undefined, { + hideErrorToast: true, + }) +} diff --git a/app/src/api/diagnosis.ts b/app/src/api/diagnosis.ts new file mode 100644 index 0000000..0247c06 --- /dev/null +++ b/app/src/api/diagnosis.ts @@ -0,0 +1,44 @@ +import type { + DiagnosisFlowParams, + DiagnosisFlowRes, + DiagnosisInfoParams, + DiagnosisInfoRes, +} from './types/diagnosis' +import { http } from '@/http/http' + +export type { + DiagnosisFlowParams, + DiagnosisFlowRes, + DiagnosisInfoItem, + DiagnosisInfoParams, + DiagnosisInfoRes, + DiagnosisModuleItem, + DiagnosisPackageItem, + DiagnosisRateLimitApnItem, + DiagnosisRateLimitPccItem, + DiagnosisSessionItem, +} from './types/diagnosis' + +export function getDiagnosisInfo(data: DiagnosisInfoParams) { + return http.post('/diagnosis/info', data, undefined, undefined, { + hideErrorToast: true, + }) +} + +export function getOpenDiagnosisInfo(data: DiagnosisInfoParams & { tenantId?: string }) { + return http.post('/open/diagnosis/info', data, undefined, undefined, { + hideErrorToast: true, + }) +} + +export function getDiagnosisFlow(data: DiagnosisFlowParams) { + return http.post('/open/diagnosis/flow', data, undefined, undefined, { + hideErrorToast: true, + }) +} + +export function getOpenDiagnosisFlow(data: DiagnosisFlowParams & { tenantId?: string }) { + return http.post('/open/diagnosis/flow', data, undefined, undefined, { + hideErrorToast: true, + }) +} diff --git a/app/src/api/foo-alova.ts b/app/src/api/foo-alova.ts new file mode 100644 index 0000000..bd5ca7f --- /dev/null +++ b/app/src/api/foo-alova.ts @@ -0,0 +1,16 @@ +import { http } from '@/http/alova' + +export interface IFoo { + id: number + name: string +} + +export function foo() { + return http.Get('/foo', { + params: { + name: '菲鸽', + page: 1, + pageSize: 10, + }, + }) +} diff --git a/app/src/api/foo.ts b/app/src/api/foo.ts new file mode 100644 index 0000000..a500002 --- /dev/null +++ b/app/src/api/foo.ts @@ -0,0 +1,43 @@ +import { http } from '@/http/http' + +export interface IFoo { + id: number + name: string +} + +export function foo() { + return http.Get('/foo', { + params: { + name: '菲鸽', + page: 1, + pageSize: 10, + }, + }) +} + +export interface IFooItem { + id: string + name: string +} + +/** GET 请求 */ +export async function getFooAPI(name: string) { + return await http.get('/foo', { name }) +} +/** GET 请求;支持 传递 header 的范例 */ +export function getFooAPI2(name: string) { + return http.get('/foo', { name }, { 'Content-Type-100': '100' }) +} + +/** POST 请求 */ +export function postFooAPI(name: string) { + return http.post('/foo', { name }) +} +/** POST 请求;需要传递 query 参数的范例;微信小程序经常有同时需要query参数和body参数的场景 */ +export function postFooAPI2(name: string) { + return http.post('/foo', { name }, { a: 1, b: 2 }) +} +/** POST 请求;支持 传递 header 的范例 */ +export function postFooAPI3(name: string) { + return http.post('/foo', { name }, { a: 1, b: 2 }, { 'Content-Type-100': '100' }) +} diff --git a/app/src/api/login.ts b/app/src/api/login.ts new file mode 100644 index 0000000..b9615a8 --- /dev/null +++ b/app/src/api/login.ts @@ -0,0 +1,152 @@ +import type { + IAuthLoginRes, + ICheckSideCaptchaForm, + IForgotPasswordForm, + ILoginForm, + IRegisterForm, + ISendSmsForm, + ISendSmsRes, + ISideCaptcha, + IUpdateInfo, + IUserInfoRes, + IWechatBindForm, + IWechatBindPhoneForm, + IWechatBindPhoneRes, + IWechatBindRes, + IWechatLoginForm, + IWechatLoginRes, +} from './types/login' +import { http } from '@/http/http' + +export type { IForgotPasswordForm, ILoginForm, IRegisterForm } + +const AUTH_REQUEST_OPTIONS = { + timeout: 15000, + hideErrorToast: true, +} as const + +export function getSideCaptcha() { + return http.post( + '/auth/side-captcha', + undefined, + undefined, + undefined, + AUTH_REQUEST_OPTIONS, + ) +} + +export function checkSideCaptcha(data: ICheckSideCaptchaForm) { + return http.post<{ success: boolean }>( + '/auth/check-side-captcha', + data, + undefined, + undefined, + AUTH_REQUEST_OPTIONS, + ) +} + +export function sendSms(data: ISendSmsForm) { + return http.post('/auth/sms', data, undefined, undefined, AUTH_REQUEST_OPTIONS) +} + +export function login(loginForm: ILoginForm) { + return http.post( + '/auth/login', + loginForm, + undefined, + undefined, + AUTH_REQUEST_OPTIONS, + ) +} + +export function wechatLogin(data: IWechatLoginForm) { + return http.post( + '/auth/wechat-login', + data, + undefined, + undefined, + AUTH_REQUEST_OPTIONS, + ) +} + +export function wechatBindPhone(data: IWechatBindPhoneForm) { + return http.post( + '/auth/wechat-bind-phone', + data, + undefined, + undefined, + AUTH_REQUEST_OPTIONS, + ) +} + +export function register(registerForm: IRegisterForm) { + return http.post( + '/auth/register', + registerForm, + undefined, + undefined, + AUTH_REQUEST_OPTIONS, + ) +} + +export function forgotPassword(data: IForgotPasswordForm) { + return http.post<{ success: boolean }>( + '/auth/forgot-password', + data, + undefined, + undefined, + AUTH_REQUEST_OPTIONS, + ) +} + +export function refreshToken(_refreshToken: string) { + return Promise.reject(new Error('当前 APP 接口使用单 token 模式,暂不支持刷新 token')) +} + +export function getUserInfo() { + return http.get('/user/info', undefined, undefined, AUTH_REQUEST_OPTIONS) +} + +export function logout() { + return Promise.resolve() +} + +export function updateInfo(data: IUpdateInfo) { + return http.post('/user/info', data, undefined, undefined, AUTH_REQUEST_OPTIONS) +} + +export function bindWechat(data: IWechatBindForm) { + return http.post( + '/user/wechat/bind', + data, + undefined, + undefined, + AUTH_REQUEST_OPTIONS, + ) +} + +export function updateUserPassword() { + return Promise.reject(new Error('请使用找回密码流程重置密码')) +} + +export interface AppWechatLoginCodeRes { + code: string + errMsg?: string +} + +export function getWxCode() { + return new Promise((resolve, reject) => { + if (typeof wx !== 'undefined' && typeof wx.login === 'function') { + wx.login({ + success: res => resolve({ code: res.code || '', errMsg: res.errMsg }), + fail: err => reject(new Error(err.errMsg || '微信登录失败')), + }) + return + } + uni.login({ + provider: 'weixin', + success: res => resolve({ code: res.code || '', errMsg: res.errMsg }), + fail: err => reject(new Error(err.errMsg)), + }) + }) +} diff --git a/app/src/api/message.ts b/app/src/api/message.ts new file mode 100644 index 0000000..fbf1a8b --- /dev/null +++ b/app/src/api/message.ts @@ -0,0 +1,78 @@ +import type { MessageCenterParams, MessageCenterRes } from './types/message' +import { http } from '@/http/http' + +export interface AppMessage { + id: number + messageId: number + messageLogId: number + messageType: number + messageTypeText?: string + title: string + content: string + link?: string + isRead: number + sendTime?: string + createdAt?: string + updatedAt?: string +} + +export interface AppMessageListParams { + page: number + pageSize: number + isRead?: '' | '0' | '1' + messageType?: number + keyword?: string +} + +export interface AppMessageListRes { + items: AppMessage[] + total: number + page?: number + pageSize?: number +} + +export interface AppMessageDetailParams { + messageId: number +} + +export interface AppMessageDetailRes { + message: AppMessage +} + +export interface AppMessageReadParams { + ids: number[] +} + +export function getMessageList(params: AppMessageListParams) { + return http.get('/message/list', params, undefined, { + hideErrorToast: true, + }) +} + +export async function getMessageCenter(params: MessageCenterParams = {}): Promise { + const unreadRes = await getMessageList({ + page: 1, + pageSize: 1, + isRead: '0', + keyword: params.keyword, + group: params.group, + } as AppMessageListParams) + + return { + groups: [], + totalUnread: Number(unreadRes.total || 0), + warnSummary: undefined, + } +} + +export function getMessageDetail(params: AppMessageDetailParams) { + return http.get('/message/detail', params, undefined, { + hideErrorToast: true, + }) +} + +export function readMessages(data: AppMessageReadParams) { + return http.post('/message/read', data, undefined, undefined, { + hideErrorToast: true, + }) +} diff --git a/app/src/api/order.ts b/app/src/api/order.ts new file mode 100644 index 0000000..fb88a3b --- /dev/null +++ b/app/src/api/order.ts @@ -0,0 +1,21 @@ +import type { + OrderDetailParams, + OrderDetailRes, + OrderListParams, + OrderListRes, +} from './types/order' +import { http } from '@/http/http' + +export type { AppOrder, AppOrderItem, OrderDetailParams, OrderListParams } from './types/order' + +export function getOrderList(params: OrderListParams) { + return http.get('/order/list', params, undefined, { + hideErrorToast: true, + }) +} + +export function getOrderDetail(params: OrderDetailParams) { + return http.get('/order/detail', params, undefined, { + hideErrorToast: true, + }) +} diff --git a/app/src/api/system-normalize.ts b/app/src/api/system-normalize.ts new file mode 100644 index 0000000..5f99235 --- /dev/null +++ b/app/src/api/system-normalize.ts @@ -0,0 +1,19 @@ +export interface AccessCodesResponse { + code?: number | string + data?: + | string[] + | { + data?: string[] + } +} + +export function normalizeAccessCodesResponse(body?: string[] | AccessCodesResponse | null) { + if (Array.isArray(body)) + return body + if (Array.isArray(body?.data)) + return body.data + const nested = body?.data + if (nested && !Array.isArray(nested) && Array.isArray(nested.data)) + return nested.data + return [] +} diff --git a/app/src/api/system.test.ts b/app/src/api/system.test.ts new file mode 100644 index 0000000..ddaa0b4 --- /dev/null +++ b/app/src/api/system.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest' +import { normalizeAccessCodesResponse } from './system-normalize' + +describe('normalizeAccessCodesResponse', () => { + it('supports xiuAdmin wrapped access code response', () => { + expect( + normalizeAccessCodesResponse({ + code: 0, + data: { + data: ['cpm:iot:tSimCardInfo:view', 'cpm:iot:tSimCardRenew:input:prepare'], + }, + }), + ).toEqual(['cpm:iot:tSimCardInfo:view', 'cpm:iot:tSimCardRenew:input:prepare']) + }) + + it('keeps compatibility with direct array response', () => { + expect( + normalizeAccessCodesResponse({ + code: 0, + data: ['cpc:current:user'], + }), + ).toEqual(['cpc:current:user']) + }) +}) diff --git a/app/src/api/system.ts b/app/src/api/system.ts new file mode 100644 index 0000000..3ab1028 --- /dev/null +++ b/app/src/api/system.ts @@ -0,0 +1,15 @@ +import type { AccessCodesResponse } from './system-normalize' +import { http } from '@/http/http' +import { normalizeAccessCodesResponse } from './system-normalize' + +export interface AccessCodesParams { + isCache?: boolean +} + +export { normalizeAccessCodesResponse } + +export function getAccessCodes(params?: AccessCodesParams) { + return http.get('/system/auth/codes', params, undefined, { + hideErrorToast: true, + }).then(normalizeAccessCodesResponse) +} diff --git a/app/src/api/types/balance.ts b/app/src/api/types/balance.ts new file mode 100644 index 0000000..f29ac43 --- /dev/null +++ b/app/src/api/types/balance.ts @@ -0,0 +1,61 @@ +export interface BalanceRechargeSubmitParams { + amount: number + buyerNote?: string + payProvider?: 'wechat' | 'mock' + payScene?: 'h5' | 'mini' + wxCode?: string +} + +export interface BalanceRechargeSubmitRes { + orderId: number + orderSn: string + totalAmount: number + priceText: string + orderStatus: number + payStatus: number + status: string + paymentExpireAt?: string + paymentRemainingSeconds?: number + paymentExpired?: boolean + payProvider: string + payUrl: string + payMethod?: string + payGateway?: string + payParams?: Record +} + +export interface BalanceRechargeStatusParams { + orderSn: string + payScene?: 'h5' | 'mini' + wxCode?: string +} + +export interface BalanceRechargeMockPayParams { + orderSn: string +} + +export interface BalanceRechargeRefundParams { + orderSn: string + refundReason?: string +} + +export interface BalanceRechargeStatusRes { + orderId: number + orderSn: string + totalAmount: number + priceText: string + orderStatus: number + payStatus: number + status: string + paymentExpireAt?: string + paymentRemainingSeconds?: number + paymentExpired?: boolean + balanceAfter: number + balanceAfterText: string + payProvider: string + payTradeNo: string + payUrl: string + payMethod?: string + payGateway?: string + payParams?: Record +} diff --git a/app/src/api/types/card.ts b/app/src/api/types/card.ts new file mode 100644 index 0000000..52f4141 --- /dev/null +++ b/app/src/api/types/card.ts @@ -0,0 +1,394 @@ +export interface AppCard { + id?: number + iccid: string + msisdn?: string + imsi?: string + imei?: string + regionCode?: string + deptName?: string + currentPackageCode?: string + packageName?: string + packageType?: number + categoryCode?: string + categoryName?: string + packageStatus?: number + bossCardStatus?: number + abnormalReason?: string + abnormalReasons?: string[] + cardType?: number + netModel?: number + onlineStatus?: string | number + deviceStatus?: string | number + imeiStatus?: string | number + regionLimitStatus?: string | number + apnName?: string + apnId?: string + rat?: string + ipv4?: string + ipv6?: string + ipv6Prefix?: string + sessionCreateTime?: string + stopReason?: string + packageTotalFlowMb?: number + usedFlowMb?: number + remainFlowMb?: number + usedFlowPercent?: number + yesterdayFlowStatDate?: string + yesterdayDeptUsedFlowMb?: number + yesterdayDeptSms?: number + yesterdayDeptVoiceMin?: number + flowPkgBeginAt?: string + flowPkgEndAt?: string + dataServiceStatus?: number + packageTotalSms?: number + usedSms?: number + smsServiceStatus?: number + packageTotalVoice?: number + usedVoice?: number + voiceServiceStatus?: number + roamServiceStatus?: number + statusSyncTime?: string + flowSyncTime?: string + remark?: string + terminalName?: string + isBound?: boolean + canBusiness?: boolean + canAllocate?: boolean + ownerType?: 'public' | 'terminal' | 'agent' | string +} + +export interface PublicQueryCardParams { + tenantId?: string + cardValue: string +} + +export type CardRecognitionCode + = | 'CARD_RECOGNIZED' + | 'CARD_AMBIGUOUS' + | 'CARD_UNREADABLE' + | 'RECOGNITION_TIMEOUT' + | 'RECOGNITION_FAILED' + | 'RECOGNITION_DISABLED' + | 'RECOGNITION_RATE_LIMITED' + +export interface CardRecognitionRes { + code: CardRecognitionCode | string + message: string + cardType: 'iccid' | string + cardValue: string + confidence: number + candidates: string[] +} + +export interface CardListParams { + page?: number + pageSize?: number + cardValue?: string + status?: string + categoryCode?: string + includeTotal?: boolean +} + +export interface AppPackage { + id: number + subscriptionNo?: string + packageCode?: string + packageName?: string + packageType?: number + packageTypeName?: string + categoryCode?: string + categoryName?: string + iccid?: string + msisdn?: string + cardId?: number + cardValue?: string + cardRemark?: string + terminalName?: string + businessType?: number + businessTypeName?: string + effectiveStatus?: number + status?: string + statusType?: 'danger' | 'primary' | 'success' | 'warning' | string + filterValue?: string + effectiveTime?: string + expireTime?: string + validText?: string + effectText?: string + flowMb?: number + flowText?: string + ownerType?: 'public' | 'terminal' | 'agent' | string + canBusiness?: boolean +} + +export interface PackageListParams { + page?: number + pageSize?: number + keyword?: string + cardValue?: string + subscriptionNo?: string + packageCode?: string + status?: string + categoryCode?: string +} + +export interface PublicPackageDetailParams { + tenantId?: string + cardValue: string + packageCode?: string + subscriptionNo?: string +} + +export interface BindCardParams { + tenantId?: string + cardValue: string + remark?: string +} + +export interface RestartCardParams { + cardValue: string + remark?: string +} + +export interface CardLifecycleParams { + cardValue?: string + cardValues?: string[] + operation: 'stop' | 'start' | 'restart' | string + remark?: string +} + +export interface CardLifecycleStatusParams { + batchNo: string +} + +export interface RenewPrepareParams { + cardValue: string + packageCode?: string + subscriptionNo?: string +} + +export interface RenewBatchPrepareParams { + cardValues: string[] + packageCode?: string +} + +export interface RenewSubmitParams { + cardValue: string + packageCode?: string + subscriptionNo?: string + durationMonths?: number + durationYears?: number + buyerNote?: string +} + +export interface RenewBatchSubmitParams { + cardValues?: string[] + batchNo?: string + packageCode?: string + durationMonths?: number + durationYears?: number + buyerNote?: string +} + +export interface AllocateTargetListParams { + page?: number + pageSize?: number + keyword?: string +} + +export interface AllocateTarget { + deptId: number + parentId?: number + deptKey?: string + deptName: string + deptCategory?: string + targetScope?: string + targetScopeName?: string + label?: string +} + +export interface AllocateBatchPrepareParams { + cardValues: string[] + targetDeptId: number +} + +export interface AllocateBatchSubmitParams { + cardValues?: string[] + batchNo: string + targetDeptId?: number + remark?: string +} + +export interface ChangePrepareParams { + cardValues: string[] + newPackageCode?: string + targetPackageCode?: string +} + +export interface ChangeSubmitParams { + cardValues?: string[] + batchNo: string + newPackageCode: string + durationMonths: number + buyerNote?: string + remark?: string +} + +export interface CardQueryRes { + card: AppCard +} + +export interface CardListRes { + items: AppCard[] + total: number + hasMore?: boolean +} + +export interface PackageListRes { + items: AppPackage[] + total: number +} + +export interface PackageDetailRes { + package: AppPackage +} + +export interface RestartCardRes { + batchNo: string + batchType: number + status: string + card: AppCard +} + +export interface CardLifecycleRes { + batchNo: string + batchType: number + operation: string + status: string + quantity: number + successCount?: number + failCount?: number + failReason?: string + cards: AppCard[] +} + +export interface AllocateTargetListRes { + items: AllocateTarget[] + total: number +} + +export interface AllocateBatchRes { + batchNo: string + sourceBatchNo?: string + batchType: number + quantity: number + totalCount?: number + validCount?: number + failCount?: number + failReason?: string + target: AllocateTarget + cards: AppCard[] + details?: Array<{ + rowNo: number + itemId: string + status: string + errorReason?: string + }> + status: string +} + +export interface ChangePackageOption { + package: AppPackage + options: AppRenewOption[] + unitPrice: number + priceText: string +} + +export interface ChangePrepareRes { + batchNo: string + batchType: number + quantity: number + cards: AppCard[] + currentPackage: AppPackage + targetPackages: ChangePackageOption[] + effectiveRule: string +} + +export interface ChangeSubmitRes { + batchNo: string + batchType: number + orderId: number + orderSn: string + totalAmount: number + priceText: string + quantity: number + orderStatus: number + payStatus: number + status: string + effectiveRule: string + cards: AppCard[] + currentPackage: AppPackage + newPackage: AppPackage + option: AppRenewOption +} + +export interface AppRenewOption { + durationMonths: number + actualDurationMonths?: number + durationYears?: number + durationValue?: number + durationUnit?: string + durationUnitText?: string + label: string + unitPrice: number + totalAmount: number + priceText: string + expireAfter?: string + recommended?: boolean +} + +export interface RenewPackageOption { + package: AppPackage + packageCode: string + packageName: string + renewUnitPrice: number + unitPrice?: number + priceSource?: string + priceText?: string + renewableDurationOptions?: number[] + options: AppRenewOption[] +} + +export interface RenewPrepareRes { + batchNo?: string + batchType?: number + card: AppCard + cards?: AppCard[] + quantity?: number + currentPackage?: AppPackage + package: AppPackage + renewablePackageCodes?: string[] + renewablePackages?: RenewPackageOption[] + options: AppRenewOption[] + priceSource: string + unitPrice: number + effectiveRule: string + recommendIndex: number +} + +export interface RenewSubmitRes { + batchNo?: string + batchType?: number + orderId: number + orderSn: string + totalAmount: number + priceText: string + quantity?: number + orderStatus: number + payStatus: number + status: string + effectiveRule: string + card: AppCard + cards?: AppCard[] + package: AppPackage + option: AppRenewOption +} diff --git a/app/src/api/types/customer.ts b/app/src/api/types/customer.ts new file mode 100644 index 0000000..bcf9576 --- /dev/null +++ b/app/src/api/types/customer.ts @@ -0,0 +1,53 @@ +import type { AppEnterpriseProfile, AppTerminalProfile } from './login' + +export type AppCustomerType = 'all' | 'enterprise' | 'terminal' + +export interface CustomerListParams { + page?: number + pageSize?: number + keyword?: string + customerType?: AppCustomerType + status?: string +} + +export interface AppCustomer { + customerType: AppCustomerType | string + customerTypeName: string + customerId: number + deptId?: number + terminalId?: number + parentDeptId?: number + customerCode?: string + customerName: string + deptCategory?: string + phone?: string + email?: string + status?: string + statusText: string + realNameStatus: string + balance?: number + cardCount: number + normalCardCount: number + address?: string + fullName?: string + createdAt?: string +} + +export interface AppCustomerDetail extends AppCustomer { + enterpriseProfile?: AppEnterpriseProfile | null + terminalProfile?: AppTerminalProfile | null +} + +export interface CustomerListRes { + items: AppCustomer[] + total: number +} + +export interface CustomerDetailParams { + customerType: Exclude | string + customerId: number +} + +export interface CustomerDetailRes { + customer: AppCustomerDetail +} diff --git a/app/src/api/types/diagnosis.ts b/app/src/api/types/diagnosis.ts new file mode 100644 index 0000000..30dcca4 --- /dev/null +++ b/app/src/api/types/diagnosis.ts @@ -0,0 +1,102 @@ +import type { AppCard } from './card' + +export interface DiagnosisInfoParams { + queryCode: string +} + +export interface DiagnosisFlowParams { + queryCode: string + queryType: '7d' | '30d' | '3M' | '6M' +} + +export interface DiagnosisInfoItem { + iccid?: string + imsi?: string + msisdn?: string + status?: string + dataStatus?: string + onlineStatus?: string + operatingStatus?: string + deviceStatus?: string + imeiStatus?: string + regionLimitStatus?: string + regionLimitOrderStatus?: string + regionLimitTriggerStatus?: string + regionLimitType?: string + regionLimitArea?: string + humanLimitStatus?: string + rateLimitStatus?: string + rateLimitReason?: string + rateLimitDetail?: string + rateLimitApnDetails?: DiagnosisRateLimitApnItem[] + packageTotalFlowMb?: number + usedFlowMb?: number + remainFlowMb?: number + flowSyncTime?: string + stopReason?: string + frozenStatus?: string + frozenReason?: string + frozenOperTime?: string +} + +export interface DiagnosisRateLimitApnItem { + apnName?: string + apnStatus?: string + modifyTime?: string + overUseDetails?: string + apnServiceUsageState?: string + pccDetails?: DiagnosisRateLimitPccItem[] +} + +export interface DiagnosisRateLimitPccItem { + pccServiceCode?: string + pccName?: string + pccPolicyStatus?: string + pccServiceUsageState?: string + modifyTime?: string +} + +export interface DiagnosisSessionItem { + apnId?: string + rat?: string + imei?: string + createTime?: string + ipv4?: string + ipv6?: string + ipv6Prefix?: string + status?: string +} + +export interface DiagnosisPackageItem { + packageName?: string + packageType?: string + effectiveStatus?: string + packageTotalFlowMb?: number + usedFlowMb?: number + remainFlowMb?: number + durationMonths?: number + expectedExpireTime?: string +} + +export interface DiagnosisInfoRes { + info?: DiagnosisInfoItem + session?: DiagnosisSessionItem + package?: DiagnosisPackageItem + modules?: Record + card?: AppCard +} + +export interface DiagnosisModuleItem { + status: 'success' | 'error' | 'cached' | 'skipped' | string + message?: string + cached?: boolean + updatedAt?: string +} + +export interface DiagnosisFlowRes { + times: string[] + flow: number[] + unit: string + granularity?: 'day' | 'month' | string + hasData?: boolean +} diff --git a/app/src/api/types/login.ts b/app/src/api/types/login.ts new file mode 100644 index 0000000..55981fe --- /dev/null +++ b/app/src/api/types/login.ts @@ -0,0 +1,246 @@ +export type AuthMode = 'single' | 'double' + +export interface ISingleTokenRes { + token: string + expiresIn: number + userInfo?: IUserInfoRes +} + +export interface IDoubleTokenRes { + accessToken: string + refreshToken: string + accessExpiresIn: number + refreshExpiresIn: number +} + +export interface ILoginCandidate { + userId: number + username: string + nickName: string + phone: string + deptId?: number + deptName: string + userType: string + role: string +} + +export interface ILoginSelectRes { + needSelect: true + candidates: ILoginCandidate[] +} + +export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes | ILoginSelectRes + +export interface IWechatNeedBindRes { + needBindPhone: true + bindTicket: string + bindExpireIn?: number + needSelect?: boolean + candidates?: ILoginCandidate[] +} + +export type IWechatLoginRes = ISingleTokenRes | IDoubleTokenRes | IWechatNeedBindRes +export type IWechatBindPhoneRes = ISingleTokenRes + | IDoubleTokenRes + | (ILoginSelectRes & Pick) + +export type UserRole = string + +export interface AppEnterpriseProfile { + deptId?: number + cardCount?: number + deptName: string + deptKey: string + phone: string + email: string + statusText: string + creditCode: string + areaCode: string + address: string + fullName: string + industry: string + legalPersonName: string + legalPersonIdCard: string + companyType: string + establishTime: string + bankName: string + bankAccount: string + realNameStatus: string +} + +export interface AppTerminalProfile { + cardCount?: number + terminalName: string + phone: string + email: string + statusText: string + creditCode: string + areaCode: string + address: string + fullName: string + industry: string + legalPersonName: string + legalPersonIdCard: string + companyType: string + establishTime: string + bankName: string + bankAccount: string + realNameStatus: string +} + +export interface ILoginForm { + tenantId: string + account: string + username?: string + password: string + selectedUserId?: number + wxCode?: string +} + +export interface IWechatLoginForm { + tenantId: string + wxCode: string +} + +export interface IWechatBindPhoneForm { + tenantId: string + bindTicket: string + phone: string + captchaKey?: string + captchaPoint?: string + selectedUserId?: number +} + +export interface IRegisterForm { + tenantId: string + phone: string + password: string + nickName: string + /** 短信验证码:当前注册未接入短信,图形验证通过后可为空 */ + smsCode?: string +} + +export interface IForgotPasswordForm { + tenantId: string + phone: string + password: string + smsCode: string +} + +export interface IUserInfoRes { + userId?: number + tenantId?: string + deptId?: number + username: string + nickName: string + nickname?: string + phone: string + avatar?: string + sex?: '0' | '1' | '2' | string + userType?: string + isTerminal?: boolean + canEnterpriseBusiness?: boolean + wechatBound?: boolean + wechatBindText?: string + balance?: number + role?: UserRole + roleName?: string + roleKey?: string + roleNames?: string[] + roles?: UserRole[] + accessCodes?: string[] + enterpriseProfile?: AppEnterpriseProfile | null + terminalProfile?: AppTerminalProfile | null + [key: string]: any +} + +export interface AuthStorage { + mode: AuthMode + tokens: ISingleTokenRes | IDoubleTokenRes + userInfo?: IUserInfoRes + loginTime: number +} + +export interface ISideCaptcha { + captchaKey: string + imageBase64: string + tileBase64: string + tileWidth: number + tileHeight: number + tileX: number + tileY: number +} + +export interface ICheckSideCaptchaForm { + captchaKey: string + point: string +} + +export interface ISendSmsForm { + tenantId: string + phone: string + scene: 'register' | 'forgotPassword' +} + +export interface ISendSmsRes { + success: boolean + smsCode?: string +} + +export interface IUploadSuccessInfo { + fileId: number + originalName: string + fileName: string + storagePath: string + fileHash: string + fileType: string + fileBusinessType: string + fileSize: number +} + +export interface IUpdateInfo { + nickName: string + avatar?: string + sex: string + terminalName?: string + phone?: string + email?: string + creditCode?: string + areaCode?: string + address?: string + fullName?: string + industry?: string + legalPersonName?: string + legalPersonIdCard?: string + companyType?: string + establishTime?: string + bankName?: string + bankAccount?: string +} + +export interface IWechatBindForm { + wxCode: string +} + +export interface IWechatBindRes { + success: boolean + userInfo: IUserInfoRes +} + +export interface IUpdatePassword { + oldPassword: string + newPassword: string + confirmPassword: string +} + +export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes { + return 'token' in tokenRes && !('refreshToken' in tokenRes) +} + +export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes { + return 'accessToken' in tokenRes && 'refreshToken' in tokenRes +} + +export function isLoginSelectRes(tokenRes: IAuthLoginRes): tokenRes is ILoginSelectRes { + return 'needSelect' in tokenRes && tokenRes.needSelect === true +} diff --git a/app/src/api/types/message.ts b/app/src/api/types/message.ts new file mode 100644 index 0000000..1bae040 --- /dev/null +++ b/app/src/api/types/message.ts @@ -0,0 +1,63 @@ +export type MessageGroupKey = 'system' | 'card' | string + +export type MessageTagType = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'volcano' | 'lightblue' | 'cyan' | 'pink' | 'purple' + +export interface MessageItem { + messageId?: number + group?: MessageGroupKey + source?: 'user_message' | 'card_warn' | string + title: string + summary: string + time: string + unread: boolean + tag?: string + tagType?: MessageTagType + link?: string + cardValue?: string + warnType?: string + severity?: number +} + +export interface MessageGroup { + key: MessageGroupKey + label: string + unreadCount: number + searchPlaceholder: string + tag: string + tagType: MessageTagType + list: MessageItem[] +} + +export interface WarnSummary { + total: number + critical: number + warning: number + reminder: number + latestTime?: string + items?: MessageItem[] +} + +export interface MessageCenterParams { + page?: number + pageSize?: number + group?: MessageGroupKey + keyword?: string + timeRange?: MessageTimeRange + sortBy?: MessageSortBy +} + +export type MessageTimeRange = 'all' | '30d' + +export type MessageSortBy = 'none' | 'time_desc' | 'time_asc' + +export interface MessageCenterRes { + groups: MessageGroup[] + totalUnread: number + warnSummary?: WarnSummary +} + +export interface MessageReadParams { + messageIds: number[] + group?: MessageGroupKey + source?: MessageItem['source'] +} diff --git a/app/src/api/types/order.ts b/app/src/api/types/order.ts new file mode 100644 index 0000000..124f179 --- /dev/null +++ b/app/src/api/types/order.ts @@ -0,0 +1,72 @@ +export interface OrderListParams { + page: number + pageSize: number + keyword?: string + orderType?: number + payStatus?: number +} + +export interface AppOrderItem { + itemId: number + productName: string + productSn: string + price: number + priceText: string + quantity: number + originPrice: number + originPriceText: string + productType: number + serviceBeginTime?: string + serviceEndTime?: string +} + +export interface AppOrder { + orderId: number + orderSn: string + orderType: number + orderTypeText: string + orderStatus: number + orderStatusText: string + payType: number + payTypeText: string + payStatus: number + payStatusText: string + payOrderSn: string + returnStatus: number + returnStatusText: string + paymentExpireAt?: string + paymentRemainingSeconds?: number + paymentExpired?: boolean + totalAmount: number + totalAmountText: string + paidAmount: number + paidAmountText: string + onlinePaidAmount: number + onlinePaidAmountText: string + balance: number + balanceText: string + unpaidAmount: number + unpaidAmountText: string + unrefundAmount: number + unrefundAmountText: string + buyerNote: string + remark: string + createdAt?: string + payTime?: string + itemSummary: string + itemCount: number + items?: AppOrderItem[] +} + +export interface OrderListRes { + items: AppOrder[] + total: number +} + +export interface OrderDetailParams { + orderSn: string +} + +export interface OrderDetailRes { + order: AppOrder +} diff --git a/app/src/api/types/warning.ts b/app/src/api/types/warning.ts new file mode 100644 index 0000000..151f944 --- /dev/null +++ b/app/src/api/types/warning.ts @@ -0,0 +1,127 @@ +import type { AppCard } from './card' + +export interface WarningConfig { + id?: number + itemType?: number + itemId?: number + itemCode?: string + warnType?: number + warnUnit: number + warnTitle?: string + unitText?: string + firstWarn: number + secondWarn?: number + thirdWarn?: number + autoAction?: number + isEnabled: number + appNotifyEnabled?: number + wxTemplateId?: string + remark?: string + updatedAt?: string +} + +export interface WarningConfigListParams { + cardValue: string +} + +export interface WarningConfigListRes { + card: AppCard + items: WarningConfig[] + templateId: string + templateReady: boolean + wechatBound: boolean + wechatBindText: string +} + +export interface WarningConfigSaveParams { + cardValue: string + warnUnit: number + firstWarn: number + secondWarn?: number + thirdWarn?: number + isEnabled: number + appNotifyEnabled: number + remark?: string +} + +export interface WarningConfigSaveRes { + id: number + config: WarningConfig + templateId: string + templateReady: boolean + wechatBound: boolean + wechatBindText: string +} + +export interface WarningConfigNotifyParams { + cardValue: string + warnUnit?: number + appNotifyEnabled: number +} + +export interface WarningConfigNotifyRes { + appNotifyEnabled: number + config?: WarningConfig + templateId: string + templateReady: boolean + wechatBound: boolean + wechatBindText: string +} + +export interface WarningConfigMetaRes { + templateId: string + templateReady: boolean + wechatBound: boolean + wechatBindText: string + appNotifyEnabled: number +} + +export interface WarningConfigBatchSaveParams { + cardValues: string[] + configs: WarningConfigBatchItemParams[] +} + +export interface WarningConfigBatchItemParams { + warnUnit: number + firstWarn: number + secondWarn?: number + thirdWarn?: number + isEnabled: number + appNotifyEnabled: number + remark?: string +} + +export interface WarningConfigBatchFailItem { + cardValue: string + reason: string +} + +export interface WarningConfigBatchSaveRes { + batchNo: string + totalCount: number + successCount: number + failCount: number + failItems: WarningConfigBatchFailItem[] + templateId: string + templateReady: boolean + wechatBound: boolean + wechatBindText: string +} + +export interface WarningConfigBatchNotifyParams { + cardValues: string[] + warnUnit?: number + appNotifyEnabled: number +} + +export interface WarningConfigBatchNotifyRes { + appNotifyEnabled: number + totalCount: number + successCount: number + failCount: number + failItems: WarningConfigBatchFailItem[] + templateId: string + templateReady: boolean + wechatBound: boolean + wechatBindText: string +} diff --git a/app/src/api/types/workOrder.ts b/app/src/api/types/workOrder.ts new file mode 100644 index 0000000..bbb586b --- /dev/null +++ b/app/src/api/types/workOrder.ts @@ -0,0 +1,62 @@ +export interface WorkOrderListParams { + page: number + pageSize: number + keyword?: string + currentStatus?: number + woType?: number +} + +export interface WorkOrderDetailParams { + woNo: string +} + +export interface WorkOrderCreateParams { + woType: number + relatedId?: number + title: string + content: string + attachmentUrls?: string[] +} + +export interface AppWorkOrderCommunication { + id: number + woNo: string + commType: number + commTypeText: string + senderName: string + content: string + attachmentUrls?: string[] + createdAt?: string +} + +export interface AppWorkOrder { + id: number + woNo: string + woType: number + woTypeText: string + relatedId: number + title: string + content: string + creatorType: number + creatorName: string + creatorPhone: string + currentStatus: number + currentStatusText: string + statusType: string + createdAt?: string + updatedAt?: string + communications?: AppWorkOrderCommunication[] +} + +export interface WorkOrderListRes { + items: AppWorkOrder[] + total: number +} + +export interface WorkOrderDetailRes { + workOrder: AppWorkOrder +} + +export interface WorkOrderCreateRes { + workOrder: AppWorkOrder +} diff --git a/app/src/api/warning.ts b/app/src/api/warning.ts new file mode 100644 index 0000000..6b20acd --- /dev/null +++ b/app/src/api/warning.ts @@ -0,0 +1,81 @@ +import type { + WarningConfigBatchNotifyParams, + WarningConfigBatchNotifyRes, + WarningConfigBatchSaveParams, + WarningConfigBatchSaveRes, + WarningConfigListParams, + WarningConfigListRes, + WarningConfigMetaRes, + WarningConfigNotifyParams, + WarningConfigNotifyRes, + WarningConfigSaveParams, + WarningConfigSaveRes, +} from './types/warning' +import { http } from '@/http/http' + +export type { + WarningConfig, + WarningConfigBatchItemParams, + WarningConfigBatchNotifyParams, + WarningConfigBatchNotifyRes, + WarningConfigBatchSaveParams, + WarningConfigBatchSaveRes, + WarningConfigListRes, + WarningConfigNotifyParams, + WarningConfigNotifyRes, + WarningConfigSaveParams, +} from './types/warning' + +export function getWarningConfigList(params: WarningConfigListParams) { + return http.get('/warning/config/list', params, undefined, { + hideErrorToast: true, + }) +} + +export function saveWarningConfig(data: WarningConfigSaveParams) { + return http.post('/warning/config/save', data, undefined, undefined, { + hideErrorToast: true, + }) +} + +export function updateWarningConfigNotify(data: WarningConfigNotifyParams) { + return http.post( + '/warning/config/notify', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} + +export function getWarningConfigMeta() { + return http.get('/warning/config/meta', undefined, undefined, { + hideErrorToast: true, + }) +} + +export function saveWarningConfigBatch(data: WarningConfigBatchSaveParams) { + return http.post( + '/warning/config/batch-save', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} + +export function updateWarningConfigNotifyBatch(data: WarningConfigBatchNotifyParams) { + return http.post( + '/warning/config/batch-notify', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} diff --git a/app/src/api/workOrder.ts b/app/src/api/workOrder.ts new file mode 100644 index 0000000..a064500 --- /dev/null +++ b/app/src/api/workOrder.ts @@ -0,0 +1,35 @@ +import type { + WorkOrderCreateParams, + WorkOrderCreateRes, + WorkOrderDetailParams, + WorkOrderDetailRes, + WorkOrderListParams, + WorkOrderListRes, +} from './types/workOrder' +import { http } from '@/http/http' + +export type { + AppWorkOrder, + AppWorkOrderCommunication, + WorkOrderCreateParams, + WorkOrderDetailParams, + WorkOrderListParams, +} from './types/workOrder' + +export function getWorkOrderList(params: WorkOrderListParams) { + return http.get('/work-order/list', params, undefined, { + hideErrorToast: true, + }) +} + +export function getWorkOrderDetail(params: WorkOrderDetailParams) { + return http.get('/work-order/detail', params, undefined, { + hideErrorToast: true, + }) +} + +export function createWorkOrder(data: WorkOrderCreateParams) { + return http.post('/work-order/create', data, undefined, undefined, { + hideErrorToast: true, + }) +} diff --git a/app/src/components/.gitkeep b/app/src/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/src/components/Navbar/index.vue b/app/src/components/Navbar/index.vue new file mode 100644 index 0000000..e186b86 --- /dev/null +++ b/app/src/components/Navbar/index.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/app/src/components/SideCaptcha.vue b/app/src/components/SideCaptcha.vue new file mode 100644 index 0000000..37aa849 --- /dev/null +++ b/app/src/components/SideCaptcha.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/app/src/constant/RouterPath.ts b/app/src/constant/RouterPath.ts new file mode 100644 index 0000000..d632ec8 --- /dev/null +++ b/app/src/constant/RouterPath.ts @@ -0,0 +1,81 @@ +/******************************* + * 此文件由脚本自动生成,请勿手动修改 * + *******************************/ + +/** + * 页面路径常量 + */ +const RouterPath = { + /** 山东平台物联卡 */ + Home: '/pages/home/index', + /** 办理 */ + Business: '/pages/business/index', + /** 卡片 */ + Card: '/pages/card/index', + /** 山东平台物联卡 */ + Login: '/pages/login/index', + /** 消息 */ + Message: '/pages/message/index', + /** 我的 */ + User: '/pages/user/index', + /** 找回密码 */ + 'LoginForgot-password': '/pages/login/forgot-password/index', + /** 注册账号 */ + LoginRegister: '/pages/login/register/index', + /** 加油包 */ + BusinessViewsAddon: '/pages/business/views/addon/index', + /** 批量卡片划拨 */ + BusinessViewsAllocate: '/pages/business/views/allocate/index', + /** 余额充值 */ + BusinessViewsBalanceRecharge: '/pages/business/views/balanceRecharge/index', + /** 单卡套餐续费 */ + BusinessViewsCardRenew: '/pages/business/views/cardRenew/index', + /** 套餐变更 */ + BusinessViewsChange: '/pages/business/views/change/index', + /** 客户管理 */ + BusinessViewsCustomer: '/pages/business/views/customer/index', + /** 客户详情 */ + BusinessViewsCustomerDetail: '/pages/business/views/customerDetail/index', + /** 卡片排查 */ + BusinessViewsDiagnosis: '/pages/business/views/diagnosis/index', + /** 我的订单 */ + BusinessViewsOrder: '/pages/business/views/order/index', + /** 批量套餐续费 */ + BusinessViewsRenew: '/pages/business/views/renew/index', + /** 预警配置 */ + BusinessViewsWarning: '/pages/business/views/warning/index', + /** 工单列表 */ + BusinessViewsWorkOrder: '/pages/business/views/workOrder/index', + /** 提交工单 */ + BusinessViewsWorkOrderCreate: '/pages/business/views/workOrderCreate/index', + /** 工单详情 */ + BusinessViewsWorkOrderDetail: '/pages/business/views/workOrderDetail/index', + /** 卡片详情 */ + CardViewsDetail: '/pages/card/views/detail/index', + /** 全部设备 */ + CardViewsManagement: '/pages/card/views/management/index', + /** 卡片详情 */ + CardViewsPackageDetail: '/pages/card/views/packageDetail/index', + /** 预警配置 */ + CardViewsWarning: '/pages/card/views/warning/index', + /** 消息详情 */ + MessageViewsCardDetail: '/pages/message/views/cardDetail/index', + /** 消息详情 */ + MessageViewsSystemDetail: '/pages/message/views/systemDetail/index', + /** 关于我们 */ + UserViewsAbout: '/pages/user/views/about/index', + /** 协议与隐私 */ + UserViewsAgreement: '/pages/user/views/agreement/index', + /** 联系客服 */ + UserViewsContact: '/pages/user/views/contact/index', + /** 帮助与反馈 */ + UserViewsFeedback: '/pages/user/views/feedback/index', + /** 微信提醒 */ + UserViewsNotification: '/pages/user/views/notification/index', + /** 账号资料 */ + UserViewsProfile: '/pages/user/views/profile/index', + /** 实名信息 */ + UserViewsRealName: '/pages/user/views/realName/index', +} + +export default RouterPath diff --git a/app/src/env.d.ts b/app/src/env.d.ts new file mode 100644 index 0000000..216e2f6 --- /dev/null +++ b/app/src/env.d.ts @@ -0,0 +1,39 @@ +/// +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + + const component: DefineComponent<{}, {}, any> + export default component +} + +declare module '@qiun/ucharts' { + export default class UCharts { + constructor(options: Record) + updateData(options?: Record): void + showToolTip(event: unknown, options?: Record): void + } +} + +interface ImportMetaEnv { + /** 网站标题,应用名称 */ + readonly VITE_APP_TITLE: string + /** 服务端口号 */ + readonly VITE_APP_PORT: string + /** 后台接口地址 */ + readonly VITE_SERVER_BASEURL: string + /** 微信小程序后台接口地址,不配置则使用 VITE_SERVER_BASEURL */ + readonly VITE_MP_SERVER_BASEURL?: string + /** 后端是否有统一前缀 /api */ + readonly VITE_SERVER_HAS_API_PREFIX: 'true' | 'false' + /** 认证模式,'single' | 'double' ==> 单token | 双token */ + readonly VITE_AUTH_MODE: 'single' | 'double' + /** 是否清除console */ + readonly VITE_DELETE_CONSOLE: string + // 更多环境变量... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/app/src/hooks/useRequest.ts b/app/src/hooks/useRequest.ts new file mode 100644 index 0000000..8ac4bfe --- /dev/null +++ b/app/src/hooks/useRequest.ts @@ -0,0 +1,54 @@ +import type { Ref } from 'vue' +import { ref } from 'vue' + +interface IUseRequestOptions { + /** 是否立即执行 */ + immediate?: boolean + /** 初始化数据 */ + initialData?: T +} + +interface IUseRequestReturn { + loading: Ref + error: Ref + data: Ref + run: (args?: P) => Promise +} + +/** + * useRequest是一个定制化的请求钩子,用于处理异步请求和响应。 + * @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。 + * @param options 包含请求选项的对象 {immediate, initialData}。 + * @param options.immediate 是否立即执行请求,默认为false。 + * @param options.initialData 初始化数据,默认为undefined。 + * @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。 + */ +export default function useRequest( + func: (args?: P) => Promise, + options: IUseRequestOptions = { immediate: false }, +): IUseRequestReturn { + const loading = ref(false) + const error = ref(false) + const data = ref(options.initialData) as Ref + const run = async (args?: P) => { + loading.value = true + return func(args) + .then((res) => { + data.value = res + error.value = false + return data.value + }) + .catch((err) => { + error.value = err + throw err + }) + .finally(() => { + loading.value = false + }) + } + + if (options.immediate) { + (run as (args: P) => Promise)({} as P) + } + return { loading, error, data, run } +} diff --git a/app/src/hooks/useScroll.md b/app/src/hooks/useScroll.md new file mode 100644 index 0000000..bb2eace --- /dev/null +++ b/app/src/hooks/useScroll.md @@ -0,0 +1,116 @@ +# 上拉刷新和下拉加载更多 + +在 unibest 框架中,我们通过组合 `useScroll` Hook 可结合 `scroll-view` 组件来轻松实现上拉刷新和下拉加载更多的功能。 +场景一 页面滚动 + +``` +definePage({ + style: { + navigationBarTitleText: '上拉刷新和下拉加载更多', + enablePullDownRefresh: true, + onReachBottomDistance: 100, + }, +}) +``` + +场景二 局部滚动 结合 `scroll-view` + +## 关键文件 + +- `src/hooks/useScroll.ts`: 提供了核心的滚动逻辑处理 Hook。 +- `src/pages-sub/demo/scroll.vue`: 一个具体的实现示例页面。 + +## `useScroll` Hook + +`useScroll` 是一个 Vue Composition API Hook,它封装了处理下拉刷新和上拉加载的通用逻辑。 + +### 主要功能 + +- **管理加载状态**: 自动处理 `loading`(加载中)、`finished`(已加载全部)和 `error`(加载失败)等状态。 +- **分页逻辑**: 内部维护分页参数(页码 `page` 和每页数量 `pageSize`)。 +- **事件处理**: 提供 `onScrollToLower`(滚动到底部)、`onRefresherRefresh`(下拉刷新)等方法,用于在视图层触发。 +- **数据合并**: 自动将新加载的数据追加到现有列表 `list` 中。 + +### 使用方法 + +```typescript +import { useScroll } from '@/hooks/useScroll' +import { getList } from '@/service/list' // 你的数据请求API + +const { + list, // 响应式的数据列表 + loading, // 是否加载中 + finished, // 是否已全部加载 + error, // 是否加载失败 + onScrollToLower, // 滚动到底部时触发的事件 + onRefresherRefresh, // 下拉刷新时触发的事件 +} = useScroll(getList) // 将获取数据的API函数传入 +``` + +## `scroll-view` 组件 + +`scroll-view` 是 uni-app 提供的可滚动视图区域组件,它提供了一系列属性来支持下拉刷新和上拉加载。 + +### 关键属性 + +- `scroll-y`: 允许纵向滚动。 +- `refresher-enabled`: 启用下拉刷新。 +- `refresher-triggered`: 控制下拉刷新动画的显示与隐藏,通过 `loading` 状态绑定。 +- `@scrolltolower`: 滚动到底部时触发的事件,绑定 `onScrollToLower` 方法。 +- `@refresherrefresh`: 触发下拉刷新时触发的事件,绑定 `onRefresherRefresh` 方法。 + +## 示例代码 + +以下是 `src/pages-sub/demo/scroll.vue` 中的核心代码,展示了如何将 `useScroll` 和 `scroll-view` 结合使用。 + +```vue + + + + + +``` + +## 实现步骤总结 + +1. **创建API**: 确保你有一个返回分页数据的API请求函数(例如 `getList`),它应该接受页码和页面大小作为参数。 +2. **调用 `useScroll`**: 在你的页面脚本中,导入并调用 `useScroll` Hook,将你的API函数作为参数传入。 +3. **模板绑定**: + - 使用 `scroll-view` 组件作为滚动容器。 + - 将其 `refresher-triggered` 属性绑定到 `useScroll` 返回的 `loading` 状态。 + - 将其 `@scrolltolower` 事件绑定到 `onScrollToLower` 方法。 + - 将其 `@refresherrefresh` 事件绑定到 `onRefresherRefresh` 方法。 +4. **渲染列表**: 使用 `v-for` 指令渲染 `useScroll` 返回的 `list` 数组。 +5. **添加加载提示**: 根据 `loading`, `finished`, `error` 状态,在列表底部显示不同的提示信息,提升用户体验。 + +通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。 \ No newline at end of file diff --git a/app/src/hooks/useScroll.ts b/app/src/hooks/useScroll.ts new file mode 100644 index 0000000..1563223 --- /dev/null +++ b/app/src/hooks/useScroll.ts @@ -0,0 +1,74 @@ +import type { Ref } from 'vue' +import { onMounted, ref } from 'vue' + +interface UseScrollOptions { + fetchData: (page: number, pageSize: number) => Promise + pageSize?: number +} + +interface UseScrollReturn { + list: Ref + loading: Ref + finished: Ref + error: Ref + refresh: () => Promise + loadMore: () => Promise +} + +export function useScroll({ + fetchData, + pageSize = 10, +}: UseScrollOptions): UseScrollReturn { + const list = ref([]) as Ref + const loading = ref(false) + const finished = ref(false) + const error = ref(null) + const page = ref(1) + + const loadData = async () => { + if (loading.value || finished.value) + return + + loading.value = true + error.value = null + + try { + const data = await fetchData(page.value, pageSize) + if (data.length < pageSize) { + finished.value = true + } + list.value.push(...data) + page.value++ + } + catch (err) { + error.value = err + } + finally { + loading.value = false + } + } + + const refresh = async () => { + page.value = 1 + finished.value = false + list.value = [] + await loadData() + } + + const loadMore = async () => { + await loadData() + } + + onMounted(() => { + refresh() + }) + + return { + list, + loading, + finished, + error, + refresh, + loadMore, + } +} diff --git a/app/src/hooks/useUpload.ts b/app/src/hooks/useUpload.ts new file mode 100644 index 0000000..7c9700a --- /dev/null +++ b/app/src/hooks/useUpload.ts @@ -0,0 +1,171 @@ +import { ref } from 'vue' +import { getEnvBaseUrl } from '@/utils/index' + +const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/upload` + +type TfileType = 'image' | 'file' +type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*' +type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage + +interface TOptions { + formData?: Record + maxSize?: number + accept?: T extends 'image' ? TImage[] : TFile[] + fileType?: T + success?: (params: any) => void + error?: (err: any) => void +} + +export default function useUpload(options: TOptions = {} as TOptions) { + const { + formData = {}, + maxSize = 5 * 1024 * 1024, + accept = ['*'], + fileType = 'image', + success, + error: onError, + } = options + + const loading = ref(false) + const error = ref(null) + const data = ref(null) + + const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => { + if (size > maxSize) { + uni.showToast({ + title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`, + icon: 'none', + }) + return + } + + // const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase() + // const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension) + + // if (!isTypeValid) { + // uni.showToast({ + // title: `仅支持 ${accept.join(', ')} 格式的文件`, + // icon: 'none', + // }) + // return + // } + + loading.value = true + uploadFile({ + tempFilePath, + formData, + onSuccess: (res) => { + // 修改这里的解析逻辑,适应不同平台的返回格式 + let parsedData = res + try { + // 尝试解析为JSON + const jsonData = JSON.parse(res) + // 检查是否包含data字段 + parsedData = jsonData.data || jsonData + } + catch (e) { + // 如果解析失败,使用原始数据 + console.log('Response is not JSON, using raw data:', res) + } + data.value = parsedData + // console.log('上传成功', res) + success?.(parsedData) + }, + onError: (err) => { + error.value = err + onError?.(err) + }, + onComplete: () => { + loading.value = false + }, + }) + } + + const run = () => { + // 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。 + // 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议 + const chooseFileOptions = { + count: 1, + success: (res: any) => { + console.log('File selected successfully:', res) + // 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]} + // h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]} + // h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"} + // App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]} + // App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976} + let tempFilePath = '' + let size = 0 + // #ifdef MP-WEIXIN + tempFilePath = res.tempFiles[0].tempFilePath + size = res.tempFiles[0].size + // #endif + // #ifndef MP-WEIXIN + tempFilePath = res.tempFilePaths[0] + size = res.tempFiles[0].size + // #endif + handleFileChoose({ tempFilePath, size }) + }, + fail: (err: any) => { + console.error('File selection failed:', err) + error.value = err + onError?.(err) + }, + } + + if (fileType === 'image') { + // #ifdef MP-WEIXIN + uni.chooseMedia({ + ...chooseFileOptions, + mediaType: ['image'], + }) + // #endif + + // #ifndef MP-WEIXIN + uni.chooseImage(chooseFileOptions) + // #endif + } + else { + uni.chooseFile({ + ...chooseFileOptions, + type: 'all', + }) + } + } + + return { loading, error, data, run } +} + +async function uploadFile({ + tempFilePath, + formData, + onSuccess, + onError, + onComplete, +}: { + tempFilePath: string + formData: Record + onSuccess: (data: any) => void + onError: (err: any) => void + onComplete: () => void +}) { + uni.uploadFile({ + url: VITE_UPLOAD_BASEURL, + filePath: tempFilePath, + name: 'file', + formData, + success: (uploadFileRes) => { + try { + const data = uploadFileRes.data + onSuccess(data) + } + catch (err) { + onError(err) + } + }, + fail: (err) => { + console.error('Upload failed:', err) + onError(err) + }, + complete: onComplete, + }) +} diff --git a/app/src/http/README.md b/app/src/http/README.md new file mode 100644 index 0000000..5bb5a4f --- /dev/null +++ b/app/src/http/README.md @@ -0,0 +1,13 @@ +# 请求库 + +目前unibest支持3种请求库: +- 菲鸽简单封装的 `简单版本http`,路径(src/http/http.ts),对应的示例在 src/api/foo.ts +- `alova 的 http`,路径(src/http/alova.ts),对应的示例在 src/api/foo-alova.ts +- `vue-query`, 路径(src/http/vue-query.ts), 目前主要用在自动生成接口,详情看(https://unibest.tech/base/17-generate),示例在 src/service/app 文件夹 + +## 如何选择 +如果您以前用过 alova 或者 vue-query,可以优先使用您熟悉的。 +如果您的项目简单,简单版本的http 就够了,也不会增加包体积。(发版的时候可以去掉alova和vue-query,如果没有超过包体积,留着也无所谓 ^_^) + +## roadmap +菲鸽最近在优化脚手架,后续可以选择是否使用第三方的请求库,以及选择什么请求库。还在开发中,大概月底出来(8月31号)。 \ No newline at end of file diff --git a/app/src/http/alova.ts b/app/src/http/alova.ts new file mode 100644 index 0000000..84940f6 --- /dev/null +++ b/app/src/http/alova.ts @@ -0,0 +1,122 @@ +import type { uniappRequestAdapter } from '@alova/adapter-uniapp' +import type { IResponse } from './types' +import AdapterUniapp from '@alova/adapter-uniapp' +import { createAlova } from 'alova' +import { createServerTokenAuthentication } from 'alova/client' +import VueHook from 'alova/vue' +import { toLoginPage } from '@/utils/toLoginPage' +import { getEnvBaseUrl } from '@/utils' +import { ContentTypeEnum, getHttpErrorMessage, REQUEST_ERROR_MESSAGE, ResultEnum, ShowMessage } from './tools/enum' + +// 配置动态Tag +export const API_DOMAINS = { + DEFAULT: getEnvBaseUrl(), +} + +/** + * 创建请求实例 + */ +const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication< + typeof VueHook, + typeof uniappRequestAdapter +>({ + // 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛 + refreshTokenOnError: { + isExpired: (error) => { + return error.response?.status === ResultEnum.Unauthorized + }, + handler: async () => { + try { + // await authLogin(); + } + catch (error) { + // 切换到登录页 + toLoginPage({ mode: 'reLaunch' }) + throw error + } + }, + }, +}) + +/** + * alova 请求实例 + */ +const alovaInstance = createAlova({ + baseURL: API_DOMAINS.DEFAULT, + ...AdapterUniapp(), + timeout: 5000, + statesHook: VueHook, + + beforeRequest: onAuthRequired((method) => { + // 设置默认 Content-Type + method.config.headers = { + ContentType: ContentTypeEnum.JSON, + Accept: 'application/json, text/plain, */*', + ...method.config.headers, + } + + const { config } = method + const ignoreAuth = !config.meta?.ignoreAuth + console.log('ignoreAuth===>', ignoreAuth) + // 处理认证信息 自行处理认证问题 + if (ignoreAuth) { + const token = 'getToken()' + if (!token) { + throw new Error('[请求错误]:未登录') + } + // method.config.headers.token = token; + } + + // 处理动态域名 + if (config.meta?.domain) { + method.baseURL = config.meta.domain + console.log('当前域名', method.baseURL) + } + }), + + responded: onResponseRefreshToken((response, method) => { + const { config } = method + const { requestType } = config + const { + statusCode, + data: rawData, + errMsg, + } = response as UniNamespace.RequestSuccessCallbackResult + + // 处理特殊请求类型(上传/下载) + if (requestType === 'upload' || requestType === 'download') { + return response + } + + // 处理 HTTP 状态码错误 + if (statusCode !== 200) { + const errorMessage = ShowMessage(statusCode) || REQUEST_ERROR_MESSAGE + console.error('errorMessage===>', errorMessage) + uni.showToast({ + title: errorMessage, + icon: 'none', + }) + console.error('[ALOVA REQUEST ERROR]', { statusCode, errMsg, url: method.url }) + throw new Error(errorMessage) + } + + // 处理业务逻辑错误 + const { code, message, data } = rawData as IResponse + // 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum + if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) { + const errorMessage = getHttpErrorMessage(rawData, REQUEST_ERROR_MESSAGE) + if (config.meta?.toast !== false) { + uni.showToast({ + title: errorMessage, + icon: 'none', + }) + } + console.error('[ALOVA BUSINESS ERROR]', { code, message, url: method.url }) + throw new Error(errorMessage) + } + // 处理成功响应,返回业务数据 + return data + }), +}) + +export const http = alovaInstance diff --git a/app/src/http/error.ts b/app/src/http/error.ts new file mode 100644 index 0000000..25780d5 --- /dev/null +++ b/app/src/http/error.ts @@ -0,0 +1,7 @@ +export { + getHttpErrorMessage, + NETWORK_ERROR_MESSAGE, + normalizeErrorText, + REQUEST_ERROR_MESSAGE, + toUserRequestError, +} from './tools/enum' diff --git a/app/src/http/http.ts b/app/src/http/http.ts new file mode 100644 index 0000000..ce80c87 --- /dev/null +++ b/app/src/http/http.ts @@ -0,0 +1,227 @@ +import type { IDoubleTokenRes } from '@/api/types/login' +import type { CustomRequestOptions, IResponse } from '@/http/types' +import { nextTick } from 'vue' +import { useTokenStore } from '@/store/token' +import { isDoubleTokenMode } from '@/utils' +import { toLoginPage } from '@/utils/toLoginPage' +import { getHttpErrorMessage, NETWORK_ERROR_MESSAGE, REQUEST_ERROR_MESSAGE, toUserRequestError } from './error' +import { ResultEnum } from './tools/enum' + +let refreshing = false +let taskQueue: (() => void)[] = [] +const DEFAULT_REQUEST_TIMEOUT = 30000 +const REQUEST_TIMEOUT_BUFFER = 1000 + +function resolveRequestTimeout(timeout?: number) { + const value = Number(timeout) + return Number.isFinite(value) && value > 0 ? value : DEFAULT_REQUEST_TIMEOUT +} + +function showErrorToast(options: CustomRequestOptions, message: string) { + if (options.hideErrorToast) + return + uni.showToast({ + icon: 'none', + title: message, + }) +} + +function showRequestFailDebug(options: CustomRequestOptions, err: UniApp.GeneralCallbackResult) { + console.error('[APP REQUEST FAIL]', { + url: options.url, + method: options.method, + errMsg: err.errMsg, + err, + }) +} + +export function http(options: CustomRequestOptions) { + return new Promise((resolve, reject) => { + let settled = false + let requestTask: UniApp.RequestTask | undefined + let timeoutTimer: ReturnType | undefined + const timeout = resolveRequestTimeout(options.timeout) + + function clearRequestTimeout() { + if (timeoutTimer) { + clearTimeout(timeoutTimer) + timeoutTimer = undefined + } + } + + function finishResolve(value: T | PromiseLike) { + if (settled) + return + settled = true + clearRequestTimeout() + resolve(value) + } + + function finishReject(error: unknown) { + if (settled) + return + settled = true + clearRequestTimeout() + reject(error) + } + + const requestOptions: CustomRequestOptions = { + ...options, + timeout, + dataType: 'json', + // #ifndef MP-WEIXIN + responseType: 'json', + // #endif + success: async (res) => { + if (settled) + return + try { + const responseData = res.data as IResponse + const code = Number((responseData as any)?.code) + const isTokenExpired = res.statusCode === 401 || code === 401 + + if (isTokenExpired) { + const tokenStore = useTokenStore() + if (!isDoubleTokenMode) { + await tokenStore.logout() + toLoginPage() + return finishReject(responseData || res) + } + + const { refreshToken } = (tokenStore.tokenInfo as IDoubleTokenRes) || {} + if (refreshToken) { + taskQueue.push(() => { + finishResolve(http(options)) + }) + } + + if (refreshToken && !refreshing) { + refreshing = true + try { + await tokenStore.refreshToken() + refreshing = false + nextTick(() => { + uni.hideToast() + }) + taskQueue.forEach(task => task()) + } + catch (refreshErr) { + console.error('刷新 token 失败:', refreshErr) + refreshing = false + showErrorToast(options, '登录已过期,请重新登录') + await tokenStore.logout() + setTimeout(() => { + toLoginPage() + }, 1200) + } + finally { + taskQueue = [] + } + } + + return finishReject(responseData || res) + } + + if (res.statusCode >= 200 && res.statusCode < 300) { + if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) { + const message = getHttpErrorMessage(responseData, REQUEST_ERROR_MESSAGE) + showErrorToast(options, message) + return finishReject({ ...responseData, msg: message }) + } + return finishResolve(responseData.data) + } + + const message = getHttpErrorMessage(res.data, REQUEST_ERROR_MESSAGE, res.statusCode) + showErrorToast(options, message) + finishReject({ ...res, msg: message, message, errMsg: message }) + } + catch (error) { + const message = getHttpErrorMessage(error, REQUEST_ERROR_MESSAGE) + showErrorToast(options, message) + finishReject(toUserRequestError(error, message)) + } + }, + fail(err) { + if (settled) + return + showRequestFailDebug(requestOptions, err) + const message = getHttpErrorMessage(err, NETWORK_ERROR_MESSAGE) + showErrorToast(requestOptions, message) + finishReject(toUserRequestError(err, message)) + }, + } + + timeoutTimer = setTimeout(() => { + if (settled) + return + settled = true + clearRequestTimeout() + const error = { errMsg: `request:fail timeout after ${timeout}ms` } + requestTask?.abort?.() + showRequestFailDebug(requestOptions, error) + showErrorToast(requestOptions, NETWORK_ERROR_MESSAGE) + reject(toUserRequestError(error, NETWORK_ERROR_MESSAGE)) + }, timeout + REQUEST_TIMEOUT_BUFFER) + + try { + requestTask = uni.request(requestOptions) as unknown as UniApp.RequestTask + } + catch (error) { + const message = getHttpErrorMessage(error, NETWORK_ERROR_MESSAGE) + showErrorToast(requestOptions, message) + finishReject(toUserRequestError(error, message)) + } + }) +} + +export function httpGet(url: string, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + query, + method: 'GET', + header, + ...options, + }) +} + +export function httpPost(url: string, data?: Record, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + query, + data, + method: 'POST', + header, + ...options, + }) +} + +export function httpPut(url: string, data?: Record, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + data, + query, + method: 'PUT', + header, + ...options, + }) +} + +export function httpDelete(url: string, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + query, + method: 'DELETE', + header, + ...options, + }) +} + +http.get = httpGet +http.post = httpPost +http.put = httpPut +http.delete = httpDelete + +http.Get = httpGet +http.Post = httpPost +http.Put = httpPut +http.Delete = httpDelete diff --git a/app/src/http/interceptor.ts b/app/src/http/interceptor.ts new file mode 100644 index 0000000..896366a --- /dev/null +++ b/app/src/http/interceptor.ts @@ -0,0 +1,58 @@ +import type { CustomRequestOptions } from '@/http/types' +import { useTokenStore } from '@/store' +import { getEnvBaseUrl } from '@/utils' +import { stringifyQuery } from './tools/queryString' + +// 请求基准地址 +const baseUrl = getEnvBaseUrl() + +// 拦截器配置 +const httpInterceptor = { + // 拦截前触发 + invoke(options: CustomRequestOptions) { + // 如果您使用了alova,则请把下面的代码放开注释 + // alova 执行流程:alova beforeRequest --> 本拦截器 --> alova responded + // return options + + // 非 alova 请求,正常执行 + // 接口请求支持通过 query 参数配置 queryString + if (options.query) { + const queryStr = stringifyQuery(options.query) + if (options.url.includes('?')) { + options.url += `&${queryStr}` + } + else { + options.url += `?${queryStr}` + } + } + // 非 http 开头需拼接地址 + if (!options.url.startsWith('http')) { + options.url = baseUrl + options.url + // TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址 + } + // 1. 请求超时:保留业务请求显式设置的超时时间。 + if (!options.timeout || options.timeout <= 0) + options.timeout = 60000 + // 2. (可选)添加小程序端请求头标识 + options.header = { + ...options.header, + } + // 3. 添加 token 请求头标识 + const tokenStore = useTokenStore() + const token = tokenStore.updateNowTime().validToken + + if (token) { + options.header.Authorization = `Bearer ${token}` + } + return options + }, +} + +export const requestInterceptor = { + install() { + // 拦截 request 请求 + uni.addInterceptor('request', httpInterceptor) + // 拦截 uploadFile 文件上传 + uni.addInterceptor('uploadFile', httpInterceptor) + }, +} diff --git a/app/src/http/tools/enum.ts b/app/src/http/tools/enum.ts new file mode 100644 index 0000000..8ea91ff --- /dev/null +++ b/app/src/http/tools/enum.ts @@ -0,0 +1,129 @@ +export enum ResultEnum { + // 0和200当做成功都很普遍,这里直接兼容两者(PS:0和200通常都不会当做错误码,但是有的接口会返回0,有的接口会返回200) + Success0 = 0, // 成功 + Success200 = 200, // 成功 + Error = 400, // 错误 + Unauthorized = 401, // 未授权 + Forbidden = 403, // 禁止访问(原为forbidden) + NotFound = 404, // 未找到(原为notFound) + MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed) + RequestTimeout = 408, // 请求超时(原为requestTimeout) + InternalServerError = 500, // 服务器错误(原为internalServerError) + NotImplemented = 501, // 未实现(原为notImplemented) + BadGateway = 502, // 网关错误(原为badGateway) + ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable) + GatewayTimeout = 504, // 网关超时(原为gatewayTimeout) + HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported) +} +export enum ContentTypeEnum { + JSON = 'application/json;charset=UTF-8', + FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', + FORM_DATA = 'multipart/form-data;charset=UTF-8', +} + +export const NETWORK_ERROR_MESSAGE = '网络异常,请检查网络后重试' +export const REQUEST_ERROR_MESSAGE = '请求处理失败,请稍后重试' + +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 + +/** + * 根据状态码,生成对应的错误信息 + * @param {number|string} status 状态码 + * @returns {string} 错误信息 + */ +export function ShowMessage(status: number | string): string { + let message: string + switch (status) { + case 400: + message = '请求错误(400)' + break + case 401: + message = '未授权,请重新登录(401)' + break + case 403: + message = '拒绝访问(403)' + break + case 404: + message = '请求出错(404)' + break + case 408: + message = '请求超时(408)' + break + case 500: + message = '服务器错误(500)' + break + case 501: + message = '服务未实现(501)' + break + case 502: + message = '网络错误(502)' + break + case 503: + message = '服务不可用(503)' + break + case 504: + message = '网络超时(504)' + break + case 505: + message = 'HTTP版本不受支持(505)' + break + default: + message = `连接出错(${status})!` + } + return `${message},请检查网络或联系管理员!` +} + +export function normalizeErrorText(value: unknown, fallback = REQUEST_ERROR_MESSAGE) { + if (typeof value !== 'string') { + return fallback + } + const message = value.trim().replace(/^Error:\s*/i, '') + if (!message) { + return fallback + } + if (/^\d+$/.test(message)) { + return ShowMessage(Number(message)) + } + if (networkErrorPattern.test(message)) { + return NETWORK_ERROR_MESSAGE + } + if (technicalDetailPattern.test(message)) { + return fallback + } + return message +} + +export function getHttpErrorMessage(data: any, fallback = REQUEST_ERROR_MESSAGE, statusCode?: number) { + if (data instanceof Error) { + return normalizeErrorText(data.message, fallback) + } + + const directMessage = data?.msg || data?.message || data?.data?.msg || data?.data?.message + if (directMessage) { + return normalizeErrorText(directMessage, fallback) + } + + const rawErrMsg = data?.errMsg + if (rawErrMsg) { + return normalizeErrorText(rawErrMsg, fallback) + } + + const code = Number(data?.code || statusCode || data?.statusCode) + if (Number.isFinite(code) && code >= 400) { + return ShowMessage(code) + } + return fallback +} + +export function toUserRequestError(error: unknown, message: string) { + return { + rawError: error, + msg: message, + message, + errMsg: message, + } +} diff --git a/app/src/http/tools/queryString.ts b/app/src/http/tools/queryString.ts new file mode 100644 index 0000000..edf973e --- /dev/null +++ b/app/src/http/tools/queryString.ts @@ -0,0 +1,29 @@ +/** + * 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积 + * 支持基本类型值和数组,不支持嵌套对象 + * @param obj 要序列化的对象 + * @returns 序列化后的查询字符串 + */ +export function stringifyQuery(obj: Record): string { + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) + return '' + + return Object.entries(obj) + .filter(([_, value]) => value !== undefined && value !== null) + .map(([key, value]) => { + // 对键进行编码 + const encodedKey = encodeURIComponent(key) + + // 处理数组类型 + if (Array.isArray(value)) { + return value + .filter(item => item !== undefined && item !== null) + .map(item => `${encodedKey}=${encodeURIComponent(item)}`) + .join('&') + } + + // 处理基本类型 + return `${encodedKey}=${encodeURIComponent(value)}` + }) + .join('&') +} diff --git a/app/src/http/types.ts b/app/src/http/types.ts new file mode 100644 index 0000000..8187822 --- /dev/null +++ b/app/src/http/types.ts @@ -0,0 +1,44 @@ +/** + * 在 uniapp 的 RequestOptions 和 IUniUploadFileOptions 基础上,添加自定义参数 + */ +export type CustomRequestOptions = UniApp.RequestOptions & { + query?: Record + /** 出错时是否隐藏错误提示 */ + hideErrorToast?: boolean +} & IUniUploadFileOptions // 添加uni.uploadFile参数类型 + +/** 主要提供给 openapi-ts-request 生成的代码使用 */ +export type CustomRequestOptions_ = Omit + +export interface HttpRequestResult { + promise: Promise + requestTask: UniApp.RequestTask +} + +// 通用响应格式(兼容 msg + message 字段) +export type IResponse = { + code: number + data: T + message: string + [key: string]: any // 允许额外属性 +} | { + code: number + data: T + msg: string + [key: string]: any // 允许额外属性 +} + +// 分页请求参数 +export interface PageParams { + page: number + pageSize: number + [key: string]: any +} + +// 分页响应数据 +export interface PageResult { + list: T[] + total: number + page: number + pageSize: number +} diff --git a/app/src/http/vue-query.ts b/app/src/http/vue-query.ts new file mode 100644 index 0000000..69ca80d --- /dev/null +++ b/app/src/http/vue-query.ts @@ -0,0 +1,30 @@ +import type { CustomRequestOptions } from '@/http/types' +import { http } from './http' + +/* + * openapi-ts-request 工具的 request 跨客户端适配方法 + */ +export default function request( + url: string, + options: Omit & { + params?: Record + headers?: Record + }, +) { + const requestOptions = { + url, + ...options, + } + + if (options.params) { + requestOptions.query = requestOptions.params + delete requestOptions.params + } + + if (options.headers) { + requestOptions.header = options.headers + delete requestOptions.headers + } + + return http(requestOptions) +} diff --git a/app/src/layouts/blank.vue b/app/src/layouts/blank.vue new file mode 100644 index 0000000..4d3372d --- /dev/null +++ b/app/src/layouts/blank.vue @@ -0,0 +1,9 @@ + + + diff --git a/app/src/layouts/default.vue b/app/src/layouts/default.vue new file mode 100644 index 0000000..c810df1 --- /dev/null +++ b/app/src/layouts/default.vue @@ -0,0 +1,5 @@ + diff --git a/app/src/locale/README.md b/app/src/locale/README.md new file mode 100644 index 0000000..1e77098 --- /dev/null +++ b/app/src/locale/README.md @@ -0,0 +1,12 @@ +# 注意事项 + +> 文件夹名字必须为 `locale`, 这是 `uniapp` 官方约定的,如果改为别的,标题将不能正常切换多语言(其他内容还是正常)。 +> +> `xxx.json` 的 `xxx` 多语言标识必须与 `uniapp` 官方约定的一致,否则也会出现 BUG。 +> +> 查看截图 `screenshots/i18n.png`。 + +## 参考文档 + +[uniapp 国际化开发指南](https://uniapp.dcloud.net.cn/tutorial/i18n.html) +[uniapp 国际化-注意事项](https://uniapp.dcloud.net.cn/api/ui/locale.html#onlocalechange) 最下面的注意事项 diff --git a/app/src/locale/en.json b/app/src/locale/en.json new file mode 100644 index 0000000..f2a758f --- /dev/null +++ b/app/src/locale/en.json @@ -0,0 +1,11 @@ +{ + "tabbar.home": "Home", + "tabbar.about": "About", + "tabbar.me": "Me", + "tabbar.i18n": "I18n", + "i18n.title": "En Title", + "alova.title": "Alova Request", + "weight": "{heavy}KG", + "detail": "{0}cm, {1}KG", + "introduction": "I am {name},height:{detail.height},weight:{detail.weight}" +} diff --git a/app/src/locale/index.ts b/app/src/locale/index.ts new file mode 100644 index 0000000..56e0540 --- /dev/null +++ b/app/src/locale/index.ts @@ -0,0 +1,81 @@ +import { createI18n } from 'vue-i18n' + +import en from './en.json' +import zhHans from './zh-Hans.json' // 简体中文 + +const messages = { + en, + 'zh-Hans': zhHans, // key 不能乱写,查看uniapp官网 +} + +const i18n = createI18n({ + locale: uni.getLocale(), // 获取已设置的语言,fallback 语言需要再 manifest.config.ts 中设置 + messages, + allowComposition: true, +}) + +console.log(uni.getLocale()) +console.log(i18n.global.locale) + +/** + * 可以拿到原始的语言模板,非 vue 文件使用这个方法, + * @param { string } key 多语言的key,eg: "app.name" + * @returns {string} 返回原始的多语言模板,eg: "{heavy}KG" + */ +export function getTemplateByKey(key: string) { + if (!key) { + console.error(`[i18n] Function getTemplateByKey(), key param is required`) + return '' + } + const locale = uni.getLocale() + console.log('locale:', locale) + + const message = messages[locale] // 拿到某个多语言的所有模板(是一个对象) + if (Object.keys(message).includes(key)) { + return message[key] + } + + try { + const keyList = key.split('.') + return keyList.reduce((pre, cur) => { + return pre[cur] + }, message) + } + catch (error) { + console.error(`[i18n] Function getTemplateByKey(), key param ${key} is not existed.`) + return '' + } +} + +/** + * formatI18n('我是{name},身高{detail.height},体重{detail.weight}',{name:'张三',detail:{height:178,weight:'75kg'}}) + * 暂不支持数组 + * @param template 多语言模板字符串,eg: `我是{name}` + * @param {object | undefined} data 需要传递的数据对象,里面的key与多语言字符串对应,eg: `{name:'菲鸽'}` + * @returns + */ +function formatI18n(template: string, data?: any) { + return template.replace(/\{([^}]+)\}/g, (match, key: string) => { + // console.log( match, key) // => { detail.height } detail.height + const arr = key.trim().split('.') + let result = data + while (arr.length) { + const first = arr.shift() + result = result[first] + } + return result + }) +} + +/** + * t('introduction',{name:'张三',detail:{height:178,weight:'75kg'}}) + * => formatI18n('我是{name},身高{detail.height},体重{detail.weight}',{name:'张三',detail:{height:178,weight:'75kg'}}) + * 没有key的,可以不传 data;暂不支持数组 + * @param template 多语言模板字符串,eg: `我是{name}` + * @param {object | undefined} data 需要传递的数据对象,里面的key与多语言字符串对应,eg: `{name:'菲鸽'}` + * @returns + */ +export function t(key, data?) { + return formatI18n(getTemplateByKey(key), data) +} +export default i18n diff --git a/app/src/locale/zh-Hans.json b/app/src/locale/zh-Hans.json new file mode 100644 index 0000000..dfb655d --- /dev/null +++ b/app/src/locale/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "tabbar.home": "首页", + "tabbar.about": "关于", + "tabbar.me": "我的", + "tabbar.i18n": "多语言", + "i18n.title": "中文标题", + "alova.title": "Alova 请求", + "weight": "{heavy}公斤", + "detail": "{0}cm, {1}公斤", + "introduction": "我是 {name},身高:{detail.height},体重:{detail.weight}" +} diff --git a/app/src/main.ts b/app/src/main.ts new file mode 100644 index 0000000..ef771e1 --- /dev/null +++ b/app/src/main.ts @@ -0,0 +1,22 @@ +import { createSSRApp } from 'vue' +import App from './App.vue' +import { requestInterceptor } from './http/interceptor' +import { routeInterceptor } from './router/interceptor' + +import store from './store' +import '@/style/index.scss' +import 'go-captcha-uni/assets/css/gocaptcha.css' +import 'virtual:uno.css' +import i18n from './locale/index' + +export function createApp() { + const app = createSSRApp(App) + app.use(store) + app.use(routeInterceptor) + app.use(requestInterceptor) + app.use(i18n) + + return { + app, + } +} diff --git a/app/src/manifest.json b/app/src/manifest.json new file mode 100644 index 0000000..05eeedf --- /dev/null +++ b/app/src/manifest.json @@ -0,0 +1,138 @@ +{ + "name": "山东平台物联卡", + "appid": "__UNI__YOURAPPID", + "description": "", + "versionName": "1.0.0", + "versionCode": "100", + "transformPx": false, + "app-plus": { + "usingComponents": true, + "nvueStyleCompiler": "uni-app", + "compilerVersion": 3, + "splashscreen": { + "alwaysShowBeforeRender": true, + "waiting": true, + "autoclose": true, + "delay": 0 + }, + "modules": {}, + "distribute": { + "android": { + "permissions": [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ], + "minSdkVersion": 21, + "targetSdkVersion": 30, + "abiFilters": [ + "armeabi-v7a", + "arm64-v8a" + ] + }, + "ios": {}, + "sdkConfigs": {}, + "icons": { + "android": { + "hdpi": "static/app/icons/72x72.png", + "xhdpi": "static/app/icons/96x96.png", + "xxhdpi": "static/app/icons/144x144.png", + "xxxhdpi": "static/app/icons/192x192.png" + }, + "ios": { + "appstore": "static/app/icons/1024x1024.png", + "ipad": { + "app": "static/app/icons/76x76.png", + "app@2x": "static/app/icons/152x152.png", + "notification": "static/app/icons/20x20.png", + "notification@2x": "static/app/icons/40x40.png", + "proapp@2x": "static/app/icons/167x167.png", + "settings": "static/app/icons/29x29.png", + "settings@2x": "static/app/icons/58x58.png", + "spotlight": "static/app/icons/40x40.png", + "spotlight@2x": "static/app/icons/80x80.png" + }, + "iphone": { + "app@2x": "static/app/icons/120x120.png", + "app@3x": "static/app/icons/180x180.png", + "notification@2x": "static/app/icons/40x40.png", + "notification@3x": "static/app/icons/60x60.png", + "settings@2x": "static/app/icons/58x58.png", + "settings@3x": "static/app/icons/87x87.png", + "spotlight@2x": "static/app/icons/80x80.png", + "spotlight@3x": "static/app/icons/120x120.png" + } + } + } + }, + "compatible": { + "ignoreVersion": true + } + }, + "app-harmony": { + "distribute": {} + }, + "mp-harmony": { + "distribute": {} + }, + "quickapp": {}, + "mp-weixin": { + "appid": "YOUR_WX_APPID", + "setting": { + "urlCheck": false, + "es6": true, + "minified": true + }, + "usingComponents": true, + "optimization": { + "subPackages": true + }, + "mergeVirtualHostAttributes": true, + "styleIsolation": "shared", + "lazyCodeLoading": "requiredComponents" + }, + "mp-alipay": { + "usingComponents": true, + "styleIsolation": "shared", + "optimization": { + "subPackages": true + }, + "compileOptions": { + "globalObjectMode": "enable", + "transpile": { + "script": { + "ignore": [ + "node_modules/**" + ] + } + } + } + }, + "mp-baidu": { + "usingComponents": true + }, + "mp-toutiao": { + "usingComponents": true + }, + "uniStatistics": { + "enable": false + }, + "vueVersion": "3", + "h5": { + "router": { + "base": "/app/" + } + } +} \ No newline at end of file diff --git a/app/src/pages.json b/app/src/pages.json new file mode 100644 index 0000000..34fd6e3 --- /dev/null +++ b/app/src/pages.json @@ -0,0 +1,345 @@ +{ + "globalStyle": { + "navigationStyle": "default", + "navigationBarTitleText": "山东平台物联卡", + "navigationBarBackgroundColor": "#f8f8f8", + "navigationBarTextStyle": "black", + "backgroundColor": "#FFFFFF" + }, + "easycom": { + "autoscan": true, + "custom": { + "^fg-(.*)": "@/components/fg-$1/fg-$1.vue", + "^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue", + "^wd-(.*)": "@wot-ui/ui/components/wd-$1/wd-$1.vue" + } + }, + "pages": [ + // #ifdef H5 || MP-WEIXIN + { + "path": "pages/home/index", + "type": "home", + "style": { + "navigationStyle": "default", + "navigationBarTitleText": "山东平台物联卡", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black" + }, + "layout": false + }, + // #endif + { + "path": "pages/business/index", + "type": "page", + "style": { + "navigationBarTitleText": "办理" + }, + "layout": "default" + }, + { + "path": "pages/card/index", + "type": "page", + "style": { + "navigationBarTitleText": "卡片" + }, + "layout": "default" + }, + { + "path": "pages/login/index", + "type": "page", + "name": "Login", + "style": { + "navigationBarTitleText": "山东平台物联卡", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black", + "disableScroll": true + }, + "layout": false + }, + { + "path": "pages/message/index", + "type": "page", + "style": { + "navigationBarTitleText": "消息" + }, + "layout": "default" + }, + { + "path": "pages/user/index", + "type": "page", + "excludeLoginPath": true, + "style": { + "navigationBarTitleText": "我的", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black" + }, + "layout": "blank" + }, + { + "path": "pages/login/forgot-password/index", + "type": "page", + "name": "LoginForgotPassword", + "style": { + "navigationBarTitleText": "找回密码" + }, + "layout": false + }, + { + "path": "pages/login/register/index", + "type": "page", + "name": "LoginRegister", + "style": { + "navigationBarTitleText": "注册账号" + }, + "layout": false + } + ], + "subPackages": [ + { + "root": "pages/business/views", + "pages": [ + // GENERATED BY UNI-PAGES, PLATFORM: H5 || MP-WEIXIN + { + "path": "addon/index", + "type": "page", + "style": { + "navigationBarTitleText": "加油包" + } + }, + { + "path": "allocate/index", + "type": "page", + "style": { + "navigationBarTitleText": "批量卡片划拨" + } + }, + { + "path": "balanceRecharge/index", + "type": "page", + "style": { + "navigationBarTitleText": "余额充值" + } + }, + { + "path": "cardRenew/index", + "type": "page", + "style": { + "navigationBarTitleText": "单卡套餐续费" + } + }, + { + "path": "change/index", + "type": "page", + "style": { + "navigationBarTitleText": "套餐变更" + } + }, + { + "path": "customer/index", + "type": "page", + "style": { + "navigationBarTitleText": "客户管理" + } + }, + { + "path": "customerDetail/index", + "type": "page", + "style": { + "navigationBarTitleText": "客户详情" + } + }, + { + "path": "diagnosis/index", + "type": "page", + "style": { + "navigationBarTitleText": "卡片排查" + } + }, + { + "path": "order/index", + "type": "page", + "style": { + "navigationBarTitleText": "我的订单" + } + }, + { + "path": "renew/index", + "type": "page", + "style": { + "navigationBarTitleText": "批量套餐续费" + } + }, + { + "path": "warning/index", + "type": "page", + "style": { + "navigationBarTitleText": "预警配置" + } + }, + { + "path": "workOrder/index", + "type": "page", + "style": { + "navigationBarTitleText": "工单列表" + } + }, + { + "path": "workOrderCreate/index", + "type": "page", + "style": { + "navigationBarTitleText": "提交工单" + } + }, + { + "path": "workOrderDetail/index", + "type": "page", + "style": { + "navigationBarTitleText": "工单详情" + } + } + ] + }, + { + "root": "pages/card/views", + "pages": [ + // GENERATED BY UNI-PAGES, PLATFORM: H5 || MP-WEIXIN + { + "path": "detail/index", + "type": "page", + "style": { + "navigationBarTitleText": "卡片详情" + } + }, + { + "path": "management/index", + "type": "page", + "style": { + "navigationBarTitleText": "全部设备" + }, + "layout": "default" + }, + { + "path": "packageDetail/index", + "type": "page", + "style": { + "navigationBarTitleText": "卡片详情" + } + }, + { + "path": "warning/index", + "type": "page", + "style": { + "navigationBarTitleText": "预警配置" + } + } + ] + }, + { + "root": "pages/message/views", + "pages": [ + // GENERATED BY UNI-PAGES, PLATFORM: H5 || MP-WEIXIN + { + "path": "cardDetail/index", + "type": "page", + "style": { + "navigationBarTitleText": "消息详情" + } + }, + { + "path": "systemDetail/index", + "type": "page", + "style": { + "navigationBarTitleText": "消息详情" + } + } + ] + }, + { + "root": "pages/user/views", + "pages": [ + // GENERATED BY UNI-PAGES, PLATFORM: H5 || MP-WEIXIN + { + "path": "about/index", + "type": "page", + "style": { + "navigationBarTitleText": "关于我们" + } + }, + { + "path": "agreement/index", + "type": "page", + "style": { + "navigationBarTitleText": "协议与隐私" + } + }, + { + "path": "contact/index", + "type": "page", + "style": { + "navigationBarTitleText": "联系客服" + } + }, + { + "path": "feedback/index", + "type": "page", + "style": { + "navigationBarTitleText": "帮助与反馈" + } + }, + { + "path": "notification/index", + "type": "page", + "style": { + "navigationBarTitleText": "微信提醒" + } + }, + { + "path": "profile/index", + "type": "page", + "excludeLoginPath": true, + "style": { + "navigationBarTitleText": "账号资料" + } + }, + { + "path": "realName/index", + "type": "page", + "style": { + "navigationBarTitleText": "实名信息" + } + } + ] + } + ], + "tabBar": { + "custom": true, + "color": "#94a3b8", + "selectedColor": "#2563eb", + "backgroundColor": "#F8F8F8", + "borderStyle": "black", + "height": "50px", + "fontSize": "10px", + "iconWidth": "24px", + "spacing": "3px", + "list": [ + // #ifdef H5 || MP-WEIXIN + { + "text": "首页", + "pagePath": "pages/home/index" + }, + // #endif + { + "text": "设备", + "pagePath": "pages/card/index" + }, + { + "text": "办理", + "pagePath": "pages/business/index" + }, + { + "text": "我的", + "pagePath": "pages/user/index" + } + ] + } +} diff --git a/app/src/pages/business/components/BusinessHero.vue b/app/src/pages/business/components/BusinessHero.vue new file mode 100644 index 0000000..800fd3d --- /dev/null +++ b/app/src/pages/business/components/BusinessHero.vue @@ -0,0 +1,39 @@ + + + diff --git a/app/src/pages/business/components/BusinessSection.vue b/app/src/pages/business/components/BusinessSection.vue new file mode 100644 index 0000000..c74d6a0 --- /dev/null +++ b/app/src/pages/business/components/BusinessSection.vue @@ -0,0 +1,216 @@ + + + + + diff --git a/app/src/pages/business/data.test.ts b/app/src/pages/business/data.test.ts new file mode 100644 index 0000000..0cce706 --- /dev/null +++ b/app/src/pages/business/data.test.ts @@ -0,0 +1,200 @@ +import fs from 'node:fs' +import { fileURLToPath, URL } from 'node:url' +import { describe, expect, it } from 'vitest' +import { businessSections, getUserBusinessSections } from './data' + +const businessDir = fileURLToPath(new URL('.', import.meta.url)) + +const expectedSubViews = [ + 'diagnosis', + 'renew', + 'allocate', + 'change', + 'addon', + 'customer', + 'customerDetail', + 'order', + 'warning', + 'workOrder', + 'workOrderCreate', + 'workOrderDetail', +] + +describe('getUserBusinessSections', () => { + it('只保留用户侧一级业务入口', () => { + const sections = getUserBusinessSections(businessSections) + const titles = sections.flatMap(section => section.items.map(item => item.title)) + + expect(titles).toContain('套餐续费') + expect(titles).not.toContain('余额充值') + expect(titles).toContain('重启卡') + expect(titles).toContain('停机 / 复机') + expect(titles).toContain('加油包') + expect(titles).toContain('预警配置') + expect(titles).toContain('卡片排查') + expect(titles).toContain('我的卡片') + expect(titles).toContain('我的订单') + expect(titles).toContain('工单服务') + expect(titles).not.toContain('进入商城') + expect(titles).not.toContain('订单记录') + expect(titles).not.toContain('流量池预警(代理商独有)') + }) + + it('可办理业务都有明确动作', () => { + const sections = getUserBusinessSections(businessSections) + const actionableItems = sections + .flatMap(section => section.items) + .filter(item => item.action) + + expect(actionableItems.map(item => [item.title, item.action])).toEqual([ + ['套餐续费', 'renew'], + ['加油包', 'addon'], + ['停机 / 复机', 'cardList'], + ['重启卡', 'cardList'], + ['卡片排查', 'diagnosis'], + ['预警配置', 'warning'], + ['我的订单', 'order'], + ['工单服务', 'workOrder'], + ['我的卡片', 'cardList'], + ]) + }) + + it('卡片排查完全开放,不受业务权限过滤', () => { + const sections = getUserBusinessSections({ + userId: 18, + deptId: 100, + userType: 'sys_user', + appAccess: { + configured: true, + permissions: [], + features: [], + routes: [], + menus: [], + }, + } as any) + const titles = sections.flatMap(section => section.items.map(item => item.title)) + + expect(titles).toContain('卡片排查') + expect(titles).toContain('我的订单') + }) + + it('企业用户显示卡片划拨、套餐变更和客户管理入口', () => { + const enterpriseUserTypes = ['sys_user', ''] + enterpriseUserTypes.forEach((userType, index) => { + const sections = getUserBusinessSections({ + userId: 10 + index, + deptId: 100, + userType, + } as any) + const titles = sections.flatMap(section => section.items.map(item => item.title)) + + expect(titles).toContain('卡片划拨') + expect(titles).toContain('套餐变更') + expect(titles).toContain('客户管理') + expect(titles).toContain('停机 / 复机') + expect(titles).toContain('重启卡') + }) + }) + + it('终端用户不显示卡片划拨、套餐变更和客户管理入口', () => { + const sections = getUserBusinessSections({ + userId: 11, + deptId: 147, + userType: 'app_user', + } as any) + const titles = sections.flatMap(section => section.items.map(item => item.title)) + + expect(titles).not.toContain('卡片划拨') + expect(titles).not.toContain('套餐变更') + expect(titles).not.toContain('客户管理') + expect(titles).toContain('停机 / 复机') + expect(titles).toContain('重启卡') + expect(titles).toContain('加油包') + expect(titles).toContain('预警配置') + expect(titles).toContain('卡片排查') + expect(titles).toContain('我的卡片') + expect(titles).toContain('工单服务') + }) + + it('配置权限时只展示已授权业务入口', () => { + const sections = getUserBusinessSections({ + userId: 12, + deptId: 100, + userType: 'sys_user', + appAccess: { + configured: true, + permissions: [], + features: ['card:list', 'customer:list'], + routes: [], + menus: [], + }, + } as any) + const titles = sections.flatMap(section => section.items.map(item => item.title)) + + expect(titles).toContain('我的卡片') + expect(titles).toContain('客户管理') + expect(titles).toContain('卡片排查') + expect(titles).toContain('我的订单') + expect(titles).not.toContain('套餐续费') + expect(titles).not.toContain('卡片划拨') + expect(titles).not.toContain('套餐变更') + }) + + it('控制台权限标识直接决定可见业务入口', () => { + const sections = getUserBusinessSections({ + userId: 13, + deptId: 100, + userType: 'sys_user', + appAccess: { + configured: true, + permissions: ['cpm:iot:tSimCardInfo:list', 'cpm:agent:tAgentEnterprise:list'], + features: [], + routes: [], + menus: [], + }, + } as any) + const actions = sections.flatMap(section => section.items.map(item => item.action)) + + expect(actions).toContain('cardList') + expect(actions).toContain('customer') + expect(actions).not.toContain('renew') + expect(actions).not.toContain('allocate') + expect(actions).not.toContain('change') + }) + + it('有工单列表或新增权限都展示工单服务入口', () => { + const listSections = getUserBusinessSections({ + userId: 14, + deptId: 100, + userType: 'sys_user', + appAccess: { + configured: true, + permissions: ['cpm:gen:tWoMain:list'], + features: [], + routes: [], + menus: [], + }, + } as any) + const createSections = getUserBusinessSections({ + userId: 15, + deptId: 100, + userType: 'sys_user', + appAccess: { + configured: true, + permissions: ['cpm:gen:tWoMain:edit'], + features: [], + routes: [], + menus: [], + }, + } as any) + + expect(listSections.flatMap(section => section.items.map(item => item.title))).toContain('工单服务') + expect(createSections.flatMap(section => section.items.map(item => item.title))).toContain('工单服务') + }) + + it('业务子界面容器文件都存在', () => { + expectedSubViews.forEach((viewName) => { + expect(fs.existsSync(`${businessDir}views/${viewName}/index.vue`)).toBe(true) + }) + }) +}) diff --git a/app/src/pages/business/data.ts b/app/src/pages/business/data.ts new file mode 100644 index 0000000..482f7b1 --- /dev/null +++ b/app/src/pages/business/data.ts @@ -0,0 +1,214 @@ +import type { IUserInfoRes } from '@/api/types/login' +import type { AppPermissionAction } from '@/utils/permission' +import { hasUserPermission } from '@/utils/permission' +import { getAccountWorkspace, isTerminalUserInfo } from '@/utils/userIdentity' + +export type BusinessTone = 'blue' | 'green' | 'orange' | 'primary' | 'purple' | 'red' + +export interface BusinessServiceItem { + title: string + desc: string + tag: string + icon: string + tone: BusinessTone + action?: + | 'addon' + | 'allocate' + | 'balanceRecharge' + | 'cardList' + | 'change' + | 'customer' + | 'diagnosis' + | 'order' + | 'renew' + | 'warning' + | 'workOrder' + agentOnly?: boolean + terminalOnly?: boolean + terminalHidden?: boolean + permissionAction?: AppPermissionAction | AppPermissionAction[] +} + +export interface BusinessServiceSection { + title: string + desc: string + type: 'grid' | 'list' + items: BusinessServiceItem[] +} + +/** ==================== 常量 ==================== */ +export const businessSections: BusinessServiceSection[] = [ + { + title: '套餐与流量', + desc: '续费、变更与加油包', + type: 'grid', + items: [ + { + title: '套餐续费', + desc: '续约当前套餐,避免停机', + tag: '续费', + icon: 'i-carbon-renew', + tone: 'green', + action: 'renew', + permissionAction: 'renew', + }, + { + title: '加油包', + desc: '流量加餐,按需购买', + tag: '流量', + icon: 'i-carbon-package', + tone: 'blue', + action: 'addon', + permissionAction: 'addon', + }, + { + title: '套餐变更', + desc: '更换套餐档位', + tag: '变更', + icon: 'i-carbon-change-catalog', + tone: 'primary', + action: 'change', + agentOnly: true, + permissionAction: 'change', + }, + ], + }, + { + title: '生命周期操作', + desc: '停复机与联网操作', + type: 'grid', + items: [ + { + title: '停机 / 复机', + desc: '进入卡片选择暂停或恢复服务', + tag: '生命周期', + icon: 'i-carbon-pause-outline', + tone: 'orange', + action: 'cardList', + permissionAction: ['stop', 'start'], + }, + { + title: '重启卡', + desc: '重新建立网络会话', + tag: '重启', + icon: 'i-carbon-restart', + tone: 'primary', + action: 'cardList', + permissionAction: 'restart', + }, + ], + }, + { + title: '管理与预警', + desc: '状态排查、预警与分配', + type: 'grid', + items: [ + { + title: '卡片排查', + desc: '状态、会话和流量异常', + tag: '排查', + icon: 'i-carbon-stethoscope', + tone: 'green', + action: 'diagnosis', + }, + { + title: '预警配置', + desc: '流量阈值与微信订阅', + tag: '预警', + icon: 'i-carbon-warning-alt', + tone: 'orange', + action: 'warning', + permissionAction: 'warning', + }, + { + title: '卡片划拨', + desc: '分配给下游客户/子账号', + tag: '划拨', + icon: 'i-carbon-share-knowledge', + tone: 'blue', + action: 'allocate', + agentOnly: true, + permissionAction: 'allocate', + }, + ], + }, + { + title: '服务记录', + desc: '订单与工单', + type: 'grid', + items: [ + { + title: '我的订单', + desc: '续费/加油包办理记录', + tag: '订单', + icon: 'i-carbon-receipt', + tone: 'blue', + action: 'order', + }, + { + title: '工单服务', + desc: '反馈问题和查看进度', + tag: '工单', + icon: 'i-carbon-chat', + tone: 'orange', + action: 'workOrder', + permissionAction: ['workOrder', 'workOrderCreate'], + }, + { + title: '我的卡片', + desc: '查看卡信息、套餐和流量', + tag: '进入', + icon: 'i-carbon-sim-card', + tone: 'primary', + action: 'cardList', + }, + { + title: '客户管理', + desc: '直属客户与客户详情', + tag: '客户', + icon: 'i-carbon-user-multiple', + tone: 'green', + action: 'customer', + agentOnly: true, + permissionAction: 'customer', + }, + ], + }, +] + +/** ==================== 方法 ==================== */ +export function getUserBusinessSections( + userInfoOrSections?: IUserInfoRes | BusinessServiceSection[], + sections: BusinessServiceSection[] = businessSections, +) { + const userInfo = Array.isArray(userInfoOrSections) ? undefined : userInfoOrSections + const sourceSections = Array.isArray(userInfoOrSections) ? userInfoOrSections : sections + const workspace = getAccountWorkspace(userInfo) + const isAgentUser = Boolean(userInfo) && workspace !== 'terminal' + const isTerminalUser = !userInfo || isTerminalUserInfo(userInfo) + + return sourceSections + .map(section => ({ + ...section, + items: section.items.filter((item) => { + if (item.agentOnly && !isAgentUser) + return false + if (item.terminalOnly && !isTerminalUser) + return false + if (item.terminalHidden && isTerminalUser) + return false + if (userInfo && item.permissionAction && !hasBusinessItemPermission(userInfo, item.permissionAction)) + return false + return true + }), + })) + .filter(section => section.items.length > 0) +} + +function hasBusinessItemPermission( + userInfo: IUserInfoRes, + action: AppPermissionAction | AppPermissionAction[], +) { + const actions = Array.isArray(action) ? action : [action] + return actions.some(item => hasUserPermission(userInfo, item)) +} diff --git a/app/src/pages/business/index.vue b/app/src/pages/business/index.vue new file mode 100644 index 0000000..1a0f480 --- /dev/null +++ b/app/src/pages/business/index.vue @@ -0,0 +1,189 @@ + + + + + + + diff --git a/app/src/pages/business/views/addon/index.vue b/app/src/pages/business/views/addon/index.vue new file mode 100644 index 0000000..b32ae8a --- /dev/null +++ b/app/src/pages/business/views/addon/index.vue @@ -0,0 +1,219 @@ + + + diff --git a/app/src/pages/business/views/allocate/index.vue b/app/src/pages/business/views/allocate/index.vue new file mode 100644 index 0000000..0312bf8 --- /dev/null +++ b/app/src/pages/business/views/allocate/index.vue @@ -0,0 +1,740 @@ + + + diff --git a/app/src/pages/business/views/api/balance.ts b/app/src/pages/business/views/api/balance.ts new file mode 100644 index 0000000..e848c9c --- /dev/null +++ b/app/src/pages/business/views/api/balance.ts @@ -0,0 +1,61 @@ +import type { + BalanceRechargeMockPayParams, + BalanceRechargeRefundParams, + BalanceRechargeStatusParams, + BalanceRechargeStatusRes, + BalanceRechargeSubmitParams, + BalanceRechargeSubmitRes, +} from '@/api/types/balance' +import type { AppOrder } from '@/api/types/order' +import { http } from '@/http/http' + +export type { + BalanceRechargeMockPayParams, + BalanceRechargeRefundParams, + BalanceRechargeStatusParams, + BalanceRechargeStatusRes, + BalanceRechargeSubmitParams, + BalanceRechargeSubmitRes, +} from '@/api/types/balance' + +export function balanceRechargeSubmit(data: BalanceRechargeSubmitParams) { + return http.post( + '/balance/recharge-submit', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} + +export function balanceRechargeStatus(data: BalanceRechargeStatusParams) { + return http.get('/balance/recharge-status', data, undefined, { + hideErrorToast: true, + }) +} + +export function balanceRechargeMockPay(data: BalanceRechargeMockPayParams) { + return http.post( + '/balance/recharge-mock-pay', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} + +export function balanceRechargeRefund(data: BalanceRechargeRefundParams) { + return http.post( + '/balance/recharge-refund', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} diff --git a/app/src/pages/business/views/api/customer.ts b/app/src/pages/business/views/api/customer.ts new file mode 100644 index 0000000..5bd66ab --- /dev/null +++ b/app/src/pages/business/views/api/customer.ts @@ -0,0 +1,27 @@ +import type { + CustomerDetailParams, + CustomerDetailRes, + CustomerListParams, + CustomerListRes, +} from '@/api/types/customer' +import { http } from '@/http/http' + +export type { + AppCustomer, + AppCustomerDetail, + AppCustomerType, + CustomerDetailParams, + CustomerListParams, +} from '@/api/types/customer' + +export function getCustomerList(params: CustomerListParams) { + return http.get('/customer/list', params, undefined, { + hideErrorToast: true, + }) +} + +export function getCustomerDetail(params: CustomerDetailParams) { + return http.get('/customer/detail', params, undefined, { + hideErrorToast: true, + }) +} diff --git a/app/src/pages/business/views/api/diagnosis.ts b/app/src/pages/business/views/api/diagnosis.ts new file mode 100644 index 0000000..29c005a --- /dev/null +++ b/app/src/pages/business/views/api/diagnosis.ts @@ -0,0 +1,44 @@ +import type { + DiagnosisFlowParams, + DiagnosisFlowRes, + DiagnosisInfoParams, + DiagnosisInfoRes, +} from '@/api/types/diagnosis' +import { http } from '@/http/http' + +export type { + DiagnosisFlowParams, + DiagnosisFlowRes, + DiagnosisInfoItem, + DiagnosisInfoParams, + DiagnosisInfoRes, + DiagnosisModuleItem, + DiagnosisPackageItem, + DiagnosisRateLimitApnItem, + DiagnosisRateLimitPccItem, + DiagnosisSessionItem, +} from '@/api/types/diagnosis' + +export function getDiagnosisInfo(data: DiagnosisInfoParams) { + return http.post('/diagnosis/info', data, undefined, undefined, { + hideErrorToast: true, + }) +} + +export function getOpenDiagnosisInfo(data: DiagnosisInfoParams & { tenantId?: string }) { + return http.post('/open/diagnosis/info', data, undefined, undefined, { + hideErrorToast: true, + }) +} + +export function getDiagnosisFlow(data: DiagnosisFlowParams) { + return http.post('/open/diagnosis/flow', data, undefined, undefined, { + hideErrorToast: true, + }) +} + +export function getOpenDiagnosisFlow(data: DiagnosisFlowParams & { tenantId?: string }) { + return http.post('/open/diagnosis/flow', data, undefined, undefined, { + hideErrorToast: true, + }) +} diff --git a/app/src/pages/business/views/api/order.ts b/app/src/pages/business/views/api/order.ts new file mode 100644 index 0000000..f19f09f --- /dev/null +++ b/app/src/pages/business/views/api/order.ts @@ -0,0 +1,21 @@ +import type { + OrderDetailParams, + OrderDetailRes, + OrderListParams, + OrderListRes, +} from '@/api/types/order' +import { http } from '@/http/http' + +export type { AppOrder, AppOrderItem, OrderDetailParams, OrderListParams } from '@/api/types/order' + +export function getOrderList(params: OrderListParams) { + return http.get('/order/list', params, undefined, { + hideErrorToast: true, + }) +} + +export function getOrderDetail(params: OrderDetailParams) { + return http.get('/order/detail', params, undefined, { + hideErrorToast: true, + }) +} diff --git a/app/src/pages/business/views/api/warning.ts b/app/src/pages/business/views/api/warning.ts new file mode 100644 index 0000000..983d7f2 --- /dev/null +++ b/app/src/pages/business/views/api/warning.ts @@ -0,0 +1,81 @@ +import type { + WarningConfigBatchNotifyParams, + WarningConfigBatchNotifyRes, + WarningConfigBatchSaveParams, + WarningConfigBatchSaveRes, + WarningConfigListParams, + WarningConfigListRes, + WarningConfigMetaRes, + WarningConfigNotifyParams, + WarningConfigNotifyRes, + WarningConfigSaveParams, + WarningConfigSaveRes, +} from '@/api/types/warning' +import { http } from '@/http/http' + +export type { + WarningConfig, + WarningConfigBatchItemParams, + WarningConfigBatchNotifyParams, + WarningConfigBatchNotifyRes, + WarningConfigBatchSaveParams, + WarningConfigBatchSaveRes, + WarningConfigListRes, + WarningConfigNotifyParams, + WarningConfigNotifyRes, + WarningConfigSaveParams, +} from '@/api/types/warning' + +export function getWarningConfigList(params: WarningConfigListParams) { + return http.get('/warning/config/list', params, undefined, { + hideErrorToast: true, + }) +} + +export function saveWarningConfig(data: WarningConfigSaveParams) { + return http.post('/warning/config/save', data, undefined, undefined, { + hideErrorToast: true, + }) +} + +export function updateWarningConfigNotify(data: WarningConfigNotifyParams) { + return http.post( + '/warning/config/notify', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} + +export function getWarningConfigMeta() { + return http.get('/warning/config/meta', undefined, undefined, { + hideErrorToast: true, + }) +} + +export function saveWarningConfigBatch(data: WarningConfigBatchSaveParams) { + return http.post( + '/warning/config/batch-save', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} + +export function updateWarningConfigNotifyBatch(data: WarningConfigBatchNotifyParams) { + return http.post( + '/warning/config/batch-notify', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} diff --git a/app/src/pages/business/views/api/workOrder.ts b/app/src/pages/business/views/api/workOrder.ts new file mode 100644 index 0000000..3d95629 --- /dev/null +++ b/app/src/pages/business/views/api/workOrder.ts @@ -0,0 +1,35 @@ +import type { + WorkOrderCreateParams, + WorkOrderCreateRes, + WorkOrderDetailParams, + WorkOrderDetailRes, + WorkOrderListParams, + WorkOrderListRes, +} from '@/api/types/workOrder' +import { http } from '@/http/http' + +export type { + AppWorkOrder, + AppWorkOrderCommunication, + WorkOrderCreateParams, + WorkOrderDetailParams, + WorkOrderListParams, +} from '@/api/types/workOrder' + +export function getWorkOrderList(params: WorkOrderListParams) { + return http.get('/work-order/list', params, undefined, { + hideErrorToast: true, + }) +} + +export function getWorkOrderDetail(params: WorkOrderDetailParams) { + return http.get('/work-order/detail', params, undefined, { + hideErrorToast: true, + }) +} + +export function createWorkOrder(data: WorkOrderCreateParams) { + return http.post('/work-order/create', data, undefined, undefined, { + hideErrorToast: true, + }) +} diff --git a/app/src/pages/business/views/balanceRecharge/index.vue b/app/src/pages/business/views/balanceRecharge/index.vue new file mode 100644 index 0000000..cea465a --- /dev/null +++ b/app/src/pages/business/views/balanceRecharge/index.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/app/src/pages/business/views/cardRenew/index.vue b/app/src/pages/business/views/cardRenew/index.vue new file mode 100644 index 0000000..19cee20 --- /dev/null +++ b/app/src/pages/business/views/cardRenew/index.vue @@ -0,0 +1,487 @@ + + + diff --git a/app/src/pages/business/views/change/index.vue b/app/src/pages/business/views/change/index.vue new file mode 100644 index 0000000..b8e4799 --- /dev/null +++ b/app/src/pages/business/views/change/index.vue @@ -0,0 +1,645 @@ + + + diff --git a/app/src/pages/business/views/customer/index.vue b/app/src/pages/business/views/customer/index.vue new file mode 100644 index 0000000..969ecca --- /dev/null +++ b/app/src/pages/business/views/customer/index.vue @@ -0,0 +1,329 @@ + + + + + diff --git a/app/src/pages/business/views/customerDetail/index.vue b/app/src/pages/business/views/customerDetail/index.vue new file mode 100644 index 0000000..522a9a0 --- /dev/null +++ b/app/src/pages/business/views/customerDetail/index.vue @@ -0,0 +1,251 @@ + + + diff --git a/app/src/pages/business/views/diagnosis/index.vue b/app/src/pages/business/views/diagnosis/index.vue new file mode 100644 index 0000000..d86f88a --- /dev/null +++ b/app/src/pages/business/views/diagnosis/index.vue @@ -0,0 +1,1261 @@ + + + + + diff --git a/app/src/pages/business/views/order/index.vue b/app/src/pages/business/views/order/index.vue new file mode 100644 index 0000000..552b978 --- /dev/null +++ b/app/src/pages/business/views/order/index.vue @@ -0,0 +1,569 @@ + + + diff --git a/app/src/pages/business/views/renew/index.vue b/app/src/pages/business/views/renew/index.vue new file mode 100644 index 0000000..b61d05d --- /dev/null +++ b/app/src/pages/business/views/renew/index.vue @@ -0,0 +1,729 @@ + + + diff --git a/app/src/pages/business/views/warning/index.vue b/app/src/pages/business/views/warning/index.vue new file mode 100644 index 0000000..62f423c --- /dev/null +++ b/app/src/pages/business/views/warning/index.vue @@ -0,0 +1,699 @@ + + + diff --git a/app/src/pages/business/views/workOrder/index.vue b/app/src/pages/business/views/workOrder/index.vue new file mode 100644 index 0000000..33495a7 --- /dev/null +++ b/app/src/pages/business/views/workOrder/index.vue @@ -0,0 +1,204 @@ + + + diff --git a/app/src/pages/business/views/workOrderCreate/index.vue b/app/src/pages/business/views/workOrderCreate/index.vue new file mode 100644 index 0000000..2aefbb7 --- /dev/null +++ b/app/src/pages/business/views/workOrderCreate/index.vue @@ -0,0 +1,293 @@ + + + diff --git a/app/src/pages/business/views/workOrderDetail/index.vue b/app/src/pages/business/views/workOrderDetail/index.vue new file mode 100644 index 0000000..51e19fd --- /dev/null +++ b/app/src/pages/business/views/workOrderDetail/index.vue @@ -0,0 +1,222 @@ + + + diff --git a/app/src/pages/card/action.ts b/app/src/pages/card/action.ts new file mode 100644 index 0000000..650b3a4 --- /dev/null +++ b/app/src/pages/card/action.ts @@ -0,0 +1,134 @@ +import type { AppCard } from '@/api/card' +import type { IUserInfoRes } from '@/api/types/login' +import { hasUserPermission } from '@/utils/permission' +import { getAccountWorkspace, isEnterpriseUserInfo } from '@/utils/userIdentity' + +export type CardMoreActionKey = 'restart' | 'stop' | 'start' | 'dataStart' | 'dataStop' | 'change' | 'allocate' | 'warning' + +export interface CardMoreAction { + key: CardMoreActionKey + name: string + description?: string + color?: string +} + +export function canBusinessCardAction( + userInfo: Partial | null | undefined, + card?: AppCard | null, +) { + const hasUser = Boolean(userInfo?.userId || userInfo?.phone || userInfo?.nickName || userInfo?.username) + return hasUser && !!card && card.ownerType !== 'public' && card.canBusiness !== false +} + +export function canRenewCardAction(userInfo: Partial | null | undefined, card?: AppCard | null) { + return canBusinessCardAction(userInfo, card) && hasUserPermission(userInfo, 'renew') +} + +export function canRestartCardAction(userInfo: Partial | null | undefined, card?: AppCard | null) { + return canBusinessCardAction(userInfo, card) && hasUserPermission(userInfo, 'restart') +} + +export function canAddonCardAction(userInfo: Partial | null | undefined, card?: AppCard | null) { + return canBusinessCardAction(userInfo, card) && hasUserPermission(userInfo, 'addon') +} + +export function canEnterpriseCardAction( + userInfo: Partial | null | undefined, + card?: AppCard | null, +) { + const workspace = getAccountWorkspace(userInfo) + return (isEnterpriseUserInfo(userInfo) || workspace === 'agent' || workspace === 'admin') + && !!card + && card.ownerType !== 'public' + && card.canBusiness !== false +} + +export function canAllocateCardAction( + userInfo: Partial | null | undefined, + card?: AppCard | null, +) { + return canEnterpriseCardAction(userInfo, card) && hasUserPermission(userInfo, 'allocate') && card?.canAllocate !== false +} + +export function canStopCardAction(userInfo: Partial | null | undefined, card?: AppCard | null) { + return canBusinessCardAction(userInfo, card) && hasUserPermission(userInfo, 'stop') +} + +export function canStartCardAction(userInfo: Partial | null | undefined, card?: AppCard | null) { + return canBusinessCardAction(userInfo, card) && hasUserPermission(userInfo, 'start') +} + +export function canDataStartCardAction(userInfo: Partial | null | undefined, card?: AppCard | null) { + return canEnterpriseCardAction(userInfo, card) && hasUserPermission(userInfo, 'dataStart') +} + +export function canDataStopCardAction(userInfo: Partial | null | undefined, card?: AppCard | null) { + return canEnterpriseCardAction(userInfo, card) && hasUserPermission(userInfo, 'dataStop') +} + +export function canChangeCardAction(userInfo: Partial | null | undefined, card?: AppCard | null) { + return canEnterpriseCardAction(userInfo, card) && hasUserPermission(userInfo, 'change') +} + +export function canWarningCardAction(userInfo: Partial | null | undefined, card?: AppCard | null) { + return canBusinessCardAction(userInfo, card) && hasUserPermission(userInfo, 'warning') +} + +export function buildCardMoreActions( + userInfo: Partial | null | undefined, + card?: AppCard | null, +) { + const actions: CardMoreAction[] = [] + + if (card?.bossCardStatus === 4) { + if (canStartCardAction(userInfo, card)) { + actions.push({ + key: 'start', + name: '复机', + description: '恢复卡片联网', + }) + } + } + else if (canStopCardAction(userInfo, card)) { + actions.push({ + key: 'stop', + name: '停机', + description: '暂停卡片联网', + color: '#e11d48', + }) + } + + if (card?.dataServiceStatus === 1 && canDataStopCardAction(userInfo, card)) { + actions.push({ + key: 'dataStop', + name: '停用数据服务', + description: '关闭这张卡的数据通信', + color: '#e11d48', + }) + } + else if (card?.dataServiceStatus === 0 && canDataStartCardAction(userInfo, card)) { + actions.push({ + key: 'dataStart', + name: '开通数据服务', + description: '恢复这张卡的数据通信', + }) + } + + if (canChangeCardAction(userInfo, card)) { + actions.push({ + key: 'change', + name: '套餐变更', + description: '切换当前套餐', + }) + } + + if (canAllocateCardAction(userInfo, card)) { + actions.push({ + key: 'allocate', + name: '卡片划拨', + description: '划拨权随卡片归属转移', + }) + } + + return actions +} diff --git a/app/src/pages/card/components/CardList.vue b/app/src/pages/card/components/CardList.vue new file mode 100644 index 0000000..82c09b9 --- /dev/null +++ b/app/src/pages/card/components/CardList.vue @@ -0,0 +1,1053 @@ + + + + + diff --git a/app/src/pages/card/components/CardToolbar.vue b/app/src/pages/card/components/CardToolbar.vue new file mode 100644 index 0000000..ac036f1 --- /dev/null +++ b/app/src/pages/card/components/CardToolbar.vue @@ -0,0 +1,41 @@ + + + diff --git a/app/src/pages/card/components/EmptyState.vue b/app/src/pages/card/components/EmptyState.vue new file mode 100644 index 0000000..eb26354 --- /dev/null +++ b/app/src/pages/card/components/EmptyState.vue @@ -0,0 +1,28 @@ + + + diff --git a/app/src/pages/card/components/FilterBar.vue b/app/src/pages/card/components/FilterBar.vue new file mode 100644 index 0000000..5e633c4 --- /dev/null +++ b/app/src/pages/card/components/FilterBar.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/app/src/pages/card/components/PackageList.vue b/app/src/pages/card/components/PackageList.vue new file mode 100644 index 0000000..908e7a8 --- /dev/null +++ b/app/src/pages/card/components/PackageList.vue @@ -0,0 +1,217 @@ + + + diff --git a/app/src/pages/card/data.test.ts b/app/src/pages/card/data.test.ts new file mode 100644 index 0000000..464f68b --- /dev/null +++ b/app/src/pages/card/data.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest' +import { getCardAbnormalReasonText, matchesCardFilter } from './data' + +describe('card status helpers', () => { + it('shows expired package reason even when boss card status is activated', () => { + const card = { + iccid: '898604B21522C0012933', + bossCardStatus: 2, + packageStatus: 3, + } + + expect(matchesCardFilter(card, 'error')).toBe(true) + expect(getCardAbnormalReasonText(card)).toBe('套餐已过期') + }) + + it('uses abnormal reasons returned by backend first', () => { + expect(getCardAbnormalReasonText({ + iccid: '898604B21522C0012934', + bossCardStatus: 2, + packageStatus: 2, + abnormalReasons: ['卡状态停机', '套餐已过期'], + })).toBe('卡状态停机、套餐已过期') + }) +}) diff --git a/app/src/pages/card/data.ts b/app/src/pages/card/data.ts new file mode 100644 index 0000000..0a49c2f --- /dev/null +++ b/app/src/pages/card/data.ts @@ -0,0 +1,385 @@ +import type { AppCard } from '@/api/card' + +export interface FilterItem { + label: string + value: string +} + +export interface PackageSummary { + code: string + name: string + type: string + validText: string + status: string + statusType: 'danger' | 'primary' | 'success' | 'warning' + filterValue: string + effectText: string + cardCount: number + firstCardValue: string +} + +export const bossCardStatusTextMap: Record = { + 0: '未知', + 1: '待激活', + 2: '已激活', + 4: '停机', + 6: '可测试', + 7: '库存', + 8: '预销户', + 9: '已销户', +} + +export const cardFilters: FilterItem[] = [ + { label: '全部', value: 'all' }, + { label: '正常', value: 'normal' }, + { label: '流量不足', value: 'low-flow' }, + { label: '即将到期', value: 'expiring' }, + { label: '异常', value: 'error' }, +] + +export const packageFilters: FilterItem[] = [ + { label: '全部', value: 'all' }, + { label: '生效中', value: 'active' }, + { label: '即将到期', value: 'expiring' }, + { label: '已到期', value: 'expired' }, + { label: '待生效', value: 'pending' }, + { label: '加油包', value: 'addon' }, +] + +export function getBossCardStatusText(status?: number) { + return typeof status === 'number' ? bossCardStatusTextMap[status] || '未知' : '未知' +} + +export function getPackageStatusText(status?: number) { + const map: Record = { + 0: '无套餐', + 1: '待生效', + 2: '生效中', + 3: '已过期', + } + return typeof status === 'number' ? map[status] || '未知' : '未知' +} + +export function getCardAbnormalReasons(card: AppCard): string[] { + const fromApi = Array.isArray(card.abnormalReasons) + ? card.abnormalReasons.map(item => String(item || '').trim()).filter(Boolean) + : [] + if (fromApi.length) + return fromApi + if (typeof card.abnormalReason === 'string' && card.abnormalReason.trim()) + return [card.abnormalReason.trim()] + + const reasons: string[] = [] + if (card.packageStatus === 3) + reasons.push('套餐已过期') + + if (card.bossCardStatus === 0) { + reasons.push('卡状态未知') + } + else if ( + card.bossCardStatus === 4 + || card.bossCardStatus === 8 + || card.bossCardStatus === 9 + ) { + reasons.push(`卡状态${getBossCardStatusText(card.bossCardStatus)}`) + } + else if (typeof card.bossCardStatus === 'number' && !(card.bossCardStatus in bossCardStatusTextMap)) { + reasons.push(`未知卡状态:${card.bossCardStatus}`) + } + return reasons +} + +export function getCardAbnormalReasonText(card: AppCard) { + return getCardAbnormalReasons(card).join('、') +} + +export function getServiceStatusText(status?: number) { + return status === 1 ? '启用' : '停用' +} + +export function getNetModelText(netModel?: number) { + const map: Record = { + 1: '3G', + 2: '2G', + 3: '4G', + 4: '5G', + 5: 'NB-IOT', + 6: '5G-SA', + 7: '5G-NSA', + 8: '2G/3G', + 9: '电信NB', + 10: '移动4G', + 11: '移动NB', + 12: '联通4G', + 13: '电信4G', + 14: 'RedCap', + } + return typeof netModel === 'number' ? map[netModel] || '未知' : '未知' +} + +export function getRatText(rat?: string | number) { + const value = normalizeStatusValue(rat) + if (!value) + return '-' + const map: Record = { + 5: '2G', + 6: '4G', + 7: '5G', + 8: 'NB', + } + return map[value] || value +} + +export function getCardStatusType(card: AppCard): 'danger' | 'primary' | 'success' | 'warning' { + if ( + card.bossCardStatus === 4 + || card.bossCardStatus === 8 + || card.bossCardStatus === 9 + ) { + return 'danger' + } + if (card.bossCardStatus === 1 || card.bossCardStatus === 7) + return 'primary' + if (card.bossCardStatus === 2) + return 'success' + return 'warning' +} + +export function getCardStatusClass(card: AppCard): string { + const type = getCardStatusType(card) + const map: Record = { + danger: 'app-tag--err', + primary: 'app-tag--info', + success: 'app-tag--ok', + warning: 'app-tag--warn', + } + return map[type] || 'app-tag--info' +} + +export function getExpireStatusText(card: AppCard) { + if (!card.flowPkgEndAt) + return '未配置到期时间' + const diffDays = getExpireDiffDays(card.flowPkgEndAt) + if (diffDays === undefined) + return '到期时间异常' + if (diffDays < 0) + return '已到期' + if (diffDays <= 1) + return '1天内到期' + if (diffDays <= 7) + return '7天内到期' + if (diffDays <= 30) + return '即将到期' + return '有效' +} + +export function getExpireStatusType(card: AppCard): 'danger' | 'primary' | 'success' | 'warning' { + if (!card.flowPkgEndAt) + return 'primary' + const diffDays = getExpireDiffDays(card.flowPkgEndAt) + if (diffDays === undefined) + return 'primary' + if (diffDays < 0 || diffDays <= 1) + return 'danger' + if (diffDays <= 30) + return 'warning' + return 'success' +} + +export function getCardFilterValue(card: AppCard) { + if ( + card.bossCardStatus === 4 + || card.bossCardStatus === 8 + || card.bossCardStatus === 9 + || card.packageStatus === 3 + ) { + return 'error' + } + if (isLowFlow(card)) + return 'low-flow' + if (isExpiring(card)) + return 'expiring' + if (card.bossCardStatus === 2) + return 'normal' + if (typeof card.bossCardStatus === 'number') + return 'error' + return 'normal' +} + +export function matchesCardFilter(card: AppCard, filter: string) { + if (filter === 'all') + return true + if (filter === 'normal') + return card.bossCardStatus === 2 && card.packageStatus !== 3 + if (filter === 'low-flow') + return isLowFlow(card) + if (filter === 'expiring') + return isExpiring(card) + if (filter === 'error') { + return ( + card.packageStatus === 3 + || card.bossCardStatus === 0 + || card.bossCardStatus === 4 + || card.bossCardStatus === 8 + || card.bossCardStatus === 9 + || (typeof card.bossCardStatus === 'number' && !(card.bossCardStatus in bossCardStatusTextMap)) + ) + } + return getCardFilterValue(card) === filter +} + +export function isLowFlow(card: AppCard) { + const total = Number(card.packageTotalFlowMb || 0) + const remain = Number(card.remainFlowMb || 0) + return total > 0 && remain / total <= 0.2 +} + +export function isExpiring(card: AppCard) { + if (!card.flowPkgEndAt) + return false + const endTime = new Date(card.flowPkgEndAt).getTime() + if (Number.isNaN(endTime)) + return false + const diffDays = (endTime - Date.now()) / 86_400_000 + return diffDays >= 0 && diffDays <= 30 +} + +export function getPackageFilterValue(card: AppCard) { + const packageName = card.packageName || card.currentPackageCode || '' + if (packageName.includes('加油包')) + return 'addon' + if (card.packageStatus === 1) + return 'pending' + if (card.packageStatus === 3) + return 'expired' + if (isExpiring(card)) + return 'expiring' + return 'active' +} + +export function formatFlow(mb?: number) { + const value = Number(mb || 0) + if (value >= 1024) + return `${trimNumber(value / 1024)}GB` + return `${trimNumber(value)}MB` +} + +export function formatRemainFlow(card: AppCard) { + return formatFlow(card.remainFlowMb) +} + +export function formatFlowPair(card: AppCard) { + return `${formatRemainFlow(card)} / ${formatFlow(card.packageTotalFlowMb)}` +} + +export function formatDateTime(value?: string) { + if (!value) + return '-' + return String(value) + .replace('T', ' ') + .replace(/\.\d[^+]*$/, '') + .replace(/\+.*$/, '') +} + +export function displayCardValue(card?: AppCard) { + if (!card) + return '-' + return card.iccid || card.msisdn || card.imsi || card.imei || '-' +} + +export function displayPackageName(card?: AppCard) { + if (!card) + return '无套餐' + return card.packageName || card.currentPackageCode || '无套餐' +} + +export function getOnlineStatusText(status?: string | number) { + const value = normalizeStatusValue(status) + if (!value) + return '-' + const map: Record = { + '0': '在线', + '00': '在线', + '1': '在线', + '2': '未知', + '-1': '离线', + '-2': '未查询到会话', + 'offline': '离线', + 'online': '在线', + 'unknown': '未知', + } + return map[value.toLowerCase()] || value +} + +export function getImeiBindStatusText(status?: string | number) { + const value = normalizeStatusValue(status) + if (!value) + return '-' + const map: Record = { + 0: '异常/机卡分离', + 1: '正常', + normal: '正常', + abnormal: '异常/机卡分离', + } + return map[value.toLowerCase()] || value +} + +export function getRegionLimitStatusText(status?: string | number) { + const value = normalizeStatusValue(status) + if (!value) + return '-' + const map: Record = { + 0: '未超出业务限制的位置区', + 1: '已超出业务限制的位置区', + limited: '已超出业务限制的位置区', + normal: '未超出业务限制的位置区', + } + return map[value.toLowerCase()] || value +} + +export function getRegionLimitExceededText(status?: string | number) { + const value = normalizeStatusValue(status).toLowerCase() + if (!value) + return '-' + if (value === '1' || value === 'limited' || value === '已超出业务限制的位置区') + return '是' + if (value === '0' || value === 'normal' || value === '未超出业务限制的位置区') + return '否' + return value +} + +export function buildSessionSummary(card?: AppCard) { + if (!card) + return '暂无会话信息' + const sessionParts = [ + getOnlineStatusText(card.onlineStatus), + card.apnName || card.apnId, + card.ipv4, + card.ipv6, + ].filter(value => value && value !== '-' && value !== '未知') + + if (sessionParts.length) + return sessionParts.join(' · ') + + const fallbackParts = [getNetModelText(card.netModel)].filter( + value => value && value !== '-' && value !== '未知', + ) + return fallbackParts.length ? fallbackParts.join(' · ') : '暂无会话信息' +} + +function getExpireDiffDays(value: string) { + const endTime = new Date(value).getTime() + if (Number.isNaN(endTime)) + return undefined + return (endTime - Date.now()) / 86_400_000 +} + +function trimNumber(value: number) { + return value.toFixed(value >= 10 ? 1 : 2).replace(/\.?0+$/, '') +} + +function normalizeStatusValue(value?: string | number) { + if (value === undefined || value === null) + return '' + return String(value).trim() +} diff --git a/app/src/pages/card/index.vue b/app/src/pages/card/index.vue new file mode 100644 index 0000000..1a18464 --- /dev/null +++ b/app/src/pages/card/index.vue @@ -0,0 +1,1399 @@ + + + + + diff --git a/app/src/pages/card/views/api/warning.ts b/app/src/pages/card/views/api/warning.ts new file mode 100644 index 0000000..983d7f2 --- /dev/null +++ b/app/src/pages/card/views/api/warning.ts @@ -0,0 +1,81 @@ +import type { + WarningConfigBatchNotifyParams, + WarningConfigBatchNotifyRes, + WarningConfigBatchSaveParams, + WarningConfigBatchSaveRes, + WarningConfigListParams, + WarningConfigListRes, + WarningConfigMetaRes, + WarningConfigNotifyParams, + WarningConfigNotifyRes, + WarningConfigSaveParams, + WarningConfigSaveRes, +} from '@/api/types/warning' +import { http } from '@/http/http' + +export type { + WarningConfig, + WarningConfigBatchItemParams, + WarningConfigBatchNotifyParams, + WarningConfigBatchNotifyRes, + WarningConfigBatchSaveParams, + WarningConfigBatchSaveRes, + WarningConfigListRes, + WarningConfigNotifyParams, + WarningConfigNotifyRes, + WarningConfigSaveParams, +} from '@/api/types/warning' + +export function getWarningConfigList(params: WarningConfigListParams) { + return http.get('/warning/config/list', params, undefined, { + hideErrorToast: true, + }) +} + +export function saveWarningConfig(data: WarningConfigSaveParams) { + return http.post('/warning/config/save', data, undefined, undefined, { + hideErrorToast: true, + }) +} + +export function updateWarningConfigNotify(data: WarningConfigNotifyParams) { + return http.post( + '/warning/config/notify', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} + +export function getWarningConfigMeta() { + return http.get('/warning/config/meta', undefined, undefined, { + hideErrorToast: true, + }) +} + +export function saveWarningConfigBatch(data: WarningConfigBatchSaveParams) { + return http.post( + '/warning/config/batch-save', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} + +export function updateWarningConfigNotifyBatch(data: WarningConfigBatchNotifyParams) { + return http.post( + '/warning/config/batch-notify', + data, + undefined, + undefined, + { + hideErrorToast: true, + }, + ) +} diff --git a/app/src/pages/card/views/detail/index.vue b/app/src/pages/card/views/detail/index.vue new file mode 100644 index 0000000..fb02e72 --- /dev/null +++ b/app/src/pages/card/views/detail/index.vue @@ -0,0 +1,1112 @@ + + + + + diff --git a/app/src/pages/card/views/management/index.vue b/app/src/pages/card/views/management/index.vue new file mode 100644 index 0000000..5ae18b3 --- /dev/null +++ b/app/src/pages/card/views/management/index.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/app/src/pages/card/views/packageDetail/index.vue b/app/src/pages/card/views/packageDetail/index.vue new file mode 100644 index 0000000..8a5614b --- /dev/null +++ b/app/src/pages/card/views/packageDetail/index.vue @@ -0,0 +1,36 @@ + + + diff --git a/app/src/pages/card/views/warning/index.vue b/app/src/pages/card/views/warning/index.vue new file mode 100644 index 0000000..6d6e7ea --- /dev/null +++ b/app/src/pages/card/views/warning/index.vue @@ -0,0 +1,587 @@ + + + diff --git a/app/src/pages/home/components/CardOverview.vue b/app/src/pages/home/components/CardOverview.vue new file mode 100644 index 0000000..ec41c58 --- /dev/null +++ b/app/src/pages/home/components/CardOverview.vue @@ -0,0 +1,344 @@ + + + + + diff --git a/app/src/pages/home/components/FlowPool.vue b/app/src/pages/home/components/FlowPool.vue new file mode 100644 index 0000000..7067a5a --- /dev/null +++ b/app/src/pages/home/components/FlowPool.vue @@ -0,0 +1,110 @@ + + + diff --git a/app/src/pages/home/components/FlowRanking.vue b/app/src/pages/home/components/FlowRanking.vue new file mode 100644 index 0000000..4413878 --- /dev/null +++ b/app/src/pages/home/components/FlowRanking.vue @@ -0,0 +1,94 @@ + + + diff --git a/app/src/pages/home/components/LifecycleStats.vue b/app/src/pages/home/components/LifecycleStats.vue new file mode 100644 index 0000000..1887ab7 --- /dev/null +++ b/app/src/pages/home/components/LifecycleStats.vue @@ -0,0 +1,102 @@ + + + diff --git a/app/src/pages/home/components/PublicCardQuery.vue b/app/src/pages/home/components/PublicCardQuery.vue new file mode 100644 index 0000000..9b7ba63 --- /dev/null +++ b/app/src/pages/home/components/PublicCardQuery.vue @@ -0,0 +1,511 @@ + + + + + diff --git a/app/src/pages/home/components/QuickBusiness.vue b/app/src/pages/home/components/QuickBusiness.vue new file mode 100644 index 0000000..8fee433 --- /dev/null +++ b/app/src/pages/home/components/QuickBusiness.vue @@ -0,0 +1,365 @@ + + + + + diff --git a/app/src/pages/home/index.vue b/app/src/pages/home/index.vue new file mode 100644 index 0000000..6cede2c --- /dev/null +++ b/app/src/pages/home/index.vue @@ -0,0 +1,298 @@ + + + + + + + diff --git a/app/src/pages/login/forgot-password/index.vue b/app/src/pages/login/forgot-password/index.vue new file mode 100644 index 0000000..9fb125a --- /dev/null +++ b/app/src/pages/login/forgot-password/index.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/app/src/pages/login/index.vue b/app/src/pages/login/index.vue new file mode 100644 index 0000000..f4a1551 --- /dev/null +++ b/app/src/pages/login/index.vue @@ -0,0 +1,1563 @@ + + + + + + + diff --git a/app/src/pages/login/register/index.vue b/app/src/pages/login/register/index.vue new file mode 100644 index 0000000..29f9b10 --- /dev/null +++ b/app/src/pages/login/register/index.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/app/src/pages/message/components/MessageList.vue b/app/src/pages/message/components/MessageList.vue new file mode 100644 index 0000000..e9ff20b --- /dev/null +++ b/app/src/pages/message/components/MessageList.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/app/src/pages/message/components/MessageTabs.vue b/app/src/pages/message/components/MessageTabs.vue new file mode 100644 index 0000000..1a490ff --- /dev/null +++ b/app/src/pages/message/components/MessageTabs.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/app/src/pages/message/components/MessageToolbar.vue b/app/src/pages/message/components/MessageToolbar.vue new file mode 100644 index 0000000..29f1f0a --- /dev/null +++ b/app/src/pages/message/components/MessageToolbar.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/app/src/pages/message/data.test.ts b/app/src/pages/message/data.test.ts new file mode 100644 index 0000000..24df1cf --- /dev/null +++ b/app/src/pages/message/data.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' +import { compareMessageAsc, compareMessageDesc, normalizeMessage, parseMessageTimestamp } from './data' + +describe('message data helpers', () => { + it('parses backend and ISO message times', () => { + expect(parseMessageTimestamp('2026-07-31 11:21:44')).toBeGreaterThan(0) + expect(parseMessageTimestamp('2026-07-31T11:21:44+08:00')).toBeGreaterThan(0) + }) + + it('sorts messages by effective time desc and id desc', () => { + const list = [ + normalizeMessage({ id: 1, messageId: 1, messageLogId: 1, messageType: 1, title: 'old', content: '', isRead: 0, sendTime: '2026-07-31 11:21:44' }), + normalizeMessage({ id: 3, messageId: 3, messageLogId: 3, messageType: 1, title: 'same-new-id', content: '', isRead: 0, sendTime: '2026-08-01T09:00:00+08:00' }), + normalizeMessage({ id: 2, messageId: 2, messageLogId: 2, messageType: 1, title: 'same-old-id', content: '', isRead: 0, sendTime: '2026-08-01T09:00:00+08:00' }), + ] + + expect(list.sort(compareMessageDesc).map(item => item.messageId)).toEqual([3, 2, 1]) + }) + + it('sorts messages by effective time asc and id asc', () => { + const list = [ + normalizeMessage({ id: 1, messageId: 1, messageLogId: 1, messageType: 1, title: 'old', content: '', isRead: 0, sendTime: '2026-07-31 11:21:44' }), + normalizeMessage({ id: 3, messageId: 3, messageLogId: 3, messageType: 1, title: 'same-new-id', content: '', isRead: 0, sendTime: '2026-08-01T09:00:00+08:00' }), + normalizeMessage({ id: 2, messageId: 2, messageLogId: 2, messageType: 1, title: 'same-old-id', content: '', isRead: 0, sendTime: '2026-08-01T09:00:00+08:00' }), + ] + + expect(list.sort(compareMessageAsc).map(item => item.messageId)).toEqual([1, 2, 3]) + }) +}) diff --git a/app/src/pages/message/data.ts b/app/src/pages/message/data.ts new file mode 100644 index 0000000..0671075 --- /dev/null +++ b/app/src/pages/message/data.ts @@ -0,0 +1,302 @@ +import type { AppMessage } from '@/api/message' + +export type MessageGroupKey = 'all' | 'system' | 'platform' | 'order' | 'account' +export type MessageModuleKey = 'warning' | 'card' | 'order' | 'system' +export type MessageFilterKey = 'all' | 'recent30' | 'asc' +export type MessageTagType = 'primary' | 'success' | 'warning' | 'danger' + +export interface MessageModule { + key: MessageModuleKey + label: string + icon: string + tagType: MessageTagType + emptyTip: string +} + +export const messageModules: MessageModule[] = [ + { + key: 'warning', + label: '流量预警', + icon: 'i-carbon-warning-alt', + tagType: 'warning', + emptyTip: '暂无流量预警消息', + }, + { + key: 'card', + label: '卡片消息', + icon: 'i-carbon-sim-card', + tagType: 'primary', + emptyTip: '暂无卡片消息', + }, + { + key: 'order', + label: '订单通知', + icon: 'i-carbon-receipt', + tagType: 'success', + emptyTip: '暂无订单通知', + }, + { + key: 'system', + label: '系统通知', + icon: 'i-carbon-notification', + tagType: 'primary', + emptyTip: '暂无系统通知', + }, +] + +export interface MessageGroup { + key: MessageGroupKey + label: string + messageTypes: number[] + unreadCount: number + searchPlaceholder: string + tag: string + tagType: MessageTagType + emptyTip: string +} + +export interface MessageFilterOption { + key: MessageFilterKey + label: string +} + +export interface MessageItem { + id: number + messageId: number + messageLogId: number + title: string + summary: string + content: string + time: string + unread: boolean + messageType: number + typeText: string + tag: string + tagType: MessageTagType + raw: AppMessage +} + +export const baseMessageGroups: Omit[] = [ + { + key: 'all', + label: '全部', + messageTypes: [1, 2, 3, 4], + searchPlaceholder: '搜索消息标题或内容', + tag: '全部', + tagType: 'primary', + emptyTip: '暂无消息', + }, + { + key: 'system', + label: '系统通知', + messageTypes: [1], + searchPlaceholder: '搜索系统通知', + tag: '系统', + tagType: 'primary', + emptyTip: '暂无系统通知', + }, + { + key: 'platform', + label: '平台公告', + messageTypes: [2], + searchPlaceholder: '搜索平台公告', + tag: '平台', + tagType: 'success', + emptyTip: '暂无平台公告', + }, + { + key: 'order', + label: '订单通知', + messageTypes: [3], + searchPlaceholder: '搜索订单通知', + tag: '订单', + tagType: 'warning', + emptyTip: '暂无订单通知', + }, + { + key: 'account', + label: '账户通知', + messageTypes: [4], + searchPlaceholder: '搜索账户通知', + tag: '账户', + tagType: 'danger', + emptyTip: '暂无账户通知', + }, +] + +export const messageFilterOptions: MessageFilterOption[] = [ + { key: 'all', label: '最新优先' }, + { key: 'recent30', label: '最近30天' }, + { key: 'asc', label: '时间正序' }, +] + +export function normalizeMessage(source: AppMessage): MessageItem { + const messageType = Number(source.messageType || 1) + const group = + baseMessageGroups.find((item) => item.messageTypes.includes(messageType)) || + baseMessageGroups[0] + const content = String(source.content || '') + const summary = stripRichText(content) || '暂无消息详细内容' + + return { + id: Number(source.id || source.messageId || 0), + messageId: Number(source.messageId || source.id || 0), + messageLogId: Number(source.messageLogId || 0), + title: source.title || '未命名消息', + summary, + content, + time: formatMessageTime(source.sendTime || source.createdAt || source.updatedAt || ''), + unread: Number(source.isRead) === 0, + messageType, + typeText: source.messageTypeText || messageTypeText(messageType), + tag: group.tag, + tagType: messageTypeTagType(messageType), + raw: source, + } +} + +export function messageMatchesKeyword(item: MessageItem, keyword: string) { + const value = keyword.trim() + if (!value) return true + + return [item.title, item.summary, item.content, item.time].some((text) => text.includes(value)) +} + +export function messageMatchesGroup( + item: MessageItem, + group: Pick, +) { + if (group.key === 'all') return true + return group.messageTypes.includes(item.messageType) +} + +const messageModulePatterns: Array<[MessageModuleKey, RegExp]> = [ + ['warning', /预警|流量|用量|到量|超量|阈值/], + ['card', /卡片|停机|复机|重启|物联卡|ICCID|MSISDN|IMSI|IMEI/], + ['order', /订单|套餐|续费|充值|支付|账单|购买/], + ['system', /系统|平台|维护|公告|活动|账户|账号/], +] + +export function getMessageModuleKey(item: MessageItem): MessageModuleKey { + const text = `${item.title}${item.summary}${item.content}` + const matched = messageModulePatterns.find(([, pattern]) => pattern.test(text)) + if (matched) return matched[0] + + switch (item.messageType) { + case 3: + return 'order' + case 1: + case 2: + case 4: + default: + return 'system' + } +} + +export function messageMatchesModule(item: MessageItem, module: MessageModule) { + return getMessageModuleKey(item) === module.key +} + +export function latestModuleMessage(items: MessageItem[], module: MessageModule) { + return items.filter((item) => messageMatchesModule(item, module)).sort(compareMessageDesc)[0] +} + +export function messageTimestamp(item: MessageItem) { + const rawTime = item.raw.sendTime || item.raw.createdAt || item.raw.updatedAt || item.time + return parseMessageTimestamp(rawTime) +} + +export function compareMessageDesc(a: MessageItem, b: MessageItem) { + const timeDiff = messageTimestamp(b) - messageTimestamp(a) + if (timeDiff !== 0) return timeDiff + return Number(b.messageId || b.id || 0) - Number(a.messageId || a.id || 0) +} + +export function compareMessageAsc(a: MessageItem, b: MessageItem) { + const timeDiff = messageTimestamp(a) - messageTimestamp(b) + if (timeDiff !== 0) return timeDiff + return Number(a.messageId || a.id || 0) - Number(b.messageId || b.id || 0) +} + +export function parseMessageTimestamp(value?: string | number | Date | null) { + if (value === undefined || value === null || value === '') return 0 + if (value instanceof Date) { + const time = value.getTime() + return Number.isFinite(time) ? time : 0 + } + if (typeof value === 'number') return Number.isFinite(value) ? value : 0 + + const text = String(value).trim() + if (!text) return 0 + + const candidates = [text, text.replace(' ', 'T'), text.replace(/-/g, '/').replace('T', ' ')] + + for (const candidate of candidates) { + const parsed = Date.parse(candidate) + if (Number.isFinite(parsed)) return parsed + } + + return 0 +} + +export function messageWithinDays(item: MessageItem, days: number) { + if (days <= 0) return true + + const timestamp = messageTimestamp(item) + if (!timestamp) return false + + const cutoff = Date.now() - days * 24 * 60 * 60 * 1000 + return timestamp >= cutoff +} + +function messageTypeText(messageType: number) { + switch (messageType) { + case 1: + return '系统通知' + case 2: + return '平台公告' + case 3: + return '订单通知' + case 4: + return '账户通知' + default: + return '消息' + } +} + +function messageTypeTagType(messageType: number): MessageTagType { + switch (messageType) { + case 1: + return 'danger' + case 2: + return 'primary' + case 3: + return 'warning' + case 4: + return 'success' + default: + return 'primary' + } +} + +export function stripRichText(content: string) { + return content + .replace(//gi, '') + .replace(//gi, '') + .replace(/<[^>]+>/g, '') + .replace(/ /g, ' ') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/\s+/g, ' ') + .trim() +} + +function formatMessageTime(value: string) { + if (!value) return '' + + return String(value) + .replace('T', ' ') + .replace(/\.\d.*$/, '') + .slice(0, 16) +} diff --git a/app/src/pages/message/index.vue b/app/src/pages/message/index.vue new file mode 100644 index 0000000..4f62fea --- /dev/null +++ b/app/src/pages/message/index.vue @@ -0,0 +1,588 @@ + + + + + diff --git a/app/src/pages/message/views/cardDetail/index.vue b/app/src/pages/message/views/cardDetail/index.vue new file mode 100644 index 0000000..d24f8f7 --- /dev/null +++ b/app/src/pages/message/views/cardDetail/index.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/app/src/pages/message/views/systemDetail/index.vue b/app/src/pages/message/views/systemDetail/index.vue new file mode 100644 index 0000000..d24f8f7 --- /dev/null +++ b/app/src/pages/message/views/systemDetail/index.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/app/src/pages/user/components/UserProfileCard.vue b/app/src/pages/user/components/UserProfileCard.vue new file mode 100644 index 0000000..d271fb9 --- /dev/null +++ b/app/src/pages/user/components/UserProfileCard.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/app/src/pages/user/components/UserServiceCells.vue b/app/src/pages/user/components/UserServiceCells.vue new file mode 100644 index 0000000..0d27cf5 --- /dev/null +++ b/app/src/pages/user/components/UserServiceCells.vue @@ -0,0 +1,421 @@ + + + + + diff --git a/app/src/pages/user/components/UserStatsCard.vue b/app/src/pages/user/components/UserStatsCard.vue new file mode 100644 index 0000000..db54c05 --- /dev/null +++ b/app/src/pages/user/components/UserStatsCard.vue @@ -0,0 +1,211 @@ + + + + + diff --git a/app/src/pages/user/components/UserSupportCells.vue b/app/src/pages/user/components/UserSupportCells.vue new file mode 100644 index 0000000..f116c3f --- /dev/null +++ b/app/src/pages/user/components/UserSupportCells.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/app/src/pages/user/index.vue b/app/src/pages/user/index.vue new file mode 100644 index 0000000..00acdf2 --- /dev/null +++ b/app/src/pages/user/index.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/app/src/pages/user/views/about/index.vue b/app/src/pages/user/views/about/index.vue new file mode 100644 index 0000000..1c60727 --- /dev/null +++ b/app/src/pages/user/views/about/index.vue @@ -0,0 +1,19 @@ + + + diff --git a/app/src/pages/user/views/agreement/index.vue b/app/src/pages/user/views/agreement/index.vue new file mode 100644 index 0000000..6380acb --- /dev/null +++ b/app/src/pages/user/views/agreement/index.vue @@ -0,0 +1,330 @@ + + + diff --git a/app/src/pages/user/views/api/warning.ts b/app/src/pages/user/views/api/warning.ts new file mode 100644 index 0000000..a97f21b --- /dev/null +++ b/app/src/pages/user/views/api/warning.ts @@ -0,0 +1,8 @@ +import type { WarningConfigMetaRes } from '@/api/types/warning' +import { http } from '@/http/http' + +export function getWarningConfigMeta() { + return http.get('/warning/config/meta', undefined, undefined, { + hideErrorToast: true, + }) +} diff --git a/app/src/pages/user/views/contact/index.vue b/app/src/pages/user/views/contact/index.vue new file mode 100644 index 0000000..ac58f6a --- /dev/null +++ b/app/src/pages/user/views/contact/index.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/src/pages/user/views/feedback/index.vue b/app/src/pages/user/views/feedback/index.vue new file mode 100644 index 0000000..9e37e0c --- /dev/null +++ b/app/src/pages/user/views/feedback/index.vue @@ -0,0 +1,18 @@ + + + diff --git a/app/src/pages/user/views/notification/index.vue b/app/src/pages/user/views/notification/index.vue new file mode 100644 index 0000000..7488ac1 --- /dev/null +++ b/app/src/pages/user/views/notification/index.vue @@ -0,0 +1,403 @@ + + + + + diff --git a/app/src/pages/user/views/profile/index.vue b/app/src/pages/user/views/profile/index.vue new file mode 100644 index 0000000..d7e3451 --- /dev/null +++ b/app/src/pages/user/views/profile/index.vue @@ -0,0 +1,550 @@ + + + + + diff --git a/app/src/pages/user/views/realName/index.vue b/app/src/pages/user/views/realName/index.vue new file mode 100644 index 0000000..01346da --- /dev/null +++ b/app/src/pages/user/views/realName/index.vue @@ -0,0 +1,67 @@ + + + diff --git a/app/src/router/README.md b/app/src/router/README.md new file mode 100644 index 0000000..60e3084 --- /dev/null +++ b/app/src/router/README.md @@ -0,0 +1,55 @@ +# 登录 说明 + +## 登录 2种策略 +- 默认无需登录策略: DEFAULT_NO_NEED_LOGIN +- 默认需要登录策略: DEFAULT_NEED_LOGIN + +### 默认无需登录策略: DEFAULT_NO_NEED_LOGIN +进入任何页面都不需要登录,只有进入到黑名单中的页面/或者页面中某些动作需要登录,才需要登录。 + +比如大部分2C的应用,美团、今日头条、抖音等,都可以直接浏览,只有点赞、评论、分享等操作或者去特殊页面(比如个人中心),才需要登录。 + +### 默认需要登录策略: DEFAULT_NEED_LOGIN + +进入任何页面都需要登录,只有进入到白名单中的页面,才不需要登录。默认进入应用需要先去登录页。 + +比如大部分2B和后台管理类的应用,比如企业微信、钉钉、飞书、内部报表系统、CMS系统等,都需要登录,只有登录后,才能使用。 + +### EXCLUDE_LOGIN_PATH_LIST +`EXCLUDE_LOGIN_PATH_LIST` 表示排除的路由列表。 + +在 `默认无需登录策略: DEFAULT_NO_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才需要登录,相当于黑名单。 + +在 `默认需要登录策略: DEFAULT_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才不需要登录,相当于白名单。 + +### excludeLoginPath +definePage 中可以通过 `excludeLoginPath` 来配置路由是否需要登录。(类似过去的 needLogin 的功能) + +```ts +definePage({ + style: { + navigationBarTitleText: '关于', + }, + // 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 src/router 文件夹 + excludeLoginPath: true, + // 角色授权(可选):如果需要根据角色授权,就配置这个 + roleAuth: { + field: 'role', + value: 'admin', + redirect: '/pages/auth/403', + }, +}) +``` + +## 登录注册页路由 + +登录页 `login.vue` 对应路由是 `/pages/login/login`. +注册页 `register.vue` 对应路由是 `/pages/login/register`. + +## 登录注册页适用性 + +登录注册页主要适用于 `h5` 和 `App`,默认不适用于 `小程序`,因为 `小程序` 通常会使用平台提供的快捷登录。 + +特殊情况例外,如业务需要跨平台复用登录注册页时,也可以用在 `小程序` 上,所以主要还是看业务需求。 + +通过一个参数 `LOGIN_PAGE_ENABLE_IN_MP` 来控制是否在 `小程序` 中使用 `H5登录页` 的登录逻辑。 diff --git a/app/src/router/appPermission.ts b/app/src/router/appPermission.ts new file mode 100644 index 0000000..5f37d82 --- /dev/null +++ b/app/src/router/appPermission.ts @@ -0,0 +1,26 @@ +import type { AppFeatureInput } from '@/utils/permission' + +export const businessPageFeatures: string[] = [ + 'card:renew', + 'card:change', + 'card:addon', + 'card:allocate', + 'warn:view', + 'customer:list', + 'workorder:list', + 'workorder:create', +] + +export const appRouteFeatureMap: Record = { + '/pages/business/views/renew/index': 'card:renew', + '/pages/business/views/cardRenew/index': 'card:renew', + '/pages/business/views/addon/index': 'card:addon', + '/pages/business/views/allocate/index': 'card:allocate', + '/pages/business/views/change/index': 'card:change', + '/pages/business/views/customer/index': 'customer:list', + '/pages/business/views/customerDetail/index': 'customer:detail', + '/pages/business/views/warning/index': 'warn:view', + '/pages/business/views/workOrder/index': ['workorder:list', 'workorder:create'], + '/pages/business/views/workOrderCreate/index': 'workorder:create', + '/pages/business/views/workOrderDetail/index': 'workorder:detail', +} diff --git a/app/src/router/config.ts b/app/src/router/config.ts new file mode 100644 index 0000000..b2b687a --- /dev/null +++ b/app/src/router/config.ts @@ -0,0 +1,28 @@ +import { getAllPages } from '@/utils' + +export const LOGIN_STRATEGY_MAP = { + DEFAULT_NO_NEED_LOGIN: 0, // 黑名单策略,默认可以进入APP + DEFAULT_NEED_LOGIN: 1, // 白名单策略,默认不可以进入APP,需要强制登录 +} +// TODO: 1/3 登录策略,默认使用`无需登录策略`,即默认不需要登录就可以访问 +export const LOGIN_STRATEGY = LOGIN_STRATEGY_MAP.DEFAULT_NO_NEED_LOGIN +export const isNeedLoginMode = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN + +export const LOGIN_PAGE = '/pages/login/index' +export const REGISTER_PAGE = '/pages/login/register/index' + +export const LOGIN_PAGE_LIST = [LOGIN_PAGE, REGISTER_PAGE] + +// 在 definePage 里面配置了 excludeLoginPath 的页面,功能与 EXCLUDE_LOGIN_PATH_LIST 相同 +export const excludeLoginPathList = getAllPages('excludeLoginPath').map(page => page.path) + +// 排除在外的列表,白名单策略指白名单列表,黑名单策略指黑名单列表 +// TODO: 2/3 在 definePage 配置 excludeLoginPath,或者在下面配置 EXCLUDE_LOGIN_PATH_LIST +export const EXCLUDE_LOGIN_PATH_LIST = [ + '/pages/xxx/index', // 示例值 + '/pages-sub/xxx/index', // 示例值 + ...excludeLoginPathList, // 都是以 / 开头的 path +] + +// 小程序端也使用项目内置账号登录页,保证 tabbar 和直接进页面时都能走统一登录拦截 +export const LOGIN_PAGE_ENABLE_IN_MP = true diff --git a/app/src/router/interceptor.ts b/app/src/router/interceptor.ts new file mode 100644 index 0000000..e555f93 --- /dev/null +++ b/app/src/router/interceptor.ts @@ -0,0 +1,165 @@ +import { isMp } from '@uni-helper/uni-env' +/** + * by 菲鸽 on 2025-08-19 + * 路由拦截,通常也是登录拦截 + * 黑、白名单的配置,请看 config.ts 文件, EXCLUDE_LOGIN_PATH_LIST + */ +import { useTokenStore } from '@/store/token' +import { useUserStore } from '@/store/user' +import { hasRouteAccess } from '@/utils/permission' +import { isPageTabbar, tabbarStore } from '@/tabbar/store' +import { getAllPages, getLastPage, HOME_PAGE, parseUrlToObj } from '@/utils/index' +import { EXCLUDE_LOGIN_PATH_LIST, isNeedLoginMode, LOGIN_PAGE, LOGIN_PAGE_ENABLE_IN_MP } from './config' +import { appRouteFeatureMap } from './appPermission' + +export const FG_LOG_ENABLE = false + +export function judgeIsExcludePath(path: string) { + const isDev = import.meta.env.DEV + if (!isDev) { + return EXCLUDE_LOGIN_PATH_LIST.includes(path) + } + const allExcludeLoginPages = getAllPages('excludeLoginPath') // dev 环境下,需要每次都重新获取,否则新配置就不会生效 + return EXCLUDE_LOGIN_PATH_LIST.includes(path) || (isDev && allExcludeLoginPages.some(page => page.path === path)) +} + +export const navigateToInterceptor = { + // 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同 + // 增加对相对路径的处理,BY 网友 @ideal + invoke({ url, query }: { url: string, query?: Record }) { + if (url === undefined) { + return + } + let { path, query: _query } = parseUrlToObj(url) + + FG_LOG_ENABLE && console.log('\n\n路由拦截器:-------------------------------------') + FG_LOG_ENABLE && console.log('路由拦截器 1: url->', url, ', query ->', query) + const myQuery = { ..._query, ...query } + // /pages/route-interceptor/index?name=feige&age=30 + FG_LOG_ENABLE && console.log('路由拦截器 2: path->', path, ', _query ->', _query) + FG_LOG_ENABLE && console.log('路由拦截器 3: myQuery ->', myQuery) + + // 处理相对路径 + if (!path.startsWith('/')) { + const currentPath = getLastPage()?.route || '' + const normalizedCurrentPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}` + const baseDir = normalizedCurrentPath.substring(0, normalizedCurrentPath.lastIndexOf('/')) + path = `${baseDir}/${path}` + } + + // // 处理路由不存在的情况 + // if (path !== '/' && !getAllPages().some(page => page.path === path)) { + // console.warn('路由不存在:', path) + // return false // 明确表示阻止原路由继续执行 + // } + + // // 插件页面 + // if (url.startsWith('plugin://')) { + // FG_LOG_ENABLE && console.log('路由拦截器 4: plugin:// 路径 ==>', url) + // path = url + // } + + // 处理直接进入路由非首页时,tabbarIndex 不正确的问题 + tabbarStore.setAutoCurIdx(path) + + // 小程序里面使用平台自带的登录,则不走下面的逻辑 + if (isMp && !LOGIN_PAGE_ENABLE_IN_MP) { + return true // 明确表示允许路由继续执行 + } + + const tokenStore = useTokenStore() + FG_LOG_ENABLE && console.log('tokenStore.hasLogin:', tokenStore.hasLogin) + + // 不管黑白名单,登录了就直接去吧(但是当前不能是登录页) + if (tokenStore.hasLogin) { + if (path !== LOGIN_PAGE) { + if (!hasRoutePermission(path)) { + uni.showToast({ title: '暂无页面权限', icon: 'none' }) + return false + } + return true // 明确表示允许路由继续执行 + } + else { + console.log('已经登录,但是还在登录页', myQuery.redirect) + const url = myQuery.redirect || HOME_PAGE + if (isPageTabbar(url)) { + uni.switchTab({ url }) + } + else { + uni.navigateTo({ url }) + } + return false // 明确表示阻止原路由继续执行 + } + } + let fullPath = path + + if (Object.keys(myQuery).length) { + fullPath += `?${Object.keys(myQuery).map(key => `${key}=${myQuery[key]}`).join('&')}` + } + const redirectUrl = `${LOGIN_PAGE}?redirect=${encodeURIComponent(fullPath)}` + + // #region 1/2 默认需要登录的情况(白名单策略) --------------------------- + if (isNeedLoginMode) { + // 需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示白名单,可以直接通过 + if (judgeIsExcludePath(path)) { + return true // 明确表示允许路由继续执行 + } + // 否则需要重定向到登录页 + else { + if (path === LOGIN_PAGE) { + return true // 明确表示允许路由继续执行 + } + FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) redirectUrl:', redirectUrl) + uni.navigateTo({ url: redirectUrl }) + return false // 明确表示阻止原路由继续执行 + } + } + // #endregion 1/2 默认需要登录的情况(白名单策略) --------------------------- + + // #region 2/2 默认不需要登录的情况(黑名单策略) --------------------------- + else { + // 不需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示黑名单,需要重定向到登录页 + if (judgeIsExcludePath(path)) { + FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) redirectUrl:', redirectUrl) + uni.navigateTo({ url: redirectUrl }) + return false // 修改为false,阻止原路由继续执行 + } + return true // 明确表示允许路由继续执行 + } + // #endregion 2/2 默认不需要登录的情况(黑名单策略) --------------------------- + }, +} + +function hasRoutePermission(path: string) { + const requiredFeature = appRouteFeatureMap[path] + if (!requiredFeature) + return true + const userStore = useUserStore() + return hasRouteAccess(userStore.userInfo, path, requiredFeature) +} + +// 针对 chooseLocation 的特殊处理 +export const chooseLocationInterceptor = { + invoke(options: any) { + // 直接放行 chooseLocation 调用 + FG_LOG_ENABLE && console.log('chooseLocation 调用,直接放行:', options) + return true + }, +} + +let routeInterceptorInstalled = false + +export const routeInterceptor = { + install() { + if (routeInterceptorInstalled) + return + routeInterceptorInstalled = true + uni.addInterceptor('navigateTo', navigateToInterceptor) + uni.addInterceptor('reLaunch', navigateToInterceptor) + uni.addInterceptor('redirectTo', navigateToInterceptor) + uni.addInterceptor('switchTab', navigateToInterceptor) + + // 添加 chooseLocation 的拦截器,确保直接放行 + uni.addInterceptor('chooseLocation', chooseLocationInterceptor) + }, +} diff --git a/app/src/router/permission.ts b/app/src/router/permission.ts new file mode 100644 index 0000000..06a7bfa --- /dev/null +++ b/app/src/router/permission.ts @@ -0,0 +1,26 @@ +import { tabbarStore } from '@/tabbar/store' +import { useTokenStore } from '@/store/token' +import { useUserStore } from '@/store/user' +import { hasRouteAccess } from '@/utils/permission' +import { appRouteFeatureMap } from './appPermission' + +let permissionInstalled = false + +export const permission = { + install(router) { + if (permissionInstalled) + return + permissionInstalled = true + router.beforeEach((to, from, next) => { + const path = to.path + tabbarStore.setAutoCurIdx(path) + const requiredFeature = appRouteFeatureMap[path] + if (requiredFeature && useTokenStore().hasLogin && !hasRouteAccess(useUserStore().userInfo, path, requiredFeature)) { + uni.showToast({ title: '暂无页面权限', icon: 'none' }) + next(false) + return + } + next() + }) + }, +} diff --git a/app/src/service/index.ts b/app/src/service/index.ts new file mode 100644 index 0000000..3bfbb5b --- /dev/null +++ b/app/src/service/index.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +// @ts-ignore +export * from './types'; + +export * from './listAll'; +export * from './info'; diff --git a/app/src/service/info.ts b/app/src/service/info.ts new file mode 100644 index 0000000..fe09da5 --- /dev/null +++ b/app/src/service/info.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +// @ts-ignore +import request from '@/http/vue-query'; +import { CustomRequestOptions_ } from '@/http/types'; + +import * as API from './types'; + +/** 用户信息 GET /user/info */ +export function infoUsingGet({ options }: { options?: CustomRequestOptions_ }) { + return request('/user/info', { + method: 'GET', + ...(options || {}), + }); +} diff --git a/app/src/service/listAll.ts b/app/src/service/listAll.ts new file mode 100644 index 0000000..bc1c683 --- /dev/null +++ b/app/src/service/listAll.ts @@ -0,0 +1,18 @@ +/* eslint-disable */ +// @ts-ignore +import request from '@/http/vue-query'; +import { CustomRequestOptions_ } from '@/http/types'; + +import * as API from './types'; + +/** 用户列表 GET /user/listAll */ +export function listAllUsingGet({ + options, +}: { + options?: CustomRequestOptions_; +}) { + return request('/user/listAll', { + method: 'GET', + ...(options || {}), + }); +} diff --git a/app/src/service/types.ts b/app/src/service/types.ts new file mode 100644 index 0000000..4e46b61 --- /dev/null +++ b/app/src/service/types.ts @@ -0,0 +1,29 @@ +/* eslint-disable */ +// @ts-ignore + +export type InfoUsingGetResponse = { + code: number; + msg: string; + data: UserItem; +}; + +export type InfoUsingGetResponses = { + 200: InfoUsingGetResponse; +}; + +export type ListAllUsingGetResponse = { + code: number; + msg: string; + data: UserItem[]; +}; + +export type ListAllUsingGetResponses = { + 200: ListAllUsingGetResponse; +}; + +export type UserItem = { + userId: number; + username: string; + nickname: string; + avatar: string; +}; diff --git a/app/src/static/app/icons/1024x1024.png b/app/src/static/app/icons/1024x1024.png new file mode 100644 index 0000000000000000000000000000000000000000..08dbd5f4a09da5a8e4f54ebb1fccfbe622ee2008 GIT binary patch literal 59336 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%0DZqZ7Y5Pr+NCwpF_!JlOAVQ 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 z
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 AmjD0& literal 0 HcmV?d00001 diff --git a/app/src/static/app/icons/120x120.png b/app/src/static/app/icons/120x120.png new file mode 100644 index 0000000000000000000000000000000000000000..718ca79880f6b2a530ee18316408da9555703d69 GIT binary patch literal 3231 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�(wEH@R7n(9F_rjwyQ$4qxa% z2`#gv2ob3JZ;k}Nk*8d#bj&>J*#1N<-ftjMU&rdM s&BMcg?6DB%aY8fVztgzWi13v_^ft`y4-utvT`mAddM3Jc+71!_2eRZa@&Et; literal 0 HcmV?d00001 diff --git a/app/src/static/app/icons/144x144.png b/app/src/static/app/icons/144x144.png new file mode 100644 index 0000000000000000000000000000000000000000..f78346b9c6163e772729134be111de0bc45f1d95 GIT binary patch literal 3791 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 a& zwH8X{L|D9GFAP17RkGU!0 zDBeXNR)>7+_Mu7gj0$@P4k?wNini<+;jVkZYd7JZgUCL*nwUuO+si5WnSA|xcMGKk z4_M$`jCP>rTz}-xA{W{9KMSYwJ~tb(bMKVJcZqbD7rpUj?(GyXrxaGBf}FF%d6dIh z3;}GiqWMCLs3iF}l)tbByZ8%1<1+-iE@u-&aG~W*W})ncM6cDq&72rB zf_#{|EL#I}hbzc}_-XLf(u!zf3b$qB&+;C-wxXTS=4!Xt48Mr(IcX&1h14%7c6RWk zHaT_i(KC59QF-$`sE^oH#2xwsP?k>n2}ZzvQ%5c=-YeBvre%TB?PfKTR3Ac^UJ;lg zgrbdQ8CK88y>_;$U+s!r&gryguwS2VIo{70A@ s?$Bth?PM=Q?f=Xf{>vT;FOMVCVj4QF55nPB;R&FtWu#fI?(q750Oxyu`~Uy| literal 0 HcmV?d00001 diff --git a/app/src/static/app/icons/180x180.png b/app/src/static/app/icons/180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..24bd062906510ec33eededef567a3a8bdd61b601 GIT binary patch literal 4844 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 NK-Wa4M$0MwzW^yXPQ(BJ literal 0 HcmV?d00001 diff --git a/app/src/static/app/icons/192x192.png b/app/src/static/app/icons/192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..a8ea1a2f780d32e825b5343bbed44178ab0ecb15 GIT binary patch literal 5319 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 literal 0 HcmV?d00001 diff --git a/app/src/static/app/icons/20x20.png b/app/src/static/app/icons/20x20.png new file mode 100644 index 0000000000000000000000000000000000000000..0abed04c9c9455c14c5d044dbb01a31e73dd44d6 GIT binary patch literal 574 zcmV-E0>S->P)3kJ6#s%PUL+!IC{%iBEv4S-pM&@Tdh#qvPg;cBEO>|!KY)mM z@gO2C?WHFPme_z;5o!`Dq#}5bQUg{~#iZ|!hc+cOt489@2NsrRW`8@&umk^C(&S8_ z6g<99k8yx`fxWke2Q-j-g7kSW^2(|pQ>(6WY317DIzR}(#S^h0&}ksv8fd$o5V+8( zdq1wg6#xb3>y9P<9@bc>jMTRZ5UB<~Wy~2Q4JLQEn3>sigCy{2yUzMc>%gW9&9a5@ zTw08orch<=W??*+Hlyr2>jk!>{}34dIpvpnA!e^kq`zgLXm#FPc)7udiW9}!;;EkV zHe2pTIOdRCdT8I1d@I1$R~Oy_cN)F?NOvMqnZmeXMp>}Y%}crnDRa1kOe1j{Xd)$e<6n)De3SuYAXe`-bTm z6&4JOYBfH?R+sb3wG#0Dch9fuQO};km@SFun1~%PNMYO^$d*(q9SO5S3 literal 0 HcmV?d00001 diff --git a/app/src/static/app/icons/29x29.png b/app/src/static/app/icons/29x29.png new file mode 100644 index 0000000000000000000000000000000000000000..a20d3736400b6641dad628891f5fc47a441abafa GIT binary patch literal 780 zcmV+n1M~ceP)84jKN3^>umXh^x(cjCn-;mQjY!lGxd@ez79p-n zXjT0I8Ez^ptQ3(jYuuRSY(%L1!emV6-nYdxGS<=Y10m2KT)6Lff9E{!;T+C6uu~+) z)&~_SC9{@aQ(SX?9g5&Y!q-2{B!w0<6nMRX+`GUPfvn8}0Tfk%L7*WN?CVX;S&2c~ z;oOG;H?|4{5JW&u0xyc&E6a?~qPEIn6}`Mwm;s+|D~(2|N3}dw-8M)$o)~(T>#@ay z8hgwtID%NfUh|mz0-XmC$OK7UVG!{ohAY<8prwWI_q^d?pFb4r%>j(N5wvkQ*ykS( z_W4!yUXn4BK~+cn2c6{rb&W0|8vhruq9)G6vHnTc zRP6kW#hmrjM=syW~d5pJDyMVWJdKCh`kI@Muq6PvhL9@@isfLWA=G(m+fYx1Em7qjX(tsJk+$cfC?7rqI9`^#(47T>**Stk;1F~50tmVN`WxZ`1IE&B@q0000< KMNUMnLSTYqnPJrc literal 0 HcmV?d00001 diff --git a/app/src/static/app/icons/40x40.png b/app/src/static/app/icons/40x40.png new file mode 100644 index 0000000000000000000000000000000000000000..2b41be67d60b6bb9b81cbd3d350c0f9ed4cbbef0 GIT binary patch literal 985 zcmV;~119{5P)j zy%0gMq7DYT7|e~yZZ_Bll`+BT=10ZORl8b63kA_BHf>S!)#m(np|SZeCT(8hko+JQ zdCv2k_xI+U_q;g=DDZ;Aj2Ue{XFcv%&*I{}ZM~69IZqaB^EvBZ4sWEzb%Z1^ePi63^zXDT$;G6eG&|!5DsE*qSm4IAX6*McpJ36#$?R({4 z+bWnX7n^2R$YK2gpc>LafcGxOgkAt)kc7DFyce!X35s%5a%%l#1Ziny+!fsm%+;?fa+@>C=HkK2gMz{j z6ItVO*Lve?vmRoC1aZ2dy)iH$J7FU_hH7H-j$)L*W9+>5O5MUPj9z%FZjB|mW9_12 zHh{C^_cJk^1WPCKkuaUqNODK6viTRg-}dZ$^LXD8ktfD~2@c2SB?uatl#X)6A{PUA zEFfawkw~Wu`W^-qn|A@X73oYX7}eN00byGWiB%`7ijLsXdT1H>|Mq#B0P`iuVD~$9 zhY2hJ0198ccC3FSu{?=}_QuZ$eqyJifbYNU@c%~WbAV~4mHwO-c+mXnnHve4s+8mr zVk@L$b_xPoy>@xUBL5WY0|1vGX}n4IDD5eYhLDc#jpC~}c|wva$HQ7}Y9LSiIp;0I9zk=STL zLIjM~5R90L7fc90KuADQG_h?I5Gn*LRd>sVG+2d*tS!r>+nt@)AJ9VEncXhCTiRxx zpYxsXo$sBQGw0--12{N1IP?J|k;a8vC+4f+j8TdvTDl5~hiTA=M%pd}%1>QNBrm?v z%1L_3RsBUmSuR8|1ozBj2ZTYvZvwNjK0ERWmYVWPv2~_InG0 zwPC|t99Va}DONk%;yoLu=BY02r~pN&FE6zaXrOTV@2h_f+O<*a0;)^h+8;~;fB;1b zWJ{bj-6?u+x+g>A4A2MqW6v=R7{$IdwGCE5%Qvb4i_pJJ0)VDcTVxlsG%CRO^wpC- zV%2S9REDOnob*A^wr=a6kt8;e=rhEqI~D!#AA(mpU7=)&;J&-i`itO=J1+<%2S9!j z^#MrID1mS^Y&`5Muf5zUP&r924!If=GD|kqH?)n#$DL<6o{dxUv<&THvNJN$NE`@xx zt~H1=tp_6!tbhK=VINRYzKA8BHly{&jy_R1yRgUlfncE9GRm)I?zK>`0T5TGR)t0^ zD;&JWu2G-tqjhEOg#_0@L_qok*wG%Nhzc?nA~QiwgNQ&xNUr?+?6qU#fn8Rbea>jL z7aA3L7q>US&RsVB7430v4Jbeq0P5zsUnjY_-7Ex~s=W{HE30X2?W&vXI4Z`_a7tJ8 z^Ryx_$imxqz=GkbXJ4-B*)m*pe^ocv^WW*RqSXMPM1$Mz%q8$Zp12lR=_X~Az@Q-} zEQRk{(-iNmpUo{q1+l&K2j5D9J4C39qGSSqspXFBmM`#H=_d8*3Pe=Fm&Fw`nuIiy z(E@;|m@DOAa{+DQMsvfy>`eDJo+G}{pVLPV3YyAiUH4lAQec&viax3eP>_lv4kD6m z!v4I0KGZ!nESP&{&bXQ}Kc4;Lly}@JMOBXg$ZBt6_q44)UBd36rWY!s0W{jiMIMF7 zwy35aARrTK&YlKP)uokXzNOJ_yJNkC05b%r2HEkTNf-jt000v-0JZ?;VZqIpL2_^x zjcO}x8>6U8UHJ(yf9-5nJ%EmL2#VmlN2(eQ0l@w$|K3QmKfko9VHp6NDswM{U`|Ii z(U^7K_})s`vOBo9%yjEf*5e+GvjjKn{QBSHs-heqL;>Nam>rVO9ogCJ~VLCsg@fvuk2I zjv8U(72fd)^%f$?)I{0;%>3+K5Uc_aZ=W?x;Eh-&ffz=8<+YbpJvxu%F9dt}FD&2HMAJnnGLzu^lESlverQiK`g*o|TjIl92uaL|K7eKJ<9= z0ZR?Rk4A>tsyElPWDYiEh^a8-uL)du_ST%YCnR=^b>qZ*RnHhD0zL7vlS8P688I&S l%4>tbJsRg792^{y<$ur)-3%O*Liqpy002ovPDHLkV1fe%so4Mk literal 0 HcmV?d00001 diff --git a/app/src/static/app/icons/60x60.png b/app/src/static/app/icons/60x60.png new file mode 100644 index 0000000000000000000000000000000000000000..167826b328a904c0b75606db77f392528008bae5 GIT binary patch literal 1521 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 literal 0 HcmV?d00001 diff --git a/app/src/static/app/icons/76x76.png b/app/src/static/app/icons/76x76.png new file mode 100644 index 0000000000000000000000000000000000000000..0d9d28e127fb68096ff163d4d314514842057e50 GIT binary patch literal 1972 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 bxtUzFGGw>2Y}c&Gdik7jaA literal 0 HcmV?d00001 diff --git a/app/src/static/app/icons/96x96.png b/app/src/static/app/icons/96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..eccf3967ee0e6551498b1014a1f322d85adf5047 GIT binary patch literal 2396 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 literal 0 HcmV?d00001 diff --git a/app/src/static/fonts/.gitkeep b/app/src/static/fonts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/src/static/images/avatar.jpg b/app/src/static/images/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2010a704fa8a3bd674eb7837fe527c6a9020009d GIT binary patch literal 58805 zcmbTd2UJsS*DV^lbb+9NAXOwFNC~}23ndUB3B3t|NP_gDpeRK^BE1Qrm(WAeglYk) zq7evHnsfvNML=xm*}U)f|Np&bj5Ef$HyO{)PS|AZXJE86Qyq*G7T~kL#=YpJ}sezV>s8TkC6xI&LkEHR*X(BJteN++|7#`{UV`sEhvVW<;+<=et`>;7o{N5 zP-PWWHFXUGLzt1Vi76ayZDVU^@8Ia+dEM)VHwGIV5*l{vc6dbWgShyFhl#k1%&hF3 z$GLgFMd{nNH?KLwgGtbX@cdq6&=MdPpYs zKprv0=u=?*^wQ>TW^pCd7v5_@qbz46pi39_PNw$H%>M5t7W4mWX8%30|C!eeh>ea0 z`0?ntKvzJ!Z>-MaVJc~fw{WVs6r8SUx>=v0k_CSW6AB3f$$3*$a?)7s7euozlylRp zv=MPVdvZFjKEN|g{KESt%U;~M`>6Y%`U=VTNSZwL`>y&?^;5?IkCM(~!k<3jnX8{{ zG^9r_wg2}!Y3T>9f2e;!*HU*bf0IrI)7-^g`Gk%y|I>Hl542o|L{oZP{->+_m+^75 zb@*ZVUJ1MK&Uu~1%D*7*{UaD^LkLswbEMT&B16pBu563n{p(cOX6w1qtB=Xywh!+G zy5?5|vqVD7dum9h`an3hP2Kubpc0F$$RGuL*wkEExr1}m&<-y=PL{;FlNR;thM#L5~HrW z=~ek(h40tNI{tj8mm^n{J?eP7_Ux-+ezZed4k5T=?nM@prGfQgXUv*`(eWLxO8O!t zt5Lg)3#9q&&xjw-Yl5)wvq8T`+aouhjp`=svPCKY7pnH`*`WsUM>(I-{a4*&7oslC zNl}d0rJzywB0mm4R~4rwrNZ?Bf1D?IMvO^S(k0fmqQfx-Qy@qESp*e|j()ho~T(sAkiPTjVvyAxQN5@$lA_Wcg2{i$DN zb?Ye@hDYav{(?e~8<&q@dUz#W`{LoNySRY1t9mxUy3kjB!=%e4gtN@YGgbHiC`;c< zZEdOj$J9En0toP*5kFCnqBm{!b#=y1wOn*Ws?|nO<4Zh0Yh=DAo;#bg66M32jF}eh zoQ!dJk))C$?S>`Y53$}xc@22w@p}dktXh%(f(_@86dQ(8MCm+*Lo~x48Hw&vzlYr$UD|jHZi%45OR;?HEH57jIMTDjP1)bkKXp|p)@x9Svboi$Rup-v( zb2Vw8QbTFXW4$misAba4B` zkPtD1HW}4jB|?4SA8FHl&mUs$Uamd!YwHTnAKp3XvLxwfi-8ub`kPg?>@P3MUVfdh zT)zJ*$C3@pearIgUr=1whc5z3sC7F?k1@dqZx-I1w*8Tr_~0ZiCQ` z=?OE62J52D2<@G5m+Whl=Wp$}5|WSTHc{*CPNJ>t(3H9ghkwExkGLBX4l%&I-8c|j3i?=PaGB&| z6UJH-i4^N#ibRN^^Jts(oodEB&vpWvwCt-a`4Yp~xN~vFZOAqhm*k$&9�#uFE+G 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 z zu3~sm*L%*`#Sw7lL4{Fng6MUT5Y05ZkqcfyHnKN_o zD}t0C5{oCDC|TU52VZ`raCOp3Nn%=WVhE49-JDjdo6WFw$D)1e#Ni>Y&eh80-sI0} zzJ*Y8*Fu8MnrP&@!F&e&y%(V zSE2H%nb8L8;s722%#$P));2w4ZcBAoOu2i>ym`5Rl1M-TC*{uVo7+v_&JIm{lPuxn z_nX)U#A6UDX34cz1LPH(%pkS+4re7+_!osBJO#n zN3p`ZI~~!?&dYO1OEeWD>m|J!W9#v-vPY%|3!ZESA^^UdXANYjMd9MMax6$6}$doz#pwM~Ag;@4kz znjvT-9(qNkDN{b$g@+^-`f+ol?u!)1im*S6+1+p4Q-|KfcP!j7YQmY6z2xp(TsLZ2 zj33O)vt7e$;yg5<)k(|u*5Wo(KW)C?Wrnvzt7(Ab-Kxbf?nm@GP>-=c6IKfSEnw{U5jhc|uToYp14 ztGk_^58G*4p#cw1)tw-vdXygmViARFj81R{$P1OLmP1qB_fgR1NA1J$38G-~LDh1< z1dR55Uqbgur*|&eCNu01R~TndxTC3`JbqthoPFvP_VV_`{9ThIPUhe|Qf8SCwelaF zGM;(PK;M({ceh02hG-W9VM98U#fpN6JFMJ^(jOUBgOu;W0;3ir}T1 zqN=P^r{n6${X$0BmLH{oN}_C{H1E-^B9Gv!eo-qH-J5OAduAgx^!I=tR7HGTzljjM z-f$ux1vb_xE({FHf)m0Vl=q{2>p~Of(L5c36+gZ}Lu(?fA8d5So>oIN%URA(4r6c4y zNq6L?E2@x|UOE)ox0~q#J?_ssSSQ&G^MBj1SrA^|kg5CL%p1Ql8}W0IQ9D)>vl_vE z6NMv_?*59R5mXnuvnJ;KG1W4RuY*Fv*vrP&-`i1)AK0I&6#FWE9To|!;8Sx-$+U;= zvXM|4`7KxjO3zZx^e@o2jGfDsXc;x!f!_{Qfk7{CM&N+6&ttGkp8`7rb?~IQix!D57`O^DOT~k&DJUn$n{i4@imdu(ik9G6wBt!;wSe z$}~oJ716?vET?v#?5X~tnh_KM_r(puN^L8G?z4#?Mbm;{0k(On zarADy99(KFmzW@ewChZ=6LF{nhMHwCREflp%i7FUOX_|Mx7GP_jx`*MihhgK8e}fJ z)o@u>9Wbxk)|hFxJBkY5Sh_0 zGQ88(Ixgw|UH&rdP)(lvSO-qV$0R-h=MO>HsD>b9C9(~uH7_Bj;YPW~ThHb8%*Rlx z!V!v1^&G0>dSX3ApC|Kc_9{#IgmcifUwmmsi?-3wpY`gQK(!4~R-Rn%@adSw@v-?4 zfsBo37}O!|qDU9TgNo3PiNx@3a4c)0NP^wm0ipbI>q!*S&UeW|e5;nOx?=EgV-rY` zq^8rc$eO(yw;X3|N1C#(Ugt-?zhF)u_R?*TYCm4~l55eSu#^=RU9JZMx|+n;nYMqF09(o60Eg7x1< z0Y+rIa(2iOfvC_GiH&gbtp_}D#`3;Of=W?B`ieZ(is7T+PnZw@WdS;vH{``7_Hh$< z+rVcu-p86(*y(_}z~4=kGZ+>V;CE_*8uj!h_=-lsX`mvs9)p}LNx(Fj0>F2B0#(of zgnD!U#O3v}ABIfo_xBQpP&c8@aY%d|5^UbiHTGE5!ChVF8o8sY1q-p@I;Ps#K5bzZSZaG}yeb!cUm4f!dhQ$KVXIvd z=6|V$T$+rE7)^gYIF1dEaBf<9(=UW?Nq&YjIUTNk6SvtagD>Xv3_#juV1YVYHKONf66o?vl=Ky1?_6KKtJ}p;)YOA!5YO z2+Wce2G%#7HfG1V<|AiKA06`2XqO`Sct@oPC$aS?9xVu`@27=Z=(_3-Io}sAIt6Zf zyo#k8+|q2E;5{Ka@{ZtDUNk>0duo)aKLu^=$f6t}lK+gffb?cG*^aWFrRG*?I-A1z z=9HW0em+8($IR-NjbmTOd^e*aRQ||~mMxRk4be*w_bSf63!HQsqkoV4pRE4R5cv-p z^DioM4FTZhHm-e<;Xb$89kX9?cCf3ah(KP7l%NEp zCnSV~8mdT>DnUx!deNy*y`Cg}owUzKEI4pay~$^R=KG#3*>8iEldeIuJ0m zL$kise~P2h0D2yi@VbV29rpA4{P>8q^@o`4Nq*&>KFo4n>AbU-AH%{N!2#?L0iC1I z5GTWel%az`01wSA1$U4&HN7UN1YGx`{D3a^F1FM3-U|l_H@){Mp?I6MGtvER(oa~8 z^Bo-rg~i7^N_pwY&>96(2U-T+X#^2)yjQXJcBVuIX5stcwMULonqrPa(C9N&Nz$Js z_>5XK;_i@W83lLmtVi5XsU{S~FAS^B2u@G8xILX4Ie=j6Zk7?a+IwVJ6rZ(X;!hRC z#)ZR2A+J22iXZj^`NpGqkjfFQ;5GofU<~ZxPWu|+fDmcAd+Hg^Y+t|t9G;r|NnP&T z*gjnzy7!T_nQIo8ul=klXkC4{R~IgGYDVv(wGPQ8UDyUeX}GZEcKV=BoCi{&yXZ`{O-BT ze6xcB`cd8m%wM77!YQ#Zp!rDQ=6$IPk1 zh-p{%-Cw9C-Mg@v&Bg!AMZIP7ex&eMzzQkuXTO8o&Lm)-14`%qZhN1X`7ev}zd1d? z9%g@_^!B%-d&rf&gB6DaQna70mU7lYvs=0O;ug{_TO2Af8Zq`V;Oj&PgAo|O4~4XY zHaVcLJ0CDKGC+bO0JEMU?y4}(c)&294$|?LW&+S}7N=co*JRu2HG5TVm6_Vd!)nD5 zB4kgoFEh2W9`2I#eXU609p&I7RF_B2F$K7@L$R$@)fv7Th znD>y=1Mi!%UAmvDQ1l_fPb)TU+(#cDcWQ2u!b0MgqgtUn6vt6IY#?vl8txP(wt$-Hbea~Rvj<%2Zw?)P?lJVX*uCq&P zSYH%L&C{P0s3H~&!?kmOwJQAZbyYE;@(pRde{N&E^|9 zT6oD*O%ZGwN7RPk^c2Fr_L*iI+TqyIkw)^oewhXd%wqBWZ8F0Cpp77M6(RD$+>}Uk zuBeJ;5t0FY=hJLD#TpK&8agq_Q|S{iR-h{U|)G|M2haj@enS`6}U<4k7MAw&%*ozH>wpV2O#d5EVS$T(XA(Tal3~Ow3ibi*m=tYd957zZ2D=a z8xGBmiU2%!zIx)oun<`LfT)rA%4puV=3flvge_Y==F2v|C`3^|+QC$l)v~o#^1M+Y zsCq(3yQof!MMSHtm$Qz)AgKaSt^h`LF2KZ)t@sCF`AvR$W6+8MY*(p;w2kXYz}PFZ zBUKHjQWGy50U6&x;Ag4N8KsdH^}i{Ln3`Lwmih80{O|OaCzom91Bi;XU+ylKy5h;Vw`1bl!oB4D; zeewC&b9gBT6qFOaAak$^e#8DV-mG;|4_adR?zk!w`S8OA7{vTm9}>{8CJqPLN_g;W z4Toyy%A{5KL5eT+HB}FJd5vdu+(T>DiRV(i&mSU2Sdh7F{IeTkYQ{byjr1^HeJr_7bqA z9FgrpK1(rUt-5NDCAG&))wc8wi+bIkd63QB8q~j;4>vcs3;@%BhxorIzuof@{c`{a zz}M!woEX`=`#=GZ^utBnU*?Sj$ehrKm^Pr#F z0I+HKiRQX$O-(Blx6WM+bpKNQEA2O;c{ z2|>&K7H+ADR{%2d4Y^ajnmlpscnzbc2nWb`Zl>Jf&GRKx~G10nZm=LC;d+PB3)GaAcL zsBz;Kfasym?BzmEYX+R$N@x+ZN|D}7krx0bd>ndVK~d+^kj^GLagDEim0}I~85Ouh z(fQP`gZFyXS*iv&8=P(cIdMQ|DEE6qJNl>L%faaQh+X~EUjqlqgxSs_IW8M!A`e;I zWq%GtksAf#MEp7`RB)n>=V*sgNB|`B2itP8jwx= zJ+)82BNmNt9suVJvL|?C!(fO5u>$^5(p2cOBQv0l17HA@tJbLvpzc3PZNErP6O-q0 zKs>a4^&Ri0v}RcQjQGvI9S+V!UWIYycV?4Mc;(Yo{Q><;7WIDEbo<6MQ*KSXoCf58 zI8gvt?=qjk^mfjFzWLWqpVj47i-CCPLc=6kY(8aES(1JhrQL@ME7@Uq5xnyJS)6_o z32t$1&ux-03I2k_!y^7cyKl$=dzd|DeX!F^%$~MsXav}30Go|XZzkczJt5qcAWOT^ zBj|7j!*EofKW@(1=uq6`$HX@aYITi`!fa-JGwUmw%!xtfH`F^jkzEmfYe~S->=rw$ zJmQ8@to@>PQ68aGDkg%B2??EXju@+y&`P|74L&k7d(v8;6B}&e5Vs!o3-fkR@Z^1f z!5P`$mvzJBE_9<*Njg&?_ozNI!r~qyei5vwauR(Ey*#xKJoKbbidPWAU9D_aO z_-s~ySf|SM+(h_C6seVn?CTIc%YKL=u)d;Mn7@)3o9K{8J&kLMc9@iZ_t^TeMAh-L*PYqC zO%O@Tc>&g6sacAs=l910f_Eg#zjoZYZl zZjxkGVe{)!x7Ma6XmGHjv{Y1GNr~g5 z`CTiINUE!-YR0!kdo{_xuaUWaSEHMM!BQB9rRQ4>GpvtPL5;3BGO`(RgJNou6~Hza zm{cZ$>==XbtrSf6SI>^hp;?+XKk7}?yvW91jty>W6~Phbsf8zcKjX?T8U?^paG+Ck z$V_c45T74lkaC&p?LDl2NFIMUHLMZ+Kq9T=VD4dsJPbh-HCdg|9UJRwQ&x~HeW>V+ zu#s`|8pn3<*x#Vn@|ESl`ft-PN!7TG7!GJyg(N zr(iz3m8g88sLF692Jk_g?A0$GjN;Fq1Qfd5)`a)qw2oE{XzBV$;f)!WC+g#wgQj*- z0Vz{klycAAXqx~&`|+PcH-$vlA5o)qn~n2a@Cu(`qo`*0GtlVjShi)^CR0>f5u&>} zH63>KWcmQb)!D986pVEo?GnU(8cG+^njN_?Xk-6Y6HmVFbU#VY8XKTKrl1wOpE!XL z`|bDi#-v=sM~kVd)mpCNx)JNaOtJcfGdQnLxGU{Y&@Z`*ZEKNy_8e&8F`~MngJdhITwPBfwNWWC|0tOiAAmnW;MM>|~1f4NMnZ;rlhT zQ70;U32W-?yfZVWctw~rKF$|Y-}#~deoyiN@r3?H0Yu<=XZfl=GunVrM^~}c%AL64 z{QTIRMeA*i8BQpVk`;=K@OSNL3mosgd_hJ&KF@IU+W>;IIUv=*Qw1 z^igx0%NKM$yL{u+kO2$Fbj_}Q~m^;c`8Z71@FSZQPW%gfGx75Sd- zI$4GmC=NY+>?7h_r@%o6Evw*c8-$Ep$xU}^|r~Hdg3re*`LEpHp45kTZR2M(=$RZ^c#-H zpDf0^qFr9OHEw?6_(qXQhTUO&^jtf=0U^n&_cKk)+c!g2@uojWI-}d_NZ^a_C{3ndZ3q#ufk@)*o*7EKx7SzA5jrXp(Tf5+-A89~sQpmuW-#S3Qd|Yj?#t-(5d$8(Yx41pqHYrDChtGQD*g^yL+?Z%zs9Y(3D3@IE7=UWuYqko| zY%{jnxtYH~iHUD$bw6Hwzui@{Fdw>QkbZt?(b(PUg88l8LFvBc-CGT)WT5^*`~1Q` zpG;|nz+J@@`v7D2ncUr5!G)%J+eFqmKG=#y;JLZ$WBI_n!W8FY2X*#Kitg73)!biO9hch{(LzeS#%TEaWwVtLG=#dVZ3xWU#t4LNFj$6iiVJc?T~aA7Po3>DWD z?%oL;n8Cj8quX2MCic-)?2vSccs|r&BjUzU&F-tKvV1Sd4#s|+;KaD7Dw@UD`U#SI zn7&ewwMT%aB+_=|9lP->xf%F2b$5`BKASsg&=!JqlaEq=n&Iw^*E1kP6ua9zE{&DD zp0*wd_wY9afva;LT-0tr1&CSm1>+QO_ zy5Qj9x3{=Gp>E`egj zB-!E$SHTP*L>1CoxM-Ugz(ZHGBg6Lz6ZRUkH!@e-47=O$R{}jTF1=$8Z(!84=r!WO zVJ<%8@h;nPuB*Ue=7Sc?zaYNO#qj0$K{FROdAxy*@svU9g8Neqf5{h5K|J>S(v1{l z1di3rJ>Z^cOuW_2(i7|r@S;oqOnXoD-lJx3)@Ucpo~xT{*J>x)-BSHLx${)-6XoVL z?L@Ofyl<~-CsIFJjoZAR^(5(KsntL85|d)y80Yk8Z}_jiN$Jw7s#Odx;{PDlDdLnX yP?aK1xlO1_`;F)-*O)H|f*=TjAP9mW?9K;~E^CkQH+8iD0000Kh(j_6S(k&&@ zEfUgvYojyIjPtzjJKrDk8$IXjd)@20uRGV;`rlNRJ9p;d83=;TDaa$$AP5HuLD)5? zPk~RGoJpO*|FG=U+N;iZb*jRBH7~0%7;&QQi2!0Jgq7p6-4IWq+IndoVGBL9jV_vPSVWu-P z6l1=_uf(JDP}<1UOy14T=$4zZ`U5wM2SSF-65{91h`I;^306i926Qe~me%&dE@Fsd z!NTCr@WMpuV2VRlXcRap$d0F57@k##Lmf?@32t%JfoR-V(0z2mT6w-8E6g)F>pXBWd z{5V`i+hbE~q357TCG0D6dh#$zUjyO-#|U5<#u# z!?AW*R?WMmAfLa0v+aiS?a|rL*SW9vQr$cqJ2papEK)lT9E@m}npb33W{X$cPR9Y1 zxSNr%ryQhs%PCO#6*8N(%Y0@d!#c5a7z^sOh@^o@^Lc2ej+E4ZrLwpsh-`>8MaDrZ zqgdQ^H^L^qU=|BX9tb1Cf}q-f4+=!YLqh=>u3-pz1R{mh@k%OS^Scng(?AH@^8Ppf z7m}^k#4pHNuh0qENcpKEAa8L=DOic8unxGGPv$wsk>NDPC8;CbNkqi5{t&ba;Sqx$ zpws-r{OO_(5DNrX>fQp8uxPPk(Y0X~Oj41VBHyc;PLU$QLrBWk;T;rCLeRzu65tt- z^y~Q3W{tveL>T)FiOr{YA_R~mHy!;62;wnxS`p#B#8~Iaw96%e>1y>un-b?xGM#|t zlo-JZ(9;%naW^}Sz@LJwC&A7R;iN(2z|t7Bq+of+42h5&4loLFg!$fmBhnNPcRA z^UTChPCyo2bVdtT!fyALKoX}M_%++lU(?NJhJAZO@}gw19gZIdDdc!*=LuAtYDQbqtuwknSEyR26bDaGPHVv^i?kmY2McU#kGU}bOd9$}d#g{K>M>AfKdwC;vYFnlk5 zsv)OazWT9JrZ*m4J(f$Fbt!6srf!0A=q)7!5{WE|{%FqnT=3vU9QlVF2pZ%#MqrLK zZ{$bR0V+#6r6CD_o&h!GMyY7LEC@kyPmZAwvxcE?%xd#TsbCc3+_#?^7Ewk`SgnLC z4Za~p3~|7FwWO3q1K2k5XDSGeqn_fEFxkDkq)0Gm3qjYg8DOHg^#<=_%E(WiejC%1RIy_zbOMqr<%P|gj61r$$ZX<}Oj{U1|5#qJU_1o)nAI`O+&;0d z?tJjfdHmSP{U$3$#L4s9b|eihlpH62ymr`~(e=4_lH!gKLSfDT*m0B^`Hz>aZTDbq zvUn|?7rAtUVMykh=)jD89^Th1qDqhX3YxK*m12qgfo+xxRm-EES_YM0R%@7knz%F- zf*gM8UNWe(S1oRES^6m{%#%2O*kfzSDs0D7BNT`dE^Zm3)1+gZZ8?ddT4Q>c%=t39 zJ8$!DC?wCyWck!Yd@?uVw0FXXyseMDD*vSQ{-&JCi5%rHd|p;n5vS@^3?=Mz85o$x zgNAt(v+RWkzN@8uI_xy2xv;wl9}_BW?sm=LT)f(Xu1Ls4zD0b<*ZhX_Ceu*Y=(b z@16oOY3~%Qzm60zz4xV}#Zub1PT}XbQk;tKYfX8k8?8(o?RiO9mdNH+tWAb0qYyJz zj_f3q)`DjR*Epuy!(OALfM|bwulFhH-%6bK%$_X!)*O))(sCk%F(Nf<^8&W)x{zcu zjA(N6((ZE4o@0t}6v+>#H(G)HcAEZ)5jSZCcerf?Ed;LHxJ)-!^Ib4?94ybVmO<2? zGr?j`d@@3*c3a`JjlBS7LgnsV&46%syy`4;J9%I|Fs9-c!E-Fv?#nt$C~Weu_6;-7UetPt81r zz?W2FT{}bhV8bS1=0v2}m!?n;$C~f}!_)mtoqQI}fJ!W|P4k~P9c(=yMHCA$6)5f< zT8tQn61_d6wP>%kI3(w_dFoKW<80OLq+SB4!Ybn}e(%)R@6VNvsf!##ka&*4N-w0e zj6vjkN(Swt0+xz{qW4a9<6ePYa59LRi&Ht8;SBOS0Xg8qsi>qScU)YIL0GV}LPMUJ z1hL~69f1;zGA>*ialY?fbj|K={-Lk7=4XCLEs9D3y^l^ffiO*`l@kzLu&Mgzq-EoN$uYq@=!NYxn zF1=hC{(tL2|2$2m~1lsReaWtr#Y zKLS+x!D(V*&ey7YK0Qh0uJkrIP_6JW%|8M&mtIDu2&Egw=$KsE-wDCtq>@KR#^WQz z>dme${#Xe0a#W72kZ$m>)%JPqE_3?{)mDwGYs+6P&=Q#Fxzw66_eYRmXGRvah>dRt zYg4eEfChPB$~$!B9(}u0DYFsZz3Wz~+#ue*L4&6y96_Y8*JamnXQjdi`y>Bg`D(+q z={v`@-k`m&uR@hYZ>df^%^s_Qz4yB%h1s56Ril#Ol#GhRmZ7S{F9(jk&4?_zci5C+ zGqJ%9)jmDmm!kDyg3YB))CvjaHcgean-pV?U;+-`-t86H!jxn5Gr#4NysOWAs5i3F zoX8RIhq(*vq(EL=oNBjm-|a8gI1hPm!m(NM+4AnP zjZ!9^_#31XS+ng}s>I?k#pWHP(pWq%-!MFCt+e^_8UK=i$Nu7#lnpG-1-IEghyRa- zEBxf$&W>r9o#ck}rQ!6|dA;(mN8puu3IRK2u~9H&{nCLZu2(g~b;TGGgxQUDCjU?F zo7S>{MaLdwX}ILgQZD^HrtRqBFxI#thnmixu=2ehst{Zw6To1|#SR7|WV)6!E|i=- zJk-{B0g9LO2r6Z--TqsyppxMKT2cw@*wS`1LA8gSqg)hD?;+P3LKjAwcl7B2ZkddY zTfnEmmN|EJO5d>*nYPco5Y+Dr7X_A`xsOAGaoctcFiTu~V@DHPht6ev{Q{}!aSX^b z)90ohnr8g#bEEaHfn)uwd?}Hbk^6pV8g99w|%L!ik0wVuETsWuB$)+57S>^nzNcWuWx)emoq#OaYdLJr8Mjq zUSoMUFi4>OuHp6t>#~Kmk5Vi6tPJrvfuMvUrToohj5hIzi6_3khP=;BL{fsSm<@RC z&)lF};ANoX(~nf^!E~GGIj<%W?$6^1czj*F^&mCXBs-jbP7LKsDRK&`HHS+SZ+=h) zQ3}y@SCshusQJ{^dVAl-lRJcnwYj*TxfrKVuX69HPes^tyGLknXb9Kp9h6C_e2MmX z3i|uaii+PhSLf@;y*_@HMWfM~bi1Jbaa+^`X}=Z)S8%l#L6QQ@n`v>!;8R{gW$E~I zhrT-;tD$%vm|MMjv~E%<*Bq_F%4EieGEcqMeRBejSVULLH_uCj77(#Nk%*Wxg|U2MBg2L! z^HwWJMml6N(CNEu`hNMo4-?Rnwm zZ@0NMeE!C$MMX5a#pLjNPbvQX6E7?%K?W9irjo$Z0%5sOpR&=e@#!yeI{>B9R^B$W ztAR6|Hi^q0l)o@bz-IC7eP!D33x8Gj8Mx+?;6 z)un1p!oz}1wbdZKZ*%%F&ynqmAE&$G?={vf(lH{x7xZT^VX*ZcjrZSsFm;S{mcB-0 zB4$69du7~E_8*9_m9RUfggp_oItArxz{PHU)r9?#y++Xpd$;f)8^Qbi{~-Q2l87~@ z!zgDn9;bC=V5K@M8#lXh#lzA)YiN+uKT#DG#TEToi}r^NAHzRDx-c9iqRJw5ybn@% zABIje?v|oxIRX~dMove^!<*;Z!gMkPlh>(R{6}GbJ|aO(e-X-=&hIxrTwHzBH=)*8 zanM_{w;JJ}$=c^G54S2j!Fa9_qgRAoHg9c#d4>p1c}4umNprv zYi=5cpJvB={h9@xlA`8P83OUv{aEV{oVYbYnu<-ztV}ajnppTxCE=X-<_xH{YCB!( z>uNsJFxOR~m+}`O{8LP@#B6Cmf1dMW+vPUPcZ3BS8gKddltwH92s9o8XVy<%7Tz9o zaa&U#G8w?4xN%Po2g<()KZtDst775ipv%gYcm|&|y*G^p8>*Fs#f4c5ZEqW9%5?-} zCOFY}`!s`wv^D_N@Kie$aJ0)^yS%yNC-7O*%L30s`ZCAzrh{>hcD+d)J43K3xQ>SR zLTo?dL&=3DE5K8!*|$=-}#afYWOe*Vjy6z3meoQt#wZh2UsTzIanb5fp@Ehsgd5%D*%7;MYpskoGa_LGuSvmR|6GZ+ zbbq$lgz;vm?K>ifMpEshr@mPcCqy6nFk_k6@@cH0* zm?YW@(T_t_LU4j_A}W$nl?C@Un>5KSbXBo9J_9=eWeq>f*pSNUAGAr-Is6@V04LBe zvK@a~0e`AWBH9G3yWv~2+onCUOaVd122rp%Jg9kbi-fueq+m!g4-O-P`8v2ALF3=| zyzEhai%-Dk$rA8n(XxfJ=4gBBu9$E(5;g=_;2gYwP}gJsKPDd3jLa`O>RASaT+ zU+{ze>C<-3(U0*@S&}Qo$M|@{bH@D3rH2zQ{NG|s6M_6WN3HBacxZ9_(Cx}i!Xsl; z_*~>eu!e^us}gwNm>~MMtq*FF2Dya=>)@T7OD9_bN^rvVu{ySSo7cAn1J#0#Tx@UM zXj8`zf6(sceDt`)6`^Y+9qbshN&?7DhciIX=+sZCwTt}{`@=6JnbuPP_}+&OyH{+O z01BB%uGPD(V{dIRIMIO7rUSbs-HXCu7L++vn=H@x9PZ%YFyu{t&13GL_ecuEzt{c2jl&I3HSTT$Z-qcbICVT$~}Rfz>!m5zG^wN z{RS5|(>f|kWJwsJV7&euw)Gn{a3y>n?W4I*LCPDVuu9t-<)DycFC97g>)ZT!X_Kb$ zUA^{CC$$LdfL`^If%hGNWH<}fdbsx;xqI)H7O9ZUqps>#=_MNJ898X> z`f6bni6|@W>*I)QW4q7MClY6zBog}y&&s|vU0ocp2DQA}LUYZTLr2>RZ3B-WN7lw* z$;v*fLUH2;ynklhqzxncy|t6Cb-%toBXIAX_`JCbTZ47dadofjm-6>sPVKEuIqAv5 zgL%-)MYkR&e7K^<{(5Yma{3IUdn*w{amuYd1cGkD@etp;bieC`Cg=C(Y6bOxBnHIx z2f#8HAAZ1xHr~Pj#!#6nvx-@%yU0eJ&G?iSUy|;fQ9c@Kvbp4|%JF$mxO8~exai@B z8<(6Uj_f`&x%@shmK38i z(_*(u;RIAeGZ%d|W7GHz>-VfzT>qAq2s1tnu62{RA3WLx!;WLbo?La<lf(qLnEK|o#7qVFST=XgJsO|4ay`PxN_UALuop}Ek_sg8wg*&|`t%Hs<7 z!u&VfWo|FKnEf%HJI<SflA;F z6$t5bSUu0$gK7>!J1%J;aLXUEL2s0akZ=H7glnIY!Qq;R#%w6x!j#@308D1`rpLV- zpdKNHT{Z9qt?-q@5{aEkY!V!1iVG(oi&P{nT*W#mnTx2PXloE5e{AAcnu=`XKitl9ayvC?NKTWLL*h;ed{rWSGK%;hp3{0HwO2B4`sXFL%-ogz!gXJBf zs$~r4z{N&T|IFpXTQ{U@jYofG{k-lqynJHp$Z+#iPX3P?OU@u7VinUyRmy)5|1?H) z1?RJ4I_OJg-YCwuJ|q6Ee%C@4#qWPqsWE$ zH89 zKq(ap;(&g_nCG0xt+!`X{9nsI1t%;>i}E+o^`lPIkhd~C#Im>wduzv~-4(3#DL?dQ zg8^*lnzBVX^_ zd+O8Q8NrWS`j4l!?kY^R_W+#YbzWGwJS5qiK@bwnV2qj6Fk_xbqE*$UL zM-3OLV9P$bo8x)Nk;-O6k#GIh?+PCzF~H_#MB2$}j|YJQ=(k|{pP5GWVR3)PZIbr* z!;Q9#02VOMXq35)l{^JDArgU8%$)@S8sxpwjrfr@{f%6C{L#85D0RH8?}MO;ZW#`7 zcjgP4_GXE?ETyrXbZ7BM=-}uIo0)|2?Mm+-5}NO>vs?0iD>m_y|6rzl#wB+6z(u2j zi=r{*U7kHW+yV2S?|<)BWzm0xdDAE0)Q_#}N`}{@(zB1pr+y$_;TOZ%EkfT=BXa2q zn}%Q~kMrVk!Xap_lzC;O7)^>myF>pwyTgdyPNp+$@BY}pI8XiaBoVmq;0;)2 z&hX07N)U^qr2oCurRKbc$*P}o1iljdE19LcomwlTcw0$_=QEnY@xdh!G^I^;Tb!AT zqp=a@cPvasheGuxBvT#G1`RZf$oTsKzicff#TY8$Xeivk^RF~4TkA=Q5TZOct2|zx#S3|l zf$qBYX4!JxYFj`wI6GbJLAxeTP&5BqnY_%+O?kY&caqBOl##U2bk9W&Y|5W-LCK|{ z;^gUkMJpm8fANtk}%&)j7 zkDjt+?#yLRkf7i-(Lt$*zl$cy3hqn47JHe^SL-=5pD$0CTjB;0Wx4SUU#+khf%`i{ zjZZSWMB_N*^n}INAJ7&oVnK7{Fli!TBBqO1=mm}#4ZOg;Fyr&ozG~vqi?pDv^lKN| z6wqb=bAT+Q#KT}LWSKIbAX1DrDwYul62F&0_pY!Z5C*Jx-BxRA4CWki
fM>hl_ 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 xv^IVE?)LU0j`nvm + + + + + + + + + + + + + + + + + diff --git a/app/src/static/my-icons/copyright.svg b/app/src/static/my-icons/copyright.svg new file mode 100644 index 0000000..8e69a8c --- /dev/null +++ b/app/src/static/my-icons/copyright.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/static/my-icons/sparkle.svg b/app/src/static/my-icons/sparkle.svg new file mode 100644 index 0000000..14eccc1 --- /dev/null +++ b/app/src/static/my-icons/sparkle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/static/tabbar/example.png b/app/src/static/tabbar/example.png new file mode 100644 index 0000000000000000000000000000000000000000..fd1e942bcf691d6d3711535d9419417cabd5d781 GIT binary patch literal 1371 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 Eg2>i=Q~&?~ literal 0 HcmV?d00001 diff --git a/app/src/static/tabbar/home.png b/app/src/static/tabbar/home.png new file mode 100644 index 0000000000000000000000000000000000000000..8f82e213c78bd38fc9e7a6eee31c05de0cd3363d GIT binary patch literal 1346 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 V*RY*zoyhZ! literal 0 HcmV?d00001 diff --git a/app/src/static/tabbar/personal.png b/app/src/static/tabbar/personal.png new file mode 100644 index 0000000000000000000000000000000000000000..0a569a2f441a1b64342cab45d0b5d85a13891962 GIT binary patch literal 2457 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 { + const card = ref() + const queriedAt = ref(0) + + const hasCard = computed(() => Boolean(card.value)) + + function setCard(value?: AppCard) { + card.value = value + queriedAt.value = value ? Date.now() : 0 + } + + function clearCard() { + setCard() + } + + return { + card, + clearCard, + hasCard, + queriedAt, + setCard, + } + }, + { + persist: { + key: 'iotlcm-last-queried-card', + paths: ['card', 'queriedAt'], + }, + }, +) diff --git a/app/src/store/message.ts b/app/src/store/message.ts new file mode 100644 index 0000000..cf9672b --- /dev/null +++ b/app/src/store/message.ts @@ -0,0 +1,107 @@ +import { getMessageCenter } from '@/api/message' +import RouterPath from '@/constant/RouterPath' +import { tabbarList, tabbarStore } from '@/tabbar/store' +import { defineStore } from 'pinia' +import { computed, ref, watch } from 'vue' +import { useTokenStore } from './token' + +let refreshTask: Promise | null = null + +function normalizeUnreadCount(value: number) { + const count = Number(value) + if (!Number.isFinite(count)) + return 0 + return Math.max(0, Math.floor(count)) +} + +function getMessageTabbarIndex() { + return tabbarList.value.findIndex(item => item.pagePath === RouterPath.Message) +} + +export const useMessageStore = defineStore( + 'message', + () => { + const tokenStore = useTokenStore() + const unreadCount = ref(0) + const lastSyncedAt = ref(0) + const syncing = ref(false) + + const hasUnread = computed(() => unreadCount.value > 0) + const tabbarBadge = computed(() => (unreadCount.value > 99 ? 100 : unreadCount.value)) + + function syncTabbarBadge() { + const index = getMessageTabbarIndex() + if (index < 0) + return + tabbarStore.setTabbarItemBadge(index, tabbarBadge.value) + } + + function setUnreadCount(value: number) { + unreadCount.value = normalizeUnreadCount(value) + lastSyncedAt.value = Date.now() + syncTabbarBadge() + return unreadCount.value + } + + function adjustUnreadCount(delta: number) { + const next = Number(delta) + return setUnreadCount(unreadCount.value + (Number.isFinite(next) ? next : 0)) + } + + function resetUnreadCount() { + return setUnreadCount(0) + } + + watch( + () => tokenStore.hasLogin, + (loggedIn) => { + if (!loggedIn) + resetUnreadCount() + }, + { immediate: true }, + ) + + async function refreshUnreadCount() { + if (!tokenStore.updateNowTime().hasLogin) { + resetUnreadCount() + return 0 + } + + if (refreshTask) + return refreshTask + + syncing.value = true + refreshTask = (async () => { + try { + const res = await getMessageCenter({ + page: 1, + pageSize: 1, + }) + return setUnreadCount(Number(res.totalUnread || 0)) + } + catch (error) { + console.error('刷新消息未读数失败:', error) + syncTabbarBadge() + return unreadCount.value + } + finally { + syncing.value = false + refreshTask = null + } + })() + + return refreshTask + } + + return { + adjustUnreadCount, + hasUnread, + lastSyncedAt, + resetUnreadCount, + refreshUnreadCount, + setUnreadCount, + syncing, + unreadCount, + } + }, +) diff --git a/app/src/store/token.ts b/app/src/store/token.ts new file mode 100644 index 0000000..ca4f55b --- /dev/null +++ b/app/src/store/token.ts @@ -0,0 +1,318 @@ +import type { ILoginForm } from '@/api/login' +import type { IAuthLoginRes, IDoubleTokenRes, ISingleTokenRes } from '@/api/types/login' +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' // 修复:导入 computed +import { + login as _login, + logout as _logout, + refreshToken as _refreshToken, +} from '@/api/login' +import { getHttpErrorMessage } from '@/http/tools/enum' +import { useUserStore } from './user' + +function isSingleTokenResponse(tokenRes: unknown): tokenRes is ISingleTokenRes { + return Boolean(tokenRes) + && typeof tokenRes === 'object' + && typeof (tokenRes as ISingleTokenRes).token === 'string' + && !('refreshToken' in tokenRes) +} + +function isDoubleTokenResponse(tokenRes: unknown): tokenRes is IDoubleTokenRes { + return Boolean(tokenRes) + && typeof tokenRes === 'object' + && typeof (tokenRes as IDoubleTokenRes).accessToken === 'string' + && typeof (tokenRes as IDoubleTokenRes).refreshToken === 'string' +} + +function isLoginSelectResponse(tokenRes: unknown): tokenRes is { needSelect: true } { + return Boolean(tokenRes) + && typeof tokenRes === 'object' + && (tokenRes as { needSelect?: unknown }).needSelect === true +} + +function getLoginErrorMessage(error: unknown) { + return getHttpErrorMessage(error, '登录失败,请重试') +} + +/** + * 是否是双token模式 + */ +export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double' +// 初始化状态 +const tokenInfoState = isDoubleTokenMode + ? { + accessToken: '', + accessExpiresIn: 0, + refreshToken: '', + refreshExpiresIn: 0, + } + : { + token: '', + expiresIn: 0, + } + +export const useTokenStore = defineStore( + 'token', + () => { + // 定义用户信息 + const tokenInfo = ref({ ...tokenInfoState }) + + // 添加一个时间戳 ref 作为响应式依赖 + const nowTime = ref(Date.now()) + /** + * 更新响应式数据:now + * 确保isTokenExpired/isRefreshTokenExpired重新计算,而不是用错误过期缓存值 + * 可useTokenStore内部适时调用;也可链式调用:tokenStore.updateNowTime().hasLogin + * @returns 最新的tokenStore实例 + */ + const updateNowTime = () => { + nowTime.value = Date.now() + return useTokenStore() + } + + // 设置用户信息 + const setTokenInfo = (val: IAuthLoginRes) => { + updateNowTime() + tokenInfo.value = val + + // 计算并存储过期时间 + const now = Date.now() + if (isSingleTokenResponse(val)) { + // 单token模式 + const expireTime = now + val.expiresIn * 1000 + uni.setStorageSync('accessTokenExpireTime', expireTime) + } + else if (isDoubleTokenResponse(val)) { + // 双token模式 + const accessExpireTime = now + val.accessExpiresIn * 1000 + const refreshExpireTime = now + val.refreshExpiresIn * 1000 + uni.setStorageSync('accessTokenExpireTime', accessExpireTime) + uni.setStorageSync('refreshTokenExpireTime', refreshExpireTime) + } + } + + /** + * 判断token是否过期 + */ + const isTokenExpired = computed(() => { + if (!tokenInfo.value) { + return true + } + + const now = nowTime.value + const expireTime = uni.getStorageSync('accessTokenExpireTime') + + if (!expireTime) + return true + return now >= expireTime + }) + + /** + * 判断refreshToken是否过期 + */ + const isRefreshTokenExpired = computed(() => { + if (!isDoubleTokenMode) + return true + + const now = nowTime.value + const refreshExpireTime = uni.getStorageSync('refreshTokenExpireTime') + + if (!refreshExpireTime) + return true + return now >= refreshExpireTime + }) + + /** + * 登录成功后处理逻辑 + * @param tokenInfo 登录返回的token信息 + */ + async function _postLogin(tokenInfo: IAuthLoginRes) { + setTokenInfo(tokenInfo) + const userStore = useUserStore() + if (isSingleTokenResponse(tokenInfo) && tokenInfo.userInfo) { + userStore.setUserInfo(tokenInfo.userInfo) + void userStore.fetchAccessCodes(false).catch(() => undefined) + } + else { + await userStore.fetchUserInfo() + } + } + + /** + * 用户登录 + * 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口,一个获取token,一个获取用户信息 + * (各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟 + * @param loginForm 登录参数 + * @returns 登录结果 + */ + const login = async (loginForm: ILoginForm) => { + try { + const res = await _login(loginForm) + console.log('普通登录-res: ', res) + if (isLoginSelectResponse(res)) { + return res + } + await _postLogin(res) + uni.showToast({ + title: '登录成功', + icon: 'success', + }) + return res + } + catch (error) { + console.error('登录失败:', error) + uni.showToast({ + title: getLoginErrorMessage(error), + icon: 'none', + }) + throw error + } + finally { + updateNowTime() + } + } + + /** + * 退出登录 并 删除用户信息 + */ + const logout = async () => { + try { + // 待办:实现自己的退出登录逻辑 + await _logout() + } + catch (error) { + console.error('退出登录失败:', error) + } + finally { + updateNowTime() + + // 无论成功失败,都需要清除本地token信息 + // 清除存储的过期时间 + uni.removeStorageSync('accessTokenExpireTime') + uni.removeStorageSync('refreshTokenExpireTime') + console.log('退出登录-清除用户信息') + tokenInfo.value = { ...tokenInfoState } + uni.removeStorageSync('token') + const userStore = useUserStore() + userStore.clearUserInfo() + } + } + + /** + * 刷新token + * @returns 刷新结果 + */ + const refreshToken = async () => { + if (!isDoubleTokenMode) { + console.error('单token模式不支持刷新token') + throw new Error('单token模式不支持刷新token') + } + + try { + // 安全检查,确保refreshToken存在 + if (!isDoubleTokenResponse(tokenInfo.value) || !tokenInfo.value.refreshToken) { + throw new Error('无效的refreshToken') + } + + const refreshToken = tokenInfo.value.refreshToken + const res = await _refreshToken(refreshToken) + console.log('刷新token-res: ', res) + setTokenInfo(res) + return res + } + catch (error) { + console.error('刷新token失败:', error) + throw error + } + finally { + updateNowTime() + } + } + + /** + * 获取有效的token + * 注意:在computed中不直接调用异步函数,只做状态判断 + * 实际的刷新操作应由调用方处理 + * 建议这样使用 tokenStore.updateNowTime().validToken + */ + const getValidToken = computed(() => { + // 登录凭证已过期,返回空 + if (isTokenExpired.value) { + return '' + } + + if (!isDoubleTokenMode) { + return isSingleTokenResponse(tokenInfo.value) ? tokenInfo.value.token : '' + } + else { + return isDoubleTokenResponse(tokenInfo.value) ? tokenInfo.value.accessToken : '' + } + }) + + /** + * 检查是否有登录信息(不考虑token是否过期) + */ + const hasLoginInfo = computed(() => { + if (!tokenInfo.value) { + return false + } + if (isDoubleTokenMode) { + return isDoubleTokenResponse(tokenInfo.value) && !!tokenInfo.value.accessToken + } + else { + return isSingleTokenResponse(tokenInfo.value) && !!tokenInfo.value.token + } + }) + + /** + * 检查是否已登录且token有效 + * 建议这样使用tokenStore.updateNowTime().hasLogin + */ + const hasValidLogin = computed(() => { + console.log('hasValidLogin', hasLoginInfo.value, !isTokenExpired.value) + return hasLoginInfo.value && !isTokenExpired.value + }) + + /** + * 尝试获取有效的token,如果过期且可刷新,则刷新token + * @returns 有效的token或空字符串 + */ + const tryGetValidToken = async (): Promise => { + updateNowTime() + if (!getValidToken.value && isDoubleTokenMode && !isRefreshTokenExpired.value) { + try { + await refreshToken() + return getValidToken.value + } + catch (error) { + console.error('尝试刷新token失败:', error) + return '' + } + } + return getValidToken.value + } + + return { + // 核心API方法 + login, + logout, + + // 认证状态判断(最常用的) + hasLogin: hasValidLogin, + + // 内部系统使用的方法 + refreshToken, + tryGetValidToken, + validToken: getValidToken, + + // 调试或特殊场景可能需要直接访问的信息 + tokenInfo, + setTokenInfo, + updateNowTime, + } + }, + { + // 添加持久化配置,确保刷新页面后token信息不丢失 + persist: true, + }, +) diff --git a/app/src/store/user.ts b/app/src/store/user.ts new file mode 100644 index 0000000..d2e052d --- /dev/null +++ b/app/src/store/user.ts @@ -0,0 +1,142 @@ +import type { IUserInfoRes } from '@/api/types/login' +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { getUserInfo } from '@/api/login' +import { getAccessCodes } from '@/api/system' + +const userInfoState: IUserInfoRes = { + userId: -1, + username: '', + nickName: '', + nickname: '', + phone: '', + userType: '', + isTerminal: false, + canEnterpriseBusiness: false, + wechatBound: false, + wechatBindText: '未绑定', + role: '企业用户', + sex: '2', + balance: 0, + avatar: '/static/images/default-avatar.png', + accessCodes: [], +} + +function normalizeAccessCodes(codes?: readonly string[] | null) { + return Array.from( + new Set( + (codes || []) + .map(code => String(code || '').trim()) + .filter(Boolean), + ), + ) +} + +function normalizeUserRole(role: string | undefined, isTerminal: boolean) { + if (isTerminal) + return '终端用户' + return role || '企业用户' +} + +function normalizeUserInfo(val: IUserInfoRes, fallbackAccessCodes: string[] = []): IUserInfoRes { + const userType = String(val.userType || '').trim() + const isTerminal = typeof val.isTerminal === 'boolean' + ? val.isTerminal + : userType === 'app_user' || userType === 'terminal' + const deptId = Number(val.deptId || val.enterpriseProfile?.deptId || 0) + const accessCodes = normalizeAccessCodes( + Array.isArray(val.accessCodes) ? val.accessCodes : fallbackAccessCodes, + ) + + return { + ...val, + deptId, + isTerminal, + userType, + canEnterpriseBusiness: Boolean(val.canEnterpriseBusiness) || (!isTerminal && deptId > 0), + wechatBound: Boolean(val.wechatBound), + wechatBindText: val.wechatBindText || (val.wechatBound ? '已绑定' : '未绑定'), + role: normalizeUserRole(val.role, isTerminal), + avatar: val.avatar || userInfoState.avatar, + nickname: val.nickname || val.nickName, + accessCodes, + } +} + +export const useUserStore = defineStore( + 'user', + () => { + const userInfo = ref({ ...userInfoState }) + let fetchUserInfoPromise: Promise | undefined + + const setUserInfo = (val: IUserInfoRes) => { + const previousUserId = Number(userInfo.value.userId || 0) + const nextUserId = Number(val.userId || 0) + const fallbackAccessCodes = previousUserId > 0 && previousUserId === nextUserId + ? userInfo.value.accessCodes + : [] + userInfo.value = normalizeUserInfo(val, fallbackAccessCodes) + } + + const setAccessCodes = (codes?: readonly string[] | null) => { + userInfo.value.accessCodes = normalizeAccessCodes(codes) + } + + const setUserAvatar = (avatar: string) => { + userInfo.value.avatar = avatar + } + + const clearUserInfo = () => { + userInfo.value = { ...userInfoState } + uni.removeStorageSync('user') + } + + const fetchAccessCodes = async (isCache = false) => { + const codes = await getAccessCodes({ isCache }) + setAccessCodes(codes) + return userInfo.value.accessCodes || [] + } + + const fetchUserInfo = async () => { + if (fetchUserInfoPromise) + return fetchUserInfoPromise + + fetchUserInfoPromise = (async () => { + const res = await getUserInfo() + const previousUserId = Number(userInfo.value.userId || 0) + const nextUserId = Number(res.userId || 0) + let accessCodes = previousUserId > 0 && previousUserId === nextUserId + ? userInfo.value.accessCodes || [] + : [] + try { + accessCodes = await getAccessCodes({ isCache: false }) + } + catch { + // 权限码接口失败时保留同一用户的旧权限,避免页面入口闪烁。 + } + setUserInfo({ ...res, accessCodes }) + return userInfo.value + })() + + try { + return await fetchUserInfoPromise + } + finally { + fetchUserInfoPromise = undefined + } + } + + return { + userInfo, + clearUserInfo, + fetchAccessCodes, + fetchUserInfo, + setAccessCodes, + setUserInfo, + setUserAvatar, + } + }, + { + persist: true, + }, +) diff --git a/app/src/style/iconfont.css b/app/src/style/iconfont.css new file mode 100644 index 0000000..35da86c --- /dev/null +++ b/app/src/style/iconfont.css @@ -0,0 +1,28 @@ +@font-face { + font-family: 'iconfont'; /* Project id 4543091 */ + src: + url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAOwAAsAAAAAB9AAAANjAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDHAqDBIJqATYCJAMQCwoABCAFhGcHPRvnBsgusG3kMyE15/44PsBX09waBHv0REDt97oHAQDFrOIyPirRiULQ+TJcXV0hCYTuVFcBC915/2vX/32Q80hkZ5PZGZ9snvwruVLloidKqYN6iKC53bOtbKwVLSIi3W6zCWZbs3VbER3j9JpGX3ySYcc94IQRTK5s4epS/jSqIgvg37qlY2/jwQN7D9ADpfRCmIknQByTscVZPTBr+hnnCKg2o4bjakvXEPjuY65DJGeJNtBUhn1JxOBuB2UZmUpBOXdsFp4oxOv4GHgs3h/+wRDcicqSZJG1q9kK1z/Af9NpqxjpC2QaAdpHlCFh4spcYXs5sMWpSk5wUj31G2dLQKVKkZ/w7f/8/i/A3JVUSZK9f7xIKJeU14IFpBI/Qfkkz46GT/CuaGREfCtKJUougWeQWHvVC5Lcz2BGS+SePR99vj3yjJx7h574tp7uWcOh4yfaTjS/245TT/vkQrN+a7RLkK8+Vd+bz+FSGh+9srDQKPeJ2s29z7ah4+efdoxefRbbGwfy7ht+SuIWukzsu1b6ePP+6kN1aamb47qsPim1Ia3xdEpDcl1dckPKGYnneI23+57r2W1Mmkqs6ajrChRCs5qyQ66rTVWhgZaG7toOeHm5cxn0sSQuNDEgcUTdNTSupKI1JRZih/JssAUKezPeOJJzbNozF6zWJuuVavVU5Tgtkop/SDzHa7ytvnCTq0PhkEfi4xLLtb0PuwyOAYqmrYQApFJyoJjTnfz+ve94vvv2f/yWgxl8Jd8Di2DRDPuob59mU/+VfDCROQyR8xSnmP9fXm7liagmN39OlmbvjqG0sMsJKrU0EFXogaRSH5bNY1CmxhyUq7QC1cY1T67RwuQk5CoM2RUQNLoEUb03kDS6h2XzcyjT7iOUa/QXqq1Hn6/GUBAaGcGcWJFlGUmCoVOp8kLvABHnVczGYiOE2SVEUH5OXj/TSnTCDjHAviAWcE4RZYaGWszNiKoayGSGTASeY+PcrMjNpVMvyREMDRoxBMYRVojFMkQiMOhohubdzxtAiOapMMbERpKMnQT9SL4ceQysVdJZVa9kEbsFogIcRyEUE2kN0mL7CDVIGhBzupWMEHA5bDvipgq5hKJcKef8ivbx1kC15KgcYkghhzLxYNntxoKCReJ82jAHAAA=') + format('woff2'), + url('//at.alicdn.com/t/c/font_4543091_njpo5b95nl.woff?t=1715485842402') format('woff'), + url('//at.alicdn.com/t/c/font_4543091_njpo5b95nl.ttf?t=1715485842402') format('truetype'); +} + +.iconfont { + font-family: 'iconfont' !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-my:before { + content: '\e78c'; +} + +.icon-package:before { + content: '\e9c2'; +} + +.icon-chat:before { + content: '\e600'; +} diff --git a/app/src/style/index.scss b/app/src/style/index.scss new file mode 100644 index 0000000..df17558 --- /dev/null +++ b/app/src/style/index.scss @@ -0,0 +1,635 @@ +// 测试用的 iconfont,可生效 +// @import './iconfont.css'; + +/* ===== Roboto Mono 数字字体(H5 端 CDN,小程序端回退 monospace) ===== */ +// #ifdef H5 +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('https://cdn.jsdelivr.net/npm/@fontsource/roboto-mono@5.0.0/files/roboto-mono-latin-500-normal.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('https://cdn.jsdelivr.net/npm/@fontsource/roboto-mono@5.0.0/files/roboto-mono-latin-600-normal.woff2') + format('woff2'); +} + +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('https://cdn.jsdelivr.net/npm/@fontsource/roboto-mono@5.0.0/files/roboto-mono-latin-700-normal.woff2') + format('woff2'); +} +// #endif + +.test { + // 可以通过 @apply 多个样式封装整体样式 + @apply mt-4 ml-4; + + padding-top: 4px; + color: red; +} + +:root, +page { + /* 设计稿基线:浅灰画布、主色 #2563eb、深蓝 #0b2bac 仅用于渐变、状态色只承担语义提示 */ + --wot-color-theme: #2563eb; + --wot-color-primary: #2563eb; + --wot-color-success: #16a34a; + --wot-color-warning: #f59e0b; + --app-primary: #2563eb; + --app-primary-strong: #0b2bac; + --app-canvas: #f6f8fb; + --app-surface: #ffffff; + --app-ink: #111827; + --app-body: #334155; + --app-muted: #64748b; + --app-line: #e2e8f0; + --app-primary-soft: #eff6ff; + font-family: + 'Noto Sans SC', + -apple-system, + BlinkMacSystemFont, + 'PingFang SC', + 'Helvetica Neue', + Helvetica, + 'Microsoft YaHei', + sans-serif; +} + +page { + background: var(--app-canvas); + color: var(--app-ink); +} + +/* 业务子页统一视觉:用于续费、变更、划拨、加油包、预警、诊断、消息等设计稿落地页 */ +.biz-page { + min-height: 100%; + background: var(--app-canvas); + padding: 20rpx 24rpx 160rpx; + box-sizing: border-box; +} + +.biz-stack { + max-width: 920rpx; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.biz-hero { + box-sizing: border-box; + border: 1px solid #dbeafe; + border-radius: 16rpx; + background: #fff; + padding: 24rpx; + box-shadow: 0 6rpx 18rpx rgba(11, 43, 172, 0.06); +} + +.biz-hero__top, +.biz-section-head, +.biz-row, +.biz-paybar__summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16rpx; +} + +.biz-hero__icon { + width: 60rpx; + height: 60rpx; + border-radius: 18rpx; + background: #2563eb; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 32rpx; +} + +.biz-hero__title, +.biz-section-title { + display: block; + color: var(--app-ink); + font-weight: 900; + line-height: 1.25; +} + +.biz-hero__title { + font-size: 34rpx; +} + +.biz-hero__sub { + display: block; + margin-top: 6rpx; + color: var(--app-muted); + font-size: 23rpx; + line-height: 34rpx; +} + +.biz-stat-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12rpx; + margin-top: 20rpx; +} + +.biz-stat { + min-width: 0; + border-radius: 8px; + background: #f8fafc; + padding: 16rpx; +} + +.biz-stat__label, +.biz-paybar__label { + display: block; + color: var(--app-muted); + font-size: 22rpx; + line-height: 30rpx; +} + +.biz-stat__value { + display: block; + margin-top: 6rpx; + color: var(--app-ink); + font-size: 25rpx; + font-weight: 800; + line-height: 34rpx; + word-break: break-word; +} + +.biz-section-title { + font-size: 29rpx; +} + +.biz-section-sub { + display: block; + margin-top: 4rpx; + color: var(--app-muted); + font-size: 22rpx; + line-height: 32rpx; +} + +.biz-card { + border: 1px solid var(--app-line); + border-radius: 16rpx; + background: #fff; + overflow: hidden; + box-shadow: 0 6rpx 18rpx rgba(11, 43, 172, 0.06); +} + +.biz-card__body { + padding: 22rpx; +} + +.biz-row { + padding: 18rpx 0; + border-bottom: 1px solid #edf2f7; +} + +.biz-row:last-child { + border-bottom: 0; +} + +.biz-row__label { + flex-shrink: 0; + color: var(--app-muted); + font-size: 24rpx; +} + +.biz-row__value { + min-width: 0; + flex: 1; + color: var(--app-ink); + font-size: 25rpx; + font-weight: 700; + text-align: right; + word-break: break-all; +} + +.biz-option { + position: relative; + display: flex; + align-items: center; + gap: 16rpx; + min-height: 118rpx; + border: 1px solid var(--app-line); + border-radius: 8px; + background: #fff; + padding: 20rpx; + box-sizing: border-box; +} + +.biz-option.is-active { + border-color: #2563eb; + background: #eff6ff; +} + +/* ===== 业务两步选择区(步骤徽标 + 卡内选项列表) ===== */ +.biz-step { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36rpx; + height: 36rpx; + flex-shrink: 0; + border-radius: 10rpx; + background: linear-gradient(135deg, #2563eb, #0b2bac); + color: #fff; + font-size: 22rpx; + font-weight: 800; + line-height: 1; +} + +.biz-option-list { + display: grid; + gap: 14rpx; + padding: 20rpx; +} + +.biz-option__main { + min-width: 0; + flex: 1; +} + +.biz-option__title { + display: block; + color: var(--app-ink); + font-size: 28rpx; + font-weight: 900; + line-height: 36rpx; +} + +.biz-option__meta { + display: block; + margin-top: 6rpx; + color: var(--app-muted); + font-size: 22rpx; + line-height: 32rpx; +} + +.biz-option__price { + flex-shrink: 0; + color: #2563eb; + font-size: 30rpx; + font-weight: 900; + line-height: 40rpx; +} + +.biz-radio { + width: 28rpx; + height: 28rpx; + border: 2rpx solid #cbd5e1; + border-radius: 50%; + box-sizing: border-box; + flex-shrink: 0; +} + +.is-active > .biz-radio, +.biz-radio.is-active { + border: 8rpx solid #2563eb; +} + +.biz-check-row { + border: 1px solid var(--app-line); + border-radius: 8px; + background: #f8fafc; + padding: 16rpx; +} + +.biz-check-row.is-active { + border-color: #2563eb; + background: #eff6ff; +} + +.biz-mini-action-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10rpx; +} + +.biz-mini-action-grid--2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.biz-mini-action-btn, +.wd-button.biz-mini-action-btn { + width: 100% !important; + min-width: 0 !important; + height: 80rpx !important; + min-height: 80rpx !important; + padding: 0 10rpx !important; + border: 1px solid #dbe3ef !important; + border-color: #dbe3ef !important; + border-radius: 12rpx !important; + background: #fff !important; + box-shadow: none !important; + color: #1f2937 !important; + font-size: 23rpx !important; + font-weight: 700 !important; +} + +.biz-mini-action-btn.is-disabled, +.biz-mini-action-btn.wd-button--disabled, +.wd-button.biz-mini-action-btn.is-disabled, +.wd-button.biz-mini-action-btn.wd-button--disabled { + border-color: #e2e8f0 !important; + background: #f8fafc !important; + color: #94a3b8 !important; + opacity: 1 !important; +} + +.biz-mini-action-btn .wd-button__text, +.wd-button.biz-mini-action-btn .wd-button__text { + min-width: 0; + width: 100%; +} + +.biz-mini-action-btn__inner { + display: inline-flex; + min-width: 0; + width: 100%; + align-items: center; + justify-content: center; + gap: 8rpx; + line-height: 1; + white-space: nowrap; +} + +.biz-mini-action-btn__icon { + flex-shrink: 0; + color: #2563eb; + font-size: 28rpx; +} + +.biz-mini-action-btn__text { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.biz-mini-action-btn__icon--blue { + color: #2563eb; +} + +.biz-mini-action-btn__icon--green { + color: #2563eb; +} + +.biz-mini-action-btn__icon--orange { + color: #2563eb; +} + +.biz-mini-action-btn__icon--gray { + color: #2563eb; +} + +.biz-paybar-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16rpx; +} + +.biz-paybar-btn, +.wd-button.biz-paybar-btn { + height: 78rpx !important; + min-height: 78rpx !important; + border-radius: 8px !important; + font-size: 26rpx !important; + font-weight: 800 !important; +} + +.biz-paybar { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 50; + border-top: 1px solid var(--app-line); + background: #fff; + padding: 18rpx 24rpx calc(18rpx + env(safe-area-inset-bottom)); + box-shadow: 0 -8rpx 24rpx rgba(15, 23, 42, 0.08); +} + +.biz-paybar__inner { + max-width: 920rpx; + margin: 0 auto; +} + +.biz-paybar__summary { + margin-bottom: 12rpx; +} + +.biz-paybar__value { + min-width: 0; + color: var(--app-ink); + font-size: 25rpx; + font-weight: 800; + text-align: right; +} + +.biz-note { + border: 1px solid #dbeafe; + border-radius: 8px; + background: #eff6ff; + color: #1d4ed8; + font-size: 23rpx; + line-height: 34rpx; + padding: 18rpx 20rpx; +} + +.mono { + font-family: 'Roboto Mono', monospace; +} + +@media (max-width: 360px) { + .biz-stat-grid { + grid-template-columns: 1fr; + } +} + +/* 设计稿按钮规范(hy3):圆角矩形 24rpx,主操作为蓝紫渐变,线框为白底+主色描边。 + 注意:仅「标签/徽章」使用胶囊形;按钮统一为圆角矩形,不再使用 999rpx 胶囊。 */ +.wd-button { + box-sizing: border-box; + min-height: 88rpx; + border-radius: 24rpx !important; + font-weight: 600 !important; + transition: + background-color 120ms ease, + border-color 120ms ease, + color 120ms ease; +} + +.wd-button--small { + min-height: 72rpx; + padding-right: 28rpx !important; + padding-left: 28rpx !important; + border-radius: 16rpx !important; + font-size: 24rpx !important; +} + +.wd-button--primary { + &:not(.is-plain):not(.is-disabled):not(.is-loading):not(.is-text) { + border: none !important; + background: linear-gradient(135deg, #2563eb 0%, #0b2bac 100%) !important; + color: #fff !important; + } + + &.is-plain { + border-color: #2563eb !important; + background: #fff !important; + color: #2563eb !important; + } + + &:not(.is-plain):not(.is-disabled):not(.is-loading):not(.is-text):active { + background: linear-gradient(135deg, #1d4ed8 0%, #0b2bac 100%) !important; + transform: translateY(1rpx); + } +} + +/* 默认线框按钮对应设计稿 btn--ghost / mini-act:白底、灰边、正文色。 */ +.wd-button--default { + &.is-plain { + border-color: #e2e8f0 !important; + background: #fff !important; + color: #334155 !important; + } +} + +.wd-button.is-disabled, +.wd-button[disabled] { + border-color: #e2e8f0 !important; + background: #f1f5f9 !important; + color: #94a3b8 !important; + box-shadow: none !important; + opacity: 1 !important; +} + +.wd-button--danger { + &:not(.is-plain):not(.is-disabled):not(.is-loading):not(.is-text) { + border: none !important; + background: #dc2626 !important; + color: #fff !important; + } + + /* 危险 ghost(退出登录 / 取消订单):白底 + 危险描边 + 危险文字 */ + &.is-plain { + border-color: #dc2626 !important; + background: #fff !important; + color: #dc2626 !important; + } +} + +/* Wot UI 卡片统一采用设计稿:白底、1px 细描边、16rpx 圆角、轻蓝阴影 */ +.wd-card { + background: var(--app-surface) !important; + border: 1px solid var(--app-line) !important; + border-radius: 16rpx !important; + box-shadow: 0 6rpx 18rpx rgba(11, 43, 172, 0.06) !important; + overflow: hidden; +} + +.wd-card__title { + padding: 24rpx !important; + font-weight: 600 !important; +} + +/* ===== 设计稿通用区块(白卡 + 渐变竖条标题) ===== */ +.app-section { + box-sizing: border-box; + border: 1px solid var(--app-line); + border-radius: 16rpx; + background: #fff; + box-shadow: 0 6rpx 18rpx rgba(11, 43, 172, 0.06); + overflow: hidden; +} + +.app-section__head { + display: flex; + align-items: center; + gap: 12rpx; + padding: 24rpx 28rpx; + border-bottom: 1px solid var(--app-line); +} + +.app-title-bar { + width: 6rpx; + height: 30rpx; + flex-shrink: 0; + border-radius: 999rpx; + background: linear-gradient(180deg, #2563eb, #0b2bac); +} + +.app-section__title { + color: var(--app-ink); + font-size: 30rpx; + font-weight: 800; + line-height: 40rpx; +} + +/* ===== 设计稿标签 / 徽章(胶囊形,仅标签使用) ===== */ +.app-tag { + display: inline-flex; + align-items: center; + padding: 4rpx 16rpx; + border-radius: 999rpx; + font-size: 22rpx; + font-weight: 600; + line-height: 32rpx; +} + +.app-tag--ok { + background: #dcfce7; + color: #16a34a; +} + +.app-tag--warn { + background: #fff7ed; + color: #f59e0b; +} + +.app-tag--low { + background: #e0f2fe; + color: #0ea5e9; +} + +.app-tag--err { + background: #ffe4e6; + color: #f43f5e; +} + +.app-tag--info { + background: #eff6ff; + color: #2563eb; +} + +/* +border-t-1 +由于uniapp中无法使用*选择器,使用魔法代替*,加上此规则可以简化border与divide的使用,并提升布局的兼容性 +1. 防止padding和border影响元素宽度。 (https://github.com/mozdevs/cssremedy/issues/4) +2. 允许仅通过添加边框宽度来向元素添加边框。 (https://github.com/tailwindcss/tailwindcss/pull/116) +3. [UnoCSS]: 允许使用css变量'--un-default-border-color'覆盖默认边框颜色 +*/ +// 这个样式有重大BUG,先去掉!!(2025-08-15) +// :not(not), +// ::before, +// ::after { +// box-sizing: border-box; /* 1 */ +// border-width: 0; /* 2 */ +// border-style: solid; /* 2 */ +// border-color: var(--un-default-border-color, #e5e7eb); /* 3 */ +// } diff --git a/app/src/tabbar/README.md b/app/src/tabbar/README.md new file mode 100644 index 0000000..575d87f --- /dev/null +++ b/app/src/tabbar/README.md @@ -0,0 +1,78 @@ +# tabbar 说明 + +## tabbar 3种策略 + +`tabbar` 分为 `4 种` 情况: + +- 0 `无 tabbar`,只有一个页面入口,底部无 `tabbar` 显示;常用语临时活动页。 + +- 1 `原生 tabbar`,使用 `switchTab` 切换 `tabbar`,`tabbar` 页面有缓存。 + - 优势:原生自带的 `tabbar`,最先渲染,有缓存。 + - 劣势:只能使用 2 组图片来切换选中和非选中状态,修改颜色只能重新换图片(或者用 iconfont)。 + +- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 `tabbar`,`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。 + - 优势:可以随意配置自己想要的 `svg icon`,切换字体颜色方便。有缓存。可以实现各种花里胡哨的动效等。 + - 劣势:首次点击 `tabbar` 会闪烁。 + +## tabbar 配置说明 + +- 如果使用的是 `原生tabbar`,需要配置 `nativeTabbarList`,每个 `item` 需要配置 `path`、`text`、`iconPath`、`selectedIconPath` 等属性。 +- 如果使用的是 `自定义tabbar`,需要配置 `customTabbarList`,每个 `item` 需要配置 `path`、`text`、`icon` 、`iconType` 等属性(如果是 `image` 图片还需要配置2种图片)。 + +## 文件说明 + +`config.ts` 专门配置 `nativeTabbarList` 和 `customTabbarList` 的相关信息,请按照文件里面的注释配置相关项。 + +使用 `原生tabbar` 时,不需要关心下面2个文件: + +- `store.ts` ,专门给 `自定义 tabbar` 提供状态管理,代码几乎不需要修改。 +- `index.vue` ,专门给 `自定义 tabbar` 提供渲染逻辑,代码可以稍微修改,以符合自己的需求。 + +## 自定义tabbar的不同类型的配置 + +- uiLib 图标 + + ```js + { + // ... 其他配置 + "iconType": "uniUi", + "icon": "home", + } + ``` + +- unocss 图标 + + ```js + { + // ... 其他配置 + // 注意 unocss 图标需要如下处理:(二选一) + // 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行) + // 2)配置到 unocss.config.ts 的 safelist 中 + iconType: 'unocss', + icon: 'i-carbon-code', + } + ``` + +- iconfont 图标 + + ```js + { + // ... 其他配置 + // 注意 iconfont 图标需要额外加上 'iconfont',如下 + iconType: 'iconfont', + icon: 'iconfont icon-my', + } + ``` + +- image 本地图片 + + ```js + { + // ... 其他配置 + // 使用 ‘image’时,需要配置 icon + iconActive 2张图片(不推荐) + // 既然已经用了自定义tabbar了,就不建议用图片了,所以不推荐 + iconType: 'image', + icon: '/static/tabbar/home.png', + iconActive: '/static/tabbar/homeHL.png', + } + ``` diff --git a/app/src/tabbar/TabbarItem.vue b/app/src/tabbar/TabbarItem.vue new file mode 100644 index 0000000..5478428 --- /dev/null +++ b/app/src/tabbar/TabbarItem.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/app/src/tabbar/config.ts b/app/src/tabbar/config.ts new file mode 100644 index 0000000..cb15151 --- /dev/null +++ b/app/src/tabbar/config.ts @@ -0,0 +1,144 @@ +import type { TabBar } from '@uni-helper/vite-plugin-uni-pages' +import type { CustomTabBarItem, NativeTabBarItem } from './types' + +/** + * tabbar 选择的策略,更详细的介绍见 tabbar.md 文件 + * 0: 'NO_TABBAR' `无 tabbar` + * 1: 'NATIVE_TABBAR' `原生 tabbar` + * 2: 'CUSTOM_TABBAR' `自定义 tabbar` + * + * 温馨提示:本文件的任何代码更改了之后,都需要重新运行,否则 pages.json 不会更新导致配置不生效 + */ +export const TABBAR_STRATEGY_MAP = { + NO_TABBAR: 0, + NATIVE_TABBAR: 1, + CUSTOM_TABBAR: 2, +} + +// TODO: 1/3. 通过这里切换使用tabbar的策略 +// 如果是使用 NO_TABBAR(0),nativeTabbarList 和 customTabbarList 都不生效 +// 如果是使用 NATIVE_TABBAR(1),只需要配置 nativeTabbarList,customTabbarList 不生效 +// 如果是使用 CUSTOM_TABBAR(2),只需要配置 customTabbarList,nativeTabbarList 不生效 +export const selectedTabbarStrategy = TABBAR_STRATEGY_MAP.CUSTOM_TABBAR + +// TODO: 2/3. 使用 NATIVE_TABBAR 时,更新下面的 tabbar 配置 +export const nativeTabbarList: NativeTabBarItem[] = [ + { + iconPath: 'static/tabbar/home.png', + selectedIconPath: 'static/tabbar/homeHL.png', + pagePath: 'pages/home/index', + text: '%tabbar.home%', + }, + { + iconPath: 'static/tabbar/personal.png', + selectedIconPath: 'static/tabbar/personalHL.png', + pagePath: 'pages/user/index', + text: '%tabbar.me%', + }, +] + +// TODO: 3/3. 使用 CUSTOM_TABBAR 时,更新下面的 tabbar 配置 +// 如果需要配置鼓包,需要在 'tabbar/store.ts' 里面设置,最后在 `tabbar/index.vue` 里面更改鼓包的图片 +export const customTabbarList: CustomTabBarItem[] = [ + { + text: '首页', + pagePath: 'pages/home/index', + // 注意 unocss 图标需要如下处理:(二选一) + // 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行) + // 2)配置到 unocss.config.ts 的 safelist 中 + iconType: 'uiLib', + icon: 'home', + // badge: 'dot', + }, + { + pagePath: 'pages/card/index', + text: '设备', + iconType: 'uiLib', + icon: 'nav', + }, + { + pagePath: 'pages/business/index', + text: '办理', + iconType: 'uiLib', + icon: 'failpayment', + }, + { + pagePath: 'pages/user/index', + text: '我的', + iconType: 'uiLib', + icon: 'user', + }, + + // 其他类型演示 + // 1、uiLib + // { + // pagePath: 'pages/index/index', + // text: '首页', + // iconType: 'uiLib', + // icon: 'home', + // }, + // 2、iconfont + // { + // pagePath: 'pages/index/index', + // text: '首页', + // // 注意 iconfont 图标需要额外加上 'iconfont',如下 + // iconType: 'iconfont', + // icon: 'iconfont icon-my', + // }, + // 3、image + // { + // pagePath: 'pages/index/index', + // text: '首页', + // // 使用 ‘image’时,需要配置 icon + iconActive 2张图片 + // iconType: 'image', + // icon: '/static/tabbar/home.png', + // iconActive: '/static/tabbar/homeHL.png', + // }, +] + +/** + * 是否启用 tabbar 缓存 + * NATIVE_TABBAR(1) 和 CUSTOM_TABBAR(2) 时,需要tabbar缓存 + */ +export const tabbarCacheEnable = [ + TABBAR_STRATEGY_MAP.NATIVE_TABBAR, + TABBAR_STRATEGY_MAP.CUSTOM_TABBAR, +].includes(selectedTabbarStrategy) + +/** + * 是否启用自定义 tabbar + * CUSTOM_TABBAR(2) 时,启用自定义tabbar + */ +export const customTabbarEnable = [TABBAR_STRATEGY_MAP.CUSTOM_TABBAR].includes( + selectedTabbarStrategy, +) + +/** + * 是否需要隐藏原生 tabbar + * CUSTOM_TABBAR(2) 时,需要隐藏原生tabbar + */ +export const needHideNativeTabbar = selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR + +const _tabbarList = customTabbarEnable + ? customTabbarList.map((item) => ({ text: item.text, pagePath: item.pagePath })) + : nativeTabbarList +export const tabbarList = customTabbarEnable ? customTabbarList : nativeTabbarList + +// NATIVE_TABBAR(1) 时,显示原生Tabbar,在i18n的情况下需要 setTabbarItem (框架已经处理) +export const isNativeTabbar = selectedTabbarStrategy === TABBAR_STRATEGY_MAP.NATIVE_TABBAR + +const _tabbar: TabBar = { + // 只有微信小程序支持 custom。App 和 H5 不生效 + custom: selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR, + color: '#94a3b8', + selectedColor: '#2563eb', + backgroundColor: '#F8F8F8', + borderStyle: 'black', + height: '50px', + fontSize: '10px', + iconWidth: '24px', + spacing: '3px', + list: _tabbarList as unknown as TabBar['list'], +} + +export const tabBar = tabbarCacheEnable ? _tabbar : undefined diff --git a/app/src/tabbar/i18n.ts b/app/src/tabbar/i18n.ts new file mode 100644 index 0000000..e50f689 --- /dev/null +++ b/app/src/tabbar/i18n.ts @@ -0,0 +1,30 @@ +import { t } from '@/locale' +import { isCurrentPageTabbar } from '@/utils' +import { isNativeTabbar, tabbarList } from './config' + +// h5 中一直可以生效,小程序里面默认是无法动态切换的,这里借助vue模板自带响应式的方式 +// 直接替换 %xxx% 为 t('xxx')即可 +export function getI18nText(key: string) { + // 获取 %xxx% 中的 xxx + const match = key.match(/%(.+?)%/) + if (match) { + key = match[1] + } + console.log('设置多语言:', key) + return t(key) +} + +export function setTabbarItem() { +// 只有使用原生Tabbar才需要 setTabbarItem +// 而且只有当前页是tabbar页才能设置 + // console.log('设置多语言:setTabBarItem', isNativeTabbar, isCurrentPageTabbar()) + if (isNativeTabbar && isCurrentPageTabbar()) { + tabbarList.forEach((item, index) => { + uni.setTabBarItem({ + index, + // text: getI18nText(item.text), + text: item.text, + }) + }) + } +} diff --git a/app/src/tabbar/index.vue b/app/src/tabbar/index.vue new file mode 100644 index 0000000..5b1b815 --- /dev/null +++ b/app/src/tabbar/index.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/app/src/tabbar/store.ts b/app/src/tabbar/store.ts new file mode 100644 index 0000000..49cfc8e --- /dev/null +++ b/app/src/tabbar/store.ts @@ -0,0 +1,105 @@ +import type { CustomTabBarItem, CustomTabBarItemBadge } from './types' +import { computed, reactive } from 'vue' + +import { tabbarList as _tabbarList, selectedTabbarStrategy, TABBAR_STRATEGY_MAP } from './config' + +/** tabbarList 里面的 path 从 pages.config.ts 得到 */ +const baseTabbarList = reactive(_tabbarList.map(item => ({ + ...item, + pagePath: item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}`, // 统一成 '/' 开头的路径 +}))) + +const tabbarList = computed(() => baseTabbarList) + +export function isPageTabbar(path: string) { + if (selectedTabbarStrategy === TABBAR_STRATEGY_MAP.NO_TABBAR) { + return false + } + const _path = normalizeRoutePath(path) + return _path === '/' || tabbarList.value.some(item => item.pagePath === _path) +} + +export function normalizeRoutePath(path?: string) { + if (!path) { + return '' + } + const _path = path.split('?')[0] + return _path.startsWith('/') ? _path : `/${_path}` +} + +function getCurrentPagePath() { + const pages = getCurrentPages() + const currentPage = pages[pages.length - 1] + return normalizeRoutePath(currentPage?.route) +} + +function findTabbarIndexByPath(path?: string) { + const normalizedPath = normalizeRoutePath(path) + if (normalizedPath === '/') { + return 0 + } + return tabbarList.value.findIndex(item => item.pagePath === normalizedPath) +} + +/** + * 自定义 tabbar 的状态管理,原生 tabbar 无需关注本文件 + * tabbar 状态,增加 storageSync 保证刷新浏览器时在正确的 tabbar 页面 + * 使用reactive简单状态,而不是 pinia 全局状态 + */ +const tabbarStore = reactive({ + curIdx: uni.getStorageSync('app-tabbar-index') || 0, + prevIdx: uni.getStorageSync('app-tabbar-index') || 0, + setCurIdx(idx: number) { + this.curIdx = idx + uni.setStorageSync('app-tabbar-index', idx) + }, + setTabbarItemBadge(idx: number, badge: CustomTabBarItemBadge) { + const list = tabbarList.value + if (list[idx]) { + list[idx].badge = badge + } + }, + setAutoCurIdx(path: string) { + const list = tabbarList.value + if (list.length === 0) { + this.setCurIdx(0) + return + } + + const index = findTabbarIndexByPath(path) + if (index >= 0) { + this.setCurIdx(index) + return + } + + if (this.curIdx < 0 || this.curIdx >= list.length) { + this.setCurIdx(0) + } + }, + syncCurIdxByCurrentPage() { + const currentPath = getCurrentPagePath() + if (currentPath) { + this.setAutoCurIdx(currentPath) + } + }, + syncCurIdxByCurrentPageAsync() { + setTimeout(() => { + this.syncCurIdxByCurrentPage() + }, 0) + }, + isCurrentRouteTabbarItem(index: number) { + const item = tabbarList.value[index] + if (!item) { + return false + } + return findTabbarIndexByPath(getCurrentPagePath()) === index + }, + restorePrevIdx() { + if (this.prevIdx === this.curIdx) + return + this.setCurIdx(this.prevIdx) + this.prevIdx = uni.getStorageSync('app-tabbar-index') || 0 + }, +}) + +export { tabbarList, tabbarStore } diff --git a/app/src/tabbar/types.ts b/app/src/tabbar/types.ts new file mode 100644 index 0000000..7db6060 --- /dev/null +++ b/app/src/tabbar/types.ts @@ -0,0 +1,39 @@ +import type { TabBar } from '@uni-helper/vite-plugin-uni-pages' +import type { UserRole } from '@/api/types/login' +import type { RemoveLeadingSlashFromUnion } from '@/typings' + +/** + * 原生 tabbar 的单个选项配置 + */ +export type NativeTabBarItem = TabBar['list'][number] & { + pagePath: RemoveLeadingSlashFromUnion<_LocationUrl> +} + +/** badge 显示一个数字或 小红点(样式可以直接在 tabbar/index.vue 里面修改) */ +export type CustomTabBarItemBadge = number | 'dot' + +/** 自定义 tabbar 的单个选项配置 */ +export interface CustomTabBarItem { + text: string + pagePath: RemoveLeadingSlashFromUnion<_LocationUrl> + /** 图标类型,不建议用 image 模式,因为需要配置 2 张图,更麻烦 */ + iconType: 'uiLib' | 'unocss' | 'iconfont' | 'image' + /** + * icon 的路径 + * - uiLib: wot-design-uni 图标的 icon prop + * - unocss: unocss 图标的类名 + * - iconfont: iconfont 图标的类名 + * - image: 图片的路径 + */ + icon: string + /** 只有在 image 模式下才需要,传递的是高亮的图片 */ + iconActive?: string + /** badge 显示一个数字或 小红点 */ + badge?: CustomTabBarItemBadge + /** 是否是中间的鼓包tabbarItem */ + isBulge?: boolean + // roles 不写 → 所有用户都能看到;roles 写了 → 只有匹配角色可见 + roles?: UserRole[] + // features 不写 → 所有用户都能看到;写了 → 有 app 权限配置时按业务能力过滤 + features?: string[] +} diff --git a/app/src/types/async-component.d.ts b/app/src/types/async-component.d.ts new file mode 100644 index 0000000..f200205 --- /dev/null +++ b/app/src/types/async-component.d.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by @uni-ku/bundle-optimizer +declare module '*?async' { + const component: any + export = component +} diff --git a/app/src/types/async-import.d.ts b/app/src/types/async-import.d.ts new file mode 100644 index 0000000..94101bc --- /dev/null +++ b/app/src/types/async-import.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by @uni-ku/bundle-optimizer +export {} + +interface ModuleMap { + [path: string]: any +} + +declare global { + function AsyncImport(arg: T): Promise +} diff --git a/app/src/types/auto-import.d.ts b/app/src/types/auto-import.d.ts new file mode 100644 index 0000000..c0ab6fc --- /dev/null +++ b/app/src/types/auto-import.d.ts @@ -0,0 +1,193 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + const EffectScope: typeof import('vue')['EffectScope'] + const computed: typeof import('vue')['computed'] + const createApp: typeof import('vue')['createApp'] + const customRef: typeof import('vue')['customRef'] + const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] + const defineComponent: typeof import('vue')['defineComponent'] + const effectScope: typeof import('vue')['effectScope'] + const getCurrentInstance: typeof import('vue')['getCurrentInstance'] + const getCurrentScope: typeof import('vue')['getCurrentScope'] + const getCurrentWatcher: typeof import('vue')['getCurrentWatcher'] + const h: typeof import('vue')['h'] + const inject: typeof import('vue')['inject'] + const isProxy: typeof import('vue')['isProxy'] + const isReactive: typeof import('vue')['isReactive'] + const isReadonly: typeof import('vue')['isReadonly'] + const isRef: typeof import('vue')['isRef'] + const isShallow: typeof import('vue')['isShallow'] + const markRaw: typeof import('vue')['markRaw'] + const nextTick: typeof import('vue')['nextTick'] + const onActivated: typeof import('vue')['onActivated'] + const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites'] + const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress'] + const onBeforeMount: typeof import('vue')['onBeforeMount'] + const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] + const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] + const onDeactivated: typeof import('vue')['onDeactivated'] + const onError: typeof import('@dcloudio/uni-app')['onError'] + const onErrorCaptured: typeof import('vue')['onErrorCaptured'] + const onHide: typeof import('@dcloudio/uni-app')['onHide'] + const onLaunch: typeof import('@dcloudio/uni-app')['onLaunch'] + const onLoad: typeof import('@dcloudio/uni-app')['onLoad'] + const onMounted: typeof import('vue')['onMounted'] + const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap'] + const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged'] + const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked'] + const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed'] + const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged'] + const onPageNotFound: typeof import('@dcloudio/uni-app')['onPageNotFound'] + const onPageScroll: typeof import('@dcloudio/uni-app')['onPageScroll'] + const onPullDownRefresh: typeof import('@dcloudio/uni-app')['onPullDownRefresh'] + const onReachBottom: typeof import('@dcloudio/uni-app')['onReachBottom'] + const onReady: typeof import('@dcloudio/uni-app')['onReady'] + const onRenderTracked: typeof import('vue')['onRenderTracked'] + const onRenderTriggered: typeof import('vue')['onRenderTriggered'] + const onResize: typeof import('@dcloudio/uni-app')['onResize'] + const onScopeDispose: typeof import('vue')['onScopeDispose'] + const onServerPrefetch: typeof import('vue')['onServerPrefetch'] + const onShareAppMessage: typeof import('@dcloudio/uni-app')['onShareAppMessage'] + const onShareTimeline: typeof import('@dcloudio/uni-app')['onShareTimeline'] + const onShow: typeof import('@dcloudio/uni-app')['onShow'] + const onTabItemTap: typeof import('@dcloudio/uni-app')['onTabItemTap'] + const onThemeChange: typeof import('@dcloudio/uni-app')['onThemeChange'] + const onUnhandledRejection: typeof import('@dcloudio/uni-app')['onUnhandledRejection'] + const onUnload: typeof import('@dcloudio/uni-app')['onUnload'] + const onUnmounted: typeof import('vue')['onUnmounted'] + const onUpdated: typeof import('vue')['onUpdated'] + const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] + const provide: typeof import('vue')['provide'] + const reactive: typeof import('vue')['reactive'] + const readonly: typeof import('vue')['readonly'] + const ref: typeof import('vue')['ref'] + const resolveComponent: typeof import('vue')['resolveComponent'] + const shallowReactive: typeof import('vue')['shallowReactive'] + const shallowReadonly: typeof import('vue')['shallowReadonly'] + const shallowRef: typeof import('vue')['shallowRef'] + const toRaw: typeof import('vue')['toRaw'] + const toRef: typeof import('vue')['toRef'] + const toRefs: typeof import('vue')['toRefs'] + const toValue: typeof import('vue')['toValue'] + const triggerRef: typeof import('vue')['triggerRef'] + const unref: typeof import('vue')['unref'] + const useAttrs: typeof import('vue')['useAttrs'] + const useCssModule: typeof import('vue')['useCssModule'] + const useCssVars: typeof import('vue')['useCssVars'] + const useId: typeof import('vue')['useId'] + const useModel: typeof import('vue')['useModel'] + const useRequest: typeof import('../hooks/useRequest')['default'] + const useScroll: typeof import('../hooks/useScroll')['useScroll'] + const useSlots: typeof import('vue')['useSlots'] + const useTemplateRef: typeof import('vue')['useTemplateRef'] + const useUpload: typeof import('../hooks/useUpload')['default'] + const watch: typeof import('vue')['watch'] + const watchEffect: typeof import('vue')['watchEffect'] + const watchPostEffect: typeof import('vue')['watchPostEffect'] + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] +} +// for type re-export +declare global { + // @ts-ignore + export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + import('vue') +} + +// for vue template auto import +import { UnwrapRef } from 'vue' +declare module 'vue' { + interface GlobalComponents {} + interface ComponentCustomProperties { + readonly EffectScope: UnwrapRef + readonly computed: UnwrapRef + readonly createApp: UnwrapRef + readonly customRef: UnwrapRef + readonly defineAsyncComponent: UnwrapRef + readonly defineComponent: UnwrapRef + readonly effectScope: UnwrapRef + readonly getCurrentInstance: UnwrapRef + readonly getCurrentScope: UnwrapRef + readonly getCurrentWatcher: UnwrapRef + readonly h: UnwrapRef + readonly inject: UnwrapRef + readonly isProxy: UnwrapRef + readonly isReactive: UnwrapRef + readonly isReadonly: UnwrapRef + readonly isRef: UnwrapRef + readonly isShallow: UnwrapRef + readonly markRaw: UnwrapRef + readonly nextTick: UnwrapRef + readonly onActivated: UnwrapRef + readonly onAddToFavorites: UnwrapRef + readonly onBackPress: UnwrapRef + readonly onBeforeMount: UnwrapRef + readonly onBeforeUnmount: UnwrapRef + readonly onBeforeUpdate: UnwrapRef + readonly onDeactivated: UnwrapRef + readonly onError: UnwrapRef + readonly onErrorCaptured: UnwrapRef + readonly onHide: UnwrapRef + readonly onLaunch: UnwrapRef + readonly onLoad: UnwrapRef + readonly onMounted: UnwrapRef + readonly onNavigationBarButtonTap: UnwrapRef + readonly onNavigationBarSearchInputChanged: UnwrapRef + readonly onNavigationBarSearchInputClicked: UnwrapRef + readonly onNavigationBarSearchInputConfirmed: UnwrapRef + readonly onNavigationBarSearchInputFocusChanged: UnwrapRef + readonly onPageNotFound: UnwrapRef + readonly onPageScroll: UnwrapRef + readonly onPullDownRefresh: UnwrapRef + readonly onReachBottom: UnwrapRef + readonly onReady: UnwrapRef + readonly onRenderTracked: UnwrapRef + readonly onRenderTriggered: UnwrapRef + readonly onResize: UnwrapRef + readonly onScopeDispose: UnwrapRef + readonly onServerPrefetch: UnwrapRef + readonly onShareAppMessage: UnwrapRef + readonly onShareTimeline: UnwrapRef + readonly onShow: UnwrapRef + readonly onTabItemTap: UnwrapRef + readonly onThemeChange: UnwrapRef + readonly onUnhandledRejection: UnwrapRef + readonly onUnload: UnwrapRef + readonly onUnmounted: UnwrapRef + readonly onUpdated: UnwrapRef + readonly onWatcherCleanup: UnwrapRef + readonly provide: UnwrapRef + readonly reactive: UnwrapRef + readonly readonly: UnwrapRef + readonly ref: UnwrapRef + readonly resolveComponent: UnwrapRef + readonly shallowReactive: UnwrapRef + readonly shallowReadonly: UnwrapRef + readonly shallowRef: UnwrapRef + readonly toRaw: UnwrapRef + readonly toRef: UnwrapRef + readonly toRefs: UnwrapRef + readonly toValue: UnwrapRef + readonly triggerRef: UnwrapRef + readonly unref: UnwrapRef + readonly useAttrs: UnwrapRef + readonly useCssModule: UnwrapRef + readonly useCssVars: UnwrapRef + readonly useId: UnwrapRef + readonly useModel: UnwrapRef + readonly useRequest: UnwrapRef + readonly useScroll: UnwrapRef + readonly useSlots: UnwrapRef + readonly useTemplateRef: UnwrapRef + readonly useUpload: UnwrapRef + readonly watch: UnwrapRef + readonly watchEffect: UnwrapRef + readonly watchPostEffect: UnwrapRef + readonly watchSyncEffect: UnwrapRef + } +} \ No newline at end of file diff --git a/app/src/types/components.d.ts b/app/src/types/components.d.ts new file mode 100644 index 0000000..44c7989 --- /dev/null +++ b/app/src/types/components.d.ts @@ -0,0 +1,35 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by vite-plugin-uni-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module 'vue' { + export interface GlobalComponents { + Navbar: typeof import('./../components/Navbar/index.vue')['default'] + SideCaptcha: typeof import('./../components/SideCaptcha.vue')['default'] + WdActionSheet: typeof import('@wot-ui/ui/components/wd-action-sheet/wd-action-sheet.vue')['default'] + WdButton: typeof import('@wot-ui/ui/components/wd-button/wd-button.vue')['default'] + WdCard: typeof import('@wot-ui/ui/components/wd-card/wd-card.vue')['default'] + WdCell: typeof import('@wot-ui/ui/components/wd-cell/wd-cell.vue')['default'] + WdCellGroup: typeof import('@wot-ui/ui/components/wd-cell-group/wd-cell-group.vue')['default'] + WdCheckbox: typeof import('@wot-ui/ui/components/wd-checkbox/wd-checkbox.vue')['default'] + WdCheckboxGroup: typeof import('@wot-ui/ui/components/wd-checkbox-group/wd-checkbox-group.vue')['default'] + WdEmpty: typeof import('@wot-ui/ui/components/wd-empty/wd-empty.vue')['default'] + WdForm: typeof import('@wot-ui/ui/components/wd-form/wd-form.vue')['default'] + WdIcon: typeof import('@wot-ui/ui/components/wd-icon/wd-icon.vue')['default'] + WdInput: typeof import('@wot-ui/ui/components/wd-input/wd-input.vue')['default'] + WdLoading: typeof import('@wot-ui/ui/components/wd-loading/wd-loading.vue')['default'] + WdNavbar: typeof import('@wot-ui/ui/components/wd-navbar/wd-navbar.vue')['default'] + WdPicker: typeof import('@wot-ui/ui/components/wd-picker/wd-picker.vue')['default'] + WdPopup: typeof import('@wot-ui/ui/components/wd-popup/wd-popup.vue')['default'] + WdProgress: typeof import('@wot-ui/ui/components/wd-progress/wd-progress.vue')['default'] + WdSearch: typeof import('@wot-ui/ui/components/wd-search/wd-search.vue')['default'] + WdSegmented: typeof import('@wot-ui/ui/components/wd-segmented/wd-segmented.vue')['default'] + WdSkeleton: typeof import('@wot-ui/ui/components/wd-skeleton/wd-skeleton.vue')['default'] + WdSwitch: typeof import('@wot-ui/ui/components/wd-switch/wd-switch.vue')['default'] + WdTag: typeof import('@wot-ui/ui/components/wd-tag/wd-tag.vue')['default'] + WdTextarea: typeof import('@wot-ui/ui/components/wd-textarea/wd-textarea.vue')['default'] + } +} diff --git a/app/src/types/uni-pages.d.ts b/app/src/types/uni-pages.d.ts new file mode 100644 index 0000000..66f94c8 --- /dev/null +++ b/app/src/types/uni-pages.d.ts @@ -0,0 +1,63 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by vite-plugin-uni-pages + +type _LocationUrl = + "/pages/home/index" | + "/pages/business/index" | + "/pages/card/index" | + "/pages/login/index" | + "/pages/message/index" | + "/pages/user/index" | + "/pages/login/forgot-password/index" | + "/pages/login/register/index" | + "/pages/business/views/addon/index" | + "/pages/business/views/allocate/index" | + "/pages/business/views/balanceRecharge/index" | + "/pages/business/views/cardRenew/index" | + "/pages/business/views/change/index" | + "/pages/business/views/customer/index" | + "/pages/business/views/customerDetail/index" | + "/pages/business/views/diagnosis/index" | + "/pages/business/views/order/index" | + "/pages/business/views/renew/index" | + "/pages/business/views/warning/index" | + "/pages/business/views/workOrder/index" | + "/pages/business/views/workOrderCreate/index" | + "/pages/business/views/workOrderDetail/index" | + "/pages/card/views/detail/index" | + "/pages/card/views/management/index" | + "/pages/card/views/packageDetail/index" | + "/pages/card/views/warning/index" | + "/pages/message/views/cardDetail/index" | + "/pages/message/views/systemDetail/index" | + "/pages/user/views/about/index" | + "/pages/user/views/agreement/index" | + "/pages/user/views/contact/index" | + "/pages/user/views/feedback/index" | + "/pages/user/views/notification/index" | + "/pages/user/views/profile/index" | + "/pages/user/views/realName/index"; + +interface NavigateToOptions { + url: _LocationUrl; +} +interface RedirectToOptions extends NavigateToOptions {} + +interface SwitchTabOptions { + url: "/pages/home/index" | "/pages/card/index" | "/pages/business/index" | "/pages/user/index" +} + +type ReLaunchOptions = NavigateToOptions | SwitchTabOptions; + +declare interface Uni { + navigateTo(options: UniNamespace.NavigateToOptions & NavigateToOptions): void; + redirectTo(options: UniNamespace.RedirectToOptions & RedirectToOptions): void; + switchTab(options: UniNamespace.SwitchTabOptions & SwitchTabOptions): void; + reLaunch(options: UniNamespace.ReLaunchOptions & ReLaunchOptions): void; +} + +declare module "virtual:uni-pages" { + export type LocationUrl = _LocationUrl; +} diff --git a/app/src/typings.d.ts b/app/src/typings.d.ts new file mode 100644 index 0000000..5c62bc7 --- /dev/null +++ b/app/src/typings.d.ts @@ -0,0 +1,171 @@ +// 全局要用的类型放到这里 + +declare global { + interface IResData { + code: number + msg: string + data: T + } + + // uni.uploadFile文件上传参数 + interface IUniUploadFileOptions { + file?: File + files?: UniApp.UploadFileOptionFiles[] + filePath?: string + name?: string + formData?: any + } + + interface IUserInfo { + nickname?: string + avatar?: string + /** 微信的 openid,非微信没有这个字段 */ + openid?: string + } + + interface IUserToken { + token: string + refreshToken?: string + refreshExpire?: number + } +} + +// 扩展 @uni-helper/vite-plugin-uni-pages 的 definePage 参数类型 +declare module '@uni-helper/vite-plugin-uni-pages' { + interface UserPageMeta { + /** + * 使用 type: "home" 属性设置首页,其他页面不需要设置,默认为page + * + * 尽量保证一个项目 只有一个 这个配置,如果有多个,会按照字母顺序来排列,最终可能不是您想要的效果。 + */ + type?: 'home' + /** + * 页面布局类型, 模板默认只有 default, 如果在 src/layouts 下新增了 layout, 可以扩展当前属性 + * @default 'default' + * + * 当前属性供 https://github.com/uni-helper/vite-plugin-uni-layouts 插件使用 + */ + layout?: 'default' | 'blank' | false + /** + * 是否从需要登录的路径中排除 + * + * 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 src/router 文件夹 + */ + excludeLoginPath?: boolean + } +} + +// patch uni 类型 +// 1. 补全 uni.hideToast() 的 options 类型 +// 2. 补全 uni.hideLoading() 的 options 类型 +// 3. 使用方式见:https://github.com/unibest-tech/unibest/pull/241 +declare global { + declare namespace UniNamespace { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + type HideLoadingCompleteCallback = (res: GeneralCallbackResult) => void + /** 接口调用失败的回调函数 */ + type HideLoadingFailCallback = (res: GeneralCallbackResult) => void + /** 接口调用成功的回调函数 */ + type HideLoadingSuccessCallback = (res: GeneralCallbackResult) => void + + interface HideLoadingOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideLoadingCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideLoadingFailCallback + test: UniNamespace.GeneralCallbackResult + /** + * 微信小程序:需要基础库: `2.22.1` + * + * 微信小程序:目前 toast 和 loading 相关接口可以相互混用,此参数可用于取消混用特性 + */ + noConflict?: boolean + /** 接口调用成功的回调函数 */ + success?: HideLoadingSuccessCallback + } + + // ---------------------------------------------------------- + + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + type HideToastCompleteCallback = (res: GeneralCallbackResult) => void + /** 接口调用失败的回调函数 */ + type HideToastFailCallback = (res: GeneralCallbackResult) => void + /** 接口调用成功的回调函数 */ + type HideToastSuccessCallback = (res: GeneralCallbackResult) => void + interface HideToastOption { + /** 接口调用结束的回调函数(调用成功、失败都会执行) */ + complete?: HideToastCompleteCallback + /** 接口调用失败的回调函数 */ + fail?: HideToastFailCallback + /** + * 微信小程序:需要基础库: `2.22.1` + * + * 微信小程序:目前 toast 和 loading 相关接口可以相互混用,此参数可用于取消混用特性 + */ + noConflict?: boolean + /** 接口调用成功的回调函数 */ + success?: HideToastSuccessCallback + } + } + interface Uni { + /** + * 隐藏 loading 提示框 + * + * 文档: [http://uniapp.dcloud.io/api/ui/prompt?id=hideloading](http://uniapp.dcloud.io/api/ui/prompt?id=hideloading) + * @example ```typescript + * uni.showLoading({ + * title: '加载中' + * }); + * + * setTimeout(function () { + * uni.hideLoading(); + * }, 2000); + * + * ``` + * @tutorial [](https://uniapp.dcloud.net.cn/api/ui/prompt.html#hideloading) + * @uniPlatform { + * "app": { + * "android": { + * "osVer": "4.4.4", + * "uniVer": "√", + * "unixVer": "3.9.0" + * }, + * "ios": { + * "osVer": "9.0", + * "uniVer": "√", + * "unixVer": "3.9.0" + * } + * } + * } + */ + // eslint-disable-next-line ts/method-signature-style + hideLoading(options?: T): void + /** + * 隐藏消息提示框 + * + * 文档: [http://uniapp.dcloud.io/api/ui/prompt?id=hidetoast](http://uniapp.dcloud.io/api/ui/prompt?id=hidetoast) + * @example ```typescript + * uni.hideToast(); + * ``` + * @tutorial [](https://uniapp.dcloud.net.cn/api/ui/prompt.html#hidetoast) + * @uniPlatform { + * "app": { + * "android": { + * "osVer": "4.4.4", + * "uniVer": "√", + * "unixVer": "3.9.0" + * }, + * "ios": { + * "osVer": "9.0", + * "uniVer": "√", + * "unixVer": "3.9.0" + * } + * } + * } + */ + // eslint-disable-next-line ts/method-signature-style + hideToast(options?: T): void + } +} + +export {} // 防止模块污染 diff --git a/app/src/typings.ts b/app/src/typings.ts new file mode 100644 index 0000000..fefe1cb --- /dev/null +++ b/app/src/typings.ts @@ -0,0 +1,21 @@ +// 枚举定义 + +export enum TestEnum { + A = '1', + B = '2', +} + +// uni.uploadFile文件上传参数 +export interface IUniUploadFileOptions { + file?: File + files?: UniApp.UploadFileOptionFiles[] + filePath?: string + name?: string + formData?: any +} + +/** 工具类型:删除字符串开头的第一个斜杠 */ +export type RemoveLeadingSlash = S extends `/${infer Rest}` ? Rest : S + +/** 工具类型:删除联合类型中每个字符串的第一个斜杠 */ +export type RemoveLeadingSlashFromUnion = T extends any ? RemoveLeadingSlash : never diff --git a/app/src/uni.scss b/app/src/uni.scss new file mode 100644 index 0000000..6179458 --- /dev/null +++ b/app/src/uni.scss @@ -0,0 +1,77 @@ +/* stylelint-disable comment-empty-line-before */ +/** + * 这里是uni-app内置的常用样式变量 + * + * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 + * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App + * + */ + +/** + * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 + * + * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 + */ + +/* 颜色变量(雪羊科技蓝 主题) */ + +/* 行为相关颜色 */ +$uni-color-primary: #2563eb; +$uni-color-success: #16a34a; +$uni-color-warning: #f59e0b; +$uni-color-error: #dc2626; + +/* 文字基本颜色 */ +$uni-text-color: #334155; // 基本色 +$uni-text-color-inverse: #fff; // 反色 +$uni-text-color-grey: #94a3b8; // 辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #94a3b8; +$uni-text-color-disable: #cbd5e1; + +/* 背景颜色 */ +$uni-bg-color: #fff; +$uni-bg-color-grey: #f5f7fb; +$uni-bg-color-hover: #eff6ff; // 点击状态颜色 +$uni-bg-color-mask: rgb(0 0 0 / 40%); // 遮罩颜色 + +/* 边框颜色 */ +$uni-border-color: #e2e8f0; + +/* 尺寸变量 */ + +/* 文字尺寸 */ +$uni-font-size-sm: 12px; +$uni-font-size-base: 14px; +$uni-font-size-lg: 16; + +/* 图片尺寸 */ +$uni-img-size-sm: 20px; +$uni-img-size-base: 26px; +$uni-img-size-lg: 40px; + +/* Border Radius */ +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; +$uni-border-radius-circle: 50%; + +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; + +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; + +/* 透明度 */ +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 + +/* 文章场景相关 */ +$uni-color-title: #2c405a; // 文章标题颜色 +$uni-font-size-title: 20px; +$uni-color-subtitle: #555; // 二级标题颜色 +$uni-font-size-subtitle: 18px; +$uni-color-paragraph: #3f536e; // 文章段落颜色 +$uni-font-size-paragraph: 15px; diff --git a/app/src/uni_modules/.gitkeep b/app/src/uni_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/src/utils/autoCreateRoutePath.ts b/app/src/utils/autoCreateRoutePath.ts new file mode 100644 index 0000000..1436508 --- /dev/null +++ b/app/src/utils/autoCreateRoutePath.ts @@ -0,0 +1,101 @@ +import fs from 'node:fs' +import { fileURLToPath, URL } from 'node:url' +import * as prettier from 'prettier' + +/** + * 自动生成路由常量文件 + * @param ctx uni-pages 上下文 + * @returns Promise + */ +export default async function autoCreateRoutePath(ctx: any) { + const pathList: string[] = [] + const mapList: string[] = [] + + /** + * 将路径转换为路由名(大驼峰) + * /pages/message/index → MessageIndex + * /pages/panel/AppIcon → PanelAppIcon + */ + function pathToRouteName(path: string) { + return path + .split('/') + .filter(Boolean) + .filter((_, i) => i > 0) + .map((segment: string) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join('') + .replace(/Index$/, '') + } + + const allPages = [...ctx.pageMetaData] + ;(ctx.subPageMetaData || []).forEach((subPackage) => { + const root = subPackage.root + ;(subPackage.pages || []).forEach((page) => { + allPages.push({ + ...page, + path: `${root}/${page.path}`, + }) + }) + }) + + allPages.forEach((page) => { + const comment = `/** ${page.style?.navigationBarTitleText || ''} */` + const routeName = pathToRouteName(page.path) + pathList.push(`${comment}\n"${routeName}": "/${page.path}",`) + mapList.push(`"/${page.path}": "${page?.style?.navigationBarTitleText || routeName}",`) + }) + + const pathContent = ` + /******************************* + * 此文件由脚本自动生成,请勿手动修改 * + *******************************/ + + /** + * 页面路径常量 + */ + const RouterPath = {\n${pathList.join('\n')}\n} + + export default RouterPath + ` + + const pathPath = fileURLToPath(new URL('../constant/RouterPath.ts', import.meta.url)) + await writeFile(pathPath, pathContent) + + // const mapContent = ` + // /******************************* + // * 此文件由脚本自动生成,请勿手动修改 * + // *******************************/ + + // /** + // * 路由映射表 + // */ + // const RouterMap:Record = {\n${mapList.join('\n')}\n} + + // export default RouterMap + // ` + + // const mapPath = fileURLToPath(new URL('../constant/RouterMap.ts', import.meta.url)) + // await writeFile(mapPath, mapContent) +} + +/** + * 写入并格式化文件 + * @param filepath 目标文件路径 + * @param content 文件内容 + * @returns Promise + */ +async function writeFile(filepath: string, content: string) { + try { + const prettierConfig = await prettier.resolveConfig(filepath) + const formattedContent = await prettier.format(content, { + ...prettierConfig, + filepath, + }) + fs.writeFileSync(filepath, formattedContent, { + encoding: 'utf-8', + }) + console.log(`✅ 页面路径常量文件已成功生成:${filepath}`) + } catch (error: any) { + console.error(`❌ 生成页面路径常量文件失败:${error.message}`) + console.error('错误详情:', error) + } +} diff --git a/app/src/utils/cardRecognition.ts b/app/src/utils/cardRecognition.ts new file mode 100644 index 0000000..fd6a39b --- /dev/null +++ b/app/src/utils/cardRecognition.ts @@ -0,0 +1,72 @@ +import type { CardRecognitionRes } from '@/api/types/card' + +export function chooseCardImage() { + return new Promise((resolve, reject) => { + // #ifdef MP-WEIXIN + uni.chooseMedia({ + count: 1, + mediaType: ['image'], + sourceType: ['camera', 'album'], + success: (response) => { + const filePath = response.tempFiles?.[0]?.tempFilePath + if (filePath) + resolve(filePath) + else + reject({ message: '没有选择图片' }) + }, + fail: (error) => reject({ message: error.errMsg || '取消选择图片' }), + }) + // #endif + + // #ifndef MP-WEIXIN + uni.chooseImage({ + count: 1, + sizeType: ['compressed', 'original'], + sourceType: ['camera', 'album'], + success: (response) => { + const filePath = response.tempFilePaths?.[0] + if (filePath) + resolve(filePath) + else + reject({ message: '没有选择图片' }) + }, + fail: (error) => reject({ message: error.errMsg || '取消选择图片' }), + }) + // #endif + }) +} + +export function cardRecognitionMessage(result: CardRecognitionRes) { + if (result.code === 'CARD_RECOGNIZED') + return result.cardValue ? `已识别 ICCID:${result.cardValue}` : '已识别 ICCID' + + switch (result.code) { + case 'CARD_AMBIGUOUS': + return '图片中存在多个可能的 ICCID,请重新拍摄单张卡片' + case 'CARD_UNREADABLE': + return '未能识别清晰的 ICCID,请拍清卡号后重试' + case 'RECOGNITION_TIMEOUT': + return '图片识别超时,请稍后重试' + case 'RECOGNITION_DISABLED': + return '图片识别服务暂未配置' + case 'RECOGNITION_RATE_LIMITED': + return '图片识别请求过于频繁,请稍后再试' + default: + return '图片识别服务暂时不可用,请稍后重试' + } +} + +export function showCardRecognitionNotice(message: string) { + if (!message) + return + if (message.length <= 12) { + uni.showToast({ title: message, icon: 'none' }) + return + } + uni.showModal({ + title: '识别结果', + content: message, + showCancel: false, + confirmText: '知道了', + }) +} diff --git a/app/src/utils/clipboard.ts b/app/src/utils/clipboard.ts new file mode 100644 index 0000000..5cc07d9 --- /dev/null +++ b/app/src/utils/clipboard.ts @@ -0,0 +1,14 @@ +export function copyText(value: unknown, label = '内容') { + const text = String(value ?? '').trim() + if (!text || text === '-') { + uni.showToast({ title: `暂无可复制${label}`, icon: 'none' }) + return + } + + uni.setClipboardData({ + data: text, + success: () => { + uni.showToast({ title: `${label}已复制`, icon: 'success' }) + }, + }) +} diff --git a/app/src/utils/debounce.ts b/app/src/utils/debounce.ts new file mode 100644 index 0000000..c13f470 --- /dev/null +++ b/app/src/utils/debounce.ts @@ -0,0 +1,166 @@ +// fork from https://github.com/toss/es-toolkit/blob/main/src/function/debounce.ts +// 文档可查看:https://es-toolkit.dev/reference/function/debounce.html +// 如需要 throttle 功能,可 copy https://github.com/toss/es-toolkit/blob/main/src/function/throttle.ts + +interface DebounceOptions { + /** + * An optional AbortSignal to cancel the debounced function. + */ + signal?: AbortSignal + + /** + * An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both. + * If `edges` includes "leading", the function will be invoked at the start of the delay period. + * If `edges` includes "trailing", the function will be invoked at the end of the delay period. + * If both "leading" and "trailing" are included, the function will be invoked at both the start and end of the delay period. + * @default ["trailing"] + */ + edges?: Array<'leading' | 'trailing'> +} + +export interface DebouncedFunction void> { + (...args: Parameters): void + + /** + * Schedules the execution of the debounced function after the specified debounce delay. + * This method resets any existing timer, ensuring that the function is only invoked + * after the delay has elapsed since the last call to the debounced function. + * It is typically called internally whenever the debounced function is invoked. + * + * @returns {void} + */ + schedule: () => void + + /** + * Cancels any pending execution of the debounced function. + * This method clears the active timer and resets any stored context or arguments. + */ + cancel: () => void + + /** + * Immediately invokes the debounced function if there is a pending execution. + * This method executes the function right away if there is a pending execution. + */ + flush: () => void +} + +/** + * Creates a debounced function that delays invoking the provided function until after `debounceMs` milliseconds + * have elapsed since the last time the debounced function was invoked. The debounced function also has a `cancel` + * method to cancel any pending execution. + * + * @template F - The type of function. + * @param {F} func - The function to debounce. + * @param {number} debounceMs - The number of milliseconds to delay. + * @param {DebounceOptions} options - The options object + * @param {AbortSignal} options.signal - An optional AbortSignal to cancel the debounced function. + * @returns A new debounced function with a `cancel` method. + * + * @example + * const debouncedFunction = debounce(() => { + * console.log('Function executed'); + * }, 1000); + * + * // Will log 'Function executed' after 1 second if not called again in that time + * debouncedFunction(); + * + * // Will not log anything as the previous call is canceled + * debouncedFunction.cancel(); + * + * // With AbortSignal + * const controller = new AbortController(); + * const signal = controller.signal; + * const debouncedWithSignal = debounce(() => { + * console.log('Function executed'); + * }, 1000, { signal }); + * + * debouncedWithSignal(); + * + * // Will cancel the debounced function call + * controller.abort(); + */ +export function debounce void>( + func: F, + debounceMs: number, + { signal, edges }: DebounceOptions = {}, +): DebouncedFunction { + let pendingThis: any + let pendingArgs: Parameters | null = null + + const leading = edges != null && edges.includes('leading') + const trailing = edges == null || edges.includes('trailing') + + const invoke = () => { + if (pendingArgs !== null) { + func.apply(pendingThis, pendingArgs) + pendingThis = undefined + pendingArgs = null + } + } + + const onTimerEnd = () => { + if (trailing) { + invoke() + } + + // eslint-disable-next-line ts/no-use-before-define + cancel() + } + + let timeoutId: ReturnType | null = null + + const schedule = () => { + if (timeoutId != null) { + clearTimeout(timeoutId) + } + + timeoutId = setTimeout(() => { + timeoutId = null + + onTimerEnd() + }, debounceMs) + } + + const cancelTimer = () => { + if (timeoutId !== null) { + clearTimeout(timeoutId) + timeoutId = null + } + } + + const cancel = () => { + cancelTimer() + pendingThis = undefined + pendingArgs = null + } + + const flush = () => { + invoke() + } + + const debounced = function (this: any, ...args: Parameters) { + if (signal?.aborted) { + return + } + + // eslint-disable-next-line ts/no-this-alias + pendingThis = this + pendingArgs = args + + const isFirstCall = timeoutId == null + + schedule() + + if (leading && isFirstCall) { + invoke() + } + } + + debounced.schedule = schedule + debounced.cancel = cancel + debounced.flush = flush + + signal?.addEventListener('abort', cancel, { once: true }) + + return debounced +} diff --git a/app/src/utils/i18n.ts b/app/src/utils/i18n.ts new file mode 100644 index 0000000..62f777d --- /dev/null +++ b/app/src/utils/i18n.ts @@ -0,0 +1,10 @@ +import { t } from '@/locale/index' + +/** 非vue 文件使用 i18n */ +export function testI18n() { + // 下面同样生效 + uni.showModal({ + title: 'i18n 测试', + content: t('i18n.title'), + }) +} diff --git a/app/src/utils/index.ts b/app/src/utils/index.ts new file mode 100644 index 0000000..e622263 --- /dev/null +++ b/app/src/utils/index.ts @@ -0,0 +1,315 @@ +import type { PageMetaDatum, SubPackages } from '@uni-helper/vite-plugin-uni-pages' +import { isMpWeixin } from '@uni-helper/uni-env' +import { pages, subPackages } from '@/pages.json' +import { isPageTabbar } from '@/tabbar/store' + +export type PageInstance = Page.PageInstance & { + route?: string + options?: Record + $page?: Page.PageInstance & { + fullPath?: string + path?: string + route?: string + options?: Record + } +} + +export function getLastPage() { + // getCurrentPages() 至少有1个元素,所以不再额外判断 + // const lastPage = getCurrentPages().at(-1) + // 上面那个在低版本安卓中打包会报错,所以改用下面这个【虽然我加了 src/interceptions/prototype.ts,但依然报错】 + const pages = getCurrentPages() + return pages[pages.length - 1] as PageInstance +} + +function normalizeRoutePath(path?: string) { + if (!path) { + return '' + } + return path.startsWith('/') ? path : `/${path}` +} + +function stringifyRouteQuery(query?: Record) { + if (!query) { + return '' + } + + return Object.entries(query) + .filter(([, value]) => value !== undefined && value !== null) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) + .join('&') +} + +function getPageFullPath(page?: PageInstance) { + if (!page) { + return '' + } + + const fullPath = page.$page?.fullPath + if (fullPath) { + return normalizeRoutePath(fullPath) + } + + const path = normalizeRoutePath(page.$page?.path || page.$page?.route || page.route) + if (!path) { + return '' + } + + const query = page.$page?.options || page.options + const queryStr = stringifyRouteQuery(query) + return queryStr ? `${path}?${queryStr}` : path +} + +/** + * 获取当前页面路由的 path 路径和 redirectPath 路径 + * path 如 '/pages/login/login' + * redirectPath 如 '/pages/demo/base/route-interceptor' + */ +export function currRoute() { + const lastPage = getLastPage() as PageInstance + const fullPath = getPageFullPath(lastPage) + if (!fullPath) { + return { + path: '', + query: {}, + } + } + // console.log('lastPage.$page:', currRoute) + // console.log('lastPage.$page.fullpath:', currRoute.fullPath) + // console.log('lastPage.$page.options:', currRoute.options) + // console.log('lastPage.options:', (lastPage as any).options) + // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱 + // const { fullPath } = currRoute + // console.log(fullPath) + // eg: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序) + // eg: /pages/login/login?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5) + return parseUrlToObj(fullPath) +} + +export function ensureDecodeURIComponent(url: string) { + if (url.startsWith('%')) { + return ensureDecodeURIComponent(decodeURIComponent(url)) + } + return url +} +/** + * 解析 url 得到 path 和 query + * 比如输入url: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor + * 输出: {path: /pages/login/login, query: {redirect: /pages/demo/base/route-interceptor}} + */ +export function parseUrlToObj(url: string) { + const [path, queryStr] = url.split('?') + // console.log(path, queryStr) + + if (!queryStr) { + return { + path, + query: {}, + } + } + const query: Record = {} + queryStr.split('&').forEach((item) => { + const [key, value] = item.split('=') + // console.log(key, value) + query[key] = ensureDecodeURIComponent(value || '') // 这里需要统一 decodeURIComponent 一下,可以兼容h5和微信y + }) + return { path, query } +} +/** + * 得到所有的需要登录的 pages,包括主包和分包的 + * 这里设计得通用一点,可以传递 key 作为判断依据,默认是 excludeLoginPath, 与 route-block 配对使用 + * 如果没有传 key,则表示所有的 pages,如果传递了 key, 则表示通过 key 过滤 + */ +export function getAllPages(key?: string) { + // 这里处理主包 + const mainPages = (pages as PageMetaDatum[]) + .filter(page => !key || page[key]) + .map(page => ({ + ...page, + path: `/${page.path}`, + })) + + // 这里处理分包 + const subPages: PageMetaDatum[] = [] + ;(subPackages as SubPackages).forEach((subPageObj) => { + // console.log(subPageObj) + const { root } = subPageObj + + subPageObj.pages + .filter(page => !key || page[key]) + .forEach((page) => { + subPages.push({ + ...page, + path: `/${root}/${page.path}`, + }) + }) + }) + const result = [...mainPages, ...subPages] + // console.log(`getAllPages by ${key} result: `, result) + return result +} + +export function getCurrentPageI18nKey() { + const routeObj = currRoute() + + let currPage = (pages as PageMetaDatum[]).find(page => `/${page.path}` === routeObj.path) + if (!currPage) { + // 在主包中找不到对应的页面,则在分包中找 + const allSubPages: PageMetaDatum[] = [] + subPackages?.forEach((config) => { + config.pages?.forEach((cur) => { + allSubPages.push({ + ...cur, + path: `/${config.root}/${cur.path}`, + }) + }) + }) + currPage = allSubPages.find(page => page.path === routeObj.path) + if (!currPage) { + console.warn('路由不正确') + return '' + } + } + console.log(currPage) + // console.log(currPage.style.navigationBarTitleText) + return currPage.style?.navigationBarTitleText || '' +} + +export function isCurrentPageTabbar() { + const { path } = currRoute() + return isPageTabbar(path) +} + +/** + * 获取当前端应使用的后端接口地址。 + */ +export function getEnvBaseUrl() { + // H5 和其他端默认使用这个地址;微信小程序优先使用专用地址。 + const baseUrl = import.meta.env.VITE_SERVER_BASEURL + + const VITE_MP_SERVER_BASEURL = import.meta.env.VITE_MP_SERVER_BASEURL + + if (isMpWeixin) { + return VITE_MP_SERVER_BASEURL || baseUrl + } + + return baseUrl +} + +/** + * 是否是双token模式 + */ +export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double' + +/** + * 首页路径,通过 page.json 里面的 type 为 home 的页面获取,如果没有,则默认是第一个页面 + * 通常为 /pages/index/index + */ +export const HOME_PAGE = `/${(pages as PageMetaDatum[]).find(page => page.type === 'home')?.path || (pages as PageMetaDatum[])[0].path}` + +export type WechatSubscribeStatus = 'accepted' | 'rejected' | 'blocked' | 'skipped' | 'failed' + +export interface WechatSubscribeResult { + status: WechatSubscribeStatus + message: string + errMsg?: string +} + +export function shouldShowWechatSubscribeNotice(result: WechatSubscribeResult) { + return result.status !== 'accepted' && result.status !== 'skipped' +} + +export function wechatSubscribeSaveMessage(result: WechatSubscribeResult) { + if (result.status === 'accepted') + return '已开启微信订阅提醒,后续触发预警时会通过微信通知。' + if (result.status === 'skipped') + return '' + return result.message +} + +export function shouldGuideWechatSubscribeSettings(result: WechatSubscribeResult) { + return result.status === 'rejected' || result.status === 'blocked' +} + +export function guideWechatSubscribeSettings(result: WechatSubscribeResult) { + if (!shouldGuideWechatSubscribeSettings(result)) + return + + const openSetting = (uni as any).openSetting + uni.showModal({ + title: '重新开启微信提醒', + content: result.status === 'blocked' + ? '微信订阅提醒已关闭,请前往小程序设置重新开启通知权限。开启后返回本页面,再点击一次申请。' + : '你之前取消了微信订阅授权,请前往小程序设置重新开启通知权限。开启后返回本页面,再点击一次申请。', + confirmText: '去设置', + cancelText: '稍后', + success: (modalResult) => { + if (modalResult.confirm && typeof openSetting === 'function') + openSetting({ withSubscriptions: true }) + }, + }) +} + +export async function requestWechatSubscribeMessage( + templateId: string, + templateReady: boolean, +): Promise { + if (!templateReady || !templateId) { + return { + status: 'skipped', + message: '微信订阅模板未配置,已跳过订阅授权。', + } + } + + const requestSubscribeMessage = (uni as any).requestSubscribeMessage + if (typeof requestSubscribeMessage !== 'function') { + return { + status: 'skipped', + message: '当前环境不支持微信订阅消息,请在微信小程序内开启提醒。', + } + } + + return new Promise((resolve) => { + requestSubscribeMessage({ + tmplIds: [templateId], + success: (res: Record) => { + const result = res?.[templateId] + if (result === 'accept') { + resolve({ + status: 'accepted', + message: '已开启微信订阅提醒。', + }) + return + } + if (result === 'reject') { + resolve({ + status: 'rejected', + message: '未开启微信订阅,本次配置会保存,但可能无法收到微信提醒。', + }) + return + } + if (result === 'ban') { + resolve({ + status: 'blocked', + message: '微信订阅消息已被关闭,请在小程序设置中开启通知权限后重试。', + }) + return + } + resolve({ + status: 'failed', + message: '微信订阅授权状态未知,本次配置会保存,请稍后在小程序内重试订阅。', + }) + }, + fail: (error: { errMsg?: string }) => { + const errMsg = error?.errMsg?.trim() + resolve({ + status: 'failed', + message: errMsg + ? `微信订阅授权失败:${errMsg}` + : '微信订阅授权失败,本次配置会保存,请稍后重试。', + errMsg, + }) + }, + }) + }) +} diff --git a/app/src/utils/initNavbarDesc.ts b/app/src/utils/initNavbarDesc.ts new file mode 100644 index 0000000..c7db842 --- /dev/null +++ b/app/src/utils/initNavbarDesc.ts @@ -0,0 +1,22 @@ +import { currRoute } from './index' +import RouterPath from '@/constant/RouterPath' + +const navbarDescMap = reactive>({}) + +function getCurrentRoutePath() { + return currRoute().path || RouterPath.Home +} + +export function useNavbarDesc() { + const path = getCurrentRoutePath() + + if (!(path in navbarDescMap)) { + navbarDescMap[path] = undefined + } + + return computed(() => navbarDescMap[path]) +} + +export function initNavbarDesc(desc: string) { + navbarDescMap[getCurrentRoutePath()] = desc || 'Ciallo' +} diff --git a/app/src/utils/lifecycleBatch.ts b/app/src/utils/lifecycleBatch.ts new file mode 100644 index 0000000..18a2e05 --- /dev/null +++ b/app/src/utils/lifecycleBatch.ts @@ -0,0 +1,123 @@ +import type { CardLifecycleRes } from '@/api/card' +import { lifecycleBatchStatus } from '@/api/card' + +const DEFAULT_POLL_INTERVAL = 1500 +const DEFAULT_MAX_ATTEMPTS = 8 +const RESTART_MAX_ATTEMPTS = 2 + +export function isLifecycleBatchFinished(status?: string) { + return status === 'success' || status === 'failed' || status === 'partial_success' +} + +export function lifecycleBatchStatusText(status?: string) { + switch (status) { + case 'success': + return '执行完成' + case 'failed': + return '执行失败' + case 'partial_success': + return '部分成功' + case 'processing': + return '处理中' + case 'pending': + return '待处理' + default: + return '已提交' + } +} + +export async function waitLifecycleBatchFinalStatus( + batchNo: string, + maxAttempts = DEFAULT_MAX_ATTEMPTS, + pollInterval = DEFAULT_POLL_INTERVAL, +) { + let latest: CardLifecycleRes | undefined + for (let attempt = 0; attempt < maxAttempts; attempt += 1) { + if (attempt > 0) + await delay(pollInterval) + latest = await lifecycleBatchStatus({ batchNo }) + if (isLifecycleBatchFinished(latest.status)) + break + } + return latest +} + +export async function showLifecycleBatchProgress( + initial: LifecycleBatchLike, + actionText: string, + options: LifecycleBatchProgressOptions = {}, +) { + const batchNo = initial.batchNo || '' + if (!batchNo) { + uni.showToast({ title: `${actionText}任务已提交`, icon: 'none' }) + return + } + + uni.showToast({ title: `${actionText}任务已提交`, icon: 'none' }) + let latest: LifecycleBatchLike = initial + try { + const maxAttempts = options.maxAttempts ?? (isRestartAction(actionText) ? RESTART_MAX_ATTEMPTS : DEFAULT_MAX_ATTEMPTS) + const pollInterval = options.pollInterval ?? DEFAULT_POLL_INTERVAL + latest = await waitLifecycleBatchFinalStatus(batchNo, maxAttempts, pollInterval) || initial + } + catch { + latest = initial + } + + const status = latest.status || initial.status + const content = buildLifecycleBatchContent(batchNo, latest, status) + uni.showModal({ + title: `${actionText}${lifecycleBatchTitleSuffix(status)}`, + content, + showCancel: false, + }) +} + +interface LifecycleBatchProgressOptions { + maxAttempts?: number + pollInterval?: number +} + +interface LifecycleBatchLike { + batchNo?: string + status?: string + quantity?: number + successCount?: number + failCount?: number + failReason?: string +} + +function isRestartAction(actionText: string) { + return actionText.includes('重启') +} + +function lifecycleBatchTitleSuffix(status?: string) { + switch (status) { + case 'success': + return '完成' + case 'failed': + return '失败' + case 'partial_success': + return '部分成功' + default: + return '处理中' + } +} + +function buildLifecycleBatchContent(batchNo: string, batch: LifecycleBatchLike, status?: string) { + const lines = [ + `批次号:${batchNo}`, + `当前状态:${lifecycleBatchStatusText(status)}`, + ] + if (typeof batch.quantity === 'number' && batch.quantity > 0) + lines.push(`处理进度:${batch.successCount || 0} 成功 / ${batch.failCount || 0} 失败 / ${batch.quantity} 总数`) + if (batch.failReason) + lines.push(`失败原因:${batch.failReason}`) + if (!isLifecycleBatchFinished(status)) + lines.push('后台仍在执行,请稍后刷新卡片状态') + return lines.join('\n') +} + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/app/src/utils/permission.test.ts b/app/src/utils/permission.test.ts new file mode 100644 index 0000000..40539fc --- /dev/null +++ b/app/src/utils/permission.test.ts @@ -0,0 +1,383 @@ +import { describe, expect, it } from 'vitest' +import { hasAnyFeature, hasFeature, hasRouteAccess, hasUserPermission } from './permission' +import { getAccountRoleLabel } from './userIdentity' + +describe('mini app console permission mapping', () => { + it('maps console card list permission to mini app card features', () => { + const userInfo = { + appAccess: { + configured: true, + permissions: ['cpm:iot:tSimCardInfo:list'], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(userInfo, 'card:list')).toBe(true) + expect(hasAnyFeature(userInfo, ['warn:view'])).toBe(false) + expect(hasFeature(userInfo, 'card:renew')).toBe(false) + }) + + it('grants card list to terminal users without granting it to enterprise users', () => { + const terminalUser = { + userId: 12, + userType: 'app_user', + appAccess: { + configured: true, + permissions: [], + features: [], + routes: [], + menus: [], + }, + } + const enterpriseUser = { + userId: 13, + userType: 'sys_user', + appAccess: { + configured: true, + permissions: [], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(terminalUser, 'card:list')).toBe(true) + expect(hasFeature(enterpriseUser, 'card:list')).toBe(false) + }) + + it('trusts backend terminal identity over legacy user type', () => { + const terminalUser = { + userId: 14, + userType: 'sys_user', + isTerminal: true, + appAccess: { + configured: true, + permissions: [], + features: [], + routes: [], + menus: [], + }, + } + const enterpriseUser = { + userId: 15, + userType: 'app_user', + isTerminal: false, + appAccess: { + configured: true, + permissions: [], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(terminalUser, 'card:list')).toBe(true) + expect(hasFeature(enterpriseUser, 'card:list')).toBe(false) + }) + + it('keeps terminal account label when bound role names look like enterprise roles', () => { + expect(getAccountRoleLabel({ + userType: 'sys_user', + isTerminal: true, + role: '终端用户', + roleNames: ['企业客户'], + })).toBe('终端用户') + }) + + it('keeps operation permissions separated from view permissions', () => { + const userInfo = { + appAccess: { + configured: true, + permissions: ['cpm:iot:tSimCardRenew:batch:confirm', 'cpm:iot:tSimCardInfo:edit'], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(userInfo, 'card:renew')).toBe(true) + expect(hasFeature(userInfo, 'card:stop')).toBe(false) + expect(hasFeature(userInfo, 'card:start')).toBe(false) + expect(hasFeature(userInfo, 'card:restart')).toBe(false) + }) + + it('requires submit permissions for renew and package change app entries', () => { + const prepareOnlyUser = { + appAccess: { + configured: true, + permissions: [ + 'cpm:iot:tSimCardRenew:input:prepare', + 'cpm:iot:tSimCardChange:input:prepare', + 'cpm:iot:tSimCardBindPkg:getChangePkg', + ], + features: [], + routes: [], + menus: [], + }, + } + const submitUser = { + appAccess: { + configured: true, + permissions: [ + 'cpm:iot:tSimCardRenew:batch:confirm', + 'cpm:iot:tSimCardChange:batch:submit', + ], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(prepareOnlyUser, 'card:renew')).toBe(false) + expect(hasFeature(prepareOnlyUser, 'card:change')).toBe(false) + expect(hasFeature(prepareOnlyUser, 'card:renew:submit')).toBe(false) + expect(hasFeature(prepareOnlyUser, 'card:change:submit')).toBe(false) + expect(hasFeature(submitUser, 'card:renew')).toBe(true) + expect(hasFeature(submitUser, 'card:change')).toBe(true) + expect(hasFeature(submitUser, 'card:renew:submit')).toBe(true) + expect(hasFeature(submitUser, 'card:change:submit')).toBe(true) + }) + + it('maps console restart permission to card restart feature', () => { + const userInfo = { + appAccess: { + configured: true, + permissions: ['cpm:iot:tSimCardRestart:input:submit'], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(userInfo, 'card:restart')).toBe(true) + }) + + it('maps console diagnosis permissions to mini app diagnosis feature', () => { + const userInfo = { + appAccess: { + configured: true, + permissions: [ + 'cpm:iot:tSimdiagnose:info', + 'cpm:iot:tSimdiagnose:flow', + 'cpm:iot:tSimCardInfo:querySimSession', + ], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(userInfo, 'card:diagnosis')).toBe(true) + expect(hasUserPermission(userInfo, 'diagnosis')).toBe(true) + }) + + it('maps the SIM status query permission to the diagnosis feature', () => { + const userInfo = { + appAccess: { + configured: true, + permissions: ['cpm:iot:tSimCardInfo:querySimStatus'], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(userInfo, 'card:diagnosis')).toBe(true) + expect(hasUserPermission(userInfo, 'diagnosis')).toBe(true) + }) + + it('maps package and order view permissions to corresponding app features', () => { + const userInfo = { + appAccess: { + configured: true, + permissions: [ + 'cpm:iot:tSimCardBindPkg:list', + 'cpm:common:tBalanceDetail:view', + ], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(userInfo, 'package:view')).toBe(true) + expect(hasFeature(userInfo, 'card:addon')).toBe(true) + expect(hasFeature(userInfo, 'order:detail')).toBe(true) + expect(hasFeature(userInfo, 'order:list')).toBe(false) + }) + + it('allows order detail from list permission like backend route permissions', () => { + const userInfo = { + appAccess: { + configured: true, + permissions: ['cpm:common:tBalanceDetail:list'], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(userInfo, 'order:list')).toBe(true) + expect(hasFeature(userInfo, 'order:detail')).toBe(true) + }) + + it('allows work order entry when user can either list or create work orders', () => { + const listUser = { + appAccess: { + configured: true, + permissions: ['cpm:gen:tWoMain:list'], + features: [], + routes: [], + menus: [], + }, + } + const createUser = { + appAccess: { + configured: true, + permissions: ['cpm:gen:tWoMain:edit'], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(listUser, 'workorder:list')).toBe(true) + expect(hasFeature(createUser, 'workorder:create')).toBe(true) + }) + + it('accepts menu permissions stored with duplicated cpm prefix', () => { + const userInfo = { + appAccess: { + configured: true, + permissions: ['cpm:cpm:iot:tSimCardStart:input:submit'], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(userInfo, 'card:start')).toBe(true) + }) + + it('requires dedicated console permission for card allocation', () => { + const legacyUser = { + appAccess: { + configured: true, + permissions: ['cpm:iot:tSimCardInfo:edit', 'cpm:agent:dept:list'], + features: [], + routes: [], + menus: [], + }, + } + const prepareUser = { + appAccess: { + configured: true, + permissions: ['cpm:iot:tSimCardAllocate:input:prepare'], + features: [], + routes: [], + menus: [], + }, + } + const submitUser = { + appAccess: { + configured: true, + permissions: ['cpm:iot:tSimCardAllocate:batch:submit'], + features: [], + routes: [], + menus: [], + }, + } + const fullUser = { + appAccess: { + configured: true, + permissions: [ + 'cpm:iot:tSimCardAllocate:targets', + 'cpm:iot:tSimCardAllocate:input:prepare', + 'cpm:iot:tSimCardAllocate:batch:submit', + ], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(legacyUser, 'card:allocate')).toBe(false) + expect(hasFeature(prepareUser, 'card:allocate')).toBe(false) + expect(hasFeature(submitUser, 'card:allocate')).toBe(false) + expect(hasFeature(fullUser, 'card:allocate')).toBe(true) + }) + + it('does not treat legacy app permissions as configured console grants', () => { + const userInfo = { + appAccess: { + configured: true, + permissions: ['cpm:app:card:list', 'app:card:renew'], + features: [], + routes: [], + menus: [], + }, + } + + expect(hasFeature(userInfo, 'card:list')).toBe(false) + expect(hasFeature(userInfo, 'card:renew')).toBe(false) + }) + + it('respects explicit unconfigured app access flag instead of current-user fallback permission', () => { + const userInfo = { + userId: 12, + appAccess: { + configured: false, + permissions: ['cpc:current:user'], + features: ['card:bind'], + routes: [], + menus: [], + }, + } + + expect(hasFeature(userInfo, 'card:list')).toBe(true) + }) + + it('does not block logged-in users before app permission codes are loaded', () => { + const userInfo = { + userId: 12, + username: 'terminal-user', + accessCodes: [], + } + + expect(hasFeature(userInfo, 'card:list')).toBe(true) + }) + + it('does not block logged-in users without userId before app permission codes are loaded', () => { + const userInfo = { + username: '18800004697', + phone: '18800004697', + accessCodes: [], + } + + expect(hasFeature(userInfo, 'card:detail')).toBe(true) + expect(hasFeature(userInfo, 'card:renew')).toBe(true) + }) + + it('does not block protected routes before user identity is restored', () => { + const userInfo = { + userId: -1, + username: '', + accessCodes: [], + } + + expect(hasRouteAccess(userInfo, '/pages/business/views/order/index', 'order:list')).toBe(true) + }) + + it('does not treat current-user permission as configured business permissions', () => { + const userInfo = { + userId: 12, + username: 'terminal-user', + accessCodes: ['cpc:current:user'], + } + + expect(hasFeature(userInfo, 'card:list')).toBe(true) + expect(hasFeature(userInfo, 'card:renew')).toBe(true) + }) +}) diff --git a/app/src/utils/permission.ts b/app/src/utils/permission.ts new file mode 100644 index 0000000..dcce954 --- /dev/null +++ b/app/src/utils/permission.ts @@ -0,0 +1,580 @@ +import type { IUserInfoRes } from '@/api/types/login' +import { isTerminalUserInfo } from './userIdentity' + +export type PermissionMatchMode = 'any' | 'all' + +export type AppPermissionAction + = | 'addon' + | 'allocate' + | 'customer' + | 'diagnosis' + | 'dataStart' + | 'dataStop' + | 'renew' + | 'renewSubmit' + | 'restart' + | 'stop' + | 'start' + | 'change' + | 'changeSubmit' + | 'order' + | 'warning' + | 'workOrder' + | 'workOrderCreate' + +export type AppFeatureInput = string | readonly string[] + +interface UserAppAccessInfo { + configured?: boolean + permissions?: readonly string[] + features?: readonly string[] + routes?: readonly string[] + menus?: readonly string[] +} + +interface UserInfoWithAppAccess extends Partial { + appAccess?: UserAppAccessInfo | null +} + +interface FeatureRule { + feature: string + permissions: readonly string[] +} + +export const RENEW_PERMISSION_CODES = [ + 'cpm:iot:tSimCardRenew:input:prepare', + 'cpm:iot:tSimCardRenew:batch:confirm', + 'cpm:iot:tSimCardRenew:prepare:result', +] as const + +const PERMISSION_CODE_MAP: Record = { + addon: [ + 'cpm:iot:tSimCardBindPkg:list', + 'cpm:iot:tSimFlowPackageInfo:view', + 'cpm:iot:tSimCardBindPkg:getPkgInfo', + ], + allocate: [ + 'cpm:iot:tSimCardAllocate:targets&cpm:iot:tSimCardAllocate:input:prepare&cpm:iot:tSimCardAllocate:batch:submit', + ], + customer: ['cpm:agent:dept:list', 'cpm:agent:tAgentEnterprise:list'], + diagnosis: [ + 'cpm:iot:tSimdiagnose:info', + 'cpm:iot:tSimdiagnose:flow', + 'cpm:iot:tSimCardInfo:querySimSession', + 'cpm:iot:tSimCardInfo:querySimStatus', + ], + dataStart: ['cpm:iot:tSimCardDataStart:input:submit'], + dataStop: ['cpm:iot:tSimCardDataStop:input:submit'], + renew: ['cpm:iot:tSimCardRenew:batch:confirm'], + renewSubmit: ['cpm:iot:tSimCardRenew:batch:confirm'], + restart: ['cpm:iot:tSimCardRestart:input:submit'], + stop: ['cpm:iot:tSimCardStop:input:submit'], + start: ['cpm:iot:tSimCardStart:input:submit'], + change: ['cpm:iot:tSimCardChange:batch:submit'], + changeSubmit: ['cpm:iot:tSimCardChange:batch:submit'], + order: [ + 'cpm:common:tBalanceDetail:list', + 'cpm:common:tFinanceApply:list', + 'cpm:gen:tFinanceApply:list', + ], + warning: [ + 'cpm:iot:simWarningConfig:list', + 'cpm:iot:simWarningConfig:view', + 'cpm:iot:simWarningConfig:save', + 'cpm:iot:tSimWarningConfig:input:submit', + 'cpm:iot:tSimWarningConfig:range:submit', + 'cpm:iot:tSimWarningConfig:excel:submit', + ], + workOrder: ['cpm:gen:tWoMain:list'], + workOrderCreate: ['cpm:gen:tWoMain:edit'], +} + +const ACTION_FEATURE_MAP: Record = { + addon: 'card:addon', + allocate: 'card:allocate', + customer: 'customer:list', + diagnosis: 'card:diagnosis', + dataStart: 'card:dataStart', + dataStop: 'card:dataStop', + renew: 'card:renew', + renewSubmit: 'card:renew:submit', + restart: 'card:restart', + stop: 'card:stop', + start: 'card:start', + change: 'card:change', + changeSubmit: 'card:change:submit', + order: 'order:list', + warning: 'warn:view', + workOrder: 'workorder:list', + workOrderCreate: 'workorder:create', +} + +const FEATURE_RULES: FeatureRule[] = [ + { + feature: 'card:list', + permissions: ['cpa:terminal:user', 'cpm:iot:tSimCardInfo:list'], + }, + { + feature: 'card:detail', + permissions: ['cpm:iot:tSimCardInfo:view', 'cpm:iot:tSimCardInfo:list'], + }, + { + feature: 'package:view', + permissions: [ + 'cpm:iot:tSimCardBindPkg:list', + 'cpm:iot:tSimFlowPackageInfo:list', + 'cpm:iot:tSimFlowPackageInfo:view', + 'cpm:iot:tSimCardBindPkg:getPkgInfo', + ], + }, + { + feature: 'card:bind', + permissions: ['cpc:current:user'], + }, + { + feature: 'card:renew', + permissions: ['cpm:iot:tSimCardRenew:batch:confirm'], + }, + { + feature: 'card:renew:submit', + permissions: ['cpm:iot:tSimCardRenew:batch:confirm'], + }, + { + feature: 'card:addon', + permissions: [ + 'cpm:iot:tSimCardBindPkg:list', + 'cpm:iot:tSimFlowPackageInfo:view', + 'cpm:iot:tSimCardBindPkg:getPkgInfo', + ], + }, + { + feature: 'card:change', + permissions: ['cpm:iot:tSimCardChange:batch:submit'], + }, + { + feature: 'card:change:submit', + permissions: ['cpm:iot:tSimCardChange:batch:submit'], + }, + { + feature: 'card:allocate', + permissions: [ + 'cpm:iot:tSimCardAllocate:targets&cpm:iot:tSimCardAllocate:input:prepare&cpm:iot:tSimCardAllocate:batch:submit', + ], + }, + { + feature: 'card:stop', + permissions: ['cpm:iot:tSimCardStop:input:submit'], + }, + { + feature: 'card:start', + permissions: ['cpm:iot:tSimCardStart:input:submit'], + }, + { + feature: 'card:restart', + permissions: ['cpm:iot:tSimCardRestart:input:submit'], + }, + { + feature: 'card:dataStart', + permissions: ['cpm:iot:tSimCardDataStart:input:submit'], + }, + { + feature: 'card:dataStop', + permissions: ['cpm:iot:tSimCardDataStop:input:submit'], + }, + { + feature: 'card:diagnosis', + permissions: [ + 'cpm:iot:tSimdiagnose:info', + 'cpm:iot:tSimdiagnose:flow', + 'cpm:iot:tSimCardInfo:querySimSession', + 'cpm:iot:tSimCardInfo:querySimStatus', + ], + }, + { + feature: 'warn:view', + permissions: [ + 'cpm:iot:simWarningConfig:list', + 'cpm:iot:simWarningConfig:view', + 'cpm:iot:simWarningConfig:save', + 'cpm:iot:tSimWarningConfig:input:submit', + 'cpm:iot:tSimWarningConfig:range:submit', + 'cpm:iot:tSimWarningConfig:excel:submit', + ], + }, + { + feature: 'order:list', + permissions: [ + 'cpm:common:tBalanceDetail:list', + 'cpm:common:tFinanceApply:list', + 'cpm:gen:tFinanceApply:list', + ], + }, + { + feature: 'order:detail', + permissions: [ + 'cpm:common:tBalanceDetail:view', + 'cpm:common:tBalanceDetail:list', + 'cpm:common:tFinanceApply:view', + 'cpm:common:tFinanceApply:list', + 'cpm:gen:tFinanceApply:list', + ], + }, + { + feature: 'customer:list', + permissions: ['cpm:agent:dept:list', 'cpm:agent:tAgentEnterprise:list'], + }, + { + feature: 'customer:detail', + permissions: ['cpm:agent:tAgentEnterprise:view', 'cpm:agent:tAgentEnterprise:list'], + }, + { + feature: 'message:view', + permissions: ['cpc:current:user', 'cpm:system:notice:list'], + }, + { + feature: 'workorder:list', + permissions: ['cpm:gen:tWoMain:list'], + }, + { + feature: 'workorder:create', + permissions: ['cpm:gen:tWoMain:edit'], + }, + { + feature: 'workorder:detail', + permissions: ['cpm:gen:tWoMain:view', 'cpm:gen:tWoMain:list'], + }, +] + +function normalizeCodes(codes?: readonly string[] | null) { + return Array.from( + new Set( + (codes || []) + .map(code => normalizePermissionCode(String(code || ''))) + .filter(Boolean), + ), + ) +} + +function normalizeFeatureTargets(target: AppFeatureInput) { + const values = Array.isArray(target) ? [...target] : [target] + return Array.from( + new Set( + values + .map(value => String(value || '').trim()) + .filter(Boolean), + ), + ) +} + +function normalizeFeatureList(values?: readonly string[] | null) { + return Array.from( + new Set( + (values || []) + .map(value => String(value || '').trim()) + .filter(Boolean), + ), + ) +} + +function getAppAccess(userInfo?: Partial | null) { + return (userInfo as UserInfoWithAppAccess | null | undefined)?.appAccess || null +} + +function isSuperAdminUser(userInfo?: Partial | null) { + return userInfo?.username === 'admin' || userInfo?.userId === 1 +} + +function hasLoadedUserIdentity(userInfo?: Partial | null) { + return Boolean( + (userInfo?.userId && Number(userInfo.userId) > 0) + || userInfo?.username + || userInfo?.phone + || userInfo?.nickName + || userInfo?.nickname, + ) +} + +function normalizePermissionCode(value: string) { + value = String(value || '').trim() + if (!value) { + return '' + } + while (value.startsWith('cpm:cpm:')) { + value = value.slice(4) + } + if ( + value === '*' + || value === 'cpc:current:user' + || value.startsWith('cpm:') + || value.startsWith('cpa:') + || value.startsWith('cpr:') + || value.startsWith('cpd:') + || value.startsWith('cpp:') + || value.startsWith('cpu:') + ) { + return value + } + return `cpm:${value}` +} + +function normalizePermissionExpression(value: string) { + const parts = permissionExpressionParts(value) + if (parts.length === 0) { + return '' + } + return parts.join('&') +} + +function permissionExpressionParts(value: string) { + const rawParts = String(value || '').split('&') + const parts: string[] = [] + for (const part of rawParts) { + const normalized = normalizePermissionCode(part) + if (normalized && !parts.includes(normalized)) { + parts.push(normalized) + } + } + return parts +} + +function matchesAccessCode(pattern: string, code: string) { + pattern = normalizePermissionCode(pattern) + code = normalizePermissionCode(code) + if (!pattern || !code) { + return false + } + if (pattern === code || pattern === '*' || pattern === 'cpm:*') { + return true + } + if (pattern.endsWith('*')) { + return code.startsWith(pattern.slice(0, -1)) + } + if (code.endsWith('*')) { + return pattern.startsWith(code.slice(0, -1)) + } + return false +} + +function matchesPermissionExpression(accessCodes: readonly string[] | null | undefined, expression: string) { + const parts = permissionExpressionParts(expression) + if (parts.length === 0) { + return false + } + const codes = normalizeCodes(accessCodes) + for (const part of parts) { + let matched = false + for (const accessCode of codes) { + if (matchesAccessCode(accessCode, part)) { + matched = true + break + } + } + if (!matched) { + return false + } + } + return true +} + +function getEffectiveAccessCodes(userInfo?: Partial | null) { + const appAccess = getAppAccess(userInfo) + return normalizeCodes([ + ...(userInfo?.accessCodes || []), + ...(appAccess?.permissions || []), + ]) +} + +function hasMenuPermissionSignal(codes?: readonly string[] | null) { + return normalizeCodes(codes).some(code => + code === '*' + || code === 'cpm:*' + || code.startsWith('cpm:'), + ) +} + +export function shouldApplyAppPermissionFilter(userInfo?: Partial | null) { + if (!userInfo) { + return false + } + + const appAccess = getAppAccess(userInfo) + if (appAccess?.configured === false) { + return false + } + if (appAccess?.configured === true) { + return true + } + + if (hasMenuPermissionSignal(userInfo?.accessCodes || [])) { + return true + } + + return hasMenuPermissionSignal(appAccess?.permissions || []) + || normalizeFeatureList(appAccess?.features || []).length > 0 +} + +function getGrantedFeatures(userInfo?: Partial | null) { + const appAccess = getAppAccess(userInfo) + const configured = appAccess?.configured + const granted = new Set() + + const directFeatures = normalizeFeatureList(appAccess?.features || []) + for (const feature of directFeatures) { + granted.add(feature) + } + + if (configured === false && getEffectiveAccessCodes(userInfo).includes('cpc:current:user')) { + return ['*'] + } + + const accessCodes = getEffectiveAccessCodes(userInfo) + for (const rule of FEATURE_RULES) { + if (rule.permissions.some(permission => matchesPermissionExpression(accessCodes, permission))) { + granted.add(rule.feature) + } + } + + if (isTerminalUserInfo(userInfo)) { + granted.add('card:list') + } + + return Array.from(granted) +} + +function shouldBypassAppPermissionFilter(userInfo?: Partial | null) { + const hasLoginUser = Boolean( + (userInfo?.userId && userInfo.userId > 0) + || userInfo?.username + || userInfo?.phone + || userInfo?.nickName + || userInfo?.nickname, + ) + return hasLoginUser && !shouldApplyAppPermissionFilter(userInfo) +} + +function hasGrantedFeature( + userInfo: Partial | null | undefined, + target?: AppFeatureInput, + mode: PermissionMatchMode = 'any', +) { + const targets = normalizeFeatureTargets(target || []) + if (targets.length === 0) { + return true + } + + if (shouldBypassAppPermissionFilter(userInfo)) { + return true + } + + const appAccess = getAppAccess(userInfo) + if (appAccess?.configured === false && getEffectiveAccessCodes(userInfo).includes('cpc:current:user')) { + return true + } + + const granted = getGrantedFeatures(userInfo) + if (granted.includes('*')) { + return true + } + if (mode === 'all') { + return targets.every(feature => granted.includes(feature)) + } + return targets.some(feature => granted.includes(feature)) +} + +export function hasAccessCode( + accessCodes?: readonly string[] | null, + target?: string | readonly string[], + mode: PermissionMatchMode = 'any', +) { + const currentCodes = normalizeCodes(accessCodes) + const targetValues = Array.isArray(target) ? target : [target || ''] + const targetCodes = normalizeCodes(targetValues) + if (targetCodes.length === 0) { + return true + } + if (currentCodes.includes('*')) { + return true + } + if (currentCodes.length === 0) { + return false + } + + if (mode === 'all') { + return targetCodes.every(code => currentCodes.some(current => matchesAccessCode(current, code))) + } + return targetCodes.some(code => currentCodes.some(current => matchesAccessCode(current, code))) +} + +export function hasUserPermission( + userInfo: Partial | null | undefined, + action: AppPermissionAction, + mode: PermissionMatchMode = 'any', +) { + if (isSuperAdminUser(userInfo)) { + return true + } + if (shouldBypassAppPermissionFilter(userInfo)) { + return true + } + + const appAccess = getAppAccess(userInfo) + const grantedCodes = getEffectiveAccessCodes(userInfo) + const permissionCandidates = PERMISSION_CODE_MAP[action] || [] + const featureName = ACTION_FEATURE_MAP[action] + + if (appAccess?.configured === false && grantedCodes.includes('cpc:current:user')) { + return true + } + + if (featureName && hasGrantedFeature(userInfo, featureName, mode)) { + return true + } + + if (mode === 'all') { + return permissionCandidates.every(permission => matchesPermissionExpression(grantedCodes, permission)) + } + return permissionCandidates.some(permission => matchesPermissionExpression(grantedCodes, permission)) +} + +export function hasAnyFeature( + userInfo: Partial | null | undefined, + target?: AppFeatureInput, + mode: PermissionMatchMode = 'any', +) { + if (isSuperAdminUser(userInfo)) { + return true + } + if (shouldBypassAppPermissionFilter(userInfo)) { + return true + } + return hasGrantedFeature(userInfo, target, mode) +} + +export function hasFeature( + userInfo: Partial | null | undefined, + target?: AppFeatureInput, +) { + return hasAnyFeature(userInfo, target) +} + +export function hasRouteAccess( + userInfo: Partial | null | undefined, + _path: string, + target?: AppFeatureInput, +) { + if (!hasLoadedUserIdentity(userInfo)) { + return true + } + if (shouldBypassAppPermissionFilter(userInfo)) { + return true + } + return hasAnyFeature(userInfo, target) +} + +export function canUseRenewPermission(userInfo?: Partial | null) { + return hasUserPermission(userInfo, 'renew') +} + +export function canSubmitRenewPermission(userInfo?: Partial | null) { + return hasUserPermission(userInfo, 'renewSubmit') +} diff --git a/app/src/utils/scanPageFilter.ts b/app/src/utils/scanPageFilter.ts new file mode 100644 index 0000000..fba4d82 --- /dev/null +++ b/app/src/utils/scanPageFilter.ts @@ -0,0 +1,16 @@ +export default function scanPageFilter(ctx, inKey) { + if (!(inKey in ctx)) + return + + const keysToRemove = [] + for (const key of ctx[inKey].keys()) { + // 只有以 index.vue 结尾的页面会被保留 + if (!key.endsWith('index.vue')) { + keysToRemove.push(key) + } + } + + keysToRemove.forEach((key) => { + ctx[inKey].delete(key) + }) +} diff --git a/app/src/utils/systemInfo.ts b/app/src/utils/systemInfo.ts new file mode 100644 index 0000000..a60f82e --- /dev/null +++ b/app/src/utils/systemInfo.ts @@ -0,0 +1,38 @@ +/* eslint-disable import/no-mutable-exports */ +// 获取屏幕边界到安全区域距离 +let systemInfo +let safeAreaInsets + +// #ifdef MP-WEIXIN +// 微信小程序使用新的API +systemInfo = uni.getWindowInfo() +safeAreaInsets = systemInfo.safeArea + ? { + top: systemInfo.safeArea.top, + right: systemInfo.windowWidth - systemInfo.safeArea.right, + bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom, + left: systemInfo.safeArea.left, + } + : null +// #endif + +// #ifndef MP-WEIXIN +// 其他平台继续使用uni API +systemInfo = uni.getSystemInfoSync() +safeAreaInsets = systemInfo.safeAreaInsets +// #endif + +console.log('systemInfo', systemInfo) +// 微信里面打印 +// pixelRatio: 3 +// safeArea: {top: 47, left: 0, right: 390, bottom: 810, width: 390, …} +// safeAreaInsets: {top: 47, left: 0, right: 0, bottom: 34} +// screenHeight: 844 +// screenTop: 91 +// screenWidth: 390 +// statusBarHeight: 47 +// windowBottom: 0 +// windowHeight: 753 +// windowTop: 0 +// windowWidth: 390 +export { safeAreaInsets, systemInfo } diff --git a/app/src/utils/toLoginPage.ts b/app/src/utils/toLoginPage.ts new file mode 100644 index 0000000..de2f8dd --- /dev/null +++ b/app/src/utils/toLoginPage.ts @@ -0,0 +1,44 @@ +import { getLastPage } from '@/utils' +import { debounce } from '@/utils/debounce' + +interface ToLoginPageOptions { + /** + * 跳转模式, uni.navigateTo | uni.reLaunch + * @default 'navigateTo' + */ + mode?: 'navigateTo' | 'reLaunch' + /** + * 查询参数 + * @example '?redirect=/pages/home/index' + */ + queryString?: string +} + +// TODO: 自己增加登录页 +const LOGIN_PAGE = '/pages/login/index' + +/** + * 跳转到登录页, 带防抖处理 + * + * 如果要立即跳转,不做延时,可以使用 `toLoginPage.flush()` 方法 + */ +export const toLoginPage = debounce((options: ToLoginPageOptions = {}) => { + const { mode = 'navigateTo', queryString = '' } = options + + const url = `${LOGIN_PAGE}${queryString}` + + // 获取当前页面路径 + const currentPage = getLastPage() + const currentPath = `/${currentPage.route}` + // 如果已经在登录页,则不跳转 + if (currentPath === LOGIN_PAGE) { + return + } + + if (mode === 'navigateTo') { + uni.navigateTo({ url }) + } + else { + uni.reLaunch({ url }) + } +}, 500) diff --git a/app/src/utils/updateManager.wx.ts b/app/src/utils/updateManager.wx.ts new file mode 100644 index 0000000..20b8b50 --- /dev/null +++ b/app/src/utils/updateManager.wx.ts @@ -0,0 +1,29 @@ +export default () => { + if (!wx.canIUse('getUpdateManager')) { + return + } + + const updateManager = wx.getUpdateManager() + + updateManager.onCheckForUpdate((res) => { + // 请求完新版本信息的回调 + console.log('版本信息', res) + }) + + updateManager.onUpdateReady(() => { + wx.showModal({ + title: '更新提示', + content: '新版本已经准备好,是否重启应用?', + success(res) { + if (res.confirm) { + // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 + updateManager.applyUpdate() + } + }, + }) + }) + + updateManager.onUpdateFailed(() => { + // 新版本下载失败 + }) +} diff --git a/app/src/utils/uploadFile.ts b/app/src/utils/uploadFile.ts new file mode 100644 index 0000000..bec8d98 --- /dev/null +++ b/app/src/utils/uploadFile.ts @@ -0,0 +1,327 @@ +/** + * 文件上传钩子函数使用示例 + * @example + * const { loading, error, data, progress, run } = useUpload( + * uploadUrl, + * {}, + * { + * maxSize: 5, // 最大5MB + * sourceType: ['album'], // 仅支持从相册选择 + * onProgress: (p) => console.log(`上传进度:${p}%`), + * onSuccess: (res) => console.log('上传成功', res), + * onError: (err) => console.error('上传失败', err), + * }, + * ) + */ + +import { getEnvBaseUrl } from '@/utils' + +/** + * 上传文件的URL配置 + */ +export const uploadFileUrl = { + /** 用户头像上传地址 */ + USER_AVATAR: `${getEnvBaseUrl()}/user/avatar`, +} + +/** + * 通用文件上传函数(支持直接传入文件路径) + * @param url 上传地址 + * @param filePath 本地文件路径 + * @param formData 额外表单数据 + * @param options 上传选项 + */ +export function useFileUpload(url: string, filePath: string, formData: Record = {}, options: Omit = {}) { + return useUpload( + url, + formData, + { + ...options, + sourceType: ['album'], + sizeType: ['original'], + }, + filePath, + ) +} + +export interface UploadOptions { + /** 最大可选择的图片数量,默认为1 */ + count?: number + /** 所选的图片的尺寸,original-原图,compressed-压缩图 */ + sizeType?: Array<'original' | 'compressed'> + /** 选择图片的来源,album-相册,camera-相机 */ + sourceType?: Array<'album' | 'camera'> + /** 文件大小限制,单位:MB */ + maxSize?: number // + /** 上传进度回调函数 */ + onProgress?: (progress: number) => void + /** 上传成功回调函数 */ + onSuccess?: (res: Record) => void + /** 上传失败回调函数 */ + onError?: (err: Error | UniApp.GeneralCallbackResult) => void + /** 上传完成回调函数(无论成功失败) */ + onComplete?: () => void +} + +/** + * 文件上传钩子函数 + * @template T 上传成功后返回的数据类型 + * @param url 上传地址 + * @param formData 额外的表单数据 + * @param options 上传选项 + * @returns 上传状态和控制对象 + */ +export function useUpload(url: string, formData: Record = {}, options: UploadOptions = {}, + /** 直接传入文件路径,跳过选择器 */ + directFilePath?: string) { + /** 上传中状态 */ + const loading = ref(false) + /** 上传错误状态 */ + const error = ref(false) + /** 上传成功后的响应数据 */ + const data = ref() + /** 上传进度(0-100) */ + const progress = ref(0) + + /** 解构上传选项,设置默认值 */ + const { + /** 最大可选择的图片数量 */ + count = 1, + /** 所选的图片的尺寸 */ + sizeType = ['original', 'compressed'], + /** 选择图片的来源 */ + sourceType = ['album', 'camera'], + /** 文件大小限制(MB) */ + maxSize = 10, + /** 进度回调 */ + onProgress, + /** 成功回调 */ + onSuccess, + /** 失败回调 */ + onError, + /** 完成回调 */ + onComplete, + } = options + + /** + * 检查文件大小是否超过限制 + * @param size 文件大小(字节) + * @returns 是否通过检查 + */ + const checkFileSize = (size: number) => { + const sizeInMB = size / 1024 / 1024 + if (sizeInMB > maxSize) { + uni.showToast({ + title: `文件大小不能超过${maxSize}MB`, + icon: 'none', + }) + return false + } + return true + } + /** + * 触发文件选择和上传 + * 根据平台使用不同的选择器: + * - 微信小程序使用 chooseMedia + * - 其他平台使用 chooseImage + */ + const run = () => { + if (directFilePath) { + // 直接使用传入的文件路径 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: directFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + return + } + + // #ifdef MP-WEIXIN + // 微信小程序环境下使用 chooseMedia API + uni.chooseMedia({ + count, + mediaType: ['image'], // 仅支持图片类型 + sourceType, + success: (res) => { + const file = res.tempFiles[0] + // 检查文件大小是否符合限制 + if (!checkFileSize(file.size)) + return + + // 开始上传 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: file.tempFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + }, + fail: (err) => { + console.error('选择媒体文件失败:', err) + error.value = true + onError?.(err) + }, + }) + // #endif + + // #ifndef MP-WEIXIN + // 非微信小程序环境下使用 chooseImage API + uni.chooseImage({ + count, + sizeType, + sourceType, + success: (res) => { + console.log('选择图片成功:', res) + + // 开始上传 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: res.tempFilePaths[0], + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + }, + fail: (err) => { + console.error('选择图片失败:', err) + error.value = true + onError?.(err) + }, + }) + // #endif + } + + return { loading, error, data, progress, run } +} + +/** + * 文件上传选项接口 + * @template T 上传成功后返回的数据类型 + */ +interface UploadFileOptions { + /** 上传地址 */ + url: string + /** 临时文件路径 */ + tempFilePath: string + /** 额外的表单数据 */ + formData: Record + /** 上传成功后的响应数据 */ + data: Ref + /** 上传错误状态 */ + error: Ref + /** 上传中状态 */ + loading: Ref + /** 上传进度(0-100) */ + progress: Ref + /** 上传进度回调 */ + onProgress?: (progress: number) => void + /** 上传成功回调 */ + onSuccess?: (res: Record) => void + /** 上传失败回调 */ + onError?: (err: Error | UniApp.GeneralCallbackResult) => void + /** 上传完成回调 */ + onComplete?: () => void +} + +/** + * 执行文件上传 + * @template T 上传成功后返回的数据类型 + * @param options 上传选项 + */ +function uploadFile({ + url, + tempFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, +}: UploadFileOptions) { + try { + // 创建上传任务 + const uploadTask = uni.uploadFile({ + url, + filePath: tempFilePath, + name: 'file', // 文件对应的 key + formData, + header: { + // H5环境下不需要手动设置Content-Type,让浏览器自动处理multipart格式 + // #ifndef H5 + 'Content-Type': 'multipart/form-data', + // #endif + }, + // 确保文件名称合法 + success: (uploadFileRes) => { + console.log('上传文件成功:', uploadFileRes) + try { + // 解析响应数据 + const { data: _data } = JSON.parse(uploadFileRes.data) + // 上传成功 + data.value = _data as T + onSuccess?.(_data) + } + catch (err) { + // 响应解析错误 + console.error('解析上传响应失败:', err) + error.value = true + onError?.(new Error('上传响应解析失败')) + } + }, + fail: (err) => { + // 上传请求失败 + console.error('上传文件失败:', err) + error.value = true + onError?.(err) + }, + complete: () => { + // 无论成功失败都执行 + loading.value = false + onComplete?.() + }, + }) + + // 监听上传进度 + uploadTask.onProgressUpdate((res) => { + progress.value = res.progress + onProgress?.(res.progress) + }) + } + catch (err) { + // 创建上传任务失败 + console.error('创建上传任务失败:', err) + error.value = true + loading.value = false + onError?.(new Error('创建上传任务失败')) + } +} diff --git a/app/src/utils/userIdentity.ts b/app/src/utils/userIdentity.ts new file mode 100644 index 0000000..90c3a01 --- /dev/null +++ b/app/src/utils/userIdentity.ts @@ -0,0 +1,103 @@ +import type { IUserInfoRes } from '@/api/types/login' + +export const APP_TERMINAL_USER_TYPE = 'app_user' + +export type AppAccountWorkspace = 'terminal' | 'enterprise' | 'agent' | 'admin' + +const ADMIN_ROLE_PATTERN = /admin|管理员|超级管理员|平台管理/i +const AGENT_ROLE_PATTERN = /agent|代理商|渠道商|经销商/i + +function getRoleSignals(userInfo?: Partial | null) { + return [ + userInfo?.role, + userInfo?.roleName, + ...(userInfo?.roleNames || []), + ...(userInfo?.roles || []), + ] + .map(role => String(role || '').trim()) + .filter(Boolean) +} + +// 账户真实角色名称列表:优先使用后端返回的角色表名称(sys_role.role_name), +// 逐层回退到 roleName、role、再回退到 workspace 标签,保证任何情况下都有可读的身份标识。 +export function getAccountRoleNames(userInfo?: Partial | null): string[] { + const fromList = (userInfo?.roleNames || []) + .map(r => String(r || '').trim()) + .filter(Boolean) + if (fromList.length) + return fromList + if (userInfo?.roleName) + return [String(userInfo.roleName).trim()].filter(Boolean) + if (userInfo?.role) + return [String(userInfo.role).trim()].filter(Boolean) + return [] +} + +export function getAccountRoleLabel(userInfo?: Partial | null): string { + if (isTerminalUserInfo(userInfo)) + return getAccountWorkspaceLabel('terminal') + const names = getAccountRoleNames(userInfo) + if (names.length) + return names.join(' · ') + return getAccountWorkspaceLabel(getAccountWorkspace(userInfo)) +} + +function hasRoleSignal(userInfo: Partial | null | undefined, pattern: RegExp) { + return getRoleSignals(userInfo).some(role => pattern.test(role)) +} + +export function getUserDeptId(userInfo?: Partial | null) { + return Number(userInfo?.deptId || userInfo?.enterpriseProfile?.deptId || 0) +} + +export function isTerminalUserInfo(userInfo?: Partial | null) { + if (typeof userInfo?.isTerminal === 'boolean') + return userInfo.isTerminal + const userType = String(userInfo?.userType || '').trim() + if (userType) + return userType === APP_TERMINAL_USER_TYPE || userType === 'terminal' + return false +} + +export function isAdminUserInfo(userInfo?: Partial | null) { + if (!userInfo || isTerminalUserInfo(userInfo)) + return false + return Number(userInfo.userId || 0) === 1 + || String(userInfo.username || '').trim().toLowerCase() === 'admin' + || hasRoleSignal(userInfo, ADMIN_ROLE_PATTERN) +} + +export function isAgentUserInfo(userInfo?: Partial | null) { + if (!userInfo || isTerminalUserInfo(userInfo) || isAdminUserInfo(userInfo)) + return false + const userType = String(userInfo.userType || '').trim().toLowerCase() + return userType === 'agent' + || userType === 'distributor' + || hasRoleSignal(userInfo, AGENT_ROLE_PATTERN) +} + +export function isEnterpriseUserInfo(userInfo?: Partial | null) { + return !isTerminalUserInfo(userInfo) + && !isAgentUserInfo(userInfo) + && !isAdminUserInfo(userInfo) + && (Boolean(userInfo?.canEnterpriseBusiness) || getUserDeptId(userInfo) > 0) +} + +export function getAccountWorkspace(userInfo?: Partial | null): AppAccountWorkspace { + if (isAdminUserInfo(userInfo)) + return 'admin' + if (isTerminalUserInfo(userInfo)) + return 'terminal' + if (isAgentUserInfo(userInfo)) + return 'agent' + return 'enterprise' +} + +export function getAccountWorkspaceLabel(workspace: AppAccountWorkspace) { + return { + terminal: '终端用户', + enterprise: '企业用户', + agent: '代理商', + admin: '管理员', + }[workspace] +} diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..07d3342 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,57 @@ +{ + "compilerOptions": { + "composite": true, + "lib": [ + "esnext", + "dom" + ], + "baseUrl": ".", + "module": "esnext", + "moduleResolution": "bundler", + "paths": { + "@/*": [ + "./src/*" + ], + "@img/*": [ + "./src/static/*" + ] + }, + "resolveJsonModule": true, + "types": [ + "@dcloudio/types", + "@uni-helper/uni-types", + "@uni-helper/vite-plugin-uni-pages", + "miniprogram-api-typings", + "z-paging/types", + "./src/types/async-component.d.ts", + "./src/types/async-import.d.ts", + "./src/typings.d.ts", + "@wot-ui/ui/global" + ], + "allowJs": true, + "noImplicitThis": true, + "outDir": "dist", + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true + }, + "vueCompilerOptions": { + "plugins": [ + "@uni-helper/uni-types/volar-plugin" + ] + }, + "include": [ + "package.json", + "src/**/*.ts", + "src/**/*.js", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.jsx", + "src/**/*.vue", + "src/**/*.json" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/app/uno.config.ts b/app/uno.config.ts new file mode 100644 index 0000000..0d7998e --- /dev/null +++ b/app/uno.config.ts @@ -0,0 +1,202 @@ +import type { Preset } from 'unocss' +import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders' + +// https://www.npmjs.com/package/@uni-helper/unocss-preset-uni +import { presetUni } from '@uni-helper/unocss-preset-uni' +// @see https://unocss.dev/presets/legacy-compat +import { presetLegacyCompat } from '@unocss/preset-legacy-compat' +import { defineConfig, presetIcons, transformerDirectives, transformerVariantGroup } from 'unocss' + +export default defineConfig({ + presets: [ + presetUni({ + attributify: false, + }), + presetIcons({ + scale: 1.2, + warn: true, + extraProperties: { + 'display': 'inline-block', + 'vertical-align': 'middle', + }, + collections: { + // 注册本地 SVG 图标集合, 从本地文件系统加载图标 + // 在 './src/static/my-icons' 目录下的所有 svg 文件将被注册为图标, + // my-icons 是图标集合名称,使用 `i-my-icons-图标名` 调用 + 'my-icons': FileSystemIconLoader( + './src/static/my-icons', + // 可选的,你可以提供一个 transform 回调来更改每个图标 + (svg) => { + let svgStr = svg + + // 如果 SVG 文件未定义 `fill` 属性,则默认填充 `currentColor`, 这样图标颜色会继承文本颜色,方便在不同场景下适配 + svgStr = svgStr.includes('fill="') + ? svgStr + : svgStr.replace(/^ `rgb(255, 0, 0)` + // `rgba(255 0 0 / 0.5)` -> `rgba(255, 0, 0, 0.5)` + presetLegacyCompat({ + commaStyleColorFunction: true, + legacyColorSpace: true, // by QQ4群-量子蔷薇 + // @菲鸽 unocss 配置中,建议在 presetLegacyCompat 中添加 legacyColorSpace: true,以去除生成的颜色样式中的 in oklch 关键字,现在发现有些渐变色生成不符合预期 + }) as Preset, + ], + transformers: [ + // 启用指令功能:主要用于支持 @apply、@screen 和 theme() 等 CSS 指令 + transformerDirectives(), + // 启用 () 分组功能 + // 支持css class组合,eg: `
测试 unocss
` + transformerVariantGroup(), + ], + shortcuts: [ + { + 'center': 'flex justify-center items-center', + 'card-base': 'box-border bg-white rounded-10rpx border border-solid border-[#e2e8f0] shadow-card', + }, + ], + // 动态图标需要在这里配置,或者写在vue页面中注释掉 + safelist: [ + 'i-carbon-code', + 'i-carbon-home', + 'i-carbon-user', + 'i-carbon-user-profile', + 'i-carbon-user-follow', + 'i-carbon-building', + 'i-carbon-ibm-watson-language-translator', + 'i-carbon-menu', + 'i-carbon-renew', + 'i-carbon-reset', + 'i-carbon-restart', + 'i-carbon-pause-outline', + 'i-carbon-change-catalog', + 'i-carbon-package', + 'i-carbon-warning', + 'i-carbon-warning-alt', + 'i-carbon-time', + 'i-carbon-calendar', + 'i-carbon-camera', + 'i-carbon-chat', + 'i-carbon-stethoscope', + 'i-carbon-apps', + 'i-carbon-wallet', + 'i-carbon-receipt', + 'i-carbon-sim-card', + 'i-carbon-share-knowledge', + 'i-carbon-add-alt', + 'i-carbon-arrows-horizontal', + 'i-carbon-arrow-left', + 'i-carbon-chevron-right', + 'i-carbon-close', + 'i-carbon-copy', + 'i-carbon-customer-service', + 'i-carbon-data-volume', + 'i-carbon-document', + 'i-carbon-download', + 'i-carbon-filter', + 'i-carbon-flash', + 'i-carbon-information', + 'i-carbon-keyboard', + 'i-carbon-link', + 'i-carbon-locked', + 'i-carbon-overflow-menu-horizontal', + 'i-carbon-notification', + 'i-carbon-search', + 'i-carbon-security', + 'i-carbon-settings', + 'i-carbon-settings-adjust', + 'i-carbon-shopping-bag', + 'i-carbon-upload', + 'i-carbon-catalog', + 'i-carbon-chart-column', + 'i-carbon-checkmark', + 'i-carbon-checkmark-outline', + 'i-carbon-user-multiple', + 'i-my-icons-sparkle', + ], + rules: [ + [ + 'p-safe', + { + padding: + 'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)', + }, + ], + ['pt-safe', { 'padding-top': 'env(safe-area-inset-top)' }], + ['pb-safe', { 'padding-bottom': 'env(safe-area-inset-bottom)' }], + ], + theme: { + colors: { + /** 主题色(山东平台方案一),用法如: text-primary / bg-primary */ + 'primary': 'var(--wot-color-theme,#0b2bac)', + 'primary-light': '#2563eb', + 'indigo': '#4f46e5', + 'cyan': '#0ea5e9', + 'mint': '#14b8a6', + 'purple': '#a5b4fc', + 'rose': '#f43f5e', + 'success': '#16a34a', + 'warning': '#f59e0b', + /** 文字色 */ + 'ink': '#111827', + 'body': '#334155', + 'muted': '#64748b', + 'disabled': '#94a3b8', + /** 背景与描边 */ + 'canvas': '#f6f8fb', + 'canvas-soft': '#eff6ff', + 'input-bg': '#f8fafc', + 'line': '#e2e8f0', + }, + fontSize: { + /** 提供更小号的字体,用法如:text-2xs */ + '2xs': ['20rpx', '28rpx'], + '3xs': ['18rpx', '26rpx'], + }, + borderRadius: { + card: '16rpx', + pill: '999rpx', + }, + boxShadow: { + // 阴影使用中性低对比度,避免干扰信息层级 + 'card': '0 4rpx 16rpx rgba(15, 23, 42, 0.04)', + 'card-lg': '0 10rpx 24rpx rgba(15, 23, 42, 0.08)', + 'btn': '0 4rpx 10rpx rgba(11, 43, 172, 0.16)', + }, + }, + // windows 系统会报错:[plugin:unocss:transformers:pre] Cannot overwrite a zero-length range - use append Left or prependRight instead. + // 去掉下面的就正常了 + // content: { + // /** + // * 解决小程序报错 `./app.wxss(78:2814): unexpected unexpected at pos 5198` + // * 为什么同时使用include和exclude?虽然看起来多余,但同时配置两者是一种常见的 `防御性编程` 做法。 + // 1. 结构变化保障 : 如果未来项目结构发生变化,某些排除目录可能被移动到包含路径下,exclude配置可以确保它们仍被排除 + // 2. 明确性 : 明确列出要排除的目录使配置意图更加清晰 + // 3. 性能优化 : 避免处理不必要的文件,提高构建性能 + // 4. 防止冲突 : 排除第三方库和构建输出目录,避免潜在的CSS冲突 + // */ + // pipeline: { + // exclude: [ + // 'node_modules/**/*', + // 'public/**/*', + // 'dist/**/*', + // ], + // include: [ + // './src/**/*', + // ], + // }, + // }, +}) diff --git a/app/vite-plugins/README.md b/app/vite-plugins/README.md new file mode 100644 index 0000000..956a73e --- /dev/null +++ b/app/vite-plugins/README.md @@ -0,0 +1,236 @@ +# unibest原生插件资源复制插件 + +## 概述 + +`copy-native-resources.ts` 是一个专为 基于unibest框架的UniApp 项目设计的 Vite 插件,用于解决使用原生插件时打包后出现"插件找不到"的问题。该插件会在构建过程中自动将本地原生插件资源复制到正确的目标目录。 + +## 功能特性 + +- ✅ 自动复制原生插件资源到构建目录 +- ✅ 支持环境变量控制插件启用/禁用 +- ✅ 支持详细日志输出用于调试 +- ✅ 智能检测源目录是否存在 + +## 目录结构 + +根据 [UniApp 官方文档](https://uniapp.dcloud.net.cn/plugin/native-plugin.html#%E6%9C%AC%E5%9C%B0%E6%8F%92%E4%BB%B6-%E9%9D%9E%E5%86%85%E7%BD%AE%E5%8E%9F%E7%94%9F%E6%8F%92%E4%BB%B6),本地原生插件应存储在项目根目录的 `nativeplugins` 目录下: + +``` +项目根目录/ +├── nativeplugins/ # 原生插件存储目录(官方规范) +│ ├── HL-HHWUHFController/ # 示例:RFID 控制器插件 +│ │ ├── android/ # Android 平台资源 +│ │ │ ├── libs/ # Android 库文件 +│ │ │ └── res/ # Android 资源文件 +│ │ ├── ios/ # iOS 平台资源(如果有) +│ │ └── package.json # 插件配置文件 +│ └── 其他原生插件/ +├── src/ +├── vite-plugins/ +│ ├── copy-native-resources.ts # 本插件文件 +│ └── README.md # 本文档 +└── vite.config.ts +``` + +## 安装配置 + +### 1. 环境变量配置 + +在 `env/.env` 文件中添加以下配置: + +```bash +# 是否启用原生插件资源复制 +VITE_COPY_NATIVE_RES_ENABLE = true +``` + +### 2. Vite 配置 + +在 `vite.config.ts` 中引入并使用插件: + +```typescript +import { createCopyNativeResourcesPlugin } from './vite-plugins/copy-native-resources' + +export default defineConfig({ + plugins: [ + // 其他插件... + + // 原生插件资源复制插件 + createCopyNativeResourcesPlugin( + UNI_PLATFORM === 'app' && VITE_COPY_NATIVE_RES_ENABLE === 'true', + { + verbose: mode === 'development', // 开发模式显示详细日志 + }, + ), + + // 其他插件... + ], +}) +``` + +### 3. manifest.config.ts 配置 + +在 `manifest.config.ts` 中配置原生插件: + +```typescript +export default defineManifest({ + // 其他配置... + + 'app-plus': { + // 其他配置... + + // 原生插件配置 + nativePlugins: { + // RFID 控制器插件示例 + 'HL-HHWUHFController': { + __plugin_info__: { + name: 'HL-HHWUHFController', + description: 'RFID UHF 控制器插件', + platforms: 'Android', + url: '', + android_package_name: '', + ios_bundle_id: '', + isCloud: false, + bought: -1, + pid: '', + parameters: {} + } + } + } + } +}) +``` + +## 插件配置选项 + +```typescript +interface CopyNativeResourcesOptions { + /** 是否启用插件 */ + enable?: boolean + + /** + * 源目录路径,相对于项目根目录 + * 默认为 'nativeplugins',符合 UniApp 官方规范 + */ + sourceDir?: string + + /** + * 目标目录名称,构建后在 dist 目录中的文件夹名 + * 默认为 'nativeplugins',与源目录保持一致 + */ + targetDirName?: string + + /** 是否显示详细日志 */ + verbose?: boolean +} +``` + +## 使用示例 + +### 基础使用 + +```typescript +// 使用默认配置 +createCopyNativeResourcesPlugin(true) +``` + +### 自定义配置 + +```typescript +// 自定义配置 +createCopyNativeResourcesPlugin(true, { + sourceDir: 'nativeplugins', // 源目录 + targetDirName: 'nativeplugins', // 目标目录名 + verbose: true // 显示详细日志 +}) +``` + +### 条件启用 + +```typescript +// 仅在 app 平台且环境变量启用时生效 +createCopyNativeResourcesPlugin( + UNI_PLATFORM === 'app' && VITE_COPY_NATIVE_RES_ENABLE === 'true', + { verbose: mode === 'development' } +) +``` + +## 工作原理 + +1. **构建时机**:插件在 Vite 的 `writeBundle` 阶段执行 +2. **目录检测**:检查源目录 `nativeplugins` 是否存在 +3. **资源复制**:将整个 `nativeplugins` 目录复制到构建输出目录 +4. **路径处理**:自动处理不同平台的路径差异 +5. **日志输出**:根据配置显示复制过程的详细信息 + +## 构建输出结构 + +插件会将原生插件资源复制到以下位置: + +``` +dist/ +├── build/ +│ └── app/ +│ └── nativeplugins/ # 生产环境构建 +│ └── HL-HHWUHFController/ +└── dev/ + └── app/ + └── nativeplugins/ # 开发环境构建 + └── HL-HHWUHFController/ +``` + +## 常见问题 + +### Q: 为什么要使用这个插件? + +A: 目前使用unibest框架在打包时可能不会自动复制原生插件资源,导致运行时出现"插件找不到"的错误。此插件确保原生插件资源被正确复制到构建目录。 + +### Q: 插件不生效怎么办? + +A: 检查以下几点: +1. 确认 `nativeplugins` 目录存在且包含插件文件 +2. 确认环境变量 `VITE_COPY_NATIVE_RES_ENABLE` 设置为 `true` +3. 确认当前平台为 `app`(插件仅在 app 平台生效) +4. 开启 `verbose: true` 查看详细日志 + +### Q: 可以自定义源目录吗? + +A: 可以,但不推荐。UniApp 官方规范要求使用 `nativeplugins` 目录,自定义可能导致其他问题。 + +### Q: 支持哪些平台? + +A: 插件本身支持所有平台,但通常只在 `app` 平台(目前只测试了Android环境,iOS有条件的伙伴可以测试后反馈)使用原生插件。 + + +### Q: 产生权限冲突问题? + +A: 有伙伴反馈过接入的原生插件之前使用【Lastly1999】提交的版本初步解决了问题,但是又遇到两个新的问题: +- 导入的两个插件内的权限配置有版本冲突,在云打包的最后一步会报错,然后通过修改其中一个aar配置版本解决的。 +- 测试发现在android版本大于12的手机,获取相册权限后,打开相册看不到里面的照片,将两个插件删除就没问题 ,可以正常显示,不删除就会有问题,怀疑是插件的AndroidManifest.xml覆盖了项目内manifest.config.ts的安卓权限申请 +也欢迎其他有伙伴反馈,望能一起解决。 + +## 更新日志 + +### v1.0.0 +- 初始版本发布 +- 支持基础的原生插件资源复制功能 + +### v1.1.0 +- 更新为符合 UniApp 官方规范的 `nativeplugins` 目录结构 +- 修复 ESLint 警告 +- 增加详细的代码注释和文档 +- 优化错误处理和日志输出 + +## 技术支持 + +如果在使用过程中遇到问题,请检查: + +1. UniApp 官方文档:[本地插件配置](https://uniapp.dcloud.net.cn/plugin/native-plugin.html#%E6%9C%AC%E5%9C%B0%E6%8F%92%E4%BB%B6-%E9%9D%9E%E5%86%85%E7%BD%AE%E5%8E%9F%E7%94%9F%E6%8F%92%E4%BB%B6) +2. 插件配置是否正确 +3. 目录结构是否符合规范 +4. 环境变量是否正确设置 + +## 特别声明及感谢 + +- 感谢【Lastly1999】,此插件时基于他pr的代码进行的还原和修改。[fix: app-plus、dev/prod、nativeResources插件未被正确移](https://gitee.com/feige996/unibest/commit/22e0bd5cfb47a4927373fe88be6809216f43d046) +- 感谢【菲鸽】造了这么好用的框架 + diff --git a/app/vite-plugins/copy-native-resources.ts b/app/vite-plugins/copy-native-resources.ts new file mode 100644 index 0000000..d0caa4b --- /dev/null +++ b/app/vite-plugins/copy-native-resources.ts @@ -0,0 +1,198 @@ +import type { Plugin } from 'vite' +import path from 'node:path' +import process from 'node:process' +import fs from 'fs-extra' + +/** + * 原生插件资源复制配置接口 + * + * 根据 UniApp 官方文档:https://uniapp.dcloud.net.cn/plugin/native-plugin.html#%E6%9C%AC%E5%9C%B0%E6%8F%92%E4%BB%B6-%E9%9D%9E%E5%86%85%E7%BD%AE%E5%8E%9F%E7%94%9F%E6%8F%92%E4%BB%B6 + * 本地插件应该存储在项目根目录的 nativeplugins 目录下 + */ +export interface CopyNativeResourcesOptions { + /** 是否启用插件 */ + enable?: boolean + /** + * 源目录路径,相对于项目根目录 + * 默认为 'nativeplugins',符合 UniApp 官方规范 + * @see https://uniapp.dcloud.net.cn/plugin/native-plugin.html#%E6%9C%AC%E5%9C%B0%E6%8F%92%E4%BB%B6-%E9%9D%9E%E5%86%85%E7%BD%AE%E5%8E%9F%E7%94%9F%E6%8F%92%E4%BB%B6 + */ + sourceDir?: string + /** + * 目标目录名称,构建后在 dist 目录中的文件夹名 + * 默认为 'nativeplugins',与源目录保持一致 + */ + targetDirName?: string + /** 是否显示详细日志,便于调试和监控复制过程 */ + verbose?: boolean + /** 自定义日志前缀,用于区分不同插件的日志输出 */ + logPrefix?: string +} + +/** + * 默认配置 + * + * 根据 UniApp 官方文档规范设置默认值: + * - sourceDir: 'nativeplugins' - 符合官方本地插件存储规范 + * - targetDirName: 'nativeplugins' - 构建后保持相同的目录结构 + */ +const DEFAULT_OPTIONS: Required = { + enable: true, + sourceDir: 'nativeplugins', + targetDirName: 'nativeplugins', + verbose: true, + logPrefix: '[copy-native-resources]', +} + +/** + * UniApp 原生插件资源复制插件 + * + * 功能说明: + * 1. 解决 UniApp 使用本地原生插件时,打包后原生插件资源找不到的问题 + * 2. 将项目根目录下的 nativeplugins 目录复制到构建输出目录中 + * 3. 支持 Android 和 iOS 平台的原生插件资源复制 + * 4. 仅在 app 平台构建时生效,其他平台(H5、小程序)不执行 + * + * 使用场景: + * - 使用了 UniApp 本地原生插件(非云端插件) + * - 原生插件包含额外的资源文件(如 .so 库文件、配置文件等) + * - 需要在打包后保持原生插件的完整目录结构 + * + * 官方文档参考: + * @see https://uniapp.dcloud.net.cn/plugin/native-plugin.html#%E6%9C%AC%E5%9C%B0%E6%8F%92%E4%BB%B6-%E9%9D%9E%E5%86%85%E7%BD%AE%E5%8E%9F%E7%94%9F%E6%8F%92%E4%BB%B6 + * @see https://uniapp.dcloud.net.cn/tutorial/nvue-api.html#dom + * + * @param options 插件配置选项 + * @returns Vite 插件对象 + */ +export function copyNativeResources(options: CopyNativeResourcesOptions = {}): Plugin { + const config = { ...DEFAULT_OPTIONS, ...options } + + // 如果插件被禁用,返回一个空插件 + if (!config.enable) { + return { + name: 'copy-native-resources-disabled', + apply: 'build', + writeBundle() { + // 插件已禁用,不执行任何操作 + }, + } + } + + return { + name: 'copy-native-resources', + apply: 'build', // 只在构建时应用 + enforce: 'post', // 在其他插件执行完毕后执行 + + async writeBundle() { + const { sourceDir, targetDirName, verbose, logPrefix } = config + + try { + // 获取项目根目录路径 + const projectRoot = process.cwd() + + // 构建源目录绝对路径(项目根目录下的 nativeplugins 目录) + const sourcePath = path.resolve(projectRoot, sourceDir) + + // 构建目标路径:dist/[build|dev]/[platform]/nativeplugins + // buildMode: 'build' (生产环境) 或 'dev' (开发环境) + // platform: 'app' (App平台) 或其他平台标识 + const buildMode = process.env.NODE_ENV === 'production' ? 'build' : 'dev' + const platform = process.env.UNI_PLATFORM || 'app' + const targetPath = path.resolve( + projectRoot, + 'dist', + buildMode, + platform, + targetDirName, + ) + + // 检查源目录是否存在 + // 如果不存在 nativeplugins 目录,说明项目没有使用本地原生插件 + const sourceExists = await fs.pathExists(sourcePath) + if (!sourceExists) { + if (verbose) { + console.warn(`${logPrefix} 源目录不存在,跳过复制操作`) + console.warn(`${logPrefix} 源目录路径: ${sourcePath}`) + console.warn(`${logPrefix} 如需使用本地原生插件,请在项目根目录创建 nativeplugins 目录`) + console.warn(`${logPrefix} 并按照官方文档放入原生插件文件`) + console.warn(`${logPrefix} 参考: https://uniapp.dcloud.net.cn/plugin/native-plugin.html`) + } + return + } + + // 检查源目录是否为空 + // 如果目录存在但为空,也跳过复制操作 + const sourceFiles = await fs.readdir(sourcePath) + if (sourceFiles.length === 0) { + if (verbose) { + console.warn(`${logPrefix} 源目录为空,跳过复制操作`) + console.warn(`${logPrefix} 源目录路径: ${sourcePath}`) + console.warn(`${logPrefix} 请在 nativeplugins 目录中放入原生插件文件`) + } + return + } + + // 确保目标目录及其父目录存在 + await fs.ensureDir(targetPath) + + if (verbose) { + console.log(`${logPrefix} 开始复制 UniApp 本地原生插件...`) + console.log(`${logPrefix} 源目录: ${sourcePath}`) + console.log(`${logPrefix} 目标目录: ${targetPath}`) + console.log(`${logPrefix} 构建模式: ${buildMode}`) + console.log(`${logPrefix} 目标平台: ${platform}`) + console.log(`${logPrefix} 发现 ${sourceFiles.length} 个原生插件文件/目录`) + } + + // 执行文件复制操作 + // 将整个 nativeplugins 目录复制到构建输出目录 + await fs.copy(sourcePath, targetPath, { + overwrite: true, // 覆盖已存在的文件,确保使用最新版本 + errorOnExist: false, // 如果目标文件存在不报错 + preserveTimestamps: true, // 保持文件的时间戳 + }) + + console.log(`${logPrefix} ✅ UniApp 本地原生插件复制完成: ${sourcePath} -> ${targetPath}`) + console.log(`${logPrefix} 已成功复制 ${sourceFiles.length} 个文件/目录到构建目录`) + } + catch (error) { + console.error(`${config.logPrefix} ❌ 复制 UniApp 本地原生插件失败:`, error) + console.error(`${config.logPrefix} 错误详情:`, error instanceof Error ? error.message : String(error)) + console.error(`${config.logPrefix} 请检查源目录权限和磁盘空间`) + // 不抛出错误,避免影响整个构建过程,但会记录详细的错误信息 + } + }, + } +} + +/** + * 创建 UniApp 本地原生插件资源复制插件的便捷函数 + * + * 这是一个便捷的工厂函数,用于快速创建插件实例 + * 特别适用于在 vite.config.ts 中进行条件性插件配置 + * + * 使用示例: + * ```typescript + * // 在 vite.config.ts 中 + * plugins: [ + * // 仅在 app 平台且启用时生效 + * UNI_PLATFORM === 'app' + * ? createCopyNativeResourcesPlugin( + * VITE_COPY_NATIVE_RES_ENABLE === 'true', + * { verbose: mode === 'development' } + * ) + * : null, + * ] + * ``` + * + * @param enable 是否启用插件,通常通过环境变量控制 + * @param options 其他配置选项,不包含 enable 属性 + * @returns Vite 插件对象 + */ +export function createCopyNativeResourcesPlugin( + enable: boolean = true, + options: Omit = {}, +): Plugin { + return copyNativeResources({ enable, ...options }) +} diff --git a/app/vite-plugins/sync-manifest-plugins.ts b/app/vite-plugins/sync-manifest-plugins.ts new file mode 100644 index 0000000..4f5b273 --- /dev/null +++ b/app/vite-plugins/sync-manifest-plugins.ts @@ -0,0 +1,68 @@ +import type { Plugin } from 'vite' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' + +interface ManifestType { + 'plus'?: { + distribute?: { + plugins?: Record + } + } + 'app-plus'?: { + distribute?: { + plugins?: Record + } + } +} + +export default function syncManifestPlugin(): Plugin { + return { + name: 'sync-manifest', + apply: 'build', + enforce: 'post', + writeBundle: { + order: 'post', + handler() { + const srcManifestPath = path.resolve(process.cwd(), './src/manifest.json') + const distAppPath = path.resolve(process.cwd(), './dist/dev/app/manifest.json') + + try { + // 读取源文件 + const srcManifest = JSON.parse(fs.readFileSync(srcManifestPath, 'utf8')) as ManifestType + + // 确保目标目录存在 + const distAppDir = path.dirname(distAppPath) + if (!fs.existsSync(distAppDir)) { + fs.mkdirSync(distAppDir, { recursive: true }) + } + + // 读取目标文件(如果存在) + let distManifest: ManifestType = {} + if (fs.existsSync(distAppPath)) { + distManifest = JSON.parse(fs.readFileSync(distAppPath, 'utf8')) + } + + // 如果源文件存在 plugins + if (srcManifest['app-plus']?.distribute?.plugins) { + // 确保目标文件中有必要的对象结构 + if (!distManifest.plus) + distManifest.plus = {} + if (!distManifest.plus.distribute) + distManifest.plus.distribute = {} + + // 复制 plugins 内容 + distManifest.plus.distribute.plugins = srcManifest['app-plus'].distribute.plugins + + // 写入更新后的内容 + fs.writeFileSync(distAppPath, JSON.stringify(distManifest, null, 2)) + console.log('✅ Manifest plugins 同步成功') + } + } + catch (error) { + console.error('❌ 同步 manifest plugins 失败:', error) + } + }, + }, + } +} diff --git a/app/vite.config.ts b/app/vite.config.ts new file mode 100644 index 0000000..d9b0ad3 --- /dev/null +++ b/app/vite.config.ts @@ -0,0 +1,341 @@ +import path from 'node:path' +import process from 'node:process' +import Uni from '@uni-helper/plugin-uni' +import UniComponents from '@uni-helper/vite-plugin-uni-components' +// @see https://uni-helper.js.org/vite-plugin-uni-layouts +import UniLayouts from '@uni-helper/vite-plugin-uni-layouts' +// @see https://github.com/uni-helper/vite-plugin-uni-manifest +import UniManifest from '@uni-helper/vite-plugin-uni-manifest' +// @see https://uni-helper.js.org/vite-plugin-uni-pages +import UniPages from '@uni-helper/vite-plugin-uni-pages' +import { WotResolver } from './wot-ui-resolver' +// @see https://github.com/uni-helper/vite-plugin-uni-platform +// 需要与 @uni-helper/vite-plugin-uni-pages 插件一起使用 +import UniPlatform from '@uni-helper/vite-plugin-uni-platform' + +/** + * 分包优化、模块异步跨包调用、组件异步跨包引用 + * @see https://github.com/uni-ku/bundle-optimizer + */ +// https://github.com/uni-ku/root +import UniKuRoot from '@uni-ku/root' +import dayjs from 'dayjs' +import { visualizer } from 'rollup-plugin-visualizer' +import UnoCSS from 'unocss/vite' +import AutoImport from 'unplugin-auto-import/vite' +import { defineConfig, loadEnv } from 'vite' +import ViteRestart from 'vite-plugin-restart' +import openDevTools from './scripts/open-dev-tools' +import vitePluginEruda from './scripts/vite-plugin-eruda' +import { createCopyNativeResourcesPlugin } from './vite-plugins/copy-native-resources' +import syncManifestPlugin from './vite-plugins/sync-manifest-plugins' +import autoCreateRoutePath from './src/utils/autoCreateRoutePath' +import scanPageFilter from './src/utils/scanPageFilter' + +function disableUniMpWeixinShadowPreloadPlugin() { + return { + name: 'disable-uni-mp-weixin-shadow-preload', + enforce: 'pre' as const, + transform(code: string, id: string) { + if (!id.includes('@dcloudio/uni-mp-weixin') || !id.endsWith('uni.mp.esm.js') || !code.includes('wx.preloadAssets')) { + return null + } + const nextCode = code.replace( + /\s*if \(!\(process\.env\.NODE_ENV !== 'production'\) &&\s*"mp-weixin" === 'mp-weixin' &&\s*isFunction\(wx\.preloadAssets\)\) \{\s*const protocol = 'https';\s*setTimeout\(\(\) => \{\s*wx\.preloadAssets\(\{\s*data: \[\{ type: 'image', src: protocol \+ __UNI_PRELOAD_SHADOW_IMAGE__ \}\],\s*\}\);\s*\}, 3000\);\s*\}/, + '', + ) + return nextCode === code ? null : { code: nextCode, map: null } + }, + } +} + +const appSubPackageRules = [ + { + root: 'pages/business/views', + paths: [ + 'addon/index', + 'allocate/index', + 'balanceRecharge/index', + 'cardRenew/index', + 'change/index', + 'customer/index', + 'customerDetail/index', + 'diagnosis/index', + 'order/index', + 'renew/index', + 'warning/index', + 'workOrder/index', + 'workOrderCreate/index', + 'workOrderDetail/index', + ], + }, + { + root: 'pages/card/views', + paths: [ + 'detail/index', + 'management/index', + 'packageDetail/index', + 'warning/index', + ], + }, + { + root: 'pages/message/views', + paths: [ + 'cardDetail/index', + 'systemDetail/index', + ], + }, + { + root: 'pages/user/views', + paths: [ + 'about/index', + 'agreement/index', + 'contact/index', + 'feedback/index', + 'notification/index', + 'profile/index', + 'realName/index', + ], + }, +] + +function splitAppPagesToSubPackages(ctx: any) { + const pageMetaData = Array.isArray(ctx.pageMetaData) ? ctx.pageMetaData : [] + const subPackageMap = new Map() + const nextMainPages: any[] = [] + + for (const rule of appSubPackageRules) { + subPackageMap.set(rule.root, []) + } + + for (const page of pageMetaData) { + const matchedRule = appSubPackageRules.find((rule) => { + const prefix = `${rule.root}/` + if (!page.path?.startsWith(prefix)) { + return false + } + return rule.paths.includes(page.path.slice(prefix.length)) + }) + + if (!matchedRule) { + nextMainPages.push(page) + continue + } + + const prefix = `${matchedRule.root}/` + subPackageMap.get(matchedRule.root)?.push({ + ...page, + path: page.path.slice(prefix.length), + }) + } + + const generatedSubPackages = Array.from(subPackageMap.entries()) + .filter(([, pages]) => pages.length > 0) + .map(([root, pages]) => ({ root, pages })) + + const remainingSubPackages = (ctx.subPageMetaData || []) + .filter((item: any) => !subPackageMap.has(item.root)) + + ctx.pageMetaData = nextMainPages + ctx.subPageMetaData = [...remainingSubPackages, ...generatedSubPackages] +} + +// https://vitejs.dev/config/ +export default defineConfig(async ({ command, mode }) => { + // @see https://unocss.dev/ + // const UnoCSS = (await import('unocss/vite')).default + // console.log(mode === process.env.NODE_ENV) // true + + // mode: 区分生产环境还是开发环境 + console.log('command, mode -> ', command, mode) + // pnpm dev:h5 时得到 => serve development + // pnpm build:h5 时得到 => build production + // pnpm dev:mp-weixin 时得到 => build development (注意区别,command为build) + // pnpm build:mp-weixin 时得到 => build production + // pnpm dev:app 时得到 => build development (注意区别,command为build) + // pnpm build:app 时得到 => build production + // dev 和 build 命令可以分别使用 .env.development 和 .env.production 的环境变量 + // 非 H5 端 dev 也是 build command,最终加载哪个 env 文件以实际 mode 为准。 + + const { UNI_PLATFORM, SKIP_OPEN_DEVTOOLS } = process.env + console.log('UNI_PLATFORM -> ', UNI_PLATFORM) // 得到 mp-weixin, h5, app 等 + + const envDir = path.resolve(process.cwd(), 'env') + const env = loadEnv(mode, envDir) + const localEnv = loadEnv(mode, envDir, '') + const { + VITE_APP_PORT, + VITE_APP_TITLE, + VITE_DELETE_CONSOLE, + VITE_APP_PUBLIC_BASE, + VITE_COPY_NATIVE_RES_ENABLE, + VITE_ERUDA_ENABLE, + } = env + const { WECHAT_DEVTOOLS_CLI_PATH } = localEnv + console.log('环境变量 env -> ', env) + + process.env.ROOT_DIR = process.env.ROOT_DIR || env.VITE_ROOT_DIR || process.cwd() + const isVitest = process.env.VITEST === 'true' + const useBundleOptimization = !isVitest + const alias = { + '@': path.join(process.cwd(), './src'), + '@img': path.join(process.cwd(), './src/static/images'), + } + + if (isVitest) { + return defineConfig({ + envDir: './env', + resolve: { + alias, + }, + }) + } + + return defineConfig({ + envDir: './env', // 自定义env目录 + // H5 使用部署子路径;微信小程序和 App 端保持根路径资源引用。 + base: UNI_PLATFORM === 'h5' ? (VITE_APP_PUBLIC_BASE || '/') : '/', + plugins: [ + disableUniMpWeixinShadowPreloadPlugin(), + // UniXXX 需要在 Uni 之前引入 + UniLayouts(), + UniPlatform(), + UniManifest(), + UniComponents({ + extensions: ['vue'], + deep: true, // 是否递归扫描子目录, + directoryAsNamespace: false, // 是否把目录名作为命名空间前缀,true 时组件名为 目录名+组件名, + dts: mode === 'development' ? 'src/types/components.d.ts' : false, // 自动生成的组件类型声明文件路径(用于 TypeScript 支持) + resolvers: [WotResolver()], + }), + UniPages({ + exclude: ['**/components/**/**.*', '**/sections/**/**.*'], + // pages 目录为 src/pages,分包目录不能配置在pages目录下!! + // 是个数组,可以配置多个,但是不能为pages里面的目录!! + // "src/pages-demo" 是unibest demo 预留的,方便后续插入demo示例 + // subPackages: ['src/pages-demo'], + dts: 'src/types/uni-pages.d.ts', + + onAfterScanPages: (ctx) => { + scanPageFilter(ctx, 'pages') + }, + onAfterMergePageMetaData: (ctx) => { + splitAppPagesToSubPackages(ctx) + }, + onAfterWriteFile: (ctx) => { + autoCreateRoutePath(ctx) + }, + }), + // UniOptimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行 + ...(useBundleOptimization + ? [ + (await import('@uni-ku/bundle-optimizer')).default({ + // 当前用户侧小程序首屏依赖多个首页组件。微信开发者工具对异步拆包缓存较敏感, + // 先关闭小程序端拆包优化,保证本地预览和真机调试优先稳定渲染。 + enable: false, + dts: { + base: 'src/types', + }, + logger: false, + }), + ] + : []), + // 若存在改变 pages.json 的插件,请将 UniKuRoot 放置其后 + UniKuRoot({ + excludePages: ['**/components/**/**.*', '**/sections/**/**.*'], + }), + Uni(), + { + // 临时解决 dcloudio 官方的 @dcloudio/uni-mp-compiler 出现的编译 BUG + // 参考 github issue: https://github.com/dcloudio/uni-app/issues/4952 + // 自定义插件禁用 vite:vue 插件的 devToolsEnabled,强制编译 vue 模板时 inline 为 true + name: 'fix-vite-plugin-vue', + configResolved(config) { + const plugin = config.plugins.find(p => p.name === 'vite:vue') + if (plugin && plugin.api && plugin.api.options) { + plugin.api.options.devToolsEnabled = false + } + }, + }, + UnoCSS(), + AutoImport({ + imports: ['vue', 'uni-app'], + dts: 'src/types/auto-import.d.ts', + dirs: ['src/hooks'], // 自动导入 hooks + vueTemplate: true, // default false + }), + ViteRestart({ + // 通过这个插件,在修改vite.config.js文件则不需要重新运行也生效配置 + restart: ['vite.config.js'], + }), + // h5环境增加 BUILD_TIME 和 BUILD_BRANCH + UNI_PLATFORM === 'h5' && { + name: 'html-transform', + transformIndexHtml(html) { + return html + .replace('%BUILD_TIME%', dayjs().format('YYYY-MM-DD HH:mm:ss')) + .replace('%VITE_APP_TITLE%', VITE_APP_TITLE) + }, + }, + // 打包分析插件,h5 + 生产环境才弹出 + UNI_PLATFORM === 'h5' + && mode === 'production' + && visualizer({ + filename: './node_modules/.cache/visualizer/stats.html', + open: true, + gzipSize: true, + brotliSize: true, + }), + // 原生插件资源复制插件 - 仅在 app 平台且启用时生效 + createCopyNativeResourcesPlugin( + UNI_PLATFORM === 'app' && VITE_COPY_NATIVE_RES_ENABLE === 'true', + { + verbose: mode === 'development', // 开发模式显示详细日志 + }, + ), + syncManifestPlugin(), + vitePluginEruda({ + open: UNI_PLATFORM === 'h5' && mode === 'development' && VITE_ERUDA_ENABLE === 'true', + }), + // 自动打开开发者工具插件 (必须修改 .env 文件中的 VITE_WX_APPID) + // 上传时通过 SKIP_OPEN_DEVTOOLS=true 跳过 + command === 'serve' + && SKIP_OPEN_DEVTOOLS !== 'true' + && WECHAT_DEVTOOLS_CLI_PATH + && openDevTools({ + mode, + wechatDevtoolsCliPath: WECHAT_DEVTOOLS_CLI_PATH, + }), + ], + css: { + postcss: { + plugins: [ + // autoprefixer({ + // // 指定目标浏览器 + // overrideBrowserslist: ['> 1%', 'last 2 versions'], + // }), + ], + }, + }, + + resolve: { + alias, + }, + server: { + host: '0.0.0.0', + hmr: true, + port: Number.parseInt(VITE_APP_PORT, 10), + }, + esbuild: { + drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : [], + }, + build: { + sourcemap: false, + // 方便非h5端调试 + // sourcemap: VITE_SHOW_SOURCEMAP === 'true', // 默认是false + target: 'es6', + // 开发环境不用压缩 + minify: mode === 'development' ? false : 'esbuild', + }, + }) +}) diff --git a/app/wot-ui-resolver.ts b/app/wot-ui-resolver.ts new file mode 100644 index 0000000..6a79867 --- /dev/null +++ b/app/wot-ui-resolver.ts @@ -0,0 +1,27 @@ +import type { ComponentResolver } from '@uni-helper/vite-plugin-uni-components' +import { kebabCase } from '@uni-helper/vite-plugin-uni-components' + +/** + * npm 安装的 Wot UI 组件在 H5 端需通过 vite-plugin-uni-components 解析, + * 才能正确挂载组件样式。 + */ +export function WotResolver(): ComponentResolver { + return { + type: 'component', + resolve: (name: string) => { + if (name.match(/^Wd[A-Z]/)) { + const compName = kebabCase(name) + return { + name, + from: `@wot-ui/ui/components/${compName}/${compName}.vue`, + } + } + if (name.startsWith('wd-')) { + return { + name, + from: `@wot-ui/ui/components/${name}/${name}.vue`, + } + } + }, + } +} diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..a1bf222 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,12 @@ +.git +.idea +.env +xiujie-wechat/ +*.log +resource/log/ +resource/cache/ +resource/upload/ +resource/certs/ +tmp/ +*.exe +*.test diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..73114aa --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,44 @@ +# Build context: server/ +# Example: +# docker build -f Dockerfile -t iotlcm-app-server:latest . + +FROM crpi-svwhn8x8nvask31h.cn-beijing.personal.cr.aliyuncs.com/xjzlpublic/golangalpine:1.25.7-alpine3.23 AS builder + +ENV GIT_TERMINAL_PROMPT=0 +ENV GOPRIVATE=git.xiujie.cn +ENV GOPROXY=https://goproxy.cn,direct +ENV TZ=Asia/Shanghai + +WORKDIR /src/server + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . ./ + +ARG BuildVersion=0.0.0 +ARG BuildTime= +ARG CommitID= + +RUN go build -v -o /out/iotlcm-app-server \ + -ldflags "-X main.BuildVersion=${BuildVersion} -X main.CommitID=${CommitID} -X main.BuildTime=${BuildTime}" \ + . + +FROM crpi-svwhn8x8nvask31h.cn-beijing.personal.cr.aliyuncs.com/xjzlpublic/alpine AS runtime + +ENV TZ=Asia/Shanghai +ENV APP_UPLOAD_DIR=/data/app/iotlcmadmin-web-svc/upload + +WORKDIR /app + +COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai +COPY --from=builder /out/iotlcm-app-server /app/iotlcm-app-server +COPY resource/public /app/resource/public + +# The environment-specific config is mounted read-only by Docker Compose at runtime. +RUN mkdir -p "${APP_UPLOAD_DIR}" /app/config /app/resource/log/server /app/resource/log/server/sql /app/resource/cache /app/resource/certs + +EXPOSE 8010 +VOLUME ["/data/app/iotlcmadmin-web-svc/upload", "/app/resource/certs"] + +CMD ["./iotlcm-app-server"] diff --git a/server/LICENSE b/server/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/server/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..2a6e6e9 --- /dev/null +++ b/server/Makefile @@ -0,0 +1,7 @@ +ROOT_DIR = $(shell pwd) +NAMESPACE = "default" +DEPLOY_NAME = "template-single" +DOCKER_NAME = "template-single" + +include ./hack/hack-cli.mk +include ./hack/hack.mk \ No newline at end of file diff --git a/server/README.MD b/server/README.MD new file mode 100644 index 0000000..d36cedd --- /dev/null +++ b/server/README.MD @@ -0,0 +1,4 @@ +# GoFrame Template For SingleRepo + +Quick Start: +- https://goframe.org/quick \ No newline at end of file diff --git a/server/api/app/app.go b/server/api/app/app.go new file mode 100644 index 0000000..6a464ca --- /dev/null +++ b/server/api/app/app.go @@ -0,0 +1,74 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package app + +import ( + "context" + + "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" +) + +type IAppV1 interface { + Captcha(ctx context.Context, req *v1.CaptchaReq) (res *v1.CaptchaRes, err error) + SideCaptcha(ctx context.Context, req *v1.SideCaptchaReq) (res *v1.SideCaptchaRes, err error) + CheckSideCaptcha(ctx context.Context, req *v1.CheckSideCaptchaReq) (res *v1.CheckSideCaptchaRes, err error) + SendSms(ctx context.Context, req *v1.SendSmsReq) (res *v1.SendSmsRes, err error) + PhoneRegister(ctx context.Context, req *v1.PhoneRegisterReq) (res *v1.PhoneRegisterRes, err error) + PhoneLogin(ctx context.Context, req *v1.PhoneLoginReq) (res *v1.PhoneLoginRes, err error) + WechatLogin(ctx context.Context, req *v1.WechatLoginReq) (res *v1.WechatLoginRes, err error) + WechatBindPhone(ctx context.Context, req *v1.WechatBindPhoneReq) (res *v1.WechatBindPhoneRes, err error) + ForgotPassword(ctx context.Context, req *v1.ForgotPasswordReq) (res *v1.ForgotPasswordRes, err error) + BalanceRechargeSubmit(ctx context.Context, req *v1.BalanceRechargeSubmitReq) (res *v1.BalanceRechargeSubmitRes, err error) + BalanceRechargeStatus(ctx context.Context, req *v1.BalanceRechargeStatusReq) (res *v1.BalanceRechargeStatusRes, err error) + BalanceRechargeMockPay(ctx context.Context, req *v1.BalanceRechargeMockPayReq) (res *v1.BalanceRechargeMockPayRes, err error) + BalanceRechargeRefund(ctx context.Context, req *v1.BalanceRechargeRefundReq) (res *v1.BalanceRechargeRefundRes, err error) + BalanceRechargeWechatNotify(ctx context.Context, req *v1.BalanceRechargeWechatNotifyReq) (res *v1.BalanceRechargeWechatNotifyRes, err error) + BalanceRechargeWechatRefundNotify(ctx context.Context, req *v1.BalanceRechargeWechatRefundNotifyReq) (res *v1.BalanceRechargeWechatRefundNotifyRes, err error) + PublicQueryCard(ctx context.Context, req *v1.PublicQueryCardReq) (res *v1.PublicQueryCardRes, err error) + RecognizeCard(ctx context.Context, req *v1.RecognizeCardReq) (res *v1.RecognizeCardRes, err error) + CardList(ctx context.Context, req *v1.CardListReq) (res *v1.CardListRes, err error) + PackageList(ctx context.Context, req *v1.PackageListReq) (res *v1.PackageListRes, err error) + PublicPackageDetail(ctx context.Context, req *v1.PublicPackageDetailReq) (res *v1.PublicPackageDetailRes, err error) + CardDetail(ctx context.Context, req *v1.CardDetailReq) (res *v1.CardDetailRes, err error) + BindCard(ctx context.Context, req *v1.BindCardReq) (res *v1.BindCardRes, err error) + RestartCard(ctx context.Context, req *v1.RestartCardReq) (res *v1.RestartCardRes, err error) + StopCard(ctx context.Context, req *v1.StopCardReq) (res *v1.StopCardRes, err error) + StartCard(ctx context.Context, req *v1.StartCardReq) (res *v1.StartCardRes, err error) + LifecycleBatchSubmit(ctx context.Context, req *v1.LifecycleBatchSubmitReq) (res *v1.LifecycleBatchSubmitRes, err error) + LifecycleBatchStatus(ctx context.Context, req *v1.LifecycleBatchStatusReq) (res *v1.LifecycleBatchStatusRes, err error) + RenewPrepare(ctx context.Context, req *v1.RenewPrepareReq) (res *v1.RenewPrepareRes, err error) + RenewBatchPrepare(ctx context.Context, req *v1.RenewBatchPrepareReq) (res *v1.RenewBatchPrepareRes, err error) + RenewSubmit(ctx context.Context, req *v1.RenewSubmitReq) (res *v1.RenewSubmitRes, err error) + RenewBatchSubmit(ctx context.Context, req *v1.RenewBatchSubmitReq) (res *v1.RenewBatchSubmitRes, err error) + AllocateTargets(ctx context.Context, req *v1.AllocateTargetsReq) (res *v1.AllocateTargetsRes, err error) + AllocateBatchPrepare(ctx context.Context, req *v1.AllocateBatchPrepareReq) (res *v1.AllocateBatchPrepareRes, err error) + AllocateBatchSubmit(ctx context.Context, req *v1.AllocateBatchSubmitReq) (res *v1.AllocateBatchSubmitRes, err error) + ChangePrepare(ctx context.Context, req *v1.ChangePrepareReq) (res *v1.ChangePrepareRes, err error) + ChangeSubmit(ctx context.Context, req *v1.ChangeSubmitReq) (res *v1.ChangeSubmitRes, err error) + DiagnosisInfo(ctx context.Context, req *v1.DiagnosisInfoReq) (res *v1.DiagnosisInfoRes, err error) + DiagnosisFlow(ctx context.Context, req *v1.DiagnosisFlowReq) (res *v1.DiagnosisFlowRes, err error) + OpenDiagnosisInfo(ctx context.Context, req *v1.OpenDiagnosisInfoReq) (res *v1.OpenDiagnosisInfoRes, err error) + OpenDiagnosisFlow(ctx context.Context, req *v1.OpenDiagnosisFlowReq) (res *v1.OpenDiagnosisFlowRes, err error) + CustomerList(ctx context.Context, req *v1.CustomerListReq) (res *v1.CustomerListRes, err error) + CustomerDetail(ctx context.Context, req *v1.CustomerDetailReq) (res *v1.CustomerDetailRes, err error) + MessageList(ctx context.Context, req *v1.MessageListReq) (res *v1.MessageListRes, err error) + MessageDetail(ctx context.Context, req *v1.MessageDetailReq) (res *v1.MessageDetailRes, err error) + MessageRead(ctx context.Context, req *v1.MessageReadReq) (res *v1.MessageReadRes, err error) + OrderList(ctx context.Context, req *v1.OrderListReq) (res *v1.OrderListRes, err error) + OrderDetail(ctx context.Context, req *v1.OrderDetailReq) (res *v1.OrderDetailRes, err error) + UserInfo(ctx context.Context, req *v1.UserInfoReq) (res *v1.UserInfoRes, err error) + UpdateUserInfo(ctx context.Context, req *v1.UpdateUserInfoReq) (res *v1.UpdateUserInfoRes, err error) + BindWechat(ctx context.Context, req *v1.BindWechatReq) (res *v1.BindWechatRes, err error) + WarningConfigList(ctx context.Context, req *v1.WarningConfigListReq) (res *v1.WarningConfigListRes, err error) + WarningConfigSave(ctx context.Context, req *v1.WarningConfigSaveReq) (res *v1.WarningConfigSaveRes, err error) + WarningConfigNotify(ctx context.Context, req *v1.WarningConfigNotifyReq) (res *v1.WarningConfigNotifyRes, err error) + WarningConfigMeta(ctx context.Context, req *v1.WarningConfigMetaReq) (res *v1.WarningConfigMetaRes, err error) + WarningConfigBatchSave(ctx context.Context, req *v1.WarningConfigBatchSaveReq) (res *v1.WarningConfigBatchSaveRes, err error) + WarningConfigBatchNotify(ctx context.Context, req *v1.WarningConfigBatchNotifyReq) (res *v1.WarningConfigBatchNotifyRes, err error) + WorkOrderList(ctx context.Context, req *v1.WorkOrderListReq) (res *v1.WorkOrderListRes, err error) + WorkOrderDetail(ctx context.Context, req *v1.WorkOrderDetailReq) (res *v1.WorkOrderDetailRes, err error) + WorkOrderCreate(ctx context.Context, req *v1.WorkOrderCreateReq) (res *v1.WorkOrderCreateRes, err error) + WorkOrderUpload(ctx context.Context, req *v1.WorkOrderUploadReq) (res *v1.WorkOrderUploadRes, err error) +} diff --git a/server/api/app/v1/auth.go b/server/api/app/v1/auth.go new file mode 100644 index 0000000..ca8c8cd --- /dev/null +++ b/server/api/app/v1/auth.go @@ -0,0 +1,87 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type CaptchaReq struct { + g.Meta `path:"/auth/captcha" method:"get" tags:"APP-授权" summary:"获取图形验证码"` +} + +type CaptchaRes struct { + CaptchaID string `json:"captchaID" dc:"验证码ID"` + CaptchaImage string `json:"captchaImage" dc:"验证码图片Base64"` +} + +type SideCaptchaReq struct { + g.Meta `path:"/auth/side-captcha" method:"post" tags:"APP-授权" summary:"获取滑块验证码"` +} + +type SideCaptchaRes struct { + model.AppSideCaptchaModel +} + +type CheckSideCaptchaReq struct { + g.Meta `path:"/auth/check-side-captcha" method:"post" tags:"APP-授权" summary:"验证滑块验证码"` + model.AppCheckSideCaptchaParam +} + +type CheckSideCaptchaRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +type SendSmsReq struct { + g.Meta `path:"/auth/sms" method:"post" tags:"APP-授权" summary:"发送短信验证码"` + model.AppSendSmsParam +} + +type SendSmsRes struct { + model.AppSendSmsModel +} + +type PhoneRegisterReq struct { + g.Meta `path:"/auth/register" method:"post" tags:"APP-授权" summary:"手机号注册"` + model.AppPhoneRegisterParam +} + +type PhoneRegisterRes struct { + model.AppAuthTokenModel +} + +type PhoneLoginReq struct { + g.Meta `path:"/auth/login" method:"post" tags:"APP-授权" summary:"账号登录"` + model.AppPhoneLoginParam +} + +type PhoneLoginRes struct { + model.AppAuthTokenModel +} + +type WechatLoginReq struct { + g.Meta `path:"/auth/wechat-login" method:"post" tags:"APP-授权" summary:"微信一键登录"` + model.AppWechatLoginParam +} + +type WechatLoginRes struct { + model.AppWechatLoginModel +} + +type WechatBindPhoneReq struct { + g.Meta `path:"/auth/wechat-bind-phone" method:"post" tags:"APP-授权" summary:"微信登录绑定手机号"` + model.AppWechatBindPhoneParam +} + +type WechatBindPhoneRes struct { + model.AppWechatBindPhoneModel +} + +type ForgotPasswordReq struct { + g.Meta `path:"/auth/forgot-password" method:"post" tags:"APP-授权" summary:"手机号找回密码"` + model.AppForgotPasswordParam +} + +type ForgotPasswordRes struct { + Success bool `json:"success" dc:"是否成功"` +} diff --git a/server/api/app/v1/balance.go b/server/api/app/v1/balance.go new file mode 100644 index 0000000..dce67e9 --- /dev/null +++ b/server/api/app/v1/balance.go @@ -0,0 +1,55 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type BalanceRechargeSubmitReq struct { + g.Meta `path:"/balance/recharge-submit" method:"post" tags:"APP-余额" summary:"提交余额充值订单"` + model.AppBalanceRechargeSubmitParam +} + +type BalanceRechargeSubmitRes struct { + *model.AppBalanceRechargeSubmitModel +} + +type BalanceRechargeStatusReq struct { + g.Meta `path:"/balance/recharge-status" method:"get" tags:"APP-余额" summary:"查询余额充值订单支付状态"` + model.AppBalanceRechargeStatusParam +} + +type BalanceRechargeStatusRes struct { + *model.AppBalanceRechargeStatusModel +} + +type BalanceRechargeMockPayReq struct { + g.Meta `path:"/balance/recharge-mock-pay" method:"post" tags:"APP-余额" summary:"模拟余额充值支付成功"` + model.AppBalanceRechargeMockPayParam +} + +type BalanceRechargeMockPayRes struct { + *model.AppBalanceRechargeStatusModel +} + +type BalanceRechargeRefundReq struct { + g.Meta `path:"/balance/recharge-refund" method:"post" tags:"APP-余额" summary:"发起余额充值退款申请"` + model.AppBalanceRechargeRefundParam +} + +type BalanceRechargeRefundRes struct { + *model.AppOrderModel +} + +type BalanceRechargeWechatNotifyReq struct { + g.Meta `path:"/payment/wechat/notify" method:"post" tags:"APP-余额" summary:"微信支付余额充值异步通知"` +} + +type BalanceRechargeWechatNotifyRes struct{} + +type BalanceRechargeWechatRefundNotifyReq struct { + g.Meta `path:"/payment/wechat/refund-notify" method:"post" tags:"APP-余额" summary:"微信支付余额充值退款异步通知"` +} + +type BalanceRechargeWechatRefundNotifyRes struct{} diff --git a/server/api/app/v1/card.go b/server/api/app/v1/card.go new file mode 100644 index 0000000..a3bc646 --- /dev/null +++ b/server/api/app/v1/card.go @@ -0,0 +1,199 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type PublicQueryCardReq struct { + g.Meta `path:"/card/public-query" method:"post" tags:"APP-卡片" summary:"未登录公开查询卡信息"` + model.AppCardQueryParam +} + +type PublicQueryCardRes struct { + Card *model.AppCardModel `json:"card" dc:"卡信息"` +} + +type RecognizeCardReq struct { + g.Meta `path:"/card/recognize" method:"post" tags:"APP-卡片" summary:"拍照识别ICCID并公开查卡"` +} + +type RecognizeCardRes struct { + *model.AppCardRecognitionModel +} + +type CardListReq struct { + g.Meta `path:"/card/list" method:"get" tags:"APP-卡片" summary:"登录用户查询持有卡列表" x-check-permission:"cpa:terminal:user,cpm:iot:tSimCardInfo:list"` + model.AppCardListParam +} + +type CardListRes struct { + Items []*model.AppCardModel `json:"items" dc:"卡列表"` + Total int `json:"total" dc:"总数"` + HasMore bool `json:"hasMore" dc:"是否还有下一页"` +} + +type PackageListReq struct { + g.Meta `path:"/card/package-list" method:"get" tags:"APP-卡片" summary:"登录用户查询持有卡套餐列表" x-check-permission:"cpm:iot:tSimCardBindPkg:list,cpm:iot:tSimFlowPackageInfo:list,cpm:iot:tSimCardInfo:list"` + model.AppPackageListParam +} + +type PackageListRes struct { + Items []*model.AppPackageModel `json:"items" dc:"套餐列表"` + Total int `json:"total" dc:"总数"` +} + +type PublicPackageDetailReq struct { + g.Meta `path:"/card/public-package-detail" method:"post" tags:"APP-卡片" summary:"未登录公开查询当前套餐详情"` + model.AppPackagePublicDetailParam +} + +type PublicPackageDetailRes struct { + Package *model.AppPackageModel `json:"package" dc:"套餐详情"` +} + +type CardDetailReq struct { + g.Meta `path:"/card/detail" method:"get" tags:"APP-卡片" summary:"登录用户查询卡详情,非持有卡按公共查询返回" x-check-permission:"cpm:iot:tSimCardInfo:view,cpm:iot:tSimCardInfo:list"` + model.AppCardDetailParam +} + +type CardDetailRes struct { + Card *model.AppCardModel `json:"card" dc:"卡信息"` +} + +type BindCardReq struct { + g.Meta `path:"/card/bind" method:"post" tags:"APP-卡片" summary:"终端用户绑定卡" x-check-permission:"cpc:current:user"` + model.AppCardBindParam +} + +type BindCardRes struct { + Card *model.AppCardModel `json:"card" dc:"卡信息"` +} + +type RestartCardReq struct { + g.Meta `path:"/card/restart" method:"post" tags:"APP-卡片" summary:"登录用户重启卡" x-check-permission:"cpm:iot:tSimCardRestart:input:submit"` + model.AppCardRestartParam +} + +type RestartCardRes struct { + *model.AppCardRestartModel +} + +type StopCardReq struct { + g.Meta `path:"/card/stop" method:"post" tags:"APP-卡片" summary:"企业用户停机卡片" x-check-permission:"cpm:iot:tSimCardStop:input:submit"` + model.AppCardRestartParam +} + +type StopCardRes struct { + *model.AppCardLifecycleModel +} + +type StartCardReq struct { + g.Meta `path:"/card/start" method:"post" tags:"APP-卡片" summary:"企业用户复机卡片" x-check-permission:"cpm:iot:tSimCardStart:input:submit"` + model.AppCardRestartParam +} + +type StartCardRes struct { + *model.AppCardLifecycleModel +} + +type LifecycleBatchSubmitReq struct { + g.Meta `path:"/card/lifecycle-batch-submit" method:"post" tags:"APP-卡片" summary:"企业用户批量提交停机复机重启" x-check-permission:"cpm:iot:tSimCardStop:input:submit,cpm:iot:tSimCardStart:input:submit,cpm:iot:tSimCardRestart:input:submit"` + model.AppCardLifecycleParam +} + +type LifecycleBatchSubmitRes struct { + *model.AppCardLifecycleModel +} + +type LifecycleBatchStatusReq struct { + g.Meta `path:"/card/lifecycle-batch-status" method:"get" tags:"APP-卡片" summary:"查询卡片停机复机重启批次状态" x-check-permission:"cpm:iot:tSimCardStop:input:submit,cpm:iot:tSimCardStart:input:submit,cpm:iot:tSimCardRestart:input:submit"` + model.AppCardLifecycleStatusParam +} + +type LifecycleBatchStatusRes struct { + *model.AppCardLifecycleModel +} + +type RenewPrepareReq struct { + g.Meta `path:"/card/renew-prepare" method:"post" tags:"APP-卡片" summary:"登录用户查询套餐续费档位和价格" x-check-permission:"cpm:iot:tSimCardRenew:input:prepare,cpm:iot:tSimCardRenew:batch:confirm"` + model.AppCardRenewPrepareParam +} + +type RenewPrepareRes struct { + *model.AppCardRenewPrepareModel +} + +type RenewBatchPrepareReq struct { + g.Meta `path:"/card/renew-batch-prepare" method:"post" tags:"APP-卡片" summary:"登录用户批量查询套餐续费档位和价格" x-check-permission:"cpm:iot:tSimCardRenew:input:prepare,cpm:iot:tSimCardRenew:batch:confirm"` + model.AppCardRenewBatchPrepareParam +} + +type RenewBatchPrepareRes struct { + *model.AppCardRenewPrepareModel +} + +type RenewSubmitReq struct { + g.Meta `path:"/card/renew-submit" method:"post" tags:"APP-卡片" summary:"登录用户提交套餐续费订单" x-check-permission:"cpm:iot:tSimCardRenew:batch:confirm"` + model.AppCardRenewSubmitParam +} + +type RenewSubmitRes struct { + *model.AppCardRenewSubmitModel +} + +type RenewBatchSubmitReq struct { + g.Meta `path:"/card/renew-batch-submit" method:"post" tags:"APP-卡片" summary:"登录用户提交批量套餐续费订单" x-check-permission:"cpm:iot:tSimCardRenew:batch:confirm"` + model.AppCardRenewBatchSubmitParam +} + +type RenewBatchSubmitRes struct { + *model.AppCardRenewSubmitModel +} + +type AllocateTargetsReq struct { + g.Meta `path:"/card/allocate-targets" method:"get" tags:"APP-卡片" summary:"企业用户查询可划拨目标客户" x-check-permission:"cpm:iot:tSimCardAllocate:targets"` + model.AppCardAllocateTargetListParam +} + +type AllocateTargetsRes struct { + Items []*model.AppCardAllocateTargetModel `json:"items" dc:"目标客户列表"` + Total int `json:"total" dc:"总数"` +} + +type AllocateBatchPrepareReq struct { + g.Meta `path:"/card/allocate-batch-prepare" method:"post" tags:"APP-卡片" summary:"企业用户批量划拨预校验" x-check-permission:"cpm:iot:tSimCardAllocate:input:prepare"` + model.AppCardAllocateBatchPrepareParam +} + +type AllocateBatchPrepareRes struct { + *model.AppCardAllocateBatchModel +} + +type AllocateBatchSubmitReq struct { + g.Meta `path:"/card/allocate-batch-submit" method:"post" tags:"APP-卡片" summary:"企业用户提交批量划拨" x-check-permission:"cpm:iot:tSimCardAllocate:batch:submit"` + model.AppCardAllocateBatchSubmitParam +} + +type AllocateBatchSubmitRes struct { + *model.AppCardAllocateBatchModel +} + +type ChangePrepareReq struct { + g.Meta `path:"/card/change-prepare" method:"post" tags:"APP-卡片" summary:"企业用户套餐变更预校验" x-check-permission:"cpm:iot:tSimCardChange:input:prepare,cpm:iot:tSimCardChange:range:prepare,cpm:iot:tSimCardBindPkg:getChangePkg"` + model.AppCardChangePrepareParam +} + +type ChangePrepareRes struct { + *model.AppCardChangePrepareModel +} + +type ChangeSubmitReq struct { + g.Meta `path:"/card/change-submit" method:"post" tags:"APP-卡片" summary:"企业用户提交套餐变更" x-check-permission:"cpm:iot:tSimCardChange:batch:submit"` + model.AppCardChangeSubmitParam +} + +type ChangeSubmitRes struct { + *model.AppCardChangeSubmitModel +} diff --git a/server/api/app/v1/customer.go b/server/api/app/v1/customer.go new file mode 100644 index 0000000..e551a64 --- /dev/null +++ b/server/api/app/v1/customer.go @@ -0,0 +1,26 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type CustomerListReq struct { + g.Meta `path:"/customer/list" method:"get" tags:"APP-客户" summary:"企业用户查询下级客户列表" x-check-permission:"cpm:agent:dept:list,cpm:agent:tAgentEnterprise:list"` + model.AppCustomerListParam +} + +type CustomerListRes struct { + Items []*model.AppCustomerModel `json:"items" dc:"客户列表"` + Total int `json:"total" dc:"总数"` +} + +type CustomerDetailReq struct { + g.Meta `path:"/customer/detail" method:"get" tags:"APP-客户" summary:"企业用户查询客户详情" x-check-permission:"cpm:agent:tAgentEnterprise:view,cpm:agent:tAgentEnterprise:list"` + model.AppCustomerDetailParam +} + +type CustomerDetailRes struct { + Customer *model.AppCustomerDetailModel `json:"customer" dc:"客户详情"` +} diff --git a/server/api/app/v1/diagnosis.go b/server/api/app/v1/diagnosis.go new file mode 100644 index 0000000..b63c758 --- /dev/null +++ b/server/api/app/v1/diagnosis.go @@ -0,0 +1,25 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type DiagnosisInfoReq struct { + g.Meta `path:"/diagnosis/info" method:"post" tags:"APP-智能诊断" summary:"查询SIM卡诊断信息"` + model.AppDiagnosisInfoParam +} + +type DiagnosisInfoRes struct { + *model.AppDiagnosisInfoModel +} + +type DiagnosisFlowReq struct { + g.Meta `path:"/diagnosis/flow" method:"post" tags:"APP-智能诊断" summary:"查询SIM卡流量趋势"` + model.AppDiagnosisFlowParam +} + +type DiagnosisFlowRes struct { + *model.AppDiagnosisFlowModel +} diff --git a/server/api/app/v1/message.go b/server/api/app/v1/message.go new file mode 100644 index 0000000..4277570 --- /dev/null +++ b/server/api/app/v1/message.go @@ -0,0 +1,33 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type MessageListReq struct { + g.Meta `path:"/message/list" method:"get" tags:"APP-消息中心" summary:"当前登录用户消息列表"` + model.AppMessageListParam +} + +type MessageListRes struct { + Items []*model.AppMessageModel `json:"items" dc:"消息列表"` + Total int `json:"total" dc:"总数"` +} + +type MessageDetailReq struct { + g.Meta `path:"/message/detail" method:"get" tags:"APP-消息中心" summary:"当前登录用户消息详情"` + model.AppMessageDetailParam +} + +type MessageDetailRes struct { + Message *model.AppMessageModel `json:"message" dc:"消息详情"` +} + +type MessageReadReq struct { + g.Meta `path:"/message/read" method:"post" tags:"APP-消息中心" summary:"标记当前登录用户消息为已读"` + model.AppMessageReadParam +} + +type MessageReadRes struct{} diff --git a/server/api/app/v1/open.go b/server/api/app/v1/open.go new file mode 100644 index 0000000..86ad65e --- /dev/null +++ b/server/api/app/v1/open.go @@ -0,0 +1,25 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type OpenDiagnosisInfoReq struct { + g.Meta `path:"/open/diagnosis/info" method:"post" tags:"APP-开放诊断" summary:"免登录查询SIM卡诊断信息"` + model.AppOpenDiagnosisInfoParam +} + +type OpenDiagnosisInfoRes struct { + *model.AppDiagnosisInfoModel +} + +type OpenDiagnosisFlowReq struct { + g.Meta `path:"/open/diagnosis/flow" method:"post" tags:"APP-开放诊断" summary:"免登录查询SIM卡流量趋势"` + model.AppOpenDiagnosisFlowParam +} + +type OpenDiagnosisFlowRes struct { + *model.AppDiagnosisFlowModel +} diff --git a/server/api/app/v1/order.go b/server/api/app/v1/order.go new file mode 100644 index 0000000..f03cc75 --- /dev/null +++ b/server/api/app/v1/order.go @@ -0,0 +1,26 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type OrderListReq struct { + g.Meta `path:"/order/list" method:"get" tags:"APP-订单" summary:"登录用户查询自己的支付订单"` + model.AppOrderListParam +} + +type OrderListRes struct { + Items []*model.AppOrderModel `json:"items" dc:"订单列表"` + Total int `json:"total" dc:"总数"` +} + +type OrderDetailReq struct { + g.Meta `path:"/order/detail" method:"get" tags:"APP-订单" summary:"登录用户查询自己的支付订单详情"` + model.AppOrderDetailParam +} + +type OrderDetailRes struct { + Order *model.AppOrderModel `json:"order" dc:"订单详情"` +} diff --git a/server/api/app/v1/user.go b/server/api/app/v1/user.go new file mode 100644 index 0000000..1da67e6 --- /dev/null +++ b/server/api/app/v1/user.go @@ -0,0 +1,33 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type UserInfoReq struct { + g.Meta `path:"/user/info" method:"get" tags:"APP-用户" summary:"当前用户信息"` +} + +type UserInfoRes struct { + model.AppUserInfoModel +} + +type UpdateUserInfoReq struct { + g.Meta `path:"/user/info" method:"post" tags:"APP-用户" summary:"修改当前用户信息"` + model.AppUpdateUserInfoParam +} + +type UpdateUserInfoRes struct { + model.AppUserInfoModel +} + +type BindWechatReq struct { + g.Meta `path:"/user/wechat/bind" method:"post" tags:"APP-用户" summary:"绑定或更换微信用户"` + model.AppWechatBindParam +} + +type BindWechatRes struct { + model.AppWechatBindModel +} diff --git a/server/api/app/v1/warning.go b/server/api/app/v1/warning.go new file mode 100644 index 0000000..d7e5a65 --- /dev/null +++ b/server/api/app/v1/warning.go @@ -0,0 +1,60 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type WarningConfigListReq struct { + g.Meta `path:"/warning/config/list" method:"get" tags:"APP-预警" summary:"查询单卡预警配置" x-check-permission:"cpm:iot:simWarningConfig:list,cpm:iot:simWarningConfig:view,cpm:iot:tSimWarningConfig:input:submit"` + model.AppWarningConfigListParam +} + +type WarningConfigListRes struct { + model.AppWarningConfigListModel +} + +type WarningConfigSaveReq struct { + g.Meta `path:"/warning/config/save" method:"post" tags:"APP-预警" summary:"保存单卡预警配置" x-check-permission:"cpm:iot:simWarningConfig:save,cpm:iot:tSimWarningConfig:input:submit"` + model.AppWarningConfigSaveParam +} + +type WarningConfigSaveRes struct { + model.AppWarningConfigSaveModel +} + +type WarningConfigNotifyReq struct { + g.Meta `path:"/warning/config/notify" method:"post" tags:"APP-预警" summary:"设置单卡小程序预警通知" x-check-permission:"cpm:iot:simWarningConfig:save,cpm:iot:tSimWarningConfig:input:submit"` + model.AppWarningConfigNotifyParam +} + +type WarningConfigNotifyRes struct { + model.AppWarningConfigNotifyModel +} + +type WarningConfigMetaReq struct { + g.Meta `path:"/warning/config/meta" method:"get" tags:"APP-预警" summary:"查询预警订阅配置元信息" x-check-permission:"cpm:iot:simWarningConfig:list,cpm:iot:simWarningConfig:view,cpm:iot:tSimWarningConfig:input:submit"` +} + +type WarningConfigMetaRes struct { + model.AppWarningConfigMetaModel +} + +type WarningConfigBatchSaveReq struct { + g.Meta `path:"/warning/config/batch-save" method:"post" tags:"APP-预警" summary:"批量保存单卡预警配置" x-check-permission:"cpm:iot:simWarningConfig:save,cpm:iot:tSimWarningConfig:input:submit,cpm:iot:tSimWarningConfig:range:submit"` + model.AppWarningConfigBatchSaveParam +} + +type WarningConfigBatchSaveRes struct { + model.AppWarningConfigBatchSaveModel +} + +type WarningConfigBatchNotifyReq struct { + g.Meta `path:"/warning/config/batch-notify" method:"post" tags:"APP-预警" summary:"批量设置单卡小程序预警通知" x-check-permission:"cpm:iot:simWarningConfig:save,cpm:iot:tSimWarningConfig:input:submit,cpm:iot:tSimWarningConfig:range:submit"` + model.AppWarningConfigBatchNotifyParam +} + +type WarningConfigBatchNotifyRes struct { + model.AppWarningConfigBatchNotifyModel +} diff --git a/server/api/app/v1/work_order.go b/server/api/app/v1/work_order.go new file mode 100644 index 0000000..7a74404 --- /dev/null +++ b/server/api/app/v1/work_order.go @@ -0,0 +1,44 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type WorkOrderListReq struct { + g.Meta `path:"/work-order/list" method:"get" tags:"APP-工单" summary:"当前登录用户工单列表" x-check-permission:"cpm:gen:tWoMain:list"` + model.AppWorkOrderListParam +} + +type WorkOrderListRes struct { + Items []*model.AppWorkOrderModel `json:"items" dc:"工单列表"` + Total int `json:"total" dc:"总数"` +} + +type WorkOrderDetailReq struct { + g.Meta `path:"/work-order/detail" method:"get" tags:"APP-工单" summary:"当前登录用户工单详情" x-check-permission:"cpm:gen:tWoMain:view,cpm:gen:tWoMain:list"` + model.AppWorkOrderDetailParam +} + +type WorkOrderDetailRes struct { + WorkOrder *model.AppWorkOrderModel `json:"workOrder" dc:"工单详情"` +} + +type WorkOrderCreateReq struct { + g.Meta `path:"/work-order/create" method:"post" tags:"APP-工单" summary:"提交工单" x-check-permission:"cpm:gen:tWoMain:edit"` + model.AppWorkOrderCreateParam +} + +type WorkOrderCreateRes struct { + WorkOrder *model.AppWorkOrderModel `json:"workOrder" dc:"工单详情"` +} + +type WorkOrderUploadReq struct { + g.Meta `path:"/work-order/upload" method:"post" tags:"APP-工单" summary:"上传工单附件"` + *model.SysOssUploadParam +} + +type WorkOrderUploadRes struct { + *model.SysOssUploadModel +} diff --git a/server/api/common/common.go b/server/api/common/common.go new file mode 100644 index 0000000..3d12eb0 --- /dev/null +++ b/server/api/common/common.go @@ -0,0 +1,16 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package common + +import ( + "context" + + "git.5iot.com/iotlcm-public/iotlcm-app/api/common/v1" +) + +type ICommonV1 interface { + SysOssUpload(ctx context.Context, req *v1.SysOssUploadReq) (res *v1.SysOssUploadRes, err error) + SysOssDownload(ctx context.Context, req *v1.SysOssDownloadReq) (res *v1.SysOssDownloadRes, err error) +} diff --git a/server/api/common/v1/upload.go b/server/api/common/v1/upload.go new file mode 100644 index 0000000..84e5002 --- /dev/null +++ b/server/api/common/v1/upload.go @@ -0,0 +1,29 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type SysOssUploadReq struct { + g.Meta `path:"/oss/upload" method:"post" tags:"系统-存储管理" summary:"上传文件" x-check-permission:"cpm:system.oss.upload"` + *model.SysOssUploadParam +} + +type SysOssUploadRes struct { + *model.SysOssUploadModel +} +type SysOssDownloadReq struct { + g.Meta `path:"/oss/download" method:"get" tags:"系统-存储管理" summary:"下载文件" x-check-permission:"cpm:system.oss.download"` + *model.SysOssDownloadParam +} + +type SysOssDownloadRes struct { + *model.SysOssDownloadModel +} diff --git a/server/api/gen/testdemo/testdemo.go b/server/api/gen/testdemo/testdemo.go new file mode 100644 index 0000000..ec5a9ea --- /dev/null +++ b/server/api/gen/testdemo/testdemo.go @@ -0,0 +1,19 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package testdemo + +import ( + "context" + + "git.5iot.com/iotlcm-public/iotlcm-app/api/gen/testdemo/v1" +) + +type ITestdemoV1 interface { + TestDemoList(ctx context.Context, req *v1.TestDemoListReq) (res *v1.TestDemoListRes, err error) + TestDemoExport(ctx context.Context, req *v1.TestDemoExportReq) (res *v1.TestDemoExportRes, err error) + TestDemoView(ctx context.Context, req *v1.TestDemoViewReq) (res *v1.TestDemoViewRes, err error) + TestDemoEdit(ctx context.Context, req *v1.TestDemoEditReq) (res *v1.TestDemoEditRes, err error) + TestDemoDelete(ctx context.Context, req *v1.TestDemoDeleteReq) (res *v1.TestDemoDeleteRes, err error) +} diff --git a/server/api/gen/testdemo/v1/testdemo.go b/server/api/gen/testdemo/v1/testdemo.go new file mode 100644 index 0000000..594e36d --- /dev/null +++ b/server/api/gen/testdemo/v1/testdemo.go @@ -0,0 +1,59 @@ +// Package testdemo +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +// @AutoGenerate Version +package v1 //testdemo + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/genin" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +// ListReq 查询测试单表列表 +type TestDemoListReq struct { + g.Meta `path:"/testDemo/list" method:"get" tags:"测试单表" summary:"获取测试单表列表" x-check-permission:"cpm:gen:testDemo:list"` + genin.TestDemoListParam +} + +type TestDemoListRes struct { + response.PageResult + Items []*genin.TestDemoListModel `json:"items" dc:"数据列表"` +} + +// ExportReq 导出测试单表列表 +type TestDemoExportReq struct { + g.Meta `path:"/testDemo/export" method:"post" tags:"测试单表" summary:"导出测试单表列表" x-check-permission:"cpm:gen:testDemo:export"` + genin.TestDemoListParam +} + +type TestDemoExportRes struct{} + +// ViewReq 获取测试单表指定信息 +type TestDemoViewReq struct { + g.Meta `path:"/testDemo/view" method:"get" tags:"测试单表" summary:"获取测试单表指定信息" x-check-permission:"cpm:gen:testDemo:view"` + genin.TestDemoViewParam +} + +type TestDemoViewRes struct { + *genin.TestDemoViewModel +} + +// EditReq 修改/新增测试单表 +type TestDemoEditReq struct { + g.Meta `path:"/testDemo/edit" method:"post" tags:"测试单表" summary:"修改/新增测试单表" x-check-permission:"cpm:gen:testDemo:edit"` + genin.TestDemoEditParam +} + +type TestDemoEditRes struct{} + +// DeleteReq 删除测试单表 +type TestDemoDeleteReq struct { + g.Meta `path:"/testDemo/delete" method:"post" tags:"测试单表" summary:"删除测试单表" x-check-permission:"cpm:gen:testDemo:delete"` + genin.TestDemoDeleteParam +} + +type TestDemoDeleteRes struct{} diff --git a/server/api/gen/testtree/testtree.go b/server/api/gen/testtree/testtree.go new file mode 100644 index 0000000..d34955d --- /dev/null +++ b/server/api/gen/testtree/testtree.go @@ -0,0 +1,20 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package testtree + +import ( + "context" + + "git.5iot.com/iotlcm-public/iotlcm-app/api/gen/testtree/v1" +) + +type ITesttreeV1 interface { + TestTreeList(ctx context.Context, req *v1.TestTreeListReq) (res *v1.TestTreeListRes, err error) + TestTreeExport(ctx context.Context, req *v1.TestTreeExportReq) (res *v1.TestTreeExportRes, err error) + TestTreeView(ctx context.Context, req *v1.TestTreeViewReq) (res *v1.TestTreeViewRes, err error) + TestTreeEdit(ctx context.Context, req *v1.TestTreeEditReq) (res *v1.TestTreeEditRes, err error) + TestTreeDelete(ctx context.Context, req *v1.TestTreeDeleteReq) (res *v1.TestTreeDeleteRes, err error) + TestTreeTreeOption(ctx context.Context, req *v1.TestTreeTreeOptionReq) (res *v1.TestTreeTreeOptionRes, err error) +} diff --git a/server/api/gen/testtree/v1/testtree.go b/server/api/gen/testtree/v1/testtree.go new file mode 100644 index 0000000..014f868 --- /dev/null +++ b/server/api/gen/testtree/v1/testtree.go @@ -0,0 +1,67 @@ +// Package testtree +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2026 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +// @AutoGenerate Version +package v1 //testtree + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/genin" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/utility/tree" + + "github.com/gogf/gf/v2/frame/g" +) + +// ListReq 查询测试树表列表 +type TestTreeListReq struct { + g.Meta `path:"/testTree/list" method:"get" tags:"测试树表" summary:"获取测试树表列表" x-check-permission:"cpm:gen:testTree:list"` + genin.TestTreeListParam +} + +type TestTreeListRes struct { + response.PageResult + Items []*genin.TestTreeListModel `json:"items" dc:"数据列表"` +} + +// ExportReq 导出测试树表列表 +type TestTreeExportReq struct { + g.Meta `path:"/testTree/export" method:"post" tags:"测试树表" summary:"导出测试树表列表" x-check-permission:"cpm:gen:testTree:export"` + genin.TestTreeListParam +} + +type TestTreeExportRes struct{} + +// ViewReq 获取测试树表指定信息 +type TestTreeViewReq struct { + g.Meta `path:"/testTree/view" method:"get" tags:"测试树表" summary:"获取测试树表指定信息" x-check-permission:"cpm:gen:testTree:view"` + genin.TestTreeViewParam +} + +type TestTreeViewRes struct { + *genin.TestTreeViewModel +} + +// EditReq 修改/新增测试树表 +type TestTreeEditReq struct { + g.Meta `path:"/testTree/edit" method:"post" tags:"测试树表" summary:"修改/新增测试树表" x-check-permission:"cpm:gen:testTree:edit"` + genin.TestTreeEditParam +} + +type TestTreeEditRes struct{} + +// DeleteReq 删除测试树表 +type TestTreeDeleteReq struct { + g.Meta `path:"/testTree/delete" method:"post" tags:"测试树表" summary:"删除测试树表" x-check-permission:"cpm:gen:testTree:delete"` + genin.TestTreeDeleteParam +} + +type TestTreeDeleteRes struct{} + +// TreeOptionReq 获取测试树表关系树选项 +type TestTreeTreeOptionReq struct { + g.Meta `path:"/testTree/treeOption" method:"get" tags:"测试树表" summary:"获取测试树表关系树选项" x-check-permission:"cpm:gen:testTree:treeOption"` +} + +type TestTreeTreeOptionRes []tree.Node diff --git a/server/api/gen_codes/gen_codes.go b/server/api/gen_codes/gen_codes.go new file mode 100644 index 0000000..b26db3a --- /dev/null +++ b/server/api/gen_codes/gen_codes.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package gen_codes + +import ( + "context" + + "git.5iot.com/iotlcm-public/iotlcm-app/api/gen_codes/v1" +) + +type IGenCodesV1 interface { + SysGenTableList(ctx context.Context, req *v1.SysGenTableListReq) (res *v1.SysGenTableListRes, err error) + SysGenTableView(ctx context.Context, req *v1.SysGenTableViewReq) (res *v1.SysGenTableViewRes, err error) + SysGenTableAdd(ctx context.Context, req *v1.SysGenTableAddReq) (res *v1.SysGenTableAddRes, err error) + SysGenTableEdit(ctx context.Context, req *v1.SysGenTableEditReq) (res *v1.SysGenTableEditRes, err error) + SysGenTableDelete(ctx context.Context, req *v1.SysGenTableDeleteReq) (res *v1.SysGenTableDeleteRes, err error) + SysGenTableSelects(ctx context.Context, req *v1.SysGenTableSelectsReq) (res *v1.SysGenTableSelectsRes, err error) + SysGenTableTableSelect(ctx context.Context, req *v1.SysGenTableTableSelectReq) (res *v1.SysGenTableTableSelectRes, err error) + SysGenTableColumnSelect(ctx context.Context, req *v1.SysGenTableColumnSelectReq) (res *v1.SysGenTableColumnSelectRes, err error) + SysGenTableColumnList(ctx context.Context, req *v1.SysGenTableColumnListReq) (res *v1.SysGenTableColumnListRes, err error) + SysGenTablePreview(ctx context.Context, req *v1.SysGenTablePreviewReq) (res *v1.SysGenTablePreviewRes, err error) + SysGenTableBuild(ctx context.Context, req *v1.SysGenTableBuildReq) (res *v1.SysGenTableBuildRes, err error) +} diff --git a/server/api/gen_codes/v1/gen_table.go b/server/api/gen_codes/v1/gen_table.go new file mode 100644 index 0000000..48a850d --- /dev/null +++ b/server/api/gen_codes/v1/gen_table.go @@ -0,0 +1,112 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type SysGenTableListReq struct { + g.Meta `path:"/genTable/list" method:"get" tags:"工具-代码生成" summary:"获取代码生成列表" x-check-permission:"cpm:tool:gen:list"` + *model.SysGenTableListParam +} + +type SysGenTableListRes struct { + response.PageResult + Items []*model.SysGenTableListModel `json:"items"` +} + +type SysGenTableViewReq struct { + g.Meta `path:"/genTable/view" method:"get" tags:"工具-代码生成" summary:"获取代码生成详情" x-check-permission:"cpm:tool:gen:query"` + *model.SysGenTableViewParam +} + +type SysGenTableViewRes struct { + *model.SysGenTableViewModel +} + +type SysGenTableAddReq struct { + g.Meta `path:"/genTable/add" method:"post" tags:"工具-代码生成" summary:"新增代码生成" x-check-permission:"cpm:tool:gen:import"` + *model.SysGenTableAddParam +} + +type SysGenTableAddRes struct { + *model.SysGenTableAddModel +} + +type SysGenTableEditReq struct { + g.Meta `path:"/genTable/edit" method:"post" tags:"工具-代码生成" summary:"修改代码生成" x-check-permission:"cpm:tool:gen:edit"` + *model.SysGenTableEditParam +} + +type SysGenTableEditRes struct { + *model.SysGenTableEditModel +} + +type SysGenTableDeleteReq struct { + g.Meta `path:"/genTable/delete" method:"post" tags:"工具-代码生成" summary:"删除代码生成" x-check-permission:"cpm:tool:gen:remove"` + *model.SysGenTableDeleteParam +} + +type SysGenTableDeleteRes struct { + *model.SysGenTableDeleteModel +} + +type SysGenTableSelectsReq struct { + g.Meta `path:"/genTable/selects" method:"get" tags:"工具-代码生成" summary:"获取代码生成选择项" x-check-permission:"cpm:tool:gen:list"` +} + +type SysGenTableSelectsRes struct { + *model.SelectsModel +} + +type SysGenTableTableSelectReq struct { + g.Meta `path:"/genTable/tableSelect" method:"get" tags:"工具-代码生成" summary:"获取代码生成表选择项" x-check-permission:"cpm:tool:gen:list"` + *model.GenCodesTableSelectParam +} + +type SysGenTableTableSelectRes struct { + Items []*model.GenCodesTableSelectModel `json:"items"` +} + +type SysGenTableColumnSelectReq struct { + g.Meta `path:"/genTable/columnSelect" method:"get" tags:"工具-代码生成" summary:"获取代码生成字段选择项" x-check-permission:"cpm:tool:gen:list"` + *model.GenCodesColumnSelectParam +} + +type SysGenTableColumnSelectRes struct { + Items []*model.GenCodesColumnSelectModel `json:"items"` +} + +type SysGenTableColumnListReq struct { + g.Meta `path:"/genTable/columnList" method:"get" tags:"工具-代码生成" summary:"获取代码生成字段列表" x-check-permission:"cpm:tool:gen:list"` + *model.GenCodesColumnListParam +} + +type SysGenTableColumnListRes struct { + Items []*model.GenCodesColumnListModel `json:"items"` +} + +type SysGenTablePreviewReq struct { + g.Meta `path:"/genTable/preview" method:"post" tags:"工具-代码生成" summary:"生成预览" x-check-permission:"cpm:tool:gen:preview"` + *model.GenCodesPreviewParam +} + +type SysGenTablePreviewRes struct { + *model.GenCodesPreviewModel +} + +type SysGenTableBuildReq struct { + g.Meta `path:"/genTable/build" method:"post" tags:"工具-代码生成" summary:"构建" x-check-permission:"cpm:tool:gen:code"` + *model.GenCodesBuildParam +} + +type SysGenTableBuildRes struct { + *model.GenCodesBuildModel +} diff --git a/server/api/info/info.go b/server/api/info/info.go new file mode 100644 index 0000000..9ae3d2c --- /dev/null +++ b/server/api/info/info.go @@ -0,0 +1,15 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package info + +import ( + "context" + + "git.5iot.com/iotlcm-public/iotlcm-app/api/info/v1" +) + +type IInfoV1 interface { + Info(ctx context.Context, req *v1.InfoReq) (res *v1.InfoRes, err error) +} diff --git a/server/api/info/v1/info.go b/server/api/info/v1/info.go new file mode 100644 index 0000000..9169db6 --- /dev/null +++ b/server/api/info/v1/info.go @@ -0,0 +1,15 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +type InfoReq struct { + g.Meta `path:"/info" method:"get" tags:"服务信息" summary:"服务信息"` +} + +type InfoRes struct { + *model.InfoModel +} diff --git a/server/api/monitor/monitor.go b/server/api/monitor/monitor.go new file mode 100644 index 0000000..c5a67eb --- /dev/null +++ b/server/api/monitor/monitor.go @@ -0,0 +1,15 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package monitor + +import ( + "context" + + "git.5iot.com/iotlcm-public/iotlcm-app/api/monitor/v1" +) + +type IMonitorV1 interface { + RedisInfo(ctx context.Context, req *v1.RedisInfoReq) (res *v1.RedisInfoRes, err error) +} diff --git a/server/api/monitor/v1/cache.go b/server/api/monitor/v1/cache.go new file mode 100644 index 0000000..976fdcd --- /dev/null +++ b/server/api/monitor/v1/cache.go @@ -0,0 +1,25 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "github.com/gogf/gf/v2/frame/g" +) + +type RedisInfoReq struct { + g.Meta `path:"/cache" method:"get" tags:"监控-缓存监控" summary:"获取缓存监控信息" x-check-permission:"cpm:monitor:cache:list"` +} + +type RedisInfoRes struct { + DBSize int `json:"dbSize"` + CommandStats []*CommandStats `json:"commandStats"` + Info map[string]string `json:"info"` +} + +type CommandStats struct { + Name string `json:"name"` + Value string `json:"value"` +} diff --git a/server/api/system/system.go b/server/api/system/system.go new file mode 100644 index 0000000..176423d --- /dev/null +++ b/server/api/system/system.go @@ -0,0 +1,134 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package system + +import ( + "context" + + "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" +) + +type ISystemV1 interface { + Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error) + RefreshToken(ctx context.Context, req *v1.RefreshTokenReq) (res *v1.RefreshTokenRes, err error) + Logout(ctx context.Context, req *v1.LogoutReq) (res *v1.LogoutRes, err error) + GetAccessCodes(ctx context.Context, req *v1.GetAccessCodesReq) (res *v1.GetAccessCodesRes, err error) + TenantList(ctx context.Context, req *v1.TenantListReq) (res *v1.TenantListRes, err error) + GetCaptcha(ctx context.Context, req *v1.GetCaptchaReq) (res *v1.GetCaptchaRes, err error) + SysClientList(ctx context.Context, req *v1.SysClientListReq) (res *v1.SysClientListRes, err error) + SysClientView(ctx context.Context, req *v1.SysClientViewReq) (res *v1.SysClientViewRes, err error) + SysClientAdd(ctx context.Context, req *v1.SysClientAddReq) (res *v1.SysClientAddRes, err error) + SysClientEdit(ctx context.Context, req *v1.SysClientEditReq) (res *v1.SysClientEditRes, err error) + SysClientDelete(ctx context.Context, req *v1.SysClientDeleteReq) (res *v1.SysClientDeleteRes, err error) + SysClientStatus(ctx context.Context, req *v1.SysClientStatusReq) (res *v1.SysClientStatusRes, err error) + GetConfigList(ctx context.Context, req *v1.GetConfigListReq) (res *v1.GetConfigListRes, err error) + AddConfig(ctx context.Context, req *v1.AddConfigReq) (res *v1.AddConfigRes, err error) + EditConfig(ctx context.Context, req *v1.EditConfigReq) (res *v1.EditConfigRes, err error) + DeleteConfig(ctx context.Context, req *v1.DeleteConfigReq) (res *v1.DeleteConfigRes, err error) + GetConfigById(ctx context.Context, req *v1.GetConfigByIdReq) (res *v1.GetConfigByIdRes, err error) + DeptList(ctx context.Context, req *v1.DeptListReq) (res *v1.DeptListRes, err error) + DeptTree(ctx context.Context, req *v1.DeptTreeReq) (res *v1.DeptTreeRes, err error) + DeptAdd(ctx context.Context, req *v1.DeptAddReq) (res *v1.DeptAddRes, err error) + DeptEdit(ctx context.Context, req *v1.DeptEditReq) (res *v1.DeptEditRes, err error) + DeptDelete(ctx context.Context, req *v1.DeptDeleteReq) (res *v1.DeptDeleteRes, err error) + DeptView(ctx context.Context, req *v1.DeptViewReq) (res *v1.DeptViewRes, err error) + SysDictTypeList(ctx context.Context, req *v1.SysDictTypeListReq) (res *v1.SysDictTypeListRes, err error) + SysDictTypeAdd(ctx context.Context, req *v1.SysDictTypeAddReq) (res *v1.SysDictTypeAddRes, err error) + SysDictTypeEdit(ctx context.Context, req *v1.SysDictTypeEditReq) (res *v1.SysDictTypeEditRes, err error) + SysDictTypeDelete(ctx context.Context, req *v1.SysDictTypeDeleteReq) (res *v1.SysDictTypeDeleteRes, err error) + SysDictTypeView(ctx context.Context, req *v1.SysDictTypeViewReq) (res *v1.SysDictTypeViewRes, err error) + SysDictDataList(ctx context.Context, req *v1.SysDictDataListReq) (res *v1.SysDictDataListRes, err error) + SysDictDataAdd(ctx context.Context, req *v1.SysDictDataAddReq) (res *v1.SysDictDataAddRes, err error) + SysDictDataEdit(ctx context.Context, req *v1.SysDictDataEditReq) (res *v1.SysDictDataEditRes, err error) + SysDictDataDelete(ctx context.Context, req *v1.SysDictDataDeleteReq) (res *v1.SysDictDataDeleteRes, err error) + SysDictDataView(ctx context.Context, req *v1.SysDictDataViewReq) (res *v1.SysDictDataViewRes, err error) + SysJobList(ctx context.Context, req *v1.SysJobListReq) (res *v1.SysJobListRes, err error) + SysJobView(ctx context.Context, req *v1.SysJobViewReq) (res *v1.SysJobViewRes, err error) + SysJobAdd(ctx context.Context, req *v1.SysJobAddReq) (res *v1.SysJobAddRes, err error) + SysJobUpdate(ctx context.Context, req *v1.SysJobUpdateReq) (res *v1.SysJobUpdateRes, err error) + SysJobUpdateStatus(ctx context.Context, req *v1.SysJobUpdateStatusReq) (res *v1.SysJobUpdateStatusRes, err error) + SysJobDelete(ctx context.Context, req *v1.SysJobDeleteReq) (res *v1.SysJobDeleteRes, err error) + SysJobExec(ctx context.Context, req *v1.SysJobExecReq) (res *v1.SysJobExecRes, err error) + ListLogininfor(ctx context.Context, req *v1.ListLogininforReq) (res *v1.ListLogininforRes, err error) + DeleteLogininfor(ctx context.Context, req *v1.DeleteLogininforReq) (res *v1.DeleteLogininforRes, err error) + MenuAll(ctx context.Context, req *v1.MenuAllReq) (res *v1.MenuAllRes, err error) + MenuList(ctx context.Context, req *v1.MenuListReq) (res *v1.MenuListRes, err error) + MenuView(ctx context.Context, req *v1.MenuViewReq) (res *v1.MenuViewRes, err error) + MenuAdd(ctx context.Context, req *v1.MenuAddReq) (res *v1.MenuAddRes, err error) + MenuUpdate(ctx context.Context, req *v1.MenuUpdateReq) (res *v1.MenuUpdateRes, err error) + MenuDelete(ctx context.Context, req *v1.MenuDeleteReq) (res *v1.MenuDeleteRes, err error) + MenuTree(ctx context.Context, req *v1.MenuTreeReq) (res *v1.MenuTreeRes, err error) + SysNoticeList(ctx context.Context, req *v1.SysNoticeListReq) (res *v1.SysNoticeListRes, err error) + SysNoticeAdd(ctx context.Context, req *v1.SysNoticeAddReq) (res *v1.SysNoticeAddRes, err error) + SysNoticeEdit(ctx context.Context, req *v1.SysNoticeEditReq) (res *v1.SysNoticeEditRes, err error) + SysNoticeDelete(ctx context.Context, req *v1.SysNoticeDeleteReq) (res *v1.SysNoticeDeleteRes, err error) + SysNoticeView(ctx context.Context, req *v1.SysNoticeViewReq) (res *v1.SysNoticeViewRes, err error) + SysUserOnlineList(ctx context.Context, req *v1.SysUserOnlineListReq) (res *v1.SysUserOnlineListRes, err error) + SysUserOnlineDelete(ctx context.Context, req *v1.SysUserOnlineDeleteReq) (res *v1.SysUserOnlineDeleteRes, err error) + GetOperLogList(ctx context.Context, req *v1.GetOperLogListReq) (res *v1.GetOperLogListRes, err error) + SysOssList(ctx context.Context, req *v1.SysOssListReq) (res *v1.SysOssListRes, err error) + SysOssView(ctx context.Context, req *v1.SysOssViewReq) (res *v1.SysOssViewRes, err error) + SysOssDelete(ctx context.Context, req *v1.SysOssDeleteReq) (res *v1.SysOssDeleteRes, err error) + SysOssConfigList(ctx context.Context, req *v1.SysOssConfigListReq) (res *v1.SysOssConfigListRes, err error) + SysOssConfigView(ctx context.Context, req *v1.SysOssConfigViewReq) (res *v1.SysOssConfigViewRes, err error) + SysOssConfigAdd(ctx context.Context, req *v1.SysOssConfigAddReq) (res *v1.SysOssConfigAddRes, err error) + SysOssConfigEdit(ctx context.Context, req *v1.SysOssConfigEditReq) (res *v1.SysOssConfigEditRes, err error) + SysOssConfigDelete(ctx context.Context, req *v1.SysOssConfigDeleteReq) (res *v1.SysOssConfigDeleteRes, err error) + PostList(ctx context.Context, req *v1.PostListReq) (res *v1.PostListRes, err error) + PostAdd(ctx context.Context, req *v1.PostAddReq) (res *v1.PostAddRes, err error) + PostEdit(ctx context.Context, req *v1.PostEditReq) (res *v1.PostEditRes, err error) + PostDelete(ctx context.Context, req *v1.PostDeleteReq) (res *v1.PostDeleteRes, err error) + PostView(ctx context.Context, req *v1.PostViewReq) (res *v1.PostViewRes, err error) + PostExport(ctx context.Context, req *v1.PostExportReq) (res *v1.PostExportRes, err error) + QrcodeLogin(ctx context.Context, req *v1.QrcodeLoginReq) (res *v1.QrcodeLoginRes, err error) + QrcodeLoginStatus(ctx context.Context, req *v1.QrcodeLoginStatusReq) (res *v1.QrcodeLoginStatusRes, err error) + QrcodeBind(ctx context.Context, req *v1.QrcodeBindReq) (res *v1.QrcodeBindRes, err error) + QrcodeBindStatus(ctx context.Context, req *v1.QrcodeBindStatusReq) (res *v1.QrcodeBindStatusRes, err error) + QrcodeScanCallback(ctx context.Context, req *v1.QrcodeScanCallbackReq) (res *v1.QrcodeScanCallbackRes, err error) + QrcodeRegisterAndBindOpenId(ctx context.Context, req *v1.QrcodeRegisterAndBindOpenIdReq) (res *v1.QrcodeRegisterAndBindOpenIdRes, err error) + QrcodeLoginAndBindOpenId(ctx context.Context, req *v1.QrcodeLoginAndBindOpenIdReq) (res *v1.QrcodeLoginAndBindOpenIdRes, err error) + QrcodeLoginSelectUser(ctx context.Context, req *v1.QrcodeLoginSelectUserReq) (res *v1.QrcodeLoginSelectUserRes, err error) + RoleList(ctx context.Context, req *v1.RoleListReq) (res *v1.RoleListRes, err error) + RoleAdd(ctx context.Context, req *v1.RoleAddReq) (res *v1.RoleAddRes, err error) + RoleEdit(ctx context.Context, req *v1.RoleEditReq) (res *v1.RoleEditRes, err error) + RoleDelete(ctx context.Context, req *v1.RoleDeleteReq) (res *v1.RoleDeleteRes, err error) + RoleView(ctx context.Context, req *v1.RoleViewReq) (res *v1.RoleViewRes, err error) + RoleDataScopeEdit(ctx context.Context, req *v1.RoleDataScopeEditReq) (res *v1.RoleDataScopeEditRes, err error) + SysSocialList(ctx context.Context, req *v1.SysSocialListReq) (res *v1.SysSocialListRes, err error) + SysSocialDelete(ctx context.Context, req *v1.SysSocialDeleteReq) (res *v1.SysSocialDeleteRes, err error) + SysNoticeUserList(ctx context.Context, req *v1.SysNoticeUserListReq) (res *v1.SysNoticeUserListRes, err error) + SysNoticeUserExport(ctx context.Context, req *v1.SysNoticeUserExportReq) (res *v1.SysNoticeUserExportRes, err error) + SysNoticeUserView(ctx context.Context, req *v1.SysNoticeUserViewReq) (res *v1.SysNoticeUserViewRes, err error) + SysNoticeUserEdit(ctx context.Context, req *v1.SysNoticeUserEditReq) (res *v1.SysNoticeUserEditRes, err error) + SysNoticeUserDelete(ctx context.Context, req *v1.SysNoticeUserDeleteReq) (res *v1.SysNoticeUserDeleteRes, err error) + SysNoticeUserStatus(ctx context.Context, req *v1.SysNoticeUserStatusReq) (res *v1.SysNoticeUserStatusRes, err error) + SysNoticeUserRead(ctx context.Context, req *v1.SysNoticeUserReadReq) (res *v1.SysNoticeUserReadRes, err error) + SysTenantList(ctx context.Context, req *v1.SysTenantListReq) (res *v1.SysTenantListRes, err error) + SysTenantAdd(ctx context.Context, req *v1.SysTenantAddReq) (res *v1.SysTenantAddRes, err error) + SysTenantEdit(ctx context.Context, req *v1.SysTenantEditReq) (res *v1.SysTenantEditRes, err error) + SysTenantDelete(ctx context.Context, req *v1.SysTenantDeleteReq) (res *v1.SysTenantDeleteRes, err error) + SysTenantStatus(ctx context.Context, req *v1.SysTenantStatusReq) (res *v1.SysTenantStatusRes, err error) + SysTenantView(ctx context.Context, req *v1.SysTenantViewReq) (res *v1.SysTenantViewRes, err error) + SysTenantSyncMenu(ctx context.Context, req *v1.SysTenantSyncMenuReq) (res *v1.SysTenantSyncMenuRes, err error) + SysTenantPackageList(ctx context.Context, req *v1.SysTenantPackageListReq) (res *v1.SysTenantPackageListRes, err error) + SysTenantPackageView(ctx context.Context, req *v1.SysTenantPackageViewReq) (res *v1.SysTenantPackageViewRes, err error) + SysTenantPackageStatus(ctx context.Context, req *v1.SysTenantPackageStatusReq) (res *v1.SysTenantPackageStatusRes, err error) + SysTenantPackageAdd(ctx context.Context, req *v1.SysTenantPackageAddReq) (res *v1.SysTenantPackageAddRes, err error) + SysTenantPackageEdit(ctx context.Context, req *v1.SysTenantPackageEditReq) (res *v1.SysTenantPackageEditRes, err error) + SysTenantPackageDelete(ctx context.Context, req *v1.SysTenantPackageDeleteReq) (res *v1.SysTenantPackageDeleteRes, err error) + UserInfo(ctx context.Context, req *v1.UserInfoReq) (res *v1.UserInfoRes, err error) + UserList(ctx context.Context, req *v1.UserListReq) (res *v1.UserListRes, err error) + AddUser(ctx context.Context, req *v1.AddUserReq) (res *v1.AddUserRes, err error) + UserProfile(ctx context.Context, req *v1.UserProfileReq) (res *v1.UserProfileRes, err error) + UpdateCurrentUser(ctx context.Context, req *v1.UpdateCurrentUserReq) (res *v1.UpdateCurrentUserRes, err error) + UpdateCurrentUserPassword(ctx context.Context, req *v1.UpdateCurrentUserPasswordReq) (res *v1.UpdateCurrentUserPasswordRes, err error) + UpdateUser(ctx context.Context, req *v1.UpdateUserReq) (res *v1.UpdateUserRes, err error) + DeleteUser(ctx context.Context, req *v1.DeleteUserReq) (res *v1.DeleteUserRes, err error) + GetUser(ctx context.Context, req *v1.GetUserReq) (res *v1.GetUserRes, err error) + ResetPassword(ctx context.Context, req *v1.ResetPasswordReq) (res *v1.ResetPasswordRes, err error) + GetUserListByDeptId(ctx context.Context, req *v1.GetUserListByDeptIdReq) (res *v1.GetUserListByDeptIdRes, err error) + UserRegister(ctx context.Context, req *v1.UserRegisterReq) (res *v1.UserRegisterRes, err error) + UserStatus(ctx context.Context, req *v1.UserStatusReq) (res *v1.UserStatusRes, err error) +} diff --git a/server/api/system/v1/auth.go b/server/api/system/v1/auth.go new file mode 100644 index 0000000..e0a0365 --- /dev/null +++ b/server/api/system/v1/auth.go @@ -0,0 +1,72 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "github.com/gogf/gf/v2/frame/g" +) + +type LoginReq struct { + g.Meta `path:"/auth/login" method:"post" tags:"系统-授权" summary:"登录"` + TenantId string `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"` + Username string `json:"username" v:"required#用户名不能为空" dc:"用户名"` + Password string `json:"password" v:"required#密码不能为空" dc:"密码"` + CaptchaID string `json:"captchaID" dc:"验证码ID"` + CaptchaValue string `json:"captchaValue" v:"required#验证码不能为空" dc:"验证码"` +} + +type LoginRes struct { + ID int64 `json:"id" dc:"用户ID"` + Username string `json:"username" dc:"用户名"` + NickName string `json:"nickName" dc:"昵称"` + Avatar string `json:"avatar" dc:"头像"` + AccessToken string `json:"accessToken" dc:"访问令牌"` +} + +type RefreshTokenReq struct { + g.Meta `path:"/auth/refresh" method:"post" tags:"系统-授权" summary:"刷新访问令牌"` + WithCredentials bool `json:"withCredentials" dc:"是否携带凭证"` +} + +type RefreshTokenRes struct { + Data string `json:"data" dc:"新的访问令牌"` + Status int `json:"status" dc:"状态码"` +} + +type LogoutReq struct { + g.Meta `path:"/auth/logout" method:"post" tags:"系统-授权" summary:"退出登录"` + WithCredentials bool `json:"withCredentials" dc:"是否携带凭证"` +} + +type LogoutRes struct { + Data string `json:"data" dc:"退出登录"` + Status int `json:"status" dc:"状态码"` +} + +type GetAccessCodesReq struct { + g.Meta `path:"/auth/codes" method:"get" tags:"系统-授权" summary:"获取用户权限码"` + IsCache bool `json:"isCache" dc:"是否从缓存中获取"` +} + +type GetAccessCodesRes struct { + Data []string `json:"data" dc:"用户权限码"` + Status int `json:"status" dc:"状态码"` +} + +type TenantListReq struct { + g.Meta `path:"/auth/getTenantList" method:"get" tags:"系统-授权" summary:"获取租户列表"` +} + +type TenantListData struct { + CompanyName string `json:"companyName" dc:"企业名称"` + Domain string `json:"domain" dc:"域名"` + TenantId string `json:"tenantId" dc:"租户ID"` +} + +type TenantListRes struct { + TenantEnabled bool `json:"tenantEnabled" dc:"是否启用租户"` + VoList []TenantListData `json:"voList" dc:"租户列表"` +} diff --git a/server/api/system/v1/captcha.go b/server/api/system/v1/captcha.go new file mode 100644 index 0000000..979a188 --- /dev/null +++ b/server/api/system/v1/captcha.go @@ -0,0 +1,18 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import "github.com/gogf/gf/v2/frame/g" + +// 获取验证码 +type GetCaptchaReq struct { + g.Meta `path:"/captcha" method:"get" tags:"系统-授权" summary:"获取验证码"` +} + +type GetCaptchaRes struct { + CaptchaID string `json:"captchaID" dc:"验证码ID"` + CaptchaImage string `json:"captchaImage" dc:"验证码图片"` +} diff --git a/server/api/system/v1/client.go b/server/api/system/v1/client.go new file mode 100644 index 0000000..273023c --- /dev/null +++ b/server/api/system/v1/client.go @@ -0,0 +1,68 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type SysClientListReq struct { + g.Meta `path:"/client/list" method:"get" tags:"系统-客户端管理" summary:"获取客户端列表" x-check-permission:"cpm:system:client:list"` + model.SysClientListParam +} + +type SysClientListRes struct { + Items []*model.SysClientListModel `json:"items"` + response.PageResult +} + +type SysClientViewReq struct { + g.Meta `path:"/client/view" method:"get" tags:"系统-客户端管理" summary:"获取客户端详情" x-check-permission:"cpm:system:client:query"` + *model.SysClientViewParam +} + +type SysClientViewRes struct { + *model.SysClientViewModel +} + +type SysClientAddReq struct { + g.Meta `path:"/client/add" method:"post" tags:"系统-客户端管理" summary:"添加客户端" x-check-permission:"cpm:system:client:add"` + *model.SysClientAddParam +} + +type SysClientAddRes struct { + *model.SysClientAddModel +} + +type SysClientEditReq struct { + g.Meta `path:"/client/edit" method:"post" tags:"系统-客户端管理" summary:"编辑客户端" x-check-permission:"cpm:system:client:edit"` + *model.SysClientEditParam +} + +type SysClientEditRes struct { + *model.SysClientEditModel +} + +type SysClientDeleteReq struct { + g.Meta `path:"/client/delete" method:"post" tags:"系统-客户端管理" summary:"删除客户端" x-check-permission:"cpm:system:client:remove"` + *model.SysClientDeleteParam +} + +type SysClientDeleteRes struct { + *model.SysClientDeleteModel +} + +type SysClientStatusReq struct { + g.Meta `path:"/client/status" method:"post" tags:"系统-客户端管理" summary:"修改客户端状态" x-check-permission:"cpm:system:client:edit"` + *model.SysClientStatusParam +} + +type SysClientStatusRes struct { + *model.SysClientStatusModel +} diff --git a/server/api/system/v1/config.go b/server/api/system/v1/config.go new file mode 100644 index 0000000..de44ba6 --- /dev/null +++ b/server/api/system/v1/config.go @@ -0,0 +1,59 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type GetConfigListReq struct { + g.Meta `path:"/config/list" method:"get" tags:"系统-配置管理" summary:"获取系统配置列表" x-check-permission:"cpm:system:config:list"` + model.SysConfigListParam +} + +type GetConfigListRes struct { + response.PageResult + Data []*model.SysConfigListModel `json:"items" dc:"系统配置列表"` +} + +type AddConfigReq struct { + g.Meta `path:"/config/add" method:"post" tags:"系统-配置管理" summary:"添加系统配置" x-check-permission:"cpm:system:config:add"` + model.SysConfigAddParam +} + +type AddConfigRes struct { + model.SysConfigAddModel +} + +type EditConfigReq struct { + g.Meta `path:"/config/edit" method:"post" tags:"系统-配置管理" summary:"编辑系统配置" x-check-permission:"cpm:system:config:edit"` + model.SysConfigEditParam +} + +type EditConfigRes struct { + model.SysConfigEditModel +} + +type DeleteConfigReq struct { + g.Meta `path:"/config/delete" method:"post" tags:"系统-配置管理" summary:"删除系统配置" x-check-permission:"cpm:system:config:remove"` + model.SysConfigDeleteParam +} + +type DeleteConfigRes struct { + model.SysConfigDeleteModel +} + +type GetConfigByIdReq struct { + g.Meta `path:"/config/view" method:"get" tags:"系统-配置管理" summary:"获取系统配置详情" x-check-permission:"cpm:system:config:query"` + model.SysConfigViewParam +} + +type GetConfigByIdRes struct { + model.SysConfigViewModel +} diff --git a/server/api/system/v1/dept.go b/server/api/system/v1/dept.go new file mode 100644 index 0000000..16ffc34 --- /dev/null +++ b/server/api/system/v1/dept.go @@ -0,0 +1,70 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type DeptListReq struct { + g.Meta `path:"/dept/list" method:"get" tags:"系统-部门管理" summary:"获取部门列表" x-check-permission:"cpm:system:dept:list"` + request.PageInfo + model.SysDeptListParam +} + +type DeptListRes struct { + response.PageResult + Data []*model.SysDeptListModel `json:"items" dc:"部门列表"` +} + +type DeptTreeReq struct { + g.Meta `path:"/dept/tree" method:"get" tags:"系统-部门管理" summary:"获取部门树" x-check-permission:"cpm:system:dept:list"` + model.SysDeptTreeParam +} + +type DeptTreeRes struct { + Data []*model.SysDeptTreeModel `json:"items" dc:"部门树"` +} + +type DeptAddReq struct { + g.Meta `path:"/dept/add" method:"post" tags:"系统-部门管理" summary:"新增部门" x-check-permission:"cpm:system:dept:add"` + model.SysDeptAddModel +} + +type DeptAddRes struct { + DeptId int64 `json:"deptId" dc:"部门id"` +} + +type DeptEditReq struct { + g.Meta `path:"/dept/edit" method:"post" tags:"系统-部门管理" summary:"编辑部门" x-check-permission:"cpm:system:dept:edit"` + model.SysDeptEditModel +} + +type DeptEditRes struct { + DeptId int64 `json:"deptId" dc:"部门id"` +} + +type DeptDeleteReq struct { + g.Meta `path:"/dept/delete" method:"post" tags:"系统-部门管理" summary:"删除部门" x-check-permission:"cpm:system:dept:remove"` + model.SysDeptDeleteModel +} + +type DeptDeleteRes struct { + DeptId int64 `json:"deptId" dc:"部门id"` +} + +type DeptViewReq struct { + g.Meta `path:"/dept/view" method:"get" tags:"系统-部门管理" summary:"获取部门详情" x-check-permission:"cpm:system:dept:query"` + model.SysDeptViewModel +} + +type DeptViewRes struct { + model.SysDeptViewModel +} diff --git a/server/api/system/v1/dict.go b/server/api/system/v1/dict.go new file mode 100644 index 0000000..c044394 --- /dev/null +++ b/server/api/system/v1/dict.go @@ -0,0 +1,108 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type SysDictTypeListReq struct { + g.Meta `path:"/dict/list" method:"get" tags:"系统-字典管理" summary:"字典类型列表" x-check-permission:"cpm:system:dict:list"` + model.SysDictTypeListParam +} + +type SysDictTypeListRes struct { + response.PageResult + Items []model.SysDictTypeListModel `json:"items"` +} + +type SysDictTypeAddReq struct { + g.Meta `path:"/dict-type/add" method:"post" tags:"系统-字典管理" summary:"新增字典类型" x-check-permission:"cpm:system:dict:add"` + model.SysDictTypeAddParam +} + +type SysDictTypeAddRes struct { + model.SysDictTypeAddModel +} + +type SysDictTypeEditReq struct { + g.Meta `path:"/dict-type/edit" method:"post" tags:"系统-字典管理" summary:"编辑字典类型" x-check-permission:"cpm:system:dict:edit"` + model.SysDictTypeEditParam +} + +type SysDictTypeEditRes struct { + model.SysDictTypeEditModel +} + +type SysDictTypeDeleteReq struct { + g.Meta `path:"/dict-type/delete" method:"post" tags:"系统-字典管理" summary:"删除字典类型" x-check-permission:"cpm:system:dict:remove"` + model.SysDictTypeDeleteParam +} + +type SysDictTypeDeleteRes struct { + model.SysDictTypeDeleteModel +} + +type SysDictTypeViewReq struct { + g.Meta `path:"/dict-type/view" method:"get" tags:"系统-字典管理" summary:"字典类型详情" x-check-permission:"cpm:system:dict:query"` + model.SysDictTypeViewParam +} + +type SysDictTypeViewRes struct { + model.SysDictTypeViewModel +} + +type SysDictDataListReq struct { + g.Meta `path:"/dict-data/list/{id}" method:"get" tags:"系统-字典管理" summary:"字典数据列表" ` + request.PageInfo + model.SysDictDataListParam +} + +type SysDictDataListRes struct { + response.PageResult + Items []model.SysDictDataListModel `json:"items"` + Type *model.SysDictTypeViewModel `json:"type"` +} + +type SysDictDataAddReq struct { + g.Meta `path:"/dict-data/add" method:"post" tags:"系统-字典管理" summary:"新增字典数据" x-check-permission:"cpm:system:dict:add"` + model.SysDictDataAddParam +} + +type SysDictDataAddRes struct { + model.SysDictDataAddModel +} + +type SysDictDataEditReq struct { + g.Meta `path:"/dict-data/edit" method:"post" tags:"系统-字典管理" summary:"编辑字典数据" x-check-permission:"cpm:system:dict:edit"` + model.SysDictDataEditParam +} + +type SysDictDataEditRes struct { + model.SysDictDataEditModel +} + +type SysDictDataDeleteReq struct { + g.Meta `path:"/dict-data/delete" method:"post" tags:"系统-字典管理" summary:"删除字典数据" x-check-permission:"cpm:system:dict:remove"` + model.SysDictDataDeleteParam +} + +type SysDictDataDeleteRes struct { + model.SysDictDataDeleteModel +} + +type SysDictDataViewReq struct { + g.Meta `path:"/dict-data/view" method:"get" tags:"系统-字典管理" summary:"字典数据详情" x-check-permission:"cpm:system:dict:query"` + model.SysDictDataViewParam +} + +type SysDictDataViewRes struct { + model.SysDictDataViewModel +} diff --git a/server/api/system/v1/job.go b/server/api/system/v1/job.go new file mode 100644 index 0000000..2a80fe6 --- /dev/null +++ b/server/api/system/v1/job.go @@ -0,0 +1,83 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +// 获取任务列表 +type SysJobListReq struct { + g.Meta `path:"/job/list" method:"get" tags:"系统-任务管理" summary:"获取任务列表" x-check-permission:"cpm:system:job:list"` + model.SysJobListParam + request.PageInfo +} + +type SysJobListRes struct { + Data []*model.SysJobListModel `json:"items" dc:"任务列表"` + response.PageResult +} + +// 获取任务详情 +type SysJobViewReq struct { + g.Meta `path:"/job/view" method:"get" tags:"系统-任务管理" summary:"获取任务详情" x-check-permission:"cpm:system:job:view"` + JobId int64 `v:"required" json:"jobId" dc:"任务ID"` +} + +type SysJobViewRes struct { + model.SysJobViewModel +} + +// 添加任务 +type SysJobAddReq struct { + g.Meta `path:"/job/add" method:"post" tags:"系统-任务管理" summary:"添加任务" x-check-permission:"cpm:system:job:add"` + model.SysJobAddModel +} + +type SysJobAddRes struct { + model.SysJobViewModel +} + +// 更新任务 +type SysJobUpdateReq struct { + g.Meta `path:"/job/update" method:"post" tags:"系统-任务管理" summary:"更新任务" x-check-permission:"cpm:system:job:update"` + model.SysJobUpdateModel +} + +type SysJobUpdateRes struct { + model.SysJobViewModel +} + +// 更新任务 +type SysJobUpdateStatusReq struct { + g.Meta `path:"/job/status" method:"post" tags:"系统" summary:"更新任务状态" x-check-permission:"cpm:system:job:status"` + model.SysJobUpdateStatusModel +} + +type SysJobUpdateStatusRes = SysJobUpdateRes + +// 删除任务 +type SysJobDeleteReq struct { + g.Meta `path:"/job/delete" method:"post" tags:"系统-任务管理" summary:"删除任务" x-check-permission:"cpm:system:job:delete"` + JobIds []int64 `json:"jobId" dc:"任务ID"` +} + +type SysJobExecReq struct { + g.Meta `path:"/job/exec" method:"post" tags:"系统" summary:"执行任务" x-check-permission:"cpm:system:job:exec"` + JobId int64 `v:"required" json:"jobId" dc:"任务ID"` +} + +type SysJobDeleteRes struct { + JobIds []int64 `json:"jobId" dc:"任务ID"` +} + +type SysJobExecRes struct { + JobId int64 `json:"jobId" dc:"任务ID"` +} diff --git a/server/api/system/v1/logininfor.go b/server/api/system/v1/logininfor.go new file mode 100644 index 0000000..8c63124 --- /dev/null +++ b/server/api/system/v1/logininfor.go @@ -0,0 +1,32 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type ListLogininforReq struct { + g.Meta `path:"/logininfor/list" method:"get" tags:"系统-日志管理" summary:"登录信息列表" x-check-permission:"cpm:monitor:logininfor:list"` + model.SysLogininforListParam +} + +type ListLogininforRes struct { + Items []*model.SysLogininforListModel `json:"items"` + response.PageResult +} + +type DeleteLogininforReq struct { + g.Meta `path:"/logininfor/delete" method:"post" tags:"系统-日志管理" summary:"删除登录信息" x-check-permission:"cpm:monitor:logininfor:remove"` + model.SysLogininforDeleteParam +} + +type DeleteLogininforRes struct { + model.SysLogininforDeleteModel +} diff --git a/server/api/system/v1/menu.go b/server/api/system/v1/menu.go new file mode 100644 index 0000000..8837128 --- /dev/null +++ b/server/api/system/v1/menu.go @@ -0,0 +1,116 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +// 路由菜单 +type RouteMenu struct { + Id int64 `json:"id" dc:"路由ID"` + ParentId int64 `json:"parentId" dc:"父路由ID"` + Path string `json:"path" dc:"路由路径"` + Redirect *string `json:"redirect,omitempty" dc:"重定向路径"` + Name string `json:"name" dc:"路由名称"` // 必须唯一 + Component string `json:"component" dc:"组件路径"` + Meta *RouteMeta `json:"meta,omitempty" dc:"路由元数据"` + Children []*RouteMenu `json:"children,omitempty" dc:"子路由"` +} + +// 路由元数据 +// 对应 vue/packages/@core/base/typings/src/vue-router.d.ts +type RouteMeta struct { + Title string `json:"title" dc:"菜单标题"` + Icon *string `json:"icon,omitempty" dc:"菜单图标"` + Order *int `json:"order,omitempty" dc:"菜单排序"` + OpenInNewWindow *bool `json:"openInNewWindow,omitempty" dc:"是否在新窗口打开"` + NoBasicLayout *bool `json:"noBasicLayout,omitempty" dc:"是否不使用基础布局"` + HideInMenu *bool `json:"hideInMenu,omitempty" dc:"是否隐藏菜单"` + HideInBreadcrumb *bool `json:"hideInBreadcrumb,omitempty" dc:"是否隐藏面包屑"` + HideChildrenInMenu *bool `json:"hideChildrenInMenu,omitempty" dc:"是否隐藏子菜单"` + HideInTab *bool `json:"hideInTab,omitempty" dc:"是否隐藏标签页"` + MenuVisibleWithForbidden *bool `json:"menuVisibleWithForbidden,omitempty" dc:"菜单可见但访问会被重定向到403"` + IgnoreAccess *bool `json:"ignoreAccess,omitempty" dc:"忽略权限,直接可以访问"` + KeepAlive *bool `json:"keepAlive,omitempty" dc:"开启KeepAlive缓存"` + Link *string `json:"link,omitempty" dc:"外链-跳转路径"` + Query map[string]string `json:"query,omitempty" dc:"路由所携带的参数"` + IframeSrc *string `json:"iframeSrc,omitempty" dc:"iframe地址"` + MaxNumOfOpenTab *int `json:"maxNumOfOpenTab,omitempty" dc:"标签页最大打开数量"` + Badge *string `json:"badge,omitempty" dc:"徽标"` + BadgeType *string `json:"badgeType,omitempty" dc:"徽标类型"` + BadgeVariants *string `json:"badgeVariants,omitempty" dc:"徽标颜色"` + Authority []string `json:"authority,omitempty" dc:"需要特定的角色标识才可以访问"` + AffixTab *bool `json:"affixTab,omitempty" dc:"是否固定标签页"` + AffixTabOrder *int `json:"affixTabOrder,omitempty" dc:"固定标签页的顺序"` + ActiveIcon *string `json:"activeIcon,omitempty" dc:"激活图标"` + ActivePath *string `json:"activePath,omitempty" dc:"当前激活的菜单"` +} + +// 获取用户所有菜单 +type MenuAllReq struct { + g.Meta `path:"/menu/all" method:"get" tags:"系统-菜单管理" summary:"获取用户所有菜单" x-check-permission:""` +} + +// type MenuAllRes struct { +// Data []RouteMenu `json:"data" dc:"菜单列表"` +// } +type MenuAllRes = []*RouteMenu + +type MenuListReq struct { + g.Meta `path:"/menu/list" method:"get" tags:"系统-菜单管理" summary:"获取菜单列表" x-check-permission:"cpm:system:menu:list"` + model.SysMenuListParam +} + +type MenuListRes struct { + Data []*model.SysMenuListModel `json:"items" dc:"菜单列表"` + Total int `json:"total" dc:"总数"` +} + +type MenuViewReq struct { + g.Meta `path:"/menu/view" method:"get" tags:"系统-菜单管理" summary:"获取菜单详情" x-check-permission:"cpm:system:menu:query"` + MenuId int64 `json:"menuId" dc:"菜单ID"` +} + +type MenuViewRes struct { + model.SysMenuViewModel +} + +type MenuAddReq struct { + g.Meta `path:"/menu/add" method:"post" tags:"系统-菜单管理" summary:"添加菜单" x-check-permission:"cpm:system:menu:add"` + model.SysMenuAddModel +} + +type MenuAddRes struct { + model.SysMenuViewModel +} + +type MenuUpdateReq struct { + g.Meta `path:"/menu/update" method:"post" tags:"系统-菜单管理" summary:"更新菜单" x-check-permission:"cpm:system:menu:edit"` + model.SysMenuUpdateModel +} + +type MenuUpdateRes struct { + model.SysMenuViewModel +} + +type MenuDeleteReq struct { + g.Meta `path:"/menu/delete" method:"post" tags:"系统-菜单管理" summary:"删除菜单" x-check-permission:"cpm:system:menu:remove"` + MenuId int64 `json:"menuId" dc:"菜单ID"` +} + +type MenuDeleteRes struct { +} + +type MenuTreeReq struct { + g.Meta `path:"/menu/tree" method:"get" tags:"系统-菜单管理" summary:"获取菜单树" x-check-permission:""` +} + +type MenuTreeRes struct { + Data []*model.SysMenuTreeModel `json:"items" dc:"菜单树"` +} diff --git a/server/api/system/v1/notice.go b/server/api/system/v1/notice.go new file mode 100644 index 0000000..4de42cf --- /dev/null +++ b/server/api/system/v1/notice.go @@ -0,0 +1,59 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type SysNoticeListReq struct { + g.Meta `path:"/notice/list" method:"get" tags:"系统-公告管理" summary:"公告列表" x-check-permission:"cpm:system:notice:list"` + *model.SysNoticeListParam +} + +type SysNoticeListRes struct { + response.PageResult + Items []*model.SysNoticeListModel `json:"items"` +} + +type SysNoticeAddReq struct { + g.Meta `path:"/notice/add" method:"post" tags:"系统-公告管理" summary:"新增公告" x-check-permission:"cpm:system:notice:add"` + *model.SysNoticeAddParam +} + +type SysNoticeAddRes struct { + *model.SysNoticeAddModel +} + +type SysNoticeEditReq struct { + g.Meta `path:"/notice/edit" method:"post" tags:"系统-公告管理" summary:"编辑公告" x-check-permission:"cpm:system:notice:edit"` + *model.SysNoticeEditParam +} + +type SysNoticeEditRes struct { + *model.SysNoticeEditModel +} + +type SysNoticeDeleteReq struct { + g.Meta `path:"/notice/delete" method:"post" tags:"系统-公告管理" summary:"删除公告" x-check-permission:"cpm:system:notice:remove"` + *model.SysNoticeDeleteParam +} + +type SysNoticeDeleteRes struct { + *model.SysNoticeDeleteModel +} + +type SysNoticeViewReq struct { + g.Meta `path:"/notice/view" method:"get" tags:"系统-公告管理" summary:"查看公告" x-check-permission:"cpm:system:notice:query"` + *model.SysNoticeViewParam +} + +type SysNoticeViewRes struct { + *model.SysNoticeViewModel +} diff --git a/server/api/system/v1/online.go b/server/api/system/v1/online.go new file mode 100644 index 0000000..7e14c2e --- /dev/null +++ b/server/api/system/v1/online.go @@ -0,0 +1,33 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type SysUserOnlineListReq struct { + g.Meta `path:"/user-online/list" method:"get" tags:"监控-在线用户管理" summary:"系统用户在线列表" x-check-permission:"cpm:monitor:online:list"` + request.PageInfo + model.SysUserOnlineListParam +} + +type SysUserOnlineListRes struct { + Items []*model.SysUserOnlineListModel `json:"items"` + response.PageResult +} + +type SysUserOnlineDeleteReq struct { + g.Meta `path:"/user-online/delete" method:"post" tags:"监控-在线用户管理" summary:"系统用户在线删除" x-check-permission:"cpm:monitor:online:remove"` + Ids []int64 `json:"ids"` +} + +type SysUserOnlineDeleteRes struct { +} diff --git a/server/api/system/v1/oper_log.go b/server/api/system/v1/oper_log.go new file mode 100644 index 0000000..f5aae05 --- /dev/null +++ b/server/api/system/v1/oper_log.go @@ -0,0 +1,25 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type GetOperLogListReq struct { + g.Meta `path:"/oper-log/list" method:"get" tags:"系统-日志管理" summary:"操作日志列表" x-check-permission:"cpm:monitor:operlog:list"` + model.SysOperLogListParam + request.PageInfo +} + +type GetOperLogListRes struct { + Items []*model.SysOperLogListModel `json:"items"` + response.PageResult +} diff --git a/server/api/system/v1/oss.go b/server/api/system/v1/oss.go new file mode 100644 index 0000000..28e2cb1 --- /dev/null +++ b/server/api/system/v1/oss.go @@ -0,0 +1,43 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type SysOssListReq struct { + g.Meta `path:"/oss/list" method:"get" tags:"系统-存储管理" summary:"获取OSS列表" x-check-permission:"cpm:system:oss:list"` + model.SysOssListParam + request.PageInfo +} + +type SysOssListRes struct { + Items []*model.SysOssListModel `json:"items"` + response.PageResult +} + +type SysOssViewReq struct { + g.Meta `path:"/oss/view" method:"get" tags:"系统-存储管理" summary:"查看文件" x-check-permission:"cpm:system:oss:query"` + *model.SysOssViewParam +} + +type SysOssViewRes struct { + *model.SysOssViewModel +} + +type SysOssDeleteReq struct { + g.Meta `path:"/oss/delete" method:"post" tags:"系统-存储管理" summary:"删除文件" x-check-permission:"cpm:system:oss:remove"` + *model.SysOssDeleteParam +} + +type SysOssDeleteRes struct { + *model.SysOssDeleteModel +} diff --git a/server/api/system/v1/oss_config.go b/server/api/system/v1/oss_config.go new file mode 100644 index 0000000..c2881dd --- /dev/null +++ b/server/api/system/v1/oss_config.go @@ -0,0 +1,54 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type SysOssConfigListReq struct { + g.Meta `path:"/oss-config/list" method:"get" tags:"系统-存储管理" summary:"获取存储配置列表" x-check-permission:"cpm:system:ossConfig:list"` + *model.SysOssConfigListParam +} +type SysOssConfigListRes struct { + response.PageResult + Items []*model.SysOssConfigListModel `json:"items"` +} + +type SysOssConfigViewReq struct { + g.Meta `path:"/oss-config/view" method:"get" tags:"系统-存储管理" summary:"获取存储配置详情" x-check-permission:"cpm:system:ossConfig:list"` + *model.SysOssConfigViewParam +} +type SysOssConfigViewRes struct { + *model.SysOssConfigViewModel +} + +type SysOssConfigAddReq struct { + g.Meta `path:"/oss-config/add" method:"post" tags:"系统-存储管理" summary:"新增存储配置" x-check-permission:"cpm:system:ossConfig:add"` + *model.SysOssConfigAddParam +} +type SysOssConfigAddRes struct { + *model.SysOssConfigAddModel +} + +type SysOssConfigEditReq struct { + g.Meta `path:"/oss-config/edit" method:"post" tags:"系统-存储管理" summary:"编辑存储配置" x-check-permission:"cpm:system:ossConfig:edit"` + *model.SysOssConfigEditParam +} +type SysOssConfigEditRes struct { + *model.SysOssConfigEditModel +} + +type SysOssConfigDeleteReq struct { + g.Meta `path:"/oss-config/delete" method:"post" tags:"系统-存储管理" summary:"删除存储配置" x-check-permission:"cpm:system:ossConfig:remove"` + *model.SysOssConfigDeleteParam +} +type SysOssConfigDeleteRes struct { + *model.SysOssConfigDeleteModel +} diff --git a/server/api/system/v1/post.go b/server/api/system/v1/post.go new file mode 100644 index 0000000..83af33b --- /dev/null +++ b/server/api/system/v1/post.go @@ -0,0 +1,68 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type PostListReq struct { + g.Meta `path:"/post/list" method:"get" tags:"系统-岗位管理" summary:"获取岗位列表" x-check-permission:"cpm:system:post:list"` + *model.SysPostListParam +} + +type PostListRes struct { + response.PageResult + Data []*model.SysPostListModel `json:"items" dc:"岗位列表"` +} + +type PostAddReq struct { + g.Meta `path:"/post/add" method:"post" tags:"系统-岗位管理" summary:"新增岗位" x-check-permission:"cpm:system:post:add"` + *model.SysPostAddParam +} + +type PostAddRes struct { + *model.SysPostAddModel +} + +type PostEditReq struct { + g.Meta `path:"/post/edit" method:"post" tags:"系统-岗位管理" summary:"编辑岗位" x-check-permission:"cpm:system:post:edit"` + *model.SysPostEditParam +} + +type PostEditRes struct { + *model.SysPostEditModel +} + +type PostDeleteReq struct { + g.Meta `path:"/post/delete" method:"post" tags:"系统-岗位管理" summary:"删除岗位"` + *model.SysPostDeleteParam +} + +type PostDeleteRes struct { + *model.SysPostDeleteModel +} + +type PostViewReq struct { + g.Meta `path:"/post/view" method:"get" tags:"系统-岗位管理" summary:"获取岗位详情" x-check-permission:"cpm:system:post:list"` + *model.SysPostViewParam +} + +type PostViewRes struct { + *model.SysPostViewModel +} + +type PostExportReq struct { + g.Meta `path:"/post/export" method:"post" tags:"系统-岗位管理" summary:"导出岗位" x-check-permission:"cpm:system:post:export"` + *model.SysPostExportParam +} + +type PostExportRes struct { + *model.SysPostExportModel +} diff --git a/server/api/system/v1/qrcode.go b/server/api/system/v1/qrcode.go new file mode 100644 index 0000000..0f1482c --- /dev/null +++ b/server/api/system/v1/qrcode.go @@ -0,0 +1,83 @@ +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/frame/g" +) + +// 获取登录二维码 +type QrcodeLoginReq struct { + g.Meta `path:"/qrcode/login" method:"get" tags:"系统-二维码" summary:"获取登录二维码"` +} + +type QrcodeLoginRes struct { + model.QrcodeLoginModel +} + +// 获取登录二维码扫码结果 +type QrcodeLoginStatusReq struct { + g.Meta `path:"/qrcode/login/status" method:"get" tags:"系统-二维码" summary:"获取登录二维码扫码状态"` + TempUserId string `json:"tempUserId" dc:"临时用户ID" v:"required#临时用户ID不能为空"` +} + +type QrcodeLoginStatusRes struct { + model.QrcodeLoginModel +} + +// 获取绑定二维码 +type QrcodeBindReq struct { + g.Meta `path:"/qrcode/bind" method:"get" tags:"系统-二维码" summary:"获取绑定二维码"` +} + +type QrcodeBindRes struct { + model.QrcodeCacheModel +} + +// 获取绑定二维码扫码结果 +type QrcodeBindStatusReq struct { + g.Meta `path:"/qrcode/bind/status" method:"get" tags:"系统-二维码" summary:"获取绑定二维码扫码状态"` +} + +type QrcodeBindStatusRes struct { + model.QrcodeBindStatusModel +} + +// 扫码成功回调 +type QrcodeScanCallbackReq struct { + g.Meta `path:"/qrcode/scan/callback" method:"post" tags:"系统-二维码" summary:"扫码成功回调"` + model.QrcodeScanCallbackParam +} + +type QrcodeScanCallbackRes struct { +} + +// 注册并关联OpenId +type QrcodeRegisterAndBindOpenIdReq struct { + g.Meta `path:"/qrcode/registerAndBindOpenId" method:"post" tags:"系统-二维码" summary:"注册并关联OpenId"` + model.QrcodeRegisterAndBindOpenIdParam +} + +type QrcodeRegisterAndBindOpenIdRes struct { + model.QrcodeLoginAndBindOpenIdModel +} + +// 登录并关联OpenId +type QrcodeLoginAndBindOpenIdReq struct { + g.Meta `path:"/qrcode/loginAndBindOpenId" method:"post" tags:"系统-二维码" summary:"登录并关联OpenId"` + model.QrcodeLoginAndBindOpenIdParam +} + +type QrcodeLoginAndBindOpenIdRes struct { + model.QrcodeLoginAndBindOpenIdModel +} + +// 扫码后选择用户登录 +type QrcodeLoginSelectUserReq struct { + g.Meta `path:"/qrcode/loginSelectUser" method:"post" tags:"系统-二维码" summary:"扫码后选择用户登录"` + model.QrcodeLoginSelectUserIdParam +} + +type QrcodeLoginSelectUserRes struct { + model.QrcodeLoginAndBindOpenIdModel +} diff --git a/server/api/system/v1/role.go b/server/api/system/v1/role.go new file mode 100644 index 0000000..d1efc51 --- /dev/null +++ b/server/api/system/v1/role.go @@ -0,0 +1,69 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type RoleListReq struct { + g.Meta `path:"/role/list" method:"get" tags:"系统-角色管理" summary:"获取角色列表" x-check-permission:"cpm:system:role:list"` + request.PageInfo + model.SysRoleListParam +} + +type RoleListRes struct { + response.PageResult + Data []*model.SysRoleListModel `json:"items" dc:"角色列表"` +} + +type RoleAddReq struct { + g.Meta `path:"/role/add" method:"post" tags:"系统-角色管理" summary:"新增角色" x-check-permission:"cpm:system:role:add"` + model.SysRoleAddParam +} + +type RoleAddRes struct { + model.SysRoleAddModel +} + +type RoleEditReq struct { + g.Meta `path:"/role/edit" method:"post" tags:"系统-角色管理" summary:"编辑角色" x-check-permission:"cpm:system:role:edit"` + model.SysRoleEditParam +} + +type RoleEditRes struct { + model.SysRoleEditModel +} + +type RoleDeleteReq struct { + g.Meta `path:"/role/delete" method:"post" tags:"系统-角色管理" summary:"删除角色" x-check-permission:"cpm:system:role:remove"` + model.SysRoleDeleteParam +} + +type RoleDeleteRes struct { + model.SysRoleDeleteModel +} + +type RoleViewReq struct { + g.Meta `path:"/role/view" method:"get" tags:"系统-角色管理" summary:"获取角色详情" x-check-permission:"cpm:system:role:query"` + model.SysRoleViewParam +} + +type RoleViewRes struct { + model.SysRoleViewModel +} + +type RoleDataScopeEditReq struct { + g.Meta `path:"/role/dataScope" method:"post" tags:"系统-角色管理" summary:"编辑角色数据权限" x-check-permission:"cpm:system:role:edit"` + model.SysRoleDataScopeEditParam +} + +type RoleDataScopeEditRes struct { +} diff --git a/server/api/system/v1/social.go b/server/api/system/v1/social.go new file mode 100644 index 0000000..5999a71 --- /dev/null +++ b/server/api/system/v1/social.go @@ -0,0 +1,34 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type SysSocialListReq struct { + g.Meta `path:"/social/list" method:"get" tags:"系统-第三方授权管理" summary:"社交列表" x-check-permission:"cpc:current:user"` + model.SysSocialListParam + request.PageInfo +} + +type SysSocialListRes struct { + Items []*model.SysSocialListModel `json:"items"` + response.PageResult +} + +// 删除绑定关系 +type SysSocialDeleteReq struct { + g.Meta `path:"/social/delete" method:"post" tags:"系统-第三方授权管理" summary:"删除社交" x-check-permission:"cpc:current:user"` + Id int64 `json:"id" dc:"主键"` +} + +type SysSocialDeleteRes struct { +} diff --git a/server/api/system/v1/sysnoticeuser.go b/server/api/system/v1/sysnoticeuser.go new file mode 100644 index 0000000..025dc16 --- /dev/null +++ b/server/api/system/v1/sysnoticeuser.go @@ -0,0 +1,75 @@ +// Package sysnoticeuser +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +// @AutoGenerate Version +package v1 //sysnoticeuser + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +// ListReq 查询用户通知公告表列表 +type SysNoticeUserListReq struct { + g.Meta `path:"/sysNoticeUser/list" method:"get" tags:"系统-用户通知公告表" summary:"获取用户通知公告表列表" x-check-permission:"cpm:system:sysNoticeUser:list,cpc:current:user"` + model.SysNoticeUserListParam +} + +type SysNoticeUserListRes struct { + response.PageResult + Items []*model.SysNoticeUserListModel `json:"items" dc:"数据列表"` +} + +// ExportReq 导出用户通知公告表列表 +type SysNoticeUserExportReq struct { + g.Meta `path:"/sysNoticeUser/export" method:"post" tags:"系统-用户通知公告表" summary:"导出用户通知公告表列表" x-check-permission:"cpm:system:sysNoticeUser:export,cpc:current:user"` + model.SysNoticeUserListParam +} + +type SysNoticeUserExportRes struct{} + +// ViewReq 获取用户通知公告表指定信息 +type SysNoticeUserViewReq struct { + g.Meta `path:"/sysNoticeUser/view" method:"get" tags:"系统-用户通知公告表" summary:"获取用户通知公告表指定信息" x-check-permission:"cpm:system:sysNoticeUser:view,cpc:current:user"` + model.SysNoticeUserViewParam +} + +type SysNoticeUserViewRes struct { + *model.SysNoticeUserViewModel +} + +// EditReq 修改/新增用户通知公告表 +type SysNoticeUserEditReq struct { + g.Meta `path:"/sysNoticeUser/edit" method:"post" tags:"系统-用户通知公告表" summary:"修改/新增用户通知公告表" x-check-permission:"cpm:system:sysNoticeUser:edit,cpc:current:user"` + model.SysNoticeUserEditParam +} + +type SysNoticeUserEditRes struct{} + +// DeleteReq 删除用户通知公告表 +type SysNoticeUserDeleteReq struct { + g.Meta `path:"/sysNoticeUser/delete" method:"post" tags:"系统-用户通知公告表" summary:"删除用户通知公告表" x-check-permission:"cpm:system:sysNoticeUser:delete,cpc:current:user"` + model.SysNoticeUserDeleteParam +} + +type SysNoticeUserDeleteRes struct{} + +// StatusReq 更新用户通知公告表状态 +type SysNoticeUserStatusReq struct { + g.Meta `path:"/sysNoticeUser/status" method:"post" tags:"系统-用户通知公告表" summary:"更新用户通知公告表状态" x-check-permission:"cpm:system:sysNoticeUser:status,cpc:current:user"` + model.SysNoticeUserStatusParam +} + +type SysNoticeUserStatusRes struct{} + +// ReadReq 已读 +type SysNoticeUserReadReq struct { + g.Meta `path:"/sysNoticeUser/read" method:"post" tags:"系统-用户通知公告表" summary:"已读" x-check-permission:"cpm:system:sysNoticeUser:read,cpc:current:user"` + model.SysNoticeUserReadParam +} + +type SysNoticeUserReadRes struct{} diff --git a/server/api/system/v1/tenant.go b/server/api/system/v1/tenant.go new file mode 100644 index 0000000..8fd1912 --- /dev/null +++ b/server/api/system/v1/tenant.go @@ -0,0 +1,135 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type SysTenantListReq struct { + g.Meta `path:"/tenant/list" method:"get" tags:"系统-租户管理" summary:"租户列表" x-check-permission:"cpm:system:tenant:list"` + request.PageInfo + *model.SysTenantListParam +} + +type SysTenantListRes struct { + response.PageResult + Items []*model.SysTenantListModel `json:"items"` +} + +type SysTenantAddReq struct { + g.Meta `path:"/tenant/add" method:"post" tags:"系统-租户管理" summary:"租户添加" x-check-permission:"cpm:system:tenant:add"` + *model.SysTenantAddParam +} + +type SysTenantAddRes struct { + *model.SysTenantAddModel +} + +type SysTenantEditReq struct { + g.Meta `path:"/tenant/edit" method:"post" tags:"系统-租户管理" summary:"租户编辑" x-check-permission:"cpm:system:tenant:edit"` + *model.SysTenantEditParam +} + +type SysTenantEditRes struct { + *model.SysTenantEditModel +} + +type SysTenantDeleteReq struct { + g.Meta `path:"/tenant/delete" method:"post" tags:"系统-租户管理" summary:"租户删除" x-check-permission:"cpm:system:tenant:remove"` + *model.SysTenantDeleteParam +} + +type SysTenantDeleteRes struct { + *model.SysTenantDeleteModel +} + +type SysTenantStatusReq struct { + g.Meta `path:"/tenant/status" method:"post" tags:"系统-租户管理" summary:"租户状态" x-check-permission:"cpm:system:tenant:edit"` + *model.SysTenantStatusParam +} + +type SysTenantStatusRes struct { + *model.SysTenantStatusModel +} + +type SysTenantViewReq struct { + g.Meta `path:"/tenant/view" method:"get" tags:"系统-租户管理" summary:"租户详情" x-check-permission:"cpm:system:tenant:query"` + *model.SysTenantViewParam +} + +type SysTenantViewRes struct { + *model.SysTenantViewModel +} + +type SysTenantSyncMenuReq struct { + g.Meta `path:"/tenant/syncmenu" method:"post" tags:"系统-租户管理" summary:"同步租户菜单" x-check-permission:"cpm:system:tenant:syncmenu"` + *model.SysTenantSyncMenuParam +} + +type SysTenantSyncMenuRes struct { + *model.SysTenantSyncMenuModel +} + +type SysTenantPackageListReq struct { + g.Meta `path:"/tenant/package/list" method:"get" tags:"系统-租户管理" summary:"租户套餐列表" x-check-permission:"cpm:system:tenant:package:list"` + request.PageInfo + model.SysTenantPackageListParam +} + +type SysTenantPackageListRes struct { + response.PageResult + Items []*model.SysTenantPackageListModel `json:"items"` +} + +type SysTenantPackageViewReq struct { + g.Meta `path:"/tenant/package/view" method:"get" tags:"系统-租户管理" summary:"租户套餐详情" x-check-permission:"cpm:system:tenantPackage:query"` + *model.SysTenantPackageViewParam +} + +type SysTenantPackageViewRes struct { + *model.SysTenantPackageViewModel +} + +type SysTenantPackageStatusReq struct { + g.Meta `path:"/tenant/package/status" method:"post" tags:"系统-租户管理" summary:"租户套餐状态" x-check-permission:"cpm:system:tenantPackage:edit"` + *model.SysTenantPackageStatusParam +} + +type SysTenantPackageStatusRes struct { + *model.SysTenantPackageStatusModel +} + +type SysTenantPackageAddReq struct { + g.Meta `path:"/tenant/package/add" method:"post" tags:"系统-租户管理" summary:"租户套餐添加" x-check-permission:"cpm:system:tenantPackage:add"` + *model.SysTenantPackageAddParam +} + +type SysTenantPackageAddRes struct { + *model.SysTenantPackageAddModel +} + +type SysTenantPackageEditReq struct { + g.Meta `path:"/tenant/package/edit" method:"post" tags:"系统-租户管理" summary:"租户套餐编辑" x-check-permission:"cpm:system:tenantPackage:edit"` + *model.SysTenantPackageEditParam +} + +type SysTenantPackageEditRes struct { + *model.SysTenantPackageEditModel +} + +type SysTenantPackageDeleteReq struct { + g.Meta `path:"/tenant/package/delete" method:"post" tags:"系统-租户管理" summary:"租户套餐删除" x-check-permission:"cpm:system:tenantPackage:remove"` + *model.SysTenantPackageDeleteParam +} + +type SysTenantPackageDeleteRes struct { + *model.SysTenantPackageDeleteModel +} diff --git a/server/api/system/v1/user.go b/server/api/system/v1/user.go new file mode 100644 index 0000000..783b109 --- /dev/null +++ b/server/api/system/v1/user.go @@ -0,0 +1,141 @@ +// Package v1 +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package v1 + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + + "github.com/gogf/gf/v2/frame/g" +) + +type UserInfo struct { + Id int64 `json:"id" dc:"用户ID"` + Username string `json:"userName" dc:"用户名"` + Nickname string `json:"realName" dc:"昵称"` + Avatar string `json:"avatar" dc:"头像"` + Email string `json:"email" dc:"邮箱"` + HomePath string `json:"homePath" dc:"首页路径"` +} + +type UserInfoReq struct { + g.Meta `path:"/user/info" method:"get" tags:"系统-用户管理" summary:"获取当前用户信息" x-check-permission:"cpc:current:user"` +} + +type UserInfoRes struct { + UserInfo +} + +type UserListReq struct { + g.Meta `path:"/user/list" method:"get" tags:"系统-用户管理" summary:"获取用户列表" x-check-permission:"cpm:system:user:list"` + request.PageInfo + model.UserListParam +} + +type UserListRes struct { + response.PageResult + Data []*model.SysUserListModel `json:"items" dc:"用户列表"` +} + +type AddUserReq struct { + g.Meta `path:"/user/add" method:"post" tags:"系统-用户管理" summary:"新增用户" x-check-permission:"cpm:system:user:add"` + model.SysUserAddModel +} + +type AddUserRes struct { + Data *model.SysUserViewModel `json:"data" dc:"用户信息"` +} + +type UserProfileReq struct { + g.Meta `path:"/user/profile" method:"get" tags:"系统-用户管理" summary:"获取当前用户信息" x-check-permission:"cpc:current:user"` +} + +type UserProfileRes struct { + User *model.UserProfileModel `json:"user" dc:"用户信息"` +} + +type UpdateCurrentUserReq struct { + g.Meta `path:"/user/profile/update" method:"post" tags:"系统-用户管理" summary:"更新当前用户信息" x-check-permission:"cpc:current:user"` + model.UpdateCurrentUserModel +} + +type UpdateCurrentUserRes struct { + UserInfo +} + +type UpdateCurrentUserPasswordReq struct { + g.Meta `path:"/user/profile/password" method:"post" tags:"系统-用户管理" summary:"更新当前用户密码" x-check-permission:"cpc:current:user"` + model.UpdateCurrentUserPasswordModel +} + +type UpdateCurrentUserPasswordRes struct { +} + +type UpdateUserReq struct { + g.Meta `path:"/user/update" method:"post" tags:"系统-用户管理" summary:"更新用户信息" x-check-permission:"cpm:system:user:edit"` + model.SysUserUpdateModel +} + +type UpdateUserRes struct { + model.SysUserViewModel +} + +type DeleteUserReq struct { + g.Meta `path:"/user/delete" method:"post" tags:"系统-用户管理" summary:"删除用户" x-check-permission:"cpm:system:user:remove"` + UserId int64 `json:"userId" dc:"用户ID"` + UserIds []int64 `json:"userIds" dc:"用户ID列表"` +} + +type DeleteUserRes struct { +} + +type GetUserReq struct { + g.Meta `path:"/user/view" method:"get" tags:"系统-用户管理" summary:"获取用户信息" x-check-permission:"cpm:system:user:query"` + UserId int64 `json:"userId" dc:"用户ID"` +} + +type GetUserRes struct { + model.SysUserViewModel +} + +type ResetPasswordReq struct { + g.Meta `path:"/user/resetPassword" method:"post" tags:"系统-用户管理" summary:"重置用户密码" x-check-permission:"cpm:system:user:resetPwd"` + UserId int64 `json:"userId" dc:"用户ID"` + Password string `json:"password" dc:"新密码"` +} + +type ResetPasswordRes struct { +} + +type GetUserListByDeptIdReq struct { + g.Meta `path:"/user/listByDeptId" method:"get" tags:"系统-用户管理" summary:"获取部门用户列表" x-check-permission:"cpm:system:user:list"` + DeptId int64 `json:"deptId" dc:"部门ID"` + request.PageInfo +} + +type GetUserListByDeptIdRes struct { + response.PageResult + Items []*model.SysUserMiniModel `json:"items" dc:"用户列表"` +} + +// 用户注册 +type UserRegisterReq struct { + g.Meta `path:"/user/register" method:"post" tags:"系统-用户管理" summary:"用户注册" x-check-permission:"cpm:system:user:register"` + model.SysUserRegisterModel +} + +type UserRegisterRes struct { +} + +// StatusReq 更新用户状态 +type UserStatusReq struct { + g.Meta `path:"/user/status" method:"post" tags:"系统-用户管理" summary:"更新用户状态" x-check-permission:"cpm:system:user:edit"` + model.SysUserStatusParam +} + +type UserStatusRes struct { +} diff --git a/server/config/config.yaml b/server/config/config.yaml new file mode 100644 index 0000000..d6f4b17 --- /dev/null +++ b/server/config/config.yaml @@ -0,0 +1,289 @@ +# config.yaml 配置文件 +# @Link https://github.com/xiujiecn/xiu-admin +# @Copyright Copyright (c) 2025 LiXiujie +# @Author Lxj +# @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +# 系统配置 +system: + name: "IotLCM" + version: "1.0" + description: "IotLCM App Server" + gfmode: "dev" + upload: + local: +# path: "/data/app/iotlcmadmin-web-svc/upload" +# 本地开发环境 + path: "resource/upload" + +# 服务配置 +server: + # 基本配置 + address: ":8010" + # 接口文档 + openapiPath: "/api.json" # OpenAPI接口文档地址 + swaggerPath: "/swagger" # 内置SwaggerUI展示地址 + # 静态服务配置 + indexFiles: ["index.html","index.htm"] # 自动首页静态文件检索。默认为["index.html", "index.htm"] + indexFolder: false # 当访问静态文件目录时,是否展示目录下的文件列表。默认关闭,那么请求将返回403 + serverRoot: "resource/public" # 静态文件服务的目录根路径,配置时自动开启静态文件服务。默认关闭 + searchPaths: ["/resource/public"] # 提供静态文件服务时额外的文件搜索路径,当根路径找不到时则按照顺序在搜索目录查找。默认关闭 + fileServerEnabled: true + initLoadMemoryDB: false # 是否初始化内存数据库,默认关闭 + + # 文件上传配置 + maxHeaderBytes: "20KB" + clientMaxBodySize: "200MB" + readTimeout: "20m" + writeTimeout: "20m" + +# 日志配置 +logger: + path: "resource/log/server" # 日志文件路径。默认为空,表示关闭,仅输出到终端 + file: "{Y-m-d-H}.log" # 日志文件格式。默认为"{Y-m-d}.log" + prefix: "" # 日志内容输出前缀。默认为空 + level: "dev" # 日志输出级别 + timeFormat: "2006-01-02T15:04:05" # 自定义日志输出的时间格式,使用Golang标准的时间格式配置 + ctxKeys: [] # 自定义Context上下文变量名称,自动打印Context的变量到日志中。默认为空 + header: true # 是否打印日志的头信息。默认true + stdout: true # 日志是否同时输出到终端。默认true + rotateSize: 0 # 按照日志文件大小对文件进行滚动切分。默认为0,表示关闭滚动切分特性 + rotateExpire: 0 # 按照日志文件时间间隔对文件滚动切分。默认为0,表示关闭滚动切分特性 + rotateBackupLimit: 0 # 按照切分的文件数量清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除 + rotateBackupExpire: 0 # 按照切分的文件有效期清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除 + rotateBackupCompress: 0 # 滚动切分文件的压缩比(0-9)。默认为0,表示不压缩 + rotateCheckInterval: "1h" # 滚动切分的时间检测间隔,一般不需要设置。默认为1小时 + stdoutColorDisabled: false # 关闭终端的颜色打印。默认开启 + writerColorEnable: false # 日志文件是否带上颜色。默认false,表示不带颜色 + +# JWT配置 +jwt: + signingKey: "${JWT_SIGNING_KEY}" + expiresTime: 7d + bufferTime: 1d + issuer: iotlcm # 发行人 + +# 验证码配置 +captcha: + keyLong: 6 + imgWidth: 240 + imgHeight: 80 + openCaptcha: 0 + openCaptchaTimeout: 3600 + +# 注册验证码配置。短信服务接口见 F:\Work\数据\test\文档\lcm\sms.md +register: + sms: + url: "http://192.0.2.29:11190/api/v1/sms/send" + method: "POST" + headers: {} + bodyTemplate: '{"config":{"serviceType":"1","regionId":"cn-hangzhou","accessKeyId":"","accessKeySecret":"","signName":"","templateId":""},"params":{"phoneNumber":"{contact}","msgContent":"{\"code\":\"{code}\"}"}}' + successStatus: 200 + +database: + logger: + path: "resource/log/server/sql" + level: "all" + stdout: true + default: # 分组名称,可自定义,默认为default + link: "mysql:${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/db_iotlcm?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai" + debug: true #开启调试模式 + charset: "utf8mb4" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312),一般设置为utf8mb4。默认为utf8。 + dryRun: false #ORM空跑(只读不写) + maxIdle: 30 #连接池最大闲置的连接数 + maxOpen: 100 #连接池最大打开的连接数 + maxLifetime: 120 #(单位秒)连接对象可重复使用的时间长度 + memorydb: + # link: "sqlite::@file(jieiotmem.db)?journal_mode=WAL" + link: "sqlite::@file(:memory:)" + # link: "" + debug: true + maxIdle: 1 + maxOpen: 1 + maxLifetime: 30 + +# 缓存配置 +cache: + adapter: "redis" # 缓存适配器,支持:file|redis + fileDir: "resource/cache" # 缓存文件目录 + +# Redis 配置示例 +redis: + # 单实例配置 + default: + mode: "single" #redis模式,支持:single|cluster|sentinel + sentinelMasterName: "iotlcm-master" #哨兵模式主节点名称 + address: 192.0.2.29:6379 + db: 0 + pass: "${REDIS_PASSWORD}" + minIdle: 5 #最小空闲连接数 + maxIdle: 30 #允许闲置的最大连接数 + maxActive: 5000 #最大活跃连接数 + idleTimeout: "60s" #连接最大空闲时间,使用时间字符串例如30s/2m/2d + ClientName: "SystemCache" + +rabbitmq: + enabled: true + # 示例:amqp://${MQ_USER}:${MQ_PASSWORD}@${MQ_HOST}/ + # dsn: "amqp://${MQ_USER}:${MQ_PASSWORD}@${MQ_HOST}/" + dsn: "amqp://${MQ_USER}:${MQ_PASSWORD}@${MQ_HOST}/" + +workerBatchAsync: + enabled: true + queueName: "iotlcm.batch.execute" + failOnPublish: true + +appBusinessAsync: + enabled: false + provider: "rabbitmq" + queueName: "iotlcm.app.business" + failOnPublish: false + +payment: + balanceRecharge: + enabled: false + mock: + enabled: false + wechat: + enabled: true + envFile: "" + apiBase: "https://api.mch.weixin.qq.com" + mchId: "1723059989" + mchSerialNo: "32A3CB6381D743A4ADBF3885BB16882C6CE06D03" + apiV3Key: "ce9e5486d2979810f21d09f398d212e0" + privateKeyPath: "resource/certs/wechatpay_apiclient_key.pem" + notifyUrl: "https://app.iotlcm.example.com/api/v1/payment/wechat/notify" + notifyBaseUrl: "https://app.iotlcm.example.com" + refundNotifyUrl: "https://app.iotlcm.example.com/api/v1/payment/wechat/refund-notify" + timeoutMinutes: 5 #待支付时间(分钟) + platformPublicKeyId: "PUB_KEY_ID_0117230599892026052200111565002404" + platformPublicKeyPath: "resource/certs/wechatpay_platform_public_key.pem" + h5: + enabled: true + appId: "wx9e26905702c1a372" + appName: "物联网卡管理平台" + appUrl: "https://app.iotlcm.example.com" + payerClientIp: "" + mini: + enabled: true + appId: "wx9e26905702c1a372" + appSecret: "3ee708d13993282fed770ea1cab95d90" + +appCardRecognition: + enabled: true + provider: "openai-compatible" + baseUrl: "https://xueyangai.com/v1" + endpoint: "" + model: "qwen3.7-flash" + apiKey: "sk-nAhIIyqOn7JvQp1ti8o24GcBLgWqBUFKBaDTwAmFYZm0Hycy" + timeout: "25s" + maxImageBytes: 8388608 + maxTokens: 256 + rateLimit: + enabled: true + count: 20 + period: "1m" + +appWarning: + wechatSubscribeTemplateId: "DMDP2PKktdtG1ES_3Ic1JcR0LGELQmWE6LunYyerHCY" + +tsdb: + database: "TdEngine" #可选择 TdEngine、Influxdb + tdengine: + # type: "taosRestful" #http连接方式,端口是6041 + # dsn: "root:taosdata@http(127.0.0.1:6041)/" + type: "taosWS" #websocket连接方式,端口是6041 + # dsn: "root:taosdata@ws(127.0.0.1:6041)/" + dsn: "root:sdyz2026__@ws(192.0.2.29:6041)/" + dbName: "iotlcm" + maxOpenConns: 100 + maxIdleConns: 10 + + +# 路由配置 +router: + # 不需要验证登录的路由地址 + exceptLogin: + - /api/v1/system/auth/login + - /api/v1/system/auth/refresh + - /api/v1/system/captcha + - /api/v1/system/auth/getTenantList + - /api/v1/auth/captcha + - /api/v1/auth/side-captcha + - /api/v1/auth/check-side-captcha + - /api/v1/auth/sms + - /api/v1/auth/register + - /api/v1/auth/login + - /api/v1/auth/wechat-login + - /api/v1/auth/wechat-bind-phone + - /api/v1/auth/forgot-password + - /api/v1/card/public-query + - /api/v1/card/recognize + - /api/v1/card/public-package-detail + - /api/v1/diagnosis/info + - /api/v1/diagnosis/flow + - /api/v1/open/diagnosis/info + - /api/v1/open/diagnosis/flow + - /api/v1/payment/wechat/notify + - /api/v1/payment/wechat/refund-notify + - /openapi/v3/query/token + +# 生成代码 +xgen: + allowedIPs: [ "127.0.0.1", "*" ] # 白名单,*代表所有,只有允许的IP后台才能使用生成代码功能 + selectDbs: [ "default" ] # 可选生成表的数据库配置名称,支持多库 + disableTables: [ ] # 禁用的表,禁用以后将不会在选择表中看到 + delimiters: [ "@{", "}" ] # 模板引擎变量分隔符号 + # 生成应用模型,所有生成模板允许自定义,可以参考default模板进行改造 + application: + # CRUD和关系树列表模板 + crud: + templates: + # 默认的主包模板 + - group: "default" # 分组名称 + isAddon: false # 是否为插件模板 false|true + masterPackage: "gen" # 主包名称,需和controllerPath、logicPath、inputPath保持关联 + templatePath: "./resource/generate/default/curd" # 模板路径 + apiPath: "./api/gen" # goApi生成路径 + controllerPath: "./internal/controller/gen" # 控制器生成路径 + logicPath: "./internal/logic/gen" # 主要业务生成路径 + inputPath: "./internal/model/genin" # 表单过滤器生成路径 + routerPath: "./internal/router/genrouter" # 生成路由表路径 + sqlPath: "./manifest/generate" # 生成sql语句路径 + webApiPath: "../vue/apps/web-antd/src/api/gen" # webApi生成路径 + webViewsPath: "../vue/apps/web-antd/src/views/gen" # web页面生成路径 + + # 默认的插件包模板,{$name}会自动替换成实际的插件名称 + - group: "addon" # 分组名称 + isAddon: true # 是否为插件模板 false|true + masterPackage: "sys" # 主包名称,需和controllerPath、logicPath、inputPath保持关联 + templatePath: "./resource/generate/default/curd" # 模板路径 + apiPath: "./addons/{$name}/api/admin" # goApi生成路径 + controllerPath: "./addons/{$name}/controller/admin/sys" # 控制器生成路径 + logicPath: "./addons/{$name}/logic/sys" # 主要业务生成路径 + inputPath: "./addons/{$name}/model/input/sysin" # 表单过滤器生成路径 + routerPath: "./addons/{$name}/router/genrouter" # 生成路由表路径 + sqlPath: "./storage/data/generate/addons" # 生成sql语句路径 + webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径 + webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径 + +upstream: + provider: "onelink_mock" + default: + # ClientNameList: "iotlcmupstreamproxy-http-svc" + # clientName: "iotlcm.example.com:8510" + clientName: "http://iotlcm.example.com:8110" + timeout: 10s + onelinkMock: + enabled: false + baseUrl: "https://iotlcm.example.com/onelink-mock/v5" + appid: "100059999270000006" + password: "577d2b16546cbac103df9e0ce38393b5014152c8e4d76d5636ea5ccc51319033" + timeout: "10s" + tokenTTL: 3600 + accounts: + - name: "默认测试应用" + appid: "100059999270000006" + password: "577d2b16546cbac103df9e0ce38393b5014152c8e4d76d5636ea5ccc51319033" + latestToken: "" + tokenExpireAt: "" + isDefault: true diff --git a/server/config/config_default.yaml b/server/config/config_default.yaml new file mode 100644 index 0000000..d52dcf8 --- /dev/null +++ b/server/config/config_default.yaml @@ -0,0 +1,269 @@ +# config_default.yaml 配置文件 +# @Link https://github.com/xiujiecn/xiu-admin +# @Copyright Copyright (c) 2025 LiXiujie +# @Author Lxj +# @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +# 系统配置 +system: + name: "XiuAdmin" + version: "1.0" + description: "XiuAdmin Server" + gfmode: "dev" + upload: + local: + path: "resource/upload" +# 服务配置 +server: + # 基本配置 + address: ":8199" + # 接口文档 + openapiPath: "/api.json" # OpenAPI接口文档地址 + swaggerPath: "/swagger" # 内置SwaggerUI展示地址 + # 静态服务配置 + indexFiles: ["index.html","index.htm"] # 自动首页静态文件检索。默认为["index.html", "index.htm"] + indexFolder: false # 当访问静态文件目录时,是否展示目录下的文件列表。默认关闭,那么请求将返回403 + serverRoot: "resource/public" # 静态文件服务的目录根路径,配置时自动开启静态文件服务。默认关闭 + searchPaths: ["/resource/public"] # 提供静态文件服务时额外的文件搜索路径,当根路径找不到时则按照顺序在搜索目录查找。默认关闭 + fileServerEnabled: true + # 文件上传配置 + maxHeaderBytes: "20KB" + clientMaxBodySize: "200MB" + +# 日志配置 +logger: + path: "resource/log/server" # 日志文件路径。默认为空,表示关闭,仅输出到终端 + file: "{Y-m-d-H}.log" # 日志文件格式。默认为"{Y-m-d}.log" + prefix: "" # 日志内容输出前缀。默认为空 + level: "dev" # 日志输出级别 + timeFormat: "2006-01-02T15:04:05" # 自定义日志输出的时间格式,使用Golang标准的时间格式配置 + ctxKeys: [] # 自定义Context上下文变量名称,自动打印Context的变量到日志中。默认为空 + header: true # 是否打印日志的头信息。默认true + stdout: true # 日志是否同时输出到终端。默认true + rotateSize: 0 # 按照日志文件大小对文件进行滚动切分。默认为0,表示关闭滚动切分特性 + rotateExpire: 0 # 按照日志文件时间间隔对文件滚动切分。默认为0,表示关闭滚动切分特性 + rotateBackupLimit: 0 # 按照切分的文件数量清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除 + rotateBackupExpire: 0 # 按照切分的文件有效期清理切分文件,当滚动切分特性开启时有效。默认为0,表示不备份,切分则删除 + rotateBackupCompress: 0 # 滚动切分文件的压缩比(0-9)。默认为0,表示不压缩 + rotateCheckInterval: "1h" # 滚动切分的时间检测间隔,一般不需要设置。默认为1小时 + stdoutColorDisabled: false # 关闭终端的颜色打印。默认开启 + writerColorEnable: false # 日志文件是否带上颜色。默认false,表示不带颜色 + +# JWT配置 +jwt: + signingKey: "${JWT_SIGNING_KEY}" + expiresTime: 7d + bufferTime: 1d + issuer: xiujie # 发行人 + +# 验证码配置 +captcha: + keyLong: 6 + imgWidth: 240 + imgHeight: 80 + openCaptcha: 0 + openCaptchaTimeout: 3600 + +# 注册验证码配置。短信服务接口见 F:\Work\数据\test\文档\lcm\sms.md +register: + sms: + url: "http://127.0.0.1:11190/api/v1/sms/send" + method: "POST" + headers: {} + bodyTemplate: '{"config":{"serviceType":"1","regionId":"cn-hangzhou","accessKeyId":"","accessKeySecret":"","signName":"","templateId":""},"params":{"phoneNumber":"{contact}","msgContent":"{\"code\":\"{code}\"}"}}' + successStatus: 200 + +database: + logger: + path: "resource/log/server/sql" + level: "all" + stdout: true + default: # 分组名称,可自定义,默认为default + link: "mysql:${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/iotlcm?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai" + debug: true #开启调试模式 + charset: "utf8mb4" # (可选)数据库编码(如: utf8mb4/utf8/gbk/gb2312),一般设置为utf8mb4。默认为utf8。 + dryRun: false #ORM空跑(只读不写) + maxIdle: 30 #连接池最大闲置的连接数 + maxOpen: 100 #连接池最大打开的连接数 + maxLifetime: 120 #(单位秒)连接对象可重复使用的时间长度 + +# 缓存配置 +cache: + adapter: "redis" # 缓存适配器,支持:file|redis + fileDir: "resource/cache" # 缓存文件目录 + +# Redis 配置示例 +redis: + # 单实例配置 + default: + mode: "single" #redis模式,支持:single|cluster|sentinel + sentinelMasterName: "${REDIS_SENTINEL_MASTER}" #哨兵模式主节点名称 + address: 192.0.2.200:6379 + db: 0 + pass: "${REDIS_PASSWORD}" + minIdle: 5 #最小空闲连接数 + maxIdle: 30 #允许闲置的最大连接数 + maxActive: 5000 #最大活跃连接数 + idleTimeout: "60s" #连接最大空闲时间,使用时间字符串例如30s/2m/2d + ClientName: "SystemCache" + +rabbitmq: + enabled: false + # 示例:amqp://${MQ_USER}:${MQ_PASSWORD}@${MQ_HOST}/ + dsn: "" + +workerBatchAsync: + enabled: false + queueName: "iotlcm.batch.execute" + failOnPublish: true + +payment: + mock: + enabled: false + wechat: + enabled: false + envFile: "" + apiBase: "https://api.mch.weixin.qq.com" + mchId: "" + mchSerialNo: "" + apiV3Key: "" + privateKeyPath: "resource/certs/wechatpay_apiclient_key.pem" + notifyUrl: "" + notifyBaseUrl: "" + refundNotifyUrl: "" + timeoutMinutes: 5 + platformPublicKeyId: "" + platformPublicKeyPath: "resource/certs/wechatpay_platform_public_key.pem" + h5: + enabled: false + appId: "" + appName: "" + appUrl: "" + payerClientIp: "" + mini: + enabled: false + appId: "" + appSecret: "" + +appCardRecognition: + enabled: false + provider: "openai-compatible" + baseUrl: "https://xueyangai.com/v1" + endpoint: "" + model: "qwen3.7-flash" + apiKey: "sk-nAhIIyqOn7JvQp1ti8o24GcBLgWqBUFKBaDTwAmFYZm0Hycy" + timeout: "25s" + maxImageBytes: 8388608 + maxTokens: 256 + rateLimit: + enabled: true + count: 20 + period: "1m" + +appWarning: + wechatSubscribeTemplateId: "DMDP2PKktdtG1ES_3Ic1JcR0LGELQmWE6LunYyerHCY" + +appBusinessAsync: + enabled: false + provider: "rabbitmq" + queueName: "iotlcm.app.business" + failOnPublish: false + +upstream: + provider: "onelink_mock" + default: + # 正式环境建议配置为上游代理 HTTP 服务名,例如 iotlcmupstreamproxy-http-svc:8510 + clientName: "" + timeout: 10s + onelinkMock: + enabled: false + baseUrl: "https://iotlcm.example.com/onelink-mock/v5" + appid: "" + password: "" + timeout: "10s" + tokenTTL: 3600 + accounts: + - name: "默认测试应用" + appid: "" + password: "" + latestToken: "" + tokenExpireAt: "" + isDefault: true + +# Queue配置 +queue: + type: "asynq" # 队列类型 + asynq: + redis: + address: 192.0.2.200:6379 + db: 0 + pass: "${REDIS_PASSWORD}" + user: + concurrency: 10 # 最大同时执行的任务数量 + logLevel: 4 # 日志级别 DebugLevel 1, InfoLevel 2, WarnLevel 3, ErrorLevel 4, FatalLevel 5 + +# 路由配置 +router: + # 不需要验证登录的路由地址 + exceptLogin: + - /api/v1/system/auth/login + - /api/v1/system/auth/refresh + - /api/v1/system/captcha + - /api/v1/system/auth/getTenantList + - /api/v1/auth/captcha + - /api/v1/auth/side-captcha + - /api/v1/auth/check-side-captcha + - /api/v1/auth/sms + - /api/v1/auth/register + - /api/v1/auth/login + - /api/v1/auth/wechat-login + - /api/v1/auth/wechat-bind-phone + - /api/v1/auth/forgot-password + - /api/v1/card/public-query + - /api/v1/card/recognize + - /api/v1/card/public-package-detail + - /api/v1/diagnosis/info + - /api/v1/diagnosis/flow + - /api/v1/open/diagnosis/info + - /api/v1/open/diagnosis/flow + - /api/v1/payment/wechat/notify + - /api/v1/payment/wechat/refund-notify + + +# 生成代码 +xgen: + allowedIPs: [ "127.0.0.1", "*" ] # 白名单,*代表所有,只有允许的IP后台才能使用生成代码功能 + selectDbs: [ "default" ] # 可选生成表的数据库配置名称,支持多库 + disableTables: [ ] # 禁用的表,禁用以后将不会在选择表中看到 + delimiters: [ "@{", "}" ] # 模板引擎变量分隔符号 + # 生成应用模型,所有生成模板允许自定义,可以参考default模板进行改造 + application: + # CRUD和关系树列表模板 + crud: + templates: + # 默认的主包模板 + - group: "default" # 分组名称 + isAddon: false # 是否为插件模板 false|true + masterPackage: "gen" # 主包名称,需和controllerPath、logicPath、inputPath保持关联 + templatePath: "./resource/generate/default/curd" # 模板路径 + apiPath: "./api/gen/v1/" # goApi生成路径 + controllerPath: "./internal/controller/gen" # 控制器生成路径 + logicPath: "./internal/logic/gen" # 主要业务生成路径 + inputPath: "./internal/model/genin" # 表单过滤器生成路径 + routerPath: "./internal/router/genrouter" # 生成路由表路径 + sqlPath: "./manifest/generate" # 生成sql语句路径 + webApiPath: "./web/generate/api" # webApi生成路径 + webViewsPath: "./web/generate/views" # web页面生成路径 + + # 默认的插件包模板,{$name}会自动替换成实际的插件名称 + - group: "addon" # 分组名称 + isAddon: true # 是否为插件模板 false|true + masterPackage: "sys" # 主包名称,需和controllerPath、logicPath、inputPath保持关联 + templatePath: "./resource/generate/default/curd" # 模板路径 + apiPath: "./addons/{$name}/api/admin" # goApi生成路径 + controllerPath: "./addons/{$name}/controller/admin/sys" # 控制器生成路径 + logicPath: "./addons/{$name}/logic/sys" # 主要业务生成路径 + inputPath: "./addons/{$name}/model/input/sysin" # 表单过滤器生成路径 + routerPath: "./addons/{$name}/router/genrouter" # 生成路由表路径 + sqlPath: "./storage/data/generate/addons" # 生成sql语句路径 + webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径 + webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径 diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..f3b4db1 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,118 @@ +module git.5iot.com/iotlcm-public/iotlcm-app + +go 1.23.0 + +toolchain go1.23.5 + +require ( + github.com/cloudwego/eino v0.9.19 + github.com/cloudwego/eino-ext/components/model/openai v0.1.13 + github.com/dromara/carbon/v2 v2.6.0 + github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.0 + github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6 + github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.0 + github.com/gogf/gf/v2 v2.9.6 + github.com/golang-jwt/jwt/v4 v4.5.2 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/mileusna/useragent v1.3.5 + github.com/mojocn/base64Captcha v1.3.8 + github.com/olekukonko/tablewriter v1.1.0 + github.com/pkg/errors v0.9.1 + github.com/rabbitmq/amqp091-go v1.10.0 + github.com/redis/go-redis/v9 v9.7.0 + github.com/robfig/cron/v3 v3.0.1 + github.com/shirou/gopsutil/v3 v3.24.5 + github.com/taosdata/driver-go/v3 v3.8.0 + github.com/wechatpay-apiv3/wechatpay-go v0.2.21 + github.com/wenlng/go-captcha-assets v1.0.7 + github.com/wenlng/go-captcha/v2 v2.0.5 + github.com/xiujiecn/asynq v0.25.10 + github.com/xuri/excelize/v2 v2.9.0 + go.opentelemetry.io/otel v1.38.0 + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 + golang.org/x/net v0.40.0 + golang.org/x/tools v0.31.0 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/eino-contrib/jsonschema v1.0.3 // indirect + github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect + github.com/evanphx/json-patch v0.5.2 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/goph/emperror v0.17.2 // indirect + github.com/grokify/html-strip-tags-go v0.1.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/meguminnnnnnnnn/go-openai v0.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/nikolalohinski/gonja v1.5.3 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect + github.com/pelletier/go-toml/v2 v2.0.9 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect + github.com/yargevad/filepathx v1.0.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/arch v0.11.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/image v0.23.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.8.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..6259ff3 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,394 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= +github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/mockey v1.3.0 h1:ONLRdvhqmCfr9rTasUB8ZKCfvbdD2tohOg4u+4Q/ed0= +github.com/bytedance/mockey v1.3.0/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cloudwego/eino v0.9.19 h1:i71YUBK3nwY4L53dkzRgZpAcPSZ4v4eRponN7W9sDtk= +github.com/cloudwego/eino v0.9.19/go.mod h1:OBD1mrkfkt/pJa4rkg1P0VnaMeOVl7l8IAdEqY//3IQ= +github.com/cloudwego/eino-ext/components/model/openai v0.1.13 h1:5XHRTiTD5bt9KQrMHcfvuWNklEC3tpm3XHejdozt9vM= +github.com/cloudwego/eino-ext/components/model/openai v0.1.13/go.mod h1:mgIoqYYOc0eECCqvLbEYpOJrQNTNxkwXzSJzFU+v5sQ= +github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17 h1:EeVcR1TslRA2IdNW1h/2LaGbPlffwGhQm99jM3zWZiI= +github.com/cloudwego/eino-ext/libs/acl/openai v0.1.17/go.mod h1:Zkcx6DPTR2NfWmtSXbhItswGw6hqUezNPhNcke0pOG8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dromara/carbon/v2 v2.6.0 h1:LyRL5QHK6q37wjHjbBkuTGHXbdQKuYy3Isu0RhsxVWo= +github.com/dromara/carbon/v2 v2.6.0/go.mod h1:Baj3A1uBBctJmpZWJd6/+WWnmIuY2pobR6IOpB6xigc= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0= +github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4= +github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= +github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.0 h1:1f7EeD0lfPHoXfaJDSL7cxRcSRelbsAKgF3MGXY+Uyo= +github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.0/go.mod h1:tToO1PjGkLIR+9DbJ0wrKicYma0H/EUHXOpwel6Dw+0= +github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6 h1:3QZvWIlz3dLjNELQU+5ZZZWuzEx9gsRFLU+qIKVUG6M= +github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6/go.mod h1:7EEAe8UYI5dLeuwCWN3HgC62OhjIYbkynaoavw1U/k4= +github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.0 h1:EEZqu1PNRSmm+7Cqm9A/8+ObgfbMzhE1ps9Z3LD7HgM= +github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.0/go.mod h1:LHrxY+2IzNTHVTPG/s5yaz1VmXbj+CQ7Hr5SeVkHiTw= +github.com/gogf/gf/v2 v2.9.6 h1:fQ6uPtS1Ra8qY+OuzPPZTlgksJ4eOXmTZ1/a2l3Idog= +github.com/gogf/gf/v2 v2.9.6/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= +github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/meguminnnnnnnnn/go-openai v0.1.2 h1:iXombGGjqjBrmE9WaSidUhhi3YQhf42QTHvHLMkgvCA= +github.com/meguminnnnnnnnn/go-openai v0.1.2/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws= +github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= +github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= +github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= +github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= +github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= +github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/taosdata/driver-go/v3 v3.8.0 h1:qY1xFidspISuuZc0TAbeJWMtAdKJOSYQOXfepkUK6fI= +github.com/taosdata/driver-go/v3 v3.8.0/go.mod h1:S6OGOinfR0xxxaMGsvBi9cLkYxEIW1p6qqr8QJATTlg= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/wechatpay-apiv3/wechatpay-go v0.2.21 h1:uIyMpzvcaHA33W/QPtHstccw+X52HO1gFdvVL9O6Lfs= +github.com/wechatpay-apiv3/wechatpay-go v0.2.21/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q= +github.com/wenlng/go-captcha-assets v1.0.7 h1:tfF84A4un/i4p+TbRVHDqDPeQeatvddOfB2xbKvLVq8= +github.com/wenlng/go-captcha-assets v1.0.7/go.mod h1:zinRACsdYcL/S6pHgI9Iv7FKTU41d00+43pNX+b9+MM= +github.com/wenlng/go-captcha/v2 v2.0.5 h1:+1FpVwJZmLCqEHxOt+HvpUArFGo107nRxOeRVHkZhTc= +github.com/wenlng/go-captcha/v2 v2.0.5/go.mod h1:5hac1em3uXoyC5ipZ0xFv9umNM/waQvYAQdr0cx/h34= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/xiujiecn/asynq v0.25.10 h1:TWy8i127GOZjXh3yTKGhjNfeThGP5O61RG92haPXX1o= +github.com/xiujiecn/asynq v0.25.10/go.mod h1:Nxtqdx+iW8gKpJHpxyNevTEq1E4eD2gQgcBISajdJQk= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= +golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= +golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/server/hack/config.yaml b/server/hack/config.yaml new file mode 100644 index 0000000..67d1017 --- /dev/null +++ b/server/hack/config.yaml @@ -0,0 +1,18 @@ + +# CLI tool, only in development environment. +# https://goframe.org/docs/cli +gfcli: + gen: + dao: + - link: "mysql:${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)/db_iotlcm?charset=utf8mb4&parseTime=True&loc=Asia%2FShanghai" + descriptionTag: true # 数据表字段注释 + noModelComment: true + jsonCase: "CamelLower" # 生成json字段时,字段名使用驼峰命名 + gJsonSupport: true + group: "default" # 分组,使用代码生成功能时必须填写 + tablesEx: "sys_addons_install,_bak_*" # 排除系统扩展表和数据库备份表 + + docker: + build: "-a amd64 -s linux -p temp -ew" + tagPrefixes: + - my.image.pub/my-app diff --git a/server/hack/hack-cli.mk b/server/hack/hack-cli.mk new file mode 100644 index 0000000..27a4337 --- /dev/null +++ b/server/hack/hack-cli.mk @@ -0,0 +1,15 @@ + +# Keep the CLI version aligned with github.com/gogf/gf/v2 in go.mod. +GF_CLI_VERSION ?= v2.9.6 +GF_CMD ?= go run github.com/gogf/gf/cmd/gf/v2@$(GF_CLI_VERSION) + +# Install the pinned CLI tool for developers who want a local executable. +.PHONY: cli +cli: + @go install github.com/gogf/gf/cmd/gf/v2@$(GF_CLI_VERSION) + +# Validate the pinned CLI can run. Generation targets invoke GF_CMD directly, +# so a different gf executable earlier on PATH cannot silently take over. +.PHONY: cli.install +cli.install: + @$(GF_CMD) -v >/dev/null diff --git a/server/hack/hack.mk b/server/hack/hack.mk new file mode 100644 index 0000000..9449238 --- /dev/null +++ b/server/hack/hack.mk @@ -0,0 +1,75 @@ +.DEFAULT_GOAL := build + +# Keep the GoFrame framework and CLI versions managed explicitly. +.PHONY: up +up: cli.install + @echo "GoFrame CLI is pinned by hack/hack-cli.mk; run 'go get github.com/gogf/gf/v2@' explicitly when upgrading the framework." + +# Build binary using configuration from hack/config.yaml. +.PHONY: build +build: cli.install + @$(GF_CMD) build -ew + +# Parse api and generate controller/sdk. +.PHONY: ctrl +ctrl: cli.install + @$(GF_CMD) gen ctrl -m + +# Generate Go files for DAO/DO/Entity. +.PHONY: dao +dao: cli.install + @$(GF_CMD) gen dao + +# Parse current project go files and generate enums go file. +.PHONY: enums +enums: cli.install + @$(GF_CMD) gen enums + +# Generate Go files for Service. +.PHONY: service +service: cli.install + @$(GF_CMD) gen service + + +# Build docker image. +.PHONY: image +image: cli.install + $(eval _TAG = $(shell git rev-parse --short HEAD)) +ifneq (, $(shell git status --porcelain 2>/dev/null)) + $(eval _TAG = $(_TAG).dirty) +endif + $(eval _TAG = $(if ${TAG}, ${TAG}, $(_TAG))) + $(eval _PUSH = $(if ${PUSH}, ${PUSH}, )) + @$(GF_CMD) docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG}; + + +# Build docker image and automatically push to docker repo. +.PHONY: image.push +image.push: cli.install + @make image PUSH=-p; + + +# Deploy image and yaml to current kubectl environment. +.PHONY: deploy +deploy: cli.install + $(eval _TAG = $(if ${TAG}, ${TAG}, develop)) + + @set -e; \ + mkdir -p $(ROOT_DIR)/temp/kustomize;\ + cd $(ROOT_DIR)/manifest/deploy/kustomize/overlays/${_ENV};\ + kustomize build > $(ROOT_DIR)/temp/kustomize.yaml;\ + kubectl apply -f $(ROOT_DIR)/temp/kustomize.yaml; \ + if [ $(DEPLOY_NAME) != "" ]; then \ + kubectl patch -n $(NAMESPACE) deployment/$(DEPLOY_NAME) -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(shell date +%s)\"}}}}}"; \ + fi; + + +# Parsing protobuf files and generating go files. +.PHONY: pb +pb: cli.install + @$(GF_CMD) gen pb + +# Generate protobuf files for database tables. +.PHONY: pbentity +pbentity: cli.install + @$(GF_CMD) gen pbentity diff --git a/server/internal/ai/chat_model.go b/server/internal/ai/chat_model.go new file mode 100644 index 0000000..a5b1f68 --- /dev/null +++ b/server/internal/ai/chat_model.go @@ -0,0 +1,112 @@ +package ai + +import ( + "context" + "net/url" + "strings" + "time" + + openaiModel "github.com/cloudwego/eino-ext/components/model/openai" + "github.com/cloudwego/eino/components/model" + "github.com/gogf/gf/v2/errors/gerror" +) + +type OpenAICompatibleConfig struct { + APIKey string + BaseURL string + Endpoint string + Model string + Timeout time.Duration + MaxTokens int + JSONOutput bool +} + +const ProviderOpenAICompatible = "openai-compatible" + +type ChatModelConfig struct { + Provider string + APIKey string + BaseURL string + Endpoint string + Model string + Timeout time.Duration + MaxTokens int + JSONOutput bool +} + +func NewChatModel(ctx context.Context, config ChatModelConfig) (model.BaseChatModel, error) { + provider := strings.ToLower(strings.TrimSpace(config.Provider)) + if provider == "" { + provider = ProviderOpenAICompatible + } + switch provider { + case ProviderOpenAICompatible, "openai": + return NewOpenAICompatibleChatModel(ctx, OpenAICompatibleConfig{ + APIKey: config.APIKey, + BaseURL: config.BaseURL, + Endpoint: config.Endpoint, + Model: config.Model, + Timeout: config.Timeout, + MaxTokens: config.MaxTokens, + JSONOutput: config.JSONOutput, + }) + default: + return nil, gerror.Newf("暂不支持AI模型提供方:%s", provider) + } +} + +func NewOpenAICompatibleChatModel(ctx context.Context, config OpenAICompatibleConfig) (model.BaseChatModel, error) { + apiKey := strings.TrimSpace(config.APIKey) + if apiKey == "" { + return nil, gerror.New("AI模型API Key不能为空") + } + modelName := strings.TrimSpace(config.Model) + if modelName == "" { + return nil, gerror.New("AI模型名称不能为空") + } + baseURL, err := ResolveOpenAIBaseURL(config.BaseURL, config.Endpoint) + if err != nil { + return nil, err + } + + modelConfig := &openaiModel.ChatModelConfig{ + APIKey: apiKey, + BaseURL: baseURL, + Model: modelName, + Timeout: config.Timeout, + } + if config.MaxTokens > 0 { + modelConfig.MaxTokens = &config.MaxTokens + } + if config.JSONOutput { + modelConfig.ResponseFormat = &openaiModel.ChatCompletionResponseFormat{ + Type: openaiModel.ChatCompletionResponseFormatTypeJSONObject, + } + } + + chatModel, err := openaiModel.NewChatModel(ctx, modelConfig) + if err != nil { + return nil, gerror.Wrap(err, "初始化Eino AI模型失败") + } + return chatModel, nil +} + +func ResolveOpenAIBaseURL(baseURL string, endpoint string) (string, error) { + value := strings.TrimSpace(baseURL) + if value == "" { + value = strings.TrimSpace(endpoint) + } + if value == "" { + return "", gerror.New("AI模型BaseURL不能为空") + } + + parsed, err := url.Parse(value) + if err != nil || parsed.Scheme == "" || parsed.Host == "" { + return "", gerror.New("AI模型BaseURL格式不正确") + } + parsed.Path = strings.TrimSuffix(strings.TrimRight(parsed.Path, "/"), "/chat/completions") + parsed.RawPath = "" + parsed.RawQuery = "" + parsed.Fragment = "" + return strings.TrimRight(parsed.String(), "/"), nil +} diff --git a/server/internal/ai/chat_model_test.go b/server/internal/ai/chat_model_test.go new file mode 100644 index 0000000..5f48c1a --- /dev/null +++ b/server/internal/ai/chat_model_test.go @@ -0,0 +1,92 @@ +package ai + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/cloudwego/eino/schema" +) + +func TestResolveOpenAIBaseURL(t *testing.T) { + tests := []struct { + name string + baseURL string + endpoint string + want string + wantErr bool + }{ + {name: "base url", baseURL: "https://xueyangai.com/v1/", want: "https://xueyangai.com/v1"}, + {name: "legacy endpoint", endpoint: "https://xueyangai.com/v1/chat/completions", want: "https://xueyangai.com/v1"}, + {name: "base url preferred", baseURL: "https://example.com/v1", endpoint: "https://legacy.example.com/v1/chat/completions", want: "https://example.com/v1"}, + {name: "invalid", baseURL: "xueyangai.com/v1", wantErr: true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := ResolveOpenAIBaseURL(test.baseURL, test.endpoint) + if test.wantErr { + if err == nil { + t.Fatal("expected error") + } + return + } + if err != nil { + t.Fatalf("ResolveOpenAIBaseURL() error = %v", err) + } + if got != test.want { + t.Fatalf("ResolveOpenAIBaseURL() = %s, want %s", got, test.want) + } + }) + } +} + +func TestOpenAICompatibleChatModel(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if request.URL.Path != "/v1/chat/completions" { + t.Errorf("path = %s, want /v1/chat/completions", request.URL.Path) + } + if request.Header.Get("Authorization") != "Bearer test-key" { + t.Errorf("authorization = %s", request.Header.Get("Authorization")) + } + var body map[string]any + if err := json.NewDecoder(request.Body).Decode(&body); err != nil { + t.Errorf("decode request: %v", err) + } + if body["model"] != "test-model" { + t.Errorf("model = %v, want test-model", body["model"]) + } + writer.Header().Set("Content-Type", "application/json") + _, _ = writer.Write([]byte(`{"id":"chatcmpl-test","object":"chat.completion","created":1,"model":"test-model","choices":[{"index":0,"message":{"role":"assistant","content":"{\"ok\":true}"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`)) + })) + defer server.Close() + + chatModel, err := NewOpenAICompatibleChatModel(context.Background(), OpenAICompatibleConfig{ + APIKey: "test-key", + BaseURL: server.URL + "/v1", + Model: "test-model", + Timeout: 3 * time.Second, + MaxTokens: 64, + JSONOutput: true, + }) + if err != nil { + t.Fatalf("NewOpenAICompatibleChatModel() error = %v", err) + } + response, err := chatModel.Generate(context.Background(), []*schema.Message{schema.UserMessage("hello")}) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + if response.Content != `{"ok":true}` { + t.Fatalf("response content = %s", response.Content) + } +} + +func TestNewChatModelRejectsUnsupportedProvider(t *testing.T) { + _, err := NewChatModel(context.Background(), ChatModelConfig{Provider: "unsupported"}) + if err == nil { + t.Fatal("expected unsupported provider error") + } +} diff --git a/server/internal/cmd/cmd.go b/server/internal/cmd/cmd.go new file mode 100644 index 0000000..7acaf5a --- /dev/null +++ b/server/internal/cmd/cmd.go @@ -0,0 +1,10 @@ +// Package cmd +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package cmd + +var ( + Main = Http +) diff --git a/server/internal/cmd/global.go b/server/internal/cmd/global.go new file mode 100644 index 0000000..a89dccc --- /dev/null +++ b/server/internal/cmd/global.go @@ -0,0 +1,61 @@ +// Package cmd +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package cmd + +import ( + "context" + "errors" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/cache" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmode" +) + +// 系统初始化 +func SystemInit(ctx context.Context) { + // 检查数据库连接 + if err := CheckConnDB(ctx); err != nil { + panic(err) + } + // 设置gf运行模式 + SetGFMode(ctx) + // 默认上海时区 + if err := gtime.SetTimeZone("Asia/Shanghai"); err != nil { + g.Log().Fatalf(ctx, "时区设置异常 err: %+v", err) + return + } + // 设置缓存适配器 + cache.SetAdapter(ctx) +} + +// CheckConnDB 检查数据库连接 +func CheckConnDB(ctx context.Context) error { + err := g.DB().PingMaster() + if err != nil { + return errors.New("连接到数据库失败") + } + return nil +} + +// 设置gf运行模式 +func SetGFMode(ctx context.Context) { + gfmode := g.Cfg().MustGet(ctx, "system.gfmode").String() + if len(gfmode) == 0 { + gfmode = gmode.NOT_SET + } + var modes = []string{gmode.DEVELOP, gmode.TESTING, gmode.STAGING, gmode.PRODUCT} + for _, mode := range modes { + if mode == gfmode { + gmode.Set(mode) + break + } + } +} + +func DBHandle(ctx context.Context) { + +} diff --git a/server/internal/cmd/http.go b/server/internal/cmd/http.go new file mode 100644 index 0000000..8aeabc9 --- /dev/null +++ b/server/internal/cmd/http.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "context" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/cmd/inithttp" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/websocket" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/router" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/tasks" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcmd" + + _ "git.5iot.com/iotlcm-public/iotlcm-app/internal/queues/httpqueues" +) + +func init() { + inithttp.RegisterHttpInitFunc("initJobTask", initJobTask) +} + +var Http = &gcmd.Command{ + Name: "http", + Usage: "http", + Brief: "HTTP控制台服务", + Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + g.Log().Infof(ctx, "Http init start") + serverWg.Add(1) + s := g.Server() + go func() { + waitServerClose(ctx) + websocket.StopWebSocket(ctx) + s.Shutdown() + serverWg.Done() + }() + err = inithttp.InitHttp(ctx) + if err != nil { + g.Log().Errorf(ctx, "http init error: %v", err) + return err + } + dumpRouterMap := g.Cfg().MustGet(ctx, "server.dumpRouterMap", false).Bool() + s.SetDumpRouterMap(dumpRouterMap) + router.InitRouter(ctx, s) + websocket.StartWebSocket(ctx) + s.Run() + inithttp.CleanupHttp(ctx) + return nil + }, +} + +func initJobTask(ctx context.Context) error { + tasks.TasksInstance(ctx) + service.SysJob().InitRegister() + return nil +} diff --git a/server/internal/cmd/initfuunc.go b/server/internal/cmd/initfuunc.go new file mode 100644 index 0000000..862a9b1 --- /dev/null +++ b/server/internal/cmd/initfuunc.go @@ -0,0 +1,46 @@ +// Package cmd +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package cmd + +import ( + "context" + "errors" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gmode" +) + +var ( + initFuncList map[string]func(ctx context.Context) error +) + +func RegisterInitFunc(name string, fn func(ctx context.Context) error) { + if initFuncList == nil { + initFuncList = make(map[string]func(ctx context.Context) error) + } + initFuncList[name] = fn +} + +func InitSystemDeferFunc(ctx context.Context) error { + for name, fn := range initFuncList { + if fn == nil { + g.Log().Errorf(ctx, "InitSystemDeferFunc %s fn is nil.", name) + return errors.New("func is nil") + } + err := fn(ctx) + if err != nil { + g.Log().Errorf(ctx, "InitSystemDeferFunc %s err: %v", name, err) + return err + } + g.Log().Infof(ctx, "InitSystemDeferFunc %s success.", name) + } + // 初始化生成代码配置 + if !gmode.IsProduct() { + xgen.Init(ctx) + } + return nil +} diff --git a/server/internal/cmd/inithttp/inithttp.go b/server/internal/cmd/inithttp/inithttp.go new file mode 100644 index 0000000..a2b45c1 --- /dev/null +++ b/server/internal/cmd/inithttp/inithttp.go @@ -0,0 +1,59 @@ +package inithttp + +import ( + "context" + "sync" + + "github.com/gogf/gf/v2/frame/g" +) + +var ( + httpInitFunc map[string]func(ctx context.Context) error + httpCleanupFunc map[string]func(ctx context.Context) error + rwLock sync.RWMutex +) + +func RegisterHttpInitFunc(name string, fn func(ctx context.Context) error) { + rwLock.Lock() + defer rwLock.Unlock() + if httpInitFunc == nil { + httpInitFunc = make(map[string]func(ctx context.Context) error) + } + httpInitFunc[name] = fn +} + +func RegisterHttpCleanupFunc(name string, fn func(ctx context.Context) error) { + rwLock.Lock() + defer rwLock.Unlock() + if httpCleanupFunc == nil { + httpCleanupFunc = make(map[string]func(ctx context.Context) error) + } + httpCleanupFunc[name] = fn +} + +func InitHttp(ctx context.Context) error { + rwLock.RLock() + defer rwLock.RUnlock() + for name, fn := range httpInitFunc { + if err := fn(ctx); err != nil { + g.Log().Errorf(ctx, "httpInitFunc %s error: %v", name, err) + return err + } else { + g.Log().Infof(ctx, "httpInitFunc %s success", name) + } + } + return nil +} + +func CleanupHttp(ctx context.Context) error { + rwLock.RLock() + defer rwLock.RUnlock() + for name, fn := range httpCleanupFunc { + if err := fn(ctx); err != nil { + g.Log().Errorf(ctx, "httpCleanupFunc %s error: %v", name, err) + } else { + g.Log().Infof(ctx, "httpCleanupFunc %s success", name) + } + } + return nil +} diff --git a/server/internal/cmd/monitor.go b/server/internal/cmd/monitor.go new file mode 100644 index 0000000..1928f6c --- /dev/null +++ b/server/internal/cmd/monitor.go @@ -0,0 +1,74 @@ +// Package cmd +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package cmd + +import ( + "context" + "fmt" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/cmd/inithttp" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/websocket" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + "time" +) + +func init() { + inithttp.RegisterHttpInitFunc("InitMonitor", func(ctx context.Context) error { + InitMonitor() + return nil + }) +} + +func InitMonitor() { + go func() { + for { + RunMonitor() + time.Sleep(time.Second * 5) + } + }() + websocket.RegisterTagCallback(consts.WSTagMonitorServer, func(client *websocket.Client, tag string) { + RunMonitor() + }) +} + +func RunMonitor() { + ctx := context.Background() + hostInfo := service.MonitorServer().GetHostInfo(ctx) + websocket.SendToTag(consts.WSTagMonitorServer, &websocket.WResponse{ + Event: fmt.Sprintf(consts.WSEventMonitorServer, consts.MonitorServerHost), + Data: hostInfo, + }) + sysLoad := service.MonitorServer().GetSysLoad(ctx) + websocket.SendToTag(consts.WSTagMonitorServer, &websocket.WResponse{ + Event: fmt.Sprintf(consts.WSEventMonitorServer, consts.MonitorServerSysLoad), + Data: sysLoad, + }) + cpuInfo := service.MonitorServer().GetCpuInfo(ctx) + websocket.SendToTag(consts.WSTagMonitorServer, &websocket.WResponse{ + Event: fmt.Sprintf(consts.WSEventMonitorServer, consts.MonitorServerCpu), + Data: cpuInfo, + }) + memInfo := service.MonitorServer().GetMemInfo(ctx) + websocket.SendToTag(consts.WSTagMonitorServer, &websocket.WResponse{ + Event: fmt.Sprintf(consts.WSEventMonitorServer, consts.MonitorServerMem), + Data: memInfo, + }) + diskInfo := service.MonitorServer().GetDiskInfo(ctx) + websocket.SendToTag(consts.WSTagMonitorServer, &websocket.WResponse{ + Event: fmt.Sprintf(consts.WSEventMonitorServer, consts.MonitorServerDisk), + Data: diskInfo, + }) + netInfo := service.MonitorServer().GetNetStatusInfo(ctx) + websocket.SendToTag(consts.WSTagMonitorServer, &websocket.WResponse{ + Event: fmt.Sprintf(consts.WSEventMonitorServer, consts.MonitorServerNet), + Data: netInfo, + }) + goInfo := service.MonitorServer().GetGoInfo(ctx) + websocket.SendToTag(consts.WSTagMonitorServer, &websocket.WResponse{ + Event: fmt.Sprintf(consts.WSEventMonitorServer, consts.MonitorServerGo), + Data: goInfo, + }) +} diff --git a/server/internal/cmd/shutdown.go b/server/internal/cmd/shutdown.go new file mode 100644 index 0000000..9ad037f --- /dev/null +++ b/server/internal/cmd/shutdown.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "context" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event" + "os" + "sync" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/os/gproc" +) + +var ( + serverCloseSignal = make(chan struct{}, 1) + serverWg = sync.WaitGroup{} + once sync.Once +) + +// 等待接收服务关闭信号 +func waitServerClose(ctx context.Context) { + signalListen(ctx, handleSignalClose) + <-serverCloseSignal +} + +// 等待所有服务关闭 +func waitAllServerClose(ctx context.Context) { + serverWg.Wait() +} + +// 处理服务关闭信号 +func handleSignalClose(sig os.Signal) { + serverCloseEvent(gctx.GetInitCtx()) + serverCloseSignal <- struct{}{} +} + +// 监听信号 +func signalListen(ctx context.Context, handler ...gproc.SigHandler) { + g.Go(ctx, func(ctx context.Context) { + // syscall.SIGINT, + // syscall.SIGQUIT, + // syscall.SIGKILL, + // syscall.SIGTERM, + // syscall.SIGABRT. + gproc.AddSigHandlerShutdown(handler...) + gproc.Listen() + }, func(ctx context.Context, exception error) { + g.Log().Fatalf(ctx, "信号监听异常 err: %+v", exception) + }) +} + +// serverCloseEvent 关闭事件 +func serverCloseEvent(ctx context.Context) { + once.Do(func() { + event.EventsInstance().Emit(ctx, consts.EventKeyServerClose, nil) + }) +} diff --git a/server/internal/consts/app_register.go b/server/internal/consts/app_register.go new file mode 100644 index 0000000..7a8cdf4 --- /dev/null +++ b/server/internal/consts/app_register.go @@ -0,0 +1,6 @@ +package consts + +const ( + ConfigKeyAppRegisterDeptId = "iot.register.app.dept" + ConfigKeyAppRegisterRoleId = "iot.register.app.role" +) diff --git a/server/internal/consts/batch.go b/server/internal/consts/batch.go new file mode 100644 index 0000000..7f63978 --- /dev/null +++ b/server/internal/consts/batch.go @@ -0,0 +1,151 @@ +package consts + +const ( + // 批次异步状态 batch_status + BatchAsyncStatusCreated = 0 + BatchAsyncStatusPending = 1 + BatchAsyncStatusProcessing = 2 + BatchAsyncStatusSuccess = 3 + BatchAsyncStatusFailed = 4 + BatchAsyncStatusPartialSuccess = 5 + + // 兼容旧业务命名。 + BatchStatusNormal = BatchAsyncStatusCreated + BatchStatusSuccess = BatchAsyncStatusSuccess + + // 批次运行结果 run_result + BatchRunResultPending = 0 + BatchRunResultRunning = 1 + BatchRunResultSuccess = 2 + BatchRunResultFailed = 3 + BatchRunResultPartialSuccess = 4 + + // 批次明细状态 detail_status + BatchDetailStatusPending = 0 + BatchDetailStatusRunning = 1 + BatchDetailStatusSuccess = 2 + BatchDetailStatusFailed = 3 + + BatchSourceTypeBatchSelect = 0 + + BatchTypePrepare = 4 + BatchTypeAllocate = 3 + BatchTypeRenew = 101 + BatchTypeChange = 102 + BatchTypeStart = 103 + BatchTypeStop = 104 + BatchTypeRestart = 105 + BatchTypeDataStart = 109 + BatchTypeDataStop = 110 + + BatchNoPrefixAllocate = "ALC" + BatchNoPrefixRenew = "REN" + BatchNoPrefixChange = "CHG" + BatchNoPrefixStart = "STR" + BatchNoPrefixStop = "STP" + BatchNoPrefixRestart = "RST" + BatchNoPrefixDataStart = "DOP" + BatchNoPrefixDataStop = "DST" + BatchNoPrefixWarning = "SWC" + + BlankOutStatusNo = 1 +) + +const ( + DownloadStatusPending = 0 + DownloadStatusGenerated = 1 + DownloadStatusRunning = 2 + DownloadStatusFailed = 3 +) + +const ( + ResourceTypeExcel = 1 + ResourceExportSceneAllocate = 3 + ResourceExportSceneRenew = 101 +) + +const ( + ResourceBatchDownloadTaskSimCardAllocate = "卡划拨结果" + ResourceBatchDownloadTaskSimCardRenew = "卡续费结果" + SimCardAllocateSubDirImportExcel = "sim-card-allocate/import" + SimCardAllocateSubDirResultExcel = "sim-card-allocate/result" + SimCardRenewSubDirImportExcel = "sim-card-renew/import" + SimCardRenewSubDirResultExcel = "sim-card-renew/result" +) + +const ( + CustomerTypeAgent = 1 + CustomerTypeTerminal = 2 + + OrderTypeCardSubscribe = 1 + OrderTypeBalanceRecharge = 2 + + OrderStatusPendingPay = 0 + OrderStatusCompleted = 5 + + OrderReturnStatusNone = 0 + OrderReturnStatusPending = 1 + OrderReturnStatusRunning = 2 + OrderReturnStatusPartial = 3 + OrderReturnStatusSuccess = 4 + + PayTypeBalance = 0 + PayTypeOnline = 1 + + PayStatusUnpaid = 0 + PayStatusPaid = 2 + + ShippingMethodNone = 3 + + ProductTypeCard = 1 + ProductTypeBalance = 4 + + PayModeYearMonth = 1 + + PackageBusinessTypeSubscribe = 0 + PackageBusinessTypeChange = 1 + PackageBusinessTypeRenew = 2 + + PackageStatusNone = 0 + PackageStatusPending = 1 + PackageStatusEffective = 2 + PackageStatusExpired = 3 + + EffectiveStatusPending = 0 + EffectiveStatusEffective = 1 + EffectiveStatusExpired = 2 + EffectiveStatusTerminated = 3 + EffectiveStatusVoided = 4 +) + +const ( + V3ApiSimBossCardStatusPendingActivate = 1 + V3ApiSimBossCardStatusActivated = 2 + V3ApiSimBossCardStatusStopped = 4 +) + +const ( + SimCardStatusOperTypeStop = "0" + SimCardStatusOperTypeStart = "1" + SimCardDataOperTypeStart = 0 + SimCardDataOperTypeStop = 1 + SimCardDataApnName = "CMIOT" +) + +const ( + ImportStatusSuccess = "success" + ImportStatusFailed = "failed" +) + +const ( + BalanceIncome = 0 + BalanceExpense = 1 + + BalanceTransactionTypeRecharge = 0 + BalanceTransactionTypeConsume = 1 + + BalanceTransactionChannelBalance = 0 + BalanceTransactionChannelWechat = 2 + BalanceTransactionChannelAlipay = 3 + BalanceTransactionChannelMock = 9 +) diff --git a/server/internal/consts/bcache.go b/server/internal/consts/bcache.go new file mode 100644 index 0000000..3ddaa07 --- /dev/null +++ b/server/internal/consts/bcache.go @@ -0,0 +1,13 @@ +// Package consts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package consts + +const ( + KeySysAuthToken = "login:%d:%s" // 系统认证token缓存 login:{userId}:{uuid} + KeySysAuthTokenReject = "login_reject:%d:%s:" // 系统认证token拒绝缓存 login_reject:{userId}:{uuid} + KeyLoginScanWechat = "login:scan:wechat:%d" // 系统二维码登录缓存 hash Key:SceneId field:tempUserId value:QrcodeCacheModel + KeyUserBindWechat = "user:bind:wechat:%d" // 系统二维码绑定缓存 hash Key:SceneId field:userId value:QrcodeCacheModel +) diff --git a/server/internal/consts/code.go b/server/internal/consts/code.go new file mode 100644 index 0000000..49fb677 --- /dev/null +++ b/server/internal/consts/code.go @@ -0,0 +1,41 @@ +// Package consts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package consts + +import "github.com/gogf/gf/v2/errors/gcode" + +var ( + // 账号不存在 + CodeUserNotFound = gcode.New(10001, "账号不存在", nil) + // 账号已删除 + CodeUserDeleted = gcode.New(10002, "账号已删除", nil) + // 密码错误 + CodeUserPasswordError = gcode.New(10003, "密码错误", nil) + // 账号已禁用 + CodeUserDisabled = gcode.New(10004, "账号已禁用", nil) + // 账号已锁定 + CodeUserLocked = gcode.New(10005, "账号已锁定", nil) + // 账号已过期 + CodeUserExpired = gcode.New(10006, "账号已过期", nil) + // 验证码错误 + CodeCaptchaError = gcode.New(10007, "验证码错误", nil) + // 用户名已存在 + CodeUserExists = gcode.New(10008, "用户名已存在", nil) + // 部门不存在 + CodeDeptNotFound = gcode.New(10009, "部门不存在", nil) + // 手机号已存在 + CodePhoneExists = gcode.New(10010, "手机号已存在", nil) + // 邮箱已存在 + CodeEmailExists = gcode.New(10011, "邮箱已存在", nil) + // 登录已过期 + CodeLoginExpired = gcode.New(10012, "登录已过期", nil) + // 部门下有子部门,不能删除 + CodeDeptHasSub = gcode.New(10013, "部门下有子部门,不能删除", nil) + // 没有权限 + CodeNoPermission = gcode.New(10014, "没有权限", nil) + // 无效Token + CodeInvalidToken = gcode.New(10015, "无效Token", nil) +) diff --git a/server/internal/consts/config.go b/server/internal/consts/config.go new file mode 100644 index 0000000..b6bf46d --- /dev/null +++ b/server/internal/consts/config.go @@ -0,0 +1,21 @@ +// Package consts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package consts + +const ( + ConfigOssUrlPath = "sys.oss.urlPath" // 上传文件Url路径 "http://ab.com/" 或 "/" 或 "resource/upload|http://ab.com/upload" + ConfigOssFileTypeKey = "sys.oss.fileType" // 上传文件类型 + ConfigOssImgTypeKey = "sys.oss.imgType" // 上传图片类型 + ConfigOssFileSizeKey = "sys.oss.fileSize" // 上传文件大小 + ConfigOssImgSizeKey = "sys.oss.imgSize" // 上传图片大小 + ConfigOnlineForceLogout = "sys.online.forceLogout" // 在线用户强制退出 + ConfigOssNewFileTypePath = "sys.oss.newFileTypePath.%d" // 上传文件类型路径 ${newFileType} +) + +const ( + ConfigOnlineForceLogoutTrue = "true" // 在线用户强制退出 + ConfigOnlineForceLogoutFalse = "false" // 在线用户强制退出 +) diff --git a/server/internal/consts/consts.go b/server/internal/consts/consts.go new file mode 100644 index 0000000..da0878d --- /dev/null +++ b/server/internal/consts/consts.go @@ -0,0 +1,10 @@ +// Package consts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package consts + +const ( + ContextKey = "ContextKey" // 上下文变量存储键名,前后端系统共享 +) diff --git a/server/internal/consts/db.go b/server/internal/consts/db.go new file mode 100644 index 0000000..2256ce4 --- /dev/null +++ b/server/internal/consts/db.go @@ -0,0 +1,6 @@ +package consts + +const ( + DBNameDefault = "default" + DBNameMemory = "memorydb" +) diff --git a/server/internal/consts/event.go b/server/internal/consts/event.go new file mode 100644 index 0000000..d568d9b --- /dev/null +++ b/server/internal/consts/event.go @@ -0,0 +1,55 @@ +// Package consts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package consts + +const ( + EventKeyServerClose = "server_close" // 服务关闭 args=[] +) + +const ( + EventKeyUserLogin = "user_login" // 用户登录 args=[uid,] + EventKeyUserLogout = "user_logout" // 用户退出 args=[uid,] + EventKeyUserCreate = "user_create" // 用户创建 args=[uid,] + EventKeyUserDelete = "user_delete" // 用户删除 args=[uidArray,] + EventKeyUserUpdate = "user_update" // 用户更新 args=[uid,] +) +const ( + EventKeyDBSysConfigUpdate = "db.sys_config_update" // 系统配置创建更新删除 args=[configId, tenantId,key?] + EventKeyDBSysConfigCreate = "db.sys_config_create" // 系统配置创建 args=[configId, tenantId,key?] + EventKeyDBSysConfigDelete = "db.sys_config_delete" // 系统配置删除 args=[configId, tenantId] +) + +const ( + EventKeyDBSysDeptCreate = "db.sys_dept_create" // 系统部门创建 args=[deptId,] + EventKeyDBSysDeptUpdate = "db.sys_dept_update" // 系统部门更新 args=[deptId,] + EventKeyDBSysDeptDelete = "db.sys_dept_delete" // 系统部门删除 args=[deptId[],] +) + +const ( + EventKeyDBSysTenantCreate = "db.sys_tenant_create" // 租户创建 args=[pk,] + EventKeyDBSysTenantUpdate = "db.sys_tenant_update" // 租户更新 args=[pk,] + EventKeyDBSysTenantDelete = "db.sys_tenant_delete" // 租户删除 args=[pk[],] + + EventKeyDBSysRoleCreate = "db.sys_role_create" // 角色创建 args=[pk,] + EventKeyDBSysRoleUpdate = "db.sys_role_update" // 角色更新 args=[pk,] + EventKeyDBSysRoleDelete = "db.sys_role_delete" // 角色删除 args=[pk[],] + + EventKeyDBSysMenuCreate = "db.sys_menu_create" // 菜单创建 args=[pk,] + EventKeyDBSysMenuUpdate = "db.sys_menu_update" // 菜单更新 args=[pk,] + EventKeyDBSysMenuDelete = "db.sys_menu_delete" // 菜单删除 args=[pk[],] + + EventKeyDBSysPostCreate = "db.sys_post_create" // 岗位创建 args=[pk,] + EventKeyDBSysPostUpdate = "db.sys_post_update" // 岗位更新 args=[pk,] + EventKeyDBSysPostDelete = "db.sys_post_delete" // 岗位删除 args=[pk[],] + + EventKeyDBSysDictDataCreate = "db.sys_dict_data_create" // 字典数据创建 args=[pk,] + EventKeyDBSysDictDataUpdate = "db.sys_dict_data_update" // 字典数据更新 args=[pk,] + EventKeyDBSysDictDataDelete = "db.sys_dict_data_delete" // 字典数据删除 args=[pk[],] + + EventKeyDBSysDictTypeCreate = "db.sys_dict_type_create" // 字典类型创建 args=[pk,] + EventKeyDBSysDictTypeUpdate = "db.sys_dict_type_update" // 字典类型更新 args=[pk,] + EventKeyDBSysDictTypeDelete = "db.sys_dict_type_delete" // 字典类型删除 args=[pk[],] +) diff --git a/server/internal/consts/http.go b/server/internal/consts/http.go new file mode 100644 index 0000000..6db02b7 --- /dev/null +++ b/server/internal/consts/http.go @@ -0,0 +1,14 @@ +// Package consts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package consts + +const ( + HTTPContentTypeXml = "text/xml" + HTTPContentTypeHtml = "text/html" + HTTPContentTypeStream = "text/event-stream" + HTTPContentTypeJson = "application/json" + HTTPContentTypeExcel = "application/octet-stream" +) diff --git a/server/internal/consts/mcache.go b/server/internal/consts/mcache.go new file mode 100644 index 0000000..65f7e54 --- /dev/null +++ b/server/internal/consts/mcache.go @@ -0,0 +1,15 @@ +// Package consts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package consts + +// 内存缓存KEY常量 注明 key组成和 过期时间 清理方式 +const ( + MemCacheDeptName = "dept_name_%d" // 部门名称缓存 部门id:name 24小时过期 + MemCacheUserInfo = "user_info_%d" // 用户信息缓存 用户id:miniinfo 24小时过期 用户修改是清理 + MemCacheUserAccessCodeList = "user_access_code_list_%d" // 用户访问码列表缓存 用户id:access_code_list 24小时过期 用户退出清理 + MemCacheUserRoleDataAccessCodeList = "user_role_data_access_code_list_%d" // 用户角色数据访问码列表缓存 用户id:role_id:data_access_code_list 24小时过期 用户退出清理 + MemCacheSystemConfig = "system_config_%s" // 系统配置缓存 租户ID:map[key]value 24小时过期 +) diff --git a/server/internal/consts/queue.go b/server/internal/consts/queue.go new file mode 100644 index 0000000..7b3a9f0 --- /dev/null +++ b/server/internal/consts/queue.go @@ -0,0 +1,20 @@ +// Package consts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package consts + +const ( + QueueSysOptLog = "queue:sys_opt_log" +) + +// 数据库变动类型 +const ( + DbChgTypeAdd = "add" // 数据库变动新增 + DbChgTypeEdit = "edit" // 数据库变动编辑 + DbChgTypeDel = "del" // 数据库变动删除 +) +const ( + BroadcastDbChg = "broadcast:db:chg" // 数据库配置表变动更新广播消息 obj{table_name,pk,type:add/edit/del,data:{主要字段数据}} +) diff --git a/server/internal/consts/system.go b/server/internal/consts/system.go new file mode 100644 index 0000000..62ac2c1 --- /dev/null +++ b/server/internal/consts/system.go @@ -0,0 +1,150 @@ +// Package consts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package consts + +var ( + // 默认系统租客编号,禁止修改编号 + DefaultSystemTenantCode = "000000" + SuperAdminRoleId = int64(1) +) + +var ( + // 状态: 0正常 1停用 + SysStatusNormal = "0" + SysStatusDisable = "1" +) + +var ( + // 菜单状态: 0正常 1停用 + SysMenuStatusNormal = "0" + SysMenuStatusDisable = "1" +) + +var ( + // 菜单类型: M目录 C菜单 F按钮 + SysMenuTypeDir = "M" + SysMenuTypeMenu = "C" + SysMenuTypeBtn = "F" +) + +var ( + // 菜单可见: 0显示 1隐藏 + SysMenuVisibleShow = "0" + SysMenuVisibleHide = "1" +) + +var ( + // 菜单是否为外链: 0是 1否 + SysMenuIsFrameNo = 1 + SysMenuIsFrameYes = 0 +) + +var ( + // 用户状态: 0正常 1停用 + SysUserStatusNormal = "0" + SysUserStatusDisable = "1" + SysUserStatusLocked = "2" + SysUserStatusExpired = "3" + SysUserStatusDeleted = "4" +) + +var ( + // 角色菜单数据范围: 0继承角色数据范围 + SysRoleMenuDataScopeRole = "0" + // 角色数据范围: 1全部数据权限 2自定数据权限 3本部门数据权限 4本部门及以下数据权限 5仅本人数据权限 6本部门及以下或本人数据权限 7本组织及本组织下一级数据权限 8本组织下一级数据权限 + SysRoleDataScopeAll = "1" + SysRoleDataScopeCustom = "2" + SysRoleDataScopeDept = "3" + SysRoleDataScopeDeptAndBelow = "4" + SysRoleDataScopePersonal = "5" + SysRoleDataScopeDeptAndBelowOrPersonal = "6" + SysRoleDataScopeOrgAndNextLevel = "7" + SysRoleDataScopeOrgNextLevel = "8" +) + +var ( + // 上传文件类型: 1文件 2图片 + SysUploadFileTypeFile = "file" + SysUploadFileTypeImg = "img" +) + +var ( + // 新文件类型 + SysUploadNewFileTypeUserFile = 1 // 用户文件 + SysUploadNewFileTypeUserImg = 2 // 用户图片 + SysUploadNewFileTypeDeviceFile = 3 // 设备文件 ${device_key} +) +var ( + // 新文件类型路径 + SysUploadNewFileTypePathUserFile = "user/file" // 用户文件 + SysUploadNewFileTypePathUserImg = "user/img" // 用户图片 + SysUploadNewFileTypePathDeviceFile = "device" // 设备文件 ${device_key} +) +var ( + // 用户性别: 0男 1女 2未知 + SysUserSexMale = "0" + SysUserSexFemale = "1" + SysUserSexUnknown = "2" +) + +var ( + // 用户类型: pc端 app端 + SysUserTypeSys = "sys_user" + SysUserTypeAPP = "app_user" +) + +var ( + // AppTerminalDeptId 仅作为旧数据兜底默认部门;终端用户身份以 sys_config 的小程序角色和所属组织配置为准。 + AppTerminalDeptId = int64(147) +) + +var ( + // 部门状态: 0正常 1停用 + SysDeptStatusNormal = "0" + SysDeptStatusDisable = "1" +) + +var ( + // 任务状态: 0正常 1停用 + SysJobStatusNormal = "0" + SysJobStatusDisable = "1" +) + +var ( + // 计划执行策略 0不限制并发 1限制并发 + SysJobMisfirePolicyMulty = 1 + SysJobMisfirePolicyOnce = 2 +) +var ( + // 权限码前缀 x-check-permission + SysCheckPermissionPrefix = "cp" + SysCheckPermissionRolePrefix = "cpr:" // 角色权限码前缀+角色权限 + SysCheckPermissionMenuPrefix = "cpm:" // 菜单权限码前缀+菜单权限 + SysCheckPermissionDeptPrefix = "cpd:" // 部门权限码前缀+部门编码 + SysCheckPermissionPostPrefix = "cpp:" // 岗位权限码前缀+岗位编码 + SysCheckPermissionUserPrefix = "cpu:" // 用户权限码前缀+用户名 + SysCheckPermissionCurrentUser = "cpc:current:user" // 当前登录用户默认权限 + SysCheckPermissionTerminalUser = "cpa:terminal:user" // 小程序终端用户专用权限 +) +var StatusSlice = []int{0, 1} + +var ( + // 公告范围: 1全部 2指定组织 3指定用户 + NoticeRangeAll = 1 + NoticeRangeDept = 2 + NoticeRangeUser = 3 +) + +var ( + // 租户配置 + ConfigKeyTenantDefaultRegisterDeptId = "iot.register.dept" + ConfigKeyTenantDefaultRegisterRoleId = "iot.register.role" +) + +var ( + // 微信平台 + SourceWechatMp = "wechat_mp" +) diff --git a/server/internal/consts/websocket.go b/server/internal/consts/websocket.go new file mode 100644 index 0000000..9c49716 --- /dev/null +++ b/server/internal/consts/websocket.go @@ -0,0 +1,23 @@ +// Package consts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package consts + +// 服务器监控信息 +const ( + MonitorServerHost = "host" + MonitorServerSysLoad = "sysLoad" + MonitorServerCpu = "cpu" + MonitorServerMem = "mem" + MonitorServerDisk = "disk" + MonitorServerNet = "net" + MonitorServerGo = "go" +) + +// WEBSOCKET订阅Tag 服务器监控信息 +const ( + WSTagMonitorServer = "ws:tag:monitor:server" // WEBSOCKET订阅Tag 服务器监控信息 + WSEventMonitorServer = "ws:event:monitor:server:%s" // WEBSOCKET订阅 服务器监控信息 %s in [host,sysLoad,cpu,disk,net,go] +) diff --git a/server/internal/controller/app/app_new.go b/server/internal/controller/app/app_new.go new file mode 100644 index 0000000..2142adf --- /dev/null +++ b/server/internal/controller/app/app_new.go @@ -0,0 +1,9 @@ +package app + +import apiApp "git.5iot.com/iotlcm-public/iotlcm-app/api/app" + +type ControllerV1 struct{} + +func NewV1() apiApp.IAppV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/app/app_v1_auth.go b/server/internal/controller/app/app_v1_auth.go new file mode 100644 index 0000000..481cd8f --- /dev/null +++ b/server/internal/controller/app/app_v1_auth.go @@ -0,0 +1,81 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) Captcha(ctx context.Context, req *v1.CaptchaReq) (res *v1.CaptchaRes, err error) { + captchaID, captchaImage, err := service.AppAuth().Captcha(ctx) + if err != nil { + return nil, err + } + return &v1.CaptchaRes{ + CaptchaID: captchaID, + CaptchaImage: captchaImage, + }, nil +} + +func (c *ControllerV1) SideCaptcha(ctx context.Context, req *v1.SideCaptchaReq) (res *v1.SideCaptchaRes, err error) { + captcha, err := service.AppAuth().SideCaptcha(ctx) + if err != nil { + return nil, err + } + return &v1.SideCaptchaRes{AppSideCaptchaModel: *captcha}, nil +} + +func (c *ControllerV1) CheckSideCaptcha(ctx context.Context, req *v1.CheckSideCaptchaReq) (res *v1.CheckSideCaptchaRes, err error) { + if err = service.AppAuth().CheckSideCaptcha(ctx, &req.AppCheckSideCaptchaParam); err != nil { + return nil, err + } + return &v1.CheckSideCaptchaRes{Success: true}, nil +} + +func (c *ControllerV1) SendSms(ctx context.Context, req *v1.SendSmsReq) (res *v1.SendSmsRes, err error) { + sms, err := service.AppAuth().SendSms(ctx, &req.AppSendSmsParam) + if err != nil { + return nil, err + } + return &v1.SendSmsRes{AppSendSmsModel: *sms}, nil +} + +func (c *ControllerV1) PhoneRegister(ctx context.Context, req *v1.PhoneRegisterReq) (res *v1.PhoneRegisterRes, err error) { + token, err := service.AppAuth().PhoneRegister(ctx, &req.AppPhoneRegisterParam) + if err != nil { + return nil, err + } + return &v1.PhoneRegisterRes{AppAuthTokenModel: *token}, nil +} + +func (c *ControllerV1) PhoneLogin(ctx context.Context, req *v1.PhoneLoginReq) (res *v1.PhoneLoginRes, err error) { + token, err := service.AppAuth().PhoneLogin(ctx, &req.AppPhoneLoginParam) + if err != nil { + return nil, err + } + return &v1.PhoneLoginRes{AppAuthTokenModel: *token}, nil +} + +func (c *ControllerV1) WechatLogin(ctx context.Context, req *v1.WechatLoginReq) (res *v1.WechatLoginRes, err error) { + data, err := service.AppAuth().WechatLogin(ctx, &req.AppWechatLoginParam) + if err != nil { + return nil, err + } + return &v1.WechatLoginRes{AppWechatLoginModel: *data}, nil +} + +func (c *ControllerV1) WechatBindPhone(ctx context.Context, req *v1.WechatBindPhoneReq) (res *v1.WechatBindPhoneRes, err error) { + data, err := service.AppAuth().WechatBindPhone(ctx, &req.AppWechatBindPhoneParam) + if err != nil { + return nil, err + } + return &v1.WechatBindPhoneRes{AppWechatBindPhoneModel: *data}, nil +} + +func (c *ControllerV1) ForgotPassword(ctx context.Context, req *v1.ForgotPasswordReq) (res *v1.ForgotPasswordRes, err error) { + if err = service.AppAuth().ForgotPassword(ctx, &req.AppForgotPasswordParam); err != nil { + return nil, err + } + return &v1.ForgotPasswordRes{Success: true}, nil +} diff --git a/server/internal/controller/app/app_v1_balance.go b/server/internal/controller/app/app_v1_balance.go new file mode 100644 index 0000000..385cee1 --- /dev/null +++ b/server/internal/controller/app/app_v1_balance.go @@ -0,0 +1,89 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" +) + +func (c *ControllerV1) BalanceRechargeSubmit(ctx context.Context, req *v1.BalanceRechargeSubmitReq) (res *v1.BalanceRechargeSubmitRes, err error) { + order, err := service.AppCard().BalanceRechargeSubmit(ctx, &req.AppBalanceRechargeSubmitParam) + if err != nil { + return nil, err + } + return &v1.BalanceRechargeSubmitRes{AppBalanceRechargeSubmitModel: order}, nil +} + +func (c *ControllerV1) BalanceRechargeStatus(ctx context.Context, req *v1.BalanceRechargeStatusReq) (res *v1.BalanceRechargeStatusRes, err error) { + status, err := service.AppCard().BalanceRechargeStatus(ctx, &req.AppBalanceRechargeStatusParam) + if err != nil { + return nil, err + } + return &v1.BalanceRechargeStatusRes{AppBalanceRechargeStatusModel: status}, nil +} + +func (c *ControllerV1) BalanceRechargeMockPay(ctx context.Context, req *v1.BalanceRechargeMockPayReq) (res *v1.BalanceRechargeMockPayRes, err error) { + status, err := service.AppCard().BalanceRechargeMockPay(ctx, &req.AppBalanceRechargeMockPayParam) + if err != nil { + return nil, err + } + return &v1.BalanceRechargeMockPayRes{AppBalanceRechargeStatusModel: status}, nil +} + +func (c *ControllerV1) BalanceRechargeRefund(ctx context.Context, req *v1.BalanceRechargeRefundReq) (res *v1.BalanceRechargeRefundRes, err error) { + order, err := service.AppCard().BalanceRechargeRefund(ctx, &req.AppBalanceRechargeRefundParam) + if err != nil { + return nil, err + } + return &v1.BalanceRechargeRefundRes{AppOrderModel: order}, nil +} + +func (c *ControllerV1) BalanceRechargeWechatNotify(ctx context.Context, req *v1.BalanceRechargeWechatNotifyReq) (res *v1.BalanceRechargeWechatNotifyRes, err error) { + r := ghttp.RequestFromCtx(ctx) + if r == nil { + return nil, nil + } + if err = service.AppCard().BalanceRechargeWechatNotify(ctx); err != nil { + g.Log().Errorf(ctx, "微信支付余额充值回调处理失败: %v", err) + r.Response.Status = 500 + r.Response.WriteJson(g.Map{ + "code": "FAIL", + "message": err.Error(), + }) + r.ExitAll() + return nil, nil + } + r.Response.WriteJson(g.Map{ + "code": "SUCCESS", + "message": "成功", + }) + r.ExitAll() + return nil, nil +} + +func (c *ControllerV1) BalanceRechargeWechatRefundNotify(ctx context.Context, req *v1.BalanceRechargeWechatRefundNotifyReq) (res *v1.BalanceRechargeWechatRefundNotifyRes, err error) { + r := ghttp.RequestFromCtx(ctx) + if r == nil { + return nil, nil + } + if err = service.AppCard().BalanceRechargeWechatRefundNotify(ctx); err != nil { + g.Log().Errorf(ctx, "微信支付退款回调处理失败: %v", err) + r.Response.Status = 500 + r.Response.WriteJson(g.Map{ + "code": "FAIL", + "message": err.Error(), + }) + r.ExitAll() + return nil, nil + } + r.Response.WriteJson(g.Map{ + "code": "SUCCESS", + "message": "成功", + }) + r.ExitAll() + return nil, nil +} diff --git a/server/internal/controller/app/app_v1_card.go b/server/internal/controller/app/app_v1_card.go new file mode 100644 index 0000000..0a961f6 --- /dev/null +++ b/server/internal/controller/app/app_v1_card.go @@ -0,0 +1,180 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) PublicQueryCard(ctx context.Context, req *v1.PublicQueryCardReq) (res *v1.PublicQueryCardRes, err error) { + card, err := service.AppCard().PublicQuery(ctx, &req.AppCardQueryParam) + if err != nil { + return nil, err + } + return &v1.PublicQueryCardRes{Card: card}, nil +} + +func (c *ControllerV1) RecognizeCard(ctx context.Context, req *v1.RecognizeCardReq) (res *v1.RecognizeCardRes, err error) { + result, err := service.AppCard().RecognizeCard(ctx) + if err != nil { + return nil, err + } + return &v1.RecognizeCardRes{AppCardRecognitionModel: result}, nil +} + +func (c *ControllerV1) CardList(ctx context.Context, req *v1.CardListReq) (res *v1.CardListRes, err error) { + items, total, err := service.AppCard().List(ctx, &req.AppCardListParam) + if err != nil { + return nil, err + } + hasMore := len(items) >= req.PageSize + if req.IncludeTotal == nil || *req.IncludeTotal { + hasMore = req.Page*req.PageSize < total + } + return &v1.CardListRes{Items: items, Total: total, HasMore: hasMore}, nil +} + +func (c *ControllerV1) PackageList(ctx context.Context, req *v1.PackageListReq) (res *v1.PackageListRes, err error) { + items, total, err := service.AppCard().PackageList(ctx, &req.AppPackageListParam) + if err != nil { + return nil, err + } + return &v1.PackageListRes{Items: items, Total: total}, nil +} + +func (c *ControllerV1) PublicPackageDetail(ctx context.Context, req *v1.PublicPackageDetailReq) (res *v1.PublicPackageDetailRes, err error) { + pkg, err := service.AppCard().PublicPackageDetail(ctx, &req.AppPackagePublicDetailParam) + if err != nil { + return nil, err + } + return &v1.PublicPackageDetailRes{Package: pkg}, nil +} + +func (c *ControllerV1) CardDetail(ctx context.Context, req *v1.CardDetailReq) (res *v1.CardDetailRes, err error) { + card, err := service.AppCard().Detail(ctx, &req.AppCardDetailParam) + if err != nil { + return nil, err + } + return &v1.CardDetailRes{Card: card}, nil +} + +func (c *ControllerV1) BindCard(ctx context.Context, req *v1.BindCardReq) (res *v1.BindCardRes, err error) { + card, err := service.AppCard().Bind(ctx, &req.AppCardBindParam) + if err != nil { + return nil, err + } + return &v1.BindCardRes{Card: card}, nil +} + +func (c *ControllerV1) RestartCard(ctx context.Context, req *v1.RestartCardReq) (res *v1.RestartCardRes, err error) { + restart, err := service.AppCard().Restart(ctx, &req.AppCardRestartParam) + if err != nil { + return nil, err + } + return &v1.RestartCardRes{AppCardRestartModel: restart}, nil +} + +func (c *ControllerV1) StopCard(ctx context.Context, req *v1.StopCardReq) (res *v1.StopCardRes, err error) { + stop, err := service.AppCard().Stop(ctx, &req.AppCardRestartParam) + if err != nil { + return nil, err + } + return &v1.StopCardRes{AppCardLifecycleModel: stop}, nil +} + +func (c *ControllerV1) StartCard(ctx context.Context, req *v1.StartCardReq) (res *v1.StartCardRes, err error) { + start, err := service.AppCard().Start(ctx, &req.AppCardRestartParam) + if err != nil { + return nil, err + } + return &v1.StartCardRes{AppCardLifecycleModel: start}, nil +} + +func (c *ControllerV1) LifecycleBatchSubmit(ctx context.Context, req *v1.LifecycleBatchSubmitReq) (res *v1.LifecycleBatchSubmitRes, err error) { + submit, err := service.AppCard().LifecycleBatchSubmit(ctx, &req.AppCardLifecycleParam) + if err != nil { + return nil, err + } + return &v1.LifecycleBatchSubmitRes{AppCardLifecycleModel: submit}, nil +} + +func (c *ControllerV1) LifecycleBatchStatus(ctx context.Context, req *v1.LifecycleBatchStatusReq) (res *v1.LifecycleBatchStatusRes, err error) { + status, err := service.AppCard().LifecycleBatchStatus(ctx, &req.AppCardLifecycleStatusParam) + if err != nil { + return nil, err + } + return &v1.LifecycleBatchStatusRes{AppCardLifecycleModel: status}, nil +} + +func (c *ControllerV1) RenewPrepare(ctx context.Context, req *v1.RenewPrepareReq) (res *v1.RenewPrepareRes, err error) { + prepare, err := service.AppCard().RenewPrepare(ctx, &req.AppCardRenewPrepareParam) + if err != nil { + return nil, err + } + return &v1.RenewPrepareRes{AppCardRenewPrepareModel: prepare}, nil +} + +func (c *ControllerV1) RenewBatchPrepare(ctx context.Context, req *v1.RenewBatchPrepareReq) (res *v1.RenewBatchPrepareRes, err error) { + prepare, err := service.AppCard().RenewBatchPrepare(ctx, &req.AppCardRenewBatchPrepareParam) + if err != nil { + return nil, err + } + return &v1.RenewBatchPrepareRes{AppCardRenewPrepareModel: prepare}, nil +} + +func (c *ControllerV1) RenewSubmit(ctx context.Context, req *v1.RenewSubmitReq) (res *v1.RenewSubmitRes, err error) { + submit, err := service.AppCard().RenewSubmit(ctx, &req.AppCardRenewSubmitParam) + if err != nil { + return nil, err + } + return &v1.RenewSubmitRes{AppCardRenewSubmitModel: submit}, nil +} + +func (c *ControllerV1) RenewBatchSubmit(ctx context.Context, req *v1.RenewBatchSubmitReq) (res *v1.RenewBatchSubmitRes, err error) { + submit, err := service.AppCard().RenewBatchSubmit(ctx, &req.AppCardRenewBatchSubmitParam) + if err != nil { + return nil, err + } + return &v1.RenewBatchSubmitRes{AppCardRenewSubmitModel: submit}, nil +} + +func (c *ControllerV1) AllocateTargets(ctx context.Context, req *v1.AllocateTargetsReq) (res *v1.AllocateTargetsRes, err error) { + items, total, err := service.AppCard().AllocateTargets(ctx, &req.AppCardAllocateTargetListParam) + if err != nil { + return nil, err + } + return &v1.AllocateTargetsRes{Items: items, Total: total}, nil +} + +func (c *ControllerV1) AllocateBatchPrepare(ctx context.Context, req *v1.AllocateBatchPrepareReq) (res *v1.AllocateBatchPrepareRes, err error) { + prepare, err := service.AppCard().AllocateBatchPrepare(ctx, &req.AppCardAllocateBatchPrepareParam) + if err != nil { + return nil, err + } + return &v1.AllocateBatchPrepareRes{AppCardAllocateBatchModel: prepare}, nil +} + +func (c *ControllerV1) AllocateBatchSubmit(ctx context.Context, req *v1.AllocateBatchSubmitReq) (res *v1.AllocateBatchSubmitRes, err error) { + submit, err := service.AppCard().AllocateBatchSubmit(ctx, &req.AppCardAllocateBatchSubmitParam) + if err != nil { + return nil, err + } + return &v1.AllocateBatchSubmitRes{AppCardAllocateBatchModel: submit}, nil +} + +func (c *ControllerV1) ChangePrepare(ctx context.Context, req *v1.ChangePrepareReq) (res *v1.ChangePrepareRes, err error) { + prepare, err := service.AppCard().ChangePrepare(ctx, &req.AppCardChangePrepareParam) + if err != nil { + return nil, err + } + return &v1.ChangePrepareRes{AppCardChangePrepareModel: prepare}, nil +} + +func (c *ControllerV1) ChangeSubmit(ctx context.Context, req *v1.ChangeSubmitReq) (res *v1.ChangeSubmitRes, err error) { + submit, err := service.AppCard().ChangeSubmit(ctx, &req.AppCardChangeSubmitParam) + if err != nil { + return nil, err + } + return &v1.ChangeSubmitRes{AppCardChangeSubmitModel: submit}, nil +} diff --git a/server/internal/controller/app/app_v1_customer.go b/server/internal/controller/app/app_v1_customer.go new file mode 100644 index 0000000..95aa617 --- /dev/null +++ b/server/internal/controller/app/app_v1_customer.go @@ -0,0 +1,24 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) CustomerList(ctx context.Context, req *v1.CustomerListReq) (res *v1.CustomerListRes, err error) { + items, total, err := service.AppUser().CustomerList(ctx, &req.AppCustomerListParam) + if err != nil { + return nil, err + } + return &v1.CustomerListRes{Items: items, Total: total}, nil +} + +func (c *ControllerV1) CustomerDetail(ctx context.Context, req *v1.CustomerDetailReq) (res *v1.CustomerDetailRes, err error) { + customer, err := service.AppUser().CustomerDetail(ctx, &req.AppCustomerDetailParam) + if err != nil { + return nil, err + } + return &v1.CustomerDetailRes{Customer: customer}, nil +} diff --git a/server/internal/controller/app/app_v1_diagnosis.go b/server/internal/controller/app/app_v1_diagnosis.go new file mode 100644 index 0000000..5e1a246 --- /dev/null +++ b/server/internal/controller/app/app_v1_diagnosis.go @@ -0,0 +1,24 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) DiagnosisInfo(ctx context.Context, req *v1.DiagnosisInfoReq) (res *v1.DiagnosisInfoRes, err error) { + info, err := service.AppDiagnosis().Info(ctx, &req.AppDiagnosisInfoParam) + if err != nil { + return nil, err + } + return &v1.DiagnosisInfoRes{AppDiagnosisInfoModel: info}, nil +} + +func (c *ControllerV1) DiagnosisFlow(ctx context.Context, req *v1.DiagnosisFlowReq) (res *v1.DiagnosisFlowRes, err error) { + flow, err := service.AppDiagnosis().Flow(ctx, &req.AppDiagnosisFlowParam) + if err != nil { + return nil, err + } + return &v1.DiagnosisFlowRes{AppDiagnosisFlowModel: flow}, nil +} diff --git a/server/internal/controller/app/app_v1_message.go b/server/internal/controller/app/app_v1_message.go new file mode 100644 index 0000000..6fa6b9d --- /dev/null +++ b/server/internal/controller/app/app_v1_message.go @@ -0,0 +1,32 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) MessageList(ctx context.Context, req *v1.MessageListReq) (res *v1.MessageListRes, err error) { + items, total, err := service.AppMessage().List(ctx, &req.AppMessageListParam) + if err != nil { + return nil, err + } + return &v1.MessageListRes{Items: items, Total: total}, nil +} + +func (c *ControllerV1) MessageDetail(ctx context.Context, req *v1.MessageDetailReq) (res *v1.MessageDetailRes, err error) { + message, err := service.AppMessage().Detail(ctx, &req.AppMessageDetailParam) + if err != nil { + return nil, err + } + return &v1.MessageDetailRes{Message: message}, nil +} + +func (c *ControllerV1) MessageRead(ctx context.Context, req *v1.MessageReadReq) (res *v1.MessageReadRes, err error) { + err = service.AppMessage().Read(ctx, &req.AppMessageReadParam) + if err != nil { + return nil, err + } + return &v1.MessageReadRes{}, nil +} diff --git a/server/internal/controller/app/app_v1_open.go b/server/internal/controller/app/app_v1_open.go new file mode 100644 index 0000000..1fbd2df --- /dev/null +++ b/server/internal/controller/app/app_v1_open.go @@ -0,0 +1,24 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) OpenDiagnosisInfo(ctx context.Context, req *v1.OpenDiagnosisInfoReq) (res *v1.OpenDiagnosisInfoRes, err error) { + info, err := service.AppDiagnosis().OpenInfo(ctx, &req.AppOpenDiagnosisInfoParam) + if err != nil { + return nil, err + } + return &v1.OpenDiagnosisInfoRes{AppDiagnosisInfoModel: info}, nil +} + +func (c *ControllerV1) OpenDiagnosisFlow(ctx context.Context, req *v1.OpenDiagnosisFlowReq) (res *v1.OpenDiagnosisFlowRes, err error) { + flow, err := service.AppDiagnosis().OpenFlow(ctx, &req.AppOpenDiagnosisFlowParam) + if err != nil { + return nil, err + } + return &v1.OpenDiagnosisFlowRes{AppDiagnosisFlowModel: flow}, nil +} diff --git a/server/internal/controller/app/app_v1_order.go b/server/internal/controller/app/app_v1_order.go new file mode 100644 index 0000000..9e4c411 --- /dev/null +++ b/server/internal/controller/app/app_v1_order.go @@ -0,0 +1,24 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) OrderList(ctx context.Context, req *v1.OrderListReq) (res *v1.OrderListRes, err error) { + items, total, err := service.AppOrder().List(ctx, &req.AppOrderListParam) + if err != nil { + return nil, err + } + return &v1.OrderListRes{Items: items, Total: total}, nil +} + +func (c *ControllerV1) OrderDetail(ctx context.Context, req *v1.OrderDetailReq) (res *v1.OrderDetailRes, err error) { + order, err := service.AppOrder().Detail(ctx, &req.AppOrderDetailParam) + if err != nil { + return nil, err + } + return &v1.OrderDetailRes{Order: order}, nil +} diff --git a/server/internal/controller/app/app_v1_user.go b/server/internal/controller/app/app_v1_user.go new file mode 100644 index 0000000..9000c06 --- /dev/null +++ b/server/internal/controller/app/app_v1_user.go @@ -0,0 +1,32 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) UserInfo(ctx context.Context, req *v1.UserInfoReq) (res *v1.UserInfoRes, err error) { + user, err := service.AppUser().Info(ctx) + if err != nil { + return nil, err + } + return &v1.UserInfoRes{AppUserInfoModel: *user}, nil +} + +func (c *ControllerV1) UpdateUserInfo(ctx context.Context, req *v1.UpdateUserInfoReq) (res *v1.UpdateUserInfoRes, err error) { + user, err := service.AppUser().UpdateInfo(ctx, &req.AppUpdateUserInfoParam) + if err != nil { + return nil, err + } + return &v1.UpdateUserInfoRes{AppUserInfoModel: *user}, nil +} + +func (c *ControllerV1) BindWechat(ctx context.Context, req *v1.BindWechatReq) (res *v1.BindWechatRes, err error) { + data, err := service.AppUser().BindWechat(ctx, &req.AppWechatBindParam) + if err != nil { + return nil, err + } + return &v1.BindWechatRes{AppWechatBindModel: *data}, nil +} diff --git a/server/internal/controller/app/app_v1_warning.go b/server/internal/controller/app/app_v1_warning.go new file mode 100644 index 0000000..e659479 --- /dev/null +++ b/server/internal/controller/app/app_v1_warning.go @@ -0,0 +1,56 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) WarningConfigList(ctx context.Context, req *v1.WarningConfigListReq) (res *v1.WarningConfigListRes, err error) { + data, err := service.AppWarning().ConfigList(ctx, &req.AppWarningConfigListParam) + if err != nil { + return nil, err + } + return &v1.WarningConfigListRes{AppWarningConfigListModel: *data}, nil +} + +func (c *ControllerV1) WarningConfigSave(ctx context.Context, req *v1.WarningConfigSaveReq) (res *v1.WarningConfigSaveRes, err error) { + data, err := service.AppWarning().ConfigSave(ctx, &req.AppWarningConfigSaveParam) + if err != nil { + return nil, err + } + return &v1.WarningConfigSaveRes{AppWarningConfigSaveModel: *data}, nil +} + +func (c *ControllerV1) WarningConfigNotify(ctx context.Context, req *v1.WarningConfigNotifyReq) (res *v1.WarningConfigNotifyRes, err error) { + data, err := service.AppWarning().ConfigNotify(ctx, &req.AppWarningConfigNotifyParam) + if err != nil { + return nil, err + } + return &v1.WarningConfigNotifyRes{AppWarningConfigNotifyModel: *data}, nil +} + +func (c *ControllerV1) WarningConfigMeta(ctx context.Context, req *v1.WarningConfigMetaReq) (res *v1.WarningConfigMetaRes, err error) { + data, err := service.AppWarning().ConfigMeta(ctx) + if err != nil { + return nil, err + } + return &v1.WarningConfigMetaRes{AppWarningConfigMetaModel: *data}, nil +} + +func (c *ControllerV1) WarningConfigBatchSave(ctx context.Context, req *v1.WarningConfigBatchSaveReq) (res *v1.WarningConfigBatchSaveRes, err error) { + data, err := service.AppWarning().ConfigBatchSave(ctx, &req.AppWarningConfigBatchSaveParam) + if err != nil { + return nil, err + } + return &v1.WarningConfigBatchSaveRes{AppWarningConfigBatchSaveModel: *data}, nil +} + +func (c *ControllerV1) WarningConfigBatchNotify(ctx context.Context, req *v1.WarningConfigBatchNotifyReq) (res *v1.WarningConfigBatchNotifyRes, err error) { + data, err := service.AppWarning().ConfigBatchNotify(ctx, &req.AppWarningConfigBatchNotifyParam) + if err != nil { + return nil, err + } + return &v1.WarningConfigBatchNotifyRes{AppWarningConfigBatchNotifyModel: *data}, nil +} diff --git a/server/internal/controller/app/app_v1_work_order.go b/server/internal/controller/app/app_v1_work_order.go new file mode 100644 index 0000000..578efbc --- /dev/null +++ b/server/internal/controller/app/app_v1_work_order.go @@ -0,0 +1,40 @@ +package app + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/app/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) WorkOrderList(ctx context.Context, req *v1.WorkOrderListReq) (res *v1.WorkOrderListRes, err error) { + items, total, err := service.AppWorkOrder().List(ctx, &req.AppWorkOrderListParam) + if err != nil { + return nil, err + } + return &v1.WorkOrderListRes{Items: items, Total: total}, nil +} + +func (c *ControllerV1) WorkOrderDetail(ctx context.Context, req *v1.WorkOrderDetailReq) (res *v1.WorkOrderDetailRes, err error) { + workOrder, err := service.AppWorkOrder().Detail(ctx, &req.AppWorkOrderDetailParam) + if err != nil { + return nil, err + } + return &v1.WorkOrderDetailRes{WorkOrder: workOrder}, nil +} + +func (c *ControllerV1) WorkOrderCreate(ctx context.Context, req *v1.WorkOrderCreateReq) (res *v1.WorkOrderCreateRes, err error) { + workOrder, err := service.AppWorkOrder().Create(ctx, &req.AppWorkOrderCreateParam) + if err != nil { + return nil, err + } + return &v1.WorkOrderCreateRes{WorkOrder: workOrder}, nil +} + +func (c *ControllerV1) WorkOrderUpload(ctx context.Context, req *v1.WorkOrderUploadReq) (res *v1.WorkOrderUploadRes, err error) { + workOrderFile, err := service.AppWorkOrder().Upload(ctx) + if err != nil { + return nil, err + } + return &v1.WorkOrderUploadRes{SysOssUploadModel: workOrderFile}, nil +} diff --git a/server/internal/controller/common/common.go b/server/internal/controller/common/common.go new file mode 100644 index 0000000..76ec691 --- /dev/null +++ b/server/internal/controller/common/common.go @@ -0,0 +1,5 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package common diff --git a/server/internal/controller/common/common_new.go b/server/internal/controller/common/common_new.go new file mode 100644 index 0000000..ff5ace2 --- /dev/null +++ b/server/internal/controller/common/common_new.go @@ -0,0 +1,15 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package common + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/api/common" +) + +type ControllerV1 struct{} + +func NewV1() common.ICommonV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/common/common_v1_upload.go b/server/internal/controller/common/common_v1_upload.go new file mode 100644 index 0000000..b429308 --- /dev/null +++ b/server/internal/controller/common/common_v1_upload.go @@ -0,0 +1,31 @@ +package common + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/common/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + + "github.com/gogf/gf/v2/net/ghttp" +) + +func (c *ControllerV1) SysOssUpload(ctx context.Context, req *v1.SysOssUploadReq) (res *v1.SysOssUploadRes, err error) { + + info, err := service.SysOss().Upload(ctx, req.SysOssUploadParam) + if err != nil { + return nil, err + } + res = &v1.SysOssUploadRes{ + SysOssUploadModel: info, + } + return +} +func (c *ControllerV1) SysOssDownload(ctx context.Context, req *v1.SysOssDownloadReq) (res *v1.SysOssDownloadRes, err error) { + if _, err = service.SysOss().Download(ctx, req.SysOssDownloadParam); err != nil { + return nil, err + } + if r := ghttp.RequestFromCtx(ctx); r != nil { + r.ExitAll() + } + return nil, nil +} diff --git a/server/internal/controller/gen/test_demo.go b/server/internal/controller/gen/test_demo.go new file mode 100644 index 0000000..effd6b6 --- /dev/null +++ b/server/internal/controller/gen/test_demo.go @@ -0,0 +1,69 @@ +// Package gen +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +// @AutoGenerate Version +package gen + +import ( + "context" + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/gen/testdemo/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/genin" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +var ( + TestDemo = cTestDemo{} +) + +type cTestDemo struct{} + +// List 查看测试单表列表 +func (c *cTestDemo) TestDemoList(ctx context.Context, req *v1.TestDemoListReq) (res *v1.TestDemoListRes, err error) { + list, totalCount, err := service.GenTestDemo().List(ctx, &req.TestDemoListParam) + if err != nil { + return + } + + if list == nil { + list = []*genin.TestDemoListModel{} + } + + res = new(v1.TestDemoListRes) + res.Items = list + res.PageResult.Page = req.Page + res.PageResult.PageSize = req.PageSize + res.PageResult.Total = totalCount + return +} + +// Export 导出测试单表列表 +func (c *cTestDemo) TestDemoExport(ctx context.Context, req *v1.TestDemoExportReq) (res *v1.TestDemoExportRes, err error) { + err = service.GenTestDemo().Export(ctx, &req.TestDemoListParam) + return +} + +// Edit 更新测试单表 +func (c *cTestDemo) TestDemoEdit(ctx context.Context, req *v1.TestDemoEditReq) (res *v1.TestDemoEditRes, err error) { + err = service.GenTestDemo().Edit(ctx, &req.TestDemoEditParam) + return +} + +// View 获取指定测试单表信息 +func (c *cTestDemo) TestDemoView(ctx context.Context, req *v1.TestDemoViewReq) (res *v1.TestDemoViewRes, err error) { + data, err := service.GenTestDemo().View(ctx, &req.TestDemoViewParam) + if err != nil { + return + } + + res = new(v1.TestDemoViewRes) + res.TestDemoViewModel = data + return +} + +// Delete 删除测试单表 +func (c *cTestDemo) TestDemoDelete(ctx context.Context, req *v1.TestDemoDeleteReq) (res *v1.TestDemoDeleteRes, err error) { + err = service.GenTestDemo().Delete(ctx, &req.TestDemoDeleteParam) + return +} diff --git a/server/internal/controller/gen/test_tree.go b/server/internal/controller/gen/test_tree.go new file mode 100644 index 0000000..d23cea4 --- /dev/null +++ b/server/internal/controller/gen/test_tree.go @@ -0,0 +1,85 @@ +// Package gen +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2026 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +// @AutoGenerate Version +package gen + +import ( + "context" + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/gen/testtree/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/genin" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +var ( + TestTree = cTestTree{} +) + +type cTestTree struct{} + +// List 查看测试树表列表 +func (c *cTestTree) TestTreeList(ctx context.Context, req *v1.TestTreeListReq) (res *v1.TestTreeListRes, err error) { + list, totalCount, err := service.GenTestTree().List(ctx, &req.TestTreeListParam) + if err != nil { + return + } + + if list == nil { + list = []*genin.TestTreeListModel{} + } + + res = new(v1.TestTreeListRes) + res.Items = list + res.PageResult.Page = req.Page + res.PageResult.PageSize = req.PageSize + res.PageResult.Total = totalCount + return +} + +// Export 导出测试树表列表 +func (c *cTestTree) TestTreeExport(ctx context.Context, req *v1.TestTreeExportReq) (res *v1.TestTreeExportRes, err error) { + err = service.GenTestTree().Export(ctx, &req.TestTreeListParam) + return +} + +// Edit 更新测试树表 +func (c *cTestTree) TestTreeEdit(ctx context.Context, req *v1.TestTreeEditReq) (res *v1.TestTreeEditRes, err error) { + err = service.GenTestTree().Edit(ctx, &req.TestTreeEditParam) + return +} + +// View 获取指定测试树表信息 +func (c *cTestTree) TestTreeView(ctx context.Context, req *v1.TestTreeViewReq) (res *v1.TestTreeViewRes, err error) { + data, err := service.GenTestTree().View(ctx, &req.TestTreeViewParam) + if err != nil { + return + } + + res = new(v1.TestTreeViewRes) + res.TestTreeViewModel = data + return +} + +// Delete 删除测试树表 +func (c *cTestTree) TestTreeDelete(ctx context.Context, req *v1.TestTreeDeleteReq) (res *v1.TestTreeDeleteRes, err error) { + err = service.GenTestTree().Delete(ctx, &req.TestTreeDeleteParam) + return +} + +// TreeOption 获取测试树表关系树选项 +func (c *cTestTree) TestTreeTreeOption(ctx context.Context, req *v1.TestTreeTreeOptionReq) (res *v1.TestTreeTreeOptionRes, err error) { + data, err := service.GenTestTree().TreeOption(ctx) + if err != nil { + return nil, err + } + + if len(data) > 0 { + res = (*v1.TestTreeTreeOptionRes)(&data) + } else { + temp := make(v1.TestTreeTreeOptionRes, 0) + res = &temp + } + return +} diff --git a/server/internal/controller/gen_codes/gen_codes.go b/server/internal/controller/gen_codes/gen_codes.go new file mode 100644 index 0000000..d8a3741 --- /dev/null +++ b/server/internal/controller/gen_codes/gen_codes.go @@ -0,0 +1,5 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package gen_codes diff --git a/server/internal/controller/gen_codes/gen_codes_new.go b/server/internal/controller/gen_codes/gen_codes_new.go new file mode 100644 index 0000000..0988bf2 --- /dev/null +++ b/server/internal/controller/gen_codes/gen_codes_new.go @@ -0,0 +1,15 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package gen_codes + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/api/gen_codes" +) + +type ControllerV1 struct{} + +func NewV1() gen_codes.IGenCodesV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/gen_codes/gen_codes_v1_gen_table.go b/server/internal/controller/gen_codes/gen_codes_v1_gen_table.go new file mode 100644 index 0000000..e3f875f --- /dev/null +++ b/server/internal/controller/gen_codes/gen_codes_v1_gen_table.go @@ -0,0 +1,127 @@ +package gen_codes + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/gen_codes/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) SysGenTableList(ctx context.Context, req *v1.SysGenTableListReq) (res *v1.SysGenTableListRes, err error) { + items, total, err := service.SysGenTable().List(ctx, req.SysGenTableListParam) + if err != nil { + return nil, err + } + res = &v1.SysGenTableListRes{ + Items: items, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + } + return res, nil +} + +func (c *ControllerV1) SysGenTableView(ctx context.Context, req *v1.SysGenTableViewReq) (res *v1.SysGenTableViewRes, err error) { + item, err := service.SysGenTable().View(ctx, req.SysGenTableViewParam) + if err != nil { + return nil, err + } + res = &v1.SysGenTableViewRes{ + SysGenTableViewModel: item, + } + return res, nil +} + +func (c *ControllerV1) SysGenTableAdd(ctx context.Context, req *v1.SysGenTableAddReq) (res *v1.SysGenTableAddRes, err error) { + output, err := service.SysGenTable().Add(ctx, req.SysGenTableAddParam) + if err != nil { + return nil, err + } + res = &v1.SysGenTableAddRes{ + SysGenTableAddModel: output, + } + return res, nil +} +func (c *ControllerV1) SysGenTableDelete(ctx context.Context, req *v1.SysGenTableDeleteReq) (res *v1.SysGenTableDeleteRes, err error) { + output, err := service.SysGenTable().Delete(ctx, req.SysGenTableDeleteParam) + if err != nil { + return nil, err + } + res = &v1.SysGenTableDeleteRes{ + SysGenTableDeleteModel: output, + } + return res, nil +} +func (c *ControllerV1) SysGenTableSelects(ctx context.Context, req *v1.SysGenTableSelectsReq) (res *v1.SysGenTableSelectsRes, err error) { + output, err := service.SysGenTable().Selects(ctx) + if err != nil { + return nil, err + } + res = &v1.SysGenTableSelectsRes{ + SelectsModel: output, + } + return res, nil +} +func (c *ControllerV1) SysGenTableTableSelect(ctx context.Context, req *v1.SysGenTableTableSelectReq) (res *v1.SysGenTableTableSelectRes, err error) { + output, err := service.SysGenTable().TableSelect(ctx, req.GenCodesTableSelectParam) + if err != nil { + return nil, err + } + res = &v1.SysGenTableTableSelectRes{ + Items: output, + } + return res, nil +} +func (c *ControllerV1) SysGenTableColumnSelect(ctx context.Context, req *v1.SysGenTableColumnSelectReq) (res *v1.SysGenTableColumnSelectRes, err error) { + output, err := service.SysGenTable().ColumnSelect(ctx, req.GenCodesColumnSelectParam) + if err != nil { + return nil, err + } + res = &v1.SysGenTableColumnSelectRes{ + Items: output, + } + return res, nil +} +func (c *ControllerV1) SysGenTablePreview(ctx context.Context, req *v1.SysGenTablePreviewReq) (res *v1.SysGenTablePreviewRes, err error) { + output, err := service.SysGenTable().Preview(ctx, req.GenCodesPreviewParam) + if err != nil { + return nil, err + } + res = &v1.SysGenTablePreviewRes{ + GenCodesPreviewModel: output, + } + return res, nil +} +func (c *ControllerV1) SysGenTableBuild(ctx context.Context, req *v1.SysGenTableBuildReq) (res *v1.SysGenTableBuildRes, err error) { + output, err := service.SysGenTable().Build(ctx, req.GenCodesBuildParam) + if err != nil { + return nil, err + } + res = &v1.SysGenTableBuildRes{ + GenCodesBuildModel: output, + } + return res, nil +} +func (c *ControllerV1) SysGenTableColumnList(ctx context.Context, req *v1.SysGenTableColumnListReq) (res *v1.SysGenTableColumnListRes, err error) { + output, err := service.SysGenTable().ColumnList(ctx, req.GenCodesColumnListParam) + if err != nil { + return nil, err + } + res = &v1.SysGenTableColumnListRes{ + Items: output, + } + return res, nil +} +func (c *ControllerV1) SysGenTableEdit(ctx context.Context, req *v1.SysGenTableEditReq) (res *v1.SysGenTableEditRes, err error) { + output, err := service.SysGenTable().Edit(ctx, req.SysGenTableEditParam) + if err != nil { + return nil, err + } + res = &v1.SysGenTableEditRes{ + SysGenTableEditModel: output, + } + return res, nil +} diff --git a/server/internal/controller/info/info.go b/server/internal/controller/info/info.go new file mode 100644 index 0000000..26abc36 --- /dev/null +++ b/server/internal/controller/info/info.go @@ -0,0 +1,5 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package info diff --git a/server/internal/controller/info/info_new.go b/server/internal/controller/info/info_new.go new file mode 100644 index 0000000..48ffa2f --- /dev/null +++ b/server/internal/controller/info/info_new.go @@ -0,0 +1,15 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package info + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/api/info" +) + +type ControllerV1 struct{} + +func NewV1() info.IInfoV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/info/info_v1_info.go b/server/internal/controller/info/info_v1_info.go new file mode 100644 index 0000000..8548a50 --- /dev/null +++ b/server/internal/controller/info/info_v1_info.go @@ -0,0 +1,15 @@ +package info + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/info/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) Info(ctx context.Context, req *v1.InfoReq) (res *v1.InfoRes, err error) { + info := service.Info().GetInfo() + return &v1.InfoRes{ + InfoModel: info, + }, nil +} diff --git a/server/internal/controller/monitor/monitor.go b/server/internal/controller/monitor/monitor.go new file mode 100644 index 0000000..46dd6de --- /dev/null +++ b/server/internal/controller/monitor/monitor.go @@ -0,0 +1,5 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package monitor diff --git a/server/internal/controller/monitor/monitor_new.go b/server/internal/controller/monitor/monitor_new.go new file mode 100644 index 0000000..2e6f9d2 --- /dev/null +++ b/server/internal/controller/monitor/monitor_new.go @@ -0,0 +1,15 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package monitor + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/api/monitor" +) + +type ControllerV1 struct{} + +func NewV1() monitor.IMonitorV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/monitor/monitor_v1_cache.go b/server/internal/controller/monitor/monitor_v1_cache.go new file mode 100644 index 0000000..eea272a --- /dev/null +++ b/server/internal/controller/monitor/monitor_v1_cache.go @@ -0,0 +1,57 @@ +package monitor + +import ( + "context" + "strings" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/monitor/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/utility" +) + +func (c *ControllerV1) RedisInfo(ctx context.Context, req *v1.RedisInfoReq) (res *v1.RedisInfoRes, err error) { + info, err := g.Redis().Do(ctx, "INFO") + if err != nil { + return nil, err + } + + infoMap := make(map[string]string) + for _, line := range strings.Split(info.String(), "\n") { + if strings.HasPrefix(line, "#") { + continue + } + parts := strings.Split(line, ":") + if len(parts) == 2 { + infoMap[parts[0]] = parts[1] + } + } + dbSize := gconv.Int(infoMap["db0"]) + + info, err = g.Redis().Do(ctx, "INFO", "COMMANDSTATS") + if err != nil { + return nil, err + } + commandStats := make([]*v1.CommandStats, 0) + for _, line := range strings.Split(info.String(), "\n") { + if strings.HasPrefix(line, "#") { + continue + } + parts := strings.Split(line, ":") + if len(parts) == 2 { + if strings.HasPrefix(parts[0], "cmdstat_") { + commandStats = append(commandStats, &v1.CommandStats{ + Name: strings.TrimPrefix(parts[0], "cmdstat_"), + Value: utility.GetBetweenStr(parts[1], "calls=", ",usec"), + }) + } + } + } + + return &v1.RedisInfoRes{ + Info: infoMap, + DBSize: dbSize, + CommandStats: commandStats, + }, nil +} diff --git a/server/internal/controller/system/sys_notice_user.go b/server/internal/controller/system/sys_notice_user.go new file mode 100644 index 0000000..2065b18 --- /dev/null +++ b/server/internal/controller/system/sys_notice_user.go @@ -0,0 +1,75 @@ +// Package gen +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +// @AutoGenerate Version +package system + +import ( + "context" + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +// List 查看用户通知公告表列表 +func (c *ControllerV1) SysNoticeUserList(ctx context.Context, req *v1.SysNoticeUserListReq) (res *v1.SysNoticeUserListRes, err error) { + list, totalCount, err := service.SysNoticeUser().List(ctx, &req.SysNoticeUserListParam) + if err != nil { + return + } + + if list == nil { + list = []*model.SysNoticeUserListModel{} + } + + res = new(v1.SysNoticeUserListRes) + res.Items = list + res.PageResult.Page = req.Page + res.PageResult.PageSize = req.PageSize + res.PageResult.Total = totalCount + return +} + +// Export 导出用户通知公告表列表 +func (c *ControllerV1) SysNoticeUserExport(ctx context.Context, req *v1.SysNoticeUserExportReq) (res *v1.SysNoticeUserExportRes, err error) { + err = service.SysNoticeUser().Export(ctx, &req.SysNoticeUserListParam) + return +} + +// Edit 更新用户通知公告表 +func (c *ControllerV1) SysNoticeUserEdit(ctx context.Context, req *v1.SysNoticeUserEditReq) (res *v1.SysNoticeUserEditRes, err error) { + err = service.SysNoticeUser().Edit(ctx, &req.SysNoticeUserEditParam) + return +} + +// View 获取指定用户通知公告表信息 +func (c *ControllerV1) SysNoticeUserView(ctx context.Context, req *v1.SysNoticeUserViewReq) (res *v1.SysNoticeUserViewRes, err error) { + data, err := service.SysNoticeUser().View(ctx, &req.SysNoticeUserViewParam) + if err != nil { + return + } + + res = new(v1.SysNoticeUserViewRes) + res.SysNoticeUserViewModel = data + return +} + +// Delete 删除用户通知公告表 +func (c *ControllerV1) SysNoticeUserDelete(ctx context.Context, req *v1.SysNoticeUserDeleteReq) (res *v1.SysNoticeUserDeleteRes, err error) { + err = service.SysNoticeUser().Delete(ctx, &req.SysNoticeUserDeleteParam) + return +} + +// Status 更新用户通知公告表状态 +func (c *ControllerV1) SysNoticeUserStatus(ctx context.Context, req *v1.SysNoticeUserStatusReq) (res *v1.SysNoticeUserStatusRes, err error) { + err = service.SysNoticeUser().Status(ctx, &req.SysNoticeUserStatusParam) + return +} + +// Read 已读 +func (c *ControllerV1) SysNoticeUserRead(ctx context.Context, req *v1.SysNoticeUserReadReq) (res *v1.SysNoticeUserReadRes, err error) { + err = service.SysNoticeUser().Read(ctx, &req.SysNoticeUserReadParam) + return +} diff --git a/server/internal/controller/system/system.go b/server/internal/controller/system/system.go new file mode 100644 index 0000000..24bd259 --- /dev/null +++ b/server/internal/controller/system/system.go @@ -0,0 +1,5 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package system diff --git a/server/internal/controller/system/system_new.go b/server/internal/controller/system/system_new.go new file mode 100644 index 0000000..93797fc --- /dev/null +++ b/server/internal/controller/system/system_new.go @@ -0,0 +1,15 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package system + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/api/system" +) + +type ControllerV1 struct{} + +func NewV1() system.ISystemV1 { + return &ControllerV1{} +} diff --git a/server/internal/controller/system/system_v1_auth.go b/server/internal/controller/system/system_v1_auth.go new file mode 100644 index 0000000..e4159d1 --- /dev/null +++ b/server/internal/controller/system/system_v1_auth.go @@ -0,0 +1,94 @@ +package system + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error) { + userOut, token, err := service.SysAuth().Login(ctx, &model.LoginParams{ + CaptchaID: req.CaptchaID, + CaptchaValue: req.CaptchaValue, + Username: req.Username, + Password: req.Password, + TenantId: req.TenantId, + }) + if err != nil { + return nil, err + } + return &v1.LoginRes{ + ID: userOut.ID, + Username: userOut.Username, + NickName: userOut.NickName, + Avatar: userOut.Avatar, + AccessToken: token, + }, nil +} +func (c *ControllerV1) RefreshToken(ctx context.Context, req *v1.RefreshTokenReq) (res *v1.RefreshTokenRes, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} +func (c *ControllerV1) Logout(ctx context.Context, req *v1.LogoutReq) (res *v1.LogoutRes, err error) { + token, _ := service.SysAuth().GetAccessToken(ctx) + service.SysUserOnline().DeleteByToken(ctx, token) + event.EventsInstance().Emit(ctx, consts.EventKeyUserLogout, contexts.GetUserId(ctx)) + return &v1.LogoutRes{ + Data: "退出成功", + Status: 200, + }, nil +} +func (c *ControllerV1) GetAccessCodes(ctx context.Context, req *v1.GetAccessCodesReq) (res *v1.GetAccessCodesRes, err error) { + if req.IsCache { + accessCodes, _, err := mcache.GetUserAccessCodeList(ctx, contexts.GetUserId(ctx)) + if err != nil { + return nil, err + } + res = &v1.GetAccessCodesRes{ + Data: accessCodes, + } + } else { + accessCodes, roleDataAccessCodes, err := service.SysAuth().GetUserAccessCodeList(ctx, contexts.GetUserId(ctx)) + if err != nil { + return nil, err + } + mcache.SetUserAccessCodeList(ctx, contexts.GetUserId(ctx), accessCodes) + mcache.SetUserRoleDataAccessCodeList(ctx, contexts.GetUserId(ctx), roleDataAccessCodes) + res = &v1.GetAccessCodesRes{ + Data: accessCodes, + } + } + return res, nil +} +func (c *ControllerV1) TenantList(ctx context.Context, req *v1.TenantListReq) (res *v1.TenantListRes, err error) { + tenantList, _, err := service.SysTenant().List(ctx, &model.SysTenantListParam{ + Status: consts.SysDeptStatusNormal, + }) + if err != nil { + return nil, err + } + tenantEnabled := false + if len(tenantList) > 1 { + tenantEnabled = true + } + voList := make([]v1.TenantListData, 0) + for _, tenant := range tenantList { + voList = append(voList, v1.TenantListData{ + CompanyName: tenant.CompanyName, + Domain: tenant.Domain, + TenantId: tenant.TenantId, + }) + } + return &v1.TenantListRes{ + TenantEnabled: tenantEnabled, + VoList: voList, + }, nil +} diff --git a/server/internal/controller/system/system_v1_captcha.go b/server/internal/controller/system/system_v1_captcha.go new file mode 100644 index 0000000..f26d46d --- /dev/null +++ b/server/internal/controller/system/system_v1_captcha.go @@ -0,0 +1,17 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) GetCaptcha(ctx context.Context, req *v1.GetCaptchaReq) (res *v1.GetCaptchaRes, err error) { + key, image, err := service.SysCaptcha().GenerateCaptcha(ctx) + res = &v1.GetCaptchaRes{ + CaptchaID: key, + CaptchaImage: image, + } + return +} diff --git a/server/internal/controller/system/system_v1_client.go b/server/internal/controller/system/system_v1_client.go new file mode 100644 index 0000000..b6c9fd0 --- /dev/null +++ b/server/internal/controller/system/system_v1_client.go @@ -0,0 +1,75 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) SysClientList(ctx context.Context, req *v1.SysClientListReq) (res *v1.SysClientListRes, err error) { + items, total, err := service.SysClient().List(ctx, &req.SysClientListParam) + if err != nil { + return nil, err + } + res = &v1.SysClientListRes{ + Items: items, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + } + return +} +func (c *ControllerV1) SysClientView(ctx context.Context, req *v1.SysClientViewReq) (res *v1.SysClientViewRes, err error) { + info, err := service.SysClient().View(ctx, req.SysClientViewParam) + if err != nil { + return nil, err + } + res = &v1.SysClientViewRes{ + SysClientViewModel: info, + } + return +} +func (c *ControllerV1) SysClientAdd(ctx context.Context, req *v1.SysClientAddReq) (res *v1.SysClientAddRes, err error) { + output, err := service.SysClient().Add(ctx, req.SysClientAddParam) + if err != nil { + return nil, err + } + res = &v1.SysClientAddRes{ + SysClientAddModel: output, + } + return +} +func (c *ControllerV1) SysClientEdit(ctx context.Context, req *v1.SysClientEditReq) (res *v1.SysClientEditRes, err error) { + output, err := service.SysClient().Edit(ctx, req.SysClientEditParam) + if err != nil { + return nil, err + } + res = &v1.SysClientEditRes{ + SysClientEditModel: output, + } + return +} +func (c *ControllerV1) SysClientDelete(ctx context.Context, req *v1.SysClientDeleteReq) (res *v1.SysClientDeleteRes, err error) { + output, err := service.SysClient().Delete(ctx, req.SysClientDeleteParam) + if err != nil { + return nil, err + } + res = &v1.SysClientDeleteRes{ + SysClientDeleteModel: output, + } + return +} +func (c *ControllerV1) SysClientStatus(ctx context.Context, req *v1.SysClientStatusReq) (res *v1.SysClientStatusRes, err error) { + output, err := service.SysClient().Status(ctx, req.SysClientStatusParam) + if err != nil { + return nil, err + } + res = &v1.SysClientStatusRes{ + SysClientStatusModel: output, + } + return +} diff --git a/server/internal/controller/system/system_v1_config.go b/server/internal/controller/system/system_v1_config.go new file mode 100644 index 0000000..b5f4c59 --- /dev/null +++ b/server/internal/controller/system/system_v1_config.go @@ -0,0 +1,63 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) GetConfigList(ctx context.Context, req *v1.GetConfigListReq) (res *v1.GetConfigListRes, err error) { + + items, total, err := service.SysConfig().List(ctx, &req.SysConfigListParam) + res = &v1.GetConfigListRes{ + Data: items, + PageResult: response.PageResult{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + } + return +} +func (c *ControllerV1) AddConfig(ctx context.Context, req *v1.AddConfigReq) (res *v1.AddConfigRes, err error) { + output, err := service.SysConfig().Add(ctx, &req.SysConfigAddParam) + if err != nil { + return nil, err + } + res = &v1.AddConfigRes{ + SysConfigAddModel: *output, + } + return +} +func (c *ControllerV1) EditConfig(ctx context.Context, req *v1.EditConfigReq) (res *v1.EditConfigRes, err error) { + output, err := service.SysConfig().Edit(ctx, &req.SysConfigEditParam) + if err != nil { + return nil, err + } + res = &v1.EditConfigRes{ + SysConfigEditModel: *output, + } + return +} +func (c *ControllerV1) DeleteConfig(ctx context.Context, req *v1.DeleteConfigReq) (res *v1.DeleteConfigRes, err error) { + output, err := service.SysConfig().Delete(ctx, &req.SysConfigDeleteParam) + if err != nil { + return nil, err + } + res = &v1.DeleteConfigRes{ + SysConfigDeleteModel: *output, + } + return +} +func (c *ControllerV1) GetConfigById(ctx context.Context, req *v1.GetConfigByIdReq) (res *v1.GetConfigByIdRes, err error) { + output, err := service.SysConfig().View(ctx, &req.SysConfigViewParam) + if err != nil { + return nil, err + } + res = &v1.GetConfigByIdRes{ + SysConfigViewModel: *output, + } + return +} diff --git a/server/internal/controller/system/system_v1_dept.go b/server/internal/controller/system/system_v1_dept.go new file mode 100644 index 0000000..02569a3 --- /dev/null +++ b/server/internal/controller/system/system_v1_dept.go @@ -0,0 +1,70 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) DeptList(ctx context.Context, req *v1.DeptListReq) (res *v1.DeptListRes, err error) { + items, total, err := service.SysDept().GetDeptList(ctx, req.SysDeptListParam) + if err != nil { + return nil, err + } + return &v1.DeptListRes{ + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + Data: items, + }, nil +} + +func (c *ControllerV1) DeptTree(ctx context.Context, req *v1.DeptTreeReq) (res *v1.DeptTreeRes, err error) { + items, err := service.SysDept().GetDeptTree(ctx, req.SysDeptTreeParam) + if err != nil { + return nil, err + } + return &v1.DeptTreeRes{ + Data: items, + }, nil +} +func (c *ControllerV1) DeptAdd(ctx context.Context, req *v1.DeptAddReq) (res *v1.DeptAddRes, err error) { + deptId, err := service.SysDept().AddDept(ctx, &req.SysDeptAddModel) + if err != nil { + return nil, err + } + return &v1.DeptAddRes{ + DeptId: deptId, + }, nil +} +func (c *ControllerV1) DeptEdit(ctx context.Context, req *v1.DeptEditReq) (res *v1.DeptEditRes, err error) { + deptId, err := service.SysDept().EditDept(ctx, &req.SysDeptEditModel) + if err != nil { + return nil, err + } + return &v1.DeptEditRes{ + DeptId: deptId, + }, nil +} +func (c *ControllerV1) DeptDelete(ctx context.Context, req *v1.DeptDeleteReq) (res *v1.DeptDeleteRes, err error) { + deptId, err := service.SysDept().DeleteDept(ctx, &req.SysDeptDeleteModel) + if err != nil { + return nil, err + } + return &v1.DeptDeleteRes{ + DeptId: deptId, + }, nil +} +func (c *ControllerV1) DeptView(ctx context.Context, req *v1.DeptViewReq) (res *v1.DeptViewRes, err error) { + dept, err := service.SysDept().GetDeptById(ctx, req.SysDeptViewModel.DeptId) + if err != nil { + return nil, err + } + return &v1.DeptViewRes{ + SysDeptViewModel: *dept, + }, nil +} diff --git a/server/internal/controller/system/system_v1_dict.go b/server/internal/controller/system/system_v1_dict.go new file mode 100644 index 0000000..aeaa28a --- /dev/null +++ b/server/internal/controller/system/system_v1_dict.go @@ -0,0 +1,123 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) SysDictTypeList(ctx context.Context, req *v1.SysDictTypeListReq) (res *v1.SysDictTypeListRes, err error) { + items, total, err := service.SysDictType().List(ctx, &req.SysDictTypeListParam) + if err != nil { + return nil, err + } + return &v1.SysDictTypeListRes{ + Items: items, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + }, nil +} + +func (c *ControllerV1) SysDictDataList(ctx context.Context, req *v1.SysDictDataListReq) (res *v1.SysDictDataListRes, err error) { + items, total, err := service.SysDictData().List(ctx, &req.SysDictDataListParam) + if err != nil { + return nil, err + } + typeData, err := service.SysDictType().View(ctx, &model.SysDictTypeViewParam{ + DictId: req.SysDictDataListParam.DictId, + DictType: req.SysDictDataListParam.DictType, + }) + if err != nil { + return nil, err + } + + return &v1.SysDictDataListRes{ + Items: items, + Type: typeData, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + }, nil +} +func (c *ControllerV1) SysDictTypeAdd(ctx context.Context, req *v1.SysDictTypeAddReq) (res *v1.SysDictTypeAddRes, err error) { + output, err := service.SysDictType().Add(ctx, &req.SysDictTypeAddParam) + if err != nil { + return nil, err + } + return &v1.SysDictTypeAddRes{ + SysDictTypeAddModel: *output, + }, nil +} +func (c *ControllerV1) SysDictTypeEdit(ctx context.Context, req *v1.SysDictTypeEditReq) (res *v1.SysDictTypeEditRes, err error) { + output, err := service.SysDictType().Edit(ctx, &req.SysDictTypeEditParam) + if err != nil { + return nil, err + } + return &v1.SysDictTypeEditRes{ + SysDictTypeEditModel: *output, + }, nil +} +func (c *ControllerV1) SysDictTypeDelete(ctx context.Context, req *v1.SysDictTypeDeleteReq) (res *v1.SysDictTypeDeleteRes, err error) { + output, err := service.SysDictType().Delete(ctx, &req.SysDictTypeDeleteParam) + if err != nil { + return nil, err + } + return &v1.SysDictTypeDeleteRes{ + SysDictTypeDeleteModel: *output, + }, nil +} +func (c *ControllerV1) SysDictTypeView(ctx context.Context, req *v1.SysDictTypeViewReq) (res *v1.SysDictTypeViewRes, err error) { + output, err := service.SysDictType().View(ctx, &req.SysDictTypeViewParam) + if err != nil { + return nil, err + } + return &v1.SysDictTypeViewRes{ + SysDictTypeViewModel: *output, + }, nil +} +func (c *ControllerV1) SysDictDataAdd(ctx context.Context, req *v1.SysDictDataAddReq) (res *v1.SysDictDataAddRes, err error) { + output, err := service.SysDictData().Add(ctx, &req.SysDictDataAddParam) + if err != nil { + return nil, err + } + return &v1.SysDictDataAddRes{ + SysDictDataAddModel: *output, + }, nil +} +func (c *ControllerV1) SysDictDataEdit(ctx context.Context, req *v1.SysDictDataEditReq) (res *v1.SysDictDataEditRes, err error) { + output, err := service.SysDictData().Edit(ctx, &req.SysDictDataEditParam) + if err != nil { + return nil, err + } + return &v1.SysDictDataEditRes{ + SysDictDataEditModel: *output, + }, nil +} +func (c *ControllerV1) SysDictDataDelete(ctx context.Context, req *v1.SysDictDataDeleteReq) (res *v1.SysDictDataDeleteRes, err error) { + _, err = service.SysDictData().Delete(ctx, &req.SysDictDataDeleteParam) + if err != nil { + return nil, err + } + return &v1.SysDictDataDeleteRes{ + SysDictDataDeleteModel: model.SysDictDataDeleteModel{ + DictCodes: req.SysDictDataDeleteParam.DictCodes, + }, + }, nil +} +func (c *ControllerV1) SysDictDataView(ctx context.Context, req *v1.SysDictDataViewReq) (res *v1.SysDictDataViewRes, err error) { + output, err := service.SysDictData().View(ctx, &req.SysDictDataViewParam) + if err != nil { + return nil, err + } + return &v1.SysDictDataViewRes{ + SysDictDataViewModel: *output, + }, nil +} diff --git a/server/internal/controller/system/system_v1_job.go b/server/internal/controller/system/system_v1_job.go new file mode 100644 index 0000000..0c8addd --- /dev/null +++ b/server/internal/controller/system/system_v1_job.go @@ -0,0 +1,118 @@ +package system + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) SysJobList(ctx context.Context, req *v1.SysJobListReq) (res *v1.SysJobListRes, err error) { + data, total, err := service.SysJob().List(ctx, &req.SysJobListParam, &req.PageInfo) + if err != nil { + return nil, err + } + + return &v1.SysJobListRes{ + Data: data, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + }, nil + +} +func (c *ControllerV1) SysJobView(ctx context.Context, req *v1.SysJobViewReq) (res *v1.SysJobViewRes, err error) { + data, err := service.SysJob().View(ctx, req.JobId) + if err != nil { + return nil, err + } + return &v1.SysJobViewRes{ + SysJobViewModel: *data, + }, nil + +} +func (c *ControllerV1) SysJobAdd(ctx context.Context, req *v1.SysJobAddReq) (res *v1.SysJobAddRes, err error) { + lastInserId, err := service.SysJob().Add(ctx, &req.SysJobAddModel) + if err != nil { + return nil, err + } + data, err := service.SysJob().View(ctx, lastInserId) + + if err != nil { + return nil, err + } + return &v1.SysJobAddRes{ + SysJobViewModel: *data, + }, nil + +} +func (c *ControllerV1) SysJobUpdate(ctx context.Context, req *v1.SysJobUpdateReq) (res *v1.SysJobUpdateRes, err error) { + effectedRow, err := service.SysJob().Update(ctx, &req.SysJobUpdateModel) + if err != nil { + return nil, err + } + + if effectedRow == 0 { + return nil, gerror.NewCode(gcode.CodeInvalidParameter, "要更新的数据不存在") + } + + data, err := service.SysJob().View(ctx, req.JobId) + if err != nil { + return nil, err + } + return &v1.SysJobUpdateRes{ + SysJobViewModel: *data, + }, nil +} + +func (c *ControllerV1) SysJobUpdateStatus(ctx context.Context, req *v1.SysJobUpdateStatusReq) (res *v1.SysJobUpdateStatusRes, err error) { + effectedRow, err := service.SysJob().UpdateStatus(ctx, &req.SysJobUpdateStatusModel) + if err != nil { + return nil, err + } + + if effectedRow == 0 { + return nil, gerror.NewCode(gcode.CodeInvalidParameter, "要更新的数据不存在") + } + + data, err := service.SysJob().View(ctx, req.JobId) + if err != nil { + return nil, err + } + return &v1.SysJobUpdateStatusRes{ + SysJobViewModel: *data, + }, nil +} + +func (c *ControllerV1) SysJobDelete(ctx context.Context, req *v1.SysJobDeleteReq) (res *v1.SysJobDeleteRes, err error) { + if len(req.JobIds) == 0 { + return nil, gerror.NewCode(gcode.CodeMissingParameter, "参数不能为空") + } + + _, err = service.SysJob().Delete(ctx, &model.SysJobDeleteModel{JobIds: req.JobIds}) + if err != nil { + return nil, err + } + + return &v1.SysJobDeleteRes{ + JobIds: req.JobIds, + }, nil +} + +func (c *ControllerV1) SysJobExec(ctx context.Context, req *v1.SysJobExecReq) (res *v1.SysJobExecRes, err error) { + err = service.SysJob().Exec(ctx, req.JobId) + if err != nil { + return nil, err + } + + return &v1.SysJobExecRes{ + JobId: req.JobId, + }, nil +} diff --git a/server/internal/controller/system/system_v1_logininfor.go b/server/internal/controller/system/system_v1_logininfor.go new file mode 100644 index 0000000..6776b6b --- /dev/null +++ b/server/internal/controller/system/system_v1_logininfor.go @@ -0,0 +1,33 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) ListLogininfor(ctx context.Context, req *v1.ListLogininforReq) (res *v1.ListLogininforRes, err error) { + items, total, err := service.SysLogininfor().List(ctx, &req.SysLogininforListParam) + if err != nil { + return nil, err + } + return &v1.ListLogininforRes{ + Items: items, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + }, nil +} +func (c *ControllerV1) DeleteLogininfor(ctx context.Context, req *v1.DeleteLogininforReq) (res *v1.DeleteLogininforRes, err error) { + out, err := service.SysLogininfor().Delete(ctx, &req.SysLogininforDeleteParam) + if err != nil { + return nil, err + } + return &v1.DeleteLogininforRes{ + SysLogininforDeleteModel: *out, + }, nil +} diff --git a/server/internal/controller/system/system_v1_menu.go b/server/internal/controller/system/system_v1_menu.go new file mode 100644 index 0000000..62d0193 --- /dev/null +++ b/server/internal/controller/system/system_v1_menu.go @@ -0,0 +1,62 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +func (c *ControllerV1) MenuAll(ctx context.Context, req *v1.MenuAllReq) (res *v1.MenuAllRes, err error) { + data, err := service.SysMenu().GetUserMenuTree(ctx) + return &data, err +} +func (c *ControllerV1) MenuList(ctx context.Context, req *v1.MenuListReq) (res *v1.MenuListRes, err error) { + data, total, err := service.SysMenu().GetTenantMenu(ctx, &req.SysMenuListParam) + return &v1.MenuListRes{ + Data: data, + Total: total, + }, err +} +func (c *ControllerV1) MenuView(ctx context.Context, req *v1.MenuViewReq) (res *v1.MenuViewRes, err error) { + data, err := service.SysMenu().GetSysMenuView(ctx, req.MenuId) + if err != nil { + return nil, err + } + return &v1.MenuViewRes{ + SysMenuViewModel: *data, + }, nil +} + +func (c *ControllerV1) MenuAdd(ctx context.Context, req *v1.MenuAddReq) (res *v1.MenuAddRes, err error) { + data, err := service.SysMenu().AddSysMenu(ctx, &req.SysMenuAddModel) + if err != nil { + return nil, err + } + return &v1.MenuAddRes{ + SysMenuViewModel: *data, + }, nil +} + +func (c *ControllerV1) MenuUpdate(ctx context.Context, req *v1.MenuUpdateReq) (res *v1.MenuUpdateRes, err error) { + data, err := service.SysMenu().UpdateSysMenu(ctx, &req.SysMenuUpdateModel) + if err != nil { + return nil, err + } + return &v1.MenuUpdateRes{ + SysMenuViewModel: *data, + }, nil +} +func (c *ControllerV1) MenuDelete(ctx context.Context, req *v1.MenuDeleteReq) (res *v1.MenuDeleteRes, err error) { + err = service.SysMenu().DeleteSysMenu(ctx, req.MenuId) + if err != nil { + return nil, err + } + return &v1.MenuDeleteRes{}, nil +} +func (c *ControllerV1) MenuTree(ctx context.Context, req *v1.MenuTreeReq) (res *v1.MenuTreeRes, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} diff --git a/server/internal/controller/system/system_v1_notice.go b/server/internal/controller/system/system_v1_notice.go new file mode 100644 index 0000000..eed2354 --- /dev/null +++ b/server/internal/controller/system/system_v1_notice.go @@ -0,0 +1,59 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) SysNoticeList(ctx context.Context, req *v1.SysNoticeListReq) (res *v1.SysNoticeListRes, err error) { + g.Log().Infof(ctx, "SystemControl.SysNoticeList %+v", req) + items, total, err := service.SysNotice().List(ctx, req.SysNoticeListParam) + if err != nil { + return nil, err + } + res = &v1.SysNoticeListRes{ + Items: items, + PageResult: response.PageResult{ + Total: total, + Page: req.PageInfo.Page, + PageSize: req.PageInfo.PageSize, + }, + } + return +} +func (c *ControllerV1) SysNoticeAdd(ctx context.Context, req *v1.SysNoticeAddReq) (res *v1.SysNoticeAddRes, err error) { + err = service.SysNotice().Add(ctx, req.SysNoticeAddParam) + if err != nil { + return nil, err + } + return +} +func (c *ControllerV1) SysNoticeEdit(ctx context.Context, req *v1.SysNoticeEditReq) (res *v1.SysNoticeEditRes, err error) { + err = service.SysNotice().Edit(ctx, req.SysNoticeEditParam) + if err != nil { + return nil, err + } + return +} +func (c *ControllerV1) SysNoticeDelete(ctx context.Context, req *v1.SysNoticeDeleteReq) (res *v1.SysNoticeDeleteRes, err error) { + err = service.SysNotice().Delete(ctx, req.SysNoticeDeleteParam) + if err != nil { + return nil, err + } + return +} +func (c *ControllerV1) SysNoticeView(ctx context.Context, req *v1.SysNoticeViewReq) (res *v1.SysNoticeViewRes, err error) { + data, err := service.SysNotice().View(ctx, req.SysNoticeViewParam) + if err != nil { + return nil, err + } + res = &v1.SysNoticeViewRes{ + SysNoticeViewModel: data, + } + return +} diff --git a/server/internal/controller/system/system_v1_online.go b/server/internal/controller/system/system_v1_online.go new file mode 100644 index 0000000..27e10ab --- /dev/null +++ b/server/internal/controller/system/system_v1_online.go @@ -0,0 +1,36 @@ +package system + +import ( + "context" + "errors" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) SysUserOnlineList(ctx context.Context, req *v1.SysUserOnlineListReq) (res *v1.SysUserOnlineListRes, err error) { + items, total, err := service.SysUserOnline().List(ctx, &req.SysUserOnlineListParam, &req.PageInfo) + if err != nil { + return nil, err + } + return &v1.SysUserOnlineListRes{ + Items: items, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + }, nil +} + +func (c *ControllerV1) SysUserOnlineDelete(ctx context.Context, req *v1.SysUserOnlineDeleteReq) (res *v1.SysUserOnlineDeleteRes, err error) { + if len(req.Ids) == 0 { + return nil, errors.New("ids不能为空") + } + err = service.SysUserOnline().Delete(ctx, req.Ids) + if err != nil { + return nil, err + } + return &v1.SysUserOnlineDeleteRes{}, nil +} diff --git a/server/internal/controller/system/system_v1_oper_log.go b/server/internal/controller/system/system_v1_oper_log.go new file mode 100644 index 0000000..b077981 --- /dev/null +++ b/server/internal/controller/system/system_v1_oper_log.go @@ -0,0 +1,25 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) GetOperLogList(ctx context.Context, req *v1.GetOperLogListReq) (res *v1.GetOperLogListRes, err error) { + items, total, err := service.SysOperLog().GetOperLogList(ctx, &req.SysOperLogListParam, &req.PageInfo) + if err != nil { + return nil, err + } + + return &v1.GetOperLogListRes{ + Items: items, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + }, nil +} diff --git a/server/internal/controller/system/system_v1_oss.go b/server/internal/controller/system/system_v1_oss.go new file mode 100644 index 0000000..2d01d5b --- /dev/null +++ b/server/internal/controller/system/system_v1_oss.go @@ -0,0 +1,43 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) SysOssList(ctx context.Context, req *v1.SysOssListReq) (res *v1.SysOssListRes, err error) { + items, total, err := service.SysOss().List(ctx, &req.SysOssListParam, &req.PageInfo) + if err != nil { + return nil, err + } + res = &v1.SysOssListRes{ + Items: items, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + } + return +} +func (c *ControllerV1) SysOssView(ctx context.Context, req *v1.SysOssViewReq) (res *v1.SysOssViewRes, err error) { + info, err := service.SysOss().View(ctx, req.SysOssViewParam) + if err != nil { + return nil, err + } + res = &v1.SysOssViewRes{ + SysOssViewModel: info, + } + return +} +func (c *ControllerV1) SysOssDelete(ctx context.Context, req *v1.SysOssDeleteReq) (res *v1.SysOssDeleteRes, err error) { + _, err = service.SysOss().Delete(ctx, req.SysOssDeleteParam) + if err != nil { + return nil, err + } + res = &v1.SysOssDeleteRes{} + return +} diff --git a/server/internal/controller/system/system_v1_oss_config.go b/server/internal/controller/system/system_v1_oss_config.go new file mode 100644 index 0000000..6ab99ac --- /dev/null +++ b/server/internal/controller/system/system_v1_oss_config.go @@ -0,0 +1,61 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) SysOssConfigList(ctx context.Context, req *v1.SysOssConfigListReq) (res *v1.SysOssConfigListRes, err error) { + items, total, err := service.SysOssConfig().List(ctx, req.SysOssConfigListParam) + if err != nil { + return nil, err + } + return &v1.SysOssConfigListRes{ + Items: items, + PageResult: response.PageResult{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + }, nil +} +func (c *ControllerV1) SysOssConfigView(ctx context.Context, req *v1.SysOssConfigViewReq) (res *v1.SysOssConfigViewRes, err error) { + item, err := service.SysOssConfig().View(ctx, req.SysOssConfigViewParam) + if err != nil { + return nil, err + } + return &v1.SysOssConfigViewRes{ + SysOssConfigViewModel: item, + }, nil +} +func (c *ControllerV1) SysOssConfigAdd(ctx context.Context, req *v1.SysOssConfigAddReq) (res *v1.SysOssConfigAddRes, err error) { + item, err := service.SysOssConfig().Add(ctx, req.SysOssConfigAddParam) + if err != nil { + return nil, err + } + return &v1.SysOssConfigAddRes{ + SysOssConfigAddModel: item, + }, nil +} +func (c *ControllerV1) SysOssConfigEdit(ctx context.Context, req *v1.SysOssConfigEditReq) (res *v1.SysOssConfigEditRes, err error) { + item, err := service.SysOssConfig().Edit(ctx, req.SysOssConfigEditParam) + if err != nil { + return nil, err + } + return &v1.SysOssConfigEditRes{ + SysOssConfigEditModel: item, + }, nil +} + +func (c *ControllerV1) SysOssConfigDelete(ctx context.Context, req *v1.SysOssConfigDeleteReq) (res *v1.SysOssConfigDeleteRes, err error) { + item, err := service.SysOssConfig().Delete(ctx, req.SysOssConfigDeleteParam) + if err != nil { + return nil, err + } + return &v1.SysOssConfigDeleteRes{ + SysOssConfigDeleteModel: item, + }, nil +} diff --git a/server/internal/controller/system/system_v1_post.go b/server/internal/controller/system/system_v1_post.go new file mode 100644 index 0000000..0fbb56a --- /dev/null +++ b/server/internal/controller/system/system_v1_post.go @@ -0,0 +1,67 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) PostList(ctx context.Context, req *v1.PostListReq) (res *v1.PostListRes, err error) { + items, total, err := service.SysPost().List(ctx, req.SysPostListParam) + if err != nil { + return nil, err + } + return &v1.PostListRes{ + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + Data: items, + }, nil +} +func (c *ControllerV1) PostAdd(ctx context.Context, req *v1.PostAddReq) (res *v1.PostAddRes, err error) { + post, err := service.SysPost().Add(ctx, req.SysPostAddParam) + if err != nil { + return nil, err + } + return &v1.PostAddRes{ + SysPostAddModel: post, + }, nil +} +func (c *ControllerV1) PostEdit(ctx context.Context, req *v1.PostEditReq) (res *v1.PostEditRes, err error) { + post, err := service.SysPost().Edit(ctx, req.SysPostEditParam) + if err != nil { + return nil, err + } + return &v1.PostEditRes{ + SysPostEditModel: post, + }, nil +} +func (c *ControllerV1) PostDelete(ctx context.Context, req *v1.PostDeleteReq) (res *v1.PostDeleteRes, err error) { + post, err := service.SysPost().Delete(ctx, req.SysPostDeleteParam) + if err != nil { + return nil, err + } + return &v1.PostDeleteRes{ + SysPostDeleteModel: post, + }, nil +} +func (c *ControllerV1) PostView(ctx context.Context, req *v1.PostViewReq) (res *v1.PostViewRes, err error) { + post, err := service.SysPost().View(ctx, req.SysPostViewParam) + if err != nil { + return nil, err + } + return &v1.PostViewRes{ + SysPostViewModel: post, + }, nil +} +func (c *ControllerV1) PostExport(ctx context.Context, req *v1.PostExportReq) (res *v1.PostExportRes, err error) { + _, err = service.SysPost().Export(ctx, req.SysPostExportParam) + if err != nil { + return nil, err + } + return &v1.PostExportRes{}, nil +} diff --git a/server/internal/controller/system/system_v1_qrcode.go b/server/internal/controller/system/system_v1_qrcode.go new file mode 100644 index 0000000..1cdc9a5 --- /dev/null +++ b/server/internal/controller/system/system_v1_qrcode.go @@ -0,0 +1,94 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +// 获取登录二维码 +func (c *ControllerV1) QrcodeLogin(ctx context.Context, req *v1.QrcodeLoginReq) (res *v1.QrcodeLoginRes, err error) { + qrcodeLoginModel, err := service.SysQrcode().GetQrcodeLogin(ctx) + if err != nil { + return nil, err + } + return &v1.QrcodeLoginRes{ + QrcodeLoginModel: *qrcodeLoginModel, + }, nil +} + +// 获取登录二维码扫码结果 +func (c *ControllerV1) QrcodeLoginStatus(ctx context.Context, req *v1.QrcodeLoginStatusReq) (res *v1.QrcodeLoginStatusRes, err error) { + qrcodeLoginModel, err := service.SysQrcode().GetQrcodeLoginStatus(ctx, req.TempUserId) + if err != nil { + return nil, err + } + return &v1.QrcodeLoginStatusRes{ + QrcodeLoginModel: *qrcodeLoginModel, + }, nil +} + +// 获取绑定二维码 +func (c *ControllerV1) QrcodeBind(ctx context.Context, req *v1.QrcodeBindReq) (res *v1.QrcodeBindRes, err error) { + qrcodeBindModel, err := service.SysQrcode().GetQrcodeBind(ctx) + if err != nil { + return nil, err + } + return &v1.QrcodeBindRes{ + QrcodeCacheModel: *qrcodeBindModel, + }, nil +} + +// 获取绑定二维码扫码结果 +func (c *ControllerV1) QrcodeBindStatus(ctx context.Context, req *v1.QrcodeBindStatusReq) (res *v1.QrcodeBindStatusRes, err error) { + qrcodeBindStatusModel, err := service.SysQrcode().GetQrcodeBindStatus(ctx) + if err != nil { + return nil, err + } + return &v1.QrcodeBindStatusRes{ + QrcodeBindStatusModel: *qrcodeBindStatusModel, + }, nil +} + +// 扫码成功回调 +func (c *ControllerV1) QrcodeScanCallback(ctx context.Context, req *v1.QrcodeScanCallbackReq) (res *v1.QrcodeScanCallbackRes, err error) { + err = service.SysQrcode().QrcodeScanCallback(ctx, &req.QrcodeScanCallbackParam) + if err != nil { + return nil, err + } + return &v1.QrcodeScanCallbackRes{}, nil +} + +// 注册并关联OpenId +func (c *ControllerV1) QrcodeRegisterAndBindOpenId(ctx context.Context, req *v1.QrcodeRegisterAndBindOpenIdReq) (res *v1.QrcodeRegisterAndBindOpenIdRes, err error) { + qrcodeLoginAndBindOpenIdModel, err := service.SysQrcode().QrcodeRegisterAndBindOpenId(ctx, &req.QrcodeRegisterAndBindOpenIdParam) + if err != nil { + return nil, err + } + return &v1.QrcodeRegisterAndBindOpenIdRes{ + QrcodeLoginAndBindOpenIdModel: *qrcodeLoginAndBindOpenIdModel, + }, nil +} + +// 登录并关联OpenId +func (c *ControllerV1) QrcodeLoginAndBindOpenId(ctx context.Context, req *v1.QrcodeLoginAndBindOpenIdReq) (res *v1.QrcodeLoginAndBindOpenIdRes, err error) { + qrcodeLoginAndBindOpenIdModel, err := service.SysQrcode().QrcodeLoginAndBindOpenId(ctx, &req.QrcodeLoginAndBindOpenIdParam) + if err != nil { + return nil, err + } + return &v1.QrcodeLoginAndBindOpenIdRes{ + QrcodeLoginAndBindOpenIdModel: *qrcodeLoginAndBindOpenIdModel, + }, nil +} + +// 扫码后选择用户登录 +func (c *ControllerV1) QrcodeLoginSelectUser(ctx context.Context, req *v1.QrcodeLoginSelectUserReq) (res *v1.QrcodeLoginSelectUserRes, err error) { + qrcodeLoginAndBindOpenIdModel, err := service.SysQrcode().QrcodeLoginSelectUserId(ctx, &req.QrcodeLoginSelectUserIdParam) + if err != nil { + return nil, err + } + return &v1.QrcodeLoginSelectUserRes{ + QrcodeLoginAndBindOpenIdModel: *qrcodeLoginAndBindOpenIdModel, + }, nil +} diff --git a/server/internal/controller/system/system_v1_role.go b/server/internal/controller/system/system_v1_role.go new file mode 100644 index 0000000..eafcdb6 --- /dev/null +++ b/server/internal/controller/system/system_v1_role.go @@ -0,0 +1,69 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) RoleList(ctx context.Context, req *v1.RoleListReq) (res *v1.RoleListRes, err error) { + data, total, err := service.SysRole().List(ctx, &req.SysRoleListParam) + if err != nil { + return nil, err + } + return &v1.RoleListRes{ + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + Data: data, + }, nil +} +func (c *ControllerV1) RoleAdd(ctx context.Context, req *v1.RoleAddReq) (res *v1.RoleAddRes, err error) { + role, err := service.SysRole().Add(ctx, &req.SysRoleAddParam) + if err != nil { + return nil, err + } + return &v1.RoleAddRes{ + SysRoleAddModel: *role, + }, nil +} +func (c *ControllerV1) RoleEdit(ctx context.Context, req *v1.RoleEditReq) (res *v1.RoleEditRes, err error) { + role, err := service.SysRole().Edit(ctx, &req.SysRoleEditParam) + if err != nil { + return nil, err + } + return &v1.RoleEditRes{ + SysRoleEditModel: *role, + }, nil +} + +func (c *ControllerV1) RoleDelete(ctx context.Context, req *v1.RoleDeleteReq) (res *v1.RoleDeleteRes, err error) { + role, err := service.SysRole().Delete(ctx, &req.SysRoleDeleteParam) + if err != nil { + return nil, err + } + return &v1.RoleDeleteRes{ + SysRoleDeleteModel: *role, + }, nil +} + +func (c *ControllerV1) RoleView(ctx context.Context, req *v1.RoleViewReq) (res *v1.RoleViewRes, err error) { + record, err := service.SysRole().View(ctx, &req.SysRoleViewParam) + if err != nil { + return nil, err + } + return &v1.RoleViewRes{ + SysRoleViewModel: *record, + }, nil +} +func (c *ControllerV1) RoleDataScopeEdit(ctx context.Context, req *v1.RoleDataScopeEditReq) (res *v1.RoleDataScopeEditRes, err error) { + err = service.SysRole().EditRoleDataScope(ctx, &req.SysRoleDataScopeEditParam) + if err != nil { + return nil, err + } + return &v1.RoleDataScopeEditRes{}, nil +} diff --git a/server/internal/controller/system/system_v1_social.go b/server/internal/controller/system/system_v1_social.go new file mode 100644 index 0000000..a472ff9 --- /dev/null +++ b/server/internal/controller/system/system_v1_social.go @@ -0,0 +1,34 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) SysSocialList(ctx context.Context, req *v1.SysSocialListReq) (res *v1.SysSocialListRes, err error) { + items, total, err := service.SysSocial().List(ctx, &req.SysSocialListParam, &req.PageInfo) + if err != nil { + return nil, err + } + res = &v1.SysSocialListRes{ + Items: items, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + } + return res, nil +} + +// 删除绑定关系 +func (c *ControllerV1) SysSocialDelete(ctx context.Context, req *v1.SysSocialDeleteReq) (res *v1.SysSocialDeleteRes, err error) { + err = service.SysSocial().Delete(ctx, req.Id) + if err != nil { + return nil, err + } + return &v1.SysSocialDeleteRes{}, nil +} diff --git a/server/internal/controller/system/system_v1_tenant.go b/server/internal/controller/system/system_v1_tenant.go new file mode 100644 index 0000000..dc4c203 --- /dev/null +++ b/server/internal/controller/system/system_v1_tenant.go @@ -0,0 +1,141 @@ +package system + +import ( + "context" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) SysTenantList(ctx context.Context, req *v1.SysTenantListReq) (res *v1.SysTenantListRes, err error) { + items, total, err := service.SysTenant().List(ctx, req.SysTenantListParam) + if err != nil { + return nil, err + } + res = &v1.SysTenantListRes{ + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + Items: items, + } + return +} + +func (c *ControllerV1) SysTenantPackageList(ctx context.Context, req *v1.SysTenantPackageListReq) (res *v1.SysTenantPackageListRes, err error) { + items, total, err := service.SysTenantPackage().List(ctx, &req.SysTenantPackageListParam) + if err != nil { + return nil, err + } + res = &v1.SysTenantPackageListRes{ + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + Items: items, + } + return +} +func (c *ControllerV1) SysTenantAdd(ctx context.Context, req *v1.SysTenantAddReq) (res *v1.SysTenantAddRes, err error) { + data, err := service.SysTenant().Add(ctx, req.SysTenantAddParam) + if err != nil { + return nil, err + } + return &v1.SysTenantAddRes{ + SysTenantAddModel: data, + }, nil +} +func (c *ControllerV1) SysTenantEdit(ctx context.Context, req *v1.SysTenantEditReq) (res *v1.SysTenantEditRes, err error) { + data, err := service.SysTenant().Edit(ctx, req.SysTenantEditParam) + if err != nil { + return nil, err + } + return &v1.SysTenantEditRes{ + SysTenantEditModel: data, + }, nil +} +func (c *ControllerV1) SysTenantDelete(ctx context.Context, req *v1.SysTenantDeleteReq) (res *v1.SysTenantDeleteRes, err error) { + data, err := service.SysTenant().Delete(ctx, req.SysTenantDeleteParam) + if err != nil { + return nil, err + } + return &v1.SysTenantDeleteRes{ + SysTenantDeleteModel: data, + }, nil +} +func (c *ControllerV1) SysTenantStatus(ctx context.Context, req *v1.SysTenantStatusReq) (res *v1.SysTenantStatusRes, err error) { + data, err := service.SysTenant().Status(ctx, req.SysTenantStatusParam) + if err != nil { + return nil, err + } + return &v1.SysTenantStatusRes{ + SysTenantStatusModel: data, + }, nil +} +func (c *ControllerV1) SysTenantView(ctx context.Context, req *v1.SysTenantViewReq) (res *v1.SysTenantViewRes, err error) { + data, err := service.SysTenant().View(ctx, req.SysTenantViewParam) + if err != nil { + return nil, err + } + return &v1.SysTenantViewRes{ + SysTenantViewModel: data, + }, nil +} +func (c *ControllerV1) SysTenantPackageView(ctx context.Context, req *v1.SysTenantPackageViewReq) (res *v1.SysTenantPackageViewRes, err error) { + data, err := service.SysTenantPackage().View(ctx, req.SysTenantPackageViewParam) + if err != nil { + return nil, err + } + return &v1.SysTenantPackageViewRes{ + SysTenantPackageViewModel: data, + }, nil +} +func (c *ControllerV1) SysTenantPackageStatus(ctx context.Context, req *v1.SysTenantPackageStatusReq) (res *v1.SysTenantPackageStatusRes, err error) { + data, err := service.SysTenantPackage().Status(ctx, req.SysTenantPackageStatusParam) + if err != nil { + return nil, err + } + return &v1.SysTenantPackageStatusRes{ + SysTenantPackageStatusModel: data, + }, nil +} +func (c *ControllerV1) SysTenantPackageAdd(ctx context.Context, req *v1.SysTenantPackageAddReq) (res *v1.SysTenantPackageAddRes, err error) { + data, err := service.SysTenantPackage().Add(ctx, req.SysTenantPackageAddParam) + if err != nil { + return nil, err + } + return &v1.SysTenantPackageAddRes{ + SysTenantPackageAddModel: data, + }, nil +} +func (c *ControllerV1) SysTenantPackageEdit(ctx context.Context, req *v1.SysTenantPackageEditReq) (res *v1.SysTenantPackageEditRes, err error) { + data, err := service.SysTenantPackage().Edit(ctx, req.SysTenantPackageEditParam) + if err != nil { + return nil, err + } + return &v1.SysTenantPackageEditRes{ + SysTenantPackageEditModel: data, + }, nil +} +func (c *ControllerV1) SysTenantPackageDelete(ctx context.Context, req *v1.SysTenantPackageDeleteReq) (res *v1.SysTenantPackageDeleteRes, err error) { + data, err := service.SysTenantPackage().Delete(ctx, req.SysTenantPackageDeleteParam) + if err != nil { + return nil, err + } + return &v1.SysTenantPackageDeleteRes{ + SysTenantPackageDeleteModel: data, + }, nil +} +func (c *ControllerV1) SysTenantSyncMenu(ctx context.Context, req *v1.SysTenantSyncMenuReq) (res *v1.SysTenantSyncMenuRes, err error) { + err = service.SysTenant().SyncTenantMenu(ctx, req.TenantId) + if err != nil { + return nil, err + } + return &v1.SysTenantSyncMenuRes{ + SysTenantSyncMenuModel: &model.SysTenantSyncMenuModel{}, + }, nil +} diff --git a/server/internal/controller/system/system_v1_user.go b/server/internal/controller/system/system_v1_user.go new file mode 100644 index 0000000..409cef8 --- /dev/null +++ b/server/internal/controller/system/system_v1_user.go @@ -0,0 +1,236 @@ +package system + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" + + v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/response" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" +) + +func (c *ControllerV1) UserInfo(ctx context.Context, req *v1.UserInfoReq) (res *v1.UserInfoRes, err error) { + // 获取token + userId := contexts.GetUserId(ctx) + if userId == 0 { + return nil, gerror.NewCode(gcode.CodeSecurityReason) + } + // 获取用户信息 + user, err := service.SysUser().GetUserById(ctx, userId) + if err != nil { + return nil, err + } + menus, err := service.SysMenu().GetUserMenuTree(ctx) + if err != nil { + return nil, err + } + homePath := "" + firstMenu := "404" + if len(menus) > 0 { + for _, menu := range menus { + firstMenu = menu.Path + if len(menu.Children) == 0 { + break + } + for _, menu2 := range menu.Children { + firstMenu = firstMenu + "/" + menu2.Path + if len(menu2.Children) == 0 { + break + } + for _, menu3 := range menu2.Children { + firstMenu = firstMenu + "/" + menu3.Path + if len(menu3.Children) == 0 { + break + } + for _, menu4 := range menu3.Children { + firstMenu = firstMenu + "/" + menu4.Path + if len(menu4.Children) == 0 { + break + } + for _, menu5 := range menu4.Children { + firstMenu = firstMenu + "/" + menu5.Path + if len(menu5.Children) == 0 { + break + } + for _, menu6 := range menu5.Children { + firstMenu = firstMenu + "/" + menu6.Path + if len(menu6.Children) == 0 { + break + } + } + } + } + } + } + } + } + g.Log().Debugf(ctx, "firstMenu: %s", firstMenu) + if homePath == "" { + homePath = "/" + firstMenu + } + + return &v1.UserInfoRes{ + UserInfo: v1.UserInfo{ + Id: user.UserId, + Username: user.UserName, + Nickname: user.NickName, + Avatar: user.Avatar, + Email: user.Email, + HomePath: homePath, + }, + }, nil +} +func (c *ControllerV1) UserList(ctx context.Context, req *v1.UserListReq) (res *v1.UserListRes, err error) { + // 获取用户列表 + users, total, err := service.SysUser().List(ctx, &req.PageInfo, &req.UserListParam) + if err != nil { + return nil, err + } + return &v1.UserListRes{ + Data: users, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + }, nil +} + +func (c *ControllerV1) AddUser(ctx context.Context, req *v1.AddUserReq) (res *v1.AddUserRes, err error) { + data, err := service.SysUser().AddUser(ctx, &req.SysUserAddModel) + if err != nil { + return nil, err + } + return &v1.AddUserRes{ + Data: data, + }, nil +} + +func (c *ControllerV1) UserProfile(ctx context.Context, req *v1.UserProfileReq) (res *v1.UserProfileRes, err error) { + user, err := service.SysUser().Profile(ctx) + if err != nil { + return nil, err + } + return &v1.UserProfileRes{ + User: user, + }, nil +} + +func (c *ControllerV1) UpdateCurrentUser(ctx context.Context, req *v1.UpdateCurrentUserReq) (res *v1.UpdateCurrentUserRes, err error) { + user, err := service.SysUser().UpdateCurrentUser(ctx, &req.UpdateCurrentUserModel) + if err != nil { + return nil, err + } + return &v1.UpdateCurrentUserRes{ + UserInfo: v1.UserInfo{ + Id: user.UserId, + Username: user.UserName, + Nickname: user.NickName, + Avatar: user.Avatar, + Email: user.Email, + }, + }, nil +} + +func (c *ControllerV1) UpdateCurrentUserPassword(ctx context.Context, req *v1.UpdateCurrentUserPasswordReq) (res *v1.UpdateCurrentUserPasswordRes, err error) { + err = service.SysUser().UpdateCurrentUserPassword(ctx, &req.UpdateCurrentUserPasswordModel) + if err != nil { + return nil, err + } + return &v1.UpdateCurrentUserPasswordRes{}, nil +} + +func (c *ControllerV1) UpdateUser(ctx context.Context, req *v1.UpdateUserReq) (res *v1.UpdateUserRes, err error) { + err = service.SysUser().UpdateUser(ctx, &req.SysUserUpdateModel) + if err != nil { + return nil, err + } + user, err := service.SysUser().GetUserById(ctx, req.UserId) + if err != nil { + return nil, err + } + return &v1.UpdateUserRes{ + SysUserViewModel: *user, + }, nil +} + +func (c *ControllerV1) DeleteUser(ctx context.Context, req *v1.DeleteUserReq) (res *v1.DeleteUserRes, err error) { + if req.UserId != 0 { + err = service.SysUser().DeleteUser(ctx, []int64{req.UserId}) + } else { + err = service.SysUser().DeleteUser(ctx, req.UserIds) + } + if err != nil { + return nil, err + } + return &v1.DeleteUserRes{}, nil +} +func (c *ControllerV1) GetUser(ctx context.Context, req *v1.GetUserReq) (res *v1.GetUserRes, err error) { + // 如果调用方传入 userId == 0,则不返回错误,直接返回空的用户视图结构 + if req.UserId == 0 { + return &v1.GetUserRes{ + SysUserViewModel: model.SysUserViewModel{}, + }, nil + } + + user, err := service.SysUser().GetUserById(ctx, req.UserId) + if err != nil { + return nil, err + } + return &v1.GetUserRes{ + SysUserViewModel: *user, + }, nil +} +func (c *ControllerV1) ResetPassword(ctx context.Context, req *v1.ResetPasswordReq) (res *v1.ResetPasswordRes, err error) { + err = service.SysUser().ResetPassword(ctx, req.UserId, req.Password) + if err != nil { + return nil, err + } + return &v1.ResetPasswordRes{}, nil +} +func (c *ControllerV1) GetUserListByDeptId(ctx context.Context, req *v1.GetUserListByDeptIdReq) (res *v1.GetUserListByDeptIdRes, err error) { + users, total, err := service.SysUser().List(ctx, &request.PageInfo{ + Page: req.Page, + PageSize: req.PageSize, + }, &model.UserListParam{ + DeptId: req.DeptId, + }) + if err != nil { + return nil, err + } + miniUsers := make([]*model.SysUserMiniModel, 0) + gconv.Structs(users, &miniUsers) + return &v1.GetUserListByDeptIdRes{ + Items: miniUsers, + PageResult: response.PageResult{ + Page: req.Page, + PageSize: req.PageSize, + Total: total, + }, + }, nil +} + +// UserRegister 用户注册 +func (c *ControllerV1) UserRegister(ctx context.Context, req *v1.UserRegisterReq) (res *v1.UserRegisterRes, err error) { + err = service.SysUser().Register(ctx, &req.SysUserRegisterModel) + if err != nil { + return nil, err + } + return &v1.UserRegisterRes{}, nil +} + +// UserStatus 更新用户状态 +func (c *ControllerV1) UserStatus(ctx context.Context, req *v1.UserStatusReq) (res *v1.UserStatusRes, err error) { + err = service.SysUser().Status(ctx, &req.SysUserStatusParam) + if err != nil { + return nil, err + } + return &v1.UserStatusRes{}, nil +} diff --git a/server/internal/dao/batch_detail.go b/server/internal/dao/batch_detail.go new file mode 100644 index 0000000..c7ced8c --- /dev/null +++ b/server/internal/dao/batch_detail.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// batchDetailDao is the data access object for the table batch_detail. +// You can define custom methods on it to extend its functionality as needed. +type batchDetailDao struct { + *internal.BatchDetailDao +} + +var ( + // BatchDetail is a globally accessible object for table batch_detail operations. + BatchDetail = batchDetailDao{internal.NewBatchDetailDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/batch_main.go b/server/internal/dao/batch_main.go new file mode 100644 index 0000000..ed9c050 --- /dev/null +++ b/server/internal/dao/batch_main.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// batchMainDao is the data access object for the table batch_main. +// You can define custom methods on it to extend its functionality as needed. +type batchMainDao struct { + *internal.BatchMainDao +} + +var ( + // BatchMain is a globally accessible object for table batch_main operations. + BatchMain = batchMainDao{internal.NewBatchMainDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/card_channel_assoc.go b/server/internal/dao/card_channel_assoc.go new file mode 100644 index 0000000..aa232e4 --- /dev/null +++ b/server/internal/dao/card_channel_assoc.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// cardChannelAssocDao is the data access object for the table card_channel_assoc. +// You can define custom methods on it to extend its functionality as needed. +type cardChannelAssocDao struct { + *internal.CardChannelAssocDao +} + +var ( + // CardChannelAssoc is a globally accessible object for table card_channel_assoc operations. + CardChannelAssoc = cardChannelAssocDao{internal.NewCardChannelAssocDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/gen_table.go b/server/internal/dao/gen_table.go new file mode 100644 index 0000000..1868ae4 --- /dev/null +++ b/server/internal/dao/gen_table.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// genTableDao is the data access object for the table gen_table. +// You can define custom methods on it to extend its functionality as needed. +type genTableDao struct { + *internal.GenTableDao +} + +var ( + // GenTable is a globally accessible object for table gen_table operations. + GenTable = genTableDao{internal.NewGenTableDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/gen_table_column.go b/server/internal/dao/gen_table_column.go new file mode 100644 index 0000000..c53889f --- /dev/null +++ b/server/internal/dao/gen_table_column.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// genTableColumnDao is the data access object for the table gen_table_column. +// You can define custom methods on it to extend its functionality as needed. +type genTableColumnDao struct { + *internal.GenTableColumnDao +} + +var ( + // GenTableColumn is a globally accessible object for table gen_table_column operations. + GenTableColumn = genTableColumnDao{internal.NewGenTableColumnDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/internal/batch_detail.go b/server/internal/dao/internal/batch_detail.go new file mode 100644 index 0000000..2facffc --- /dev/null +++ b/server/internal/dao/internal/batch_detail.go @@ -0,0 +1,109 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// BatchDetailDao is the data access object for the table batch_detail. +type BatchDetailDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns BatchDetailColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// BatchDetailColumns defines and stores column names for the table batch_detail. +type BatchDetailColumns struct { + Id string // 主键ID + BatchNo string // 批次编号 + BatchType string // 批次类型 枚举(与主表一致) + DetailStatus string // 明细异步处理状态 0-待处理 1-处理中 2-成功 3-失败 + BlankOutStatus string // 作废状态 0-是 1-否 + RowNo string // 行号 + ItemId string // 项ID:卡ICCID/设备编码 + ErrorCode string // 异常编码 + ErrorReason string // 异常原因 + Snapshot string // 信息快照 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// batchDetailColumns holds the columns for the table batch_detail. +var batchDetailColumns = BatchDetailColumns{ + Id: "id", + BatchNo: "batch_no", + BatchType: "batch_type", + DetailStatus: "detail_status", + BlankOutStatus: "blank_out_status", + RowNo: "row_no", + ItemId: "item_id", + ErrorCode: "error_code", + ErrorReason: "error_reason", + Snapshot: "snapshot", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewBatchDetailDao creates and returns a new DAO object for table data access. +func NewBatchDetailDao(handlers ...gdb.ModelHandler) *BatchDetailDao { + return &BatchDetailDao{ + group: "default", + table: "batch_detail", + columns: batchDetailColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *BatchDetailDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *BatchDetailDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *BatchDetailDao) Columns() BatchDetailColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *BatchDetailDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *BatchDetailDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *BatchDetailDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/batch_main.go b/server/internal/dao/internal/batch_main.go new file mode 100644 index 0000000..6a531bb --- /dev/null +++ b/server/internal/dao/internal/batch_main.go @@ -0,0 +1,135 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// BatchMainDao is the data access object for the table batch_main. +type BatchMainDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns BatchMainColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// BatchMainColumns defines and stores column names for the table batch_main. +type BatchMainColumns struct { + Id string // 主键ID + BatchNo string // 批次编号 + OrderSn string // 订单编号 + BatchType string // 批次类型 枚举 + BatchStatus string // 批次状态 枚举 + BatchAsyncStatus string // 异步处理状态 0=已创建 1=待处理 2=处理中 3=成功 4=失败 5=部分成功 + SourceType string // 来源方式 枚举 + SourceBatchNo string // 来源批次编号 + SourceOssId string // 源文件OSS ID + ResultOssId string // 结果文件OSS ID + IsCrossDept string // 是否跨级操作机构 0-否 1-是 + TargetDeptId string // 目标机构编号 + DeptId string // 机构编号 + TotalCount string // 总数量 + ValidCount string // 有效数量 + SuccessCount string // 成功数量 + FailCount string // 失败数量 + RunResult string // 运行结果:0-待运行 1-运行中 2-成功 3-失败 + FailReason string // 失败原因 + BatchInfo string // 批次信息:入库/出库展示字段 + Remark string // 备注扩展字段 + StartTime string // 开始时间 + EndTime string // 结束时间 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// batchMainColumns holds the columns for the table batch_main. +var batchMainColumns = BatchMainColumns{ + Id: "id", + BatchNo: "batch_no", + OrderSn: "order_sn", + BatchType: "batch_type", + BatchStatus: "batch_status", + BatchAsyncStatus: "batch_async_status", + SourceType: "source_type", + SourceBatchNo: "source_batch_no", + SourceOssId: "source_oss_id", + ResultOssId: "result_oss_id", + IsCrossDept: "is_cross_dept", + TargetDeptId: "target_dept_id", + DeptId: "dept_id", + TotalCount: "total_count", + ValidCount: "valid_count", + SuccessCount: "success_count", + FailCount: "fail_count", + RunResult: "run_result", + FailReason: "fail_reason", + BatchInfo: "batch_info", + Remark: "remark", + StartTime: "start_time", + EndTime: "end_time", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewBatchMainDao creates and returns a new DAO object for table data access. +func NewBatchMainDao(handlers ...gdb.ModelHandler) *BatchMainDao { + return &BatchMainDao{ + group: "default", + table: "batch_main", + columns: batchMainColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *BatchMainDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *BatchMainDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *BatchMainDao) Columns() BatchMainColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *BatchMainDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *BatchMainDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *BatchMainDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/card_channel_assoc.go b/server/internal/dao/internal/card_channel_assoc.go new file mode 100644 index 0000000..652a67b --- /dev/null +++ b/server/internal/dao/internal/card_channel_assoc.go @@ -0,0 +1,97 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// CardChannelAssocDao is the data access object for the table card_channel_assoc. +type CardChannelAssocDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns CardChannelAssocColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// CardChannelAssocColumns defines and stores column names for the table card_channel_assoc. +type CardChannelAssocColumns struct { + Id string // 主键 + ChannelId string // 通道ID + MatchType string // 匹配类型: 1单卡 2号段 + Iccid string // 单卡ICCID + IccidStart string // 号段起始ICCID + IccidEnd string // 号段结束ICCID + Status string // 状态: 1启用 0停用 + Remark string // 备注 + CreatedAt string // 创建时间 + UpdatedAt string // 更新时间 +} + +// cardChannelAssocColumns holds the columns for the table card_channel_assoc. +var cardChannelAssocColumns = CardChannelAssocColumns{ + Id: "id", + ChannelId: "channel_id", + MatchType: "match_type", + Iccid: "iccid", + IccidStart: "iccid_start", + IccidEnd: "iccid_end", + Status: "status", + Remark: "remark", + CreatedAt: "created_at", + UpdatedAt: "updated_at", +} + +// NewCardChannelAssocDao creates and returns a new DAO object for table data access. +func NewCardChannelAssocDao(handlers ...gdb.ModelHandler) *CardChannelAssocDao { + return &CardChannelAssocDao{ + group: "default", + table: "card_channel_assoc", + columns: cardChannelAssocColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *CardChannelAssocDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *CardChannelAssocDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *CardChannelAssocDao) Columns() CardChannelAssocColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *CardChannelAssocDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *CardChannelAssocDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *CardChannelAssocDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/gen_table.go b/server/internal/dao/internal/gen_table.go new file mode 100644 index 0000000..a04334a --- /dev/null +++ b/server/internal/dao/internal/gen_table.go @@ -0,0 +1,121 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// GenTableDao is the data access object for the table gen_table. +type GenTableDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns GenTableColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// GenTableColumns defines and stores column names for the table gen_table. +type GenTableColumns struct { + TableId string // 编号 + DataName string // 数据源名称 + TableName string // 表名称 + TableComment string // 表描述 + SubTableName string // 关联子表的表名 + SubTableFkName string // 子表关联的外键名 + ClassName string // 实体类名称 + TplCategory string // 使用的模板(crud单表操作 tree树表操作) + PackageName string // 生成包路径 + ModuleName string // 生成模块名 + BusinessName string // 生成业务名 + FunctionName string // 生成功能名 + FunctionAuthor string // 生成功能作者 + GenType string // 生成代码方式(0zip压缩包 1自定义路径) + GenPath string // 生成路径(不填默认项目路径) + Options string // 其它生成选项 + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + Remark string // 备注 +} + +// genTableColumns holds the columns for the table gen_table. +var genTableColumns = GenTableColumns{ + TableId: "table_id", + DataName: "data_name", + TableName: "table_name", + TableComment: "table_comment", + SubTableName: "sub_table_name", + SubTableFkName: "sub_table_fk_name", + ClassName: "class_name", + TplCategory: "tpl_category", + PackageName: "package_name", + ModuleName: "module_name", + BusinessName: "business_name", + FunctionName: "function_name", + FunctionAuthor: "function_author", + GenType: "gen_type", + GenPath: "gen_path", + Options: "options", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + Remark: "remark", +} + +// NewGenTableDao creates and returns a new DAO object for table data access. +func NewGenTableDao(handlers ...gdb.ModelHandler) *GenTableDao { + return &GenTableDao{ + group: "default", + table: "gen_table", + columns: genTableColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *GenTableDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *GenTableDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *GenTableDao) Columns() GenTableColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *GenTableDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *GenTableDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *GenTableDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/gen_table_column.go b/server/internal/dao/internal/gen_table_column.go new file mode 100644 index 0000000..1c2d00c --- /dev/null +++ b/server/internal/dao/internal/gen_table_column.go @@ -0,0 +1,123 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// GenTableColumnDao is the data access object for the table gen_table_column. +type GenTableColumnDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns GenTableColumnColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// GenTableColumnColumns defines and stores column names for the table gen_table_column. +type GenTableColumnColumns struct { + ColumnId string // 编号 + TableId string // 归属表编号 + ColumnName string // 列名称 + ColumnComment string // 列描述 + ColumnType string // 列类型 + JavaType string // JAVA类型 + JavaField string // JAVA字段名 + IsPk string // 是否主键(1是) + IsIncrement string // 是否自增(1是) + IsRequired string // 是否必填(1是) + IsInsert string // 是否为插入字段(1是) + IsEdit string // 是否编辑字段(1是) + IsList string // 是否列表字段(1是) + IsQuery string // 是否查询字段(1是) + QueryType string // 查询方式(等于、不等于、大于、小于、范围) + HtmlType string // 显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件) + DictType string // 字典类型 + Sort string // 排序 + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// genTableColumnColumns holds the columns for the table gen_table_column. +var genTableColumnColumns = GenTableColumnColumns{ + ColumnId: "column_id", + TableId: "table_id", + ColumnName: "column_name", + ColumnComment: "column_comment", + ColumnType: "column_type", + JavaType: "java_type", + JavaField: "java_field", + IsPk: "is_pk", + IsIncrement: "is_increment", + IsRequired: "is_required", + IsInsert: "is_insert", + IsEdit: "is_edit", + IsList: "is_list", + IsQuery: "is_query", + QueryType: "query_type", + HtmlType: "html_type", + DictType: "dict_type", + Sort: "sort", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewGenTableColumnDao creates and returns a new DAO object for table data access. +func NewGenTableColumnDao(handlers ...gdb.ModelHandler) *GenTableColumnDao { + return &GenTableColumnDao{ + group: "default", + table: "gen_table_column", + columns: genTableColumnColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *GenTableColumnDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *GenTableColumnDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *GenTableColumnDao) Columns() GenTableColumnColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *GenTableColumnDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *GenTableColumnDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *GenTableColumnDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_client.go b/server/internal/dao/internal/sys_client.go new file mode 100644 index 0000000..68581a0 --- /dev/null +++ b/server/internal/dao/internal/sys_client.go @@ -0,0 +1,109 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysClientDao is the data access object for the table sys_client. +type SysClientDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysClientColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysClientColumns defines and stores column names for the table sys_client. +type SysClientColumns struct { + Id string // id + ClientId string // 客户端id + ClientKey string // 客户端key + ClientSecret string // 客户端秘钥 + GrantType string // 授权类型 + DeviceType string // 设备类型 + ActiveTimeout string // token活跃超时时间 + Timeout string // token固定超时 + Status string // 状态(0正常 1停用) + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 +} + +// sysClientColumns holds the columns for the table sys_client. +var sysClientColumns = SysClientColumns{ + Id: "id", + ClientId: "client_id", + ClientKey: "client_key", + ClientSecret: "client_secret", + GrantType: "grant_type", + DeviceType: "device_type", + ActiveTimeout: "active_timeout", + Timeout: "timeout", + Status: "status", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", +} + +// NewSysClientDao creates and returns a new DAO object for table data access. +func NewSysClientDao(handlers ...gdb.ModelHandler) *SysClientDao { + return &SysClientDao{ + group: "default", + table: "sys_client", + columns: sysClientColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysClientDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysClientDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysClientDao) Columns() SysClientColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysClientDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysClientDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysClientDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_config.go b/server/internal/dao/internal/sys_config.go new file mode 100644 index 0000000..9bd0555 --- /dev/null +++ b/server/internal/dao/internal/sys_config.go @@ -0,0 +1,101 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysConfigDao is the data access object for the table sys_config. +type SysConfigDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysConfigColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysConfigColumns defines and stores column names for the table sys_config. +type SysConfigColumns struct { + ConfigId string // 参数主键 + TenantId string // 租户编号 + ConfigName string // 参数名称 + ConfigKey string // 参数键名 + ConfigValue string // 参数键值 + ConfigType string // 系统内置(Y是 N否) + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + Remark string // 备注 +} + +// sysConfigColumns holds the columns for the table sys_config. +var sysConfigColumns = SysConfigColumns{ + ConfigId: "config_id", + TenantId: "tenant_id", + ConfigName: "config_name", + ConfigKey: "config_key", + ConfigValue: "config_value", + ConfigType: "config_type", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + Remark: "remark", +} + +// NewSysConfigDao creates and returns a new DAO object for table data access. +func NewSysConfigDao(handlers ...gdb.ModelHandler) *SysConfigDao { + return &SysConfigDao{ + group: "default", + table: "sys_config", + columns: sysConfigColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysConfigDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysConfigDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysConfigDao) Columns() SysConfigColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysConfigDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysConfigDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysConfigDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_dept.go b/server/internal/dao/internal/sys_dept.go new file mode 100644 index 0000000..724901e --- /dev/null +++ b/server/internal/dao/internal/sys_dept.go @@ -0,0 +1,125 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysDeptDao is the data access object for the table sys_dept. +type SysDeptDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysDeptColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysDeptColumns defines and stores column names for the table sys_dept. +type SysDeptColumns struct { + DeptId string // 部门id + TenantId string // 租户编号 + ParentId string // 父部门id + Ancestors string // 祖级列表 + DeptKey string // 机构编号 + DeptName string // 部门名称 + DeptType string // 部门类型(0:部门 1:公司) + DeptCategory string // 部门类别编码 + OrderNum string // 显示顺序 + Leader string // 负责人 + Phone string // 联系电话 + Email string // 邮箱 + Status string // 部门状态(0正常 1停用) + CreditCode string // 统一社会信用代码 + AreaCode string // 行政区域代码 + Address string // 地址 + DeptRole string // 机构默认角色ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 +} + +// sysDeptColumns holds the columns for the table sys_dept. +var sysDeptColumns = SysDeptColumns{ + DeptId: "dept_id", + TenantId: "tenant_id", + ParentId: "parent_id", + Ancestors: "ancestors", + DeptKey: "dept_key", + DeptName: "dept_name", + DeptType: "dept_type", + DeptCategory: "dept_category", + OrderNum: "order_num", + Leader: "leader", + Phone: "phone", + Email: "email", + Status: "status", + CreditCode: "credit_code", + AreaCode: "area_code", + Address: "address", + DeptRole: "dept_role", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", +} + +// NewSysDeptDao creates and returns a new DAO object for table data access. +func NewSysDeptDao(handlers ...gdb.ModelHandler) *SysDeptDao { + return &SysDeptDao{ + group: "default", + table: "sys_dept", + columns: sysDeptColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysDeptDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysDeptDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysDeptDao) Columns() SysDeptColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysDeptDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysDeptDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysDeptDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_dict_data.go b/server/internal/dao/internal/sys_dict_data.go new file mode 100644 index 0000000..b8c182e --- /dev/null +++ b/server/internal/dao/internal/sys_dict_data.go @@ -0,0 +1,107 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysDictDataDao is the data access object for the table sys_dict_data. +type SysDictDataDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysDictDataColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysDictDataColumns defines and stores column names for the table sys_dict_data. +type SysDictDataColumns struct { + DictCode string // 字典编码 + TenantId string // 租户编号 + DictSort string // 字典排序 + DictLabel string // 字典标签 + DictValue string // 字典键值 + DictType string // 字典类型 + CssClass string // 样式属性(其他样式扩展) + ListClass string // 表格回显样式 + IsDefault string // 是否默认(Y是 N否) + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + Remark string // 备注 +} + +// sysDictDataColumns holds the columns for the table sys_dict_data. +var sysDictDataColumns = SysDictDataColumns{ + DictCode: "dict_code", + TenantId: "tenant_id", + DictSort: "dict_sort", + DictLabel: "dict_label", + DictValue: "dict_value", + DictType: "dict_type", + CssClass: "css_class", + ListClass: "list_class", + IsDefault: "is_default", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + Remark: "remark", +} + +// NewSysDictDataDao creates and returns a new DAO object for table data access. +func NewSysDictDataDao(handlers ...gdb.ModelHandler) *SysDictDataDao { + return &SysDictDataDao{ + group: "default", + table: "sys_dict_data", + columns: sysDictDataColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysDictDataDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysDictDataDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysDictDataDao) Columns() SysDictDataColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysDictDataDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysDictDataDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysDictDataDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_dict_type.go b/server/internal/dao/internal/sys_dict_type.go new file mode 100644 index 0000000..0fd068d --- /dev/null +++ b/server/internal/dao/internal/sys_dict_type.go @@ -0,0 +1,99 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysDictTypeDao is the data access object for the table sys_dict_type. +type SysDictTypeDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysDictTypeColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysDictTypeColumns defines and stores column names for the table sys_dict_type. +type SysDictTypeColumns struct { + DictId string // 字典主键 + TenantId string // 租户编号 + DictName string // 字典名称 + DictType string // 字典类型 + IsSys string // 是否系统(0是 1否) + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + Remark string // 备注 +} + +// sysDictTypeColumns holds the columns for the table sys_dict_type. +var sysDictTypeColumns = SysDictTypeColumns{ + DictId: "dict_id", + TenantId: "tenant_id", + DictName: "dict_name", + DictType: "dict_type", + IsSys: "is_sys", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + Remark: "remark", +} + +// NewSysDictTypeDao creates and returns a new DAO object for table data access. +func NewSysDictTypeDao(handlers ...gdb.ModelHandler) *SysDictTypeDao { + return &SysDictTypeDao{ + group: "default", + table: "sys_dict_type", + columns: sysDictTypeColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysDictTypeDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysDictTypeDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysDictTypeDao) Columns() SysDictTypeColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysDictTypeDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysDictTypeDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysDictTypeDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_gen_table.go b/server/internal/dao/internal/sys_gen_table.go new file mode 100644 index 0000000..cfdcf1b --- /dev/null +++ b/server/internal/dao/internal/sys_gen_table.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysGenTableDao is the data access object for the table sys_gen_table. +type SysGenTableDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysGenTableColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysGenTableColumns defines and stores column names for the table sys_gen_table. +type SysGenTableColumns struct { + TableId string // 生成ID + GenType string // 生成类型 + GenTemplate string // 生成模板 + VarName string // 实体命名 + Options string // 配置选项 + DbName string // 数据库名称 + TableName string // 主表名称 + TableComment string // 主表注释 + DaoName string // 主表dao模型 + MasterColumns string // 主表字段 + AddonName string // 插件名称 + Status string // 生成状态(0成功 1未开始) + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// sysGenTableColumns holds the columns for the table sys_gen_table. +var sysGenTableColumns = SysGenTableColumns{ + TableId: "table_id", + GenType: "gen_type", + GenTemplate: "gen_template", + VarName: "var_name", + Options: "options", + DbName: "db_name", + TableName: "table_name", + TableComment: "table_comment", + DaoName: "dao_name", + MasterColumns: "master_columns", + AddonName: "addon_name", + Status: "status", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewSysGenTableDao creates and returns a new DAO object for table data access. +func NewSysGenTableDao(handlers ...gdb.ModelHandler) *SysGenTableDao { + return &SysGenTableDao{ + group: "default", + table: "sys_gen_table", + columns: sysGenTableColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysGenTableDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysGenTableDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysGenTableDao) Columns() SysGenTableColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysGenTableDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysGenTableDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysGenTableDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_job.go b/server/internal/dao/internal/sys_job.go new file mode 100644 index 0000000..d9384f6 --- /dev/null +++ b/server/internal/dao/internal/sys_job.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysJobDao is the data access object for the table sys_job. +type SysJobDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysJobColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysJobColumns defines and stores column names for the table sys_job. +type SysJobColumns struct { + JobId string // 任务ID + JobName string // 任务名称 + JobParams string // 参数 + JobGroup string // 任务组名 + InvokeTarget string // 调用目标字符串 + CronExpression string // cron执行表达式 + MisfirePolicy string // 计划执行策略(1多次执行 2执行一次) + Concurrent string // 是否并发执行(0允许 1禁止) + Status string // 状态(0正常 1暂停) + Remark string // 备注信息 + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 +} + +// sysJobColumns holds the columns for the table sys_job. +var sysJobColumns = SysJobColumns{ + JobId: "job_id", + JobName: "job_name", + JobParams: "job_params", + JobGroup: "job_group", + InvokeTarget: "invoke_target", + CronExpression: "cron_expression", + MisfirePolicy: "misfire_policy", + Concurrent: "concurrent", + Status: "status", + Remark: "remark", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", +} + +// NewSysJobDao creates and returns a new DAO object for table data access. +func NewSysJobDao(handlers ...gdb.ModelHandler) *SysJobDao { + return &SysJobDao{ + group: "default", + table: "sys_job", + columns: sysJobColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysJobDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysJobDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysJobDao) Columns() SysJobColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysJobDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysJobDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysJobDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_logininfor.go b/server/internal/dao/internal/sys_logininfor.go new file mode 100644 index 0000000..4db9a83 --- /dev/null +++ b/server/internal/dao/internal/sys_logininfor.go @@ -0,0 +1,101 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysLogininforDao is the data access object for the table sys_logininfor. +type SysLogininforDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysLogininforColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysLogininforColumns defines and stores column names for the table sys_logininfor. +type SysLogininforColumns struct { + InfoId string // 访问ID + TenantId string // 租户编号 + UserName string // 用户账号 + ClientKey string // 客户端 + DeviceType string // 设备类型 + Ipaddr string // 登录IP地址 + LoginLocation string // 登录地点 + Browser string // 浏览器类型 + Os string // 操作系统 + Status string // 登录状态(0成功 1失败) + Msg string // 提示消息 + LoginTime string // 访问时间 +} + +// sysLogininforColumns holds the columns for the table sys_logininfor. +var sysLogininforColumns = SysLogininforColumns{ + InfoId: "info_id", + TenantId: "tenant_id", + UserName: "user_name", + ClientKey: "client_key", + DeviceType: "device_type", + Ipaddr: "ipaddr", + LoginLocation: "login_location", + Browser: "browser", + Os: "os", + Status: "status", + Msg: "msg", + LoginTime: "login_time", +} + +// NewSysLogininforDao creates and returns a new DAO object for table data access. +func NewSysLogininforDao(handlers ...gdb.ModelHandler) *SysLogininforDao { + return &SysLogininforDao{ + group: "default", + table: "sys_logininfor", + columns: sysLogininforColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysLogininforDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysLogininforDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysLogininforDao) Columns() SysLogininforColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysLogininforDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysLogininforDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysLogininforDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_menu.go b/server/internal/dao/internal/sys_menu.go new file mode 100644 index 0000000..3a22bef --- /dev/null +++ b/server/internal/dao/internal/sys_menu.go @@ -0,0 +1,121 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysMenuDao is the data access object for the table sys_menu. +type SysMenuDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysMenuColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysMenuColumns defines and stores column names for the table sys_menu. +type SysMenuColumns struct { + MenuId string // 菜单ID + MenuName string // 菜单名称 + ParentId string // 父菜单ID + Level string // 关系树等级 + Tree string // 关系树 + OrderNum string // 显示顺序 + Path string // 路由地址 + Component string // 组件路径 + QueryParam string // 路由参数 + IsFrame string // 是否为外链(0是 1否) + IsCache string // 是否缓存(0缓存 1不缓存) + MenuType string // 菜单类型(M目录 C菜单 F按钮) + Visible string // 显示状态(0显示 1隐藏) + Status string // 菜单状态(0正常 1停用) + Perms string // 权限标识 + Icon string // 菜单图标 + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + Remark string // 备注 +} + +// sysMenuColumns holds the columns for the table sys_menu. +var sysMenuColumns = SysMenuColumns{ + MenuId: "menu_id", + MenuName: "menu_name", + ParentId: "parent_id", + Level: "level", + Tree: "tree", + OrderNum: "order_num", + Path: "path", + Component: "component", + QueryParam: "query_param", + IsFrame: "is_frame", + IsCache: "is_cache", + MenuType: "menu_type", + Visible: "visible", + Status: "status", + Perms: "perms", + Icon: "icon", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + Remark: "remark", +} + +// NewSysMenuDao creates and returns a new DAO object for table data access. +func NewSysMenuDao(handlers ...gdb.ModelHandler) *SysMenuDao { + return &SysMenuDao{ + group: "default", + table: "sys_menu", + columns: sysMenuColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysMenuDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysMenuDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysMenuDao) Columns() SysMenuColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysMenuDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysMenuDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysMenuDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_notice.go b/server/internal/dao/internal/sys_notice.go new file mode 100644 index 0000000..72ad751 --- /dev/null +++ b/server/internal/dao/internal/sys_notice.go @@ -0,0 +1,107 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysNoticeDao is the data access object for the table sys_notice. +type SysNoticeDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysNoticeColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysNoticeColumns defines and stores column names for the table sys_notice. +type SysNoticeColumns struct { + NoticeId string // 公告ID + TenantId string // 租户编号 + NoticeTitle string // 公告标题 + NoticeType string // 公告类型(1通知 2公告) + NoticeContent string // 公告内容 + Status string // 公告状态(0正常 1关闭) + NoticeRange string // 通知范围(1全员 2指定机构 3指定用户) + DeptIds string // 通知机构ID列表JSON + UserIds string // 通知用户ID列表JSON + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + Remark string // 备注 +} + +// sysNoticeColumns holds the columns for the table sys_notice. +var sysNoticeColumns = SysNoticeColumns{ + NoticeId: "notice_id", + TenantId: "tenant_id", + NoticeTitle: "notice_title", + NoticeType: "notice_type", + NoticeContent: "notice_content", + Status: "status", + NoticeRange: "notice_range", + DeptIds: "dept_ids", + UserIds: "user_ids", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + Remark: "remark", +} + +// NewSysNoticeDao creates and returns a new DAO object for table data access. +func NewSysNoticeDao(handlers ...gdb.ModelHandler) *SysNoticeDao { + return &SysNoticeDao{ + group: "default", + table: "sys_notice", + columns: sysNoticeColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysNoticeDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysNoticeDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysNoticeDao) Columns() SysNoticeColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysNoticeDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysNoticeDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysNoticeDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_notice_user.go b/server/internal/dao/internal/sys_notice_user.go new file mode 100644 index 0000000..00836b9 --- /dev/null +++ b/server/internal/dao/internal/sys_notice_user.go @@ -0,0 +1,107 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysNoticeUserDao is the data access object for the table sys_notice_user. +type SysNoticeUserDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysNoticeUserColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysNoticeUserColumns defines and stores column names for the table sys_notice_user. +type SysNoticeUserColumns struct { + Id string // ID + NoticeId string // 公告ID + TenantId string // 租户编号 + NoticeTitle string // 公告标题 + NoticeType string // 公告类型(1通知 2公告) + NoticeContent string // 公告内容 + Status string // 公告状态(0正常 1关闭) + UserId string // 用户ID + IsRead string // 是否已读(0未读 1已读) + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + Remark string // 备注 +} + +// sysNoticeUserColumns holds the columns for the table sys_notice_user. +var sysNoticeUserColumns = SysNoticeUserColumns{ + Id: "id", + NoticeId: "notice_id", + TenantId: "tenant_id", + NoticeTitle: "notice_title", + NoticeType: "notice_type", + NoticeContent: "notice_content", + Status: "status", + UserId: "user_id", + IsRead: "is_read", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + Remark: "remark", +} + +// NewSysNoticeUserDao creates and returns a new DAO object for table data access. +func NewSysNoticeUserDao(handlers ...gdb.ModelHandler) *SysNoticeUserDao { + return &SysNoticeUserDao{ + group: "default", + table: "sys_notice_user", + columns: sysNoticeUserColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysNoticeUserDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysNoticeUserDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysNoticeUserDao) Columns() SysNoticeUserColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysNoticeUserDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysNoticeUserDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysNoticeUserDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_oper_log.go b/server/internal/dao/internal/sys_oper_log.go new file mode 100644 index 0000000..0d82659 --- /dev/null +++ b/server/internal/dao/internal/sys_oper_log.go @@ -0,0 +1,113 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysOperLogDao is the data access object for the table sys_oper_log. +type SysOperLogDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysOperLogColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysOperLogColumns defines and stores column names for the table sys_oper_log. +type SysOperLogColumns struct { + OperId string // 日志主键 + TenantId string // 租户编号 + Title string // 模块标题 + BusinessType string // 业务类型(0其它 1新增 2修改 3删除) + Method string // 方法名称 + RequestMethod string // 请求方式 + OperatorType string // 操作类别(0其它 1后台用户 2手机端用户) + OperName string // 操作人员 + DeptName string // 部门名称 + OperUrl string // 请求URL + OperIp string // 主机地址 + OperLocation string // 操作地点 + OperParam string // 请求参数 + JsonResult string // 返回参数 + Status string // 操作状态(0正常 1异常) + ErrorMsg string // 错误消息 + OperTime string // 操作时间 + CostTime string // 消耗时间 +} + +// sysOperLogColumns holds the columns for the table sys_oper_log. +var sysOperLogColumns = SysOperLogColumns{ + OperId: "oper_id", + TenantId: "tenant_id", + Title: "title", + BusinessType: "business_type", + Method: "method", + RequestMethod: "request_method", + OperatorType: "operator_type", + OperName: "oper_name", + DeptName: "dept_name", + OperUrl: "oper_url", + OperIp: "oper_ip", + OperLocation: "oper_location", + OperParam: "oper_param", + JsonResult: "json_result", + Status: "status", + ErrorMsg: "error_msg", + OperTime: "oper_time", + CostTime: "cost_time", +} + +// NewSysOperLogDao creates and returns a new DAO object for table data access. +func NewSysOperLogDao(handlers ...gdb.ModelHandler) *SysOperLogDao { + return &SysOperLogDao{ + group: "default", + table: "sys_oper_log", + columns: sysOperLogColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysOperLogDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysOperLogDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysOperLogDao) Columns() SysOperLogColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysOperLogDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysOperLogDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysOperLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_oss.go b/server/internal/dao/internal/sys_oss.go new file mode 100644 index 0000000..c0ce038 --- /dev/null +++ b/server/internal/dao/internal/sys_oss.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysOssDao is the data access object for the table sys_oss. +type SysOssDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysOssColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysOssColumns defines and stores column names for the table sys_oss. +type SysOssColumns struct { + OssId string // 对象存储主键 + TenantId string // 租户编号 + FileName string // 文件名 + OriginalName string // 原名 + FileSuffix string // 文件后缀名 + Path string // 存储路径 + Url string // URL地址 + CreatedDept string // 创建部门 + CreatedAt string // 创建时间 + CreatedBy string // 创建者 + UpdatedAt string // 更新时间 + UpdatedBy string // 更新者 + Service string // 服务商 + Md5 string // 文件MD5 + FileSize string // 文件大小 + FileCrc16 string // 文件Crc16 + FileSum string // 文件校验和 +} + +// sysOssColumns holds the columns for the table sys_oss. +var sysOssColumns = SysOssColumns{ + OssId: "oss_id", + TenantId: "tenant_id", + FileName: "file_name", + OriginalName: "original_name", + FileSuffix: "file_suffix", + Path: "path", + Url: "url", + CreatedDept: "created_dept", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", + Service: "service", + Md5: "md5", + FileSize: "file_size", + FileCrc16: "file_crc16", + FileSum: "file_sum", +} + +// NewSysOssDao creates and returns a new DAO object for table data access. +func NewSysOssDao(handlers ...gdb.ModelHandler) *SysOssDao { + return &SysOssDao{ + group: "default", + table: "sys_oss", + columns: sysOssColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysOssDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysOssDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysOssDao) Columns() SysOssColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysOssDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysOssDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysOssDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_oss_config.go b/server/internal/dao/internal/sys_oss_config.go new file mode 100644 index 0000000..f373526 --- /dev/null +++ b/server/internal/dao/internal/sys_oss_config.go @@ -0,0 +1,117 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysOssConfigDao is the data access object for the table sys_oss_config. +type SysOssConfigDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysOssConfigColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysOssConfigColumns defines and stores column names for the table sys_oss_config. +type SysOssConfigColumns struct { + OssConfigId string // 主键 + TenantId string // 租户编号 + ConfigKey string // 配置key + AccessKey string // accessKey + SecretKey string // 秘钥 + BucketName string // 桶名称 + Prefix string // 前缀 + Endpoint string // 访问站点 + Domain string // 自定义域名 + IsHttps string // 是否https(Y=是,N=否) + Region string // 域 + AccessPolicy string // 桶权限类型(0=private 1=public 2=custom) + Status string // 是否默认(0=是,1=否) + Ext1 string // 扩展字段 + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + Remark string // 备注 +} + +// sysOssConfigColumns holds the columns for the table sys_oss_config. +var sysOssConfigColumns = SysOssConfigColumns{ + OssConfigId: "oss_config_id", + TenantId: "tenant_id", + ConfigKey: "config_key", + AccessKey: "access_key", + SecretKey: "secret_key", + BucketName: "bucket_name", + Prefix: "prefix", + Endpoint: "endpoint", + Domain: "domain", + IsHttps: "is_https", + Region: "region", + AccessPolicy: "access_policy", + Status: "status", + Ext1: "ext1", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + Remark: "remark", +} + +// NewSysOssConfigDao creates and returns a new DAO object for table data access. +func NewSysOssConfigDao(handlers ...gdb.ModelHandler) *SysOssConfigDao { + return &SysOssConfigDao{ + group: "default", + table: "sys_oss_config", + columns: sysOssConfigColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysOssConfigDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysOssConfigDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysOssConfigDao) Columns() SysOssConfigColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysOssConfigDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysOssConfigDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysOssConfigDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_post.go b/server/internal/dao/internal/sys_post.go new file mode 100644 index 0000000..ee6f95e --- /dev/null +++ b/server/internal/dao/internal/sys_post.go @@ -0,0 +1,109 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysPostDao is the data access object for the table sys_post. +type SysPostDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysPostColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysPostColumns defines and stores column names for the table sys_post. +type SysPostColumns struct { + PostId string // 岗位ID + TenantId string // 租户编号 + DeptId string // 部门id + PostCode string // 岗位编码 + PostCategory string // 岗位类别编码 + PostName string // 岗位名称 + PostSort string // 显示顺序 + Status string // 状态(0正常 1停用) + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 + Remark string // 备注 +} + +// sysPostColumns holds the columns for the table sys_post. +var sysPostColumns = SysPostColumns{ + PostId: "post_id", + TenantId: "tenant_id", + DeptId: "dept_id", + PostCode: "post_code", + PostCategory: "post_category", + PostName: "post_name", + PostSort: "post_sort", + Status: "status", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", + Remark: "remark", +} + +// NewSysPostDao creates and returns a new DAO object for table data access. +func NewSysPostDao(handlers ...gdb.ModelHandler) *SysPostDao { + return &SysPostDao{ + group: "default", + table: "sys_post", + columns: sysPostColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysPostDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysPostDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysPostDao) Columns() SysPostColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysPostDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysPostDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysPostDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_role.go b/server/internal/dao/internal/sys_role.go new file mode 100644 index 0000000..9a888e0 --- /dev/null +++ b/server/internal/dao/internal/sys_role.go @@ -0,0 +1,115 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysRoleDao is the data access object for the table sys_role. +type SysRoleDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysRoleColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysRoleColumns defines and stores column names for the table sys_role. +type SysRoleColumns struct { + RoleId string // 角色ID + TenantId string // 租户编号 + DeptId string // 部门机构id + RoleName string // 角色名称 + RoleKey string // 角色权限字符串 + RoleSort string // 显示顺序 + DataScope string // 数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限 5:仅本人数据权限 6:仅本人数据权限 7:本组织及下一级数据权限 + MenuCheckStrictly string // 菜单树选择项是否关联显示 + DeptCheckStrictly string // 部门树选择项是否关联显示 + Status string // 角色状态(0正常 1停用) + IsPass string // 是否传递数据权限(0:传递 1:不传递) + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 + Remark string // 备注 +} + +// sysRoleColumns holds the columns for the table sys_role. +var sysRoleColumns = SysRoleColumns{ + RoleId: "role_id", + TenantId: "tenant_id", + DeptId: "dept_id", + RoleName: "role_name", + RoleKey: "role_key", + RoleSort: "role_sort", + DataScope: "data_scope", + MenuCheckStrictly: "menu_check_strictly", + DeptCheckStrictly: "dept_check_strictly", + Status: "status", + IsPass: "is_pass", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", + Remark: "remark", +} + +// NewSysRoleDao creates and returns a new DAO object for table data access. +func NewSysRoleDao(handlers ...gdb.ModelHandler) *SysRoleDao { + return &SysRoleDao{ + group: "default", + table: "sys_role", + columns: sysRoleColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysRoleDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysRoleDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysRoleDao) Columns() SysRoleColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysRoleDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysRoleDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysRoleDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_role_dept.go b/server/internal/dao/internal/sys_role_dept.go new file mode 100644 index 0000000..8498277 --- /dev/null +++ b/server/internal/dao/internal/sys_role_dept.go @@ -0,0 +1,83 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysRoleDeptDao is the data access object for the table sys_role_dept. +type SysRoleDeptDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysRoleDeptColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysRoleDeptColumns defines and stores column names for the table sys_role_dept. +type SysRoleDeptColumns struct { + TenantId string // 租户编号 + RoleId string // 角色ID + DeptId string // 部门ID +} + +// sysRoleDeptColumns holds the columns for the table sys_role_dept. +var sysRoleDeptColumns = SysRoleDeptColumns{ + TenantId: "tenant_id", + RoleId: "role_id", + DeptId: "dept_id", +} + +// NewSysRoleDeptDao creates and returns a new DAO object for table data access. +func NewSysRoleDeptDao(handlers ...gdb.ModelHandler) *SysRoleDeptDao { + return &SysRoleDeptDao{ + group: "default", + table: "sys_role_dept", + columns: sysRoleDeptColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysRoleDeptDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysRoleDeptDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysRoleDeptDao) Columns() SysRoleDeptColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysRoleDeptDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysRoleDeptDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysRoleDeptDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_role_menu.go b/server/internal/dao/internal/sys_role_menu.go new file mode 100644 index 0000000..1c99abd --- /dev/null +++ b/server/internal/dao/internal/sys_role_menu.go @@ -0,0 +1,85 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysRoleMenuDao is the data access object for the table sys_role_menu. +type SysRoleMenuDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysRoleMenuColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysRoleMenuColumns defines and stores column names for the table sys_role_menu. +type SysRoleMenuColumns struct { + TenantId string // 租户编号 + RoleId string // 角色ID + MenuId string // 菜单ID + DataScope string // 数据范围(0按角色数据权限 1全部数据权限 3本部门数据权限 4本部门及以下数据权限 5仅本人数据权限 6本部门及以下或本人数据权限 7本组织及本组织下一级数据权限 8本组织下一级数据权限) +} + +// sysRoleMenuColumns holds the columns for the table sys_role_menu. +var sysRoleMenuColumns = SysRoleMenuColumns{ + TenantId: "tenant_id", + RoleId: "role_id", + MenuId: "menu_id", + DataScope: "data_scope", +} + +// NewSysRoleMenuDao creates and returns a new DAO object for table data access. +func NewSysRoleMenuDao(handlers ...gdb.ModelHandler) *SysRoleMenuDao { + return &SysRoleMenuDao{ + group: "default", + table: "sys_role_menu", + columns: sysRoleMenuColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysRoleMenuDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysRoleMenuDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysRoleMenuDao) Columns() SysRoleMenuColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysRoleMenuDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysRoleMenuDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysRoleMenuDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_social.go b/server/internal/dao/internal/sys_social.go new file mode 100644 index 0000000..79dc0b3 --- /dev/null +++ b/server/internal/dao/internal/sys_social.go @@ -0,0 +1,137 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysSocialDao is the data access object for the table sys_social. +type SysSocialDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysSocialColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysSocialColumns defines and stores column names for the table sys_social. +type SysSocialColumns struct { + Id string // 主键 + UserId string // 用户ID + TenantId string // 租户id + AuthId string // 平台+平台唯一id + Source string // 用户来源 + OpenId string // 平台编号唯一id + UserName string // 登录账号 + NickName string // 用户昵称 + Email string // 用户邮箱 + Avatar string // 头像地址 + AccessToken string // 用户的授权令牌 + ExpireIn string // 用户的授权令牌的有效期,部分平台可能没有 + RefreshToken string // 刷新令牌,部分平台可能没有 + AccessCode string // 平台的授权信息,部分平台可能没有 + UnionId string // 用户的 unionid + Scope string // 授予的权限,部分平台可能没有 + TokenType string // 个别平台的授权信息,部分平台可能没有 + IdToken string // id token,部分平台可能没有 + MacAlgorithm string // 小米平台用户的附带属性,部分平台可能没有 + MacKey string // 小米平台用户的附带属性,部分平台可能没有 + Code string // 用户的授权code,部分平台可能没有 + OauthToken string // Twitter平台用户的附带属性,部分平台可能没有 + OauthTokenSecret string // Twitter平台用户的附带属性,部分平台可能没有 + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 +} + +// sysSocialColumns holds the columns for the table sys_social. +var sysSocialColumns = SysSocialColumns{ + Id: "id", + UserId: "user_id", + TenantId: "tenant_id", + AuthId: "auth_id", + Source: "source", + OpenId: "open_id", + UserName: "user_name", + NickName: "nick_name", + Email: "email", + Avatar: "avatar", + AccessToken: "access_token", + ExpireIn: "expire_in", + RefreshToken: "refresh_token", + AccessCode: "access_code", + UnionId: "union_id", + Scope: "scope", + TokenType: "token_type", + IdToken: "id_token", + MacAlgorithm: "mac_algorithm", + MacKey: "mac_key", + Code: "code", + OauthToken: "oauth_token", + OauthTokenSecret: "oauth_token_secret", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", +} + +// NewSysSocialDao creates and returns a new DAO object for table data access. +func NewSysSocialDao(handlers ...gdb.ModelHandler) *SysSocialDao { + return &SysSocialDao{ + group: "default", + table: "sys_social", + columns: sysSocialColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysSocialDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysSocialDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysSocialDao) Columns() SysSocialColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysSocialDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysSocialDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysSocialDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_tenant.go b/server/internal/dao/internal/sys_tenant.go new file mode 100644 index 0000000..f897116 --- /dev/null +++ b/server/internal/dao/internal/sys_tenant.go @@ -0,0 +1,125 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysTenantDao is the data access object for the table sys_tenant. +type SysTenantDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysTenantColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysTenantColumns defines and stores column names for the table sys_tenant. +type SysTenantColumns struct { + Id string // id + TenantId string // 租户编号 + ContactUserName string // 联系人 + ContactPhone string // 联系电话 + CompanyName string // 企业名称 + LicenseNumber string // 统一社会信用代码 + Address string // 地址 + Intro string // 企业简介 + Domain string // 域名 + Remark string // 备注 + PackageId string // 租户套餐编号 + AdminRoleId string // 管理员角色ID + AdminDeptId string // 管理员部门ID + AdminUserId string // 管理员用户ID + ExpireTime string // 过期时间 + AccountCount string // 用户数量(-1不限制) + Status string // 租户状态(0正常 1停用) + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 +} + +// sysTenantColumns holds the columns for the table sys_tenant. +var sysTenantColumns = SysTenantColumns{ + Id: "id", + TenantId: "tenant_id", + ContactUserName: "contact_user_name", + ContactPhone: "contact_phone", + CompanyName: "company_name", + LicenseNumber: "license_number", + Address: "address", + Intro: "intro", + Domain: "domain", + Remark: "remark", + PackageId: "package_id", + AdminRoleId: "admin_role_id", + AdminDeptId: "admin_dept_id", + AdminUserId: "admin_user_id", + ExpireTime: "expire_time", + AccountCount: "account_count", + Status: "status", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", +} + +// NewSysTenantDao creates and returns a new DAO object for table data access. +func NewSysTenantDao(handlers ...gdb.ModelHandler) *SysTenantDao { + return &SysTenantDao{ + group: "default", + table: "sys_tenant", + columns: sysTenantColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysTenantDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysTenantDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysTenantDao) Columns() SysTenantColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysTenantDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysTenantDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysTenantDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_tenant_package.go b/server/internal/dao/internal/sys_tenant_package.go new file mode 100644 index 0000000..f2a3720 --- /dev/null +++ b/server/internal/dao/internal/sys_tenant_package.go @@ -0,0 +1,103 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysTenantPackageDao is the data access object for the table sys_tenant_package. +type SysTenantPackageDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysTenantPackageColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysTenantPackageColumns defines and stores column names for the table sys_tenant_package. +type SysTenantPackageColumns struct { + PackageId string // 租户套餐id + PackageName string // 套餐名称 + MenuIds string // 关联菜单id + Remark string // 备注 + MenuCheckStrictly string // 菜单树选择项是否关联显示 + Status string // 状态(0正常 1停用) + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 +} + +// sysTenantPackageColumns holds the columns for the table sys_tenant_package. +var sysTenantPackageColumns = SysTenantPackageColumns{ + PackageId: "package_id", + PackageName: "package_name", + MenuIds: "menu_ids", + Remark: "remark", + MenuCheckStrictly: "menu_check_strictly", + Status: "status", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", +} + +// NewSysTenantPackageDao creates and returns a new DAO object for table data access. +func NewSysTenantPackageDao(handlers ...gdb.ModelHandler) *SysTenantPackageDao { + return &SysTenantPackageDao{ + group: "default", + table: "sys_tenant_package", + columns: sysTenantPackageColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysTenantPackageDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysTenantPackageDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysTenantPackageDao) Columns() SysTenantPackageColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysTenantPackageDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysTenantPackageDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysTenantPackageDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_user.go b/server/internal/dao/internal/sys_user.go new file mode 100644 index 0000000..cd5d855 --- /dev/null +++ b/server/internal/dao/internal/sys_user.go @@ -0,0 +1,125 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysUserDao is the data access object for the table sys_user. +type SysUserDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysUserColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysUserColumns defines and stores column names for the table sys_user. +type SysUserColumns struct { + UserId string // 用户ID + TenantId string // 租户编号 + DeptId string // 部门ID + UserName string // 用户账号 + NickName string // 用户昵称 + UserType string // 用户类型(sys_user系统用户) + Email string // 用户邮箱 + Phonenumber string // 手机号码 + Sex string // 用户性别(0男 1女 2未知) + Avatar string // 头像地址 + Salt string // 加密盐 + Password string // 密码 + Status string // 帐号状态(0正常 1停用) + LoginIp string // 最后登录IP + LoginDate string // 最后登录时间 + MfaEnabled string // 是否开启邮箱MFA 0否 1是 + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 + Remark string // 备注 +} + +// sysUserColumns holds the columns for the table sys_user. +var sysUserColumns = SysUserColumns{ + UserId: "user_id", + TenantId: "tenant_id", + DeptId: "dept_id", + UserName: "user_name", + NickName: "nick_name", + UserType: "user_type", + Email: "email", + Phonenumber: "phonenumber", + Sex: "sex", + Avatar: "avatar", + Salt: "salt", + Password: "password", + Status: "status", + LoginIp: "login_ip", + LoginDate: "login_date", + MfaEnabled: "mfa_enabled", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", + Remark: "remark", +} + +// NewSysUserDao creates and returns a new DAO object for table data access. +func NewSysUserDao(handlers ...gdb.ModelHandler) *SysUserDao { + return &SysUserDao{ + group: "default", + table: "sys_user", + columns: sysUserColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysUserDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysUserDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysUserDao) Columns() SysUserColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysUserDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysUserDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysUserDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_user_online.go b/server/internal/dao/internal/sys_user_online.go new file mode 100644 index 0000000..034b2cf --- /dev/null +++ b/server/internal/dao/internal/sys_user_online.go @@ -0,0 +1,105 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysUserOnlineDao is the data access object for the table sys_user_online. +type SysUserOnlineDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysUserOnlineColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysUserOnlineColumns defines and stores column names for the table sys_user_online. +type SysUserOnlineColumns struct { + OnlineId string // 访问ID + TenantId string // 租户编号 + Uuid string // UUID + UserName string // 用户账号 + ClientKey string // 客户端 + DeviceType string // 设备类型 + Ipaddr string // 登录IP地址 + LoginLocation string // 登录地点 + Browser string // 浏览器类型 + Os string // 操作系统 + Token string // Token + LoginTime string // 访问时间 + ExpireTime string // 过期时间 + DeletedAt string // 删除时间 +} + +// sysUserOnlineColumns holds the columns for the table sys_user_online. +var sysUserOnlineColumns = SysUserOnlineColumns{ + OnlineId: "online_id", + TenantId: "tenant_id", + Uuid: "uuid", + UserName: "user_name", + ClientKey: "client_key", + DeviceType: "device_type", + Ipaddr: "ipaddr", + LoginLocation: "login_location", + Browser: "browser", + Os: "os", + Token: "token", + LoginTime: "login_time", + ExpireTime: "expire_time", + DeletedAt: "deleted_at", +} + +// NewSysUserOnlineDao creates and returns a new DAO object for table data access. +func NewSysUserOnlineDao(handlers ...gdb.ModelHandler) *SysUserOnlineDao { + return &SysUserOnlineDao{ + group: "default", + table: "sys_user_online", + columns: sysUserOnlineColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysUserOnlineDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysUserOnlineDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysUserOnlineDao) Columns() SysUserOnlineColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysUserOnlineDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysUserOnlineDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysUserOnlineDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_user_post.go b/server/internal/dao/internal/sys_user_post.go new file mode 100644 index 0000000..3a182ca --- /dev/null +++ b/server/internal/dao/internal/sys_user_post.go @@ -0,0 +1,83 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysUserPostDao is the data access object for the table sys_user_post. +type SysUserPostDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysUserPostColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysUserPostColumns defines and stores column names for the table sys_user_post. +type SysUserPostColumns struct { + TenantId string // 租户编号 + UserId string // 用户ID + PostId string // 岗位ID +} + +// sysUserPostColumns holds the columns for the table sys_user_post. +var sysUserPostColumns = SysUserPostColumns{ + TenantId: "tenant_id", + UserId: "user_id", + PostId: "post_id", +} + +// NewSysUserPostDao creates and returns a new DAO object for table data access. +func NewSysUserPostDao(handlers ...gdb.ModelHandler) *SysUserPostDao { + return &SysUserPostDao{ + group: "default", + table: "sys_user_post", + columns: sysUserPostColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysUserPostDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysUserPostDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysUserPostDao) Columns() SysUserPostColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysUserPostDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysUserPostDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysUserPostDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/sys_user_role.go b/server/internal/dao/internal/sys_user_role.go new file mode 100644 index 0000000..9646157 --- /dev/null +++ b/server/internal/dao/internal/sys_user_role.go @@ -0,0 +1,83 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysUserRoleDao is the data access object for the table sys_user_role. +type SysUserRoleDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysUserRoleColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysUserRoleColumns defines and stores column names for the table sys_user_role. +type SysUserRoleColumns struct { + TenantId string // 租户编号 + UserId string // 用户ID + RoleId string // 角色ID +} + +// sysUserRoleColumns holds the columns for the table sys_user_role. +var sysUserRoleColumns = SysUserRoleColumns{ + TenantId: "tenant_id", + UserId: "user_id", + RoleId: "role_id", +} + +// NewSysUserRoleDao creates and returns a new DAO object for table data access. +func NewSysUserRoleDao(handlers ...gdb.ModelHandler) *SysUserRoleDao { + return &SysUserRoleDao{ + group: "default", + table: "sys_user_role", + columns: sysUserRoleColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysUserRoleDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysUserRoleDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysUserRoleDao) Columns() SysUserRoleColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysUserRoleDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysUserRoleDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysUserRoleDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_agent_balance.go b/server/internal/dao/internal/t_agent_balance.go new file mode 100644 index 0000000..ae75320 --- /dev/null +++ b/server/internal/dao/internal/t_agent_balance.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TAgentBalanceDao is the data access object for the table t_agent_balance. +type TAgentBalanceDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TAgentBalanceColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TAgentBalanceColumns defines and stores column names for the table t_agent_balance. +type TAgentBalanceColumns struct { + Id string // 账户ID + DeptId string // 机构ID + Balance string // 账户余额 + WithdrawAmount string // 可提现金额 + WithdrawAmountStatus string // 可提现金额状态: 0-正常,1-冻结 + InvoiceAmount string // 已开票金额 + CanInvoiceAmount string // 可开票金额 + CreditLimit string // 信用额度 + UsedCredit string // 已使用信用额度 + PendingRepayment string // 待还款金额 + Status string // 状态: 0-正常, 1-禁用 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tAgentBalanceColumns holds the columns for the table t_agent_balance. +var tAgentBalanceColumns = TAgentBalanceColumns{ + Id: "id", + DeptId: "dept_id", + Balance: "balance", + WithdrawAmount: "withdraw_amount", + WithdrawAmountStatus: "withdraw_amount_status", + InvoiceAmount: "invoice_amount", + CanInvoiceAmount: "can_invoice_amount", + CreditLimit: "credit_limit", + UsedCredit: "used_credit", + PendingRepayment: "pending_repayment", + Status: "status", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTAgentBalanceDao creates and returns a new DAO object for table data access. +func NewTAgentBalanceDao(handlers ...gdb.ModelHandler) *TAgentBalanceDao { + return &TAgentBalanceDao{ + group: "default", + table: "t_agent_balance", + columns: tAgentBalanceColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TAgentBalanceDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TAgentBalanceDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TAgentBalanceDao) Columns() TAgentBalanceColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TAgentBalanceDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TAgentBalanceDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TAgentBalanceDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_agent_enterprise.go b/server/internal/dao/internal/t_agent_enterprise.go new file mode 100644 index 0000000..026daa9 --- /dev/null +++ b/server/internal/dao/internal/t_agent_enterprise.go @@ -0,0 +1,109 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TAgentEnterpriseDao is the data access object for the table t_agent_enterprise. +type TAgentEnterpriseDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TAgentEnterpriseColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TAgentEnterpriseColumns defines and stores column names for the table t_agent_enterprise. +type TAgentEnterpriseColumns struct { + Id string // 主键ID + DeptId string // 部门机构id + FullName string // 单位全称 + Industry string // 行业 + LegalPersonName string // 法人名称 + LegalPersonIdCard string // 法人身份证号 + CompanyType string // 单位类型 + EstablishTime string // 单位成立时间 + BankName string // 开户行名称 + BankAccount string // 开户行账号 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tAgentEnterpriseColumns holds the columns for the table t_agent_enterprise. +var tAgentEnterpriseColumns = TAgentEnterpriseColumns{ + Id: "id", + DeptId: "dept_id", + FullName: "full_name", + Industry: "industry", + LegalPersonName: "legal_person_name", + LegalPersonIdCard: "legal_person_id_card", + CompanyType: "company_type", + EstablishTime: "establish_time", + BankName: "bank_name", + BankAccount: "bank_account", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTAgentEnterpriseDao creates and returns a new DAO object for table data access. +func NewTAgentEnterpriseDao(handlers ...gdb.ModelHandler) *TAgentEnterpriseDao { + return &TAgentEnterpriseDao{ + group: "default", + table: "t_agent_enterprise", + columns: tAgentEnterpriseColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TAgentEnterpriseDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TAgentEnterpriseDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TAgentEnterpriseDao) Columns() TAgentEnterpriseColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TAgentEnterpriseDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TAgentEnterpriseDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TAgentEnterpriseDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_api.go b/server/internal/dao/internal/t_api.go new file mode 100644 index 0000000..fe43d9e --- /dev/null +++ b/server/internal/dao/internal/t_api.go @@ -0,0 +1,105 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TApiDao is the data access object for the table t_api. +type TApiDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TApiColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TApiColumns defines and stores column names for the table t_api. +type TApiColumns struct { + Id string // ID + AppKey string // APPKey + AppSecret string // APPSecret + RateLimitPeriod string // 限流周期(秒,0:不限流) + RateLimitCount string // 限流次数 + DeptId string // 所属机构 + GroupId string // 开放接口权限组ID + Status string // 状态:0启用,1停用 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tApiColumns holds the columns for the table t_api. +var tApiColumns = TApiColumns{ + Id: "id", + AppKey: "app_key", + AppSecret: "app_secret", + RateLimitPeriod: "rate_limit_period", + RateLimitCount: "rate_limit_count", + DeptId: "dept_id", + GroupId: "group_id", + Status: "status", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTApiDao creates and returns a new DAO object for table data access. +func NewTApiDao(handlers ...gdb.ModelHandler) *TApiDao { + return &TApiDao{ + group: "default", + table: "t_api", + columns: tApiColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TApiDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TApiDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TApiDao) Columns() TApiColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TApiDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TApiDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TApiDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_api_group.go b/server/internal/dao/internal/t_api_group.go new file mode 100644 index 0000000..d2fb7ad --- /dev/null +++ b/server/internal/dao/internal/t_api_group.go @@ -0,0 +1,99 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TApiGroupDao is the data access object for the table t_api_group. +type TApiGroupDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TApiGroupColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TApiGroupColumns defines and stores column names for the table t_api_group. +type TApiGroupColumns struct { + Id string // 主键ID + GroupName string // 权限组名称 + GroupCode string // 权限组编码 + Status string // 状态:0启用,1停用 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tApiGroupColumns holds the columns for the table t_api_group. +var tApiGroupColumns = TApiGroupColumns{ + Id: "id", + GroupName: "group_name", + GroupCode: "group_code", + Status: "status", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTApiGroupDao creates and returns a new DAO object for table data access. +func NewTApiGroupDao(handlers ...gdb.ModelHandler) *TApiGroupDao { + return &TApiGroupDao{ + group: "default", + table: "t_api_group", + columns: tApiGroupColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TApiGroupDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TApiGroupDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TApiGroupDao) Columns() TApiGroupColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TApiGroupDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TApiGroupDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TApiGroupDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_api_group_resource.go b/server/internal/dao/internal/t_api_group_resource.go new file mode 100644 index 0000000..7548f3d --- /dev/null +++ b/server/internal/dao/internal/t_api_group_resource.go @@ -0,0 +1,81 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TApiGroupResourceDao is the data access object for the table t_api_group_resource. +type TApiGroupResourceDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TApiGroupResourceColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TApiGroupResourceColumns defines and stores column names for the table t_api_group_resource. +type TApiGroupResourceColumns struct { + GroupId string // 权限组ID,对应t_api_group.id + ApiId string // 开放接口资源ID,对应t_api_resource.id +} + +// tApiGroupResourceColumns holds the columns for the table t_api_group_resource. +var tApiGroupResourceColumns = TApiGroupResourceColumns{ + GroupId: "group_id", + ApiId: "api_id", +} + +// NewTApiGroupResourceDao creates and returns a new DAO object for table data access. +func NewTApiGroupResourceDao(handlers ...gdb.ModelHandler) *TApiGroupResourceDao { + return &TApiGroupResourceDao{ + group: "default", + table: "t_api_group_resource", + columns: tApiGroupResourceColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TApiGroupResourceDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TApiGroupResourceDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TApiGroupResourceDao) Columns() TApiGroupResourceColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TApiGroupResourceDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TApiGroupResourceDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TApiGroupResourceDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_api_resource.go b/server/internal/dao/internal/t_api_resource.go new file mode 100644 index 0000000..ae8a2a2 --- /dev/null +++ b/server/internal/dao/internal/t_api_resource.go @@ -0,0 +1,105 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TApiResourceDao is the data access object for the table t_api_resource. +type TApiResourceDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TApiResourceColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TApiResourceColumns defines and stores column names for the table t_api_resource. +type TApiResourceColumns struct { + Id string // 主键ID + ApiName string // 接口名称 + ApiCode string // 接口权限标识 + Method string // 请求方法:1:POST,2:PUT,3:DELETE,4:GET + Path string // 接口路径 + Status string // 状态:0启用,1停用 + Version string // 接口版本 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tApiResourceColumns holds the columns for the table t_api_resource. +var tApiResourceColumns = TApiResourceColumns{ + Id: "id", + ApiName: "api_name", + ApiCode: "api_code", + Method: "method", + Path: "path", + Status: "status", + Version: "version", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTApiResourceDao creates and returns a new DAO object for table data access. +func NewTApiResourceDao(handlers ...gdb.ModelHandler) *TApiResourceDao { + return &TApiResourceDao{ + group: "default", + table: "t_api_resource", + columns: tApiResourceColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TApiResourceDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TApiResourceDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TApiResourceDao) Columns() TApiResourceColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TApiResourceDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TApiResourceDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TApiResourceDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_balance_detail.go b/server/internal/dao/internal/t_balance_detail.go new file mode 100644 index 0000000..5161691 --- /dev/null +++ b/server/internal/dao/internal/t_balance_detail.go @@ -0,0 +1,117 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TBalanceDetailDao is the data access object for the table t_balance_detail. +type TBalanceDetailDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TBalanceDetailColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TBalanceDetailColumns defines and stores column names for the table t_balance_detail. +type TBalanceDetailColumns struct { + Id string // 明细ID + DeptId string // 账户ID + SerialNo string // 流水号 + TransactionTime string // 交易时间 + IncomeExpenseType string // 收支类型: 0收入,1支出 + TransactionType string // 交易类型:0充值 1消费 2转可提现 3可提现转入 4人工调整 + TransactionChannel string // 交易渠道:1支付宝 2微信 3可提现金额 + TransactionStatus string // 交易状态:0待支付、1已支付、2已过期、3交易失败 + Amount string // 交易金额 + BeforeBalance string // 变动前余额 + BalanceAfter string // 变动后余额 + ChannelSerialNo string // 交易渠道流水号 + OrderSn string // 关联订单号 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tBalanceDetailColumns holds the columns for the table t_balance_detail. +var tBalanceDetailColumns = TBalanceDetailColumns{ + Id: "id", + DeptId: "dept_id", + SerialNo: "serial_no", + TransactionTime: "transaction_time", + IncomeExpenseType: "income_expense_type", + TransactionType: "transaction_type", + TransactionChannel: "transaction_channel", + TransactionStatus: "transaction_status", + Amount: "amount", + BeforeBalance: "before_balance", + BalanceAfter: "balance_after", + ChannelSerialNo: "channel_serial_no", + OrderSn: "order_sn", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTBalanceDetailDao creates and returns a new DAO object for table data access. +func NewTBalanceDetailDao(handlers ...gdb.ModelHandler) *TBalanceDetailDao { + return &TBalanceDetailDao{ + group: "default", + table: "t_balance_detail", + columns: tBalanceDetailColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TBalanceDetailDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TBalanceDetailDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TBalanceDetailDao) Columns() TBalanceDetailColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TBalanceDetailDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TBalanceDetailDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TBalanceDetailDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_brand.go b/server/internal/dao/internal/t_brand.go new file mode 100644 index 0000000..42cd5a7 --- /dev/null +++ b/server/internal/dao/internal/t_brand.go @@ -0,0 +1,113 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TBrandDao is the data access object for the table t_brand. +type TBrandDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TBrandColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TBrandColumns defines and stores column names for the table t_brand. +type TBrandColumns struct { + BrandId string // 自增id号 + BrandName string // 品牌名称 + BrandLogo string // 上传的该公司Logo图片 + BrandDesc string // 品牌描述 + SiteUrl string // 品牌的网址 + SortOrder string // 品牌在前台页面的显示顺序,数字越大越靠后 + IsShow string // 该品牌是否显示 枚举 + BrandType string // 品牌分类 + BrandIsHot string // 是否热销品牌 枚举:yesno_enum + FirstWord string // 首字母 + BrandEnName string // 品牌英文名 + Status string // 状态 枚举sys_status + TenantId string // + CreatedDept string // + CreatedBy string // + CreatedAt string // + UpdatedBy string // + UpdatedAt string // +} + +// tBrandColumns holds the columns for the table t_brand. +var tBrandColumns = TBrandColumns{ + BrandId: "brand_id", + BrandName: "brand_name", + BrandLogo: "brand_logo", + BrandDesc: "brand_desc", + SiteUrl: "site_url", + SortOrder: "sort_order", + IsShow: "is_show", + BrandType: "brand_type", + BrandIsHot: "brand_is_hot", + FirstWord: "first_word", + BrandEnName: "brand_en_name", + Status: "status", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTBrandDao creates and returns a new DAO object for table data access. +func NewTBrandDao(handlers ...gdb.ModelHandler) *TBrandDao { + return &TBrandDao{ + group: "default", + table: "t_brand", + columns: tBrandColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TBrandDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TBrandDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TBrandDao) Columns() TBrandColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TBrandDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TBrandDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TBrandDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_category.go b/server/internal/dao/internal/t_category.go new file mode 100644 index 0000000..ba3f21b --- /dev/null +++ b/server/internal/dao/internal/t_category.go @@ -0,0 +1,119 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TCategoryDao is the data access object for the table t_category. +type TCategoryDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TCategoryColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TCategoryColumns defines and stores column names for the table t_category. +type TCategoryColumns struct { + CategoryId string // 分类ID + CategoryName string // 分类名称 + ShortName string // 分类短名 + Level string // 层级深度:1-一级 2-二级 3-三级... + Keywords string // 分类关键字(搜索用) + SearchKeywords string // 搜索关键字 + CategoryDesc string // 分类描述 + ParentId string // 父分类ID(0=顶级分类) + SortOrder string // 排序序号(数字越大越靠后) + MeasureUnit string // 计量单位 + IsShow string // 是否前台显示 枚举 + IsHot string // 是否热门 枚举:yesno_enum + SeoTitle string // SEO标题 + CategoryPic string // 分类图片 + CategoryIco string // 分类图标 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// tCategoryColumns holds the columns for the table t_category. +var tCategoryColumns = TCategoryColumns{ + CategoryId: "category_id", + CategoryName: "category_name", + ShortName: "short_name", + Level: "level", + Keywords: "keywords", + SearchKeywords: "search_keywords", + CategoryDesc: "category_desc", + ParentId: "parent_id", + SortOrder: "sort_order", + MeasureUnit: "measure_unit", + IsShow: "is_show", + IsHot: "is_hot", + SeoTitle: "seo_title", + CategoryPic: "category_pic", + CategoryIco: "category_ico", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTCategoryDao creates and returns a new DAO object for table data access. +func NewTCategoryDao(handlers ...gdb.ModelHandler) *TCategoryDao { + return &TCategoryDao{ + group: "default", + table: "t_category", + columns: tCategoryColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TCategoryDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TCategoryDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TCategoryDao) Columns() TCategoryColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TCategoryDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TCategoryDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TCategoryDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_consume_account_month_bill.go b/server/internal/dao/internal/t_consume_account_month_bill.go new file mode 100644 index 0000000..b4821e8 --- /dev/null +++ b/server/internal/dao/internal/t_consume_account_month_bill.go @@ -0,0 +1,133 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TConsumeAccountMonthBillDao is the data access object for the table t_consume_account_month_bill. +type TConsumeAccountMonthBillDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TConsumeAccountMonthBillColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TConsumeAccountMonthBillColumns defines and stores column names for the table t_consume_account_month_bill. +type TConsumeAccountMonthBillColumns struct { + Id string // 账单ID + DeptId string // 机构ID + AccountPeriod string // 账期YYYY-MM + OpeningBalance string // 月初余额 + RechargeIncome string // 充值收入 + WithdrawableTransferInIncome string // 可提现转入收入 + ManualAdjustIncome string // 人工调账收入 + RefundIncome string // 退款收入 + ConsumeExpense string // 平台消费支出 + BalanceTransferOutExpense string // 转可提现支出 + ManualAdjustExpense string // 人工调账支出 + ConsumeCreditRepayExpense string // 消费账户还款支出 + TotalIncome string // 本月收入总额 + TotalExpense string // 本月支出总额 + ClosingBalance string // 月末余额(月初余额+本月收入-本月支出) + IncomeCount string // 收入笔数 + ExpenseCount string // 支出笔数 + CreditUsed string // 本月使用信用额度 + CreditRepaid string // 本月还款信用额度 + BillStatus string // 账单状态:1正常,2已调整,3已作废 + SettleTime string // 结算时间 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tConsumeAccountMonthBillColumns holds the columns for the table t_consume_account_month_bill. +var tConsumeAccountMonthBillColumns = TConsumeAccountMonthBillColumns{ + Id: "id", + DeptId: "dept_id", + AccountPeriod: "account_period", + OpeningBalance: "opening_balance", + RechargeIncome: "recharge_income", + WithdrawableTransferInIncome: "withdrawable_transfer_in_income", + ManualAdjustIncome: "manual_adjust_income", + RefundIncome: "refund_income", + ConsumeExpense: "consume_expense", + BalanceTransferOutExpense: "balance_transfer_out_expense", + ManualAdjustExpense: "manual_adjust_expense", + ConsumeCreditRepayExpense: "consume_credit_repay_expense", + TotalIncome: "total_income", + TotalExpense: "total_expense", + ClosingBalance: "closing_balance", + IncomeCount: "income_count", + ExpenseCount: "expense_count", + CreditUsed: "credit_used", + CreditRepaid: "credit_repaid", + BillStatus: "bill_status", + SettleTime: "settle_time", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTConsumeAccountMonthBillDao creates and returns a new DAO object for table data access. +func NewTConsumeAccountMonthBillDao(handlers ...gdb.ModelHandler) *TConsumeAccountMonthBillDao { + return &TConsumeAccountMonthBillDao{ + group: "default", + table: "t_consume_account_month_bill", + columns: tConsumeAccountMonthBillColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TConsumeAccountMonthBillDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TConsumeAccountMonthBillDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TConsumeAccountMonthBillDao) Columns() TConsumeAccountMonthBillColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TConsumeAccountMonthBillDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TConsumeAccountMonthBillDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TConsumeAccountMonthBillDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_credit_detail.go b/server/internal/dao/internal/t_credit_detail.go new file mode 100644 index 0000000..3a5f834 --- /dev/null +++ b/server/internal/dao/internal/t_credit_detail.go @@ -0,0 +1,113 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TCreditDetailDao is the data access object for the table t_credit_detail. +type TCreditDetailDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TCreditDetailColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TCreditDetailColumns defines and stores column names for the table t_credit_detail. +type TCreditDetailColumns struct { + Id string // 明细ID + DeptId string // 组织机构ID + AccountPeriod string // 账期(YYYY-MM) + TransactionType string // 交易类型: 使用/还款 + Amount string // 金额 + CreditBalanceBefore string // 变动前信用额度余额 + CreditBalanceAfter string // 变动后信用额度余额 + RelatedOrderSn string // 关联订单号 + RepaymentMethod string // 还款方式: 0 余额 1 银行卡 2 支付宝 3 微信 + RepaymentChannelSerial string // 交易渠道流水号 + TransactionTime string // 交易时间 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tCreditDetailColumns holds the columns for the table t_credit_detail. +var tCreditDetailColumns = TCreditDetailColumns{ + Id: "id", + DeptId: "dept_id", + AccountPeriod: "account_period", + TransactionType: "transaction_type", + Amount: "amount", + CreditBalanceBefore: "credit_balance_before", + CreditBalanceAfter: "credit_balance_after", + RelatedOrderSn: "related_order_sn", + RepaymentMethod: "repayment_method", + RepaymentChannelSerial: "repayment_channel_serial", + TransactionTime: "transaction_time", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTCreditDetailDao creates and returns a new DAO object for table data access. +func NewTCreditDetailDao(handlers ...gdb.ModelHandler) *TCreditDetailDao { + return &TCreditDetailDao{ + group: "default", + table: "t_credit_detail", + columns: tCreditDetailColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TCreditDetailDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TCreditDetailDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TCreditDetailDao) Columns() TCreditDetailColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TCreditDetailDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TCreditDetailDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TCreditDetailDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_cust_withdraw_account.go b/server/internal/dao/internal/t_cust_withdraw_account.go new file mode 100644 index 0000000..0f21bf2 --- /dev/null +++ b/server/internal/dao/internal/t_cust_withdraw_account.go @@ -0,0 +1,107 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TCustWithdrawAccountDao is the data access object for the table t_cust_withdraw_account. +type TCustWithdrawAccountDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TCustWithdrawAccountColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TCustWithdrawAccountColumns defines and stores column names for the table t_cust_withdraw_account. +type TCustWithdrawAccountColumns struct { + Id string // 主键ID + DeptId string // 企业客户ID + AccountType string // 提现账号类型,1:支付宝,2:微信 + AccountNo string // 账号 + AccountName string // 账户名称/真实姓名 + Mobile string // 手机号 + IsDefault string // 是否默认账号:0-否 1-是 + Enabled string // 是否启用:0-未启用 1-启用 + Status string // 状态:0-禁用 1-正常 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// tCustWithdrawAccountColumns holds the columns for the table t_cust_withdraw_account. +var tCustWithdrawAccountColumns = TCustWithdrawAccountColumns{ + Id: "id", + DeptId: "dept_id", + AccountType: "account_type", + AccountNo: "account_no", + AccountName: "account_name", + Mobile: "mobile", + IsDefault: "is_default", + Enabled: "enabled", + Status: "status", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTCustWithdrawAccountDao creates and returns a new DAO object for table data access. +func NewTCustWithdrawAccountDao(handlers ...gdb.ModelHandler) *TCustWithdrawAccountDao { + return &TCustWithdrawAccountDao{ + group: "default", + table: "t_cust_withdraw_account", + columns: tCustWithdrawAccountColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TCustWithdrawAccountDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TCustWithdrawAccountDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TCustWithdrawAccountDao) Columns() TCustWithdrawAccountColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TCustWithdrawAccountDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TCustWithdrawAccountDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TCustWithdrawAccountDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_dept_flow_daily.go b/server/internal/dao/internal/t_dept_flow_daily.go new file mode 100644 index 0000000..ef56fcc --- /dev/null +++ b/server/internal/dao/internal/t_dept_flow_daily.go @@ -0,0 +1,103 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TDeptFlowDailyDao is the data access object for the table t_dept_flow_daily. +type TDeptFlowDailyDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TDeptFlowDailyColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TDeptFlowDailyColumns defines and stores column names for the table t_dept_flow_daily. +type TDeptFlowDailyColumns struct { + Id string // 自增主键 + DeptId string // 渠道客户ID + StatDate string // 统计日期 + TotalUsedFlowMb string // 当日所有卡总消耗流量GB + TotalSms string // 当日总短信使用条数 + TotalVoiceMin string // 当日总语音分钟数 + Remark string // 备注,同步失败原因/特殊说明 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tDeptFlowDailyColumns holds the columns for the table t_dept_flow_daily. +var tDeptFlowDailyColumns = TDeptFlowDailyColumns{ + Id: "id", + DeptId: "dept_id", + StatDate: "stat_date", + TotalUsedFlowMb: "total_used_flow_mb", + TotalSms: "total_sms", + TotalVoiceMin: "total_voice_min", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTDeptFlowDailyDao creates and returns a new DAO object for table data access. +func NewTDeptFlowDailyDao(handlers ...gdb.ModelHandler) *TDeptFlowDailyDao { + return &TDeptFlowDailyDao{ + group: "default", + table: "t_dept_flow_daily", + columns: tDeptFlowDailyColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TDeptFlowDailyDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TDeptFlowDailyDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TDeptFlowDailyDao) Columns() TDeptFlowDailyColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TDeptFlowDailyDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TDeptFlowDailyDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TDeptFlowDailyDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_division_code.go b/server/internal/dao/internal/t_division_code.go new file mode 100644 index 0000000..b56fd6d --- /dev/null +++ b/server/internal/dao/internal/t_division_code.go @@ -0,0 +1,89 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TDivisionCodeDao is the data access object for the table t_division_code. +type TDivisionCodeDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TDivisionCodeColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TDivisionCodeColumns defines and stores column names for the table t_division_code. +type TDivisionCodeColumns struct { + RegionId string // ID + RegionCode string // 行政区域代码 + RegionName string // 行政区域名称 + RegionLevel string // 等级 + RegionParent string // 上级代码 + RegionType string // 城乡分类代码 +} + +// tDivisionCodeColumns holds the columns for the table t_division_code. +var tDivisionCodeColumns = TDivisionCodeColumns{ + RegionId: "region_id", + RegionCode: "region_code", + RegionName: "region_name", + RegionLevel: "region_level", + RegionParent: "region_parent", + RegionType: "region_type", +} + +// NewTDivisionCodeDao creates and returns a new DAO object for table data access. +func NewTDivisionCodeDao(handlers ...gdb.ModelHandler) *TDivisionCodeDao { + return &TDivisionCodeDao{ + group: "default", + table: "t_division_code", + columns: tDivisionCodeColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TDivisionCodeDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TDivisionCodeDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TDivisionCodeDao) Columns() TDivisionCodeColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TDivisionCodeDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TDivisionCodeDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TDivisionCodeDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_fin_commission_detail.go b/server/internal/dao/internal/t_fin_commission_detail.go new file mode 100644 index 0000000..d753961 --- /dev/null +++ b/server/internal/dao/internal/t_fin_commission_detail.go @@ -0,0 +1,115 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TFinCommissionDetailDao is the data access object for the table t_fin_commission_detail. +type TFinCommissionDetailDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TFinCommissionDetailColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TFinCommissionDetailColumns defines and stores column names for the table t_fin_commission_detail. +type TFinCommissionDetailColumns struct { + Id string // 主键ID + CommissionNo string // 分润明细编号 + OrderSn string // 订单编号 + OrderType string // 订单类型 + ProductId string // 商品ID + PayerDeptId string // 下级机构ID + BenefitDeptId string // 受益机构ID + BenefitDeptLevel string // 受益机构层级 + CommissionAmount string // 分润金额 + CommissionStatus string // 分润状态:0待结算,1可提现 + BenefitSnapshot string // 价格快照(受益机构的成本价+利润+利润悉数=出货单价) + AccountPeriod string // 账期(YYYY-MM) + SettlementTime string // 结算时间 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tFinCommissionDetailColumns holds the columns for the table t_fin_commission_detail. +var tFinCommissionDetailColumns = TFinCommissionDetailColumns{ + Id: "id", + CommissionNo: "commission_no", + OrderSn: "order_sn", + OrderType: "order_type", + ProductId: "product_id", + PayerDeptId: "payer_dept_id", + BenefitDeptId: "benefit_dept_id", + BenefitDeptLevel: "benefit_dept_level", + CommissionAmount: "commission_amount", + CommissionStatus: "commission_status", + BenefitSnapshot: "benefit_snapshot", + AccountPeriod: "account_period", + SettlementTime: "settlement_time", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTFinCommissionDetailDao creates and returns a new DAO object for table data access. +func NewTFinCommissionDetailDao(handlers ...gdb.ModelHandler) *TFinCommissionDetailDao { + return &TFinCommissionDetailDao{ + group: "default", + table: "t_fin_commission_detail", + columns: tFinCommissionDetailColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TFinCommissionDetailDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TFinCommissionDetailDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TFinCommissionDetailDao) Columns() TFinCommissionDetailColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TFinCommissionDetailDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TFinCommissionDetailDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TFinCommissionDetailDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_fin_withdrawable_change.go b/server/internal/dao/internal/t_fin_withdrawable_change.go new file mode 100644 index 0000000..589b6a4 --- /dev/null +++ b/server/internal/dao/internal/t_fin_withdrawable_change.go @@ -0,0 +1,119 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TFinWithdrawableChangeDao is the data access object for the table t_fin_withdrawable_change. +type TFinWithdrawableChangeDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TFinWithdrawableChangeColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TFinWithdrawableChangeColumns defines and stores column names for the table t_fin_withdrawable_change. +type TFinWithdrawableChangeColumns struct { + Id string // 主键ID + DeptId string // 机构ID + ChangeType string // 变动类型:0分润,1提现,2转出(可提现转余额)3转入(余额转可提现)4还款 + IncomeExpenseType string // 收支类型: 0收入,1支出 + ChangeAmount string // 变动金额 + BeforeBalance string // 变动前可提现余额 + AfterBalance string // 变动后可提现余额 + AccountType string // 提现账号类型,1:银行卡,2:支付宝,3:微信 + BankName string // 开户行名称 + BankAccount string // 开户行账号或账号 + AccountName string // 用户姓名 + IdentityCard string // 用户身份证号 + SourceNo string // 来源单号 + Remark string // 备注 + AccountPeriod string // 账期(YYYY-MM) + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tFinWithdrawableChangeColumns holds the columns for the table t_fin_withdrawable_change. +var tFinWithdrawableChangeColumns = TFinWithdrawableChangeColumns{ + Id: "id", + DeptId: "dept_id", + ChangeType: "change_type", + IncomeExpenseType: "income_expense_type", + ChangeAmount: "change_amount", + BeforeBalance: "before_balance", + AfterBalance: "after_balance", + AccountType: "account_type", + BankName: "bank_name", + BankAccount: "bank_account", + AccountName: "account_name", + IdentityCard: "identity_card", + SourceNo: "source_no", + Remark: "remark", + AccountPeriod: "account_period", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTFinWithdrawableChangeDao creates and returns a new DAO object for table data access. +func NewTFinWithdrawableChangeDao(handlers ...gdb.ModelHandler) *TFinWithdrawableChangeDao { + return &TFinWithdrawableChangeDao{ + group: "default", + table: "t_fin_withdrawable_change", + columns: tFinWithdrawableChangeColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TFinWithdrawableChangeDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TFinWithdrawableChangeDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TFinWithdrawableChangeDao) Columns() TFinWithdrawableChangeColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TFinWithdrawableChangeDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TFinWithdrawableChangeDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TFinWithdrawableChangeDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_finance_apply.go b/server/internal/dao/internal/t_finance_apply.go new file mode 100644 index 0000000..984d44e --- /dev/null +++ b/server/internal/dao/internal/t_finance_apply.go @@ -0,0 +1,125 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TFinanceApplyDao is the data access object for the table t_finance_apply. +type TFinanceApplyDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TFinanceApplyColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TFinanceApplyColumns defines and stores column names for the table t_finance_apply. +type TFinanceApplyColumns struct { + Id string // 主键ID + ApplyNo string // 申请单号 + ApplyType string // 申请类型(1:提现申请,2:余额转提现) + AuditType string // 审核类型:1初审,2复审 + ApplyDeptId string // 申请机构ID + AuditDeptId string // 审核方机构ID,通常为直属上级或顶级 + AvailableAmountSnapshot string // 申请前可提现余额快照 + ApplyAmount string // 申请金额 + AccountType string // 提现账号类型,1:银行卡,2:支付宝,3:微信 + BankName string // 开户行名称 + BankAccount string // 开户行账号或账号 + AccountName string // 用户姓名 + IdentityCard string // 用户身份证号 + ReceiveAccount string // 收款账户信息快照 + ApplyStatus string // 状态:0待审核,1已通过,2已驳回,3已打款,4已取消 + ApplyTime string // 申请时间 + AuditTime string // 最近审核时间 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tFinanceApplyColumns holds the columns for the table t_finance_apply. +var tFinanceApplyColumns = TFinanceApplyColumns{ + Id: "id", + ApplyNo: "apply_no", + ApplyType: "apply_type", + AuditType: "audit_type", + ApplyDeptId: "apply_dept_id", + AuditDeptId: "audit_dept_id", + AvailableAmountSnapshot: "available_amount_snapshot", + ApplyAmount: "apply_amount", + AccountType: "account_type", + BankName: "bank_name", + BankAccount: "bank_account", + AccountName: "account_name", + IdentityCard: "identity_card", + ReceiveAccount: "receive_account", + ApplyStatus: "apply_status", + ApplyTime: "apply_time", + AuditTime: "audit_time", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTFinanceApplyDao creates and returns a new DAO object for table data access. +func NewTFinanceApplyDao(handlers ...gdb.ModelHandler) *TFinanceApplyDao { + return &TFinanceApplyDao{ + group: "default", + table: "t_finance_apply", + columns: tFinanceApplyColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TFinanceApplyDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TFinanceApplyDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TFinanceApplyDao) Columns() TFinanceApplyColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TFinanceApplyDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TFinanceApplyDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TFinanceApplyDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_finance_audit_record.go b/server/internal/dao/internal/t_finance_audit_record.go new file mode 100644 index 0000000..0b5c0e3 --- /dev/null +++ b/server/internal/dao/internal/t_finance_audit_record.go @@ -0,0 +1,105 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TFinanceAuditRecordDao is the data access object for the table t_finance_audit_record. +type TFinanceAuditRecordDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TFinanceAuditRecordColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TFinanceAuditRecordColumns defines and stores column names for the table t_finance_audit_record. +type TFinanceAuditRecordColumns struct { + Id string // 主键ID + AuditNo string // 审核记录编号 + ApplyNo string // 申请单号 + AuditDeptId string // 审核方机构ID + AuditorUserId string // 审核人ID + AuditResult string // 审核结果:0通过,1驳回 + AuditOpinion string // 审核意见 + AuditTime string // 审核时间 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tFinanceAuditRecordColumns holds the columns for the table t_finance_audit_record. +var tFinanceAuditRecordColumns = TFinanceAuditRecordColumns{ + Id: "id", + AuditNo: "audit_no", + ApplyNo: "apply_no", + AuditDeptId: "audit_dept_id", + AuditorUserId: "auditor_user_id", + AuditResult: "audit_result", + AuditOpinion: "audit_opinion", + AuditTime: "audit_time", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTFinanceAuditRecordDao creates and returns a new DAO object for table data access. +func NewTFinanceAuditRecordDao(handlers ...gdb.ModelHandler) *TFinanceAuditRecordDao { + return &TFinanceAuditRecordDao{ + group: "default", + table: "t_finance_audit_record", + columns: tFinanceAuditRecordColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TFinanceAuditRecordDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TFinanceAuditRecordDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TFinanceAuditRecordDao) Columns() TFinanceAuditRecordColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TFinanceAuditRecordDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TFinanceAuditRecordDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TFinanceAuditRecordDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_flow_package_config.go b/server/internal/dao/internal/t_flow_package_config.go new file mode 100644 index 0000000..1d7c2c2 --- /dev/null +++ b/server/internal/dao/internal/t_flow_package_config.go @@ -0,0 +1,99 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TFlowPackageConfigDao is the data access object for the table t_flow_package_config. +type TFlowPackageConfigDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TFlowPackageConfigColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TFlowPackageConfigColumns defines and stores column names for the table t_flow_package_config. +type TFlowPackageConfigColumns struct { + Id string // 主键ID + PoolPackageCode string // 池编码 + PackageCodes string // 套餐编码列表 + TopupPackageCodes string // 可订购加油包套餐编码列表 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// tFlowPackageConfigColumns holds the columns for the table t_flow_package_config. +var tFlowPackageConfigColumns = TFlowPackageConfigColumns{ + Id: "id", + PoolPackageCode: "pool_package_code", + PackageCodes: "package_codes", + TopupPackageCodes: "topup_package_codes", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTFlowPackageConfigDao creates and returns a new DAO object for table data access. +func NewTFlowPackageConfigDao(handlers ...gdb.ModelHandler) *TFlowPackageConfigDao { + return &TFlowPackageConfigDao{ + group: "default", + table: "t_flow_package_config", + columns: tFlowPackageConfigColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TFlowPackageConfigDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TFlowPackageConfigDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TFlowPackageConfigDao) Columns() TFlowPackageConfigColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TFlowPackageConfigDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TFlowPackageConfigDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TFlowPackageConfigDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_message_log.go b/server/internal/dao/internal/t_message_log.go new file mode 100644 index 0000000..a7bc2ae --- /dev/null +++ b/server/internal/dao/internal/t_message_log.go @@ -0,0 +1,107 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TMessageLogDao is the data access object for the table t_message_log. +type TMessageLogDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TMessageLogColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TMessageLogColumns defines and stores column names for the table t_message_log. +type TMessageLogColumns struct { + MessageLogId string // 主键ID + MessageType string // 消息类型 1系统通知 2活动通知 3订单通知 4账户通知 + SendUserType string // 发送用户类型 1代理商 2终端用户 + UserIds string // 接收用户ID集合(JSON数组) + MessageTitle string // 消息标题 + MessageContent string // 消息内容 + MessageLink string // 跳转链接地址 + SendTime string // 发送时间 + IsRecall string // 是否撤回 0未撤回 1已撤回 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tMessageLogColumns holds the columns for the table t_message_log. +var tMessageLogColumns = TMessageLogColumns{ + MessageLogId: "message_log_id", + MessageType: "message_type", + SendUserType: "send_user_type", + UserIds: "user_ids", + MessageTitle: "message_title", + MessageContent: "message_content", + MessageLink: "message_link", + SendTime: "send_time", + IsRecall: "is_recall", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTMessageLogDao creates and returns a new DAO object for table data access. +func NewTMessageLogDao(handlers ...gdb.ModelHandler) *TMessageLogDao { + return &TMessageLogDao{ + group: "default", + table: "t_message_log", + columns: tMessageLogColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TMessageLogDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TMessageLogDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TMessageLogDao) Columns() TMessageLogColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TMessageLogDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TMessageLogDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TMessageLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_message_type.go b/server/internal/dao/internal/t_message_type.go new file mode 100644 index 0000000..d635812 --- /dev/null +++ b/server/internal/dao/internal/t_message_type.go @@ -0,0 +1,97 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TMessageTypeDao is the data access object for the table t_message_type. +type TMessageTypeDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TMessageTypeColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TMessageTypeColumns defines and stores column names for the table t_message_type. +type TMessageTypeColumns struct { + MessageId string // 主键ID + Name string // 通知类型 + Describe string // 通知描述 + SendType string // 发送类型 1代理商 2终端 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tMessageTypeColumns holds the columns for the table t_message_type. +var tMessageTypeColumns = TMessageTypeColumns{ + MessageId: "message_id", + Name: "name", + Describe: "describe", + SendType: "send_type", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTMessageTypeDao creates and returns a new DAO object for table data access. +func NewTMessageTypeDao(handlers ...gdb.ModelHandler) *TMessageTypeDao { + return &TMessageTypeDao{ + group: "default", + table: "t_message_type", + columns: tMessageTypeColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TMessageTypeDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TMessageTypeDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TMessageTypeDao) Columns() TMessageTypeColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TMessageTypeDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TMessageTypeDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TMessageTypeDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_notify_log.go b/server/internal/dao/internal/t_notify_log.go new file mode 100644 index 0000000..ada713c --- /dev/null +++ b/server/internal/dao/internal/t_notify_log.go @@ -0,0 +1,99 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TNotifyLogDao is the data access object for the table t_notify_log. +type TNotifyLogDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TNotifyLogColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TNotifyLogColumns defines and stores column names for the table t_notify_log. +type TNotifyLogColumns struct { + NotifyId string // 主键ID + NotifyTitle string // 标题 + NotifyContent string // 内容 + SendUserType string // 发送用户类型 1代理商 2终端用户 + SendTime string // 发送时间 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tNotifyLogColumns holds the columns for the table t_notify_log. +var tNotifyLogColumns = TNotifyLogColumns{ + NotifyId: "notify_id", + NotifyTitle: "notify_title", + NotifyContent: "notify_content", + SendUserType: "send_user_type", + SendTime: "send_time", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTNotifyLogDao creates and returns a new DAO object for table data access. +func NewTNotifyLogDao(handlers ...gdb.ModelHandler) *TNotifyLogDao { + return &TNotifyLogDao{ + group: "default", + table: "t_notify_log", + columns: tNotifyLogColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TNotifyLogDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TNotifyLogDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TNotifyLogDao) Columns() TNotifyLogColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TNotifyLogDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TNotifyLogDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TNotifyLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_order.go b/server/internal/dao/internal/t_order.go new file mode 100644 index 0000000..fe3819d --- /dev/null +++ b/server/internal/dao/internal/t_order.go @@ -0,0 +1,183 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TOrderDao is the data access object for the table t_order. +type TOrderDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TOrderColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TOrderColumns defines and stores column names for the table t_order. +type TOrderColumns struct { + Id string // 主键ID + OrderSn string // 订单编号 + ParentOrderId string // 父订单ID + ParentOrderSn string // 父订单编号 + CustomerType string // 客户类型 枚举 + CustomerAgentId string // 渠道编号(购买方) + EndUserId string // 终端客户编号 + AgentId string // 渠道编号(销售方) + OrderType string // 订单类型 枚举 + OrderStatus string // 订单状态 枚举 + ShippingStatus string // 配送状态 枚举 + ReturnStatus string // 退货状态 枚举 + Consignee string // 收货人姓名 + Mobile string // 收货人手机 + Email string // 收货人邮箱 + Address string // 详细地址 + AreaCode string // 行政区域代码 + BuyerNote string // 买家备注 + AdminNote string // 商家备注 + Remark string // 系统备注 + ShippingMethod string // 发货方式 枚举 + LogisticsId string // 快递公司ID + LogisticsName string // 物流公司名称 + ShippingTypeId string // 配送类型ID + ShippingTypeName string // 配送类型名称 + TrackingNo string // 物流单号 + ShippingTime string // 发货时间 + ReceivedTime string // 收货时间 + PayType string // 支付类型 枚举 + PayStatus string // 支付状态 枚举 + PayOrderSn string // 支付订单编号 + PayTime string // 支付时间 + TotalAmount string // 订单总金额 + ProductAmount string // 商品总金额 + ShippingFee string // 运费金额 + ServiceFee string // 服务费金额 + InvoiceFee string // 发票费金额 + CouponAmount string // 优惠券抵扣金额 + DiscountAmount string // 活动优惠金额 + Balance string // 余额抵扣金额 + PaidAmount string // 已支付总金额 + OnlinePaidAmount string // 线上支付金额 + OfflinePaidAmount string // 线下支付金额 + UnpaidAmount string // 未支付金额 + UnrefundAmount string // 未退款金额 + IsNeedCommission string // 是否需要分佣 枚举:yesno_enum + DistributionStatus string // 分销分成状态 枚举 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// tOrderColumns holds the columns for the table t_order. +var tOrderColumns = TOrderColumns{ + Id: "id", + OrderSn: "order_sn", + ParentOrderId: "parent_order_id", + ParentOrderSn: "parent_order_sn", + CustomerType: "customer_type", + CustomerAgentId: "customer_agent_id", + EndUserId: "end_user_id", + AgentId: "agent_id", + OrderType: "order_type", + OrderStatus: "order_status", + ShippingStatus: "shipping_status", + ReturnStatus: "return_status", + Consignee: "consignee", + Mobile: "mobile", + Email: "email", + Address: "address", + AreaCode: "area_code", + BuyerNote: "buyer_note", + AdminNote: "admin_note", + Remark: "remark", + ShippingMethod: "shipping_method", + LogisticsId: "logistics_id", + LogisticsName: "logistics_name", + ShippingTypeId: "shipping_type_id", + ShippingTypeName: "shipping_type_name", + TrackingNo: "tracking_no", + ShippingTime: "shipping_time", + ReceivedTime: "received_time", + PayType: "pay_type", + PayStatus: "pay_status", + PayOrderSn: "pay_order_sn", + PayTime: "pay_time", + TotalAmount: "total_amount", + ProductAmount: "product_amount", + ShippingFee: "shipping_fee", + ServiceFee: "service_fee", + InvoiceFee: "invoice_fee", + CouponAmount: "coupon_amount", + DiscountAmount: "discount_amount", + Balance: "balance", + PaidAmount: "paid_amount", + OnlinePaidAmount: "online_paid_amount", + OfflinePaidAmount: "offline_paid_amount", + UnpaidAmount: "unpaid_amount", + UnrefundAmount: "unrefund_amount", + IsNeedCommission: "is_need_commission", + DistributionStatus: "distribution_status", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTOrderDao creates and returns a new DAO object for table data access. +func NewTOrderDao(handlers ...gdb.ModelHandler) *TOrderDao { + return &TOrderDao{ + group: "default", + table: "t_order", + columns: tOrderColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TOrderDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TOrderDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TOrderDao) Columns() TOrderColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TOrderDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TOrderDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TOrderDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_order_item.go b/server/internal/dao/internal/t_order_item.go new file mode 100644 index 0000000..345a1b5 --- /dev/null +++ b/server/internal/dao/internal/t_order_item.go @@ -0,0 +1,141 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TOrderItemDao is the data access object for the table t_order_item. +type TOrderItemDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TOrderItemColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TOrderItemColumns defines and stores column names for the table t_order_item. +type TOrderItemColumns struct { + ItemId string // 订单商品信息自增id + OrderId string // 订单ID + OrderSn string // 订单编号 + CustomerType string // 客户类型 枚举 + CustomerAgentId string // 渠道编号(购买方) + EndUserId string // 客户编号 + Price string // 商品最终单价 + Quantity string // 商品的购买数量 + ProductId string // 商品ID + ProductName string // 商品的名称 + ProductSn string // 商品的唯一货号 + PicThumb string // 商品缩略图 + SkuId string // 规格ID + SkuData string // 【JSON】sku信息[{sku_id,sku_value,sku_name}] + DeliveryQuantity string // 发货数量 + ProductType string // 商品类型 枚举 + IsGift string // 是否为赠品 枚举:yesno_enum + AgentId string // 渠道编号(销售方) + PrepayPrice string // 预售价格 + Commission string // 佣金数据 + OriginPrice string // 商品初始价格 + PromotionData string // 优惠信息 + ExtraSkuData string // 附加属性 + PayMode string // 付费方式 枚举 + ServiceBeginTime string // 服务开始时间 + ServiceEndTime string // 服务结束时间 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tOrderItemColumns holds the columns for the table t_order_item. +var tOrderItemColumns = TOrderItemColumns{ + ItemId: "item_id", + OrderId: "order_id", + OrderSn: "order_sn", + CustomerType: "customer_type", + CustomerAgentId: "customer_agent_id", + EndUserId: "end_user_id", + Price: "price", + Quantity: "quantity", + ProductId: "product_id", + ProductName: "product_name", + ProductSn: "product_sn", + PicThumb: "pic_thumb", + SkuId: "sku_id", + SkuData: "sku_data", + DeliveryQuantity: "delivery_quantity", + ProductType: "product_type", + IsGift: "is_gift", + AgentId: "agent_id", + PrepayPrice: "prepay_price", + Commission: "commission", + OriginPrice: "origin_price", + PromotionData: "promotion_data", + ExtraSkuData: "extra_sku_data", + PayMode: "pay_mode", + ServiceBeginTime: "service_begin_time", + ServiceEndTime: "service_end_time", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTOrderItemDao creates and returns a new DAO object for table data access. +func NewTOrderItemDao(handlers ...gdb.ModelHandler) *TOrderItemDao { + return &TOrderItemDao{ + group: "default", + table: "t_order_item", + columns: tOrderItemColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TOrderItemDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TOrderItemDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TOrderItemDao) Columns() TOrderItemColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TOrderItemDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TOrderItemDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TOrderItemDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_package_apply.go b/server/internal/dao/internal/t_package_apply.go new file mode 100644 index 0000000..bdbb1a0 --- /dev/null +++ b/server/internal/dao/internal/t_package_apply.go @@ -0,0 +1,121 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TPackageApplyDao is the data access object for the table t_package_apply. +type TPackageApplyDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TPackageApplyColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TPackageApplyColumns defines and stores column names for the table t_package_apply. +type TPackageApplyColumns struct { + Id string // 主键ID + BatchNo string // 批次编号 + ApplyNo string // 申请单号 + ApplyType string // 申请类型(1:变更,2:退订) + AuditType string // 审核类型:1初审,2复审 + ApplyDeptId string // 申请机构ID + AuditDeptId string // 审核方机构ID,通常为直属上级或顶级 + ApplyPackageId string // 申请前套餐ID + ChangePackageId string // 变更套餐ID + DurationMonths string // 套餐时长(月) + PackageEffectiveType string // 生效方式:1:立即生效,2:到期生效 + PackageEffectiveTime string // 生效时间 + ApplyStatus string // 状态:0待审核,1已通过,2已驳回,3已打款,4已取消 + ApplyTime string // 申请时间 + AuditTime string // 最近审核时间 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tPackageApplyColumns holds the columns for the table t_package_apply. +var tPackageApplyColumns = TPackageApplyColumns{ + Id: "id", + BatchNo: "batch_no", + ApplyNo: "apply_no", + ApplyType: "apply_type", + AuditType: "audit_type", + ApplyDeptId: "apply_dept_id", + AuditDeptId: "audit_dept_id", + ApplyPackageId: "apply_package_id", + ChangePackageId: "change_package_id", + DurationMonths: "duration_months", + PackageEffectiveType: "package_effective_type", + PackageEffectiveTime: "package_effective_time", + ApplyStatus: "apply_status", + ApplyTime: "apply_time", + AuditTime: "audit_time", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTPackageApplyDao creates and returns a new DAO object for table data access. +func NewTPackageApplyDao(handlers ...gdb.ModelHandler) *TPackageApplyDao { + return &TPackageApplyDao{ + group: "default", + table: "t_package_apply", + columns: tPackageApplyColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TPackageApplyDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TPackageApplyDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TPackageApplyDao) Columns() TPackageApplyColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TPackageApplyDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TPackageApplyDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TPackageApplyDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_package_audit_record.go b/server/internal/dao/internal/t_package_audit_record.go new file mode 100644 index 0000000..562c0da --- /dev/null +++ b/server/internal/dao/internal/t_package_audit_record.go @@ -0,0 +1,105 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TPackageAuditRecordDao is the data access object for the table t_package_audit_record. +type TPackageAuditRecordDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TPackageAuditRecordColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TPackageAuditRecordColumns defines and stores column names for the table t_package_audit_record. +type TPackageAuditRecordColumns struct { + Id string // 主键ID + AuditNo string // 审核记录编号 + ApplyNo string // 申请单号 + AuditDeptId string // 审核方机构ID + AuditorUserId string // 审核人ID + AuditResult string // 审核结果:0通过,1驳回 + AuditOpinion string // 审核意见 + AuditTime string // 审核时间 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tPackageAuditRecordColumns holds the columns for the table t_package_audit_record. +var tPackageAuditRecordColumns = TPackageAuditRecordColumns{ + Id: "id", + AuditNo: "audit_no", + ApplyNo: "apply_no", + AuditDeptId: "audit_dept_id", + AuditorUserId: "auditor_user_id", + AuditResult: "audit_result", + AuditOpinion: "audit_opinion", + AuditTime: "audit_time", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTPackageAuditRecordDao creates and returns a new DAO object for table data access. +func NewTPackageAuditRecordDao(handlers ...gdb.ModelHandler) *TPackageAuditRecordDao { + return &TPackageAuditRecordDao{ + group: "default", + table: "t_package_audit_record", + columns: tPackageAuditRecordColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TPackageAuditRecordDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TPackageAuditRecordDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TPackageAuditRecordDao) Columns() TPackageAuditRecordColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TPackageAuditRecordDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TPackageAuditRecordDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TPackageAuditRecordDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_package_customer.go b/server/internal/dao/internal/t_package_customer.go new file mode 100644 index 0000000..f4f1afa --- /dev/null +++ b/server/internal/dao/internal/t_package_customer.go @@ -0,0 +1,101 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TPackageCustomerDao is the data access object for the table t_package_customer. +type TPackageCustomerDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TPackageCustomerColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TPackageCustomerColumns defines and stores column names for the table t_package_customer. +type TPackageCustomerColumns struct { + Id string // 主键ID + PackageCode string // 套餐编码 + PackageName string // 套餐名称 + DeptId string // 组织客户ID + DeptName string // 组织客户名称 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// tPackageCustomerColumns holds the columns for the table t_package_customer. +var tPackageCustomerColumns = TPackageCustomerColumns{ + Id: "id", + PackageCode: "package_code", + PackageName: "package_name", + DeptId: "dept_id", + DeptName: "dept_name", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTPackageCustomerDao creates and returns a new DAO object for table data access. +func NewTPackageCustomerDao(handlers ...gdb.ModelHandler) *TPackageCustomerDao { + return &TPackageCustomerDao{ + group: "default", + table: "t_package_customer", + columns: tPackageCustomerColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TPackageCustomerDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TPackageCustomerDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TPackageCustomerDao) Columns() TPackageCustomerColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TPackageCustomerDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TPackageCustomerDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TPackageCustomerDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_package_terminal.go b/server/internal/dao/internal/t_package_terminal.go new file mode 100644 index 0000000..fa9f453 --- /dev/null +++ b/server/internal/dao/internal/t_package_terminal.go @@ -0,0 +1,101 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TPackageTerminalDao is the data access object for the table t_package_terminal. +type TPackageTerminalDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TPackageTerminalColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TPackageTerminalColumns defines and stores column names for the table t_package_terminal. +type TPackageTerminalColumns struct { + Id string // 主键ID + PackageCode string // 套餐编码 + PackageName string // 套餐名称 + TerminalId string // 终端客户ID + TerminalName string // 终端客户名称 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// tPackageTerminalColumns holds the columns for the table t_package_terminal. +var tPackageTerminalColumns = TPackageTerminalColumns{ + Id: "id", + PackageCode: "package_code", + PackageName: "package_name", + TerminalId: "terminal_id", + TerminalName: "terminal_name", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTPackageTerminalDao creates and returns a new DAO object for table data access. +func NewTPackageTerminalDao(handlers ...gdb.ModelHandler) *TPackageTerminalDao { + return &TPackageTerminalDao{ + group: "default", + table: "t_package_terminal", + columns: tPackageTerminalColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TPackageTerminalDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TPackageTerminalDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TPackageTerminalDao) Columns() TPackageTerminalColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TPackageTerminalDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TPackageTerminalDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TPackageTerminalDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_product.go b/server/internal/dao/internal/t_product.go new file mode 100644 index 0000000..fcc7c8e --- /dev/null +++ b/server/internal/dao/internal/t_product.go @@ -0,0 +1,189 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TProductDao is the data access object for the table t_product. +type TProductDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TProductColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TProductColumns defines and stores column names for the table t_product. +type TProductColumns struct { + ProductId string // 商品id + ProductName string // 商品的名称 + ProductSn string // 商品的唯一货号 + ProductTsn string // 条形编码 + ProductStock string // 商品库存数量 + ProductPrice string // 本店售价 + MarketPrice string // 市场售价 + ProductStatus string // 该商品状态 枚举 + ProductType string // 商品类型 枚举 + CategoryId string // 商品所属商品分类id + BrandId string // 品牌id + Keywords string // 商品关键字,放在商品页的关键字中,为搜索引擎收录用 + CheckStatus string // 审核状态 枚举 + CheckReason string // 审核失败原因 + ClickCount string // 商品点击数 + ProductWeight string // 商品的重量,以千克为单位 + IsPromote string // 是否特价促销 枚举yesno_enum + PromotePrice string // 促销价格 + PromoteStartDate string // 促销价格开始日期 + PromoteEndDate string // 促销价格结束日期 + SeckillMaxNum string // 最大秒杀库存 + ProductBrief string // 商品的简短描述 + ProductDesc string // 商品的详细描述 + PicUrl string // 商品的实际大小图片,如进入该商品页时介绍商品属性所显示的大图片 + PicThumb string // 商品在前台显示的微缩图片,如在分类筛选时显示的小图片 + PicOriginal string // 应该是上传的商品的原始图片 + CommentTag string // 标签 + FreeShipping string // 是否包邮 枚举yesno_enum + SortOrder string // 商品的显示顺序 + StoreSortOrder string // 排序 + IsBest string // 是否是精品 枚举yesno_enum + IsNew string // 是否是新品 枚举yesno_enum + IsHot string // 是否热销 枚举yesno_enum + LastUpdate string // 最近一次更新商品配置的时间 + Remark string // 商品的商家备注,仅商家可见 + VirtualSales string // 虚拟销量 + LimitNumber string // 限购数量 + ProductCare string // 温馨提示 + ProductRelated string // [JSON]关联的推荐商品Id + ProductServiceIds string // [JSON]服务项ids + IsSupportReturn string // 是否支持无理由退货 枚举yesno_enum + IsSupportCod string // 是否支持货到付款 枚举yesno_enum + ProductVideo string // 商品视频 + PrepayPrice string // 预付金额 + VirtualSample string // 虚拟商品 + PaidContent string // 付费内容 + NoShipping string // 是否无需配送 枚举yesno_enum + FixedShippingType string // 运费类型 枚举 + FixedShippingFee string // 固定运费金额 + DeptId string // 机构id + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tProductColumns holds the columns for the table t_product. +var tProductColumns = TProductColumns{ + ProductId: "product_id", + ProductName: "product_name", + ProductSn: "product_sn", + ProductTsn: "product_tsn", + ProductStock: "product_stock", + ProductPrice: "product_price", + MarketPrice: "market_price", + ProductStatus: "product_status", + ProductType: "product_type", + CategoryId: "category_id", + BrandId: "brand_id", + Keywords: "keywords", + CheckStatus: "check_status", + CheckReason: "check_reason", + ClickCount: "click_count", + ProductWeight: "product_weight", + IsPromote: "is_promote", + PromotePrice: "promote_price", + PromoteStartDate: "promote_start_date", + PromoteEndDate: "promote_end_date", + SeckillMaxNum: "seckill_max_num", + ProductBrief: "product_brief", + ProductDesc: "product_desc", + PicUrl: "pic_url", + PicThumb: "pic_thumb", + PicOriginal: "pic_original", + CommentTag: "comment_tag", + FreeShipping: "free_shipping", + SortOrder: "sort_order", + StoreSortOrder: "store_sort_order", + IsBest: "is_best", + IsNew: "is_new", + IsHot: "is_hot", + LastUpdate: "last_update", + Remark: "remark", + VirtualSales: "virtual_sales", + LimitNumber: "limit_number", + ProductCare: "product_care", + ProductRelated: "product_related", + ProductServiceIds: "product_service_ids", + IsSupportReturn: "is_support_return", + IsSupportCod: "is_support_cod", + ProductVideo: "product_video", + PrepayPrice: "prepay_price", + VirtualSample: "virtual_sample", + PaidContent: "paid_content", + NoShipping: "no_shipping", + FixedShippingType: "fixed_shipping_type", + FixedShippingFee: "fixed_shipping_fee", + DeptId: "dept_id", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTProductDao creates and returns a new DAO object for table data access. +func NewTProductDao(handlers ...gdb.ModelHandler) *TProductDao { + return &TProductDao{ + group: "default", + table: "t_product", + columns: tProductColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TProductDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TProductDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TProductDao) Columns() TProductColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TProductDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TProductDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TProductDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_profit_account_month_bill.go b/server/internal/dao/internal/t_profit_account_month_bill.go new file mode 100644 index 0000000..d837d1a --- /dev/null +++ b/server/internal/dao/internal/t_profit_account_month_bill.go @@ -0,0 +1,125 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TProfitAccountMonthBillDao is the data access object for the table t_profit_account_month_bill. +type TProfitAccountMonthBillDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TProfitAccountMonthBillColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TProfitAccountMonthBillColumns defines and stores column names for the table t_profit_account_month_bill. +type TProfitAccountMonthBillColumns struct { + Id string // 账单ID + DeptId string // 机构ID + AccountPeriod string // 账期YYYY-MM + OpeningBalance string // 月初余额 + CommissionIncome string // 分润收入 + BalanceTransferInIncome string // 余额转提现收入 + WithdrawalExpense string // 提现支出 + WithdrawableTransferOutExpense string // 可提现转余额支出 + ProfitCreditRepayExpense string // 盈利账户还款支出 + TotalIncome string // 本月收入总额 + TotalExpense string // 本月支出总额 + ClosingBalance string // 月末余额(月初余额+本月收入-本月支出) + IncomeCount string // 收入笔数 + ExpenseCount string // 支出笔数 + BillStatus string // 账单状态:1正常,2已调整,3已作废 + SettleTime string // 结算时间 + SettleBy string // 结算人ID + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tProfitAccountMonthBillColumns holds the columns for the table t_profit_account_month_bill. +var tProfitAccountMonthBillColumns = TProfitAccountMonthBillColumns{ + Id: "id", + DeptId: "dept_id", + AccountPeriod: "account_period", + OpeningBalance: "opening_balance", + CommissionIncome: "commission_income", + BalanceTransferInIncome: "balance_transfer_in_income", + WithdrawalExpense: "withdrawal_expense", + WithdrawableTransferOutExpense: "withdrawable_transfer_out_expense", + ProfitCreditRepayExpense: "profit_credit_repay_expense", + TotalIncome: "total_income", + TotalExpense: "total_expense", + ClosingBalance: "closing_balance", + IncomeCount: "income_count", + ExpenseCount: "expense_count", + BillStatus: "bill_status", + SettleTime: "settle_time", + SettleBy: "settle_by", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTProfitAccountMonthBillDao creates and returns a new DAO object for table data access. +func NewTProfitAccountMonthBillDao(handlers ...gdb.ModelHandler) *TProfitAccountMonthBillDao { + return &TProfitAccountMonthBillDao{ + group: "default", + table: "t_profit_account_month_bill", + columns: tProfitAccountMonthBillColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TProfitAccountMonthBillDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TProfitAccountMonthBillDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TProfitAccountMonthBillDao) Columns() TProfitAccountMonthBillColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TProfitAccountMonthBillDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TProfitAccountMonthBillDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TProfitAccountMonthBillDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_resource_batch_download.go b/server/internal/dao/internal/t_resource_batch_download.go new file mode 100644 index 0000000..c7a8b4d --- /dev/null +++ b/server/internal/dao/internal/t_resource_batch_download.go @@ -0,0 +1,141 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TResourceBatchDownloadDao is the data access object for the table t_resource_batch_download. +type TResourceBatchDownloadDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TResourceBatchDownloadColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TResourceBatchDownloadColumns defines and stores column names for the table t_resource_batch_download. +type TResourceBatchDownloadColumns struct { + Id string // 主键ID + BatchNo string // 批次编号(全局唯一) + TaskName string // 任务名称 + ResourceType string // 资源类型 枚举 + ExportScene string // 导出场景 枚举 + QueryParams string // 导出查询条件快照 + TotalCount string // 预计导出总条数 + SuccessCount string // 已成功导出条数 + FailCount string // 失败条数 + DownloadStatus string // 下载状态 枚举 + OssId string // OSS文件ID + FileName string // 导出文件名 + FileExt string // 文件扩展名 + FileUrl string // 文件访问地址 + FilePath string // 服务器本地路径 + FileSize string // 文件大小(字节) + FileMd5 string // 文件MD5 + DownloadCount string // 下载次数 + LastDownloadTime string // 最后下载时间 + MaxDownloadCount string // 最大下载次数限制 + ErrorMessage string // 失败原因 + DeptId string // 所属机构ID + StartTime string // 任务开始时间 + FinishTime string // 任务完成时间 + ExpireTime string // 文件过期时间 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// tResourceBatchDownloadColumns holds the columns for the table t_resource_batch_download. +var tResourceBatchDownloadColumns = TResourceBatchDownloadColumns{ + Id: "id", + BatchNo: "batch_no", + TaskName: "task_name", + ResourceType: "resource_type", + ExportScene: "export_scene", + QueryParams: "query_params", + TotalCount: "total_count", + SuccessCount: "success_count", + FailCount: "fail_count", + DownloadStatus: "download_status", + OssId: "oss_id", + FileName: "file_name", + FileExt: "file_ext", + FileUrl: "file_url", + FilePath: "file_path", + FileSize: "file_size", + FileMd5: "file_md5", + DownloadCount: "download_count", + LastDownloadTime: "last_download_time", + MaxDownloadCount: "max_download_count", + ErrorMessage: "error_message", + DeptId: "dept_id", + StartTime: "start_time", + FinishTime: "finish_time", + ExpireTime: "expire_time", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTResourceBatchDownloadDao creates and returns a new DAO object for table data access. +func NewTResourceBatchDownloadDao(handlers ...gdb.ModelHandler) *TResourceBatchDownloadDao { + return &TResourceBatchDownloadDao{ + group: "default", + table: "t_resource_batch_download", + columns: tResourceBatchDownloadColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TResourceBatchDownloadDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TResourceBatchDownloadDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TResourceBatchDownloadDao) Columns() TResourceBatchDownloadColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TResourceBatchDownloadDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TResourceBatchDownloadDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TResourceBatchDownloadDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_bind_package_logs.go b/server/internal/dao/internal/t_sim_bind_package_logs.go new file mode 100644 index 0000000..f48b2c9 --- /dev/null +++ b/server/internal/dao/internal/t_sim_bind_package_logs.go @@ -0,0 +1,127 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimBindPackageLogsDao is the data access object for the table t_sim_bind_package_logs. +type TSimBindPackageLogsDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimBindPackageLogsColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimBindPackageLogsColumns defines and stores column names for the table t_sim_bind_package_logs. +type TSimBindPackageLogsColumns struct { + Id string // 主键ID + SubscriptionNo string // 订购记录编号 + BatchNo string // 批次编号 + Iccid string // ICCID + Msisdn string // MSISDN/卡号 + DeptId string // 部门机构编号 + PackageType string // 套餐类型 + PackageCode string // 套餐编号 + PreviousPackageCode string // 变更前套餐编号 + BusinessType string // 类型 枚举 + EffectiveStatus string // 订购状态 枚举 + HandleTime string // 办理时间 + EffectiveTime string // 生效时间 + ExpectedExpireTime string // 预计到期时间 + ActualExpireTime string // 实际到期时间 + DurationMonths string // 套餐时长(月) + GiftDurationMonths string // 赠送时长(月) + OrderCode string // 订单编号 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimBindPackageLogsColumns holds the columns for the table t_sim_bind_package_logs. +var tSimBindPackageLogsColumns = TSimBindPackageLogsColumns{ + Id: "id", + SubscriptionNo: "subscription_no", + BatchNo: "batch_no", + Iccid: "iccid", + Msisdn: "msisdn", + DeptId: "dept_id", + PackageType: "package_type", + PackageCode: "package_code", + PreviousPackageCode: "previous_package_code", + BusinessType: "business_type", + EffectiveStatus: "effective_status", + HandleTime: "handle_time", + EffectiveTime: "effective_time", + ExpectedExpireTime: "expected_expire_time", + ActualExpireTime: "actual_expire_time", + DurationMonths: "duration_months", + GiftDurationMonths: "gift_duration_months", + OrderCode: "order_code", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimBindPackageLogsDao creates and returns a new DAO object for table data access. +func NewTSimBindPackageLogsDao(handlers ...gdb.ModelHandler) *TSimBindPackageLogsDao { + return &TSimBindPackageLogsDao{ + group: "default", + table: "t_sim_bind_package_logs", + columns: tSimBindPackageLogsColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimBindPackageLogsDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimBindPackageLogsDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimBindPackageLogsDao) Columns() TSimBindPackageLogsColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimBindPackageLogsDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimBindPackageLogsDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimBindPackageLogsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_card_bind_pkg.go b/server/internal/dao/internal/t_sim_card_bind_pkg.go new file mode 100644 index 0000000..d4d9f87 --- /dev/null +++ b/server/internal/dao/internal/t_sim_card_bind_pkg.go @@ -0,0 +1,127 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimCardBindPkgDao is the data access object for the table t_sim_card_bind_pkg. +type TSimCardBindPkgDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimCardBindPkgColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimCardBindPkgColumns defines and stores column names for the table t_sim_card_bind_pkg. +type TSimCardBindPkgColumns struct { + Id string // 主键ID + SubscriptionNo string // 订购记录编号 + BatchNo string // 批次编号 + Iccid string // ICCID + Msisdn string // MSISDN/卡号 + DeptId string // 部门机构编号 + PackageType string // 套餐类型 + PackageCode string // 套餐编号 + PreviousPackageCode string // 变更前套餐编号 + BusinessType string // 类型 枚举 + EffectiveStatus string // 订购状态 枚举 + HandleTime string // 办理时间 + EffectiveTime string // 生效时间 + ExpectedExpireTime string // 预计到期时间 + ActualExpireTime string // 实际到期时间 + DurationMonths string // 套餐时长(月) + GiftDurationMonths string // 赠送时长(月) + OrderCode string // 订单编号 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimCardBindPkgColumns holds the columns for the table t_sim_card_bind_pkg. +var tSimCardBindPkgColumns = TSimCardBindPkgColumns{ + Id: "id", + SubscriptionNo: "subscription_no", + BatchNo: "batch_no", + Iccid: "iccid", + Msisdn: "msisdn", + DeptId: "dept_id", + PackageType: "package_type", + PackageCode: "package_code", + PreviousPackageCode: "previous_package_code", + BusinessType: "business_type", + EffectiveStatus: "effective_status", + HandleTime: "handle_time", + EffectiveTime: "effective_time", + ExpectedExpireTime: "expected_expire_time", + ActualExpireTime: "actual_expire_time", + DurationMonths: "duration_months", + GiftDurationMonths: "gift_duration_months", + OrderCode: "order_code", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimCardBindPkgDao creates and returns a new DAO object for table data access. +func NewTSimCardBindPkgDao(handlers ...gdb.ModelHandler) *TSimCardBindPkgDao { + return &TSimCardBindPkgDao{ + group: "default", + table: "t_sim_card_bind_pkg", + columns: tSimCardBindPkgColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimCardBindPkgDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimCardBindPkgDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimCardBindPkgDao) Columns() TSimCardBindPkgColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimCardBindPkgDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimCardBindPkgDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimCardBindPkgDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_card_cancellation.go b/server/internal/dao/internal/t_sim_card_cancellation.go new file mode 100644 index 0000000..0c2c7bc --- /dev/null +++ b/server/internal/dao/internal/t_sim_card_cancellation.go @@ -0,0 +1,191 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimCardCancellationDao is the data access object for the table t_sim_card_cancellation. +type TSimCardCancellationDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimCardCancellationColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimCardCancellationColumns defines and stores column names for the table t_sim_card_cancellation. +type TSimCardCancellationColumns struct { + Id string // 主键ID + Iccid string // ICCID + Msisdn string // MSISDN/卡号 + Imsi string // IMSI + Imei string // IMEI + RegionCode string // 归属地 + DeptId string // 渠道客户ID + CurrentPackageCode string // 当前套餐编号 + PackageStatus string // 套餐状态:0 无套餐,1待生效,2生效中,3已过期 + AccessType string // 接入方式:ipv4、ipv6、ipv4_ipv6 + BossCardStatus string // 运营商卡状态 枚举 + CancelBatchNo string // 系统销户批次号 + CancelledAt string // 系统销户完成时间 + CardType string // 卡类型 枚举 + BossOpenAt string // 运营商开户日期 + BossActivateAt string // 运营商激活日期 + IsBossActivateEnabled string // 是否按运营商激活减去赠送时长方:1是,0否 + InboundBatchNo string // 入库批次 + OutboundBatchNo string // 出库批次 + AllocationBatchNo string // 划拨批次 + IsFlowPool string // 是否为池卡:1是,0否 + FlowPoolCode string // 流量池编号 + FlowPkgBeginAt string // 当前套餐生效时间 + FlowPkgPresentAt string // 套餐赠送结束时间 + FlowPkgRealPresentAt string // 实际赠送结束时间 + FlowPkgEndAt string // 当前套餐预计到期时间 + DataServiceStatus string // 数据服务状态:0启用,1停用 + PackageTotalFlowMb string // 套餐总流量MB + UsedFlowMb string // 已用流量MB + TrafficCutoffStatus string // 达量断网配置状态:0未配置,1已开启,2已关闭 + TrafficCutoffThreshold string // 达量断网阈值 + SmsServiceStatus string // 短信服务状态:0启用,1停用 + PackageTotalSms string // 套餐短信总条数 + UsedSms string // 已用短信条数 + VoiceServiceStatus string // 语音服务状态:0启用,1停用 + PackageTotalVoice string // 套餐语音总分钟数 + UsedVoice string // 已用语音分钟数 + RoamServiceStatus string // 国漫服务状态:0启用,1停用 + PackageTotalRoamFlowMb string // 套餐国际流量MB + UsedRoamFlowMb string // 已用国际流量MB + StatusSyncTime string // 状态最后一次同步时间 + FlowSyncTime string // 流量最后一次同步时间 + SmsSyncTime string // 短信最后一次同步时间 + VoiceSyncTime string // 语音最后一次同步时间 + RoamSyncTime string // 国漫最后一次同步时间 + ExtraFeeEnabled string // 是否收取附加费:0是,1否 + ExtraFeeMultiple string // 附加费用倍数 + ExtraFee string // 附加金额 + ChanKey string // 通道编号 + NetModel string // 网络类型 枚举 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimCardCancellationColumns holds the columns for the table t_sim_card_cancellation. +var tSimCardCancellationColumns = TSimCardCancellationColumns{ + Id: "id", + Iccid: "iccid", + Msisdn: "msisdn", + Imsi: "imsi", + Imei: "imei", + RegionCode: "region_code", + DeptId: "dept_id", + CurrentPackageCode: "current_package_code", + PackageStatus: "package_status", + AccessType: "access_type", + BossCardStatus: "boss_card_status", + CancelBatchNo: "cancel_batch_no", + CancelledAt: "cancelled_at", + CardType: "card_type", + BossOpenAt: "boss_open_at", + BossActivateAt: "boss_activate_at", + IsBossActivateEnabled: "is_boss_activate_enabled", + InboundBatchNo: "inbound_batch_no", + OutboundBatchNo: "outbound_batch_no", + AllocationBatchNo: "allocation_batch_no", + IsFlowPool: "is_flow_pool", + FlowPoolCode: "flow_pool_code", + FlowPkgBeginAt: "flow_pkg_begin_at", + FlowPkgPresentAt: "flow_pkg_present_at", + FlowPkgRealPresentAt: "flow_pkg_real_present_at", + FlowPkgEndAt: "flow_pkg_end_at", + DataServiceStatus: "data_service_status", + PackageTotalFlowMb: "package_total_flow_mb", + UsedFlowMb: "used_flow_mb", + TrafficCutoffStatus: "traffic_cutoff_status", + TrafficCutoffThreshold: "traffic_cutoff_threshold", + SmsServiceStatus: "sms_service_status", + PackageTotalSms: "package_total_sms", + UsedSms: "used_sms", + VoiceServiceStatus: "voice_service_status", + PackageTotalVoice: "package_total_voice", + UsedVoice: "used_voice", + RoamServiceStatus: "roam_service_status", + PackageTotalRoamFlowMb: "package_total_roam_flow_mb", + UsedRoamFlowMb: "used_roam_flow_mb", + StatusSyncTime: "status_sync_time", + FlowSyncTime: "flow_sync_time", + SmsSyncTime: "sms_sync_time", + VoiceSyncTime: "voice_sync_time", + RoamSyncTime: "roam_sync_time", + ExtraFeeEnabled: "extra_fee_enabled", + ExtraFeeMultiple: "extra_fee_multiple", + ExtraFee: "extra_fee", + ChanKey: "chan_key", + NetModel: "net_model", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimCardCancellationDao creates and returns a new DAO object for table data access. +func NewTSimCardCancellationDao(handlers ...gdb.ModelHandler) *TSimCardCancellationDao { + return &TSimCardCancellationDao{ + group: "default", + table: "t_sim_card_cancellation", + columns: tSimCardCancellationColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimCardCancellationDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimCardCancellationDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimCardCancellationDao) Columns() TSimCardCancellationColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimCardCancellationDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimCardCancellationDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimCardCancellationDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_card_info.go b/server/internal/dao/internal/t_sim_card_info.go new file mode 100644 index 0000000..ee05722 --- /dev/null +++ b/server/internal/dao/internal/t_sim_card_info.go @@ -0,0 +1,189 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimCardInfoDao is the data access object for the table t_sim_card_info. +type TSimCardInfoDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimCardInfoColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimCardInfoColumns defines and stores column names for the table t_sim_card_info. +type TSimCardInfoColumns struct { + Id string // 主键ID + Iccid string // ICCID + Msisdn string // MSISDN/卡号 + Imsi string // IMSI + Imei string // IMEI + RegionCode string // 归属地 + DeptId string // 渠道客户ID + CurrentPackageCode string // 当前套餐编号 + PackageStatus string // 套餐状态:0 无套餐,1待生效,2生效中,3已过期 + AccessType string // 接入方式:ipv4、ipv6、ipv4_ipv6 + BossCardStatus string // 运营商卡状态 枚举 + CardType string // 卡类型 枚举 + BossOpenAt string // 运营商开户日期 + BossActivateAt string // 运营商激活日期 + IsBossActivateEnabled string // 是否按运营商激活减去赠送时长方:1是,0否 + InboundBatchNo string // 入库批次 + OutboundBatchNo string // 出库批次 + AllocationBatchNo string // 划拨批次 + IsFlowPool string // 是否为池卡:1是,0否 + FlowPoolCode string // 流量池编号 + OutStockTime string // 出库时间 + FlowPkgBeginAt string // 当前套餐生效时间 + FlowPkgPresentAt string // 套餐赠送结束时间 + FlowPkgRealPresentAt string // 实际赠送结束时间 + FlowPkgEndAt string // 当前套餐预计到期时间 + DataServiceStatus string // 数据服务状态:1启用、0禁用 + PackageTotalFlowMb string // 套餐总流量MB + UsedFlowMb string // 已用流量MB + TrafficCutoffStatus string // 达量断网配置状态:0未配置,1已开启,2已关闭 + TrafficCutoffThreshold string // 达量断网阈值 + SmsServiceStatus string // 短信服务状态:1启用、0禁用 + PackageTotalSms string // 套餐短信总条数 + UsedSms string // 已用短信条数 + VoiceServiceStatus string // 语音服务状态:1启用、0禁用 + PackageTotalVoice string // 套餐语音总分钟数 + UsedVoice string // 已用语音分钟数 + RoamServiceStatus string // 国漫服务状态:1启用、0禁用 + PackageTotalRoamFlowMb string // 套餐国际流量MB + UsedRoamFlowMb string // 已用国际流量MB + StatusSyncTime string // 状态最后一次同步时间 + FlowSyncTime string // 流量最后一次同步时间 + SmsSyncTime string // 短信最后一次同步时间 + VoiceSyncTime string // 语音最后一次同步时间 + RoamSyncTime string // 国漫最后一次同步时间 + ExtraFeeEnabled string // 是否收取附加费:1是,0否 + ExtraFeeMultiple string // 附加费用倍数 + ExtraFee string // 附加金额 + ChanKey string // 通道编号 + NetModel string // 网络类型:4G、5G、NB-IoT + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimCardInfoColumns holds the columns for the table t_sim_card_info. +var tSimCardInfoColumns = TSimCardInfoColumns{ + Id: "id", + Iccid: "iccid", + Msisdn: "msisdn", + Imsi: "imsi", + Imei: "imei", + RegionCode: "region_code", + DeptId: "dept_id", + CurrentPackageCode: "current_package_code", + PackageStatus: "package_status", + AccessType: "access_type", + BossCardStatus: "boss_card_status", + CardType: "card_type", + BossOpenAt: "boss_open_at", + BossActivateAt: "boss_activate_at", + IsBossActivateEnabled: "is_boss_activate_enabled", + InboundBatchNo: "inbound_batch_no", + OutboundBatchNo: "outbound_batch_no", + AllocationBatchNo: "allocation_batch_no", + IsFlowPool: "is_flow_pool", + FlowPoolCode: "flow_pool_code", + OutStockTime: "out_stock_time", + FlowPkgBeginAt: "flow_pkg_begin_at", + FlowPkgPresentAt: "flow_pkg_present_at", + FlowPkgRealPresentAt: "flow_pkg_real_present_at", + FlowPkgEndAt: "flow_pkg_end_at", + DataServiceStatus: "data_service_status", + PackageTotalFlowMb: "package_total_flow_mb", + UsedFlowMb: "used_flow_mb", + TrafficCutoffStatus: "traffic_cutoff_status", + TrafficCutoffThreshold: "traffic_cutoff_threshold", + SmsServiceStatus: "sms_service_status", + PackageTotalSms: "package_total_sms", + UsedSms: "used_sms", + VoiceServiceStatus: "voice_service_status", + PackageTotalVoice: "package_total_voice", + UsedVoice: "used_voice", + RoamServiceStatus: "roam_service_status", + PackageTotalRoamFlowMb: "package_total_roam_flow_mb", + UsedRoamFlowMb: "used_roam_flow_mb", + StatusSyncTime: "status_sync_time", + FlowSyncTime: "flow_sync_time", + SmsSyncTime: "sms_sync_time", + VoiceSyncTime: "voice_sync_time", + RoamSyncTime: "roam_sync_time", + ExtraFeeEnabled: "extra_fee_enabled", + ExtraFeeMultiple: "extra_fee_multiple", + ExtraFee: "extra_fee", + ChanKey: "chan_key", + NetModel: "net_model", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimCardInfoDao creates and returns a new DAO object for table data access. +func NewTSimCardInfoDao(handlers ...gdb.ModelHandler) *TSimCardInfoDao { + return &TSimCardInfoDao{ + group: "default", + table: "t_sim_card_info", + columns: tSimCardInfoColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimCardInfoDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimCardInfoDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimCardInfoDao) Columns() TSimCardInfoColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimCardInfoDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimCardInfoDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimCardInfoDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_card_stock.go b/server/internal/dao/internal/t_sim_card_stock.go new file mode 100644 index 0000000..744b7f8 --- /dev/null +++ b/server/internal/dao/internal/t_sim_card_stock.go @@ -0,0 +1,103 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimCardStockDao is the data access object for the table t_sim_card_stock. +type TSimCardStockDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimCardStockColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimCardStockColumns defines and stores column names for the table t_sim_card_stock. +type TSimCardStockColumns struct { + Id string // 主键ID + BatchNo string // + Iccid string // ICCID + Msisdn string // MSISDN + Imsi string // IMSI + Imei string // IMEI + ChanKey string // 通道编号 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimCardStockColumns holds the columns for the table t_sim_card_stock. +var tSimCardStockColumns = TSimCardStockColumns{ + Id: "id", + BatchNo: "batch_no", + Iccid: "iccid", + Msisdn: "msisdn", + Imsi: "imsi", + Imei: "imei", + ChanKey: "chan_key", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimCardStockDao creates and returns a new DAO object for table data access. +func NewTSimCardStockDao(handlers ...gdb.ModelHandler) *TSimCardStockDao { + return &TSimCardStockDao{ + group: "default", + table: "t_sim_card_stock", + columns: tSimCardStockColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimCardStockDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimCardStockDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimCardStockDao) Columns() TSimCardStockColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimCardStockDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimCardStockDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimCardStockDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_channel_template.go b/server/internal/dao/internal/t_sim_channel_template.go new file mode 100644 index 0000000..168c1a5 --- /dev/null +++ b/server/internal/dao/internal/t_sim_channel_template.go @@ -0,0 +1,103 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimChannelTemplateDao is the data access object for the table t_sim_channel_template. +type TSimChannelTemplateDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimChannelTemplateColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimChannelTemplateColumns defines and stores column names for the table t_sim_channel_template. +type TSimChannelTemplateColumns struct { + Id string // ID + TempKey string // 模板标识 + TempName string // 模板名称 + TempModel string // 模板模型 + TempConfig string // 模板配置 + TempStatus string // 模板状态(0未启用,1启用) + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimChannelTemplateColumns holds the columns for the table t_sim_channel_template. +var tSimChannelTemplateColumns = TSimChannelTemplateColumns{ + Id: "id", + TempKey: "temp_key", + TempName: "temp_name", + TempModel: "temp_model", + TempConfig: "temp_config", + TempStatus: "temp_status", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimChannelTemplateDao creates and returns a new DAO object for table data access. +func NewTSimChannelTemplateDao(handlers ...gdb.ModelHandler) *TSimChannelTemplateDao { + return &TSimChannelTemplateDao{ + group: "default", + table: "t_sim_channel_template", + columns: tSimChannelTemplateColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimChannelTemplateDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimChannelTemplateDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimChannelTemplateDao) Columns() TSimChannelTemplateColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimChannelTemplateDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimChannelTemplateDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimChannelTemplateDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_channels.go b/server/internal/dao/internal/t_sim_channels.go new file mode 100644 index 0000000..18ed9d4 --- /dev/null +++ b/server/internal/dao/internal/t_sim_channels.go @@ -0,0 +1,127 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimChannelsDao is the data access object for the table t_sim_channels. +type TSimChannelsDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimChannelsColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimChannelsColumns defines and stores column names for the table t_sim_channels. +type TSimChannelsColumns struct { + Id string // ID + TempKey string // 模板标识 + ChanKey string // 通道编号 + ChanName string // 通道名称 + ChanConfig string // 通道配置 + ChanStatus string // 通道状态(0启用,1停用, 2未申请, 3申请中, 4接口信息不全) + ShareEnabled string // 是否开放共享Token(0否,1是) + ChanAlias string // 通道别名 + RegionCode string // 通道行政区域代码(省市) + AppKey string // 应用标识 + AppSecret string // 应用密钥 + AccessToken string // 接口调用凭证 + AccessTokenCreated string // 凭证申请时间 + AccessTokenExpire string // 凭证过期时间 + RefreshToken string // 刷新凭证 + LoginUser string // 登录账号 + LoginPassword string // 登录密码 + LoginUrl string // 登录地址 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimChannelsColumns holds the columns for the table t_sim_channels. +var tSimChannelsColumns = TSimChannelsColumns{ + Id: "id", + TempKey: "temp_key", + ChanKey: "chan_key", + ChanName: "chan_name", + ChanConfig: "chan_config", + ChanStatus: "chan_status", + ShareEnabled: "share_enabled", + ChanAlias: "chan_alias", + RegionCode: "region_code", + AppKey: "app_key", + AppSecret: "app_secret", + AccessToken: "access_token", + AccessTokenCreated: "access_token_created", + AccessTokenExpire: "access_token_expire", + RefreshToken: "refresh_token", + LoginUser: "login_user", + LoginPassword: "login_password", + LoginUrl: "login_url", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimChannelsDao creates and returns a new DAO object for table data access. +func NewTSimChannelsDao(handlers ...gdb.ModelHandler) *TSimChannelsDao { + return &TSimChannelsDao{ + group: "default", + table: "t_sim_channels", + columns: tSimChannelsColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimChannelsDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimChannelsDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimChannelsDao) Columns() TSimChannelsColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimChannelsDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimChannelsDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimChannelsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_flow_package_info.go b/server/internal/dao/internal/t_sim_flow_package_info.go new file mode 100644 index 0000000..3df80c4 --- /dev/null +++ b/server/internal/dao/internal/t_sim_flow_package_info.go @@ -0,0 +1,143 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimFlowPackageInfoDao is the data access object for the table t_sim_flow_package_info. +type TSimFlowPackageInfoDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimFlowPackageInfoColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimFlowPackageInfoColumns defines and stores column names for the table t_sim_flow_package_info. +type TSimFlowPackageInfoColumns struct { + Id string // 主键ID + PackageCode string // 套餐编码 + CategoryCode string // 套餐分类编码 + PoolPackageCode string // 池套餐 + PackageName string // 套餐名称 + PackageType string // 套餐类型:101-卡套餐 102-池套餐 103-单卡加油包 104-池卡加油包 + BillingMethod string // 计费方式 + SortNo string // 排序号 + FlowMb string // 套餐流量(MB) + FlowGb string // 套餐流量(GB) + Enabled string // 是否启用:0-否 1-是(yesno_enum) + ListPrice string // 目录售价 + CostPrice string // 成本价 + SysDefaultProfit string // 系统默认利润 + SysDefaultRatio string // 系统默认系数 + TerminalCost string // 终端成本 + SysDefaultTerminalProfit string // 系统默认终端利润 + SysDefaultTerminalRatio string // 系统默认终端系数 + RenewableDurationConfig string // 可续费时长配置:[1,2,3] + DiscountRateConfig string // 折扣率配置:{"duration":1,"rate":100} + RenewablePackageCodes string // 可续费套餐编码列表 + ChangeablePackageCodes string // 可变更套餐编码列表 + TopupPackageCodes string // 可订购加油包套餐编码列表 + IsAutoRenew string // 是否自动续费:0-否 1-是 + AutoRenewPackageId string // 自动续费套餐ID + AutoRenewYears string // 自动续费年限 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// tSimFlowPackageInfoColumns holds the columns for the table t_sim_flow_package_info. +var tSimFlowPackageInfoColumns = TSimFlowPackageInfoColumns{ + Id: "id", + PackageCode: "package_code", + CategoryCode: "category_code", + PoolPackageCode: "pool_package_code", + PackageName: "package_name", + PackageType: "package_type", + BillingMethod: "billing_method", + SortNo: "sort_no", + FlowMb: "flow_mb", + FlowGb: "flow_gb", + Enabled: "enabled", + ListPrice: "list_price", + CostPrice: "cost_price", + SysDefaultProfit: "sys_default_profit", + SysDefaultRatio: "sys_default_ratio", + TerminalCost: "terminal_cost", + SysDefaultTerminalProfit: "sys_default_terminal_profit", + SysDefaultTerminalRatio: "sys_default_terminal_ratio", + RenewableDurationConfig: "renewable_duration_config", + DiscountRateConfig: "discount_rate_config", + RenewablePackageCodes: "renewable_package_codes", + ChangeablePackageCodes: "changeable_package_codes", + TopupPackageCodes: "topup_package_codes", + IsAutoRenew: "is_auto_renew", + AutoRenewPackageId: "auto_renew_package_id", + AutoRenewYears: "auto_renew_years", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimFlowPackageInfoDao creates and returns a new DAO object for table data access. +func NewTSimFlowPackageInfoDao(handlers ...gdb.ModelHandler) *TSimFlowPackageInfoDao { + return &TSimFlowPackageInfoDao{ + group: "default", + table: "t_sim_flow_package_info", + columns: tSimFlowPackageInfoColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimFlowPackageInfoDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimFlowPackageInfoDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimFlowPackageInfoDao) Columns() TSimFlowPackageInfoColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimFlowPackageInfoDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimFlowPackageInfoDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimFlowPackageInfoDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_flow_package_price.go b/server/internal/dao/internal/t_sim_flow_package_price.go new file mode 100644 index 0000000..a709a3e --- /dev/null +++ b/server/internal/dao/internal/t_sim_flow_package_price.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimFlowPackagePriceDao is the data access object for the table t_sim_flow_package_price. +type TSimFlowPackagePriceDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimFlowPackagePriceColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimFlowPackagePriceColumns defines and stores column names for the table t_sim_flow_package_price. +type TSimFlowPackagePriceColumns struct { + Id string // 主键ID + PackageCode string // 套餐编码 + AgentScopeType string // 渠道适用类型:0-所有客户 1-指定客户 + TargetAgentId string // 指定渠道客户ID + DeptId string // 机构编号 + AgentCostRatio string // 渠道成本系数 + AgentCostProfit string // 渠道成本利润 + UserCostRatio string // 终端用户成本系数 + UserCostProfit string // 终端用户成本利润 + Enabled string // 是否启用:0-否 1-是(yesno_enum) + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 +} + +// tSimFlowPackagePriceColumns holds the columns for the table t_sim_flow_package_price. +var tSimFlowPackagePriceColumns = TSimFlowPackagePriceColumns{ + Id: "id", + PackageCode: "package_code", + AgentScopeType: "agent_scope_type", + TargetAgentId: "target_agent_id", + DeptId: "dept_id", + AgentCostRatio: "agent_cost_ratio", + AgentCostProfit: "agent_cost_profit", + UserCostRatio: "user_cost_ratio", + UserCostProfit: "user_cost_profit", + Enabled: "enabled", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimFlowPackagePriceDao creates and returns a new DAO object for table data access. +func NewTSimFlowPackagePriceDao(handlers ...gdb.ModelHandler) *TSimFlowPackagePriceDao { + return &TSimFlowPackagePriceDao{ + group: "default", + table: "t_sim_flow_package_price", + columns: tSimFlowPackagePriceColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimFlowPackagePriceDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimFlowPackagePriceDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimFlowPackagePriceDao) Columns() TSimFlowPackagePriceColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimFlowPackagePriceDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimFlowPackagePriceDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimFlowPackagePriceDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_flow_pool.go b/server/internal/dao/internal/t_sim_flow_pool.go new file mode 100644 index 0000000..72b890a --- /dev/null +++ b/server/internal/dao/internal/t_sim_flow_pool.go @@ -0,0 +1,119 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimFlowPoolDao is the data access object for the table t_sim_flow_pool. +type TSimFlowPoolDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimFlowPoolColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimFlowPoolColumns defines and stores column names for the table t_sim_flow_pool. +type TSimFlowPoolColumns struct { + Id string // 主键ID + FlowPoolCode string // 流量池编号 + DeptId string // 客户编号 + PackageCodes string // 套餐编号列表(冗余) + PoolPackageCode string // 池套餐编码 + CardCount string // 关联卡数量 + CardFlowMb string // 套餐卡流量MB + PoolPackageFlowMb string // 套内总流量MB + AddOnFlowMb string // 加油包总流量MB + TotalFlowMb string // 总流量MB(套内+加油包) + UsedFlowMb string // 已使用流量MB + RemainFlowMb string // 剩余流量MB + LastStatTime string // 最后流量统计时间 + PoolStatus string // 状态:0 正常、1 停用、2 锁定 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimFlowPoolColumns holds the columns for the table t_sim_flow_pool. +var tSimFlowPoolColumns = TSimFlowPoolColumns{ + Id: "id", + FlowPoolCode: "flow_pool_code", + DeptId: "dept_id", + PackageCodes: "package_codes", + PoolPackageCode: "pool_package_code", + CardCount: "card_count", + CardFlowMb: "card_flow_mb", + PoolPackageFlowMb: "pool_package_flow_mb", + AddOnFlowMb: "add_on_flow_mb", + TotalFlowMb: "total_flow_mb", + UsedFlowMb: "used_flow_mb", + RemainFlowMb: "remain_flow_mb", + LastStatTime: "last_stat_time", + PoolStatus: "pool_status", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimFlowPoolDao creates and returns a new DAO object for table data access. +func NewTSimFlowPoolDao(handlers ...gdb.ModelHandler) *TSimFlowPoolDao { + return &TSimFlowPoolDao{ + group: "default", + table: "t_sim_flow_pool", + columns: tSimFlowPoolColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimFlowPoolDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimFlowPoolDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimFlowPoolDao) Columns() TSimFlowPoolColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimFlowPoolDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimFlowPoolDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimFlowPoolDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_package_category.go b/server/internal/dao/internal/t_sim_package_category.go new file mode 100644 index 0000000..39d5703 --- /dev/null +++ b/server/internal/dao/internal/t_sim_package_category.go @@ -0,0 +1,99 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimPackageCategoryDao is the data access object for the table t_sim_package_category. +type TSimPackageCategoryDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimPackageCategoryColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimPackageCategoryColumns defines and stores column names for the table t_sim_package_category. +type TSimPackageCategoryColumns struct { + Id string // 主键ID + CategoryCode string // 套餐分类编号 + CategoryName string // 套餐分类名称 + SortNo string // 排序 + Enabled string // 是否启用:1启用,0停用 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimPackageCategoryColumns holds the columns for the table t_sim_package_category. +var tSimPackageCategoryColumns = TSimPackageCategoryColumns{ + Id: "id", + CategoryCode: "category_code", + CategoryName: "category_name", + SortNo: "sort_no", + Enabled: "enabled", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimPackageCategoryDao creates and returns a new DAO object for table data access. +func NewTSimPackageCategoryDao(handlers ...gdb.ModelHandler) *TSimPackageCategoryDao { + return &TSimPackageCategoryDao{ + group: "default", + table: "t_sim_package_category", + columns: tSimPackageCategoryColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimPackageCategoryDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimPackageCategoryDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimPackageCategoryDao) Columns() TSimPackageCategoryColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimPackageCategoryDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimPackageCategoryDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimPackageCategoryDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_pool.go b/server/internal/dao/internal/t_sim_pool.go new file mode 100644 index 0000000..dad1053 --- /dev/null +++ b/server/internal/dao/internal/t_sim_pool.go @@ -0,0 +1,117 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimPoolDao is the data access object for the table t_sim_pool. +type TSimPoolDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimPoolColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimPoolColumns defines and stores column names for the table t_sim_pool. +type TSimPoolColumns struct { + Id string // 主键ID + FlowPoolCode string // 流量池编号 + DeptId string // 客户编号 + PackageCodes string // 套餐编号列表(冗余) + PoolPackageCode string // 池套餐编码 + CardCount string // 关联卡数量 + CardFlowMb string // 套餐卡流量MB + PoolPackageFlowMb string // 套内总流量MB + AddOnFlowMb string // 加油包总流量MB + TotalFlowMb string // 总流量MB(套内+加油包) + UsedFlowMb string // 已使用流量MB + RemainFlowMb string // 剩余流量MB + Status string // 状态:0 正常、1 停用、2 锁定 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimPoolColumns holds the columns for the table t_sim_pool. +var tSimPoolColumns = TSimPoolColumns{ + Id: "id", + FlowPoolCode: "flow_pool_code", + DeptId: "dept_id", + PackageCodes: "package_codes", + PoolPackageCode: "pool_package_code", + CardCount: "card_count", + CardFlowMb: "card_flow_mb", + PoolPackageFlowMb: "pool_package_flow_mb", + AddOnFlowMb: "add_on_flow_mb", + TotalFlowMb: "total_flow_mb", + UsedFlowMb: "used_flow_mb", + RemainFlowMb: "remain_flow_mb", + Status: "status", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimPoolDao creates and returns a new DAO object for table data access. +func NewTSimPoolDao(handlers ...gdb.ModelHandler) *TSimPoolDao { + return &TSimPoolDao{ + group: "default", + table: "t_sim_pool", + columns: tSimPoolColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimPoolDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimPoolDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimPoolDao) Columns() TSimPoolColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimPoolDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimPoolDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimPoolDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_pool_bind_package.go b/server/internal/dao/internal/t_sim_pool_bind_package.go new file mode 100644 index 0000000..7ce2242 --- /dev/null +++ b/server/internal/dao/internal/t_sim_pool_bind_package.go @@ -0,0 +1,123 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimPoolBindPackageDao is the data access object for the table t_sim_pool_bind_package. +type TSimPoolBindPackageDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimPoolBindPackageColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimPoolBindPackageColumns defines and stores column names for the table t_sim_pool_bind_package. +type TSimPoolBindPackageColumns struct { + Id string // 主键ID + SubscriptionNo string // 订购记录编号 + PoolNo string // 流量池编号 + DeptId string // 部门机构编号 + PackageType string // 套餐类型 + PackageCode string // 套餐编号 + PreviousPackageCode string // 变更前套餐编号 + BusinessType string // 类型 枚举 + EffectiveStatus string // 订购状态 枚举 + HandleTime string // 办理时间 + EffectiveTime string // 生效时间 + ExpectedExpireTime string // 预计到期时间 + ActualExpireTime string // 实际到期时间 + DurationMonths string // 套餐时长(月) + GiftDurationMonths string // 赠送时长(月) + OrderCode string // 订单编号 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimPoolBindPackageColumns holds the columns for the table t_sim_pool_bind_package. +var tSimPoolBindPackageColumns = TSimPoolBindPackageColumns{ + Id: "id", + SubscriptionNo: "subscription_no", + PoolNo: "pool_no", + DeptId: "dept_id", + PackageType: "package_type", + PackageCode: "package_code", + PreviousPackageCode: "previous_package_code", + BusinessType: "business_type", + EffectiveStatus: "effective_status", + HandleTime: "handle_time", + EffectiveTime: "effective_time", + ExpectedExpireTime: "expected_expire_time", + ActualExpireTime: "actual_expire_time", + DurationMonths: "duration_months", + GiftDurationMonths: "gift_duration_months", + OrderCode: "order_code", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimPoolBindPackageDao creates and returns a new DAO object for table data access. +func NewTSimPoolBindPackageDao(handlers ...gdb.ModelHandler) *TSimPoolBindPackageDao { + return &TSimPoolBindPackageDao{ + group: "default", + table: "t_sim_pool_bind_package", + columns: tSimPoolBindPackageColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimPoolBindPackageDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimPoolBindPackageDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimPoolBindPackageDao) Columns() TSimPoolBindPackageColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimPoolBindPackageDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimPoolBindPackageDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimPoolBindPackageDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_pool_bind_pkg_logs.go b/server/internal/dao/internal/t_sim_pool_bind_pkg_logs.go new file mode 100644 index 0000000..d79e2fe --- /dev/null +++ b/server/internal/dao/internal/t_sim_pool_bind_pkg_logs.go @@ -0,0 +1,123 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimPoolBindPkgLogsDao is the data access object for the table t_sim_pool_bind_pkg_logs. +type TSimPoolBindPkgLogsDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimPoolBindPkgLogsColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimPoolBindPkgLogsColumns defines and stores column names for the table t_sim_pool_bind_pkg_logs. +type TSimPoolBindPkgLogsColumns struct { + Id string // 主键ID + SubscriptionNo string // 订购记录编号 + PoolNo string // 流量池编号 + DeptId string // 部门机构编号 + PackageType string // 套餐类型 + PackageCode string // 套餐编号 + PreviousPackageCode string // 变更前套餐编号 + BusinessType string // 类型 枚举 + EffectiveStatus string // 订购状态 枚举 + HandleTime string // 办理时间 + EffectiveTime string // 生效时间 + ExpectedExpireTime string // 预计到期时间 + ActualExpireTime string // 实际到期时间 + DurationMonths string // 套餐时长(月) + GiftDurationMonths string // 赠送时长(月) + OrderCode string // 订单编号 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimPoolBindPkgLogsColumns holds the columns for the table t_sim_pool_bind_pkg_logs. +var tSimPoolBindPkgLogsColumns = TSimPoolBindPkgLogsColumns{ + Id: "id", + SubscriptionNo: "subscription_no", + PoolNo: "pool_no", + DeptId: "dept_id", + PackageType: "package_type", + PackageCode: "package_code", + PreviousPackageCode: "previous_package_code", + BusinessType: "business_type", + EffectiveStatus: "effective_status", + HandleTime: "handle_time", + EffectiveTime: "effective_time", + ExpectedExpireTime: "expected_expire_time", + ActualExpireTime: "actual_expire_time", + DurationMonths: "duration_months", + GiftDurationMonths: "gift_duration_months", + OrderCode: "order_code", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimPoolBindPkgLogsDao creates and returns a new DAO object for table data access. +func NewTSimPoolBindPkgLogsDao(handlers ...gdb.ModelHandler) *TSimPoolBindPkgLogsDao { + return &TSimPoolBindPkgLogsDao{ + group: "default", + table: "t_sim_pool_bind_pkg_logs", + columns: tSimPoolBindPkgLogsColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimPoolBindPkgLogsDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimPoolBindPkgLogsDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimPoolBindPkgLogsDao) Columns() TSimPoolBindPkgLogsColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimPoolBindPkgLogsDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimPoolBindPkgLogsDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimPoolBindPkgLogsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_pool_warn_config.go b/server/internal/dao/internal/t_sim_pool_warn_config.go new file mode 100644 index 0000000..2f4a8c4 --- /dev/null +++ b/server/internal/dao/internal/t_sim_pool_warn_config.go @@ -0,0 +1,109 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimPoolWarnConfigDao is the data access object for the table t_sim_pool_warn_config. +type TSimPoolWarnConfigDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimPoolWarnConfigColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimPoolWarnConfigColumns defines and stores column names for the table t_sim_pool_warn_config. +type TSimPoolWarnConfigColumns struct { + Id string // 主键ID + PoolId string // 流量池ID + WarnLevel string // 预警等级:1提示,2注意,3警告,4危险,5紧急 + ThresholdPercent string // 阈值(例如0.7) + WarnMethods string // 预警方式:0短信,1邮箱,2电话,3平台消息 多选逗号分隔 + WarnPhones string // 预警手机号 多个逗号分隔 + WarnEmails string // 预警邮箱 多个逗号分隔 + AutoAction string // 自动动作:0无,1限速,2断网 + IsEnabled string // 是否启用:1启用,0停用 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimPoolWarnConfigColumns holds the columns for the table t_sim_pool_warn_config. +var tSimPoolWarnConfigColumns = TSimPoolWarnConfigColumns{ + Id: "id", + PoolId: "pool_id", + WarnLevel: "warn_level", + ThresholdPercent: "threshold_percent", + WarnMethods: "warn_methods", + WarnPhones: "warn_phones", + WarnEmails: "warn_emails", + AutoAction: "auto_action", + IsEnabled: "is_enabled", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimPoolWarnConfigDao creates and returns a new DAO object for table data access. +func NewTSimPoolWarnConfigDao(handlers ...gdb.ModelHandler) *TSimPoolWarnConfigDao { + return &TSimPoolWarnConfigDao{ + group: "default", + table: "t_sim_pool_warn_config", + columns: tSimPoolWarnConfigColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimPoolWarnConfigDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimPoolWarnConfigDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimPoolWarnConfigDao) Columns() TSimPoolWarnConfigColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimPoolWarnConfigDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimPoolWarnConfigDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimPoolWarnConfigDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_pool_warn_log.go b/server/internal/dao/internal/t_sim_pool_warn_log.go new file mode 100644 index 0000000..fbe3a37 --- /dev/null +++ b/server/internal/dao/internal/t_sim_pool_warn_log.go @@ -0,0 +1,115 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimPoolWarnLogDao is the data access object for the table t_sim_pool_warn_log. +type TSimPoolWarnLogDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimPoolWarnLogColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimPoolWarnLogColumns defines and stores column names for the table t_sim_pool_warn_log. +type TSimPoolWarnLogColumns struct { + Id string // 主键ID + PoolId string // 流量池ID + WarnLevel string // 预警级别 + ThresholdPercent string // 触发阈值百分比(例如0.7) + UsedPercent string // 实际使用百分比 + UsedFlowMb string // 触发时已用流量MB + TotalFlowMb string // 触发时总流量MB + WarnMethods string // 预警方式:0短信,1邮箱,2电话,3平台消息 + WarnPhones string // 预警手机号 + WarnEmails string // 预警邮箱 + WarnStatus string // 预警状态:0成功,1失败 + WarnTime string // 预警时间 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tSimPoolWarnLogColumns holds the columns for the table t_sim_pool_warn_log. +var tSimPoolWarnLogColumns = TSimPoolWarnLogColumns{ + Id: "id", + PoolId: "pool_id", + WarnLevel: "warn_level", + ThresholdPercent: "threshold_percent", + UsedPercent: "used_percent", + UsedFlowMb: "used_flow_mb", + TotalFlowMb: "total_flow_mb", + WarnMethods: "warn_methods", + WarnPhones: "warn_phones", + WarnEmails: "warn_emails", + WarnStatus: "warn_status", + WarnTime: "warn_time", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTSimPoolWarnLogDao creates and returns a new DAO object for table data access. +func NewTSimPoolWarnLogDao(handlers ...gdb.ModelHandler) *TSimPoolWarnLogDao { + return &TSimPoolWarnLogDao{ + group: "default", + table: "t_sim_pool_warn_log", + columns: tSimPoolWarnLogColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimPoolWarnLogDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimPoolWarnLogDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimPoolWarnLogDao) Columns() TSimPoolWarnLogColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimPoolWarnLogDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimPoolWarnLogDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimPoolWarnLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_terminal.go b/server/internal/dao/internal/t_sim_terminal.go new file mode 100644 index 0000000..40bc881 --- /dev/null +++ b/server/internal/dao/internal/t_sim_terminal.go @@ -0,0 +1,107 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimTerminalDao is the data access object for the table t_sim_terminal. +type TSimTerminalDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimTerminalColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimTerminalColumns defines and stores column names for the table t_sim_terminal. +type TSimTerminalColumns struct { + Id string // 主键ID + Iccid string // ICCID + Msisdn string // MSISDN/卡号 + Imsi string // IMSI + TerminalId string // 终端客户ID + TerminalName string // 终端客户名称 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门ID + CreatedBy string // 创建者ID + CreatedAt string // 创建时间 + UpdatedBy string // 更新者ID + UpdatedAt string // 更新时间 + DeptId string // 渠道客户ID + CurrentPackageCode string // 当前套餐编号 +} + +// tSimTerminalColumns holds the columns for the table t_sim_terminal. +var tSimTerminalColumns = TSimTerminalColumns{ + Id: "id", + Iccid: "iccid", + Msisdn: "msisdn", + Imsi: "imsi", + TerminalId: "terminal_id", + TerminalName: "terminal_name", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeptId: "dept_id", + CurrentPackageCode: "current_package_code", +} + +// NewTSimTerminalDao creates and returns a new DAO object for table data access. +func NewTSimTerminalDao(handlers ...gdb.ModelHandler) *TSimTerminalDao { + return &TSimTerminalDao{ + group: "default", + table: "t_sim_terminal", + columns: tSimTerminalColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimTerminalDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimTerminalDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimTerminalDao) Columns() TSimTerminalColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimTerminalDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimTerminalDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimTerminalDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_sim_upstream_package.go b/server/internal/dao/internal/t_sim_upstream_package.go new file mode 100644 index 0000000..377ea48 --- /dev/null +++ b/server/internal/dao/internal/t_sim_upstream_package.go @@ -0,0 +1,101 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TSimUpstreamPackageDao is the data access object for the table t_sim_upstream_package. +type TSimUpstreamPackageDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TSimUpstreamPackageColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TSimUpstreamPackageColumns defines and stores column names for the table t_sim_upstream_package. +type TSimUpstreamPackageColumns struct { + Id string // 主键 + UpPkgCode string // 上游套餐编码 + UpPkgName string // 上游套餐名称 + Operator string // 运营商:1移动,2联通,3电信 + ApnName string // APN名称 + Remark string // 备注 + TenantId string // 租户编号 + CreatedDept string // 创建部门 + CreatedAt string // 创建时间 + CreatedBy string // 创建者 + UpdatedAt string // 更新时间 + UpdatedBy string // 更新者 +} + +// tSimUpstreamPackageColumns holds the columns for the table t_sim_upstream_package. +var tSimUpstreamPackageColumns = TSimUpstreamPackageColumns{ + Id: "id", + UpPkgCode: "up_pkg_code", + UpPkgName: "up_pkg_name", + Operator: "operator", + ApnName: "apn_name", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", +} + +// NewTSimUpstreamPackageDao creates and returns a new DAO object for table data access. +func NewTSimUpstreamPackageDao(handlers ...gdb.ModelHandler) *TSimUpstreamPackageDao { + return &TSimUpstreamPackageDao{ + group: "default", + table: "t_sim_upstream_package", + columns: tSimUpstreamPackageColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TSimUpstreamPackageDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TSimUpstreamPackageDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TSimUpstreamPackageDao) Columns() TSimUpstreamPackageColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TSimUpstreamPackageDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TSimUpstreamPackageDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TSimUpstreamPackageDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_terminal.go b/server/internal/dao/internal/t_terminal.go new file mode 100644 index 0000000..fbe21ca --- /dev/null +++ b/server/internal/dao/internal/t_terminal.go @@ -0,0 +1,129 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TTerminalDao is the data access object for the table t_terminal. +type TTerminalDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TTerminalColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TTerminalColumns defines and stores column names for the table t_terminal. +type TTerminalColumns struct { + TerminalId string // 终端客户id + TerminalName string // 客户名称 + Leader string // 负责人 + Phone string // 联系电话 + Email string // 邮箱 + Status string // 客户状态(0正常 1停用) + CreditCode string // 统一社会信用代码 + AreaCode string // 行政区域代码 + Address string // 地址 + FullName string // 单位全称 + Industry string // 行业 + LegalPersonName string // 法人名称 + LegalPersonIdCard string // 法人身份证号 + CompanyType string // 单位类型 + EstablishTime string // 单位成立时间 + BankName string // 开户行名称 + BankAccount string // 开户行账号 + Balance string // 账户余额 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 +} + +// tTerminalColumns holds the columns for the table t_terminal. +var tTerminalColumns = TTerminalColumns{ + TerminalId: "terminal_id", + TerminalName: "terminal_name", + Leader: "leader", + Phone: "phone", + Email: "email", + Status: "status", + CreditCode: "credit_code", + AreaCode: "area_code", + Address: "address", + FullName: "full_name", + Industry: "industry", + LegalPersonName: "legal_person_name", + LegalPersonIdCard: "legal_person_id_card", + CompanyType: "company_type", + EstablishTime: "establish_time", + BankName: "bank_name", + BankAccount: "bank_account", + Balance: "balance", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", +} + +// NewTTerminalDao creates and returns a new DAO object for table data access. +func NewTTerminalDao(handlers ...gdb.ModelHandler) *TTerminalDao { + return &TTerminalDao{ + group: "default", + table: "t_terminal", + columns: tTerminalColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TTerminalDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TTerminalDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TTerminalDao) Columns() TTerminalColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TTerminalDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TTerminalDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TTerminalDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_tplatform_setting.go b/server/internal/dao/internal/t_tplatform_setting.go new file mode 100644 index 0000000..0fb27f2 --- /dev/null +++ b/server/internal/dao/internal/t_tplatform_setting.go @@ -0,0 +1,111 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TTplatformSettingDao is the data access object for the table t_tplatform_setting. +type TTplatformSettingDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TTplatformSettingColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TTplatformSettingColumns defines and stores column names for the table t_tplatform_setting. +type TTplatformSettingColumns struct { + Id string // 主键ID + SubDomain string // 二级域名前缀 + FullDomain string // 完整域名 + AppTitle string // 平台标题 + AppIcon string // 图标地址 + LoginPageTitle string // 登录页大标题 + LoginPageDescription string // 登录页描述文案 + LoginSloganImage string // 登录标语图地址 + Sort string // 排序 + Status string // 状态 0启用 1禁用 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tTplatformSettingColumns holds the columns for the table t_tplatform_setting. +var tTplatformSettingColumns = TTplatformSettingColumns{ + Id: "id", + SubDomain: "sub_domain", + FullDomain: "full_domain", + AppTitle: "app_title", + AppIcon: "app_icon", + LoginPageTitle: "login_page_title", + LoginPageDescription: "login_page_description", + LoginSloganImage: "login_slogan_image", + Sort: "sort", + Status: "status", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTTplatformSettingDao creates and returns a new DAO object for table data access. +func NewTTplatformSettingDao(handlers ...gdb.ModelHandler) *TTplatformSettingDao { + return &TTplatformSettingDao{ + group: "default", + table: "t_tplatform_setting", + columns: tTplatformSettingColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TTplatformSettingDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TTplatformSettingDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TTplatformSettingDao) Columns() TTplatformSettingColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TTplatformSettingDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TTplatformSettingDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TTplatformSettingDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_user_message.go b/server/internal/dao/internal/t_user_message.go new file mode 100644 index 0000000..9f387ab --- /dev/null +++ b/server/internal/dao/internal/t_user_message.go @@ -0,0 +1,97 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TUserMessageDao is the data access object for the table t_user_message. +type TUserMessageDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TUserMessageColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TUserMessageColumns defines and stores column names for the table t_user_message. +type TUserMessageColumns struct { + MessageId string // 主键ID + MessageLogId string // 消息日志ID + UserId string // 用户ID + IsRead string // 是否已读 0未读 1已读 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tUserMessageColumns holds the columns for the table t_user_message. +var tUserMessageColumns = TUserMessageColumns{ + MessageId: "message_id", + MessageLogId: "message_log_id", + UserId: "user_id", + IsRead: "is_read", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTUserMessageDao creates and returns a new DAO object for table data access. +func NewTUserMessageDao(handlers ...gdb.ModelHandler) *TUserMessageDao { + return &TUserMessageDao{ + group: "default", + table: "t_user_message", + columns: tUserMessageColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TUserMessageDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TUserMessageDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TUserMessageDao) Columns() TUserMessageColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TUserMessageDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TUserMessageDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TUserMessageDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_warn_config.go b/server/internal/dao/internal/t_warn_config.go new file mode 100644 index 0000000..1747b93 --- /dev/null +++ b/server/internal/dao/internal/t_warn_config.go @@ -0,0 +1,123 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TWarnConfigDao is the data access object for the table t_warn_config. +type TWarnConfigDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TWarnConfigColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TWarnConfigColumns defines and stores column names for the table t_warn_config. +type TWarnConfigColumns struct { + Id string // 主键ID + TenantId string // 租户ID + BatchNo string // 批次编号 + ItemType string // 对象类型:1卡,2流量池 + ItemId string // 对象ID:单卡为t_sim_card_info.id,流量池为t_sim_flow_pool.id + ItemCode string // 对象编码快照:ICCID/MSISDN/流量池编码 + WarnUnit string // 预警单位:1MB,2百分比,3天数,4月数 + FirstWarn string // 第一档预警阈值,单位由warn_unit决定 + SecondWarn string // 第二档预警阈值,单位由warn_unit决定 + ThirdWarn string // 第三档预警阈值,单位由warn_unit决定 + WarnMethods string // 预警方式:0短信,1邮箱,2电话,3平台消息;多选逗号分隔 + WarnPhones string // 预警手机号,多个逗号分隔 + WarnEmails string // 预警邮箱,多个逗号分隔 + AutoAction string // 自动动作:0无,2断网;第一期只保存配置不自动执行 + IsEnabled string // 是否启用:1启用,0停用 + WxTemplateId string // 微信订阅消息模板ID + Remark string // 备注 + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 + AppNotifyEnabled string // 是否开启小程序预警通知:0关闭,1开启 +} + +// tWarnConfigColumns holds the columns for the table t_warn_config. +var tWarnConfigColumns = TWarnConfigColumns{ + Id: "id", + TenantId: "tenant_id", + BatchNo: "batch_no", + ItemType: "item_type", + ItemId: "item_id", + ItemCode: "item_code", + WarnUnit: "warn_unit", + FirstWarn: "first_warn", + SecondWarn: "second_warn", + ThirdWarn: "third_warn", + WarnMethods: "warn_methods", + WarnPhones: "warn_phones", + WarnEmails: "warn_emails", + AutoAction: "auto_action", + IsEnabled: "is_enabled", + WxTemplateId: "wx_template_id", + Remark: "remark", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", + AppNotifyEnabled: "app_notify_enabled", +} + +// NewTWarnConfigDao creates and returns a new DAO object for table data access. +func NewTWarnConfigDao(handlers ...gdb.ModelHandler) *TWarnConfigDao { + return &TWarnConfigDao{ + group: "default", + table: "t_warn_config", + columns: tWarnConfigColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TWarnConfigDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TWarnConfigDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TWarnConfigDao) Columns() TWarnConfigColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TWarnConfigDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TWarnConfigDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TWarnConfigDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_warn_log.go b/server/internal/dao/internal/t_warn_log.go new file mode 100644 index 0000000..97cba18 --- /dev/null +++ b/server/internal/dao/internal/t_warn_log.go @@ -0,0 +1,125 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TWarnLogDao is the data access object for the table t_warn_log. +type TWarnLogDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TWarnLogColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TWarnLogColumns defines and stores column names for the table t_warn_log. +type TWarnLogColumns struct { + Id string // 主键ID + TenantId string // 租户ID + ConfigId string // 预警配置ID + ItemType string // 对象类型:1单卡,2流量池 + ItemId string // 对象ID:单卡为t_sim_card_info.id,流量池为t_sim_flow_pool.id + ItemCode string // 对象编码快照:ICCID/MSISDN/流量池编码 + WarnStage string // 触发档位:1第一档,2第二档,3第三档 + WarnLevel string // 预警级别:1提示,2注意,3警告,4危险,5紧急 + WarnUnit string // 预警单位/维度:1MB,2百分比,3天数,4月数 + ThresholdValue string // 触发阈值,单位由warn_unit决定 + ActualValue string // 触发时实际值,单位由warn_unit决定 + TotalFlowMb string // 触发时总流量MB;流量类预警使用 + RemainFlowMb string // 触发时剩余流量MB;流量类预警使用 + WarnMethods string // 实际预警方式快照 + WarnPhones string // 实际预警手机号快照 + WarnEmails string // 实际预警邮箱快照 + MessageLogId string // 平台消息ID;聚合通知时可能为空 + WarnTime string // 预警时间 + Remark string // 备注 + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tWarnLogColumns holds the columns for the table t_warn_log. +var tWarnLogColumns = TWarnLogColumns{ + Id: "id", + TenantId: "tenant_id", + ConfigId: "config_id", + ItemType: "item_type", + ItemId: "item_id", + ItemCode: "item_code", + WarnStage: "warn_stage", + WarnLevel: "warn_level", + WarnUnit: "warn_unit", + ThresholdValue: "threshold_value", + ActualValue: "actual_value", + TotalFlowMb: "total_flow_mb", + RemainFlowMb: "remain_flow_mb", + WarnMethods: "warn_methods", + WarnPhones: "warn_phones", + WarnEmails: "warn_emails", + MessageLogId: "message_log_id", + WarnTime: "warn_time", + Remark: "remark", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTWarnLogDao creates and returns a new DAO object for table data access. +func NewTWarnLogDao(handlers ...gdb.ModelHandler) *TWarnLogDao { + return &TWarnLogDao{ + group: "default", + table: "t_warn_log", + columns: tWarnLogColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TWarnLogDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TWarnLogDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TWarnLogDao) Columns() TWarnLogColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TWarnLogDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TWarnLogDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TWarnLogDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_wo_communication.go b/server/internal/dao/internal/t_wo_communication.go new file mode 100644 index 0000000..0a648ce --- /dev/null +++ b/server/internal/dao/internal/t_wo_communication.go @@ -0,0 +1,105 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TWoCommunicationDao is the data access object for the table t_wo_communication. +type TWoCommunicationDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TWoCommunicationColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TWoCommunicationColumns defines and stores column names for the table t_wo_communication. +type TWoCommunicationColumns struct { + Id string // 主键ID + WoNo string // 工单编号 + CommType string // 沟通类型:0回复,1反馈 + SenderDeptId string // 发送人机构ID + SenderUserId string // 发送人用户ID + SenderName string // 发送人姓名 + Content string // 内容 + AttachmentUrls string // 附件URL列表 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tWoCommunicationColumns holds the columns for the table t_wo_communication. +var tWoCommunicationColumns = TWoCommunicationColumns{ + Id: "id", + WoNo: "wo_no", + CommType: "comm_type", + SenderDeptId: "sender_dept_id", + SenderUserId: "sender_user_id", + SenderName: "sender_name", + Content: "content", + AttachmentUrls: "attachment_urls", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTWoCommunicationDao creates and returns a new DAO object for table data access. +func NewTWoCommunicationDao(handlers ...gdb.ModelHandler) *TWoCommunicationDao { + return &TWoCommunicationDao{ + group: "default", + table: "t_wo_communication", + columns: tWoCommunicationColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TWoCommunicationDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TWoCommunicationDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TWoCommunicationDao) Columns() TWoCommunicationColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TWoCommunicationDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TWoCommunicationDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TWoCommunicationDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_wo_main.go b/server/internal/dao/internal/t_wo_main.go new file mode 100644 index 0000000..06a6dea --- /dev/null +++ b/server/internal/dao/internal/t_wo_main.go @@ -0,0 +1,137 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TWoMainDao is the data access object for the table t_wo_main. +type TWoMainDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TWoMainColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TWoMainColumns defines and stores column names for the table t_wo_main. +type TWoMainColumns struct { + Id string // 主键ID + WoNo string // 工单编号 + WoType string // 工单类型 0卡,1模组,2设备,3订单,4余额,5提现 + RelatedId string // 关联业务ID + Title string // 工单标题 + Content string // 问题描述 + Attachment string // 附件 + CreatorType string // 创建人类型:1终端用户,2代理商 + CreatorDeptId string // 创建机构ID + CreatorUserId string // 创建用户ID + CreatorName string // 创建人姓名 + CreatorPhone string // 创建人手机号 + AssignDeptId string // 分派方机构ID + AssignUserId string // 分派人用户ID + CurrentStatus string // 当前处理状态:1已分派,2处理中,3已解决,4已完结_关单,5已取消 + HandlerDeptId string // 处理方机构ID + HandlerUserId string // 处理人用户ID + FirstResponseTime string // 首次响应时间 + ResolveTime string // 确认时间 + CloseTime string // 关闭时间 + SatisfactionLevel string // 满意度:1-5分 + EvalContent string // 评价内容 + EvalTime string // 评价时间 + Remark string // 备注 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tWoMainColumns holds the columns for the table t_wo_main. +var tWoMainColumns = TWoMainColumns{ + Id: "id", + WoNo: "wo_no", + WoType: "wo_type", + RelatedId: "related_id", + Title: "title", + Content: "content", + Attachment: "attachment", + CreatorType: "creator_type", + CreatorDeptId: "creator_dept_id", + CreatorUserId: "creator_user_id", + CreatorName: "creator_name", + CreatorPhone: "creator_phone", + AssignDeptId: "assign_dept_id", + AssignUserId: "assign_user_id", + CurrentStatus: "current_status", + HandlerDeptId: "handler_dept_id", + HandlerUserId: "handler_user_id", + FirstResponseTime: "first_response_time", + ResolveTime: "resolve_time", + CloseTime: "close_time", + SatisfactionLevel: "satisfaction_level", + EvalContent: "eval_content", + EvalTime: "eval_time", + Remark: "remark", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTWoMainDao creates and returns a new DAO object for table data access. +func NewTWoMainDao(handlers ...gdb.ModelHandler) *TWoMainDao { + return &TWoMainDao{ + group: "default", + table: "t_wo_main", + columns: tWoMainColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TWoMainDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TWoMainDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TWoMainDao) Columns() TWoMainColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TWoMainDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TWoMainDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TWoMainDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/t_wo_record.go b/server/internal/dao/internal/t_wo_record.go new file mode 100644 index 0000000..365bf7c --- /dev/null +++ b/server/internal/dao/internal/t_wo_record.go @@ -0,0 +1,107 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TWoRecordDao is the data access object for the table t_wo_record. +type TWoRecordDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TWoRecordColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TWoRecordColumns defines and stores column names for the table t_wo_record. +type TWoRecordColumns struct { + Id string // 主键ID + WoNo string // 工单编号 + OperationType string // 操作类型:0创建,1分派,2处理,3完成,4关单,5取消 + OperationDeptId string // 操作机构ID + OperationUserId string // 操作用户ID + TargetDeptId string // 目标部门ID + TargetUserId string // 目标用户ID + BeforeStatus string // 变更前状态 + AfterStatus string // 变更后状态 + TenantId string // 租户ID + CreatedDept string // 创建部门 + CreatedBy string // 创建者 + CreatedAt string // 创建时间 + UpdatedBy string // 更新者 + UpdatedAt string // 更新时间 +} + +// tWoRecordColumns holds the columns for the table t_wo_record. +var tWoRecordColumns = TWoRecordColumns{ + Id: "id", + WoNo: "wo_no", + OperationType: "operation_type", + OperationDeptId: "operation_dept_id", + OperationUserId: "operation_user_id", + TargetDeptId: "target_dept_id", + TargetUserId: "target_user_id", + BeforeStatus: "before_status", + AfterStatus: "after_status", + TenantId: "tenant_id", + CreatedDept: "created_dept", + CreatedBy: "created_by", + CreatedAt: "created_at", + UpdatedBy: "updated_by", + UpdatedAt: "updated_at", +} + +// NewTWoRecordDao creates and returns a new DAO object for table data access. +func NewTWoRecordDao(handlers ...gdb.ModelHandler) *TWoRecordDao { + return &TWoRecordDao{ + group: "default", + table: "t_wo_record", + columns: tWoRecordColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TWoRecordDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TWoRecordDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TWoRecordDao) Columns() TWoRecordColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TWoRecordDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TWoRecordDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TWoRecordDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/test_demo.go b/server/internal/dao/internal/test_demo.go new file mode 100644 index 0000000..8d625c2 --- /dev/null +++ b/server/internal/dao/internal/test_demo.go @@ -0,0 +1,107 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TestDemoDao is the data access object for the table test_demo. +type TestDemoDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TestDemoColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TestDemoColumns defines and stores column names for the table test_demo. +type TestDemoColumns struct { + Id string // 主键 + TenantId string // 租户编号 + DeptId string // 部门id + UserId string // 用户id + OrderNum string // 排序号 + TestKey string // key键 + Value string // 值 + Version string // 版本 + CreatedDept string // 创建部门 + CreatedAt string // 创建时间 + CreatedBy string // 创建者 + UpdatedAt string // 更新时间 + UpdatedBy string // 更新者 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 +} + +// testDemoColumns holds the columns for the table test_demo. +var testDemoColumns = TestDemoColumns{ + Id: "id", + TenantId: "tenant_id", + DeptId: "dept_id", + UserId: "user_id", + OrderNum: "order_num", + TestKey: "test_key", + Value: "value", + Version: "version", + CreatedDept: "created_dept", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", +} + +// NewTestDemoDao creates and returns a new DAO object for table data access. +func NewTestDemoDao(handlers ...gdb.ModelHandler) *TestDemoDao { + return &TestDemoDao{ + group: "default", + table: "test_demo", + columns: testDemoColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TestDemoDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TestDemoDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TestDemoDao) Columns() TestDemoColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TestDemoDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TestDemoDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TestDemoDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/internal/test_tree.go b/server/internal/dao/internal/test_tree.go new file mode 100644 index 0000000..5e52705 --- /dev/null +++ b/server/internal/dao/internal/test_tree.go @@ -0,0 +1,105 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TestTreeDao is the data access object for the table test_tree. +type TestTreeDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TestTreeColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TestTreeColumns defines and stores column names for the table test_tree. +type TestTreeColumns struct { + Id string // 主键 + TenantId string // 租户编号 + ParentId string // 父id + DeptId string // 部门id + UserId string // 用户id + TreeName string // 值 + Version string // 版本 + CreatedDept string // 创建部门 + CreatedAt string // 创建时间 + CreatedBy string // 创建者 + UpdatedAt string // 更新时间 + UpdatedBy string // 更新者 + DeletedBy string // 删除人 + DeletedAt string // 删除时间 +} + +// testTreeColumns holds the columns for the table test_tree. +var testTreeColumns = TestTreeColumns{ + Id: "id", + TenantId: "tenant_id", + ParentId: "parent_id", + DeptId: "dept_id", + UserId: "user_id", + TreeName: "tree_name", + Version: "version", + CreatedDept: "created_dept", + CreatedAt: "created_at", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + UpdatedBy: "updated_by", + DeletedBy: "deleted_by", + DeletedAt: "deleted_at", +} + +// NewTestTreeDao creates and returns a new DAO object for table data access. +func NewTestTreeDao(handlers ...gdb.ModelHandler) *TestTreeDao { + return &TestTreeDao{ + group: "default", + table: "test_tree", + columns: testTreeColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TestTreeDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TestTreeDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TestTreeDao) Columns() TestTreeColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TestTreeDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TestTreeDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TestTreeDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/server/internal/dao/sys_client.go b/server/internal/dao/sys_client.go new file mode 100644 index 0000000..d88ab66 --- /dev/null +++ b/server/internal/dao/sys_client.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysClientDao is the data access object for the table sys_client. +// You can define custom methods on it to extend its functionality as needed. +type sysClientDao struct { + *internal.SysClientDao +} + +var ( + // SysClient is a globally accessible object for table sys_client operations. + SysClient = sysClientDao{internal.NewSysClientDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_config.go b/server/internal/dao/sys_config.go new file mode 100644 index 0000000..d5b6fb7 --- /dev/null +++ b/server/internal/dao/sys_config.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysConfigDao is the data access object for the table sys_config. +// You can define custom methods on it to extend its functionality as needed. +type sysConfigDao struct { + *internal.SysConfigDao +} + +var ( + // SysConfig is a globally accessible object for table sys_config operations. + SysConfig = sysConfigDao{internal.NewSysConfigDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_dept.go b/server/internal/dao/sys_dept.go new file mode 100644 index 0000000..265fe2c --- /dev/null +++ b/server/internal/dao/sys_dept.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysDeptDao is the data access object for the table sys_dept. +// You can define custom methods on it to extend its functionality as needed. +type sysDeptDao struct { + *internal.SysDeptDao +} + +var ( + // SysDept is a globally accessible object for table sys_dept operations. + SysDept = sysDeptDao{internal.NewSysDeptDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_dict_data.go b/server/internal/dao/sys_dict_data.go new file mode 100644 index 0000000..40eaf8c --- /dev/null +++ b/server/internal/dao/sys_dict_data.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysDictDataDao is the data access object for the table sys_dict_data. +// You can define custom methods on it to extend its functionality as needed. +type sysDictDataDao struct { + *internal.SysDictDataDao +} + +var ( + // SysDictData is a globally accessible object for table sys_dict_data operations. + SysDictData = sysDictDataDao{internal.NewSysDictDataDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_dict_type.go b/server/internal/dao/sys_dict_type.go new file mode 100644 index 0000000..b8cee41 --- /dev/null +++ b/server/internal/dao/sys_dict_type.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysDictTypeDao is the data access object for the table sys_dict_type. +// You can define custom methods on it to extend its functionality as needed. +type sysDictTypeDao struct { + *internal.SysDictTypeDao +} + +var ( + // SysDictType is a globally accessible object for table sys_dict_type operations. + SysDictType = sysDictTypeDao{internal.NewSysDictTypeDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_gen_table.go b/server/internal/dao/sys_gen_table.go new file mode 100644 index 0000000..710dbc2 --- /dev/null +++ b/server/internal/dao/sys_gen_table.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysGenTableDao is the data access object for the table sys_gen_table. +// You can define custom methods on it to extend its functionality as needed. +type sysGenTableDao struct { + *internal.SysGenTableDao +} + +var ( + // SysGenTable is a globally accessible object for table sys_gen_table operations. + SysGenTable = sysGenTableDao{internal.NewSysGenTableDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_job.go b/server/internal/dao/sys_job.go new file mode 100644 index 0000000..b3ca922 --- /dev/null +++ b/server/internal/dao/sys_job.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysJobDao is the data access object for the table sys_job. +// You can define custom methods on it to extend its functionality as needed. +type sysJobDao struct { + *internal.SysJobDao +} + +var ( + // SysJob is a globally accessible object for table sys_job operations. + SysJob = sysJobDao{internal.NewSysJobDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_logininfor.go b/server/internal/dao/sys_logininfor.go new file mode 100644 index 0000000..7f11f45 --- /dev/null +++ b/server/internal/dao/sys_logininfor.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysLogininforDao is the data access object for the table sys_logininfor. +// You can define custom methods on it to extend its functionality as needed. +type sysLogininforDao struct { + *internal.SysLogininforDao +} + +var ( + // SysLogininfor is a globally accessible object for table sys_logininfor operations. + SysLogininfor = sysLogininforDao{internal.NewSysLogininforDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_menu.go b/server/internal/dao/sys_menu.go new file mode 100644 index 0000000..239f2f7 --- /dev/null +++ b/server/internal/dao/sys_menu.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysMenuDao is the data access object for the table sys_menu. +// You can define custom methods on it to extend its functionality as needed. +type sysMenuDao struct { + *internal.SysMenuDao +} + +var ( + // SysMenu is a globally accessible object for table sys_menu operations. + SysMenu = sysMenuDao{internal.NewSysMenuDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_notice.go b/server/internal/dao/sys_notice.go new file mode 100644 index 0000000..60b8b55 --- /dev/null +++ b/server/internal/dao/sys_notice.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysNoticeDao is the data access object for the table sys_notice. +// You can define custom methods on it to extend its functionality as needed. +type sysNoticeDao struct { + *internal.SysNoticeDao +} + +var ( + // SysNotice is a globally accessible object for table sys_notice operations. + SysNotice = sysNoticeDao{internal.NewSysNoticeDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_notice_user.go b/server/internal/dao/sys_notice_user.go new file mode 100644 index 0000000..848b09e --- /dev/null +++ b/server/internal/dao/sys_notice_user.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysNoticeUserDao is the data access object for the table sys_notice_user. +// You can define custom methods on it to extend its functionality as needed. +type sysNoticeUserDao struct { + *internal.SysNoticeUserDao +} + +var ( + // SysNoticeUser is a globally accessible object for table sys_notice_user operations. + SysNoticeUser = sysNoticeUserDao{internal.NewSysNoticeUserDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_oper_log.go b/server/internal/dao/sys_oper_log.go new file mode 100644 index 0000000..c374418 --- /dev/null +++ b/server/internal/dao/sys_oper_log.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysOperLogDao is the data access object for the table sys_oper_log. +// You can define custom methods on it to extend its functionality as needed. +type sysOperLogDao struct { + *internal.SysOperLogDao +} + +var ( + // SysOperLog is a globally accessible object for table sys_oper_log operations. + SysOperLog = sysOperLogDao{internal.NewSysOperLogDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_oss.go b/server/internal/dao/sys_oss.go new file mode 100644 index 0000000..bbc22d5 --- /dev/null +++ b/server/internal/dao/sys_oss.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysOssDao is the data access object for the table sys_oss. +// You can define custom methods on it to extend its functionality as needed. +type sysOssDao struct { + *internal.SysOssDao +} + +var ( + // SysOss is a globally accessible object for table sys_oss operations. + SysOss = sysOssDao{internal.NewSysOssDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_oss_config.go b/server/internal/dao/sys_oss_config.go new file mode 100644 index 0000000..8b9df7f --- /dev/null +++ b/server/internal/dao/sys_oss_config.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysOssConfigDao is the data access object for the table sys_oss_config. +// You can define custom methods on it to extend its functionality as needed. +type sysOssConfigDao struct { + *internal.SysOssConfigDao +} + +var ( + // SysOssConfig is a globally accessible object for table sys_oss_config operations. + SysOssConfig = sysOssConfigDao{internal.NewSysOssConfigDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_post.go b/server/internal/dao/sys_post.go new file mode 100644 index 0000000..2939642 --- /dev/null +++ b/server/internal/dao/sys_post.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysPostDao is the data access object for the table sys_post. +// You can define custom methods on it to extend its functionality as needed. +type sysPostDao struct { + *internal.SysPostDao +} + +var ( + // SysPost is a globally accessible object for table sys_post operations. + SysPost = sysPostDao{internal.NewSysPostDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_role.go b/server/internal/dao/sys_role.go new file mode 100644 index 0000000..a9e4722 --- /dev/null +++ b/server/internal/dao/sys_role.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysRoleDao is the data access object for the table sys_role. +// You can define custom methods on it to extend its functionality as needed. +type sysRoleDao struct { + *internal.SysRoleDao +} + +var ( + // SysRole is a globally accessible object for table sys_role operations. + SysRole = sysRoleDao{internal.NewSysRoleDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_role_dept.go b/server/internal/dao/sys_role_dept.go new file mode 100644 index 0000000..f928365 --- /dev/null +++ b/server/internal/dao/sys_role_dept.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysRoleDeptDao is the data access object for the table sys_role_dept. +// You can define custom methods on it to extend its functionality as needed. +type sysRoleDeptDao struct { + *internal.SysRoleDeptDao +} + +var ( + // SysRoleDept is a globally accessible object for table sys_role_dept operations. + SysRoleDept = sysRoleDeptDao{internal.NewSysRoleDeptDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_role_menu.go b/server/internal/dao/sys_role_menu.go new file mode 100644 index 0000000..eec5858 --- /dev/null +++ b/server/internal/dao/sys_role_menu.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysRoleMenuDao is the data access object for the table sys_role_menu. +// You can define custom methods on it to extend its functionality as needed. +type sysRoleMenuDao struct { + *internal.SysRoleMenuDao +} + +var ( + // SysRoleMenu is a globally accessible object for table sys_role_menu operations. + SysRoleMenu = sysRoleMenuDao{internal.NewSysRoleMenuDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_social.go b/server/internal/dao/sys_social.go new file mode 100644 index 0000000..5bb7305 --- /dev/null +++ b/server/internal/dao/sys_social.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysSocialDao is the data access object for the table sys_social. +// You can define custom methods on it to extend its functionality as needed. +type sysSocialDao struct { + *internal.SysSocialDao +} + +var ( + // SysSocial is a globally accessible object for table sys_social operations. + SysSocial = sysSocialDao{internal.NewSysSocialDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_tenant.go b/server/internal/dao/sys_tenant.go new file mode 100644 index 0000000..ab96446 --- /dev/null +++ b/server/internal/dao/sys_tenant.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysTenantDao is the data access object for the table sys_tenant. +// You can define custom methods on it to extend its functionality as needed. +type sysTenantDao struct { + *internal.SysTenantDao +} + +var ( + // SysTenant is a globally accessible object for table sys_tenant operations. + SysTenant = sysTenantDao{internal.NewSysTenantDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_tenant_package.go b/server/internal/dao/sys_tenant_package.go new file mode 100644 index 0000000..c1b1cfd --- /dev/null +++ b/server/internal/dao/sys_tenant_package.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysTenantPackageDao is the data access object for the table sys_tenant_package. +// You can define custom methods on it to extend its functionality as needed. +type sysTenantPackageDao struct { + *internal.SysTenantPackageDao +} + +var ( + // SysTenantPackage is a globally accessible object for table sys_tenant_package operations. + SysTenantPackage = sysTenantPackageDao{internal.NewSysTenantPackageDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_user.go b/server/internal/dao/sys_user.go new file mode 100644 index 0000000..74901b8 --- /dev/null +++ b/server/internal/dao/sys_user.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysUserDao is the data access object for the table sys_user. +// You can define custom methods on it to extend its functionality as needed. +type sysUserDao struct { + *internal.SysUserDao +} + +var ( + // SysUser is a globally accessible object for table sys_user operations. + SysUser = sysUserDao{internal.NewSysUserDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_user_online.go b/server/internal/dao/sys_user_online.go new file mode 100644 index 0000000..6a552d6 --- /dev/null +++ b/server/internal/dao/sys_user_online.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysUserOnlineDao is the data access object for the table sys_user_online. +// You can define custom methods on it to extend its functionality as needed. +type sysUserOnlineDao struct { + *internal.SysUserOnlineDao +} + +var ( + // SysUserOnline is a globally accessible object for table sys_user_online operations. + SysUserOnline = sysUserOnlineDao{internal.NewSysUserOnlineDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_user_post.go b/server/internal/dao/sys_user_post.go new file mode 100644 index 0000000..531846e --- /dev/null +++ b/server/internal/dao/sys_user_post.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysUserPostDao is the data access object for the table sys_user_post. +// You can define custom methods on it to extend its functionality as needed. +type sysUserPostDao struct { + *internal.SysUserPostDao +} + +var ( + // SysUserPost is a globally accessible object for table sys_user_post operations. + SysUserPost = sysUserPostDao{internal.NewSysUserPostDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/sys_user_role.go b/server/internal/dao/sys_user_role.go new file mode 100644 index 0000000..9093314 --- /dev/null +++ b/server/internal/dao/sys_user_role.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// sysUserRoleDao is the data access object for the table sys_user_role. +// You can define custom methods on it to extend its functionality as needed. +type sysUserRoleDao struct { + *internal.SysUserRoleDao +} + +var ( + // SysUserRole is a globally accessible object for table sys_user_role operations. + SysUserRole = sysUserRoleDao{internal.NewSysUserRoleDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_agent_balance.go b/server/internal/dao/t_agent_balance.go new file mode 100644 index 0000000..e573c51 --- /dev/null +++ b/server/internal/dao/t_agent_balance.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tAgentBalanceDao is the data access object for the table t_agent_balance. +// You can define custom methods on it to extend its functionality as needed. +type tAgentBalanceDao struct { + *internal.TAgentBalanceDao +} + +var ( + // TAgentBalance is a globally accessible object for table t_agent_balance operations. + TAgentBalance = tAgentBalanceDao{internal.NewTAgentBalanceDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_agent_enterprise.go b/server/internal/dao/t_agent_enterprise.go new file mode 100644 index 0000000..ac65252 --- /dev/null +++ b/server/internal/dao/t_agent_enterprise.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tAgentEnterpriseDao is the data access object for the table t_agent_enterprise. +// You can define custom methods on it to extend its functionality as needed. +type tAgentEnterpriseDao struct { + *internal.TAgentEnterpriseDao +} + +var ( + // TAgentEnterprise is a globally accessible object for table t_agent_enterprise operations. + TAgentEnterprise = tAgentEnterpriseDao{internal.NewTAgentEnterpriseDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_api.go b/server/internal/dao/t_api.go new file mode 100644 index 0000000..511584b --- /dev/null +++ b/server/internal/dao/t_api.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tApiDao is the data access object for the table t_api. +// You can define custom methods on it to extend its functionality as needed. +type tApiDao struct { + *internal.TApiDao +} + +var ( + // TApi is a globally accessible object for table t_api operations. + TApi = tApiDao{internal.NewTApiDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_api_group.go b/server/internal/dao/t_api_group.go new file mode 100644 index 0000000..3ea75aa --- /dev/null +++ b/server/internal/dao/t_api_group.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tApiGroupDao is the data access object for the table t_api_group. +// You can define custom methods on it to extend its functionality as needed. +type tApiGroupDao struct { + *internal.TApiGroupDao +} + +var ( + // TApiGroup is a globally accessible object for table t_api_group operations. + TApiGroup = tApiGroupDao{internal.NewTApiGroupDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_api_group_resource.go b/server/internal/dao/t_api_group_resource.go new file mode 100644 index 0000000..3bc3a18 --- /dev/null +++ b/server/internal/dao/t_api_group_resource.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tApiGroupResourceDao is the data access object for the table t_api_group_resource. +// You can define custom methods on it to extend its functionality as needed. +type tApiGroupResourceDao struct { + *internal.TApiGroupResourceDao +} + +var ( + // TApiGroupResource is a globally accessible object for table t_api_group_resource operations. + TApiGroupResource = tApiGroupResourceDao{internal.NewTApiGroupResourceDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_api_resource.go b/server/internal/dao/t_api_resource.go new file mode 100644 index 0000000..959b046 --- /dev/null +++ b/server/internal/dao/t_api_resource.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tApiResourceDao is the data access object for the table t_api_resource. +// You can define custom methods on it to extend its functionality as needed. +type tApiResourceDao struct { + *internal.TApiResourceDao +} + +var ( + // TApiResource is a globally accessible object for table t_api_resource operations. + TApiResource = tApiResourceDao{internal.NewTApiResourceDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_balance_detail.go b/server/internal/dao/t_balance_detail.go new file mode 100644 index 0000000..7eccdc3 --- /dev/null +++ b/server/internal/dao/t_balance_detail.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tBalanceDetailDao is the data access object for the table t_balance_detail. +// You can define custom methods on it to extend its functionality as needed. +type tBalanceDetailDao struct { + *internal.TBalanceDetailDao +} + +var ( + // TBalanceDetail is a globally accessible object for table t_balance_detail operations. + TBalanceDetail = tBalanceDetailDao{internal.NewTBalanceDetailDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_brand.go b/server/internal/dao/t_brand.go new file mode 100644 index 0000000..48f694f --- /dev/null +++ b/server/internal/dao/t_brand.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tBrandDao is the data access object for the table t_brand. +// You can define custom methods on it to extend its functionality as needed. +type tBrandDao struct { + *internal.TBrandDao +} + +var ( + // TBrand is a globally accessible object for table t_brand operations. + TBrand = tBrandDao{internal.NewTBrandDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_category.go b/server/internal/dao/t_category.go new file mode 100644 index 0000000..821c5a5 --- /dev/null +++ b/server/internal/dao/t_category.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tCategoryDao is the data access object for the table t_category. +// You can define custom methods on it to extend its functionality as needed. +type tCategoryDao struct { + *internal.TCategoryDao +} + +var ( + // TCategory is a globally accessible object for table t_category operations. + TCategory = tCategoryDao{internal.NewTCategoryDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_consume_account_month_bill.go b/server/internal/dao/t_consume_account_month_bill.go new file mode 100644 index 0000000..4462a09 --- /dev/null +++ b/server/internal/dao/t_consume_account_month_bill.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tConsumeAccountMonthBillDao is the data access object for the table t_consume_account_month_bill. +// You can define custom methods on it to extend its functionality as needed. +type tConsumeAccountMonthBillDao struct { + *internal.TConsumeAccountMonthBillDao +} + +var ( + // TConsumeAccountMonthBill is a globally accessible object for table t_consume_account_month_bill operations. + TConsumeAccountMonthBill = tConsumeAccountMonthBillDao{internal.NewTConsumeAccountMonthBillDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_credit_detail.go b/server/internal/dao/t_credit_detail.go new file mode 100644 index 0000000..673bc80 --- /dev/null +++ b/server/internal/dao/t_credit_detail.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tCreditDetailDao is the data access object for the table t_credit_detail. +// You can define custom methods on it to extend its functionality as needed. +type tCreditDetailDao struct { + *internal.TCreditDetailDao +} + +var ( + // TCreditDetail is a globally accessible object for table t_credit_detail operations. + TCreditDetail = tCreditDetailDao{internal.NewTCreditDetailDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_cust_withdraw_account.go b/server/internal/dao/t_cust_withdraw_account.go new file mode 100644 index 0000000..e374ed9 --- /dev/null +++ b/server/internal/dao/t_cust_withdraw_account.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tCustWithdrawAccountDao is the data access object for the table t_cust_withdraw_account. +// You can define custom methods on it to extend its functionality as needed. +type tCustWithdrawAccountDao struct { + *internal.TCustWithdrawAccountDao +} + +var ( + // TCustWithdrawAccount is a globally accessible object for table t_cust_withdraw_account operations. + TCustWithdrawAccount = tCustWithdrawAccountDao{internal.NewTCustWithdrawAccountDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_dept_flow_daily.go b/server/internal/dao/t_dept_flow_daily.go new file mode 100644 index 0000000..2803cad --- /dev/null +++ b/server/internal/dao/t_dept_flow_daily.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tDeptFlowDailyDao is the data access object for the table t_dept_flow_daily. +// You can define custom methods on it to extend its functionality as needed. +type tDeptFlowDailyDao struct { + *internal.TDeptFlowDailyDao +} + +var ( + // TDeptFlowDaily is a globally accessible object for table t_dept_flow_daily operations. + TDeptFlowDaily = tDeptFlowDailyDao{internal.NewTDeptFlowDailyDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_division_code.go b/server/internal/dao/t_division_code.go new file mode 100644 index 0000000..5a1de94 --- /dev/null +++ b/server/internal/dao/t_division_code.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tDivisionCodeDao is the data access object for the table t_division_code. +// You can define custom methods on it to extend its functionality as needed. +type tDivisionCodeDao struct { + *internal.TDivisionCodeDao +} + +var ( + // TDivisionCode is a globally accessible object for table t_division_code operations. + TDivisionCode = tDivisionCodeDao{internal.NewTDivisionCodeDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_fin_commission_detail.go b/server/internal/dao/t_fin_commission_detail.go new file mode 100644 index 0000000..b7f8d59 --- /dev/null +++ b/server/internal/dao/t_fin_commission_detail.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tFinCommissionDetailDao is the data access object for the table t_fin_commission_detail. +// You can define custom methods on it to extend its functionality as needed. +type tFinCommissionDetailDao struct { + *internal.TFinCommissionDetailDao +} + +var ( + // TFinCommissionDetail is a globally accessible object for table t_fin_commission_detail operations. + TFinCommissionDetail = tFinCommissionDetailDao{internal.NewTFinCommissionDetailDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_fin_withdrawable_change.go b/server/internal/dao/t_fin_withdrawable_change.go new file mode 100644 index 0000000..d0f4c8a --- /dev/null +++ b/server/internal/dao/t_fin_withdrawable_change.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tFinWithdrawableChangeDao is the data access object for the table t_fin_withdrawable_change. +// You can define custom methods on it to extend its functionality as needed. +type tFinWithdrawableChangeDao struct { + *internal.TFinWithdrawableChangeDao +} + +var ( + // TFinWithdrawableChange is a globally accessible object for table t_fin_withdrawable_change operations. + TFinWithdrawableChange = tFinWithdrawableChangeDao{internal.NewTFinWithdrawableChangeDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_finance_apply.go b/server/internal/dao/t_finance_apply.go new file mode 100644 index 0000000..9640cb4 --- /dev/null +++ b/server/internal/dao/t_finance_apply.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tFinanceApplyDao is the data access object for the table t_finance_apply. +// You can define custom methods on it to extend its functionality as needed. +type tFinanceApplyDao struct { + *internal.TFinanceApplyDao +} + +var ( + // TFinanceApply is a globally accessible object for table t_finance_apply operations. + TFinanceApply = tFinanceApplyDao{internal.NewTFinanceApplyDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_finance_audit_record.go b/server/internal/dao/t_finance_audit_record.go new file mode 100644 index 0000000..6139eba --- /dev/null +++ b/server/internal/dao/t_finance_audit_record.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tFinanceAuditRecordDao is the data access object for the table t_finance_audit_record. +// You can define custom methods on it to extend its functionality as needed. +type tFinanceAuditRecordDao struct { + *internal.TFinanceAuditRecordDao +} + +var ( + // TFinanceAuditRecord is a globally accessible object for table t_finance_audit_record operations. + TFinanceAuditRecord = tFinanceAuditRecordDao{internal.NewTFinanceAuditRecordDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_flow_package_config.go b/server/internal/dao/t_flow_package_config.go new file mode 100644 index 0000000..896493a --- /dev/null +++ b/server/internal/dao/t_flow_package_config.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tFlowPackageConfigDao is the data access object for the table t_flow_package_config. +// You can define custom methods on it to extend its functionality as needed. +type tFlowPackageConfigDao struct { + *internal.TFlowPackageConfigDao +} + +var ( + // TFlowPackageConfig is a globally accessible object for table t_flow_package_config operations. + TFlowPackageConfig = tFlowPackageConfigDao{internal.NewTFlowPackageConfigDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_message_log.go b/server/internal/dao/t_message_log.go new file mode 100644 index 0000000..44bd831 --- /dev/null +++ b/server/internal/dao/t_message_log.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tMessageLogDao is the data access object for the table t_message_log. +// You can define custom methods on it to extend its functionality as needed. +type tMessageLogDao struct { + *internal.TMessageLogDao +} + +var ( + // TMessageLog is a globally accessible object for table t_message_log operations. + TMessageLog = tMessageLogDao{internal.NewTMessageLogDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_message_type.go b/server/internal/dao/t_message_type.go new file mode 100644 index 0000000..81a3765 --- /dev/null +++ b/server/internal/dao/t_message_type.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tMessageTypeDao is the data access object for the table t_message_type. +// You can define custom methods on it to extend its functionality as needed. +type tMessageTypeDao struct { + *internal.TMessageTypeDao +} + +var ( + // TMessageType is a globally accessible object for table t_message_type operations. + TMessageType = tMessageTypeDao{internal.NewTMessageTypeDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_notify_log.go b/server/internal/dao/t_notify_log.go new file mode 100644 index 0000000..f062f68 --- /dev/null +++ b/server/internal/dao/t_notify_log.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tNotifyLogDao is the data access object for the table t_notify_log. +// You can define custom methods on it to extend its functionality as needed. +type tNotifyLogDao struct { + *internal.TNotifyLogDao +} + +var ( + // TNotifyLog is a globally accessible object for table t_notify_log operations. + TNotifyLog = tNotifyLogDao{internal.NewTNotifyLogDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_order.go b/server/internal/dao/t_order.go new file mode 100644 index 0000000..f3c9681 --- /dev/null +++ b/server/internal/dao/t_order.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tOrderDao is the data access object for the table t_order. +// You can define custom methods on it to extend its functionality as needed. +type tOrderDao struct { + *internal.TOrderDao +} + +var ( + // TOrder is a globally accessible object for table t_order operations. + TOrder = tOrderDao{internal.NewTOrderDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_order_item.go b/server/internal/dao/t_order_item.go new file mode 100644 index 0000000..35b6fbc --- /dev/null +++ b/server/internal/dao/t_order_item.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tOrderItemDao is the data access object for the table t_order_item. +// You can define custom methods on it to extend its functionality as needed. +type tOrderItemDao struct { + *internal.TOrderItemDao +} + +var ( + // TOrderItem is a globally accessible object for table t_order_item operations. + TOrderItem = tOrderItemDao{internal.NewTOrderItemDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_package_apply.go b/server/internal/dao/t_package_apply.go new file mode 100644 index 0000000..d9ac9ee --- /dev/null +++ b/server/internal/dao/t_package_apply.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tPackageApplyDao is the data access object for the table t_package_apply. +// You can define custom methods on it to extend its functionality as needed. +type tPackageApplyDao struct { + *internal.TPackageApplyDao +} + +var ( + // TPackageApply is a globally accessible object for table t_package_apply operations. + TPackageApply = tPackageApplyDao{internal.NewTPackageApplyDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_package_audit_record.go b/server/internal/dao/t_package_audit_record.go new file mode 100644 index 0000000..8908575 --- /dev/null +++ b/server/internal/dao/t_package_audit_record.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tPackageAuditRecordDao is the data access object for the table t_package_audit_record. +// You can define custom methods on it to extend its functionality as needed. +type tPackageAuditRecordDao struct { + *internal.TPackageAuditRecordDao +} + +var ( + // TPackageAuditRecord is a globally accessible object for table t_package_audit_record operations. + TPackageAuditRecord = tPackageAuditRecordDao{internal.NewTPackageAuditRecordDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_package_customer.go b/server/internal/dao/t_package_customer.go new file mode 100644 index 0000000..c8456a0 --- /dev/null +++ b/server/internal/dao/t_package_customer.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tPackageCustomerDao is the data access object for the table t_package_customer. +// You can define custom methods on it to extend its functionality as needed. +type tPackageCustomerDao struct { + *internal.TPackageCustomerDao +} + +var ( + // TPackageCustomer is a globally accessible object for table t_package_customer operations. + TPackageCustomer = tPackageCustomerDao{internal.NewTPackageCustomerDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_package_terminal.go b/server/internal/dao/t_package_terminal.go new file mode 100644 index 0000000..00e54e1 --- /dev/null +++ b/server/internal/dao/t_package_terminal.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tPackageTerminalDao is the data access object for the table t_package_terminal. +// You can define custom methods on it to extend its functionality as needed. +type tPackageTerminalDao struct { + *internal.TPackageTerminalDao +} + +var ( + // TPackageTerminal is a globally accessible object for table t_package_terminal operations. + TPackageTerminal = tPackageTerminalDao{internal.NewTPackageTerminalDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_product.go b/server/internal/dao/t_product.go new file mode 100644 index 0000000..551ae1b --- /dev/null +++ b/server/internal/dao/t_product.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tProductDao is the data access object for the table t_product. +// You can define custom methods on it to extend its functionality as needed. +type tProductDao struct { + *internal.TProductDao +} + +var ( + // TProduct is a globally accessible object for table t_product operations. + TProduct = tProductDao{internal.NewTProductDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_profit_account_month_bill.go b/server/internal/dao/t_profit_account_month_bill.go new file mode 100644 index 0000000..867919c --- /dev/null +++ b/server/internal/dao/t_profit_account_month_bill.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tProfitAccountMonthBillDao is the data access object for the table t_profit_account_month_bill. +// You can define custom methods on it to extend its functionality as needed. +type tProfitAccountMonthBillDao struct { + *internal.TProfitAccountMonthBillDao +} + +var ( + // TProfitAccountMonthBill is a globally accessible object for table t_profit_account_month_bill operations. + TProfitAccountMonthBill = tProfitAccountMonthBillDao{internal.NewTProfitAccountMonthBillDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_resource_batch_download.go b/server/internal/dao/t_resource_batch_download.go new file mode 100644 index 0000000..4aecef4 --- /dev/null +++ b/server/internal/dao/t_resource_batch_download.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tResourceBatchDownloadDao is the data access object for the table t_resource_batch_download. +// You can define custom methods on it to extend its functionality as needed. +type tResourceBatchDownloadDao struct { + *internal.TResourceBatchDownloadDao +} + +var ( + // TResourceBatchDownload is a globally accessible object for table t_resource_batch_download operations. + TResourceBatchDownload = tResourceBatchDownloadDao{internal.NewTResourceBatchDownloadDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_bind_package_logs.go b/server/internal/dao/t_sim_bind_package_logs.go new file mode 100644 index 0000000..fc3e5c7 --- /dev/null +++ b/server/internal/dao/t_sim_bind_package_logs.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimBindPackageLogsDao is the data access object for the table t_sim_bind_package_logs. +// You can define custom methods on it to extend its functionality as needed. +type tSimBindPackageLogsDao struct { + *internal.TSimBindPackageLogsDao +} + +var ( + // TSimBindPackageLogs is a globally accessible object for table t_sim_bind_package_logs operations. + TSimBindPackageLogs = tSimBindPackageLogsDao{internal.NewTSimBindPackageLogsDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_card_bind_pkg.go b/server/internal/dao/t_sim_card_bind_pkg.go new file mode 100644 index 0000000..9add4c5 --- /dev/null +++ b/server/internal/dao/t_sim_card_bind_pkg.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimCardBindPkgDao is the data access object for the table t_sim_card_bind_pkg. +// You can define custom methods on it to extend its functionality as needed. +type tSimCardBindPkgDao struct { + *internal.TSimCardBindPkgDao +} + +var ( + // TSimCardBindPkg is a globally accessible object for table t_sim_card_bind_pkg operations. + TSimCardBindPkg = tSimCardBindPkgDao{internal.NewTSimCardBindPkgDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_card_cancellation.go b/server/internal/dao/t_sim_card_cancellation.go new file mode 100644 index 0000000..51bf11a --- /dev/null +++ b/server/internal/dao/t_sim_card_cancellation.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimCardCancellationDao is the data access object for the table t_sim_card_cancellation. +// You can define custom methods on it to extend its functionality as needed. +type tSimCardCancellationDao struct { + *internal.TSimCardCancellationDao +} + +var ( + // TSimCardCancellation is a globally accessible object for table t_sim_card_cancellation operations. + TSimCardCancellation = tSimCardCancellationDao{internal.NewTSimCardCancellationDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_card_info.go b/server/internal/dao/t_sim_card_info.go new file mode 100644 index 0000000..51360e2 --- /dev/null +++ b/server/internal/dao/t_sim_card_info.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimCardInfoDao is the data access object for the table t_sim_card_info. +// You can define custom methods on it to extend its functionality as needed. +type tSimCardInfoDao struct { + *internal.TSimCardInfoDao +} + +var ( + // TSimCardInfo is a globally accessible object for table t_sim_card_info operations. + TSimCardInfo = tSimCardInfoDao{internal.NewTSimCardInfoDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_card_stock.go b/server/internal/dao/t_sim_card_stock.go new file mode 100644 index 0000000..7ba6a21 --- /dev/null +++ b/server/internal/dao/t_sim_card_stock.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimCardStockDao is the data access object for the table t_sim_card_stock. +// You can define custom methods on it to extend its functionality as needed. +type tSimCardStockDao struct { + *internal.TSimCardStockDao +} + +var ( + // TSimCardStock is a globally accessible object for table t_sim_card_stock operations. + TSimCardStock = tSimCardStockDao{internal.NewTSimCardStockDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_channel_template.go b/server/internal/dao/t_sim_channel_template.go new file mode 100644 index 0000000..ab278f5 --- /dev/null +++ b/server/internal/dao/t_sim_channel_template.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimChannelTemplateDao is the data access object for the table t_sim_channel_template. +// You can define custom methods on it to extend its functionality as needed. +type tSimChannelTemplateDao struct { + *internal.TSimChannelTemplateDao +} + +var ( + // TSimChannelTemplate is a globally accessible object for table t_sim_channel_template operations. + TSimChannelTemplate = tSimChannelTemplateDao{internal.NewTSimChannelTemplateDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_channels.go b/server/internal/dao/t_sim_channels.go new file mode 100644 index 0000000..492bc67 --- /dev/null +++ b/server/internal/dao/t_sim_channels.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimChannelsDao is the data access object for the table t_sim_channels. +// You can define custom methods on it to extend its functionality as needed. +type tSimChannelsDao struct { + *internal.TSimChannelsDao +} + +var ( + // TSimChannels is a globally accessible object for table t_sim_channels operations. + TSimChannels = tSimChannelsDao{internal.NewTSimChannelsDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_flow_package_info.go b/server/internal/dao/t_sim_flow_package_info.go new file mode 100644 index 0000000..0cf67c0 --- /dev/null +++ b/server/internal/dao/t_sim_flow_package_info.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimFlowPackageInfoDao is the data access object for the table t_sim_flow_package_info. +// You can define custom methods on it to extend its functionality as needed. +type tSimFlowPackageInfoDao struct { + *internal.TSimFlowPackageInfoDao +} + +var ( + // TSimFlowPackageInfo is a globally accessible object for table t_sim_flow_package_info operations. + TSimFlowPackageInfo = tSimFlowPackageInfoDao{internal.NewTSimFlowPackageInfoDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_flow_package_price.go b/server/internal/dao/t_sim_flow_package_price.go new file mode 100644 index 0000000..c4273f6 --- /dev/null +++ b/server/internal/dao/t_sim_flow_package_price.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimFlowPackagePriceDao is the data access object for the table t_sim_flow_package_price. +// You can define custom methods on it to extend its functionality as needed. +type tSimFlowPackagePriceDao struct { + *internal.TSimFlowPackagePriceDao +} + +var ( + // TSimFlowPackagePrice is a globally accessible object for table t_sim_flow_package_price operations. + TSimFlowPackagePrice = tSimFlowPackagePriceDao{internal.NewTSimFlowPackagePriceDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_flow_pool.go b/server/internal/dao/t_sim_flow_pool.go new file mode 100644 index 0000000..f042127 --- /dev/null +++ b/server/internal/dao/t_sim_flow_pool.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimFlowPoolDao is the data access object for the table t_sim_flow_pool. +// You can define custom methods on it to extend its functionality as needed. +type tSimFlowPoolDao struct { + *internal.TSimFlowPoolDao +} + +var ( + // TSimFlowPool is a globally accessible object for table t_sim_flow_pool operations. + TSimFlowPool = tSimFlowPoolDao{internal.NewTSimFlowPoolDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_package_category.go b/server/internal/dao/t_sim_package_category.go new file mode 100644 index 0000000..9627ac0 --- /dev/null +++ b/server/internal/dao/t_sim_package_category.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimPackageCategoryDao is the data access object for the table t_sim_package_category. +// You can define custom methods on it to extend its functionality as needed. +type tSimPackageCategoryDao struct { + *internal.TSimPackageCategoryDao +} + +var ( + // TSimPackageCategory is a globally accessible object for table t_sim_package_category operations. + TSimPackageCategory = tSimPackageCategoryDao{internal.NewTSimPackageCategoryDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_pool.go b/server/internal/dao/t_sim_pool.go new file mode 100644 index 0000000..01b02b1 --- /dev/null +++ b/server/internal/dao/t_sim_pool.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimPoolDao is the data access object for the table t_sim_pool. +// You can define custom methods on it to extend its functionality as needed. +type tSimPoolDao struct { + *internal.TSimPoolDao +} + +var ( + // TSimPool is a globally accessible object for table t_sim_pool operations. + TSimPool = tSimPoolDao{internal.NewTSimPoolDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_pool_bind_package.go b/server/internal/dao/t_sim_pool_bind_package.go new file mode 100644 index 0000000..1f98452 --- /dev/null +++ b/server/internal/dao/t_sim_pool_bind_package.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimPoolBindPackageDao is the data access object for the table t_sim_pool_bind_package. +// You can define custom methods on it to extend its functionality as needed. +type tSimPoolBindPackageDao struct { + *internal.TSimPoolBindPackageDao +} + +var ( + // TSimPoolBindPackage is a globally accessible object for table t_sim_pool_bind_package operations. + TSimPoolBindPackage = tSimPoolBindPackageDao{internal.NewTSimPoolBindPackageDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_pool_bind_pkg_logs.go b/server/internal/dao/t_sim_pool_bind_pkg_logs.go new file mode 100644 index 0000000..812ed25 --- /dev/null +++ b/server/internal/dao/t_sim_pool_bind_pkg_logs.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimPoolBindPkgLogsDao is the data access object for the table t_sim_pool_bind_pkg_logs. +// You can define custom methods on it to extend its functionality as needed. +type tSimPoolBindPkgLogsDao struct { + *internal.TSimPoolBindPkgLogsDao +} + +var ( + // TSimPoolBindPkgLogs is a globally accessible object for table t_sim_pool_bind_pkg_logs operations. + TSimPoolBindPkgLogs = tSimPoolBindPkgLogsDao{internal.NewTSimPoolBindPkgLogsDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_pool_warn_config.go b/server/internal/dao/t_sim_pool_warn_config.go new file mode 100644 index 0000000..88d269d --- /dev/null +++ b/server/internal/dao/t_sim_pool_warn_config.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimPoolWarnConfigDao is the data access object for the table t_sim_pool_warn_config. +// You can define custom methods on it to extend its functionality as needed. +type tSimPoolWarnConfigDao struct { + *internal.TSimPoolWarnConfigDao +} + +var ( + // TSimPoolWarnConfig is a globally accessible object for table t_sim_pool_warn_config operations. + TSimPoolWarnConfig = tSimPoolWarnConfigDao{internal.NewTSimPoolWarnConfigDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_pool_warn_log.go b/server/internal/dao/t_sim_pool_warn_log.go new file mode 100644 index 0000000..f94ce9d --- /dev/null +++ b/server/internal/dao/t_sim_pool_warn_log.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimPoolWarnLogDao is the data access object for the table t_sim_pool_warn_log. +// You can define custom methods on it to extend its functionality as needed. +type tSimPoolWarnLogDao struct { + *internal.TSimPoolWarnLogDao +} + +var ( + // TSimPoolWarnLog is a globally accessible object for table t_sim_pool_warn_log operations. + TSimPoolWarnLog = tSimPoolWarnLogDao{internal.NewTSimPoolWarnLogDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_terminal.go b/server/internal/dao/t_sim_terminal.go new file mode 100644 index 0000000..1fef6d6 --- /dev/null +++ b/server/internal/dao/t_sim_terminal.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimTerminalDao is the data access object for the table t_sim_terminal. +// You can define custom methods on it to extend its functionality as needed. +type tSimTerminalDao struct { + *internal.TSimTerminalDao +} + +var ( + // TSimTerminal is a globally accessible object for table t_sim_terminal operations. + TSimTerminal = tSimTerminalDao{internal.NewTSimTerminalDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_sim_upstream_package.go b/server/internal/dao/t_sim_upstream_package.go new file mode 100644 index 0000000..4cad7d4 --- /dev/null +++ b/server/internal/dao/t_sim_upstream_package.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tSimUpstreamPackageDao is the data access object for the table t_sim_upstream_package. +// You can define custom methods on it to extend its functionality as needed. +type tSimUpstreamPackageDao struct { + *internal.TSimUpstreamPackageDao +} + +var ( + // TSimUpstreamPackage is a globally accessible object for table t_sim_upstream_package operations. + TSimUpstreamPackage = tSimUpstreamPackageDao{internal.NewTSimUpstreamPackageDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_terminal.go b/server/internal/dao/t_terminal.go new file mode 100644 index 0000000..ac3690c --- /dev/null +++ b/server/internal/dao/t_terminal.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tTerminalDao is the data access object for the table t_terminal. +// You can define custom methods on it to extend its functionality as needed. +type tTerminalDao struct { + *internal.TTerminalDao +} + +var ( + // TTerminal is a globally accessible object for table t_terminal operations. + TTerminal = tTerminalDao{internal.NewTTerminalDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_tplatform_setting.go b/server/internal/dao/t_tplatform_setting.go new file mode 100644 index 0000000..694ab8e --- /dev/null +++ b/server/internal/dao/t_tplatform_setting.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tTplatformSettingDao is the data access object for the table t_tplatform_setting. +// You can define custom methods on it to extend its functionality as needed. +type tTplatformSettingDao struct { + *internal.TTplatformSettingDao +} + +var ( + // TTplatformSetting is a globally accessible object for table t_tplatform_setting operations. + TTplatformSetting = tTplatformSettingDao{internal.NewTTplatformSettingDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_user_message.go b/server/internal/dao/t_user_message.go new file mode 100644 index 0000000..40b5e3b --- /dev/null +++ b/server/internal/dao/t_user_message.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tUserMessageDao is the data access object for the table t_user_message. +// You can define custom methods on it to extend its functionality as needed. +type tUserMessageDao struct { + *internal.TUserMessageDao +} + +var ( + // TUserMessage is a globally accessible object for table t_user_message operations. + TUserMessage = tUserMessageDao{internal.NewTUserMessageDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_warn_config.go b/server/internal/dao/t_warn_config.go new file mode 100644 index 0000000..8ec3e87 --- /dev/null +++ b/server/internal/dao/t_warn_config.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tWarnConfigDao is the data access object for the table t_warn_config. +// You can define custom methods on it to extend its functionality as needed. +type tWarnConfigDao struct { + *internal.TWarnConfigDao +} + +var ( + // TWarnConfig is a globally accessible object for table t_warn_config operations. + TWarnConfig = tWarnConfigDao{internal.NewTWarnConfigDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_warn_log.go b/server/internal/dao/t_warn_log.go new file mode 100644 index 0000000..db88a6f --- /dev/null +++ b/server/internal/dao/t_warn_log.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tWarnLogDao is the data access object for the table t_warn_log. +// You can define custom methods on it to extend its functionality as needed. +type tWarnLogDao struct { + *internal.TWarnLogDao +} + +var ( + // TWarnLog is a globally accessible object for table t_warn_log operations. + TWarnLog = tWarnLogDao{internal.NewTWarnLogDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_wo_communication.go b/server/internal/dao/t_wo_communication.go new file mode 100644 index 0000000..5e99af2 --- /dev/null +++ b/server/internal/dao/t_wo_communication.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tWoCommunicationDao is the data access object for the table t_wo_communication. +// You can define custom methods on it to extend its functionality as needed. +type tWoCommunicationDao struct { + *internal.TWoCommunicationDao +} + +var ( + // TWoCommunication is a globally accessible object for table t_wo_communication operations. + TWoCommunication = tWoCommunicationDao{internal.NewTWoCommunicationDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_wo_main.go b/server/internal/dao/t_wo_main.go new file mode 100644 index 0000000..e2c5949 --- /dev/null +++ b/server/internal/dao/t_wo_main.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tWoMainDao is the data access object for the table t_wo_main. +// You can define custom methods on it to extend its functionality as needed. +type tWoMainDao struct { + *internal.TWoMainDao +} + +var ( + // TWoMain is a globally accessible object for table t_wo_main operations. + TWoMain = tWoMainDao{internal.NewTWoMainDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/t_wo_record.go b/server/internal/dao/t_wo_record.go new file mode 100644 index 0000000..7b1ad4a --- /dev/null +++ b/server/internal/dao/t_wo_record.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// tWoRecordDao is the data access object for the table t_wo_record. +// You can define custom methods on it to extend its functionality as needed. +type tWoRecordDao struct { + *internal.TWoRecordDao +} + +var ( + // TWoRecord is a globally accessible object for table t_wo_record operations. + TWoRecord = tWoRecordDao{internal.NewTWoRecordDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/test_demo.go b/server/internal/dao/test_demo.go new file mode 100644 index 0000000..c7b79fe --- /dev/null +++ b/server/internal/dao/test_demo.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// testDemoDao is the data access object for the table test_demo. +// You can define custom methods on it to extend its functionality as needed. +type testDemoDao struct { + *internal.TestDemoDao +} + +var ( + // TestDemo is a globally accessible object for table test_demo operations. + TestDemo = testDemoDao{internal.NewTestDemoDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/dao/test_tree.go b/server/internal/dao/test_tree.go new file mode 100644 index 0000000..f6e83ce --- /dev/null +++ b/server/internal/dao/test_tree.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao/internal" +) + +// testTreeDao is the data access object for the table test_tree. +// You can define custom methods on it to extend its functionality as needed. +type testTreeDao struct { + *internal.TestTreeDao +} + +var ( + // TestTree is a globally accessible object for table test_tree operations. + TestTree = testTreeDao{internal.NewTestTreeDao()} +) + +// Add your custom methods and functionality below. diff --git a/server/internal/library/addons/addons.go b/server/internal/library/addons/addons.go new file mode 100644 index 0000000..ed23ca4 --- /dev/null +++ b/server/internal/library/addons/addons.go @@ -0,0 +1,16 @@ +// Package addons +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package addons + +import ( + "context" + genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model" +) + +func ModuleSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) { + output = make([]*genmodel.SelectItemModel, 0) + return +} diff --git a/server/internal/library/appidentity/terminal_user.go b/server/internal/library/appidentity/terminal_user.go new file mode 100644 index 0000000..0e9e994 --- /dev/null +++ b/server/internal/library/appidentity/terminal_user.go @@ -0,0 +1,190 @@ +package appidentity + +import ( + "context" + "strconv" + "strings" + + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity" + + "github.com/gogf/gf/v2/errors/gerror" +) + +type TerminalUserConfig struct { + DeptId int64 + RoleId int64 +} + +func IsTerminalUser(ctx context.Context, user *entity.SysUser) (bool, error) { + if user == nil || user.UserId <= 0 { + return false, nil + } + cfg, err := LoadTerminalUserConfig(ctx, user.TenantId) + if err != nil { + return false, err + } + if cfg == nil { + return false, nil + } + hasRole, err := userHasRole(ctx, user.UserId, cfg.RoleId) + if err != nil { + return false, err + } + return hasRole, nil +} + +func IsTerminalUserId(ctx context.Context, userId int64) (bool, error) { + if userId <= 0 { + return false, nil + } + var user *entity.SysUser + if err := dao.SysUser.Ctx(ctx). + Where(dao.SysUser.Columns().UserId, userId). + WhereNull(dao.SysUser.Columns().DeletedAt). + Limit(1). + Scan(&user); err != nil { + return false, gerror.Wrap(err, "查询小程序用户身份失败") + } + if user == nil || user.UserId <= 0 { + return false, nil + } + return IsTerminalUser(ctx, user) +} + +func LoadTerminalUserConfig(ctx context.Context, tenantId string) (*TerminalUserConfig, error) { + tenantId = strings.TrimSpace(tenantId) + if tenantId == "" { + tenantId = consts.DefaultSystemTenantCode + } + deptValue, err := tenantConfigValue(ctx, tenantId, consts.ConfigKeyAppRegisterDeptId) + if err != nil { + return nil, gerror.Wrap(err, "读取小程序所属组织配置失败") + } + roleValue, err := tenantConfigValue(ctx, tenantId, consts.ConfigKeyAppRegisterRoleId) + if err != nil { + return nil, gerror.Wrap(err, "读取小程序角色配置失败") + } + deptValue = strings.TrimSpace(deptValue) + roleValue = strings.TrimSpace(roleValue) + if roleValue == "" { + return nil, nil + } + roleId, err := positiveConfigId(roleValue, "小程序角色ID") + if err != nil { + return nil, err + } + deptId := int64(0) + if deptValue != "" { + deptId, err = positiveConfigId(deptValue, "小程序所属组织ID") + if err != nil { + return nil, err + } + } + return &TerminalUserConfig{ + DeptId: deptId, + RoleId: roleId, + }, nil +} + +func tenantConfigValue(ctx context.Context, tenantId string, key string) (string, error) { + var config *entity.SysConfig + if err := dao.SysConfig.Ctx(ctx). + Where(dao.SysConfig.Columns().TenantId, tenantId). + Where(dao.SysConfig.Columns().ConfigKey, key). + OrderDesc(dao.SysConfig.Columns().ConfigId). + Limit(1). + Scan(&config); err != nil { + return "", err + } + if config == nil || config.ConfigId == 0 { + return "", nil + } + return strings.TrimSpace(config.ConfigValue), nil +} + +func positiveConfigId(value string, label string) (int64, error) { + id, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64) + if err != nil || id <= 0 { + return 0, gerror.Newf("%s配置不正确", label) + } + return id, nil +} + +func userHasRole(ctx context.Context, userId int64, roleId int64) (bool, error) { + if userId <= 0 || roleId <= 0 { + return false, nil + } + count, err := dao.SysUserRole.Ctx(ctx). + Where(dao.SysUserRole.Columns().UserId, userId). + Where(dao.SysUserRole.Columns().RoleId, roleId). + Count() + if err != nil { + return false, gerror.Wrap(err, "校验小程序角色失败") + } + return count > 0, nil +} + +func userInTerminalDept(ctx context.Context, user *entity.SysUser, terminalDeptId int64) (bool, error) { + if user == nil || user.DeptId <= 0 || terminalDeptId <= 0 { + return false, nil + } + if user.DeptId == terminalDeptId { + return true, nil + } + deptIds, err := deptAndSubIds(ctx, user.TenantId, terminalDeptId) + if err != nil { + return false, err + } + for _, deptId := range deptIds { + if deptId == user.DeptId { + return true, nil + } + } + return false, nil +} + +func deptAndSubIds(ctx context.Context, tenantId string, rootDeptId int64) ([]int64, error) { + if rootDeptId <= 0 { + return []int64{}, nil + } + tenantId = strings.TrimSpace(tenantId) + if tenantId == "" { + tenantId = consts.DefaultSystemTenantCode + } + + seen := map[int64]struct{}{ + rootDeptId: {}, + } + result := []int64{rootDeptId} + level := []int64{rootDeptId} + + for len(level) > 0 { + var rows []*entity.SysDept + err := dao.SysDept.Ctx(ctx). + Fields(dao.SysDept.Columns().DeptId). + Where(dao.SysDept.Columns().TenantId, tenantId). + WhereIn(dao.SysDept.Columns().ParentId, level). + WhereNull(dao.SysDept.Columns().DeletedAt). + Scan(&rows) + if err != nil { + return nil, gerror.Wrap(err, "查询小程序所属组织下级失败") + } + + nextLevel := make([]int64, 0, len(rows)) + for _, row := range rows { + if row == nil || row.DeptId <= 0 { + continue + } + if _, ok := seen[row.DeptId]; ok { + continue + } + seen[row.DeptId] = struct{}{} + result = append(result, row.DeptId) + nextLevel = append(nextLevel, row.DeptId) + } + level = nextLevel + } + return result, nil +} diff --git a/server/internal/library/batchimport/async.go b/server/internal/library/batchimport/async.go new file mode 100644 index 0000000..25dc4f5 --- /dev/null +++ b/server/internal/library/batchimport/async.go @@ -0,0 +1,168 @@ +package batchimport + +import ( + "context" + "strings" + + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +type PendingOptions struct { + PrepareRows func(ctx context.Context, rows []Row) error +} + +func CreatePendingUpload(ctx context.Context, req SubmitRequest, p Processor, options PendingOptions) (*Result, error) { + cfg := p.Config() + if req.File == nil { + return nil, gerror.New("请选择上传文件") + } + if err := service.SysOss().CheckType(ctx, consts.SysUploadFileTypeFile, req.File); err != nil { + return nil, err + } + if req.File.Size > cfg.MaxFileSize { + return nil, gerror.Newf("上传文件大小超过限制,最大为%dM", cfg.MaxFileSize>>20) + } + + batchNo, err := NextBatchNoContext(ctx, cfg.BatchNoPrefix) + if err != nil { + return nil, err + } + req.BatchNo = batchNo + content, err := readUploadContent(req.File) + if err != nil { + return nil, err + } + importOss, err := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{ + Content: content, + FileName: batchNo + "-import.xlsx", + NewFileType: consts.SysUploadNewFileTypeUserFile, + SubDirName: cfg.ImportDir, + }) + if err != nil { + return nil, gerror.Wrap(err, "保存原始Excel失败") + } + + rows, err := p.ParseRows(ctx, importOss.Path) + if err != nil { + return nil, err + } + return createPending(ctx, &req, p, rows, importOss, options) +} + +func CreatePendingRows(ctx context.Context, req SubmitRequest, p Processor, rows []Row, options PendingOptions) (*Result, error) { + batchNo, err := NextBatchNoContext(ctx, p.Config().BatchNoPrefix) + if err != nil { + return nil, err + } + req.BatchNo = batchNo + return createPending(ctx, &req, p, rows, nil, options) +} + +func createPending(ctx context.Context, req *SubmitRequest, p Processor, rows []Row, importOss *model.SysOssSaveContentModel, options PendingOptions) (*Result, error) { + if len(rows) == 0 { + return nil, gerror.New("导入数据为空") + } + if err := p.ValidateRows(ctx, rows); err != nil { + return nil, err + } + if options.PrepareRows != nil { + if err := options.PrepareRows(ctx, rows); err != nil { + return nil, err + } + } + + result := &Result{ + ImportStatus: consts.ImportStatusSuccess, + BatchNo: req.BatchNo, + BatchType: p.Config().BatchType, + TotalRows: len(rows), + } + if importOss != nil { + result.ImportFileOssId = importOss.OssId + result.ImportFileUrl = importOss.Url + } + + if err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + sourceBatchNo := strings.TrimSpace(req.SourceBatchNo) + if sourceBatchNo == "" { + sourceBatchNo = req.BatchNo + } + if _, err := dao.BatchMain.Ctx(ctx).TX(tx).Data(do.BatchMain{ + BatchNo: req.BatchNo, + OrderSn: "", + BatchType: p.Config().BatchType, + BatchStatus: consts.BatchAsyncStatusPending, + BatchAsyncStatus: consts.BatchAsyncStatusPending, + SourceType: req.SourceType, + SourceBatchNo: sourceBatchNo, + IsCrossDept: 0, + TargetDeptId: req.TargetDeptId, + DeptId: req.Operator.DeptId, + TotalCount: len(rows), + ValidCount: 0, + SuccessCount: 0, + FailCount: 0, + RunResult: consts.BatchRunResultPending, + FailReason: "", + BatchInfo: p.BuildBatchInfo(ctx, *req, result, importOss, nil, 0), + TenantId: req.Operator.TenantId, + CreatedDept: req.Operator.DeptId, + CreatedBy: req.Operator.UserId, + UpdatedBy: req.Operator.UserId, + }).Insert(); err != nil { + return gerror.Wrap(err, "写入待处理批次失败") + } + + details := make([]do.BatchDetail, 0, len(rows)) + for _, row := range rows { + details = append(details, do.BatchDetail{ + BatchNo: req.BatchNo, + BatchType: p.Config().BatchType, + DetailStatus: consts.BatchDetailStatusPending, + BlankOutStatus: consts.BlankOutStatusNo, + RowNo: row.GetRowNo(), + ItemId: row.GetItemID(), + ErrorCode: "", + ErrorReason: strings.Join(row.GetErrorReasons(), ";"), + Snapshot: row.Snapshot(), + TenantId: req.Operator.TenantId, + CreatedDept: req.Operator.DeptId, + CreatedBy: req.Operator.UserId, + UpdatedBy: req.Operator.UserId, + }) + } + _, err := dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert() + return gerror.Wrap(err, "写入待处理批次明细失败") + }); err != nil { + return nil, err + } + return result, nil +} + +func SaveAsyncResultFiles( + ctx context.Context, + req SubmitRequest, + p Processor, + result *Result, + rows []Row, + importOss *model.SysOssSaveContentModel, + startTime, endTime *gtime.Time, +) (*model.SysOssSaveContentModel, int64, error) { + resultOss, downloadId, err := saveResultExcelAndDownload(ctx, req, p, result, rows, importOss, startTime, endTime) + if err != nil { + return nil, 0, err + } + result.ResultDownloadId = downloadId + result.ResultFileOssId = resultOss.OssId + result.ResultFileUrl = resultOss.Url + return resultOss, downloadId, nil +} diff --git a/server/internal/library/batchimport/batch_no.go b/server/internal/library/batchimport/batch_no.go new file mode 100644 index 0000000..afcc514 --- /dev/null +++ b/server/internal/library/batchimport/batch_no.go @@ -0,0 +1,117 @@ +package batchimport + +import ( + "context" + "crypto/rand" + "fmt" + "strings" + "time" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" +) + +const ( + batchNoPrefixLength = 3 + batchNoSequenceLength = 3 + batchNoSequenceMax = 999 + batchNoSequenceTTL = 48 * time.Hour + batchNoSequenceRedisPrefix = "iotlcm:batch:no:seq:" + randomNoSuffixLength = 8 + randomNoAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" +) + +func NextBatchNo(prefix string) (string, error) { + return NextBatchNoContext(context.Background(), prefix) +} + +func NextBatchNoContext(ctx context.Context, prefix string) (string, error) { + prefix, err := normalizeBatchNoPrefix(prefix) + if err != nil { + return "", err + } + if ctx == nil { + ctx = context.Background() + } + now := time.Now() + seq, err := nextDailyBatchSequence(ctx, now) + if err != nil { + return "", err + } + return formatBatchNo(prefix, now, seq) +} + +func NextRandomNo(prefix string) (string, error) { + prefix = strings.TrimSpace(strings.ToUpper(prefix)) + if prefix == "" { + return "", gerror.New("编号前缀不能为空") + } + + random, err := randomNoSuffix(randomNoSuffixLength) + if err != nil { + return "", err + } + return fmt.Sprintf("%s%s%s", prefix, time.Now().Format("20060102150405"), random), nil +} + +func normalizeBatchNoPrefix(prefix string) (string, error) { + prefix = strings.TrimSpace(strings.ToUpper(prefix)) + if prefix == "" { + return "", gerror.New("批次号前缀不能为空") + } + if len(prefix) != batchNoPrefixLength { + return "", gerror.Newf("批次号前缀必须为%d位", batchNoPrefixLength) + } + return prefix, nil +} + +func nextDailyBatchSequence(ctx context.Context, now time.Time) (int64, error) { + key := batchNoSequenceKey(now) + script := ` +local v = redis.call("INCR", KEYS[1]) +if v == 1 then + redis.call("EXPIRE", KEYS[1], ARGV[1]) +end +return v` + res, err := g.Redis().Do(ctx, "EVAL", script, 1, key, int(batchNoSequenceTTL.Seconds())) + if err != nil { + return 0, gerror.Wrap(err, "生成批次号序号失败") + } + seq := res.Int64() + if seq <= 0 { + return 0, gerror.New("生成批次号序号失败") + } + if seq > batchNoSequenceMax { + return 0, gerror.Newf("当日批次号序号超过%03d", batchNoSequenceMax) + } + return seq, nil +} + +func batchNoSequenceKey(now time.Time) string { + return batchNoSequenceRedisPrefix + now.Format("20060102") +} + +func formatBatchNo(prefix string, now time.Time, seq int64) (string, error) { + if seq <= 0 || seq > batchNoSequenceMax { + return "", gerror.Newf("批次号序号必须在1-%03d之间", batchNoSequenceMax) + } + return fmt.Sprintf("%s%s%0*d", prefix, now.Format("20060102150405"), batchNoSequenceLength, seq), nil +} + +func randomNoSuffix(length int) (string, error) { + if length <= 0 { + return "", gerror.New("随机码长度必须大于0") + } + + buf := make([]byte, length) + randomBytes := make([]byte, length) + if _, err := rand.Read(randomBytes); err != nil { + return "", gerror.Wrap(err, "生成随机码失败") + } + + alphabetLen := byte(len(randomNoAlphabet)) + for i, b := range randomBytes { + buf[i] = randomNoAlphabet[b%alphabetLen] + } + return string(buf), nil +} diff --git a/server/internal/library/batchimport/batch_no_test.go b/server/internal/library/batchimport/batch_no_test.go new file mode 100644 index 0000000..fb0ecac --- /dev/null +++ b/server/internal/library/batchimport/batch_no_test.go @@ -0,0 +1,45 @@ +package batchimport + +import ( + "strings" + "testing" + "time" +) + +func TestNormalizeBatchNoPrefix(t *testing.T) { + prefix, err := normalizeBatchNoPrefix(" inb ") + if err != nil { + t.Fatalf("normalize prefix: %v", err) + } + if prefix != "INB" { + t.Fatalf("unexpected prefix: %s", prefix) + } + + if _, err = normalizeBatchNoPrefix("IN"); err == nil { + t.Fatal("expected error for non-three-character prefix") + } +} + +func TestFormatBatchNo(t *testing.T) { + now := time.Date(2026, 8, 10, 12, 34, 56, 789000000, time.Local) + batchNo, err := formatBatchNo("REN", now, 7) + if err != nil { + t.Fatalf("format batch no: %v", err) + } + if batchNo != "REN20260810123456007" { + t.Fatalf("unexpected batch no: %s", batchNo) + } + if len(batchNo) != 20 { + t.Fatalf("unexpected batch no length: %d", len(batchNo)) + } + if strings.Contains(batchNo, "789") { + t.Fatalf("batch no should not include milliseconds: %s", batchNo) + } +} + +func TestFormatBatchNoRejectsOverflowSequence(t *testing.T) { + _, err := formatBatchNo("REN", time.Now(), 1000) + if err == nil { + t.Fatal("expected overflow sequence error") + } +} diff --git a/server/internal/library/batchimport/runner.go b/server/internal/library/batchimport/runner.go new file mode 100644 index 0000000..df4f78c --- /dev/null +++ b/server/internal/library/batchimport/runner.go @@ -0,0 +1,596 @@ +package batchimport + +import ( + "context" + "fmt" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + "git.5iot.com/iotlcm-public/iotlcm-app/utility/hash" + "git.5iot.com/iotlcm-public/iotlcm-app/utility/iot_excelize" + "io" + "os" + "strings" + + "github.com/gogf/gf/v2/crypto/gmd5" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" + "github.com/xuri/excelize/v2" +) + +func SubmitAndRun(ctx context.Context, req SubmitRequest, p Processor) (*Result, error) { + cfg := p.Config() + if req.File == nil { + return nil, gerror.New("请选择上传文件") + } + if err := service.SysOss().CheckType(ctx, consts.SysUploadFileTypeFile, req.File); err != nil { + return nil, err + } + if req.File.Size > cfg.MaxFileSize { + return nil, gerror.Newf("上传文件大小超过限制,最大为%dM", cfg.MaxFileSize>>20) + } + + batchNo, err := NextBatchNoContext(ctx, cfg.BatchNoPrefix) + if err != nil { + return nil, err + } + req.BatchNo = batchNo + result := &Result{ + ImportStatus: consts.ImportStatusFailed, + BatchNo: batchNo, + } + + content, err := readUploadContent(req.File) + if err != nil { + return nil, err + } + + importOss, err := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{ + Content: content, + FileName: batchNo + "-import.xlsx", + NewFileType: consts.SysUploadNewFileTypeUserFile, + SubDirName: cfg.ImportDir, + NotAddDate: 0, + }) + if err != nil { + return nil, gerror.Wrap(err, "保存原始Excel失败") + } + result.ImportFileOssId = importOss.OssId + result.ImportFileUrl = importOss.Url + + rows, err := p.ParseRows(ctx, importOss.Path) + if err != nil { + result.FailReason = userFacingFailReason(err) + if err = archiveFailedBatchAfterImport(ctx, req, p, result, nil, importOss); err != nil { + return nil, err + } + return result, nil + } + result.TotalRows = len(rows) + + if err = p.ValidateRows(ctx, rows); err != nil { + for _, row := range rows { + row.AddErrorReason(userFacingFailReason(err)) + } + } + fillResultCounts(result, rows) + + startTime := gtime.Now() + endTime := gtime.Now() + if err = saveBatchAndExecute(ctx, &req, p, result, rows, importOss, startTime, endTime); err != nil { + markRowsFailed(result, rows, userFacingFailReason(err)) + if err = saveBatchAndExecute(ctx, &req, p, result, rows, importOss, startTime, endTime); err != nil { + return nil, err + } + } + result.ExecutionResult = req.ExecutionResult + + resultOss, downloadId, err := saveResultExcelAndDownload(ctx, req, p, result, rows, importOss, startTime, endTime) + if err != nil { + return nil, err + } + result.ResultDownloadId = downloadId + result.ResultFileOssId = resultOss.OssId + result.ResultFileUrl = resultOss.Url + + if err = updateBatchResultFileInfo(ctx, req, p, result, importOss, resultOss, downloadId); err != nil { + return nil, err + } + + return result, nil +} + +func RunRows(ctx context.Context, req SubmitRequest, p Processor, rows []Row, sourceName string) (*Result, error) { + cfg := p.Config() + batchNo, err := NextBatchNoContext(ctx, cfg.BatchNoPrefix) + if err != nil { + return nil, err + } + req.BatchNo = batchNo + result := &Result{ + ImportStatus: consts.ImportStatusFailed, + BatchNo: batchNo, + TotalRows: len(rows), + } + + if len(rows) == 0 { + result.FailReason = "导入数据为空" + } else if err = p.ValidateRows(ctx, rows); err != nil { + for _, row := range rows { + row.AddErrorReason(userFacingFailReason(err)) + } + } + fillResultCounts(result, rows) + if result.FailReason != "" { + result.ImportStatus = consts.ImportStatusFailed + result.InsertedRows = 0 + } + + startTime := gtime.Now() + endTime := gtime.Now() + importOss := &model.SysOssSaveContentModel{SysOss: &entity.SysOss{}} + if sourceName != "" { + importOss.OriginalName = sourceName + } + if err = saveBatchAndExecute(ctx, &req, p, result, rows, importOss, startTime, endTime); err != nil { + markRowsFailed(result, rows, userFacingFailReason(err)) + if err = saveBatchAndExecute(ctx, &req, p, result, rows, importOss, startTime, endTime); err != nil { + return nil, err + } + } + result.ExecutionResult = req.ExecutionResult + + resultOss, downloadId, err := saveResultExcelAndDownload(ctx, req, p, result, rows, importOss, startTime, endTime) + if err != nil { + return nil, err + } + result.ResultDownloadId = downloadId + result.ResultFileOssId = resultOss.OssId + result.ResultFileUrl = resultOss.Url + + if err = updateBatchResultFileInfo(ctx, req, p, result, importOss, resultOss, downloadId); err != nil { + return nil, err + } + + return result, nil +} + +func archiveFailedBatchAfterImport(ctx context.Context, req SubmitRequest, p Processor, result *Result, rows []Row, importOss *model.SysOssSaveContentModel) error { + result.ImportStatus = consts.ImportStatusFailed + result.InsertedRows = 0 + if result.FailReason == "" { + result.FailReason = "导入失败" + } + + startTime := gtime.Now() + endTime := gtime.Now() + if err := saveBatchAndExecute(ctx, &req, p, result, rows, importOss, startTime, endTime); err != nil { + return err + } + result.ExecutionResult = req.ExecutionResult + + resultOss, downloadId, err := saveResultExcelAndDownload(ctx, req, p, result, rows, importOss, startTime, endTime) + if err != nil { + return err + } + result.ResultDownloadId = downloadId + result.ResultFileOssId = resultOss.OssId + result.ResultFileUrl = resultOss.Url + + return updateBatchResultFileInfo(ctx, req, p, result, importOss, resultOss, downloadId) +} + +func readUploadContent(file *ghttp.UploadFile) ([]byte, error) { + reader, err := file.Open() + if err != nil { + return nil, gerror.Wrap(err, "打开上传文件失败") + } + defer reader.Close() + + content, err := io.ReadAll(reader) + if err != nil { + return nil, gerror.Wrap(err, "读取上传文件失败") + } + return content, nil +} + +func fillResultCounts(result *Result, rows []Row) { + failCount := 0 + for _, row := range rows { + if len(row.GetErrorReasons()) > 0 { + failCount++ + } + } + result.ErrorRows = failCount + result.ValidRows = result.TotalRows - failCount + if result.ValidRows < 0 { + result.ValidRows = 0 + } + if failCount == 0 { + result.ImportStatus = consts.ImportStatusSuccess + result.InsertedRows = len(rows) + return + } + result.ImportStatus = consts.ImportStatusFailed + result.InsertedRows = 0 +} + +func markRowsFailed(result *Result, rows []Row, reason string) { + if result == nil { + return + } + if reason == "" { + reason = "业务处理失败" + } + for _, row := range rows { + row.AddErrorReason(reason) + } + result.ImportStatus = consts.ImportStatusFailed + result.FailReason = reason + result.TotalRows = len(rows) + result.ValidRows = 0 + result.ErrorRows = len(rows) + result.InsertedRows = 0 +} + +// saveBatchAndExecute writes stock data only when all rows pass validation. +// Failed validation keeps the batch and result file, but skips business inserts. +func saveBatchAndExecute(ctx context.Context, req *SubmitRequest, p Processor, result *Result, rows []Row, importOss *model.SysOssSaveContentModel, startTime, endTime *gtime.Time) error { + return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + now := gtime.Now() + failReason := result.FailReason + if failReason == "" && result.ErrorRows > 0 { + failReason = "存在校验失败数据,本次未写入" + } + + batchType := batchTypeByImportStatus(req, p, result.ImportStatus) + result.BatchType = batchType + if result.ImportStatus == consts.ImportStatusSuccess && len(rows) > 0 && req.BeforeSave != nil { + if err := req.BeforeSave(ctx, tx, req, rows); err != nil { + return err + } + } + sourceBatchNo := req.SourceBatchNo + if sourceBatchNo == "" { + sourceBatchNo = result.BatchNo + } + + if _, err := dao.BatchMain.Ctx(ctx).TX(tx).Data(do.BatchMain{ + BatchNo: result.BatchNo, + OrderSn: "", + BatchType: batchType, + BatchStatus: batchStatusByImportStatus(result.ImportStatus), + BatchAsyncStatus: batchStatusByImportStatus(result.ImportStatus), + SourceType: req.SourceType, + SourceBatchNo: sourceBatchNo, + IsCrossDept: 0, + TargetDeptId: req.TargetDeptId, + DeptId: req.Operator.DeptId, + TotalCount: result.TotalRows, + ValidCount: result.ValidRows, + SuccessCount: result.InsertedRows, + FailCount: result.ErrorRows, + RunResult: runResultByImportStatus(result.ImportStatus), + FailReason: failReason, + BatchInfo: p.BuildBatchInfo(ctx, *req, result, importOss, nil, 0), + StartTime: startTime, + EndTime: endTime, + TenantId: req.Operator.TenantId, + CreatedDept: req.Operator.DeptId, + CreatedBy: req.Operator.UserId, + CreatedAt: now, + UpdatedBy: req.Operator.UserId, + UpdatedAt: now, + }).Insert(); err != nil { + return gerror.Wrap(err, "写入批次主表失败") + } + + details := make([]do.BatchDetail, 0, len(rows)) + for _, row := range rows { + details = append(details, do.BatchDetail{ + BatchNo: result.BatchNo, + BatchType: batchType, + DetailStatus: detailStatus(row), + BlankOutStatus: consts.BlankOutStatusNo, + RowNo: row.GetRowNo(), + ItemId: row.GetItemID(), + ErrorCode: "", + ErrorReason: strings.Join(row.GetErrorReasons(), ";"), + Snapshot: row.Snapshot(), + TenantId: req.Operator.TenantId, + CreatedDept: req.Operator.DeptId, + CreatedBy: req.Operator.UserId, + CreatedAt: now, + UpdatedBy: req.Operator.UserId, + UpdatedAt: now, + }) + } + if len(details) > 0 { + if _, err := dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert(); err != nil { + return gerror.Wrap(err, "写入批次明细失败") + } + } + + if result.ImportStatus == consts.ImportStatusSuccess && len(rows) > 0 && !req.SkipExecute { + if err := p.Execute(ctx, tx, req, rows); err != nil { + return err + } + } + return nil + }) +} + +func saveResultExcelAndDownload(ctx context.Context, req SubmitRequest, p Processor, result *Result, rows []Row, importOss *model.SysOssSaveContentModel, startTime, endTime *gtime.Time) (*model.SysOssSaveContentModel, int64, error) { + cfg := p.Config() + resultRows := make([]iot_excelize.BatchResultRow, 0, len(rows)) + for _, row := range rows { + resultRows = append(resultRows, iot_excelize.BatchResultRow{ + RowNo: row.GetRowNo(), + ItemID: row.GetItemID(), + Status: displayRowStatus(row), + ErrorReason: strings.Join(row.GetErrorReasons(), ";"), + }) + } + + importName := "" + importOssId := "" + if importOss != nil { + importName = importOss.OriginalName + if importOss.OssId > 0 { + importOssId = fmt.Sprintf("%d", importOss.OssId) + } + } + + infos := []iot_excelize.BatchInfoItem{ + {Field: "批次号", Value: result.BatchNo}, + {Field: "批次类型", Value: taskTypeName(p)}, + {Field: "处理结果", Value: displayImportStatus(result.ImportStatus)}, + {Field: "总行数", Value: fmt.Sprintf("%d", result.TotalRows)}, + {Field: "校验通过行数", Value: fmt.Sprintf("%d", result.ValidRows)}, + {Field: "失败行数", Value: fmt.Sprintf("%d", result.ErrorRows)}, + {Field: "实际写入行数", Value: fmt.Sprintf("%d", result.InsertedRows)}, + {Field: "原始文件名", Value: importName}, + {Field: "原始文件OSS ID", Value: importOssId}, + {Field: "结果文件OSS ID", Value: ""}, + {Field: "操作人", Value: fmt.Sprintf("%d", req.Operator.UserId)}, + {Field: "开始时间", Value: formatGTime(startTime)}, + {Field: "结束时间", Value: formatGTime(endTime)}, + {Field: "备注", Value: resultRemark(result)}, + } + + f, err := iot_excelize.BuildBatchResultExcel(resultRows, infos) + if err != nil { + return nil, 0, err + } + defer f.Close() + + reader, err := iot_excelize.ToReadSeeker(f) + if err != nil { + return nil, 0, err + } + content, err := io.ReadAll(reader) + if err != nil { + return nil, 0, err + } + + resultOss, err := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{ + Content: content, + FileName: result.BatchNo + "-result.xlsx", + NewFileType: consts.SysUploadNewFileTypeUserFile, + SubDirName: cfg.ResultDir, + NotAddDate: 0, + }) + if err != nil { + return nil, 0, gerror.Wrap(err, "保存结果Excel失败") + } + if err = fillResultOssId(ctx, req, resultOss); err != nil { + return nil, 0, err + } + + now := gtime.Now() + downloadId, err := dao.TResourceBatchDownload.Ctx(ctx).Data(do.TResourceBatchDownload{ + BatchNo: result.BatchNo, + TaskName: cfg.TaskName, + ResourceType: consts.ResourceTypeExcel, + ExportScene: cfg.ExportScene, + TotalCount: result.TotalRows, + SuccessCount: result.InsertedRows, + FailCount: result.ErrorRows, + DownloadStatus: consts.DownloadStatusGenerated, + OssId: resultOss.OssId, + FileName: resultOss.FileName, + FileExt: resultOss.FileSuffix, + FileUrl: resultOss.Url, + FilePath: resultOss.Path, + FileSize: resultOss.FileSize, + FileMd5: resultOss.Md5, + DownloadCount: 0, + MaxDownloadCount: 0, + DeptId: req.Operator.DeptId, + StartTime: startTime, + FinishTime: now, + TenantId: req.Operator.TenantId, + CreatedDept: req.Operator.DeptId, + CreatedBy: req.Operator.UserId, + CreatedAt: now, + UpdatedBy: req.Operator.UserId, + UpdatedAt: now, + }).InsertAndGetId() + if err != nil { + return nil, 0, gerror.Wrap(err, "写入结果下载任务失败") + } + + return resultOss, downloadId, nil +} + +func fillResultOssId(ctx context.Context, req SubmitRequest, resultOss *model.SysOssSaveContentModel) error { + f, err := excelize.OpenFile(resultOss.Path) + if err != nil { + return gerror.Wrap(err, "打开结果Excel失败") + } + defer f.Close() + + const batchInfoSheet = "批次信息" + rows, err := f.GetRows(batchInfoSheet) + if err != nil { + return gerror.Wrap(err, "读取结果Excel批次信息失败") + } + + found := false + for i, row := range rows { + if len(row) == 0 || strings.TrimSpace(row[0]) != "结果文件OSS ID" { + continue + } + cell, err := excelize.CoordinatesToCellName(2, i+1) + if err != nil { + return err + } + if err = f.SetCellStr(batchInfoSheet, cell, fmt.Sprintf("%d", resultOss.OssId)); err != nil { + return err + } + found = true + break + } + if !found { + return gerror.New("结果Excel缺少结果文件OSS ID字段") + } + if err = f.SaveAs(resultOss.Path); err != nil { + return gerror.Wrap(err, "回写结果Excel OSS ID失败") + } + + fileInfo, err := os.Stat(resultOss.Path) + if err != nil { + return gerror.Wrap(err, "读取结果Excel文件信息失败") + } + md5, _ := gmd5.EncryptFile(resultOss.Path) + crc16 := hash.Crc16ChecksumFile(resultOss.Path) + sum16 := hash.Sum16ChecksumFile(resultOss.Path) + + resultOss.FileSize = int(fileInfo.Size()) + resultOss.Md5 = md5 + resultOss.FileCrc16 = int(crc16) + resultOss.FileSum = int(sum16) + + _, err = dao.SysOss.Ctx(ctx). + Where(dao.SysOss.Columns().OssId, resultOss.OssId). + Data(do.SysOss{ + Md5: resultOss.Md5, + FileSize: resultOss.FileSize, + FileCrc16: resultOss.FileCrc16, + FileSum: resultOss.FileSum, + UpdatedAt: gtime.Now(), + UpdatedBy: req.Operator.UserId, + }). + Update() + if err != nil { + return gerror.Wrap(err, "更新结果Excel文件信息失败") + } + return nil +} + +func updateBatchResultFileInfo(ctx context.Context, req SubmitRequest, p Processor, result *Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) error { + _, err := dao.BatchMain.Ctx(ctx). + Where(dao.BatchMain.Columns().BatchNo, result.BatchNo). + Data(do.BatchMain{ + BatchInfo: p.BuildBatchInfo(ctx, req, result, importOss, resultOss, downloadId), + UpdatedBy: req.Operator.UserId, + UpdatedAt: gtime.Now(), + }). + Update() + if err != nil { + return gerror.Wrap(err, "更新批次结果文件信息失败") + } + return nil +} + +func batchStatusByImportStatus(importStatus string) int { + if importStatus == consts.ImportStatusSuccess { + return consts.BatchAsyncStatusSuccess + } + return consts.BatchAsyncStatusFailed +} + +func runResultByImportStatus(importStatus string) int { + if importStatus == consts.ImportStatusSuccess { + return consts.BatchRunResultSuccess + } + return consts.BatchRunResultFailed +} + +func batchTypeByImportStatus(req *SubmitRequest, p Processor, importStatus string) int { + if req == nil { + return p.Config().BatchType + } + if importStatus != consts.ImportStatusSuccess && req.FailureBatchType > 0 { + return req.FailureBatchType + } + if req.BatchType > 0 { + return req.BatchType + } + return p.Config().BatchType +} + +func detailStatus(row Row) int { + if len(row.GetErrorReasons()) > 0 { + return consts.BatchDetailStatusFailed + } + return consts.BatchDetailStatusSuccess +} + +func displayRowStatus(row Row) string { + if len(row.GetErrorReasons()) > 0 { + return "失败" + } + return "成功" +} + +func displayImportStatus(status string) string { + if status == consts.ImportStatusSuccess { + return "成功" + } + return "失败" +} + +func resultRemark(result *Result) string { + if result != nil && result.ImportStatus == consts.ImportStatusSuccess { + return "导入成功" + } + if result != nil && result.FailReason != "" { + return result.FailReason + } + return "存在校验失败数据,本次未写入" +} + +func userFacingFailReason(err error) string { + if err == nil { + return "" + } + reason := strings.TrimSpace(err.Error()) + if reason == "" { + return "导入失败" + } + const maxLen = 500 + if len([]rune(reason)) <= maxLen { + return reason + } + runes := []rune(reason) + return string(runes[:maxLen]) +} + +func taskTypeName(p Processor) string { + taskName := p.Config().TaskName + return strings.TrimSuffix(taskName, "结果") +} + +func formatGTime(t *gtime.Time) string { + if t == nil { + return "" + } + return t.Format("Y-m-d H:i:s") +} diff --git a/server/internal/library/batchimport/types.go b/server/internal/library/batchimport/types.go new file mode 100644 index 0000000..5350f30 --- /dev/null +++ b/server/internal/library/batchimport/types.go @@ -0,0 +1,82 @@ +package batchimport + +import ( + "context" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/net/ghttp" +) + +type SubmitRequest struct { + File *ghttp.UploadFile + BizParams any + SourceType int + SourceBatchNo string + TargetDeptId int64 + Operator OperatorContext + BatchNo string + BatchType int + FailureBatchType int + SkipExecute bool + BeforeSave func(ctx context.Context, tx gdb.TX, req *SubmitRequest, rows []Row) error + ExecutionResult any +} + +type OperatorContext struct { + TenantId string + DeptId int64 + UserId int64 +} + +type Result struct { + ImportStatus string + BatchNo string + BatchType int + FailReason string + ImportFileOssId int64 + ImportFileUrl string + TotalRows int + ValidRows int + ErrorRows int + InsertedRows int + ResultDownloadId int64 + ResultFileOssId int64 + ResultFileUrl string + ExecutionResult any +} + +type Row interface { + GetRowNo() int + GetItemID() string + GetErrorReasons() []string + AddErrorReason(reason string) + Snapshot() *gjson.Json +} + +type ProcessorConfig struct { + Code string + BatchType int + BatchNoPrefix string + TaskName string + ExportScene int + ImportDir string + ResultDir string + MaxFileSize int64 + TemplateHeaders []string + RequiredHeaders []string +} + +type Processor interface { + Config() ProcessorConfig + ParseRows(ctx context.Context, filePath string) ([]Row, error) + ValidateRows(ctx context.Context, rows []Row) error + Execute(ctx context.Context, tx gdb.TX, req *SubmitRequest, rows []Row) error + BuildBatchInfo(ctx context.Context, req SubmitRequest, result *Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json +} + +type Message struct { + BatchNo string `json:"batchNo"` + BatchType int `json:"batchType"` +} diff --git a/server/internal/library/bcache/bcache.go b/server/internal/library/bcache/bcache.go new file mode 100644 index 0000000..083c8a3 --- /dev/null +++ b/server/internal/library/bcache/bcache.go @@ -0,0 +1,6 @@ +// Package bcache +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package bcache diff --git a/server/internal/library/bcache/bcache_system_auth.go b/server/internal/library/bcache/bcache_system_auth.go new file mode 100644 index 0000000..66f7720 --- /dev/null +++ b/server/internal/library/bcache/bcache_system_auth.go @@ -0,0 +1,77 @@ +// Package bcache +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package bcache + +import ( + "context" + "fmt" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/cache" + "time" + + "github.com/gogf/gf/v2/frame/g" +) + +// 设置系统认证token缓存 +func SetSysAuthToken(ctx context.Context, userId int64, uuid string, token string, timeout time.Duration) (err error) { + key := fmt.Sprintf(consts.KeySysAuthToken, userId, uuid) + err = cache.Instance().Set(ctx, key, token, timeout) + if err != nil { + return err + } + return nil +} + +// 获取系统认证token缓存 +func GetSysAuthToken(ctx context.Context, userId int64, uuid string) (token string, err error) { + key := fmt.Sprintf(consts.KeySysAuthToken, userId, uuid) + t, err := cache.Instance().Get(ctx, key) + if err != nil { + g.Log().Errorf(ctx, "bcache.GetSysAuthToken Get failed,error: %v, userId: %d, uuid: %s, key: %s", err, userId, uuid, key) + return "", err + } + return t.String(), nil +} + +// 删除系统认证token缓存 +func DelSysAuthToken(ctx context.Context, userId int64, uuid string) (err error) { + key := fmt.Sprintf(consts.KeySysAuthToken, userId, uuid) + _, err = cache.Instance().Remove(ctx, key) + if err != nil { + return err + } + return nil +} + +// 设置系统认证token拒绝缓存 +func SetSysAuthTokenReject(ctx context.Context, userId int64, uuid string, token string, timeout time.Duration) (err error) { + key := fmt.Sprintf(consts.KeySysAuthTokenReject, userId, uuid) + err = cache.Instance().Set(ctx, key, token, timeout) + if err != nil { + return err + } + return nil +} + +// 获取系统认证token拒绝缓存 +func GetSysAuthTokenReject(ctx context.Context, userId int64, uuid string) (token string, err error) { + key := fmt.Sprintf(consts.KeySysAuthTokenReject, userId, uuid) + t, err := cache.Instance().Get(ctx, key) + if err != nil { + return "", err + } + return t.String(), nil +} + +// 删除系统认证token拒绝缓存 +func DelSysAuthTokenReject(ctx context.Context, userId int64, uuid string) (err error) { + key := fmt.Sprintf(consts.KeySysAuthTokenReject, userId, uuid) + _, err = cache.Instance().Remove(ctx, key) + if err != nil { + return err + } + return nil +} diff --git a/server/internal/library/cache/cache.go b/server/internal/library/cache/cache.go new file mode 100644 index 0000000..8310c22 --- /dev/null +++ b/server/internal/library/cache/cache.go @@ -0,0 +1,60 @@ +// Package cache +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package cache + +import ( + "context" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/cache/file" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcache" + "github.com/gogf/gf/v2/os/gfile" +) + +// cache 缓存驱动 +var cacheVal *gcache.Cache + +// Instance 缓存实例 +func Instance() *gcache.Cache { + if cacheVal == nil { + panic("cache uninitialized.") + } + return cacheVal +} + +// SetAdapter 设置缓存适配器 +func SetAdapter(ctx context.Context) { + var cacheAdapter gcache.Adapter + adapter := g.Cfg().MustGet(ctx, "cache.adapter").String() + fileDir := g.Cfg().MustGet(ctx, "cache.fileDir").String() + + switch adapter { + case "redis": + cacheAdapter = gcache.NewAdapterRedis(g.Redis()) + case "file": + if fileDir == "" { + g.Log().Fatal(ctx, "file path must be configured for file caching.") + return + } + + if !gfile.Exists(fileDir) { + if err := gfile.Mkdir(fileDir); err != nil { + g.Log().Fatalf(ctx, "failed to create the cache directory. procedure, err:%+v", err) + return + } + } + cacheAdapter = file.NewAdapterFile(fileDir) + default: + cacheAdapter = gcache.NewAdapterMemory() + } + + // 数据库缓存,默认和通用缓冲驱动一致,如果你不想使用默认的,可以自行调整 + g.DB().GetCache().SetAdapter(cacheAdapter) + + // 通用缓存 + cacheVal = gcache.New() + cacheVal.SetAdapter(cacheAdapter) +} diff --git a/server/internal/library/cache/file/file.go b/server/internal/library/cache/file/file.go new file mode 100644 index 0000000..3fdb7ba --- /dev/null +++ b/server/internal/library/cache/file/file.go @@ -0,0 +1,440 @@ +// Package file +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package file + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/os/gcache" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/util/gconv" +) + +type ( + // AdapterFile gcache适配器使用文件的实现 + AdapterFile struct { + dir string + mutex sync.RWMutex // 添加互斥锁以支持并发操作 + } + + fileContent struct { + Duration int64 `json:"duration"` + Data interface{} `json:"data,omitempty"` + } +) + +const perm = 0o666 //设置文件所有者、同用户组成员和其他用户都可以读写该文件,但都不能执。 + +var ( + CacheExpiredErr = errors.New("cache expired") +) + +// NewAdapterFile creates and returns a new memory cache object. +func NewAdapterFile(dir string) gcache.Adapter { + return &AdapterFile{ + dir: dir, + } +} + +func (c *AdapterFile) Set(ctx context.Context, key interface{}, value interface{}, lifeTime time.Duration) (err error) { + c.mutex.Lock() // 加锁 + defer c.mutex.Unlock() // 解锁 + fileKey := gconv.String(key) + if value == nil || lifeTime < 0 { + return c.Delete(fileKey) + } + return c.Save(fileKey, gconv.String(value), lifeTime) +} + +// SetMap 批量设置多个键值对,并且每个键值对的有效期是一样的 +func (c *AdapterFile) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) (err error) { + c.mutex.Lock() + defer c.mutex.Unlock() + for key, value := range data { + fileKey := gconv.String(key) + if err = c.Save(fileKey, gconv.String(value), duration); err != nil { + return err + } + } + return nil +} + +// SetIfNotExist 在指定的键不存在时设置其值 +func (c *AdapterFile) SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + fileKey := gconv.String(key) + if c.Has(fileKey) { + return false, nil + } + + err = c.Save(fileKey, gconv.String(value), duration) + return true, err +} + +// SetIfNotExistFunc 在指定的键不存在时,通过一个函数来生成值并设置它 +func (c *AdapterFile) SetIfNotExistFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + fileKey := gconv.String(key) + if c.Has(fileKey) { + return false, nil + } + + value, err := f(ctx) + if err != nil { + return false, err + } + err = c.Save(fileKey, gconv.String(value), duration) + return true, err +} + +// SetIfNotExistFuncLock 与 SetIfNotExistFunc 类似,但它在调用生成值的函数时提供了额外的锁机制,以避免在生成值期间的并发问题。 +func (c *AdapterFile) SetIfNotExistFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + fileKey := gconv.String(key) + if c.Has(fileKey) { + return false, nil + } + + // 在这里加锁是为了确保在值生成期间不会有并发的写入操作 + value, err := f(ctx) + if err != nil { + return false, err + } + err = c.Save(fileKey, gconv.String(value), duration) + return true, err +} + +func (c *AdapterFile) Get(ctx context.Context, key interface{}) (*gvar.Var, error) { + fetch, err := c.Fetch(gconv.String(key)) + if err != nil { + return nil, err + } + return gvar.New(fetch), nil +} + +func (c *AdapterFile) GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) { + result, err = c.Get(ctx, key) + if err != nil && !errors.Is(err, CacheExpiredErr) { + return nil, err + } + if result.IsNil() { + return gvar.New(value), c.Set(ctx, key, value, duration) + } + return +} + +func (c *AdapterFile) GetOrSetFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) { + v, err := c.Get(ctx, key) + if err != nil && !errors.Is(err, CacheExpiredErr) { + return nil, err + } + if v.IsNil() { + value, err := f(ctx) + if err != nil { + return nil, err + } + if value == nil { + return nil, nil + } + return gvar.New(value), c.Set(ctx, key, value, duration) + } else { + return v, nil + } +} + +func (c *AdapterFile) GetOrSetFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) { + return c.GetOrSetFunc(ctx, key, f, duration) +} + +func (c *AdapterFile) Contains(ctx context.Context, key interface{}) (bool, error) { + return c.Has(gconv.String(key)), nil +} + +func (c *AdapterFile) Size(ctx context.Context) (size int, err error) { + return 0, nil +} + +// Data 获取所有缓存的键值对 +func (c *AdapterFile) Data(ctx context.Context) (data map[interface{}]interface{}, err error) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + // 初始化返回的数据结构 + data = make(map[interface{}]interface{}) + + // 读取文件目录中的所有缓存文件 + files, err := os.ReadDir(c.dir) + if err != nil { + return nil, err + } + + // 遍历缓存文件,提取数据 + for _, file := range files { + content, err := c.read(file.Name()) + if err != nil { + continue // 忽略无法读取的文件 + } + data[file.Name()] = content.Data + } + return data, nil +} + +// Keys 获取所有缓存的键 +func (c *AdapterFile) Keys(ctx context.Context) (keys []interface{}, err error) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + files, err := os.ReadDir(c.dir) + if err != nil { + return nil, err + } + + keys = make([]interface{}, 0, len(files)) + for _, file := range files { + keys = append(keys, file.Name()) + } + return keys, nil +} + +// Values 获取所有缓存的值 +func (c *AdapterFile) Values(ctx context.Context) (values []interface{}, err error) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + files, err := os.ReadDir(c.dir) + if err != nil { + return nil, err + } + + values = make([]interface{}, 0, len(files)) + for _, file := range files { + content, err := c.read(file.Name()) + if err != nil { + continue // 忽略无法读取的文件 + } + values = append(values, content.Data) + } + return values, nil +} + +// Update 更新一个指定键的缓存值,如果这个键不存在,则返回错误 +func (c *AdapterFile) Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + fileKey := gconv.String(key) + content, err := c.read(fileKey) + if err != nil { + return nil, false, err + } + if content == nil { + return nil, false, nil + } + + oldValue = gvar.New(content.Data) + err = c.Save(fileKey, gconv.String(value), time.Duration(content.Duration)*time.Second) + return oldValue, true, err +} + +func (c *AdapterFile) UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) { + var ( + v *gvar.Var + oldTTL int64 + fileKey = gconv.String(key) + ) + // TTL. + expire, err := c.GetExpire(ctx, fileKey) + if err != nil { + return + } + oldTTL = int64(expire) + if oldTTL == -2 { + // It does not exist. + return + } + oldDuration = time.Duration(oldTTL) * time.Second + // DEL. + if duration < 0 { + err = c.Delete(fileKey) + return + } + v, err = c.Get(ctx, fileKey) + if err != nil { + return + } + err = c.Set(ctx, fileKey, v.Val(), duration) + return +} + +func (c *AdapterFile) GetExpire(ctx context.Context, key interface{}) (time.Duration, error) { + content, err := c.read(gconv.String(key)) + if err != nil { + return -1, nil + } + + if content.Duration <= time.Now().Unix() { + return -1, nil + } + return time.Duration(time.Now().Unix()-content.Duration) * time.Second, nil +} + +func (c *AdapterFile) Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) { + if len(keys) == 0 { + return nil, nil + } + // Retrieves the last key value. + if lastValue, err = c.Get(ctx, gconv.String(keys[len(keys)-1])); err != nil { + return nil, err + } + // Deletes all given keys. + err = c.DeleteMulti(gconv.Strings(keys)...) + return +} + +func (c *AdapterFile) Clear(ctx context.Context) error { + return c.Flush() +} + +func (c *AdapterFile) Close(ctx context.Context) error { + return nil +} + +func (c *AdapterFile) createName(key string) string { + h := sha256.New() + _, _ = h.Write([]byte(key)) + hash := hex.EncodeToString(h.Sum(nil)) + return filepath.Join(c.dir, fmt.Sprintf("%s.cache", hash)) +} + +func (c *AdapterFile) read(key string) (*fileContent, error) { + rp := gfile.RealPath(c.createName(key)) + if rp == "" { + return nil, nil + } + + value, err := os.ReadFile(rp) + if err != nil { + return nil, err + } + + content := &fileContent{} + if err := json.Unmarshal(value, content); err != nil { + return nil, err + } + + if content.Duration == 0 { + return content, nil + } + + if content.Duration <= time.Now().Unix() { + _ = c.Delete(key) + return nil, CacheExpiredErr + } + return content, nil +} + +// Has checks if the cached key exists into the File storage +func (c *AdapterFile) Has(key string) bool { + fc, err := c.read(key) + return err == nil && fc != nil +} + +// Delete the cached key from File storage +func (c *AdapterFile) Delete(key string) error { + _, err := os.Stat(c.createName(key)) + if err != nil && os.IsNotExist(err) { + return nil + } + return os.Remove(c.createName(key)) +} + +// DeleteMulti the cached key from File storage +func (c *AdapterFile) DeleteMulti(keys ...string) (err error) { + for _, key := range keys { + if err = c.Delete(key); err != nil { + return + } + } + return +} + +// Fetch retrieves the cached value from key of the File storage +func (c *AdapterFile) Fetch(key string) (interface{}, error) { + content, err := c.read(key) + if err != nil { + return nil, err + } + + if content == nil { + return nil, nil + } + return content.Data, nil +} + +// FetchMulti retrieve multiple cached values from keys of the File storage +func (c *AdapterFile) FetchMulti(keys []string) map[string]interface{} { + result := make(map[string]interface{}) + for _, key := range keys { + if value, err := c.Fetch(key); err == nil { + result[key] = value + } + } + return result +} + +// Flush removes all cached keys of the File storage +func (c *AdapterFile) Flush() error { + dir, err := os.Open(c.dir) + if err != nil { + return err + } + + defer func() { + _ = dir.Close() + }() + + names, _ := dir.Readdirnames(-1) + + for _, name := range names { + _ = os.Remove(filepath.Join(c.dir, name)) + } + return nil +} + +// Save a value in File storage by key +func (c *AdapterFile) Save(key string, value string, lifeTime time.Duration) error { + duration := int64(0) + + if lifeTime > 0 { + duration = time.Now().Unix() + int64(lifeTime.Seconds()) + } + + content := &fileContent{duration, value} + + data, err := json.Marshal(content) + if err != nil { + return err + } + + err = os.WriteFile(c.createName(key), data, perm) + return err +} diff --git a/server/internal/library/contexts/contexts.go b/server/internal/library/contexts/contexts.go new file mode 100644 index 0000000..5cb4dce --- /dev/null +++ b/server/internal/library/contexts/contexts.go @@ -0,0 +1,198 @@ +// Package contexts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package contexts + +import ( + "context" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "slices" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" +) + +type ContextKey struct { + name string +} + +var contextKey = &ContextKey{ + name: consts.ContextKey, +} + +// Init初始化上下文对象指针到上下文对象中,以便后续的请求流程中可以修改。 +func Init(r *ghttp.Request, customCtx *model.Context) { + r.SetCtxVar(contextKey, customCtx) +} + +// Set设置本地上下文 +func Set(ctx context.Context, customCtx *model.Context) context.Context { + ctx = context.WithValue(ctx, contextKey, customCtx) + return ctx +} + +// Get 获取上下文变量; +func Get(ctx context.Context) *model.Context { + customCtx, ok := ctx.Value(contextKey).(*model.Context) + if !ok { + return nil + } + return customCtx +} + +// Set 设置上下文变量 +func SetUser(ctx context.Context, user *model.Identity) { + customCtx := Get(ctx) + customCtx.User = user +} + +// GetUser 获取上下文变量中的用户信息 +func GetUser(ctx context.Context) *model.Identity { + customCtx := Get(ctx) + if customCtx == nil { + return nil + } + return customCtx.User +} + +// GetUserId 获取上下文变量中的用户ID +func GetUserId(ctx context.Context) int64 { + user := GetUser(ctx) + if user == nil { + return 0 + } + return user.ID +} + +// GetUserUuid 获取上下文变量中的用户UUID +func GetUserUuid(ctx context.Context) string { + user := GetUser(ctx) + if user == nil { + return "" + } + return user.UUID +} + +// GetUserName 获取上下文变量中的用户名 +func GetUserName(ctx context.Context) string { + user := GetUser(ctx) + if user == nil { + return "" + } + return user.Username +} + +// GetNickName 获取上下文变量中的用户昵称 +func GetNickName(ctx context.Context) string { + user := GetUser(ctx) + if user == nil { + return "" + } + return user.NickName +} + +// GetDeptId 获取上下文变量中的部门ID +func GetDeptId(ctx context.Context) int64 { + user := GetUser(ctx) + if user == nil { + return 0 + } + return user.DeptId +} + +// GetTenantId 获取上下文变量中的租户ID +func GetTenantId(ctx context.Context) string { + user := GetUser(ctx) + if user == nil { + return "" + } + return user.TenantId +} + +// GetRoles 获取上下文变量中的角色列表 +func GetRoles(ctx context.Context) []model.Role { + user := GetUser(ctx) + if user == nil { + return nil + } + return user.Roles +} + +// GetLoginAt 获取上下文变量中的登录时间 +func GetLoginAt(ctx context.Context) int64 { + user := GetUser(ctx) + if user == nil { + return 0 + } + return user.LoginAt +} + +// IsSystemTenant 判断是否是系统租户 +func IsSystemTenant(ctx context.Context) bool { + tenantId := GetTenantId(ctx) + return tenantId == consts.DefaultSystemTenantCode +} + +// IsSuperAdmin 判断是否是超级管理员角色 +func IsSuperAdmin(ctx context.Context) bool { + roles := GetRoles(ctx) + return slices.ContainsFunc(roles, func(role model.Role) bool { + return role.RoleId == consts.SuperAdminRoleId + }) +} +func IsWebSocket(ctx context.Context) bool { + customCtx := Get(ctx) + if customCtx == nil { + return false + } + return customCtx.IsWebSocket +} +func SetIsWebSocket(ctx context.Context, isWebSocket bool) { + customCtx := Get(ctx) + if customCtx == nil { + return + } + customCtx.IsWebSocket = isWebSocket + g.Log().Debugf(ctx, "SetIsWebSocket: %v", isWebSocket) +} + +// GetAccessCodeList 获取上下文变量中的访问码列表 +func GetAccessCodeList(ctx context.Context) []string { + user := GetUser(ctx) + if user == nil { + return nil + } + if len(user.AccessCodeList) == 0 { + return nil + } + return user.AccessCodeList +} + +func GetDataValue(ctx context.Context, key string) interface{} { + customCtx := Get(ctx) + if customCtx == nil { + return nil + } + if customCtx.Data == nil { + return nil + } + value, ok := customCtx.Data[key] + if !ok { + return nil + } + return value +} + +func SetDataValue(ctx context.Context, key string, value interface{}) { + customCtx := Get(ctx) + if customCtx == nil { + return + } + if customCtx.Data == nil { + customCtx.Data = make(g.Map) + } + customCtx.Data[key] = value +} diff --git a/server/internal/library/event/event.go b/server/internal/library/event/event.go new file mode 100644 index 0000000..68fa5a8 --- /dev/null +++ b/server/internal/library/event/event.go @@ -0,0 +1,75 @@ +// Package event +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package event + +import ( + "context" + "sync" +) + +type EventCallbackFunc func(ctx context.Context, eventKey string, args ...interface{}) + +type sEvent struct { + sync.Mutex + eventList map[string][]EventCallbackFunc // 事件列表 [事件Key]:回调函数 +} + +var ( + instance *sEvent + once sync.Once +) + +// EventsInstance 事件实例 +func EventsInstance() *sEvent { + once.Do(func() { + instance = &sEvent{ + eventList: make(map[string][]EventCallbackFunc), + } + }) + return instance +} + +// Register 注册事件 +func (e *sEvent) Register(eventKey interface{}, callback EventCallbackFunc) { + e.Lock() + defer e.Unlock() + eventKeyStr, ok1 := eventKey.(string) + eventKeyList, ok2 := eventKey.([]string) + if !ok1 && !ok2 { + return + } + if ok1 { + e.eventList[eventKeyStr] = append(e.eventList[eventKeyStr], callback) + } else { + for _, eventKey := range eventKeyList { + e.eventList[eventKey] = append(e.eventList[eventKey], callback) + } + } +} + +// Call 回调事件 +func (e *sEvent) Call(ctx context.Context, eventKey string, args ...interface{}) { + if events, ok := e.eventList[eventKey]; ok { + for _, f := range events { + f(ctx, eventKey, args...) + } + } +} + +// Emit 触发事件 +func (e *sEvent) Emit(ctx context.Context, eventKey string, args ...interface{}) { + e.Call(ctx, eventKey, args...) +} + +// Remove 删除事件 +func (e *sEvent) Remove(eventKey string) { + delete(e.eventList, eventKey) +} + +// Clear 清空事件列表 +func (e *sEvent) Clear() { + e.eventList = make(map[string][]EventCallbackFunc) +} diff --git a/server/internal/library/mcache/mcache.go b/server/internal/library/mcache/mcache.go new file mode 100644 index 0000000..e17955b --- /dev/null +++ b/server/internal/library/mcache/mcache.go @@ -0,0 +1,46 @@ +// Package mcache +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +// 内存缓存管理 +package mcache + +import ( + "context" + "sync" + "time" + + "github.com/gogf/gf/v2/os/gcache" +) + +// cache 缓存驱动 +var ( + mcache *gcache.Cache + mu sync.Mutex +) + +// Instance 缓存实例 +func Instance() *gcache.Cache { + if mcache == nil { + mu.Lock() + if mcache == nil { + mcache = gcache.NewWithAdapter(gcache.NewAdapterMemory()) + } + mu.Unlock() + } + return mcache +} + +func Set(ctx context.Context, key string, value interface{}, timeout time.Duration) error { + return Instance().Set(ctx, key, value, timeout) +} + +func Get(ctx context.Context, key string) (interface{}, error) { + return Instance().Get(ctx, key) +} + +func Remove(ctx context.Context, key string) error { + _, err := Instance().Remove(ctx, key) + return err +} diff --git a/server/internal/library/mcache/mcache_access_list.go b/server/internal/library/mcache/mcache_access_list.go new file mode 100644 index 0000000..8f22f80 --- /dev/null +++ b/server/internal/library/mcache/mcache_access_list.go @@ -0,0 +1,75 @@ +// Package mcache +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package mcache + +import ( + "context" + "fmt" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + "time" + + "github.com/gogf/gf/v2/frame/g" +) + +func init() { + event.EventsInstance().Register(consts.EventKeyUserLogout, func(ctx context.Context, eventKey string, args ...interface{}) { + if len(args) == 0 { + return + } + userId := args[0].(int64) + RemoveUserAccessCodeList(ctx, userId) + g.Log().Infof(ctx, "mcache_access_list.go EventKeyUserLogout userId: %d", userId) + }) + +} + +func GetUserAccessCodeList(ctx context.Context, userId int64) ([]string, []string, error) { + accessCodeList, err := Instance().Get(ctx, fmt.Sprintf(consts.MemCacheUserAccessCodeList, userId)) + menuRoleDataAccessCodeList, err := Instance().Get(ctx, fmt.Sprintf(consts.MemCacheUserRoleDataAccessCodeList, userId)) + if err != nil || accessCodeList == nil { + accessCodeCurrList, menuRoleDataAccessCodeList, err := service.SysAuth().GetUserAccessCodeList(ctx, userId) + if err != nil { + return nil, nil, err + } + accessCodeCurrList = ensureCurrentUserAccessCode(accessCodeCurrList) + if len(accessCodeCurrList) == 0 { + accessCodeCurrList = []string{"null"} + } + Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserAccessCodeList, userId), accessCodeCurrList, time.Hour*24) + Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserRoleDataAccessCodeList, userId), menuRoleDataAccessCodeList, time.Hour*24) + return accessCodeCurrList, menuRoleDataAccessCodeList, nil + } + return ensureCurrentUserAccessCode(accessCodeList.Strings()), menuRoleDataAccessCodeList.Strings(), nil +} + +func ensureCurrentUserAccessCode(accessCodeList []string) []string { + for _, code := range accessCodeList { + if code == consts.SysCheckPermissionCurrentUser { + return accessCodeList + } + } + return append(accessCodeList, consts.SysCheckPermissionCurrentUser) +} + +func SetUserAccessCodeList(ctx context.Context, userId int64, accessCodeList []string) error { + return Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserAccessCodeList, userId), accessCodeList, time.Hour*24) +} + +func SetUserRoleDataAccessCodeList(ctx context.Context, userId int64, roleDataAccessCodeList []string) error { + return Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserRoleDataAccessCodeList, userId), roleDataAccessCodeList, time.Hour*24) +} + +func RemoveUserRoleDataAccessCodeList(ctx context.Context, userId int64) error { + _, err := Instance().Remove(ctx, fmt.Sprintf(consts.MemCacheUserRoleDataAccessCodeList, userId)) + return err +} + +func RemoveUserAccessCodeList(ctx context.Context, userId int64) error { + _, err := Instance().Remove(ctx, fmt.Sprintf(consts.MemCacheUserAccessCodeList, userId)) + return err +} diff --git a/server/internal/library/mcache/mcache_system_config.go b/server/internal/library/mcache/mcache_system_config.go new file mode 100644 index 0000000..e122e13 --- /dev/null +++ b/server/internal/library/mcache/mcache_system_config.go @@ -0,0 +1,77 @@ +// Package mcache +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package mcache + +import ( + "context" + "fmt" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + "time" +) + +func init() { + event.EventsInstance().Register([]string{consts.EventKeyDBSysConfigUpdate, consts.EventKeyDBSysConfigCreate, consts.EventKeyDBSysConfigDelete}, func(ctx context.Context, eventKey string, args ...interface{}) { + if len(args) < 1 { + return + } + // tenantId := args[0].(string) + Instance().Remove(ctx, getKey(ctx)) + }) +} +func getKey(ctx context.Context) string { + return fmt.Sprintf(consts.MemCacheSystemConfig, contexts.GetTenantId(ctx)) +} + +func GetSystemConfig(ctx context.Context, key string, defaultValue string) (string, error) { + mapV, err := Instance().Get(ctx, getKey(ctx)) + if err != nil { + data, _, err := service.SysConfig().List(ctx, &model.SysConfigListParam{ + PageInfo: request.PageInfo{ + Page: 1, + PageSize: 9999, + }, + }) + if err != nil { + return defaultValue, err + } + mapConfig := make(map[string]string) + for _, item := range data { + mapConfig[item.ConfigKey] = item.ConfigValue + } + Instance().Set(ctx, getKey(ctx), mapConfig, time.Hour*24) + return mapConfig[key], nil + } + mapConfig := mapV.MapStrStr() + if v, ok := mapConfig[key]; ok { + return v, nil + } + return defaultValue, nil +} + +func SetSystemConfig(ctx context.Context, key string, value string) error { + mapV, err := Instance().Get(ctx, getKey(ctx)) + if err != nil { + return err + } + mapConfig := mapV.MapStrStr() + mapConfig[key] = value + return Instance().Set(ctx, getKey(ctx), mapConfig, time.Hour*24) +} + +func RemoveSystemConfig(ctx context.Context, key string) error { + mapV, err := Instance().Get(ctx, getKey(ctx)) + if err != nil { + return err + } + mapConfig := mapV.MapStrStr() + delete(mapConfig, key) + return Instance().Set(ctx, getKey(ctx), mapConfig, time.Hour*24) +} diff --git a/server/internal/library/mcache/mcache_system_dept.go b/server/internal/library/mcache/mcache_system_dept.go new file mode 100644 index 0000000..533c97d --- /dev/null +++ b/server/internal/library/mcache/mcache_system_dept.go @@ -0,0 +1,49 @@ +// Package mcache +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package mcache + +import ( + "context" + "fmt" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + "time" +) + +func init() { + event.EventsInstance().Register([]string{consts.EventKeyDBSysDeptUpdate, + consts.EventKeyDBSysDeptCreate, + consts.EventKeyDBSysDeptDelete}, func(ctx context.Context, eventKey string, args ...interface{}) { + if len(args) == 0 { + return + } + deptId := args[0].(int64) + RemoveDeptName(ctx, deptId) + }) +} + +func GetDeptName(ctx context.Context, deptId int64) (string, error) { + deptName, err := Instance().Get(ctx, fmt.Sprintf(consts.MemCacheDeptName, deptId)) + if err != nil { + dept, err := service.SysDept().GetDeptById(ctx, deptId) + if err != nil { + return "", err + } + Instance().Set(ctx, fmt.Sprintf(consts.MemCacheDeptName, deptId), dept.DeptName, time.Hour*24) + return dept.DeptName, nil + } + return deptName.String(), nil +} + +func SetDeptName(ctx context.Context, deptId int64, deptName string) error { + return Instance().Set(ctx, fmt.Sprintf(consts.MemCacheDeptName, deptId), deptName, time.Hour*24) +} + +func RemoveDeptName(ctx context.Context, deptId int64) error { + _, err := Instance().Remove(ctx, fmt.Sprintf(consts.MemCacheDeptName, deptId)) + return err +} diff --git a/server/internal/library/mcache/mcache_system_user.go b/server/internal/library/mcache/mcache_system_user.go new file mode 100644 index 0000000..f34264b --- /dev/null +++ b/server/internal/library/mcache/mcache_system_user.go @@ -0,0 +1,58 @@ +// Package mcache +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package mcache + +import ( + "context" + "fmt" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + "time" + + "github.com/gogf/gf/v2/util/gconv" +) + +func init() { + event.EventsInstance().Register(consts.EventKeyUserUpdate, func(ctx context.Context, eventKey string, args ...interface{}) { + if len(args) == 0 { + return + } + userId := args[0].(int64) + RemoveUserInfo(ctx, userId) + }) +} + +func GetUserInfo(ctx context.Context, userId int64) (*model.SysUserMiniModel, error) { + userInfo, err := Instance().Get(ctx, fmt.Sprintf(consts.MemCacheUserInfo, userId)) + if err != nil { + user, err := service.SysUser().GetUserById(ctx, userId) + if err != nil { + return nil, err + } + + miniUser := &model.SysUserMiniModel{} + gconv.Struct(miniUser, user) + Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserInfo, userId), miniUser, time.Hour*24) + return miniUser, nil + } + miniUser := &model.SysUserMiniModel{} + err = userInfo.Struct(miniUser) + if err != nil { + return nil, err + } + return miniUser, nil +} + +func SetUserInfo(ctx context.Context, userId int64, userInfo *model.SysUserMiniModel) error { + return Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserInfo, userId), userInfo, time.Hour*24) +} + +func RemoveUserInfo(ctx context.Context, userId int64) error { + _, err := Instance().Remove(ctx, fmt.Sprintf(consts.MemCacheUserInfo, userId)) + return err +} diff --git a/server/internal/library/rabbitmq/publisher.go b/server/internal/library/rabbitmq/publisher.go new file mode 100644 index 0000000..94ec2eb --- /dev/null +++ b/server/internal/library/rabbitmq/publisher.go @@ -0,0 +1,98 @@ +package rabbitmq + +import ( + "context" + "encoding/json" + "sync" + "time" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + amqp "github.com/rabbitmq/amqp091-go" +) + +type Config struct { + Enabled bool + Dsn string +} + +var ( + conn *amqp.Connection + connMu sync.Mutex +) + +func GetConfig(ctx context.Context) Config { + return Config{ + Enabled: g.Cfg().MustGet(ctx, "rabbitmq.enabled", false).Bool(), + Dsn: g.Cfg().MustGet(ctx, "rabbitmq.dsn", "").String(), + } +} + +func Enabled(ctx context.Context) bool { + cfg := GetConfig(ctx) + return cfg.Enabled && cfg.Dsn != "" +} + +func Publish(ctx context.Context, queue string, data any) error { + body, err := json.Marshal(data) + if err != nil { + return gerror.Wrap(err, "序列化 RabbitMQ 消息失败") + } + return PublishBytes(ctx, queue, body) +} + +func PublishBytes(ctx context.Context, queue string, body []byte) error { + if queue == "" { + return gerror.New("RabbitMQ 队列名不能为空") + } + ch, err := channel(ctx) + if err != nil { + return err + } + defer ch.Close() + + if _, err = ch.QueueDeclare(queue, true, false, false, false, nil); err != nil { + return gerror.Wrapf(err, "声明 RabbitMQ 队列失败: %s", queue) + } + return ch.PublishWithContext(ctx, "", queue, false, false, amqp.Publishing{ + ContentType: "application/json", + DeliveryMode: amqp.Persistent, + Timestamp: time.Now(), + Body: body, + }) +} + +func channel(ctx context.Context) (*amqp.Channel, error) { + c, err := connection(ctx) + if err != nil { + return nil, err + } + ch, err := c.Channel() + if err != nil { + return nil, gerror.Wrap(err, "打开 RabbitMQ Channel 失败") + } + return ch, nil +} + +func connection(ctx context.Context) (*amqp.Connection, error) { + cfg := GetConfig(ctx) + if !cfg.Enabled { + return nil, gerror.New("RabbitMQ 未启用") + } + if cfg.Dsn == "" { + return nil, gerror.New("RabbitMQ DSN 未配置") + } + + connMu.Lock() + defer connMu.Unlock() + if conn != nil && !conn.IsClosed() { + return conn, nil + } + + c, err := amqp.Dial(cfg.Dsn) + if err != nil { + return nil, gerror.Wrap(err, "连接 RabbitMQ 失败") + } + conn = c + return conn, nil +} diff --git a/server/internal/library/websocket/client.go b/server/internal/library/websocket/client.go new file mode 100644 index 0000000..f9b0bd9 --- /dev/null +++ b/server/internal/library/websocket/client.go @@ -0,0 +1,156 @@ +// Package websocket +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package websocket + +import ( + "context" + "fmt" + "runtime/debug" + + "git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32" + + "github.com/gogf/gf/v2/container/garray" + "github.com/gogf/gf/v2/frame/g" + "github.com/gorilla/websocket" +) + +const ( + // 用户连接超时时间 + heartbeatExpirationTime = 6 * 60 +) + +// 用户登录 +type login struct { + UserId uint64 + Client *Client +} + +// GetKey 读取客户端数据 +func (l *login) GetKey() (key string) { + key = GetUserKey(l.UserId) + return +} + +// Client 客户端连接 +type Client struct { + Addr string // 客户端地址 + ID string // 连接唯一标识 + Socket *websocket.Conn // 用户连接 + Send chan *WResponse // 待发送的数据 + SendClose bool // 发送是否关闭 + UserId uint64 // 用户ID,用户登录以后才有 + FirstTime uint64 // 首次连接事件 + HeartbeatTime uint64 // 用户上次心跳时间 + LoginTime uint64 // 登录时间 登录以后才有 + isApp bool // 是否是app + tags *garray.StrArray // 标签 + Ctx context.Context // 上下文 + closeSignal chan struct{} // 关闭信号 +} + +// NewClient 初始化 +func NewClient(addr string, socket *websocket.Conn, firstTime uint64, ctx context.Context) (client *Client) { + client = &Client{ + Addr: addr, + ID: uuid32.New(), + Socket: socket, + Send: make(chan *WResponse, 100), + SendClose: false, + FirstTime: firstTime, + HeartbeatTime: firstTime, + Ctx: ctx, + closeSignal: make(chan struct{}, 1), + tags: garray.NewStrArray(true), + } + return +} + +// 读取客户端数据 +func (c *Client) read() { + defer func() { + if r := recover(); r != nil { + fmt.Println("write stop", string(debug.Stack()), r) + } + }() + + defer func() { + c.close() + }() + + for { + _, message, err := c.Socket.ReadMessage() + if err != nil { + g.Log().Info(c.Ctx, "websocket Client.read failed.", err) + return + } + // 处理程序 + // fmt.Println(message) + ProcessData(c, message) + } +} + +// 向客户端写数据 +func (c *Client) write() { + defer func() { + if r := recover(); r != nil { + fmt.Println("write stop", string(debug.Stack()), r) + } + }() + defer func() { + clientManager.Unregister <- c + _ = c.Socket.Close() + }() + for { + select { + case <-c.closeSignal: + g.Log().Infof(mctx, "websocket client exit, user:%+v", c.UserId) + return + case message, ok := <-c.Send: + if !ok { + g.Log().Info(c.Ctx, "websocket Client.write failed.") + // 发送数据错误 关闭连接 + return + } + _ = c.Socket.WriteJSON(message) + } + } +} + +// SendMsg 发送数据 +func (c *Client) SendMsg(msg *WResponse) { + if c == nil || c.SendClose { + return + } + defer func() { + if r := recover(); r != nil { + fmt.Println("SendMsg stop:", r, string(debug.Stack())) + } + }() + c.Send <- msg +} + +// Heartbeat 心跳更新 +func (c *Client) Heartbeat(currentTime uint64) { + c.HeartbeatTime = currentTime +} + +// IsHeartbeatTimeout 心跳是否超时 +func (c *Client) IsHeartbeatTimeout(currentTime uint64) (timeout bool) { + if c.HeartbeatTime+heartbeatExpirationTime <= currentTime { + timeout = true + } + return +} + +// 关闭客户端 +func (c *Client) close() { + g.Log().Info(c.Ctx, "websocket Client.close") + if c.SendClose { + return + } + c.SendClose = true + c.closeSignal <- struct{}{} +} diff --git a/server/internal/library/websocket/client_manager.go b/server/internal/library/websocket/client_manager.go new file mode 100644 index 0000000..cd0a6c0 --- /dev/null +++ b/server/internal/library/websocket/client_manager.go @@ -0,0 +1,384 @@ +// Package websocket +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package websocket + +import ( + "context" + "fmt" + "runtime/debug" + "sync" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcron" + "github.com/gogf/gf/v2/os/gtime" +) + +// ClientManager 客户端管理 +type ClientManager struct { + Clients map[*Client]bool // 全部的连接 + ClientsLock sync.RWMutex // 读写锁 + Users map[string]*Client // 登录的用户 // uuid + UserLock sync.RWMutex // 读写锁 + Register chan *Client // 连接连接处理 + Login chan *login // 用户登录处理 + Unregister chan *Client // 断开连接处理程序 + Broadcast chan *WResponse // 广播 向全部成员发送数据 + ClientBroadcast chan *ClientWResponse // 广播 向某个客户端发送数据 + TagBroadcast chan *TagWResponse // 广播 向某个标签成员发送数据 + UserBroadcast chan *UserWResponse // 广播 向某个用户的所有链接发送数据 + TagCallbackMap map[string]func(client *Client, tag string) // 注册标签回调 + closeSignal chan struct{} // 关闭信号 + TagCountMap map[string]int // 标签map + TagLock sync.RWMutex // 标签读写锁 +} + +func NewClientManager() (clientManager *ClientManager) { + clientManager = &ClientManager{ + Clients: make(map[*Client]bool), + Users: make(map[string]*Client), + Register: make(chan *Client, 1000), + Unregister: make(chan *Client, 1000), + Broadcast: make(chan *WResponse, 1000), + TagBroadcast: make(chan *TagWResponse, 1000), + UserBroadcast: make(chan *UserWResponse, 1000), + TagCallbackMap: make(map[string]func(client *Client, tag string)), + closeSignal: make(chan struct{}, 1), + TagCountMap: make(map[string]int), + TagLock: sync.RWMutex{}, + } + return +} + +// GetUserKey 获取用户key +func GetUserKey(userId uint64) (key string) { + key = fmt.Sprintf("%s_%d", "ws", userId) + return +} + +// InClient 客户端是否存在 +func (manager *ClientManager) InClient(client *Client) (ok bool) { + manager.ClientsLock.RLock() + defer manager.ClientsLock.RUnlock() + _, ok = manager.Clients[client] + return +} + +// GetClients 获取所有客户端 +func (manager *ClientManager) GetClients() (clients map[*Client]bool) { + clients = make(map[*Client]bool) + manager.ClientsRange(func(client *Client, value bool) (result bool) { + clients[client] = value + return true + }) + return +} + +// ClientsRange 遍历 +func (manager *ClientManager) ClientsRange(f func(client *Client, value bool) bool) { + manager.ClientsLock.RLock() + defer func() { + manager.ClientsLock.RUnlock() + }() + for key, value := range manager.Clients { + r := f(key, value) + if !r { + break + } + } +} + +// GetClientsLen 获取客户端总数 +func (manager *ClientManager) GetClientsLen() (clientsLen int) { + clientsLen = len(manager.Clients) + return +} + +// AddClients 添加客户端 +func (manager *ClientManager) AddClients(client *Client) { + manager.ClientsLock.Lock() + defer manager.ClientsLock.Unlock() + manager.Clients[client] = true +} + +// DelClients 删除客户端 +func (manager *ClientManager) DelClients(client *Client) { + manager.ClientsLock.Lock() + defer manager.ClientsLock.Unlock() + if _, ok := manager.Clients[client]; ok { + // 删除标签 + client.tags.Iterator(func(k int, name string) bool { + DelTagCount(name) + return true + }) + delete(manager.Clients, client) + } +} + +// GetUserClient 获取用户的连接 +func (manager *ClientManager) GetUserClient(userId uint64) (client *Client) { + manager.UserLock.RLock() + defer manager.UserLock.RUnlock() + userKey := GetUserKey(userId) + if value, ok := manager.Users[userKey]; ok { + client = value + } + return +} + +// AddUsers 添加用户 +func (manager *ClientManager) AddUsers(key string, client *Client) { + manager.UserLock.Lock() + defer manager.UserLock.Unlock() + manager.Users[key] = client +} + +// DelUsers 删除用户 +func (manager *ClientManager) DelUsers(client *Client) (result bool) { + manager.UserLock.Lock() + defer manager.UserLock.Unlock() + key := GetUserKey(client.UserId) + if value, ok := manager.Users[key]; ok { + // 判断是否为相同的用户 + if value.Addr != client.Addr { + return + } + delete(manager.Users, key) + result = true + } + return +} + +// GetUsersLen 已登录用户数 +func (manager *ClientManager) GetUsersLen() (userLen int) { + userLen = len(manager.Users) + return +} + +// EventRegister 用户建立连接事件 +func (manager *ClientManager) EventRegister(client *Client) { + manager.AddClients(client) + //发送当前客户端标识 + client.SendMsg(&WResponse{Event: "connected", Data: g.Map{ + "ID": client.ID, + }}) + if client.UserId > 0 { + manager.AddUsers(GetUserKey(client.UserId), client) + } + g.Log().Info(client.Ctx, "websocket EventRegister.", client.UserId) +} + +// EventLogin 用户登录事件 +func (manager *ClientManager) EventLogin(login *login) { + client := login.Client + if manager.InClient(client) { + userKey := login.GetKey() + manager.AddUsers(userKey, login.Client) + } +} + +// EventUnregister 用户断开连接事件 +func (manager *ClientManager) EventUnregister(client *Client) { + g.Log().Info(client.Ctx, "websocket EventUnregister.", client.UserId) + manager.DelClients(client) + // 删除用户连接 + deleteResult := manager.DelUsers(client) + if !deleteResult { + // 不是当前连接的客户端 + return + } + // 关闭 chan + // close(client.Send) + client.close() +} + +// ClearTimeoutConnections 定时清理超时连接 +func (manager *ClientManager) clearTimeoutConnections() { + currentTime := uint64(gtime.Now().Unix()) + clients := clientManager.GetClients() + for client := range clients { + if client.IsHeartbeatTimeout(currentTime) { + //fmt.Println("心跳时间超时 关闭连接", client.Addr, client.UserId, client.LoginTime, client.HeartbeatTime) + _ = client.Socket.Close() + } + } +} + +// WebsocketPing 心跳处理 +func (manager *ClientManager) ping(ctx context.Context) { + //定时任务,发送心跳包 + _, _ = gcron.Add(ctx, "0 */1 * * * *", func(ctx context.Context) { + res := &WResponse{ + Event: Ping, + Data: g.Map{}, + } + SendToAll(res) + }) + // 定时任务,清理超时连接 + _, _ = gcron.Add(ctx, "*/30 * * * * *", func(ctx context.Context) { + manager.clearTimeoutConnections() + }) + +} + +// 管道处理程序 +func (manager *ClientManager) start() { + defer func() { + if r := recover(); r != nil { + g.Log().Warningf(mctx, "websocket start recover:%+v, stack:%+v", r, string(debug.Stack())) + return + } + }() + for { + select { + case conn := <-manager.Register: + // 建立连接事件 + manager.EventRegister(conn) + + case login := <-manager.Login: + // 用户登录 + manager.EventLogin(login) + + case conn := <-manager.Unregister: + // 断开连接事件 + manager.EventUnregister(conn) + + case message := <-manager.Broadcast: + // 全部客户端广播事件 + clients := manager.GetClients() + for conn := range clients { + conn.SendMsg(message) + } + case message := <-manager.TagBroadcast: + // 标签广播事件 + clients := manager.GetClients() + // find := false + for conn := range clients { + if conn.tags.Contains(message.Tag) { + conn.SendMsg(message.WResponse) + // find = true + } + } + // g.Log().Debugf(context.TODO(), "client_manager.go 标签广播事件 find:%v msg:%v", find, message) + case message := <-manager.UserBroadcast: + // 用户广播事件 + clients := manager.GetClients() + for conn := range clients { + if conn.UserId == message.UserID { + conn.SendMsg(message.WResponse) + } + } + case message := <-manager.ClientBroadcast: + // 单个客户端广播事件 + clients := manager.GetClients() + for conn := range clients { + if conn.ID == message.ID { + conn.SendMsg(message.WResponse) + } + } + case <-manager.closeSignal: + g.Log().Debug(mctx, "websocket closeSignal exit.") + return + } + + } +} + +// SendToAll 发送全部客户端 +func SendToAll(response *WResponse) { + clientManager.Broadcast <- response +} + +// SendToClientID 发送单个客户端 +func SendToClientID(id string, response *WResponse) { + clientRes := &ClientWResponse{ + ID: id, + WResponse: response, + } + clientManager.ClientBroadcast <- clientRes +} + +// SendToUser 发送单个用户 +func SendToUser(userID uint64, response *WResponse) { + userRes := &UserWResponse{ + UserID: userID, + WResponse: response, + } + clientManager.UserBroadcast <- userRes + // g.Log().Debugf(context.Background(), "websocket SendToUser userID:%v response:%+v", userID, response) +} + +// SendToTag 发送某个标签 +func SendToTag(tag string, response *WResponse) { + count := GetTagCount(tag) + if count == 0 { + return + } + + tagRes := &TagWResponse{ + Tag: tag, + WResponse: response, + } + clientManager.TagBroadcast <- tagRes +} + +// RegisterTagCallback 注册标签回调 +func RegisterTagCallback(tag string, callback func(client *Client, tag string)) { + clientManager.TagLock.Lock() + defer clientManager.TagLock.Unlock() + clientManager.TagCallbackMap[tag] = callback +} + +// UnregisterTagCallback 注销标签回调 +func UnregisterTagCallback(tag string) { + clientManager.TagLock.Lock() + defer clientManager.TagLock.Unlock() + delete(clientManager.TagCallbackMap, tag) +} + +// 添加标签订阅计数 +func AddTagCount(tag string) { + clientManager.TagLock.Lock() + defer clientManager.TagLock.Unlock() + if _, ok := clientManager.TagCountMap[tag]; !ok { + clientManager.TagCountMap[tag] = 1 + } else { + clientManager.TagCountMap[tag]++ + } +} + +// 删除标签订阅计数 +func DelTagCount(tag string) { + clientManager.TagLock.Lock() + defer clientManager.TagLock.Unlock() + count, ok := clientManager.TagCountMap[tag] + if ok { + if count > 0 { + clientManager.TagCountMap[tag]-- + } else { + delete(clientManager.TagCountMap, tag) + } + } +} + +// GetTagCount 获取标签订阅计数 +func GetTagCount(tag string) (count int) { + clientManager.TagLock.RLock() + defer clientManager.TagLock.RUnlock() + count, ok := clientManager.TagCountMap[tag] + if ok { + return count + } + return 0 +} + +// 变量Tag Count +func RangeTagCount(callback func(tag string, count int)) { + clientManager.TagLock.RLock() + defer clientManager.TagLock.RUnlock() + for tag, count := range clientManager.TagCountMap { + callback(tag, count) + } +} diff --git a/server/internal/library/websocket/controller.go b/server/internal/library/websocket/controller.go new file mode 100644 index 0000000..da2ebbc --- /dev/null +++ b/server/internal/library/websocket/controller.go @@ -0,0 +1,125 @@ +// Package websocket +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package websocket + +import ( + "fmt" + "strings" + + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv" +) + +// LoginController 用户登录 +func LoginController(client *Client, req *request) { + l := &WsLoginDataReq{} + gconv.Struct(req.Data, &l) + userId := gconv.Uint64(l.UserID) + client.UserId = userId + // 用户登录 + login := &login{ + UserId: userId, + Client: client, + } + clientManager.Login <- login + + client.SendMsg(&WResponse{ + Event: Login, + Data: "success", + }) +} + +func IsAppController(client *Client) { + client.isApp = true +} + +// JoinController 加入 +func JoinController(client *Client, req *request) { + name := gconv.String(req.Data["name"]) + + if !client.tags.Contains(name) { + client.tags.Append(name) + for k, v := range clientManager.TagCallbackMap { + if strings.HasPrefix(name, k) { + if v != nil { + v(client, name) + } + } + } + AddTagCount(name) + } + client.SendMsg(&WResponse{ + Event: Join, + Data: client.tags.Slice(), + }) +} + +// QuitController 退出 +func QuitController(client *Client, req *request) { + name := gconv.String(req.Data["name"]) + if client.tags.Contains(name) { + client.tags.RemoveValue(name) + DelTagCount(name) + } + client.SendMsg(&WResponse{ + Event: Quit, + Data: client.tags.Slice(), + }) +} + +func PingController(client *Client) { + currentTime := uint64(gtime.Now().Unix()) + client.Heartbeat(currentTime) +} + +// JoinsController 加入 +func JoinsController(client *Client, req *request) { + if req == nil || req.Data == nil { + fmt.Println("websocket.JoinsController req is nil or req.Data is nil") + return + } + // fmt.Println("websocket.JoinsController req.Data", req.Data) + names, ok := req.Data["names"] + if !ok || names == nil { + fmt.Println("websocket.JoinsController names is nil or names is empty") + return + } + // fmt.Println("websocket.JoinsController names", names) + for _, name := range gconv.Strings(names) { + if name == "" { + continue + } + if !client.tags.Contains(name) { + client.tags.Append(name) + for k, v := range clientManager.TagCallbackMap { + if strings.HasPrefix(name, k) { + if v != nil { + v(client, name) + } + } + } + AddTagCount(name) + } + } + client.SendMsg(&WResponse{ + Event: Joins, + Data: client.tags.Slice(), + }) +} + +// QuitController 退出 +func QuitsController(client *Client, req *request) { + for _, name := range gconv.Strings(req.Data["names"]) { + if client.tags.Contains(name) { + client.tags.RemoveValue(name) + DelTagCount(name) + } + } + client.SendMsg(&WResponse{ + Event: Quits, + Data: client.tags.Slice(), + }) +} diff --git a/server/internal/library/websocket/init.go b/server/internal/library/websocket/init.go new file mode 100644 index 0000000..fa97274 --- /dev/null +++ b/server/internal/library/websocket/init.go @@ -0,0 +1,63 @@ +// Package websocket +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package websocket + +import ( + "context" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts" + "net/http" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gorilla/websocket" +) + +var ( + mctx = gctx.GetInitCtx() // 上下文 + clientManager = NewClientManager() // 管理者 +) +var upGrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func StartWebSocket(ctx context.Context) { + g.Log().Info(ctx, "启动:WebSocket") + go clientManager.start() + go clientManager.ping(ctx) +} + +func StopWebSocket(ctx context.Context) { + g.Log().Info(ctx, "停止:WebSocket") + clientManager.closeSignal <- struct{}{} +} + +func WsPage(r *ghttp.Request) { + g.Log().Infof(r.GetCtx(), "websocket.WsPage Begin. UserID:%v ", contexts.GetUserId(r.GetCtx())) + // r.Response.Header().Set("Content-Type", "text/event-stream") + contexts.SetIsWebSocket(r.GetCtx(), true) + conn, err := upGrader.Upgrade(r.Response.Writer, r.Request, nil) + if err != nil { + r.Response.Write(err.Error()) + return + } + currentTime := uint64(gtime.Now().Unix()) + client := NewClient(conn.RemoteAddr().String(), conn, currentTime, r.GetCtx()) + + client.UserId = uint64(contexts.GetUserId(r.GetCtx())) + go client.read() + go client.write() + + // 用户连接事件 + clientManager.Register <- client + + g.Log().Infof(r.GetCtx(), "websocket.WsPage End. UserID:%v", client.UserId) +} diff --git a/server/internal/library/websocket/model.go b/server/internal/library/websocket/model.go new file mode 100644 index 0000000..0660961 --- /dev/null +++ b/server/internal/library/websocket/model.go @@ -0,0 +1,39 @@ +// Package websocket +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package websocket + +import "github.com/gogf/gf/v2/frame/g" + +// 当前输入对象 +type request struct { + Event string `json:"e"` //事件名称 + Data g.Map `json:"d"` //数据 +} + +// WResponse 输出对象 +type WResponse struct { + Event string `json:"e"` //事件名称 + Data interface{} `json:"d"` //数据 +} + +type TagWResponse struct { + Tag string + WResponse *WResponse +} + +type UserWResponse struct { + UserID uint64 + WResponse *WResponse +} + +type ClientWResponse struct { + ID string + WResponse *WResponse +} + +type WsLoginDataReq struct { + UserID uint `json:"userId" ` +} diff --git a/server/internal/library/websocket/router.go b/server/internal/library/websocket/router.go new file mode 100644 index 0000000..c1aca1d --- /dev/null +++ b/server/internal/library/websocket/router.go @@ -0,0 +1,62 @@ +// Package websocket +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package websocket + +import ( + "fmt" + + "github.com/gogf/gf/v2/util/gconv" +) + +const ( + Error = "error" + Login = "login" + Join = "join" + Quit = "quit" + IsApp = "is_app" + Ping = "ping" + Joins = "joins" + Quits = "quits" +) + +// ProcessData 处理数据 +func ProcessData(client *Client, message []byte) { + defer func() { + if r := recover(); r != nil { + fmt.Println("websocket.ProcessData 处理数据 stop", r, "message=", message) + } + }() + request := &request{} + err := gconv.Struct(message, request) + if err != nil { + fmt.Println("websocket.ProcessData 数据解析失败:", err) + return + } + fmt.Println("websocket.ProcessData request", request) + switch request.Event { + case Login: + LoginController(client, request) + break + case Join: + JoinController(client, request) + break + case Quit: + QuitController(client, request) + break + case IsApp: + IsAppController(client) + break + case Ping: + PingController(client) + break + case Joins: + JoinsController(client, request) + break + case Quits: + QuitsController(client, request) + break + } +} diff --git a/server/internal/library/worker/options.go b/server/internal/library/worker/options.go new file mode 100644 index 0000000..880dc89 --- /dev/null +++ b/server/internal/library/worker/options.go @@ -0,0 +1,225 @@ +// Package worker +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package worker + +import ( + "context" + "time" + + "github.com/gogf/gf/v2/frame/g" +) + +type Payload struct { + IsAggregate bool `json:"isAggregate"` + Group string `json:"group"` + Uid string `json:"uid"` + Payload []byte `json:"payload"` +} + +type AggregatorOptions struct { + GroupGracePeriod int // 组的优雅延迟时间(秒) + GroupMaxDelay int // 组的最大延迟时间(秒) + GroupMaxSize int // 组的最大大小 +} + +type WorkerOptions struct { + group string //任务处理器的组名 + redisAddr string //redis地址 + redisUser string //redis用户名 + redisPassword string //redis密码 + redisDB int //redis数据库 + handler func(ctx context.Context, p Payload) error //任务处理函数 + maxRetry int //任务最大重试次数 + clearArchived int //清除已归档任务的时间间隔 + timeout int //任务超时时间 + redisPeriodKey string //redis周期任务键 + aggregator *AggregatorOptions //聚合器选项 +} + +func GetDefaultWorkerOptions(ops *WorkerOptions) *WorkerOptions { + if ops != nil { + return ops + } + addr := g.Cfg().MustGet(context.Background(), "queue.asynq.redis.address").String() + pass := g.Cfg().MustGet(context.Background(), "queue.asynq.redis.pass").String() + db := g.Cfg().MustGet(context.Background(), "queue.asynq.redis.db").Int() + user := g.Cfg().MustGet(context.Background(), "queue.asynq.redis.user").String() + if user == "" { + user = "default" + } + return &WorkerOptions{ + group: "task", + redisAddr: addr, + redisPassword: pass, + redisDB: db, + redisUser: user, + } +} + +func WithWorkerGroup(group string) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).group = group + } +} + +func WithWorkerRedisAddr(addr string) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).redisAddr = addr + } +} + +func WithWorkerRedisUser(user string) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).redisUser = user + } +} + +func WithWorkerRedisPassword(pass string) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).redisPassword = pass + } +} + +func WithWorkerRedisDB(db int) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).redisDB = db + } +} + +func WithWorkerHandler(handler func(ctx context.Context, p Payload) error) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).handler = handler + } +} + +func WithWorkerMaxRetry(maxRetry int) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).maxRetry = maxRetry + } +} + +func WithWorkerClearArchived(clearArchived int) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).clearArchived = clearArchived + } +} + +func WithWorkerTimeout(timeout int) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).timeout = timeout + } +} + +func WithWorkerRedisPeriodKey(redisPeriodKey string) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).redisPeriodKey = redisPeriodKey + } +} + +func WithWorkerAggregator(aggregator *AggregatorOptions) func(*WorkerOptions) { + return func(o *WorkerOptions) { + GetDefaultWorkerOptions(o).aggregator = aggregator + } +} + +type TaskOptions struct { + uid string + group string + payload []byte + expr string // 仅周期任务 + in *time.Duration // 仅一次性任务 当前时间的延迟 + at *time.Time // 仅一次性任务 指定时间 + now bool // 仅一次性任务 当前时间 + retention int // 仅一次性任务 任务保留时间 在这个时间内任务会被保留为已完成状态,过了这个时间后,任务将被自动删除。 + replace bool // 仅一次性任务 是否替换已存在的任务 + ctx context.Context // 仅一次性任务 + maxRetry int + timeout int +} + +func GetDefaultTaskOptions(opt *TaskOptions) *TaskOptions { + if opt != nil { + return opt + } + return &TaskOptions{ + group: "group", + maxRetry: 3, + timeout: 120, + } +} + +func WithTaskUid(uid string) func(*TaskOptions) { + + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).uid = uid + } +} + +func WithTaskGroup(group string) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).group = group + } +} + +func WithTaskPayload(payload []byte) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).payload = payload + } +} + +func WithTaskExpr(expr string) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).expr = expr + } +} + +func WithTaskIn(in *time.Duration) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).in = in + } +} + +func WithTaskAt(at *time.Time) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).at = at + } +} + +func WithTaskNow(now bool) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).now = now + } +} + +func WithTaskRetention(retention int) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).retention = retention + } +} + +func WithTaskReplace(replace bool) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).replace = replace + } +} + +func WithTaskCtx(ctx context.Context) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).ctx = ctx + } +} + +func WithTaskMaxRetry(maxRetry int) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).maxRetry = maxRetry + } +} + +func WithTaskTimeout(timeout int) func(*TaskOptions) { + return func(o *TaskOptions) { + GetDefaultTaskOptions(o).timeout = timeout + } +} diff --git a/server/internal/library/worker/work_proces.go b/server/internal/library/worker/work_proces.go new file mode 100644 index 0000000..7b22a06 --- /dev/null +++ b/server/internal/library/worker/work_proces.go @@ -0,0 +1,84 @@ +// package worker +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package worker + +import ( + "context" + + "git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32" + + "github.com/gogf/gf/v2/frame/g" +) + +type Queue struct { + topic string + w *Worker +} + +type Process interface { + GetTopic() string // 获取消费主题 + Handle(ctx context.Context, p Payload) (err error) // 处理过程的方法 + GetAggregator() *AggregatorOptions // 获取聚合器配置,返回nil不使用聚合 +} + +func RegisterQueueProcess(p Process) (q *Queue) { + q = &Queue{ + topic: p.GetTopic(), + w: New(WithWorkerGroup(p.GetTopic()), WithWorkerHandler(p.Handle), WithWorkerAggregator(p.GetAggregator())), + } + return +} + +func (q *Queue) Push(ctx context.Context, topic string, data []byte, timeout int) (err error) { + err = q.w.Once( + WithTaskUid(uuid32.New()), + WithTaskGroup(topic), + WithTaskPayload(data), + WithTaskTimeout(timeout), + ) + if err != nil { + g.Log().Debug(ctx, "Run Queue TaskWorker %s Error: %v", topic, err) + } + return nil +} + +func (q *Queue) Stop() { + if q.w != nil { + q.w.Stop() + } +} + +type Scheduled struct { + topic string + w *Worker +} + +func RegisterScheduledProcess(p Process) (q *Queue) { + q = &Queue{ + topic: p.GetTopic(), + w: New(WithWorkerGroup(p.GetTopic()), WithWorkerHandler(p.Handle), WithWorkerAggregator(p.GetAggregator())), + } + return +} +func (s *Scheduled) Cron(ctx context.Context, topic, cronExpr string, data []byte) (entryID string, err error) { + s.topic = topic + entryID, err = s.w.Cron( + 1, + WithTaskUid(uuid32.S()), + WithTaskGroup(topic), + WithTaskExpr(cronExpr), + WithTaskPayload(data), + ) + if err != nil { + g.Log().Debug(ctx, "Run Cron TaskWorker %s Error: %v", topic, err) + } + return +} + +func (s *Scheduled) Remove(entryID string) (err error) { + err = s.w.RemoveCron(entryID) + return +} diff --git a/server/internal/library/worker/worker.go b/server/internal/library/worker/worker.go new file mode 100644 index 0000000..c58db63 --- /dev/null +++ b/server/internal/library/worker/worker.go @@ -0,0 +1,306 @@ +// package worker +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package worker + +import ( + "context" + "encoding/json" + "errors" + "strings" + "time" + + "git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32" + + "github.com/dromara/carbon/v2" + "github.com/gogf/gf/v2/frame/g" + "github.com/xiujiecn/asynq" +) + +type Worker struct { + ops *WorkerOptions + client *asynq.Client + inspector *asynq.Inspector + scheduler *asynq.Scheduler + svr *asynq.Server + Error error +} + +type periodTask struct { + Expr string `json:"expr"` // asynq + Group string `json:"group"` + Uid string `json:"uid"` + Payload []byte `json:"payload"` + Next int64 `json:"next"` // next schedule unix timestamp + Processed int64 `json:"processed"` // run times + MaxRetry int `json:"maxRetry"` + Timeout int `json:"timeout"` +} + +func (p *periodTask) String() (str string) { + bs, _ := json.Marshal(p) + str = string(bs) + return +} + +func (p *periodTask) FromString(str string) { + err := json.Unmarshal([]byte(str), p) + if err != nil { + return + } + return +} + +func aggregate(group string, tasks []*asynq.Task) *asynq.Task { + // g.Log().Infof(context.Background(), "server/internal/library/worker/worker.go aggregate group %s, tasks %d", group, len(tasks)) + list := make([][]byte, 0) + for _, t := range tasks { + list = append(list, t.Payload()) + } + payload, _ := json.Marshal(list) + return asynq.NewTask(group+".aggregate", payload, asynq.TaskID(uuid32.S())) +} + +func New(options ...func(*WorkerOptions)) *Worker { + ops := GetDefaultWorkerOptions(nil) + for _, op := range options { + op(ops) + } + + rs := asynq.RedisClientOpt{ + Addr: ops.redisAddr, + Password: ops.redisPassword, + DB: ops.redisDB, + } + + w := &Worker{} + if ops.redisAddr == "" { + w.Error = errors.New("redisAddr is nil") + return w + } + w.ops = ops + w.client = asynq.NewClient(rs) + w.inspector = asynq.NewInspector(rs) + if ops.redisPeriodKey != "" { + w.scheduler = asynq.NewScheduler(rs, &asynq.SchedulerOpts{ + Location: time.Now().Location(), + LogLevel: asynq.LogLevel(g.Cfg().MustGet(context.Background(), "queue.asynq.logLevel").Int()), // 日志级别 DebugLevel 1, InfoLevel 2, WarnLevel 3, ErrorLevel 4, FatalLevel 5 + }) + go func() { + if err := w.scheduler.Start(); err != nil { + g.Log().Error(context.Background(), "server/internal/library/worker/worker.go New 启动调度器失败", err) + panic(err) + } + g.Log().Info(context.Background(), "server/internal/library/worker/worker.go New 启动调度器成功", "group", w.ops.group) + }() + } + conf := asynq.Config{ + Concurrency: g.Cfg().MustGet(context.Background(), "queue.asynq.concurrency").Int(), // 最大同时执行的任务数量 + Queues: map[string]int{ops.group: 10}, // 队列名称和优先级 + LogLevel: asynq.LogLevel(g.Cfg().MustGet(context.Background(), "queue.asynq.logLevel").Int()), // 日志级别 DebugLevel 1, InfoLevel 2, WarnLevel 3, ErrorLevel 4, FatalLevel 5 + } + if w.ops.aggregator != nil { + conf.GroupAggregator = asynq.GroupAggregatorFunc(aggregate) + conf.GroupGracePeriod = time.Duration(w.ops.aggregator.GroupGracePeriod) * time.Second + conf.GroupMaxDelay = time.Duration(w.ops.aggregator.GroupMaxDelay) * time.Second + conf.GroupMaxSize = w.ops.aggregator.GroupMaxSize + g.Log().Infof(context.Background(), "server/internal/library/worker/worker.go New 聚合器配置 group %s, gracePeriod %d, maxDelay %d, maxSize %d", + w.ops.group, w.ops.aggregator.GroupGracePeriod, w.ops.aggregator.GroupMaxDelay, w.ops.aggregator.GroupMaxSize) + } + svr := asynq.NewServer(rs, conf) + h := &taskHandlerBase{w: w} + + mux := asynq.NewServeMux() + if w.ops.aggregator != nil { + mux.HandleFunc(w.ops.group+".aggregate", h.ProcessTask) + } else { + mux.HandleFunc(w.ops.group+".once", h.ProcessTask) + mux.HandleFunc(w.ops.group+".cron", h.ProcessTask) + } + + if err := svr.Start(mux); err != nil { + panic(err) + } + w.svr = svr + if w.ops.clearArchived > 0 { + go func() { + time.Sleep(time.Duration(w.ops.clearArchived) * time.Second) + w.clearArchived() + }() + } + g.Log().Debug(context.Background(), "server/internal/library/worker/worker.go New", "group", w.ops.group) + return w +} + +func (w *Worker) Once(options ...func(*TaskOptions)) (err error) { + ops := GetDefaultTaskOptions(nil) + for _, op := range options { + op(ops) + } + if ops.uid == "" { + err = errors.New("uid is nil") + return + } + t := asynq.NewTask(ops.group+".once", ops.payload, asynq.TaskID(ops.uid), asynq.Group(ops.group)) + taskOpts := []asynq.Option{ + asynq.Queue(w.ops.group), + asynq.MaxRetry(w.ops.maxRetry), + asynq.Timeout(time.Duration(ops.timeout) * time.Second), + } + if ops.maxRetry > 0 { + taskOpts = append(taskOpts, asynq.MaxRetry(ops.maxRetry)) + } + if ops.retention > 0 { + taskOpts = append(taskOpts, asynq.Retention(time.Duration(ops.retention)*time.Second)) + } else { + taskOpts = append(taskOpts, asynq.Retention(time.Duration(w.ops.timeout)*time.Second)) + } + if ops.in != nil { + taskOpts = append(taskOpts, asynq.ProcessIn(*ops.in)) + } else if ops.at != nil { + taskOpts = append(taskOpts, asynq.ProcessAt(*ops.at)) + } else if ops.now { + taskOpts = append(taskOpts, asynq.ProcessIn(time.Millisecond)) + } + _, err = w.client.Enqueue(t, taskOpts...) + if ops.replace && errors.Is(err, asynq.ErrTaskIDConflict) { + // 错误表示由于给定的任务ID已经存在,因此无法将该任务加入队列。这可能是因为任务已经存在,或者任务ID冲突。 + err = w.inspector.DeleteTask(w.ops.group, ops.uid) + if err != nil { + return + } + _, err = w.client.Enqueue(t, taskOpts...) + } + return +} + +// Cron 注册一个cron任务, 如果concurrent为0,则不限制并发,否则限制并发 +func (w *Worker) Cron(concurrent int, options ...func(*TaskOptions)) (entryID string, err error) { + ops := GetDefaultTaskOptions(nil) + for _, op := range options { + op(ops) + } + if ops.expr == "" { + err = errors.New("expr is nil") + return + } + if concurrent == 0 { + // 不限制并发, 不传递任务id,任务ID随机,可并发 + entryID, err = w.scheduler.Register(ops.expr, asynq.NewTask(ops.group+".cron", ops.payload), asynq.Queue(w.ops.group)) + } else { + // 限制并发, 传递任务id,任务ID固定,不可并发 + entryID, err = w.scheduler.Register(ops.expr, asynq.NewTask(ops.group+".cron", ops.payload, asynq.TaskID(ops.uid)), asynq.Queue(w.ops.group)) + } + if err != nil { + return + } + g.Log().Debug(context.Background(), "server/internal/library/worker/worker.go Cron", "group", w.ops.group, "entryID", entryID, "expr", ops.expr, "concurrent", concurrent) + return +} + +func (w *Worker) Remove(uid string) (err error) { + entries, err := w.inspector.SchedulerEntries() + if err != nil { + return + } + for _, entry := range entries { + payload := string(entry.Task.Payload()) + g.Log().Debug(context.Background(), "server/internal/library/worker/worker.go Remove", "group", w.ops.group, "entryID", entry.ID, "payload", payload) + if strings.Contains(string(entry.Task.Payload()), "\""+uid+"\"") { + g.Log().Debug(context.Background(), "strings.Contains(string(entry.Task.Payload()), uid) ") + err = w.scheduler.Unregister(entry.ID) + } + } + return +} + +func (w *Worker) GetTaskInfo(uid string) (task *asynq.TaskInfo, err error) { + task, err = w.inspector.GetTaskInfo(w.ops.group, uid) + if err != nil { + return + } + return task, nil +} + +func (w *Worker) RemoveCron(entryID string) (err error) { + err = w.scheduler.Unregister(entryID) + return +} + +// 获取带有默认超时的上下文 +func (w *Worker) getDefaultTimeoutCtx() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), time.Duration(w.ops.timeout)*time.Second) +} + +// 清除已归档任务 +func (w *Worker) clearArchived() { + list, err := w.inspector.ListArchivedTasks(w.ops.group, asynq.Page(1), asynq.PageSize(100)) + if err != nil { + return + } + // ctx := w.getDefaultTimeoutCtx() + for _, item := range list { + last := carbon.CreateFromStdTime(item.LastFailedAt) + if !last.IsZero() && item.Retried < item.MaxRetry { + continue + } + uid := item.ID + var flag bool + if strings.HasSuffix(item.Type, ".cron") { + if carbon.Now().Gt(last.AddMinutes(5)) { + flag = true + } + } else { + if carbon.Now().Gt(last.AddMinutes(5)) { + flag = true + } + } + if flag { + err := w.inspector.DeleteTask(w.ops.group, uid) + if err != nil { + return + } + } + } +} + +type taskHandlerBase struct { + w *Worker +} + +func (h *taskHandlerBase) ProcessTask(ctx context.Context, task *asynq.Task) (err error) { + var isAggregate bool + if strings.Contains(task.Type(), ".aggregate") { + isAggregate = true + } + group := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(task.Type(), ".aggregate"), ".once"), ".cron") + if h.w.ops.handler == nil { + g.Log().Errorf(ctx, "server/internal/library/worker/worker.go ProcessTask handler is nil, group: %s, uid: %s, payload: %s", group, task.ResultWriter().TaskID(), string(task.Payload())) + return errors.New("handler is nil") + } + err = h.w.ops.handler(ctx, Payload{ + IsAggregate: isAggregate, + Group: group, + Uid: task.ResultWriter().TaskID(), + Payload: task.Payload(), + }) + + return +} + +func (w *Worker) Stop() { + if w.scheduler != nil { + w.scheduler.Shutdown() + } + if w.client != nil { + w.client.Close() + } + if w.inspector != nil { + w.inspector.Close() + } + if w.svr != nil { + w.svr.Shutdown() + } +} diff --git a/server/internal/library/xgen/gen_consts/gen_codes.go b/server/internal/library/xgen/gen_consts/gen_codes.go new file mode 100644 index 0000000..f087eee --- /dev/null +++ b/server/internal/library/xgen/gen_consts/gen_codes.go @@ -0,0 +1,86 @@ +// package genconsts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genconsts + +// 生成类型值定义 +const ( + GenCodesTypeCurd = 0 + GenCodesTypeTree = 1 +) + +// 生成类型标签映射 +var GenCodesTypeNameMap = map[int]string{ + GenCodesTypeCurd: "增删改查列表", + GenCodesTypeTree: "关系树列表", +} + +// 生成类型配置映射 +var GenCodesTypeConfMap = map[int]string{ + GenCodesTypeCurd: "crud", + GenCodesTypeTree: "tree", +} + +// 生成代码状态 +const ( + GenCodesStatusOk = "0" // 生成成功 + GenCodesStatusWait = "1" // 未生成 + GenCodesStatusFail = "2" // 生成失败 +) + +var GenCodesStatusNameMap = map[string]string{ + GenCodesStatusOk: "生成成功", + GenCodesStatusWait: "未生成", + GenCodesStatusFail: "生成失败", +} + +// 生成代码关联表方式 +const ( + GenCodesJoinLeft = 1 // 左关联 + GenCodesJoinRight = 2 // 右关联 + GenCodesJoinInner = 3 // 内关联 +) + +var GenCodesJoinNameMap = map[int]string{ + GenCodesJoinLeft: "左关联", + GenCodesJoinRight: "右关联", + GenCodesJoinInner: "内关联", +} + +var GenCodesJoinLinkMap = map[int]string{ + GenCodesJoinLeft: "LeftJoin", + GenCodesJoinRight: "RightJoin", + GenCodesJoinInner: "InnerJoin", +} + +// 生成代码的生成方式 +const ( + GenCodesBuildMethCreate = 1 // 创建 + GenCodesBuildMethCover = 2 // 覆盖 + GenCodesBuildMethSkip = 3 // 跳过 + GenCodesBuildIgnore = 4 // 不生成 +) + +var GenCodesBuildMethNameMap = map[int]string{ + GenCodesBuildMethCreate: "创建文件", + GenCodesBuildMethCover: "强制覆盖", + GenCodesBuildMethSkip: "已存在跳过", + GenCodesBuildIgnore: "不生成", +} + +const ( + GenCodesIndexPK = "PRI" // 主键索引 + GenCodesIndexUNI = "UNI" // 唯一索引 +) + +const ( + GenCodesTreeStyleTypeNormal = 1 // 普通树表格 + GenCodesTreeStyleTypeOption = 2 // 选项式树表 +) + +var GenCodesTreeStyleTypeNameMap = map[int]string{ + GenCodesTreeStyleTypeNormal: "普通树表格", + GenCodesTreeStyleTypeOption: "选项式树表", +} diff --git a/server/internal/library/xgen/gen_consts/gen_config.go b/server/internal/library/xgen/gen_consts/gen_config.go new file mode 100644 index 0000000..7726642 --- /dev/null +++ b/server/internal/library/xgen/gen_consts/gen_config.go @@ -0,0 +1,87 @@ +// package genconsts +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genconsts + +import "github.com/gogf/gf/v2/util/gconv" + +// 配置数据类型 +const ( + ConfigTypeString = "string" + ConfigTypeInt = "int" + ConfigTypeInt8 = "int8" + ConfigTypeInt16 = "int16" + ConfigTypeInt32 = "int32" + ConfigTypeInt64 = "int64" + ConfigTypeUint = "uint" + ConfigTypeUint8 = "uint8" + ConfigTypeUint16 = "uint16" + ConfigTypeUint32 = "uint32" + ConfigTypeUint64 = "uint64" + ConfigTypeFloat32 = "float32" + ConfigTypeFloat64 = "float64" + ConfigTypeBool = "bool" + ConfigTypeDate = "date" + ConfigTypeDateTime = "datetime" + ConfigTypeSliceString = "[]string" + ConfigTypeSliceInt = "[]int" + ConfigTypeSliceInt64 = "[]int64" +) + +var ConfigTypes = []string{ConfigTypeString, + ConfigTypeInt, ConfigTypeInt8, ConfigTypeInt16, ConfigTypeInt32, ConfigTypeInt64, + ConfigTypeUint, ConfigTypeUint8, ConfigTypeUint16, ConfigTypeUint32, ConfigTypeUint64, + ConfigTypeFloat32, ConfigTypeFloat64, + ConfigTypeBool, + ConfigTypeDate, ConfigTypeDateTime, + ConfigTypeSliceString, ConfigTypeSliceInt, ConfigTypeSliceInt64, +} + +// ConvType 类型转换 +func ConvType(val interface{}, t string) interface{} { + switch t { + case ConfigTypeString: + val = gconv.String(val) + case ConfigTypeInt: + val = gconv.Int(val) + case ConfigTypeInt8: + val = gconv.Int8(val) + case ConfigTypeInt16: + val = gconv.Int16(val) + case ConfigTypeInt32: + val = gconv.Int32(val) + case ConfigTypeInt64: + val = gconv.Int64(val) + case ConfigTypeUint: + val = gconv.Uint(val) + case ConfigTypeUint8: + val = gconv.Uint8(val) + case ConfigTypeUint16: + val = gconv.Uint16(val) + case ConfigTypeUint32: + val = gconv.Uint32(val) + case ConfigTypeUint64: + val = gconv.Uint64(val) + case ConfigTypeFloat32: + val = gconv.Float32(val) + case ConfigTypeFloat64: + val = gconv.Float64(val) + case ConfigTypeBool: + val = gconv.Bool(val) + case ConfigTypeDate: + val = gconv.Time(val, "Y-m-d") + case ConfigTypeDateTime: + val = gconv.Time(val, "Y-m-d H:i:s") + case ConfigTypeSliceInt: + val = gconv.SliceInt(val) + case ConfigTypeSliceInt64: + val = gconv.SliceInt64(val) + case ConfigTypeSliceString: + val = gconv.SliceStr(val) + default: + val = gconv.String(val) + } + return val +} diff --git a/server/internal/library/xgen/gen_dao/gen_dao.go b/server/internal/library/xgen/gen_dao/gen_dao.go new file mode 100644 index 0000000..48dae85 --- /dev/null +++ b/server/internal/library/xgen/gen_dao/gen_dao.go @@ -0,0 +1,65 @@ +// package gendao +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package gendao + +import ( + "context" + genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcfg" + "github.com/gogf/gf/v2/util/gconv" +) + +var ( + daoConfigList = make([]*genmodel.GenDaoConfig, 0) + cliConfigPath = `hack/config.yaml` + cliConfig *gcfg.Config + defaultGenServiceConfig = genmodel.GenServiceConfig{ + SrcFolder: "internal/logic", + DstFolder: "internal/service", + DstFileNameCase: "Snake", + StPattern: `s([A-Z]\w+)`, + // gf gen service -l currently has a Windows path handling issue that + // can remove all generated service files. Keep generation portable. + Clear: false, + } + serviceConfig = genmodel.GenServiceConfig{} +) + +func GetDaoConfig(ctx context.Context, group string) (output *genmodel.GenDaoConfig, err error) { + for _, v := range daoConfigList { + if v == nil { + continue + } + if v.Group == group { + output = v + return + } + } + return +} + +func Init(ctx context.Context) (err error) { + adapter, err := gcfg.NewAdapterFile(cliConfigPath) + if err != nil { + g.Log().Fatalf(ctx, "get cli configuration file:%v, err:%+v", cliConfigPath, err) + return + } + cliConfig = gcfg.NewWithAdapter(adapter) + + cliConfig.MustGet(ctx, "gfcli.gen.dao").Scan(&daoConfigList) + cliConfig.MustGet(ctx, "gfcli.gen.service").Scan(&serviceConfig) + + return +} + +func GetServiceConfig() *genmodel.GenServiceConfig { + conf := &genmodel.GenServiceConfig{} + _ = gconv.Scan(defaultGenServiceConfig, &conf) + _ = gconv.Scan(serviceConfig, &conf) + return conf +} diff --git a/server/internal/library/xgen/gen_model/gen_codes.go b/server/internal/library/xgen/gen_model/gen_codes.go new file mode 100644 index 0000000..ea600a9 --- /dev/null +++ b/server/internal/library/xgen/gen_model/gen_codes.go @@ -0,0 +1,146 @@ +// package genmodel +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genmodel + +import "github.com/gogf/gf/v2/encoding/gjson" + +type SelectItemModel struct { + Value interface{} `json:"value"` // 选择项值 + Label string `json:"label"` // 选择项标签 + Name string `json:"name"` // 选择项名称 + Disabled bool `json:"disabled"` // 是否禁用 +} + +// 生成类型选择项 +type SelectGenTypeModel struct { + SelectItemModel + Templates []*SelectGenTemplateModel `json:"templates"` // 选择项模板 +} + +// 生成模板选择项 +type SelectGenTemplateModel struct { + SelectItemModel + IsAddon bool `json:"isAddon"` // 是否为插件 +} + +type SelectsModel struct { + GenType []*SelectGenTypeModel `json:"genType"` // 生成类型 0:增删改查列表 1:关系树列表 + Db []*SelectItemModel `json:"db"` // 数据库选择项 + Status []*SelectItemModel `json:"status"` // 状态选择项 0:生成成功 1:未生产 2:生成失败 + LinkMode []*SelectItemModel `json:"linkMode"` // 链接模式选择项 1左关联 2右关联 3内关联 + BuildMethod []*SelectItemModel `json:"buildMethod"` // 构建标志选择项 1:创建文件 2强制覆盖 3已存在跳过 4不生成 + FormMode []*SelectItemModel `json:"formMode"` // 表单项类型选择项 + FormRole []*SelectItemModel `json:"formRole"` // 表单项校验规则选择项 + DictMode []*SelectItemModel `json:"dictMode"` // 字典模式选择项 + WhereMode []*SelectItemModel `json:"whereMode"` // 查询条件选择项 + Addons []*SelectItemModel `json:"addons"` // 插件选择项 + TableAlign []*SelectItemModel `json:"tableAlign"` // 表格对齐方式选择项 + TreeStyleType []*SelectItemModel `json:"treeStyleType"` // 树形结构选择项 +} + +// GenCodesColumn 生成表字段属性 +type GenCodesColumn struct { + // 表属性 + Id int64 `json:"id" dc:"序号"` + Name string `json:"name" dc:"字段列名"` + Dc string `json:"dc" dc:"字段描述"` + DataType string `json:"dataType" dc:"字段类型"` + SqlType string `json:"sqlType" dc:"物理类型"` + Length int64 `json:"length" dc:"字段长度"` + IsAllowNull string `json:"isAllowNull" dc:"是否允许为空"` + DefaultValue interface{} `json:"defaultValue" dc:"默认值"` + Index string `json:"index" dc:"索引"` + Extra string `json:"extra" dc:"额外选项"` + // 自定义生成属性 + // Alias string `json:"alias" dc:"字段别名"` + GoName string `json:"goName" dc:"Go属性"` + GoType string `json:"goType" dc:"Go类型"` + TsName string `json:"tsName" dc:"Ts属性"` + TsType string `json:"tsType" dc:"Ts类型"` + IsList bool `json:"isList" dc:"列表"` + IsExport bool `json:"isExport" dc:"导出"` + IsSort bool `json:"isSort" dc:"排序"` + IsQuery bool `json:"isQuery" dc:"查询"` + QueryWhere string `json:"queryWhere" dc:"查询条件"` + IsEdit bool `json:"isEdit" dc:"编辑"` + Required bool `json:"required" dc:"必填"` + Unique bool `json:"unique" dc:"唯一性"` + FormMode string `json:"formMode" dc:"表单组件"` + FormRole string `json:"formRole" dc:"表单验证"` + FormGridSpan int `json:"formGridSpan" dc:"栅格占位数量"` + DictType string `json:"dictType" dc:"字典类型ID"` + Align string `json:"align" dc:"排列方式"` + Width int64 `json:"width" dc:"列宽"` +} + +type GenCodesTableSelectParam struct { + DbGroup string `json:"dbGroup"` +} + +type GenCodesTableSelectModel struct { + Value string `json:"value"` + Label string `json:"label"` + Name string `json:"name"` + DaoName string `json:"daoName" dc:"orm模型名称"` + DefVarName string `json:"defVarName" dc:"默认实体名称"` + DefAlias string `json:"defAlias" dc:"默认关联表别名"` + DefTableComment string `json:"defTableComment" dc:"默认菜单名称"` +} + +type GenCodesColumnSelectParam struct { + DbGroup string `json:"dbGroup"` + TableName string `json:"tableName"` +} + +type GenCodesColumnSelectModel struct { + Value string `json:"value"` + Label string `json:"label"` + Name string `json:"name"` +} + +type GenCodesColumnListParam struct { + DbGroup string `json:"dbGroup" dc:"数据库配置名称"` + TableName string `json:"tableName" dc:"表名称"` + IsLink int64 `json:"isLink" dc:"是否是关联表"` + Alias string `json:"alias" dc:"关联表别名"` +} + +type GenCodesColumnListModel struct { + GenCodesColumn +} + +type GenCodesPreviewParam struct { + TableId int64 `json:"tableId" orm:"table_id" description:"生成ID"` + GenType uint `json:"genType" orm:"gen_type" description:"生成类型"` + GenTemplate int `json:"genTemplate" orm:"gen_template" description:"生成模板"` + VarName string `json:"varName" orm:"var_name" description:"实体命名"` + Options *gjson.Json `json:"options" orm:"options" description:"配置选项"` + DbName string `json:"dbName" orm:"db_name" description:"数据库名称"` + TableName string `json:"tableName" orm:"table_name" description:"主表名称"` + TableComment string `json:"tableComment" orm:"table_comment" description:"主表注释"` + DaoName string `json:"daoName" orm:"dao_name" description:"主表dao模型"` + MasterColumns *gjson.Json `json:"masterColumns" orm:"master_columns" description:"主表字段"` + AddonName string `json:"addonName" orm:"addon_name" description:"插件名称"` + Status int `json:"status" orm:"status" description:"生成状态"` +} + +// GenFile 生成文件配置 +type GenFile struct { + Content string `json:"content" dc:"页面内容"` + Path string `json:"path" dc:"生成路径"` + Meth int `json:"meth" dc:"生成方式"` + Required bool `json:"required" dc:"是否是必要构建文件"` +} + +type GenCodesPreviewModel struct { + Config *GenCodesConfig `json:"config"` + Views map[string]*GenFile `json:"views" dc:"页面"` +} + +type GenCodesBuildParam = GenCodesPreviewParam + +type GenCodesBuildModel struct { +} diff --git a/server/internal/library/xgen/gen_model/gen_config.go b/server/internal/library/xgen/gen_model/gen_config.go new file mode 100644 index 0000000..216721f --- /dev/null +++ b/server/internal/library/xgen/gen_model/gen_config.go @@ -0,0 +1,126 @@ +// package genmodel +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genmodel + +import ( + genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts" + + "github.com/gogf/gf/v2/util/gconv" +) + +// 模板配置 config/config.yaml xgen.application.crud.templates +type GenCodesConfigCrudTemplate struct { + Group string `name:"group" json:"group"` + IsAddon bool `name:"isAddon" json:"isAddon"` + MasterPackage string `name:"masterPackage" json:"masterPackage"` + TemplatePath string `name:"templatePath" json:"templatePath"` + ApiPath string `name:"apiPath" json:"apiPath"` + InputPath string `name:"inputPath" json:"inputPath"` + ControllerPath string `name:"controllerPath" json:"controllerPath"` + LogicPath string `name:"logicPath" json:"logicPath"` + RouterPath string `name:"routerPath" json:"routerPath"` + SqlPath string `name:"sqlPath" json:"sqlPath"` + WebApiPath string `name:"webApiPath" json:"webApiPath"` + WebViewsPath string `name:"webViewsPath" json:"webViewsPath"` +} +type GenCodesConfigTreeTemplate struct { + Group string `json:"group"` + TemplatePath string `json:"templatePath"` +} + +type GenCodesConfigBuildAddon struct { + SrcPath string `json:"srcPath"` + WebApiPath string `json:"webApiPath"` + WebViewsPath string `json:"webViewsPath"` +} + +// 工具生成配置 hack/config.yaml gfcli.gen.dao +type GenDaoConfig struct { + Group string `name:"group" json:"group"` + Link string `name:"link" json:"link"` + DbName string `name:"dbName" json:"dbName"` + TableName string `name:"tableName" json:"tableName"` + JsonCase string `name:"jsonCase" json:"jsonCase"` + StdTime bool `name:"stdTime" json:"stdTime"` + GJsonSupport bool `name:"gJsonSupport" json:"gJsonSupport"` +} + +// 工具生成配置 hack/config.yaml gfcli.gen.service +type GenServiceConfig struct { + SrcFolder string `name:"srcFolder"` + DstFolder string `name:"dstFolder"` + DstFileNameCase string `name:"dstFileNameCase"` + WatchFile string `name:"watchFile" ` + StPattern string `name:"stPattern" ` + Packages []string `name:"packages" ` + ImportPrefix string `name:"importPrefix" ` + Clear bool `name:"clear" ` +} + +// 模板配置 config/config.yaml xgen +type GenCodesConfig struct { + AllowedIPs []string `json:"allowedIPs"` + Application struct { + Crud struct { + Templates []*GenCodesConfigCrudTemplate `json:"templates"` + } `json:"crud"` + Tree struct { + Templates []*GenCodesConfigTreeTemplate `json:"templates"` + } `json:"tree"` + } `json:"application"` + Delimiters []string `json:"delimiters"` + DevPath string `json:"devPath"` + DisableTables []string `json:"disableTables"` + SelectDbs []string `json:"selectDbs"` + Addon *GenCodesConfigBuildAddon `json:"addon"` +} + +// ConvType 类型转换 +func ConvType(val interface{}, t string) interface{} { + switch t { + case genconsts.ConfigTypeString: + val = gconv.String(val) + case genconsts.ConfigTypeInt: + val = gconv.Int(val) + case genconsts.ConfigTypeInt8: + val = gconv.Int8(val) + case genconsts.ConfigTypeInt16: + val = gconv.Int16(val) + case genconsts.ConfigTypeInt32: + val = gconv.Int32(val) + case genconsts.ConfigTypeInt64: + val = gconv.Int64(val) + case genconsts.ConfigTypeUint: + val = gconv.Uint(val) + case genconsts.ConfigTypeUint8: + val = gconv.Uint8(val) + case genconsts.ConfigTypeUint16: + val = gconv.Uint16(val) + case genconsts.ConfigTypeUint32: + val = gconv.Uint32(val) + case genconsts.ConfigTypeUint64: + val = gconv.Uint64(val) + case genconsts.ConfigTypeFloat32: + val = gconv.Float32(val) + case genconsts.ConfigTypeFloat64: + val = gconv.Float64(val) + case genconsts.ConfigTypeBool: + val = gconv.Bool(val) + case genconsts.ConfigTypeDate: + val = gconv.Time(val, "Y-m-d") + case genconsts.ConfigTypeDateTime: + val = gconv.Time(val, "Y-m-d H:i:s") + case genconsts.ConfigTypeSliceInt: + val = gconv.SliceInt(val) + case genconsts.ConfigTypeSliceInt64: + val = gconv.SliceInt64(val) + case genconsts.ConfigTypeSliceString: + val = gconv.SliceStr(val) + default: + val = gconv.String(val) + } + return val +} diff --git a/server/internal/library/xgen/gen_model/gen_view.go b/server/internal/library/xgen/gen_model/gen_view.go new file mode 100644 index 0000000..d7870bb --- /dev/null +++ b/server/internal/library/xgen/gen_model/gen_view.go @@ -0,0 +1,145 @@ +// package genmodel +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genmodel + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gview" +) + +// ImportModel 导包 - model.ts +type ImportModel struct { + NaiveUI []string + UtilsIs []string + UtilsUrl []string + UtilsDate []string + UtilsValidate []string + UtilsIndex []string +} + +// CurdStep 生成选项 +type CurdStep struct { + HasMaxSort bool // 最大排序 + HasAdd bool // 表单添加 + HasBatchDel bool // 批量删除 + HasExport bool // 表格导出 + HasNotFilterAuth bool // 不过滤认证权限 + HasEdit bool // 表单编辑 + HasDel bool // 删除 + HasView bool // 查看详情 + HasStatus bool // 修改状态 + HasSwitch bool // 数值开关 + HasCheck bool // 勾选列 + HasMenu bool // 菜单权限 + IsTreeTable bool // 树型列表 + IsOptionTreeTable bool // 选项式树型列表 + HasRules bool // 表单验证规则 + HasRulesValidator bool // 表单验证器 + HasSearchForm bool // 列表搜索 + HasDict bool // 字典 + HasFuncDict bool // 注册方法字典 + HasQueryMemberSummary bool // 查询用户摘要 + HasHookMemberSummary bool // hook用户摘要 + ImportModel ImportModel // 公用导包 - model.ts + ActionColumnWidth int64 // 列表操作栏宽度 + IsAddon bool // 是否是插件 + IsEditModal bool // 新增修改模态框模式 +} + +type FuncDict struct { + ValueColumn string // 选项值 + LabelColumn string // 选项名称 + Value *GenCodesColumnListModel + Label *GenCodesColumnListModel +} + +// PresetStep 预设生成流程参数 +type PresetStep struct { + FormGridCols int `json:"formGridCols" dc:"表单显示的栅格数量"` +} + +// OptionsTree 树形结构选项 +type OptionsTree struct { + TitleColumn string `json:"titleColumn"` + PidColumn string `json:"pidColumn"` + LevelColumn string `json:"levelColumn"` + TreeColumn string `json:"treeColumn"` + StyleType int `json:"styleType"` + TitleField *GenCodesColumnListModel + PidField *GenCodesColumnListModel + LevelField *GenCodesColumnListModel + TreeField *GenCodesColumnListModel + HasTreePath bool +} + +// CurdOptionsMenu 菜单选项 +type CurdOptionsMenu struct { + Icon string `json:"icon"` + Pid int `json:"pid"` + Sort int `json:"sort"` +} + +// CurdOptionsJoin 关联表选项 +type CurdOptionsJoin struct { + Uuid string `json:"uuid"` + LinkTable string `json:"linkTable"` + Alias string `json:"alias"` + LinkMode int `json:"linkMode"` + Field string `json:"field"` + MasterField string `json:"masterField"` + DaoName string `json:"daoName"` + Columns []*GenCodesColumnListModel `json:"columns"` +} + +type OptionsSchemasField struct { + Field string + Type string +} + +type CurdOptionsDict struct { + Has bool + Types []string + Schemas []*OptionsSchemasField +} + +// CurdOptions 生成选项 +type CurdOptions struct { + AutoOps []string `json:"autoOps"` + ColumnOps []string `json:"columnOps"` + HeadOps []string `json:"headOps"` + Join []*CurdOptionsJoin `json:"join"` + Menu *CurdOptionsMenu `json:"menu"` + Tree *OptionsTree `json:"tree"` + TemplateGroup string `json:"templateGroup"` + ApiPrefix string `json:"apiPrefix"` + ImportWebApi string `json:"importWebApi"` + FuncDict *FuncDict `json:"funcDict"` + PresetStep *PresetStep `json:"presetStep"` + Step *CurdStep // 转换后的流程控制条件 + DictOps CurdOptionsDict // 字典选项 + DictMap g.Map // 字典选项 -> 字段映射关系 +} + +type CurdPreviewParam struct { + In *GenCodesPreviewParam // 提交参数 + DaoConfig *GenDaoConfig // 生成dao配置 + Config *GenCodesConfig // 生成配置 + View *gview.View // 视图模板 + Content *GenCodesPreviewModel // 页面代码 + MasterFields []*GenCodesColumnListModel // 主表字段属性 + Pk *GenCodesColumnListModel // 主键属性 + Options *CurdOptions // 生成选项 +} + +type CurdBuildEvent map[string]func(ctx context.Context) (err error) + +type CurdBuildParam struct { + PreviewIn *CurdPreviewParam // 预览参数 + BeforeEvent CurdBuildEvent // 前置事件 + AfterEvent CurdBuildEvent // 后置事件 +} diff --git a/server/internal/library/xgen/gen_view/gen_column.go b/server/internal/library/xgen/gen_view/gen_column.go new file mode 100644 index 0000000..e0f2cf5 --- /dev/null +++ b/server/internal/library/xgen/gen_view/gen_column.go @@ -0,0 +1,253 @@ +// package genview +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genview + +import ( + "context" + gendao "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_dao" + genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +func getJsonTagFromCase(str, caseStr string) string { + switch gstr.ToLower(caseStr) { + case gstr.ToLower("Camel"): + return gstr.CaseCamel(str) + + case gstr.ToLower("CamelLower"): + return gstr.CaseCamelLower(str) + + case gstr.ToLower("Kebab"): + return gstr.CaseKebab(str) + + case gstr.ToLower("KebabScreaming"): + return gstr.CaseKebabScreaming(str) + + case gstr.ToLower("Snake"): + return gstr.CaseSnake(str) + + case gstr.ToLower("SnakeFirstUpper"): + return gstr.CaseSnakeFirstUpper(str) + + case gstr.ToLower("SnakeScreaming"): + return gstr.CaseSnakeScreaming(str) + } + return str +} + +func CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue interface{}) (string, error) { + var ( + typeName string + typePattern string + ) + match, _ := gregex.MatchString(`(.+?)\((.+)\)`, fieldType) + if len(match) == 3 { + typeName = gstr.Trim(match[1]) + typePattern = gstr.Trim(match[2]) + } else { + typeName = gstr.Split(fieldType, " ")[0] + } + typeName = strings.ToLower(typeName) + switch typeName { + case + "binary", + "varbinary", + "blob", + "tinyblob", + "mediumblob", + "longblob": + return string(gdb.LocalTypeBytes), nil + + case + "int", + "tinyint", + "small_int", + "smallint", + "medium_int", + "mediumint", + "serial": + if gstr.ContainsI(fieldType, "unsigned") { + return string(gdb.LocalTypeUint), nil + } + return string(gdb.LocalTypeInt), nil + + case + "big_int", + "bigint", + "bigserial": + if gstr.ContainsI(fieldType, "unsigned") { + return string(gdb.LocalTypeUint64), nil + } + return string(gdb.LocalTypeInt64), nil + + case + "real": + return string(gdb.LocalTypeFloat32), nil + + case + "float", + "double", + "decimal", + "money", + "numeric", + "smallmoney": + return string(gdb.LocalTypeFloat64), nil + + case + "bit": + // It is suggested using bit(1) as boolean. + if typePattern == "1" { + return string(gdb.LocalTypeBool), nil + } + s := gconv.String(fieldValue) + // mssql is true|false string. + if strings.EqualFold(s, "true") || strings.EqualFold(s, "false") { + return string(gdb.LocalTypeBool), nil + } + if gstr.ContainsI(fieldType, "unsigned") { + return string(gdb.LocalTypeUint64Bytes), nil + } + return string(gdb.LocalTypeInt64Bytes), nil + + case + "bool": + return string(gdb.LocalTypeBool), nil + + case + "date": + return string(gdb.LocalTypeDate), nil + + case + "datetime", + "timestamp", + "timestamptz": + return string(gdb.LocalTypeDatetime), nil + + case + "json": + return string(gdb.LocalTypeJson), nil + + case + "jsonb": + return string(gdb.LocalTypeJsonb), nil + + default: + // Auto-detect field type, using key match. + switch { + case strings.Contains(typeName, "text") || strings.Contains(typeName, "char") || strings.Contains(typeName, "character"): + return string(gdb.LocalTypeString), nil + + case strings.Contains(typeName, "float") || strings.Contains(typeName, "double") || strings.Contains(typeName, "numeric"): + return string(gdb.LocalTypeFloat64), nil + + case strings.Contains(typeName, "bool"): + return string(gdb.LocalTypeBool), nil + + case strings.Contains(typeName, "binary") || strings.Contains(typeName, "blob"): + return string(gdb.LocalTypeBytes), nil + + case strings.Contains(typeName, "int"): + if gstr.ContainsI(fieldType, "unsigned") { + return string(gdb.LocalTypeUint), nil + } + return string(gdb.LocalTypeInt), nil + + case strings.Contains(typeName, "time"): + return string(gdb.LocalTypeDatetime), nil + + case strings.Contains(typeName, "date"): + return string(gdb.LocalTypeDatetime), nil + + default: + return string(gdb.LocalTypeString), nil + } + } +} + +// GenGotype 生成字段的go类型 +func GenGotype(ctx context.Context, field *genmodel.GenCodesColumnListModel, in *genmodel.GenDaoConfig) (goName, typeName, tsName string, tsType string) { + var err error + tsName = getJsonTagFromCase(field.Name, in.JsonCase) + goName = gstr.CaseCamel(field.Name) + + typeName, err = CheckLocalTypeForField(ctx, field.DataType, nil) + if err != nil { + panic(err) + } + + switch gdb.LocalType(typeName) { + case gdb.LocalTypeDate, gdb.LocalTypeDatetime: + if in.StdTime { + typeName = "time.Time" + } else { + typeName = "*gtime.Time" + } + + case gdb.LocalTypeInt64Bytes: + typeName = "int64" + + case gdb.LocalTypeUint64Bytes: + typeName = "uint64" + + // Special type handle. + case gdb.LocalTypeJson, gdb.LocalTypeJsonb: + if in.GJsonSupport { + typeName = "*gjson.Json" + } else { + typeName = "string" + } + } + + tsType = ShiftMap[typeName] + return +} + +func CustomLinkAttributes(ctx context.Context, alias string, field *genmodel.GenCodesColumnListModel, in *genmodel.GenDaoConfig) { + field.GoName, field.GoType, field.TsName, field.TsType = GenGotype(ctx, field, in) + + field.GoName = gstr.UcFirst(alias + field.GoName) + field.TsName = gstr.LcFirst(field.GoName) + + setDefaultQueryWhere(field) + setDefaultValue(field) +} + +func CustomAttributes(ctx context.Context, field *genmodel.GenCodesColumnListModel, in *genmodel.GenDaoConfig) { + field.GoName, field.GoType, field.TsName, field.TsType = GenGotype(ctx, field, in) + setDefault(field) +} + +func TableColumnList(ctx context.Context, params *genmodel.GenCodesColumnListParam) (output []*genmodel.GenCodesColumnListModel, err error) { + sql := "select ORDINAL_POSITION as `id`, COLUMN_NAME as `name`, COLUMN_COMMENT as `dc`, DATA_TYPE as `dataType`, COLUMN_TYPE as `sqlType`, CHARACTER_MAXIMUM_LENGTH as `length`, IS_NULLABLE as `isAllowNull`, COLUMN_DEFAULT as `defaultValue`, COLUMN_KEY as `index`, EXTRA as `extra` from information_schema.COLUMNS where TABLE_SCHEMA = ? and TABLE_NAME = ? ORDER BY `id` ASC" + dbConfig := g.DB(params.DbGroup).GetConfig() + daoConfig, err := gendao.GetDaoConfig(ctx, params.DbGroup) + if err != nil { + return + } + + output = make([]*genmodel.GenCodesColumnListModel, 0) + err = g.DB(params.DbGroup).Ctx(ctx).Raw(sql, dbConfig.Name, params.TableName).Scan(&output) + if err != nil { + return + } + if len(output) == 0 { + return + } + for _, v := range output { + if params.IsLink == 1 { + CustomLinkAttributes(ctx, params.Alias, v, daoConfig) + } else { + CustomAttributes(ctx, v, daoConfig) + } + } + return +} diff --git a/server/internal/library/xgen/gen_view/gen_column_default.go b/server/internal/library/xgen/gen_view/gen_column_default.go new file mode 100644 index 0000000..f66efbc --- /dev/null +++ b/server/internal/library/xgen/gen_view/gen_column_default.go @@ -0,0 +1,341 @@ +// package genview +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genview + +import ( + genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts" + genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model" + + "github.com/gogf/gf/v2/text/gstr" +) + +// 默认表单组件映射 Ts -> 表单组件 +var defaultFormModeMap = map[string]string{ + TsTypeString: FMInput, + TsTypeNumber: FMInputNumber, + TsTypeBoolean: FMInputNumber, + TsTypeArray: FMInputDynamic, + TsTypeTuple: FMInputDynamic, + TsTypeAny: FMInput, +} + +var defaultEditFields = map[string]bool{ + "id": false, + "created_dept": false, + "created_by": false, + "created_at": false, + "updated_by": false, + "updated_at": false, + "deleted_by": false, + "deleted_at": false, +} + +var defaultEditSwitch = map[string]bool{ + "id": false, + "level": false, + "tree": false, + "created_by": false, + "updated_by": false, + "created_dept": false, + "created_at": false, + "updated_at": false, + "deleted_at": false, +} + +var defaultListSwitch = map[string]bool{ + "level": false, + "tree": false, + "deleted_at": false, +} + +var defaultExportSwitch = map[string]bool{ + "level": false, + "tree": false, + "deleted_at": false, +} + +var defaultQuerySwitch = map[string]bool{ + "level": false, + "tree": false, + "deleted_at": false, +} + +var defaultSort = map[string]bool{ + "id": true, + "sort": true, +} + +var defaultTreeFields = []string{"pid", "level", "tree"} + +// 默认表单验证映射 物理类型命名识别 +var defaultFormRoleMap = map[string]string{ + "mobile": FRPhone, + "qq": FRQq, + "email": FREmail, + "id_card": FRIdCard, + "bank_card": FRBankCard, + "password": FRPassword, + "pass": FRPassword, + "price": FRAmount, +} + +// 默认查询条件映射 go类型识别 +var defaultWhereModeMap = map[string]string{ + GoTypeString: WMLike, + GoTypeDate: WMEq, + GoTypeDatetime: WMEq, + GoTypeInt: WMEq, + GoTypeUint: WMEq, + GoTypeInt64: WMEq, + GoTypeUint64: WMEq, + GoTypeIntSlice: WMIn, + GoTypeInt64Slice: WMIn, + GoTypeUint64Slice: WMIn, + GoTypeFloat32: WMEq, + GoTypeFloat64: WMEq, + GoTypeBytes: WMEq, + GoTypeTime: WMEq, + GoTypeGTime: WMEq, + GoTypeJson: WMJsonContains, +} + +// IsIndexPK 是否是主键 +func IsIndexPK(index string) bool { + return gstr.ToUpper(index) == gstr.ToUpper(genconsts.GenCodesIndexPK) +} + +// IsIndexUNI 是否是唯一索引 +func IsIndexUNI(index string) bool { + return gstr.ToUpper(index) == gstr.ToUpper(genconsts.GenCodesIndexUNI) +} + +// setDefault 设置默认属性 +func setDefault(field *genmodel.GenCodesColumnListModel) { + setDefaultEdit(field) + + setDefaultFormMode(field) + + setDefaultFormRole(field) + + setDefaultDictType(field) + + setDefaultList(field) + + setDefaultExport(field) + + setDefaultQuery(field) + + setDefaultQueryWhere(field) + + setDefaultValue(field) + + if field.IsAllowNull == "NO" { + field.Required = true + } + + if IsIndexUNI(field.Index) { + field.Unique = true + } + + if df, ok := defaultSort[field.Name]; ok { + field.IsSort = df + } + + if field.Dc == "" { + field.Dc = field.Name + } +} + +// setDefaultEdit 设置默认编辑 +func setDefaultEdit(field *genmodel.GenCodesColumnListModel) { + field.IsEdit = true + + if IsIndexPK(field.Index) { + field.IsEdit = false + return + } + + if df, ok := defaultEditFields[field.Name]; ok { + field.IsEdit = df + } + + if df, ok := defaultEditSwitch[field.Name]; ok { + field.IsEdit = df + } +} + +// setDefaultFormMode 设置默认表单组件 +func setDefaultFormMode(field *genmodel.GenCodesColumnListModel) { + field.FormMode = FMInput + if df, ok := defaultFormModeMap[field.TsType]; ok { + field.FormMode = df + } + + if gstr.HasSuffix(field.GoName, "Status") && IsNumberType(field.GoType) { + field.FormMode = FMSelect + return + } + + if field.GoName == "CreatedAt" { + field.FormMode = FMDateRange + return + } + + if (field.GoName == "ProvinceId" || field.GoName == "CityId") && IsNumberType(field.GoType) { + field.FormMode = FMCitySelector + return + } + + if field.DataType == "datetime" || field.DataType == "timestamp" || field.DataType == "timestamptz" { + field.FormMode = FMTime + return + } + + if field.DataType == "date" { + field.FormMode = FMDate + return + } + + if field.GoType == GoTypeString && field.Length >= 256 && field.Length <= 512 { + field.FormMode = FMInputTextarea + return + } + + if field.GoType == GoTypeString && field.Length > 512 { + field.FormMode = FMInputEditor + return + } +} + +// setDefaultFormRole 设置默认表单验证 +func setDefaultFormRole(field *genmodel.GenCodesColumnListModel) { + field.FormRole = FRNone + + switch field.GoType { + case GoTypeUint, GoTypeUint64: + field.FormRole = FRNum + return + } + + if df, ok := defaultFormRoleMap[field.Name]; ok { + field.FormRole = df + } +} + +// setDefaultDictType 设置默认字典类型 +func setDefaultDictType(field *genmodel.GenCodesColumnListModel) { + if gstr.HasSuffix(field.GoName, "Status") && IsNumberType(field.GoType) { + field.DictType = "sys_common_status" // 默认系统状态ID + return + } +} + +// setDefaultList 设置默认列表 +func setDefaultList(field *genmodel.GenCodesColumnListModel) { + field.IsList = true + switch field.GoType { + case GoTypeIntSlice, GoTypeInt64Slice, GoTypeUint64Slice, GoTypeBytes, GoTypeJson: + field.IsList = false + return + } + + if field.Length >= 500 { + field.IsList = false + return + } + + if df, ok := defaultListSwitch[field.Name]; ok { + field.IsList = df + } +} + +// setDefaultExport 设置默认导出 +func setDefaultExport(field *genmodel.GenCodesColumnListModel) { + field.IsExport = true + switch field.GoType { + case GoTypeIntSlice, GoTypeInt64Slice, GoTypeUint64Slice, GoTypeBytes, GoTypeJson: + field.IsExport = false + return + } + + if field.Length >= 500 { + field.IsExport = false + return + } + + if df, ok := defaultExportSwitch[field.Name]; ok { + field.IsExport = df + } +} + +// setDefaultQuery 设置默认查询 +func setDefaultQuery(field *genmodel.GenCodesColumnListModel) { + field.IsQuery = false + + if IsIndexPK(field.Index) { + field.IsQuery = true + return + } + + if gstr.HasSuffix(field.GoName, "Status") && IsNumberType(field.GoType) { + field.IsQuery = true + return + } + + if field.GoName == "CreatedAt" { + field.IsQuery = true + return + } + + if df, ok := defaultQuerySwitch[field.Name]; ok { + field.IsQuery = df + } +} + +// setDefaultQueryWhere 设置默认查询条件 +func setDefaultQueryWhere(field *genmodel.GenCodesColumnListModel) { + field.QueryWhere = WMEq + + if field.GoName == "CreatedAt" { + field.QueryWhere = WMBetween + return + } + + if field.Length >= 500 { + field.QueryWhere = WMLikeAll + return + } + + if df, ok := defaultWhereModeMap[field.GoType]; ok { + field.QueryWhere = df + } +} + +// setDefaultValue 设置默认value +func setDefaultValue(field *genmodel.GenCodesColumnListModel) { + var value interface{} + if field.DefaultValue == nil { + switch field.GoType { + case GoTypeString, GoTypeBytes, GoTypeDate, GoTypeDatetime, GoTypeTime, GoTypeGTime: + value = "" + case GoTypeIntSlice, GoTypeInt64Slice, GoTypeUint64Slice, GoTypeJson: + value = nil + case GoTypeInt, GoTypeUint, GoTypeInt64, GoTypeUint64: + value = 0 + case GoTypeBool: + value = false + } + } else { + value = genconsts.ConvType(field.DefaultValue, field.GoType) + } + + // 时间类型不做默认值处理 + if field.GoType == GoTypeGTime { + value = "" + } + + field.DefaultValue = value +} diff --git a/server/internal/library/xgen/gen_view/gen_column_map.go b/server/internal/library/xgen/gen_view/gen_column_map.go new file mode 100644 index 0000000..cf7c977 --- /dev/null +++ b/server/internal/library/xgen/gen_view/gen_column_map.go @@ -0,0 +1,339 @@ +// package genview +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genview + +import ( + genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model" + + "github.com/gogf/gf/v2/text/gstr" +) + +// 表单组件 +const ( + FMInput = "Input" // 文本输入 + FMInputNumber = "InputNumber" // 数字输入 + FMInputTextarea = "InputTextarea" // 文本域 + FMInputEditor = "InputEditor" // 富文本 + FMInputDynamic = "InputDynamic" // 动态键值对 + FMDate = "Date" // 日期选择(Y-M-D) + FMDateRange = "DateRange" // 日期范围选择 + FMTime = "Time" // 时间选择(Y-M-D H:i:s) + FMTimeRange = "TimeRange" // 时间范围选择 + FMRadio = "Radio" // 单选按钮 + FMCheckbox = "Checkbox" // 复选按钮 + FMSelect = "Select" // 单选下拉框 + FMSelectMultiple = "SelectMultiple" // 多选下拉框 + FMTreeSelect = "TreeSelect" // 树型选择 + FMCascader = "Cascader" // 级联选择 + FMUploadImage = "UploadImage" // 单图上传 + FMUploadImages = "UploadImages" // 多图上传 + FMUploadFile = "UploadFile" // 单文件上传 + FMUploadFiles = "UploadFiles" // 多文件上传 + FMSwitch = "Switch" // 开关 + FMRate = "Rate" // 评分 + FMCitySelector = "CitySelector" // 省市区选择 + FMPidTreeSelect = "PidTreeSelect" // 树型上级选择,树表生成专用 +) + +var FMs = []string{ + FMInput, FMInputNumber, FMInputTextarea, FMInputEditor, FMInputDynamic, + FMDate, FMDateRange, FMTime, FMTimeRange, + FMRadio, FMCheckbox, FMSelect, FMSelectMultiple, FMTreeSelect, FMCascader, + FMUploadImage, FMUploadImages, FMUploadFile, FMUploadFiles, + FMSwitch, + FMRate, + FMCitySelector, +} + +var FMMap = map[string]string{ + FMInput: "文本输入", + FMInputNumber: "数字输入", + FMInputTextarea: "文本域", + FMInputEditor: "富文本", + FMInputDynamic: "动态键值对", + FMDate: "日期选择(Y-M-D)", + FMDateRange: "日期范围选择", + FMTime: "时间选择(Y-M-D H:i:s)", + FMTimeRange: "时间范围选择", + FMRadio: "单选按钮", + FMCheckbox: "复选按钮", + FMSelect: "单选下拉框", + FMSelectMultiple: "多选下拉框", + FMTreeSelect: "树型选择", + FMCascader: "级联选择", + FMUploadImage: "单图上传", + FMUploadImages: "多图上传", + FMUploadFile: "单文件上传", + FMUploadFiles: "多文件上传", + FMSwitch: "开关", + FMRate: "评分", + FMCitySelector: "省市区选择", +} + +// 表单验证 +const ( + FRNone = "none" + FRIp = "ip" + FRPercentage = "percentage" + FRTel = "tel" + FRPhone = "phone" + FRQq = "qq" + FREmail = "email" + FRIdCard = "idCard" + FRNum = "num" + FRBankCard = "bankCard" + FRWeibo = "weibo" + FRUserName = "userName" + FRAccount = "account" + FRPassword = "password" + FRAmount = "amount" +) + +var FRMap = map[string]string{ + FRNone: "不验证", + FRIp: "Ipv4或Ipv6", + FRPercentage: "0-100百分比", + FRTel: "固话格式", + FRPhone: "手机号", + FRQq: "QQ号码", + FREmail: "邮箱", + FRIdCard: "身份证", + FRNum: "非零正整数", + FRBankCard: "银行卡", + FRWeibo: "微博号", + FRUserName: "用户名", + FRAccount: "账号", + FRPassword: "密码", + FRAmount: "金额", +} + +// 查询条件 +const ( + WMEq = "=" // = + WMNeq = "!=" // != + WMGt = ">" // > + WMGte = ">=" // >= + WMLt = "<" // < + WMLte = "<=" // <= + WMIn = "IN" // IN (...) + WMNotIn = "NOT IN" // NOT IN (...) + WMBetween = "BETWEEN" // BETWEEN + WMNotBetween = "NOT BETWEEN" // NOT BETWEEN + WMLike = "LIKE" // LIKE + WMLikeAll = "LIKE %...%" // LIKE %...% + WMNotLike = "NOT LIKE" // NOT LIKE + WMJsonContains = "JSON_CONTAINS(doc, val)" // JSON_CONTAINS(json_doc, val[, path]) // 判断是否包含某个json值 +) + +var WMs = []string{WMEq, + WMNeq, WMGt, WMGte, WMLt, WMLte, + WMIn, WMNotIn, + WMBetween, WMNotBetween, + WMLike, WMLikeAll, WMNotLike, + WMJsonContains, +} + +// 表格列的排序方式 +const ( + TableAlignLeft = "left" + TableAlignRight = "right" + TableAlignCenter = "center" +) + +var TableAligns = []string{TableAlignLeft, TableAlignRight, TableAlignCenter} + +var TableAlignMap = map[string]string{ + TableAlignLeft: "居左", + TableAlignRight: "居右", + TableAlignCenter: "居中", +} + +// 是否是数字类型 +func IsNumberType(goType string) bool { + switch goType { + case GoTypeInt, GoTypeUint, GoTypeInt64, GoTypeUint64: + return true + case GoTypeFloat32, GoTypeFloat64: + return true + } + return false +} + +// IsSelectFormModel 是否是选择器组件 +func IsSelectFormModel(FM string) bool { + switch FM { + case FMRadio, FMCheckbox, FMSelect, FMSelectMultiple, FMCitySelector, FMTreeSelect, FMCascader: + return true + } + return false +} + +// 字段映射关系 + +// go类型 +const ( + GoTypeString = "string" + GoTypeDate = "date" + GoTypeDatetime = "datetime" + GoTypeInt = "int" + GoTypeUint = "uint" + GoTypeInt64 = "int64" + GoTypeUint64 = "uint64" + GoTypeIntSlice = "[]int" + GoTypeInt64Slice = "[]int64" + GoTypeUint64Slice = "[]uint64" + GoTypeFloat32 = "float32" + GoTypeFloat64 = "float64" + GoTypeBytes = "[]byte" + GoTypeBool = "bool" + GoTypeTime = "time.Time" + GoTypeGTime = "*gtime.Time" + GoTypeJson = "*gjson.Json" +) + +var GoTypeNameMap = map[string]string{ + GoTypeString: GoTypeString, + GoTypeDate: GoTypeDate, + GoTypeDatetime: GoTypeDatetime, + GoTypeInt: GoTypeInt, + GoTypeUint: GoTypeUint, + GoTypeInt64: GoTypeInt64, + GoTypeUint64: GoTypeUint64, + GoTypeIntSlice: GoTypeIntSlice, + GoTypeInt64Slice: GoTypeInt64Slice, + GoTypeUint64Slice: GoTypeUint64Slice, + GoTypeFloat32: GoTypeFloat32, + GoTypeFloat64: GoTypeFloat64, + GoTypeBytes: GoTypeBytes, + GoTypeBool: GoTypeBool, + GoTypeTime: GoTypeTime, + GoTypeGTime: GoTypeGTime, + GoTypeJson: GoTypeJson, +} + +// ts类型 +const ( + TsTypeString = "string" + TsTypeNumber = "number" + TsTypeBoolean = "boolean" + TsTypeArray = "array" + TsTypeTuple = "tuple" + TsTypeAny = "any" +) + +var TsTypeNameMap = map[string]string{ + TsTypeString: TsTypeString, + TsTypeNumber: TsTypeNumber, + TsTypeBoolean: TsTypeBoolean, + TsTypeArray: TsTypeArray, + TsTypeTuple: TsTypeTuple, + TsTypeAny: TsTypeAny, +} + +// ShiftMap Go -> Ts 类型转换 +var ShiftMap = map[string]string{ + GoTypeString: TsTypeString, + GoTypeDate: TsTypeString, + GoTypeDatetime: TsTypeString, + GoTypeInt: TsTypeNumber, + GoTypeUint: TsTypeNumber, + GoTypeInt64: TsTypeNumber, + GoTypeUint64: TsTypeNumber, + GoTypeIntSlice: TsTypeArray, + GoTypeInt64Slice: TsTypeArray, + GoTypeUint64Slice: TsTypeArray, + GoTypeFloat32: TsTypeNumber, + GoTypeFloat64: TsTypeNumber, + GoTypeBytes: TsTypeString, + GoTypeBool: TsTypeBoolean, + GoTypeTime: TsTypeString, + GoTypeGTime: TsTypeString, + GoTypeJson: TsTypeAny, +} + +func HasColumn(fields []*genmodel.GenCodesColumnListModel, column string) bool { + for _, field := range fields { + if field.GoName == column { + return true + } + } + return false +} + +func HasColumnWithFormMode(fields []*genmodel.GenCodesColumnListModel, formMode string) bool { + for _, field := range fields { + if field.FormMode == formMode { + return true + } + } + return false +} + +func HasMaxSort(fields []*genmodel.GenCodesColumnListModel) bool { + return HasColumn(fields, "Sort") +} + +func HasStatus(headOps []string, fields []*genmodel.GenCodesColumnListModel) bool { + if !gstr.InArray(headOps, "status") { + return false + } + return HasColumn(fields, "Status") +} + +func HasSwitch(fields []*genmodel.GenCodesColumnListModel) bool { + return HasColumnWithFormMode(fields, FMSwitch) +} + +func HasHookMemberSummary(fields []*genmodel.GenCodesColumnListModel) bool { + for _, field := range fields { + if IsMemberSummaryField(field.Name) { + if field.IsList { + return true + } + } + } + return false +} + +func HasQueryMemberSummary(fields []*genmodel.GenCodesColumnListModel) bool { + for _, field := range fields { + if IsMemberSummaryField(field.Name) { + if field.IsQuery { + return true + } + } + } + return false +} + +func IsMemberSummaryField(name string) bool { + switch name { + case "created_by", "updated_by", "deleted_by": + return true + } + return false +} + +// ReviseFields 校正字段值,兼容版本升级前的老数据格式 +func ReviseFields(fields []*genmodel.GenCodesColumnListModel) []*genmodel.GenCodesColumnListModel { + for _, field := range fields { + if !gstr.InArray(TableAligns, field.Align) { + field.Align = TableAlignLeft + } + + if field.Width < 1 { + field.Width = -1 + } + if field.Width > 2000 { + field.Width = 2000 + } + + if field.FormGridSpan < 1 { + field.FormGridSpan = 1 + } + } + return fields +} diff --git a/server/internal/library/xgen/gen_view/gen_curd.go b/server/internal/library/xgen/gen_view/gen_curd.go new file mode 100644 index 0000000..ce3beaa --- /dev/null +++ b/server/internal/library/xgen/gen_view/gen_curd.go @@ -0,0 +1,993 @@ +// package genview +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genview + +import ( + "context" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" + genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts" + genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + "git.5iot.com/iotlcm-public/iotlcm-app/utility" + "git.5iot.com/iotlcm-public/iotlcm-app/utility/tree" + version "git.5iot.com/iotlcm-public/iotlcm-app/utility/version" + "runtime" + "strings" + + "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/os/gview" + "github.com/gogf/gf/v2/text/gstr" +) + +var Curd = gCurd{} + +type gCurd struct{} + +func (l *gCurd) DoBuild(ctx context.Context, in *genmodel.CurdBuildParam) (err error) { + st := gtime.Now() + preview, err := l.DoPreview(ctx, in.PreviewIn) + if err != nil { + return + } + + db, err := g.DB().Open(ParseDBConfigNodeLink(&gdb.ConfigNode{Link: in.PreviewIn.DaoConfig.Link})) + if err != nil { + err = gerror.Newf("连接数据库失败,请检查配置文件[server/hack/config.yaml]数据库配置是否正确!err:%v", err.Error()) + return err + } + + defer db.Close() + if err = db.Ping(); err != nil { + err = gerror.Newf("数据库访问异常,请检查配置文件[server/hack/config.yaml]数据库配置是否正确!err:%v", err.Error()) + return + } + + // 前置操作 + if len(in.BeforeEvent) > 0 { + for name, f := range in.BeforeEvent { + if gstr.InArray(in.PreviewIn.Options.AutoOps, name) { + if err = f(ctx); err != nil { + return gerror.Newf("in doBuild operation beforeEvent to '%s' failed:%v", name, err) + } + } + } + } + + // 处理sql文件 + handleSqlFile := func(vi *genmodel.GenFile) (err error) { + // 无需生成 + if vi.Meth != genconsts.GenCodesBuildMethCreate && vi.Meth != genconsts.GenCodesBuildMethCover { + return + } + + if err = gfile.PutContents(vi.Path, strings.TrimSpace(vi.Content)); err != nil { + return gerror.Newf("writing content to '%s' failed: %v", vi.Path, err) + } + + // 导入失败,将sql文件删除 + if err = ImportSql(ctx, vi.Path); err != nil { + _ = gfile.Remove(vi.Path) + } + return + } + + // 将sql文件提取出来优先处理 + // sql执行过程出错是高概率事件,后期在执行前要进行预效验,尽量减少在执行过程中出错的可能性 + sqlGenFile, ok := preview.Views["source.sql"] + if ok { + delete(preview.Views, "source.sql") + if err = handleSqlFile(sqlGenFile); err != nil { + return + } + } + + for _, vi := range preview.Views { + // 无需生成 + if vi.Meth != genconsts.GenCodesBuildMethCreate && vi.Meth != genconsts.GenCodesBuildMethCover { + continue + } + + if err = gfile.PutContents(vi.Path, strings.TrimSpace(vi.Content)); err != nil { + return gerror.Newf("writing content to '%s' failed: %v", vi.Path, err) + } + } + + // 后置操作 + if len(in.AfterEvent) > 0 { + for name, f := range in.AfterEvent { + if gstr.InArray(in.PreviewIn.Options.AutoOps, name) { + if err = f(ctx); err != nil { + return gerror.Newf("in doBuild operation afterEvent to '%s' failed:%v", name, err) + } + } + } + } + g.Log().Debugf(ctx, "generate code operation completed, %vms", gtime.Now().Sub(st).Milliseconds()) + return +} + +func (l *gCurd) DoPreview(ctx context.Context, in *genmodel.CurdPreviewParam) (res *genmodel.GenCodesPreviewModel, err error) { + // 初始化 + if err = l.initInput(ctx, in); err != nil { + return nil, err + } + + // 加载模板 + if err = l.loadView(ctx, in); err != nil { + return nil, err + } + + if err = l.generateApiContent(ctx, in); err != nil { + return nil, err + } + + if err = l.generateInputContent(ctx, in); err != nil { + return nil, err + } + + if err = l.generateControllerContent(ctx, in); err != nil { + return nil, err + } + + if err = l.generateLogicContent(ctx, in); err != nil { + return nil, err + } + + if err = l.generateRouterContent(ctx, in); err != nil { + return nil, err + } + + if err = l.generateWebApiContent(ctx, in); err != nil { + return nil, err + } + + if err = l.generateWebModelContent(ctx, in); err != nil { + return nil, err + } + + if err = l.generateWebIndexContent(ctx, in); err != nil { + return nil, err + } + + if err = l.generateWebEditContent(ctx, in); err != nil { + return nil, err + } + + if err = l.generateWebViewContent(ctx, in); err != nil { + return nil, err + } + + if err = l.generateSqlContent(ctx, in); err != nil { + return nil, err + } + + in.Content.Config = in.Config + res = in.Content + return +} + +func (l *gCurd) initInput(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + in.Content = new(genmodel.GenCodesPreviewModel) + in.Content.Views = make(map[string]*genmodel.GenFile) + + // 初始化生成选项 + if err = initOptions(in); err != nil { + return err + } + + // 初始化表字段配置 + if err = initTableField(ctx, in); err != nil { + return err + } + + // 初始化树表 + if err = initTableTree(in); err != nil { + return err + } + + // 初始化生成选型 + initStep(in) + + // 初始化方法字典 + if err = initFuncDict(in); err != nil { + return err + } + + // 初始化生成模板 + if err = initTemplate(in); err != nil { + return err + } + return +} +func initOptions(in *genmodel.CurdPreviewParam) (err error) { + if err = in.In.Options.Scan(&in.Options); err != nil { + return + } + in.Options.DictMap = make(g.Map) + return +} + +func initTableField(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + // 加载主表配置 + if err = in.In.MasterColumns.Scan(&in.MasterFields); err != nil { + return + } + + if len(in.MasterFields) == 0 { + if in.MasterFields, err = TableColumnList(ctx, &genmodel.GenCodesColumnListParam{DbGroup: in.In.DbName, TableName: in.In.TableName}); err != nil { + return + } + } + + // 主键属性 + in.Pk = getPkField(in) + if in.Pk == nil { + return gerror.New("initInput no primary key is set in the table!") + } + + in.MasterFields = ReviseFields(in.MasterFields) + + // 检查表命名 + var names = []string{in.In.DaoName} + for _, v := range in.Options.Join { + v.Columns = ReviseFields(v.Columns) + names = append(names, v.DaoName) + g.Log().Infof(ctx, "initTableField in.Options.Join: %+v", v) + } + if err = CheckIllegalName("数据库表名", names...); err != nil { + return + } + + if err = CheckIllegalName("实体命名", in.In.VarName); err != nil { + return + } + return +} + +// getPkField 获取主键 +func getPkField(in *genmodel.CurdPreviewParam) *genmodel.GenCodesColumnListModel { + if len(in.MasterFields) == 0 { + panic("getPkField masterFields uninitialized.") + } + for _, field := range in.MasterFields { + if IsIndexPK(field.Index) { + return field + } + } + return nil +} + +func initTableTree(in *genmodel.CurdPreviewParam) (err error) { + // 检查树表字段 + if in.In.GenType == genconsts.GenCodesTypeTree { + if err = CheckTreeTableFields(in.MasterFields); err != nil { + return err + } + if in.Options.Tree == nil { + in.Options.Tree = &genmodel.OptionsTree{} + } + + if in.Options.Tree.PidColumn == "" { + in.Options.Tree.PidColumn = "parent_id" + } + if in.Options.Tree.TitleColumn == "" { + if field := inferTreeTitleField(in.MasterFields, in.Options.Tree.PidColumn); field != nil { + in.Options.Tree.TitleColumn = field.Name + } + } + + hasTitle := false + hasPid := false + hasLevel := false + hasTree := false + for _, field := range in.MasterFields { + if in.Options.Tree.TitleColumn == field.Name { + in.Options.Tree.TitleField = field + hasTitle = true + } + if in.Options.Tree.PidColumn == field.Name { + in.Options.Tree.PidField = field + hasPid = true + } + if in.Options.Tree.LevelColumn != "" && in.Options.Tree.LevelColumn == field.Name { + in.Options.Tree.LevelField = field + hasLevel = true + } + if in.Options.Tree.TreeColumn != "" && in.Options.Tree.TreeColumn == field.Name { + in.Options.Tree.TreeField = field + hasTree = true + } + } + if !hasTitle { + err = gerror.New("请选择一个有效的树名称字段") + return + } + if !hasPid { + err = gerror.New("请选择一个有效的父级字段") + return + } + in.Options.Tree.HasTreePath = hasLevel && hasTree + } + return err +} + +func inferTreeTitleField(fields []*genmodel.GenCodesColumnListModel, pidColumn string) *genmodel.GenCodesColumnListModel { + preferredNames := []string{"tree_name", "name", "title", "label", "menu_name", "dept_name", "category_name"} + for _, name := range preferredNames { + for _, field := range fields { + if field.Name == name { + return field + } + } + } + for _, field := range fields { + if IsIndexPK(field.Index) || field.Name == pidColumn || field.Name == "tenant_id" { + continue + } + goType := strings.TrimPrefix(field.GoType, "*") + if goType == "string" { + return field + } + } + return nil +} + +// CheckTreeTableFields 检查树表字段 +func CheckTreeTableFields(columns []*genmodel.GenCodesColumnListModel) (err error) { + return +} + +func initStep(in *genmodel.CurdPreviewParam) { + in.Options.Step = new(genmodel.CurdStep) + in.Options.Step.HasMaxSort = HasMaxSort(in.MasterFields) + in.Options.Step.HasAdd = gstr.InArray(in.Options.HeadOps, "add") + in.Options.Step.HasBatchDel = gstr.InArray(in.Options.HeadOps, "batchDel") && gstr.InArray(in.Options.ColumnOps, "check") + in.Options.Step.HasExport = gstr.InArray(in.Options.HeadOps, "export") + in.Options.Step.HasNotFilterAuth = gstr.InArray(in.Options.ColumnOps, "notFilterAuth") + in.Options.Step.HasEdit = gstr.InArray(in.Options.ColumnOps, "edit") + in.Options.Step.HasDel = gstr.InArray(in.Options.ColumnOps, "del") + in.Options.Step.HasView = gstr.InArray(in.Options.ColumnOps, "view") + in.Options.Step.HasStatus = HasStatus(in.Options.ColumnOps, in.MasterFields) + in.Options.Step.HasSwitch = HasSwitch(in.MasterFields) + in.Options.Step.HasCheck = gstr.InArray(in.Options.ColumnOps, "check") + in.Options.Step.HasMenu = gstr.InArray(in.Options.AutoOps, "genMenuPermissions") + in.Options.Step.HasQueryMemberSummary = HasQueryMemberSummary(in.MasterFields) + in.Options.Step.HasHookMemberSummary = HasHookMemberSummary(in.MasterFields) + in.Options.Step.IsTreeTable = in.In.GenType == genconsts.GenCodesTypeTree + if in.Options.Step.IsTreeTable { + in.Options.Step.IsOptionTreeTable = in.Options.Tree.StyleType == genconsts.GenCodesTreeStyleTypeOption + } + in.Options.Step.HasFuncDict = gstr.InArray(in.Options.AutoOps, "genFuncDict") + in.Options.Step.IsAddon = in.Config.Application.Crud.Templates[in.In.GenTemplate].IsAddon + if in.Options.PresetStep.FormGridCols < 1 { + in.Options.PresetStep.FormGridCols = 1 + } + in.Options.Step.IsEditModal = gstr.InArray(in.Options.AutoOps, "genEditModal") +} + +func initFuncDict(in *genmodel.CurdPreviewParam) (err error) { + g.Log().Infof(context.Background(), "server/internal/library/xgen/gen_curd.go initFuncDict in.Options.Step.HasFuncDict:%v in.Options.FuncDict:%+v", in.Options.Step.HasFuncDict, in.Options.FuncDict) + if !in.Options.Step.HasFuncDict || in.Options.FuncDict == nil { + return + } + + if len(in.Options.FuncDict.LabelColumn) == 0 || len(in.Options.FuncDict.ValueColumn) == 0 { + err = gerror.New("生成字典选项必须设置选项值和选项名称") + return err + } + + for _, field := range in.MasterFields { + if field.Name == in.Options.FuncDict.ValueColumn { + in.Options.FuncDict.Value = field + } + + if field.Name == in.Options.FuncDict.LabelColumn { + in.Options.FuncDict.Label = field + } + } + g.Log().Infof(context.Background(), "server/internal/library/xgen/gen_curd.go initFuncDict in.Options.Step.HasFuncDict:%v in.Options.FuncDict:%+v", in.Options.Step.HasFuncDict, in.Options.FuncDict) + return +} + +func initTemplate(in *genmodel.CurdPreviewParam) (err error) { + if len(in.Config.Application.Crud.Templates)-1 < in.In.GenTemplate { + return gerror.New("没有找到生成模板的配置,请检查!") + } + + // api前缀 + apiPrefix := gstr.LcFirst(in.In.VarName) + if in.Config.Application.Crud.Templates[in.In.GenTemplate].IsAddon { + apiPrefix = in.In.AddonName + "/" + apiPrefix + } + in.Options.ApiPrefix = apiPrefix + + if err = checkCurdPath(in.Config.Application.Crud.Templates[in.In.GenTemplate], in.In.AddonName); err != nil { + return + } + in.Options.TemplateGroup = in.Config.Application.Crud.Templates[in.In.GenTemplate].MasterPackage + return +} + +func checkCurdPath(temp *genmodel.GenCodesConfigCrudTemplate, addonName string) (err error) { + if temp == nil { + return gerror.New("生成模板配置不能为空") + } + + if temp.IsAddon { + temp.TemplatePath = gstr.Replace(temp.TemplatePath, "{$name}", addonName) + temp.ApiPath = gstr.Replace(temp.ApiPath, "{$name}", addonName) + temp.InputPath = gstr.Replace(temp.InputPath, "{$name}", addonName) + temp.ControllerPath = gstr.Replace(temp.ControllerPath, "{$name}", addonName) + temp.LogicPath = gstr.Replace(temp.LogicPath, "{$name}", addonName) + temp.RouterPath = gstr.Replace(temp.RouterPath, "{$name}", addonName) + temp.SqlPath = gstr.Replace(temp.SqlPath, "{$name}", addonName) + temp.WebApiPath = gstr.Replace(temp.WebApiPath, "{$name}", addonName) + temp.WebViewsPath = gstr.Replace(temp.WebViewsPath, "{$name}", addonName) + } + + tip := `生成模板配置参数'%s'路径不存在,请先创建路径:%s` + + if !gfile.Exists(temp.TemplatePath) { + return gerror.Newf(tip, "TemplatePath", temp.TemplatePath) + } + if !gfile.Exists(temp.ApiPath) { + return gerror.Newf(tip, "ApiPath", temp.ApiPath) + } + if !gfile.Exists(temp.InputPath) { + return gerror.Newf(tip, "InputPath", temp.InputPath) + } + if !gfile.Exists(temp.ControllerPath) { + return gerror.Newf(tip, "ControllerPath", temp.ControllerPath) + } + if !gfile.Exists(temp.LogicPath) { + return gerror.Newf(tip, "LogicPath", temp.LogicPath) + } + if !gfile.Exists(temp.RouterPath) { + return gerror.Newf(tip, "RouterPath", temp.RouterPath) + } + if !gfile.Exists(temp.SqlPath) { + return gerror.Newf(tip, "SqlPath", temp.SqlPath) + } + if !gfile.Exists(temp.WebApiPath) { + return gerror.Newf(tip, "WebApiPath", temp.WebApiPath) + } + if !gfile.Exists(temp.WebViewsPath) { + return gerror.Newf(tip, "WebViewsPath", temp.WebViewsPath) + } + return +} + +func (l *gCurd) loadView(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + temp := in.Config.Application.Crud.Templates[in.In.GenTemplate] + view := gview.New() + err = view.SetConfigWithMap(g.Map{ + "Paths": temp.TemplatePath, + "Delimiters": in.Config.Delimiters, + }) + if err != nil { + return + } + + now := gtime.Now() + view.BindFuncMap(g.Map{ + "NowYear": now.Year, // 当前年 + "ToLower": strings.ToLower, // 全部小写 + "LcFirst": gstr.LcFirst, // 首字母小写 + "UcFirst": gstr.UcFirst, // 首字母大写 + "ToTSArray": ToTSArray, // 转为ts数组格式 + "DictToTSArray": DictToTSArray, // 转为ts数组格式 + }) + + if err = l.generateWebModelDictOptions(ctx, in); err != nil { + return + } + + modName, err := GetModName(ctx) + if err != nil { + return + } + importApi := gstr.Replace(temp.ApiPath, "./", modName+"/") + "/" + strings.ToLower(in.In.VarName) + "/v1" + importInput := gstr.Replace(temp.InputPath, "./", modName+"/") + importController := gstr.Replace(temp.ControllerPath, "./", modName+"/") + importService := "git.5iot.com/iotlcm-public/iotlcm-app/internal/service" + if temp.IsAddon { + importService = "git.5iot.com/iotlcm-public/iotlcm-app/addons/" + in.In.AddonName + "/service" + } + + in.Options.ImportWebApi = "#/api/gen/" + gstr.LcFirst(in.In.VarName) + if temp.IsAddon { + in.Options.ImportWebApi = "#/api/gen/addons/" + in.In.AddonName + "/" + gstr.LcFirst(in.In.VarName) + } + + componentPrefix := gstr.LcFirst(in.In.VarName) + if temp.IsAddon { + componentPrefix = "addons/" + in.In.AddonName + "/" + componentPrefix + } + + nowTime := now.Format("Y-m-d H:i:s") + view.Assigns(gview.Params{ + "templateGroup": in.Options.TemplateGroup, // 生成模板分组名称 + "servFunName": l.parseServFunName(in.Options.TemplateGroup, in.In.VarName), // 业务服务名称 + "nowTime": nowTime, // 当前时间 + "version": runtime.Version(), // GO 版本 + "xjVersion": version.BuildVersion, // XJ 版本 + "varName": in.In.VarName, // 实体名称 + "tableComment": in.In.TableComment, // 对外名称 + "daoName": in.In.DaoName, // ORM模型 + "masterFields": in.MasterFields, // 主表字段 + "pk": in.Pk, // 主键属性 + "options": in.Options, // 提交选项 + "dictOptions": in.Options.DictOps, // web字典选项 + "importApi": importApi, // 导入goApi包 + "importInput": importInput, // 导入input包 + "importController": importController, // 导入控制器包 + "importService": importService, // 导入业务服务 + "importWebApi": in.Options.ImportWebApi, // 导入webApi + "apiPrefix": in.Options.ApiPrefix, // api前缀 + "componentPrefix": componentPrefix, // vue子组件前缀 + }) + + in.View = view + return +} + +func (l *gCurd) generateApiContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "api.go" + tplData = g.Map{} + genFile = new(genmodel.GenFile) + ) + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return err + } + + genFile.Content, err = FormatGo(ctx, name, genFile.Content) + if err != nil { + return err + } + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].ApiPath, strings.ToLower(in.In.VarName), "v1", strings.ToLower(in.In.VarName)+".go") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethSkip + } + + genFile.Required = true + + if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") { + genFile.Meth = genconsts.GenCodesBuildMethCover + } + + in.Content.Views[name] = genFile + return +} + +func (l *gCurd) generateInputContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "input.go" + genFile = new(genmodel.GenFile) + ) + + tplData, err := l.inputTplData(ctx, in) + if err != nil { + return err + } + + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return err + } + + genFile.Content, err = FormatGo(ctx, name, genFile.Content) + if err != nil { + return err + } + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].InputPath, CamelCaseToUnderline(in.In.VarName)+".go") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethSkip + } + genFile.Required = true + + if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") { + genFile.Meth = genconsts.GenCodesBuildMethCover + } + + in.Content.Views[name] = genFile + return +} + +func (l *gCurd) generateControllerContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "controller.go" + tplData = g.Map{} + genFile = new(genmodel.GenFile) + ) + + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return err + } + + genFile.Content, err = FormatGo(ctx, name, genFile.Content) + if err != nil { + return err + } + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].ControllerPath, CamelCaseToUnderline(in.In.VarName)+".go") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethSkip + } + genFile.Required = true + + if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") { + genFile.Meth = genconsts.GenCodesBuildMethCover + } + + in.Content.Views[name] = genFile + return +} + +func (l *gCurd) generateLogicContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "logic.go" + genFile = new(genmodel.GenFile) + ) + + tplData, err := l.logicTplData(ctx, in) + if err != nil { + return err + } + + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return err + } + + genFile.Content, err = FormatGo(ctx, name, genFile.Content) + if err != nil { + return err + } + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].LogicPath, CamelCaseToUnderline(in.In.VarName)+".go") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethSkip + } + genFile.Required = true + + if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") { + genFile.Meth = genconsts.GenCodesBuildMethCover + } + + in.Content.Views[name] = genFile + return +} + +func (l *gCurd) generateRouterContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "router.go" + tplData = g.Map{} + genFile = new(genmodel.GenFile) + ) + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return err + } + + genFile.Content, err = FormatGo(ctx, name, genFile.Content) + if err != nil { + return err + } + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].RouterPath, CamelCaseToUnderline(in.In.VarName)+".go") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethSkip + } + genFile.Required = true + + if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") { + genFile.Meth = genconsts.GenCodesBuildMethCover + } + + in.Content.Views[name] = genFile + return +} + +func (l *gCurd) generateWebApiContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "web.api.ts" + tplData = g.Map{} + genFile = new(genmodel.GenFile) + ) + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return err + } + + genFile.Content = FormatTs(genFile.Content) + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebApiPath, gstr.LcFirst(in.In.VarName), "index.ts") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethSkip + } + genFile.Required = true + + if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") { + genFile.Meth = genconsts.GenCodesBuildMethCover + } + + in.Content.Views[name] = genFile + return +} + +func (l *gCurd) generateWebModelContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "web.model.ts" + genFile = new(genmodel.GenFile) + ) + + tplData, err := l.webModelTplData(ctx, in) + if err != nil { + return + } + + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return + } + + genFile.Content = FormatTs(genFile.Content) + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "model.tsx") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethCover + } + genFile.Required = true + in.Content.Views[name] = genFile + return +} + +func (l *gCurd) generateWebIndexContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "web.index.vue" + genFile = new(genmodel.GenFile) + ) + + tplData, err := l.webIndexTplData(ctx, in) + if err != nil { + return err + } + + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return err + } + + genFile.Content = FormatVue(genFile.Content) + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "index.vue") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethSkip + } + genFile.Required = true + + if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") { + genFile.Meth = genconsts.GenCodesBuildMethCover + } + in.Content.Views[name] = genFile + return +} + +func (l *gCurd) generateWebEditContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "web.edit.vue" + genFile = new(genmodel.GenFile) + ) + + tplData, err := l.webEditTplData(ctx, in) + if err != nil { + return err + } + + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return err + } + + genFile.Content = FormatVue(genFile.Content) + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "edit.vue") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethSkip + } + genFile.Required = true + if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") { + genFile.Meth = genconsts.GenCodesBuildMethCover + } + + if !in.Options.Step.HasEdit { + genFile.Meth = genconsts.GenCodesBuildIgnore + genFile.Required = false + } + + in.Content.Views[name] = genFile + return +} + +func (l *gCurd) generateWebViewContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "web.view.vue" + genFile = new(genmodel.GenFile) + ) + + tplData, err := l.webViewTplData(ctx, in) + if err != nil { + return err + } + + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return err + } + + genFile.Content = FormatVue(genFile.Content) + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "view.vue") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethSkip + } + genFile.Required = true + + if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") { + genFile.Meth = genconsts.GenCodesBuildMethCover + } + + if !in.Options.Step.HasView { + genFile.Meth = genconsts.GenCodesBuildIgnore + genFile.Required = false + } + + in.Content.Views[name] = genFile + return +} + +func (l *gCurd) generateSqlContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) { + var ( + name = "source.sql" + config = g.DB("default").GetConfig() + tplData = g.Map{ + "dbName": config.Name, + "menuTable": config.Prefix + "sys_menu", + "roleMenuTable": config.Prefix + "sys_role_menu", + "mainComponent": "BasicLayout", + } + genFile = new(genmodel.GenFile) + ) + + menus, err := service.SysMenu().GetFastList(ctx) + if err != nil { + return err + } + + tplData["dirPid"], tplData["dirLevel"], tplData["dirTree"], err = xgorm.AutoUpdateTree(ctx, &dao.SysMenu, 0, int64(in.Options.Menu.Pid), &xgorm.TreeFiledOption{ + IdField: "menu_id", + PidField: "parent_id", + LevelField: "level", + TreeField: "tree", + }) + if err != nil { + return err + } + + tplData["listLevel"] = tplData["dirLevel"].(int) + 1 + tplData["btnLevel"] = tplData["dirLevel"].(int) + 2 + tplData["sortLevel"] = tplData["dirLevel"].(int) + 3 + + pageRedirect := "" + if in.Options.Menu.Pid > 0 { + tplData["mainComponent"] = "ParentLayout" + menu, ok := menus[int64(in.Options.Menu.Pid)] + if !ok { + err = gerror.New("选择的上级菜单不存在") + return + } + for _, id := range tree.GetIds(menu.Tree) { + if v, ok2 := menus[id]; ok2 { + if !gstr.HasSuffix(pageRedirect, "/") && !gstr.HasPrefix(v.Path, "/") { + pageRedirect += "/" + } + pageRedirect += v.Path + } + } + + if !gstr.HasSuffix(pageRedirect, "/") && !gstr.HasPrefix(menu.Path, "/") { + pageRedirect += "/" + } + pageRedirect += menu.Path + } + pageRedirect += "/" + gstr.LcFirst(in.In.VarName) + "/index" + tplData["pageRedirect"] = pageRedirect + + genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].SqlPath, CamelCaseToUnderline(in.In.VarName)+"_menu.sql") + genFile.Meth = genconsts.GenCodesBuildMethCreate + if gfile.Exists(genFile.Path) { + genFile.Meth = genconsts.GenCodesBuildMethSkip + } + genFile.Required = true + + if !in.Options.Step.HasMenu { + genFile.Meth = genconsts.GenCodesBuildIgnore + genFile.Required = false + } + + // 需要生成时,检查菜单命名是否存在 + if genFile.Meth == genconsts.GenCodesBuildMethCreate { + menuNamePrefix := gstr.LcFirst(in.In.VarName) + menuNames := []string{menuNamePrefix, menuNamePrefix + "Index"} + if in.Options.Step.HasEdit { + menuNames = append(menuNames, menuNamePrefix+"Edit") + menuNames = append(menuNames, menuNamePrefix+"View") + } + if in.Options.Step.HasView { + menuNames = append(menuNames, menuNamePrefix+"View") + } + if in.Options.Step.HasMaxSort { + menuNames = append(menuNames, menuNamePrefix+"MaxSort") + } + if in.Options.Step.HasDel { + menuNames = append(menuNames, menuNamePrefix+"Delete") + } + if in.Options.Step.HasStatus { + menuNames = append(menuNames, menuNamePrefix+"Status") + } + if in.Options.Step.HasSwitch { + menuNames = append(menuNames, menuNamePrefix+"Switch") + } + if in.Options.Step.HasExport { + menuNames = append(menuNames, menuNamePrefix+"Export") + } + if in.Options.Step.IsTreeTable { + menuNames = append(menuNames, menuNamePrefix+"TreeOption") + } + + menuNames = utility.UniqueSlice(menuNames) + + hasMenus, err := service.SysMenu().Model(ctx).Fields("path").WhereIn("path", menuNames).Array() + if err != nil { + return err + } + + if len(hasMenus) > 0 { + err = gerror.Newf("要生成的菜单中有已存在的路由别名,请检查并删除:%v", strings.Join(gvar.New(hasMenus).Strings(), `、`)) + return err + } + } + + tplData["generatePath"] = genFile.Path + genFile.Content, err = in.View.Parse(ctx, name+".template", tplData) + if err != nil { + return err + } + + in.Content.Views[name] = genFile + return +} diff --git a/server/internal/library/xgen/gen_view/gen_curd_input.go b/server/internal/library/xgen/gen_view/gen_curd_input.go new file mode 100644 index 0000000..845cb47 --- /dev/null +++ b/server/internal/library/xgen/gen_view/gen_curd_input.go @@ -0,0 +1,414 @@ +// package genview +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genview + +import ( + "bytes" + "context" + "fmt" + "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao" + genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model" + "strings" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/text/gstr" + "github.com/olekukonko/tablewriter" + "github.com/olekukonko/tablewriter/tw" +) + +const ( + InputTypeListInp = 1 // 列表输入 + InputTypeListModel = 2 // 列表输出 + InputTypeExportModel = 3 // 列表导出 + InputTypeEditInpValidator = 4 // 添加&编辑验证器 + InputTypeUpdateFields = 5 // 编辑修改过滤字段 + InputTypeInsertFields = 6 // 编辑新增过滤字段 + InputTypeTreeOptionFields = 7 // 关系树查询字段 + InputTypeEditParamFields = 8 // 编辑参数过滤字段 + EditInpValidatorGenerally = "if err := g.Validator().Rules(\"%s\").Data(in.%s).Messages(\"%s\").Run(ctx); err != nil {\n\t\treturn err.Current()\n\t}\n" +) + +func (l *gCurd) inputTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) { + data = make(g.Map) + data["listInpColumns"] = l.genInputListColumns(ctx, in, InputTypeListInp) + data["listModelColumns"] = l.genInputListColumns(ctx, in, InputTypeListModel) + data["exportModelColumns"] = l.genInputListColumns(ctx, in, InputTypeExportModel) + data["editInpValidator"] = l.genInputListColumns(ctx, in, InputTypeEditInpValidator) + data["updateFieldsColumns"] = l.genInputListColumns(ctx, in, InputTypeUpdateFields) + data["insertFieldsColumns"] = l.genInputListColumns(ctx, in, InputTypeInsertFields) + data["editParamFieldsColumns"] = l.genInputListColumns(ctx, in, InputTypeEditParamFields) + data["viewModelColumns"] = l.genInputViewColumns(ctx, in) + if in.Options.Step.IsTreeTable { + data["treeOptionFields"] = l.genInputListColumns(ctx, in, InputTypeTreeOptionFields) + } + return +} + +func (l *gCurd) genInputViewColumns(ctx context.Context, in *genmodel.CurdPreviewParam) string { + buffer := bytes.NewBuffer(nil) + + index := 0 + array := make([][]string, 1000) + // 主表 + for _, field := range in.MasterFields { + // 查询用户摘要 + if field.IsList && in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) { + tagKey := "`" + descriptionTag := gstr.Replace(formatComment(field.Dc)+"摘要信息", `"`, `\"`) + result := []string{" #" + field.GoName + "Summa"} + result = append(result, " #*hook.MemberSumma") + result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName+"Summa")) + result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag)) + array[index] = result + index++ + } + } + table := tablewriter.NewTable(buffer) + + table.Options(tablewriter.WithRendition(tw.Rendition{ + Borders: tw.Border{ + Top: tw.Fail, + Bottom: tw.Fail, + Left: tw.Success, + Right: tw.Success, + }, + Settings: tw.Settings{ + Lines: tw.Lines{ + ShowTop: tw.Fail, + ShowBottom: tw.Fail, + ShowHeaderLine: tw.Fail, + ShowFooterLine: tw.Fail, + }, + }, + }), tablewriter.WithRowConfig(tw.CellConfig{ + Formatting: tw.CellFormatting{ + AutoWrap: tw.WrapNone, + }, + }), tablewriter.WithSymbols(tw.NewSymbols(tw.StyleDefault))) + + // tw.SetBorder(false) + // tw.SetRowLine(false) + // tw.SetAutoWrapText(false) + // tw.SetColumnSeparator("") + table.Bulk(array) + table.Render() + stContent := buffer.String() + // Let's do this hack of table writer for indent! + stContent = gstr.Replace(stContent, "│#", " ") + stContent = gstr.Replace(stContent, "│", " ") + stContent = gstr.Replace(stContent, " #", " ") + stContent = gstr.Replace(stContent, "` ", "`") + stContent = gstr.Replace(stContent, "``", "") + stContent = removeEndWrap(stContent) + + buffer.Reset() + buffer.WriteString(stContent) + return "\tentity." + in.In.DaoName + "\n" + buffer.String() +} + +func (l *gCurd) genInputListColumns(ctx context.Context, in *genmodel.CurdPreviewParam, inputType int) string { + buffer := bytes.NewBuffer(nil) + index := 0 + array := make([][]string, 1000) + // 主表 + for _, field := range in.MasterFields { + row := l.genStructFieldDefinition(in, field, inputType, true) + if row == nil { + continue + } + array[index] = row + index++ + + switch inputType { + case InputTypeListModel: + // 查询用户摘要 + if field.IsList && in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) { + tagKey := "`" + descriptionTag := gstr.Replace(formatComment(field.Dc)+"摘要信息", `"`, `\"`) + result := []string{" #" + field.GoName + "Summa"} + result = append(result, " #*hook.MemberSumma") + result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName+"Summa")) + result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag)) + array[index] = result + index++ + } + } + } + + // 关联表 + if len(in.Options.Join) > 0 { + for _, v := range in.Options.Join { + if !isEffectiveJoin(v) { + g.Log().Infof(ctx, "genInputListColumns isEffectiveJoin: %+v", v) + continue + } + g.Log().Infof(ctx, "genInputListColumns v: %+v", v) + for _, field := range v.Columns { + row := l.genStructFieldDefinition(in, field, inputType, false) + g.Log().Infof(ctx, "genInputListColumns field: %+v row: %+v", field, row) + if row != nil { + array[index] = row + index++ + } + } + } + } + g.Log().Infof(ctx, "genInputListColumns in.Options.Join: %+v", in.Options.Join) + + table := tablewriter.NewWriter(buffer) + table.Options(tablewriter.WithRendition(tw.Rendition{ + Borders: tw.Border{ + Top: tw.Fail, + Bottom: tw.Fail, + Left: tw.Success, + Right: tw.Success, + }, + Settings: tw.Settings{ + Lines: tw.Lines{ + ShowTop: tw.Fail, + ShowBottom: tw.Fail, + ShowHeaderLine: tw.Fail, + ShowFooterLine: tw.Fail, + }, + }, + }), tablewriter.WithRowConfig(tw.CellConfig{ + Formatting: tw.CellFormatting{ + AutoWrap: tw.WrapNone, + }, + }), tablewriter.WithSymbols(tw.NewSymbols(tw.StyleDefault))) + // tw.SetBorder(false) + // tw.SetRowLine(false) + // tw.SetAutoWrapText(false) + // tw.SetColumnSeparator("") + table.Bulk(array) + table.Render() + stContent := buffer.String() + // Let's do this hack of table writer for indent! + stContent = gstr.Replace(stContent, "│#", " ") + stContent = gstr.Replace(stContent, "│", " ") + stContent = gstr.Replace(stContent, " #", " ") + stContent = gstr.Replace(stContent, "` ", "`") + stContent = gstr.Replace(stContent, "``", "") + stContent = removeEndWrap(stContent) + + buffer.Reset() + buffer.WriteString(stContent) + return buffer.String() +} + +// genStructFieldForModel gens and returns the attribute definition for specified field. +func (l *gCurd) genStructFieldDefinition(in *genmodel.CurdPreviewParam, field *genmodel.GenCodesColumnListModel, inputType int, isMaster bool) []string { + var ( + tagKey = "`" + result = []string{" #" + field.GoName} + descriptionTag = gstr.Replace(formatComment(field.Dc), `"`, `\"`) + ) + + addResult := func() []string { + isTreeManagedField := in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn + isTreeManagedField = isTreeManagedField || (in.Options.Step.IsTreeTable && in.Options.Tree.HasTreePath && (field.Name == in.Options.Tree.LevelColumn || field.Name == in.Options.Tree.TreeColumn)) + if inputType == InputTypeEditParamFields && !IsIndexPK(field.Index) && !isTreeManagedField && !strings.HasPrefix(field.GoType, "*") { + result = append(result, " #*"+field.GoType) + } else { + result = append(result, " #"+field.GoType) + } + result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName)) + result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag)) + return result + } + + isUpdateOrInsertInputFields := func() bool { + if !isMaster { + return field.IsEdit + } + + if field.IsEdit { + return true + } + + // 树表内部维护字段 + if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn { + return true + } + if in.Options.Step.IsTreeTable && in.Options.Tree.HasTreePath && (field.Name == in.Options.Tree.LevelColumn || field.Name == in.Options.Tree.TreeColumn) { + return true + } + + // 修改人 + if inputType == InputTypeUpdateFields && field.GoName == "UpdatedBy" { + return true + } + if inputType == InputTypeUpdateFields && field.GoName == "UpdatedAt" { + return true + } + // 创建人 + if inputType == InputTypeInsertFields && field.GoName == "CreatedBy" { + return true + } + if inputType == InputTypeInsertFields && field.GoName == "CreatedAt" { + return true + } + if inputType == InputTypeInsertFields && field.GoName == "CreatedDept" { + return true + } + if inputType == InputTypeInsertFields && field.GoName == "TenantId" { + return true + } + return false + } + + isQuery := false + + switch inputType { + case InputTypeListInp: + if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn { + isQuery = true + field.QueryWhere = WMEq + } + if !field.IsQuery && !isQuery { + return nil + } + + if field.QueryWhere == WMBetween { + result = append(result, " #[]"+field.GoType) + } else { + // 查询用户摘要时,固定接收字符串类型 + if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) { + result = append(result, " #string") + } else { + result = append(result, " #"+field.GoType) + } + } + result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName)) + result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag)) + + case InputTypeListModel: + // 主表的主键 + if IsIndexPK(field.Index) && isMaster { + addResult() + // 树表的pid字段 + } else if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn { + addResult() + } else if field.IsList { + addResult() + } else { + return nil + } + case InputTypeExportModel: + if !field.IsExport { + return nil + } + addResult() + case InputTypeEditInpValidator: + if !field.IsEdit { + return nil + } + if !field.Required && (field.FormRole == "none" || field.FormRole == "") { + return nil + } + rule := "// 验证" + field.Dc + "\n" + if field.Required && (field.FormRole == FRNone || field.FormRole == "") { + field.FormRole = "required" + } + if err, s := makeValidatorFunc(field); err != nil { + return nil + } else { + rule += s + } + result = []string{rule} + case InputTypeUpdateFields: + if !isUpdateOrInsertInputFields() { + return nil + } + addResult() + case InputTypeInsertFields: + if !isUpdateOrInsertInputFields() { + return nil + } + addResult() + case InputTypeTreeOptionFields: + if IsIndexPK(field.Index) { + return addResult() + } + if field.Name == in.Options.Tree.PidColumn { + return addResult() + } + if in.Options.Tree.TitleColumn == field.Name { + return addResult() + } + return nil + case InputTypeEditParamFields: + return addResult() + default: + panic("inputType is invalid") + } + return result +} + +func makeValidatorFunc(field *genmodel.GenCodesColumnListModel) (err error, rule string) { + if field.FormRole == "required" { + rule = fmt.Sprintf(EditInpValidatorGenerally, "required", field.GoName, field.Dc+"不能为空") + } else if field.FormRole == FRIp { + rule = fmt.Sprintf(EditInpValidatorGenerally, "ip", field.GoName, field.Dc+"必须为IPV4或IPV6") + } else if field.FormRole == FRPercentage { + rule = fmt.Sprintf(EditInpValidatorGenerally, "min:0|max:100", field.GoName, field.Dc+"必须0-100之间") + } else if field.FormRole == FRTel { + rule = fmt.Sprintf(EditInpValidatorGenerally, "telephone", field.GoName, field.Dc+"不是座机号码") + } else if field.FormRole == FRPhone { + rule = fmt.Sprintf(EditInpValidatorGenerally, "phone", field.GoName, field.Dc+"不是手机号码") + } else if field.FormRole == FRQq { + rule = fmt.Sprintf(EditInpValidatorGenerally, "qq", field.GoName, field.Dc+"不是QQ号码") + } else if field.FormRole == FREmail { + rule = fmt.Sprintf(EditInpValidatorGenerally, "email", field.GoName, field.Dc+"不是邮箱地址") + } else if field.FormRole == FRIdCard { + rule = fmt.Sprintf(EditInpValidatorGenerally, "resident-id", field.GoName, field.Dc+"不是身份证号码") + } else if field.FormRole == FRNum { + rule = fmt.Sprintf(EditInpValidatorGenerally, "min:1", field.GoName, field.Dc+"必须大于0") + } else if field.FormRole == FRBankCard { + rule = fmt.Sprintf(EditInpValidatorGenerally, "bank-card", field.GoName, field.Dc+"不是银行卡号") + } else if field.FormRole == FRWeibo { + rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^[0-9a-zA-Z\\u4e00-\\u9fa5_-]*$", field.GoName, field.Dc+"不是微博号") + } else if field.FormRole == FRUserName { + rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^[0-9a-zA-Z]{6,16}$", field.GoName, field.Dc+"必须为6-16位由字母和数字组成") + } else if field.FormRole == FRAccount { + rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^[\\w_\\d]{6,16}$", field.GoName, field.Dc+"必须为6-16位由字母、数字或下划线组成") + } else if field.FormRole == FRPassword { + rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,18}$", field.GoName, field.Dc+"必须包含6-18为字母和数字") + } else if field.FormRole == FRAmount { + rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:(^[0-9]{1,10}$)|(^[0-9]{1,10}[\\\\.]{1}[0-9]{1,2}$)", field.GoName, field.Dc+"最多允许输入10位整数及2位小数") + } else { + err = gerror.New("not support") + } + + // 生成验证字典 + if field.DictType != "" { + var ( + ctx = context.Background() + valueType gdb.Value + resultValue gdb.Result + ) + valueType, err = dao.SysDictType.Ctx(ctx).Fields(dao.SysDictType.Columns().DictType).Where(dao.SysDictType.Columns().DictId, field.DictType).Value() + if err != nil { + return + } + resultValue, err = dao.SysDictData.Ctx(context.Background()).Fields(dao.SysDictData.Columns().DictValue).Where(dao.SysDictData.Columns().DictType, valueType.String()).All() + if err != nil { + return + } + if resultValue.Len() > 0 { + names := make([]string, 0) + for _, item := range resultValue { + names = append(names, item["value"].String()) + } + dictRule := "in:" + strings.Join(names, ",") + rule += fmt.Sprintf(EditInpValidatorGenerally, dictRule, field.GoName, field.Dc+"值不正确") + err = nil + } + } + return +} diff --git a/server/internal/library/xgen/gen_view/gen_curd_logic.go b/server/internal/library/xgen/gen_view/gen_curd_logic.go new file mode 100644 index 0000000..c7c3bf1 --- /dev/null +++ b/server/internal/library/xgen/gen_view/gen_curd_logic.go @@ -0,0 +1,357 @@ +// package genview +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genview + +import ( + "bytes" + "context" + "fmt" + + genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts" + genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/text/gstr" +) + +const ( + LogicWhereComments = "\n\t// 查询%s\n" + LogicWhereNoSupport = "\t// TODO 暂不支持生成[ %s ]查询方式,请自行补充此处代码!" + LogicEditUpdate = "\t\tdata := do.%s{}\n\t\tgconv.Struct(in, &data)\n\t\tdata.%s = nil\n\t\tif _, err = s.Model(ctx%s).TX(tx).\n\t\t\tFields(%sin.%sUpdateFields{}).\n\t\t\tWherePri(in.%s).Data(data).Update(); err != nil {\n\t\t\terr = gerror.Wrap(err, \"修改%s失败,请稍后重试!\")\n\t\t}\n\t\treturn" + LogicEditInsert = "\t\tdata := do.%s{}\n\t\tgconv.Struct(in, &data)\n\t\tdata.%s = nil\n\t\tif _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).TX(tx).\n\t\t\tFields(%sin.%sInsertFields{}).\n\t\t\tData(data).Insert(); err != nil {\n\t\t\terr = gerror.Wrap(err, \"新增%s失败,请稍后重试!\")\n\t\t}" + LogicEditUnique = "\t// 验证'%s'唯一\n\tif err = xgorm.IsUnique(ctx, &dao.%s, g.Map{dao.%s.Columns().%s: in.%s}, \"%s已存在\", in.%s); err != nil {\n\t\treturn\n\t}\n" + LogicSwitchUpdate = "%s" + LogicStatusUpdate = "do.%s{\n\t\tStatus: in.Status,\n%s}" + LogicDeletedUpdate = "do.%s{\n%s}" +) + +func (l *gCurd) logicTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) { + g.Log().Infof(ctx, "logicTplData in.Options.Join: %+v", in.Options.Join) + data = make(g.Map) + data["listWhere"] = l.genLogicListWhere(ctx, in) + data["listJoin"] = l.genLogicListJoin(ctx, in) + data["listFields"] = l.genLogicListFields(ctx, in) + data["listOrder"] = l.genLogicListOrder(ctx, in) + data["edit"] = l.genLogicEdit(ctx, in) + data["switchFields"] = l.genLogicSwitchFields(ctx, in) + data["switchUpdate"] = l.genLogicSwitchUpdate(ctx, in) + data["statusUpdate"] = l.genLogicStatusUpdate(ctx, in) + data["deletedUpdate"] = l.genLogicDeletedUpdate(ctx, in) + return +} + +func (l *gCurd) genLogicDeletedUpdate(ctx context.Context, in *genmodel.CurdPreviewParam) string { + hasDeletedAt := false + var update string + for _, field := range in.MasterFields { + switch field.GoName { + case "DeletedBy": + update += "\t\tDeletedBy: contexts.GetUserId(ctx),\n" + case "DeletedAt": + update += "\t\tDeletedAt: gtime.Now(),\n" + hasDeletedAt = true + } + } + + if !hasDeletedAt { + return "" + } + + update += "\t" + return fmt.Sprintf(LogicDeletedUpdate, in.In.DaoName, update) +} + +func (l *gCurd) genLogicStatusUpdate(ctx context.Context, in *genmodel.CurdPreviewParam) string { + var update string + for _, field := range in.MasterFields { + switch field.GoName { + case "UpdatedBy": + update += "\t\tUpdatedBy: contexts.GetUserId(ctx),\n" + case "UpdatedAt": + update += "\t\tUpdatedAt: gtime.Now(),\n" + } + } + + update += "\t" + return fmt.Sprintf(LogicStatusUpdate, in.In.DaoName, update) +} + +func (l *gCurd) genLogicSwitchUpdate(ctx context.Context, in *genmodel.CurdPreviewParam) string { + buffer := bytes.NewBuffer(nil) + buffer.WriteString("\tvar data do." + in.In.DaoName + "\n") + buffer.WriteString("\tswitch in.Key {\n") + for _, field := range in.MasterFields { + if field.FormMode != FMSwitch { + continue + } + buffer.WriteString("\tcase \"" + field.Name + "\":\n") + buffer.WriteString("\t\tdata." + field.GoName + " = " + switchValueExpression(field.GoType) + "\n") + } + buffer.WriteString("\tdefault:\n") + buffer.WriteString("\t\terr = gerror.New(\"开关键名不在白名单\")\n") + buffer.WriteString("\t\treturn\n") + buffer.WriteString("\t}\n") + for _, field := range in.MasterFields { + switch field.GoName { + case "UpdatedBy": + buffer.WriteString("\tdata.UpdatedBy = contexts.GetUserId(ctx)\n") + case "UpdatedAt": + buffer.WriteString("\tdata.UpdatedAt = gtime.Now()\n") + } + } + + return fmt.Sprintf(LogicSwitchUpdate, buffer.String()) +} + +func switchValueExpression(goType string) string { + switch goType { + case GoTypeBool: + return "gconv.Bool(in.Value)" + case GoTypeInt: + return "gconv.Int(in.Value)" + case GoTypeUint: + return "gconv.Uint(in.Value)" + case GoTypeUint64: + return "gconv.Uint64(in.Value)" + case GoTypeInt64: + return "gconv.Int64(in.Value)" + case GoTypeFloat32: + return "gconv.Float32(in.Value)" + case GoTypeFloat64: + return "gconv.Float64(in.Value)" + case GoTypeString: + return "gconv.String(in.Value)" + default: + return "gconv.Int(in.Value)" + } +} + +func (l *gCurd) genLogicSwitchFields(ctx context.Context, in *genmodel.CurdPreviewParam) string { + buffer := bytes.NewBuffer(nil) + if in.Options.Step.HasSwitch { + for _, field := range in.MasterFields { + if field.FormMode == FMSwitch { + buffer.WriteString("\t\tdao." + in.In.DaoName + ".Columns()." + field.GoName + ",\n") + } + } + } + return buffer.String() +} + +func (l *gCurd) genLogicEdit(ctx context.Context, in *genmodel.CurdPreviewParam) g.Map { + var ( + data = make(g.Map) + updateBuffer = bytes.NewBuffer(nil) + insertBuffer = bytes.NewBuffer(nil) + uniqueBuffer = bytes.NewBuffer(nil) + ) + + for _, field := range in.MasterFields { + if field.GoName == "TenantId" { + insertBuffer.WriteString("\t\ttenantId := contexts.GetTenantId(ctx)\n\t\tin.TenantId = &tenantId\n") + } + if field.GoName == "CreatedDept" { + insertBuffer.WriteString("\t\tin.CreatedDept = contexts.GetDeptId(ctx)\n") + } + if field.GoName == "CreatedAt" { + insertBuffer.WriteString("\t\tin.CreatedAt = gtime.Now()\n") + } + if field.GoName == "CreatedBy" { + insertBuffer.WriteString("\tin.CreatedBy = contexts.GetUserId(ctx)\n") + } + if field.GoName == "UpdatedBy" { + updateBuffer.WriteString("\t\tin.UpdatedBy = contexts.GetUserId(ctx)\n") + } + if field.GoName == "UpdatedAt" { + updateBuffer.WriteString("\t\tin.UpdatedAt = gtime.Now()\n") + } + + if field.Unique { + uniqueBuffer.WriteString(fmt.Sprintf(LogicEditUnique, field.GoName, in.In.DaoName, in.In.DaoName, field.GoName, field.GoName, field.Dc, in.Pk.GoName)) + } + } + + notFilterAuth := "" + if gstr.InArray(in.Options.ColumnOps, "notFilterAuth") { + notFilterAuth = ", &handler.Option{FilterAuth: false}" + } + + updateBuffer.WriteString(fmt.Sprintf(LogicEditUpdate, in.In.DaoName, in.Pk.GoName, notFilterAuth, in.Options.TemplateGroup, in.In.VarName, in.Pk.GoName, in.In.TableComment)) + insertBuffer.WriteString(fmt.Sprintf(LogicEditInsert, in.In.DaoName, in.Pk.GoName, in.Options.TemplateGroup, in.In.VarName, in.In.TableComment)) + + data["update"] = updateBuffer.String() + data["insert"] = insertBuffer.String() + data["unique"] = uniqueBuffer.String() + return data +} + +func (l *gCurd) genLogicListOrder(ctx context.Context, in *genmodel.CurdPreviewParam) string { + statement := "" + if hasEffectiveJoins(in.Options.Join) { + statement = "dao." + in.In.DaoName + ".Table() + \".\" +" + } + buffer := bytes.NewBuffer(nil) + if in.Options.Step.HasMaxSort { + buffer.WriteString("OrderAsc(" + statement + "dao." + in.In.DaoName + ".Columns().Sort).") + } + buffer.WriteString("OrderDesc(" + statement + "dao." + in.In.DaoName + ".Columns()." + in.Pk.GoName + ")") + return buffer.String() +} + +func (l *gCurd) genLogicListJoin(ctx context.Context, in *genmodel.CurdPreviewParam) (link string) { + connector := `"="` + if hasEffectiveJoins(in.Options.Join) { + linkBuffer := bytes.NewBuffer(nil) + for _, join := range in.Options.Join { + if isEffectiveJoin(join) { + linkBuffer.WriteString("\tmod = mod." + genconsts.GenCodesJoinLinkMap[join.LinkMode] + "OnFields(dao." + join.DaoName + ".Table(), dao." + in.In.DaoName + ".Columns()." + gstr.CaseCamel(join.MasterField) + "," + connector + ", dao." + join.DaoName + ".Columns()." + gstr.CaseCamel(join.Field) + ")\n") + } + } + link = linkBuffer.String() + } + return +} + +func (l *gCurd) genLogicListFields(ctx context.Context, in *genmodel.CurdPreviewParam) (fields string) { + selectBuffer := bytes.NewBuffer(nil) + if hasEffectiveJoins(in.Options.Join) { + selectBuffer.WriteString("mod = mod.FieldsPrefix(dao." + in.In.DaoName + ".Table(), " + in.Options.TemplateGroup + "in." + in.In.VarName + "ListModel{})\n") + for _, join := range in.Options.Join { + if isEffectiveJoin(join) { + selectBuffer.WriteString("mod = mod.Fields(xgorm.JoinFields(ctx, " + in.Options.TemplateGroup + "in." + in.In.VarName + "ListModel{}, &dao." + join.DaoName + ", \"" + join.Alias + "\"))\n") + } + } + fields = selectBuffer.String() + } else { + fields = fmt.Sprintf("mod = mod.Fields(%sin.%sListModel{})", in.Options.TemplateGroup, in.In.VarName) + } + return +} + +func (l *gCurd) genLogicListWhere(ctx context.Context, in *genmodel.CurdPreviewParam) string { + buffer := bytes.NewBuffer(nil) + + // 主表 + l.genLogicListWhereEach(buffer, in, in.MasterFields, in.In.DaoName, "") + // 关联表 + g.Log().Infof(ctx, "genLogicListWhere in.Options.Join: %+v", in.Options.Join) + if hasEffectiveJoins(in.Options.Join) { + for _, v := range in.Options.Join { + if isEffectiveJoin(v) { + l.genLogicListWhereEach(buffer, in, v.Columns, v.DaoName, v.Alias) + } + } + } + return buffer.String() +} + +func (l *gCurd) genLogicListWhereEach(buffer *bytes.Buffer, in *genmodel.CurdPreviewParam, fields []*genmodel.GenCodesColumnListModel, daoName string, alias string) { + isLink := false + if alias != "" { + alias = `"` + alias + `."+` + isLink = true + } + + tablePrefix := "" + wherePrefix := "Where" + if isLink { + wherePrefix = "WherePrefix" + tablePrefix = "dao." + daoName + ".Table(), " + } + + for _, field := range fields { + isQuery := false + // 树表查询上级 + if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn { + isQuery = true + field.QueryWhere = WMEq + } + + if (!field.IsQuery && !isQuery) || field.QueryWhere == "" { + continue + } + + buffer.WriteString(fmt.Sprintf(LogicWhereComments, field.Dc)) + + var ( + linkMode string + whereTag string + columnName string + ) + + // 查询用户摘要 + if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) { + servicePackName := "service" + if in.Options.Step.IsAddon { + servicePackName = "isc" + } + buffer.WriteString(fmt.Sprintf("if in.%v != \"\" {\n\t\t\t\tids, err := %v.AdminMember().GetIdsByKeyword(ctx, in.%v)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, 0, err\n\t\t\t\t}\n\t\t\t\tmod = mod.WhereIn(dao.%v.Columns().%v, ids)\n\t\t\t}\n", field.GoName, servicePackName, field.GoName, in.In.DaoName, field.GoName)) + continue + } + + if IsNumberType(field.GoType) { + linkMode = `in.` + field.GoName + ` > 0` + } else if field.GoType == GoTypeGTime { + linkMode = `in.` + field.GoName + ` != nil` + } else if field.GoType == GoTypeJson { + linkMode = `!in.` + field.GoName + `.IsNil()` + } else { + linkMode = `in.` + field.GoName + ` != ""` + } + + if field.QueryWhere == WMBetween || field.QueryWhere == WMNotBetween { + linkMode = `len(in.` + field.GoName + `) == 2` + } + + // 如果是关联表重新转换字段 + columnName = field.GoName + if isLink { + columnName = gstr.CaseCamel(field.Name) + } + if field.FormMode == FMDateRange { + tmp := "\tif " + linkMode + " {\n\t\tin." + field.GoName + "[1] = in." + field.GoName + "[1].EndOfDay()\n\t}\n" + buffer.WriteString(tmp) + } + + switch field.QueryWhere { + case WMEq: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}" + case WMNeq: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Not(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}" + case WMGt: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "GT(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}" + case WMGte: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "GTE(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}" + case WMLt: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "LT(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}" + case WMLte: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "LTE(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}" + case WMIn: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "In(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}" + case WMNotIn: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "NotIn(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}" + case WMBetween: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Between(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + "[0], in." + field.GoName + "[1])\n\t}" + case WMNotBetween: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "NotBetween(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + "[0], in." + field.GoName + "[1])\n\t}" + case WMLike: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Like(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}" + case WMLikeAll: + val := `"%"+in.` + field.GoName + `+"%"` + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Like(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", " + val + ")\n\t}" + case WMNotLike: + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "NotLike(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}" + case WMJsonContains: + val := tablePrefix + `"JSON_CONTAINS("+dao.` + daoName + `.Columns().` + columnName + `+",?)", in.` + field.GoName + whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "(" + val + ")\n\t}" + + default: + buffer.WriteString(fmt.Sprintf(LogicWhereNoSupport, field.QueryWhere)) + } + + buffer.WriteString(whereTag + "\n") + } +} diff --git a/server/internal/library/xgen/gen_view/gen_curd_model.go b/server/internal/library/xgen/gen_view/gen_curd_model.go new file mode 100644 index 0000000..b14124b --- /dev/null +++ b/server/internal/library/xgen/gen_view/gen_curd_model.go @@ -0,0 +1,903 @@ +// package genview +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genview + +import ( + "bytes" + "context" + "fmt" + genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model" + "git.5iot.com/iotlcm-public/iotlcm-app/utility" + "strconv" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +type StateItem struct { + Name string + DefaultValue interface{} + Dc string +} + +func (l *gCurd) webModelTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) { + data = make(g.Map) + data["stateItems"] = l.genWebModelStateItems(ctx, in) + data["rules"] = l.genWebModelRules(ctx, in) + data["formSchema"] = l.genWebModelFormSchema(ctx, in) + if data["columns"], err = l.genWebModelColumns(ctx, in); err != nil { + return nil, err + } + if data["columnsInterface"], err = l.genWebModelColumnsInterface(ctx, in); err != nil { + return nil, err + } + data["viewSchema"] = l.genWebModelViewSchema(ctx, in) + data["editSchema"] = l.genWebModelEditSchema(ctx, in) + // 根据表单生成情况,按需导包 + data["import"], data["const"] = l.genWebModelImport(ctx, in) + return +} + +func (l *gCurd) genWebModelImport(ctx context.Context, in *genmodel.CurdPreviewParam) (string, string) { + importBuffer := bytes.NewBuffer(nil) + constBuffer := bytes.NewBuffer(nil) + + importBuffer.WriteString(`import { h, ref } from 'vue'; +import { Tag } from 'ant-design-vue'; +import type { VxeGridProps } from '#/adapter/vxe-table'; +import { getPopupContainer } from '@vben/utils'; +import type { DescItem } from '#/components/description'; +`) + + // 导入基础组件 + if len(in.Options.Step.ImportModel.NaiveUI) > 0 { + importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.NaiveUI) + " from 'ant-design-vue';\n") + } + + importBuffer.WriteString("import { cloneDeep } from 'lodash-es';\n") + + // 导入表单搜索 + if in.Options.Step.HasSearchForm { + importBuffer.WriteString("import type { VbenFormSchema } from '@vben/common-ui';\n") + } + + // 导入工具类 + if len(in.Options.Step.ImportModel.UtilsIs) > 0 { + importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsIs) + " from '#/utils/is';\n") + } + + if len(in.Options.Step.ImportModel.UtilsUrl) > 0 { + importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsUrl) + " from '#/utils/urlUtils';\n") + } + + if len(in.Options.Step.ImportModel.UtilsDate) > 0 { + // importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsDate) + " from '@/utils/dateUtil';\n") + importBuffer.WriteString("import dayjs from 'dayjs';\n") + } + + if in.Options.Step.HasRulesValidator { + importBuffer.WriteString("import { z } from '@vben/common-ui';\n") + } + + if len(in.Options.Step.ImportModel.UtilsIndex) > 0 { + importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsIndex) + " from '#/utils';\n") + } + + // 导入api + var importApiMethod []string + if in.Options.Step.HasSwitch { + importApiMethod = append(importApiMethod, "Switch") + } + if in.Options.Step.IsTreeTable { + importApiMethod = append(importApiMethod, "TreeOption") + } + if len(importApiMethod) > 0 { + importBuffer.WriteString("import " + ImportWebMethod(importApiMethod) + " from '" + in.Options.ImportWebApi + "';\n") + } + + // 导入字典选项 + if in.Options.DictOps.Has { + importBuffer.WriteString(`import { DictEnum } from '@vben/constants'; +import { getDictOptions } from '#/utils/dict';`) + constBuffer.WriteString("\n") + } + + if in.Options.Step.HasSwitch { + importBuffer.WriteString("import { usePermission } from '@/hooks/web/usePermission';\n") + constBuffer.WriteString("const { hasPermission } = usePermission();\n") + constBuffer.WriteString("const $message = window['$message'];\n") + } + + return importBuffer.String(), constBuffer.String() +} + +func (l *gCurd) genWebModelStateItems(ctx context.Context, in *genmodel.CurdPreviewParam) (items []*StateItem) { + for _, field := range in.MasterFields { + var value = webModelDefaultValue(field) + + // 选项组件默认值调整 + if gconv.Int(value) == 0 && IsSelectFormModel(field.FormMode) { + value = "null" + } + + if field.Name == "status" { + value = 0 + } + if field.FormMode == FMSwitch { + value = 0 + } + if field.FormMode == FMInputDynamic { + value = "[]" + } + items = append(items, &StateItem{ + Name: field.TsName, + DefaultValue: value, + Dc: field.Dc, + }) + + // 查询用户摘要 + if field.IsList && in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) { + items = append(items, &StateItem{ + Name: field.TsName + "Summa?: null | MemberSumma", + DefaultValue: "null", + Dc: field.Dc + "摘要信息", + }) + } + } + return +} + +func webModelDefaultValue(field *genmodel.GenCodesColumnListModel) interface{} { + value := field.DefaultValue + if value == nil { + return "null" + } + + if gconv.String(value) == "NULL" { + return "null" + } + + switch field.GoType { + case GoTypeString, GoTypeBytes, GoTypeDate, GoTypeDatetime, GoTypeTime, GoTypeGTime: + return strconv.Quote(gconv.String(value)) + default: + if value == "" { + return "null" + } + return value + } +} + +func (l *gCurd) genWebModelDictOptions(ctx context.Context, in *genmodel.CurdPreviewParam) error { + type DictType struct { + DictType string `json:"dictType"` + DictName string `json:"dictName"` + } + + var ( + dictTypeIds []string + dictTypeList []*DictType + builtinDictTypeIds []int64 + builtinDictTypeList []*DictType + ) + + for _, field := range in.MasterFields { + if field.DictType != "" { + dictTypeIds = append(dictTypeIds, field.DictType) + } + + // if field.DictType < 0 { + // builtinDictTypeIds = append(builtinDictTypeIds, field.DictType) + // } + } + + dictTypeIds = utility.UniqueSlice(dictTypeIds) + builtinDictTypeIds = utility.UniqueSlice(builtinDictTypeIds) + + if len(dictTypeIds) == 0 && len(builtinDictTypeIds) == 0 { + return nil + } + + if len(dictTypeIds) > 0 { + err := g.Model("sys_dict_type").Ctx(ctx). + Fields("dict_type", "dict_name"). + WhereIn("dict_type", dictTypeIds). + Scan(&dictTypeList) + if err != nil { + return err + } + } + + // if len(builtinDictTypeIds) > 0 { + // for _, id := range builtinDictTypeIds { + // typ, err := dict.GetTypeById(ctx, id) + // if err != nil && !errors.Is(err, dict.NotExistKeyError) { + // return err + // } + // if len(typ) > 0 { + // row := new(DictType) + // row.Id = id + // row.Type = typ + // builtinDictTypeList = append(builtinDictTypeList, row) + // } + // } + // } + + if len(dictTypeList) == 0 && len(builtinDictTypeList) == 0 { + return nil + } + + if len(builtinDictTypeList) > 0 { + dictTypeList = append(dictTypeList, builtinDictTypeList...) + } + + in.Options.DictOps.Has = true + + for _, v := range dictTypeList { + // 字段映射字典 + for _, field := range in.MasterFields { + if field.DictType != "" && v.DictType == field.DictType { + in.Options.DictMap[field.TsName] = v.DictType + in.Options.DictOps.Schemas = append(in.Options.DictOps.Schemas, &genmodel.OptionsSchemasField{ + Field: field.TsName, + Type: v.DictType, + }) + } + } + in.Options.DictOps.Types = append(in.Options.DictOps.Types, v.DictName) + } + return nil +} + +func (l *gCurd) genWebModelRules(ctx context.Context, in *genmodel.CurdPreviewParam) string { + // buffer := bytes.NewBuffer(nil) + // buffer.WriteString("export const rules = {\n") + // for _, field := range in.MasterFields { + // if !field.IsEdit || (!field.Required && (field.FormRole == "" || field.FormRole == FRNone)) { + // continue + // } + + // in.Options.Step.HasRules = true + // if field.FormRole == "" || field.FormRole == FRNone || field.FormRole == "required" { + // buffer.WriteString(fmt.Sprintf(" %s: {\n required: %v,\n trigger: ['blur', 'input'],\n type: '%s',\n message: '请输入%s',\n },\n", field.TsName, field.Required, field.TsType, field.Dc)) + // } else { + // in.Options.Step.HasRulesValidator = true + // buffer.WriteString(fmt.Sprintf(" %s: {\n required: %v,\n trigger: ['blur', 'input'],\n type: '%s',\n validator: validate.%v,\n },\n", field.TsName, field.Required, field.TsType, field.FormRole)) + // } + // } + // buffer.WriteString("};\n") + // return buffer.String() + return "" +} + +func (l *gCurd) genWebModelFormSchema(ctx context.Context, in *genmodel.CurdPreviewParam) string { + buffer := bytes.NewBuffer(nil) + buffer.WriteString("export const querySchema: VbenFormSchema[] = [\n") + + // 主表 + l.genWebModelFormSchemaEach(buffer, in.MasterFields, in, false) + + // 关联表 + if len(in.Options.Join) > 0 { + for _, v := range in.Options.Join { + if !isEffectiveJoin(v) { + continue + } + l.genWebModelFormSchemaEach(buffer, v.Columns, in, false) + } + } + + buffer.WriteString("];\n") + return buffer.String() +} + +// genWebDictOption 生产字典选项 +func (l *gCurd) genWebDictOption(typ any) string { + return fmt.Sprintf(`getDictOptions('%v')`, typ) +} + +func (l *gCurd) genWebModelFormSchemaEach(buffer *bytes.Buffer, fields []*genmodel.GenCodesColumnListModel, in *genmodel.CurdPreviewParam, isEdit bool) { + for _, field := range fields { + if isEdit { + if !field.IsEdit && field.Name != in.Pk.Name { + continue + } + if field.Name == in.Pk.Name { + buffer.WriteString(fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + dependencies: { show: () => false, triggerFields: [''], }, + componentProps: { + placeholder: '', + onUpdateValue: (e: any) => { + console.log(e); + }, + }, + formItemClass: 'col-span-%d', + },`, field.TsName, "Input", field.Dc, field.FormGridSpan)) + + continue + } + } else { + if !field.IsQuery { + continue + } + } + rules := "null" + if in.Options.Step.IsTreeTable && isEdit && field.Name == in.Options.Tree.PidColumn { + field.FormMode = FMPidTreeSelect + } + if field.IsEdit { + if field.Required { + if field.FormMode == FMInputNumber { + rules = fmt.Sprintf("z.number({required_error: '请输入%s', invalid_type_error: '无效数字'})", field.Dc) + in.Options.Step.HasRulesValidator = true + } else if field.FormMode == FMSelect || field.FormMode == FMSelectMultiple || field.FormMode == FMCheckbox || field.FormMode == FMDate || field.FormMode == FMDateRange || field.FormMode == FMTime || field.FormMode == FMTimeRange { + rules = "'selectRequired'" + } else { + rules = "'required'" + } + } + } + if field.FormRole != "" { + in.Options.Step.HasRulesValidator = true + if field.FormRole == FRNum { + rules = fmt.Sprintf("z.number({required_error: '请输入%s', invalid_type_error: '无效数字'} )", field.Dc) + } + if field.FormRole == FRIp { + rules = "z.string().ip({message: '无效IP格式'})" + } + if field.FormRole == FRPhone { + rules = "z.string().regex(/^1[3-9]\\d{9}$/, '无效手机号格式')" + } + if field.FormRole == FRTel { + rules = "z.string().regex(/^(?:\\+?\\d{1,3}[-.\\s]?)?(?:$?\\d{1,4}$?[-.\\s]?)?\\d{1,4}[-.\\s]?\\d{1,4}[-.\\s]?\\d{1,9}$/, '无效手机号格式')" + } + if field.FormRole == FRPercentage { + rules = fmt.Sprintf("z.number({required_error: '请输入%s', invalid_type_error: '无效百分比'}).min(0).max(100)", field.Dc) + } + if field.FormRole == FRQq { + rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效QQ号格式')" + } + if field.FormRole == FRAmount { + rules = "z.number({required_error: '必填', invalid_type_error: '无效金额'} ).min(0)" + } + if field.FormRole == FRBankCard { + rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效银行卡号格式')" + } + if field.FormRole == FRWeibo { + rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效微博号格式')" + } + if field.FormRole == FRUserName { + rules = "z.string().regex(/^[a-zA-Z0-9_]+$/, '无效用户名格式')" + } + if field.FormRole == FRPassword { + rules = "z.string().regex(/^[a-zA-Z0-9_]+$/, '无效密码格式')" + } + if field.FormRole == FRAccount { + rules = "z.string().regex(/^[a-zA-Z0-9_]+$/, '无效账号格式')" + } + if field.FormRole == FRIdCard { + rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效身份证号格式')" + } + if field.FormRole == FREmail { + rules = "z.string().email('无效邮箱格式')" + } + } + + in.Options.Step.HasSearchForm = true + + // 查询用户摘要 + if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) { + buffer.WriteString(fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + componentProps: { + placeholder: '请输入ID|用户名|姓名|手机号', + onUpdateValue: (e: any) => { + console.log(e); + }, + }, + rules:%v, + formItemClass: 'col-span-%d', + },`, field.TsName, "Input", field.Dc, rules, field.FormGridSpan)) + + continue + } + + var ( + defaultComponent = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + componentProps: { + placeholder: '请输入%s', + onUpdateValue: (e: any) => { + console.log(e); + }, + }, + rules:%v, + formItemClass: 'col-span-%d', + },`, field.TsName, "Input", field.Dc, field.Dc, rules, field.FormGridSpan) + component string + ) + + // 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况 + switch field.FormMode { + case FMInput, FMInputTextarea, FMInputEditor: + component = defaultComponent + + case FMInputNumber: + component = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + componentProps: { + placeholder: '请输入%s', + onUpdateValue: (e: any) => { + console.log(e); + }, + }, rules:%v, + formItemClass: 'col-span-%d', + },`, field.TsName, "InputNumber", field.Dc, field.Dc, rules, field.FormGridSpan) + case FMDate: + component = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + componentProps: { + type: '%s', + clearable: true, + shortcuts: %s, + onUpdateValue: (e: any) => { + console.log(e); + }, + }, rules:%v, + formItemClass: 'col-span-%d', + },`, field.TsName, "DatePicker", field.Dc, "date", "'FMDate'", rules, field.FormGridSpan) + in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defShortcuts") + + case FMDateRange: + component = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + componentProps: { + type: '%s', + clearable: true, + valueFormat: %s, + onUpdateValue: (e: any) => { + console.log(e); + }, + }, rules:%v, + formItemClass: 'col-span-%d', + },`, field.TsName, "RangePicker", field.Dc, "datetimerange", "'FMDateRange'", rules, field.FormGridSpan) + in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defRangeShortcuts") + + // in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defRangeShortcuts") + + case FMTime: + component = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + componentProps: { + type: '%s', + clearable: true, + showTime: true, + shortcuts: %s, + onUpdateValue: (e: any) => { + console.log(e); + }, + }, rules:%v, + formItemClass: 'col-span-%d', + },`, field.TsName, "DatePicker", field.Dc, "datetime", "'FMTime'", rules, field.FormGridSpan) + in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defShortcuts") + + case FMTimeRange: + component = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + componentProps: { + type: '%s', + clearable: true, + shortcuts: %s, + showTime: true, + onUpdateValue: (e: any) => { + console.log(e); + }, + }, rules:%v, + formItemClass: 'col-span-%d', + },`, field.TsName, "RangePicker", field.Dc, "datetimerange", "'FMTimeRange'", rules, field.FormGridSpan) + + in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defRangeShortcuts") + + case FMSwitch: + fallthrough + case FMRadio: + component = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + componentProps: { + options: %v + }, + rules:%v, + formItemClass: 'col-span-%d', + }, + `, field.TsName, "RadioGroup", field.Dc, l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan) + + case FMCheckbox: + component = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + componentProps: { + placeholder: '请选择%s', + options: %v + }, + rules:%v, + formItemClass: 'col-span-%d', + }, + `, field.TsName, "CheckboxGroup", field.Dc, field.Dc, + l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan) + + case FMSelect: + component = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + defaultValue: null, + componentProps: { + placeholder: '请选择%s', + options: %v, + onUpdateValue: (e: any) => { + console.log(e); + }, + }, + rules:%v, + formItemClass: 'col-span-%d', + }, + `, field.TsName, "Select", field.Dc, field.Dc, l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan) + + case FMSelectMultiple: + component = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + defaultValue: null, + componentProps: { + mode: 'multiple', + placeholder: '请选择%s', + options: %v, + onUpdateValue: (e: any) => { + console.log(e); + }, + }, + rules:%v, + formItemClass: 'col-span-%d', + }, + `, field.TsName, "Select", field.Dc, field.Dc, l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan) + + case FMPidTreeSelect: + component = fmt.Sprintf(` { + fieldName: '%s', + component: '%s', + label: '%s', + defaultValue: 0, + componentProps: { + allowClear: true, + fieldNames: { label: '%s', value: '%s' }, + showSearch: true, + treeData: [], + treeDefaultExpandAll: true, + treeLine: { showLeafIcon: false }, + treeNodeFilterProp: '%s', + }, + rules:%v, + formItemClass: 'col-span-%d', + }, + `, field.TsName, "TreeSelect", field.Dc, in.Options.Tree.TitleField.TsName, in.Pk.TsName, in.Options.Tree.TitleField.TsName, rules, field.FormGridSpan) + + default: + component = defaultComponent + } + + buffer.WriteString(component) + } +} + +func (l *gCurd) genWebModelColumns(ctx context.Context, in *genmodel.CurdPreviewParam) (string, error) { + buffer := bytes.NewBuffer(nil) + buffer.WriteString("export const columns: VxeGridProps['columns'] = [\n") + + // 主表 + if err := l.genWebModelColumnsEach(buffer, in, in.MasterFields); err != nil { + return "", err + } + + // 关联表 + if len(in.Options.Join) > 0 { + for _, v := range in.Options.Join { + if !isEffectiveJoin(v) { + continue + } + if err := l.genWebModelColumnsEach(buffer, in, v.Columns); err != nil { + return "", err + } + } + } + buffer.WriteString(" { title: '操作', width: 120, slots: { default: 'action' } },\n") + buffer.WriteString("];\n") + return buffer.String(), nil +} + +func (l *gCurd) genWebModelColumnsInterface(ctx context.Context, in *genmodel.CurdPreviewParam) (string, error) { + buffer := bytes.NewBuffer(nil) + buffer.WriteString("export interface RowType {\n") + + // 主表 + for _, field := range in.MasterFields { + buffer.WriteString(fmt.Sprintf(" %s: %s;\n", field.TsName, field.TsType)) + } + + // 关联表 + if len(in.Options.Join) > 0 { + for _, v := range in.Options.Join { + if !isEffectiveJoin(v) { + continue + } + for _, field := range v.Columns { + buffer.WriteString(fmt.Sprintf(" %s: %s;\n", field.TsName, field.TsType)) + } + } + } + buffer.WriteString("};\n") + return buffer.String(), nil +} + +func (l *gCurd) genWebModelViewSchema(ctx context.Context, in *genmodel.CurdPreviewParam) string { + buffer := bytes.NewBuffer(nil) + buffer.WriteString("export const viewSchema: DescItem[] = [\n") + + // 主表 + for _, field := range in.MasterFields { + l.genWebModelViewSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in) + } + + // 关联表 + if len(in.Options.Join) > 0 { + for _, v := range in.Options.Join { + if !isEffectiveJoin(v) { + continue + } + for _, field := range v.Columns { + l.genWebModelViewSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in) + } + } + } + buffer.WriteString("];\n") + return buffer.String() +} + +func (l *gCurd) genWebModelEditSchema(ctx context.Context, in *genmodel.CurdPreviewParam) string { + buffer := bytes.NewBuffer(nil) + buffer.WriteString("export const editSchema: VbenFormSchema[] = [\n") + + // 主表 + for _, field := range in.MasterFields { + + l.genWebModelFormSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in, true) + } + + // 关联表 + if len(in.Options.Join) > 0 { + for _, v := range in.Options.Join { + if !isEffectiveJoin(v) { + continue + } + for _, field := range v.Columns { + l.genWebModelFormSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in, true) + } + } + } + buffer.WriteString("];\n") + return buffer.String() +} + +func (l *gCurd) genWebModelColumnsEach(buffer *bytes.Buffer, in *genmodel.CurdPreviewParam, fields []*genmodel.GenCodesColumnListModel) (err error) { + for _, field := range fields { + if !field.IsList { + continue + } + + if field.Name == in.Pk.Name { + buffer.WriteString(fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n type: 'checkbox',\n },\n", field.Dc, field.TsName, field.Align, field.Width)) + continue + } + var ( + defaultComponent = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n },\n", field.Dc, field.TsName, field.Align, field.Width) + component string + ) + if in.Options.Step.IsTreeTable && in.Options.Tree.TitleColumn == field.Name { + defaultComponent = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n treeNode: true,\n },\n", field.Dc, field.TsName, field.Align, field.Width) + } + + // 查询用户摘要 + if in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) { + buffer.WriteString(fmt.Sprintf(" {\n title: '%v',\n field: '%v',\n align: '%v',\n width: %v,\n slots: {\n default: ({ row }) => {\n return renderPopoverMemberSumma(row.%vSumma);\n },\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)) + in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, []string{"renderPopoverMemberSumma", "type MemberSumma"}...) + continue + } + + // 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况 + switch field.FormMode { + case FMDate: + component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return formatDateTime(row.%s);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName) + in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "formatToDate") + + case FMRadio: + fallthrough + case FMSelect: + + if g.IsEmpty(in.Options.DictMap[field.TsName]) { + g.Log().Infof(context.Background(), "server/internal/library/xgen/gen_curd_model.go genWebModelColumnsEach in.Options.DictMap:%+v", in.Options.DictMap) + err = gerror.Newf("设置单选下拉框选项时,必须选择字典类型,字段名称:%v", field.Name) + return + } + + component = fmt.Sprintf(` { + title: '%s', field: '%s', align: '%v', width: %v, + slots: { + default: ({ row }) => { + return renderDict(row.%v, '%v'); + } + }, + }, + `, + field.Dc, field.TsName, field.Align, field.Width, field.TsName, in.Options.DictMap[field.TsName]) + in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDict") + + case FMSelectMultiple: + if g.IsEmpty(in.Options.DictMap[field.TsName]) { + err = gerror.Newf("设置多选下拉框选项时,必须选择字典类型,字段名称:%v", field.Name) + return + } + component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n if (isNullObject(row.%s) || !isArray(row.%s)) {\n return ``;\n }\n return row.%s.map((tagKey) => {\n return renderOptionTag('%s', row.tagKey)\n });\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName, field.TsName, field.TsName, in.Options.DictMap[field.TsName]) + in.Options.Step.ImportModel.NaiveUI = append(in.Options.Step.ImportModel.NaiveUI, "NTag") + in.Options.Step.ImportModel.UtilsIs = append(in.Options.Step.ImportModel.UtilsIs, "isNullObject") + + case FMUploadImage: + component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderImage(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName) + in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderImage") + + case FMUploadImages: + component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderImageGroup(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName) + in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderImageGroup") + + case FMUploadFile: + component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderFile(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName) + in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderFile") + + case FMUploadFiles: + component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderFileGroup(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName) + in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderFileGroup") + + case FMSwitch: + component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return h(%s, {\n value: row.%s === 1,\n checked: '开启',\n unchecked: '关闭',\n disabled: !hasPermission(['%s']),\n onUpdateValue: function (e) {\n console.log('onUpdateValue e:' + JSON.stringify(e));\n row.%s = e ? 1 : 0;\n Switch({ %s: row.%s, key: '%s', value: row.%s }).then((_res) => {\n $message.success('操作成功');\n });\n },\n });\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, "NSwitch", field.TsName, "/"+in.Options.ApiPrefix+"/switch", field.TsName, in.Pk.TsName, in.Pk.TsName, CamelCaseToUnderline(field.TsName), field.TsName) + in.Options.Step.ImportModel.NaiveUI = append(in.Options.Step.ImportModel.NaiveUI, "NSwitch") + + case FMRate: + component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return h(%s, {\n allowHalf: true,\n readonly: true,\n defaultValue: row.%s,\n });\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, "NRate", field.TsName) + in.Options.Step.ImportModel.NaiveUI = append(in.Options.Step.ImportModel.NaiveUI, "NRate") + + default: + component = defaultComponent + } + + buffer.WriteString(component) + } + return +} + +func (l *gCurd) genWebModelViewSchemaEach(buffer *bytes.Buffer, fields []*genmodel.GenCodesColumnListModel, in *genmodel.CurdPreviewParam) { + for _, field := range fields { + + if field.Name == in.Pk.Name { + tmp := fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc) + buffer.WriteString(tmp) + continue + } + + // 查询用户摘要 + if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) { + tmp := fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc) + buffer.WriteString(tmp) + continue + } + + var ( + defaultComponent = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc) + component string + ) + + // 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况 + switch field.FormMode { + case FMInput, FMInputTextarea, FMInputEditor: + component = defaultComponent + + case FMInputNumber: + component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc) + + case FMDate: + component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc) + + case FMDateRange: + component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc) + + case FMTime: + component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc) + + case FMTimeRange: + component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc) + + case FMSwitch: + fallthrough + case FMRadio: + component = fmt.Sprintf(` { + field: '%s', + label: '%s', + render(row: any) { + return renderDict(row.%s, '%v'); + }, + }, + `, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName]) + in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDict") + + case FMCheckbox: + component = fmt.Sprintf(` { + field: '%s', + label: '%s', + render(row: any) { + return renderDictTags(row.%s, '%v'); + }, + }, + `, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName]) + in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDictTags") + + case FMSelect: + component = fmt.Sprintf(` { + field: '%s', + label: '%s', + render(row: any) { + return renderDict(row.%s, '%v'); + }, + }, + `, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName]) + in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDict") + + case FMSelectMultiple: + component = fmt.Sprintf(` { + field: '%s', + label: '%s', + render(row: any) { + return renderDictTags(row.%s, '%v'); + }, + }, + `, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName]) + in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDictTags") + + default: + component = defaultComponent + } + + buffer.WriteString(component) + } +} diff --git a/server/internal/library/xgen/gen_view/gen_curd_web_edit.go b/server/internal/library/xgen/gen_view/gen_curd_web_edit.go new file mode 100644 index 0000000..b7da339 --- /dev/null +++ b/server/internal/library/xgen/gen_view/gen_curd_web_edit.go @@ -0,0 +1,342 @@ +// package genview +// @Link https://github.com/xiujiecn/xiu-admin +// @Copyright Copyright (c) 2025 LiXiujie +// @Author Lxj +// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE +package genview + +import ( + "bytes" + "context" + "fmt" + genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model" + "git.5iot.com/iotlcm-public/iotlcm-app/utility" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/text/gstr" +) + +func (l *gCurd) webEditTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) { + data = make(g.Map) + data["formItem"] = l.genWebEditFormItem(ctx, in) + data["script"] = l.genWebEditScript(ctx, in) + data["isEditModal"] = in.Options.Step.IsEditModal + return +} + +func (l *gCurd) genWebEditFormItem(ctx context.Context, in *genmodel.CurdPreviewParam) string { + buffer := bytes.NewBuffer(nil) + for _, field := range in.MasterFields { + if !field.IsEdit { + continue + } + + if IsIndexPK(field.Index) { + continue + } + + var ( + defaultComponent = fmt.Sprintf(` + + + + `, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName) + component string + ) + + if in.Options.Step.IsTreeTable && IsPidName(field.Name) { + field.FormMode = FMPidTreeSelect + } + + switch field.FormMode { + case FMInput: + component = defaultComponent + + case FMInputNumber: + component = fmt.Sprintf(` + + + + `, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName) + + case FMInputTextarea: + component = fmt.Sprintf(` + +