更新
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@ cbuild
|
||||
*.exe
|
||||
vender/
|
||||
vendor/
|
||||
# 各产品线 CI 编译产物(goframe_elevue / goframe_antdvue / goframe_naivevue …)
|
||||
# 各产品线 CI 编译产物
|
||||
/goframe_*
|
||||
|
||||
# ----- 运行时数据(不入库) -----
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ======================================================================
|
||||
# EasyGoAdmin_GoFrame_EleVue 常用命令
|
||||
# EasyGoAdmin_GoFrame_AntdVue 常用命令
|
||||
# 前置:go install github.com/gogf/gf/cmd/gf/v2@latest
|
||||
# ======================================================================
|
||||
GF ?= gf
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 📚 项目介绍
|
||||
|
||||
基于 GoFrame v2、Vue3、TypeScript、ElementPlus 打造的模块化、高性能前后端分离敏捷开发框架,可快速搭建企业级后台管理系统。框架以简化开发、提升效率为初衷,内置代码生成器,可根据已建表结构一键生成整个模块的增删改查代码,并同步创建菜单、权限节点与字典数据,切实实现低代码开发,缩短研发周期。
|
||||
基于 GoFrame v2、Vue3、TypeScript、AntDesign 打造的模块化、高性能前后端分离敏捷开发框架,可快速搭建企业级后台管理系统。框架以简化开发、提升效率为初衷,内置代码生成器,可根据已建表结构一键生成整个模块的增删改查代码,并同步创建菜单、权限节点与字典数据,切实实现低代码开发,缩短研发周期。
|
||||
|
||||
框架提供体系化的后端能力:多租户数据隔离、RBAC 权限与分级数据权限、JWT 双令牌认证、接口限流、操作日志审计、在线用户/服务器/缓存三组监控、可视化定时任务等开箱即用;并配有全局钩子机制,审计字段、数据软删、租户归属等横切逻辑统一在数据层自动完成,业务开发零负担。
|
||||
|
||||
@@ -49,12 +49,12 @@
|
||||
+ 案例演示:代码生成器一键生成后的演示案例。
|
||||
|
||||
## 👷 软件信息
|
||||
* 软件名称:EasyGoAdmin敏捷开发框架GoFrame+ElementPlus版本
|
||||
* 软件名称:EasyGoAdmin敏捷开发框架GoFrame+AntDesign版本
|
||||
* 软件作者:@小蚂蚁云 团队
|
||||
* 软件协议:Apache-2.0
|
||||
* 官网网址:[https://www.easygoadmin.vip](https://www.easygoadmin.vip)
|
||||
* 文档网址:[http://docs.goframe.elevue.easygoadmin.vip](http://docs.goframe.elevue.easygoadmin.vip)
|
||||
* 演示地址:[http://manage.goframe.elevue.easygoadmin.vip](http://manage.goframe.elevue.easygoadmin.vip)
|
||||
* 文档网址:[http://docs.goframe.antdvue.easygoadmin.vip](http://docs.goframe.antdvue.easygoadmin.vip)
|
||||
* 演示地址:[http://manage.goframe.antdvue.easygoadmin.vip](http://manage.goframe.antdvue.easygoadmin.vip)
|
||||
|
||||
## 📌 版本说明
|
||||
|
||||
@@ -80,7 +80,7 @@ Iris+NaiveUI前后端分离版 | 采用Iris、Vue3、TypeScript、NaiveUI等框
|
||||
## 🍪 项目结构
|
||||
|
||||
```text
|
||||
EasyGoAdmin_GoFrame_EleVue/
|
||||
EasyGoAdmin_GoFrame_AntdVue/
|
||||
├── main.go # 服务入口:注册五库驱动 + Redis 适配器 → cmd.Run
|
||||
├── Makefile # run/build/test/vet/fmt/gen(gen-dao)/clean
|
||||
├── api/ # 全部请求/响应结构体(单包 v1,每模块一文件)
|
||||
@@ -130,7 +130,7 @@ EasyGoAdmin_GoFrame_EleVue/
|
||||
|
||||
```sh
|
||||
# 1. 初始化数据库:导入定稿脚本
|
||||
# document/mysql/easygoadmin.goframe.elevue.sql → 库 easygoadmin.goframe.elevue
|
||||
# document/mysql/easygoadmin.goframe.antdvue.sql → 库 easygoadmin.goframe.antdvue
|
||||
|
||||
# 2. 启动后端(默认 :8000,配置见 manifest/config/config.yaml)
|
||||
go run main.go
|
||||
@@ -142,7 +142,7 @@ cd ui && pnpm install && pnpm dev
|
||||
cd docker && copy .env.example .env && docker compose up -d --build
|
||||
```
|
||||
|
||||
登录账号 `admin / 123456`。更多启动细节与常见问题见[在线文档·快速入门](http://docs.goframe.elevue.easygoadmin.vip/zh/quickstart/environment.html)。
|
||||
登录账号 `admin / 123456`。更多启动细节与常见问题见[在线文档·快速入门](http://docs.goframe.antdvue.easygoadmin.vip/zh/quickstart/environment.html)。
|
||||
|
||||
## 🔧 模块展示
|
||||
|
||||
@@ -168,4 +168,4 @@ cd docker && copy .env.example .env && docker compose up -d --build
|
||||
|  |  |
|
||||
|
||||
## ✨ 特别鸣谢
|
||||
感谢[GoFrame](https://goframe.org/#all-updates)、[Vue.js](https://cn.vuejs.org/)、[Element Plus](https://element-plus.org/zh-CN/)、[TypeScript](https://www.typescriptlang.org/)、[Vite](https://cn.vitejs.dev/)、[Pinia](https://pinia.vuejs.org/)等优秀开源项目。
|
||||
感谢[GoFrame](https://goframe.org/#all-updates)、[Vue.js](https://cn.vuejs.org/)、[AntDesign](https://www.antdv.com)、[TypeScript](https://www.typescriptlang.org/)、[Vite](https://cn.vitejs.dev/)、[Pinia](https://pinia.vuejs.org/)等优秀开源项目。
|
||||
@@ -42,6 +42,8 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gmeta"
|
||||
@@ -177,6 +179,15 @@ type ExampleImportRes struct {
|
||||
Reasons []string `json:"reasons"` // 失败原因列表
|
||||
}
|
||||
|
||||
// ResMsg 导入结果提示语(response.MsgProvider,前端成功通道直接弹出):
|
||||
// 无失败行时"成功导入 X 条数据",有失败行时"成功导入 X 条,失败 Y 条"
|
||||
func (r *ExampleImportRes) ResMsg() string {
|
||||
if r.Failed > 0 {
|
||||
return fmt.Sprintf("成功导入 %d 条,失败 %d 条", r.Success, r.Failed)
|
||||
}
|
||||
return fmt.Sprintf("成功导入 %d 条数据", r.Success)
|
||||
}
|
||||
|
||||
// ExampleExportReq 导出案例演示请求
|
||||
type ExampleExportReq struct {
|
||||
gmeta.Meta `method:"get" path:"/api/example/export" summary:"导出案例演示" permission:"sys:example:export" log:"案例演示-导出数据" logType:"export"`
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gmeta"
|
||||
@@ -174,6 +176,15 @@ type LevelImportRes struct {
|
||||
Reasons []string `json:"reasons"` // 失败原因列表
|
||||
}
|
||||
|
||||
// ResMsg 导入结果提示语(response.MsgProvider,前端成功通道直接弹出):
|
||||
// 无失败行时"成功导入 X 条数据",有失败行时"成功导入 X 条,失败 Y 条"
|
||||
func (r *LevelImportRes) ResMsg() string {
|
||||
if r.Failed > 0 {
|
||||
return fmt.Sprintf("成功导入 %d 条,失败 %d 条", r.Success, r.Failed)
|
||||
}
|
||||
return fmt.Sprintf("成功导入 %d 条数据", r.Success)
|
||||
}
|
||||
|
||||
// LevelExportReq 导出职级请求
|
||||
// 按查询条件导出符合条件的记录
|
||||
type LevelExportReq struct {
|
||||
|
||||
@@ -71,6 +71,7 @@ type LoginLogItem struct {
|
||||
StatusText string `json:"statusText"` // 状态文本
|
||||
Error string `json:"error"` // 错误信息
|
||||
CreateUser string `json:"createUser"` // 创建人
|
||||
Avatar string `json:"avatar"` // 登录账号头像(读侧按用户名回查补全,已补访问域名)
|
||||
CreateTime *gtime.Time `json:"createTime"` // 创建时间
|
||||
UpdateTime *gtime.Time `json:"updateTime"` // 更新时间
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ type OperLogItem struct {
|
||||
StatusText string `json:"statusText"` // 状态文本
|
||||
Error string `json:"error"` // 错误信息
|
||||
CreateUser string `json:"createUser"` // 操作人
|
||||
Avatar string `json:"avatar"` // 操作人头像(读侧按用户名回查补全,已补访问域名)
|
||||
CreateTime *gtime.Time `json:"createTime"` // 创建时间
|
||||
UpdateTime *gtime.Time `json:"updateTime"` // 更新时间
|
||||
}
|
||||
|
||||
+25
-16
@@ -189,28 +189,37 @@ type TenantBatchDeleteReq struct {
|
||||
// TenantBatchDeleteRes 批量删除结果
|
||||
type TenantBatchDeleteRes struct{}
|
||||
|
||||
// TenantAccountReq 创建租户账号请求
|
||||
// 同时创建租户及其初始管理员账号
|
||||
// TenantAccountReq 添加租户账号请求
|
||||
// 在指定租户下创建用户账号(租户管理页的用户列表入口),
|
||||
// 字段口径与 UserAddReq 一致(avatar 提交上传接口返回的地址,
|
||||
// city 为四级区划编码数组),另带目标租户ID tenantId
|
||||
type TenantAccountReq struct {
|
||||
gmeta.Meta `method:"post" path:"/api/tenant/account" summary:"创建租户账号" permission:"sys:tenant:add" log:"租户管理-创建租户账号" logType:"add"`
|
||||
Name string `json:"name" v:"required#请输入租户名称" dc:"租户名称"` // 租户名称
|
||||
Contact string `json:"contact" dc:"联系人"` // 联系人
|
||||
Mobile string `json:"mobile" v:"phone#手机号格式不正确" dc:"联系电话"` // 联系电话
|
||||
Email string `json:"email" v:"email#邮箱格式不正确" dc:"邮箱"` // 邮箱
|
||||
Address string `json:"address" dc:"地址"` // 地址
|
||||
ExpireTime string `json:"expireTime" dc:"过期时间"` // 过期时间
|
||||
MaxUserNum int `json:"maxUserNum" dc:"用户数上限"` // 用户数上限
|
||||
Note string `json:"note" dc:"备注"` // 备注
|
||||
Username string `json:"username" v:"required#请输入管理员账号|length:2,30#账号长度需在2~30位" dc:"初始管理员账号"` // 初始管理员账号
|
||||
Password string `json:"password" v:"required#请输入密码|length:6,32#密码长度需在6~32位" dc:"初始管理员密码"` // 初始管理员密码
|
||||
Realname string `json:"realname" dc:"管理员姓名"` // 管理员姓名
|
||||
gmeta.Meta `method:"post" path:"/api/tenant/account" summary:"添加租户账号" permission:"sys:tenant:add" log:"租户管理-添加账号" logType:"add"`
|
||||
TenantId int `json:"tenantId" v:"required#缺少租户ID" dc:"目标租户ID"` // 目标租户ID
|
||||
Username string `json:"username" v:"required#请输入用户名|length:2,30#用户名长度需在2~30位" dc:"用户名"` // 用户名
|
||||
Password string `json:"password" v:"required#请输入密码|length:6,32#密码长度需在6~32位" dc:"密码"` // 密码
|
||||
Realname string `json:"realname" v:"required#请输入姓名" dc:"姓名"` // 姓名
|
||||
Gender int `json:"gender" dc:"性别:1-男 2-女 3-保密"` // 性别
|
||||
Mobile string `json:"mobile" v:"phone#手机号格式不正确" dc:"手机号"` // 手机号
|
||||
Email string `json:"email" v:"email#邮箱格式不正确" dc:"邮箱"` // 邮箱
|
||||
Roles []int64 `json:"roles" dc:"角色ID列表"` // 角色ID列表
|
||||
DeptId int `json:"deptId" dc:"部门ID"` // 部门ID
|
||||
LevelId int `json:"levelId" dc:"职级ID"` // 职级ID
|
||||
PositionId int `json:"positionId" dc:"岗位ID"` // 岗位ID
|
||||
City []string `json:"city" dc:"省市区街道编码数组"` // 省市区街道编码数组
|
||||
Address string `json:"address" dc:"详细地址"` // 详细地址
|
||||
Avatar string `json:"avatar" dc:"头像"` // 头像
|
||||
Intro string `json:"intro" dc:"个人简介"` // 个人简介
|
||||
Status int `json:"status" dc:"状态:1-正常 2-禁用"` // 状态
|
||||
Sort int `json:"sort" dc:"排序"` // 排序
|
||||
Note string `json:"note" dc:"备注"` // 备注
|
||||
}
|
||||
|
||||
// TenantAccountRes 创建租户账号结果
|
||||
// TenantAccountRes 添加租户账号结果
|
||||
type TenantAccountRes struct {
|
||||
TenantId int64 `json:"tenantId"` // 租户ID
|
||||
Code string `json:"code"` // 租户编码
|
||||
UserId int64 `json:"userId"` // 管理员用户ID
|
||||
UserId int64 `json:"userId"` // 新用户ID
|
||||
}
|
||||
|
||||
// TenantUsersReq 查询租户用户请求
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gmeta"
|
||||
@@ -255,6 +257,15 @@ type UserImportRes struct {
|
||||
Reasons []string `json:"reasons"` // 失败原因列表
|
||||
}
|
||||
|
||||
// ResMsg 导入结果提示语(response.MsgProvider,前端成功通道直接弹出):
|
||||
// 无失败行时"成功导入 X 条数据",有失败行时"成功导入 X 条,失败 Y 条"
|
||||
func (r *UserImportRes) ResMsg() string {
|
||||
if r.Failed > 0 {
|
||||
return fmt.Sprintf("成功导入 %d 条,失败 %d 条", r.Success, r.Failed)
|
||||
}
|
||||
return fmt.Sprintf("成功导入 %d 条数据", r.Success)
|
||||
}
|
||||
|
||||
// UserExportReq 导出用户请求
|
||||
// 按查询条件导出符合条件的记录
|
||||
type UserExportReq struct {
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
//
|
||||
// go run ./cmd/migrator \
|
||||
// -src-type=mysql -src-host=127.0.0.1 -src-hostport=3306 \
|
||||
// -src-database=easygoadmin.goframe.elevue -src-username=root -src-password=root \
|
||||
// -src-database=easygoadmin.goframe.antdvue -src-username=root -src-password=root \
|
||||
// -dst-type=pgsql -dst-host=127.0.0.1 -dst-hostport=5432 \
|
||||
// -dst-database=easygoadmin.goframe.elevue -dst-username=postgres -dst-password=123456 \
|
||||
// -dst-database=easygoadmin.goframe.antdvue -dst-username=postgres -dst-password=123456 \
|
||||
// -drop-target-first
|
||||
//
|
||||
// 可选:-tables "goframe_user,goframe_level"(只迁指定表)、-batch-size=1000(数据批大小)
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ MYSQL_HOST=mysql
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_USER=root
|
||||
MYSQL_PASSWORD=root
|
||||
MYSQL_DATABASE=easygoadmin.goframe.elevue
|
||||
MYSQL_DATABASE=easygoadmin.goframe.antdvue
|
||||
|
||||
# ===== Redis =====
|
||||
# 同 MYSQL_HOST 规则;容器内 127.0.0.1 指向容器自身,连不上宿主机 Redis
|
||||
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
# (GF_ 键名 = 配置路径大写下划线,如 file.uploadDir → GF_FILE_UPLOADDIR)
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
GF_DATABASE_DEFAULT_LINK: "mysql:${MYSQL_USER:-root}:${MYSQL_PASSWORD:-root}@tcp(${MYSQL_HOST:-mysql}:${MYSQL_PORT:-3306})/${MYSQL_DATABASE:-easygoadmin.goframe.elevue}?charset=utf8mb4&parseTime=true&loc=Local"
|
||||
GF_DATABASE_DEFAULT_LINK: "mysql:${MYSQL_USER:-root}:${MYSQL_PASSWORD:-root}@tcp(${MYSQL_HOST:-mysql}:${MYSQL_PORT:-3306})/${MYSQL_DATABASE:-easygoadmin.goframe.antdvue}?charset=utf8mb4&parseTime=true&loc=Local"
|
||||
GF_REDIS_DEFAULT_ADDRESS: "${REDIS_HOST:-redis}:${REDIS_PORT:-6379}"
|
||||
GF_REDIS_DEFAULT_PASS: "${REDIS_PASSWORD:-123456}"
|
||||
GF_FILE_UPLOADDIR: "/data/upload"
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD:-root}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-easygoadmin.goframe.elevue}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-easygoadmin.goframe.antdvue}
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
# 宿主映射端口写死(仅供宿主机客户端直连调试;宿主机 3306 被占用时改此处)
|
||||
@@ -65,7 +65,7 @@ services:
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
# 首次初始化:导入定稿 SQL(仅空数据卷时执行一次)
|
||||
- ../document/mysql/easygoadmin.goframe.elevue.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||
- ../document/mysql/easygoadmin.goframe.antdvue.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_unicode_ci
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
gfcli:
|
||||
gen:
|
||||
dao:
|
||||
link: "mysql:root:root@tcp(127.0.0.1:3306)/easygoadmin.goframe.elevue?charset=utf8mb4"
|
||||
link: "mysql:root:root@tcp(127.0.0.1:3306)/easygoadmin.goframe.antdvue?charset=utf8mb4"
|
||||
removePrefix: "goframe_" # goframe_user → User/UserDao
|
||||
group: "default"
|
||||
jsonCase: "CamelLower"
|
||||
|
||||
@@ -102,8 +102,8 @@ func (c *CTenant) BatchDelete(ctx context.Context, req *v1.TenantBatchDeleteReq)
|
||||
return service.Tenant().BatchDelete(ctx, req)
|
||||
}
|
||||
|
||||
// Account 创建租户账号
|
||||
// 同时创建租户及其初始管理员账号
|
||||
// Account 添加租户账号
|
||||
// 在指定租户下创建用户账号,委托 service.Tenant.Account
|
||||
func (c *CTenant) Account(ctx context.Context, req *v1.TenantAccountReq) (res *v1.TenantAccountRes, err error) {
|
||||
return service.Tenant().Account(ctx, req)
|
||||
}
|
||||
|
||||
@@ -241,7 +241,7 @@ func SerializeMapOnRead(ctx context.Context, data map[string]any, fieldDict map[
|
||||
|
||||
// LogTypeText 操作日志类型转中文名
|
||||
//
|
||||
// 该字段存储的是注解常量值(1=新增…40=解绑),与字典编码体系不一致,
|
||||
// 该字段存储的是标签常量值(1=新增…40=解绑),与字典编码体系不一致,
|
||||
// 故使用静态映射表而非字典;未知值回退原数字。
|
||||
func LogTypeText(v int) string {
|
||||
if n, ok := consts.LogTypeNames[v]; ok {
|
||||
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
"easygoadmin/api/v1"
|
||||
"easygoadmin/internal/dao"
|
||||
"easygoadmin/internal/service"
|
||||
"easygoadmin/utility/upload"
|
||||
)
|
||||
|
||||
// ============================== 登录日志业务实现 ==============================
|
||||
@@ -72,6 +73,44 @@ var loginLogSpec = ModuleSpec[v1.LoginLogItem, v1.LoginLogItem, v1.LoginLogDetai
|
||||
EqFields: []string{"log_type", "status"},
|
||||
OrderBy: "id DESC",
|
||||
SerializeMaps: loginLogSerializeMaps,
|
||||
// 头像按登录账号用户名回查补齐(失败登录无对应用户时留空)
|
||||
AfterPageList: func(ctx context.Context, list []*v1.LoginLogItem, req any) error {
|
||||
return fillLoginLogAvatars(ctx, list)
|
||||
},
|
||||
AfterDetail: func(ctx context.Context, item *v1.LoginLogDetailRes, req any) error {
|
||||
return fillLoginLogAvatars(ctx, []*v1.LoginLogItem{&item.LoginLogItem})
|
||||
},
|
||||
}
|
||||
|
||||
// fillLoginLogAvatars 按登录账号用户名批量查询并回填头像
|
||||
//
|
||||
// 日志表不冗余存头像:读侧将用户名去重后一次 IN 查询用户表的
|
||||
// username/avatar 相对路径,经 upload.GetFileUrl 补全访问域名;
|
||||
// 查询失败原样上抛。无有效用户名或用户不存在(登录失败记录)时留空。
|
||||
func fillLoginLogAvatars(ctx context.Context, items []*v1.LoginLogItem) error {
|
||||
nameMap := map[string]bool{}
|
||||
names := make([]string, 0, len(items))
|
||||
for _, it := range items {
|
||||
if it.Username != "" && !nameMap[it.Username] {
|
||||
nameMap[it.Username] = true
|
||||
names = append(names, it.Username)
|
||||
}
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
rows, err := dao.User.Ctx(ctx).WhereIn("username", names).Fields("username, avatar").All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
avatarMap := make(map[string]string, len(rows))
|
||||
for _, r := range rows {
|
||||
avatarMap[r["username"].String()] = upload.GetFileUrl(ctx, r["avatar"].String())
|
||||
}
|
||||
for i := range items {
|
||||
items[i].Avatar = avatarMap[items[i].Username]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -95,7 +134,15 @@ func (s *sLoginLog) List(ctx context.Context, req *v1.LoginLogListReq) (res v1.L
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return derefSlice(list), nil
|
||||
rows := derefSlice(list)
|
||||
ptrs := make([]*v1.LoginLogItem, len(rows))
|
||||
for i := range rows {
|
||||
ptrs[i] = &rows[i]
|
||||
}
|
||||
if err := fillLoginLogAvatars(ctx, ptrs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// Detail 登录日志详情
|
||||
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
"easygoadmin/internal/dao"
|
||||
"easygoadmin/internal/middleware"
|
||||
"easygoadmin/internal/service"
|
||||
"easygoadmin/utility/upload"
|
||||
)
|
||||
|
||||
// ============================== 操作日志业务实现 ==============================
|
||||
@@ -70,19 +71,50 @@ var operLogSpec = ModuleSpec[v1.OperLogItem, v1.OperLogItem, v1.OperLogDetailRes
|
||||
EqFields: []string{"type", "status"},
|
||||
OrderBy: "id DESC",
|
||||
SerializeMaps: operLogSerializeMaps,
|
||||
// typeText 走静态映射(注解常量值,与字典体系无关)
|
||||
// typeText 走静态映射(标签常量值,与字典体系无关);头像按操作人用户名回查补齐
|
||||
AfterPageList: func(ctx context.Context, list []*v1.OperLogItem, req any) error {
|
||||
for _, item := range list {
|
||||
item.TypeText = LogTypeText(item.Type)
|
||||
}
|
||||
return nil
|
||||
return fillOperLogAvatars(ctx, list)
|
||||
},
|
||||
AfterDetail: func(ctx context.Context, item *v1.OperLogDetailRes, req any) error {
|
||||
item.TypeText = LogTypeText(item.Type)
|
||||
return nil
|
||||
return fillOperLogAvatars(ctx, []*v1.OperLogItem{&item.OperLogItem})
|
||||
},
|
||||
}
|
||||
|
||||
// fillOperLogAvatars 按操作人用户名批量查询并回填头像
|
||||
//
|
||||
// 日志表只落 create_user(用户名),头像不冗余存储:读侧将用户名去重后
|
||||
// 一次 IN 查询用户表的 username/avatar 相对路径,经 upload.GetFileUrl
|
||||
// 补全访问域名;查询失败原样上抛。无有效用户名(未认证请求)时直接返回。
|
||||
func fillOperLogAvatars(ctx context.Context, items []*v1.OperLogItem) error {
|
||||
nameMap := map[string]bool{}
|
||||
names := make([]string, 0, len(items))
|
||||
for _, it := range items {
|
||||
if it.CreateUser != "" && !nameMap[it.CreateUser] {
|
||||
nameMap[it.CreateUser] = true
|
||||
names = append(names, it.CreateUser)
|
||||
}
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
rows, err := dao.User.Ctx(ctx).WhereIn("username", names).Fields("username, avatar").All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
avatarMap := make(map[string]string, len(rows))
|
||||
for _, r := range rows {
|
||||
avatarMap[r["username"].String()] = upload.GetFileUrl(ctx, r["avatar"].String())
|
||||
}
|
||||
for i := range items {
|
||||
items[i].Avatar = avatarMap[items[i].CreateUser]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterOperLog(&sOperLog{})
|
||||
// 操作日志中间件落库接线(异步批量,见 middleware/operation_log.go)
|
||||
@@ -131,10 +163,16 @@ func (s *sOperLog) List(ctx context.Context, req *v1.OperLogListReq) (res v1.Ope
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range list {
|
||||
item.TypeText = LogTypeText(item.Type)
|
||||
rows := derefSlice(list)
|
||||
ptrs := make([]*v1.OperLogItem, len(rows))
|
||||
for i := range rows {
|
||||
rows[i].TypeText = LogTypeText(rows[i].Type)
|
||||
ptrs[i] = &rows[i]
|
||||
}
|
||||
return derefSlice(list), nil
|
||||
if err := fillOperLogAvatars(ctx, ptrs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// Detail 操作日志详情
|
||||
|
||||
+40
-68
@@ -55,7 +55,6 @@ import (
|
||||
"easygoadmin/internal/dao"
|
||||
"easygoadmin/internal/service"
|
||||
"easygoadmin/utility/cache"
|
||||
"easygoadmin/utility/crypto"
|
||||
)
|
||||
|
||||
// ============================== 租户业务实现 ==============================
|
||||
@@ -307,80 +306,53 @@ func (s *sTenant) Add(ctx context.Context, req *v1.TenantAddReq) (res *v1.IdRes,
|
||||
return &v1.IdRes{Id: idv}, nil
|
||||
}
|
||||
|
||||
// Account 创建租户账号
|
||||
// Account 添加租户账号
|
||||
//
|
||||
// 同时创建租户及其初始管理员账号;
|
||||
// bcrypt 属 CPU 密集操作,放在事务外预计算,保持短事务
|
||||
// 在指定租户(req.TenantId)下创建用户账号,返回目标租户ID、租户编码与新用户ID。
|
||||
// 平台级专属:拦截租户侧身份——本操作按请求显式指定的目标租户写入,
|
||||
// 且唯一性校验/用户数上限的查询链依赖平台视角(无租户收敛)。
|
||||
// 目标租户不存在返回 CodeTenantNotFound;账号唯一性、用户数上限、编码生成、
|
||||
// 密码加盐、头像转正、角色关联复用用户新增链路(sUser.addWithTenant)。
|
||||
func (s *sTenant) Account(ctx context.Context, req *v1.TenantAccountReq) (res *v1.TenantAccountRes, err error) {
|
||||
// 租户:自增回填(临时编码占位 → T%04d 修正)
|
||||
tempCode := fmt.Sprintf("T%010d", time.Now().UnixNano()%10000000000)
|
||||
tData := map[string]any{
|
||||
"name": req.Name,
|
||||
"code": tempCode,
|
||||
"contact": req.Contact,
|
||||
"mobile": req.Mobile,
|
||||
"email": req.Email,
|
||||
"address": req.Address,
|
||||
"status": 1,
|
||||
"max_user_num": req.MaxUserNum,
|
||||
"note": req.Note,
|
||||
if !consts.IsPlatform(ctx) {
|
||||
return nil, gerror.NewCode(consts.CodeForbidden)
|
||||
}
|
||||
if req.ExpireTime != "" {
|
||||
if t := gtime.New(req.ExpireTime); t != nil {
|
||||
tData["expire_time"] = t
|
||||
}
|
||||
// 目标租户存在性校验(同时取编码供响应回传)
|
||||
one, e := dao.Tenant.Ctx(ctx).Where("id", req.TenantId).Fields("code").One()
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
// bcrypt 属 CPU 密集操作,放在事务外预计算(短事务纪律)
|
||||
salt := crypto.GenerateSalt()
|
||||
hashed, err := crypto.HashPassword(req.Password, salt)
|
||||
if len(one) == 0 {
|
||||
return nil, gerror.NewCode(consts.CodeTenantNotFound)
|
||||
}
|
||||
// 用户表单字段转用户新增入参,按目标租户落库
|
||||
idRes, err := (&sUser{}).addWithTenant(ctx, req.TenantId, &v1.UserAddReq{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Realname: req.Realname,
|
||||
Gender: req.Gender,
|
||||
Mobile: req.Mobile,
|
||||
Email: req.Email,
|
||||
Roles: req.Roles,
|
||||
DeptId: req.DeptId,
|
||||
LevelId: req.LevelId,
|
||||
PositionId: req.PositionId,
|
||||
City: req.City,
|
||||
Address: req.Address,
|
||||
Avatar: req.Avatar,
|
||||
Intro: req.Intro,
|
||||
Status: req.Status,
|
||||
Sort: req.Sort,
|
||||
Note: req.Note,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
realname := req.Realname
|
||||
if realname == "" {
|
||||
realname = req.Username
|
||||
}
|
||||
|
||||
// 租户 + 初始管理员三段写入保持原子性(全部经 txCtx)
|
||||
var tenantId, uid int64
|
||||
var code string
|
||||
if err = WithTrans(ctx, func(txCtx context.Context) error {
|
||||
tResult, e := dao.Tenant.Ctx(txCtx).Data(tData).Insert()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
tenantId, _ = tResult.LastInsertId()
|
||||
code = fmt.Sprintf("T%04d", tenantId)
|
||||
if _, e = dao.Tenant.Ctx(txCtx).Where("id", tenantId).Data(map[string]any{"code": code}).Update(); e != nil {
|
||||
return e
|
||||
}
|
||||
// 初始管理员(tenant_id 由 hooks 自动填充)
|
||||
tenantCtx := consts.WithTenantID(txCtx, int(tenantId))
|
||||
uResult, e := dao.User.Ctx(tenantCtx).Data(map[string]any{
|
||||
"username": req.Username,
|
||||
"code": fmt.Sprintf("%s-000001", code), // 首个用户序号 000001
|
||||
"password": hashed,
|
||||
"salt": salt,
|
||||
"realname": realname,
|
||||
"mobile": req.Mobile,
|
||||
"email": req.Email,
|
||||
"province_code": "",
|
||||
"city_code": "",
|
||||
"district_code": "",
|
||||
"street_code": "",
|
||||
"address": "",
|
||||
"status": 1,
|
||||
"tenant_id": int(tenantId),
|
||||
}).Insert()
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
uid, _ = uResult.LastInsertId()
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.TenantAccountRes{TenantId: tenantId, Code: code, UserId: uid}, nil
|
||||
return &v1.TenantAccountRes{
|
||||
TenantId: int64(req.TenantId),
|
||||
Code: one["code"].String(),
|
||||
UserId: idRes.Id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Users 查询租户用户列表
|
||||
|
||||
+13
-2
@@ -277,9 +277,20 @@ func (s *sUser) Detail(ctx context.Context, req *v1.UserDetailReq) (res *v1.User
|
||||
|
||||
// Add 新增用户
|
||||
//
|
||||
// 用户名与手机号租户内唯一,校验租户用户上限,自动生成用户编码并写入 user_role
|
||||
// 入参 *v1.UserAddReq;返回新记录ID供前端刷新定位。
|
||||
// 租户取当前登录租户,落库链路见 addWithTenant。
|
||||
func (s *sUser) Add(ctx context.Context, req *v1.UserAddReq) (res *v1.IdRes, err error) {
|
||||
tenantId := consts.GetTenantID(ctx)
|
||||
return s.addWithTenant(ctx, consts.GetTenantID(ctx), req)
|
||||
}
|
||||
|
||||
// addWithTenant 在指定租户下创建用户(Add 与租户账号添加的共用实现)
|
||||
//
|
||||
// tenantId 为目标租户ID:Add 传当前登录租户,租户账号添加(Tenant.Account)
|
||||
// 传请求携带的目标租户。顺序契约:用户名/手机号租户内唯一校验 → 租户用户数
|
||||
// 上限校验 → 生成用户编码 → 密码双重加盐 → 组装落库 map → 头像 temp 转正 →
|
||||
// 写入 gin_user → 重建角色关联;重复返回 CodeRecordExists,超限返回
|
||||
// CodeTenantUserLimit,查询/加密错误原样上抛。
|
||||
func (s *sUser) addWithTenant(ctx context.Context, tenantId int, req *v1.UserAddReq) (res *v1.IdRes, err error) {
|
||||
if exists, e := s.usernameExists(ctx, req.Username, tenantId, 0); e != nil {
|
||||
return nil, e
|
||||
} else if exists {
|
||||
|
||||
@@ -164,6 +164,13 @@ func makeHandler(method reflect.Value, reqType reflect.Type, rm meta.RouteMeta)
|
||||
response.Write(r, response.Ok(nil))
|
||||
return
|
||||
}
|
||||
response.Write(r, response.Ok(out[0].Interface()))
|
||||
data := out[0].Interface()
|
||||
// 成功提示语:结果对象实现 response.MsgProvider 时按其口径定制
|
||||
// (如导入结果"成功导入 X 条数据"),否则回退默认"操作成功"
|
||||
if mp, ok := data.(response.MsgProvider); ok {
|
||||
response.Write(r, response.OkMsg(mp.ResMsg(), data))
|
||||
return
|
||||
}
|
||||
response.Write(r, response.Ok(data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ type ITenant interface {
|
||||
Delete(ctx context.Context, req *v1.TenantDeleteReq) (res *v1.TenantDeleteRes, err error)
|
||||
// BatchDelete 批量删除租户
|
||||
BatchDelete(ctx context.Context, req *v1.TenantBatchDeleteReq) (res *v1.TenantBatchDeleteRes, err error)
|
||||
// Account 创建租户账号
|
||||
// 同时创建租户及其初始管理员账号
|
||||
// Account 添加租户账号
|
||||
// 在指定租户下创建用户账号
|
||||
Account(ctx context.Context, req *v1.TenantAccountReq) (res *v1.TenantAccountRes, err error)
|
||||
// Users 查询租户用户列表
|
||||
Users(ctx context.Context, req *v1.TenantUsersReq) (res *v1.TenantUsersRes, err error)
|
||||
|
||||
@@ -32,7 +32,7 @@ logger:
|
||||
database:
|
||||
default:
|
||||
type: "mysql" # 数据库类型
|
||||
link: "mysql:root:root@tcp(127.0.0.1:3306)/easygoadmin.goframe.elevue?charset=utf8mb4&parseTime=true&loc=Local" # 连接串
|
||||
link: "mysql:root:root@tcp(127.0.0.1:3306)/easygoadmin.goframe.antdvue?charset=utf8mb4&parseTime=true&loc=Local" # 连接串
|
||||
debug: false # 是否打印 SQL 调试日志(调试期开启,生产环境改回 false)
|
||||
# 其他方言示例:
|
||||
# type: "pgsql" link: "pgsql:user:password@tcp(127.0.0.1:5432)/easygoadmin?sslmode=disable"
|
||||
@@ -56,7 +56,7 @@ redis:
|
||||
# JWT 双令牌:accessToken 短时效 + refreshToken 长时效轮换
|
||||
# ----------------------------------------------------------------------------
|
||||
jwt:
|
||||
secret: "easygoadmin-goframe-elevue-jwt-secret-2026" # 签名密钥
|
||||
secret: "easygoadmin-goframe-jwt-secret-2026" # 签名密钥
|
||||
issuer: "easygoadmin" # 签发者
|
||||
accessTokenExpire: 7200 # 访问令牌有效期(秒),2 小时
|
||||
refreshTokenExpire: 604800 # 刷新令牌有效期(秒),7 天
|
||||
@@ -134,8 +134,8 @@ cors:
|
||||
# 文件上传:临时目录中转,提交保存时转正至正式目录
|
||||
# ----------------------------------------------------------------------------
|
||||
file:
|
||||
uploadDir: "E:\\EasyGoAdmin_V3\\EasyGoAdmin_GoFrame_EleVue\\upload" # 物理根目录,相对项目根
|
||||
domainUrl: "http://file.goframe.elevue" # 文件访问域名,为空时使用 /upload/ 相对路径
|
||||
uploadDir: "E:\\EasyGoAdmin_GoFrame_AntdVue\\upload" # 物理根目录,相对项目根
|
||||
domainUrl: "http://file.goframe.antdvue" # 文件访问域名,为空时使用 /upload/ 相对路径
|
||||
tempDir: "temp" # 临时目录名,位于 uploadDir 下
|
||||
imageExt: "gif,jpg,jpeg,png,bmp,webp,svg" # 允许的图片扩展名
|
||||
fileExt: "gif,jpg,jpeg,png,bmp,webp,svg,doc,docx,xls,xlsx,ppt,pptx,pdf,zip,rar,txt,csv,mp3,mp4,sql,js,css" # 允许的文件扩展名
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -43,6 +43,8 @@ package v1
|
||||
|
||||
import (
|
||||
{{- if .HasImport}}
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
{{- end}}
|
||||
{{- if .NeedGtime}}
|
||||
@@ -182,6 +184,15 @@ type {{.ModelClass}}ImportRes struct {
|
||||
Failed int `json:"failed"` // 失败数
|
||||
Reasons []string `json:"reasons"` // 失败原因列表
|
||||
}
|
||||
|
||||
// ResMsg 导入结果提示语(response.MsgProvider,前端成功通道直接弹出):
|
||||
// 无失败行时"成功导入 X 条数据",有失败行时"成功导入 X 条,失败 Y 条"
|
||||
func (r *{{.ModelClass}}ImportRes) ResMsg() string {
|
||||
if r.Failed > 0 {
|
||||
return fmt.Sprintf("成功导入 %d 条,失败 %d 条", r.Success, r.Failed)
|
||||
}
|
||||
return fmt.Sprintf("成功导入 %d 条数据", r.Success)
|
||||
}
|
||||
{{- end}}
|
||||
{{- if .HasExport}}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -30,6 +30,14 @@ type PageData struct {
|
||||
Total int `json:"total"` // 总记录数
|
||||
}
|
||||
|
||||
// MsgProvider 业务结果自定义成功提示语接口
|
||||
//
|
||||
// 成功信封默认 msg 为"操作成功";实现本接口的结果对象可按自身数据生成
|
||||
// 面向用户的提示语(如导入结果"成功导入 X 条数据"),由统一出口优先采用。
|
||||
type MsgProvider interface {
|
||||
ResMsg() string
|
||||
}
|
||||
|
||||
// Ok 构造成功响应
|
||||
//
|
||||
// data 为 nil 时输出空对象,避免前端判空逻辑失败
|
||||
|
||||
Reference in New Issue
Block a user