From ddf90424baa22ef5d0e69bb04221826c213cb3aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=AB=E5=9C=B0=E5=83=A7?= <357099073@qq.com> Date: Wed, 29 Oct 2025 23:07:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=A7=9F=E6=88=B7=E7=99=BB?= =?UTF-8?q?=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pc/package-lock.json | 539 +++++++++++++ pc/package.json | 2 + pc/src/api/file.js | 99 +++ pc/src/api/knowledge.js | 115 +++ pc/src/api/knowlege.js | 83 +- pc/src/api/login.js | 5 +- pc/src/api/menu.js | 34 + pc/src/api/tenant.js | 55 ++ pc/src/assets/less/style.less | 752 +++++++++++++++++- pc/src/components/CommonHeader.vue | 3 + pc/src/main.js | 6 +- pc/src/router/index.js | 112 ++- pc/src/utils/request.js | 6 + .../apps/knowledge/components/detail.vue | 189 ++++- .../views/apps/knowledge/components/edit.vue | 274 +++++-- pc/src/views/apps/knowledge/index.vue | 223 +++--- pc/src/views/components/WangEditor.vue | 621 +++++++++++++++ pc/src/views/login/index.vue | 479 +++++++++-- pc/src/views/system/files/categories.vue | 472 +++++++++++ pc/src/views/system/files/index.vue | 673 ++++++++++++++++ pc/src/views/system/files/upload.vue | 369 +++++++++ pc/src/views/system/menus/manager.vue | 548 +++++++++++++ pc/src/views/system/permissions/index.vue | 112 +++ pc/src/views/system/programs/index.vue | 245 ++++++ pc/src/views/system/roles/index.vue | 290 +++++++ .../views/system/tenant/components/audit.vue | 0 .../views/system/tenant/components/detail.vue | 0 .../views/system/tenant/components/edit.vue | 0 pc/src/views/system/tenant/index.vue | 646 +++++++++++++++ pc/src/views/system/users/index.vue | 144 ++++ server/controllers/auth.go | 226 ++++-- server/controllers/menu.go | 72 ++ server/controllers/tenant.go | 208 +++++ server/database/README_TENANTS.md | 119 +++ server/database/create_missing_tables.sql | 38 + server/database/init_database.sql | 52 +- server/database/yz_tenants.sql | 111 +++ server/main.go | 5 +- server/models/auth.go | 8 +- server/models/menu.go | 12 + server/models/tenant.go | 127 +++ server/models/user.go | 150 +++- server/routers/router.go | 11 +- 43 files changed, 7761 insertions(+), 474 deletions(-) create mode 100644 pc/src/api/file.js create mode 100644 pc/src/api/knowledge.js create mode 100644 pc/src/api/tenant.js create mode 100644 pc/src/views/components/WangEditor.vue create mode 100644 pc/src/views/system/files/categories.vue create mode 100644 pc/src/views/system/files/index.vue create mode 100644 pc/src/views/system/files/upload.vue create mode 100644 pc/src/views/system/menus/manager.vue create mode 100644 pc/src/views/system/permissions/index.vue create mode 100644 pc/src/views/system/programs/index.vue create mode 100644 pc/src/views/system/roles/index.vue create mode 100644 pc/src/views/system/tenant/components/audit.vue create mode 100644 pc/src/views/system/tenant/components/detail.vue create mode 100644 pc/src/views/system/tenant/components/edit.vue create mode 100644 pc/src/views/system/tenant/index.vue create mode 100644 pc/src/views/system/users/index.vue create mode 100644 server/controllers/tenant.go create mode 100644 server/database/README_TENANTS.md create mode 100644 server/database/yz_tenants.sql create mode 100644 server/models/tenant.go diff --git a/pc/package-lock.json b/pc/package-lock.json index 92a48ee..cb354d9 100644 --- a/pc/package-lock.json +++ b/pc/package-lock.json @@ -9,11 +9,13 @@ "version": "0.0.0", "dependencies": { "@element-plus/icons-vue": "^2.3.2", + "@wangeditor/editor": "^5.1.23", "axios": "^1.13.1", "chart": "^0.1.2", "chart.js": "^4.5.1", "element-plus": "^2.11.5", "less": "^4.4.2", + "marked": "^16.4.1", "pinia": "^3.0.3", "vue": "^3.5.22", "vue-router": "^4.6.3" @@ -59,6 +61,15 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.28.5", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", @@ -938,6 +949,12 @@ "win32" ] }, + "node_modules/@transloadit/prettier-bytes": { + "version": "0.0.7", + "resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz", + "integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", @@ -945,6 +962,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==", + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.20", "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz", @@ -976,6 +999,61 @@ "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", "license": "MIT" }, + "node_modules/@uppy/companion-client": { + "version": "2.2.2", + "resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz", + "integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "namespace-emitter": "^2.0.1" + } + }, + "node_modules/@uppy/core": { + "version": "2.3.4", + "resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz", + "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==", + "license": "MIT", + "dependencies": { + "@transloadit/prettier-bytes": "0.0.7", + "@uppy/store-default": "^2.1.1", + "@uppy/utils": "^4.1.3", + "lodash.throttle": "^4.1.1", + "mime-match": "^1.0.2", + "namespace-emitter": "^2.0.1", + "nanoid": "^3.1.25", + "preact": "^10.5.13" + } + }, + "node_modules/@uppy/store-default": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz", + "integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==", + "license": "MIT" + }, + "node_modules/@uppy/utils": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz", + "integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==", + "license": "MIT", + "dependencies": { + "lodash.throttle": "^4.1.1" + } + }, + "node_modules/@uppy/xhr-upload": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz", + "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/utils": "^4.1.2", + "nanoid": "^3.1.25" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "6.0.1", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz", @@ -1223,6 +1301,155 @@ } } }, + "node_modules/@wangeditor/basic-modules": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz", + "integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==", + "license": "MIT", + "dependencies": { + "is-url": "^1.2.4" + }, + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "lodash.throttle": "^4.1.1", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/code-highlight": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz", + "integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.23.0" + }, + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/core": { + "version": "1.1.19", + "resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz", + "integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==", + "license": "MIT", + "dependencies": { + "@types/event-emitter": "^0.3.3", + "event-emitter": "^0.3.5", + "html-void-elements": "^2.0.0", + "i18next": "^20.4.0", + "scroll-into-view-if-needed": "^2.2.28", + "slate-history": "^0.66.0" + }, + "peerDependencies": { + "@uppy/core": "^2.1.1", + "@uppy/xhr-upload": "^2.0.3", + "dom7": "^3.0.0", + "is-hotkey": "^0.2.0", + "lodash.camelcase": "^4.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.foreach": "^4.5.0", + "lodash.isequal": "^4.5.0", + "lodash.throttle": "^4.1.1", + "lodash.toarray": "^4.4.0", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/editor": { + "version": "5.1.23", + "resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz", + "integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==", + "license": "MIT", + "dependencies": { + "@uppy/core": "^2.1.1", + "@uppy/xhr-upload": "^2.0.3", + "@wangeditor/basic-modules": "^1.1.7", + "@wangeditor/code-highlight": "^1.0.3", + "@wangeditor/core": "^1.1.19", + "@wangeditor/list-module": "^1.0.5", + "@wangeditor/table-module": "^1.1.4", + "@wangeditor/upload-image-module": "^1.0.2", + "@wangeditor/video-module": "^1.1.4", + "dom7": "^3.0.0", + "is-hotkey": "^0.2.0", + "lodash.camelcase": "^4.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.debounce": "^4.0.8", + "lodash.foreach": "^4.5.0", + "lodash.isequal": "^4.5.0", + "lodash.throttle": "^4.1.1", + "lodash.toarray": "^4.4.0", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/list-module": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz", + "integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==", + "license": "MIT", + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/table-module": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz", + "integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==", + "license": "MIT", + "peerDependencies": { + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "lodash.isequal": "^4.5.0", + "lodash.throttle": "^4.1.1", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/upload-image-module": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz", + "integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==", + "license": "MIT", + "peerDependencies": { + "@uppy/core": "^2.0.3", + "@uppy/xhr-upload": "^2.0.3", + "@wangeditor/basic-modules": "1.x", + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "lodash.foreach": "^4.5.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, + "node_modules/@wangeditor/video-module": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz", + "integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==", + "license": "MIT", + "peerDependencies": { + "@uppy/core": "^2.1.4", + "@uppy/xhr-upload": "^2.0.7", + "@wangeditor/core": "1.x", + "dom7": "^3.0.0", + "nanoid": "^3.2.0", + "slate": "^0.72.0", + "snabbdom": "^3.1.0" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", @@ -1434,6 +1661,12 @@ "node": ">= 0.8" } }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==", + "license": "MIT" + }, "node_modules/confbox": { "version": "0.2.2", "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz", @@ -1459,6 +1692,19 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -1577,6 +1823,15 @@ "node": ">=0.4.0" } }, + "node_modules/dom7": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz", + "integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==", + "license": "MIT", + "dependencies": { + "ssr-window": "^3.0.0-alpha.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1771,6 +2026,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/esbuild": { "version": "0.25.11", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.11.tgz", @@ -1826,6 +2121,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1836,6 +2146,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz", @@ -1843,6 +2163,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", @@ -2168,6 +2497,25 @@ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, + "node_modules/html-void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz", + "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/i18next": { + "version": "20.6.1", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz", + "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -2194,6 +2542,16 @@ "node": ">=0.10.0" } }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", @@ -2354,6 +2712,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==", + "license": "MIT" + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", @@ -2394,6 +2758,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", @@ -2487,6 +2860,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -2616,6 +2995,49 @@ "lodash-es": "*" } }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==", + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", @@ -2639,6 +3061,18 @@ "node": ">=6" } }, + "node_modules/marked": { + "version": "16.4.1", + "resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.1.tgz", + "integrity": "sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2676,6 +3110,15 @@ "node": ">= 0.6" } }, + "node_modules/mime-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz", + "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==", + "license": "ISC", + "dependencies": { + "wildcard": "^1.1.0" + } + }, "node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", @@ -2742,6 +3185,12 @@ "dev": true, "license": "MIT" }, + "node_modules/namespace-emitter": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz", + "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", @@ -2777,6 +3226,12 @@ "node": ">= 4.4.x" } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, "node_modules/normalize-wheel-es": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", @@ -2971,6 +3426,25 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.27.2", + "resolved": "https://registry.npmmirror.com/preact/-/preact-10.27.2.tgz", + "integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3171,6 +3645,15 @@ "license": "ISC", "optional": true }, + "node_modules/scroll-into-view-if-needed": { + "version": "2.2.31", + "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", + "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^1.0.20" + } + }, "node_modules/scule": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", @@ -3306,6 +3789,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/slate": { + "version": "0.72.8", + "resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz", + "integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==", + "license": "MIT", + "dependencies": { + "immer": "^9.0.6", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, + "node_modules/slate-history": { + "version": "0.66.0", + "resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz", + "integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, + "node_modules/snabbdom": { + "version": "3.6.3", + "resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.3.tgz", + "integrity": "sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==", + "license": "MIT", + "engines": { + "node": ">=12.17.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", @@ -3334,6 +3849,12 @@ "node": ">=0.10.0" } }, + "node_modules/ssr-window": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz", + "integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==", + "license": "MIT" + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -3455,6 +3976,12 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3495,6 +4022,12 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmmirror.com/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -3953,6 +4486,12 @@ "funding": { "url": "https://github.com/sponsors/ljharb" } + }, + "node_modules/wildcard": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz", + "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==", + "license": "MIT" } } } diff --git a/pc/package.json b/pc/package.json index f62947a..4a4dd70 100644 --- a/pc/package.json +++ b/pc/package.json @@ -10,11 +10,13 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.2", + "@wangeditor/editor": "^5.1.23", "axios": "^1.13.1", "chart": "^0.1.2", "chart.js": "^4.5.1", "element-plus": "^2.11.5", "less": "^4.4.2", + "marked": "^16.4.1", "pinia": "^3.0.3", "vue": "^3.5.22", "vue-router": "^4.6.3" diff --git a/pc/src/api/file.js b/pc/src/api/file.js new file mode 100644 index 0000000..bfeb413 --- /dev/null +++ b/pc/src/api/file.js @@ -0,0 +1,99 @@ +import request from "@/utils/request"; + +/** + * 获取所有文件 + * @returns {Promise} + */ +export function getAllFiles() { + return request({ + url: "/api/files", + method: "get", + }); +} + +/** + * 获取我的文件 + * @returns {Promise} + */ +export function getMyFiles() { + return request({ + url: "/api/files/my", + method: "get", + }); +} + +/** + * 根据ID获取文件 + * @param {number|string} id 文件ID + * @returns {Promise} + */ +export function getFileById(id) { + return request({ + url: `/api/files/${id}`, + method: "get", + }); +} + +/** + * 上传文件 + * @param {FormData} formData 文件数据 + * @param {Object} options 额外选项 + * @param {string} [options.category] + * @param {string} [options.tenantId] + * @returns {Promise} + */ +export function uploadFile(formData, options = {}) { + if (options.category) { + formData.append('category', options.category); + } + if (options.tenantId) { + formData.append('tenant_id', options.tenantId); + } + return request({ + url: "/api/files", + method: "post", + data: formData, + headers: { + "Content-Type": "multipart/form-data" + } + }); +} + +/** + * 更新文件信息 + * @param {number|string} id 文件ID + * @param {Object} fileData 更新的数据 + * @returns {Promise} + */ +export function updateFile(id, fileData) { + return request({ + url: `/api/files/${id}`, + method: "put", + data: fileData, + }); +} + +/** + * 删除文件 + * @param {number|string} id 文件ID + * @returns {Promise} + */ +export function deleteFile(id) { + return request({ + url: `/api/files/${id}`, + method: "delete", + }); +} + +/** + * 搜索文件 + * @param {string} keyword 关键字 + * @returns {Promise} + */ +export function searchFiles(keyword) { + return request({ + url: `/api/files/search`, + method: "get", + params: { keyword }, + }); +} diff --git a/pc/src/api/knowledge.js b/pc/src/api/knowledge.js new file mode 100644 index 0000000..1721419 --- /dev/null +++ b/pc/src/api/knowledge.js @@ -0,0 +1,115 @@ +import request from "@/utils/request"; + +/** + * 获取知识列表 + * @param {Object} params 查询参数 + * @returns {Promise} + */ +export function getKnowledgeList(params) { + return request({ + url: "/api/knowledge/list", + method: "get", + params, + }); +} + +/** + * 获取知识详情 + * @param {number|string} id 知识ID + * @returns {Promise} + */ +export function getKnowledgeDetail(id) { + return request({ + url: `/api/knowledge/detail`, + method: "get", + params: { id }, + }); +} + +/** + * 创建知识 + * @param {Object} data 知识数据 + * @returns {Promise} + */ +export function createKnowledge(data) { + return request({ + url: "/api/knowledge/create", + method: "post", + data, + }); +} + +/** + * 更新知识 + * @param {number|string} id 知识ID + * @param {Object} data 更新的数据 + * @returns {Promise} + */ +export function updateKnowledge(id, data) { + return request({ + url: `/api/knowledge/update`, + method: "post", + data: { ...data, id }, + }); +} + +/** + * 删除知识 + * @param {number|string} id 知识ID + * @returns {Promise} + */ +export function deleteKnowledge(id) { + return request({ + url: `/api/knowledge/delete`, + method: "post", + data: { id }, + }); +} + +/** + * 获取分类列表 + * @returns {Promise} + */ +export function getCategoryList() { + return request({ + url: "/api/knowledge/categories", + method: "get", + }); +} + +/** + * 获取标签列表 + * @returns {Promise} + */ +export function getTagList() { + return request({ + url: "/api/knowledge/tags", + method: "get", + }); +} + +/** + * 添加分类 + * @param {Object} data 分类数据 + * @returns {Promise} + */ +export function addCategory(data) { + return request({ + url: "/api/knowledge/category/add", + method: "post", + data, + }); +} + +/** + * 添加标签 + * @param {Object} data 标签数据 + * @returns {Promise} + */ +export function addTag(data) { + return request({ + url: "/api/knowledge/tag/add", + method: "post", + data, + }); +} \ No newline at end of file diff --git a/pc/src/api/knowlege.js b/pc/src/api/knowlege.js index f7a5b94..d982468 100644 --- a/pc/src/api/knowlege.js +++ b/pc/src/api/knowlege.js @@ -1,70 +1,113 @@ import request from "@/utils/request"; -// 获取知识列表 +/** + * 获取知识列表 + * @param {Object} params 查询参数 + * @returns {Promise} + */ export function getKnowledgeList(params) { return request({ - url: `/api/knowledge/list`, + url: "/api/knowledge/list", method: "get", - params: params, + params, }); } -// 获取知识详情 + +/** + * 获取知识详情 + * @param {number|string} id 知识ID + * @returns {Promise} + */ export function getKnowledgeDetail(id) { return request({ url: `/api/knowledge/detail/${id}`, method: "get", }); } -// 创建知识 + +/** + * 创建知识 + * @param {Object} data 知识数据 + * @returns {Promise} + */ export function createKnowledge(data) { return request({ - url: `/api/knowledge/create`, + url: "/api/knowledge/create", method: "post", - data: data, + data, }); } -// 更新知识 + +/** + * 更新知识 + * @param {number|string} id 知识ID + * @param {Object} data 更新的数据 + * @returns {Promise} + */ export function updateKnowledge(id, data) { return request({ url: `/api/knowledge/update/${id}`, method: "put", - data: data, + data, }); } -// 删除知识 + +/** + * 删除知识 + * @param {number|string} id 知识ID + * @returns {Promise} + */ export function deleteKnowledge(id) { return request({ url: `/api/knowledge/delete/${id}`, method: "delete", }); } -// 获取分类列表 + +/** + * 获取分类列表 + * @returns {Promise} + */ export function getCategoryList() { return request({ - url: `/api/knowledge/category/list`, + url: "/api/knowledge/category/list", method: "get", }); } -// 获取标签列表 + +/** + * 获取标签列表 + * @returns {Promise} + */ export function getTagList() { return request({ - url: `/api/knowledge/tag/list`, + url: "/api/knowledge/tag/list", method: "get", }); } -// 添加分类 + +/** + * 添加分类 + * @param {Object} data 分类数据 + * @returns {Promise} + */ export function addCategory(data) { return request({ - url: `/api/knowledge/category/add`, + url: "/api/knowledge/category/add", method: "post", - data: data, + data, }); } -// 添加标签 + +/** + * 添加标签 + * @param {Object} data 标签数据 + * @returns {Promise} + */ export function addTag(data) { return request({ - url: `/api/knowledge/tag/add`, + url: "/api/knowledge/tag/add", method: "post", - data: data, + data, }); } \ No newline at end of file diff --git a/pc/src/api/login.js b/pc/src/api/login.js index 2437d67..bea2b56 100644 --- a/pc/src/api/login.js +++ b/pc/src/api/login.js @@ -1,13 +1,14 @@ import request from "@/utils/request"; -// 登录 -export function login(username, password) { +// 登录(使用租户名称) +export function login(username, password, tenantName) { return request({ url: `/api/login`, method: "post", data: { username: username, password: password, + tenant_name: tenantName, // 后端期望的字段名是 tenant_name }, }); } diff --git a/pc/src/api/menu.js b/pc/src/api/menu.js index a287587..8919d3b 100644 --- a/pc/src/api/menu.js +++ b/pc/src/api/menu.js @@ -8,3 +8,37 @@ export function getAllMenus() { }); } +// 更新菜单状态 +export function updateMenuStatus(menuId, status) { + return request({ + url: `/api/menu/status/${menuId}`, + method: "patch", + data: { status }, + }); +} + +// 创建菜单 +export function createMenu(menuData) { + return request({ + url: `/api/menu`, + method: "post", + data: menuData, + }); +} + +// 更新菜单 +export function updateMenu(menuId, menuData) { + return request({ + url: `/api/menu/${menuId}`, + method: "put", + data: menuData, + }); +} + +// 删除菜单 +export function deleteMenu(menuId) { + return request({ + url: `/api/menu/${menuId}`, + method: "delete", + }); +} diff --git a/pc/src/api/tenant.js b/pc/src/api/tenant.js new file mode 100644 index 0000000..9b46834 --- /dev/null +++ b/pc/src/api/tenant.js @@ -0,0 +1,55 @@ +import request from "@/utils/request"; + +//获取所有租户 +export function getAllTenants() { + return request({ + url: "/api/tenant/list", + method: "get", + }); +} + + +//新增租户 +export function createTenant(tenantData) { + return request({ + url: "/api/tenant", + method: "post", + data: tenantData, + }); +} + +// 编辑租户 +export function updateTenant(tenantId, tenantData) { + return request({ + url: `/api/tenant/${tenantId}`, + method: "put", + data: tenantData, + }); +} + +// 删除租户 +export function deleteTenant(tenantId) { + return request({ + url: `/api/tenant/${tenantId}`, + method: "delete", + }); +} + +// 审核租户 +export function auditTenant(tenantId, auditData) { + return request({ + url: `/api/tenant/${tenantId}/audit`, + method: "post", + data: auditData, + }); +} + +// 查看租户详情 +export function getTenantDetail(tenantId) { + return request({ + url: `/api/tenant/${tenantId}`, + method: "get", + }); +} + + diff --git a/pc/src/assets/less/style.less b/pc/src/assets/less/style.less index 16abb78..89ce311 100644 --- a/pc/src/assets/less/style.less +++ b/pc/src/assets/less/style.less @@ -54,6 +54,16 @@ --card-bg-color: #ffffff; --card-border-color: #ebeef5; + // container-box 亮色主题 + --container-box-bg: #fff; + --container-box-border: #ebeef5; + --container-box-shadow: 0 2px 8px 0 rgba(0,0,0,0.04); + + // 滚动条颜色 + --scrollbar-track-color: #f1f1f1; + --scrollbar-thumb-color: #c1c1c1; + --scrollbar-thumb-hover-color: #a8a8a8; + // Element Plus 组件主题适配 --el-bg-color: var(--bg-color); --el-bg-color-page: var(--bg-color-page); @@ -125,6 +135,16 @@ // 卡片背景 --card-bg-color: #252526; --card-border-color: #3c3c3d; + + // container-box 暗色主题 + --container-box-bg: #232325; + --container-box-border: #363637; + --container-box-shadow: 0 2px 8px 0 rgba(0,0,0,0.09); + + // 滚动条颜色 + --scrollbar-track-color: #2d2d2d; + --scrollbar-thumb-color: #555555; + --scrollbar-thumb-hover-color: #666666; // Element Plus 组件主题适配 --el-bg-color: var(--bg-color); @@ -150,6 +170,12 @@ body { transition: background-color 0.3s ease, color 0.3s ease; } +.header-bar { + display: flex; + align-items: center; + justify-content: space-between; +} + // 卡片样式 .el-card { background-color: var(--card-bg-color); @@ -157,6 +183,19 @@ body { color: var(--text-color-primary); } +// container-box样式 +.container-box { + background-color: var(--container-box-bg); + border: 1px solid var(--container-box-border); + box-shadow: var(--container-box-shadow); + border-radius: 8px; + padding: 24px; + transition: + background-color 0.3s, + border-color 0.3s, + box-shadow 0.3s; +} + // 输入框样式 .el-input__inner { background-color: var(--fill-color-blank); @@ -168,24 +207,427 @@ body { } } -// 按钮样式过渡 +// 按钮样式(全局主题适配) .el-button { transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; + + // 默认按钮 + &.el-button--default { + background-color: var(--fill-color-blank) !important; + border-color: var(--border-color) !important; + color: var(--text-color-primary) !important; + + &:hover:not(:disabled) { + background-color: var(--fill-color-light) !important; + border-color: var(--primary-color) !important; + color: var(--primary-color) !important; + } + + &:disabled { + background-color: var(--fill-color) !important; + border-color: var(--border-color-lighter) !important; + color: var(--text-color-disabled) !important; + } + } + + // 文本按钮 + &.el-button--text { + color: var(--text-color-primary) !important; + + &:hover:not(:disabled) { + background-color: var(--fill-color-light) !important; + color: var(--primary-color) !important; + } + + &:disabled { + color: var(--text-color-disabled) !important; + } + } } -// 表格样式 +// 表格样式(全局主题适配) .el-table { - background-color: var(--bg-color); - color: var(--text-color-primary); + background-color: var(--bg-color) !important; + color: var(--text-color-primary) !important; + border-color: var(--border-color-lighter) !important; - th { - background-color: var(--fill-color-light); - color: var(--text-color-primary); + // 表头 + th.el-table__cell { + background-color: var(--fill-color-light) !important; + color: var(--text-color-primary) !important; + border-bottom-color: var(--border-color) !important; } - td { - border-color: var(--border-color-lighter); + // 表格行 + tr { + background-color: var(--bg-color) !important; + + // 鼠标悬停 + &:hover > td { + background-color: var(--fill-color-lighter) !important; + } } + + // 表格单元格 + td.el-table__cell { + border-bottom-color: var(--border-color-lighter) !important; + color: var(--text-color-primary) !important; + } + + // 斑马纹表格 + &.el-table--striped { + .el-table__body tr.el-table__row--striped td { + background-color: var(--fill-color-extra-light) !important; + } + + .el-table__body tr.el-table__row--striped:hover td { + background-color: var(--fill-color-lighter) !important; + } + } + + // 边框表格 + &.el-table--border { + border-color: var(--border-color-lighter) !important; + + &::after, + &::before { + background-color: var(--border-color-lighter) !important; + } + + th.el-table__cell, + td.el-table__cell { + border-right-color: var(--border-color-lighter) !important; + } + } + + // 空状态 + .el-table__empty-block { + background-color: var(--bg-color) !important; + color: var(--text-color-secondary) !important; + } + + // 空状态文字 + .el-table__empty-text { + color: var(--text-color-secondary) !important; + } +} + +// 标签样式(全局主题适配) +.el-tag { + background-color: var(--fill-color-light) !important; + border-color: var(--border-color-lighter) !important; + color: var(--text-color-primary) !important; +} + +// 对话框样式(全局主题适配) +.el-dialog { + background-color: var(--bg-color) !important; + border-color: var(--border-color) !important; + + .el-dialog__header { + border-bottom-color: var(--border-color-lighter) !important; + color: var(--text-color-primary) !important; + } + + .el-dialog__title { + color: var(--text-color-primary) !important; + } + + .el-dialog__body { + background-color: var(--bg-color) !important; + color: var(--text-color-primary) !important; + } + + .el-dialog__footer { + border-top-color: var(--border-color-lighter) !important; + } +} + +// MessageBox 确认框样式(全局主题适配) +.el-overlay.is-message-box { + background-color: rgba(0, 0, 0, 0.5) !important; + + &[data-theme="dark"] { + background-color: rgba(0, 0, 0, 0.7) !important; + } +} + +.el-message-box { + background-color: var(--bg-color) !important; + + .el-message-box__header { + .el-message-box__title { + color: var(--text-color-primary) !important; + } + } + + .el-message-box__content { + color: var(--text-color-primary) !important; + + .el-message-box__message { + color: var(--text-color-primary) !important; + + p { + color: var(--text-color-primary) !important; + } + } + } +} + +// 描述列表样式(全局主题适配) +.el-descriptions { + .el-descriptions__header { + .el-descriptions__title { + color: var(--text-color-primary) !important; + } + } + + .el-descriptions__table { + .el-descriptions__label { + background-color: var(--fill-color-light) !important; + color: var(--text-color-primary) !important; + border-color: var(--border-color-lighter) !important; + } + + .el-descriptions__content { + background-color: var(--bg-color) !important; + color: var(--text-color-primary) !important; + border-color: var(--border-color-lighter) !important; + } + + td.el-descriptions__label, + td.el-descriptions__content { + border-color: var(--border-color-lighter) !important; + } + } +} + +// 表单样式(全局主题适配) +.el-form { + .el-form-item__label { + color: var(--text-color-primary) !important; + } + + .el-form-item__error { + color: var(--danger-color) !important; + } +} + +// 选择器样式(全局主题适配) +.el-select { + // Element Plus 2.x 使用 el-select__wrapper + .el-select__wrapper { + background-color: var(--fill-color-blank) !important; + border-color: var(--border-color) !important; + border: 1px solid var(--border-color) !important; + box-shadow: none !important; + + &:hover { + box-shadow: 0 0 0 1px var(--border-color) inset !important; + } + + &.is-focus { + box-shadow: 0 0 0 1px var(--primary-color) inset !important; + } + } + + // 如果选择器内部使用了 el-input__wrapper(兼容性处理) + .el-input__wrapper { + background-color: var(--fill-color-blank) !important; + border-color: var(--border-color) !important; + border: 1px solid var(--border-color) !important; + box-shadow: none !important; + + &:hover { + box-shadow: 0 0 0 1px var(--border-color) inset !important; + } + + &.is-focus { + box-shadow: 0 0 0 1px var(--primary-color) inset !important; + } + } + + .el-input__inner { + background-color: transparent !important; + color: var(--text-color-primary) !important; + } + + .el-input__suffix { + .el-input__suffix-inner { + .el-select__caret { + color: var(--text-color-secondary) !important; + } + } + } +} + +// 选择器下拉菜单样式(全局主题适配) +.el-select-dropdown, +.el-select-dropdown__wrap { + background-color: var(--bg-color-overlay) !important; + border-color: var(--border-color) !important; + + .el-select-dropdown__item { + color: var(--text-color-primary) !important; + + &:hover { + background-color: var(--fill-color-light) !important; + color: var(--text-color-primary) !important; + } + + &.selected { + color: var(--primary-color) !important; + font-weight: 700; + } + + &.is-disabled { + color: var(--text-color-disabled) !important; + + &:hover { + background-color: transparent !important; + } + } + } + + .el-select-dropdown__empty { + color: var(--text-color-secondary) !important; + } +} + +// Element Plus Popper 组件样式(全局主题适配,用于所有通过 Popper 渲染的下拉组件) +.el-popper, +.el-popper__arrow::before { + background-color: var(--bg-color-overlay) !important; + border-color: var(--border-color) !important; +} + +.el-popper.is-dark { + background-color: var(--bg-color-overlay) !important; + color: var(--text-color-primary) !important; +} + +// 输入框组件样式(全局主题适配,兼容 Element Plus 2.x) +.el-input { + .el-input__wrapper { + background-color: var(--fill-color-blank) !important; + border-color: var(--border-color) !important; + border: 1px solid var(--border-color) !important; + box-shadow: none !important; + + &:hover { + box-shadow: 0 0 0 1px var(--border-color) inset !important; + } + + &.is-focus { + box-shadow: 0 0 0 1px var(--primary-color) inset !important; + } + } + + .el-input__inner { + background-color: transparent !important; + color: var(--text-color-primary) !important; + + &::placeholder { + color: var(--text-color-placeholder) !important; + } + } +} + +// 文本域样式(全局主题适配) +.el-textarea { + .el-textarea__inner { + background-color: var(--fill-color-blank) !important; + border-color: var(--border-color) !important; + color: var(--text-color-primary) !important; + + &::placeholder { + color: var(--text-color-placeholder) !important; + } + + &:focus { + border-color: var(--primary-color) !important; + } + } +} + +// 单选按钮组样式(全局主题适配) +.el-radio-group { + .el-radio { + color: var(--text-color-primary) !important; + + .el-radio__label { + color: var(--text-color-primary) !important; + } + + &.is-checked { + .el-radio__label { + color: var(--primary-color) !important; + } + } + } +} + +// 数字输入框样式(全局主题适配) +.el-input-number { + .el-input__wrapper { + background-color: var(--fill-color-blank) !important; + border-color: var(--border-color) !important; + + .el-input__inner { + background-color: transparent !important; + color: var(--text-color-primary) !important; + } + } + + .el-input-number__decrease, + .el-input-number__increase { + background-color: var(--fill-color-light) !important; + border-color: var(--border-color) !important; + color: var(--text-color-primary) !important; + + &:hover:not(.is-disabled) { + background-color: var(--fill-color) !important; + color: var(--primary-color) !important; + } + + &.is-disabled { + background-color: var(--fill-color) !important; + color: var(--text-color-disabled) !important; + } + } +} + +// 开关组件样式(全局主题适配) +.el-switch { + --el-switch-on-color: var(--primary-color); + --el-switch-off-color: var(--fill-color-dark); + + .el-switch__core { + background-color: var(--fill-color-dark) !important; + border-color: var(--border-color) !important; + } + + &.is-checked { + .el-switch__core { + background-color: var(--primary-color) !important; + border-color: var(--primary-color) !important; + } + } + + &.is-disabled { + .el-switch__core { + background-color: var(--fill-color) !important; + border-color: var(--border-color-lighter) !important; + opacity: 0.6; + } + } +} + +// 警告提示样式(全局主题适配) +.el-alert { + background-color: var(--fill-color-light) !important; + border-color: var(--border-color-lighter) !important; + color: var(--text-color-primary) !important; } // 菜单样式 @@ -377,3 +819,295 @@ body { } } +// 滚动条样式(全局) +// WebKit 浏览器(Chrome, Safari, Edge) +* { + // Firefox 滚动条样式 + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color); + + // WebKit 滚动条样式 + &::-webkit-scrollbar { + width: 8px; + height: 8px; + } + + &::-webkit-scrollbar-track { + background: var(--scrollbar-track-color); + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb-color); + border-radius: 4px; + transition: background-color 0.3s ease; + + &:hover { + background: var(--scrollbar-thumb-hover-color); + } + } + + &::-webkit-scrollbar-corner { + background: var(--scrollbar-track-color); + } +} + +// 更细的滚动条样式(可选) +.thin-scrollbar { + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } +} + + +.pagination-bar { + display: flex; + justify-content: flex-end; + margin: 14px 0 0 0; +} +/* 标准文章样式 支持黑白主题 */ +.markdown-body { + color: var(--text-color, #222); + font-size: 16px; + line-height: 1.8; + word-break: break-word; + background: var(--content-bg); + transition: color 0.3s, background 0.3s; + + h1, + h2, + h3, + h4, + h5, + h6 { + color: var(--title-color, var(--text-color, #222)); + margin-top: 1.6em; + margin-bottom: 0.8em; + font-family: inherit; + font-weight: 700; + line-height: 1.3; + transition: color 0.3s; + } + h1 { + font-size: 2.2em; + border-bottom: 1px solid var(--border-color-light, #eaecef); + padding-bottom: 0.4em; + } + h2 { + font-size: 1.8em; + border-bottom: 1px solid var(--border-color-light, #eaecef); + padding-bottom: 0.3em; + } + h3 { + font-size: 1.4em; + } + h4 { + font-size: 1.2em; + } + h5 { + font-size: 1.06em; + } + h6 { + font-size: 1em; + color: var(--text-color-secondary, #969696); + } + + p { + margin: 0.9em 0; + color: inherit; + } + + ul, + ol { + padding-left: 2em; + margin: 0.9em 0; + } + ul { + list-style-type: disc; + } + ol { + list-style-type: decimal; + } + li { + margin: 0.7em 0 0.7em 0.7em; + color: inherit; + } + + a { + color: var(--primary-color, #2d8cf0); + text-decoration: underline; + transition: color 0.2s; + &:hover { + color: var(--primary-hover, #1a73e8); + } + } + img { + max-width: 100%; + height: auto; + display: block; + margin: 1em auto; + border-radius: 4px; + background: var(--img-bg, #f7f7fa); + } + + blockquote { + border-left: 4px solid var(--blockquote-border, #d0dde9); + background: var(--blockquote-bg, #f7f9fa); + padding: 0.6em 1.2em; + margin: 1.1em 0; + color: var(--blockquote-color, #6580a0); + font-style: italic; + transition: background 0.3s, border-color 0.3s, color 0.3s; + } + + code { + font-family: var( + --code-font, + "Fira Mono", + "Menlo", + "Consolas", + "monospace" + ); + background: var(--code-bg, #f4f4f4); + border-radius: 3px; + padding: 0.15em 0.4em; + color: var(--code-color, #c7254e); + font-size: 0.97em; + margin: 0 0.1em; + transition: background 0.3s, color 0.3s; + } + + pre { + background: var(--pre-bg, #f6f8fa); + border-radius: 4px; + padding: 1em 1.2em; + font-family: var( + --code-font, + "Fira Mono", + "Menlo", + "Consolas", + "monospace" + ); + font-size: 0.98em; + overflow-x: auto; + color: var(--pre-color, #212529); + margin: 1.2em 0; + transition: background 0.3s, color 0.3s; + code { + background: none; + color: inherit; + font-size: inherit; + padding: 0; + } + } + + table { + width: 100%; + border-collapse: collapse; + margin: 1.2em 0; + color: inherit; + font-size: 1em; + background: var(--table-bg, #fff); + transition: background 0.3s; + } + th, + td { + border: 1px solid var(--table-border, #dee2e6); + padding: 0.5em 1em; + color: inherit; + } + th { + background: var(--table-head-bg, #f4f8fb); + font-weight: 600; + } + tr:nth-child(even) { + background: var(--table-row-even-bg, #f9fbfc); + } + + hr { + border: none; + border-top: 1px solid var(--border-color-light, #eaecef); + margin: 2em 0; + } + + br { + display: block; + margin: 0.2em 0; + content: ""; + } + + // 兼容 Element 表单 + .el-form-item__label, + .el-form-item__content, + .el-form-item__error, + .el-form-item__error-tip { + color: var(--text-color, #222); + } +} + +/* 深色主题支持 */ +.dark .markdown-body { + color: #fff; + background: #181b21; + + h1, + h2, + h3, + h4, + h5, + h6 { + color: #fff; + } + h6 { + color: #ccc; + } + a { + color: #fff; + &:hover { + color: #e6e6e6; + } + } + img { + background: #23272b; + } + blockquote { + border-left-color: #5d7896; + background: #232a33; + color: #cce3fa; + } + code { + background: #31343b; + color: #fff; + } + pre { + background: #23272d; + color: #fff; + } + table { + background: #23272b; + } + th, + td { + border-color: #333b47; + color: #fff; + } + th { + background: #273041; + } + tr:nth-child(even) { + background: #20232a; + } + hr { + border-top-color: #333b47; + } + // Element兼容 + .el-form-item__label, + .el-form-item__content, + .el-form-item__error, + .el-form-item__error-tip { + color: #fff; + } +} diff --git a/pc/src/components/CommonHeader.vue b/pc/src/components/CommonHeader.vue index 5620889..4ce5a32 100644 --- a/pc/src/components/CommonHeader.vue +++ b/pc/src/components/CommonHeader.vue @@ -91,6 +91,9 @@ const handleCommand = (command) => { // 清除缓存中的user数据 localStorage.removeItem('user'); sessionStorage.removeItem('user'); + //清除租户数据 + localStorage.removeItem('tenant'); + sessionStorage.removeItem('tenant'); router.push('/login'); } }; diff --git a/pc/src/main.js b/pc/src/main.js index c9e1f50..fab0ef9 100644 --- a/pc/src/main.js +++ b/pc/src/main.js @@ -8,9 +8,13 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue' import { createPinia } from 'pinia' import { useAuthStore } from './stores/auth' import { initTheme } from './utils/theme' +// 导入全局组件 +import WangEditor from '@/views/components/WangEditor.vue'; const app = createApp(App) const pinia = createPinia() +// 全局注册 WangEditor 组件 +app.component('WangEditor', WangEditor); for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) @@ -29,7 +33,7 @@ authStore.checkAuth() // 如果用户已登录,在应用启动时加载动态路由 if (authStore.isLoggedIn) { loadAndAddDynamicRoutes().then(() => { - console.log('应用启动时已加载动态路由'); + // console.log('应用启动时已加载动态路由'); }).catch(err => { console.error('应用启动时加载动态路由失败:', err); }); diff --git a/pc/src/router/index.js b/pc/src/router/index.js index bf3bdc3..6e95ba2 100644 --- a/pc/src/router/index.js +++ b/pc/src/router/index.js @@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from "vue-router"; import { convertMenusToRoutes } from "./dynamicRoutes"; // 静态路由:登录页独立,404 页面独立 +// 仪表盘路由作为静态路由,永远放在第一个位置 const staticRoutes = [ { path: "/login", @@ -13,8 +14,22 @@ const staticRoutes = [ path: "/", name: "Main", component: () => import("@/views/Main.vue"), - redirect: "/dashboard", // 默认重定向,动态路由加载后会更新 - children: [], // 所有页面路由都会作为 children 添加到这里 + redirect: "/dashboard", // 默认重定向到仪表盘 + children: [ + // 仪表盘路由:静态路由,永远放在第一个 + { + path: "dashboard", + name: "Dashboard", + component: () => import("@/views/dashboard/index.vue"), + meta: { + requiresAuth: true, + title: "仪表盘", + icon: "fa-solid fa-gauge", + id: 1, + isStatic: true // 标记为静态路由 + } + } + ], meta: { requiresAuth: true } }, { @@ -76,15 +91,18 @@ function addDynamicRoutes(menus) { return; } - const dynamicRoutes = convertMenusToRoutes(menus); + // 过滤掉 ID 为 1 的仪表盘菜单,因为它是静态路由 + const filteredMenus = menus.filter(menu => menu.id !== 1); + + const dynamicRoutes = convertMenusToRoutes(filteredMenus); // 打印路由树结构(只打印路径信息,不序列化组件) - console.log('生成的路由树:', dynamicRoutes.map(r => ({ - path: r.path, - name: r.name, - hasComponent: !!r.component, - childrenCount: r.children?.length || 0 - }))); + // console.log('生成的路由树:', dynamicRoutes.map(r => ({ + // path: r.path, + // name: r.name, + // hasComponent: !!r.component, + // childrenCount: r.children?.length || 0 + // }))); // 获取主路由 const mainRoute = router.getRoutes().find(r => r.name === 'Main'); @@ -93,45 +111,71 @@ function addDynamicRoutes(menus) { return; } + // 获取现有的静态路由(仪表盘路由) + const staticChildren = mainRoute.children || []; + const dashboardRoute = staticChildren.find(r => r.name === 'Dashboard'); + // 移除旧的主路由 router.removeRoute('Main'); - // 查找第一个有效路由作为默认重定向 - const firstRoute = findFirstValidRoute(dynamicRoutes); - let redirectPath = "/dashboard"; // 默认值 - if (firstRoute && firstRoute.meta?.menuPath) { - redirectPath = firstRoute.meta.menuPath; - } - - // 重新添加主路由,包含所有动态子路由 + // 重新添加主路由,仪表盘路由放在第一个,后面跟动态路由 router.addRoute({ ...mainRoute, - redirect: redirectPath, - children: dynamicRoutes // 所有动态路由作为 Main 的 children + redirect: "/dashboard", // 始终重定向到仪表盘 + children: [ + // 仪表盘静态路由永远放在第一个 + ...(dashboardRoute ? [dashboardRoute] : []), + // 动态路由跟在后面 + ...dynamicRoutes + ] + }); + + // 添加知识库的子路由(详情页和编辑页) + // 直接在 Main 路由下添加完整路径的子路由 + router.addRoute('Main', { + path: 'apps/knowledge/detail/:id', + name: 'apps-knowledge-detail', + component: () => import('@/views/apps/knowledge/components/detail.vue'), + meta: { + requiresAuth: true, + title: '知识详情' + } + }); + router.addRoute('Main', { + path: 'apps/knowledge/edit/:id', + name: 'apps-knowledge-edit', + component: () => import('@/views/apps/knowledge/components/edit.vue'), + meta: { + requiresAuth: true, + title: '编辑知识' + } }); dynamicRoutesAdded = true; // 打印路由信息用于调试 const finalMainRoute = router.getRoutes().find(r => r.name === 'Main'); - console.log('动态路由已添加:', { - redirect: redirectPath, - childrenCount: dynamicRoutes.length, - childrenPaths: finalMainRoute?.children?.map(c => ({ - path: c.path, - name: c.name, - metaPath: c.meta?.menuPath - })) - }); + // console.log('动态路由已添加:', { + // redirect: "/dashboard", + // staticRoutesCount: dashboardRoute ? 1 : 0, + // dynamicRoutesCount: dynamicRoutes.length, + // totalChildrenCount: (dashboardRoute ? 1 : 0) + dynamicRoutes.length, + // childrenPaths: finalMainRoute?.children?.map(c => ({ + // path: c.path, + // name: c.name, + // isStatic: c.meta?.isStatic || false, + // metaPath: c.meta?.menuPath + // })) + // }); // 测试路由解析(使用完整路径) const testResolve = router.resolve('/dashboard'); - console.log('测试路由解析 /dashboard:', { - matched: testResolve.matched.map(m => ({ name: m.name, path: m.path })), - fullPath: testResolve.fullPath, - name: testResolve.name, - hasMatched: testResolve.matched.length > 0 - }); + // console.log('测试路由解析 /dashboard:', { + // matched: testResolve.matched.map(m => ({ name: m.name, path: m.path })), + // fullPath: testResolve.fullPath, + // name: testResolve.name, + // hasMatched: testResolve.matched.length > 0 + // }); } // 查找第一个有效的路由(有组件的路由) diff --git a/pc/src/utils/request.js b/pc/src/utils/request.js index 27e1055..f93ac4a 100644 --- a/pc/src/utils/request.js +++ b/pc/src/utils/request.js @@ -13,6 +13,12 @@ service.interceptors.request.use( if (token) { config.headers['Authorization'] = `Bearer ${token}`; } + // 对于有 body 的请求(POST、PUT、PATCH),确保设置 Content-Type + if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) { + if (!config.headers['Content-Type'] && !config.headers['content-type']) { + config.headers['Content-Type'] = 'application/json'; + } + } return config; }, error => { diff --git a/pc/src/views/apps/knowledge/components/detail.vue b/pc/src/views/apps/knowledge/components/detail.vue index 29c7ab3..2bafd4d 100644 --- a/pc/src/views/apps/knowledge/components/detail.vue +++ b/pc/src/views/apps/knowledge/components/detail.vue @@ -61,6 +61,7 @@
正文内容
+
@@ -123,7 +124,8 @@ async function fetchDetail() { try { const idValue = id.value as string | number const res = await getKnowledgeDetail(idValue) - const data = res.data + // API 返回的数据结构: { code: 0, data: {...}, message: "success" } + const data = (res.code === 0 && res.data) ? res.data : (res.data || res) formData.value = { title: data.title || '', category: data.categoryName || '', @@ -133,6 +135,9 @@ async function fetchDetail() { updateTime: data.updateTime || '', content: data.content || '', } + if (data.title) { + knowledgeTitle.value = data.title + } } catch (e) { ElMessage.error("获取详情失败") } @@ -143,10 +148,7 @@ function goBack() { router.push("/apps/knowledge") } function handleEdit() { - router.push({ - name: "apps-knowledge-edit", - params: { id: id.value as string } - }) + router.push(`/apps/knowledge/edit/${id.value}`) } function handleDelete() { ElMessageBox.confirm( @@ -160,77 +162,196 @@ function handleDelete() { ).then(async () => { try { const idValue = id.value as string | number - await deleteKnowledge(idValue) - ElMessage.success("删除成功") - goBack() - } catch (e) { - ElMessage.error("删除失败") + const res = await deleteKnowledge(idValue) + // API 返回的数据结构: { code: 0, message: "删除成功", data: null } + if (res.code === 0) { + ElMessage.success("删除成功") + goBack() + } else { + ElMessage.error(res.message || "删除失败") + } + } catch (e: any) { + ElMessage.error(e.message || "删除失败") } }) } - diff --git a/pc/src/views/apps/knowledge/index.vue b/pc/src/views/apps/knowledge/index.vue index 25e296e..325b7b8 100644 --- a/pc/src/views/apps/knowledge/index.vue +++ b/pc/src/views/apps/knowledge/index.vue @@ -8,15 +8,6 @@
- - - { + + + diff --git a/pc/src/views/login/index.vue b/pc/src/views/login/index.vue index 84a7708..3e94bb7 100644 --- a/pc/src/views/login/index.vue +++ b/pc/src/views/login/index.vue @@ -1,5 +1,5 @@ diff --git a/pc/src/views/system/files/categories.vue b/pc/src/views/system/files/categories.vue new file mode 100644 index 0000000..84ef016 --- /dev/null +++ b/pc/src/views/system/files/categories.vue @@ -0,0 +1,472 @@ + + + + + \ No newline at end of file diff --git a/pc/src/views/system/files/index.vue b/pc/src/views/system/files/index.vue new file mode 100644 index 0000000..cd4c346 --- /dev/null +++ b/pc/src/views/system/files/index.vue @@ -0,0 +1,673 @@ + + + + + diff --git a/pc/src/views/system/files/upload.vue b/pc/src/views/system/files/upload.vue new file mode 100644 index 0000000..bfc033a --- /dev/null +++ b/pc/src/views/system/files/upload.vue @@ -0,0 +1,369 @@ + + + + + \ No newline at end of file diff --git a/pc/src/views/system/menus/manager.vue b/pc/src/views/system/menus/manager.vue new file mode 100644 index 0000000..71c44af --- /dev/null +++ b/pc/src/views/system/menus/manager.vue @@ -0,0 +1,548 @@ + + + + + diff --git a/pc/src/views/system/permissions/index.vue b/pc/src/views/system/permissions/index.vue new file mode 100644 index 0000000..a85aacc --- /dev/null +++ b/pc/src/views/system/permissions/index.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/pc/src/views/system/programs/index.vue b/pc/src/views/system/programs/index.vue new file mode 100644 index 0000000..2f51fc2 --- /dev/null +++ b/pc/src/views/system/programs/index.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/pc/src/views/system/roles/index.vue b/pc/src/views/system/roles/index.vue new file mode 100644 index 0000000..b3f9835 --- /dev/null +++ b/pc/src/views/system/roles/index.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/pc/src/views/system/tenant/components/audit.vue b/pc/src/views/system/tenant/components/audit.vue new file mode 100644 index 0000000..e69de29 diff --git a/pc/src/views/system/tenant/components/detail.vue b/pc/src/views/system/tenant/components/detail.vue new file mode 100644 index 0000000..e69de29 diff --git a/pc/src/views/system/tenant/components/edit.vue b/pc/src/views/system/tenant/components/edit.vue new file mode 100644 index 0000000..e69de29 diff --git a/pc/src/views/system/tenant/index.vue b/pc/src/views/system/tenant/index.vue new file mode 100644 index 0000000..2accfee --- /dev/null +++ b/pc/src/views/system/tenant/index.vue @@ -0,0 +1,646 @@ + + + + + diff --git a/pc/src/views/system/users/index.vue b/pc/src/views/system/users/index.vue new file mode 100644 index 0000000..5a4f18a --- /dev/null +++ b/pc/src/views/system/users/index.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/server/controllers/auth.go b/server/controllers/auth.go index 758f17a..4f520ed 100644 --- a/server/controllers/auth.go +++ b/server/controllers/auth.go @@ -16,19 +16,21 @@ type AuthController struct { beego.Controller } -// Login 处理登录请求 +// Login 处理登录请求(支持租户模式,使用租户名称) func (c *AuthController) Login() { - var username, password string + var username, password, tenantName string // 优先尝试从URL参数获取(Apifox测试方式) username = c.GetString("username") password = c.GetString("password") + tenantName = c.GetString("tenant_name") // 如果URL参数为空,尝试从JSON请求体获取(前端方式) - if username == "" || password == "" { + if username == "" || password == "" || tenantName == "" { var loginData struct { - Username string `json:"username"` - Password string `json:"password"` + Username string `json:"username"` + Password string `json:"password"` + TenantName string `json:"tenant_name"` } err := json.Unmarshal(c.Ctx.Input.RequestBody, &loginData) @@ -44,31 +46,43 @@ func (c *AuthController) Login() { username = loginData.Username password = loginData.Password + tenantName = loginData.TenantName + } + + // 验证参数 + if tenantName == "" { + c.Data["json"] = map[string]interface{}{ + "code": 1, + "message": "租户名称不能为空", + "data": nil, + } + c.ServeJSON() + return } // 添加日志调试 fmt.Println("接收到的登录请求:") fmt.Println("用户名:", username) - fmt.Println("密码:", password) + fmt.Println("租户名称:", tenantName) - // 验证用户 - fmt.Println("开始验证用户:", username) - user, err := models.ValidateUser(username, password) + // 验证用户(先验证租户,再验证租户下的用户) + fmt.Println("开始验证用户:", username, "租户:", tenantName) + user, err := models.ValidateUser(username, password, tenantName) fmt.Println("验证结果:", err) if user != nil { - fmt.Println("用户信息:ID=", user.Id, "Username=", user.Username, "Salt=", user.Salt) + fmt.Println("用户信息:ID=", user.Id, "Username=", user.Username, "TenantId=", user.TenantId) } if err != nil { // 登录失败 c.Data["json"] = map[string]interface{}{ "code": 1, - "message": "用户名或密码错误", + "message": err.Error(), "data": nil, } } else { - // 使用models包中的GenerateToken函数生成token - tokenString, err := models.GenerateToken(user.Id, user.Username) + // 使用models包中的GenerateToken函数生成token(包含租户ID) + tokenString, err := models.GenerateToken(user.Id, user.Username, user.TenantId) if err != nil { c.Data["json"] = map[string]interface{}{ @@ -85,11 +99,12 @@ func (c *AuthController) Login() { "accessToken": tokenString, "token": tokenString, // 兼容性 "user": map[string]interface{}{ - "id": user.Id, - "username": user.Username, - "email": user.Email, - "avatar": user.Avatar, - "nickname": user.Nickname, + "id": user.Id, + "username": user.Username, + "email": user.Email, + "avatar": user.Avatar, + "nickname": user.Nickname, + "tenant_id": user.TenantId, }, }, } @@ -99,14 +114,35 @@ func (c *AuthController) Login() { c.ServeJSON() } -// ResetPassword 重置用户密码 +// ResetPassword 重置用户密码(支持租户模式) func (c *AuthController) ResetPassword() { // 获取请求参数 username := c.GetString("username") superPassword := c.GetString("superPassword") + tenantId, _ := c.GetInt("tenant_id", 0) + + // 如果URL参数中没有租户ID,尝试从JSON请求体获取 + if tenantId == 0 { + var resetData struct { + Username string `json:"username"` + SuperPassword string `json:"superPassword"` + TenantId int `json:"tenant_id"` + } + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &resetData); err == nil { + username = resetData.Username + superPassword = resetData.SuperPassword + tenantId = resetData.TenantId + } + } + + if tenantId <= 0 { + c.Data["json"] = map[string]interface{}{"success": false, "message": "租户ID不能为空"} + c.ServeJSON() + return + } // 调用模型方法 - err := models.ResetPassword(username, superPassword) + err := models.ResetPassword(username, superPassword, tenantId) if err != nil { c.Data["json"] = map[string]interface{}{"success": false, "message": err.Error()} @@ -117,15 +153,38 @@ func (c *AuthController) ResetPassword() { c.ServeJSON() } -// ChangePassword 修改用户密码 +// ChangePassword 修改用户密码(支持租户模式) func (c *AuthController) ChangePassword() { // 获取请求参数 username := c.GetString("username") oldPassword := c.GetString("oldPassword") newPassword := c.GetString("newPassword") + tenantId, _ := c.GetInt("tenant_id", 0) + + // 如果URL参数中没有租户ID,尝试从JSON请求体获取 + if tenantId == 0 { + var changeData struct { + Username string `json:"username"` + OldPassword string `json:"oldPassword"` + NewPassword string `json:"newPassword"` + TenantId int `json:"tenant_id"` + } + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &changeData); err == nil { + username = changeData.Username + oldPassword = changeData.OldPassword + newPassword = changeData.NewPassword + tenantId = changeData.TenantId + } + } + + if tenantId <= 0 { + c.Data["json"] = map[string]interface{}{"success": false, "message": "租户ID不能为空"} + c.ServeJSON() + return + } // 调用模型方法 - err := models.ChangePassword(username, oldPassword, newPassword) + err := models.ChangePassword(username, oldPassword, newPassword, tenantId) if err != nil { c.Data["json"] = map[string]interface{}{"success": false, "message": err.Error()} } else { @@ -144,9 +203,11 @@ func (c *AuthController) Logout() { c.ServeJSON() } -// FindAllUsers 获取所有用户 +// FindAllUsers 获取所有用户(支持按租户过滤) func (c *AuthController) FindAllUsers() { - users := models.FindAllUsers() + // 从查询参数获取租户ID(可选) + tenantId, _ := c.GetInt("tenant_id", 0) + users := models.FindAllUsers(tenantId) c.Data["json"] = map[string]interface{}{ "success": true, "message": "获取用户列表成功", @@ -155,10 +216,12 @@ func (c *AuthController) FindAllUsers() { c.ServeJSON() } -// GetUserByUsername 通过用户名查询用户信息 +// GetUserByUsername 通过用户名查询用户信息(支持租户模式) func (c *AuthController) GetUserByUsername() { - // 获取请求参数中的用户名 + // 获取请求参数中的用户名和租户ID username := c.GetString("username") + tenantId, _ := c.GetInt("tenant_id", 0) + if username == "" { c.Data["json"] = map[string]interface{}{ "code": 1, @@ -169,8 +232,18 @@ func (c *AuthController) GetUserByUsername() { return } + if tenantId <= 0 { + c.Data["json"] = map[string]interface{}{ + "code": 1, + "message": "租户ID不能为空", + "data": nil, + } + c.ServeJSON() + return + } + // 调用模型层方法查询用户 - user, err := models.GetUserByUsername(username) // 假设models层有这个方法 + user, err := models.GetUserByUsername(username, tenantId) if err != nil { c.Data["json"] = map[string]interface{}{ "code": 1, @@ -188,19 +261,19 @@ func (c *AuthController) GetUserByUsername() { "code": 0, "message": "查询成功", "data": map[string]interface{}{ - "id": user.Id, - "username": user.Username, - "email": user.Email, - "avatar": user.Avatar, - "nickname": user.Nickname, - // 其他需要返回的用户字段 + "id": user.Id, + "username": user.Username, + "email": user.Email, + "avatar": user.Avatar, + "nickname": user.Nickname, + "tenant_id": user.TenantId, }, } } c.ServeJSON() } -// AddUser 添加新用户 +// AddUser 添加新用户(支持租户模式) func (c *AuthController) AddUser() { // 定义接收用户数据的结构体(与JSON请求体对应) var userData struct { @@ -209,6 +282,7 @@ func (c *AuthController) AddUser() { Email string `json:"email"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` + TenantId int `json:"tenant_id"` } // 解析请求体JSON数据 @@ -242,6 +316,15 @@ func (c *AuthController) AddUser() { c.ServeJSON() return } + if userData.TenantId <= 0 { + c.Data["json"] = map[string]interface{}{ + "code": 1, + "message": "租户ID不能为空", + "data": nil, + } + c.ServeJSON() + return + } // 调用模型层方法添加用户(传递参数,接收新用户对象) newUser, err := models.AddUser( @@ -250,6 +333,7 @@ func (c *AuthController) AddUser() { userData.Email, userData.Nickname, userData.Avatar, + userData.TenantId, ) if err != nil { c.Data["json"] = map[string]interface{}{ @@ -262,26 +346,28 @@ func (c *AuthController) AddUser() { "code": 0, "message": "用户添加成功", "data": map[string]interface{}{ - "id": newUser.Id, - "username": newUser.Username, - "email": newUser.Email, - "nickname": newUser.Nickname, - "avatar": newUser.Avatar, + "id": newUser.Id, + "username": newUser.Username, + "email": newUser.Email, + "nickname": newUser.Nickname, + "avatar": newUser.Avatar, + "tenant_id": newUser.TenantId, }, } } c.ServeJSON() } -// UpdateUser 更新用户信息 +// UpdateUser 更新用户信息(支持租户模式) func (c *AuthController) UpdateUser() { // 定义接收更新数据的结构体 var updateData struct { - Id int `json:"id"` // 必须包含用户ID,用于定位要更新的用户 - Username string `json:"username"` // 可选更新字段 - Email string `json:"email"` // 可选更新字段 - Nickname string `json:"nickname"` // 可选更新字段 - Avatar string `json:"avatar"` // 可选更新字段 + Id int `json:"id"` // 必须包含用户ID,用于定位要更新的用户 + Username string `json:"username"` // 可选更新字段 + Email string `json:"email"` // 可选更新字段 + Nickname string `json:"nickname"` // 可选更新字段 + Avatar string `json:"avatar"` // 可选更新字段 + TenantId int `json:"tenant_id"` // 必须包含租户ID,用于验证用户归属 } // 解析请求体JSON @@ -296,7 +382,7 @@ func (c *AuthController) UpdateUser() { return } - // 校验必要参数(用户ID不能为空) + // 校验必要参数(用户ID和租户ID不能为空) if updateData.Id == 0 { c.Data["json"] = map[string]interface{}{ "code": 1, @@ -306,6 +392,15 @@ func (c *AuthController) UpdateUser() { c.ServeJSON() return } + if updateData.TenantId <= 0 { + c.Data["json"] = map[string]interface{}{ + "code": 1, + "message": "租户ID不能为空", + "data": nil, + } + c.ServeJSON() + return + } // 调用模型层方法更新用户 updatedUser, err := models.UpdateUser( @@ -314,6 +409,7 @@ func (c *AuthController) UpdateUser() { updateData.Email, updateData.Nickname, updateData.Avatar, + updateData.TenantId, ) if err != nil { c.Data["json"] = map[string]interface{}{ @@ -326,39 +422,44 @@ func (c *AuthController) UpdateUser() { "code": 0, "message": "用户更新成功", "data": map[string]interface{}{ - "id": updatedUser.Id, - "username": updatedUser.Username, - "email": updatedUser.Email, - "nickname": updatedUser.Nickname, - "avatar": updatedUser.Avatar, + "id": updatedUser.Id, + "username": updatedUser.Username, + "email": updatedUser.Email, + "nickname": updatedUser.Nickname, + "avatar": updatedUser.Avatar, + "tenant_id": updatedUser.TenantId, }, } } c.ServeJSON() } -// DeleteUser 删除用户 +// DeleteUser 删除用户(支持租户模式) func (c *AuthController) DeleteUser() { - // 获取要删除的用户ID(从URL参数或请求体中获取) + // 获取要删除的用户ID和租户ID(从URL参数或请求体中获取) userId, err := c.GetInt("id") // 从URL参数获取,如 /user?id=1 - if err != nil { + tenantId, _ := c.GetInt("tenant_id", 0) + + if err != nil || tenantId == 0 { // 若URL参数获取失败,尝试从JSON请求体获取 var deleteData struct { - Id int `json:"id"` + Id int `json:"id"` + TenantId int `json:"tenant_id"` } if err := json.Unmarshal(c.Ctx.Input.RequestBody, &deleteData); err != nil { c.Data["json"] = map[string]interface{}{ "code": 1, - "message": "用户ID获取失败: " + err.Error(), + "message": "用户ID或租户ID获取失败: " + err.Error(), "data": nil, } c.ServeJSON() return } userId = deleteData.Id + tenantId = deleteData.TenantId } - // 校验用户ID + // 校验用户ID和租户ID if userId <= 0 { c.Data["json"] = map[string]interface{}{ "code": 1, @@ -368,9 +469,18 @@ func (c *AuthController) DeleteUser() { c.ServeJSON() return } + if tenantId <= 0 { + c.Data["json"] = map[string]interface{}{ + "code": 1, + "message": "租户ID不能为空", + "data": nil, + } + c.ServeJSON() + return + } // 调用模型层方法删除用户 - err = models.DeleteUser(userId) + err = models.DeleteUser(userId, tenantId) if err != nil { c.Data["json"] = map[string]interface{}{ "code": 1, diff --git a/server/controllers/menu.go b/server/controllers/menu.go index c92a77b..f1975a3 100644 --- a/server/controllers/menu.go +++ b/server/controllers/menu.go @@ -4,6 +4,7 @@ import ( "encoding/json" "server/models" + "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) @@ -111,6 +112,77 @@ func (c *MenuController) UpdateMenu() { c.ServeJSON() } +// UpdateMenuStatus 更新菜单状态 +func (c *MenuController) UpdateMenuStatus() { + id, err := c.GetInt(":id") + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "参数错误", + } + c.ServeJSON() + return + } + + // 解析请求体获取 status 字段 + var req struct { + Status int8 `json:"status"` + } + + // 尝试从请求体获取数据 + requestBody := c.Ctx.Input.RequestBody + if len(requestBody) > 0 { + // 有请求体,从 JSON 解析 + if err := json.Unmarshal(requestBody, &req); err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "请求参数错误", + } + c.ServeJSON() + return + } + } else { + // 如果请求体为空,尝试从表单参数或查询参数获取 + statusValue, err := c.GetInt("status", -1) + if err != nil || statusValue == -1 { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "请求参数错误:缺少 status 参数", + } + c.ServeJSON() + return + } + req.Status = int8(statusValue) + } + + // 先查询该菜单,确保存在 + o := orm.NewOrm() + menu := models.Menu{Id: id} + if err = o.Read(&menu); err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "菜单不存在", + } + c.ServeJSON() + return + } + + // 更新状态 + menu.Status = req.Status + if _, err := o.Update(&menu, "Status"); err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "更新菜单状态失败", + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "菜单状态更新成功", + } + } + c.ServeJSON() +} + // DeleteMenu 删除菜单 func (c *MenuController) DeleteMenu() { id, err := c.GetInt(":id") diff --git a/server/controllers/tenant.go b/server/controllers/tenant.go new file mode 100644 index 0000000..b612d31 --- /dev/null +++ b/server/controllers/tenant.go @@ -0,0 +1,208 @@ +package controllers + +import ( + "encoding/json" + "strconv" + + "server/models" + + "github.com/beego/beego/v2/server/web" +) + +type TenantController struct { + web.Controller +} + +// GetAllTenants 获取所有租户 +func (c *TenantController) GetAllTenants() { + tenants, err := models.GetTenantList() + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "获取租户列表失败: " + err.Error(), + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "获取租户列表成功", + "data": tenants, + } + } + c.ServeJSON() +} + +// CreateTenant 新增租户 +func (c *TenantController) CreateTenant() { + var tenant models.Tenant + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tenant); err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "解析租户数据失败: " + err.Error(), + } + c.ServeJSON() + return + } + err := models.CreateTenant(&tenant) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "创建租户失败: " + err.Error(), + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "租户创建成功", + "data": tenant, + } + } + c.ServeJSON() +} + +// UpdateTenant 编辑租户 +func (c *TenantController) UpdateTenant() { + idStr := c.Ctx.Input.Param(":id") + id, err := strconv.Atoi(idStr) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "无效的租户ID", + } + c.ServeJSON() + return + } + var data map[string]interface{} + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &data); err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "解析更新数据失败: " + err.Error(), + } + c.ServeJSON() + return + } + err = models.UpdateTenant(id, data) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "编辑租户失败: " + err.Error(), + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "租户编辑成功", + } + } + c.ServeJSON() +} + +// DeleteTenant 删除租户 +func (c *TenantController) DeleteTenant() { + idStr := c.Ctx.Input.Param(":id") + id, err := strconv.Atoi(idStr) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "无效的租户ID", + } + c.ServeJSON() + return + } + err = models.DeleteTenant(id) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "删除租户失败: " + err.Error(), + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "租户删除成功", + } + } + c.ServeJSON() +} + +// AuditTenant 审核租户 +func (c *TenantController) AuditTenant() { + idStr := c.Ctx.Input.Param(":id") + id, err := strconv.Atoi(idStr) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "无效的租户ID", + } + c.ServeJSON() + return + } + + // 解析 JSON 请求体 + var auditData struct { + AuditStatus string `json:"audit_status"` + AuditComment string `json:"audit_comment"` + AuditBy string `json:"audit_by"` + } + + if err := json.Unmarshal(c.Ctx.Input.RequestBody, &auditData); err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "解析审核数据失败: " + err.Error(), + } + c.ServeJSON() + return + } + + if auditData.AuditStatus == "" { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "审核状态不能为空", + } + c.ServeJSON() + return + } + + err = models.AuditTenant(id, auditData.AuditStatus, auditData.AuditComment, auditData.AuditBy) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "审核租户失败: " + err.Error(), + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "审核成功", + } + } + c.ServeJSON() +} + +// GetTenantDetail 查看租户详情 +func (c *TenantController) GetTenantDetail() { + idStr := c.Ctx.Input.Param(":id") + id, err := strconv.Atoi(idStr) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "无效的租户ID", + } + c.ServeJSON() + return + } + tenant, err := models.GetTenantById(id) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "获取租户详情失败: " + err.Error(), + } + } else if tenant == nil { + c.Data["json"] = map[string]interface{}{ + "success": false, + "message": "租户不存在", + } + } else { + c.Data["json"] = map[string]interface{}{ + "success": true, + "message": "获取租户详情成功", + "data": tenant, + } + } + c.ServeJSON() +} diff --git a/server/database/README_TENANTS.md b/server/database/README_TENANTS.md new file mode 100644 index 0000000..68ef5b8 --- /dev/null +++ b/server/database/README_TENANTS.md @@ -0,0 +1,119 @@ +# 租户表数据库创建说明 + +## 创建步骤 + +### 方法1:执行独立的 SQL 文件(推荐) +```bash +# 在 MySQL 中执行 +mysql -u root -p your_database < server/database/yz_tenants.sql +``` + +### 方法2:手动执行 SQL +```sql +-- 1. 进入 MySQL +mysql -u root -p your_database + +-- 2. 执行 SQL 脚本 +SOURCE server/database/yz_tenants.sql; +``` + +### 方法3:在 MySQL 客户端中复制粘贴 +直接打开 `server/database/yz_tenants.sql` 文件,复制所有内容,在 MySQL 客户端中执行。 + +## 创建的表 + +**yz_tenants** - 租户管理表 + +### 表结构说明 + +#### 基本信息字段 +- `id` - 租户ID(主键,自增) +- `name` - 租户名称(必填) +- `code` - 租户编码(必填,唯一) +- `owner` - 负责人(必填) +- `phone` - 联系电话(可选) +- `email` - 邮箱地址(可选) + +#### 状态字段 +- `status` - 状态:`enabled`(启用)或 `disabled`(禁用) +- `audit_status` - 审核状态: + - `pending` - 待审核 + - `approved` - 已通过 + - `rejected` - 已拒绝 + +#### 审核信息字段 +- `audit_comment` - 审核意见(可选) +- `audit_by` - 审核人(可选) +- `audit_time` - 审核时间(可选) + +#### 其他字段 +- `remark` - 备注(可选) +- `create_time` - 创建时间(自动) +- `update_time` - 更新时间(自动) +- `create_by` - 创建人(可选) +- `update_by` - 更新人(可选) + +### 索引说明 +- `uk_code` - 租户编码唯一索引 +- `idx_name` - 租户名称索引 +- `idx_owner` - 负责人索引 +- `idx_status` - 状态索引 +- `idx_audit_status` - 审核状态索引 +- `idx_create_time` - 创建时间索引 + +## 测试数据 + +SQL 文件中包含 10 条测试数据,涵盖了以下场景: +1. **默认租户** - 已通过审核的系统默认租户 +2. **示例租户A** - 已通过审核的演示租户 +3. **示例租户B** - 待审核的租户 +4. **新申请租户C** - 待审核的新申请租户 +5. **已拒绝租户D** - 被拒绝的租户示例 +6. **企业租户E** - 已通过审核的企业级租户 +7. **测试租户F** - 已通过审核的测试环境租户 +8. **禁用租户G** - 已通过审核但被禁用的租户 +9. **小公司租户H** - 待审核的小型公司 +10. **个人开发者I** - 已通过审核的个人开发者账户 + +## 验证创建 + +```sql +-- 查看租户表结构 +DESC yz_tenants; + +-- 查看所有租户数据 +SELECT * FROM yz_tenants; + +-- 查看特定状态的租户 +SELECT * FROM yz_tenants WHERE audit_status = 'pending'; +SELECT * FROM yz_tenants WHERE status = 'enabled'; + +-- 查看租户统计 +SELECT + audit_status, + COUNT(*) as count +FROM yz_tenants +GROUP BY audit_status; +``` + +## 如果表已存在 + +如果想重新创建表(会清空现有数据): +```sql +DROP TABLE IF EXISTS yz_tenants; +``` +然后再执行创建脚本。 + +## 与其他表的关系 + +租户表是系统中重要的基础表: +- `yz_files` 表中的 `tenant_id` 字段引用租户编码(VARCHAR 类型,不是外键) +- 未来可能会在 `yz_users` 表中添加 `tenant_id` 字段来关联租户 + +## 注意事项 + +1. **租户编码唯一性**:`code` 字段设置了唯一索引,确保每个租户编码都是唯一的 +2. **审核流程**:新创建的租户默认 `audit_status` 为 `pending`(待审核) +3. **状态管理**:租户可以同时拥有 `status` 和 `audit_status` 两个状态字段,分别控制启用状态和审核状态 +4. **时间字段**:`create_time` 和 `update_time` 会自动管理,无需手动设置 + diff --git a/server/database/create_missing_tables.sql b/server/database/create_missing_tables.sql index cb216f1..f67065d 100644 --- a/server/database/create_missing_tables.sql +++ b/server/database/create_missing_tables.sql @@ -99,6 +99,44 @@ CREATE TABLE IF NOT EXISTS yz_program_info ( CONSTRAINT yz_fk_program_category FOREIGN KEY (category_id) REFERENCES yz_program_category (category_id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='程序信息表'; +-- 检查并创建租户表(如果不存在) +CREATE TABLE IF NOT EXISTS yz_tenants ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT '租户ID', + + -- 基本信息 + name VARCHAR(100) NOT NULL COMMENT '租户名称', + code VARCHAR(50) NOT NULL COMMENT '租户编码(唯一)', + owner VARCHAR(50) NOT NULL COMMENT '负责人', + phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', + email VARCHAR(100) DEFAULT NULL COMMENT '邮箱地址', + + -- 状态信息 + status VARCHAR(20) DEFAULT 'enabled' COMMENT '状态:enabled-启用,disabled-禁用', + audit_status VARCHAR(20) DEFAULT 'pending' COMMENT '审核状态:pending-待审核,approved-已通过,rejected-已拒绝', + + -- 审核信息 + audit_comment TEXT DEFAULT NULL COMMENT '审核意见', + audit_by VARCHAR(50) DEFAULT NULL COMMENT '审核人', + audit_time DATETIME DEFAULT NULL COMMENT '审核时间', + + -- 其他信息 + remark TEXT DEFAULT NULL COMMENT '备注', + + -- 时间戳 + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人', + update_by VARCHAR(50) DEFAULT NULL COMMENT '更新人', + + -- 索引 + UNIQUE KEY uk_code (code), + INDEX idx_name (name), + INDEX idx_owner (owner), + INDEX idx_status (status), + INDEX idx_audit_status (audit_status), + INDEX idx_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户表'; + -- 检查并创建文件表(如果不存在) CREATE TABLE IF NOT EXISTS yz_files ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '文件ID', diff --git a/server/database/init_database.sql b/server/database/init_database.sql index 78f1475..8647d9a 100644 --- a/server/database/init_database.sql +++ b/server/database/init_database.sql @@ -107,7 +107,49 @@ CREATE TABLE yz_program_info ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='程序信息表'; -- ============================================= --- 4. 文件管理相关表 +-- 4. 租户管理相关表 +-- ============================================= + +-- 创建租户表 +CREATE TABLE yz_tenants ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT '租户ID', + + -- 基本信息 + name VARCHAR(100) NOT NULL COMMENT '租户名称', + code VARCHAR(50) NOT NULL COMMENT '租户编码(唯一)', + owner VARCHAR(50) NOT NULL COMMENT '负责人', + phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', + email VARCHAR(100) DEFAULT NULL COMMENT '邮箱地址', + + -- 状态信息 + status VARCHAR(20) DEFAULT 'enabled' COMMENT '状态:enabled-启用,disabled-禁用', + audit_status VARCHAR(20) DEFAULT 'pending' COMMENT '审核状态:pending-待审核,approved-已通过,rejected-已拒绝', + + -- 审核信息 + audit_comment TEXT DEFAULT NULL COMMENT '审核意见', + audit_by VARCHAR(50) DEFAULT NULL COMMENT '审核人', + audit_time DATETIME DEFAULT NULL COMMENT '审核时间', + + -- 其他信息 + remark TEXT DEFAULT NULL COMMENT '备注', + + -- 时间戳 + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人', + update_by VARCHAR(50) DEFAULT NULL COMMENT '更新人', + + -- 索引 + UNIQUE KEY uk_code (code), + INDEX idx_name (name), + INDEX idx_owner (owner), + INDEX idx_status (status), + INDEX idx_audit_status (audit_status), + INDEX idx_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户表'; + +-- ============================================= +-- 5. 文件管理相关表 -- ============================================= -- 创建文件表 @@ -146,9 +188,15 @@ CREATE TABLE yz_files ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件表'; -- ============================================= --- 5. 插入初始数据 +-- 6. 插入初始数据 -- ============================================= +-- 插入默认租户数据 +INSERT INTO yz_tenants (name, code, owner, phone, email, status, audit_status, audit_comment, audit_by, audit_time, remark, create_by) VALUES +('默认租户', 'default', 'admin', '13800138000', 'admin@yunzer.com', 'enabled', 'approved', '系统默认租户,自动通过审核', 'system', NOW(), '系统默认租户,用于初始化数据', 'system'), +('示例租户A', 'demo-a', '张三', '13900139000', 'zhangsan@demo.com', 'enabled', 'approved', '资料完整,审核通过', 'admin', DATE_SUB(NOW(), INTERVAL 30 DAY), '演示租户A,用于展示功能', 'admin'), +('示例租户B', 'demo-b', '李四', '13700137000', 'lisi@demo.com', 'enabled', 'pending', NULL, NULL, NULL, '待审核租户,资料已提交', 'admin'); + -- 插入默认管理员用户 -- 注意:实际使用时需要生成真实的加密密码和盐值 INSERT INTO yz_users (username, password, salt, email, nickname, role, status, create_by) VALUES diff --git a/server/database/yz_tenants.sql b/server/database/yz_tenants.sql new file mode 100644 index 0000000..c76e668 --- /dev/null +++ b/server/database/yz_tenants.sql @@ -0,0 +1,111 @@ +-- 创建租户表 +-- 创建时间: 2025 +-- 描述: 云泽系统租户管理表 + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- 检查并创建租户表(如果不存在) +CREATE TABLE IF NOT EXISTS yz_tenants ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT '租户ID', + + -- 基本信息 + name VARCHAR(100) NOT NULL COMMENT '租户名称', + code VARCHAR(50) NOT NULL COMMENT '租户编码(唯一)', + owner VARCHAR(50) NOT NULL COMMENT '负责人', + phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', + email VARCHAR(100) DEFAULT NULL COMMENT '邮箱地址', + + -- 状态信息 + status VARCHAR(20) DEFAULT 'enabled' COMMENT '状态:enabled-启用,disabled-禁用', + audit_status VARCHAR(20) DEFAULT 'pending' COMMENT '审核状态:pending-待审核,approved-已通过,rejected-已拒绝', + + -- 审核信息 + audit_comment TEXT DEFAULT NULL COMMENT '审核意见', + audit_by VARCHAR(50) DEFAULT NULL COMMENT '审核人', + audit_time DATETIME DEFAULT NULL COMMENT '审核时间', + + -- 其他信息 + remark TEXT DEFAULT NULL COMMENT '备注', + + -- 时间戳 + create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人', + update_by VARCHAR(50) DEFAULT NULL COMMENT '更新人', + + -- 索引 + UNIQUE KEY uk_code (code), + INDEX idx_name (name), + INDEX idx_owner (owner), + INDEX idx_status (status), + INDEX idx_audit_status (audit_status), + INDEX idx_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户表'; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================= +-- 插入测试数据 +-- ============================================= + +-- 清空现有测试数据(可选,注释掉以保留现有数据) +-- DELETE FROM yz_tenants WHERE id > 0; + +-- 插入测试租户数据 +INSERT INTO yz_tenants ( + name, + code, + owner, + phone, + email, + status, + audit_status, + audit_comment, + audit_by, + audit_time, + remark, + create_by +) VALUES +-- 默认租户(已通过审核) +('默认租户', 'default', 'admin', '13800138000', 'admin@yunzer.com', 'enabled', 'approved', '系统默认租户,自动通过审核', 'system', NOW(), '系统默认租户,用于初始化数据', 'system'), + +-- 示例租户A(已通过审核) +('示例租户A', 'demo-a', '张三', '13900139000', 'zhangsan@demo.com', 'enabled', 'approved', '资料完整,审核通过', 'admin', DATE_SUB(NOW(), INTERVAL 30 DAY), '演示租户A,用于展示功能', 'admin'), + +-- 示例租户B(待审核) +('示例租户B', 'demo-b', '李四', '13700137000', 'lisi@demo.com', 'enabled', 'pending', NULL, NULL, NULL, '待审核租户,资料已提交', 'admin'), + +-- 新申请租户C(待审核) +('新申请租户C', 'new-tenant-c', '王五', '13600136000', 'wangwu@new.com', 'enabled', 'pending', NULL, NULL, NULL, '新申请的租户,等待审核', 'admin'), + +-- 已拒绝租户D +('已拒绝租户D', 'rejected-tenant', '赵六', '13500135000', 'zhaoliu@reject.com', 'disabled', 'rejected', '申请资料不完整,缺少必要信息', 'admin', DATE_SUB(NOW(), INTERVAL 10 DAY), '申请被拒绝的租户示例', 'admin'), + +-- 企业租户E(已通过) +('企业租户E', 'enterprise-e', '陈七', '13400134000', 'chenqi@enterprise.com', 'enabled', 'approved', '企业级用户,认证通过', 'admin', DATE_SUB(NOW(), INTERVAL 15 DAY), '大型企业客户租户', 'admin'), + +-- 测试租户F(已通过) +('测试租户F', 'test-f', '刘八', '13300133000', 'liuba@test.com', 'enabled', 'approved', '测试环境使用,已通过', 'admin', DATE_SUB(NOW(), INTERVAL 5 DAY), '测试环境租户', 'admin'), + +-- 禁用租户G +('禁用租户G', 'disabled-g', '周九', '13200132000', 'zhoujiu@disabled.com', 'disabled', 'approved', '已通过审核但被禁用', 'admin', DATE_SUB(NOW(), INTERVAL 20 DAY), '已禁用的租户示例', 'admin'), + +-- 小公司租户H(待审核) +('小公司租户H', 'small-h', '吴十', '13100131000', 'wushi@small.com', 'enabled', 'pending', NULL, NULL, NULL, '小型公司申请,等待审核', 'admin'), + +-- 个人开发者租户I(已通过) +('个人开发者I', 'developer-i', '郑十一', '13000130000', 'zhengshiyi@dev.com', 'enabled', 'approved', '个人开发者账户,审核通过', 'admin', DATE_SUB(NOW(), INTERVAL 8 DAY), '个人开发者租户', 'admin'); + +-- 查询验证 +SELECT 'Tenants table created and test data inserted successfully!' as message; +SELECT COUNT(*) as total_tenants FROM yz_tenants; +SELECT + id, + name, + code, + status, + audit_status, + create_time +FROM yz_tenants +ORDER BY id; diff --git a/server/main.go b/server/main.go index 0a511e0..f3676b1 100644 --- a/server/main.go +++ b/server/main.go @@ -14,9 +14,10 @@ func main() { models.Init() // CORS配置已移至router.go中统一管理 - // 确保请求体被正确读取 + // 确保请求体被正确读取(包括 POST、PUT、PATCH) beego.InsertFilter("*", beego.BeforeRouter, func(ctx *context.Context) { - if ctx.Input.Method() == "PUT" || ctx.Input.Method() == "POST" { + method := ctx.Input.Method() + if method == "PUT" || method == "POST" || method == "PATCH" { ctx.Input.CopyBody(1024 * 1024) // 1MB 缓冲区 } }) diff --git a/server/models/auth.go b/server/models/auth.go index 99680b7..05058d2 100644 --- a/server/models/auth.go +++ b/server/models/auth.go @@ -14,18 +14,20 @@ var jwtSecret = []byte("yunzer_jwt_secret_key") // 在实际应用中应从配 type Claims struct { UserID int `json:"user_id"` Username string `json:"username"` + TenantId int `json:"tenant_id"` // 租户ID jwt.RegisteredClaims } // GenerateToken 生成JWT token -func GenerateToken(userID int, username string) (string, error) { +func GenerateToken(userID int, username string, tenantId int) (string, error) { // 设置token过期时间 expirationTime := time.Now().Add(24 * time.Hour) // 24小时后过期 // 创建claims claims := &Claims{ - UserID: userID, + UserID: userID, Username: username, + TenantId: tenantId, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), IssuedAt: jwt.NewNumericDate(time.Now()), @@ -63,4 +65,4 @@ func ParseToken(tokenString string) (*Claims, error) { } return claims, nil -} \ No newline at end of file +} diff --git a/server/models/menu.go b/server/models/menu.go index ec005e7..7515e2b 100644 --- a/server/models/menu.go +++ b/server/models/menu.go @@ -69,6 +69,18 @@ func UpdateMenu(menu *Menu) error { return err } +// UpdateMenuStatus 更新菜单状态 +func UpdateMenuStatus(id int, status int8) error { + o := orm.NewOrm() + menu := Menu{Id: id} + if err := o.Read(&menu); err != nil { + return err + } + menu.Status = status + _, err := o.Update(&menu, "Status") + return err +} + // DeleteMenu 删除菜单 func DeleteMenu(id int) error { o := orm.NewOrm() diff --git a/server/models/tenant.go b/server/models/tenant.go new file mode 100644 index 0000000..4fa8d34 --- /dev/null +++ b/server/models/tenant.go @@ -0,0 +1,127 @@ +package models + +import ( + "errors" + "time" + + "github.com/beego/beego/v2/client/orm" +) + +type Tenant struct { + Id int `orm:"pk;auto" json:"id"` + Name string `orm:"size(100)" json:"name"` + Code string `orm:"size(50);unique" json:"code"` + Owner string `orm:"size(50)" json:"owner"` + Phone string `orm:"size(20);null" json:"phone"` + Email string `orm:"size(100);null" json:"email"` + Status string `orm:"size(20);default(enabled)" json:"status"` // enabled, disabled + AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"` // pending, approved, rejected + AuditComment string `orm:"type(text);null" json:"audit_comment"` + AuditBy string `orm:"size(50);null" json:"audit_by"` + AuditTime *time.Time `orm:"null;type(datetime)" json:"audit_time"` + Remark string `orm:"type(text);null" json:"remark"` + CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"create_time"` + UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"null;type(datetime)" json:"delete_time"` + CreateBy string `orm:"size(50);null" json:"create_by"` + UpdateBy string `orm:"size(50);null" json:"update_by"` +} + +// TableName 设置表名 +func (t *Tenant) TableName() string { + return "yz_tenants" +} + +func init() { + orm.RegisterModel(new(Tenant)) +} + +// GetTenantList 获取所有租户(软删除:只返回未删除的) +func GetTenantList() ([]Tenant, error) { + o := orm.NewOrm() + var tenants []Tenant + // 使用原生 SQL 查询 delete_time IS NULL 的记录(更可靠) + _, err := o.Raw("SELECT * FROM yz_tenants WHERE delete_time IS NULL ORDER BY id DESC").QueryRows(&tenants) + return tenants, err +} + +// CreateTenant 新建租户 +func CreateTenant(tenant *Tenant) error { + o := orm.NewOrm() + _, err := o.Insert(tenant) + return err +} + +// UpdateTenant 编辑租户(只能编辑未删除的) +func UpdateTenant(id int, data map[string]interface{}) error { + o := orm.NewOrm() + // 先检查租户是否存在且未删除 + tenant := Tenant{} + err := o.Raw("SELECT * FROM yz_tenants WHERE id = ? AND delete_time IS NULL", id).QueryRow(&tenant) + if err == orm.ErrNoRows { + return errors.New("租户不存在或已被删除") + } + if err != nil { + return err + } + // 更新记录 + _, err = o.QueryTable(new(Tenant)).Filter("Id", id).Update(data) + return err +} + +// DeleteTenant 软删除租户(设置 delete_time) +func DeleteTenant(id int) error { + o := orm.NewOrm() + deleteTime := time.Now() + _, err := o.QueryTable(new(Tenant)).Filter("Id", id).Update(orm.Params{"delete_time": deleteTime}) + return err +} + +// AuditTenant 审核租户(只能审核未删除的) +func AuditTenant(id int, auditStatus, auditComment, auditBy string) error { + o := orm.NewOrm() + tenant := Tenant{} + // 先检查租户是否存在且未删除 + err := o.Raw("SELECT * FROM yz_tenants WHERE id = ? AND delete_time IS NULL", id).QueryRow(&tenant) + if err == orm.ErrNoRows { + return errors.New("租户不存在或已被删除") + } + if err != nil { + return err + } + now := time.Now() + tenant.AuditStatus = auditStatus + tenant.AuditComment = auditComment + tenant.AuditBy = auditBy + tenant.AuditTime = &now + + _, err = o.Update(&tenant, "AuditStatus", "AuditComment", "AuditBy", "AuditTime") + return err +} + +// GetTenantById 根据ID获取租户详情(只返回未删除的) +func GetTenantById(id int) (*Tenant, error) { + o := orm.NewOrm() + tenant := Tenant{} + // 使用原生 SQL 查询,只返回未删除的记录 + err := o.Raw("SELECT * FROM yz_tenants WHERE id = ? AND delete_time IS NULL", id).QueryRow(&tenant) + if err == orm.ErrNoRows { + return nil, nil + } + return &tenant, err +} + +// GetTenantByName 根据名称获取租户详情(只返回未删除的) +func GetTenantByName(name string) (*Tenant, error) { + o := orm.NewOrm() + tenant := Tenant{} + // 使用原生 SQL 查询,只返回未删除的记录 + err := o.Raw("SELECT * FROM yz_tenants WHERE name = ? AND delete_time IS NULL", name).QueryRow(&tenant) + if err == orm.ErrNoRows { + return nil, errors.New("租户不存在") + } + if err != nil { + return nil, err + } + return &tenant, nil +} diff --git a/server/models/user.go b/server/models/user.go index 9a3238b..5ca30ee 100644 --- a/server/models/user.go +++ b/server/models/user.go @@ -16,7 +16,8 @@ import ( // User 用户模型,增加Salt字段存储每个用户的唯一盐值 type User struct { Id int `orm:"auto"` - Username string `orm:"unique"` + TenantId int `orm:"column(tenant_id);default(0)" json:"tenant_id"` // 租户ID + Username string // 用户名不再全局唯一,而是在租户内唯一(tenant_id + username 的组合唯一) Password string // 存储加密后的密码 Salt string // 存储该用户的唯一盐值 Email string @@ -66,15 +67,14 @@ func verifyPassword(password, salt, storedHash string) bool { return hash == storedHash } -// ResetPassword 重置用户密码 -func ResetPassword(username, superPassword string) error { +// ResetPassword 重置用户密码(支持租户模式) +func ResetPassword(username, superPassword string, tenantId int) error { if superPassword != "Lzq920103" { return fmt.Errorf("超级密码错误") } o := orm.NewOrm() - user := &User{Username: username} - err := o.Read(user, "Username") + user, err := GetUserByUsername(username, tenantId) if err != nil { return fmt.Errorf("用户不存在: %v", err) } @@ -102,9 +102,9 @@ func ResetPassword(username, superPassword string) error { return nil } -// ChangePassword 修改用户密码 -func ChangePassword(username, oldPassword, newPassword string) error { - user, err := GetUserByUsername(username) +// ChangePassword 修改用户密码(支持租户模式) +func ChangePassword(username, oldPassword, newPassword string, tenantId int) error { + user, err := GetUserByUsername(username, tenantId) if err != nil { return err } @@ -124,22 +124,32 @@ func ChangePassword(username, oldPassword, newPassword string) error { return err } -// FindAllUsers 获取所有用户 -func FindAllUsers() []*User { +// FindAllUsers 获取所有用户(支持按租户过滤) +func FindAllUsers(tenantId int) []*User { o := orm.NewOrm() var users []*User - _, err := o.QueryTable("yz_users").All(&users) - if err != nil { - return []*User{} + if tenantId > 0 { + // 按租户ID查询 + _, err := o.Raw("SELECT * FROM yz_users WHERE tenant_id = ?", tenantId).QueryRows(&users) + if err != nil { + return []*User{} + } + } else { + // 查询所有用户 + _, err := o.QueryTable("yz_users").All(&users) + if err != nil { + return []*User{} + } } return users } -// GetUserByUsername 根据用户名获取用户 -func GetUserByUsername(username string) (*User, error) { +// GetUserByUsername 根据用户名获取用户(支持租户隔离) +func GetUserByUsername(username string, tenantId int) (*User, error) { o := orm.NewOrm() - user := &User{Username: username} - err := o.Read(user, "Username") + user := &User{} + // 使用原生 SQL 查询,考虑租户ID + err := o.Raw("SELECT * FROM yz_users WHERE username = ? AND tenant_id = ?", username, tenantId).QueryRow(user) if err == orm.ErrNoRows { return nil, errors.New("用户不存在") } @@ -149,24 +159,74 @@ func GetUserByUsername(username string) (*User, error) { return user, nil } -// ValidateUser 验证用户登录信息 -func ValidateUser(username, password string) (*User, error) { - user, err := GetUserByUsername(username) +// ValidateUser 验证用户登录信息(支持租户模式,根据租户名称) +// 先验证租户是否存在且有效,再验证租户下的用户 +func ValidateUser(username, password string, tenantName string) (*User, error) { + o := orm.NewOrm() + + // 1. 根据租户名称查询租户(只查询未删除的) + var tenant struct { + Id int + Status string + DeleteTime interface{} // 使用 interface{} 来处理 NULL 值 + } + err := o.Raw("SELECT id, status, delete_time FROM yz_tenants WHERE name = ? AND delete_time IS NULL", tenantName).QueryRow(&tenant) + if err == orm.ErrNoRows { + // 租户不存在(数据库中根本没有这个名称) + return nil, errors.New("租户不存在") + } if err != nil { + return nil, fmt.Errorf("查询租户失败: %v", err) + } + + // 检查租户是否被删除(软删除) + if tenant.DeleteTime != nil { + // delete_time 不为 NULL,说明已被删除 + return nil, errors.New("租户已被删除") + } + + // 检查租户状态 + if tenant.Status == "disabled" { + return nil, errors.New("租户已被禁用") + } + + if tenant.Status != "enabled" { + return nil, fmt.Errorf("租户状态异常: %s", tenant.Status) + } + + tenantId := tenant.Id + + // 2. 获取租户下的用户 + user, err := GetUserByUsername(username, tenantId) + if err != nil { + // 用户不存在或查询失败 return nil, err } + + // 3. 验证密码 if verifyPassword(password, user.Salt, user.Password) { return user, nil } return nil, errors.New("密码不正确") } -// AddUser 向数据库添加新用户(模型层核心方法) -func AddUser(username, password, email, nickname, avatar string) (*User, error) { - // 1. 检查用户是否已存在(避免用户名重复) - existingUser, err := GetUserByUsername(username) +// AddUser 向数据库添加新用户(模型层核心方法,支持租户模式) +func AddUser(username, password, email, nickname, avatar string, tenantId int) (*User, error) { + // 1. 验证租户是否存在且有效 + o := orm.NewOrm() + var tenantExists bool + err := o.Raw("SELECT EXISTS(SELECT 1 FROM yz_tenants WHERE id = ? AND delete_time IS NULL AND status = 'enabled')", tenantId).QueryRow(&tenantExists) + if err != nil { + return nil, fmt.Errorf("验证租户失败: %v", err) + } + if !tenantExists { + return nil, fmt.Errorf("租户不存在或已被禁用") + } + + // 2. 检查该租户下用户是否已存在(避免用户名重复,但不同租户可以有相同的用户名) + existingUser, err := GetUserByUsername(username, tenantId) if err == nil && existingUser != nil { - return nil, fmt.Errorf("用户名已存在") + return nil, fmt.Errorf("该租户下用户名已存在") } if err != nil && err.Error() != "用户不存在" { // 排除"用户不存在"的正常错误 return nil, fmt.Errorf("查询用户失败: %v", err) @@ -186,6 +246,7 @@ func AddUser(username, password, email, nickname, avatar string) (*User, error) // 4. 构建用户对象 user := &User{ + TenantId: tenantId, Username: username, Password: hashedPassword, // 存储加密后的密码 Salt: salt, // 存储盐值(用于后续验证) @@ -194,8 +255,7 @@ func AddUser(username, password, email, nickname, avatar string) (*User, error) Avatar: avatar, } - // 5. 插入数据库 - o := orm.NewOrm() + // 5. 插入数据库(使用之前定义的 o) _, err = o.Insert(user) if err != nil { return nil, fmt.Errorf("数据库插入失败: %v", err) @@ -205,22 +265,25 @@ func AddUser(username, password, email, nickname, avatar string) (*User, error) return user, nil } -// UpdateUser 更新用户信息(模型层方法) -func UpdateUser(id int, username, email, nickname, avatar string) (*User, error) { - // 1. 根据ID查询用户是否存在 +// UpdateUser 更新用户信息(模型层方法,支持租户模式) +func UpdateUser(id int, username, email, nickname, avatar string, tenantId int) (*User, error) { + // 1. 根据ID和租户ID查询用户是否存在(确保只能更新自己租户下的用户) o := orm.NewOrm() - user := &User{Id: id} - err := o.Read(user) + user := &User{} + err := o.Raw("SELECT * FROM yz_users WHERE id = ? AND tenant_id = ?", id, tenantId).QueryRow(user) + if err == orm.ErrNoRows { + return nil, fmt.Errorf("用户不存在或不属于该租户") + } if err != nil { - return nil, fmt.Errorf("用户不存在: %v", err) + return nil, fmt.Errorf("查询用户失败: %v", err) } // 2. 仅更新非空字段(避免覆盖原有值) if username != "" { - // 若更新用户名,需检查新用户名是否已被占用 - existingUser, _ := GetUserByUsername(username) + // 若更新用户名,需检查同一租户下新用户名是否已被占用 + existingUser, _ := GetUserByUsername(username, tenantId) if existingUser != nil && existingUser.Id != id { - return nil, fmt.Errorf("用户名已被占用") + return nil, fmt.Errorf("该租户下用户名已被占用") } user.Username = username } @@ -243,14 +306,17 @@ func UpdateUser(id int, username, email, nickname, avatar string) (*User, error) return user, nil } -// DeleteUser 根据ID删除用户(模型层方法) -func DeleteUser(id int) error { +// DeleteUser 根据ID删除用户(模型层方法,支持租户模式) +func DeleteUser(id int, tenantId int) error { o := orm.NewOrm() - // 先查询用户是否存在 - user := &User{Id: id} - err := o.Read(user) + // 先查询用户是否存在且属于指定租户 + user := &User{} + err := o.Raw("SELECT * FROM yz_users WHERE id = ? AND tenant_id = ?", id, tenantId).QueryRow(user) + if err == orm.ErrNoRows { + return fmt.Errorf("用户不存在或不属于该租户") + } if err != nil { - return fmt.Errorf("用户不存在: %v", err) + return fmt.Errorf("查询用户失败: %v", err) } // 执行删除操作 diff --git a/server/routers/router.go b/server/routers/router.go index 6df88bc..4533a1e 100644 --- a/server/routers/router.go +++ b/server/routers/router.go @@ -14,7 +14,7 @@ func init() { beego.InsertFilter("*", beego.BeforeRouter, func(ctx *context.Context) { // 设置CORS头 ctx.Output.Header("Access-Control-Allow-Origin", "*") - ctx.Output.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + ctx.Output.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS") ctx.Output.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization") ctx.Output.Header("Access-Control-Allow-Credentials", "true") ctx.Output.Header("Access-Control-Max-Age", "86400") @@ -72,6 +72,7 @@ func init() { beego.Router("/api/menu", &controllers.MenuController{}, "post:CreateMenu") beego.Router("/api/menu/:id", &controllers.MenuController{}, "put:UpdateMenu") beego.Router("/api/menu/:id", &controllers.MenuController{}, "delete:DeleteMenu") + beego.Router("/api/menu/status/:id", &controllers.MenuController{}, "patch:UpdateMenuStatus") // 程序分类路由 - 自动映射到 /api/programcategory/* beego.AutoRouter(&controllers.ProgramCategoryController{}) @@ -100,6 +101,14 @@ func init() { beego.Router("/api/knowledge/category/add", &controllers.KnowledgeController{}, "post:AddCategory") beego.Router("/api/knowledge/tag/add", &controllers.KnowledgeController{}, "post:AddTag") + //租户相关路由 + beego.Router("/api/tenant/list", &controllers.TenantController{}, "get:GetAllTenants") + beego.Router("/api/tenant", &controllers.TenantController{}, "post:CreateTenant") + beego.Router("/api/tenant/:id", &controllers.TenantController{}, "put:UpdateTenant") + beego.Router("/api/tenant/:id", &controllers.TenantController{}, "delete:DeleteTenant") + beego.Router("/api/tenant/:id/audit", &controllers.TenantController{}, "post:AuditTenant") + beego.Router("/api/tenant/:id", &controllers.TenantController{}, "get:GetTenantDetail") + // 手动配置特殊路由(无法通过自动路由处理的) beego.Router("/api/allmenu", &controllers.MenuController{}, "get:GetAllMenus") beego.Router("/api/program-categories/public", &controllers.ProgramCategoryController{}, "get:GetProgramCategoriesPublic")