chore: 并入 iotlcm-app 历史到 app/(原提交 f40bda9)

This commit is contained in:
MiFaZhan
2026-11-02 13:28:00 +08:00
1107 changed files with 147143 additions and 0 deletions
@@ -0,0 +1,513 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"slices"
"strings"
"time"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/bcache"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
)
type sSysAuth struct {
}
func NewSysAuth() *sSysAuth {
return &sSysAuth{}
}
func init() {
service.RegisterSysAuth(NewSysAuth())
}
func (s *sSysAuth) Login(ctx context.Context, param *model.LoginParams) (res *model.LoginUserOut, token string, err error) {
ip := g.RequestFromCtx(ctx).GetClientIp()
logininfor := &model.SysLogininforAddModel{
TenantId: param.TenantId,
UserName: param.Username,
ClientKey: "web",
DeviceType: "web",
Ipaddr: ip,
LoginLocation: utility.GetCityByIp(ip),
Browser: utility.GetBrowser(ctx),
Os: utility.GetOs(ctx),
Status: "0",
Msg: "登录成功",
LoginTime: gtime.Now(),
}
// 验证验证码
err = service.SysCaptcha().VerifyCaptcha(ctx, param.CaptchaID, param.CaptchaValue)
if err != nil {
logininfor.Status = "1"
logininfor.Msg = err.Error()
return nil, "", err
}
// 获取用户信息
user, err := service.SysUser().GetUserByUsernameAndPassword(ctx, param.TenantId, param.Username, param.Password)
if err != nil {
logininfor.Status = "1"
logininfor.Msg = err.Error()
return nil, "", err
}
userOut := &model.LoginUserOut{
ID: user.UserId,
Username: user.UserName,
NickName: user.NickName,
Avatar: user.Avatar,
TenantId: user.TenantId,
DeptId: user.DeptId,
}
// 构建临时的上下文
identity := &model.Identity{
BaseClaims: model.BaseClaims{
ID: user.UserId,
Username: user.UserName,
NickName: user.NickName,
TenantId: user.TenantId,
DeptId: user.DeptId,
Roles: []model.Role{},
LoginAt: time.Now().Unix(),
},
}
contextModel := new(model.Context)
contextModel.User = identity
ctx = contexts.Set(ctx, contextModel)
// 生成token
claims, token, err := s.GenerateToken(ctx, userOut)
if err != nil {
logininfor.Status = "1"
logininfor.Msg = err.Error()
return nil, "", err
}
if err = service.SysUser().UpdateLoginInfo(ctx, user.UserId, ip); err != nil {
g.Log().Warningf(ctx, "sSysAuth.Login UpdateLoginInfo failed, userId: %d, ip: %s, error: %v", user.UserId, ip, err)
}
// 保存登录日志
go func() {
id, err := service.SysLogininfor().AddLogininfor(ctx, logininfor)
if err != nil {
return
}
// 保存在线列表
service.SysUserOnline().Add(ctx, &model.SysUserOnlineAddModel{
OnlineId: id,
TenantId: user.TenantId,
Uuid: claims.UUID,
UserName: user.UserName,
ClientKey: "web",
DeviceType: "web",
Ipaddr: ip,
LoginLocation: utility.GetCityByIp(ip),
Browser: utility.GetBrowser(ctx),
Os: utility.GetOs(ctx),
Token: token,
LoginTime: gtime.Now(),
ExpireTime: gtime.NewFromTime(claims.ExpiresAt.Time),
})
}()
event.EventsInstance().Emit(ctx, consts.EventKeyUserLogin, user.UserId)
return userOut, token, nil
}
// 生成Token
func (s *sSysAuth) GenerateToken(ctx context.Context, user *model.LoginUserOut) (claims *model.CustomClaims, token string, err error) {
ets := g.Cfg().MustGet(ctx, "jwt.expiresTime", "7d").String()
bts := g.Cfg().MustGet(ctx, "jwt.bufferTime", "1d").String()
iss := g.Cfg().MustGet(ctx, "jwt.issuer", "XiuAdmin").String()
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "39c54195e73304e74a8429b178965865").String()
et, _ := utility.ParseDuration(ets)
bt, _ := utility.ParseDuration(bts)
g.Log().Infof(ctx, "sSysAuth.GenerateToken 生成Token: user:%+v, et:%s, bt:%s, iss:%s, sk:%s", user, et, bt, iss, sk)
roles := make([]model.Role, 0)
if user.RoleIds != nil {
for _, roleId := range user.RoleIds {
roles = append(roles, model.Role{
RoleId: roleId,
DataScope: consts.SysRoleDataScopeAll,
})
}
} else {
authorityIds, err := service.SysUser().GetUserRoleIds(ctx, user.ID)
if err != nil {
return nil, "", err
}
rl, _, err := service.SysRole().List(ctx, &model.SysRoleListParam{
PageInfo: request.PageInfo{
Page: 1,
PageSize: 5000,
},
RoleIds: authorityIds,
})
if err != nil {
return nil, "", err
}
for _, r := range rl {
roles = append(roles, model.Role{
RoleId: r.RoleId,
DataScope: r.DataScope,
})
}
}
g.Log().Debugf(ctx, "sSysAuth.GenerateToken roles: %v", roles)
uuidStr := user.UUID
if uuidStr == "" {
uuidStr = strings.ReplaceAll(uuid.New().String(), "-", "")
}
claims = &model.CustomClaims{
BaseClaims: model.BaseClaims{
UUID: uuidStr,
ID: user.ID,
Username: user.Username,
NickName: user.NickName,
TenantId: user.TenantId,
DeptId: user.DeptId,
Roles: roles,
LoginAt: time.Now().Unix(),
},
BufferTime: int64(bt / time.Second),
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{"XiuAdmin"}, // 受众
ExpiresAt: jwt.NewNumericDate(time.Now().Add(et)), // 过期时间 7天 配置文件
NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间
Issuer: iss, // 发行人
},
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err = t.SignedString([]byte(sk))
if err != nil {
return nil, "", err
}
g.Log().Infof(ctx, "sSysAuth.GenerateToken token: %s, claims: %+v", token, claims)
bcache.SetSysAuthToken(ctx, claims.BaseClaims.ID, claims.BaseClaims.UUID, token, et)
return claims, token, nil
}
// 解析Token
func (s *sSysAuth) ParseToken(ctx context.Context, token string) (claims *model.CustomClaims, err error) {
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "39c54195e73304e74a8429b178965865").String()
t, err := jwt.ParseWithClaims(token, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(sk), nil
})
if err != nil {
return nil, err
}
if t.Claims.(*model.CustomClaims).ExpiresAt.Unix() < time.Now().Unix() {
return nil, errors.New("token已过期")
}
baseClaims := t.Claims.(*model.CustomClaims).BaseClaims
_, err = bcache.GetSysAuthToken(ctx, baseClaims.ID, baseClaims.UUID)
if err != nil {
g.Log().Errorf(ctx, "sSysAuth.ParseToken GetSysAuthToken failed, userId: %d, uuid: %s, error: %v", baseClaims.ID, baseClaims.UUID, err)
return nil, err
}
return t.Claims.(*model.CustomClaims), nil
}
// 删除Token
func (s *sSysAuth) DeleteToken(ctx context.Context, token string) (err error) {
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "39c54195e73304e74a8429b178965865").String()
t, err := jwt.ParseWithClaims(token, &model.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(sk), nil
})
if err != nil {
return err
}
timeout := t.Claims.(*model.CustomClaims).ExpiresAt.Unix() - time.Now().Unix()
if timeout <= 0 {
return nil
}
err = bcache.DelSysAuthToken(ctx, t.Claims.(*model.CustomClaims).BaseClaims.ID, t.Claims.(*model.CustomClaims).BaseClaims.UUID)
if err != nil {
return err
}
bcache.SetSysAuthTokenReject(ctx, t.Claims.(*model.CustomClaims).BaseClaims.ID, t.Claims.(*model.CustomClaims).BaseClaims.UUID, token, time.Duration(timeout)*time.Second)
return nil
}
// 获取token
func (s *sSysAuth) GetAccessToken(ctx context.Context) (token string, err error) {
authorization := g.RequestFromCtx(ctx).Header.Get("Authorization")
authParam := g.RequestFromCtx(ctx).Get("access_token").String()
if authorization == "" && authParam == "" {
return "", gerror.NewCode(gcode.CodeNotImplemented)
}
token = strings.TrimPrefix(authorization, "Bearer ")
if token == "" {
token = authParam
}
return token, nil
}
// 根据Token获取当前登录用户信息
func (s *sSysAuth) GetCurrentUser(ctx context.Context) (claims *model.CustomClaims, err error) {
// 获取token
token, err := s.GetAccessToken(ctx)
if err != nil {
return nil, err
}
// 解析token
claims, err = s.ParseToken(ctx, token)
if err != nil {
g.Log().Warningf(ctx, "sSysAuth.GetCurrentUser ParseToken failed, token: %s, error: %v", token, err)
return nil, err
}
return claims, nil
}
// 获取用户权限码
func (s *sSysAuth) GetUserAccessCodeList(ctx context.Context, userId int64) (accessCodeList []string, menuRoleAccessCodeList []string, err error) {
accessCodeList = make([]string, 0)
menuRoleAccessCodeList = make([]string, 0)
roleDataScopeMap := make(map[int64]string)
// 角色权限码
roleIds, err := service.SysUser().GetUserRoleIds(ctx, userId)
if err != nil {
return nil, nil, err
}
if len(roleIds) > 0 {
rl, _, err := service.SysRole().List(ctx, &model.SysRoleListParam{
PageInfo: request.PageInfo{
Page: 1,
PageSize: 5000,
},
RoleIds: roleIds,
})
if err != nil {
return nil, nil, err
}
for _, r := range rl {
accessCodeList = append(accessCodeList, consts.SysCheckPermissionRolePrefix+r.RoleKey)
roleDataScopeMap[r.RoleId] = r.DataScope
}
}
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList roleIds: %v", roleIds)
// 菜单权限码
// 获取角色菜单列表
rmList, err := service.SysRole().GetRoleListMenu(ctx, roleIds)
if err != nil {
return nil, nil, err
}
type roleMenuDataScopeRow struct {
RoleId int64 `orm:"role_id"`
MenuId int64 `orm:"menu_id"`
DataScope string `orm:"data_scope"`
}
roleMenuDataScopeMap := make(map[string]string)
if len(roleIds) > 0 {
var roleMenuDataScopes []*roleMenuDataScopeRow
roleMenuScopeModel := dao.SysRoleMenu.Ctx(ctx).
Fields(
dao.SysRoleMenu.Columns().RoleId,
dao.SysRoleMenu.Columns().MenuId,
dao.SysRoleMenu.Columns().DataScope,
).
WhereIn(dao.SysRoleMenu.Columns().RoleId, roleIds)
if tenantId := contexts.GetTenantId(ctx); tenantId != "" {
roleMenuScopeModel = roleMenuScopeModel.Where(dao.SysRoleMenu.Columns().TenantId, tenantId)
}
if err = roleMenuScopeModel.Scan(&roleMenuDataScopes); err != nil {
return nil, nil, err
}
for _, row := range roleMenuDataScopes {
if row == nil {
continue
}
roleMenuDataScopeMap[gconv.String(row.RoleId)+"_"+gconv.String(row.MenuId)] = row.DataScope
}
}
menuIds := make([]int64, 0)
for _, menu := range rmList {
menuIds = append(menuIds, menu.MenuId)
}
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList menuIds: %v", menuIds)
if len(menuIds) > 0 {
// 权限码必须读取当前业务库,避免内存菜单库未刷新时拿不到新配置的按钮权限。
var menuList []*entity.SysMenu
err = dao.SysMenu.Ctx(ctx).
WhereIn(dao.SysMenu.Columns().MenuId, menuIds).
Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
Order(dao.SysMenu.Columns().OrderNum).
Scan(&menuList)
if err != nil {
return nil, nil, err
}
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList menuList: %v", menuList)
for _, m := range menuList {
menuAccessCode := normalizeSysMenuAccessCode(m.Perms)
if menuAccessCode == "" {
continue
}
accessCodeList = append(accessCodeList, menuAccessCode)
for _, r := range rmList {
if r.MenuId == m.MenuId {
roleDS := roleDataScopeMap[r.RoleId]
if menuDataScope := roleMenuDataScopeMap[gconv.String(r.RoleId)+"_"+gconv.String(r.MenuId)]; menuDataScope != "" && menuDataScope != consts.SysRoleMenuDataScopeRole {
roleDS = menuDataScope
}
if len(roleDS) < 1 {
roleDS = consts.SysRoleDataScopeAll
}
menuRoleAccessCodeList = append(menuRoleAccessCodeList, menuAccessCode+"|"+gconv.String(r.RoleId)+"|"+roleDS)
}
}
}
}
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList accessCodeList: %v", accessCodeList)
// 获取用户岗位信息
postIds, err := service.SysUser().GetUserPostIds(ctx, userId)
if err != nil {
return nil, nil, err
}
if len(postIds) > 0 {
pl, _, err := service.SysPost().List(ctx, &model.SysPostListParam{
PageInfo: request.PageInfo{
Page: 1,
PageSize: 5000,
},
PostIds: postIds,
})
if err != nil {
return nil, nil, err
}
for _, p := range pl {
accessCodeList = append(accessCodeList, consts.SysCheckPermissionPostPrefix+p.PostCode)
}
}
// 获取用户信息
user, err := service.SysUser().GetUserById(ctx, userId)
if err != nil {
return nil, nil, err
}
accessCodeList = append(accessCodeList, consts.SysCheckPermissionUserPrefix+user.UserName)
accessCodeList = append(accessCodeList, consts.SysCheckPermissionCurrentUser)
accessCodeList = slices.Compact(accessCodeList)
return accessCodeList, menuRoleAccessCodeList, nil
}
func normalizeSysMenuAccessCode(perms string) string {
perms = strings.TrimSpace(perms)
if perms == "" {
return ""
}
for strings.HasPrefix(perms, consts.SysCheckPermissionMenuPrefix+consts.SysCheckPermissionMenuPrefix) {
perms = strings.TrimPrefix(perms, consts.SysCheckPermissionMenuPrefix)
}
if strings.HasPrefix(perms, consts.SysCheckPermissionMenuPrefix) {
return perms
}
return consts.SysCheckPermissionMenuPrefix + perms
}
// 根据openId登录
func (s *sSysAuth) LoginByOpenId(ctx context.Context, social *model.SysSocialListModel) (res *model.LoginUserOut, token string, err error) {
ip := g.RequestFromCtx(ctx).GetClientIp()
logininfor := &model.SysLogininforAddModel{
TenantId: social.User.TenantId,
UserName: social.User.UserName,
ClientKey: "web",
DeviceType: "web",
Ipaddr: ip,
LoginLocation: utility.GetCityByIp(ip),
Browser: utility.GetBrowser(ctx),
Os: utility.GetOs(ctx),
Status: "0",
Msg: "登录成功",
LoginTime: gtime.Now(),
}
user, err := service.SysUser().GetUserByUsername(ctx, social.User.UserName, social.User.TenantId)
if err != nil {
return nil, "用户不存在", err
}
userOut := &model.LoginUserOut{
ID: user.UserId,
Username: user.UserName,
NickName: user.NickName,
Avatar: user.Avatar,
TenantId: user.TenantId,
DeptId: user.DeptId,
}
// 构建临时的上下文
identity := &model.Identity{
BaseClaims: model.BaseClaims{
ID: user.UserId,
Username: user.UserName,
NickName: user.NickName,
TenantId: user.TenantId,
DeptId: user.DeptId,
Roles: []model.Role{},
LoginAt: time.Now().Unix(),
},
}
contextModel := new(model.Context)
contextModel.User = identity
ctx = contexts.Set(ctx, contextModel)
// 生成token
claims, token, err := s.GenerateToken(ctx, userOut)
if err != nil {
logininfor.Status = "1"
logininfor.Msg = err.Error()
return nil, "", err
}
if err = service.SysUser().UpdateLoginInfo(ctx, user.UserId, ip); err != nil {
g.Log().Warningf(ctx, "sSysAuth.LoginByOpenId UpdateLoginInfo failed, userId: %d, ip: %s, error: %v", user.UserId, ip, err)
}
// 保存登录日志
go func() {
id, err := service.SysLogininfor().AddLogininfor(ctx, logininfor)
if err != nil {
return
}
// 保存在线列表
service.SysUserOnline().Add(ctx, &model.SysUserOnlineAddModel{
OnlineId: id,
TenantId: user.TenantId,
Uuid: claims.UUID,
UserName: user.UserName,
ClientKey: "web",
DeviceType: "web",
Ipaddr: ip,
LoginLocation: utility.GetCityByIp(ip),
Browser: utility.GetBrowser(ctx),
Os: utility.GetOs(ctx),
Token: token,
LoginTime: gtime.Now(),
ExpireTime: gtime.NewFromTime(claims.ExpiresAt.Time),
})
}()
event.EventsInstance().Emit(ctx, consts.EventKeyUserLogin, user.UserId)
return userOut, token, nil
}
@@ -0,0 +1,61 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"time"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/mojocn/base64Captcha"
)
var store = base64Captcha.NewMemoryStore(100, 120*time.Second)
type sSysCaptcha struct {
}
func NewSysCaptcha() *sSysCaptcha {
return &sSysCaptcha{}
}
func init() {
service.RegisterSysCaptcha(NewSysCaptcha())
}
type CaptchaConfig struct {
KeyLong int
ImgWidth int
ImgHeight int
OpenCaptcha int
OpenCaptchaTimeout int
}
// 生成验证码
func (l *sSysCaptcha) GenerateCaptcha(ctx context.Context) (key string, image string, err error) {
var captchaConfig CaptchaConfig
g.Cfg().MustGet(ctx, "captcha").Struct(&captchaConfig)
driver := base64Captcha.NewDriverDigit(captchaConfig.ImgHeight, captchaConfig.ImgWidth, captchaConfig.KeyLong, 0.7, 80)
captcha := base64Captcha.NewCaptcha(driver, store)
key, image, _, err = captcha.Generate()
return
}
// 验证验证码
func (l *sSysCaptcha) VerifyCaptcha(ctx context.Context, key string, value string) (err error) {
v := store.Get(key, false)
if len(v) == 0 {
return gerror.NewCode(consts.CodeCaptchaError, "验证码已失效")
}
ok := store.Verify(key, value, true)
if !ok {
err = gerror.NewCode(consts.CodeCaptchaError, "验证码错误")
}
return
}
@@ -0,0 +1,141 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysClient struct {
}
func NewSysClient() *sSysClient {
return &sSysClient{}
}
func init() {
service.RegisterSysClient(NewSysClient())
}
func (s *sSysClient) List(ctx context.Context, query *model.SysClientListParam) (items []*model.SysClientListModel, total int, err error) {
db := dao.SysClient.Ctx(ctx)
if query.ClientId != "" {
db = db.Where(dao.SysClient.Columns().ClientId, query.ClientId)
}
if query.ClientKey != "" {
db = db.Where(dao.SysClient.Columns().ClientKey, query.ClientKey)
}
if query.ClientSecret != "" {
db = db.Where(dao.SysClient.Columns().ClientSecret, query.ClientSecret)
}
if query.Status != "" {
db = db.Where(dao.SysClient.Columns().Status, query.Status)
}
total, err = db.Count()
if err != nil {
return nil, 0, err
}
err = db.Page(query.Page, query.PageSize).OrderAsc(dao.SysClient.Columns().Id).Scan(&items)
if err != nil {
return nil, 0, err
}
return items, total, nil
}
func (s *sSysClient) View(ctx context.Context, param *model.SysClientViewParam) (item *model.SysClientViewModel, err error) {
db := dao.SysClient.Ctx(ctx)
if param.Id != 0 {
db = db.Where(dao.SysClient.Columns().Id, param.Id)
} else if param.ClientId != "" {
db = db.Where(dao.SysClient.Columns().ClientId, param.ClientId)
} else {
return nil, errors.New("未填写查询条件")
}
err = db.Scan(&item)
return
}
func (s *sSysClient) Add(ctx context.Context, param *model.SysClientAddParam) (output *model.SysClientAddModel, err error) {
data := &do.SysClient{}
gconv.Struct(param, data)
data.ClientId = uuid32.S()
data.CreatedAt = gtime.Now()
data.CreatedBy = contexts.GetUserId(ctx)
data.CreatedDept = contexts.GetDeptId(ctx)
db := dao.SysClient.Ctx(ctx)
id, err := db.Data(data).OmitNil().InsertAndGetId()
if err != nil {
return
}
output = &model.SysClientAddModel{
Id: id,
}
return
}
func (s *sSysClient) Edit(ctx context.Context, param *model.SysClientEditParam) (output *model.SysClientEditModel, err error) {
data := &do.SysClient{}
gconv.Struct(param, data)
data.ClientId = nil
data.UpdatedAt = gtime.Now()
data.UpdatedBy = contexts.GetUserId(ctx)
db := dao.SysClient.Ctx(ctx)
_, err = db.Data(data).OmitNil().Where(dao.SysClient.Columns().Id, param.Id).Update()
if err != nil {
return
}
output = &model.SysClientEditModel{
Id: param.Id,
}
return
}
func (s *sSysClient) Delete(ctx context.Context, param *model.SysClientDeleteParam) (output *model.SysClientDeleteModel, err error) {
db := dao.SysClient.Ctx(ctx)
data := &do.SysClient{}
data.DeletedAt = gtime.Now()
data.DeletedBy = contexts.GetUserId(ctx)
_, err = db.Data(data).OmitNil().Where(dao.SysClient.Columns().Id, param.Ids).Update()
if err != nil {
return
}
output = &model.SysClientDeleteModel{
Ids: param.Ids,
}
return
}
func (s *sSysClient) Status(ctx context.Context, param *model.SysClientStatusParam) (output *model.SysClientStatusModel, err error) {
db := dao.SysClient.Ctx(ctx)
data := &do.SysClient{}
gconv.Struct(param, data)
data.Id = nil
_, err = db.Data(data).OmitNil().Where(dao.SysClient.Columns().Id, param.Id).Update()
if err != nil {
return
}
output = &model.SysClientStatusModel{
Id: param.Id,
}
return
}
@@ -0,0 +1,191 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysConfig struct {
}
func NewSysConfig() *sSysConfig {
return &sSysConfig{}
}
func init() {
service.RegisterSysConfig(NewSysConfig())
}
func (l *sSysConfig) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: false,
})
}
return handler.Model(dao.SysConfig.Ctx(ctx), option...)
}
func (l *sSysConfig) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: false,
})
}
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysConfig.Table()), option...)
}
func (s *sSysConfig) List(ctx context.Context, param *model.SysConfigListParam) (items []*model.SysConfigListModel, total int, err error) {
m := s.ModelQuery(ctx)
if param.ConfigName != "" {
m = m.WhereLike(dao.SysConfig.Columns().ConfigName, "%"+param.ConfigName+"%")
}
if param.ConfigKey != "" {
m = m.WhereLike(dao.SysConfig.Columns().ConfigKey, "%"+param.ConfigKey+"%")
}
if param.ConfigType != "" {
m = m.Where(dao.SysConfig.Columns().ConfigType, param.ConfigType)
}
if param.ConfigValue != "" {
m = m.WhereLike(dao.SysConfig.Columns().ConfigValue, "%"+param.ConfigValue+"%")
}
if len(param.CreatedAt) > 0 {
startTime := gtime.NewFromStr(param.CreatedAt[0])
endTime := gtime.NewFromStr(param.CreatedAt[1])
m = m.WhereBetween(dao.SysConfig.Columns().CreatedAt, startTime, endTime.EndOfDay())
}
total, err = m.Count()
if err != nil {
return nil, 0, err
}
err = m.Page(param.Page, param.PageSize).Order(dao.SysConfig.Columns().ConfigId, "ASC").Scan(&items)
if err != nil {
return nil, 0, err
}
return
}
func (s *sSysConfig) Add(ctx context.Context, param *model.SysConfigAddParam) (output *model.SysConfigAddModel, err error) {
if param == nil || param.ConfigName == "" {
return nil, errors.New("配置名称不能为空")
}
m := s.Model(ctx)
data := &do.SysConfig{}
gconv.Struct(param, data)
data.TenantId = contexts.GetTenantId(ctx)
data.CreatedDept = contexts.GetDeptId(ctx)
data.CreatedBy = contexts.GetUserId(ctx)
data.CreatedAt = gtime.Now()
data.UpdatedBy = contexts.GetUserId(ctx)
data.UpdatedAt = gtime.Now()
lastInsertId, err := m.Data(data).InsertAndGetId()
if err != nil {
return nil, err
}
output = &model.SysConfigAddModel{
ConfigId: lastInsertId,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigCreate, lastInsertId, data.TenantId, data.ConfigKey)
return
}
func (s *sSysConfig) Edit(ctx context.Context, param *model.SysConfigEditParam) (output *model.SysConfigEditModel, err error) {
if param == nil || param.ConfigId == 0 {
return nil, errors.New("配置ID不能为空")
}
data := &do.SysConfig{}
gconv.Struct(param, data)
data.UpdatedBy = contexts.GetUserId(ctx)
data.UpdatedAt = gtime.Now()
data.ConfigId = nil
m := s.Model(ctx)
_, err = m.Data(data).Where(dao.SysConfig.Columns().ConfigId, param.ConfigId).Update()
if err != nil {
return nil, err
}
output = &model.SysConfigEditModel{
ConfigId: param.ConfigId,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigUpdate, param.ConfigId, contexts.GetTenantId(ctx), data.ConfigKey)
return
}
func (s *sSysConfig) Delete(ctx context.Context, param *model.SysConfigDeleteParam) (output *model.SysConfigDeleteModel, err error) {
m := s.Model(ctx)
if len(param.ConfigIds) == 0 {
return nil, errors.New("请选择要删除的配置")
}
m = m.WhereIn(dao.SysConfig.Columns().ConfigId, param.ConfigIds)
data := make([]entity.SysConfig, 0)
err = m.Scan(&data)
if err != nil {
return nil, err
}
for _, v := range data {
if v.ConfigType == "Y" {
return nil, errors.New("系统内置配置不能删除")
}
}
_, err = m.Delete()
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigDelete, param.ConfigIds, contexts.GetTenantId(ctx))
return &model.SysConfigDeleteModel{
ConfigIds: param.ConfigIds,
}, nil
}
func (s *sSysConfig) View(ctx context.Context, param *model.SysConfigViewParam) (output *model.SysConfigViewModel, err error) {
m := s.ModelQuery(ctx)
err = m.Where(dao.SysConfig.Columns().ConfigId, param.ConfigId).Scan(&output)
if err != nil {
return nil, err
}
return
}
func (s *sSysConfig) GetConfigByKey(ctx context.Context, configKey string) (config *entity.SysConfig, err error) {
m := s.ModelQuery(ctx)
err = m.Where(dao.SysConfig.Columns().ConfigKey, configKey).Scan(&config)
if err != nil {
return nil, err
}
return
}
@@ -0,0 +1,422 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysDept struct {
}
func NewSysDept() *sSysDept {
return &sSysDept{}
}
func init() {
service.RegisterSysDept(NewSysDept())
}
func (l *sSysDept) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.SysDept.Ctx(ctx), option...)
}
func (l *sSysDept) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysDept.Table()), option...)
}
func (l *sSysDept) GetDeptList(ctx context.Context, query model.SysDeptListParam) (items []*model.SysDeptListModel, total int, err error) {
m := l.ModelQuery(ctx)
if query.DeptName != "" {
m = m.WhereLike(dao.SysDept.Columns().DeptName, "%"+query.DeptName+"%")
}
if query.Status != "" {
m = m.Where(dao.SysDept.Columns().Status, query.Status)
}
if query.DeptType != nil {
m = m.Where(dao.SysDept.Columns().DeptType, *query.DeptType)
}
// 排序:按照显示顺序OrderNum从小到大排序,如果相同则按照部门ID从小到大排序
err = m.Page(1, 5000).Order(dao.SysDept.Columns().OrderNum, "ASC").Order(dao.SysDept.Columns().DeptId, "ASC").Scan(&items)
if err != nil {
return nil, 0, err
}
total = len(items)
return
}
func (l *sSysDept) GetDeptById(ctx context.Context, id int64) (dept *model.SysDeptViewModel, err error) {
user := contexts.GetUser(ctx)
if user == nil {
return nil, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
}
err = l.ModelQuery(ctx).Where(dao.SysDept.Columns().DeptId, id).Scan(&dept)
if err != nil {
return nil, err
}
return dept, nil
}
// 构建树结构
func (l *sSysDept) DeptTree(ctx context.Context, parentDept *model.SysDeptTreeModel, deptList []*model.SysDeptListModel, ancestors string) (data []*model.SysDeptTreeModel, err error) {
data = make([]*model.SysDeptTreeModel, 0)
for _, dept := range deptList {
pId := int64(0)
if parentDept != nil {
pId = parentDept.DeptId
}
if dept.ParentId == pId {
item := &model.SysDeptTreeModel{
DeptId: dept.DeptId,
ParentId: dept.ParentId,
DeptName: dept.DeptName,
Key: fmt.Sprintf("%d", dept.DeptId),
Children: nil,
Ancestors: ancestors,
}
subDept, err := l.DeptTree(ctx, item, deptList, ancestors+","+fmt.Sprintf("%d", dept.DeptId))
if err != nil {
return nil, err
}
item.Children = subDept
data = append(data, item)
}
}
return
}
func (l *sSysDept) GetDeptTree(ctx context.Context, query model.SysDeptTreeParam) (items []*model.SysDeptTreeModel, err error) {
depts, _, err := l.GetDeptList(ctx, model.SysDeptListParam{
DeptType: query.DeptType,
})
if err != nil {
return nil, err
}
if len(depts) == 0 {
return make([]*model.SysDeptTreeModel, 0), nil
}
//找出列表中的顶级部门(可能包含多个)
topDepts := make([]*model.SysDeptTreeModel, 0)
deptMap := make(map[int64]*model.SysDeptTreeModel)
for _, dept := range depts {
deptMap[dept.DeptId] = &model.SysDeptTreeModel{
DeptId: dept.DeptId,
ParentId: dept.ParentId,
DeptName: dept.DeptName,
Key: fmt.Sprintf("%d", dept.DeptId),
Children: nil,
Ancestors: "0",
}
}
for _, dept := range depts {
if _, ok := deptMap[dept.ParentId]; !ok {
topDepts = append(topDepts, deptMap[dept.DeptId])
}
}
for _, dept := range topDepts {
children, err := l.DeptTree(ctx, dept, depts, fmt.Sprintf("%d", dept.DeptId))
if err != nil {
return nil, err
}
dept.Children = children
}
return topDepts, nil
}
// 递归构建结构
func (l *sSysDept) RecursionDeptIds(ctx context.Context, parentId int64, deptList []*model.SysDeptListModel, data *[]int64) (err error) {
for _, dept := range deptList {
if dept.ParentId == parentId {
*data = append(*data, dept.DeptId)
err := l.RecursionDeptIds(ctx, dept.DeptId, deptList, data)
if err != nil {
return err
}
}
}
return
}
// 根据父部门id获取部门列表
func (l *sSysDept) GetDeptIdsByParentId(ctx context.Context, parentId int64) (ids []int64, err error) {
depts, _, err := l.GetDeptList(ctx, model.SysDeptListParam{})
if err != nil {
return nil, err
}
ids = make([]int64, 0)
err = l.RecursionDeptIds(ctx, parentId, depts, &ids)
if err != nil {
return nil, err
}
// g.Log().Infof(ctx, "sSysDept.GetDeptIdsByParentId 获取部门列表: ids:%+v", ids)
return ids, nil
}
func (l *sSysDept) AddDept(ctx context.Context, dept *model.SysDeptAddModel) (deptId int64, err error) {
user := contexts.GetUser(ctx)
if user == nil {
return 0, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
}
if dept.DeptType == 1 {
isCompany, err := l.ValidateParentIsCompany(ctx, dept.ParentId)
if err != nil {
g.Log().Errorf(ctx, "sSysDept.AddDept ValidateParentIsCompany err: %v, parentId:%d, dept:%+v", err, dept.ParentId, dept)
return 0, err
}
if !isCompany {
return 0, gerror.New("上级部门必须是公司")
}
}
data := do.SysDept{}
gconv.Struct(dept, &data)
data.CreatedDept = user.DeptId
data.CreatedBy = user.ID
data.CreatedAt = gtime.Now()
data.UpdatedBy = user.ID
data.UpdatedAt = gtime.Now()
data.TenantId = user.TenantId
deptId, err = l.Model(ctx).Data(data).InsertAndGetId()
if err != nil {
return 0, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDeptCreate, deptId)
// 刷新部门祖先
err = l.RefreshDeptAncestors(ctx)
if err != nil {
return 0, err
}
return deptId, nil
}
func (l *sSysDept) EditDept(ctx context.Context, dept *model.SysDeptEditModel) (deptId int64, err error) {
user := contexts.GetUser(ctx)
if user == nil {
return dept.DeptId, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
}
// 查询现有数据
existingDept, err := l.GetDeptById(ctx, dept.DeptId)
if err != nil {
g.Log().Errorf(ctx, "sSysDept.EditDept GetDeptById err: %v, deptId:%d, dept:%+v", err, dept.DeptId, gconv.String(dept))
return 0, err
}
if existingDept == nil || existingDept.DeptId == 0 || existingDept.DeptId != dept.DeptId {
g.Log().Warningf(ctx, "sSysDept.EditDept GetDeptById err: %v, deptId:%d, dept:%+v", err, dept.DeptId, gconv.String(dept))
return 0, gerror.New("部门不存在")
}
newParentId := existingDept.ParentId
if dept.ParentId != nil {
newParentId = *dept.ParentId
}
if dept.DeptType != nil && *dept.DeptType == 1 {
isCompany, err := l.ValidateParentIsCompany(ctx, newParentId)
if err != nil {
return 0, err
}
if !isCompany {
return 0, gerror.New("上级组织必须是公司")
}
}
if dept.DeptType != nil && *dept.DeptType != existingDept.DeptType {
if existingDept.DeptType == 1 {
return 0, gerror.New("组织类型不能修改")
}
}
data := do.SysDept{}
gconv.Struct(dept, &data)
data.UpdatedBy = user.ID
data.UpdatedAt = gtime.Now()
_, err = l.Model(ctx).Data(data).Where(dao.SysDept.Columns().DeptId, dept.DeptId).Update()
if err != nil {
return dept.DeptId, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDeptUpdate, dept.DeptId)
// 刷新部门祖先
err = l.RefreshDeptAncestors(ctx)
if err != nil {
return 0, err
}
return dept.DeptId, nil
}
func (l *sSysDept) DeleteDept(ctx context.Context, dept *model.SysDeptDeleteModel) (deptId int64, err error) {
count, err := l.ModelQuery(ctx).Where(dao.SysDept.Columns().ParentId, dept.DeptId).Count()
if err != nil {
return dept.DeptId, err
}
if count > 0 {
return dept.DeptId, gerror.NewCode(consts.CodeDeptHasSub, "该部门下有子部门,不能删除")
}
// 部门下有用户,不能删除
userList, _, err := service.SysUser().List(ctx, &request.PageInfo{
Page: 1,
PageSize: 100,
}, &model.UserListParam{
DeptId: dept.DeptId,
})
if err != nil {
return dept.DeptId, err
}
if len(userList) > 0 {
return dept.DeptId, gerror.New("该部门下有用户,不能删除")
}
_, err = l.Model(ctx).Where(dao.SysDept.Columns().DeptId, dept.DeptId).Delete()
if err != nil {
return dept.DeptId, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDeptDelete, dept.DeptId)
return dept.DeptId, nil
}
// 刷新部门 ancestors
func (l *sSysDept) RefreshDeptAncestors(ctx context.Context) (err error) {
var depts []*model.SysDeptViewModel
err = l.ModelQuery(ctx, &handler.Option{
FilterTenant: true,
FilterAuth: false,
}).Scan(&depts)
if err != nil {
return err
}
for _, dept := range depts {
ancestors := ""
l.GetParentIDAncestors(ctx, depts, dept.ParentId, &ancestors)
if ancestors != dept.Ancestors {
_, err = l.Model(ctx).Data(do.SysDept{
Ancestors: ancestors,
}).Where(dao.SysDept.Columns().DeptId, dept.DeptId).Update()
if err != nil {
return err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDeptUpdate, dept.DeptId)
g.Log().Infof(ctx, "sSysDept.RefreshDeptAncestors 刷新部门祖先: deptId:%d, ancestors:%s", dept.DeptId, ancestors)
}
}
return nil
}
func (l *sSysDept) GetParentIDAncestors(ctx context.Context, depts []*model.SysDeptViewModel, printId int64, ancestors *string) (err error) {
for _, dept := range depts {
if dept.DeptId == printId {
*ancestors = fmt.Sprintf("%d", dept.DeptId) + "," + *ancestors
l.GetParentIDAncestors(ctx, depts, dept.ParentId, ancestors)
return nil
}
}
*ancestors = "0," + *ancestors
return nil
}
func (l *sSysDept) GetDeptListByIds(ctx context.Context, ids []int64) (depts []*model.SysDeptListModel, err error) {
err = l.ModelQuery(ctx).WhereIn(dao.SysDept.Columns().DeptId, ids).Scan(&depts)
if err != nil {
return nil, err
}
return depts, nil
}
// 验证上级是否是公司
func (l *sSysDept) ValidateParentIsCompany(ctx context.Context, parentId int64) (isCompany bool, err error) {
dept, err := l.GetDeptById(ctx, parentId)
if err != nil {
return false, err
}
return dept.DeptType == 1 || dept.ParentId == 0, nil
}
// 获取上级公司名称
func (l *sSysDept) getParentCompanyInfo(ctx context.Context, depts []*model.SysDeptListModel, deptId int64) *model.SysDeptListModel {
var topDept *model.SysDeptListModel
for _, dept := range depts {
if dept.ParentId == 0 {
topDept = dept
}
if dept.DeptId == deptId {
if dept.ParentId == 0 {
return dept
}
if dept.DeptType == 1 {
return dept
}
return l.getParentCompanyInfo(ctx, depts, dept.ParentId)
}
}
return topDept
}
// 获取组织公司Map
func (l *sSysDept) GetDeptCompanyMap(ctx context.Context) (map[int64]string, error) {
depts, _, err := l.GetDeptList(ctx, model.SysDeptListParam{})
if err != nil {
return nil, err
}
companyMap := make(map[int64]string)
for _, dept := range depts {
if dept.ParentId == 0 {
companyMap[0] = dept.DeptName
}
if dept.DeptType == 1 {
companyMap[dept.DeptId] = dept.DeptName
} else if dept.ParentId == 0 {
companyMap[dept.DeptId] = dept.DeptName
} else {
parentDept := l.getParentCompanyInfo(ctx, depts, dept.DeptId)
if parentDept != nil {
companyMap[dept.DeptId] = parentDept.DeptName
} else {
companyMap[dept.DeptId] = ""
}
}
}
return companyMap, nil
}
// 获取上级公司组织信息
func (l *sSysDept) GetParentCompanyInfo(ctx context.Context, deptId int64) *model.SysDeptListModel {
depts, _, err := l.GetDeptList(ctx, model.SysDeptListParam{})
if err != nil {
g.Log().Errorf(ctx, "sSysDept.GetParentCompanyInfo GetDeptList err: %v, deptId:%d", err, deptId)
return nil
}
return l.getParentCompanyInfo(ctx, depts, deptId)
}
@@ -0,0 +1,82 @@
// package system_test
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system_test
import (
"context"
"testing"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
_ "git.5iot.com/iotlcm-public/iotlcm-app/internal/logic/memorydb"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
type deptTestMemoryDB struct {
service.IMemoryDB
database gdb.DB
}
func (memory *deptTestMemoryDB) DB(ctx context.Context) gdb.DB {
return memory.database
}
func TestSysDept_RefreshDeptAncestors(t *testing.T) {
var (
ctx = contexts.Set(context.Background(), &model.Context{
User: &model.Identity{BaseClaims: model.BaseClaims{ID: 1, TenantId: "A", DeptId: 1}},
})
previous = gdb.GetConfig("default")
previousMemory = service.MemoryDB()
)
if err := gdb.SetConfigGroup("default", gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}); err != nil {
t.Fatal(err)
}
database := g.DB()
service.RegisterMemoryDB(&deptTestMemoryDB{database: database})
t.Cleanup(func() {
service.RegisterMemoryDB(previousMemory)
if err := database.GetCore().ClearCacheAll(ctx); err != nil {
t.Error(err)
}
if err := database.Close(ctx); err != nil {
t.Error(err)
}
if err := gdb.SetConfigGroup("default", previous); err != nil {
t.Error(err)
}
})
if err := database.GetCore().ClearCacheAll(ctx); err != nil {
t.Fatal(err)
}
statements := []string{
"CREATE TABLE sys_dept (dept_id INTEGER PRIMARY KEY, parent_id INTEGER, tenant_id TEXT, ancestors TEXT, updated_at DATETIME, deleted_at DATETIME)",
"INSERT INTO sys_dept VALUES (1,0,'A','wrong',NULL,NULL),(2,1,'A','wrong',NULL,NULL),(3,2,'A','wrong',NULL,NULL),(4,1,'B','other-tenant',NULL,NULL),(5,1,'A','deleted',NULL,'2026-01-01')",
}
for _, statement := range statements {
if _, err := database.Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
for attempt := 0; attempt < 2; attempt++ {
if err := service.SysDept().RefreshDeptAncestors(ctx); err != nil {
t.Fatal(err)
}
for deptID, expected := range map[int]string{1: "0,", 2: "0,1,", 3: "0,1,2,", 4: "other-tenant", 5: "deleted"} {
actual, err := database.GetValue(ctx, "SELECT ancestors FROM sys_dept WHERE dept_id = ?", deptID)
if err != nil {
t.Fatal(err)
}
if actual.String() != expected {
t.Fatalf("attempt=%d, dept=%d, ancestors=%q, expected=%q", attempt, deptID, actual.String(), expected)
}
}
}
}
@@ -0,0 +1,225 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysDictData struct{}
func NewSysDictData() *sSysDictData {
return &sSysDictData{}
}
func init() {
service.RegisterSysDictData(NewSysDictData())
}
func (l *sSysDictData) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: false,
FilterAuth: false,
})
}
return handler.Model(dao.SysDictData.Ctx(ctx), option...)
}
func (l *sSysDictData) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: false,
FilterAuth: false,
})
}
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysDictData.Table()), option...)
}
func (s *sSysDictData) List(ctx context.Context, param *model.SysDictDataListParam) (items []model.SysDictDataListModel, total int, err error) {
typeData := &model.SysDictTypeViewModel{}
// 获取字典类型
if param.DictType == "" && param.DictId != 0 {
typeData, err = service.SysDictType().View(ctx, &model.SysDictTypeViewParam{
DictId: param.DictId,
})
if err != nil {
return nil, 0, err
}
if typeData == nil {
return nil, 0, gerror.New("字典类型不存在")
}
param.DictType = typeData.DictType
}
if param.DictType == "" {
return nil, 0, gerror.New("字典类型不能为空")
}
// 获取字典数据
items = make([]model.SysDictDataListModel, 0)
m := s.ModelQuery(ctx).Where(dao.SysDictData.Columns().DictType, param.DictType)
if param.DictValue != "" {
m = m.Where(dao.SysDictData.Columns().DictValue, param.DictValue)
}
total, err = m.Count()
if err != nil {
return nil, 0, err
}
err = m.Order(dao.SysDictData.Columns().DictSort, "ASC").Page(param.Page, param.PageSize).Scan(&items)
if err != nil {
return nil, 0, err
}
return
}
func (s *sSysDictData) View(ctx context.Context, param *model.SysDictDataViewParam) (data *model.SysDictDataViewModel, err error) {
if param.DictCode == 0 {
return nil, gerror.New("字典编码不能为空")
}
m := s.ModelQuery(ctx)
if param.DictCode != 0 {
m = m.Where(dao.SysDictData.Columns().DictCode, param.DictCode)
}
err = m.Scan(&data)
if err != nil {
return nil, err
}
return
}
func (s *sSysDictData) Add(ctx context.Context, param *model.SysDictDataAddParam) (output *model.SysDictDataAddModel, err error) {
m := s.Model(ctx)
data := do.SysDictData{}
gconv.Struct(param, &data)
// 通过 DictType 查询字典类型列表
listParam := &model.SysDictTypeListParam{
DictType: param.DictType,
}
typeList, _, err := service.SysDictType().List(ctx, listParam)
if err != nil {
return nil, err
}
if len(typeList) == 0 {
return nil, gerror.New("字典类型不存在")
}
isSys := typeList[0].IsSys
if isSys == "0" && !contexts.IsSuperAdmin(ctx) {
return nil, gerror.New("非超级管理员不能添加系统内置字典数据")
}
data.TenantId = contexts.GetTenantId(ctx)
data.CreatedAt = gtime.Now()
data.CreatedBy = contexts.GetUserId(ctx)
data.CreatedDept = contexts.GetDeptId(ctx)
m = m.Data(data).OmitNil()
output = &model.SysDictDataAddModel{}
output.DictCode, err = m.InsertAndGetId()
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictDataCreate, output.DictCode)
return
}
func (s *sSysDictData) Edit(ctx context.Context, param *model.SysDictDataEditParam) (output *model.SysDictDataEditModel, err error) {
if param.DictCode == 0 {
return nil, gerror.New("字典编码不能为空")
}
m := s.Model(ctx)
data := do.SysDictData{}
gconv.Struct(param, &data)
data.UpdatedAt = gtime.Now()
data.UpdatedBy = contexts.GetUserId(ctx)
m = m.Data(data).OmitNil().Where(dao.SysDictData.Columns().DictCode, param.DictCode)
output = &model.SysDictDataEditModel{
DictCode: param.DictCode,
}
_, err = m.Update()
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictDataUpdate, param.DictCode)
return
}
func (s *sSysDictData) Delete(ctx context.Context, param *model.SysDictDataDeleteParam) (output *model.SysDictDataDeleteModel, err error) {
if len(param.DictCodes) == 0 {
return nil, gerror.New("字典编码不能为空")
}
// 检查要删除的字典数据所属的字典类型是否为系统内置类型
if !contexts.IsSuperAdmin(ctx) {
// 查询字典数据所属的字典类型
var dictTypes []string
err = dao.SysDictData.Ctx(ctx).
Fields(dao.SysDictData.Columns().DictType).
WhereIn(dao.SysDictData.Columns().DictCode, param.DictCodes).
Distinct().
Scan(&dictTypes)
if err != nil {
return nil, err
}
// 检查这些字典类型是否有系统内置类型
var systemTypeCount int
systemTypeCount, err = dao.SysDictType.Ctx(ctx).
WhereIn(dao.SysDictType.Columns().DictType, dictTypes).
Where(dao.SysDictType.Columns().IsSys, "0").
Count()
if err != nil {
return nil, err
}
if systemTypeCount > 0 {
return nil, gerror.New("系统内置字典数据不能删除,请联系管理员")
}
}
m := s.Model(ctx)
m = m.WhereIn(dao.SysDictData.Columns().DictCode, param.DictCodes)
_, err = m.Delete()
if err != nil {
return nil, err
}
output = &model.SysDictDataDeleteModel{
DictCodes: param.DictCodes,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictDataDelete, param.DictCodes)
return
}
func (s *sSysDictData) GetDictLabel(ctx context.Context, dictType string, dictCode string) string {
dictData, _, err := s.List(ctx, &model.SysDictDataListParam{
DictType: dictType,
DictValue: dictCode,
})
if err != nil {
return ""
}
if len(dictData) > 0 {
return dictData[0].DictLabel
}
return ""
}
func (s *sSysDictData) GetDictListByTypes(ctx context.Context, dictTypes []string) (dictDataList []model.SysDictDataListModel, err error) {
dictDataList = make([]model.SysDictDataListModel, 0)
m := s.ModelQuery(ctx).WhereIn(dao.SysDictData.Columns().DictType, dictTypes)
err = m.Scan(&dictDataList)
return
}
@@ -0,0 +1,185 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"fmt"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysDictType struct{}
func NewSysDictType() *sSysDictType {
return &sSysDictType{}
}
func init() {
service.RegisterSysDictType(NewSysDictType())
}
func (l *sSysDictType) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
model := dao.SysDictType.Ctx(ctx)
model = model.Where("is_sys = '0' OR tenant_id = ?", contexts.GetTenantId(ctx))
return model
}
func (l *sSysDictType) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
model := service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysDictType.Table())
model = model.Where("is_sys = '0' OR tenant_id = ?", contexts.GetTenantId(ctx))
return model
}
func (s *sSysDictType) List(ctx context.Context, param *model.SysDictTypeListParam) (items []model.SysDictTypeListModel, total int, err error) {
m := s.ModelQuery(ctx)
if param.DictName != "" {
m = m.WhereLike(dao.SysDictType.Columns().DictName, "%"+param.DictName+"%")
}
if param.DictType != "" {
m = m.WhereLike(dao.SysDictType.Columns().DictType, "%"+param.DictType+"%")
}
if param.IsSys != "" {
m = m.Where(dao.SysDictType.Columns().IsSys, param.IsSys)
}
if len(param.CreatedAt) == 2 {
createdAt := gtime.NewFromStr(param.CreatedAt[0])
createdAtEnd := gtime.NewFromStr(param.CreatedAt[1])
m = m.WhereBetween(dao.SysDictType.Columns().CreatedAt, createdAt, createdAtEnd.EndOfDay())
}
total, err = m.Count()
if err != nil {
return nil, 0, err
}
err = m.Order(dao.SysDictType.Columns().DictId, "DESC").Page(param.Page, param.PageSize).Scan(&items)
if err != nil {
return nil, 0, err
}
return
}
func (s *sSysDictType) View(ctx context.Context, param *model.SysDictTypeViewParam) (data *model.SysDictTypeViewModel, err error) {
m := s.ModelQuery(ctx)
if param.DictId != 0 {
m = m.Where(dao.SysDictType.Columns().DictId, param.DictId)
} else if param.DictType != "" {
m = m.Where(dao.SysDictType.Columns().DictType, param.DictType)
} else {
return nil, errors.New("字典类型ID不能为空")
}
err = m.Scan(&data)
if err != nil {
return nil, err
}
return
}
func (s *sSysDictType) Add(ctx context.Context, param *model.SysDictTypeAddParam) (output *model.SysDictTypeAddModel, err error) {
// 如果设置为系统字典类型,需要检查权限
if param.IsSys == "0" {
if !contexts.IsSuperAdmin(ctx) {
return nil, errors.New("非超级管理员不能添加系统字典类型")
}
}
m := s.Model(ctx)
data := do.SysDictType{}
gconv.Struct(param, &data)
data.TenantId = contexts.GetTenantId(ctx)
data.CreatedAt = gtime.Now()
data.CreatedBy = contexts.GetUserId(ctx)
data.CreatedDept = contexts.GetDeptId(ctx)
m = m.Data(data).OmitNil()
output = &model.SysDictTypeAddModel{}
output.DictId, err = m.InsertAndGetId()
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictTypeCreate, output.DictId)
return
}
func (s *sSysDictType) Edit(ctx context.Context, param *model.SysDictTypeEditParam) (output *model.SysDictTypeEditModel, err error) {
if param.DictId == 0 {
g.Log().Errorf(ctx, "字典类型ID不能为空,param:%+v", param)
return nil, errors.New("字典类型ID不能为空")
}
// 如果设置为系统字典类型,需要检查权限
if param.IsSys == "0" {
if !contexts.IsSuperAdmin(ctx) {
return nil, errors.New("非超级管理员不能修改为系统字典类型")
}
}
m := s.Model(ctx)
data := do.SysDictType{}
gconv.Struct(param, &data)
data.UpdatedAt = gtime.Now()
data.UpdatedBy = contexts.GetUserId(ctx)
m = m.Data(data).OmitNil().Where(dao.SysDictType.Columns().DictId, param.DictId)
output = &model.SysDictTypeEditModel{
DictId: param.DictId,
}
_, err = m.Update()
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictTypeUpdate, param.DictId)
return
}
func (s *sSysDictType) Delete(ctx context.Context, param *model.SysDictTypeDeleteParam) (output *model.SysDictTypeDeleteModel, err error) {
if len(param.DictIds) == 0 {
return nil, errors.New("字典类型ID不能为空")
}
// 检查是否有系统内置字典类型
var systemDictTypes []entity.SysDictType
err = s.Model(ctx).WhereIn(dao.SysDictType.Columns().DictId, param.DictIds).
Where(dao.SysDictType.Columns().IsSys, "0").
Scan(&systemDictTypes)
if err != nil {
return nil, err
}
// 如果有系统内置字典类型,检查当前用户权限
if len(systemDictTypes) > 0 {
if !contexts.IsSuperAdmin(ctx) {
var dictNames []string
for _, dict := range systemDictTypes {
dictNames = append(dictNames, dict.DictName)
}
return nil, fmt.Errorf("系统内置字典类型 [%s] 只有超级管理员可以删除", strings.Join(dictNames, ", "))
}
}
m := s.Model(ctx)
m = m.WhereIn(dao.SysDictType.Columns().DictId, param.DictIds)
_, err = m.Delete()
if err != nil {
return nil, err
}
output = &model.SysDictTypeDeleteModel{
DictIds: param.DictIds,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDictTypeDelete, param.DictIds)
return
}
+392
View File
@@ -0,0 +1,392 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/tasks"
)
func init() {
service.RegisterSysJob(NewSysJob())
}
type sSysJob struct{}
func NewSysJob() *sSysJob {
return &sSysJob{}
}
func (s *sSysJob) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: false,
FilterAuth: true,
})
}
return handler.Model(dao.SysJob.Ctx(ctx), option...)
}
func (c *sSysJob) List(ctx context.Context, query *model.SysJobListParam, pageInfo *request.PageInfo) (Data []*model.SysJobListModel, total int, err error) {
m := c.Model(ctx)
if query.JobName != "" {
m = m.WhereLike(dao.SysJob.Columns().JobName, "%"+strings.Trim(query.JobName, " ")+"%")
}
if query.JobGroup != "" {
m = m.Where(dao.SysJob.Columns().JobGroup, query.JobGroup)
}
if query.Status != "" {
m = m.Where(dao.SysJob.Columns().Status, query.Status)
}
t, err := m.Count()
if err != nil {
return nil, 0, err
}
Data = make([]*model.SysJobListModel, 0)
if err := m.Page(pageInfo.Page, pageInfo.PageSize).Order(dao.SysJob.Columns().JobId, "DESC").Scan(&Data); err != nil {
return nil, 0, err
}
return Data, t, nil
}
func (c *sSysJob) View(ctx context.Context, jobId int64) (Data *model.SysJobViewModel, err error) {
m := c.Model(ctx)
Data = &model.SysJobViewModel{}
if err := m.Where(dao.SysJob.Columns().JobId, jobId).Scan(&Data); err != nil {
return nil, err
}
return Data, nil
}
func (c *sSysJob) Add(ctx context.Context, jobAdd *model.SysJobAddModel) (LastInsertId int64, err error) {
claims := contexts.GetUser(ctx)
if claims == nil {
return 0, gerror.NewCode(gcode.CodeMissingParameter, "用户信息不存在")
}
//获取task目录下是否绑定对应的方法
_, exist := tasks.TasksInstance(ctx).CheckFuncName(jobAdd.InvokeTarget)
if !exist {
errInfo := fmt.Sprintf("没有绑定对应的方法:%s", jobAdd.InvokeTarget)
return 0, gerror.New(errInfo)
}
jobAdd.CreatedDept = claims.BaseClaims.DeptId
jobAdd.CreatedAt = gtime.Now()
jobAdd.CreatedBy = claims.BaseClaims.ID
jobAdd.UpdatedAt = gtime.Now()
jobAdd.UpdatedBy = claims.BaseClaims.ID
data := do.SysJob{}
gconv.Struct(jobAdd, &data)
result, err := c.Model(ctx).Data(data).Insert()
if err != nil {
return 0, err
}
lastInserId, _ := result.LastInsertId()
if lastInserId > 0 && jobAdd.Status == consts.SysJobStatusNormal {
job := &model.SysJobViewModel{}
if err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, lastInserId).Scan(&job); err != nil {
return lastInserId, err
}
//任务启动
err = c.taskRun(ctx, &job.SysJob)
if err != nil {
return lastInserId, err
}
}
return result.LastInsertId()
}
func (c *sSysJob) Update(ctx context.Context, jobUpdate *model.SysJobUpdateModel) (RowsAffected int64, err error) {
claims := contexts.GetUser(ctx)
if claims == nil {
return 0, gerror.NewCode(gcode.CodeMissingParameter, "用户信息不存在")
}
//获取task目录下是否绑定对应的方法
_, exist := tasks.TasksInstance(ctx).CheckFuncName(jobUpdate.InvokeTarget)
if !exist {
errInfo := fmt.Sprintf("没有绑定对应的方法:%s", jobUpdate.InvokeTarget)
return 0, gerror.New(errInfo)
}
jobUpdate.UpdatedAt = gtime.Now()
jobUpdate.UpdatedBy = claims.BaseClaims.ID
data := do.SysJob{}
gconv.Struct(jobUpdate, &data)
result, err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobUpdate.JobId).Data(data).Update()
if err != nil {
return 0, err
}
row, err := result.RowsAffected()
if row > 0 && jobUpdate.Status == consts.SysJobStatusNormal {
job := &model.SysJobViewModel{}
if err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobUpdate.JobId).Scan(&job); err != nil {
return 0, err
}
//任务启动
err = c.taskRestart(ctx, &job.SysJob)
if err != nil {
return row, err
}
}
return row, err
}
func (c *sSysJob) UpdateStatus(ctx context.Context, jobUpdate *model.SysJobUpdateStatusModel) (RowsAffected int64, err error) {
claims := contexts.GetUser(ctx)
if claims == nil {
return 0, gerror.NewCode(gcode.CodeMissingParameter, "用户信息不存在")
}
exists := &model.SysJobViewModel{}
if err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobUpdate.JobId).Scan(&exists); err != nil {
return 0, err
}
if jobUpdate.Status == consts.SysJobStatusNormal {
//任务启动
err = c.taskRun(ctx, &exists.SysJob)
if err != nil {
return 0, err
}
}
if jobUpdate.Status == consts.SysJobStatusDisable {
//任务停止
if err = c.taskStop(ctx, &exists.SysJob); err != nil {
return 0, err
}
}
jobUpdate.UpdatedAt = gtime.Now()
jobUpdate.UpdatedBy = claims.BaseClaims.ID
data := do.SysJob{}
gconv.Struct(jobUpdate, &data)
result, err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobUpdate.JobId).Data(data).Update()
if err != nil {
return 0, err
}
row, err := result.RowsAffected()
return row, err
}
// 任务重启
func (c *sSysJob) taskRestart(ctx context.Context, job *entity.SysJob) (err error) {
//任务停止
if err = c.taskStop(ctx, job); err != nil {
return
}
//任务启动
err = c.taskRun(ctx, job)
if err != nil {
return
}
return
}
// 任务启动
func (c *sSysJob) taskRun(ctx context.Context, job *entity.SysJob) error {
//获取task目录下是否绑定对应的方法
actualName, exist := tasks.TasksInstance(ctx).CheckFuncName(job.InvokeTarget)
if !exist {
errInfo := fmt.Sprintf("没有绑定对应的方法:%s", job.InvokeTarget)
return gerror.New(errInfo)
}
//传参解析
paramArr, err := tasks.TasksInstance(ctx).ParseParameters(job.JobParams)
if err != nil {
return err
}
taskData := tasks.Task{
ID: fmt.Sprintf("%s-job-%d", job.InvokeTarget, job.JobId),
TaskType: "Type-" + gconv.String(job.MisfirePolicy),
MethodName: actualName,
Params: paramArr,
Explain: job.JobName,
}
runPayload, _ := json.Marshal(taskData)
if job.MisfirePolicy == consts.SysJobMisfirePolicyMulty {
// entryID, err := tasks.TasksInstance().Cron(
// job.Concurrent,
// worker.WithTaskCtx(context.Background()),
// worker.WithTaskUid(taskData.ID), // 任务ID
// worker.WithTaskGroup(taskData.MethodName), // 任务组
// worker.WithTaskExpr(job.CronExpression),
// worker.WithTaskTimeout(10),
// worker.WithTaskReplace(true),
// worker.WithTaskPayload(runPayload),
// )
entryID, err := tasks.TasksInstance(ctx).Cron(ctx, tasks.TaskOptions{
Uid: taskData.ID,
Payload: runPayload,
Expr: &job.CronExpression,
Concurrent: job.Concurrent,
})
if err != nil {
g.Log().Debug(ctx, taskData.MethodName, taskData.Explain, "启动任务失败")
return err
}
g.Log().Debug(ctx, taskData.MethodName, taskData.Explain, "启动任务成功",
"entryID", entryID, "CronExpression", job.CronExpression,
"JobId", job.JobId,
"JobName", job.JobName)
} else {
now := true
err := tasks.TasksInstance(ctx).Once(ctx, tasks.TaskOptions{
Uid: taskData.ID,
Now: &now,
Payload: runPayload,
})
if err != nil {
g.Log().Debug(ctx, taskData.MethodName, taskData.Explain, "启动任务失败")
return err
}
}
return nil
}
func (c *sSysJob) taskStop(ctx context.Context, job *entity.SysJob) error {
//获取task目录下是否绑定对应的方法
_, exist := tasks.TasksInstance(ctx).CheckFuncName(job.InvokeTarget)
if !exist {
errInfo := fmt.Sprintf("没有绑定对应的方法:%s", job.InvokeTarget)
return gerror.New(errInfo)
}
taskJobId := fmt.Sprintf("%s-job-%d", job.InvokeTarget, job.JobId)
err := tasks.TasksInstance(ctx).Remove(taskJobId)
if err != nil {
g.Log().Debug(ctx, job.JobName, "启动停止失败")
return err
}
return nil
}
func (c *sSysJob) Delete(ctx context.Context, jobDelete *model.SysJobDeleteModel) (RowsAffected int64, err error) {
claims, err := service.SysAuth().GetCurrentUser(ctx)
if err != nil {
return 0, err
}
exists := &[]*model.SysJobViewModel{}
c.Model(ctx).WhereIn(dao.SysJob.Columns().JobId, jobDelete.JobIds).Where(dao.SysJob.Columns().Status, consts.SysJobStatusNormal).Scan(exists)
if len(*exists) > 0 {
return 0, gerror.New("存在正常运行的任务,请先停止后再删除")
}
jobDelete.DeletedAt = gtime.Now()
jobDelete.DeletedBy = claims.BaseClaims.ID
data := do.SysJob{
DeletedAt: jobDelete.DeletedAt,
DeletedBy: jobDelete.DeletedBy,
}
result, err := c.Model(ctx).WhereIn(dao.SysJob.Columns().JobId, jobDelete.JobIds).Data(data).Update()
if err != nil {
return 0, err
}
return result.RowsAffected()
}
func (c *sSysJob) Exec(ctx context.Context, jobId int64) error {
exists := &model.SysJobViewModel{}
err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobId).Scan(&exists)
if err != nil {
return err
}
actualName, exist := tasks.TasksInstance(ctx).CheckFuncName(exists.InvokeTarget)
if !exist {
errInfo := fmt.Sprintf("没有绑定对应的方法:%s", exists.InvokeTarget)
return gerror.New(errInfo)
}
//传参解析
paramArr, err := tasks.TasksInstance(ctx).ParseParameters(exists.JobParams)
if err != nil {
g.Log().Error(ctx, "sSysJob.JobRun worker.ParseParameters", err)
return err
}
taskData := tasks.Task{
ID: fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId),
TaskType: "Type-" + gconv.String(exists.MisfirePolicy),
MethodName: actualName,
Params: paramArr,
Explain: exists.JobName,
}
runPayload, _ := json.Marshal(taskData)
now := true
err = tasks.TasksInstance(ctx).Once(ctx, tasks.TaskOptions{
Uid: taskData.ID,
Payload: runPayload,
Now: &now,
})
// worker.WithTaskCtx(context.Background()),
// worker.WithTaskUid(taskData.ID), // 任务ID
// worker.WithTaskGroup(taskData.MethodName), // 任务组
// worker.WithTaskTimeout(10),
// worker.WithTaskNow(true),
// worker.WithTaskReplace(true),
// worker.WithTaskPayload(runPayload),
// )
if err != nil {
errInfo := fmt.Sprintf(exists.InvokeTarget, taskData.Explain, "启动任务失败")
return errors.New(errInfo)
}
return nil
}
// 查询所有的状态正常的任务列表,并进行初始注册
func (c *sSysJob) InitRegister() error {
g.Log().Debug(context.Background(), "初始化任务注册")
jobs := &[]*model.SysJobViewModel{}
err := c.Model(context.Background()).Where(dao.SysJob.Columns().Status, consts.SysJobStatusNormal).Scan(jobs)
if err != nil {
return err
}
for _, job := range *jobs {
err = c.taskRun(context.Background(), &job.SysJob)
if err != nil {
g.Log().Error(context.Background(), "初始化注册任务 %s 失败", job.JobName, err)
continue
}
g.Log().Debug(context.Background(), "初始化注册任务 %s 成功", job.JobName)
}
g.Log().Debug(context.Background(), "初始化任务注册完成")
return nil
}
@@ -0,0 +1,97 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysLogininfor struct{}
func NewSysLogininfor() *sSysLogininfor {
return &sSysLogininfor{}
}
func init() {
service.RegisterSysLogininfor(NewSysLogininfor())
}
func (s *sSysLogininfor) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.SysLogininfor.Ctx(ctx), option...)
}
func (s *sSysLogininfor) List(ctx context.Context, param *model.SysLogininforListParam) (items []*model.SysLogininforListModel, total int, err error) {
m := s.Model(ctx)
if param.Ipaddr != "" {
m = m.WhereLike(dao.SysLogininfor.Columns().Ipaddr, "%"+param.Ipaddr+"%")
}
if param.UserName != "" {
m = m.WhereLike(dao.SysLogininfor.Columns().UserName, "%"+param.UserName+"%")
}
if param.Status != "" {
m = m.Where(dao.SysLogininfor.Columns().Status, param.Status)
}
if len(param.LoginTime) == 2 {
startTime := gtime.NewFromStr(param.LoginTime[0])
endTime := gtime.NewFromStr(param.LoginTime[1])
m = m.WhereBetween(dao.SysLogininfor.Columns().LoginTime, startTime, endTime.EndOfDay())
}
total, err = m.Count()
if err != nil {
return nil, 0, err
}
err = m.Page(param.Page, param.PageSize).OrderDesc(dao.SysLogininfor.Columns().InfoId).Scan(&items)
if err != nil {
return nil, 0, err
}
return items, total, nil
}
func (s *sSysLogininfor) AddLogininfor(ctx context.Context, logininfor *model.SysLogininforAddModel) (id int64, err error) {
data := do.SysLogininfor{}
gconv.Struct(logininfor, &data)
result, err := dao.SysLogininfor.Ctx(ctx).Data(data).Save()
if err != nil {
return 0, err
}
id, err = result.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
func (s *sSysLogininfor) Delete(ctx context.Context, param *model.SysLogininforDeleteParam) (output *model.SysLogininforDeleteModel, err error) {
m := s.Model(ctx)
output = &model.SysLogininforDeleteModel{
InfoIds: param.InfoIds,
}
if len(param.InfoIds) == 0 {
return nil, errors.New("请选择要删除的配置")
}
_, err = m.WhereIn(dao.SysLogininfor.Columns().InfoId, param.InfoIds).Delete()
if err != nil {
return
}
return
}
@@ -0,0 +1,361 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"fmt"
"slices"
"sort"
"strings"
v1 "git.5iot.com/iotlcm-public/iotlcm-app/api/system/v1"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysMenu struct {
}
func NewSysMenu() *sSysMenu {
return &sSysMenu{}
}
func init() {
service.RegisterSysMenu(NewSysMenu())
}
func (s *sSysMenu) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: false,
FilterAuth: false,
})
}
return handler.Model(dao.SysMenu.Ctx(ctx), option...)
}
func (s *sSysMenu) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: false,
FilterAuth: false,
})
}
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysMenu.Table()), option...)
}
func (l *sSysMenu) List(ctx context.Context, param *model.SysMenuListParam) (data []*model.SysMenuListModel, total int, err error) {
m := l.ModelQuery(ctx)
if param.MenuIds != nil {
m = m.WhereIn(dao.SysMenu.Columns().MenuId, param.MenuIds)
}
if param.MenuName != "" {
m = m.WhereLike(dao.SysMenu.Columns().MenuName, "%"+param.MenuName+"%")
}
if param.Status != "" {
m = m.Where(dao.SysMenu.Columns().Status, param.Status)
}
total, err = m.Count()
if err != nil {
return nil, 0, err
}
err = m.Order(dao.SysMenu.Columns().OrderNum).Page(0, 5000).Scan(&data)
return
}
// 获取租户菜单列表, 系统租户返回所有菜单,其他租户返回当前租户菜单
func (l *sSysMenu) GetTenantMenu(ctx context.Context, query *model.SysMenuListParam) (data []*model.SysMenuListModel, total int, err error) {
// 获取当前用户信息
tenantId := contexts.GetTenantId(ctx)
m := l.ModelQuery(ctx)
if tenantId != consts.DefaultSystemTenantCode {
// 获取租户信息
tenantInfo, err := service.SysTenant().View(ctx, &model.SysTenantViewParam{
TenantId: tenantId,
})
if err != nil {
return nil, 0, err
}
// 获取租户套餐
tenantPackage, err := service.SysTenantPackage().View(ctx, &model.SysTenantPackageViewParam{
PackageId: tenantInfo.PackageId,
})
if err != nil {
return nil, 0, err
}
if tenantPackage == nil {
g.Log().Errorf(ctx, "sSysMenu.GetTenantMenu not found tenantPackage. TenantId:%s PackageId:%d",
tenantId, tenantInfo.PackageId)
return nil, 0, gerror.NewCode(gcode.CodeInternalError, "系统内部错误")
}
if tenantPackage.MenuIds != "" {
menuIds := strings.Split(tenantPackage.MenuIds, ",")
m = m.WhereIn(dao.SysMenu.Columns().MenuId, menuIds)
} else {
return nil, 0, nil
}
}
if query.MenuName != "" {
m = m.WhereLike(dao.SysMenu.Columns().MenuName, "%"+query.MenuName+"%")
}
if query.Status != "" {
m = m.Where(dao.SysMenu.Columns().Status, query.Status)
}
if query.MenuIds != nil {
m = m.WhereIn(dao.SysMenu.Columns().MenuId, query.MenuIds)
}
total, err = m.Count()
if err != nil {
return nil, 0, err
}
err = m.Order(dao.SysMenu.Columns().OrderNum).Page(0, 5000).Scan(&data)
return
}
// 构建树结构
func (l *sSysMenu) MenuTree(ctx context.Context, parentMenu *model.SysMenuTree, menuList []*entity.SysMenu) (data []*model.SysMenuTree, err error) {
data = make([]*model.SysMenuTree, 0)
for _, menu := range menuList {
pId := int64(0)
if parentMenu != nil {
pId = parentMenu.MenuId
}
if menu.ParentId == pId {
item := &model.SysMenuTree{
SysMenu: *menu,
Children: nil,
}
subMenu, err := l.MenuTree(ctx, item, menuList)
if err != nil {
return nil, err
}
item.Children = subMenu
data = append(data, item)
}
}
return
}
// 获取用户动态路由列表
func (l *sSysMenu) GetUserMenu(ctx context.Context) (data []*entity.SysMenu, err error) {
roles := contexts.GetRoles(ctx)
roleIds := make([]int64, 0)
for _, role := range roles {
roleIds = append(roleIds, role.RoleId)
}
g.Log().Infof(ctx, "sSysMenu.GetUserMenu roleIds: %v", roleIds)
// 判断是否存在超级管理员角色
if slices.Contains(roleIds, consts.SuperAdminRoleId) {
// 获取所有菜单
err = l.ModelQuery(ctx).Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
Order(dao.SysMenu.Columns().OrderNum).Scan(&data)
return
}
// 获取角色菜单列表
menuList, err := service.SysRole().GetRoleListMenu(ctx, roleIds)
if err != nil {
return nil, err
}
g.Log().Infof(ctx, "sSysMenu.GetUserMenu menuList: %v", menuList)
menuIds := make([]int64, 0)
for _, menu := range menuList {
menuIds = append(menuIds, menu.MenuId)
}
g.Log().Infof(ctx, "sSysMenu.GetUserMenu menuIds: %v", menuIds)
// 获取用户角色菜单
err = l.ModelQuery(ctx).WhereIn(dao.SysMenu.Columns().MenuId, menuIds).
Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
Order(dao.SysMenu.Columns().OrderNum).Scan(&data)
g.Log().Infof(ctx, "sSysMenu.GetUserMenu data: %v", data)
return
}
// 构建用户动态路由树
func (l *sSysMenu) BuildUserMenuTree(ctx context.Context, parentMenu *v1.RouteMenu, menuList []*entity.SysMenu, allPath string) (data v1.MenuAllRes, err error) {
data = make(v1.MenuAllRes, 0)
for _, menu := range menuList {
if menu.Status == consts.SysMenuStatusDisable {
continue
}
if menu.MenuType == consts.SysMenuTypeBtn {
continue
}
pId := int64(0)
if parentMenu != nil {
pId = parentMenu.Id
}
if menu.ParentId == pId {
openInNewWindow := false
var link *string
if menu.IsFrame == consts.SysMenuIsFrameYes {
openInNewWindow = true
link = &menu.Path
}
query := make(map[string]string)
if menu.QueryParam != "" {
query2 := strings.Split(menu.QueryParam, "&")
for _, q := range query2 {
kv := strings.Split(q, "=")
if len(kv) == 2 {
query[kv[0]] = kv[1]
}
}
}
hideInMenu := false
if menu.Visible == consts.SysMenuVisibleHide {
hideInMenu = true
}
keepAlive := true
item := &v1.RouteMenu{
Id: menu.MenuId,
ParentId: menu.ParentId,
Path: menu.Path,
Redirect: nil,
Name: fmt.Sprintf("menu_%d", menu.MenuId),
Component: menu.Component,
Meta: &v1.RouteMeta{
Title: menu.MenuName,
Icon: &menu.Icon,
Order: &menu.OrderNum,
OpenInNewWindow: &openInNewWindow,
Query: query,
HideInMenu: &hideInMenu,
Authority: strings.Split(menu.Perms, ","),
Link: link,
KeepAlive: &keepAlive,
},
}
if hideInMenu {
activePath := allPath + "/"
if strings.Contains(menu.Path, "-") {
activePath = activePath + strings.Split(menu.Path, "-")[0]
}
item.Meta.ActivePath = &activePath
}
subMenu, err := l.BuildUserMenuTree(ctx, item, menuList, allPath+"/"+menu.Path)
if err != nil {
return nil, err
}
item.Children = subMenu
if len(subMenu) == 0 && menu.MenuType == consts.SysMenuTypeDir {
item.Children = []*v1.RouteMenu{}
}
data = append(data, item)
}
}
// 排序
sort.Slice(data, func(i, j int) bool {
return *data[i].Meta.Order < *data[j].Meta.Order
})
return
}
// 获取用户动态路由树
func (l *sSysMenu) GetUserMenuTree(ctx context.Context) (data v1.MenuAllRes, err error) {
// 获取用户动态路由列表
menuList, err := l.GetUserMenu(ctx)
if err != nil {
return nil, err
}
// 构建树结构
data, err = l.BuildUserMenuTree(ctx, nil, menuList, "")
if err != nil {
return nil, err
}
return
}
func (l *sSysMenu) GetSysMenuView(ctx context.Context, menuId int64) (data *model.SysMenuViewModel, err error) {
menu := &entity.SysMenu{}
err = l.ModelQuery(ctx).Where(dao.SysMenu.Columns().MenuId, menuId).Scan(menu)
if err != nil {
return nil, err
}
data = &model.SysMenuViewModel{
SysMenu: *menu,
}
return
}
func (l *sSysMenu) UpdateSysMenu(ctx context.Context, menu *model.SysMenuUpdateModel) (data *model.SysMenuViewModel, err error) {
menu.UpdatedAt = gtime.Now()
menu.UpdatedBy = contexts.GetUserId(ctx)
updateData := do.SysMenu{}
gconv.Struct(menu, &updateData)
updateData.MenuId = nil
_, err = l.Model(ctx).Where(dao.SysMenu.Columns().MenuId, menu.MenuId).Data(updateData).OmitNil().Update()
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysMenuUpdate, menu.MenuId)
data, err = l.GetSysMenuView(ctx, menu.MenuId)
if err != nil {
return nil, err
}
return
}
func (l *sSysMenu) AddSysMenu(ctx context.Context, menu *model.SysMenuAddModel) (data *model.SysMenuViewModel, err error) {
menu.CreatedDept = contexts.GetDeptId(ctx)
menu.CreatedAt = gtime.Now()
menu.CreatedBy = contexts.GetUserId(ctx)
menu.UpdatedAt = gtime.Now()
menu.UpdatedBy = contexts.GetUserId(ctx)
insertData := do.SysMenu{}
gconv.Struct(menu, &insertData)
insertData.MenuId = nil
re, err := l.Model(ctx).Data(insertData).Insert()
if err != nil {
return nil, err
}
menuId, err := re.LastInsertId()
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysMenuCreate, menuId)
data, err = l.GetSysMenuView(ctx, menuId)
if err != nil {
return nil, err
}
return
}
func (l *sSysMenu) DeleteSysMenu(ctx context.Context, menuId int64) (err error) {
_, err = l.Model(ctx).Where(dao.SysMenu.Columns().MenuId, menuId).Delete()
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysMenuDelete, menuId)
return
}
func (s *sSysMenu) GetFastList(ctx context.Context) (res map[int64]*entity.SysMenu, err error) {
var models []*entity.SysMenu
if err = s.ModelQuery(ctx).Scan(&models); err != nil {
return
}
res = make(map[int64]*entity.SysMenu, len(models))
for _, v := range models {
res[v.MenuId] = v
}
return
}
@@ -0,0 +1,348 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"encoding/json"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/container/gset"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysNotice struct {
}
func NewSysNotice() *sSysNotice {
return &sSysNotice{}
}
func init() {
service.RegisterSysNotice(NewSysNotice())
}
func (l *sSysNotice) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.SysNotice.Ctx(ctx), option...)
}
func (s *sSysNotice) List(ctx context.Context, param *model.SysNoticeListParam) (items []*model.SysNoticeListModel, total int, err error) {
db := s.Model(ctx)
if param.NoticeTitle != "" {
db = db.WhereLike(dao.SysNotice.Columns().NoticeTitle, "%"+param.NoticeTitle+"%")
}
if param.NoticeType != "" {
db = db.Where(dao.SysNotice.Columns().NoticeType, param.NoticeType)
}
if param.CreatedBy != "" {
db = db.Where(dao.SysNotice.Columns().CreatedBy, param.CreatedBy)
}
if len(param.CreatedAt) == 2 {
createdAt1 := gtime.NewFromStr(param.CreatedAt[0])
createdAt2 := gtime.NewFromStr(param.CreatedAt[1])
db = db.WhereBetween(dao.SysNotice.Columns().CreatedAt, createdAt1, createdAt2.EndOfDay())
}
db = db.OrderDesc(dao.SysNotice.Columns().NoticeId)
total, err = db.Count()
if err != nil {
return nil, 0, err
}
err = db.Page(param.Page, param.PageSize).WithAll().Scan(&items)
if err != nil {
return nil, 0, err
}
return
}
func (s *sSysNotice) insertNoticeUserList(ctx context.Context, tx gdb.TX, userIds *gset.IntSet, param *entity.SysNotice) (err error) {
sysNotifyUserList := make([]*do.SysNoticeUser, 0, userIds.Size())
for _, userId := range userIds.Slice() {
sysNotifyUserList = append(sysNotifyUserList, &do.SysNoticeUser{
NoticeId: param.NoticeId,
NoticeTitle: param.NoticeTitle,
NoticeType: param.NoticeType,
NoticeContent: param.NoticeContent,
Status: param.Status,
UserId: int64(userId),
IsRead: 0,
Remark: param.Remark,
TenantId: contexts.GetTenantId(ctx),
CreatedDept: contexts.GetDeptId(ctx),
CreatedBy: contexts.GetUserId(ctx),
CreatedAt: gtime.Now(),
UpdatedBy: contexts.GetUserId(ctx),
UpdatedAt: gtime.Now(),
})
}
_, err = dao.SysNoticeUser.Ctx(ctx).TX(tx).Data(sysNotifyUserList).OmitNil().Insert()
if err != nil {
return err
}
return
}
func (s *sSysNotice) Add(ctx context.Context, param *model.SysNoticeAddParam) (err error) {
if param == nil || param.NoticeTitle == "" {
return errors.New("公告标题不能为空")
}
if param.NoticeType == "" {
return errors.New("公告类型不能为空")
}
if param.NoticeContent == "" {
return errors.New("公告内容不能为空")
}
if param.NoticeRange == 0 {
return errors.New("公告范围不能为空")
}
data := do.SysNotice{}
gconv.Struct(param, &data)
if param.NoticeRange == consts.NoticeRangeDept {
if len(param.DeptIdList) == 0 {
return errors.New("通知组织ID列表不能为空")
}
deptIds, err := json.Marshal(param.DeptIdList)
if err != nil {
return err
}
data.DeptIds = string(deptIds)
}
if param.NoticeRange == consts.NoticeRangeUser {
if len(param.UserIdList) == 0 {
return errors.New("通知用户ID列表不能为空")
}
userIds, err := json.Marshal(param.UserIdList)
if err != nil {
return err
}
data.UserIds = string(userIds)
}
data.TenantId = contexts.GetTenantId(ctx)
data.CreatedDept = contexts.GetDeptId(ctx)
data.CreatedBy = contexts.GetUserId(ctx)
data.CreatedAt = gtime.Now()
data.UpdatedBy = contexts.GetUserId(ctx)
data.UpdatedAt = gtime.Now()
err = dao.SysNotice.Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
id, err := s.Model(ctx).TX(tx).Data(data).OmitEmpty().InsertAndGetId()
if err != nil {
return err
}
if id <= 0 {
return errors.New("新增公告失败")
}
data.NoticeId = id
// getUserIds
userIds := gset.NewIntSet()
if param.NoticeRange == consts.NoticeRangeUser {
userIds.Add(gconv.Ints(param.UserIdList)...)
g.Log().Debugf(ctx, "userIds: %+v", userIds.Slice())
}
if param.NoticeRange == consts.NoticeRangeDept {
userList, _, err := service.SysUser().List(ctx, &request.PageInfo{
Page: 1,
PageSize: 10000,
}, &model.UserListParam{
DeptIdList: param.DeptIdList,
})
if err != nil {
return err
}
for _, user := range userList {
userIds.Add(int(user.UserId))
}
}
if param.NoticeRange == consts.NoticeRangeAll {
userList, _, err := service.SysUser().List(ctx, &request.PageInfo{
Page: 1,
PageSize: 10000,
}, &model.UserListParam{})
if err != nil {
return err
}
for _, user := range userList {
userIds.Add(int(user.UserId))
}
}
if userIds.Size() <= 0 {
return
}
entityData := entity.SysNotice{}
gconv.Struct(data, &entityData)
err = s.insertNoticeUserList(ctx, tx, userIds, &entityData)
if err != nil {
return err
}
return
})
return err
}
func (s *sSysNotice) Edit(ctx context.Context, param *model.SysNoticeEditParam) (err error) {
if param == nil || param.NoticeId == 0 {
return errors.New("公告ID不能为空")
}
if param.NoticeTitle == nil || *param.NoticeTitle == "" {
return errors.New("公告标题不能为空")
}
if param.NoticeType == nil || *param.NoticeType == "" {
return errors.New("公告类型不能为空")
}
if param.NoticeContent == nil || len(*param.NoticeContent) == 0 {
return errors.New("公告内容不能为空")
}
data := do.SysNotice{}
gconv.Struct(param, &data)
if param.NoticeRange == consts.NoticeRangeDept {
if len(param.DeptIdList) == 0 {
return errors.New("通知组织ID列表不能为空")
}
deptIds, err := json.Marshal(param.DeptIdList)
if err != nil {
return err
}
data.DeptIds = string(deptIds)
}
if param.NoticeRange == consts.NoticeRangeUser {
if len(param.UserIdList) == 0 {
return errors.New("通知用户ID列表不能为空")
}
userIds, err := json.Marshal(param.UserIdList)
if err != nil {
return err
}
data.UserIds = string(userIds)
}
data.TenantId = contexts.GetTenantId(ctx)
data.NoticeContent = []byte(*param.NoticeContent)
data.UpdatedBy = contexts.GetUserId(ctx)
data.UpdatedAt = gtime.Now()
err = dao.SysNotice.Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
m := s.Model(ctx).TX(tx)
m = m.Where(dao.SysNotice.Columns().NoticeId, param.NoticeId)
g.Log().Debugf(ctx, "param: %+v", string(*param.NoticeContent))
g.Log().Debugf(ctx, "data: %+v", string(data.NoticeContent))
_, err = m.Data(data).OmitEmpty().Update()
if err != nil {
return err
}
// 根据 notifyId删除,添加新的列表
userIds := gset.NewIntSet()
if param.NoticeRange == consts.NoticeRangeUser {
userIds.Add(gconv.Ints(param.UserIdList)...)
}
if param.NoticeRange == consts.NoticeRangeDept || param.NoticeRange == consts.NoticeRangeAll {
userList, _, err := service.SysUser().List(ctx, &request.PageInfo{
Page: 1,
PageSize: 10000,
}, &model.UserListParam{
DeptIdList: param.DeptIdList,
})
if err != nil {
return err
}
for _, user := range userList {
userIds.Add(int(user.UserId))
}
}
// 删除
_, err = dao.SysNoticeUser.Ctx(ctx).TX(tx).Where(dao.SysNoticeUser.Columns().NoticeId, param.NoticeId).Delete()
if err != nil {
return err
}
if userIds.Size() <= 0 {
return
}
entityData := entity.SysNotice{}
gconv.Struct(data, &entityData)
err = s.insertNoticeUserList(ctx, tx, userIds, &entityData)
if err != nil {
return err
}
return
})
return err
}
func (s *sSysNotice) Delete(ctx context.Context, param *model.SysNoticeDeleteParam) (err error) {
err = dao.SysNotice.Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
_, err = s.Model(ctx).TX(tx).WhereIn(dao.SysNotice.Columns().NoticeId, param.NoticeIds).Delete()
if err != nil {
return err
}
// 删除SysNoticeUser
_, err = dao.SysNoticeUser.Ctx(ctx).TX(tx).WhereIn(dao.SysNoticeUser.Columns().NoticeId, param.NoticeIds).Delete()
if err != nil {
return err
}
return
})
return err
}
func (s *sSysNotice) View(ctx context.Context, param *model.SysNoticeViewParam) (data *model.SysNoticeViewModel, err error) {
if param == nil || param.NoticeId == 0 {
return nil, errors.New("公告ID不能为空")
}
data = &model.SysNoticeViewModel{}
err = s.Model(ctx).Where(dao.SysNotice.Columns().NoticeId, param.NoticeId).Scan(&data)
g.Log().Debugf(ctx, "data: %+v", string(data.NoticeContent))
if err != nil {
return nil, err
}
if data.UserIds != "" {
json.Unmarshal([]byte(data.UserIds), &data.UserIdList)
}
if data.DeptIds != "" {
json.Unmarshal([]byte(data.DeptIds), &data.DeptIdList)
}
return
}
@@ -0,0 +1,204 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package system
import (
"context"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/hook"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility/convert"
"git.5iot.com/iotlcm-public/iotlcm-app/utility/excel"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysNoticeUser struct{}
func NewSysNoticeUser() *sSysNoticeUser {
return &sSysNoticeUser{}
}
func init() {
service.RegisterSysNoticeUser(NewSysNoticeUser())
}
// Model 用户通知公告表ORM模型
func (s *sSysNoticeUser) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.SysNoticeUser.Ctx(ctx), option...)
}
// List 获取用户通知公告表列表
func (s *sSysNoticeUser) List(ctx context.Context, in *model.SysNoticeUserListParam) (list []*model.SysNoticeUserListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 根据当前用户查询
mod = mod.Where(dao.SysNoticeUser.Columns().UserId, contexts.GetUserId(ctx))
mod = mod.Where(dao.SysNoticeUser.Columns().Status, consts.SysStatusNormal)
// 字段过滤
mod = mod.Fields(model.SysNoticeUserListModel{})
if in.IsRead != "" {
mod = mod.Where(dao.SysNoticeUser.Columns().IsRead, in.IsRead)
}
if in.NoticeId > 0 {
mod = mod.Where(dao.SysNoticeUser.Columns().NoticeId, in.NoticeId)
}
if in.NoticeTitle != "" {
mod = mod.WhereLike(dao.SysNoticeUser.Columns().NoticeTitle, "%"+in.NoticeTitle+"%")
}
if in.NoticeType != "" {
mod = mod.Where(dao.SysNoticeUser.Columns().NoticeType, in.NoticeType)
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderDesc(dao.SysNoticeUser.Columns().Id)
// 操作人摘要信息
mod = mod.Hook(hook.MemberSummary)
// 查询数据
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
err = gerror.Wrap(err, "获取用户通知公告表列表失败,请稍后重试!")
return
}
return
}
// Export 导出用户通知公告表
func (s *sSysNoticeUser) Export(ctx context.Context, in *model.SysNoticeUserListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.SysNoticeUserExportModel{})
if err != nil {
return
}
var (
fileName = "导出用户通知公告表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", totalCount, (totalCount+in.PageSize-1)/in.PageSize, in.Page, len(list))
exports []model.SysNoticeUserExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增用户通知公告表
func (s *sSysNoticeUser) Edit(ctx context.Context, in *model.SysNoticeUserEditParam) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
in.UpdatedBy = contexts.GetUserId(ctx)
in.UpdatedAt = gtime.Now()
data := do.SysNoticeUser{}
gconv.Struct(in, &data)
data.Id = nil
data.NoticeContent = []byte(in.NoticeContent)
if _, err = s.Model(ctx).TX(tx).
Fields(model.SysNoticeUserUpdateFields{}).
WherePri(in.Id).Data(data).Update(); err != nil {
err = gerror.Wrap(err, "修改用户通知公告表失败,请稍后重试!")
}
return
}
// 新增
in.TenantId = contexts.GetTenantId(ctx)
in.CreatedDept = contexts.GetDeptId(ctx)
in.CreatedBy = contexts.GetUserId(ctx)
in.CreatedAt = gtime.Now()
data := do.SysNoticeUser{}
gconv.Struct(in, &data)
data.Id = nil
data.NoticeContent = []byte(in.NoticeContent)
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).TX(tx).
Fields(model.SysNoticeUserInsertFields{}).
Data(data).Insert(); err != nil {
err = gerror.Wrap(err, "新增用户通知公告表失败,请稍后重试!")
}
return
})
}
// Delete 删除用户通知公告表
func (s *sSysNoticeUser) Delete(ctx context.Context, in *model.SysNoticeUserDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除用户通知公告表失败,请稍后重试!")
return
}
return
}
// View 获取用户通知公告表指定信息
func (s *sSysNoticeUser) View(ctx context.Context, in *model.SysNoticeUserViewParam) (res *model.SysNoticeUserViewModel, err error) {
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取用户通知公告表信息,请稍后重试!")
return
}
return
}
// Status 更新用户通知公告表状态
func (s *sSysNoticeUser) Status(ctx context.Context, in *model.SysNoticeUserStatusParam) (err error) {
data := do.SysNoticeUser{
Status: in.Status,
UpdatedBy: contexts.GetUserId(ctx),
}
if _, err = s.Model(ctx).WherePri(in.Id).Data(data).Update(); err != nil {
err = gerror.Wrap(err, "更新用户通知公告表状态失败,请稍后重试!")
return
}
return
}
// Read 已读
func (s *sSysNoticeUser) Read(ctx context.Context, in *model.SysNoticeUserReadParam) (err error) {
data := do.SysNoticeUser{
IsRead: 1,
UpdatedBy: contexts.GetUserId(ctx),
UpdatedAt: gtime.Now(),
}
if _, err = s.Model(ctx).WherePri(in.Ids).Data(data).Update(); err != nil {
err = gerror.Wrap(err, "更新用户通知公告表状态失败,请稍后重试!")
return
}
return
}
@@ -0,0 +1,163 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysOperLog struct {
}
func NewSysOperLog() *sSysOperLog {
return &sSysOperLog{}
}
func init() {
service.RegisterSysOperLog(NewSysOperLog())
}
func (s *sSysOperLog) GetOperLogList(ctx context.Context, query *model.SysOperLogListParam, page *request.PageInfo) (items []*model.SysOperLogListModel, total int, err error) {
// 获取当前用户租户编码
tenantId := contexts.GetTenantId(ctx)
db := dao.SysOperLog.Ctx(ctx).Where(dao.SysOperLog.Columns().TenantId, tenantId)
if query.Title != "" {
db = db.WhereLike(dao.SysOperLog.Columns().Title, "%"+query.Title+"%")
}
if query.BusinessType != "" {
db = db.Where(dao.SysOperLog.Columns().BusinessType, query.BusinessType)
}
if query.Method != "" {
db = db.WhereLike(dao.SysOperLog.Columns().Method, "%"+query.Method+"%")
}
total, err = db.Count()
if err != nil {
return nil, 0, err
}
err = db.Page(page.Page, page.PageSize).OrderDesc(dao.SysOperLog.Columns().OperId).Scan(&items)
if err != nil {
return nil, 0, err
}
return items, total, nil
}
func (s *sSysOperLog) AnalysisLog(ctx context.Context) (data *model.SysOperLogAddParam, err error) {
ctxData := contexts.Get(ctx)
request := ghttp.RequestFromCtx(ctx)
handlerResponse := request.GetHandlerResponse() // 响应结果
param := request.GetMap() // 请求参数
path := request.URL.Path
res := gconv.Map(handlerResponse)
deptName := ""
operName := ""
businessType := 0
operatorType := 0
status := 0
costTime := gtime.Now().Sub(gtime.New(request.EnterTime)).Milliseconds()
message := ""
if ctxData != nil && ctxData.User != nil {
deptName, _ = mcache.GetDeptName(ctx, ctxData.User.DeptId)
operName = ctxData.User.Username
}
jsonResult := ""
var code gcode.Code = gcode.CodeOK
if erro := request.GetError(); erro != nil {
status = 1
code = gerror.Code(erro)
message = gjson.New(g.Map{
"code": code,
"message": erro.Error(),
}).String()
} else {
status = 0
if b, err := gjson.Encode(res); err == nil {
if len(b) > 1024 {
jsonResult = "数据过大,未记录"
} else {
jsonResult = string(b)
}
}
}
if strings.Contains(path, "/add") {
businessType = 1
}
if strings.Contains(path, "/edit") {
businessType = 2
}
if strings.Contains(path, "/delete") {
businessType = 3
}
url := request.URL.String()
if len(url) > 100 {
url = url[:100] + "..."
}
operParam := gconv.String(param)
if len(operParam) > 1024 {
operParam = "数据过大,未记录"
}
data = &model.SysOperLogAddParam{
TenantId: contexts.GetTenantId(ctx),
Title: "",
BusinessType: businessType,
Method: request.URL.Path,
RequestMethod: request.Method,
OperatorType: operatorType,
OperName: operName,
DeptName: deptName,
OperUrl: url,
OperIp: utility.GetClientIp(ctx),
OperLocation: utility.GetCityByIp(utility.GetClientIp(ctx)),
OperParam: operParam,
JsonResult: jsonResult,
Status: status,
ErrorMsg: message,
OperTime: gtime.Now(),
CostTime: costTime,
}
return data, nil
}
func (s *sSysOperLog) ClearOperationLogByDays(ctx context.Context, days int) error {
_, err := dao.SysOperLog.Ctx(ctx).Delete("to_days(now())-to_days(`oper_time`) > ?", days+1)
return err
}
func (s *sSysOperLog) RealWrite(ctx context.Context, data model.SysOperLogAddParam) (err error) {
writeData := do.SysOperLog{}
if err = gconv.Struct(data, &writeData); err != nil {
return err
}
_, err = dao.SysOperLog.Ctx(ctx).Data(writeData).Save()
if err != nil {
g.Log().Error(ctx, "sSysOperLog.RealWrite error:%v,data:%+v", err, data)
}
return
}
+801
View File
@@ -0,0 +1,801 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"fmt"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility/hash"
"github.com/gogf/gf/v2/crypto/gmd5"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysOss struct {
}
func NewSysOss() *sSysOss {
return &sSysOss{}
}
func sysOssDataFromUpload(ctx context.Context, result model.UploadResponse) do.SysOss {
now := gtime.Now()
return do.SysOss{
TenantId: contexts.GetTenantId(ctx),
FileName: result.Name,
OriginalName: result.OriginalName,
FileSuffix: result.Ext,
Path: result.Path,
Url: result.FullPath,
CreatedDept: contexts.GetDeptId(ctx),
CreatedAt: now,
CreatedBy: contexts.GetUserId(ctx),
UpdatedAt: now,
UpdatedBy: contexts.GetUserId(ctx),
Service: "local",
Md5: result.Md5,
FileSize: result.Size,
FileCrc16: result.Crc16,
FileSum: result.Sum16,
}
}
func init() {
service.RegisterSysOss(NewSysOss())
}
func (s *sSysOss) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.SysOss.Ctx(ctx), option...)
}
func (s *sSysOss) List(ctx context.Context, param *model.SysOssListParam, pageInfo *request.PageInfo) (items []*model.SysOssListModel, total int, err error) {
db := s.Model(ctx)
if param.FileName != "" {
db = db.WhereLike(dao.SysOss.Columns().FileName, "%"+param.FileName+"%")
}
if param.OriginalName != "" {
db = db.WhereLike(dao.SysOss.Columns().OriginalName, "%"+param.OriginalName+"%")
}
if param.FileSuffix != "" {
db = db.Where(dao.SysOss.Columns().FileSuffix, param.FileSuffix)
}
if param.Service != "" {
db = db.Where(dao.SysOss.Columns().Service, param.Service)
}
if len(param.CreatedAt) == 2 {
startTime, err := gtime.StrToTime(param.CreatedAt[0])
if err != nil {
return nil, 0, err
}
endTime, err := gtime.StrToTime(param.CreatedAt[1])
if err != nil {
return nil, 0, err
}
db = db.WhereBetween(dao.SysOss.Columns().CreatedAt, startTime, endTime.EndOfDay())
}
total, err = db.Count()
if err != nil {
return nil, 0, err
}
err = db.Page(pageInfo.Page, pageInfo.PageSize).Order(dao.SysOss.Columns().OssId, "DESC").Scan(&items)
if err != nil {
return nil, 0, err
}
return
}
func (s *sSysOss) View(ctx context.Context, param *model.SysOssViewParam) (output *model.SysOssViewModel, err error) {
if param.OssId == 0 {
return nil, errors.New("未选择文件")
}
db := s.Model(ctx).Where(dao.SysOss.Columns().OssId, param.OssId)
err = db.Scan(&output)
if err != nil {
return nil, err
}
return
}
func (s *sSysOss) Download(ctx context.Context, param *model.SysOssDownloadParam) (output *model.SysOssDownloadModel, err error) {
if param.OssId == 0 {
return nil, errors.New("未选择文件")
}
data := &entity.SysOss{}
db := s.Model(ctx).Where(dao.SysOss.Columns().OssId, param.OssId)
err = db.Scan(&data)
if err != nil {
return nil, err
}
if data.Service != "local" {
err = gerror.New("非本地文件")
return
}
output = &model.SysOssDownloadModel{
SysOss: data,
}
err = s.DownloadLocal(ctx, data)
if err != nil {
return nil, err
}
return
}
func (s *sSysOss) Delete(ctx context.Context, param *model.SysOssDeleteParam) (output *model.SysOssDeleteModel, err error) {
if len(param.OssIds) == 0 {
return nil, errors.New("未选择文件")
}
items := make([]*entity.SysOss, 0)
err = s.Model(ctx).WhereIn(dao.SysOss.Columns().OssId, param.OssIds).Scan(&items)
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, errors.New("文件不存在")
}
_, err = s.Model(ctx).WhereIn(dao.SysOss.Columns().OssId, param.OssIds).Delete()
if err != nil {
return nil, err
}
for _, item := range items {
if item.Service == "local" {
s.DeleteLocal(ctx, item)
}
}
return
}
func (s *sSysOss) Upload(ctx context.Context, param *model.SysOssUploadParam) (output *model.SysOssUploadModel, err error) {
r := g.RequestFromCtx(ctx)
file := r.GetUploadFile("file")
if file == nil {
err = gerror.New("上传文件必须")
return
}
// 检查文件类型
err = s.CheckType(ctx, param.FileType, file)
if err != nil {
return
}
// 检查文件大小
err = s.CheckSize(ctx, param.FileType, file)
if err != nil {
return
}
// TODO:待实现其他云存储
if param.NewFileType == 0 {
if param.IsDevice {
param.NewFileType = consts.SysUploadNewFileTypeDeviceFile
} else if param.FileType == consts.SysUploadFileTypeImg {
param.NewFileType = consts.SysUploadNewFileTypeUserImg
} else {
param.NewFileType = consts.SysUploadNewFileTypeUserFile
}
}
result, err := s.UploadLocal(ctx, file, param.NewFileType, param.SaveOriginalName, param.SubDirName, param.NotAddDate)
if err != nil {
return nil, err
}
id, err := s.Model(ctx).Data(sysOssDataFromUpload(ctx, result)).InsertAndGetId()
if err != nil {
return nil, err
}
output = &model.SysOssUploadModel{}
err = s.Model(ctx).Where(dao.SysOss.Columns().OssId, id).Scan(&output)
if err != nil {
return nil, err
}
return
}
func (s *sSysOss) MoveFile(ctx context.Context, param *model.SysOssMoveFileParam) (output *model.SysOssMoveFileModel, err error) {
result, err := s.MoveFileLocal(ctx, param.FilePath, param.NewFileType, param.SaveOriginalName, param.SubDirName, param.NotAddDate)
if err != nil {
return nil, err
}
id, err := s.Model(ctx).Data(sysOssDataFromUpload(ctx, result)).InsertAndGetId()
if err != nil {
return nil, err
}
oss := &entity.SysOss{}
err = s.Model(ctx).Where(dao.SysOss.Columns().OssId, id).Scan(&oss)
if err != nil {
return nil, err
}
output = &model.SysOssMoveFileModel{
SysOss: oss,
}
return output, nil
}
func (s *sSysOss) SaveContent(ctx context.Context, param *model.SysOssSaveContentParam) (output *model.SysOssSaveContentModel, err error) {
result, err := s.SaveContentLocal(ctx, param.Content, param.NewFileType, param.FileName, param.SubDirName, param.NotAddDate)
if err != nil {
return nil, err
}
id, err := s.Model(ctx).Data(sysOssDataFromUpload(ctx, result)).InsertAndGetId()
if err != nil {
return nil, err
}
oss := &entity.SysOss{}
err = s.Model(ctx).Where(dao.SysOss.Columns().OssId, id).Scan(&oss)
if err != nil {
return nil, err
}
output = &model.SysOssSaveContentModel{
SysOss: oss,
}
return output, nil
}
func (s *sSysOss) getNewFileTypePath(ctx context.Context, newFileType int) string {
switch newFileType {
case consts.SysUploadNewFileTypeUserFile:
return consts.SysUploadNewFileTypePathUserFile
case consts.SysUploadNewFileTypeUserImg:
return consts.SysUploadNewFileTypePathUserImg
case consts.SysUploadNewFileTypeDeviceFile:
return consts.SysUploadNewFileTypePathDeviceFile
default:
key := fmt.Sprintf(consts.ConfigOssNewFileTypePath, newFileType)
config, err := service.SysConfig().GetConfigByKey(ctx, key)
if err == nil && config != nil {
return config.ConfigValue
}
}
return "other"
}
func (s *sSysOss) GetSaveFilePathConfig(ctx context.Context, newFileType int, notAddDate int) (string, error) {
uploadPath := g.Cfg().MustGet(ctx, "system.upload.local.path").String()
if uploadPath == "" {
return "", errors.New("上传路径未配置")
}
tenantId := contexts.GetTenantId(ctx)
if tenantId == "" {
tenantId = "default"
}
fileTypePath := s.getNewFileTypePath(ctx, newFileType)
fullDirPath := uploadPath + "/" + tenantId + "/" + fileTypePath
if notAddDate == 0 {
nowData := time.Now().Format("20060102")
fullDirPath = fullDirPath + "/" + nowData
}
return fullDirPath, nil
}
func (s *sSysOss) MoveFileLocal(ctx context.Context, filePath string, newFileType int, useOriginalName int, subDirName string, isAddDate int) (result model.UploadResponse, err error) {
if filePath == "" {
err = errors.New("文件路径必须")
return
}
fileName := gfile.Basename(filePath)
fullDirPath, err := s.GetSaveFilePathConfig(ctx, newFileType, isAddDate)
if err != nil {
return
}
if subDirName != "" {
fullDirPath = fullDirPath + "/" + subDirName
}
if !gfile.Exists(fullDirPath) {
err = gfile.Mkdir(fullDirPath)
if err != nil {
return
}
}
err = gfile.Move(filePath, fullDirPath)
if err != nil {
g.Log().Errorf(ctx, "sSysOss MoveFileLocal move file error: %+v, filePath: %s, fullDirPath: %s, newFileType: %d, useOriginalName: %d, subDirName: %s", err, filePath, fullDirPath, newFileType, useOriginalName, subDirName)
return
}
fullFilePath := fullDirPath + "/" + fileName
url := s.buildLocalFileURL(ctx, fullFilePath)
md5, _ := gmd5.EncryptFile(fullFilePath)
crc16 := hash.Crc16ChecksumFile(fullFilePath)
sum16 := hash.Sum16ChecksumFile(fullFilePath)
result = model.UploadResponse{
Size: gfile.Size(fullFilePath),
Path: fullFilePath,
FullPath: url,
OriginalName: fileName,
Name: fileName,
Type: gfile.ExtName(fileName),
Ext: gfile.ExtName(fileName),
Md5: md5,
Crc16: crc16,
Sum16: sum16,
}
return result, nil
}
// 保存内容
func (s *sSysOss) SaveContentLocal(ctx context.Context, content []byte, newFileType int, fileName string, subDirName string, isAddDate int) (result model.UploadResponse, err error) {
fullDirPath, err := s.GetSaveFilePathConfig(ctx, newFileType, isAddDate)
if err != nil {
return
}
if subDirName != "" {
fullDirPath = fullDirPath + "/" + subDirName
}
if !gfile.Exists(fullDirPath) {
err = gfile.Mkdir(fullDirPath)
if err != nil {
return
}
}
fullFilePath := fullDirPath + "/" + fileName
err = gfile.PutBytes(fullFilePath, content)
if err != nil {
return
}
url := s.buildLocalFileURL(ctx, fullFilePath)
md5, _ := gmd5.EncryptFile(fullFilePath)
crc16 := hash.Crc16ChecksumFile(fullFilePath)
sum16 := hash.Sum16ChecksumFile(fullFilePath)
result = model.UploadResponse{
Size: gfile.Size(fullFilePath),
Path: fullFilePath,
FullPath: url,
OriginalName: fileName,
Name: fileName,
Type: "application/json",
Ext: gfile.ExtName(fileName),
Md5: md5,
Crc16: crc16,
Sum16: sum16,
}
return result, nil
}
func (s *sSysOss) UploadLocal(ctx context.Context, file *ghttp.UploadFile, newFileType int, useOriginalName int, subDirName string, notAddDate int) (result model.UploadResponse, err error) {
if file == nil {
err = errors.New("文件必须")
return
}
fullDirPath, err := s.GetSaveFilePathConfig(ctx, newFileType, notAddDate)
if err != nil {
return
}
if subDirName != "" {
fullDirPath = fullDirPath + "/" + subDirName
}
if !gfile.Exists(fullDirPath) {
err = gfile.Mkdir(fullDirPath)
if err != nil {
return
}
}
fileName := gfile.Basename(file.Filename)
ext := gfile.Ext(file.Filename)
fileNewName, err := file.Save(fullDirPath, useOriginalName == 0)
if err != nil {
return
}
fullFilePath := fullDirPath + "/" + fileNewName
url := s.buildLocalFileURL(ctx, fullFilePath)
md5, _ := gmd5.EncryptFile(fullFilePath)
crc16 := hash.Crc16ChecksumFile(fullFilePath)
sum16 := hash.Sum16ChecksumFile(fullFilePath)
result = model.UploadResponse{
Size: file.Size,
Path: fullFilePath,
FullPath: url,
OriginalName: fileName,
Name: fileNewName,
Type: file.Header.Get("Content-type"),
Ext: ext,
Md5: md5,
Crc16: crc16,
Sum16: sum16,
}
return
}
func (s *sSysOss) buildLocalFileURL(ctx context.Context, fullFilePath string) string {
fullFilePath = strings.TrimSpace(fullFilePath)
if fullFilePath == "" {
return ""
}
uploadPath := strings.TrimSpace(g.Cfg().MustGet(ctx, "system.upload.local.path").String())
publicURL := strings.TrimSpace(os.Getenv("IOTLCM_UPLOAD_PUBLIC_URL"))
if publicURL == "" {
publicURL = strings.TrimSpace(g.Cfg().MustGet(ctx, "system.upload.local.publicUrl", "").String())
}
if publicURL != "" {
if fileURL, ok := localFileURLByBase(fullFilePath, uploadPath, publicURL); ok {
return fileURL
}
}
configValue := ""
configType, err := service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssUrlPath)
if err == nil && configType != nil {
configValue = strings.TrimSpace(configType.ConfigValue)
}
if url, ok := localFileURLByMapping(fullFilePath, configValue); ok {
return url
}
baseURL := localFileURLFallbackBase(configValue)
if baseURL != "" {
if url, ok := localFileURLByBase(fullFilePath, uploadPath, baseURL); ok {
return url
}
}
if url, ok := localFileURLByBase(fullFilePath, uploadPath, "/upload"); ok {
return url
}
if configValue != "" && !strings.Contains(configValue, "|") {
return strings.TrimRight(configValue, "/") + "/" + strings.TrimLeft(filepath.ToSlash(fullFilePath), "/")
}
return fullFilePath
}
func localFileURLFallbackBase(configValue string) string {
pairs := localFileURLMappingPairs(configValue)
for _, pair := range pairs {
if pair[1] != "" {
return pair[1]
}
}
configValue = strings.TrimSpace(configValue)
if configValue == "" || strings.Contains(configValue, "|") {
return ""
}
return configValue
}
func localFileURLByMapping(fullFilePath string, configValue string) (string, bool) {
for _, pair := range localFileURLMappingPairs(configValue) {
if suffix, ok := localPathSuffix(fullFilePath, pair[0]); ok {
return joinURLPath(pair[1], suffix), true
}
}
return "", false
}
func localFileURLByBase(fullFilePath string, uploadPath string, baseURL string) (string, bool) {
if strings.TrimSpace(uploadPath) == "" || strings.TrimSpace(baseURL) == "" {
return "", false
}
suffix, ok := localPathSuffix(fullFilePath, uploadPath)
if !ok {
return "", false
}
return joinURLPath(baseURL, suffix), true
}
func localFileURLMappingPairs(configValue string) [][2]string {
configValue = strings.TrimSpace(configValue)
if configValue == "" {
return nil
}
items := strings.FieldsFunc(configValue, func(r rune) bool {
return r == ';' || r == '\n' || r == '\r'
})
pairs := make([][2]string, 0, len(items))
for _, item := range items {
parts := strings.SplitN(strings.TrimSpace(item), "|", 2)
if len(parts) != 2 {
continue
}
source := strings.TrimSpace(parts[0])
target := strings.TrimSpace(parts[1])
if source == "" || target == "" {
continue
}
pairs = append(pairs, [2]string{source, target})
}
return pairs
}
func localPathSuffix(fullFilePath string, prefix string) (string, bool) {
full := normalizeLocalPath(fullFilePath)
base := normalizeLocalPath(prefix)
if full == "" || base == "" {
return "", false
}
if strings.EqualFold(full, base) {
return "", true
}
prefixWithSlash := strings.TrimRight(base, "/") + "/"
if len(full) > len(prefixWithSlash) && strings.EqualFold(full[:len(prefixWithSlash)], prefixWithSlash) {
return full[len(prefixWithSlash):], true
}
return "", false
}
func normalizeLocalPath(path string) string {
path = strings.TrimSpace(path)
if path == "" {
return ""
}
path = filepath.ToSlash(filepath.Clean(path))
return strings.TrimRight(path, "/")
}
func joinURLPath(baseURL string, suffix string) string {
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
suffix = strings.TrimLeft(filepath.ToSlash(strings.TrimSpace(suffix)), "/")
if suffix == "" {
if baseURL == "" {
return "/"
}
return baseURL
}
return baseURL + "/" + suffix
}
func (s *sSysOss) DownloadLocal(ctx context.Context, file *entity.SysOss) (err error) {
if file == nil {
err = errors.New("文件必须")
return
}
finalPath, err := s.resolveLocalOssPath(ctx, file.Path)
if err != nil {
return err
}
r := ghttp.RequestFromCtx(ctx)
if r == nil {
err = gerror.New("ctx not http request")
return
}
reader, err := os.Open(finalPath)
if err != nil {
return gerror.Wrap(err, "打开下载文件失败")
}
defer reader.Close()
fileInfo, err := reader.Stat()
if err != nil {
return gerror.Wrap(err, "读取下载文件信息失败")
}
fileName := strings.TrimSpace(file.FileName)
if fileName == "" {
fileName = filepath.Base(finalPath)
}
contentType := appOssDownloadContentType(finalPath, file.FileSuffix)
r.Response.Header().Set("Content-Type", contentType)
r.Response.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
r.Response.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, strings.ReplaceAll(fileName, `"`, "")))
r.Response.Status = http.StatusOK
http.ServeContent(r.Response.Writer, r.Request, fileName, fileInfo.ModTime(), reader)
r.Response.Flush()
return
}
func appOssDownloadContentType(filePath string, suffix string) string {
ext := strings.TrimSpace(suffix)
if ext == "" {
ext = filepath.Ext(filePath)
}
ext = strings.TrimPrefix(ext, ".")
if ext == "" {
return "application/octet-stream"
}
if contentType := mime.TypeByExtension("." + ext); contentType != "" {
return contentType
}
return "application/octet-stream"
}
func (s *sSysOss) DeleteLocal(ctx context.Context, file *entity.SysOss) (err error) {
if file == nil {
err = errors.New("文件不存在")
return
}
filePath, err := s.resolveLocalOssPath(ctx, file.Path)
if err != nil {
return
}
err = gfile.Remove(filePath)
if err != nil {
return
}
return
}
func (s *sSysOss) resolveLocalOssPath(ctx context.Context, filePath string) (string, error) {
filePath = strings.TrimSpace(filePath)
if filePath == "" {
return "", errors.New("文件路径为空")
}
candidates := make([]string, 0, 4)
addCandidate := func(path string) {
path = strings.TrimSpace(path)
if path == "" {
return
}
cleaned := filepath.Clean(path)
for _, item := range candidates {
if item == cleaned {
return
}
}
candidates = append(candidates, cleaned)
}
addCandidate(filePath)
if !filepath.IsAbs(filePath) {
if absPath, err := filepath.Abs(filePath); err == nil {
addCandidate(absPath)
}
if workDir, err := os.Getwd(); err == nil {
addCandidate(filepath.Join(workDir, "server", filePath))
}
if uploadPath := strings.TrimSpace(g.Cfg().MustGet(ctx, "system.upload.local.path").String()); uploadPath != "" && !filepath.IsAbs(uploadPath) {
if absUploadPath, err := filepath.Abs(uploadPath); err == nil {
normalizedUploadPath := filepath.ToSlash(filepath.Clean(uploadPath))
normalizedFilePath := filepath.ToSlash(filepath.Clean(filePath))
if strings.HasPrefix(normalizedFilePath, normalizedUploadPath+"/") {
suffix := strings.TrimPrefix(normalizedFilePath, normalizedUploadPath+"/")
addCandidate(filepath.Join(absUploadPath, filepath.FromSlash(suffix)))
}
}
}
}
for _, candidate := range candidates {
if gfile.Exists(candidate) {
return candidate, nil
}
}
return "", gerror.Newf("文件不存在:%s", filePath)
}
func (s *sSysOss) CheckType(ctx context.Context, checkFileType string, file *ghttp.UploadFile) (err error) {
var (
configType *entity.SysConfig
)
if checkFileType == consts.SysUploadFileTypeFile {
//获取上传类型配置
configType, err = service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssFileTypeKey)
if err != nil {
return
}
} else if checkFileType == consts.SysUploadFileTypeImg {
//获取上传类型配置
configType, err = service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssImgTypeKey)
if err != nil {
return
}
} else {
return fmt.Errorf("文件检查类型错误:%s|%s, 文件类型:%s", consts.SysUploadFileTypeFile, consts.SysUploadFileTypeImg, checkFileType)
}
ext := gfile.ExtName(file.Filename)
ext = strings.ToLower(ext)
imageType := gstr.Split(configType.ConfigValue, ",")
rightType := false
for _, v := range imageType {
if v == ext {
rightType = true
break
}
}
if !rightType {
err = gerror.New("上传文件类型错误,只能包含后缀为:" + configType.ConfigValue + "的文件。")
return
}
return
}
func (s *sSysOss) CheckSize(ctx context.Context, checkFileType string, file *ghttp.UploadFile) (err error) {
var (
configType *entity.SysConfig
)
if checkFileType == consts.SysUploadFileTypeFile {
//获取上传类型配置
configType, err = service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssFileSizeKey)
if err != nil {
return
}
} else if checkFileType == consts.SysUploadFileTypeImg {
//获取上传类型配置
configType, err = service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssImgSizeKey)
if err != nil {
return
}
} else {
return fmt.Errorf("文件检查类型错误:%s|%s", consts.SysUploadFileTypeFile, consts.SysUploadFileTypeImg)
}
match, err := gregex.MatchString(`^([0-9]+)(?i:([a-z]*))$`, configType.ConfigValue)
if err != nil {
return
}
if len(match) == 0 {
err = gerror.New("上传文件大小未设置,请在后台配置,格式为(30M,30k,30MB)")
return
}
var cfSize int64
switch gstr.ToUpper(match[2]) {
case "MB", "M":
cfSize = gconv.Int64(match[1]) * 1024 * 1024
case "KB", "K":
cfSize = gconv.Int64(match[1]) * 1024
case "":
cfSize = gconv.Int64(match[1])
}
if cfSize == 0 {
err = gerror.New("上传文件大小未设置,请在后台配置,格式为(30M,30k,30MB),最大单位为MB")
return
}
if file.Size > cfSize {
err = gerror.New("上传文件大小超过限制,最大为" + configType.ConfigValue)
return
}
return
}
func (s *sSysOss) GetAllUrl(ctx context.Context, url string) (allUrl string, ossId int64, fileSize int64, OriginalName string, md5 string, hmac string, err error) {
entity := &entity.SysOss{}
s.Model(ctx).Where(dao.SysOss.Columns().Path, url).Scan(&entity)
if entity.OssId <= 0 {
return
}
ossId = entity.OssId
ossConfig, err := service.SysOssConfig().View(ctx, &model.SysOssConfigViewParam{
ConfigKey: entity.Service,
})
if err != nil {
g.Log().Errorf(ctx, "sSysOss GetAllUrl error: %+v, url: %s", err, url)
return "", 0, 0, "", "", "", err
}
if ossConfig.IsHttps == "Y" {
allUrl = "https://" + ossConfig.Domain + entity.Path
} else {
allUrl = "http://" + ossConfig.Domain + entity.Path
}
fileSize = gfile.Size(entity.Path)
OriginalName = entity.OriginalName
md5, _ = gmd5.Encrypt(entity.Path)
hmac = ""
return
}
@@ -0,0 +1,168 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysOssConfig struct {
}
func NewSysOssConfig() *sSysOssConfig {
return &sSysOssConfig{}
}
func init() {
service.RegisterSysOssConfig(NewSysOssConfig())
}
func (s *sSysOssConfig) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.SysOssConfig.Ctx(ctx), option...)
}
func (s *sSysOssConfig) List(ctx context.Context, param *model.SysOssConfigListParam) (items []*model.SysOssConfigListModel, total int, err error) {
db := s.Model(ctx)
if param.ConfigKey != "" {
db = db.WhereLike(dao.SysOssConfig.Columns().ConfigKey, "%"+param.ConfigKey+"%")
}
if param.BucketName != "" {
db = db.WhereLike(dao.SysOssConfig.Columns().BucketName, "%"+param.BucketName+"%")
}
if param.Status != "" {
db = db.Where(dao.SysOssConfig.Columns().Status, param.Status)
}
total, err = db.Count()
if err != nil {
return nil, 0, err
}
err = db.Page(param.Page, param.PageSize).OrderAsc(dao.SysOssConfig.Columns().OssConfigId).Scan(&items)
if err != nil {
return nil, 0, err
}
return
}
func (s *sSysOssConfig) View(ctx context.Context, param *model.SysOssConfigViewParam) (item *model.SysOssConfigViewModel, err error) {
db := s.Model(ctx)
if param.OssConfigId > 0 {
db = db.Where(dao.SysOssConfig.Columns().OssConfigId, param.OssConfigId)
} else if param.ConfigKey != "" {
db = db.Where(dao.SysOssConfig.Columns().ConfigKey, param.ConfigKey)
} else {
g.Log().Errorf(ctx, "sSysOssConfig View 参数错误: %+v", param)
return nil, gerror.New("参数错误")
}
err = db.Scan(&item)
return
}
func (s *sSysOssConfig) Add(ctx context.Context, param *model.SysOssConfigAddParam) (item *model.SysOssConfigAddModel, err error) {
db := s.Model(ctx)
data := &do.SysOssConfig{}
gconv.Struct(param, data)
data.CreatedAt = gtime.Now()
data.CreatedBy = contexts.GetUserId(ctx)
data.CreatedDept = contexts.GetDeptId(ctx)
data.TenantId = contexts.GetTenantId(ctx)
if len(param.Status) == 0 {
data.Status = "1"
}
id, err := db.Data(data).OmitNil().InsertAndGetId()
if err != nil {
return nil, err
}
item = &model.SysOssConfigAddModel{
OssConfigId: id,
}
return
}
func (s *sSysOssConfig) Edit(ctx context.Context, param *model.SysOssConfigEditParam) (item *model.SysOssConfigEditModel, err error) {
item = &model.SysOssConfigEditModel{
OssConfigId: param.OssConfigId,
}
db := s.Model(ctx)
data := &do.SysOssConfig{}
gconv.Struct(param, data)
data.OssConfigId = nil
data.UpdatedAt = gtime.Now()
data.UpdatedBy = contexts.GetUserId(ctx)
if param.Status == nil || *param.Status == "" {
data.Status = nil
}
_, err = db.Data(data).OmitNil().Where(dao.SysOssConfig.Columns().OssConfigId, param.OssConfigId).Update()
if err != nil {
return item, err
}
return
}
func (s *sSysOssConfig) Delete(ctx context.Context, param *model.SysOssConfigDeleteParam) (item *model.SysOssConfigDeleteModel, err error) {
item = &model.SysOssConfigDeleteModel{
OssConfigIds: param.OssConfigIds,
}
db := s.Model(ctx)
_, err = db.WhereIn(dao.SysOssConfig.Columns().OssConfigId, param.OssConfigIds).Delete()
if err != nil {
return item, err
}
return
}
func (s *sSysOssConfig) GetAllUrlByService(ctx context.Context, path string, tenantId string, ossService string) (allUrl string, err error) {
ossConfig := &entity.SysOssConfig{}
db := s.Model(ctx)
db = db.Where(dao.SysOssConfig.Columns().ConfigKey, ossService)
db = db.Where(dao.SysOssConfig.Columns().TenantId, tenantId)
err = db.Scan(&ossConfig)
if err != nil {
g.Log().Errorf(ctx, "sSysOss GetAllUrlByService error: %+v, url: %s ,service: %s", err, path, ossService)
return "", err
}
if ossConfig == nil {
g.Log().Errorf(ctx, "sSysOss GetAllUrlByService error: ossConfig is empty, url: %s ,service: %s", path, ossService)
err = gerror.New("ossConfig is empty")
return
}
domain := ossConfig.Endpoint
if ossConfig.Domain != "" {
domain = ossConfig.Domain
}
if ossConfig.IsHttps == "Y" {
allUrl = "https://" + domain + path
} else {
allUrl = "http://" + domain + path
}
g.Log().Infof(ctx, "sSysOss GetAllUrlByService success: %s, url: %s, ossConfig: %+v", ossService, allUrl, ossConfig)
return
}
@@ -0,0 +1,137 @@
package system
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
)
func TestLocalFileURLUsesConfiguredPublicURL(t *testing.T) {
t.Setenv("IOTLCM_UPLOAD_PUBLIC_URL", "")
previous := g.Cfg().GetAdapter()
t.Cleanup(func() { g.Cfg().SetAdapter(previous) })
cases := []struct {
name string
root string
base string
file string
want string
}{
{"本地跨端预览", "resource/upload", "http://localhost:8010/upload/", "resource/upload/000000/user/file/photo.jpeg", "http://localhost:8010/upload/000000/user/file/photo.jpeg"},
{"独立部署前缀", "/data/app/upload", "https://files.example.com/app/upload", "/data/app/upload/000000/user/file/photo.jpeg", "https://files.example.com/app/upload/000000/user/file/photo.jpeg"},
{"Windows共享目录", "F:/shared/upload", "https://files.example.com/upload", "F:/shared/upload/000000/user/file/photo.jpeg", "https://files.example.com/upload/000000/user/file/photo.jpeg"},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
adapter, err := gcfg.NewAdapterContent(
"system:\n upload:\n local:\n path: " + testCase.root + "\n publicUrl: " + testCase.base + "\n",
)
if err != nil {
t.Fatal(err)
}
g.Cfg().SetAdapter(adapter)
got := NewSysOss().buildLocalFileURL(context.Background(), testCase.file)
if got != testCase.want {
t.Fatalf("url = %q, want %q", got, testCase.want)
}
})
}
}
func TestLocalFileURLUsesEnvironmentOverride(t *testing.T) {
t.Setenv("IOTLCM_UPLOAD_PUBLIC_URL", "http://localhost:8010/upload")
previous := g.Cfg().GetAdapter()
t.Cleanup(func() { g.Cfg().SetAdapter(previous) })
adapter, err := gcfg.NewAdapterContent("system:\n upload:\n local:\n path: resource/upload\n publicUrl: https://files.example.com/upload\n")
if err != nil {
t.Fatal(err)
}
g.Cfg().SetAdapter(adapter)
got := NewSysOss().buildLocalFileURL(context.Background(), "resource/upload/000000/photo.jpeg")
if got != "http://localhost:8010/upload/000000/photo.jpeg" {
t.Fatalf("environment override did not take precedence: %q", got)
}
}
func TestResolveLocalOssPathFromProjectRoot(t *testing.T) {
tempDir := t.TempDir()
relativePath := filepath.FromSlash("resource/upload/000000/user/file/test.xlsx")
realPath := filepath.Join(tempDir, "server", relativePath)
if err := os.MkdirAll(filepath.Dir(realPath), 0o755); err != nil {
t.Fatalf("mkdir test oss path: %v", err)
}
if err := os.WriteFile(realPath, []byte("xlsx"), 0o644); err != nil {
t.Fatalf("write test oss file: %v", err)
}
oldWd, err := os.Getwd()
if err != nil {
t.Fatalf("get wd: %v", err)
}
if err = os.Chdir(tempDir); err != nil {
t.Fatalf("chdir temp root: %v", err)
}
defer func() {
if err := os.Chdir(oldWd); err != nil {
t.Fatalf("restore wd: %v", err)
}
}()
got, err := NewSysOss().resolveLocalOssPath(context.Background(), relativePath)
if err != nil {
t.Fatalf("resolve local oss path: %v", err)
}
if got != realPath {
t.Fatalf("resolved path = %q, want %q", got, realPath)
}
}
func TestAppOssDownloadContentTypeXlsx(t *testing.T) {
got := appOssDownloadContentType("resource/upload/test.xlsx", "xlsx")
if got != "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" {
t.Fatalf("content type = %q", got)
}
}
func TestLocalFileURLByMappingWithProductionUploadPath(t *testing.T) {
filePath := "/data/app/iotlcmadmin-web-svc/upload/000000/user/file/20260710/sim-card-renew/result/REN-result.xlsx"
configValue := "/data/app/iotlcmadmin-web-svc/upload|http://example.com:8008/upload"
got, ok := localFileURLByMapping(filePath, configValue)
if !ok {
t.Fatal("expected production upload path mapping to match")
}
want := "http://example.com:8008/upload/000000/user/file/20260710/sim-card-renew/result/REN-result.xlsx"
if got != want {
t.Fatalf("url = %q, want %q", got, want)
}
}
func TestLocalFileURLByBaseWithProductionUploadPath(t *testing.T) {
filePath := "/data/app/iotlcmadmin-web-svc/upload/000000/user/file/20260710/sim-card-renew/import/REN-import.xlsx"
got, ok := localFileURLByBase(filePath, "/data/app/iotlcmadmin-web-svc/upload", "http://example.com:8008/upload")
if !ok {
t.Fatal("expected upload path base mapping to match")
}
want := "http://example.com:8008/upload/000000/user/file/20260710/sim-card-renew/import/REN-import.xlsx"
if got != want {
t.Fatalf("url = %q, want %q", got, want)
}
}
func TestLocalFileURLByMappingWithDevelopmentUploadPath(t *testing.T) {
got, ok := localFileURLByMapping(
filepath.FromSlash("resource/upload/000000/user/file/test.xlsx"),
"resource/upload|/upload",
)
if !ok {
t.Fatal("expected development upload path mapping to match")
}
if got != "/upload/000000/user/file/test.xlsx" {
t.Fatalf("url = %q", got)
}
}
@@ -0,0 +1,214 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility/convert"
"git.5iot.com/iotlcm-public/iotlcm-app/utility/excel"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysPost struct {
}
func NewSysPost() *sSysPost {
return &sSysPost{}
}
func init() {
service.RegisterSysPost(NewSysPost())
}
func (l *sSysPost) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.SysPost.Ctx(ctx), option...)
}
func (l *sSysPost) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysPost.Table()), option...)
}
func (l *sSysPost) List(ctx context.Context, query *model.SysPostListParam) (items []*model.SysPostListModel, total int, err error) {
m := l.ModelQuery(ctx)
deptIds := make([]int64, 0)
if query.DeptId != 0 {
deptIds = append(deptIds, query.DeptId)
} else if query.BelongDeptId != 0 {
deptIds = append(deptIds, query.BelongDeptId)
subDeptIds, err := service.SysDept().GetDeptIdsByParentId(ctx, query.BelongDeptId)
if err != nil {
return nil, 0, err
}
deptIds = append(deptIds, subDeptIds...)
}
if query.PostCode != "" {
m = m.WhereLike(dao.SysPost.Columns().PostCode, "%"+query.PostCode+"%")
}
if query.PostName != "" {
m = m.WhereLike(dao.SysPost.Columns().PostName, "%"+query.PostName+"%")
}
if len(deptIds) > 0 {
m = m.WhereIn(dao.SysPost.Columns().DeptId, deptIds)
}
if query.Status != "" {
m = m.Where(dao.SysPost.Columns().Status, query.Status)
}
if len(query.CreatedAt) == 2 {
startTime := gtime.NewFromStr(query.CreatedAt[0])
endTime := gtime.NewFromStr(query.CreatedAt[1])
m = m.WhereBetween(dao.SysPost.Columns().CreatedAt, startTime, endTime.EndOfDay())
}
total, err = m.Count()
if err != nil {
return nil, 0, err
}
err = m.WithAll().Page(query.Page, query.PageSize).OrderAsc(dao.SysPost.Columns().PostSort).Scan(&items)
if err != nil {
return nil, 0, err
}
return
}
func (l *sSysPost) View(ctx context.Context, param *model.SysPostViewParam) (post *model.SysPostViewModel, err error) {
err = l.ModelQuery(ctx).WithAll().Where(dao.SysPost.Columns().PostId, param.PostId).Scan(&post)
if err != nil {
return nil, err
}
return
}
func (l *sSysPost) Add(ctx context.Context, param *model.SysPostAddParam) (post *model.SysPostAddModel, err error) {
user := contexts.GetUser(ctx)
if user == nil {
return nil, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
}
data := do.SysPost{}
gconv.Struct(param, &data)
data.TenantId = user.TenantId
data.CreatedDept = user.DeptId
data.CreatedBy = user.ID
data.CreatedAt = gtime.Now()
data.UpdatedBy = user.ID
data.UpdatedAt = gtime.Now()
postId, err := l.Model(ctx).Data(data).OmitNil().InsertAndGetId()
if err != nil {
return nil, err
}
post = &model.SysPostAddModel{
PostId: postId,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysPostCreate, postId)
return
}
func (l *sSysPost) Edit(ctx context.Context, param *model.SysPostEditParam) (post *model.SysPostEditModel, err error) {
user := contexts.GetUser(ctx)
if user == nil {
return nil, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
}
data := do.SysPost{}
gconv.Struct(param, &data)
data.UpdatedBy = user.ID
data.UpdatedAt = gtime.Now()
_, err = l.Model(ctx).Data(data).OmitNil().Where(dao.SysPost.Columns().PostId, param.PostId).Update()
if err != nil {
return nil, err
}
post = &model.SysPostEditModel{
PostId: param.PostId,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysPostUpdate, param.PostId)
return
}
func (l *sSysPost) Delete(ctx context.Context, param *model.SysPostDeleteParam) (post *model.SysPostDeleteModel, err error) {
user := contexts.GetUser(ctx)
if user == nil {
return nil, gerror.NewCode(consts.CodeLoginExpired, "登录已过期")
}
ids := make([]int64, 0)
if param.PostId != 0 {
ids = append(ids, param.PostId)
}
if len(param.PostIds) > 0 {
ids = append(ids, param.PostIds...)
}
if len(ids) == 0 {
return nil, gerror.New("请选择要删除的岗位")
}
data := do.SysPost{}
data.DeletedBy = user.ID
data.DeletedAt = gtime.Now()
_, err = l.Model(ctx).Data(data).OmitNil().WhereIn(dao.SysPost.Columns().PostId, ids).Update()
if err != nil {
return nil, err
}
post = &model.SysPostDeleteModel{
PostId: param.PostId,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysPostDelete, ids)
return
}
func (l *sSysPost) Export(ctx context.Context, param *model.SysPostExportParam) (post *model.SysPostExportModel, err error) {
if param.Page == 0 {
param.Page = 1
}
if param.PageSize == 0 {
param.PageSize = 1000
}
items, total, err := l.List(ctx, param.SysPostListParam)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.SysPostExportModel{})
if err != nil {
return
}
var (
fileName = "导出CURD列表-" + gctx.CtxId(ctx)
sheetName = fmt.Sprintf("索引条件共%v行,共%v页,当前导出是第%v页,本页共%v行", total, total+param.PageSize-1/param.PageSize, param.Page, len(items))
exports []model.SysPostExportModel
)
g.Log().Info(ctx, " sSysPost.Export total", total)
if err = gconv.Scan(items, &exports); err != nil {
return
}
g.Log().Info(ctx, " sSysPost.Export exports", len(exports))
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
g.Log().Info(ctx, " sSysPost.Export err", err)
post = &model.SysPostExportModel{}
return
}
@@ -0,0 +1,816 @@
package system
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/container/gset"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/google/uuid"
)
type sSysQrcode struct {
}
func NewSysQrcode() *sSysQrcode {
return &sSysQrcode{}
}
func init() {
service.RegisterSysQrcode(NewSysQrcode())
go func() {
for {
time.Sleep(time.Second * 60)
g.Log().Debug(context.Background(), "检查二维码是否过期,过期则删除, 每分钟检查一次")
checkQrcodeExpiredToRemove(context.Background())
}
}()
}
type SysQrcodeConfig struct {
ThirdId int `json:"thirdId" dc:"第三方ID"`
SceneIdLogin int `json:"sceneIdLogin" dc:"场景ID-登录"`
SceneIdBind int `json:"sceneIdBind" dc:"场景ID-绑定"`
Token string `json:"token" dc:"Token"`
ServerApi string `json:"serverApi" dc:"服务端API"`
}
// 检查二维码是否过期,过期则删除
func checkQrcodeExpiredToRemove(ctx context.Context) {
var sysQrcodeConfig *SysQrcodeConfig
g.TryCatch(ctx, func(ctx context.Context) {
var err error
sysQrcodeConfig, err = getSysQrcodeConfig(ctx)
if err != nil {
g.Log().Errorf(ctx, "获取系统二维码配置失败, 错误: %v", err)
return
}
}, func(ctx context.Context, exception error) {
g.Log().Errorf(ctx, "检查二维码是否过期, 获取系统二维码配置失败, 错误: %v", exception)
})
if sysQrcodeConfig == nil {
return
}
g.TryCatch(ctx, func(ctx context.Context) {
redisKey := fmt.Sprintf(consts.KeyLoginScanWechat, sysQrcodeConfig.SceneIdLogin)
err := checkQrcodeExpiredToRemoveByKey(ctx, redisKey)
if err != nil {
g.Log().Errorf(ctx, "检查登录二维码是否过期失败, 错误: %v", err)
return
}
}, func(ctx context.Context, exception error) {
g.Log().Errorf(ctx, "检查登录二维码是否过期, 错误: %v", exception)
})
g.TryCatch(ctx, func(ctx context.Context) {
redisKey := fmt.Sprintf(consts.KeyUserBindWechat, sysQrcodeConfig.SceneIdBind)
err := checkQrcodeExpiredToRemoveByKey(ctx, redisKey)
if err != nil {
g.Log().Errorf(ctx, "检查绑定二维码是否过期失败, 错误: %v", err)
return
}
}, func(ctx context.Context, exception error) {
g.Log().Errorf(ctx, "检查绑定二维码是否过期, 错误: %v", exception)
})
}
// 检查指定redisKey的Hash的Field是否过期,过期则删除对应的feild
func checkQrcodeExpiredToRemoveByKey(ctx context.Context, redisKey string) (err error) {
val, err := g.Redis().HGetAll(ctx, redisKey)
if err != nil {
g.Log().Errorf(ctx, "获取登录二维码缓存信息失败 redisKey:%s, 错误: %v", redisKey, err)
return err
}
mapData := val.Map()
if len(mapData) == 0 {
return nil
}
for field, v := range mapData {
cacheModel := &model.QrcodeCacheModel{}
err = gconv.Struct(v, cacheModel)
if err != nil {
// 删除redisKey
g.Log().Debug(ctx, "数据转换失败, 删除对应的数据 redisKey:%s, field:%s", redisKey, field)
g.Redis().Do(ctx, "HDEL", redisKey, field)
continue
}
// 判断是否过期 一分钟
if cacheModel.ExpireTime.Before(gtime.Now().Add(time.Minute * -1)) {
// 删除redisKey
g.Log().Debug(ctx, "删除过期数据, redisKey:%s, field:%s", redisKey, field)
g.Redis().Do(ctx, "HDEL", redisKey, field)
}
}
return nil
}
// 获取系统二维码配置
func getSysQrcodeConfig(ctx context.Context) (*SysQrcodeConfig, error) {
thirdId, err := g.Cfg().Get(ctx, "wx.qrcode.thirdId")
if err != nil {
return nil, err
}
sceneIdLogin, err := g.Cfg().Get(ctx, "wx.qrcode.sceneIdLogin")
if err != nil {
return nil, err
}
sceneIdBind, err := g.Cfg().Get(ctx, "wx.qrcode.sceneIdBind")
if err != nil {
return nil, err
}
token, err := g.Cfg().Get(ctx, "wx.qrcode.token")
if err != nil {
return nil, err
}
serverApi, err := g.Cfg().Get(ctx, "wx.qrcode.serverApi")
if err != nil {
return nil, err
}
return &SysQrcodeConfig{
ThirdId: thirdId.Int(),
SceneIdLogin: sceneIdLogin.Int(),
SceneIdBind: sceneIdBind.Int(),
Token: token.String(),
ServerApi: serverApi.String(),
}, nil
}
// 调用二维码API
func callQrcodeApi(ctx context.Context, sysQrcodeConfig *SysQrcodeConfig, isLogin bool) (res *model.ApiCallQrcodeModel, err error) {
// 调用API
sceneId := sysQrcodeConfig.SceneIdLogin
if !isLogin {
sceneId = sysQrcodeConfig.SceneIdBind
}
request := g.Client()
request.SetHeader("Content-Type", "application/json")
timeStamp, sign := tokenEncode(sysQrcodeConfig.Token)
response, err := request.Get(ctx, sysQrcodeConfig.ServerApi, g.Map{
"thirdId": sysQrcodeConfig.ThirdId,
"sceneId": sceneId,
"timeStamp": timeStamp,
"sign": sign,
})
if err != nil {
g.Log().Errorf(ctx, "调用二维码API失败, 请求数据: %v, 错误: %v", request, err)
return nil, err
}
if response.StatusCode != 200 {
g.Log().Errorf(ctx, "调用二维码API失败, 请求数据: %v, 响应数据: %v", request, response)
return nil, errors.New("调用二维码API失败")
}
responseData := response.ReadAllString()
g.Log().Debugf(ctx, "调用二维码API成功, 请求数据: %v, 响应数据: %v", request, responseData)
result := &model.ApiCallQrcodeResult{}
err = json.Unmarshal([]byte(responseData), &result)
if err != nil {
g.Log().Errorf(ctx, "调用二维码API失败, 数据转换失败, 请求数据: %v, 响应数据: %v, 错误: %v", request, responseData, err)
return nil, err
}
if result.Code != 0 {
g.Log().Errorf(ctx, "调用二维码API失败, 错误信息: %v", result.Error)
return nil, errors.New(result.Error)
}
return &model.ApiCallQrcodeModel{
Ticket: result.Data.Ticket,
ExpireTime: result.Data.ExpireTime,
QrUrl: result.Data.QrUrl,
}, nil
}
// 获取登录二维码
func (l *sSysQrcode) GetQrcodeLogin(ctx context.Context) (res *model.QrcodeLoginModel, err error) {
sysQrcodeConfig, err := getSysQrcodeConfig(ctx)
if err != nil {
g.Log().Errorf(ctx, "获取系统二维码配置失败, 错误: %v", err)
return nil, gerror.New("获取系统二维码配置失败")
}
if sysQrcodeConfig.ServerApi == "" || sysQrcodeConfig.Token == "" || sysQrcodeConfig.ThirdId == 0 || sysQrcodeConfig.SceneIdLogin == 0 || sysQrcodeConfig.SceneIdBind == 0 {
return nil, errors.New("系统二维码配置错误")
}
apiResult, err := callQrcodeApi(ctx, sysQrcodeConfig, true)
if err != nil {
g.Log().Errorf(ctx, "获取二维码失败, 错误: %v", err)
return nil, gerror.New("获取二维码失败")
}
expireTime := gtime.Now().Add(time.Second * time.Duration(apiResult.ExpireTime))
// 将结果保存到redis中
tempUserId := uuid.New().String()
redisKey := fmt.Sprintf(consts.KeyLoginScanWechat, sysQrcodeConfig.SceneIdLogin)
cacheModel := &model.QrcodeCacheModel{
Ticket: apiResult.Ticket,
ExpireTime: expireTime,
QrUrl: apiResult.QrUrl,
Scanned: false,
OpenId: "",
}
mapData := make(map[string]interface{})
mapData[tempUserId] = cacheModel
g.Redis().HSet(ctx, redisKey, mapData)
return &model.QrcodeLoginModel{
TempUserId: tempUserId,
QrcodeCacheModel: *cacheModel,
Bound: false,
Expired: false,
}, nil
}
// 对openId进行加密
func openIdEncode(openId string) string {
data := []byte(openId)
hash := md5.Sum(data)
md5String := hex.EncodeToString(hash[:])
return base64.StdEncoding.EncodeToString([]byte(openId + "," + md5String))
}
// 对openId进行解密
func openIdDecode(openIdEncode string) (string, error) {
decode, err := base64.StdEncoding.DecodeString(openIdEncode)
if err != nil {
return "", err
}
strs := strings.Split(string(decode), ",")
if len(strs) != 2 {
return "", errors.New("openId 格式错误")
}
openId := strs[0]
md5String := strs[1]
data := []byte(openId)
hash := md5.Sum(data)
md5String2 := hex.EncodeToString(hash[:])
if md5String != md5String2 {
return "", errors.New("openId 格式错误")
}
return openId, nil
}
// 对token进行加密
func tokenEncode(token string) (timeStamp int64, md5String string) {
nowTime := time.Now().Unix()
tokenStr := fmt.Sprintf("%d%s", nowTime, token)
data := []byte(tokenStr)
hash := md5.Sum(data)
md5String = hex.EncodeToString(hash[:])
return nowTime, md5String
}
func tokenTimeStampEncode(timeStamp int64, token string) (md5String string) {
tokenStr := fmt.Sprintf("%d%s", timeStamp, token)
data := []byte(tokenStr)
hash := md5.Sum(data)
md5String = hex.EncodeToString(hash[:])
return md5String
}
// 获取登录二维码扫码结果
func (l *sSysQrcode) GetQrcodeLoginStatus(ctx context.Context, tempUserId string) (res *model.QrcodeLoginModel, err error) {
sysQrcodeConfig, err := getSysQrcodeConfig(ctx)
if err != nil {
g.Log().Errorf(ctx, "获取系统二维码配置失败, 错误: %v", err)
return nil, gerror.New("获取系统二维码配置失败")
}
res = &model.QrcodeLoginModel{
TempUserId: tempUserId,
QrcodeCacheModel: model.QrcodeCacheModel{
Ticket: "",
ExpireTime: gtime.Now(),
QrUrl: "",
Scanned: false,
OpenId: "",
},
Bound: false,
Expired: false,
}
// 获取不到缓存,统一标记为过期
redisKey := fmt.Sprintf(consts.KeyLoginScanWechat, sysQrcodeConfig.SceneIdLogin)
cashValue, err := g.Redis().HGet(ctx, redisKey, tempUserId)
if err != nil || cashValue == nil || cashValue.Map() == nil {
g.Log().Errorf(ctx, "未获取到%s的二维码扫码结果, 错误: %v", tempUserId, err)
res.Expired = true
return res, nil
}
// 将val 的数据保存到cacheModel中'
cacheModel := &model.QrcodeCacheModel{}
g.Log().Debugf(ctx, "获取登录二维码扫码结果, 缓存数据: %v", cashValue.Map())
mapData := cashValue.Map()
err = gconv.Struct(mapData, cacheModel)
if err != nil {
g.Log().Errorf(ctx, "获取登录二维码扫码结果失败, 错误: %v", err)
res.Expired = true
return res, nil
}
// 将缓存结果赋值给返回结果
res.Ticket = cacheModel.Ticket
res.ExpireTime = cacheModel.ExpireTime
res.QrUrl = cacheModel.QrUrl
res.Scanned = cacheModel.Scanned
res.OpenId = openIdEncode(cacheModel.OpenId)
g.Log().Debugf(ctx, "获取登录二维码扫码结果, 缓存数据: %v", cacheModel)
//先看扫码状态
if !cacheModel.Scanned {
// 未扫码 需要判断是否过期
if cacheModel.ExpireTime.Before(gtime.Now()) {
res.Expired = true
// 删除redisKey
g.Redis().Do(ctx, "HDEL", redisKey, tempUserId)
}
return res, nil
}
// 已扫码 需要判断是否绑定
//根据openId和source查询数据库
userBindList, _, err := service.SysSocial().List(ctx, &model.SysSocialListParam{
OpenId: cacheModel.OpenId,
Source: "wechat_mp",
}, &request.PageInfo{Page: 1, PageSize: 100})
if err != nil {
g.Log().Errorf(ctx, "获取社会化关系表信息失败, 错误: %v", err)
return nil, gerror.New("获取绑定信息失败")
}
if len(userBindList) == 0 {
res.Bound = false
// 删除redisKey
g.Redis().Do(ctx, "HDEL", redisKey, tempUserId)
return res, nil
}
validBindUserList := make([]*model.SysSocialListModel, 0)
tenantIdList := gset.NewStrSet()
for _, bindUser := range userBindList {
user, err := service.SysUser().GetUserByUsername(ctx, bindUser.UserName, bindUser.TenantId)
if err != nil || user == nil {
g.Log().Errorf(ctx, "关联信息无效, Id:%d, openId:%s, source:%s, userName:%s, tenantId:%s", bindUser.Id, cacheModel.OpenId, "wechat_mp", bindUser.UserName, bindUser.TenantId)
continue
}
bindUser.User = &model.SysUserViewModel{
UserId: user.UserId,
UserName: user.UserName,
NickName: user.NickName,
Avatar: user.Avatar,
TenantId: user.TenantId,
}
tenantIdList.Add(bindUser.TenantId)
validBindUserList = append(validBindUserList, bindUser)
}
//租户Idmap
tenantMap := make(map[string]*model.SysTenantListModel)
if tenantIdList.Size() > 0 {
// 查询租户列表
tenantList := make([]*model.SysTenantListModel, 0)
err := dao.SysTenant.Ctx(ctx).WhereIn(dao.SysTenant.Columns().TenantId, tenantIdList.Slice()).Scan(&tenantList)
if err != nil {
g.Log().Errorf(ctx, "获取租户列表失败, 错误: %v", err)
return nil, gerror.New("获取租户列表失败")
}
for _, tenant := range tenantList {
tenantMap[tenant.TenantId] = tenant
}
}
if len(validBindUserList) == 1 {
//单账号绑定 执行登录操作
bindUser := validBindUserList[0]
res.Bound = true
// 执行登录操作
userOut, token, err := service.SysAuth().LoginByOpenId(ctx, bindUser)
if err != nil {
g.Log().Errorf(ctx, "登录操作失败, 错误: %v", err)
return nil, gerror.New("登录失败, 请稍后重试")
}
res.LoginUserOut = userOut
res.Token = token
} else {
// 多账号绑定,返回用户列表
userList := make([]*model.SysUserSocialModel, 0)
for _, bindUser := range validBindUserList {
user := bindUser.User
tenant := tenantMap[user.TenantId]
tenantName := ""
if tenant != nil {
tenantName = tenant.CompanyName
}
userList = append(userList, &model.SysUserSocialModel{
TenantId: user.TenantId,
UserId: user.UserId,
UserName: user.UserName,
NickName: user.NickName,
Phonenumber: user.Phonenumber,
Avatar: user.Avatar,
TenantName: tenantName,
})
}
res.UserList = userList
}
// 删除redisKey
g.Redis().Do(ctx, "HDEL", redisKey, tempUserId)
return res, nil
}
// 获取绑定二维码
func (l *sSysQrcode) GetQrcodeBind(ctx context.Context) (res *model.QrcodeCacheModel, err error) {
userId := contexts.GetUserId(ctx)
if userId == 0 {
return nil, gerror.New("用户未登录")
}
sysQrcodeConfig, err := getSysQrcodeConfig(ctx)
if err != nil {
return nil, err
}
if sysQrcodeConfig.ServerApi == "" || sysQrcodeConfig.Token == "" || sysQrcodeConfig.ThirdId == 0 || sysQrcodeConfig.SceneIdLogin == 0 || sysQrcodeConfig.SceneIdBind == 0 {
return nil, errors.New("系统二维码配置错误")
}
apiResult, err := callQrcodeApi(ctx, sysQrcodeConfig, false)
if err != nil {
g.Log().Errorf(ctx, "获取绑定二维码失败, 错误: %v", err)
return nil, gerror.New("获取绑定二维码失败")
}
expireTime := gtime.Now().Add(time.Second * time.Duration(apiResult.ExpireTime))
// 将结果保存到redis中
res = &model.QrcodeCacheModel{
Ticket: apiResult.Ticket,
ExpireTime: expireTime,
QrUrl: apiResult.QrUrl,
Scanned: false,
OpenId: "",
}
redisKey := fmt.Sprintf(consts.KeyUserBindWechat, sysQrcodeConfig.SceneIdBind)
mapData := make(map[string]interface{})
g.Log().Debugf(ctx, "获取绑定二维码, 缓存数据: %v", res)
mapData[strconv.FormatInt(userId, 10)] = res
g.Log().Debugf(ctx, "用户 %d 设置绑定二维码缓存: key: %s field: %s value: %v", userId, redisKey, strconv.FormatInt(userId, 10), res)
g.Redis().HSet(ctx, redisKey, mapData)
return res, nil
}
// 获取绑定二维码扫码结果
func (l *sSysQrcode) GetQrcodeBindStatus(ctx context.Context) (res *model.QrcodeBindStatusModel, err error) {
userId := contexts.GetUserId(ctx)
if userId == 0 {
return nil, gerror.New("用户未登录")
}
sysQrcodeConfig, err := getSysQrcodeConfig(ctx)
if err != nil {
return nil, err
}
if sysQrcodeConfig.ServerApi == "" || sysQrcodeConfig.Token == "" || sysQrcodeConfig.ThirdId == 0 || sysQrcodeConfig.SceneIdLogin == 0 || sysQrcodeConfig.SceneIdBind == 0 {
return nil, errors.New("系统二维码配置错误")
}
redisKey := fmt.Sprintf(consts.KeyUserBindWechat, sysQrcodeConfig.SceneIdBind)
val, err := g.Redis().HGet(ctx, redisKey, strconv.FormatInt(userId, 10))
g.Log().Debugf(ctx, "获取绑定二维码扫码结果, 用户 %d 的缓存数据: %v", userId, val.Map())
if err != nil || val == nil || val.Map() == nil {
return &model.QrcodeBindStatusModel{
QrcodeCacheModel: model.QrcodeCacheModel{
Ticket: "",
ExpireTime: gtime.Now(),
QrUrl: "",
},
Expired: true,
Bound: false,
}, nil
}
cacheModel := &model.QrcodeCacheModel{}
mapData := val.Map()
err = gconv.Struct(mapData, cacheModel)
if err != nil {
return nil, gerror.New("获取绑定二维码扫码结果失败")
}
res = &model.QrcodeBindStatusModel{
QrcodeCacheModel: *cacheModel,
Bound: false,
Expired: false,
}
if !cacheModel.Scanned {
// TODO 判断是否过期
if cacheModel.ExpireTime.Before(gtime.Now()) {
res.Expired = true
// 删除redisKey
g.Redis().Do(ctx, "HDEL", redisKey, strconv.FormatInt(userId, 10))
}
return res, nil
} else {
// 已扫码 需要判断是否绑定
// 判断是否已绑定
userBindList, _, err := service.SysSocial().List(ctx, &model.SysSocialListParam{
UserId: userId,
OpenId: cacheModel.OpenId,
Source: "wechat_mp",
}, &request.PageInfo{Page: 1, PageSize: 1})
if err != nil {
return nil, gerror.New("获取绑定信息失败")
}
if len(userBindList) > 0 {
res.Bound = true
} else {
res.Bound = false
}
// 删除redisKey
g.Redis().Do(ctx, "HDEL", redisKey, strconv.FormatInt(userId, 10))
}
return res, nil
}
// 扫码回调
func (l *sSysQrcode) QrcodeScanCallback(ctx context.Context, param *model.QrcodeScanCallbackParam) (err error) {
g.Log().Debug(ctx, "扫码回调, 参数: %v", param)
if param.TimeStamp == 0 || param.Sign == "" {
return gerror.New("签名或时间戳不能为空")
}
sysQrcodeConfig, err := getSysQrcodeConfig(ctx)
if err != nil {
return err
}
if sysQrcodeConfig.ServerApi == "" || sysQrcodeConfig.Token == "" || sysQrcodeConfig.ThirdId == 0 || sysQrcodeConfig.SceneIdLogin == 0 || sysQrcodeConfig.SceneIdBind == 0 {
return errors.New("系统二维码配置错误")
}
//nowTime := gtime.Now().Unix()
sign := tokenTimeStampEncode(param.TimeStamp, sysQrcodeConfig.Token)
// if math.Abs(float64(param.TimeStamp-nowTime)) > 60 {
// return gerror.New("时间戳无效")
// }
if param.Sign != sign {
return gerror.New("签名无效")
}
if param.SceneId == sysQrcodeConfig.SceneIdLogin {
redisKey := fmt.Sprintf(consts.KeyLoginScanWechat, param.SceneId)
// 登录回调 更新一个扫码缓存状态为已扫码
val, err := g.Redis().HGetAll(ctx, redisKey)
if err != nil {
return gerror.New("获取登录二维码扫码结果失败")
}
g.Log().Debugf(ctx, "登录回调, 获取登录二维码扫码结果: %v", val)
mapData := val.Map()
if len(mapData) == 0 {
return gerror.New("获取登录二维码扫码结果失败")
}
for tempUserId, v := range mapData {
cacheModel := &model.QrcodeCacheModel{}
err = gconv.Struct(v, cacheModel)
if err != nil {
g.Log().Errorf(ctx, "缓存数据转换失败,数据: %v, 错误: %v", v, err)
continue
}
if cacheModel.Scanned || cacheModel.Ticket != param.Ticket {
continue
}
// 未扫码
cacheModel.Scanned = true
cacheModel.OpenId = param.OpenId
updateMap := make(map[string]interface{})
updateMap[tempUserId] = cacheModel
g.Redis().HSet(ctx, redisKey, updateMap)
break
}
}
if param.SceneId == sysQrcodeConfig.SceneIdBind {
// 绑定回调 保存绑定结果
redisKey := fmt.Sprintf(consts.KeyUserBindWechat, param.SceneId)
val, err := g.Redis().HGetAll(ctx, redisKey)
if err != nil {
return gerror.New("获取绑定二维码扫码结果失败 redisKey: " + redisKey)
}
mapData := val.Map()
if len(mapData) == 0 {
return gerror.New("获取绑定二维码扫码结果失败 redisKey: " + redisKey)
}
for userId, v := range mapData {
cacheModel := &model.QrcodeCacheModel{}
err = gconv.Struct(v, cacheModel)
if err != nil {
g.Log().Errorf(ctx, "缓存数据转换失败,数据: %v, 错误: %v", v, err)
continue
}
if cacheModel.Scanned || cacheModel.Ticket != param.Ticket {
continue
}
// 未扫码
cacheModel.Scanned = true
cacheModel.OpenId = param.OpenId
updateMap := make(map[string]interface{})
updateMap[userId] = cacheModel
g.Redis().HSet(ctx, redisKey, updateMap)
user := &entity.SysUser{}
// 查询用户信息
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserId, gconv.Int64(userId)).Scan(&user)
if err != nil {
g.Log().Errorf(ctx, "查询用户信息失败, 错误: %v", err)
return gerror.New("查询用户信息失败")
}
if user == nil {
g.Log().Errorf(ctx, "用户不存在, 用户ID: %s", userId)
return gerror.New("用户不存在")
}
// 保存绑定结果
err = service.SysSocial().Create(ctx, &model.SysSocialSaveParam{
UserId: gconv.Int64(userId),
AuthId: fmt.Sprintf("%s-%s", consts.SourceWechatMp, param.OpenId),
OpenId: param.OpenId,
Source: consts.SourceWechatMp,
UserName: user.UserName,
AccessToken: param.OpenId,
TenantId: user.TenantId,
CreatedBy: 0, // 创建者为0 表示系统自动创建
CreatedAt: gtime.Now(),
})
if err != nil {
g.Log().Errorf(ctx, "保存绑定结果失败, 错误: %v", err)
break
}
break
}
}
return nil
}
// 扫码后选择用户登录
func (l *sSysQrcode) QrcodeLoginSelectUserId(ctx context.Context, param *model.QrcodeLoginSelectUserIdParam) (res *model.QrcodeLoginAndBindOpenIdModel, err error) {
openId, err := openIdDecode(param.OpenId)
if err != nil {
g.Log().Errorf(ctx, "openId 解密失败, 错误: %v", err)
return nil, err
}
g.Log().Debugf(ctx, "扫码后选择用户登录, 参数: %v", param)
user, err := service.SysUser().GetUserByUsername(ctx, param.UserName, param.TenantId)
if err != nil {
return nil, err
}
if user == nil {
return nil, gerror.New("用户不存在")
}
userOut, token, err := service.SysAuth().LoginByOpenId(ctx, &model.SysSocialListModel{
UserId: user.UserId,
OpenId: openId,
Source: consts.SourceWechatMp,
TenantId: user.TenantId,
CreatedBy: 0, // 创建者为0 表示系统自动创建
CreatedAt: gtime.Now(),
User: &model.SysUserViewModel{
UserId: user.UserId,
UserName: user.UserName,
NickName: user.NickName,
Avatar: user.Avatar,
TenantId: user.TenantId,
},
})
return &model.QrcodeLoginAndBindOpenIdModel{
LoginUserOut: userOut,
Token: token,
}, nil
}
// 登录并绑定
func (l *sSysQrcode) QrcodeLoginAndBindOpenId(ctx context.Context, param *model.QrcodeLoginAndBindOpenIdParam) (res *model.QrcodeLoginAndBindOpenIdModel, err error) {
openId, err := openIdDecode(param.OpenId)
if err != nil {
g.Log().Errorf(ctx, "openId 解密失败, 错误: %v", err)
return nil, err
}
// 1. 执行登录
userOut, token, err := service.SysAuth().Login(ctx, &model.LoginParams{
Username: param.Username,
Password: param.Password,
CaptchaID: param.CaptchaID,
CaptchaValue: param.CaptchaValue,
TenantId: param.TenantId,
})
if err != nil {
return nil, err
}
// 2. 执行绑定
err = service.SysSocial().Create(ctx, &model.SysSocialSaveParam{
UserId: userOut.ID,
AuthId: fmt.Sprintf("%s-%s", consts.SourceWechatMp, openId),
OpenId: openId,
Source: consts.SourceWechatMp,
UserName: param.Username,
AccessToken: openId,
TenantId: param.TenantId,
CreatedBy: 0, // 创建者为0 表示系统自动创建
CreatedAt: gtime.Now(),
})
if err != nil {
return nil, err
}
return &model.QrcodeLoginAndBindOpenIdModel{
LoginUserOut: userOut,
Token: token,
}, nil
}
// 注册并绑定
func (l *sSysQrcode) QrcodeRegisterAndBindOpenId(ctx context.Context, param *model.QrcodeRegisterAndBindOpenIdParam) (res *model.QrcodeLoginAndBindOpenIdModel, err error) {
openId, err := openIdDecode(param.OpenId)
if err != nil || len(openId) == 0 {
g.Log().Errorf(ctx, "openId 解密失败, 错误: %v", err)
return nil, err
}
// 1. 执行注册
err = service.SysUser().Register(ctx, &model.SysUserRegisterModel{
UserName: param.UserName,
Password: param.Password,
TenantId: param.TenantId,
CaptchaID: param.CaptchaID,
CaptchaValue: param.CaptchaValue,
})
if err != nil {
return nil, err
}
user, err := service.SysUser().GetUserByUsername(ctx, param.UserName, param.TenantId)
if err != nil {
return nil, err
}
if user == nil {
return nil, gerror.New("用户注册失败")
}
// 2. 执行绑定
err = service.SysSocial().Create(ctx, &model.SysSocialSaveParam{
UserId: user.UserId,
AuthId: fmt.Sprintf("%s-%s", consts.SourceWechatMp, openId),
OpenId: openId,
Source: consts.SourceWechatMp,
UserName: param.UserName,
AccessToken: openId,
TenantId: param.TenantId,
CreatedBy: 0, // 创建者为0 表示系统自动创建
CreatedAt: gtime.Now(),
})
if err != nil {
return nil, err
}
// 3. 执行登录
userOut, token, err := service.SysAuth().LoginByOpenId(ctx, &model.SysSocialListModel{
UserId: user.UserId,
OpenId: openId,
Source: consts.SourceWechatMp,
TenantId: param.TenantId,
CreatedBy: 0, // 创建者为0 表示系统自动创建
CreatedAt: gtime.Now(),
User: &model.SysUserViewModel{
UserId: user.UserId,
UserName: user.UserName,
NickName: user.NickName,
Avatar: user.Avatar,
TenantId: user.TenantId,
},
})
if err != nil {
return nil, err
}
return &model.QrcodeLoginAndBindOpenIdModel{
LoginUserOut: userOut,
Token: token,
}, nil
}
@@ -0,0 +1,355 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"slices"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysRole struct {
}
func NewSysRole() *sSysRole {
return &sSysRole{}
}
func init() {
service.RegisterSysRole(NewSysRole())
}
func (l *sSysRole) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: false, // 角色表不验证数据权限
})
}
return handler.Model(dao.SysRole.Ctx(ctx), option...)
}
func (l *sSysRole) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: false, // 角色表不验证数据权限
})
}
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysRole.Table()), option...)
}
// 获取租户下角色列表
func (s *sSysRole) List(ctx context.Context, param *model.SysRoleListParam) (res []*model.SysRoleListModel, total int, err error) {
db := s.ModelQuery(ctx)
// 注意:ModelQuery使用内存数据库,删除的记录会被物理删除,所以不需要过滤deleted_at
if param.RoleName != "" {
db = db.WhereLike(dao.SysRole.Columns().RoleName, "%"+param.RoleName+"%")
}
if param.Status != "" {
db = db.Where(dao.SysRole.Columns().Status, param.Status)
}
if param.DeptId != nil {
db = db.Where(dao.SysRole.Columns().DeptId, *param.DeptId)
}
if len(param.CreatedAt) == 2 {
start, end := gtime.NewFromStr(param.CreatedAt[0]), gtime.NewFromStr(param.CreatedAt[1])
db = db.WhereBetween(dao.SysRole.Columns().CreatedAt, start, end.EndOfDay())
}
if len(param.RoleIds) > 0 {
db = db.WhereIn(dao.SysRole.Columns().RoleId, param.RoleIds)
}
total, err = db.Count()
if err != nil {
return nil, 0, err
}
if total == 0 {
return nil, 0, nil
}
companyMap, err := service.SysDept().GetDeptCompanyMap(ctx)
if err != nil {
return nil, 0, err
}
g.Log().Debugf(ctx, "sSysRole.List companyMap: %v", companyMap)
err = db.Page(param.Page, param.PageSize).Scan(&res)
if err != nil {
return nil, 0, err
}
for _, role := range res {
role.DeptName = companyMap[role.DeptId]
}
return
}
// 获取角色详情
func (s *sSysRole) View(ctx context.Context, param *model.SysRoleViewParam) (res *model.SysRoleViewModel, err error) {
err = s.ModelQuery(ctx).Where(dao.SysRole.Columns().RoleId, param.RoleId).Scan(&res)
if err != nil {
return nil, err
}
// 获取角色菜单
roleMenus, err := s.GetRoleMenu(ctx, param.RoleId)
if err != nil {
return nil, err
}
menuIdList := make([]int64, 0)
for _, menu := range roleMenus {
menuIdList = append(menuIdList, menu.MenuId)
}
res.MenuIds = menuIdList
// 获取角色部门
roleDepts, err := s.GetRoleDept(ctx, param.RoleId)
if err != nil {
return nil, err
}
deptIdList := make([]int64, 0)
for _, dept := range roleDepts {
deptIdList = append(deptIdList, dept.DeptId)
}
res.DeptIds = deptIdList
return
}
// 获取角色菜单
func (s *sSysRole) GetRoleMenu(ctx context.Context, id int64) (res []*entity.SysMenu, err error) {
mod := dao.SysRoleMenu.Ctx(ctx)
if tenantId := contexts.GetTenantId(ctx); tenantId != "" {
mod = mod.Where("tenant_id", tenantId)
}
err = mod.Where(dao.SysRoleMenu.Columns().RoleId, id).Scan(&res)
return
}
// 获取角色部门
func (s *sSysRole) GetRoleDept(ctx context.Context, id int64) (res []*entity.SysDept, err error) {
mod := dao.SysRoleDept.Ctx(ctx)
if tenantId := contexts.GetTenantId(ctx); tenantId != "" {
mod = mod.Where("tenant_id", tenantId)
}
err = mod.Where(dao.SysRoleDept.Columns().RoleId, id).Scan(&res)
return
}
// 获取角色列表对应菜单
func (s *sSysRole) GetRoleListMenu(ctx context.Context, ids []int64) (res []*entity.SysRoleMenu, err error) {
res = make([]*entity.SysRoleMenu, 0)
if slices.Contains(ids, consts.SuperAdminRoleId) {
menus, _, err := service.SysMenu().List(ctx, &model.SysMenuListParam{
Status: consts.SysMenuStatusNormal,
})
if err != nil {
return nil, err
}
for _, menu := range menus {
res = append(res, &entity.SysRoleMenu{
RoleId: consts.SuperAdminRoleId,
MenuId: menu.MenuId,
})
}
return res, nil
}
mod := dao.SysRoleMenu.Ctx(ctx)
if tenantId := contexts.GetTenantId(ctx); tenantId != "" {
mod = mod.Where("tenant_id", tenantId)
}
err = mod.WhereIn(dao.SysRoleMenu.Columns().RoleId, ids).Scan(&res)
return
}
// 新增角色
func (s *sSysRole) Add(ctx context.Context, param *model.SysRoleAddParam) (role *model.SysRoleAddModel, err error) {
user := contexts.GetUser(ctx)
if user == nil {
return nil, gerror.New("登录已过期")
}
data := do.SysRole{}
gconv.Struct(param, &data)
data.DataScope = "1"
data.CreatedBy = user.ID
data.CreatedAt = gtime.Now()
data.CreatedDept = user.DeptId
data.TenantId = user.TenantId
data.UpdatedBy = user.ID
data.UpdatedAt = gtime.Now()
roleId, err := s.Model(ctx).Data(data).OmitNil().InsertAndGetId()
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleCreate, roleId)
role = &model.SysRoleAddModel{
RoleId: roleId,
}
// 新增角色菜单
err = s.RoleMenu(ctx, roleId, param.MenuIds)
if err != nil {
return role, err
}
return
}
// 编辑角色
func (s *sSysRole) Edit(ctx context.Context, param *model.SysRoleEditParam) (role *model.SysRoleEditModel, err error) {
role = &model.SysRoleEditModel{
RoleId: param.RoleId,
}
user := contexts.GetUser(ctx)
if user == nil {
return role, gerror.New("登录已过期")
}
data := do.SysRole{}
gconv.Struct(param, &data)
data.UpdatedBy = user.ID
data.UpdatedAt = gtime.Now()
_, err = s.Model(ctx).Data(data).Where(dao.SysRole.Columns().RoleId, param.RoleId).OmitNil().Update()
if err != nil {
return role, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, param.RoleId)
// 更新角色菜单
err = s.RoleMenu(ctx, param.RoleId, param.MenuIds)
if err != nil {
return role, err
}
return
}
// 删除角色
func (s *sSysRole) Delete(ctx context.Context, param *model.SysRoleDeleteParam) (role *model.SysRoleDeleteModel, err error) {
role = &model.SysRoleDeleteModel{
RoleId: param.RoleId,
RoleIds: param.RoleIds,
}
roleIds := make([]int64, 0)
if param.RoleId != 0 {
roleIds = append(roleIds, param.RoleId)
} else {
roleIds = param.RoleIds
}
if len(roleIds) == 0 {
return role, gerror.New("请选择要删除的角色")
}
user := contexts.GetUser(ctx)
if user == nil {
return role, gerror.New("登录已过期")
}
data := do.SysRole{}
data.DeletedBy = user.ID
data.DeletedAt = gtime.Now()
_, err = s.Model(ctx).WhereIn(dao.SysRole.Columns().RoleId, roleIds).Data(data).OmitNil().Update()
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleDelete, roleIds)
return
}
func (s *sSysRole) RoleMenu(ctx context.Context, roleId int64, menuIds []int64) (err error) {
// 查询角色菜单
roleMenu := make([]*entity.SysRoleMenu, 0)
err = dao.SysRoleMenu.Ctx(ctx).Where(dao.SysRoleMenu.Columns().RoleId, roleId).Scan(&roleMenu)
if err != nil {
return err
}
currMenuIds := make([]int64, 0)
for _, menu := range roleMenu {
currMenuIds = append(currMenuIds, menu.MenuId)
}
delMenuIds := utility.ArrayRightDiff(currMenuIds, menuIds)
addMenuIds := utility.ArrayRightDiff(menuIds, currMenuIds)
// 删除角色菜单
if len(delMenuIds) > 0 {
_, err = dao.SysRoleMenu.Ctx(ctx).Where(dao.SysRoleMenu.Columns().RoleId, roleId).WhereIn(dao.SysRoleMenu.Columns().MenuId, delMenuIds).Delete()
}
// 新增角色菜单
if len(addMenuIds) > 0 {
addData := make([]*do.SysRoleMenu, 0, len(addMenuIds))
for _, menuId := range addMenuIds {
addData = append(addData, &do.SysRoleMenu{
RoleId: roleId,
MenuId: menuId,
})
}
_, err = dao.SysRoleMenu.Ctx(ctx).Data(addData).Insert()
}
return
}
// 自定义角色部门数据权限
func (s *sSysRole) RoleDept(ctx context.Context, roleId int64, deptIds []int64) (err error) {
// 查询角色部门
roleDept := make([]*entity.SysRoleDept, 0)
err = dao.SysRoleDept.Ctx(ctx).Where(dao.SysRoleDept.Columns().RoleId, roleId).Scan(&roleDept)
if err != nil {
return err
}
currDeptIds := make([]int64, 0)
for _, dept := range roleDept {
currDeptIds = append(currDeptIds, dept.DeptId)
}
delDeptIds := utility.ArrayRightDiff(currDeptIds, deptIds)
addDeptIds := utility.ArrayRightDiff(deptIds, currDeptIds)
// 删除角色部门
if len(delDeptIds) > 0 {
_, err = dao.SysRoleDept.Ctx(ctx).Where(dao.SysRoleDept.Columns().RoleId, roleId).WhereIn(dao.SysRoleDept.Columns().DeptId, delDeptIds).Delete()
}
// 新增角色部门
if len(addDeptIds) > 0 {
addData := make([]*do.SysRoleDept, 0, len(addDeptIds))
for _, deptId := range addDeptIds {
addData = append(addData, &do.SysRoleDept{
RoleId: roleId,
DeptId: deptId,
})
}
_, err = dao.SysRoleDept.Ctx(ctx).Data(addData).Insert()
}
return
}
// 编辑角色数据权限
func (s *sSysRole) EditRoleDataScope(ctx context.Context, model *model.SysRoleDataScopeEditParam) (err error) {
data := do.SysRole{}
gconv.Struct(model, &data)
data.UpdatedBy = contexts.GetUserId(ctx)
data.UpdatedAt = gtime.Now()
_, err = s.Model(ctx).Data(data).Where(dao.SysRole.Columns().RoleId, model.RoleId).OmitNil().Update()
if err != nil {
return err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, model.RoleId)
// 更新角色部门
err = s.RoleDept(ctx, model.RoleId, model.DeptIds)
if err != nil {
return err
}
return
}
@@ -0,0 +1,86 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysSocial struct {
}
func NewSysSocial() *sSysSocial {
return &sSysSocial{}
}
func init() {
service.RegisterSysSocial(NewSysSocial())
}
// 查询社会化关系表
func (s *sSysSocial) List(ctx context.Context, query *model.SysSocialListParam, page *request.PageInfo) (items []*model.SysSocialListModel, total int, err error) {
if query.UserId == 0 {
query.UserId = contexts.GetUserId(ctx)
}
db := dao.SysSocial.Ctx(ctx)
if query.UserId != 0 {
db = db.Where(dao.SysSocial.Columns().UserId, query.UserId)
}
if query.Source != "" {
db = db.Where(dao.SysSocial.Columns().Source, query.Source)
}
if query.OpenId != "" {
db = db.Where(dao.SysSocial.Columns().OpenId, query.OpenId)
}
total, err = db.Count()
if err != nil {
return nil, 0, err
}
err = db.Page(page.Page, page.PageSize).Scan(&items)
if err != nil {
return nil, 0, err
}
return items, total, nil
}
// 删除社会化关系表
func (s *sSysSocial) Delete(ctx context.Context, id int64) (err error) {
userId := contexts.GetUserId(ctx)
_, err = dao.SysSocial.Ctx(ctx).WherePri(id).Where(dao.SysSocial.Columns().UserId, userId).Delete()
return err
}
func (s *sSysSocial) Create(ctx context.Context, social *model.SysSocialSaveParam) (err error) {
return dao.SysSocial.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// 查询是否存在
exist, err := dao.SysSocial.Ctx(ctx).Where(dao.SysSocial.Columns().OpenId, social.OpenId).Where(dao.SysSocial.Columns().Source, social.Source).Where(dao.SysSocial.Columns().UserId, social.UserId).Exist()
if err != nil {
return err
}
if exist {
// 绑定信息已存在
g.Log().Errorf(ctx, "绑定信息已存在, openId: %s, source: %s, userId: %d", social.OpenId, social.Source, social.UserId)
return gerror.New("绑定信息已存在")
}
data := do.SysSocial{}
gconv.Struct(social, &data)
_, err = dao.SysSocial.Ctx(ctx).Data(data).OmitEmpty().Save()
return err
})
}
@@ -0,0 +1,639 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"database/sql"
"errors"
"fmt"
"slices"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysTenant struct {
}
func NewSysTenant() *sSysTenant {
return &sSysTenant{}
}
func init() {
s := NewSysTenant()
service.RegisterSysTenant(s)
}
func (l *sSysTenant) ModelQuery(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: false,
})
}
return handler.Model(service.MemoryDB().DB(ctx).Ctx(ctx).Model(dao.SysTenant.Table()), option...)
}
// 获取租户信息
func (l *sSysTenant) View(ctx context.Context, param *model.SysTenantViewParam) (data *model.SysTenantViewModel, err error) {
db := l.ModelQuery(ctx)
if param.Id != 0 {
db = db.Where(dao.SysTenant.Columns().Id, param.Id)
} else if param.TenantId != "" {
db = db.Where(dao.SysTenant.Columns().TenantId, param.TenantId)
} else {
return nil, errors.New("参数错误")
}
err = db.Scan(&data)
return
}
// 获取租户列表
func (l *sSysTenant) List(ctx context.Context, param *model.SysTenantListParam) (data []*model.SysTenantListModel, total int, err error) {
// db := dao.SysTenant.Ctx(ctx)
// 内存数据库优化
db := l.ModelQuery(ctx)
if param.TenantId != "" {
db = db.Where(dao.SysTenant.Columns().TenantId, param.TenantId)
}
if param.ContactUserName != "" {
db = db.WhereLike(dao.SysTenant.Columns().ContactUserName, "%"+param.ContactUserName+"%")
}
if param.ContactPhone != "" {
db = db.WhereLike(dao.SysTenant.Columns().ContactPhone, "%"+param.ContactPhone+"%")
}
if param.CompanyName != "" {
db = db.WhereLike(dao.SysTenant.Columns().CompanyName, "%"+param.CompanyName+"%")
}
if param.LicenseNumber != "" {
db = db.WhereLike(dao.SysTenant.Columns().LicenseNumber, "%"+param.LicenseNumber+"%")
}
if param.Status != "" {
db = db.Where(dao.SysTenant.Columns().Status, param.Status)
}
total, err = db.Count()
if err != nil {
return nil, 0, err
}
err = db.Page(param.Page, param.PageSize).OrderAsc(dao.SysTenant.Columns().PackageId).Scan(&data)
if err != nil {
return nil, 0, err
}
return data, total, nil
}
func (l *sSysTenant) Add(ctx context.Context, param *model.SysTenantAddParam) (output *model.SysTenantAddModel, err error) {
data := &do.SysTenant{}
gconv.Struct(param, &data)
data.CreatedAt = gtime.Now()
data.CreatedBy = contexts.GetUserId(ctx)
data.CreatedDept = contexts.GetDeptId(ctx)
// 查询租户套餐
tenantPackage, err := service.SysTenantPackage().View(ctx, &model.SysTenantPackageViewParam{
PackageId: param.PackageId,
})
if err != nil || tenantPackage == nil {
g.Log().Errorf(ctx, "sSysTenant.Add SysTenantPackage().View err: %v, packageId: %d, param: %+v",
err, param.PackageId, param)
err = errors.New("租户套餐不存在")
return nil, err
}
if tenantPackage.MenuIds == "" {
err = errors.New("租户套餐菜单为空")
return nil, err
}
menuIds := strings.Split(tenantPackage.MenuIds, ",")
tenantId := ""
deptId := int64(0)
roleId := int64(0)
userId := int64(0)
sysConfigIds := make([]int64, 0)
err = g.DB().Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error {
data.TenantId = "100000"
id, err := dao.SysTenant.Ctx(ctx).TX(tx).Data(data).OmitNil().InsertAndGetId()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add SysTenant.Table().InsertAndGetId err: %v, data: %+v", err, data)
return err
}
tenantId = fmt.Sprintf("%06d", 100000+id)
// 创建部门
dataDeptInsert := do.SysDept{}
dataDeptInsert.TenantId = tenantId
dataDeptInsert.DeptName = param.CompanyName
dataDeptInsert.ParentId = 0
dataDeptInsert.OrderNum = 0
dataDeptInsert.DeptType = 1 // 0:部门 1是公司
dataDeptInsert.Status = consts.SysDeptStatusNormal
dataDeptInsert.CreatedBy = contexts.GetUserId(ctx)
dataDeptInsert.CreatedAt = gtime.Now()
dataDeptInsert.UpdatedBy = contexts.GetUserId(ctx)
dataDeptInsert.UpdatedAt = gtime.Now()
deptId, err = dao.SysDept.Ctx(ctx).TX(tx).Data(dataDeptInsert).OmitNil().InsertAndGetId()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add SysDept.Table().InsertAndGetId err: %v, data: %+v", err, dataDeptInsert)
return err
}
// 创建角色
dataRoleInsert := do.SysRole{}
dataRoleInsert.TenantId = tenantId
dataRoleInsert.RoleName = "租户管理员"
dataRoleInsert.RoleKey = "tenantAdmin"
dataRoleInsert.RoleSort = 1
dataRoleInsert.Status = "0"
dataRoleInsert.DataScope = consts.SysRoleDataScopeAll
dataRoleInsert.MenuCheckStrictly = 0
dataRoleInsert.DeptCheckStrictly = 0
dataRoleInsert.CreatedBy = contexts.GetUserId(ctx)
dataRoleInsert.CreatedAt = gtime.Now()
dataRoleInsert.UpdatedBy = contexts.GetUserId(ctx)
dataRoleInsert.UpdatedAt = gtime.Now()
roleId, err = dao.SysRole.Ctx(ctx).TX(tx).Data(dataRoleInsert).OmitNil().InsertAndGetId()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add SysRole.Table().InsertAndGetId err: %v, data: %+v", err, dataRoleInsert)
return err
}
// 角色关联菜单
dataRoleMenuInserts := []*do.SysRoleMenu{}
for _, menuId := range menuIds {
dataRoleMenuInserts = append(dataRoleMenuInserts, &do.SysRoleMenu{
RoleId: roleId,
MenuId: menuId,
})
}
_, err = dao.SysRoleMenu.Ctx(ctx).TX(tx).Data(dataRoleMenuInserts).OmitNil().Insert()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add SysRoleMenu.Table().Insert err: %v, data: %+v", err, dataRoleMenuInserts)
return err
}
// 创建系统管理员
salt := utility.RandomString(5)
password := utility.PasswordEncrypt(param.Password, salt)
dataUserInsert := do.SysUser{}
dataUserInsert.TenantId = tenantId
dataUserInsert.DeptId = deptId
dataUserInsert.NickName = param.ContactUserName
dataUserInsert.UserName = param.Username
dataUserInsert.Password = password
dataUserInsert.UserType = consts.SysUserTypeSys
dataUserInsert.Salt = salt
dataUserInsert.Phonenumber = param.ContactPhone
dataUserInsert.Email = ""
dataUserInsert.Sex = consts.SysUserSexUnknown
dataUserInsert.Status = consts.SysUserStatusNormal
dataUserInsert.CreatedDept = contexts.GetDeptId(ctx)
dataUserInsert.CreatedBy = contexts.GetUserId(ctx)
dataUserInsert.CreatedAt = gtime.Now()
dataUserInsert.UpdatedBy = contexts.GetUserId(ctx)
dataUserInsert.UpdatedAt = gtime.Now()
userId, err = dao.SysUser.Ctx(ctx).TX(tx).Data(dataUserInsert).OmitNil().InsertAndGetId()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add SysUser.Table().InsertAndGetId err: %v, data: %+v", err, dataUserInsert)
return err
}
// 修改租户信息
tenantUpdate := do.SysTenant{
TenantId: tenantId,
AdminRoleId: roleId,
AdminDeptId: deptId,
AdminUserId: userId,
}
_, err = dao.SysTenant.Ctx(ctx).TX(tx).Data(tenantUpdate).
Where(dao.SysTenant.Columns().Id, id).Update()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add SysTenant.Table().Update err: %v, data: %+v", err, data)
return err
}
// 修改部门负责人
dataDeptUpdate := do.SysDept{}
dataDeptUpdate.DeptId = deptId
dataDeptUpdate.Leader = userId
dataDeptUpdate.UpdatedBy = contexts.GetUserId(ctx)
dataDeptUpdate.UpdatedAt = gtime.Now()
_, err = dao.SysDept.Ctx(ctx).TX(tx).Data(dataDeptUpdate).OmitNil().Where(dao.SysDept.Columns().DeptId, deptId).Update()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add SysDept.Table().Update err: %v, data: %+v", err, dataDeptUpdate)
return err
}
output = &model.SysTenantAddModel{
Id: id,
}
// 角色关联用户
dataUserRoleInserts := []*do.SysUserRole{}
dataUserRoleInserts = append(dataUserRoleInserts, &do.SysUserRole{
UserId: userId,
RoleId: roleId,
})
_, err = dao.SysUserRole.Ctx(ctx).TX(tx).Data(dataUserRoleInserts).OmitNil().Insert()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add SysUserRole.Table().Insert err: %v, data: %+v", err, dataUserRoleInserts)
return err
}
// 插入系统参数
sysConfigIds, err = l.BatchInsertSysParam(ctx, tx, tenantId, userId, deptId)
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add batchInsertSysParam err: %v, tenantId: %s, createdBy: %d, createdDept: %d", err, tenantId, contexts.GetUserId(ctx), contexts.GetDeptId(ctx))
return err
}
return nil
})
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add err: %v", err)
return nil, err
}
// 更新缓存
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysDeptCreate, deptId)
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleCreate, roleId)
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysTenantCreate, output.Id)
event.EventsInstance().Emit(ctx, consts.EventKeyUserCreate, userId)
for _, sysConfigId := range sysConfigIds {
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigCreate, sysConfigId)
}
return
}
func (l *sSysTenant) Edit(ctx context.Context, param *model.SysTenantEditParam) (output *model.SysTenantEditModel, err error) {
if param.Id == 0 {
return nil, errors.New("参数错误")
}
data := &do.SysTenant{}
gconv.Struct(param, &data)
data.UpdatedAt = gtime.Now()
data.UpdatedBy = contexts.GetUserId(ctx)
db := dao.SysTenant.Ctx(ctx)
db = db.Where(dao.SysTenant.Columns().Id, param.Id)
_, err = db.Data(data).OmitNil().Update()
if err != nil {
return nil, err
}
output = &model.SysTenantEditModel{
Id: param.Id,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysTenantUpdate, param.Id)
return
}
func (l *sSysTenant) Delete(ctx context.Context, param *model.SysTenantDeleteParam) (output *model.SysTenantDeleteModel, err error) {
if len(param.Ids) == 0 {
return nil, errors.New("参数错误")
}
if slices.Contains(param.Ids, 1) {
return nil, errors.New("不能删除默认租户")
}
data := &do.SysTenant{}
gconv.Struct(param, &data)
data.DeletedAt = gtime.Now()
data.DeletedBy = contexts.GetUserId(ctx)
db := dao.SysTenant.Ctx(ctx)
db = db.WhereIn(dao.SysTenant.Columns().Id, param.Ids)
_, err = db.Data(data).OmitNil().Update()
if err != nil {
return nil, err
}
output = &model.SysTenantDeleteModel{
Ids: param.Ids,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysTenantDelete, param.Ids)
return
}
func (l *sSysTenant) Status(ctx context.Context, param *model.SysTenantStatusParam) (output *model.SysTenantStatusModel, err error) {
if param.Id == 0 {
return nil, errors.New("参数错误")
}
data := &do.SysTenant{}
gconv.Struct(param, &data)
data.UpdatedAt = gtime.Now()
data.UpdatedBy = contexts.GetUserId(ctx)
db := dao.SysTenant.Ctx(ctx)
db = db.Where(dao.SysTenant.Columns().Id, param.Id)
_, err = db.Data(data).OmitNil().Update()
if err != nil {
return nil, err
}
output = &model.SysTenantStatusModel{
Id: param.Id,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysTenantUpdate, param.Id)
return
}
// 新建租户插入系统参数表数据
func (l *sSysTenant) BatchInsertSysParam(ctx context.Context, tx gdb.TX, tenantId string, createdBy int64, createdDept int64) (ids []int64, err error) {
now := gtime.Now()
configs := []do.SysConfig{
{
TenantId: tenantId,
ConfigName: "主框架页-默认皮肤样式名称",
ConfigKey: "sys.index.skinName",
ConfigValue: "skin-blue",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
Remark: "蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow",
},
{
TenantId: tenantId,
ConfigName: "用户管理-账号初始密码",
ConfigKey: "sys.user.initPassword",
ConfigValue: "123456",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
Remark: "初始化密码 123456",
},
{
TenantId: tenantId,
ConfigName: "主框架页-侧边栏主题",
ConfigKey: "sys.index.sideTheme",
ConfigValue: "theme-dark",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
Remark: "深色主题theme-dark,浅色主题theme-light",
},
{
TenantId: tenantId,
ConfigName: "账号自助-是否开启用户注册功能",
ConfigKey: "sys.account.registerUser",
ConfigValue: "false",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
Remark: "是否开启注册用户功能(true开启,false关闭)",
},
{
TenantId: tenantId,
ConfigName: "OSS预览列表资源开关",
ConfigKey: "sys.oss.previewListResource",
ConfigValue: "true",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
Remark: "true:开启, false:关闭",
},
{
TenantId: tenantId,
ConfigName: "上传文件类型",
ConfigKey: "sys.oss.fileType",
ConfigValue: "txt,md,doc,docx,pdf,xls,xlsx,ppt,pptx,txt,jpg,jpeg,png,gif,mp4,avi,mov,mp3,wav,m4a,csv,json,application/json,bin,hex,fw,zip",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "上传图片类型",
ConfigKey: "sys.oss.imgType",
ConfigValue: "jpg,jpeg,png,gif,webp",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "上传文件大小",
ConfigKey: "sys.oss.fileSize",
ConfigValue: "20M",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "上传图片大小",
ConfigKey: "sys.oss.imgSize",
ConfigValue: "5M",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "上传文件Url路径",
ConfigKey: "sys.oss.urlPath",
ConfigValue: "/data/app/iotlcmadmin-web-svc/upload|http://example.com:8008/upload",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
Remark: "\"http://ab.com/\" 或 \"/\" 或 \"resource/upload|http://ab.com/upload\"",
},
{
TenantId: tenantId,
ConfigName: "是否启动在线用户强制退出",
ConfigKey: "sys.online.forceLogout",
ConfigValue: "true",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
Remark: "true开启,false关闭",
},
{
TenantId: tenantId,
ConfigName: "系统启用的监控通道",
ConfigKey: "iot.default.channel.gb28181",
ConfigValue: "0",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "报警资源匹配时间",
ConfigKey: "iot.alarm.image.timesec",
ConfigValue: "10",
ConfigType: "N",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "自主注册组织ID",
ConfigKey: "iot.register.dept",
ConfigValue: "0",
ConfigType: "N",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "自主注册角色ID",
ConfigKey: "iot.register.role",
ConfigValue: "0",
ConfigType: "N",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "系统默认MQTT渠道",
ConfigKey: "iot.default.channel.mqtt",
ConfigValue: "0",
ConfigType: "N",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "系统默认TCP渠道",
ConfigKey: "iot.default.channel.tcp",
ConfigValue: "0",
ConfigType: "N",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "系统默认UDP渠道",
ConfigKey: "iot.default.channel.udp",
ConfigValue: "0",
ConfigType: "N",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
{
TenantId: tenantId,
ConfigName: "项目API默认地址",
ConfigKey: "iot.project.api.default.addr",
ConfigValue: "http://127.0.0.1:8510",
ConfigType: "Y",
CreatedDept: createdDept,
CreatedBy: createdBy,
CreatedAt: now,
},
}
ids = make([]int64, 0, len(configs))
configDao := dao.SysConfig.Ctx(ctx).TX(tx)
for _, config := range configs {
id, insertErr := configDao.Data(config).InsertAndGetId()
if insertErr != nil {
return nil, insertErr
}
ids = append(ids, id)
}
return ids, nil
}
// 同步租户菜单
func (l *sSysTenant) SyncTenantMenu(ctx context.Context, tenantId string) (err error) {
tenantInfo, err := l.View(ctx, &model.SysTenantViewParam{
TenantId: tenantId,
})
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.SyncTenantMenu SysTenant().View err: %v, tenantId: %s", err, tenantId)
return err
}
tenantPackage, err := service.SysTenantPackage().View(ctx, &model.SysTenantPackageViewParam{
PackageId: tenantInfo.PackageId,
})
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.SyncTenantMenu SysTenantPackage().View err: %v, tenantId: %s, packageId: %d", err, tenantId, tenantInfo.PackageId)
return err
}
if tenantPackage == nil {
return errors.New("租户套餐不存在")
}
menuIds := strings.Split(tenantPackage.MenuIds, ",")
// 获取角色对应菜单
roleMenuList, err := service.SysRole().GetRoleMenu(ctx, tenantInfo.AdminRoleId)
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.SyncTenantMenu SysRole().GetRoleMenu err: %v, tenantId: %s, roleId: %d", err, tenantId, tenantInfo.AdminRoleId)
return err
}
insertMenuIds := make([]do.SysRoleMenu, 0)
for _, menuId := range menuIds {
found := false
for _, roleMenu := range roleMenuList {
if roleMenu.MenuId == gconv.Int64(menuId) {
found = true
break
}
}
if !found {
insertMenuIds = append(insertMenuIds, do.SysRoleMenu{
RoleId: tenantInfo.AdminRoleId,
MenuId: gconv.Int64(menuId),
})
}
}
delMenuIds := make([]int64, 0)
for _, roleMenu := range roleMenuList {
if !slices.Contains(menuIds, gconv.String(roleMenu.MenuId)) {
delMenuIds = append(delMenuIds, roleMenu.MenuId)
continue
}
}
if len(delMenuIds) > 0 {
_, err = dao.SysRoleMenu.Ctx(ctx).WhereIn(dao.SysRoleMenu.Columns().RoleId, tenantInfo.AdminRoleId).
WhereIn(dao.SysRoleMenu.Columns().MenuId, delMenuIds).Delete()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.SyncTenantMenu SysRoleMenu.Ctx(ctx).Delete err: %v, tenantId: %s, roleId: %d, delMenuIds: %v", err, tenantId, tenantInfo.AdminRoleId, delMenuIds)
return err
}
}
if len(insertMenuIds) > 0 {
_, err = dao.SysRoleMenu.Ctx(ctx).Data(insertMenuIds).Insert()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.SyncTenantMenu SysRoleMenu.Ctx(ctx).Insert err: %v, tenantId: %s, roleId: %d, insertMenuIds: %v", err, tenantId, tenantInfo.AdminRoleId, insertMenuIds)
return err
}
}
return nil
}
// 查询用户ID是否是租户管理员
func (l *sSysTenant) IsTenantAdmin(ctx context.Context, userIds []int64) (isAdminUserIds []int64, err error) {
entityList := make([]*entity.SysTenant, 0)
err = dao.SysTenant.Ctx(ctx).WhereIn(dao.SysTenant.Columns().AdminUserId, userIds).Scan(&entityList)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
isAdminUserIds = make([]int64, 0)
for _, entity := range entityList {
isAdminUserIds = append(isAdminUserIds, entity.AdminUserId)
}
return isAdminUserIds, nil
}
@@ -0,0 +1,140 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysTenantPackage struct {
}
func NewSysTenantPackage() *sSysTenantPackage {
return &sSysTenantPackage{}
}
func init() {
service.RegisterSysTenantPackage(NewSysTenantPackage())
}
// 获取租户套餐
func (l *sSysTenantPackage) View(ctx context.Context, param *model.SysTenantPackageViewParam) (data *model.SysTenantPackageViewModel, err error) {
if param.PackageId == 0 {
return nil, errors.New("参数错误")
}
db := dao.SysTenantPackage.Ctx(ctx)
db = db.Where(dao.SysTenantPackage.Columns().PackageId, param.PackageId)
err = db.Scan(&data)
return
}
func (l *sSysTenantPackage) List(ctx context.Context, param *model.SysTenantPackageListParam) (data []*model.SysTenantPackageListModel, total int, err error) {
db := dao.SysTenantPackage.Ctx(ctx)
if param.PackageName != "" {
db = db.WhereLike(dao.SysTenantPackage.Columns().PackageName, "%"+param.PackageName+"%")
}
total, err = db.Count()
if err != nil {
return nil, 0, err
}
err = db.Page(param.Page, param.PageSize).OrderAsc(dao.SysTenantPackage.Columns().PackageId).Scan(&data)
if err != nil {
return nil, 0, err
}
return data, total, nil
}
func (l *sSysTenantPackage) Add(ctx context.Context, param *model.SysTenantPackageAddParam) (output *model.SysTenantPackageAddModel, err error) {
data := &do.SysTenantPackage{}
gconv.Struct(param, &data)
data.CreatedAt = gtime.Now()
data.CreatedBy = contexts.GetUserId(ctx)
data.CreatedDept = contexts.GetDeptId(ctx)
db := dao.SysTenantPackage.Ctx(ctx)
id, err := db.Data(data).OmitNil().InsertAndGetId()
if err != nil {
return nil, err
}
output = &model.SysTenantPackageAddModel{
PackageId: id,
}
return
}
func (l *sSysTenantPackage) Edit(ctx context.Context, param *model.SysTenantPackageEditParam) (output *model.SysTenantPackageEditModel, err error) {
if param.PackageId == 0 {
return nil, errors.New("参数错误")
}
data := &do.SysTenantPackage{}
gconv.Struct(param, &data)
data.UpdatedAt = gtime.Now()
data.UpdatedBy = contexts.GetUserId(ctx)
db := dao.SysTenantPackage.Ctx(ctx)
db = db.Where(dao.SysTenantPackage.Columns().PackageId, param.PackageId)
_, err = db.Data(data).OmitNil().Update()
if err != nil {
return nil, err
}
output = &model.SysTenantPackageEditModel{
PackageId: param.PackageId,
}
return
}
func (l *sSysTenantPackage) Delete(ctx context.Context, param *model.SysTenantPackageDeleteParam) (output *model.SysTenantPackageDeleteModel, err error) {
if len(param.PackageIds) == 0 {
return nil, errors.New("参数错误")
}
data := &do.SysTenantPackage{}
gconv.Struct(param, &data)
data.DeletedAt = gtime.Now()
data.DeletedBy = contexts.GetUserId(ctx)
db := dao.SysTenantPackage.Ctx(ctx)
db = db.WhereIn(dao.SysTenantPackage.Columns().PackageId, param.PackageIds)
_, err = db.Data(data).OmitNil().Update()
if err != nil {
return nil, err
}
output = &model.SysTenantPackageDeleteModel{
PackageIds: param.PackageIds,
}
return
}
func (l *sSysTenantPackage) Status(ctx context.Context, param *model.SysTenantPackageStatusParam) (output *model.SysTenantPackageStatusModel, err error) {
if param.PackageId == 0 {
return nil, errors.New("参数错误")
}
data := &do.SysTenantPackage{}
gconv.Struct(param, &data)
data.UpdatedAt = gtime.Now()
data.UpdatedBy = contexts.GetUserId(ctx)
db := dao.SysTenantPackage.Ctx(ctx)
db = db.Where(dao.SysTenantPackage.Columns().PackageId, param.PackageId)
_, err = db.Data(data).OmitNil().Update()
if err != nil {
return nil, err
}
output = &model.SysTenantPackageStatusModel{
PackageId: param.PackageId,
}
return
}
@@ -0,0 +1,768 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"fmt"
"slices"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"gorm.io/gorm"
)
type sSysUser struct {
}
func NewSysUser() *sSysUser {
return &sSysUser{}
}
func init() {
service.RegisterSysUser(NewSysUser())
}
func (l *sSysUser) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.SysUser.Ctx(ctx), option...)
}
// 根据用户名获取用户信息,不验证当前租户
func (l *sSysUser) GetUserByUsername(ctx context.Context, username string, tenantId string) (user *entity.SysUser, err error) {
var data *entity.SysUser
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserName, username).Where(dao.SysUser.Columns().TenantId, tenantId).Scan(&data)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
}
return nil, err
}
if data == nil {
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
}
if data.Status == consts.SysUserStatusDisable {
return nil, gerror.NewCode(consts.CodeUserDisabled, "账号已禁用")
}
if data.Status == consts.SysUserStatusLocked {
return nil, gerror.NewCode(consts.CodeUserLocked, "账号已锁定")
}
if data.Status == consts.SysUserStatusExpired {
return nil, gerror.NewCode(consts.CodeUserExpired, "账号已过期")
}
if data.Status == consts.SysUserStatusDeleted {
return nil, gerror.NewCode(consts.CodeUserDeleted, "账号已删除")
}
return data, nil
}
// 根据邮箱获取用户信息,不验证当前租户
func (l *sSysUser) GetUserByEmail(ctx context.Context, email string, tenantId string) (user *entity.SysUser, err error) {
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().Email, email).Where(dao.SysUser.Columns().TenantId, tenantId).Scan(&user)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
}
return nil, err
}
if user == nil {
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
}
if user.Status == consts.SysUserStatusDisable {
return nil, gerror.NewCode(consts.CodeUserDisabled, "账号已禁用")
}
if user.Status == consts.SysUserStatusLocked {
return nil, gerror.NewCode(consts.CodeUserLocked, "账号已锁定")
}
if user.Status == consts.SysUserStatusExpired {
return nil, gerror.NewCode(consts.CodeUserExpired, "账号已过期")
}
if user.Status == consts.SysUserStatusDeleted {
return nil, gerror.NewCode(consts.CodeUserDeleted, "账号已删除")
}
return user, nil
}
// 根据手机号获取用户信息,不验证当前租户
func (l *sSysUser) GetUserByPhone(ctx context.Context, phone string, tenantId string) (user *entity.SysUser, err error) {
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().Phonenumber, phone).Where(dao.SysUser.Columns().TenantId, tenantId).Scan(&user)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
}
return nil, err
}
if user == nil {
return nil, gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
}
if user.Status == consts.SysUserStatusDisable {
return nil, gerror.NewCode(consts.CodeUserDisabled, "账号已禁用")
}
if user.Status == consts.SysUserStatusLocked {
return nil, gerror.NewCode(consts.CodeUserLocked, "账号已锁定")
}
if user.Status == consts.SysUserStatusExpired {
return nil, gerror.NewCode(consts.CodeUserExpired, "账号已过期")
}
return
}
// 根据用户名和密码获取用户信息
func (l *sSysUser) GetUserByUsernameAndPassword(ctx context.Context, tenantId string, username string, password string) (user *entity.SysUser, err error) {
// 判断用户名是否存在
user, err = l.GetUserByUsername(ctx, username, tenantId)
if err != nil {
return nil, err
}
// 判断密码是否正确
password = utility.PasswordEncrypt(password, user.Salt)
if password != user.Password {
g.Log().Errorf(ctx, "密码错误: username:%s, password:%s, user:%+v", username, password, user)
return nil, gerror.NewCode(consts.CodeUserPasswordError, "密码错误")
}
return user, nil
}
// 根据用户ID获取用户信息
func (l *sSysUser) GetUserById(ctx context.Context, id int64) (user *model.SysUserViewModel, err error) {
err = l.Model(ctx).WithAll().Where(dao.SysUser.Columns().UserId, id).Scan(&user)
if err != nil {
return nil, err
}
// 获取用户角色
userRoles := make([]*entity.SysUserRole, 0)
err = dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().UserId, id).Scan(&userRoles)
if err != nil {
return nil, err
}
roleIds := make([]int64, 0)
for _, userRole := range userRoles {
roleIds = append(roleIds, userRole.RoleId)
}
roles := make([]*entity.SysRole, 0)
err = dao.SysRole.Ctx(ctx).WhereIn(dao.SysRole.Columns().RoleId, roleIds).Scan(&roles)
if err != nil {
return nil, err
}
user.Roles = make([]*model.SysRoleMiniModel, 0)
for _, role := range roles {
mini := &model.SysRoleMiniModel{}
gconv.Struct(role, mini)
user.Roles = append(user.Roles, mini)
}
// 获取用户岗位
userPosts := make([]*entity.SysUserPost, 0)
err = dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().UserId, id).Scan(&userPosts)
if err != nil {
return nil, err
}
postIds := make([]int64, 0)
for _, userPost := range userPosts {
postIds = append(postIds, userPost.PostId)
}
posts := make([]*entity.SysPost, 0)
err = dao.SysPost.Ctx(ctx).WhereIn(dao.SysPost.Columns().PostId, postIds).Scan(&posts)
if err != nil {
return nil, err
}
user.Posts = make([]*model.SysPostMiniModel, 0)
for _, post := range posts {
mini := &model.SysPostMiniModel{}
gconv.Struct(post, mini)
user.Posts = append(user.Posts, mini)
}
return
}
// 获取用户列表
func (l *sSysUser) List(ctx context.Context, page *request.PageInfo, query *model.UserListParam) (items []*model.SysUserListModel, total int, err error) {
deptIds := make([]int64, 0)
if query.DeptId != 0 {
deptIds = append(deptIds, query.DeptId)
subDeptIds, err := service.SysDept().GetDeptIdsByParentId(ctx, query.DeptId)
if err != nil {
return nil, 0, err
}
deptIds = append(deptIds, subDeptIds...)
}
g.Log().Infof(ctx, "sSysUser.List 获取部门列表: deptIds:%+v", deptIds)
// 实现分页查询逻辑
var users []*model.SysUserListModel
m := l.Model(ctx)
if query.UserId != 0 {
m = m.Where(dao.SysUser.Columns().UserId, query.UserId)
}
if len(deptIds) > 0 {
m = m.WhereIn(dao.SysUser.Columns().DeptId, deptIds)
}
if query.UserName != "" {
m = m.WhereLike(dao.SysUser.Columns().UserName, "%"+query.UserName+"%")
}
if query.NickName != "" {
m = m.WhereLike(dao.SysUser.Columns().NickName, "%"+query.NickName+"%")
}
if query.Email != "" {
m = m.WhereLike(dao.SysUser.Columns().Email, "%"+query.Email+"%")
}
if query.Phonenumber != "" {
m = m.WhereLike(dao.SysUser.Columns().Phonenumber, "%"+query.Phonenumber+"%")
}
if query.Status != "" {
m = m.Where(dao.SysUser.Columns().Status, query.Status)
}
if len(query.CreatedAt) == 2 {
begin, end := gtime.NewFromStr(query.CreatedAt[0]), gtime.NewFromStr(query.CreatedAt[1])
m = m.WhereBetween(dao.SysUser.Columns().CreatedAt, begin, end.EndOfDay())
}
if len(query.DeptIdList) > 0 {
m = m.WhereIn(dao.SysUser.Columns().DeptId, query.DeptIdList)
}
total, err = m.Count()
if err != nil {
return nil, 0, err
}
err = m.WithAll().Page(page.Page, page.PageSize).Scan(&users)
if err != nil {
return nil, 0, err
}
// g.Log().Infof(ctx, "获取用户列表成功: total:%d, users:%+v", total, users)
return users, total, nil
}
// 新增用户
func (l *sSysUser) AddUser(ctx context.Context, req *model.SysUserAddModel) (data *model.SysUserViewModel, err error) {
// 获取当前登录用户的租户
if req.TenantId == "" {
req.TenantId = contexts.GetTenantId(ctx)
}
// 判断部门是否存在
dept, err := service.SysDept().GetDeptById(ctx, req.DeptId)
if err != nil {
return nil, err
}
if dept == nil || dept.TenantId != req.TenantId {
return nil, gerror.NewCode(consts.CodeDeptNotFound, "部门不存在")
}
// 判断用户名是否存在(在当前租户下)
var user *entity.SysUser
err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserName, req.UserName).Where(dao.SysUser.Columns().TenantId, req.TenantId).Scan(&user)
if err != nil {
if err != gorm.ErrRecordNotFound {
return nil, err
}
}
if user != nil {
return nil, gerror.NewCode(consts.CodeUserExists, "用户名已存在")
}
// // 判断邮箱是否存在
// user, err = l.GetUserByEmail(ctx, req.Email)
// if err != nil {
// return nil, err
// }
// if user != nil {
// return nil, gerror.NewCode(consts.CodeEmailExists, "邮箱已存在")
// }
// // 判断手机号是否存在
// user, err = l.GetUserByPhone(ctx, req.Phone)
// if err != nil {
// return nil, err
// }
// if user != nil {
// return nil, gerror.NewCode(consts.CodePhoneExists, "手机号已存在")
// }
// 随机生成5位字符
salt := utility.RandomString(5)
password := utility.PasswordEncrypt(req.Password, salt)
dataInsert := do.SysUser{}
gconv.Struct(req, &dataInsert)
dataInsert.Password = password
dataInsert.Salt = salt
dataInsert.CreatedDept = contexts.GetDeptId(ctx)
dataInsert.CreatedBy = contexts.GetUserId(ctx)
dataInsert.CreatedAt = gtime.Now()
dataInsert.UpdatedBy = contexts.GetUserId(ctx)
dataInsert.UpdatedAt = gtime.Now()
dataInsert.TenantId = req.TenantId
result, err := dao.SysUser.Ctx(ctx).Data(dataInsert).Insert()
if err != nil {
return nil, err
}
userId, err := result.LastInsertId()
if err != nil {
return nil, err
}
data, err = l.GetUserById(ctx, userId)
if err != nil {
return nil, err
}
// 新增用户角色
d := make([]*do.SysUserRole, 0, len(req.RoleIds))
if len(req.RoleIds) > 0 {
for _, roleId := range req.RoleIds {
d = append(d, &do.SysUserRole{
UserId: userId,
RoleId: roleId,
})
}
_, err = dao.SysUserRole.Ctx(ctx).Data(d).Insert()
if err != nil {
return nil, err
}
}
// 新增用户岗位
if len(req.PostIds) > 0 {
postData := make([]*do.SysUserPost, 0, len(req.PostIds))
for _, postId := range req.PostIds {
postData = append(postData, &do.SysUserPost{
UserId: userId,
PostId: postId,
})
}
_, err = dao.SysUserPost.Ctx(ctx).Data(postData).Insert()
if err != nil {
return nil, err
}
}
return data, nil
}
func (l *sSysUser) Profile(ctx context.Context) (user *model.UserProfileModel, err error) {
u, err := l.GetUserById(ctx, contexts.GetUserId(ctx))
if err != nil {
return nil, err
}
companyInfo := service.SysDept().GetParentCompanyInfo(ctx, u.DeptId)
return &model.UserProfileModel{
SysUserViewModel: *u,
CompanyInfo: &model.SysDeptMiniModel{
DeptId: companyInfo.DeptId,
DeptName: companyInfo.DeptName,
ParentId: companyInfo.ParentId,
},
}, nil
}
func (l *sSysUser) UpdateCurrentUser(ctx context.Context, req *model.UpdateCurrentUserModel) (user *model.SysUserViewModel, err error) {
userId := contexts.GetUserId(ctx)
data := do.SysUser{}
gconv.Struct(req, &data)
data.UserId = nil
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(data).OmitEmpty().Update()
if err != nil {
return nil, err
}
user, err = l.GetUserById(ctx, userId)
if err != nil {
return nil, err
}
event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, userId)
return user, nil
}
func (l *sSysUser) UpdateCurrentUserPassword(ctx context.Context, req *model.UpdateCurrentUserPasswordModel) (err error) {
userId := contexts.GetUserId(ctx)
var user *entity.SysUser
err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Scan(&user)
if err != nil {
return err
}
if user == nil {
return gerror.NewCode(consts.CodeUserNotFound, "用户不存在")
}
oldPassword := utility.PasswordEncrypt(req.OldPassword, user.Salt)
if oldPassword != user.Password {
return gerror.NewCode(consts.CodeUserPasswordError, "旧密码错误")
}
salt := utility.RandomString(5)
password := utility.PasswordEncrypt(req.NewPassword, salt)
data := do.SysUser{
Password: password,
Salt: salt,
UpdatedBy: userId,
UpdatedAt: gtime.Now(),
}
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(data).OmitEmpty().Update()
if err != nil {
return err
}
return nil
}
func (l *sSysUser) UpdateUser(ctx context.Context, req *model.SysUserUpdateModel) (err error) {
currUserId := contexts.GetUserId(ctx)
req.UpdatedAt = gtime.Now()
req.UpdatedBy = &currUserId
data := do.SysUser{}
gconv.Struct(req, &data)
data.UserId = nil
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, req.UserId).Data(data).OmitNil().Update()
if err != nil {
return err
}
// 用户角色
urList := make([]*entity.SysUserRole, 0)
err = dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().UserId, req.UserId).Scan(&urList)
if err != nil {
return err
}
// 计算删除和新增的角色
delRoleIds := make([]int64, 0)
addRoleIds := make([]int64, 0)
hasRoleId := false
for _, ur := range urList {
hasRoleId = false
for _, roleId := range req.RoleIds {
if ur.RoleId == roleId {
hasRoleId = true
break
}
}
if !hasRoleId {
delRoleIds = append(delRoleIds, ur.RoleId)
}
}
for _, roleId := range req.RoleIds {
hasRoleId = false
for _, ur := range urList {
if ur.RoleId == roleId {
hasRoleId = true
break
}
}
if !hasRoleId {
addRoleIds = append(addRoleIds, roleId)
}
}
// 删除多余的角色
if len(delRoleIds) > 0 {
_, err = dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().UserId, req.UserId).WhereIn(dao.SysUserRole.Columns().RoleId, delRoleIds).Delete()
if err != nil {
return err
}
}
// 新增角色
d := make([]*do.SysUserRole, 0, len(addRoleIds))
if len(addRoleIds) > 0 {
for _, roleId := range addRoleIds {
d = append(d, &do.SysUserRole{
UserId: req.UserId,
RoleId: roleId,
})
}
_, err = dao.SysUserRole.Ctx(ctx).Data(d).Insert()
if err != nil {
return err
}
}
// 查询用户岗位
upList := make([]*entity.SysUserPost, 0)
err = dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().UserId, req.UserId).Scan(&upList)
if err != nil {
return err
}
// 计算删除和新增的岗位
delPostIds := make([]int64, 0)
addPostIds := make([]int64, 0)
hasPostId := false
for _, up := range upList {
hasPostId = false
for _, postId := range req.PostIds {
if up.PostId == postId {
hasPostId = true
break
}
}
if !hasPostId {
delPostIds = append(delPostIds, up.PostId)
}
}
for _, postId := range req.PostIds {
hasPostId = false
for _, up := range upList {
if up.PostId == postId {
hasPostId = true
break
}
}
if !hasPostId {
addPostIds = append(addPostIds, postId)
}
}
// 删除多余岗位
if len(delPostIds) > 0 {
_, err = dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().UserId, req.UserId).WhereIn(dao.SysUserPost.Columns().PostId, delPostIds).Delete()
if err != nil {
return err
}
}
// 新增岗位
postData := make([]*do.SysUserPost, 0, len(addPostIds))
if len(addPostIds) > 0 {
for _, postId := range addPostIds {
postData = append(postData, &do.SysUserPost{
UserId: req.UserId,
PostId: postId,
})
}
_, err = dao.SysUserPost.Ctx(ctx).Data(postData).Insert()
if err != nil {
return err
}
}
event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, req.UserId)
return nil
}
func (l *sSysUser) DeleteUser(ctx context.Context, userIds []int64) (err error) {
if len(userIds) == 0 {
g.Log().Warningf(ctx, "sSysUser.DeleteUser 用户ID列表不能为空. userIds: %v", userIds)
return errors.New("用户ID列表不能为空")
}
currUserId := contexts.GetUserId(ctx)
if slices.Contains(userIds, 1) {
g.Log().Warningf(ctx, "sSysUser.DeleteUser 不能删除超级管理员. userIds: %v", userIds)
return errors.New("不能删除超级管理员")
}
if slices.Contains(userIds, currUserId) {
g.Log().Warningf(ctx, "sSysUser.DeleteUser 不能删除当前用户. userIds: %v, currUserId: %d", userIds, currUserId)
return errors.New("不能删除当前用户")
}
isAdminUserIds, err := service.SysTenant().IsTenantAdmin(ctx, userIds)
if err != nil {
g.Log().Errorf(ctx, "sSysUser.DeleteUser IsTenantAdmin err: %v, userIds: %v", err, userIds)
return err
}
if len(isAdminUserIds) > 0 {
g.Log().Warningf(ctx, "sSysUser.DeleteUser 不能删除租户管理员. isAdminUserIds: %v, userIds: %v", isAdminUserIds, userIds)
return errors.New(fmt.Sprintf("不能删除租户管理员ID: %v", isAdminUserIds))
}
data := do.SysUser{
DeletedAt: gtime.Now(),
DeletedBy: currUserId,
}
_, err = l.Model(ctx).WhereIn(dao.SysUser.Columns().UserId, userIds).Data(data).OmitEmpty().Update()
if err != nil {
g.Log().Errorf(ctx, "sSysUser.DeleteUser Update err: %v, userIds: %v", err, userIds)
return err
}
// 删除用户绑定关系
_, err = dao.SysSocial.Ctx(ctx).WhereIn(dao.SysSocial.Columns().UserId, userIds).Delete()
if err != nil {
g.Log().Errorf(ctx, "sSysUser.DeleteUser SysSocial.Ctx(ctx).Delete err: %v, userIds: %v", err, userIds)
return err
}
event.EventsInstance().Emit(ctx, consts.EventKeyUserDelete, userIds)
return nil
}
func (l *sSysUser) ResetPassword(ctx context.Context, userId int64, password string) (err error) {
currUserId := contexts.GetUserId(ctx)
salt := utility.RandomString(5)
password = utility.PasswordEncrypt(password, salt)
data := do.SysUser{
Password: password,
Salt: salt,
UpdatedBy: currUserId,
UpdatedAt: gtime.Now(),
}
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(data).OmitEmpty().Update()
if err != nil {
return err
}
event.EventsInstance().Emit(ctx, consts.EventKeyUserUpdate, userId)
return nil
}
func (l *sSysUser) UpdateLoginInfo(ctx context.Context, userId int64, loginIp string) (err error) {
data := do.SysUser{
LoginIp: loginIp,
LoginDate: gtime.Now(),
}
_, err = dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserId, userId).Data(data).Update()
if err != nil {
g.Log().Errorf(ctx, "sSysUser.UpdateLoginInfo err: %v, userId: %d, loginIp: %s", err, userId, loginIp)
return err
}
return nil
}
// 获取用户角色ID列表
func (l *sSysUser) GetUserRoleIds(ctx context.Context, userId int64) (roleIds []int64, err error) {
urList := make([]*entity.SysUserRole, 0)
err = dao.SysUserRole.Ctx(ctx).Where(dao.SysUserRole.Columns().UserId, userId).Scan(&urList)
if err != nil {
return nil, err
}
for _, ur := range urList {
roleIds = append(roleIds, ur.RoleId)
}
return roleIds, nil
}
// 获取用户岗位ID列表
func (l *sSysUser) GetUserPostIds(ctx context.Context, userId int64) (postIds []int64, err error) {
upList := make([]*entity.SysUserPost, 0)
err = dao.SysUserPost.Ctx(ctx).Where(dao.SysUserPost.Columns().UserId, userId).Scan(&upList)
if err != nil {
return nil, err
}
for _, up := range upList {
postIds = append(postIds, up.PostId)
}
return postIds, nil
}
func (l *sSysUser) Register(ctx context.Context, param *model.SysUserRegisterModel) (err error) {
// 验证验证码
err = service.SysCaptcha().VerifyCaptcha(ctx, param.CaptchaID, param.CaptchaValue)
if err != nil {
return err
}
// 检查是否开启用户注册功能
registerConfig := &model.SysConfigViewModel{}
mod := dao.SysConfig.Ctx(ctx)
mod = mod.Where(dao.SysConfig.Columns().ConfigKey, "sys.account.registerUser")
mod = mod.Where(dao.SysConfig.Columns().TenantId, param.TenantId)
err = mod.Scan(&registerConfig)
if err != nil {
return gerror.New("请联系管理员开启用户注册功能")
}
if registerConfig == nil || registerConfig.ConfigValue != "true" {
return gerror.New("请联系管理员开启用户注册功能")
}
// 获取租户信息
tenant, err := service.SysTenant().View(ctx, &model.SysTenantViewParam{
TenantId: param.TenantId,
})
if err != nil {
return err
}
if tenant == nil {
return gerror.New("租户无效")
}
// 获取租户的默认注册组织ID
config := &model.SysConfigViewModel{}
mod = dao.SysConfig.Ctx(ctx)
mod = mod.Where(dao.SysConfig.Columns().ConfigKey, consts.ConfigKeyTenantDefaultRegisterDeptId)
mod = mod.Where(dao.SysConfig.Columns().TenantId, param.TenantId)
err = mod.Scan(&config)
if err != nil {
return gerror.New("该租户不允许自主注册")
}
if config == nil || config.ConfigValue == "" {
return gerror.New("该租户不允许自主注册")
}
// 验证用户名是否存在
mod1 := dao.SysUser.Ctx(ctx)
mod1 = mod1.Where(dao.SysUser.Columns().UserName, param.UserName)
mod1 = mod1.Where(dao.SysUser.Columns().TenantId, param.TenantId)
total, err := mod1.Count()
if err != nil {
return err
}
if total > 0 {
return gerror.New("用户名已存在")
}
// 随机生成5位字符
salt := utility.RandomString(5)
password := utility.PasswordEncrypt(param.Password, salt)
newUser := do.SysUser{
UserName: param.UserName,
NickName: "",
Password: password,
Salt: salt,
TenantId: param.TenantId,
DeptId: gconv.Int64(config.ConfigValue),
Status: consts.SysDeptStatusNormal,
CreatedBy: 0,
CreatedAt: gtime.Now(),
UpdatedBy: 0,
UpdatedAt: gtime.Now(),
}
userId, err := l.Model(ctx).Data(newUser).InsertAndGetId()
if err != nil {
return err
}
//配置角色
// 新增用户角色
mod = dao.SysConfig.Ctx(ctx)
mod = mod.Where(dao.SysConfig.Columns().ConfigKey, consts.ConfigKeyTenantDefaultRegisterRoleId)
mod = mod.Where(dao.SysConfig.Columns().TenantId, param.TenantId)
err = mod.Scan(&config)
if err != nil {
g.Log().Error(ctx, "获取租户的默认角色失败", err)
}
if config != nil && config.ConfigValue != "" {
roleId := gconv.Int64(config.ConfigValue)
data := do.SysUserRole{
UserId: userId,
RoleId: roleId,
}
_, err = dao.SysUserRole.Ctx(ctx).Data(data).Insert()
if err != nil {
g.Log().Error(ctx, "获取租户的默认角色失败", err)
}
}
return nil
}
// Status 更新用户状态
func (l *sSysUser) Status(ctx context.Context, param *model.SysUserStatusParam) (err error) {
data := do.SysUser{
Status: param.Status,
}
_, err = l.Model(ctx).Where(dao.SysUser.Columns().UserId, param.UserId).Data(data).OmitEmpty().Update()
if err != nil {
return err
}
return nil
}
// 批量查询用户迷你信息
func (l *sSysUser) BatchGetUserMiniInfo(ctx context.Context, userIds []int64) (users []*model.SysUserMiniModel, err error) {
err = dao.SysUser.Ctx(ctx).WhereIn(dao.SysUser.Columns().UserId, userIds).Scan(&users)
if err != nil {
return nil, err
}
return users, nil
}
@@ -0,0 +1,113 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
)
type sSysUserOnline struct {
}
func NewSysUserOnline() *sSysUserOnline {
return &sSysUserOnline{}
}
func init() {
service.RegisterSysUserOnline(NewSysUserOnline())
}
func (s *sSysUserOnline) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
option = append(option, &handler.Option{
FilterTenant: true,
FilterAuth: true,
})
}
return handler.Model(dao.SysUserOnline.Ctx(ctx), option...)
}
func (s *sSysUserOnline) Add(ctx context.Context, userOnline *model.SysUserOnlineAddModel) (err error) {
data := do.SysUserOnline{}
gconv.Struct(userOnline, &data)
_, err = s.Model(ctx).Data(data).OmitEmpty().Save()
return
}
func (s *sSysUserOnline) List(ctx context.Context, query *model.SysUserOnlineListParam, page *request.PageInfo) (items []*model.SysUserOnlineListModel, total int, err error) {
tenantId := contexts.GetTenantId(ctx)
db := s.Model(ctx).Where(dao.SysUserOnline.Columns().TenantId, tenantId)
if query.UserName != "" {
db = db.Where(dao.SysUserOnline.Columns().UserName, query.UserName)
}
if query.ClientKey != "" {
db = db.WhereLike(dao.SysUserOnline.Columns().ClientKey, "%"+query.ClientKey+"%")
}
if query.DeviceType != "" {
db = db.Where(dao.SysUserOnline.Columns().DeviceType, query.DeviceType)
}
if query.Ipaddr != "" {
db = db.Where(dao.SysUserOnline.Columns().Ipaddr, query.Ipaddr)
}
if len(query.LoginTime) == 2 {
startTime := gtime.NewFromStr(query.LoginTime[0])
endTime := gtime.NewFromStr(query.LoginTime[1])
db = db.WhereBetween(dao.SysUserOnline.Columns().LoginTime, startTime, endTime.EndOfDay())
}
if query.Token != "" {
db = db.Where(dao.SysUserOnline.Columns().Token, query.Token)
}
total, err = db.Count()
if err != nil {
return nil, 0, err
}
err = db.Page(page.Page, page.PageSize).Order(dao.SysUserOnline.Columns().LoginTime, "desc").Scan(&items)
return
}
func (s *sSysUserOnline) Delete(ctx context.Context, ids []int64) (err error) {
if len(ids) == 0 {
return errors.New("ids不能为空")
}
var data = make([]*model.SysUserOnlineViewModel, 0)
err = s.Model(ctx).WhereIn(dao.SysUserOnline.Columns().OnlineId, ids).Scan(&data)
if err != nil {
return err
}
for _, v := range data {
service.SysAuth().DeleteToken(ctx, v.Token)
}
_, err = s.Model(ctx).WhereIn(dao.SysUserOnline.Columns().OnlineId, ids).Delete()
if err != nil {
return err
}
return
}
func (s *sSysUserOnline) DeleteByToken(ctx context.Context, token string) (err error) {
var data *model.SysUserOnlineViewModel
err = s.Model(ctx).Where(dao.SysUserOnline.Columns().Token, token).Scan(&data)
if err != nil {
return err
}
if data != nil && data.Token != "" {
service.SysAuth().DeleteToken(ctx, data.Token)
}
_, err = s.Model(ctx).Where(dao.SysUserOnline.Columns().Token, token).Delete()
return
}
@@ -0,0 +1,82 @@
// package system_test
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@example.com>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system_test
import (
"testing"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
func TestSysUser_UpdateUserPassword(t *testing.T) {
// 设置上下文用户身份为用户
ctx := contexts.Set(gctx.New(), &model.Context{
// 为了测试只设置了hook中需要用到的数据
User: &model.Identity{
BaseClaims: model.BaseClaims{
ID: 1,
TenantId: "000000",
DeptId: 1,
},
},
})
previous := gdb.GetConfig("default")
if err := gdb.SetConfigGroup("default", gdb.ConfigGroup{{Type: "sqlite", Link: "sqlite::@file(:memory:)", MaxOpenConnCount: 1}}); err != nil {
t.Fatal(err)
}
database := g.DB()
t.Cleanup(func() {
if err := database.GetCore().ClearCacheAll(ctx); err != nil {
t.Error(err)
}
if err := database.Close(ctx); err != nil {
t.Error(err)
}
if err := gdb.SetConfigGroup("default", previous); err != nil {
t.Error(err)
}
})
if err := database.GetCore().ClearCacheAll(ctx); err != nil {
t.Fatal(err)
}
for _, statement := range []string{
"CREATE TABLE sys_user (user_id INTEGER PRIMARY KEY, password TEXT, salt TEXT, updated_by INTEGER, updated_at DATETIME, tenant_id TEXT)",
"INSERT INTO sys_user VALUES (1,'old','old',0,NULL,'000000'),(2,'unchanged','unchanged',0,NULL,'000000')",
} {
if _, err := database.Exec(ctx, statement); err != nil {
t.Fatal(err)
}
}
newPassword := "123456"
salt := "123456"
password := utility.PasswordEncrypt(utility.Md5(newPassword), salt)
data := do.SysUser{
Password: password,
Salt: salt,
UpdatedBy: 1,
}
_, err := dao.SysUser.Ctx(ctx).Where(dao.SysUser.Columns().UserId, 1).Data(data).OmitEmpty().Update()
if err != nil {
t.Fatal(err)
}
actual, err := database.GetValue(ctx, "SELECT password FROM sys_user WHERE user_id = 1")
if err != nil || actual.String() != password {
t.Fatalf("password=%v, error=%v", actual, err)
}
unchanged, err := database.GetValue(ctx, "SELECT password FROM sys_user WHERE user_id = 2")
if err != nil || unchanged.String() != "unchanged" {
t.Fatalf("unrelated user changed: password=%v, error=%v", unchanged, err)
}
}
@@ -0,0 +1,169 @@
// // package system_test
// // @Link https://github.com/xiujiecn/xiu-admin
// // @Copyright Copyright (c) 2025 LiXiujie
// // @Author Lxj <li@example.com>
// // @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system_test
// import (
// "context"
// "encoding/json"
// "fmt"
// "testing"
// "time"
// "git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
// "git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
// "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/worker"
// "git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
// "git.5iot.com/iotlcm-public/iotlcm-app/internal/tasks"
// _ "github.com/gogf/gf/contrib/drivers/mysql/v2"
// "github.com/gogf/gf/v2/frame/g"
// "github.com/gogf/gf/v2/os/gctx"
// "github.com/gogf/gf/v2/util/gconv"
// )
// func init() {
// // 数据库只读,不执行
// // g.DB().SetDryRun(true)
// g.DB().SetDebug(true)
// testing.Init()
// }
// func TestTask_runOnce(t *testing.T) {
// // 设置上下文用户身份为用户
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
// // 为了测试只设置了hook中需要用到的数据
// User: &model.Identity{
// BaseClaims: model.BaseClaims{
// ID: 1,
// TenantId: "000000",
// DeptId: 1,
// },
// },
// })
// exists := &model.SysJobViewModel{}
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
// paramArr, _ := tasks.TasksInstance().ParseParameters(exists.JobParams)
// taskData := tasks.Task{
// ID: fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId),
// TaskType: "Type-" + gconv.String(exists.MisfirePolicy),
// MethodName: exists.InvokeTarget,
// Params: paramArr,
// Explain: exists.JobName,
// }
// runPayload, _ := json.Marshal(taskData)
// tasks.TasksInstance().Once(
// worker.WithTaskCtx(context.Background()),
// worker.WithTaskUid(taskData.ID), // 任务ID
// worker.WithTaskGroup(taskData.MethodName), // 任务组
// worker.WithTaskTimeout(10),
// worker.WithTaskNow(true),
// worker.WithTaskReplace(true),
// worker.WithTaskPayload(runPayload),
// )
// }
// func TestTask_schedule(t *testing.T) {
// // 设置上下文用户身份为用户
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
// // 为了测试只设置了hook中需要用到的数据
// User: &model.Identity{
// BaseClaims: model.BaseClaims{
// ID: 1,
// TenantId: "000000",
// DeptId: 1,
// },
// },
// })
// exists := &model.SysJobViewModel{}
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
// paramArr, _ := tasks.TasksInstance().ParseParameters(exists.JobParams)
// //获取task目录下是否绑定对应的方法
// actualName, exist := tasks.TasksInstance().CheckFuncName(exists.InvokeTarget)
// if !exist {
// errInfo := fmt.Sprintf("没有绑定对应的方法:%s", exists.InvokeTarget)
// g.Log().Error(ctx, errInfo)
// }
// //传参解析
// paramArr, err := tasks.TasksInstance().ParseParameters(exists.JobParams)
// if err != nil {
// g.Log().Error(ctx, err)
// }
// taskData := tasks.Task{
// ID: fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId),
// TaskType: "Type-" + gconv.String(exists.MisfirePolicy),
// MethodName: actualName,
// Params: paramArr,
// Explain: exists.JobName,
// }
// runPayload, _ := json.Marshal(taskData)
// _, err2 := tasks.TasksInstance().Cron(
// 1,
// worker.WithTaskCtx(context.Background()),
// worker.WithTaskUid(taskData.ID), // 任务ID
// worker.WithTaskGroup(taskData.MethodName), // 任务组
// worker.WithTaskExpr("0/2 * * * * *"),
// //worker.WithTaskExpr("@every 2s"),
// worker.WithTaskTimeout(10),
// worker.WithTaskReplace(true),
// worker.WithTaskPayload(runPayload),
// )
// if err2 != nil {
// g.Log().Debug(ctx, taskData.MethodName, taskData.Explain, "启动任务失败")
// }
// time.Sleep(10 * time.Minute)
// }
// func TestTask_remove(t *testing.T) {
// // 设置上下文用户身份为用户
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
// // 为了测试只设置了hook中需要用到的数据
// User: &model.Identity{
// BaseClaims: model.BaseClaims{
// ID: 1,
// TenantId: "000000",
// DeptId: 1,
// },
// },
// })
// exists := &model.SysJobViewModel{}
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
// taskJobId := fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId)
// g.Log().Debug(ctx, taskJobId)
// err := tasks.TasksInstance().Remove(taskJobId)
// if err != nil {
// g.Log().Debug(ctx, exists.JobName, "启动停止失败")
// g.Log().Error(ctx, err)
// }
// }
// func TestTask_getTask(t *testing.T) {
// ctx := context.WithValue(gctx.New(), consts.ContextKey, &model.Context{
// // 为了测试只设置了hook中需要用到的数据
// User: &model.Identity{
// BaseClaims: model.BaseClaims{
// ID: 1,
// TenantId: "000000",
// DeptId: 1,
// },
// },
// })
// exists := &model.SysJobViewModel{}
// dao.SysJob.Ctx(ctx).Where(dao.SysJob.Columns().JobId, 4).Scan(&exists)
// taskJobId := fmt.Sprintf("%s-job-%d", exists.InvokeTarget, exists.JobId)
// task, err := tasks.TasksInstance().GetTask(taskJobId)
// if err != nil {
// g.Log().Debug(ctx, exists.JobName, "获取任务失败")
// g.Log().Error(ctx, err)
// }
// fmt.Println("task", task)
// }