mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-24 19:17:43 +00:00
Compare commits
338
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39ea1f6aef | ||
|
|
b2053f507a | ||
|
|
9520117914 | ||
|
|
7a5fc7d440 | ||
|
|
bd5e83d464 | ||
|
|
63dd40a8d7 | ||
|
|
32bd88504d | ||
|
|
d70818a9db | ||
|
|
9d4a425fc0 | ||
|
|
34773a0a81 | ||
|
|
36a018400b | ||
|
|
7bb02c5f1d | ||
|
|
15fb128236 | ||
|
|
3581e060ec | ||
|
|
0604a29596 | ||
|
|
d8a2958797 | ||
|
|
ab28fa7bed | ||
|
|
e88d751039 | ||
|
|
b836945eea | ||
|
|
d7a8e66753 | ||
|
|
68780a845c | ||
|
|
487dc94a2e | ||
|
|
016e977776 | ||
|
|
dcfe512204 | ||
|
|
fe6ebfd47c | ||
|
|
eba5fba3da | ||
|
|
b7e9a79225 | ||
|
|
deffb19fd8 | ||
|
|
c858b322bd | ||
|
|
1b5b52f0f1 | ||
|
|
ecb31a158b | ||
|
|
1aecc140dc | ||
|
|
e464a4aedd | ||
|
|
595c4a6be5 | ||
|
|
d115c5299c | ||
|
|
9bd542bb59 | ||
|
|
0fa015b6d0 | ||
|
|
ec7d838ebd | ||
|
|
26e116c16c | ||
|
|
90d98893f5 | ||
|
|
10f162bf5d | ||
|
|
19909746f5 | ||
|
|
205febdb8a | ||
|
|
5aec4ba32b | ||
|
|
8f1ea50dfe | ||
|
|
1483ca401d | ||
|
|
ed74623a73 | ||
|
|
1bc2e22833 | ||
|
|
cd8edfa5d4 | ||
|
|
dcc2c8e175 | ||
|
|
d991a285ba | ||
|
|
76c9d1211e | ||
|
|
f5273f5a58 | ||
|
|
54ffaac9c5 | ||
|
|
484de2e698 | ||
|
|
d72ff76aad | ||
|
|
4a523bed92 | ||
|
|
55dc33b865 | ||
|
|
3d13f5856a | ||
|
|
aa2976ba17 | ||
|
|
8e141ff8a0 | ||
|
|
2628ab8e3e | ||
|
|
1b7dcd843c | ||
|
|
f0d91fb763 | ||
|
|
7238c6a26d | ||
|
|
0964cf98e2 | ||
|
|
04c6a081ae | ||
|
|
fcbd9ae02e | ||
|
|
d34d30a197 | ||
|
|
ecfea845c2 | ||
|
|
e05ff7c809 | ||
|
|
28a9626661 | ||
|
|
96b2cb3acf | ||
|
|
87fe6b7d9b | ||
|
|
722de8ea65 | ||
|
|
8ffde94433 | ||
|
|
08eef12bca | ||
|
|
ab0e8e6056 | ||
|
|
2cef52d906 | ||
|
|
95077b116b | ||
|
|
d4cf11d313 | ||
|
|
f201792d8f | ||
|
|
d16f5e7180 | ||
|
|
f8f697af2b | ||
|
|
eb8da38a46 | ||
|
|
f19568c69a | ||
|
|
91bf25e5fe | ||
|
|
85666f160f | ||
|
|
b2baf48dc6 | ||
|
|
c906e1d503 | ||
|
|
3b93ac19f8 | ||
|
|
9914373d45 | ||
|
|
4911730012 | ||
|
|
88bab51056 | ||
|
|
0fd4f68b6c | ||
|
|
c66cb5c6a8 | ||
|
|
b16ec0af77 | ||
|
|
82ea8539eb | ||
|
|
d17d5c1206 | ||
|
|
041d22d0d2 | ||
|
|
5864058a81 | ||
|
|
45035a16e4 | ||
|
|
f4d0108d49 | ||
|
|
b81611ba72 | ||
|
|
bb34108831 | ||
|
|
b7fd92f39b | ||
|
|
63b800a3ba | ||
|
|
ed9450a2d5 | ||
|
|
1d551a10ab | ||
|
|
4d7c9e5a12 | ||
|
|
d1f5fe5681 | ||
|
|
e8c2e0a966 | ||
|
|
cef0a19a9c | ||
|
|
c0e81363dc | ||
|
|
df2e4a2b48 | ||
|
|
9088ebc2e1 | ||
|
|
9f2dec3036 | ||
|
|
ea049f9b06 | ||
|
|
1f1349a685 | ||
|
|
dcef2df38e | ||
|
|
92834d6e39 | ||
|
|
f06540883b | ||
|
|
3c9ce5b6b0 | ||
|
|
ff8a59550a | ||
|
|
7cddef33a2 | ||
|
|
7013c2fa4a | ||
|
|
65bacacb38 | ||
|
|
45587028c3 | ||
|
|
887c9cca4b | ||
|
|
a6ddb113fc | ||
|
|
b83eef8670 | ||
|
|
43dcd61c51 | ||
|
|
1bd64d4562 | ||
|
|
44e81bc72f | ||
|
|
3312f8b7b9 | ||
|
|
d6a2272f9d | ||
|
|
a5cc0a9e29 | ||
|
|
3f995735e9 | ||
|
|
b65b74dee5 | ||
|
|
98cf3ad95a | ||
|
|
8649d8d791 | ||
|
|
817e34c6aa | ||
|
|
952cd92648 | ||
|
|
6b1e961a7f | ||
|
|
762eba5af7 | ||
|
|
e82128f679 | ||
|
|
8f8a197db1 | ||
|
|
364854eda0 | ||
|
|
b259e91f4d | ||
|
|
76411f80bc | ||
|
|
5494353229 | ||
|
|
afe5efbe36 | ||
|
|
db422785fc | ||
|
|
4ac68323da | ||
|
|
44002fcb11 | ||
|
|
5bbd919745 | ||
|
|
54dd3de5b6 | ||
|
|
9540fdfc30 | ||
|
|
937775e2a7 | ||
|
|
84721265dd | ||
|
|
3ab67dfa7d | ||
|
|
6a1941a820 | ||
|
|
3ae7c44585 | ||
|
|
9d809f6392 | ||
|
|
4b477b3103 | ||
|
|
e7ae2fe019 | ||
|
|
d5ba3d9770 | ||
|
|
f3d744f6f5 | ||
|
|
0315631b53 | ||
|
|
48e7ce88ff | ||
|
|
357db6b1c9 | ||
|
|
83e0531f43 | ||
|
|
898ba7d8eb | ||
|
|
8751f34539 | ||
|
|
4aa0068d2d | ||
|
|
b954a2f092 | ||
|
|
9227bd2be1 | ||
|
|
e70a0b1314 | ||
|
|
21c262a31e | ||
|
|
f30889bd19 | ||
|
|
23e519999e | ||
|
|
bedf064ace | ||
|
|
9a8e0cddde | ||
|
|
c0c16036d3 | ||
|
|
dd905a2bed | ||
|
|
2d76430f89 | ||
|
|
5dde1d2a00 | ||
|
|
93f25c6cdf | ||
|
|
d366df372d | ||
|
|
7281d05efc | ||
|
|
e6d6a65267 | ||
|
|
9ff094b6f5 | ||
|
|
fc9c253a9f | ||
|
|
9d735ed5aa | ||
|
|
d782b00117 | ||
|
|
239159dd2a | ||
|
|
0c1e91c3b5 | ||
|
|
c09347b387 | ||
|
|
e49e47c7a1 | ||
|
|
98b46535aa | ||
|
|
014a23aac3 | ||
|
|
f1dfba79e0 | ||
|
|
a282e44b1d | ||
|
|
d1279e67fb | ||
|
|
37a5963cd6 | ||
|
|
69df1b3d34 | ||
|
|
eae97f7a15 | ||
|
|
ce0b5ff7bf | ||
|
|
73118e49b9 | ||
|
|
7b43982595 | ||
|
|
31cd1ee768 | ||
|
|
8df7551946 | ||
|
|
79c1295a70 | ||
|
|
26d9a2e9e4 | ||
|
|
b09b7b6ccc | ||
|
|
ef6fdaa221 | ||
|
|
23c80f0217 | ||
|
|
95dc699e2e | ||
|
|
74b7d62c75 | ||
|
|
68accd5448 | ||
|
|
3edbef8696 | ||
|
|
5527f6386a | ||
|
|
c48f70a7c6 | ||
|
|
a078e31664 | ||
|
|
04d2d7dde1 | ||
|
|
b846053bea | ||
|
|
a1a5634c4e | ||
|
|
6036c6e4e3 | ||
|
|
7d74e6f325 | ||
|
|
1bf9f74bdf | ||
|
|
f61de5beeb | ||
|
|
79fb2d0bee | ||
|
|
c973d6819c | ||
|
|
3d8b879e64 | ||
|
|
ac971bda4b | ||
|
|
44f62abbca | ||
|
|
7d7d8484d4 | ||
|
|
c8b27492eb | ||
|
|
783f79dcb6 | ||
|
|
ddd97d5a9e | ||
|
|
fa73c3d6b1 | ||
|
|
5b2f3e9316 | ||
|
|
42f3025217 | ||
|
|
5df43b4d11 | ||
|
|
eac1bf197e | ||
|
|
5c834939af | ||
|
|
642e86951b | ||
|
|
bc42412e92 | ||
|
|
d9122d29cb | ||
|
|
b532ad994c | ||
|
|
b62fbc803c | ||
|
|
ed8bac8a1b | ||
|
|
d56142463a | ||
|
|
f795a356e7 | ||
|
|
a359c25e36 | ||
|
|
0ef422d309 | ||
|
|
e0519a4d7e | ||
|
|
5043ce5411 | ||
|
|
cc0cdc7d0e | ||
|
|
c004b3d333 | ||
|
|
453dd65cb1 | ||
|
|
b55cbe1992 | ||
|
|
b6e1b7b210 | ||
|
|
3685510d25 | ||
|
|
d7e685536c | ||
|
|
4df859a0ea | ||
|
|
7d1b84e837 | ||
|
|
be1f0be9b2 | ||
|
|
0f742900f5 | ||
|
|
e6f4fac859 | ||
|
|
d39faa1aca | ||
|
|
c3fe13d9dd | ||
|
|
90db381b5a | ||
|
|
508137da4b | ||
|
|
695ac7b29b | ||
|
|
c06df13a75 | ||
|
|
e663de3697 | ||
|
|
10b4f03ff5 | ||
|
|
c7a4434a0e | ||
|
|
1f8babd9e7 | ||
|
|
8e8fe906fd | ||
|
|
386d08a03f | ||
|
|
af38e1694b | ||
|
|
5609e004cc | ||
|
|
5be468308a | ||
|
|
39702abfba | ||
|
|
a97d86e801 | ||
|
|
75582539fe | ||
|
|
466723c55e | ||
|
|
6b3b2125df | ||
|
|
8c8d268708 | ||
|
|
73ac7273a0 | ||
|
|
dcafcdc6ce | ||
|
|
5b1405391e | ||
|
|
0993173b1f | ||
|
|
483ec2bf3e | ||
|
|
60fe272ba0 | ||
|
|
8df2e8190e | ||
|
|
66c8eb5ee8 | ||
|
|
bb65e76219 | ||
|
|
a585e29073 | ||
|
|
7655d0fd38 | ||
|
|
5bec640f20 | ||
|
|
62d9084ee8 | ||
|
|
df8ab39aa0 | ||
|
|
a7d3666811 | ||
|
|
ea9e3d2fe1 | ||
|
|
f14085f3ee | ||
|
|
e8b9db1df5 | ||
|
|
dc625997c4 | ||
|
|
4f458591e8 | ||
|
|
7a074d93cd | ||
|
|
9d1e1f6482 | ||
|
|
19153170bb | ||
|
|
6bf774c463 | ||
|
|
0de5ba77aa | ||
|
|
f4c0134d9c | ||
|
|
2c5c1b69b4 | ||
|
|
d34a33b691 | ||
|
|
869394c898 | ||
|
|
b97bde11b2 | ||
|
|
852cfa66e8 | ||
|
|
289fbba8e0 | ||
|
|
a6ffac657e | ||
|
|
096663ac91 | ||
|
|
d40a0c0837 | ||
|
|
f94b437852 | ||
|
|
7e571b038f | ||
|
|
56968c0bd2 | ||
|
|
ef6b85faec | ||
|
|
0d47fb4e68 | ||
|
|
8bfee8af16 | ||
|
|
0fc7276ccd | ||
|
|
9a596397ca | ||
|
|
1bea64bb6d | ||
|
|
09bfdc3d39 | ||
|
|
fe8b39691b | ||
|
|
98b60f0564 |
@@ -0,0 +1,100 @@
|
||||
---
|
||||
name: new-business-module
|
||||
description: Scaffold a new single-table CRUD business module end to end — migration, Actions-mode model/dto/router, and the sys_menu/sys_api/casbin seed data that makes it show up in the UI with working permissions. Use when the user wants to add a new business table/module to go-admin, not for cross-table or non-CRUD business logic.
|
||||
---
|
||||
|
||||
# 新增业务模块
|
||||
|
||||
给一张新的业务表配齐"能跑、能看见、能授权"的完整闭环:迁移 → 后端代码 → 菜单与权限种子数据。
|
||||
只适用于单表增删改查;跨表事务、外部调用、复杂校验等超出这个范围(见下方"何时不适用")。
|
||||
|
||||
开始前先读 `AGENTS.md`(分层边界、通用 Action 使用前提、命名规则)和 `app/demo/` 下的全部文件——
|
||||
这是可编译、有测试、CI 会跑的参照物,本文与它冲突时以它为准。
|
||||
|
||||
## 何时不适用
|
||||
|
||||
业务超出单表 CRUD(跨表事务、外部服务调用、复杂校验)时,不要用这个 skill 硬套——
|
||||
改成手写 Handler + Service,参照 `app/admin/apis/sys_post.go` 及其 Service,遵守
|
||||
`AGENTS.md` 的分层约束(Api 不碰 Orm,Service 不碰 `gin.Context`,一律用 `e.Orm`)。
|
||||
|
||||
## 步骤
|
||||
|
||||
### 1. 确认表结构
|
||||
|
||||
表结构需符合命名规范:`sys_`/业务前缀 + 下划线(如 `tb_article`)。核对字段是否已有
|
||||
`created_at`/`updated_at`/`deleted_at` 这类约定字段。
|
||||
|
||||
### 2. 写数据库迁移
|
||||
|
||||
放在 `cmd/migrate/migration/version/` 目录(**不是** `version-local/` —— 后者在
|
||||
`.gitignore` 中,提交时会被忽略,`git status` 也看不到)。
|
||||
|
||||
- 文件名前 13 位是时间戳版本号
|
||||
- 已执行过的迁移文件不可修改;需要修正时新增一个迁移
|
||||
- 包名为 `version`
|
||||
|
||||
### 3. 生成 model / dto / router 三个文件(Actions 模式)
|
||||
|
||||
不要手写 Api 与 Service。使用 `common/actions` 的通用 Action,一个模块只需
|
||||
model、dto、router 三个文件,完整写法照抄 `app/demo/` 的结构。
|
||||
|
||||
**关键正确性要求**(这三条是实际出问题最多的地方):
|
||||
|
||||
- Model 实现 `models.ActiveRecord`(`Generate` / `GetId` / `TableName`),
|
||||
`TableName()` 必须显式声明——GORM 配置了 `SingularTable`,不会自动推导
|
||||
- **`Generate()` 必须返回副本,不要就地返回**——Action 在并发请求间复用实例,
|
||||
就地返回会导致请求之间串数据;这个问题单人测试时几乎不出现,上线后才暴露
|
||||
- 完成后确认 `cmd/api/` 中已用 `_` 导入新包,否则路由不会被注册
|
||||
|
||||
### 4. 写菜单、接口与权限种子数据
|
||||
|
||||
这一步最容易被漏掉——代码能编译、接口能测通,但界面上看不到菜单、点了按钮说
|
||||
没权限,往往就是漏了这一步。结构参照 `cmd/migrate/migration/version/1786700001000_demo_menu.go`
|
||||
——它是可运行、幂等(用 `upsert`,重复跑不会报错)的真实例子。
|
||||
|
||||
:::danger
|
||||
**但不要照抄它的 import。** 那个文件用的是 `cmd/migrate/migration/models`,
|
||||
只因为它的版本号排在软删除转换(`1786700003000`)之前才是安全的。
|
||||
|
||||
**你新写的迁移版本号在转换之后,必须改用 `app/` 下的运行时模型**
|
||||
(`app/admin/models.SysApi`、`SysMenu`),否则第一条 insert 就会
|
||||
`NOT NULL constraint failed: sys_api.deleted_at`。
|
||||
`TestPostConversionMigrationsAvoidFrozenSeedModels` 会拦住这个错误。
|
||||
:::
|
||||
|
||||
一个模块要在界面上可用,需要四类数据,缺一样都不行:
|
||||
|
||||
| 表 | 作用 |
|
||||
|---|---|
|
||||
| `sys_api` | 后端路由登记,Casbin 据此判定权限 |
|
||||
| `sys_menu` | 侧边栏菜单(目录用 `M`、菜单用 `C`、按钮用 `F`) |
|
||||
| `sys_menu_api_rule` | 菜单与接口的多对多关联,角色保存时据此生成策略 |
|
||||
| `casbin_rule` | 实际生效的权限策略(**不是** `sys_casbin_rule`,那张表的唯一索引在 MySQL 下会超长,不要迁移它) |
|
||||
|
||||
必须核对的两处一致性——**错了不会报错,只会在界面上表现为"看不到/点不动"**:
|
||||
|
||||
- `sys_menu.menu_name` 必须与前端组件的 `defineOptions({ name: 'XxxManage' })` 一致,
|
||||
否则 `keep-alive` 缓存静默失效
|
||||
- 按钮级 `sys_menu.permission`(格式 `模块:资源:操作`)必须与前端
|
||||
`v-permisaction="['模块:资源:操作']"` 完全一致,否则按钮权限判断静默失效
|
||||
|
||||
### 5. 收尾检查
|
||||
|
||||
| 检查项 | 出错后果 |
|
||||
| --- | --- |
|
||||
| `Generate()` 是否返回副本 | 并发请求之间串数据 |
|
||||
| 是否使用 `e.Orm` 而非全局 DB | 多租户下拿到错误的数据库连接 |
|
||||
| `TableName()` 是否显式声明 | GORM 不会自动推导 |
|
||||
| 迁移文件是否放在 `version/` | 放进 `version-local/` 会被忽略,别人拉代码看不到 |
|
||||
| `sys_menu.menu_name` 是否与前端组件 `name` 一致 | keep-alive 缓存静默失效 |
|
||||
| `sys_menu.permission` 是否与前端 `v-permisaction` 一致 | 按钮权限静默失效 |
|
||||
|
||||
跑一遍 `go run -tags sqlite3 . migrate -c config/settings.sqlite.yml` 验证迁移可执行,
|
||||
再 `go run -tags sqlite3 . server -c config/settings.sqlite.yml` 启动服务,用 admin
|
||||
账号登录确认新菜单和按钮权限都出现了。
|
||||
|
||||
如果前端页面还没生成,下一步用 go-admin-ui 仓库里的 `new-list-page` skill——两边靠
|
||||
`sys_menu.permission` / `v-permisaction` 这个字符串对齐。
|
||||
|
||||
> 不要把 `config/settings.yml` 的真实内容贴给 AI 工具——`database.source` 含数据库
|
||||
> 账号密码,`jwt.secret` 泄露后可被用来伪造任意用户的 token。
|
||||
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 🆕 Create new issue
|
||||
url: http://new-issue.go-admin.dev
|
||||
about: The issue which is not created via http://new-issue.go-admin.dev will be closed immediately.
|
||||
- name: 🆕 创建一个新 Issue
|
||||
url: http://new-issue.go-admin.dev
|
||||
about: 不是用 http://new-issue.go-admin.dev 创建的 issue 会被机器人自动关闭。
|
||||
@@ -0,0 +1,66 @@
|
||||
<!--
|
||||
First of all, thank you for your contribution! 😄
|
||||
|
||||
For requesting to pull a new feature or bugfix, please send it from a feature/bugfix branch based on the `master` branch.
|
||||
|
||||
Before submitting your pull request, please make sure the checklist below is confirmed.
|
||||
|
||||
Your pull requests will be merged after one of the collaborators approve.
|
||||
|
||||
Thank you!
|
||||
|
||||
-->
|
||||
|
||||
[[中文版模板 / Chinese template](https://github.com/go-admin-team/go-admin/blob/master/.github/PULL_REQUEST_TEMPLATE/pr_cn.md)]
|
||||
|
||||
### 🤔 This is a ...
|
||||
|
||||
- [ ] New feature
|
||||
- [ ] Bug fix
|
||||
- [ ] Site / documentation update
|
||||
- [ ] Demo update
|
||||
- [ ] Component style update
|
||||
- [ ] TypeScript definition update
|
||||
- [ ] Bundle size optimization
|
||||
- [ ] Performance optimization
|
||||
- [ ] Enhancement feature
|
||||
- [ ] Internationalization
|
||||
- [ ] Refactoring
|
||||
- [ ] Code style optimization
|
||||
- [ ] Test Case
|
||||
- [ ] Branch merge
|
||||
- [ ] Other (about what?)
|
||||
|
||||
### 🔗 Related issue link
|
||||
|
||||
<!--
|
||||
1. Put the related issue or discussion links here.
|
||||
-->
|
||||
|
||||
### 💡 Background and solution
|
||||
|
||||
<!--
|
||||
1. Describe the problem and the scenario.
|
||||
2. GIF or snapshot should be provided if includes UI/interactive modification.
|
||||
3. How to fix the problem, and list the final API implementation and usage sample if that is a new feature.
|
||||
-->
|
||||
|
||||
### 📝 Changelog
|
||||
|
||||
<!--
|
||||
Describe changes from the user side, and list all potential break changes or other risks.
|
||||
--->
|
||||
|
||||
| Language | Changelog |
|
||||
| ---------- | --------- |
|
||||
| 🇺🇸 English | |
|
||||
| 🇨🇳 Chinese | |
|
||||
|
||||
### ☑️ Self-Check before Merge
|
||||
|
||||
⚠️ Please check all items below before review. ⚠️
|
||||
|
||||
- [ ] Doc is updated/provided or not needed
|
||||
- [ ] Demo is updated/provided or not needed
|
||||
- [ ] TypeScript's definition is updated/provided or not needed
|
||||
- [ ] Changelog is provided or not needed
|
||||
@@ -0,0 +1,61 @@
|
||||
<!--
|
||||
首先,感谢你的贡献!😄
|
||||
|
||||
新特性请提交至 feature 分支,其余可提交至 master 分支。
|
||||
在维护者审核通过后会合并。
|
||||
请确保填写以下 pull request 的信息,谢谢!~
|
||||
-->
|
||||
|
||||
[[English Template / 英文模板](https://github.com/go-admin-team/go-admin/blob/master/.github/PULL_REQUEST_TEMPLATE.md)]
|
||||
|
||||
### 🤔 这个变动的性质是?
|
||||
|
||||
- [ ] 新特性提交
|
||||
- [ ] 日常 bug 修复
|
||||
- [ ] 站点、文档改进
|
||||
- [ ] 演示代码改进
|
||||
- [ ] 组件样式/交互改进
|
||||
- [ ] TypeScript 定义更新
|
||||
- [ ] 包体积优化
|
||||
- [ ] 性能优化
|
||||
- [ ] 功能增强
|
||||
- [ ] 国际化改进
|
||||
- [ ] 重构
|
||||
- [ ] 代码风格优化
|
||||
- [ ] 测试用例
|
||||
- [ ] 分支合并
|
||||
- [ ] 其他改动(是关于什么的改动?)
|
||||
|
||||
### 🔗 相关 Issue
|
||||
|
||||
<!--
|
||||
1. 描述相关需求的来源,如相关的 issue 讨论链接。
|
||||
-->
|
||||
|
||||
### 💡 需求背景和解决方案
|
||||
|
||||
<!--
|
||||
1. 要解决的具体问题。
|
||||
2. 列出最终的 API 实现和用法。
|
||||
3. 涉及UI/交互变动需要有截图或 GIF。
|
||||
-->
|
||||
|
||||
### 📝 更新日志
|
||||
|
||||
<!--
|
||||
从用户角度描述具体变化,以及可能的 breaking change 和其他风险。
|
||||
-->
|
||||
|
||||
| 语言 | 更新描述 |
|
||||
| ------- | -------- |
|
||||
| 🇺🇸 英文 | |
|
||||
| 🇨🇳 中文 | |
|
||||
|
||||
### ☑️ 请求合并前的自查清单
|
||||
|
||||
⚠️ 请自检并全部**勾选全部选项**。⚠️
|
||||
|
||||
- [ ] 文档已补充或无须补充
|
||||
- [ ] 代码演示已提供或无须提供
|
||||
- [ ] TypeScript 定义已补充或无须补充
|
||||
- [ ] Changelog 已提供或无须提供
|
||||
@@ -0,0 +1,146 @@
|
||||
name: Build
|
||||
|
||||
# Documentation-only changes skip this workflow entirely.
|
||||
#
|
||||
# A push to master here does not just build - it pushes an image, runs the
|
||||
# migrations and restarts the demo container, so the site takes a short outage.
|
||||
# Paying that for a README edit is waste at best; at worst a deploy fails for a
|
||||
# reason unrelated to anything in the change. Code coverage is unaffected,
|
||||
# because go.yml still builds every push and pull request.
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- 'LICENSE*'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- 'LICENSE*'
|
||||
- '.github/ISSUE_TEMPLATE/**'
|
||||
|
||||
# One deploy at a time. Two merges seconds apart raced here: both runs did
|
||||
# docker rm -f then docker run, the second removed the container the first had
|
||||
# just created, and the first's docker run then failed on a name conflict -
|
||||
# leaving the demo on the older image with a red deploy.
|
||||
concurrency:
|
||||
group: deploy-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
IMAGE_NAME: registry.ap-northeast-1.aliyuncs.com/go-admin/go-admin-api # 镜像名称
|
||||
TAG: ${{ github.sha }}
|
||||
IMAGE_NAME_TAG: registry.ap-northeast-1.aliyuncs.com/go-admin/go-admin-api:${{ github.sha }}
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: 1.26.5
|
||||
|
||||
- name: Tidy
|
||||
run: go mod tidy
|
||||
|
||||
- name: Build
|
||||
run: env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -tags "sqlite3,json1" --ldflags "-extldflags -static" -o main .
|
||||
|
||||
# 以下推镜像与重启步骤仅在 master 收到 push 时执行。
|
||||
# pull_request 事件同样会触发本工作流,若不加限制,任何指向 master 的
|
||||
# PR 一经创建就会把 PR 分支的镜像推上仓库,并直接重启线上 API 服务,
|
||||
# 且发生在合并之前。构建与编译校验不受影响,PR 仍会执行。
|
||||
- name: Build the Docker image and push
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
docker login --username=${{ secrets.DOCKER_USERNAME }} registry.ap-northeast-1.aliyuncs.com --password=${{ secrets.DOCKER_PASSWORD }}
|
||||
echo "************ docker login end"
|
||||
docker build -t go-admin-api:latest .
|
||||
echo "************ docker build end"
|
||||
docker tag go-admin-api ${{ env.IMAGE_NAME_TAG }}
|
||||
echo "************ docker tag end"
|
||||
docker images
|
||||
echo "************ docker images end"
|
||||
docker push ${{ env.IMAGE_NAME_TAG }} # 推送
|
||||
echo "************ docker push end"
|
||||
|
||||
- name: Restart server # 第五步,重启服务
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1.2.5
|
||||
env:
|
||||
GITHUB_SHA_X: ${GITHUB_SHA}
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }} # 下面三个配置与上一步类似
|
||||
username: ${{ secrets.SSH_USERNAME }}
|
||||
key: ${{ secrets.DEPLOY_KEY }}
|
||||
# 重启的脚本,根据自身情况做相应改动,一般要做的是migrate数据库以及重启服务器
|
||||
#
|
||||
# 配置从宿主机挂载,不使用镜像里的那份:演示站连的是托管数据库,
|
||||
# 而 config/settings.demo.yml 会随仓库公开、也会打进镜像,凭据不能写在那里。
|
||||
# 镜像里那份保持 sqlite,供 clone 仓库的人开箱即用。
|
||||
#
|
||||
# 路径本身走 secret:它不是凭据,但本仓库公开,没有理由把服务器的
|
||||
# 目录结构一并公布。DEMO_CONFIG_PATH 指向宿主机上那份配置。
|
||||
#
|
||||
# 顺序是有意的:迁移先跑,跑不过就保持现有版本不动;
|
||||
# 旧容器改名保留而不是删除,新容器不健康时能原样恢复。
|
||||
# 健康检查两条都要过——HTTP 活着不代表数据库通了。
|
||||
script: |
|
||||
set -u
|
||||
CFG="${{ secrets.DEMO_CONFIG_PATH }}"
|
||||
IMG="${{ env.IMAGE_NAME_TAG }}"
|
||||
NAME=go-admin-api
|
||||
PREV="$NAME-prev"
|
||||
|
||||
test -f "$CFG" || { echo "宿主机配置缺失,中止部署"; exit 1; }
|
||||
|
||||
sudo docker login --username=${{ secrets.DOCKER_USERNAME }} registry.ap-northeast-1.aliyuncs.com --password=${{ secrets.DOCKER_PASSWORD }}
|
||||
sudo docker pull "$IMG" || { echo "拉取镜像失败,中止部署"; exit 1; }
|
||||
|
||||
# 迁移用新镜像跑。失败时线上仍是旧版本配旧 schema,是自洽的;
|
||||
# 硬切过去才会得到代码与表对不上的服务。
|
||||
if ! sudo docker run --rm -v "$CFG":/config/settings.yml:ro "$IMG" \
|
||||
/main migrate -c /config/settings.yml; then
|
||||
echo "迁移失败,保持现有版本"; exit 1
|
||||
fi
|
||||
|
||||
if sudo docker ps -a --format '{{.Names}}' | grep -qx "$NAME"; then
|
||||
sudo docker rm -f "$PREV" >/dev/null 2>&1 || true
|
||||
sudo docker rename "$NAME" "$PREV"
|
||||
sudo docker stop "$PREV" >/dev/null
|
||||
fi
|
||||
|
||||
sudo docker run -d -p 8000:8000 \
|
||||
-v "$CFG":/config/settings.yml:ro \
|
||||
--name "$NAME" "$IMG"
|
||||
|
||||
ok=0
|
||||
for i in $(seq 1 20); do
|
||||
sleep 3
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -m 5 http://127.0.0.1:8000/api/v1/captcha 2>/dev/null || true)
|
||||
if [ "$code" = "200" ] && sudo docker logs "$NAME" 2>&1 | grep -q 'connect success'; then
|
||||
ok=1; echo "健康检查通过(第 $i 次探测)"; break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ok" = "1" ]; then
|
||||
sudo docker rm -f "$PREV" >/dev/null 2>&1 || true
|
||||
else
|
||||
echo "健康检查失败,回滚到上一版本"
|
||||
sudo docker logs --tail 40 "$NAME" 2>&1 || true
|
||||
sudo docker rm -f "$NAME" >/dev/null 2>&1 || true
|
||||
if sudo docker ps -a --format '{{.Names}}' | grep -qx "$PREV"; then
|
||||
sudo docker rename "$PREV" "$NAME"
|
||||
sudo docker start "$NAME" >/dev/null
|
||||
echo "已恢复"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@24c7eb380a2dc368f2d129e4c65e51d172983a1e # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@24c7eb380a2dc368f2d129e4c65e51d172983a1e # v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -48,4 +48,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@24c7eb380a2dc368f2d129e4c65e51d172983a1e # v4
|
||||
|
||||
+29
-10
@@ -3,6 +3,7 @@ name: build
|
||||
on:
|
||||
push:
|
||||
branches: [ master, dev ]
|
||||
tags: [ 'v*', '[0-9]*' ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
env:
|
||||
@@ -16,23 +17,41 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.18
|
||||
uses: actions/setup-go@v3
|
||||
- name: Set up Go 1.26
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: 1.26.5
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Get dependencies
|
||||
run: go mod tidy
|
||||
|
||||
# go build does not compile _test.go, so building alone never ran a single
|
||||
# test. This is the only workflow that fires on every push and pull request,
|
||||
# which makes it the one place a test gate belongs.
|
||||
- name: Test
|
||||
run: make test
|
||||
|
||||
- name: Build
|
||||
run: make build
|
||||
|
||||
# Fails the build on the silent-failure classes listed in
|
||||
# tools/checksilent, one of which is the contract boundary: nothing under
|
||||
# common/ may import app/. A boundary that is only written down erodes; this
|
||||
# is what keeps it true.
|
||||
- name: Silent-failure checks
|
||||
run: make checksilent
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
if: startsWith(${{github.ref}}, 'refs/tags/')
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -40,8 +59,8 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
if: startsWith(${{github.ref}}, 'refs/tags/')
|
||||
uses: docker/metadata-action@v4
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
@@ -52,8 +71,8 @@ jobs:
|
||||
type=sha,prefix=,format=long,enable=true,priority=100
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
if: startsWith(${{github.ref}}, 'refs/tags/')
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
context: .
|
||||
file: scripts/Dockerfile
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
name: 'GitHub Actions Mirror'
|
||||
|
||||
on: [push, delete]
|
||||
|
||||
jobs:
|
||||
mirror_to_gitee:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: actions/checkout@v1
|
||||
- name: 'Mirror to gitee'
|
||||
uses: pixta-dev/repository-mirroring-action@v1
|
||||
with:
|
||||
target_repo_url:
|
||||
git@gitee.com:go-admin-team/go-admin.git
|
||||
ssh_private_key:
|
||||
${{ secrets.GITEE_KEY }}
|
||||
mirror_to_gitlab:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: actions/checkout@v1
|
||||
- name: 'Mirror to gitlab'
|
||||
uses: pixta-dev/repository-mirroring-action@v1
|
||||
with:
|
||||
target_repo_url:
|
||||
git@gitlab.com:go-admin-team/go-admin.git
|
||||
ssh_private_key:
|
||||
${{ secrets.GITLAB_KEY }}
|
||||
+24
-6
@@ -1,15 +1,19 @@
|
||||
.idea
|
||||
.vscode
|
||||
*/.DS_Store
|
||||
.DS_Store
|
||||
static/uploadfile
|
||||
main.exe
|
||||
*.exe
|
||||
go-admin
|
||||
go-admin.exe
|
||||
# `go build ./tools/checksilent` drops the binary here, next to the one for the
|
||||
# server. Anchored with a leading slash: unanchored, the same pattern matches
|
||||
# tools/checksilent/ as well and the tool's own source never gets committed.
|
||||
/checksilent
|
||||
temp/
|
||||
!temp
|
||||
vendor
|
||||
config/settings.dev.yml
|
||||
go-admin
|
||||
common/middleware/demo.go
|
||||
config/settings.dev.*.yml
|
||||
config/settings.dev.*.yml.log
|
||||
temp/logs
|
||||
@@ -17,8 +21,22 @@ config/settings.dev.yml.log
|
||||
config/settings.b.dev.yml
|
||||
cmd/migrate/migration/version-local/*
|
||||
!cmd/migrate/migration/version-local/doc.go
|
||||
*/.DS_Store
|
||||
|
||||
# go sum
|
||||
go.sum
|
||||
config/settings.deva.yml
|
||||
go-admin-server
|
||||
CLAUDE.md
|
||||
# Everything under .claude is private by default. Skills meant for people using
|
||||
# go-admin are re-included one directory at a time, so a personal one dropped in
|
||||
# here is never committed by accident.
|
||||
.claude/*
|
||||
!.claude/skills/
|
||||
.claude/skills/*
|
||||
!.claude/skills/new-business-module/
|
||||
config/settings.local.dev.yml
|
||||
|
||||
# Go workspace files. They exist to point this module at a local checkout of
|
||||
# go-admin-core while the two are developed together, which is a private
|
||||
# arrangement between one machine's directories - committing one would break
|
||||
# the build for everyone else.
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# AGENTS.md — go-admin 后端
|
||||
|
||||
> 给 AI 编码工具与新贡献者的约定。**只写"不遵守就会出错"的规则**;技术栈版本以
|
||||
> `go.mod` 为准,命令以 `Makefile` 为准,此处不复述,避免与代码脱节。
|
||||
>
|
||||
> 标准 CRUD 模块的完整写法见 **`app/demo/`** —— 那是可编译、有测试、CI 会跑的参照物。
|
||||
> 本文与它冲突时,以 `app/demo/` 为准。
|
||||
|
||||
## 分层
|
||||
|
||||
```
|
||||
Router → Api → Service → Model
|
||||
路由注册 参数绑定 业务逻辑 GORM 结构体
|
||||
中间件链 调用 Service 操作数据库 TableName()
|
||||
```
|
||||
|
||||
对应目录:`app/{模块}/router|apis|service|models`,DTO 位于 `service/dto`。
|
||||
|
||||
**不可跨层**:Api 不直接操作 `Orm`,Service 不接触 `gin.Context`。
|
||||
|
||||
## 优先使用通用 Action
|
||||
|
||||
单表 CRUD **不要手写 Handler 与 Service**。`common/actions` 提供的五个
|
||||
Action 已覆盖参数绑定、数据权限过滤、操作人注入、分页与错误响应:
|
||||
|
||||
```go
|
||||
r := v1.Group("/demo-product").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
|
||||
{
|
||||
m := &models.DemoProduct{}
|
||||
r.GET("", actions.PermissionAction(), actions.IndexAction(m, new(dto.DemoProductSearch), func() interface{} {
|
||||
list := make([]models.DemoProduct, 0); return &list
|
||||
}))
|
||||
r.GET("/:id", actions.PermissionAction(), actions.ViewAction(new(dto.DemoProductById), func() interface{} {
|
||||
return &models.DemoProduct{}
|
||||
}))
|
||||
r.POST("", actions.CreateAction(new(dto.DemoProductControl)))
|
||||
r.PUT("/:id", actions.PermissionAction(), actions.UpdateAction(new(dto.DemoProductControl)))
|
||||
r.DELETE("", actions.PermissionAction(), actions.DeleteAction(new(dto.DemoProductById)))
|
||||
}
|
||||
```
|
||||
|
||||
这样一个模块只需 **model + dto + router** 三个文件,完整示例见 `app/demo/`。
|
||||
|
||||
使用通用 Action 的前提:
|
||||
|
||||
- Model 实现 `models.ActiveRecord`(`Generate` / `GetId` / `TableName`)
|
||||
- 列表 DTO 实现 `dto.Index`,增改删 DTO 实现 `dto.Control`
|
||||
- **所有 `Generate()` 必须返回副本** —— Action 在并发请求间复用实例,
|
||||
就地返回会串数据(`app/demo` 的测试锁定了这一点)
|
||||
- 详情/删除 DTO 内嵌 `dto.ObjectById` 即可继承 `Bind` 与 `GetId`,无需重写
|
||||
|
||||
仅当业务超出单表 CRUD(跨表事务、外部调用、复杂校验)时才自行编写 Handler
|
||||
与 Service,写法见下。
|
||||
|
||||
## Api 层(仅在通用 Action 不适用时)
|
||||
|
||||
结构体嵌入 `api.Api`,链式初始化后**必须检查 `Errors`**:
|
||||
|
||||
```go
|
||||
func (e SysPost) GetPage(c *gin.Context) {
|
||||
s := service.SysPost{}
|
||||
req := dto.SysPostPageReq{}
|
||||
err := e.MakeContext(c).MakeOrm().Bind(&req, binding.Form).MakeService(&s.Service).Errors
|
||||
if err != nil {
|
||||
e.Logger.Error(err)
|
||||
e.Error(500, err, err.Error())
|
||||
return
|
||||
}
|
||||
// ... 调用 s.GetPage(...)
|
||||
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
|
||||
}
|
||||
```
|
||||
|
||||
响应一律走 `e.OK` / `e.PageOK` / `e.Error`,不要自行 `c.JSON`。
|
||||
|
||||
## Service 层(仅在通用 Action 不适用时)
|
||||
|
||||
结构体嵌入 `service.Service`(持有 `Orm` 与 `Log`)。查询通过 Scopes 组合:
|
||||
|
||||
```go
|
||||
err = e.Orm.Model(&data).Scopes(
|
||||
cDto.MakeCondition(c.GetNeedSearch()), // 由 search tag 生成 WHERE
|
||||
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
|
||||
actions.Permission(data.TableName(), p), // 数据权限,列表/详情必须带
|
||||
).Find(list).Limit(-1).Offset(-1).Count(count).Error
|
||||
```
|
||||
|
||||
**遗漏 `actions.Permission` 会使数据权限配置静默失效** —— 这是最容易出的错。
|
||||
|
||||
错误一律 `return err` 向上传递,日志用 `e.Log.Errorf`,不使用 `panic`。
|
||||
|
||||
## DTO
|
||||
|
||||
搜索条件由 tag 声明,`MakeCondition` 据此拼 SQL:
|
||||
|
||||
```go
|
||||
type SysPostPageReq struct {
|
||||
dto.Pagination `search:"-"`
|
||||
PostName string `form:"postName" search:"type:contains;column:post_name;table:sys_post"`
|
||||
}
|
||||
func (m *SysPostPageReq) GetNeedSearch() interface{} { return *m }
|
||||
```
|
||||
|
||||
`type` 可选:`exact` `iexact` `contains` `gt` `gte` `lt` `lte` `order` `left`(联表)。
|
||||
|
||||
## Model
|
||||
|
||||
```go
|
||||
type SysPost struct {
|
||||
PostId int `gorm:"primaryKey;autoIncrement" json:"postId"`
|
||||
// ... 业务字段
|
||||
models.ControlBy // CreateBy / UpdateBy
|
||||
models.ModelTime // CreatedAt / UpdatedAt / DeletedAt
|
||||
}
|
||||
func (SysPost) TableName() string { return "sys_post" }
|
||||
```
|
||||
|
||||
`TableName()` 必须显式声明(GORM 配置了 `SingularTable`,不会自动推导复数)。
|
||||
|
||||
## 公共契约面
|
||||
|
||||
第三方应用(`app/` 下的业务模块)可以稳定依赖哪些包、路由与迁移怎么注册、
|
||||
哪些约束是硬的,见 `docs/contract.md`。
|
||||
|
||||
两条与主仓贡献者直接相关的:
|
||||
|
||||
- **`common/`、`core/` 不得 import `app/`** —— `make checksilent` 在 CI 里守着,违反即红。
|
||||
- **注册类 API(`AppRouters` / `sdk.Runtime.SetAppRouters` / `migration.ForApp`)
|
||||
只允许在 `init()` 中调用** —— 注册期靠 Go 的包初始化顺序保证无并发写,
|
||||
core 侧的 setter 没有加锁。
|
||||
|
||||
## 路由注册
|
||||
|
||||
通过 `init()` 自注册,不在中心文件手工添加:
|
||||
|
||||
```go
|
||||
func init() { routerCheckRole = append(routerCheckRole, registerSysPostRouter) }
|
||||
|
||||
func registerSysPostRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
|
||||
api := apis.SysPost{}
|
||||
r := v1.Group("/post").
|
||||
Use(authMiddleware.MiddlewareFunc()).
|
||||
Use(middleware.AuthCheckRole()). // Casbin 鉴权
|
||||
Use(actions.PermissionAction()) // 注入数据权限
|
||||
{ r.GET("", api.GetPage); r.POST("", api.Insert); /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
新增路由文件后,需确认 `cmd/api/` 中已用 `_` 导入该包。
|
||||
|
||||
## 命名
|
||||
|
||||
| 对象 | 规则 | 示例 |
|
||||
|---|---|---|
|
||||
| 数据表 | `sys_` 前缀 + 下划线 | `sys_post` |
|
||||
| API 路径 | `/api/v1/` + kebab-case | `/api/v1/sys-user` |
|
||||
| DTO | `{Model}{Action}Req` | `SysPostPageReq` |
|
||||
| 权限标识 | `模块:资源:操作` | `admin:sysPost:add` |
|
||||
|
||||
权限标识需与前端 `v-permisaction` 一致,并写入 `sys_menu` 种子数据——完整可运行的
|
||||
参照见 `cmd/migrate/migration/version/1786700001000_demo_menu.go`(sys_api /
|
||||
sys_menu / sys_menu_api_rule / casbin_rule 四张表如何配齐,用的是幂等 upsert,
|
||||
可以直接照抄结构)。
|
||||
|
||||
## Swagger
|
||||
|
||||
Handler 必须带完整注解,`go generate` 会据此生成文档:
|
||||
|
||||
```go
|
||||
// @Summary 岗位列表
|
||||
// @Tags 岗位
|
||||
// @Success 200 {object} response.Response
|
||||
// @Router /api/v1/post [get]
|
||||
// @Security Bearer
|
||||
```
|
||||
|
||||
## 本地运行
|
||||
|
||||
**配置 `driver: sqlite3` 时必须带构建标签**,否则启动即 panic:
|
||||
|
||||
```bash
|
||||
go run -tags sqlite3 . migrate -c config/settings.sqlite.yml
|
||||
go run -tags sqlite3 . server -c config/settings.sqlite.yml
|
||||
```
|
||||
|
||||
原因:`common/database/open.go` 带 `//go:build !sqlite3`,不加标签时编进的是
|
||||
不含 sqlite3 的版本,`opens["sqlite3"]` 为 nil,调用时在 nil 函数上崩溃。
|
||||
报错信息不会提到构建标签,容易误判成环境损坏。MySQL / PostgreSQL 无此问题。
|
||||
|
||||
对应 `Makefile` 的 `build-sqlite` 目标。
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
文件名前 13 位为时间戳版本号。**已执行过的迁移文件不可修改** ——
|
||||
`sys_migration` 表按版本号去重,改动不会重跑,只能新增一个迁移来修正。
|
||||
|
||||
放哪个目录取决于身份:
|
||||
|
||||
| 目录 | 用途 | 是否入库 |
|
||||
|---|---|---|
|
||||
| `version/` | 框架自带迁移,随仓库分发给所有使用者 | 是 |
|
||||
| `version-local/` | 使用者自己项目的迁移 | 否(已在 `.gitignore`) |
|
||||
|
||||
**向本仓库提交迁移必须放 `version/`** —— 放进 `version-local/` 会被忽略掉,
|
||||
`git status` 看不到,PR 里也不会出现。两个目录的包名分别是 `version` 与
|
||||
`version_local`(后者与目录名不一致,因为标识符不能含连字符)。
|
||||
|
||||
### 写种子数据用哪个 models 包
|
||||
|
||||
`1786700003000` 之后新增的迁移,**种子数据要用 `app/` 下的运行时模型**
|
||||
(如 `app/admin/models.SysApi`、`SysMenu`),**不要用 `cmd/migrate/migration/models`**。
|
||||
|
||||
后者的 `ModelTime` 声明的是可空的 `gorm.DeletedAt`,这对它之前的迁移是对的(那正是
|
||||
当时列的形状),转换之后就不再成立,两个方向都会出问题:
|
||||
|
||||
- **写**:往 NOT NULL 列里塞 NULL,第一条 insert 就 `NOT NULL constraint failed`
|
||||
- **读**:GORM 拼 `WHERE deleted_at IS NULL`,而活跃行存的是 `0`,静默查不到——
|
||||
照抄 `demo_menu.go` 的授权段落会因此跳过授权,菜单建好、权限没授、迁移仍记为成功
|
||||
|
||||
干净库跑不出这个问题,今天所有用该包的迁移都排在转换之前。完整推导见
|
||||
`schema_coverage_test.go` 里 `TestPostConversionMigrationsAvoidFrozenSeedModels`
|
||||
的注释,那个测试也守着这条边界。
|
||||
|
||||
## 静默失败校验
|
||||
|
||||
`make checksilent` 检查六类**不报错、不记日志、行为悄悄变得不对**的问题,
|
||||
CI 会跑,命中 ERROR 即失败:
|
||||
|
||||
| 检查 | 级别 | 静默后果 |
|
||||
|---|---|---|
|
||||
| `modeltime-mix` | ERROR | 两个 `ModelTime` 混用,整张表查不到数据 |
|
||||
| `menu-sort-overflow` | ERROR | 菜单 `sort` 超 127,MySQL tinyint 拒绝写入,迁移中断 |
|
||||
| `config-value-truncation` | ERROR | `sys_config.config_value` 超 255 字符被静默截断 |
|
||||
| `menu-id-collision` | ERROR | 两个模块硬编码同一菜单 ID,互相覆盖 |
|
||||
| `contract-import-boundary` | ERROR | 契约包 import `app/`,应用无法独立编译 |
|
||||
| `menu-name-mismatch` | WARN | 菜单名与前端组件 `name` 不一致,keep-alive 缓存静默失效 |
|
||||
|
||||
最后一条要跨仓库比对,只能做正则启发式,因此是 WARN,**不影响退出码**,
|
||||
且默认跳过;要跑它得指定前端目录:
|
||||
|
||||
```bash
|
||||
make checksilent UI_DIR=../go-admin-ui/src
|
||||
```
|
||||
|
||||
升级门槛:连续 2 个发版周期零误报后转为 ERROR。
|
||||
|
||||
## 提交规范
|
||||
|
||||
格式 `type+emoji: 描述`:
|
||||
|
||||
`feat✨` `fix🐛` `style💄` `docs📝` `perf👌` `test✅` `refactor🎨` `chore🔧`
|
||||
|
||||
一个提交只做一件事。改动跨越多个语义时拆分提交,不要混在一起。
|
||||
|
||||
## 红线
|
||||
|
||||
- 不使用全局 DB 变量,一律用 `e.Orm`(来自请求上下文,多租户依赖它)
|
||||
- 不在 Service 中引用 `gin.Context`
|
||||
- 生产部署前确认 `mode: prod` 且已修改 `jwt.secret`(dev 模式下 token 几乎不过期)
|
||||
- 不提交 `config/settings.yml` 中的真实凭据
|
||||
+12
-22
@@ -1,28 +1,18 @@
|
||||
FROM golang:alpine as builder
|
||||
|
||||
MAINTAINER lwnmengjing
|
||||
|
||||
ENV GOPROXY https://goproxy.cn/
|
||||
|
||||
WORKDIR /go/release
|
||||
#RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
RUN apk update && apk add tzdata
|
||||
|
||||
COPY go.mod ./go.mod
|
||||
RUN go mod tidy
|
||||
COPY . .
|
||||
RUN pwd && ls
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -a -installsuffix cgo -o go-admin .
|
||||
|
||||
FROM alpine
|
||||
|
||||
COPY --from=builder /go/release/go-admin /
|
||||
# ENV GOPROXY https://goproxy.cn/
|
||||
|
||||
COPY --from=builder /go/release/config/settings.yml /config/settings.yml
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
|
||||
|
||||
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN apk update --no-cache
|
||||
RUN apk add --update gcc g++ libc6-compat
|
||||
RUN apk add --no-cache ca-certificates
|
||||
RUN apk add --no-cache tzdata
|
||||
ENV TZ Asia/Shanghai
|
||||
|
||||
COPY ./main /main
|
||||
COPY ./config/settings.demo.yml /config/settings.yml
|
||||
COPY ./go-admin-db.db /go-admin-db.db
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["/go-admin","server","-c", "/config/settings.yml"]
|
||||
RUN chmod +x /main
|
||||
CMD ["/main","server","-c", "/config/settings.yml"]
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 go-admin-team
|
||||
Copyright (c) 2026 go-admin-team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -2,15 +2,71 @@ PROJECT:=go-admin
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
CGO_ENABLED=0 go build -ldflags="-w -s" -a -installsuffix -o go-admin .
|
||||
CGO_ENABLED=0 go build -ldflags="-w -s" -a -installsuffix "" -o go-admin .
|
||||
|
||||
# make build-linux
|
||||
build-linux:
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -a -installsuffix -o go-admin .
|
||||
@docker build -t go-admin:latest .
|
||||
@echo "build successful"
|
||||
|
||||
build-sqlite:
|
||||
go build -tags sqlite3 -ldflags="-w -s" -a -installsuffix -o go-admin .
|
||||
#.PHONY: test
|
||||
#test:
|
||||
# go test -v ./... -cover
|
||||
|
||||
# make run
|
||||
run:
|
||||
# delete go-admin-api container
|
||||
@if [ $(shell docker ps -aq --filter name=go-admin --filter publish=8000) ]; then docker rm -f go-admin; fi
|
||||
|
||||
# 启动方法一 run go-admin-api container docker-compose 启动方式
|
||||
# 进入到项目根目录 执行 make run 命令
|
||||
@docker-compose up -d
|
||||
|
||||
# 启动方式二 docker run 这里注意-v挂载的宿主机的地址改为部署时的实际绝对路径
|
||||
#@docker run --name=go-admin -p 8000:8000 -v /home/code/go/src/go-admin/go-admin/config:/go-admin-api/config -v /home/code/go/src/go-admin/go-admin-api/static:/go-admin/static -v /home/code/go/src/go-admin/go-admin/temp:/go-admin-api/temp -d --restart=always go-admin:latest
|
||||
|
||||
@echo "go-admin service is running..."
|
||||
|
||||
# delete Tag=<none> 的镜像
|
||||
@docker image prune -f
|
||||
@docker ps -a | grep "go-admin"
|
||||
|
||||
stop:
|
||||
# delete go-admin-api container
|
||||
@if [ $(shell docker ps -aq --filter name=go-admin --filter publish=8000) ]; then docker-compose down; fi
|
||||
#@if [ $(shell docker ps -aq --filter name=go-admin --filter publish=8000) ]; then docker rm -f go-admin; fi
|
||||
#@echo "go-admin stop success"
|
||||
|
||||
|
||||
# -race is worth the extra minute here: common/actions reuses model instances
|
||||
# across concurrent requests, so a Generate() that returns in place instead of
|
||||
# a copy leaks data between them - and that is invisible to a single-threaded
|
||||
# test run.
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -race -cover ./...
|
||||
|
||||
# Reports the failures that do not announce themselves - see
|
||||
# tools/checksilent. Exits non-zero on an ERROR; the one WARN-level check
|
||||
# prints and does not fail the build.
|
||||
#
|
||||
# Pass UI_DIR to enable the cross-repository menu-name check, which is skipped
|
||||
# without it: make checksilent UI_DIR=../go-admin-ui/src
|
||||
.PHONY: checksilent
|
||||
checksilent:
|
||||
ifdef UI_DIR
|
||||
go run ./tools/checksilent -ui-dir $(UI_DIR)
|
||||
else
|
||||
go run ./tools/checksilent
|
||||
endif
|
||||
|
||||
#.PHONY: docker
|
||||
#docker:
|
||||
# docker build . -t go-admin:latest
|
||||
|
||||
# make deploy
|
||||
deploy:
|
||||
|
||||
#@git checkout master
|
||||
#@git pull origin master
|
||||
make build-linux
|
||||
make run
|
||||
|
||||
+64
-36
@@ -1,17 +1,17 @@
|
||||
# go-admin
|
||||
|
||||
<img align="right" width="320" src="https://raw.githubusercontent.com/wenjianzhang/image/a44d60756c9fdedbd70f6bff076a31cbf314936a/img/go-admin.svg">
|
||||
<img align="right" width="320" src="https://doc-image.zhangwj.com/img/go-admin.svg">
|
||||
|
||||
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
[](https://github.com/go-admin-team/go-admin/releases)
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
|
||||
[English](https://github.com/go-admin-team/go-admin/blob/master/README.md) | 简体中文
|
||||
[English](https://github.com/go-admin-team/go-admin/blob/master/README.md) | 简体中文 | [繁體中文](https://github.com/go-admin-team/go-admin/blob/master/README.zh-TW.md) | [日本語](https://github.com/go-admin-team/go-admin/blob/master/README.ja-JP.md)
|
||||
|
||||
基于Gin + Vue + Element UI的前后端分离权限管理系统,系统初始化极度简单,只需要配置文件中,修改数据库连接,系统支持多指令操作,迁移指令可以让初始化数据库信息变得更简单,服务指令可以很简单的启动api服务
|
||||
基于Gin + Vue + Element UI OR Arco Design OR Ant Design的前后端分离权限管理系统,系统初始化极度简单,只需要配置文件中,修改数据库连接,系统支持多指令操作,迁移指令可以让初始化数据库信息变得更简单,服务指令可以很简单的启动api服务
|
||||
|
||||
[在线文档](https://doc.go-admin.dev)
|
||||
[在线文档](https://www.go-admin.pro)
|
||||
|
||||
[前端项目](https://github.com/go-admin-team/go-admin-ui)
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
|
||||
## 🎬 在线体验
|
||||
|
||||
vue体验:[https://www.go-admin.dev](https://www.go-admin.dev/#/login)
|
||||
Element Plus vue3 体验:[https://vue.go-admin.pro](https://vue.go-admin.pro/#/login)
|
||||
> ⚠️⚠️⚠️ 账号 / 密码: admin / 123456
|
||||
|
||||
antd体验:[https://preview.go-admin.dev](https://preview.go-admin.dev/)
|
||||
antd 体验(go-admin-pro):[https://antd.go-admin.pro](https://antd.go-admin.pro/)
|
||||
> ⚠️⚠️⚠️ 账号 / 密码: admin / 123456
|
||||
|
||||
## ✨ 特性
|
||||
@@ -78,9 +78,9 @@ antd体验:[https://preview.go-admin.dev](https://preview.go-admin.dev/)
|
||||
|
||||
### 轻松实现go-admin写出第一个应用 - 文档教程
|
||||
|
||||
[步骤一 - 基础内容介绍](https://doc.zhangwj.com/guide/intro/tutorial01.html)
|
||||
[步骤一 - 基础内容介绍](https://www.go-admin.pro/guide/intro/tutorial01.html)
|
||||
|
||||
[步骤二 - 实际应用 - 编写增删改查](https://doc.zhangwj.com/guide/intro/tutorial02.html)
|
||||
[步骤二 - 实际应用 - 编写增删改查](https://www.go-admin.pro/guide/intro/tutorial02.html)
|
||||
|
||||
### 手把手教你从入门到放弃 - 视频教程
|
||||
|
||||
@@ -106,11 +106,11 @@ antd体验:[https://preview.go-admin.dev](https://preview.go-admin.dev/)
|
||||
|
||||
### 环境要求
|
||||
|
||||
go 1.18
|
||||
go 1.26.5
|
||||
|
||||
node版本: v14.16.0
|
||||
node版本: v22+(推荐 v24 LTS)
|
||||
|
||||
npm版本: 6.14.11
|
||||
包管理器: pnpm v9+(UI 项目使用 pnpm)
|
||||
|
||||
### 开发目录创建
|
||||
|
||||
@@ -142,19 +142,22 @@ git clone https://github.com/go-admin-team/go-admin-ui.git
|
||||
# 进入 go-admin 后端项目
|
||||
cd ./go-admin
|
||||
|
||||
# 更新整理依赖
|
||||
go mod tidy
|
||||
|
||||
# 编译项目
|
||||
go build
|
||||
|
||||
# 修改配置
|
||||
# 文件路径 go-admin/config/settings.yml
|
||||
vi ./config/setting.yml
|
||||
vi ./config/settings.yml
|
||||
|
||||
# 1. 配置文件中修改数据库信息
|
||||
# 注意: settings.database 下对应的配置数据
|
||||
# 2. 确认log路径
|
||||
```
|
||||
|
||||
:::tip ⚠️注意 在windows环境如果没有安装中CGO,会出现这个问题;
|
||||
⚠️注意 在windows环境如果没有安装中CGO,会出现这个问题;
|
||||
|
||||
```bash
|
||||
E:\go-admin>go build
|
||||
@@ -170,9 +173,8 @@ D:\Code\go-admin>go build
|
||||
cgo: exec gcc: exec: "gcc": executable file not found in %PATH%
|
||||
```
|
||||
|
||||
[解决cgo问题进入](https://doc.go-admin.dev/zh-CN/guide/faq#cgo-%E7%9A%84%E9%97%AE%E9%A2%98)
|
||||
[解决cgo问题进入](https://www.go-admin.pro/zh-CN/guide/faq#cgo-%E7%9A%84%E9%97%AE%E9%A2%98)
|
||||
|
||||
:::
|
||||
|
||||
#### 初始化数据库,以及服务启动
|
||||
|
||||
@@ -232,14 +234,17 @@ env GOOS=linux GOARCH=amd64 go build main.go
|
||||
### UI交互端启动说明
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
# 安装 pnpm(若未安装)
|
||||
npm install -g pnpm
|
||||
|
||||
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
|
||||
npm install --registry=https://registry.npm.taobao.org
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 国内网络可指定镜像源加速
|
||||
pnpm install --registry=https://registry.npmmirror.com
|
||||
|
||||
# 启动服务
|
||||
npm run dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 📨 互动
|
||||
@@ -247,18 +252,21 @@ npm run dev
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td>
|
||||
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq.png" width="200px"></td>
|
||||
<td><img src="https://doc-image.zhangwj.com/img/qrcode_for_gh_b798dc7db30c_258.jpg" width="180px"></td>
|
||||
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td>
|
||||
<td><a href="https://space.bilibili.com/565616721">wenjianzhang</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>微信</td>
|
||||
<td><a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=I8ZMqsExqCHpyu8SL4rbya700rBBXYLO&jump_from=webapi"><img border="0" src="//pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流甲号" title="go-admin技术交流甲号"></a></td>
|
||||
<td>公众号🔥🔥🔥</td>
|
||||
<td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td>
|
||||
<td>哔哩哔哩🔥🔥🔥</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 💎 贡献者
|
||||
|
||||
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wenjianzhang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3890175?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/G-Akiraka" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45746659?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/lwnmengjing" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/12806223?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
@@ -284,7 +292,25 @@ npm run dev
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/misakichan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16569274?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/zhuxuyang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19301024?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/mss-boot" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/109259065?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/AuroraV" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/37330199?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Vingurzhou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/57127283?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/haimait" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/40926384?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/zyd" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3446278?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/infnan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/38274826?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/d1y" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45585937?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/qlijin" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/515900?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/logtous
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/88697234?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/stepway
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/9927079?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/NaturalGao
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/43291304?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/DemoLiang
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/23476007?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/jfcg
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1410597?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Nicole0724
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/10487328?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
|
||||
## JetBrains 开源证书支持
|
||||
|
||||
@@ -296,15 +322,17 @@ npm run dev
|
||||
|
||||
1. [ant-design](https://github.com/ant-design/ant-design)
|
||||
2. [ant-design-pro](https://github.com/ant-design/ant-design-pro)
|
||||
2. [gin](https://github.com/gin-gonic/gin)
|
||||
2. [casbin](https://github.com/casbin/casbin)
|
||||
2. [spf13/viper](https://github.com/spf13/viper)
|
||||
2. [gorm](https://github.com/jinzhu/gorm)
|
||||
2. [gin-swagger](https://github.com/swaggo/gin-swagger)
|
||||
2. [jwt-go](https://github.com/dgrijalva/jwt-go)
|
||||
2. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
||||
2. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue)
|
||||
2. [form-generator](https://github.com/JakHuang/form-generator)
|
||||
2. [arco-design](https://github.com/arco-design/arco-design)
|
||||
2. [arco-design-pro](https://github.com/arco-design/arco-design-pro)
|
||||
4. [gin](https://github.com/gin-gonic/gin)
|
||||
5. [casbin](https://github.com/casbin/casbin)
|
||||
6. [spf13/viper](https://github.com/spf13/viper)
|
||||
7. [gorm](https://github.com/go-gorm/gorm)
|
||||
8. [gin-swagger](https://github.com/swaggo/gin-swagger)
|
||||
9. [golang-jwt](https://github.com/golang-jwt/jwt)
|
||||
10. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
||||
11. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue)
|
||||
12. [form-generator](https://github.com/JakHuang/form-generator)
|
||||
|
||||
|
||||
## 🤟 打赏
|
||||
@@ -315,10 +343,10 @@ npm run dev
|
||||
|
||||
## 🤝 链接
|
||||
|
||||
[Go开发者成长线路图](http://www.golangroadmap.com/)
|
||||
- [mss-boot-io](https://docs.mss-boot-io.top/)
|
||||
|
||||
## 🔑 License
|
||||
|
||||
[MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md)
|
||||
|
||||
Copyright (c) 2022 wenjianzhang
|
||||
Copyright (c) 2026 wenjianzhang
|
||||
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
# go-admin
|
||||
|
||||
<img align="right" width="320" src="https://doc-image.zhangwj.com/img/go-admin.svg">
|
||||
|
||||
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
[](https://github.com/go-admin-team/go-admin/releases)
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
|
||||
[English](https://github.com/go-admin-team/go-admin/blob/master/README.md) | [简体中文](https://github.com/go-admin-team/go-admin/blob/master/README.Zh-cn.md) | [繁體中文](https://github.com/go-admin-team/go-admin/blob/master/README.zh-TW.md) | 日本語
|
||||
|
||||
Gin + Vue + Element UI / Arco Design / Ant Design による、フロントエンドとバックエンドを分離した権限管理システムです。初期化は非常に簡単で、設定ファイルのデータベース接続情報を変更するだけで動作します。複数のコマンドに対応しており、マイグレーションコマンドでデータベースの初期化が容易になり、サーバーコマンドで API を手軽に起動できます。
|
||||
|
||||
[オンラインドキュメント](https://www.go-admin.pro)
|
||||
|
||||
[フロントエンドプロジェクト](https://github.com/go-admin-team/go-admin-ui)
|
||||
|
||||
[動画チュートリアル](https://space.bilibili.com/565616721/channel/detail?cid=125737)
|
||||
|
||||
## 🎬 オンラインデモ
|
||||
|
||||
Element Plus vue3 デモ:[https://vue.go-admin.pro](https://vue.go-admin.pro/#/login)
|
||||
> ⚠️⚠️⚠️ アカウント / パスワード: admin / 123456
|
||||
|
||||
antd デモ(go-admin-pro):[https://antd.go-admin.pro](https://antd.go-admin.pro/)
|
||||
> ⚠️⚠️⚠️ アカウント / パスワード: admin / 123456
|
||||
|
||||
## ✨ 特徴
|
||||
|
||||
- RESTful API の設計規約に準拠
|
||||
|
||||
- GIN WEB API フレームワークをベースに、豊富なミドルウェアを提供(ユーザー認証、CORS、アクセスログ、トレース ID など)
|
||||
|
||||
- Casbin による RBAC アクセス制御モデル
|
||||
|
||||
- JWT 認証
|
||||
|
||||
- Swagger ドキュメントに対応(swaggo ベース)
|
||||
|
||||
- GORM によるデータベース永続化、複数種類のデータベースに拡張可能
|
||||
|
||||
- 設定ファイルからモデルへの単純なマッピングで、必要な設定をすぐに取得
|
||||
|
||||
- コード生成ツール
|
||||
|
||||
- フォームビルダー
|
||||
|
||||
- マルチコマンド方式
|
||||
|
||||
- マルチテナント対応
|
||||
|
||||
- TODO: ユニットテスト
|
||||
|
||||
## 🎁 標準機能
|
||||
|
||||
1. マルチテナント:デフォルトで対応。データベース単位で分離し、1 データベースにつき 1 テナント。
|
||||
1. ユーザー管理:システムの操作者であるユーザーの設定を行います。
|
||||
2. 部門管理:組織構造(会社・部門・グループ)を設定します。ツリー構造で表示し、データ権限に対応します。
|
||||
3. 役職管理:ユーザーが担当する職務を設定します。
|
||||
4. メニュー管理:メニュー、操作権限、ボタン権限識別子、API 権限などを設定します。
|
||||
5. ロール管理:ロールへのメニュー権限の割り当て、および組織単位でのデータ範囲権限の設定を行います。
|
||||
6. 辞書管理:システム内で頻繁に使う固定的なデータを管理します。
|
||||
7. パラメータ管理:よく使うパラメータを動的に設定します。
|
||||
8. 操作ログ:正常系の操作ログと異常情報のログを記録・検索します。
|
||||
9. ログインログ:ログイン履歴を記録・検索します。ログイン異常も含みます。
|
||||
1. API ドキュメント:業務コードから API ドキュメントを自動生成します。
|
||||
1. コード生成:テーブル定義から CRUD 業務を生成します。すべて画面上で操作でき、基本的な業務をコードなしで実現できます。
|
||||
1. フォームビルダー:ページのスタイルをカスタマイズし、ドラッグ&ドロップでレイアウトを作成します。
|
||||
1. サービス監視:サーバーの基本情報を確認します。
|
||||
1. コンテンツ管理:デモ機能。カテゴリ管理とコンテンツ管理を含み、入門用の参考実装として利用できます。
|
||||
1. スケジュールタスク:自動実行タスク。現在は API 呼び出しと関数呼び出しに対応しています。
|
||||
|
||||
## 事前準備
|
||||
|
||||
ローカルに [go] [gin] [node](http://nodejs.org/) と [git](https://git-scm.com/) をインストールしてください。
|
||||
|
||||
ダウンロードから使いこなすまでを解説した動画とドキュメントのチュートリアルを用意しています。本プロジェクトを試す前に、まずこれらに目を通すことを強くおすすめします。
|
||||
|
||||
### go-admin で最初のアプリケーションを作る - ドキュメント
|
||||
|
||||
[ステップ 1 - 基礎の紹介](https://www.go-admin.pro/guide/intro/tutorial01.html)
|
||||
|
||||
[ステップ 2 - 実践 - CRUD を書く](https://www.go-admin.pro/guide/intro/tutorial02.html)
|
||||
|
||||
### 動画チュートリアル
|
||||
|
||||
[go-admin の起動方法](https://www.bilibili.com/video/BV1z5411x7JG)
|
||||
|
||||
[生成ツールで業務を手軽に実装する](https://www.bilibili.com/video/BV1Dg4y1i79D)
|
||||
|
||||
[v1.1.0 のコード生成ツール](https://www.bilibili.com/video/BV1N54y1i71P) [応用]
|
||||
|
||||
[マルチコマンドでの起動方法と IDE 設定](https://www.bilibili.com/video/BV1Fg4y1q7ph)
|
||||
|
||||
[go-admin のメニュー設定](https://www.bilibili.com/video/BV1Wp4y1D715) [必見]
|
||||
|
||||
[メニュー情報と API 情報の設定方法](https://www.bilibili.com/video/BV1zv411B7nG) [必見]
|
||||
|
||||
[go-admin の権限設定](https://www.bilibili.com/video/BV1rt4y197d3) [必見]
|
||||
|
||||
[go-admin のデータ権限](https://www.bilibili.com/video/BV1LK4y1s71e) [必見]
|
||||
|
||||
**不明点はまず上記のドキュメントと記事をご確認ください。解決しない場合は issue や pr をお寄せください。動画とドキュメントは継続的に更新しています**
|
||||
|
||||
## 📦 ローカル開発
|
||||
|
||||
### 動作要件
|
||||
|
||||
go 1.26.5
|
||||
|
||||
node バージョン: v22 以上(v24 LTS 推奨)
|
||||
|
||||
パッケージマネージャー: pnpm v9 以上(UI プロジェクトは pnpm を使用)
|
||||
|
||||
### 開発ディレクトリの作成
|
||||
|
||||
```bash
|
||||
|
||||
# 開発ディレクトリを作成
|
||||
mkdir goadmin
|
||||
cd goadmin
|
||||
```
|
||||
|
||||
### コードの取得
|
||||
|
||||
> 重要:2 つのプロジェクトは同じディレクトリに配置してください。
|
||||
|
||||
```bash
|
||||
# バックエンドのコードを取得
|
||||
git clone https://github.com/go-admin-team/go-admin.git
|
||||
|
||||
# フロントエンドのコードを取得
|
||||
git clone https://github.com/go-admin-team/go-admin-ui.git
|
||||
|
||||
```
|
||||
|
||||
### 起動方法
|
||||
|
||||
#### サーバーの起動
|
||||
|
||||
```bash
|
||||
# go-admin バックエンドプロジェクトへ移動
|
||||
cd ./go-admin
|
||||
|
||||
# 依存関係を整理
|
||||
go mod tidy
|
||||
|
||||
# ビルド
|
||||
go build
|
||||
|
||||
# 設定を変更
|
||||
# ファイルパス go-admin/config/settings.yml
|
||||
vi ./config/settings.yml
|
||||
|
||||
# 1. 設定ファイル内のデータベース情報を変更
|
||||
# 注意: settings.database 配下の設定項目
|
||||
# 2. log のパスを確認
|
||||
```
|
||||
|
||||
⚠️注意 Windows 環境で CGO が未導入の場合、次のエラーが発生します。
|
||||
|
||||
```bash
|
||||
E:\go-admin>go build
|
||||
# github.com/mattn/go-sqlite3
|
||||
cgo: exec /missing-cc: exec: "/missing-cc": file does not exist
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
D:\Code\go-admin>go build
|
||||
# github.com/mattn/go-sqlite3
|
||||
cgo: exec gcc: exec: "gcc": executable file not found in %PATH%
|
||||
```
|
||||
|
||||
[cgo の問題の解決方法はこちら](https://www.go-admin.pro/zh-CN/guide/faq#cgo-%E7%9A%84%E9%97%AE%E9%A2%98)
|
||||
|
||||
|
||||
#### データベースの初期化とサービス起動
|
||||
|
||||
``` bash
|
||||
# 初回はデータベースのリソース情報を初期化する必要があります
|
||||
# macOS または linux の場合
|
||||
$ ./go-admin migrate -c config/settings.dev.yml
|
||||
|
||||
# ⚠️注意: windows の場合
|
||||
$ go-admin.exe migrate -c config/settings.dev.yml
|
||||
|
||||
|
||||
# プロジェクトを起動します。IDE からデバッグ実行することもできます
|
||||
# macOS または linux の場合
|
||||
$ ./go-admin server -c config/settings.yml
|
||||
|
||||
|
||||
# ⚠️注意: windows の場合
|
||||
$ go-admin.exe server -c config/settings.yml
|
||||
```
|
||||
|
||||
#### sys_api テーブルへのデータ追加方法
|
||||
|
||||
起動時に `-a true` を付けると、不足している API データが自動的に追加されます。
|
||||
```bash
|
||||
./go-admin server -c config/settings.yml -a true
|
||||
```
|
||||
|
||||
#### docker でのビルドと起動
|
||||
|
||||
```shell
|
||||
# イメージをビルド
|
||||
docker build -t go-admin .
|
||||
|
||||
# コンテナを起動します。1 つ目の go-admin はコンテナ名、2 つ目はイメージ名です
|
||||
# -v は設定ファイルのマウント ローカルパス:コンテナ内パス
|
||||
docker run --name go-admin -p 8000:8000 -v /config/settings.yml:/config/settings.yml -d go-admin-server
|
||||
```
|
||||
|
||||
#### ドキュメント生成
|
||||
|
||||
```bash
|
||||
go generate
|
||||
```
|
||||
|
||||
#### クロスコンパイル
|
||||
|
||||
```bash
|
||||
# windows
|
||||
env GOOS=windows GOARCH=amd64 go build main.go
|
||||
|
||||
# or
|
||||
# linux
|
||||
env GOOS=linux GOARCH=amd64 go build main.go
|
||||
```
|
||||
|
||||
### UI 側の起動方法
|
||||
|
||||
```bash
|
||||
# pnpm をインストール(未導入の場合)
|
||||
npm install -g pnpm
|
||||
|
||||
# 依存関係をインストール
|
||||
pnpm install
|
||||
|
||||
# 中国本土のネットワークではミラーを指定すると高速化できます
|
||||
pnpm install --registry=https://registry.npmmirror.com
|
||||
|
||||
# 開発サーバーを起動
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 📨 コミュニティ
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td>
|
||||
<td><img src="https://doc-image.zhangwj.com/img/qrcode_for_gh_b798dc7db30c_258.jpg" width="180px"></td>
|
||||
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td>
|
||||
<td><a href="https://space.bilibili.com/565616721">wenjianzhang</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>微信</td>
|
||||
<td>公众号🔥🔥🔥</td>
|
||||
<td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td>
|
||||
<td>哔哩哔哩🔥🔥🔥</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 💎 コントリビューター
|
||||
|
||||
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wenjianzhang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3890175?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/G-Akiraka" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45746659?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/lwnmengjing" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/12806223?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/bing127" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31166183?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/chengxiao" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1379545?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/NightFire0307" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19854086?v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/appleboy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/21979?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/ninstein" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/580303?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/kikiyou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/17959053?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/horizonzy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/22524871?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Cassuis" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/48005724?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/hqcchina" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/5179057?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/nodece" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16235121?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/stephenzhang0713" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/18169290?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/zhouxixi-dev" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/100399679?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Jalins" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31172582?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wkf928592" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/6063351?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wxxiong6" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/6983441?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Silicon-He" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/52478309?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/GizmoOAO" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20385106?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/bestgopher" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/36840497?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wxb1207" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20775558?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/misakichan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16569274?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/zhuxuyang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19301024?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/mss-boot" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/109259065?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/AuroraV" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/37330199?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Vingurzhou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/57127283?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/haimait" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/40926384?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/zyd" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3446278?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/infnan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/38274826?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/d1y" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45585937?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/qlijin" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/515900?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/logtous
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/88697234?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/stepway
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/9927079?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/NaturalGao
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/43291304?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/DemoLiang
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/23476007?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/jfcg
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1410597?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Nicole0724
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/10487328?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
## JetBrains のオープンソースライセンス支援
|
||||
|
||||
`go-admin` は一貫して JetBrains 社の GoLand 統合開発環境で開発されています。**free JetBrains Open Source license(s)** による正規の無償ライセンス提供に、この場を借りて感謝を申し上げます。
|
||||
|
||||
<a href="https://www.jetbrains.com/?from=kubeadm-ha" target="_blank"><img src="https://raw.githubusercontent.com/panjf2000/illustrations/master/jetbrains/jetbrains-variant-4.png" width="250" align="middle"/></a>
|
||||
|
||||
## 🤝 謝辞
|
||||
|
||||
1. [ant-design](https://github.com/ant-design/ant-design)
|
||||
2. [ant-design-pro](https://github.com/ant-design/ant-design-pro)
|
||||
2. [arco-design](https://github.com/arco-design/arco-design)
|
||||
2. [arco-design-pro](https://github.com/arco-design/arco-design-pro)
|
||||
4. [gin](https://github.com/gin-gonic/gin)
|
||||
5. [casbin](https://github.com/casbin/casbin)
|
||||
6. [spf13/viper](https://github.com/spf13/viper)
|
||||
7. [gorm](https://github.com/go-gorm/gorm)
|
||||
8. [gin-swagger](https://github.com/swaggo/gin-swagger)
|
||||
9. [golang-jwt](https://github.com/golang-jwt/jwt)
|
||||
10. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
||||
11. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue)
|
||||
12. [form-generator](https://github.com/JakHuang/form-generator)
|
||||
|
||||
## 🤟 支援
|
||||
|
||||
> このプロジェクトがお役に立ちましたら、作者にジュースを一杯おごる形で応援いただけます :tropical_drink:
|
||||
|
||||
<img class="no-margin" src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/pay.png" height="200px" >
|
||||
|
||||
## 🤝 関連リンク
|
||||
|
||||
- [mss-boot-io](https://docs.mss-boot-io.top/)
|
||||
|
||||
## 🔑 License
|
||||
|
||||
[MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md)
|
||||
|
||||
Copyright (c) 2026 wenjianzhang
|
||||
@@ -1,18 +1,18 @@
|
||||
|
||||
# go-admin
|
||||
|
||||
<img align="right" width="320" src="https://raw.githubusercontent.com/wenjianzhang/image/a44d60756c9fdedbd70f6bff076a31cbf314936a/img/go-admin.svg">
|
||||
<img align="right" width="320" src="https://raw.githubusercontent.com/wenjianzhang/image/203c5930b9ed08d5cf2fcb4516b85e412f8e0e60/img/go-admin.svg">
|
||||
|
||||
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
[](https://github.com/go-admin-team/go-admin/releases)
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
|
||||
English | [简体中文](https://github.com/go-admin-team/go-admin/blob/master/README.Zh-cn.md)
|
||||
English | [简体中文](https://github.com/go-admin-team/go-admin/blob/master/README.Zh-cn.md) | [繁體中文](https://github.com/go-admin-team/go-admin/blob/master/README.zh-TW.md) | [日本語](https://github.com/go-admin-team/go-admin/blob/master/README.ja-JP.md)
|
||||
|
||||
The front-end and back-end separation authority management system based on Gin + Vue + Element UI is extremely simple to initialize the system. You only need to modify the database connection in the configuration file. The system supports multi-instruction operations. Migration instructions can make it easier to initialize database information. Service instructions It's easy to start the api service.
|
||||
The front-end and back-end separation authority management system based on Gin + Vue + Element UI OR Arco Design OR Ant Design is extremely simple to initialize the system. You only need to modify the database connection in the configuration file. The system supports multi-instruction operations. Migration instructions can make it easier to initialize database information. Service instructions It's easy to start the api service.
|
||||
|
||||
[documentation](https://doc.go-admin.dev)
|
||||
[documentation](https://www.go-admin.pro)
|
||||
|
||||
[Front-end project](https://github.com/go-admin-team/go-admin-ui)
|
||||
|
||||
@@ -20,11 +20,11 @@ The front-end and back-end separation authority management system based on Gin +
|
||||
|
||||
## 🎬 Online Demo
|
||||
|
||||
vue demo:[https://www.go-admin.dev](https://www.go-admin.dev/#/login)
|
||||
> 账号 / 密码: admin / 123456
|
||||
Element Plus vue3 demo:[https://vue.go-admin.pro](https://vue.go-admin.pro/#/login)
|
||||
> Account / Password: admin / 123456
|
||||
|
||||
antd demo:[https://preview.go-admin.dev](https://preview.go-admin.dev/)
|
||||
> 账号 / 密码: admin / 123456
|
||||
antd demo (go-admin-pro):[https://antd.go-admin.pro](https://antd.go-admin.pro/)
|
||||
> Account / Password: admin / 123456
|
||||
>
|
||||
## ✨ Feature
|
||||
|
||||
@@ -76,9 +76,9 @@ At the same time, a series of tutorials including videos and documents are provi
|
||||
|
||||
### Easily implement go-admin to write the first application-documentation tutorial
|
||||
|
||||
[Step 1 - basic content introduction](https://doc.zhangwj.com/guide/intro/tutorial01.html)
|
||||
[Step 1 - basic content introduction](https://www.go-admin.pro/guide/intro/tutorial01.html)
|
||||
|
||||
[Step 2 - Practical application - writing database operations](https://doc.zhangwj.com/guide/intro/tutorial02.html)
|
||||
[Step 2 - Practical application - writing database operations](https://www.go-admin.pro/guide/intro/tutorial02.html)
|
||||
|
||||
### Teach you from getting started to giving up-video tutorial
|
||||
|
||||
@@ -104,11 +104,11 @@ At the same time, a series of tutorials including videos and documents are provi
|
||||
|
||||
### Environmental requirements
|
||||
|
||||
go 1.18
|
||||
go 1.26.5
|
||||
|
||||
nodejs: v14.16.0
|
||||
nodejs: v22+ (v24 LTS recommended)
|
||||
|
||||
npm: 6.14.11
|
||||
package manager: pnpm v9+ (the UI project uses pnpm)
|
||||
|
||||
### Development directory creation
|
||||
|
||||
@@ -140,19 +140,22 @@ git clone https://github.com/go-admin-team/go-admin-ui.git
|
||||
# Enter the go-admin backend project
|
||||
cd ./go-admin
|
||||
|
||||
# Update dependencies
|
||||
go mod tidy
|
||||
|
||||
# Compile the project
|
||||
go build
|
||||
|
||||
# Change setting
|
||||
# File path go-admin/config/settings.yml
|
||||
vi ./config/setting.yml
|
||||
vi ./config/settings.yml
|
||||
|
||||
# 1. Modify the database information in the configuration file
|
||||
# Note: The corresponding configuration data under settings.database
|
||||
# 2. Confirm the log path
|
||||
```
|
||||
|
||||
:::tip ⚠️Note that this problem will occur if CGO is not installed in the windows environment;
|
||||
⚠️ Note that this problem will occur if CGO is not installed in the windows10+ environment;
|
||||
|
||||
```bash
|
||||
E:\go-admin>go build
|
||||
@@ -168,9 +171,7 @@ D:\Code\go-admin>go build
|
||||
cgo: exec gcc: exec: "gcc": executable file not found in %PATH%
|
||||
```
|
||||
|
||||
[Solve the cgo problem and enter](https://doc.go-admin.dev/guide/faq#cgo-%E7%9A%84%E9%97%AE%E9%A2%98)
|
||||
|
||||
:::
|
||||
[Solve the cgo problem and enter](https://www.go-admin.pro/guide/faq#cgo-%E7%9A%84%E9%97%AE%E9%A2%98)
|
||||
|
||||
#### Initialize the database, and start the service
|
||||
|
||||
@@ -223,11 +224,14 @@ env GOOS=linux GOARCH=amd64 go build main.go
|
||||
### UI interactive terminal startup instructions
|
||||
|
||||
```bash
|
||||
# Install pnpm if you don't have it
|
||||
npm install -g pnpm
|
||||
|
||||
# Installation dependencies
|
||||
npm install # or cnpm install
|
||||
pnpm install
|
||||
|
||||
# Start service
|
||||
npm run dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 📨 Interactive
|
||||
@@ -235,11 +239,15 @@ npm run dev
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td>
|
||||
<td><img src="https://doc-image.zhangwj.com/img/qrcode_for_gh_b798dc7db30c_258.jpg" width="180px"></td>
|
||||
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td>
|
||||
<td><a href="https://space.bilibili.com/565616721">wenjianzhang</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wechat</td>
|
||||
<td>Wechat公众号🔥🔥🔥</td>
|
||||
<td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td>
|
||||
<td>bilibili🔥🔥🔥</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -270,6 +278,25 @@ npm run dev
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/misakichan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16569274?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/zhuxuyang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19301024?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/mss-boot" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/109259065?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/AuroraV" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/37330199?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Vingurzhou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/57127283?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/haimait" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/40926384?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/zyd" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3446278?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/infnan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/38274826?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/d1y" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45585937?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/qlijin" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/515900?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/logtous
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/88697234?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/stepway
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/9927079?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/NaturalGao
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/43291304?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/DemoLiang
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/23476007?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/jfcg
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1410597?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Nicole0724
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/10487328?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
|
||||
|
||||
|
||||
@@ -284,12 +311,14 @@ The `go-admin` project has always been developed in the GoLand integrated develo
|
||||
|
||||
1. [ant-design](https://github.com/ant-design/ant-design)
|
||||
2. [ant-design-pro](https://github.com/ant-design/ant-design-pro)
|
||||
2. [arco-design](https://github.com/arco-design/arco-design)
|
||||
2. [arco-design-pro](https://github.com/arco-design/arco-design-pro)
|
||||
2. [gin](https://github.com/gin-gonic/gin)
|
||||
2. [casbin](https://github.com/casbin/casbin)
|
||||
2. [spf13/viper](https://github.com/spf13/viper)
|
||||
2. [gorm](https://github.com/jinzhu/gorm)
|
||||
2. [gorm](https://github.com/go-gorm/gorm)
|
||||
2. [gin-swagger](https://github.com/swaggo/gin-swagger)
|
||||
2. [jwt-go](https://github.com/dgrijalva/jwt-go)
|
||||
2. [golang-jwt](https://github.com/golang-jwt/jwt)
|
||||
2. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
||||
2. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue)
|
||||
2. [form-generator](https://github.com/JakHuang/form-generator)
|
||||
@@ -301,10 +330,10 @@ The `go-admin` project has always been developed in the GoLand integrated develo
|
||||
<img class="no-margin" src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/pay.png" height="200px" >
|
||||
|
||||
## 🤝 Link
|
||||
[Go developer growth roadmap](http://www.golangroadmap.com/)
|
||||
- [mss-boot-io](https://docs.mss-boot-io.top/)
|
||||
|
||||
## 🔑 License
|
||||
|
||||
[MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md)
|
||||
|
||||
Copyright (c) 2022 wenjianzhang
|
||||
Copyright (c) 2026 wenjianzhang
|
||||
|
||||
+350
@@ -0,0 +1,350 @@
|
||||
# go-admin
|
||||
|
||||
<img align="right" width="320" src="https://doc-image.zhangwj.com/img/go-admin.svg">
|
||||
|
||||
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
[](https://github.com/go-admin-team/go-admin/releases)
|
||||
[](https://github.com/go-admin-team/go-admin)
|
||||
|
||||
[English](https://github.com/go-admin-team/go-admin/blob/master/README.md) | [简体中文](https://github.com/go-admin-team/go-admin/blob/master/README.Zh-cn.md) | 繁體中文 | [日本語](https://github.com/go-admin-team/go-admin/blob/master/README.ja-JP.md)
|
||||
|
||||
基於 Gin + Vue + Element UI OR Arco Design OR Ant Design 的前後端分離權限管理系統。系統初始化極為簡單,只需在設定檔中修改資料庫連線資訊即可。系統支援多指令操作:遷移指令讓資料庫初始化變得更簡單,服務指令則能輕鬆啟動 API 服務。
|
||||
|
||||
[線上文件](https://www.go-admin.pro)
|
||||
|
||||
[前端專案](https://github.com/go-admin-team/go-admin-ui)
|
||||
|
||||
[影片教學](https://space.bilibili.com/565616721/channel/detail?cid=125737)
|
||||
|
||||
## 🎬 線上體驗
|
||||
|
||||
Element Plus vue3 體驗:[https://vue.go-admin.pro](https://vue.go-admin.pro/#/login)
|
||||
> ⚠️⚠️⚠️ 帳號 / 密碼: admin / 123456
|
||||
|
||||
antd 體驗(go-admin-pro):[https://antd.go-admin.pro](https://antd.go-admin.pro/)
|
||||
> ⚠️⚠️⚠️ 帳號 / 密碼: admin / 123456
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 遵循 RESTful API 設計規範
|
||||
|
||||
- 基於 GIN WEB API 框架,提供豐富的中介軟體支援(使用者認證、跨域、存取日誌、追蹤 ID 等)
|
||||
|
||||
- 基於 Casbin 的 RBAC 存取控制模型
|
||||
|
||||
- JWT 認證
|
||||
|
||||
- 支援 Swagger 文件(基於 swaggo)
|
||||
|
||||
- 基於 GORM 的資料庫儲存,可擴充多種類型資料庫
|
||||
|
||||
- 設定檔簡單的模型映射,快速取得所需設定
|
||||
|
||||
- 程式碼產生工具
|
||||
|
||||
- 表單建構工具
|
||||
|
||||
- 多指令模式
|
||||
|
||||
- 多租戶的支援
|
||||
|
||||
- TODO: 單元測試
|
||||
|
||||
## 🎁 內建
|
||||
|
||||
1. 多租戶:系統預設支援多租戶,按資料庫分離,一個資料庫一個租戶。
|
||||
1. 使用者管理:使用者是系統操作者,該功能主要完成系統使用者設定。
|
||||
2. 部門管理:設定系統組織架構(公司、部門、小組),以樹狀結構呈現並支援資料權限。
|
||||
3. 職位管理:設定系統使用者所擔任的職務。
|
||||
4. 選單管理:設定系統選單、操作權限、按鈕權限標識、介面權限等。
|
||||
5. 角色管理:角色選單權限分配、設定角色按機構進行資料範圍權限劃分。
|
||||
6. 字典管理:對系統中經常使用且較為固定的資料進行維護。
|
||||
7. 參數管理:對系統動態設定常用參數。
|
||||
8. 操作日誌:系統正常操作的日誌記錄與查詢;系統異常資訊的日誌記錄與查詢。
|
||||
9. 登入日誌:系統登入日誌記錄查詢,包含登入異常。
|
||||
1. 介面文件:根據業務程式碼自動產生相關的 API 介面文件。
|
||||
1. 程式碼產生:根據資料表結構產生對應的增刪改查業務,全程視覺化操作,讓基本業務可以零程式碼實現。
|
||||
1. 表單建構:自訂頁面樣式,拖拉放實現頁面佈局。
|
||||
1. 服務監控:檢視伺服器的基本資訊。
|
||||
1. 內容管理:demo 功能,下設分類管理、內容管理,可參考使用以快速入門。
|
||||
1. 排程任務:自動化任務,目前支援介面呼叫與函式呼叫。
|
||||
|
||||
## 準備工作
|
||||
|
||||
你需要在本機安裝 [go] [gin] [node](http://nodejs.org/) 和 [git](https://git-scm.com/)
|
||||
|
||||
同時配套了系列教學(含影片與文件),說明如何從下載到熟練使用。強烈建議先看完這些教學再來實作本專案!!!
|
||||
|
||||
### 輕鬆用 go-admin 寫出第一個應用 - 文件教學
|
||||
|
||||
[步驟一 - 基礎內容介紹](https://www.go-admin.pro/guide/intro/tutorial01.html)
|
||||
|
||||
[步驟二 - 實際應用 - 撰寫增刪改查](https://www.go-admin.pro/guide/intro/tutorial02.html)
|
||||
|
||||
### 手把手教你從入門到放棄 - 影片教學
|
||||
|
||||
[如何啟動 go-admin](https://www.bilibili.com/video/BV1z5411x7JG)
|
||||
|
||||
[使用產生工具輕鬆實現業務](https://www.bilibili.com/video/BV1Dg4y1i79D)
|
||||
|
||||
[v1.1.0 版本程式碼產生工具 - 釋放雙手](https://www.bilibili.com/video/BV1N54y1i71P) [進階]
|
||||
|
||||
[多指令啟動方式講解以及 IDE 設定](https://www.bilibili.com/video/BV1Fg4y1q7ph)
|
||||
|
||||
[go-admin 選單的設定說明](https://www.bilibili.com/video/BV1Wp4y1D715) [必看]
|
||||
|
||||
[如何設定選單資訊以及介面資訊](https://www.bilibili.com/video/BV1zv411B7nG) [必看]
|
||||
|
||||
[go-admin 權限設定使用說明](https://www.bilibili.com/video/BV1rt4y197d3) [必看]
|
||||
|
||||
[go-admin 資料權限使用說明](https://www.bilibili.com/video/BV1LK4y1s71e) [必看]
|
||||
|
||||
**如有問題請先參閱上述文件與文章,若仍無法解決,歡迎提出 issue 與 pr。影片教學與文件持續更新中**
|
||||
|
||||
## 📦 本機開發
|
||||
|
||||
### 環境需求
|
||||
|
||||
go 1.26.5
|
||||
|
||||
node 版本: v22+(建議 v24 LTS)
|
||||
|
||||
套件管理器: pnpm v9+(UI 專案使用 pnpm)
|
||||
|
||||
### 建立開發目錄
|
||||
|
||||
```bash
|
||||
|
||||
# 建立開發目錄
|
||||
mkdir goadmin
|
||||
cd goadmin
|
||||
```
|
||||
|
||||
### 取得程式碼
|
||||
|
||||
> 重點注意:兩個專案必須放在同一資料夾下;
|
||||
|
||||
```bash
|
||||
# 取得後端程式碼
|
||||
git clone https://github.com/go-admin-team/go-admin.git
|
||||
|
||||
# 取得前端程式碼
|
||||
git clone https://github.com/go-admin-team/go-admin-ui.git
|
||||
|
||||
```
|
||||
|
||||
### 啟動說明
|
||||
|
||||
#### 伺服器端啟動說明
|
||||
|
||||
```bash
|
||||
# 進入 go-admin 後端專案
|
||||
cd ./go-admin
|
||||
|
||||
# 更新整理相依套件
|
||||
go mod tidy
|
||||
|
||||
# 編譯專案
|
||||
go build
|
||||
|
||||
# 修改設定
|
||||
# 檔案路徑 go-admin/config/settings.yml
|
||||
vi ./config/settings.yml
|
||||
|
||||
# 1. 在設定檔中修改資料庫資訊
|
||||
# 注意: settings.database 下對應的設定資料
|
||||
# 2. 確認 log 路徑
|
||||
```
|
||||
|
||||
⚠️注意 在 Windows 環境若未安裝 CGO,會出現這個問題;
|
||||
|
||||
```bash
|
||||
E:\go-admin>go build
|
||||
# github.com/mattn/go-sqlite3
|
||||
cgo: exec /missing-cc: exec: "/missing-cc": file does not exist
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
D:\Code\go-admin>go build
|
||||
# github.com/mattn/go-sqlite3
|
||||
cgo: exec gcc: exec: "gcc": executable file not found in %PATH%
|
||||
```
|
||||
|
||||
[解決 cgo 問題請進入](https://www.go-admin.pro/zh-CN/guide/faq#cgo-%E7%9A%84%E9%97%AE%E9%A2%98)
|
||||
|
||||
|
||||
#### 初始化資料庫,以及服務啟動
|
||||
|
||||
``` bash
|
||||
# 首次設定需要初始化資料庫資源資訊
|
||||
# macOS or linux 下使用
|
||||
$ ./go-admin migrate -c config/settings.dev.yml
|
||||
|
||||
# ⚠️注意:windows 下使用
|
||||
$ go-admin.exe migrate -c config/settings.dev.yml
|
||||
|
||||
|
||||
# 啟動專案,也可以用 IDE 進行除錯
|
||||
# macOS or linux 下使用
|
||||
$ ./go-admin server -c config/settings.yml
|
||||
|
||||
|
||||
# ⚠️注意:windows 下使用
|
||||
$ go-admin.exe server -c config/settings.yml
|
||||
```
|
||||
|
||||
#### sys_api 表的資料如何新增
|
||||
|
||||
在專案啟動時,使用 `-a true` 系統會自動新增缺少的介面資料
|
||||
```bash
|
||||
./go-admin server -c config/settings.yml -a true
|
||||
```
|
||||
|
||||
#### 使用 docker 編譯啟動
|
||||
|
||||
```shell
|
||||
# 編譯映像檔
|
||||
docker build -t go-admin .
|
||||
|
||||
# 啟動容器,第一個 go-admin 是容器名稱,第二個 go-admin 是映像檔名稱
|
||||
# -v 映射設定檔 本機路徑:容器路徑
|
||||
docker run --name go-admin -p 8000:8000 -v /config/settings.yml:/config/settings.yml -d go-admin-server
|
||||
```
|
||||
|
||||
#### 文件產生
|
||||
|
||||
```bash
|
||||
go generate
|
||||
```
|
||||
|
||||
#### 交叉編譯
|
||||
|
||||
```bash
|
||||
# windows
|
||||
env GOOS=windows GOARCH=amd64 go build main.go
|
||||
|
||||
# or
|
||||
# linux
|
||||
env GOOS=linux GOARCH=amd64 go build main.go
|
||||
```
|
||||
|
||||
### UI 互動端啟動說明
|
||||
|
||||
```bash
|
||||
# 安裝 pnpm(若未安裝)
|
||||
npm install -g pnpm
|
||||
|
||||
# 安裝相依套件
|
||||
pnpm install
|
||||
|
||||
# 中國大陸網路可指定鏡像來源加速
|
||||
pnpm install --registry=https://registry.npmmirror.com
|
||||
|
||||
# 啟動服務
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 📨 互動
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/wx.png" width="180px"></td>
|
||||
<td><img src="https://doc-image.zhangwj.com/img/qrcode_for_gh_b798dc7db30c_258.jpg" width="180px"></td>
|
||||
<td><img src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/qq2.png" width="200px"></td>
|
||||
<td><a href="https://space.bilibili.com/565616721">wenjianzhang</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>微信</td>
|
||||
<td>公众号🔥🔥🔥</td>
|
||||
<td><a target="_blank" href="https://shang.qq.com/wpa/qunwpa?idkey=0f2bf59f5f2edec6a4550c364242c0641f870aa328e468c4ee4b7dbfb392627b"><img border="0" src="https://pub.idqqimg.com/wpa/images/group.png" alt="go-admin技术交流乙号" title="go-admin技术交流乙号"></a></td>
|
||||
<td>哔哩哔哩🔥🔥🔥</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 💎 貢獻者
|
||||
|
||||
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wenjianzhang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3890175?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/G-Akiraka" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45746659?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/lwnmengjing" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/12806223?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/bing127" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31166183?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/chengxiao" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1379545?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/NightFire0307" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19854086?v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/appleboy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/21979?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/ninstein" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/580303?v=4&h=60&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/kikiyou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/17959053?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/horizonzy" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/22524871?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Cassuis" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/48005724?s=64&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/hqcchina" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/5179057?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/nodece" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16235121?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/stephenzhang0713" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/18169290?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/zhouxixi-dev" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/100399679?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Jalins" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/31172582?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wkf928592" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/6063351?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wxxiong6" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/6983441?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Silicon-He" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/52478309?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/GizmoOAO" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20385106?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/bestgopher" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/36840497?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/wxb1207" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/20775558?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/misakichan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/16569274?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/zhuxuyang" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/19301024?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/mss-boot" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/109259065?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/AuroraV" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/37330199?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Vingurzhou" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/57127283?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/haimait" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/40926384?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/zyd" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3446278?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/infnan" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/38274826?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/d1y" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45585937?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/qlijin" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/515900?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/logtous
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/88697234?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/stepway
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/9927079?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/NaturalGao
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/43291304?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/DemoLiang
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/23476007?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/jfcg
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/1410597?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
<span style="margin: 0 5px;" ><a href="https://github.com/Nicole0724
|
||||
" ><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/10487328?s=60&v=4&w=60&fit=cover&mask=circle&maxage=7d" /></a></span>
|
||||
## JetBrains 開源證書支援
|
||||
|
||||
`go-admin` 專案一直以來都是在 JetBrains 公司旗下的 GoLand 整合開發環境中進行開發,基於 **free JetBrains Open Source license(s)** 正版免費授權,在此表達我的謝意。
|
||||
|
||||
<a href="https://www.jetbrains.com/?from=kubeadm-ha" target="_blank"><img src="https://raw.githubusercontent.com/panjf2000/illustrations/master/jetbrains/jetbrains-variant-4.png" width="250" align="middle"/></a>
|
||||
|
||||
## 🤝 特別感謝
|
||||
|
||||
1. [ant-design](https://github.com/ant-design/ant-design)
|
||||
2. [ant-design-pro](https://github.com/ant-design/ant-design-pro)
|
||||
2. [arco-design](https://github.com/arco-design/arco-design)
|
||||
2. [arco-design-pro](https://github.com/arco-design/arco-design-pro)
|
||||
4. [gin](https://github.com/gin-gonic/gin)
|
||||
5. [casbin](https://github.com/casbin/casbin)
|
||||
6. [spf13/viper](https://github.com/spf13/viper)
|
||||
7. [gorm](https://github.com/go-gorm/gorm)
|
||||
8. [gin-swagger](https://github.com/swaggo/gin-swagger)
|
||||
9. [golang-jwt](https://github.com/golang-jwt/jwt)
|
||||
10. [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
|
||||
11. [ruoyi-vue](https://gitee.com/y_project/RuoYi-Vue)
|
||||
12. [form-generator](https://github.com/JakHuang/form-generator)
|
||||
|
||||
## 🤟 贊助
|
||||
|
||||
> 如果你覺得這個專案幫助到了你,可以幫作者買一杯果汁表示鼓勵 :tropical_drink:
|
||||
|
||||
<img class="no-margin" src="https://raw.githubusercontent.com/wenjianzhang/image/master/img/pay.png" height="200px" >
|
||||
|
||||
## 🤝 連結
|
||||
|
||||
- [mss-boot-io](https://docs.mss-boot-io.top/)
|
||||
|
||||
## 🔑 License
|
||||
|
||||
[MIT](https://github.com/go-admin-team/go-admin/blob/master/LICENSE.md)
|
||||
|
||||
Copyright (c) 2026 wenjianzhang
|
||||
@@ -2,8 +2,8 @@ package apis
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/captcha"
|
||||
"github.com/go-admin-team/go-admin-core/v2/captcha"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
)
|
||||
|
||||
type System struct {
|
||||
@@ -17,12 +17,15 @@ type System struct {
|
||||
// @Success 200 {object} response.Response{data=string,id=string,msg=string} "{"code": 200, "data": [...]}"
|
||||
// @Router /api/v1/captcha [get]
|
||||
func (e System) GenerateCaptchaHandler(c *gin.Context) {
|
||||
err := e.MakeContext(c).Errors
|
||||
if err != nil {
|
||||
if err := e.MakeContext(c).Errors; err != nil {
|
||||
e.Error(500, err, "服务初始化失败!")
|
||||
return
|
||||
}
|
||||
id, b64s, err := captcha.DriverDigitFunc()
|
||||
// The answer is deliberately discarded rather than logged. It used to be
|
||||
// written at info level, which put a currently valid captcha answer in the
|
||||
// application log - anyone able to read the log could bypass the check the
|
||||
// captcha exists to enforce.
|
||||
id, b64s, _, err := captcha.DriverDigitFunc()
|
||||
if err != nil {
|
||||
e.Logger.Errorf("DriverDigitFunc error, %s", err.Error())
|
||||
e.Error(500, err, "验证码获取失败")
|
||||
|
||||
@@ -11,13 +11,14 @@ const INDEX = `
|
||||
<meta charset="utf-8">
|
||||
<title>GO-ADMIN欢迎您</title>
|
||||
<style>
|
||||
body{
|
||||
margin:0;
|
||||
padding:0;
|
||||
overflow-y:hidden
|
||||
html,body{
|
||||
margin:0;
|
||||
padding:0;
|
||||
height:100%;
|
||||
overflow-y:hidden;
|
||||
}
|
||||
</style>
|
||||
<script src="http://libs.baidu.com/jquery/1.9.0/jquery.js"></script>
|
||||
<script src="https://libs.baidu.com/jquery/1.9.0/jquery.js"></script>
|
||||
<script type="text/javascript">
|
||||
window.onerror=function(){return true;}
|
||||
$(function(){
|
||||
@@ -28,7 +29,7 @@ $(function(){
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<iframe id="iframe" frameborder="0" src="https://doc.go-admin.dev" style="width:100%;"></iframe>
|
||||
<iframe id="iframe" frameborder="0" src="https://www.go-admin.pro" style="width:100%;height:100%;"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
@@ -3,9 +3,9 @@ package apis
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service"
|
||||
|
||||
@@ -3,8 +3,8 @@ package apis
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service"
|
||||
@@ -198,7 +198,7 @@ func (e SysConfig) Get2SysApp(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
// 控制只读前台的数据
|
||||
req.IsFrontend = 1
|
||||
req.IsFrontend = "1"
|
||||
list := make([]models.SysConfig, 0)
|
||||
err = s.GetWithKeyList(&req, &list)
|
||||
if err != nil {
|
||||
@@ -310,4 +310,4 @@ func (e SysConfig) GetSysConfigByKEYForService(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
e.OK(resp, s.Msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package apis
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
"go-admin/app/admin/models"
|
||||
|
||||
"go-admin/app/admin/service"
|
||||
@@ -187,7 +187,7 @@ func (e SysDept) Get2Tree(c *gin.Context) {
|
||||
req := dto.SysDeptGetPageReq{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
Bind(&req,binding.Form).
|
||||
Bind(&req, binding.Form).
|
||||
MakeService(&s.Service).
|
||||
Errors
|
||||
if err != nil {
|
||||
@@ -235,4 +235,4 @@ func (e SysDept) GetDeptTreeRoleSelect(c *gin.Context) {
|
||||
"depts": result,
|
||||
"checkedKeys": menuIds,
|
||||
}, "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package apis
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"go-admin/app/admin/models"
|
||||
|
||||
"go-admin/app/admin/service"
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"go-admin/app/admin/models"
|
||||
|
||||
"go-admin/app/admin/service"
|
||||
|
||||
@@ -3,7 +3,7 @@ package apis
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"go-admin/app/admin/models"
|
||||
|
||||
"go-admin/app/admin/service"
|
||||
|
||||
@@ -3,8 +3,8 @@ package apis
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"go-admin/app/admin/models"
|
||||
|
||||
"go-admin/app/admin/service"
|
||||
@@ -202,44 +202,6 @@ func (e SysMenu) GetMenuRole(c *gin.Context) {
|
||||
e.OK(result, "")
|
||||
}
|
||||
|
||||
//// GetMenuIDS 获取角色对应的菜单id数组
|
||||
//// @Summary 获取角色对应的菜单id数组,设置角色权限使用
|
||||
//// @Description 获取JSON
|
||||
//// @Tags 菜单
|
||||
//// @Param id path int true "id"
|
||||
//// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
|
||||
//// @Router /api/v1/menuids/{id} [get]
|
||||
//// @Security Bearer
|
||||
//func (e SysMenu) GetMenuIDS(c *gin.Context) {
|
||||
// s := new(service.SysMenu)
|
||||
// r := service.SysRole{}
|
||||
// m := dto.SysRoleByName{}
|
||||
// err := e.MakeContext(c).
|
||||
// MakeOrm().
|
||||
// Bind(&m, binding.JSON).
|
||||
// MakeService(&s.Service).
|
||||
// MakeService(&r.Service).
|
||||
// Errors
|
||||
// if err != nil {
|
||||
// e.Logger.Error(err)
|
||||
// e.Error(500, err, err.Error())
|
||||
// return
|
||||
// }
|
||||
// var data models.SysRole
|
||||
// err = r.GetWithName(&m, &data).Error
|
||||
//
|
||||
// //data.RoleName = c.GetString("role")
|
||||
// //data.UpdateBy = user.GetUserId(c)
|
||||
// //result, err := data.GetIDS(s.Orm)
|
||||
//
|
||||
// if err != nil {
|
||||
// e.Logger.Errorf("GetIDS error, %s", err.Error())
|
||||
// e.Error(500, err, "获取失败")
|
||||
// return
|
||||
// }
|
||||
// e.OK(result, "")
|
||||
//}
|
||||
|
||||
// GetMenuTreeSelect 根据角色ID查询菜单下拉树结构
|
||||
// @Summary 角色修改使用的菜单列表
|
||||
// @Description 获取JSON
|
||||
@@ -253,7 +215,7 @@ func (e SysMenu) GetMenuRole(c *gin.Context) {
|
||||
func (e SysMenu) GetMenuTreeSelect(c *gin.Context) {
|
||||
m := service.SysMenu{}
|
||||
r := service.SysRole{}
|
||||
req :=dto.SelectRole{}
|
||||
req := dto.SelectRole{}
|
||||
err := e.MakeContext(c).
|
||||
MakeOrm().
|
||||
MakeService(&m.Service).
|
||||
@@ -284,4 +246,4 @@ func (e SysMenu) GetMenuTreeSelect(c *gin.Context) {
|
||||
"menus": result,
|
||||
"checkedKeys": menuIds,
|
||||
}, "获取成功")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service"
|
||||
"go-admin/app/admin/service/dto"
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service"
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
||||
"go-admin/app/admin/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
|
||||
"go-admin/app/admin/service"
|
||||
"go-admin/app/admin/service/dto"
|
||||
@@ -122,7 +122,7 @@ func (e SysRole) Insert(c *gin.Context) {
|
||||
if req.Status == "" {
|
||||
req.Status = "2"
|
||||
}
|
||||
cb := sdk.Runtime.GetCasbinKey(c.Request.Host)
|
||||
cb := sdk.Runtime.GetCasbinByTenant(c.Request.Host)
|
||||
err = s.Insert(&req, cb)
|
||||
if err != nil {
|
||||
e.Logger.Error(err)
|
||||
@@ -161,7 +161,7 @@ func (e SysRole) Update(c *gin.Context) {
|
||||
e.Error(500, err, err.Error())
|
||||
return
|
||||
}
|
||||
cb := sdk.Runtime.GetCasbinKey(c.Request.Host)
|
||||
cb := sdk.Runtime.GetCasbinByTenant(c.Request.Host)
|
||||
|
||||
req.SetUpdateBy(user.GetUserId(c))
|
||||
|
||||
@@ -203,7 +203,7 @@ func (e SysRole) Delete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
cb := sdk.Runtime.GetCasbinKey(c.Request.Host)
|
||||
cb := sdk.Runtime.GetCasbinByTenant(c.Request.Host)
|
||||
err = s.Remove(&req, cb)
|
||||
if err != nil {
|
||||
e.Logger.Error(err)
|
||||
|
||||
@@ -2,15 +2,14 @@ package apis
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
"go-admin/app/admin/models"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth/user"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"go-admin/app/admin/service"
|
||||
@@ -313,19 +312,12 @@ func (e SysUser) ResetPwd(c *gin.Context) {
|
||||
//数据权限检查
|
||||
p := actions.GetPermissionFromContext(c)
|
||||
|
||||
if req.UserId == 1 && config.ApplicationConfig.Mode == "demo" {
|
||||
req.Password = "123456"
|
||||
}
|
||||
err = s.ResetPwd(&req, p)
|
||||
if err != nil {
|
||||
e.Logger.Error(err)
|
||||
return
|
||||
}
|
||||
if req.UserId == 1 && config.ApplicationConfig.Mode == "demo" {
|
||||
e.OK(req.GetId(), "admin:现在使用的预览环境,休想改掉我!否则会影响其他朋友体验的哦!可以创建其他用户体验该功能!")
|
||||
} else {
|
||||
e.OK(req.GetId(), "更新成功")
|
||||
}
|
||||
e.OK(req.GetId(), "更新成功")
|
||||
}
|
||||
|
||||
// UpdatePwd
|
||||
@@ -354,10 +346,6 @@ func (e SysUser) UpdatePwd(c *gin.Context) {
|
||||
|
||||
// 数据权限检查
|
||||
p := actions.GetPermissionFromContext(c)
|
||||
if user.GetUserId(c) == 1 && config.ApplicationConfig.Mode == "demo" {
|
||||
req.NewPassword = "123456"
|
||||
}
|
||||
|
||||
var hash []byte
|
||||
if hash, err = bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost); err != nil {
|
||||
req.NewPassword = string(hash)
|
||||
@@ -369,11 +357,8 @@ func (e SysUser) UpdatePwd(c *gin.Context) {
|
||||
e.Error(http.StatusForbidden, err, "密码修改失败")
|
||||
return
|
||||
}
|
||||
if user.GetUserId(c) == 1 && config.ApplicationConfig.Mode == "demo" {
|
||||
e.OK(nil, "admin:现在使用的预览环境,休想改掉我!否则会影响其他朋友体验的哦!可以创建其他用户体验该功能!")
|
||||
} else {
|
||||
e.OK(nil, "密码修改成功")
|
||||
}
|
||||
|
||||
e.OK(nil, "密码修改成功")
|
||||
}
|
||||
|
||||
// GetProfile
|
||||
@@ -465,7 +450,7 @@ func (e SysUser) GetInfo(c *gin.Context) {
|
||||
if sysUser.Avatar != "" {
|
||||
mp["avatar"] = sysUser.Avatar
|
||||
}
|
||||
mp["userName"] = sysUser.NickName
|
||||
mp["userName"] = sysUser.Username
|
||||
mp["userId"] = sysUser.UserId
|
||||
mp["deptId"] = sysUser.DeptId
|
||||
mp["name"] = sysUser.NickName
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package models
|
||||
|
||||
//sys_casbin_rule
|
||||
type CasbinRule struct {
|
||||
PType string `json:"p_type" gorm:"size:100;"`
|
||||
V0 string `json:"v0" gorm:"size:100;"`
|
||||
V1 string `json:"v1" gorm:"size:100;"`
|
||||
V2 string `json:"v2" gorm:"size:100;"`
|
||||
V3 string `json:"v3" gorm:"size:100;"`
|
||||
V4 string `json:"v4" gorm:"size:100;"`
|
||||
V5 string `json:"v5" gorm:"size:100;"`
|
||||
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||
Ptype string `gorm:"size:512;uniqueIndex:unique_index"`
|
||||
V0 string `gorm:"size:512;uniqueIndex:unique_index"`
|
||||
V1 string `gorm:"size:512;uniqueIndex:unique_index"`
|
||||
V2 string `gorm:"size:512;uniqueIndex:unique_index"`
|
||||
V3 string `gorm:"size:512;uniqueIndex:unique_index"`
|
||||
V4 string `gorm:"size:512;uniqueIndex:unique_index"`
|
||||
V5 string `gorm:"size:512;uniqueIndex:unique_index"`
|
||||
}
|
||||
|
||||
func (CasbinRule) TableName() string {
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
"gorm.io/gorm"
|
||||
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
)
|
||||
|
||||
type DataPermission struct {
|
||||
DataScope string
|
||||
UserId int
|
||||
DeptId int
|
||||
RoleId int
|
||||
}
|
||||
|
||||
func (e *DataPermission) GetDataScope(tableName string, db *gorm.DB) (*gorm.DB, error) {
|
||||
|
||||
if !config.ApplicationConfig.EnableDP {
|
||||
usageStr := `数据权限已经为您` + pkg.Green(`关闭`) + `,如需开启请参考配置文件字段说明`
|
||||
log.Debug("%s\n", usageStr)
|
||||
return db, nil
|
||||
}
|
||||
user := new(SysUser)
|
||||
role := new(SysRole)
|
||||
err := db.Find(user, e.UserId).Error
|
||||
if err != nil {
|
||||
return nil, errors.New("获取用户数据出错 msg:" + err.Error())
|
||||
}
|
||||
err = db.Find(role, user.RoleId).Error
|
||||
if err != nil {
|
||||
return nil, errors.New("获取用户数据出错 msg:" + err.Error())
|
||||
}
|
||||
if role.DataScope == "2" {
|
||||
db = db.Where(tableName+".create_by in (select sys_user.user_id from sys_role_dept left join sys_user on sys_user.dept_id=sys_role_dept.dept_id where sys_role_dept.role_id = ?)", user.RoleId)
|
||||
}
|
||||
if role.DataScope == "3" {
|
||||
db = db.Where(tableName+".create_by in (SELECT user_id from sys_user where dept_id = ? )", user.DeptId)
|
||||
}
|
||||
if role.DataScope == "4" {
|
||||
db = db.Where(tableName+".create_by in (SELECT user_id from sys_user where sys_user.dept_id in(select dept_id from sys_dept where dept_path like ? ))", "%"+pkg.IntToString(user.DeptId)+"%")
|
||||
}
|
||||
if role.DataScope == "5" || role.DataScope == "" {
|
||||
db = db.Where(tableName+".create_by = ?", e.UserId)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
//func DataScopes(tableName string, userId int) func(db *gorm.DB) *gorm.DB {
|
||||
// return func(db *gorm.DB) *gorm.DB {
|
||||
// user := new(SysUser)
|
||||
// role := new(SysRole)
|
||||
// user.UserId = userId
|
||||
// err := db.Find(user, userId).Error
|
||||
// if err != nil {
|
||||
// db.Error = errors.New("获取用户数据出错 msg:" + err.Error())
|
||||
// return db
|
||||
// }
|
||||
// err = db.Find(role, user.RoleId).Error
|
||||
// if err != nil {
|
||||
// db.Error = errors.New("获取用户数据出错 msg:" + err.Error())
|
||||
// return db
|
||||
// }
|
||||
// if role.DataScope == "2" {
|
||||
// return db.Where(tableName+".create_by in (select sys_user.user_id from sys_role_dept left join sys_user on sys_user.dept_id=sys_role_dept.dept_id where sys_role_dept.role_id = ?)", user.RoleId)
|
||||
// }
|
||||
// if role.DataScope == "3" {
|
||||
// return db.Where(tableName+".create_by in (SELECT user_id from sys_user where dept_id = ? )", user.DeptId)
|
||||
// }
|
||||
// if role.DataScope == "4" {
|
||||
// return db.Where(tableName+".create_by in (SELECT user_id from sys_user where sys_user.dept_id in(select dept_id from sys_dept where dept_path like ? ))", "%"+pkg.IntToString(user.DeptId)+"%")
|
||||
// }
|
||||
// if role.DataScope == "5" || role.DataScope == "" {
|
||||
// return db.Where(tableName+".create_by = ?", userId)
|
||||
// }
|
||||
// return db
|
||||
// }
|
||||
//}
|
||||
@@ -31,7 +31,7 @@ func ExecSql(db *gorm.DB, filePath string) error {
|
||||
fmt.Println(sqlList[i])
|
||||
continue
|
||||
}
|
||||
sql := strings.Replace(sqlList[i]+";", "\n", "", 0)
|
||||
sql := strings.Replace(sqlList[i]+";", "\n", "", -1)
|
||||
sql = strings.TrimSpace(sql)
|
||||
if err = db.Exec(sql).Error; err != nil {
|
||||
log.Printf("error sql: %s", sql)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseModel struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
DeletedAt *time.Time `json:"deletedAt"`
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/bitly/go-simplejson"
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/runtime"
|
||||
"github.com/go-admin-team/go-admin-core/storage"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/runtime"
|
||||
"github.com/go-admin-team/go-admin-core/v2/storage"
|
||||
|
||||
"go-admin/common/models"
|
||||
)
|
||||
@@ -27,7 +27,7 @@ type SysApi struct {
|
||||
models.ControlBy
|
||||
}
|
||||
|
||||
func (SysApi) TableName() string {
|
||||
func (*SysApi) TableName() string {
|
||||
return "sys_api"
|
||||
}
|
||||
|
||||
@@ -44,17 +44,17 @@ func SaveSysApi(message storage.Messager) (err error) {
|
||||
var rb []byte
|
||||
rb, err = json.Marshal(message.GetValues())
|
||||
if err != nil {
|
||||
fmt.Errorf("json Marshal error, %s", err.Error())
|
||||
err = fmt.Errorf("json Marshal error, %v", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
var l runtime.Routers
|
||||
err = json.Unmarshal(rb, &l)
|
||||
if err != nil {
|
||||
fmt.Errorf("json Unmarshal error, %s", err.Error())
|
||||
err = fmt.Errorf("json Unmarshal error, %s", err.Error())
|
||||
return err
|
||||
}
|
||||
dbList := sdk.Runtime.GetDb()
|
||||
dbList := sdk.Runtime.GetAllDb()
|
||||
for _, d := range dbList {
|
||||
for _, v := range l.List {
|
||||
if v.HttpMethod != "HEAD" ||
|
||||
|
||||
@@ -10,13 +10,13 @@ type SysConfig struct {
|
||||
ConfigKey string `json:"configKey" gorm:"size:128;comment:ConfigKey"` //
|
||||
ConfigValue string `json:"configValue" gorm:"size:255;comment:ConfigValue"` //
|
||||
ConfigType string `json:"configType" gorm:"size:64;comment:ConfigType"`
|
||||
IsFrontend int `json:"isFrontend" gorm:"size:64;comment:是否前台"` //
|
||||
IsFrontend string `json:"isFrontend" gorm:"size:64;comment:是否前台"` //
|
||||
Remark string `json:"remark" gorm:"size:128;comment:Remark"` //
|
||||
models.ControlBy
|
||||
models.ModelTime
|
||||
}
|
||||
|
||||
func (SysConfig) TableName() string {
|
||||
func (*SysConfig) TableName() string {
|
||||
return "sys_config"
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ type SysDept struct {
|
||||
ParentId int `json:"parentId" gorm:""` //上级部门
|
||||
DeptPath string `json:"deptPath" gorm:"size:255;"` //
|
||||
DeptName string `json:"deptName" gorm:"size:128;"` //部门名称
|
||||
Sort int `json:"sort" gorm:"size:4;"` //排序
|
||||
Sort int `json:"sort" gorm:"size:4;"` //排序
|
||||
Leader string `json:"leader" gorm:"size:128;"` //负责人
|
||||
Phone string `json:"phone" gorm:"size:11;"` //手机
|
||||
Email string `json:"email" gorm:"size:64;"` //邮箱
|
||||
@@ -19,7 +19,7 @@ type SysDept struct {
|
||||
Children []SysDept `json:"children" gorm:"-"`
|
||||
}
|
||||
|
||||
func (SysDept) TableName() string {
|
||||
func (*SysDept) TableName() string {
|
||||
return "sys_dept"
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ type SysDictData struct {
|
||||
models.ModelTime
|
||||
}
|
||||
|
||||
func (SysDictData) TableName() string {
|
||||
func (*SysDictData) TableName() string {
|
||||
return "sys_dict_data"
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ type SysDictType struct {
|
||||
models.ModelTime
|
||||
}
|
||||
|
||||
func (SysDictType) TableName() string {
|
||||
func (*SysDictType) TableName() string {
|
||||
return "sys_dict_type"
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/storage"
|
||||
log "github.com/go-admin-team/go-admin-core/v2/logger"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/v2/storage"
|
||||
|
||||
"go-admin/common/models"
|
||||
)
|
||||
@@ -29,7 +29,7 @@ type SysLoginLog struct {
|
||||
models.ControlBy
|
||||
}
|
||||
|
||||
func (SysLoginLog) TableName() string {
|
||||
func (*SysLoginLog) TableName() string {
|
||||
return "sys_login_log"
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (e *SysLoginLog) GetId() interface{} {
|
||||
// SaveLoginLog 从队列中获取登录日志
|
||||
func SaveLoginLog(message storage.Messager) (err error) {
|
||||
//准备db
|
||||
db := sdk.Runtime.GetDbByKey(message.GetPrefix())
|
||||
db := sdk.Runtime.GetDbByTenant(message.GetPrefix())
|
||||
if db == nil {
|
||||
err = errors.New("db not exist")
|
||||
log.Errorf("host[%s]'s %s", message.GetPrefix(), err.Error())
|
||||
|
||||
@@ -30,7 +30,13 @@ type SysMenu struct {
|
||||
models.ModelTime
|
||||
}
|
||||
|
||||
func (SysMenu) TableName() string {
|
||||
type SysMenuSlice []SysMenu
|
||||
|
||||
func (x SysMenuSlice) Len() int { return len(x) }
|
||||
func (x SysMenuSlice) Less(i, j int) bool { return x[i].Sort < x[j].Sort }
|
||||
func (x SysMenuSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
|
||||
func (*SysMenu) TableName() string {
|
||||
return "sys_menu"
|
||||
}
|
||||
|
||||
@@ -41,4 +47,4 @@ func (e *SysMenu) Generate() models.ActiveRecord {
|
||||
|
||||
func (e *SysMenu) GetId() interface{} {
|
||||
return e.MenuId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/storage"
|
||||
log "github.com/go-admin-team/go-admin-core/v2/logger"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/v2/storage"
|
||||
|
||||
"go-admin/common/models"
|
||||
)
|
||||
@@ -18,15 +18,15 @@ type SysOperaLog struct {
|
||||
BusinessType string `json:"businessType" gorm:"size:128;comment:操作类型"`
|
||||
BusinessTypes string `json:"businessTypes" gorm:"size:128;comment:BusinessTypes"`
|
||||
Method string `json:"method" gorm:"size:128;comment:函数"`
|
||||
RequestMethod string `json:"requestMethod" gorm:"size:128;comment:请求方式"`
|
||||
RequestMethod string `json:"requestMethod" gorm:"size:128;comment:请求方式 GET POST PUT DELETE"`
|
||||
OperatorType string `json:"operatorType" gorm:"size:128;comment:操作类型"`
|
||||
OperName string `json:"operName" gorm:"size:128;comment:操作者"`
|
||||
DeptName string `json:"deptName" gorm:"size:128;comment:部门名称"`
|
||||
OperUrl string `json:"operUrl" gorm:"size:255;comment:访问地址"`
|
||||
OperIp string `json:"operIp" gorm:"size:128;comment:客户端ip"`
|
||||
OperLocation string `json:"operLocation" gorm:"size:128;comment:访问位置"`
|
||||
OperParam string `json:"operParam" gorm:"size:255;comment:请求参数"`
|
||||
Status string `json:"status" gorm:"size:4;comment:操作状态"`
|
||||
OperParam string `json:"operParam" gorm:"text;comment:请求参数"`
|
||||
Status string `json:"status" gorm:"size:4;comment:操作状态 1:正常 2:关闭"`
|
||||
OperTime time.Time `json:"operTime" gorm:"comment:操作时间"`
|
||||
JsonResult string `json:"jsonResult" gorm:"size:255;comment:返回数据"`
|
||||
Remark string `json:"remark" gorm:"size:255;comment:备注"`
|
||||
@@ -37,7 +37,7 @@ type SysOperaLog struct {
|
||||
models.ControlBy
|
||||
}
|
||||
|
||||
func (SysOperaLog) TableName() string {
|
||||
func (*SysOperaLog) TableName() string {
|
||||
return "sys_opera_log"
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func (e *SysOperaLog) GetId() interface{} {
|
||||
// SaveOperaLog 从队列中获取操作日志
|
||||
func SaveOperaLog(message storage.Messager) (err error) {
|
||||
//准备db
|
||||
db := sdk.Runtime.GetDbByKey(message.GetPrefix())
|
||||
db := sdk.Runtime.GetDbByTenant(message.GetPrefix())
|
||||
if db == nil {
|
||||
err = errors.New("db not exist")
|
||||
log.Errorf("host[%s]'s %s", message.GetPrefix(), err.Error())
|
||||
|
||||
@@ -16,7 +16,7 @@ type SysPost struct {
|
||||
Params string `gorm:"-" json:"params"`
|
||||
}
|
||||
|
||||
func (SysPost) TableName() string {
|
||||
func (*SysPost) TableName() string {
|
||||
return "sys_post"
|
||||
}
|
||||
|
||||
@@ -27,4 +27,4 @@ func (e *SysPost) Generate() models.ActiveRecord {
|
||||
|
||||
func (e *SysPost) GetId() interface{} {
|
||||
return e.PostId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,25 +3,25 @@ package models
|
||||
import "go-admin/common/models"
|
||||
|
||||
type SysRole struct {
|
||||
RoleId int `json:"roleId" gorm:"primaryKey;autoIncrement"` // 角色编码
|
||||
RoleName string `json:"roleName" gorm:"size:128;"` // 角色名称
|
||||
Status string `json:"status" gorm:"size:4;"` //
|
||||
RoleKey string `json:"roleKey" gorm:"size:128;"` //角色代码
|
||||
RoleSort int `json:"roleSort" gorm:""` //角色排序
|
||||
Flag string `json:"flag" gorm:"size:128;"` //
|
||||
Remark string `json:"remark" gorm:"size:255;"` //备注
|
||||
Admin bool `json:"admin" gorm:"size:4;"`
|
||||
DataScope string `json:"dataScope" gorm:"size:128;"`
|
||||
Params string `json:"params" gorm:"-"`
|
||||
MenuIds []int `json:"menuIds" gorm:"-"`
|
||||
DeptIds []int `json:"deptIds" gorm:"-"`
|
||||
SysDept []SysDept `json:"sysDept" gorm:"many2many:sys_role_dept;foreignKey:RoleId;joinForeignKey:role_id;references:DeptId;joinReferences:dept_id;"`
|
||||
RoleId int `json:"roleId" gorm:"primaryKey;autoIncrement"` // 角色编码
|
||||
RoleName string `json:"roleName" gorm:"size:128;"` // 角色名称
|
||||
Status string `json:"status" gorm:"size:4;"` // 状态 1禁用 2正常
|
||||
RoleKey string `json:"roleKey" gorm:"size:128;"` //角色代码
|
||||
RoleSort int `json:"roleSort" gorm:""` //角色排序
|
||||
Flag string `json:"flag" gorm:"size:128;"` //
|
||||
Remark string `json:"remark" gorm:"size:255;"` //备注
|
||||
Admin bool `json:"admin" gorm:"size:4;"`
|
||||
DataScope string `json:"dataScope" gorm:"size:128;"`
|
||||
Params string `json:"params" gorm:"-"`
|
||||
MenuIds []int `json:"menuIds" gorm:"-"`
|
||||
DeptIds []int `json:"deptIds" gorm:"-"`
|
||||
SysDept []SysDept `json:"sysDept" gorm:"many2many:sys_role_dept;foreignKey:RoleId;joinForeignKey:role_id;references:DeptId;joinReferences:dept_id;"`
|
||||
SysMenu *[]SysMenu `json:"sysMenu" gorm:"many2many:sys_role_menu;foreignKey:RoleId;joinForeignKey:role_id;references:MenuId;joinReferences:menu_id;"`
|
||||
models.ControlBy
|
||||
models.ModelTime
|
||||
}
|
||||
|
||||
func (SysRole) TableName() string {
|
||||
func (*SysRole) TableName() string {
|
||||
return "sys_role"
|
||||
}
|
||||
|
||||
@@ -32,4 +32,4 @@ func (e *SysRole) Generate() models.ActiveRecord {
|
||||
|
||||
func (e *SysRole) GetId() interface{} {
|
||||
return e.RoleId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type SysUser struct {
|
||||
models.ModelTime
|
||||
}
|
||||
|
||||
func (SysUser) TableName() string {
|
||||
func (*SysUser) TableName() string {
|
||||
return "sys_user"
|
||||
}
|
||||
|
||||
@@ -42,19 +42,35 @@ func (e *SysUser) GetId() interface{} {
|
||||
return e.UserId
|
||||
}
|
||||
|
||||
//加密
|
||||
func (e *SysUser) Encrypt() (err error) {
|
||||
// Encrypt hashes Password, unless it already holds a hash.
|
||||
//
|
||||
// The hooks below run on whatever is in the struct, and a user read from the
|
||||
// database carries the stored hash in that field. Hashing it again produces a
|
||||
// hash of a hash, and the password that user knows no longer matches anything:
|
||||
// they cannot log in, and nothing reports an error. The only thing preventing
|
||||
// that today is an Omit("password") on the one update that loads a user first,
|
||||
// which makes every other write to this model one line away from destroying
|
||||
// credentials.
|
||||
//
|
||||
// bcrypt.Cost parses a hash and fails on anything else, so it distinguishes
|
||||
// the two cases without the call site having to say which it is. The cost is
|
||||
// that a password which is itself a well-formed bcrypt hash would be stored
|
||||
// unchanged - a 60-character string beginning "$2a$", not something a person
|
||||
// types, and it grants whoever set it no access they did not already have.
|
||||
func (e *SysUser) Encrypt() error {
|
||||
if e.Password == "" {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if _, err := bcrypt.Cost([]byte(e.Password)); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var hash []byte
|
||||
if hash, err = bcrypt.GenerateFromPassword([]byte(e.Password), bcrypt.DefaultCost); err != nil {
|
||||
return
|
||||
} else {
|
||||
e.Password = string(hash)
|
||||
return
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(e.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.Password = string(hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *SysUser) BeforeCreate(_ *gorm.DB) error {
|
||||
@@ -62,11 +78,7 @@ func (e *SysUser) BeforeCreate(_ *gorm.DB) error {
|
||||
}
|
||||
|
||||
func (e *SysUser) BeforeUpdate(_ *gorm.DB) error {
|
||||
var err error
|
||||
if e.Password != "" {
|
||||
err = e.Encrypt()
|
||||
}
|
||||
return err
|
||||
return e.Encrypt()
|
||||
}
|
||||
|
||||
func (e *SysUser) AfterFind(_ *gorm.DB) error {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const knownPassword = "correct-horse-battery-staple"
|
||||
|
||||
// A user loaded from the database carries the stored hash in Password, and the
|
||||
// hooks run on whatever is in the struct. Hashing it a second time produces a
|
||||
// hash of a hash: the password the user knows stops matching, they cannot log
|
||||
// in, and nothing reports an error.
|
||||
//
|
||||
// Only an Omit("password") on one call site stood between this and every write
|
||||
// to the model. This is the test that removes the need for it.
|
||||
func TestEncryptLeavesAnAlreadyHashedPasswordAlone(t *testing.T) {
|
||||
fresh := SysUser{Password: knownPassword}
|
||||
if err := fresh.Encrypt(); err != nil {
|
||||
t.Fatalf("Encrypt: %v", err)
|
||||
}
|
||||
stored := fresh.Password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(knownPassword)); err != nil {
|
||||
t.Fatalf("setup failed: the password was not hashed: %v", err)
|
||||
}
|
||||
|
||||
// What a query puts in the struct, and what an update then hands the hook.
|
||||
loaded := SysUser{Password: stored}
|
||||
if err := loaded.Encrypt(); err != nil {
|
||||
t.Fatalf("Encrypt on a loaded user: %v", err)
|
||||
}
|
||||
if loaded.Password != stored {
|
||||
t.Error("Encrypt re-hashed a stored hash; the user can no longer log in")
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(loaded.Password), []byte(knownPassword)); err != nil {
|
||||
t.Errorf("the user can no longer log in with their password: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The other half: a password that is not a hash still gets hashed, on create
|
||||
// and on update alike.
|
||||
func TestEncryptHashesAPlaintextPassword(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
hook func(*SysUser) error
|
||||
}{
|
||||
{"BeforeCreate", func(u *SysUser) error { return u.BeforeCreate(nil) }},
|
||||
{"BeforeUpdate", func(u *SysUser) error { return u.BeforeUpdate(nil) }},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
u := SysUser{Password: knownPassword}
|
||||
if err := c.hook(&u); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u.Password == knownPassword {
|
||||
t.Fatal("the password was stored as it was typed")
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(knownPassword)); err != nil {
|
||||
t.Errorf("the stored value does not verify the password: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An empty Password means "not being set", and must not become a hash of "".
|
||||
func TestEncryptIgnoresAnEmptyPassword(t *testing.T) {
|
||||
u := SysUser{}
|
||||
if err := u.Encrypt(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u.Password != "" {
|
||||
t.Errorf("an unset password became %q", u.Password)
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt runs on every update of this model, including the ones that change
|
||||
// something else entirely. What it costs when there is nothing to do is the
|
||||
// difference between a profile update and a bcrypt round; the correctness test
|
||||
// above is what catches a regression, this reports the size of it.
|
||||
func BenchmarkEncrypt(b *testing.B) {
|
||||
fresh := SysUser{Password: knownPassword}
|
||||
if err := fresh.Encrypt(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.Run("already hashed", func(b *testing.B) {
|
||||
u := SysUser{Password: fresh.Password}
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := u.Encrypt(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("plaintext", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
u := SysUser{Password: knownPassword}
|
||||
if err := u.Encrypt(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
log "github.com/go-admin-team/go-admin-core/v2/logger"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
||||
common "go-admin/common/middleware"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package router
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -12,7 +12,6 @@ var (
|
||||
routerCheckRole = make([]func(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware), 0)
|
||||
)
|
||||
|
||||
// 路由示例
|
||||
func InitExamplesRouter(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) *gin.Engine {
|
||||
|
||||
// 无需认证的路由
|
||||
@@ -39,4 +38,4 @@ func examplesCheckRoleRouter(r *gin.Engine, authMiddleware *jwtauth.GinJWTMiddle
|
||||
for _, f := range routerCheckRole {
|
||||
f(v1, authMiddleware)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
|
||||
"go-admin/app/admin/apis"
|
||||
"go-admin/common/middleware"
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"go-admin/common/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
"go-admin/app/admin/apis"
|
||||
"go-admin/common/middleware"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
"go-admin/app/admin/apis"
|
||||
"go-admin/common/middleware"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
"go-admin/app/admin/apis"
|
||||
"go-admin/common/middleware"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
"go-admin/app/admin/apis"
|
||||
"go-admin/common/middleware"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
"go-admin/app/admin/apis"
|
||||
"go-admin/common/middleware"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
"go-admin/app/admin/apis"
|
||||
"go-admin/common/middleware"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
|
||||
"go-admin/app/admin/apis"
|
||||
"go-admin/common/middleware"
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
"go-admin/app/admin/apis"
|
||||
"mime"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/ws"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg/ws"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
"github.com/swaggo/gin-swagger/swaggerFiles"
|
||||
|
||||
swaggerfiles "github.com/swaggo/files"
|
||||
|
||||
"go-admin/common/middleware"
|
||||
"go-admin/common/middleware/handler"
|
||||
_ "go-admin/docs"
|
||||
_ "go-admin/docs/admin"
|
||||
)
|
||||
|
||||
func InitSysRouter(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) *gin.RouterGroup {
|
||||
@@ -54,11 +56,11 @@ func sysStaticFileRouter(r *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
func sysSwaggerRouter(r *gin.RouterGroup) {
|
||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
r.GET("/swagger/admin/*any", ginSwagger.WrapHandler(swaggerfiles.NewHandler(), ginSwagger.InstanceName("admin")))
|
||||
}
|
||||
|
||||
func sysCheckRoleRouterInit(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
|
||||
wss:=r.Group("").Use(authMiddleware.MiddlewareFunc())
|
||||
wss := r.Group("").Use(authMiddleware.MiddlewareFunc())
|
||||
{
|
||||
wss.GET("/ws/:id/:channel", ws.WebsocketManager.WsClient)
|
||||
wss.GET("/wslogout/:id/:channel", ws.WebsocketManager.UnWsClient)
|
||||
@@ -67,8 +69,15 @@ func sysCheckRoleRouterInit(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddle
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
v1.POST("/login", authMiddleware.LoginHandler)
|
||||
// Refresh time can be longer than token timeout
|
||||
v1.GET("/refresh_token", authMiddleware.RefreshHandler)
|
||||
// GET /api/v1/refresh_token 已移除,原因见 issue #820:
|
||||
// 该接口用业务 token 即可换取新 token,而续期上限 MaxRefresh 依据的
|
||||
// orig_iat 在每次续期时被一并重置,上限永远无法到达 —— token 一旦泄
|
||||
// 露即等同于永久访问权。它此前还位于 CasbinExclude 中,任何角色的已
|
||||
// 登录用户都能调用,不受权限约束。
|
||||
//
|
||||
// 官方前端从未调用该接口(store 中的 refreshToken action 无人 dispatch),
|
||||
// 移除不影响正常使用。若确需无感续期,应在 go-admin-core 中区分
|
||||
// access token 与 refresh token 后重新实现,而非沿用此路由。
|
||||
}
|
||||
registerBaseRouter(v1, authMiddleware)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
"go-admin/app/admin/apis"
|
||||
"go-admin/common/actions"
|
||||
"go-admin/common/middleware"
|
||||
|
||||
@@ -11,8 +11,9 @@ type SysApiGetPageReq struct {
|
||||
dto.Pagination `search:"-"`
|
||||
Title string `form:"title" search:"type:contains;column:title;table:sys_api" comment:"标题"`
|
||||
Path string `form:"path" search:"type:contains;column:path;table:sys_api" comment:"地址"`
|
||||
Action string `form:"action" search:"type:exact;column:action;table:sys_api" comment:"类型"`
|
||||
Action string `form:"action" search:"type:exact;column:action;table:sys_api" comment:"请求方式"`
|
||||
ParentId string `form:"parentId" search:"type:exact;column:parent_id;table:sys_api" comment:"按钮id"`
|
||||
Type string `form:"type" search:"-" comment:"类型"`
|
||||
SysApiOrder
|
||||
}
|
||||
|
||||
@@ -84,7 +85,6 @@ func (s *SysApiGetReq) GetId() interface{} {
|
||||
return s.Id
|
||||
}
|
||||
|
||||
|
||||
// SysApiDeleteReq 功能删除请求参数
|
||||
type SysApiDeleteReq struct {
|
||||
Ids []int `json:"ids"`
|
||||
@@ -93,4 +93,3 @@ type SysApiDeleteReq struct {
|
||||
func (s *SysApiDeleteReq) GetId() interface{} {
|
||||
return s.Ids
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ type SysConfigGetPageReq struct {
|
||||
ConfigName string `form:"configName" search:"type:contains;column:config_name;table:sys_config"`
|
||||
ConfigKey string `form:"configKey" search:"type:contains;column:config_key;table:sys_config"`
|
||||
ConfigType string `form:"configType" search:"type:exact;column:config_type;table:sys_config"`
|
||||
IsFrontend int `form:"isFrontend" search:"type:exact;column:is_frontend;table:sys_config"`
|
||||
IsFrontend string `form:"isFrontend" search:"type:exact;column:is_frontend;table:sys_config"`
|
||||
SysConfigOrder
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func (m *SysConfigGetPageReq) GetNeedSearch() interface{} {
|
||||
}
|
||||
|
||||
type SysConfigGetToSysAppReq struct {
|
||||
IsFrontend int `form:"isFrontend" search:"type:exact;column:is_frontend;table:sys_config"`
|
||||
IsFrontend string `form:"isFrontend" search:"type:exact;column:is_frontend;table:sys_config"`
|
||||
}
|
||||
|
||||
func (m *SysConfigGetToSysAppReq) GetNeedSearch() interface{} {
|
||||
@@ -43,7 +43,7 @@ type SysConfigControl struct {
|
||||
ConfigKey string `uri:"configKey" json:"configKey" comment:""`
|
||||
ConfigValue string `json:"configValue" comment:""`
|
||||
ConfigType string `json:"configType" comment:""`
|
||||
IsFrontend int `json:"isFrontend"`
|
||||
IsFrontend string `json:"isFrontend"`
|
||||
Remark string `json:"remark" comment:""`
|
||||
common.ControlBy
|
||||
}
|
||||
|
||||
@@ -5,19 +5,29 @@ import (
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/common/dto"
|
||||
"go-admin/common/global"
|
||||
common "go-admin/common/models"
|
||||
)
|
||||
|
||||
// Deprecated: use global.OperaStatusEnabled / global.OperaStatusDisabled.
|
||||
// These two names are kept - misspelling and all - because forks import them;
|
||||
// the values moved to common/global so common/middleware no longer has to
|
||||
// import this package. See docs/contract.md.
|
||||
const (
|
||||
OperaStatusEnabel = global.OperaStatusEnabled // 状态-正常
|
||||
OperaStatusDisable = global.OperaStatusDisabled // 状态-关闭
|
||||
)
|
||||
|
||||
type SysOperaLogGetPageReq struct {
|
||||
dto.Pagination `search:"-"`
|
||||
Title string `form:"title" search:"type:contains;column:title;table:sys_opera_log" comment:"操作模块"`
|
||||
Method string `form:"method" search:"type:contains;column:method;table:sys_opera_log" comment:"函数"`
|
||||
RequestMethod string `form:"requestMethod" search:"type:contains;column:request_method;table:sys_opera_log" comment:"请求方式"`
|
||||
RequestMethod string `form:"requestMethod" search:"type:contains;column:request_method;table:sys_opera_log" comment:"请求方式: GET POST PUT DELETE"`
|
||||
OperUrl string `form:"operUrl" search:"type:contains;column:oper_url;table:sys_opera_log" comment:"访问地址"`
|
||||
OperIp string `form:"operIp" search:"type:exact;column:oper_ip;table:sys_opera_log" comment:"客户端ip"`
|
||||
Status int `form:"status" search:"type:exact;column:status;table:sys_opera_log" comment:"状态"`
|
||||
BeginTime string `form:"beginTime" search:"type:gte;column:ctime;table:sys_opera_log" comment:"创建时间"`
|
||||
EndTime string `form:"endTime" search:"type:lte;column:ctime;table:sys_opera_log" comment:"创建时间"`
|
||||
Status int `form:"status" search:"type:exact;column:status;table:sys_opera_log" comment:"状态 1:正常 2:关闭"`
|
||||
BeginTime string `form:"beginTime" search:"type:gte;column:created_at;table:sys_opera_log" comment:"创建时间"`
|
||||
EndTime string `form:"endTime" search:"type:lte;column:created_at;table:sys_opera_log" comment:"更新时间"`
|
||||
SysOperaLogOrder
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go-admin/common/global"
|
||||
)
|
||||
|
||||
// The values moved to common/global so common/middleware would stop importing
|
||||
// this package; these two names stayed behind as aliases, misspelling and all,
|
||||
// because forks import them.
|
||||
//
|
||||
// If they ever drift apart, rows written through the two spellings land in
|
||||
// different buckets and the operation-log filter silently misses half of them.
|
||||
func TestDeprecatedStatusAliasesStillMatch(t *testing.T) {
|
||||
if OperaStatusEnabel != global.OperaStatusEnabled {
|
||||
t.Errorf("OperaStatusEnabel = %q, global.OperaStatusEnabled = %q",
|
||||
OperaStatusEnabel, global.OperaStatusEnabled)
|
||||
}
|
||||
if OperaStatusDisable != global.OperaStatusDisabled {
|
||||
t.Errorf("OperaStatusDisable = %q, global.OperaStatusDisabled = %q",
|
||||
OperaStatusDisable, global.OperaStatusDisabled)
|
||||
}
|
||||
}
|
||||
@@ -36,13 +36,13 @@ func (m *SysRoleGetPageReq) GetNeedSearch() interface{} {
|
||||
type SysRoleInsertReq struct {
|
||||
RoleId int `uri:"id" comment:"角色编码"` // 角色编码
|
||||
RoleName string `form:"roleName" comment:"角色名称"` // 角色名称
|
||||
Status string `form:"status" comment:"状态"` // 状态
|
||||
Status string `form:"status" comment:"状态"` // 状态 1禁用 2正常
|
||||
RoleKey string `form:"roleKey" comment:"角色代码"` // 角色代码
|
||||
RoleSort int `form:"roleSort" comment:"角色排序"` // 角色排序
|
||||
Flag string `form:"flag" comment:"标记"` // 标记
|
||||
Remark string `form:"remark" comment:"备注"` // 备注
|
||||
Admin bool `form:"admin" comment:"是否管理员"`
|
||||
DataScope string `form:"dataScope"`
|
||||
DataScope string `form:"dataScope" vd:"$=='1'||$=='2'||$=='3'||$=='4'||$=='5'"` // must be one of actions.DataScope{All,Custom,Dept,DeptTree,Self}; PRD 006 F14/H2
|
||||
SysMenu []models.SysMenu `form:"sysMenu"`
|
||||
MenuIds []int `form:"menuIds"`
|
||||
SysDept []models.SysDept `form:"sysDept"`
|
||||
@@ -79,7 +79,7 @@ type SysRoleUpdateReq struct {
|
||||
Flag string `form:"flag" comment:"标记"` // 标记
|
||||
Remark string `form:"remark" comment:"备注"` // 备注
|
||||
Admin bool `form:"admin" comment:"是否管理员"`
|
||||
DataScope string `form:"dataScope"`
|
||||
DataScope string `form:"dataScope" vd:"$=='1'||$=='2'||$=='3'||$=='4'||$=='5'"` // must be one of actions.DataScope{All,Custom,Dept,DeptTree,Self}; PRD 006 F14/H2
|
||||
SysMenu []models.SysMenu `form:"sysMenu"`
|
||||
MenuIds []int `form:"menuIds"`
|
||||
SysDept []models.SysDept `form:"sysDept"`
|
||||
@@ -147,7 +147,7 @@ func (s *SysRoleDeleteReq) GetId() interface{} {
|
||||
// RoleDataScopeReq 角色数据权限修改
|
||||
type RoleDataScopeReq struct {
|
||||
RoleId int `json:"roleId" binding:"required"`
|
||||
DataScope string `json:"dataScope" binding:"required"`
|
||||
DataScope string `json:"dataScope" binding:"required" vd:"$=='1'||$=='2'||$=='3'||$=='4'||$=='5'"` // must be one of actions.DataScope{All,Custom,Dept,DeptTree,Self}; PRD 006 F14/H2
|
||||
DeptIds []int `json:"deptIds"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
vd "github.com/bytedance/go-tagexpr/v2/validator"
|
||||
)
|
||||
|
||||
// api.Bind calls vd.Validate unconditionally on every request, regardless of
|
||||
// which binding stage ran, so a vd tag on DataScope is enough to reject
|
||||
// anything actions.Permission's fail-closed default would otherwise have to
|
||||
// deal with. PRD 006 F14/H2 named this the real trigger for the default
|
||||
// branch: SysRoleInsertReq.DataScope had no validation at all, so leaving
|
||||
// dataScope out of a create-role request wrote an empty string straight to
|
||||
// sys_role.
|
||||
func TestDataScopeRejectsWhatPermissionCannotRecognize(t *testing.T) {
|
||||
invalid := []string{"", "0", "6", "all", " 1", "1 "}
|
||||
valid := []string{"1", "2", "3", "4", "5"}
|
||||
|
||||
t.Run("SysRoleInsertReq", func(t *testing.T) {
|
||||
for _, s := range invalid {
|
||||
req := SysRoleInsertReq{RoleName: "r", RoleKey: "r", DataScope: s}
|
||||
if err := vd.Validate(&req); err == nil {
|
||||
t.Errorf("DataScope %q was accepted, want rejected", s)
|
||||
}
|
||||
}
|
||||
for _, s := range valid {
|
||||
req := SysRoleInsertReq{RoleName: "r", RoleKey: "r", DataScope: s}
|
||||
if err := vd.Validate(&req); err != nil {
|
||||
t.Errorf("DataScope %q was rejected: %v", s, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SysRoleUpdateReq", func(t *testing.T) {
|
||||
for _, s := range invalid {
|
||||
req := SysRoleUpdateReq{RoleName: "r", RoleKey: "r", DataScope: s}
|
||||
if err := vd.Validate(&req); err == nil {
|
||||
t.Errorf("DataScope %q was accepted, want rejected", s)
|
||||
}
|
||||
}
|
||||
for _, s := range valid {
|
||||
req := SysRoleUpdateReq{RoleName: "r", RoleKey: "r", DataScope: s}
|
||||
if err := vd.Validate(&req); err != nil {
|
||||
t.Errorf("DataScope %q was rejected: %v", s, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RoleDataScopeReq", func(t *testing.T) {
|
||||
for _, s := range invalid {
|
||||
req := RoleDataScopeReq{RoleId: 1, DataScope: s}
|
||||
if err := vd.Validate(&req); err == nil {
|
||||
t.Errorf("DataScope %q was accepted, want rejected", s)
|
||||
}
|
||||
}
|
||||
for _, s := range valid {
|
||||
req := RoleDataScopeReq{RoleId: 1, DataScope: s}
|
||||
if err := vd.Validate(&req); err != nil {
|
||||
t.Errorf("DataScope %q was rejected: %v", s, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -121,6 +121,7 @@ func (s *SysUserInsertReq) Generate(model *models.SysUser) {
|
||||
model.PostId = s.PostId
|
||||
model.Remark = s.Remark
|
||||
model.Status = s.Status
|
||||
model.CreateBy = s.CreateBy
|
||||
}
|
||||
|
||||
func (s *SysUserInsertReq) GetId() interface{} {
|
||||
@@ -185,4 +186,4 @@ func (s *SysUserById) GenerateM() (common.ActiveRecord, error) {
|
||||
type PassWord struct {
|
||||
NewPassword string `json:"newPassword" vd:"len($)>0"`
|
||||
OldPassword string `json:"oldPassword" vd:"len($)>0"`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/runtime"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/runtime"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service/dto"
|
||||
"go-admin/common/actions"
|
||||
cDto "go-admin/common/dto"
|
||||
"go-admin/common/global"
|
||||
)
|
||||
|
||||
type SysApi struct {
|
||||
@@ -23,13 +24,25 @@ func (e *SysApi) GetPage(c *dto.SysApiGetPageReq, p *actions.DataPermission, lis
|
||||
var err error
|
||||
var data models.SysApi
|
||||
|
||||
err = e.Orm.Debug().Model(&data).
|
||||
orm := e.Orm.Debug().Model(&data).
|
||||
Scopes(
|
||||
cDto.MakeCondition(c.GetNeedSearch()),
|
||||
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
|
||||
actions.Permission(data.TableName(), p),
|
||||
).
|
||||
Find(list).Limit(-1).Offset(-1).
|
||||
)
|
||||
if c.Type != "" {
|
||||
qType := c.Type
|
||||
if qType == "暂无" {
|
||||
qType = ""
|
||||
}
|
||||
if global.Driver == "postgres" {
|
||||
orm = orm.Where("type = ?", qType)
|
||||
} else {
|
||||
orm = orm.Where("`type` = ?", qType)
|
||||
}
|
||||
|
||||
}
|
||||
err = orm.Find(list).Limit(-1).Offset(-1).
|
||||
Count(count).Error
|
||||
if err != nil {
|
||||
e.Log.Errorf("Service GetSysApiPage error:%s", err)
|
||||
@@ -45,15 +58,15 @@ func (e *SysApi) Get(d *dto.SysApiGetReq, p *actions.DataPermission, model *mode
|
||||
Scopes(
|
||||
actions.Permission(data.TableName(), p),
|
||||
).
|
||||
First(model, d.GetId()).Error
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = errors.New("查看对象不存在或无权查看")
|
||||
e.Log.Errorf("Service GetSysApi error:%s", err)
|
||||
FirstOrInit(model, d.GetId()).Error
|
||||
if err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
_ = e.AddError(err)
|
||||
return e
|
||||
}
|
||||
if err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
if model.Id == 0 {
|
||||
err = errors.New("查看对象不存在或无权查看")
|
||||
e.Log.Errorf("Service GetSysApi error: %s", err)
|
||||
_ = e.AddError(err)
|
||||
return e
|
||||
}
|
||||
@@ -63,9 +76,18 @@ func (e *SysApi) Get(d *dto.SysApiGetReq, p *actions.DataPermission, model *mode
|
||||
// Update 修改SysApi对象
|
||||
func (e *SysApi) Update(c *dto.SysApiUpdateReq, p *actions.DataPermission) error {
|
||||
var model = models.SysApi{}
|
||||
db := e.Orm.Debug().First(&model, c.GetId())
|
||||
if db.RowsAffected == 0 {
|
||||
return errors.New("无权更新该数据")
|
||||
db := e.Orm.Scopes(
|
||||
actions.Permission(model.TableName(), p),
|
||||
).First(&model, c.GetId())
|
||||
if err := db.Error; err != nil {
|
||||
// First reports a row the data permission excluded exactly as it
|
||||
// reports one that does not exist, and the caller should not be able
|
||||
// to tell those apart either.
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("无权更新该数据")
|
||||
}
|
||||
e.Log.Errorf("Service UpdateSysApi error:%s", err)
|
||||
return err
|
||||
}
|
||||
c.Generate(&model)
|
||||
db = e.Orm.Save(&model)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/go-admin-team/go-admin-core/v2/logger"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service/dto"
|
||||
"go-admin/common/actions"
|
||||
)
|
||||
|
||||
// An update the data permission excludes has to be refused, and refused in a
|
||||
// way that does not tell the caller whether the row exists. First reports both
|
||||
// cases the same way - no rows - so the message has to come from there rather
|
||||
// than from a RowsAffected check the error return has already skipped past.
|
||||
func TestSysApiUpdateRefusesARowOutsideTheDataPermission(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file:sysapi-perm?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Skipf("sqlite unavailable: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&models.SysApi{}); err != nil {
|
||||
t.Skipf("automigrate: %v", err)
|
||||
}
|
||||
|
||||
prev := config.ApplicationConfig.EnableDP
|
||||
config.ApplicationConfig.EnableDP = true
|
||||
t.Cleanup(func() { config.ApplicationConfig.EnableDP = prev })
|
||||
|
||||
// Owned by user 1.
|
||||
row := models.SysApi{Handle: "h", Title: "t", Path: "/api/v1/probe", Type: "BUS", Action: "GET"}
|
||||
row.CreateBy = 1
|
||||
if err := db.Create(&row).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
e := &SysApi{Service: service.Service{Orm: db, Log: logger.NewHelper(logger.DefaultLogger)}}
|
||||
req := &dto.SysApiUpdateReq{Id: row.Id, Title: "changed"}
|
||||
|
||||
// User 2, scope 5: only rows they created.
|
||||
outsider := &actions.DataPermission{DataScope: "5", UserId: 2, DeptId: 1, RoleId: 2}
|
||||
err = e.Update(req, outsider)
|
||||
if err == nil {
|
||||
t.Fatal("the update was allowed on a row the data permission excludes")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "无权更新该数据") {
|
||||
t.Errorf("refused with %q, want the permission message; a raw database error tells the "+
|
||||
"caller the row exists", err)
|
||||
}
|
||||
|
||||
var after models.SysApi
|
||||
if err := db.First(&after, row.Id).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after.Title != "t" {
|
||||
t.Errorf("the row was modified: title is now %q", after.Title)
|
||||
}
|
||||
|
||||
// The owner still gets through, so the scope is refusing rather than
|
||||
// everything failing.
|
||||
owner := &actions.DataPermission{DataScope: "5", UserId: 1, DeptId: 1, RoleId: 1}
|
||||
if err := e.Update(&dto.SysApiUpdateReq{Id: row.Id, Title: "by owner"}, owner); err != nil {
|
||||
t.Fatalf("the owner could not update their own row: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"go-admin/app/admin/service/dto"
|
||||
cDto "go-admin/common/dto"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
)
|
||||
|
||||
type SysConfig struct {
|
||||
@@ -33,14 +32,18 @@ func (e *SysConfig) GetPage(c *dto.SysConfigGetPageReq, list *[]models.SysConfig
|
||||
|
||||
// Get 获取SysConfig对象
|
||||
func (e *SysConfig) Get(d *dto.SysConfigGetReq, model *models.SysConfig) error {
|
||||
err := e.Orm.First(model, d.GetId()).Error
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = errors.New("查看对象不存在或无权查看")
|
||||
e.Log.Errorf("Service GetSysConfigPage error:%s", err)
|
||||
err := e.Orm.
|
||||
FirstOrInit(model, d.GetId()).
|
||||
Error
|
||||
if err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
_ = e.AddError(err)
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
e.Log.Errorf("Service GetSysConfig error:%s", err)
|
||||
if model.Id == 0 {
|
||||
err = errors.New("查看对象不存在或无权查看")
|
||||
e.Log.Errorf("Service GetSysApi error: %s", err)
|
||||
_ = e.AddError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -142,8 +145,7 @@ func (e *SysConfig) Remove(d *dto.SysConfigDeleteReq) error {
|
||||
var data models.SysConfig
|
||||
|
||||
db := e.Orm.Delete(&data, d.Ids)
|
||||
if db.Error != nil {
|
||||
err = db.Error
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("Service RemoveSysConfig error:%s", err)
|
||||
return err
|
||||
}
|
||||
@@ -168,8 +170,7 @@ func (e *SysConfig) GetWithKey(c *dto.SysConfigByKeyReq, resp *dto.GetSysConfigB
|
||||
}
|
||||
|
||||
func (e *SysConfig) GetWithKeyList(c *dto.SysConfigGetToSysAppReq, list *[]models.SysConfig) error {
|
||||
var err error
|
||||
err = e.Orm.
|
||||
err := e.Orm.
|
||||
Scopes(
|
||||
cDto.MakeCondition(c.GetNeedSearch()),
|
||||
).
|
||||
|
||||
@@ -4,15 +4,13 @@ import (
|
||||
"errors"
|
||||
"go-admin/app/admin/models"
|
||||
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
|
||||
"gorm.io/gorm"
|
||||
log "github.com/go-admin-team/go-admin-core/v2/logger"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
|
||||
"go-admin/app/admin/service/dto"
|
||||
cDto "go-admin/common/dto"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
)
|
||||
|
||||
type SysDept struct {
|
||||
@@ -41,16 +39,18 @@ func (e *SysDept) Get(d *dto.SysDeptGetReq, model *models.SysDept) error {
|
||||
var err error
|
||||
var data models.SysDept
|
||||
|
||||
db := e.Orm.Model(&data).
|
||||
First(model, d.GetId())
|
||||
err = db.Error
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = errors.New("查看对象不存在或无权查看")
|
||||
err = e.Orm.Model(&data).
|
||||
FirstOrInit(model, d.GetId()).
|
||||
Error
|
||||
if err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
_ = e.AddError(err)
|
||||
return err
|
||||
}
|
||||
if db.Error != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
if model.DeptId == 0 {
|
||||
err = errors.New("查看对象不存在或无权查看")
|
||||
e.Log.Errorf("Service GetSysApi error: %s", err)
|
||||
_ = e.AddError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -84,7 +84,7 @@ func (e *SysDept) Insert(c *dto.SysDeptInsertReq) error {
|
||||
}
|
||||
var mp = map[string]string{}
|
||||
mp["dept_path"] = deptPath
|
||||
if err := tx.Model(&data).Update("dept_path", deptPath).Error; err != nil {
|
||||
if err = tx.Model(&data).Update("dept_path", deptPath).Error; err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
return err
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func (e *SysDept) Update(c *dto.SysDeptUpdateReq) error {
|
||||
}
|
||||
model.DeptPath = deptPath
|
||||
db := tx.Save(&model)
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("UpdateSysDept error:%s", err)
|
||||
return err
|
||||
}
|
||||
@@ -132,8 +132,7 @@ func (e *SysDept) Remove(d *dto.SysDeptDeleteReq) error {
|
||||
var data models.SysDept
|
||||
|
||||
db := e.Orm.Model(&data).Delete(&data, d.GetId())
|
||||
if db.Error != nil {
|
||||
err = db.Error
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("Delete error: %s", err)
|
||||
return err
|
||||
}
|
||||
@@ -184,16 +183,16 @@ func (e *SysDept) SetDeptTree(c *dto.SysDeptGetPageReq) (m []dto.DeptLabel, err
|
||||
// Call 递归构造组织数据
|
||||
func deptTreeCall(deptList *[]models.SysDept, dept dto.DeptLabel) dto.DeptLabel {
|
||||
list := *deptList
|
||||
min := make([]dto.DeptLabel, 0)
|
||||
childrenList := make([]dto.DeptLabel, 0)
|
||||
for j := 0; j < len(list); j++ {
|
||||
if dept.Id != list[j].ParentId {
|
||||
continue
|
||||
}
|
||||
mi := dto.DeptLabel{Id: list[j].DeptId, Label: list[j].DeptName, Children: []dto.DeptLabel{}}
|
||||
ms := deptTreeCall(deptList, mi)
|
||||
min = append(min, ms)
|
||||
childrenList = append(childrenList, ms)
|
||||
}
|
||||
dept.Children = min
|
||||
dept.Children = childrenList
|
||||
return dept
|
||||
}
|
||||
|
||||
@@ -213,7 +212,7 @@ func (e *SysDept) SetDeptPage(c *dto.SysDeptGetPageReq) (m []models.SysDept, err
|
||||
|
||||
func (e *SysDept) deptPageCall(deptlist *[]models.SysDept, menu models.SysDept) models.SysDept {
|
||||
list := *deptlist
|
||||
min := make([]models.SysDept, 0)
|
||||
childrenList := make([]models.SysDept, 0)
|
||||
for j := 0; j < len(list); j++ {
|
||||
if menu.DeptId != list[j].ParentId {
|
||||
continue
|
||||
@@ -231,13 +230,13 @@ func (e *SysDept) deptPageCall(deptlist *[]models.SysDept, menu models.SysDept)
|
||||
mi.CreatedAt = list[j].CreatedAt
|
||||
mi.Children = []models.SysDept{}
|
||||
ms := e.deptPageCall(deptlist, mi)
|
||||
min = append(min, ms)
|
||||
childrenList = append(childrenList, ms)
|
||||
}
|
||||
menu.Children = min
|
||||
menu.Children = childrenList
|
||||
return menu
|
||||
}
|
||||
|
||||
// GetRoleDeptId 获取角色的部门ID集合
|
||||
// GetWithRoleId 获取角色的部门ID集合
|
||||
func (e *SysDept) GetWithRoleId(roleId int) ([]int, error) {
|
||||
deptIds := make([]int, 0)
|
||||
deptList := make([]dto.DeptIdList, 0)
|
||||
@@ -281,15 +280,15 @@ func (e *SysDept) SetDeptLabel() (m []dto.DeptLabel, err error) {
|
||||
func deptLabelCall(deptList *[]models.SysDept, dept dto.DeptLabel) dto.DeptLabel {
|
||||
list := *deptList
|
||||
var mi dto.DeptLabel
|
||||
min := make([]dto.DeptLabel, 0)
|
||||
childrenList := make([]dto.DeptLabel, 0)
|
||||
for j := 0; j < len(list); j++ {
|
||||
if dept.Id != list[j].ParentId {
|
||||
continue
|
||||
}
|
||||
mi = dto.DeptLabel{Id: list[j].DeptId, Label: list[j].DeptName, Children: []dto.DeptLabel{}}
|
||||
ms := deptLabelCall(deptList, mi)
|
||||
min = append(min, ms)
|
||||
childrenList = append(childrenList, ms)
|
||||
}
|
||||
dept.Children = min
|
||||
dept.Children = childrenList
|
||||
return dept
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
@@ -47,7 +47,7 @@ func (e *SysDictData) Get(d *dto.SysDictDataGetReq, model *models.SysDictData) e
|
||||
e.Log.Errorf("db error: %s", err)
|
||||
return err
|
||||
}
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("db error: %s", err)
|
||||
return err
|
||||
}
|
||||
@@ -91,8 +91,7 @@ func (e *SysDictData) Remove(c *dto.SysDictDataDeleteReq) error {
|
||||
var data models.SysDictData
|
||||
|
||||
db := e.Orm.Delete(&data, c.GetId())
|
||||
if db.Error != nil {
|
||||
err = db.Error
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("Delete error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
@@ -45,7 +46,7 @@ func (e *SysDictType) Get(d *dto.SysDictTypeGetReq, model *models.SysDictType) e
|
||||
e.Log.Errorf("db error: %s", err)
|
||||
return err
|
||||
}
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("db error: %s", err)
|
||||
return err
|
||||
}
|
||||
@@ -58,9 +59,14 @@ func (e *SysDictType) Insert(c *dto.SysDictTypeInsertReq) error {
|
||||
var data models.SysDictType
|
||||
c.Generate(&data)
|
||||
var count int64
|
||||
e.Orm.Model(&data).Where("dict_type = ?", data.DictType).Count(&count)
|
||||
// The error was dropped, so a query that failed left count at zero and the
|
||||
// insert went ahead as though the name were free.
|
||||
if err = e.Orm.Model(&data).Where("dict_type = ?", data.DictType).Count(&count).Error; err != nil {
|
||||
e.Log.Errorf("db error: %s", err)
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New(fmt.Sprintf("当前字典类型[%s]已经存在!", data.DictType))
|
||||
return fmt.Errorf("当前字典类型[%s]已经存在!", data.DictType)
|
||||
}
|
||||
err = e.Orm.Create(&data).Error
|
||||
if err != nil {
|
||||
@@ -77,7 +83,7 @@ func (e *SysDictType) Update(c *dto.SysDictTypeUpdateReq) error {
|
||||
e.Orm.First(&model, c.GetId())
|
||||
c.Generate(&model)
|
||||
db := e.Orm.Save(&model)
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("db error: %s", err)
|
||||
return err
|
||||
}
|
||||
@@ -94,8 +100,7 @@ func (e *SysDictType) Remove(d *dto.SysDictTypeDeleteReq) error {
|
||||
var data models.SysDictType
|
||||
|
||||
db := e.Orm.Delete(&data, d.GetId())
|
||||
if db.Error != nil {
|
||||
err = db.Error
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("Delete error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
@@ -44,7 +44,7 @@ func (e *SysLoginLog) Get(d *dto.SysLoginLogGetReq, model *models.SysLoginLog) e
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
return err
|
||||
}
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
return err
|
||||
}
|
||||
@@ -57,8 +57,7 @@ func (e *SysLoginLog) Remove(c *dto.SysLoginLogDeleteReq) error {
|
||||
var data models.SysLoginLog
|
||||
|
||||
db := e.Orm.Delete(&data, c.GetId())
|
||||
if db.Error != nil {
|
||||
err = db.Error
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("Delete error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service/dto"
|
||||
cDto "go-admin/common/dto"
|
||||
cModels "go-admin/common/models"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
)
|
||||
|
||||
type SysMenu struct {
|
||||
@@ -87,26 +90,46 @@ func (e *SysMenu) Insert(c *dto.SysMenuInsertReq) *SysMenu {
|
||||
var err error
|
||||
var data models.SysMenu
|
||||
c.Generate(&data)
|
||||
err = e.Orm.Create(&data).Error
|
||||
tx := e.Orm.Debug().Begin()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
}()
|
||||
err = tx.Where("id in ?", c.Apis).Find(&data.SysApi).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
_ = e.AddError(err)
|
||||
}
|
||||
err = tx.Create(&data).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
_ = e.AddError(err)
|
||||
}
|
||||
c.MenuId = data.MenuId
|
||||
err = e.initPaths(&data)
|
||||
err = e.initPaths(tx, &data)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
_ = e.AddError(err)
|
||||
}
|
||||
tx.Commit()
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *SysMenu) initPaths(menu *models.SysMenu) error {
|
||||
func (e *SysMenu) initPaths(tx *gorm.DB, menu *models.SysMenu) error {
|
||||
var err error
|
||||
var data models.SysMenu
|
||||
parentMenu := new(models.SysMenu)
|
||||
if menu.ParentId != 0 {
|
||||
e.Orm.Model(&data).First(parentMenu, menu.ParentId)
|
||||
err = tx.Model(&data).First(parentMenu, menu.ParentId).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if parentMenu.Paths == "" {
|
||||
err = errors.New("父级paths异常,请尝试对当前节点父级菜单进行更新操作!")
|
||||
return err
|
||||
@@ -115,7 +138,7 @@ func (e *SysMenu) initPaths(menu *models.SysMenu) error {
|
||||
} else {
|
||||
menu.Paths = "/0/" + pkg.IntToString(menu.MenuId)
|
||||
}
|
||||
e.Orm.Model(&data).Where("menu_id = ?", menu.MenuId).Update("paths", menu.Paths)
|
||||
err = tx.Model(&data).Where("menu_id = ?", menu.MenuId).Update("paths", menu.Paths).Error
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -144,7 +167,7 @@ func (e *SysMenu) Update(c *dto.SysMenuUpdateReq) *SysMenu {
|
||||
c.Generate(&model)
|
||||
model.SysApi = alist
|
||||
db := tx.Model(&model).Session(&gorm.Session{FullSaveAssociations: true}).Debug().Save(&model)
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
_ = e.AddError(err)
|
||||
return e
|
||||
@@ -168,8 +191,7 @@ func (e *SysMenu) Remove(d *dto.SysMenuDeleteReq) *SysMenu {
|
||||
var data models.SysMenu
|
||||
|
||||
db := e.Orm.Model(&data).Delete(&data, d.Ids)
|
||||
if db.Error != nil {
|
||||
err = db.Error
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("Delete error: %s", err)
|
||||
_ = e.AddError(err)
|
||||
}
|
||||
@@ -319,6 +341,40 @@ func menuCall(menuList *[]models.SysMenu, menu models.SysMenu) models.SysMenu {
|
||||
return menu
|
||||
}
|
||||
|
||||
func menuDistinct(menuList []models.SysMenu) (result []models.SysMenu) {
|
||||
distinctMap := make(map[int]struct{}, len(menuList))
|
||||
for _, menu := range menuList {
|
||||
if _, ok := distinctMap[menu.MenuId]; !ok {
|
||||
distinctMap[menu.MenuId] = struct{}{}
|
||||
result = append(result, menu)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func recursiveSetMenu(orm *gorm.DB, mIds []int, menus *[]models.SysMenu) error {
|
||||
if len(mIds) == 0 || menus == nil {
|
||||
return nil
|
||||
}
|
||||
var subMenus []models.SysMenu
|
||||
err := orm.Where(fmt.Sprintf(" menu_type in ('%s', '%s', '%s') and menu_id in ?",
|
||||
cModels.Directory, cModels.Menu, cModels.Button), mIds).Order("sort").Find(&subMenus).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subIds := make([]int, 0)
|
||||
for _, menu := range subMenus {
|
||||
if menu.ParentId != 0 {
|
||||
subIds = append(subIds, menu.ParentId)
|
||||
}
|
||||
if menu.MenuType != cModels.Button {
|
||||
*menus = append(*menus, menu)
|
||||
}
|
||||
}
|
||||
return recursiveSetMenu(orm, subIds, menus)
|
||||
}
|
||||
|
||||
// SetMenuRole 获取左侧菜单树使用
|
||||
func (e *SysMenu) SetMenuRole(roleName string) (m []models.SysMenu, err error) {
|
||||
menus, err := e.getByRoleName(roleName)
|
||||
@@ -336,44 +392,35 @@ func (e *SysMenu) SetMenuRole(roleName string) (m []models.SysMenu, err error) {
|
||||
func (e *SysMenu) getByRoleName(roleName string) ([]models.SysMenu, error) {
|
||||
var role models.SysRole
|
||||
var err error
|
||||
data := make([]models.SysMenu, 0)
|
||||
|
||||
if roleName == "admin" {
|
||||
var data []models.SysMenu
|
||||
err = e.Orm.Where(" menu_type in ('M','C') and deleted_at is null and created_at>'2021-08-13'").
|
||||
// The soft-delete condition is GORM's to add: it appends one for the
|
||||
// model's DeletedAt field on every query. Writing it by hand duplicates
|
||||
// that and hard-codes what "deleted" looks like — a column that stops
|
||||
// being nullable turns this clause into one that matches nothing.
|
||||
err = e.Orm.Where("menu_type in ('M','C')").
|
||||
Order("sort").
|
||||
Find(&data).
|
||||
Error
|
||||
err = errors.WithStack(err)
|
||||
return data, err
|
||||
} else {
|
||||
role.RoleKey = roleName
|
||||
err = e.Orm.
|
||||
Model(&role).
|
||||
Where("role_key = ? ", roleName).
|
||||
Preload("SysMenu", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where(" menu_type in ('F','C')").Order("sort")
|
||||
}).
|
||||
Find(&role).Error
|
||||
MenuList := *role.SysMenu
|
||||
mIds := make([]string, 0)
|
||||
mp := make(map[string]string, 0)
|
||||
for _, menu := range MenuList {
|
||||
ids := strings.Split(menu.Paths, "/")
|
||||
for _, id := range ids {
|
||||
if mp[id] == "" {
|
||||
mp[id] = id
|
||||
mIds = append(mIds, id)
|
||||
}
|
||||
err = e.Orm.Model(&role).Where("role_key = ? ", roleName).Preload("SysMenu").First(&role).Error
|
||||
|
||||
if role.SysMenu != nil {
|
||||
mIds := make([]int, 0)
|
||||
for _, menu := range *role.SysMenu {
|
||||
mIds = append(mIds, menu.MenuId)
|
||||
}
|
||||
if err := recursiveSetMenu(e.Orm, mIds, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data = menuDistinct(data)
|
||||
}
|
||||
var data []models.SysMenu
|
||||
err = e.Orm.Where(" menu_id in ? and menu_type in ('M','C')", mIds).
|
||||
Order("sort").
|
||||
Find(&data).
|
||||
Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
sort.Sort(models.SysMenuSlice(data))
|
||||
return data, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
)
|
||||
|
||||
// The admin branch of getSysMenuByRoleName carried "deleted_at is null" in its
|
||||
// where clause. GORM adds that condition itself for a model with a DeletedAt
|
||||
// field, so the clause was a duplicate — and one written in terms of a column
|
||||
// being null, which stops being true the moment the column stops being
|
||||
// nullable. This pins the behaviour the clause was there for.
|
||||
func TestSoftDeletedMenusAreNotReturned(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&models.SysMenu{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
live := models.SysMenu{MenuName: "live", MenuType: "M"}
|
||||
gone := models.SysMenu{MenuName: "gone", MenuType: "M"}
|
||||
if err := db.Create(&live).Error; err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
if err := db.Create(&gone).Error; err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
if err := db.Delete(&gone).Error; err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
|
||||
// Through getByRoleName rather than a copy of its query: a test that
|
||||
// reissues the statement passes whether or not the production line still
|
||||
// says what it is supposed to, which is what the first version of this
|
||||
// test did.
|
||||
e := &SysMenu{}
|
||||
e.Orm = db
|
||||
got, err := e.getByRoleName("admin")
|
||||
if err != nil {
|
||||
t.Fatalf("getByRoleName: %v", err)
|
||||
}
|
||||
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("got %d rows, want 1", len(got))
|
||||
}
|
||||
if got[0].MenuName != "live" {
|
||||
t.Errorf("got %q, want the row that was not deleted", got[0].MenuName)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"go-admin/app/admin/service/dto"
|
||||
cDto "go-admin/common/dto"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
@@ -47,7 +47,7 @@ func (e *SysPost) Get(d *dto.SysPostGetReq, model *models.SysPost) error {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
return err
|
||||
}
|
||||
if db.Error != nil {
|
||||
if err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
return err
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func (e *SysPost) Update(c *dto.SysPostUpdateReq) error {
|
||||
c.Generate(&model)
|
||||
|
||||
db := e.Orm.Save(&model)
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
return err
|
||||
}
|
||||
@@ -92,8 +92,7 @@ func (e *SysPost) Remove(d *dto.SysPostDeleteReq) error {
|
||||
var data models.SysPost
|
||||
|
||||
db := e.Orm.Model(&data).Delete(&data, d.GetId())
|
||||
if db.Error != nil {
|
||||
err = db.Error
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("Delete error: %s", err)
|
||||
return err
|
||||
}
|
||||
@@ -102,4 +101,4 @@ func (e *SysPost) Remove(d *dto.SysPostDeleteReq) error {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package service
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/casbin/casbin/v2"
|
||||
"github.com/casbin/casbin/v3"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
@@ -75,7 +75,7 @@ func (e *SysRole) Insert(c *dto.SysRoleInsertReq, cb *casbin.SyncedEnforcer) err
|
||||
c.Generate(&data)
|
||||
tx := e.Orm
|
||||
if config.DatabaseConfig.Driver != "sqlite3" {
|
||||
tx := e.Orm.Begin()
|
||||
tx = e.Orm.Begin()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
@@ -113,21 +113,17 @@ func (e *SysRole) Insert(c *dto.SysRoleInsertReq, cb *casbin.SyncedEnforcer) err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(polices) <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 写入 sys_casbin_rule 权限表里 当前角色数据的记录
|
||||
_, err = cb.AddNamedPolicies("p", polices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//if len(c.MenuIds) > 0 {
|
||||
// s := SysRoleMenu{}
|
||||
// s.Orm = e.Orm
|
||||
// s.Log = e.Log
|
||||
// err = s.ReloadRule(tx, c.RoleId, c.MenuIds)
|
||||
// if err != nil {
|
||||
// e.Log.Errorf("reload casbin rule error, %", err.Error())
|
||||
// return err
|
||||
// }
|
||||
//}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -136,7 +132,7 @@ func (e *SysRole) Update(c *dto.SysRoleUpdateReq, cb *casbin.SyncedEnforcer) err
|
||||
var err error
|
||||
tx := e.Orm
|
||||
if config.DatabaseConfig.Driver != "sqlite3" {
|
||||
tx := e.Orm.Begin()
|
||||
tx = e.Orm.Begin()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
@@ -156,9 +152,10 @@ func (e *SysRole) Update(c *dto.SysRoleUpdateReq, cb *casbin.SyncedEnforcer) err
|
||||
}
|
||||
c.Generate(&model)
|
||||
model.SysMenu = &mlist
|
||||
// 更新关联的数据,使用 FullSaveAssociations 模式
|
||||
db := tx.Session(&gorm.Session{FullSaveAssociations: true}).Debug().Save(&model)
|
||||
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
return err
|
||||
}
|
||||
@@ -166,6 +163,7 @@ func (e *SysRole) Update(c *dto.SysRoleUpdateReq, cb *casbin.SyncedEnforcer) err
|
||||
return errors.New("无权更新该数据")
|
||||
}
|
||||
|
||||
// 清除 sys_casbin_rule 权限表里 当前角色的所有记录
|
||||
_, err = cb.RemoveFilteredPolicy(0, model.RoleKey)
|
||||
if err != nil {
|
||||
e.Log.Errorf("delete policy error:%s", err)
|
||||
@@ -182,11 +180,15 @@ func (e *SysRole) Update(c *dto.SysRoleUpdateReq, cb *casbin.SyncedEnforcer) err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(polices) <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 写入 sys_casbin_rule 权限表里 当前角色数据的记录
|
||||
_, err = cb.AddNamedPolicies("p", polices)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -195,7 +197,7 @@ func (e *SysRole) Remove(c *dto.SysRoleDeleteReq, cb *casbin.SyncedEnforcer) err
|
||||
var err error
|
||||
tx := e.Orm
|
||||
if config.DatabaseConfig.Driver != "sqlite3" {
|
||||
tx := e.Orm.Begin()
|
||||
tx = e.Orm.Begin()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
@@ -206,9 +208,10 @@ func (e *SysRole) Remove(c *dto.SysRoleDeleteReq, cb *casbin.SyncedEnforcer) err
|
||||
}
|
||||
var model = models.SysRole{}
|
||||
tx.Preload("SysMenu").Preload("SysDept").First(&model, c.GetId())
|
||||
//删除 SysRole 时,同时删除角色所有 关联其它表 记录 (SysMenu 和 SysMenu)
|
||||
db := tx.Select(clause.Associations).Delete(&model)
|
||||
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
return err
|
||||
}
|
||||
@@ -216,6 +219,7 @@ func (e *SysRole) Remove(c *dto.SysRoleDeleteReq, cb *casbin.SyncedEnforcer) err
|
||||
return errors.New("无权更新该数据")
|
||||
}
|
||||
|
||||
// 清除 sys_casbin_rule 权限表里 当前角色的所有记录
|
||||
_, _ = cb.RemoveFilteredPolicy(0, model.RoleKey)
|
||||
|
||||
return nil
|
||||
@@ -240,7 +244,7 @@ func (e *SysRole) UpdateDataScope(c *dto.RoleDataScopeReq) *SysRole {
|
||||
var err error
|
||||
tx := e.Orm
|
||||
if config.DatabaseConfig.Driver != "sqlite3" {
|
||||
tx := e.Orm.Begin()
|
||||
tx = e.Orm.Begin()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
@@ -253,6 +257,7 @@ func (e *SysRole) UpdateDataScope(c *dto.RoleDataScopeReq) *SysRole {
|
||||
var model = models.SysRole{}
|
||||
tx.Preload("SysDept").First(&model, c.RoleId)
|
||||
tx.Where("dept_id in ?", c.DeptIds).Find(&dlist)
|
||||
// 删除SysRole 和 SysDept 的关联关系
|
||||
err = tx.Model(&model).Association("SysDept").Delete(model.SysDept)
|
||||
if err != nil {
|
||||
e.Log.Errorf("delete SysDept error:%s", err)
|
||||
@@ -261,8 +266,9 @@ func (e *SysRole) UpdateDataScope(c *dto.RoleDataScopeReq) *SysRole {
|
||||
}
|
||||
c.Generate(&model)
|
||||
model.SysDept = dlist
|
||||
// 更新关联的数据,使用 FullSaveAssociations 模式
|
||||
db := tx.Model(&model).Session(&gorm.Session{FullSaveAssociations: true}).Debug().Save(&model)
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
_ = e.AddError(err)
|
||||
return e
|
||||
@@ -279,7 +285,7 @@ func (e *SysRole) UpdateStatus(c *dto.UpdateStatusReq) error {
|
||||
var err error
|
||||
tx := e.Orm
|
||||
if config.DatabaseConfig.Driver != "sqlite3" {
|
||||
tx := e.Orm.Begin()
|
||||
tx = e.Orm.Begin()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
@@ -291,8 +297,9 @@ func (e *SysRole) UpdateStatus(c *dto.UpdateStatusReq) error {
|
||||
var model = models.SysRole{}
|
||||
tx.First(&model, c.GetId())
|
||||
c.Generate(&model)
|
||||
// 更新关联的数据,使用 FullSaveAssociations 模式
|
||||
db := tx.Session(&gorm.Session{FullSaveAssociations: true}).Debug().Save(&model)
|
||||
if db.Error != nil {
|
||||
if err = db.Error; err != nil {
|
||||
e.Log.Errorf("db error:%s", err)
|
||||
return err
|
||||
}
|
||||
@@ -337,7 +344,9 @@ func (e *SysRole) GetById(roleId int) ([]string, error) {
|
||||
}
|
||||
l := *model.SysMenu
|
||||
for i := 0; i < len(l); i++ {
|
||||
permissions = append(permissions, l[i].Permission)
|
||||
if l[i].Permission != "" {
|
||||
permissions = append(permissions, l[i].Permission)
|
||||
}
|
||||
}
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
)
|
||||
|
||||
// SysRoleMenu 即将弃用结构体
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service/dto"
|
||||
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
log "github.com/go-admin-team/go-admin-core/v2/logger"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/common/actions"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"go-admin/common/models"
|
||||
)
|
||||
|
||||
// DemoProduct 示例模型
|
||||
//
|
||||
// 内嵌 ControlBy 与 ModelTime 后,创建人/更新人与时间戳由框架自动维护;
|
||||
// 数据权限(actions.Permission)正是按 create_by 过滤,缺少 ControlBy 会使其失效。
|
||||
type DemoProduct struct {
|
||||
models.Model
|
||||
|
||||
Name string `json:"name" gorm:"size:128;comment:名称"`
|
||||
Code string `json:"code" gorm:"size:64;comment:编码"`
|
||||
Price float64 `json:"price" gorm:"comment:单价"`
|
||||
Status string `json:"status" gorm:"size:4;comment:状态"`
|
||||
Remark string `json:"remark" gorm:"size:255;comment:备注"`
|
||||
|
||||
models.ControlBy
|
||||
models.ModelTime
|
||||
}
|
||||
|
||||
func (DemoProduct) TableName() string {
|
||||
return "demo_product"
|
||||
}
|
||||
|
||||
// Generate 返回副本,供通用 Action 使用。
|
||||
// 必须返回新实例:Action 在并发请求间复用同一个模型指针,就地返回会串数据。
|
||||
func (e *DemoProduct) Generate() models.ActiveRecord {
|
||||
o := *e
|
||||
return &o
|
||||
}
|
||||
|
||||
func (e *DemoProduct) GetId() interface{} {
|
||||
return e.Id
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
|
||||
"go-admin/app/demo/models"
|
||||
"go-admin/app/demo/service/dto"
|
||||
"go-admin/common/actions"
|
||||
"go-admin/common/middleware"
|
||||
)
|
||||
|
||||
// 路由通过 init 自注册,无需在任何中心文件登记。
|
||||
// 新建应用时用 `go run main.go app -n <名称>` 生成骨架,
|
||||
// 它会同时产出 cmd/api/<名称>.go 完成注册。
|
||||
func init() {
|
||||
routerCheckRole = append(routerCheckRole, registerDemoProductRouter)
|
||||
}
|
||||
|
||||
// registerDemoProductRouter 标准 CRUD 的推荐写法。
|
||||
//
|
||||
// 五个通用 Action 覆盖了增删改查的全部样板逻辑——参数绑定、数据权限过滤、
|
||||
// 操作人注入、分页、错误响应,因此本模块没有 apis 与 service 文件。
|
||||
//
|
||||
// 仅当业务逻辑超出单表 CRUD(如跨表事务、外部调用、复杂校验)时,才需要
|
||||
// 自行编写 Handler 与 Service,写法参照 app/admin/apis/sys_post.go。
|
||||
func registerDemoProductRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
|
||||
r := v1.Group("/demo-product").
|
||||
Use(authMiddleware.MiddlewareFunc()). // JWT 认证
|
||||
Use(middleware.AuthCheckRole()) // Casbin 鉴权
|
||||
{
|
||||
m := &models.DemoProduct{}
|
||||
|
||||
// actions.PermissionAction() 注入数据权限上下文,
|
||||
// 列表与详情缺少它会绕过 DataScope 过滤
|
||||
r.GET("", actions.PermissionAction(), actions.IndexAction(m, new(dto.DemoProductSearch), func() interface{} {
|
||||
list := make([]models.DemoProduct, 0)
|
||||
return &list
|
||||
}))
|
||||
|
||||
r.GET("/:id", actions.PermissionAction(), actions.ViewAction(new(dto.DemoProductById), func() interface{} {
|
||||
return &models.DemoProduct{}
|
||||
}))
|
||||
|
||||
r.POST("", actions.CreateAction(new(dto.DemoProductControl)))
|
||||
r.PUT("/:id", actions.PermissionAction(), actions.UpdateAction(new(dto.DemoProductControl)))
|
||||
r.DELETE("", actions.PermissionAction(), actions.DeleteAction(new(dto.DemoProductById)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/gin-gonic/gin"
|
||||
log "github.com/go-admin-team/go-admin-core/v2/logger"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
||||
// "github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
"github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
common "go-admin/common/middleware"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
routerNoCheckRole = make([]func(*gin.RouterGroup), 0)
|
||||
routerCheckRole = make([]func(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware), 0)
|
||||
)
|
||||
|
||||
// InitRouter 路由初始化
|
||||
func InitRouter() {
|
||||
var r *gin.Engine
|
||||
h := sdk.Runtime.GetEngine()
|
||||
if h == nil {
|
||||
h = gin.New()
|
||||
sdk.Runtime.SetEngine(h)
|
||||
}
|
||||
switch h.(type) {
|
||||
case *gin.Engine:
|
||||
r = h.(*gin.Engine)
|
||||
default:
|
||||
log.Fatal("not support other engine")
|
||||
os.Exit(-1)
|
||||
}
|
||||
|
||||
// the jwt middleware
|
||||
authMiddleware, err := common.AuthInit()
|
||||
if err != nil {
|
||||
log.Fatalf("JWT Init Error, %s", err.Error())
|
||||
}
|
||||
|
||||
// 注册业务路由
|
||||
InitBusinessRouter(r, authMiddleware)
|
||||
}
|
||||
|
||||
func InitBusinessRouter(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) *gin.Engine {
|
||||
|
||||
// 无需认证的路由
|
||||
noCheckRoleRouter(r)
|
||||
// 需要认证的路由
|
||||
checkRoleRouter(r, authMiddleware)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// noCheckRoleRouter 无需认证的路由
|
||||
func noCheckRoleRouter(r *gin.Engine) {
|
||||
// 可根据业务需求来设置接口版本
|
||||
v := r.Group("/api/v1")
|
||||
|
||||
for _, f := range routerNoCheckRole {
|
||||
f(v)
|
||||
}
|
||||
}
|
||||
|
||||
// checkRoleRouter 需要认证的路由
|
||||
func checkRoleRouter(r *gin.Engine, authMiddleware *jwtauth.GinJWTMiddleware) {
|
||||
// 可根据业务需求来设置接口版本
|
||||
v := r.Group("/api/v1")
|
||||
|
||||
for _, f := range routerCheckRole {
|
||||
f(v, authMiddleware)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"go-admin/app/demo/models"
|
||||
"go-admin/common/dto"
|
||||
common "go-admin/common/models"
|
||||
)
|
||||
|
||||
// DemoProductSearch 列表查询条件
|
||||
//
|
||||
// search tag 决定 MakeCondition 拼出的 WHERE:
|
||||
//
|
||||
// exact 精确匹配 / icontains 忽略大小写模糊 / gte 大于等于 …
|
||||
//
|
||||
// 未打 search tag 的字段不参与查询,可避免无意间开放过滤维度。
|
||||
type DemoProductSearch struct {
|
||||
dto.Pagination `search:"-"`
|
||||
|
||||
Name string `form:"name" search:"type:icontains;column:name;table:demo_product"`
|
||||
Code string `form:"code" search:"type:exact;column:code;table:demo_product"`
|
||||
Status string `form:"status" search:"type:exact;column:status;table:demo_product"`
|
||||
|
||||
DemoProductOrder
|
||||
}
|
||||
|
||||
// DemoProductOrder 排序字段单独成组,避免与查询字段混在一起
|
||||
type DemoProductOrder struct {
|
||||
CreatedAtOrder string `form:"createdAtOrder" search:"type:order;column:created_at;table:demo_product"`
|
||||
}
|
||||
|
||||
func (m *DemoProductSearch) GetNeedSearch() interface{} { return *m }
|
||||
|
||||
func (m *DemoProductSearch) Bind(ctx *gin.Context) error {
|
||||
return ctx.ShouldBind(m)
|
||||
}
|
||||
|
||||
func (m *DemoProductSearch) Generate() dto.Index {
|
||||
o := *m
|
||||
return &o
|
||||
}
|
||||
|
||||
// DemoProductControl 新增与修改共用的入参
|
||||
//
|
||||
// 通用 Action(Create / Update)通过 GenerateM 拿到落库对象,
|
||||
// 因此这里不直接暴露 Model,字段校验用 validate tag 声明。
|
||||
type DemoProductControl struct {
|
||||
Id int `json:"id" comment:"主键"`
|
||||
Name string `json:"name" comment:"名称" validate:"required"`
|
||||
Code string `json:"code" comment:"编码" validate:"required"`
|
||||
Price float64 `json:"price" comment:"单价" validate:"gte=0"`
|
||||
Status string `json:"status" comment:"状态"`
|
||||
Remark string `json:"remark" comment:"备注"`
|
||||
}
|
||||
|
||||
func (s *DemoProductControl) Bind(ctx *gin.Context) error {
|
||||
return ctx.ShouldBind(s)
|
||||
}
|
||||
|
||||
func (s *DemoProductControl) Generate() dto.Control {
|
||||
o := *s
|
||||
return &o
|
||||
}
|
||||
|
||||
func (s *DemoProductControl) GetId() interface{} { return s.Id }
|
||||
|
||||
// GenerateM 组装落库对象。CreateBy / UpdateBy 由通用 Action 在此之后注入,
|
||||
// 此处不要手动赋值。
|
||||
func (s *DemoProductControl) GenerateM() (common.ActiveRecord, error) {
|
||||
return &models.DemoProduct{
|
||||
Model: common.Model{Id: s.Id},
|
||||
Name: s.Name,
|
||||
Code: s.Code,
|
||||
Price: s.Price,
|
||||
Status: s.Status,
|
||||
Remark: s.Remark,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DemoProductById 详情与删除共用,支持单个 id 与批量 ids
|
||||
type DemoProductById struct {
|
||||
dto.ObjectById
|
||||
}
|
||||
|
||||
// Bind 与 GetId 由内嵌的 dto.ObjectById 提供:它已处理好 uri 绑定、
|
||||
// DELETE 时的批量 ids 合并与参数校验,无需在此重复实现。
|
||||
|
||||
func (s *DemoProductById) Generate() dto.Control {
|
||||
o := *s
|
||||
return &o
|
||||
}
|
||||
|
||||
func (s *DemoProductById) GenerateM() (common.ActiveRecord, error) {
|
||||
return &models.DemoProduct{}, nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go-admin/app/demo/models"
|
||||
"go-admin/common/dto"
|
||||
common "go-admin/common/models"
|
||||
)
|
||||
|
||||
// 通用 Action 依赖 DTO 与 Model 实现一组接口。这些约束在编译期无法完全覆盖
|
||||
// (接口是在路由注册处才被要求的),因此用测试锁定,避免改动后在运行时才暴露。
|
||||
|
||||
func TestImplementsIndexInterface(t *testing.T) {
|
||||
var _ dto.Index = (*DemoProductSearch)(nil)
|
||||
}
|
||||
|
||||
func TestImplementsControlInterface(t *testing.T) {
|
||||
var _ dto.Control = (*DemoProductControl)(nil)
|
||||
var _ dto.Control = (*DemoProductById)(nil)
|
||||
}
|
||||
|
||||
func TestModelImplementsActiveRecord(t *testing.T) {
|
||||
var _ common.ActiveRecord = (*models.DemoProduct)(nil)
|
||||
}
|
||||
|
||||
// Generate 必须返回副本:通用 Action 在并发请求间复用同一个实例,
|
||||
// 就地返回会导致请求之间串数据。
|
||||
func TestGenerateReturnsCopy(t *testing.T) {
|
||||
src := &DemoProductControl{Id: 1, Name: "原始"}
|
||||
got := src.Generate().(*DemoProductControl)
|
||||
|
||||
if got == src {
|
||||
t.Fatal("Generate 返回了同一指针,应返回副本")
|
||||
}
|
||||
got.Name = "被修改"
|
||||
if src.Name != "原始" {
|
||||
t.Errorf("修改副本影响了原对象:src.Name = %q", src.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchGenerateReturnsCopy(t *testing.T) {
|
||||
src := &DemoProductSearch{Name: "原始"}
|
||||
got := src.Generate().(*DemoProductSearch)
|
||||
|
||||
if got == src {
|
||||
t.Fatal("Generate 返回了同一指针,应返回副本")
|
||||
}
|
||||
got.Name = "被修改"
|
||||
if src.Name != "原始" {
|
||||
t.Errorf("修改副本影响了原对象:src.Name = %q", src.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelGenerateReturnsCopy(t *testing.T) {
|
||||
src := &models.DemoProduct{Name: "原始"}
|
||||
got := src.Generate().(*models.DemoProduct)
|
||||
|
||||
if got == src {
|
||||
t.Fatal("Generate 返回了同一指针,应返回副本")
|
||||
}
|
||||
got.Name = "被修改"
|
||||
if src.Name != "原始" {
|
||||
t.Errorf("修改副本影响了原对象:src.Name = %q", src.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateM 组装落库对象,主键需正确传递,否则更新会退化成插入。
|
||||
func TestGenerateMCarriesId(t *testing.T) {
|
||||
c := &DemoProductControl{Id: 42, Name: "示例", Code: "P-42", Price: 9.9}
|
||||
m, err := c.GenerateM()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateM 返回错误: %v", err)
|
||||
}
|
||||
p, ok := m.(*models.DemoProduct)
|
||||
if !ok {
|
||||
t.Fatalf("GenerateM 返回类型错误: %T", m)
|
||||
}
|
||||
if p.Id != 42 {
|
||||
t.Errorf("主键未传递: got %d, want 42", p.Id)
|
||||
}
|
||||
if p.Name != "示例" || p.Code != "P-42" || p.Price != 9.9 {
|
||||
t.Errorf("字段映射有误: %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableName(t *testing.T) {
|
||||
if got := (models.DemoProduct{}).TableName(); got != "demo_product" {
|
||||
t.Errorf("TableName() = %q, want %q", got, "demo_product")
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
|
||||
"go-admin/app/jobs/service"
|
||||
"go-admin/common/dto"
|
||||
@@ -29,7 +29,7 @@ func (e SysJob) RemoveJobForService(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
s.Cron = sdk.Runtime.GetCrontabKey(c.Request.Host)
|
||||
s.Cron = sdk.Runtime.GetCrontabByTenant(c.Request.Host)
|
||||
err = s.RemoveJob(&v)
|
||||
if err != nil {
|
||||
e.Logger.Errorf("RemoveJob error, %s", err.Error())
|
||||
@@ -58,7 +58,7 @@ func (e SysJob) StartJobForService(c *gin.Context) {
|
||||
s := service.SysJob{}
|
||||
s.Orm = db
|
||||
s.Log = log
|
||||
s.Cron = sdk.Runtime.GetCrontabKey(c.Request.Host)
|
||||
s.Cron = sdk.Runtime.GetCrontabByTenant(c.Request.Host)
|
||||
err = s.StartJob(&v)
|
||||
if err != nil {
|
||||
log.Errorf("GetCrontabKey error, %s", err.Error())
|
||||
|
||||
@@ -5,15 +5,17 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// InitJob
|
||||
// 需要将定义的struct 添加到字典中;
|
||||
// 字典 key 可以配置到 自动任务 调用目标 中;
|
||||
func InitJob() {
|
||||
jobList = map[string]JobsExec{
|
||||
jobList = map[string]JobExec{
|
||||
"ExamplesOne": ExamplesOne{},
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// ExamplesOne
|
||||
// 新添加的job 必须按照以下格式定义,并实现Exec函数
|
||||
type ExamplesOne struct {
|
||||
}
|
||||
|
||||
+20
-20
@@ -2,24 +2,24 @@ package jobs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
log "github.com/go-admin-team/go-admin-core/v2/logger"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
||||
models2 "go-admin/app/jobs/models"
|
||||
"gorm.io/gorm"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/cronjob"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg/cronjob"
|
||||
)
|
||||
|
||||
var timeFormat = "2006-01-02 15:04:05"
|
||||
var retryCount = 3
|
||||
|
||||
var jobList map[string]JobsExec
|
||||
var lock sync.Mutex
|
||||
var jobList map[string]JobExec
|
||||
|
||||
//var lock sync.Mutex
|
||||
|
||||
type JobCore struct {
|
||||
InvokeTarget string
|
||||
@@ -30,7 +30,7 @@ type JobCore struct {
|
||||
Args string
|
||||
}
|
||||
|
||||
// 任务类型 http
|
||||
// HttpJob 任务类型 http
|
||||
type HttpJob struct {
|
||||
JobCore
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (e *ExecJob) Run() {
|
||||
log.Warn("[Job] ExecJob Run job nil")
|
||||
return
|
||||
}
|
||||
err := CallExec(obj.(JobsExec), e.Args)
|
||||
err := CallExec(obj.(JobExec), e.Args)
|
||||
if err != nil {
|
||||
// 如果失败暂停一段时间重试
|
||||
fmt.Println(time.Now().Format(timeFormat), " [ERROR] mission failed! ", err)
|
||||
@@ -59,11 +59,11 @@ func (e *ExecJob) Run() {
|
||||
//TODO: 待完善部分
|
||||
//str := time.Now().Format(timeFormat) + " [INFO] JobCore " + string(e.EntryId) + "exec success , spend :" + latencyTime.String()
|
||||
//ws.SendAll(str)
|
||||
log.Info("[Job] JobCore %s exec success , spend :%v", e.Name, latencyTime)
|
||||
log.Infof("[Job] JobCore %s exec success , spend :%v", e.Name, latencyTime)
|
||||
return
|
||||
}
|
||||
|
||||
//http 任务接口
|
||||
// Run http 任务接口
|
||||
func (h *HttpJob) Run() {
|
||||
|
||||
startTime := time.Now()
|
||||
@@ -77,8 +77,8 @@ LOOP:
|
||||
str, err = pkg.Get(h.InvokeTarget)
|
||||
if err != nil {
|
||||
// 如果失败暂停一段时间重试
|
||||
fmt.Println(time.Now().Format(timeFormat), " [ERROR] mission failed! ", err)
|
||||
fmt.Printf(time.Now().Format(timeFormat)+" [INFO] Retry after the task fails %d seconds! %s \n", (count+1)*5, str)
|
||||
log.Warnf("[Job] mission failed! %v", err)
|
||||
log.Warnf("[Job] Retry after the task fails %d seconds! %s \n", (count+1)*5, str)
|
||||
time.Sleep(time.Duration(count+1) * 5 * time.Second)
|
||||
count = count + 1
|
||||
goto LOOP
|
||||
@@ -95,19 +95,19 @@ LOOP:
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化
|
||||
// Setup 初始化
|
||||
func Setup(dbs map[string]*gorm.DB) {
|
||||
|
||||
fmt.Println(time.Now().Format(timeFormat), " [INFO] JobCore Starting...")
|
||||
|
||||
for k, db := range dbs {
|
||||
sdk.Runtime.SetCrontab(k, cronjob.NewWithSeconds())
|
||||
sdk.Runtime.SetCrontabByTenant(k, cronjob.NewWithSeconds())
|
||||
setup(k, db)
|
||||
}
|
||||
}
|
||||
|
||||
func setup(key string, db *gorm.DB) {
|
||||
crontab := sdk.Runtime.GetCrontabKey(key)
|
||||
crontab := sdk.Runtime.GetCrontabByTenant(key)
|
||||
sysJob := models2.SysJob{}
|
||||
jobList := make([]models2.SysJob, 0)
|
||||
err := sysJob.GetList(db, &jobList)
|
||||
@@ -152,7 +152,7 @@ func setup(key string, db *gorm.DB) {
|
||||
select {}
|
||||
}
|
||||
|
||||
// 添加任务 AddJob(invokeTarget string, jobId int, jobName string, cronExpression string)
|
||||
// AddJob 添加任务 AddJob(invokeTarget string, jobId int, jobName string, cronExpression string)
|
||||
func AddJob(c *cron.Cron, job Job) (int, error) {
|
||||
if job == nil {
|
||||
fmt.Println("unknown")
|
||||
@@ -171,8 +171,8 @@ func (h *HttpJob) addJob(c *cron.Cron) (int, error) {
|
||||
return EntryId, nil
|
||||
}
|
||||
|
||||
func (h *ExecJob) addJob(c *cron.Cron) (int, error) {
|
||||
id, err := c.AddJob(h.CronExpression, h)
|
||||
func (e *ExecJob) addJob(c *cron.Cron) (int, error) {
|
||||
id, err := c.AddJob(e.CronExpression, e)
|
||||
if err != nil {
|
||||
fmt.Println(time.Now().Format(timeFormat), " [ERROR] JobCore AddJob error", err)
|
||||
return 0, err
|
||||
@@ -181,7 +181,7 @@ func (h *ExecJob) addJob(c *cron.Cron) (int, error) {
|
||||
return EntryId, nil
|
||||
}
|
||||
|
||||
// 移除任务
|
||||
// Remove 移除任务
|
||||
func Remove(c *cron.Cron, entryID int) chan bool {
|
||||
ch := make(chan bool)
|
||||
go func() {
|
||||
|
||||
@@ -23,7 +23,7 @@ type SysJob struct {
|
||||
DataScope string `json:"dataScope" gorm:"-"`
|
||||
}
|
||||
|
||||
func (SysJob) TableName() string {
|
||||
func (*SysJob) TableName() string {
|
||||
return "sys_job"
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (e *SysJob) GetList(tx *gorm.DB, list interface{}) (err error) {
|
||||
return tx.Table(e.TableName()).Where("status = ?", 2).Find(list).Error
|
||||
}
|
||||
|
||||
// 更新SysJob
|
||||
// Update 更新SysJob
|
||||
func (e *SysJob) Update(tx *gorm.DB, id interface{}) (err error) {
|
||||
return tx.Table(e.TableName()).Where(id).Updates(&e).Error
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
//"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
//"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
log "github.com/go-admin-team/go-admin-core/v2/logger"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
||||
common "go-admin/common/middleware"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -2,7 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||
jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth"
|
||||
"go-admin/app/jobs/apis"
|
||||
models2 "go-admin/app/jobs/models"
|
||||
dto2 "go-admin/app/jobs/service/dto"
|
||||
|
||||
@@ -2,7 +2,7 @@ package dto
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"go-admin/app/jobs/models"
|
||||
|
||||
"go-admin/common/dto"
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/service"
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"go-admin/app/jobs"
|
||||
|
||||
+2
-2
@@ -7,10 +7,10 @@ type Job interface {
|
||||
addJob(*cron.Cron) (int, error)
|
||||
}
|
||||
|
||||
type JobsExec interface {
|
||||
type JobExec interface {
|
||||
Exec(arg interface{}) error
|
||||
}
|
||||
|
||||
func CallExec(e JobsExec, arg interface{}) error {
|
||||
func CallExec(e JobExec, arg interface{}) error {
|
||||
return e.Exec(arg)
|
||||
}
|
||||
|
||||
+112
-104
@@ -8,12 +8,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/utils"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg/utils"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"go-admin/common/file_store"
|
||||
"go-admin/config"
|
||||
)
|
||||
|
||||
type FileResponse struct {
|
||||
@@ -44,63 +45,61 @@ type File struct {
|
||||
func (e File) UploadFile(c *gin.Context) {
|
||||
e.MakeContext(c)
|
||||
tag, _ := c.GetPostForm("type")
|
||||
urlPrefix := fmt.Sprintf("http://%s/", c.Request.Host)
|
||||
var fileResponse FileResponse
|
||||
urlPrefix := fmt.Sprintf("%s://%s/", "http", c.Request.Host)
|
||||
|
||||
switch tag {
|
||||
case "1": // 单图
|
||||
var done bool
|
||||
fileResponse, done = e.singleFile(c, fileResponse, urlPrefix)
|
||||
if done {
|
||||
return
|
||||
}
|
||||
e.OK(fileResponse, "上传成功")
|
||||
return
|
||||
e.handleSingleFile(c, urlPrefix)
|
||||
case "2": // 多图
|
||||
multipartFile := e.multipleFile(c, urlPrefix)
|
||||
e.OK(multipartFile, "上传成功")
|
||||
return
|
||||
e.handleMultipleFiles(c, urlPrefix)
|
||||
case "3": // base64
|
||||
fileResponse = e.baseImg(c, fileResponse, urlPrefix)
|
||||
e.OK(fileResponse, "上传成功")
|
||||
e.handleBase64File(c, urlPrefix)
|
||||
default:
|
||||
var done bool
|
||||
fileResponse, done = e.singleFile(c, fileResponse, urlPrefix)
|
||||
if done {
|
||||
return
|
||||
}
|
||||
e.OK(fileResponse, "上传成功")
|
||||
return
|
||||
e.handleSingleFile(c, urlPrefix)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (e File) baseImg(c *gin.Context, fileResponse FileResponse, urlPerfix string) FileResponse {
|
||||
func (e File) handleSingleFile(c *gin.Context, urlPrefix string) {
|
||||
fileResponse, done := e.singleFile(c, FileResponse{}, urlPrefix)
|
||||
if done {
|
||||
return
|
||||
}
|
||||
e.OK(fileResponse, "上传成功")
|
||||
}
|
||||
|
||||
func (e File) handleMultipleFiles(c *gin.Context, urlPrefix string) {
|
||||
multipartFile := e.multipleFile(c, urlPrefix)
|
||||
e.OK(multipartFile, "上传成功")
|
||||
}
|
||||
|
||||
func (e File) handleBase64File(c *gin.Context, urlPrefix string) {
|
||||
fileResponse := e.baseImg(c, FileResponse{}, urlPrefix)
|
||||
e.OK(fileResponse, "上传成功")
|
||||
}
|
||||
|
||||
func (e File) baseImg(c *gin.Context, fileResponse FileResponse, urlPrefix string) FileResponse {
|
||||
files, _ := c.GetPostForm("file")
|
||||
file2list := strings.Split(files, ",")
|
||||
ddd, _ := base64.StdEncoding.DecodeString(file2list[1])
|
||||
guid := uuid.New().String()
|
||||
fileName := guid + ".jpg"
|
||||
err := utils.IsNotExistMkDir(path)
|
||||
if err != nil {
|
||||
decodedData, _ := base64.StdEncoding.DecodeString(file2list[1])
|
||||
fileName := uuid.New().String() + ".jpg"
|
||||
|
||||
if err := utils.IsNotExistMkDir(path); err != nil {
|
||||
e.Error(500, errors.New(""), "初始化文件路径失败")
|
||||
}
|
||||
base64File := path + fileName
|
||||
_ = ioutil.WriteFile(base64File, ddd, 0666)
|
||||
typeStr := strings.Replace(strings.Replace(file2list[0], "data:", "", -1), ";base64", "", -1)
|
||||
fileResponse = FileResponse{
|
||||
Size: pkg.GetFileSize(base64File),
|
||||
Path: base64File,
|
||||
FullPath: urlPerfix + base64File,
|
||||
Name: "",
|
||||
Type: typeStr,
|
||||
}
|
||||
source, _ := c.GetPostForm("source")
|
||||
err = thirdUpload(source, fileName, base64File)
|
||||
if err != nil {
|
||||
e.Error(200, errors.New(""), "上传第三方失败")
|
||||
return fileResponse
|
||||
}
|
||||
|
||||
base64File := path + fileName
|
||||
_ = ioutil.WriteFile(base64File, decodedData, 0666)
|
||||
typeStr := strings.Replace(strings.Replace(file2list[0], "data:", "", -1), ";base64", "", -1)
|
||||
|
||||
fileResponse = e.buildFileResponse(base64File, urlPrefix, "", typeStr)
|
||||
source, _ := c.GetPostForm("source")
|
||||
|
||||
if err := thirdUpload(source, fileName, base64File); err != nil {
|
||||
e.Error(200, err, "上传第三方失败")
|
||||
return fileResponse
|
||||
}
|
||||
|
||||
if source != "1" {
|
||||
fileResponse.Path = "/static/uploadfile/" + fileName
|
||||
fileResponse.FullPath = "/static/uploadfile/" + fileName
|
||||
@@ -108,97 +107,106 @@ func (e File) baseImg(c *gin.Context, fileResponse FileResponse, urlPerfix strin
|
||||
return fileResponse
|
||||
}
|
||||
|
||||
func (e File) multipleFile(c *gin.Context, urlPerfix string) []FileResponse {
|
||||
func (e File) multipleFile(c *gin.Context, urlPrefix string) []FileResponse {
|
||||
files := c.Request.MultipartForm.File["file"]
|
||||
source, _ := c.GetPostForm("source")
|
||||
var multipartFile []FileResponse
|
||||
for _, f := range files {
|
||||
guid := uuid.New().String()
|
||||
fileName := guid + utils.GetExt(f.Filename)
|
||||
|
||||
err := utils.IsNotExistMkDir(path)
|
||||
if err != nil {
|
||||
for _, f := range files {
|
||||
fileName := uuid.New().String() + utils.GetExt(f.Filename)
|
||||
|
||||
if err := utils.IsNotExistMkDir(path); err != nil {
|
||||
e.Error(500, errors.New(""), "初始化文件路径失败")
|
||||
continue
|
||||
}
|
||||
|
||||
multipartFileName := path + fileName
|
||||
err1 := c.SaveUploadedFile(f, multipartFileName)
|
||||
fileType, _ := utils.GetType(multipartFileName)
|
||||
if err1 == nil {
|
||||
err := thirdUpload(source, fileName, multipartFileName)
|
||||
if err != nil {
|
||||
e.Error(500, errors.New(""), "上传第三方失败")
|
||||
} else {
|
||||
fileResponse := FileResponse{
|
||||
Size: pkg.GetFileSize(multipartFileName),
|
||||
Path: multipartFileName,
|
||||
FullPath: urlPerfix + multipartFileName,
|
||||
Name: f.Filename,
|
||||
Type: fileType,
|
||||
}
|
||||
if source != "1" {
|
||||
fileResponse.Path = "/static/uploadfile/" + fileName
|
||||
fileResponse.FullPath = "/static/uploadfile/" + fileName
|
||||
}
|
||||
multipartFile = append(multipartFile, fileResponse)
|
||||
}
|
||||
if err := c.SaveUploadedFile(f, multipartFileName); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
fileType, _ := utils.GetType(multipartFileName)
|
||||
if err := thirdUpload(source, fileName, multipartFileName); err != nil {
|
||||
e.Error(500, err, "上传第三方失败")
|
||||
continue
|
||||
}
|
||||
|
||||
fileResponse := e.buildFileResponse(multipartFileName, urlPrefix, f.Filename, fileType)
|
||||
if source != "1" {
|
||||
fileResponse.Path = "/static/uploadfile/" + fileName
|
||||
fileResponse.FullPath = "/static/uploadfile/" + fileName
|
||||
}
|
||||
multipartFile = append(multipartFile, fileResponse)
|
||||
}
|
||||
return multipartFile
|
||||
}
|
||||
|
||||
func (e File) singleFile(c *gin.Context, fileResponse FileResponse, urlPerfix string) (FileResponse, bool) {
|
||||
func (e File) singleFile(c *gin.Context, fileResponse FileResponse, urlPrefix string) (FileResponse, bool) {
|
||||
files, err := c.FormFile("file")
|
||||
|
||||
if err != nil {
|
||||
e.Error(200, errors.New(""), "图片不能为空")
|
||||
return FileResponse{}, true
|
||||
}
|
||||
// 上传文件至指定目录
|
||||
guid := uuid.New().String()
|
||||
|
||||
fileName := guid + utils.GetExt(files.Filename)
|
||||
|
||||
err = utils.IsNotExistMkDir(path)
|
||||
if err != nil {
|
||||
fileName := uuid.New().String() + utils.GetExt(files.Filename)
|
||||
if err := utils.IsNotExistMkDir(path); err != nil {
|
||||
e.Error(500, errors.New(""), "初始化文件路径失败")
|
||||
return FileResponse{}, true
|
||||
}
|
||||
|
||||
singleFile := path + fileName
|
||||
_ = c.SaveUploadedFile(files, singleFile)
|
||||
fileType, _ := utils.GetType(singleFile)
|
||||
fileResponse = FileResponse{
|
||||
Size: pkg.GetFileSize(singleFile),
|
||||
Path: singleFile,
|
||||
FullPath: urlPerfix + singleFile,
|
||||
Name: files.Filename,
|
||||
Type: fileType,
|
||||
if err := c.SaveUploadedFile(files, singleFile); err != nil {
|
||||
e.Error(500, errors.New(""), "文件保存失败")
|
||||
return FileResponse{}, true
|
||||
}
|
||||
//source, _ := c.GetPostForm("source")
|
||||
//err = thirdUpload(source, fileName, singleFile)
|
||||
//if err != nil {
|
||||
// e.Error(200, errors.New(""), "上传第三方失败")
|
||||
// return FileResponse{}, true
|
||||
//}
|
||||
|
||||
fileType, _ := utils.GetType(singleFile)
|
||||
fileResponse = e.buildFileResponse(singleFile, urlPrefix, files.Filename, fileType)
|
||||
fileResponse.Path = "/static/uploadfile/" + fileName
|
||||
fileResponse.FullPath = "/static/uploadfile/" + fileName
|
||||
return fileResponse, false
|
||||
}
|
||||
|
||||
func (e File) buildFileResponse(filePath, urlPrefix, fileName, fileType string) FileResponse {
|
||||
return FileResponse{
|
||||
Size: pkg.GetFileSize(filePath),
|
||||
Path: filePath,
|
||||
FullPath: urlPrefix + filePath,
|
||||
Name: fileName,
|
||||
Type: fileType,
|
||||
}
|
||||
}
|
||||
|
||||
// thirdUpload copies the file that was already stored locally to the object
|
||||
// store the request asked for. source "1", and anything unrecognised, keeps the
|
||||
// local copy only.
|
||||
//
|
||||
// Both branches used to construct a zero-value ALiYunOSS and call UpLoad on it,
|
||||
// which panicked - and the qiniu branch constructed the aliyun client, so
|
||||
// source=3 never reached qiniu even in principle.
|
||||
func thirdUpload(source string, name string, path string) error {
|
||||
switch source {
|
||||
case "2":
|
||||
return ossUpload("img/"+name, path)
|
||||
return upload(file_store.AliYunOSS, config.ExtConfig.FileStore.AliYun, "img/"+name, path)
|
||||
case "3":
|
||||
return qiniuUpload("img/"+name, path)
|
||||
return upload(file_store.QiNiuKodo, config.ExtConfig.FileStore.QiNiu, "img/"+name, path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ossUpload(name string, path string) error {
|
||||
oss := file_store.ALiYunOSS{}
|
||||
return oss.UpLoad(name, path)
|
||||
func upload(driver file_store.DriverType, store config.ObjectStore, name, path string) error {
|
||||
if !store.Configured() {
|
||||
return fmt.Errorf("file store %s is not configured; set it under extend.fileStore", driver)
|
||||
}
|
||||
oxs := file_store.OXS{
|
||||
Endpoint: store.Endpoint,
|
||||
AccessKeyID: store.AccessKeyID,
|
||||
AccessKeySecret: store.AccessKeySecret,
|
||||
BucketName: store.BucketName,
|
||||
}
|
||||
client, err := oxs.Setup(driver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.UpLoad(name, path)
|
||||
}
|
||||
|
||||
func qiniuUpload(name string, path string) error {
|
||||
oss := file_store.ALiYunOSS{}
|
||||
return oss.UpLoad(name, path)
|
||||
}
|
||||
@@ -3,16 +3,14 @@ package apis
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shirou/gopsutil/v3/net"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
"github.com/shirou/gopsutil/v3/host"
|
||||
@@ -26,22 +24,9 @@ const (
|
||||
GB = 1024 * MB
|
||||
)
|
||||
|
||||
var (
|
||||
Version string
|
||||
expectDiskFsTypes = []string{
|
||||
"apfs", "ext4", "ext3", "ext2", "f2fs", "reiserfs", "jfs", "btrfs",
|
||||
"fuseblk", "zfs", "simfs", "ntfs", "fat32", "exfat", "xfs", "fuse.rclone",
|
||||
}
|
||||
excludeNetInterfaces = []string{
|
||||
"lo", "tun", "docker", "veth", "br-", "vmbr", "vnet", "kube",
|
||||
}
|
||||
getMacDiskNo = regexp.MustCompile(`\/dev\/disk(\d)s.*`)
|
||||
)
|
||||
|
||||
var (
|
||||
netInSpeed, netOutSpeed, netInTransfer, netOutTransfer, lastUpdateNetStats uint64
|
||||
cachedBootTime time.Time
|
||||
)
|
||||
var excludeNetInterfaces = []string{
|
||||
"lo", "tun", "docker", "veth", "br-", "vmbr", "vnet", "kube",
|
||||
}
|
||||
|
||||
type ServerMonitor struct {
|
||||
api.Api
|
||||
@@ -49,137 +34,148 @@ type ServerMonitor struct {
|
||||
|
||||
// GetHourDiffer 获取相差时间
|
||||
func GetHourDiffer(startTime, endTime string) int64 {
|
||||
var hour int64
|
||||
t1, err := time.ParseInLocation("2006-01-02 15:04:05", startTime, time.Local)
|
||||
t2, err := time.ParseInLocation("2006-01-02 15:04:05", endTime, time.Local)
|
||||
if err == nil && t1.Before(t2) {
|
||||
diff := t2.Unix() - t1.Unix() //
|
||||
hour = diff / 3600
|
||||
return hour
|
||||
} else {
|
||||
return hour
|
||||
t1, err1 := time.ParseInLocation("2006-01-02 15:04:05", startTime, time.Local)
|
||||
t2, err2 := time.ParseInLocation("2006-01-02 15:04:05", endTime, time.Local)
|
||||
if err1 != nil || err2 != nil || !t1.Before(t2) {
|
||||
return 0
|
||||
}
|
||||
return (t2.Unix() - t1.Unix()) / 3600
|
||||
}
|
||||
|
||||
// ServerInfo 获取系统信息
|
||||
// @Summary 系统信息
|
||||
// @Description 获取JSON
|
||||
// @Tags 系统信息
|
||||
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
|
||||
// @Router /api/v1/server-monitor [get]
|
||||
// @Security Bearer
|
||||
func (e ServerMonitor) ServerInfo(c *gin.Context) {
|
||||
e.Context = c
|
||||
|
||||
sysInfo, err := host.Info()
|
||||
osDic := make(map[string]interface{}, 0)
|
||||
osDic["goOs"] = runtime.GOOS
|
||||
osDic["arch"] = runtime.GOARCH
|
||||
osDic["mem"] = runtime.MemProfileRate
|
||||
osDic["compiler"] = runtime.Compiler
|
||||
osDic["version"] = runtime.Version()
|
||||
osDic["numGoroutine"] = runtime.NumGoroutine()
|
||||
osDic["ip"] = pkg.GetLocaHonst()
|
||||
osDic["projectDir"] = pkg.GetCurrentPath()
|
||||
osDic["hostName"] = sysInfo.Hostname
|
||||
osDic["time"] = time.Now().Format("2006-01-02 15:04:05")
|
||||
osInfo := getOSInfo()
|
||||
memInfo := getMemoryInfo()
|
||||
swapInfo := getSwapInfo()
|
||||
cpuInfo := getCPUInfo()
|
||||
diskInfo := getDiskInfo()
|
||||
netInfo := getNetworkInfo()
|
||||
|
||||
mem, _ := mem.VirtualMemory()
|
||||
memDic := make(map[string]interface{}, 0)
|
||||
memDic["used"] = mem.Used / MB
|
||||
memDic["total"] = mem.Total / MB
|
||||
bootTime, _ := host.BootTime()
|
||||
cachedBootTime := time.Unix(int64(bootTime), 0)
|
||||
|
||||
fmt.Println("mem", int(mem.Total/mem.Used*100))
|
||||
memDic["percent"] = pkg.Round(mem.UsedPercent, 2)
|
||||
e.Custom(gin.H{
|
||||
"code": 200,
|
||||
"os": osInfo,
|
||||
"mem": memInfo,
|
||||
"cpu": cpuInfo,
|
||||
"disk": diskInfo,
|
||||
"net": netInfo,
|
||||
"swap": swapInfo,
|
||||
"location": "Aliyun",
|
||||
"bootTime": GetHourDiffer(cachedBootTime.Format("2006-01-02 15:04:05"), time.Now().Format("2006-01-02 15:04:05")),
|
||||
})
|
||||
}
|
||||
|
||||
swapDic := make(map[string]interface{}, 0)
|
||||
swapDic["used"] = mem.SwapTotal - mem.SwapFree
|
||||
swapDic["total"] = mem.SwapTotal
|
||||
func getOSInfo() map[string]interface{} {
|
||||
sysInfo, _ := host.Info()
|
||||
return map[string]interface{}{
|
||||
"goOs": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
"mem": runtime.MemProfileRate,
|
||||
"compiler": runtime.Compiler,
|
||||
"version": runtime.Version(),
|
||||
"numGoroutine": runtime.NumGoroutine(),
|
||||
"ip": pkg.GetLocalHost(),
|
||||
"projectDir": pkg.GetCurrentPath(),
|
||||
"hostName": sysInfo.Hostname,
|
||||
"time": time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
}
|
||||
|
||||
cpuDic := make(map[string]interface{}, 0)
|
||||
cpuDic["cpuInfo"], _ = cpu.Info()
|
||||
func getMemoryInfo() map[string]interface{} {
|
||||
memory, _ := mem.VirtualMemory()
|
||||
return map[string]interface{}{
|
||||
"used": memory.Used / MB,
|
||||
"total": memory.Total / MB,
|
||||
"percent": pkg.Round(memory.UsedPercent, 2),
|
||||
}
|
||||
}
|
||||
|
||||
func getSwapInfo() map[string]interface{} {
|
||||
memory, _ := mem.VirtualMemory()
|
||||
return map[string]interface{}{
|
||||
"used": memory.SwapTotal - memory.SwapFree,
|
||||
"total": memory.SwapTotal,
|
||||
}
|
||||
}
|
||||
|
||||
func getCPUInfo() map[string]interface{} {
|
||||
cpuInfo, _ := cpu.Info()
|
||||
percent, _ := cpu.Percent(0, false)
|
||||
cpuDic["percent"] = pkg.Round(percent[0], 2)
|
||||
cpuDic["cpuNum"], _ = cpu.Counts(false)
|
||||
cpuNum, _ := cpu.Counts(false)
|
||||
return map[string]interface{}{
|
||||
"cpuInfo": cpuInfo,
|
||||
"percent": pkg.Round(percent[0], 2),
|
||||
"cpuNum": cpuNum,
|
||||
}
|
||||
}
|
||||
|
||||
//服务器磁盘信息
|
||||
disklist := make([]disk.UsageStat, 0)
|
||||
//所有分区
|
||||
func getDiskInfo() map[string]interface{} {
|
||||
var diskTotal, diskUsed, diskUsedPercent float64
|
||||
diskList := make([]disk.UsageStat, 0)
|
||||
|
||||
diskInfo, err := disk.Partitions(true)
|
||||
if err == nil {
|
||||
for _, p := range diskInfo {
|
||||
diskDetail, err := disk.Usage(p.Mountpoint)
|
||||
if err == nil {
|
||||
diskDetail.UsedPercent, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", diskDetail.UsedPercent), 64)
|
||||
diskDetail.Total = diskDetail.Total / 1024 / 1024
|
||||
diskDetail.Used = diskDetail.Used / 1024 / 1024
|
||||
diskDetail.Free = diskDetail.Free / 1024 / 1024
|
||||
disklist = append(disklist, *diskDetail)
|
||||
|
||||
diskDetail.Total /= MB
|
||||
diskDetail.Used /= MB
|
||||
diskDetail.Free /= MB
|
||||
diskList = append(diskList, *diskDetail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d, _ := disk.Usage("/")
|
||||
|
||||
diskTotal = float64(d.Total / GB)
|
||||
diskUsed = float64(d.Used / GB)
|
||||
diskUsedPercent, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", d.UsedPercent), 64)
|
||||
|
||||
diskDic := make(map[string]interface{}, 0)
|
||||
diskDic["total"] = diskTotal
|
||||
diskDic["used"] = diskUsed
|
||||
diskDic["percent"] = diskUsedPercent
|
||||
|
||||
bootTime, _ := host.BootTime()
|
||||
cachedBootTime = time.Unix(int64(bootTime), 0)
|
||||
|
||||
TrackNetworkSpeed()
|
||||
netDic := make(map[string]interface{}, 0)
|
||||
netDic["in"] = pkg.Round(float64(netInSpeed/KB), 2)
|
||||
netDic["out"] = pkg.Round(float64(netOutSpeed/KB), 2)
|
||||
e.Custom(gin.H{
|
||||
"code": 200,
|
||||
"os": osDic,
|
||||
"mem": memDic,
|
||||
"cpu": cpuDic,
|
||||
"disk": diskDic,
|
||||
"net": netDic,
|
||||
"swap": swapDic,
|
||||
"location": "Aliyun",
|
||||
"bootTime": GetHourDiffer(cachedBootTime.Format("2006-01-02 15:04:05"), time.Now().Format("2006-01-02 15:04:05")),
|
||||
})
|
||||
return map[string]interface{}{
|
||||
"total": diskTotal,
|
||||
"used": diskUsed,
|
||||
"percent": diskUsedPercent,
|
||||
}
|
||||
}
|
||||
|
||||
func TrackNetworkSpeed() {
|
||||
var innerNetInTransfer, innerNetOutTransfer uint64
|
||||
func getNetworkInfo() map[string]interface{} {
|
||||
netInSpeed, netOutSpeed := trackNetworkSpeed()
|
||||
return map[string]interface{}{
|
||||
"in": pkg.Round(float64(netInSpeed/KB), 2),
|
||||
"out": pkg.Round(float64(netOutSpeed/KB), 2),
|
||||
}
|
||||
}
|
||||
|
||||
func trackNetworkSpeed() (uint64, uint64) {
|
||||
var netInSpeed, netOutSpeed, netInTransfer, netOutTransfer, lastUpdateNetStats uint64
|
||||
nc, err := net.IOCounters(true)
|
||||
if err == nil {
|
||||
for _, v := range nc {
|
||||
if isListContainsStr(excludeNetInterfaces, v.Name) {
|
||||
continue
|
||||
}
|
||||
innerNetInTransfer += v.BytesRecv
|
||||
innerNetOutTransfer += v.BytesSent
|
||||
netInTransfer += v.BytesRecv
|
||||
netOutTransfer += v.BytesSent
|
||||
}
|
||||
now := uint64(time.Now().Unix())
|
||||
diff := now - lastUpdateNetStats
|
||||
if diff > 0 {
|
||||
netInSpeed = (innerNetInTransfer - netInTransfer) / diff
|
||||
fmt.Println("netInSpeed", netInSpeed)
|
||||
netOutSpeed = (innerNetOutTransfer - netOutTransfer) / diff
|
||||
fmt.Println("netOutSpeed", netOutSpeed)
|
||||
netInSpeed = (netInTransfer - netInTransfer) / diff
|
||||
netOutSpeed = (netOutTransfer - netOutTransfer) / diff
|
||||
}
|
||||
netInTransfer = innerNetInTransfer
|
||||
netOutTransfer = innerNetOutTransfer
|
||||
lastUpdateNetStats = now
|
||||
}
|
||||
return netInSpeed, netOutSpeed
|
||||
}
|
||||
|
||||
func isListContainsStr(list []string, str string) bool {
|
||||
for i := 0; i < len(list); i++ {
|
||||
if strings.Contains(str, list[i]) {
|
||||
for _, item := range list {
|
||||
if strings.Contains(str, item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package tools
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
)
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// @Param pageIndex query int false "pageIndex / 页码"
|
||||
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
|
||||
// @Router /api/v1/db/columns/page [get]
|
||||
func (e *Gen) GetDBColumnList(c *gin.Context) {
|
||||
func (e Gen) GetDBColumnList(c *gin.Context) {
|
||||
e.Context = c
|
||||
log := e.GetLogger()
|
||||
var data tools.DBColumns
|
||||
@@ -41,7 +41,7 @@ func (e *Gen) GetDBColumnList(c *gin.Context) {
|
||||
}
|
||||
|
||||
data.TableName = c.Request.FormValue("tableName")
|
||||
pkg.Assert(data.TableName == "", "table name cannot be empty!", 500)
|
||||
pkg.Assert(data.TableName != "", "table name cannot be empty!", 500)
|
||||
result, count, err := data.GetPage(db, pageSize, pageIndex)
|
||||
if err != nil {
|
||||
log.Errorf("GetPage error, %s", err.Error())
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/go-admin-team/go-admin-core/v2/logger"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/common/middleware"
|
||||
)
|
||||
|
||||
const emptyTableNameMsg = "table name cannot be empty!"
|
||||
|
||||
// bodyOf covers both the success and the CustomError shape: both carry msg.
|
||||
type bodyOf struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// newColumnListEngine wires the handler the way the router does, including the
|
||||
// middleware that turns pkg.Assert's panic into a response.
|
||||
func newColumnListEngine(t *testing.T) *gin.Engine {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
|
||||
// The query targets MySQL's information_schema; the driver setting only has
|
||||
// to select that branch, the statement itself is never expected to succeed.
|
||||
previous := config.DatabaseConfig.Driver
|
||||
config.DatabaseConfig.Driver = "mysql"
|
||||
t.Cleanup(func() { config.DatabaseConfig.Driver = previous })
|
||||
|
||||
r := gin.New()
|
||||
r.Use(middleware.CustomError)
|
||||
r.GET("/db/columns/page", func(c *gin.Context) {
|
||||
c.Set("db", db)
|
||||
c.Set(pkg.LoggerKey, logger.NewHelper(logger.DefaultLogger))
|
||||
Gen{}.GetDBColumnList(c)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func columnListMsg(t *testing.T, r *gin.Engine, query string) bodyOf {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/db/columns/page"+query, nil))
|
||||
|
||||
var body bodyOf
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("decode %q: %v", w.Body.String(), err)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func TestGetDBColumnList_AcceptsATableName(t *testing.T) {
|
||||
body := columnListMsg(t, newColumnListEngine(t), "?tableName=sys_user")
|
||||
if body.Msg == emptyTableNameMsg {
|
||||
t.Fatalf("request carried a table name and was still rejected as empty: %+v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDBColumnList_RejectsAMissingTableName(t *testing.T) {
|
||||
body := columnListMsg(t, newColumnListEngine(t), "")
|
||||
if body.Msg != emptyTableNameMsg {
|
||||
t.Fatalf("missing table name should be rejected, got %+v", body)
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ package tools
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||
_ "github.com/go-admin-team/go-admin-core/v2/response"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
|
||||
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
||||
|
||||
"go-admin/app/other/models/tools"
|
||||
)
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
// @Param pageIndex query int false "pageIndex / 页码"
|
||||
// @Success 200 {object} response.Response "{"code": 200, "data": [...]}"
|
||||
// @Router /api/v1/db/tables/page [get]
|
||||
func (e *Gen) GetDBTableList(c *gin.Context) {
|
||||
func (e Gen) GetDBTableList(c *gin.Context) {
|
||||
//var res response.Response
|
||||
var data tools.DBTables
|
||||
var err error
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user