chore: 初始开源发布

iotlcm-admin-svr:管理后台服务。

- Go module:iotlcm → git.5iot.com/iotlcm-public/iotlcm-admin-svr(729 文件)
- 凭据与地址脱敏:JWT signingKey(含代码里的 MustGet 默认值)、
  MySQL DSN、Redis 口令、告警邮件发件人邮箱全部改为占位;
  私网 IP → 文档地址段;库名 xiujie_admin → iotlcm
- 真实手机号/邮箱 → 合法 11 位编造号 / example.com
- 移除内部资产:CI 触发器、过时 DDL(sql/db_iotlcm.sql)、
  私有依赖 go get 行、明文 registry 登录口令
- 启动 banner 统一 IOTLCM 方块字 + 社区惯例署名
- Dockerfile 改用公共基础镜像
- 拼错的 RADEME.md → README.md,README 重写
- 需要真实数据库的用例改为可用 IOTLCM_TEST_WITH_DB=1 显式启用
- 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes

数据库 schema 不随本仓发布(生产 DDL 已过时),README 已说明。
This commit is contained in:
MiFaZhan
2026-10-23 14:46:05 +08:00
commit 59eb34976a
1132 changed files with 138197 additions and 0 deletions
+731
View File
@@ -0,0 +1,731 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/bcache"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/logic/smsgateway"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility"
"slices"
"strings"
"time"
"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 {
}
// 手机验证码登录:验证码 Redis 键与有效期。
const (
smsLoginCodeKeyPrefix = "auth:sms:login:code:"
smsLoginLimitKeyPrefix = "auth:sms:login:limit:"
smsLoginAttemptKeyPrefix = "auth:sms:login:attempt:"
smsLoginCodeTTLSeconds = 300 // 验证码有效期 5 分钟
smsLoginLimitTTLSeconds = 60 // 发送间隔 60 秒
smsLoginMaxAttempts = 5 // 每个验证码最多尝试次数
)
// smsLoginConsumeScript 原子校验并删除验证码:值匹配返回 1,否则返回 0。
const smsLoginConsumeScript = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end`
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
}
func (s *sSysAuth) HasAccessCode(ctx context.Context, requiredCode string) (bool, error) {
requiredCode = strings.TrimSpace(requiredCode)
if requiredCode == "" {
return false, nil
}
userId := contexts.GetUserId(ctx)
if userId <= 0 {
return false, nil
}
if contexts.IsSuperAdmin(ctx) && contexts.GetTenantId(ctx) == consts.DefaultSystemTenantCode {
isAdmin, err := rolepolicy.CurrentSuperAdmin(ctx, userId)
if err != nil || isAdmin {
return isAdmin, err
}
}
accessCodeList, _, err := s.GetUserAccessCodeList(ctx, userId)
if err != nil {
return false, err
}
for _, userCode := range accessCodeList {
if sysAuthMatchAccessCode(strings.TrimSpace(userCode), requiredCode) {
return true, nil
}
}
return false, nil
}
func sysAuthMatchAccessCode(pattern, code string) bool {
if pattern == code {
return true
}
if strings.HasSuffix(pattern, "*") {
prefix := pattern[:len(pattern)-1]
return strings.HasPrefix(code, prefix)
}
return false
}
// 生成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", "IotLCM").String()
sk := g.Cfg().MustGet(ctx, "jwt.signingKey", "${JWT_SIGNING_KEY}").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 len(user.RoleIds) > 0 {
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{"IotLCM"}, // 受众
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", "${JWT_SIGNING_KEY}").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", "${JWT_SIGNING_KEY}").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)
template, templateErr := rolepolicy.Template(ctx)
if templateErr != nil {
return nil, nil, templateErr
}
managed := template != nil && template.TenantId == contexts.GetTenantId(ctx) && template.DeptId != contexts.GetDeptId(ctx)
// 角色权限码
roleIds, err := service.SysUser().GetUserRoleIds(ctx, userId)
if err != nil {
return nil, nil, err
}
var effectiveRoles []*entity.SysRole
if len(roleIds) > 0 {
if err := dao.SysRole.Ctx(ctx).Where("tenant_id", contexts.GetTenantId(ctx)).WhereIn("role_id", roleIds).Where("status", "0").Scan(&effectiveRoles); err != nil {
return nil, nil, err
}
}
roleIds = nil
for _, role := range effectiveRoles {
roleIds = append(roleIds, role.RoleId)
}
for _, role := range effectiveRoles {
accessCodeList = append(accessCodeList, consts.SysCheckPermissionRolePrefix+role.RoleKey)
roleDataScopeMap[role.RoleId] = role.DataScope
}
if user := contexts.GetUser(ctx); user != nil && user.ID == userId {
user.Roles = make([]model.Role, 0, len(effectiveRoles))
for _, role := range effectiveRoles {
scope := role.DataScope
if managed && (scope == consts.SysRoleDataScopeAll || scope == consts.SysRoleDataScopeCustom) {
scope = consts.SysRoleDataScopeDeptAndBelow
}
if scope == "" {
scope = consts.SysRoleDataScopePersonal
}
user.Roles = append(user.Roles, model.Role{RoleId: role.RoleId, DataScope: scope})
}
}
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList roleIds: %v", roleIds)
// 菜单权限码
// 获取角色菜单列表
rmList, err := service.SysRole().GetRoleListMenu(ctx, roleIds)
if err != nil {
return nil, nil, err
}
menuIds := make([]int64, 0)
for _, menu := range rmList {
menuIds = append(menuIds, menu.MenuId)
}
if !contexts.IsSuperAdmin(ctx) {
menuIds, err = rolepolicy.RestrictMenus(ctx, contexts.GetTenantId(ctx), contexts.GetDeptId(ctx), menuIds)
if err != nil {
return nil, nil, err
}
}
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList menuIds: %v", menuIds)
if len(menuIds) > 0 {
// 获取用户角色菜单
menuList, _, err := service.SysMenu().List(ctx, &model.SysMenuListParam{
MenuIds: menuIds,
})
if err != nil {
return nil, nil, err
}
g.Log().Infof(ctx, "sSysAuth.GetUserAccessCodeList menuList: %v", menuList)
for _, m := range menuList {
menuAccessCodes := normalizeMenuAccessCodes(m.Perms)
if len(menuAccessCodes) == 0 {
continue
}
accessCodeList = append(accessCodeList, menuAccessCodes...)
for _, r := range rmList {
if r.MenuId == m.MenuId {
roleDS := roleDataScopeMap[r.RoleId]
if r.DataScope != "" && r.DataScope != consts.SysRoleMenuDataScopeRole {
roleDS = r.DataScope
}
if managed && (roleDS == consts.SysRoleDataScopeAll || roleDS == consts.SysRoleDataScopeCustom) {
roleDS = consts.SysRoleDataScopeDeptAndBelow
}
if len(roleDS) < 1 {
roleDS = consts.SysRoleDataScopePersonal
}
for _, accessCode := range menuAccessCodes {
menuRoleAccessCodeList = append(menuRoleAccessCodeList, accessCode+"|"+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
}
// 根据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
}
// SendSmsLoginCode 发送手机验证码登录短信。
// 验证码 5 分钟有效,60 秒内不允许重复发送(SET NX EX 原子占用,防并发绕过)。
// 本接口未鉴权,网关不可用时必须 fail closed:不落库验证码、不打印验证码。
func (s *sSysAuth) SendSmsLoginCode(ctx context.Context, phone string) (err error) {
phone = strings.TrimSpace(phone)
if phone == "" {
return gerror.New("手机号不能为空")
}
// 原子占用发送资格:NX 抢占失败说明间隔内已发送过。
limitKey := smsLoginLimitKeyPrefix + phone
acquired, err := g.Redis().Do(ctx, "SET", limitKey, "1", "EX", smsLoginLimitTTLSeconds, "NX")
if err != nil {
return gerror.Wrap(err, "校验发送频率失败,请稍后重试")
}
if acquired.IsNil() {
return gerror.New("验证码发送过于频繁,请稍后再试")
}
code, err := GenerateMfaCode(ctx)
if err != nil {
return err
}
codeKey := smsLoginCodeKeyPrefix + phone
if _, err = g.Redis().Do(ctx, "SETEX", codeKey, smsLoginCodeTTLSeconds, code); err != nil {
return gerror.Wrap(err, "保存验证码失败,请稍后重试")
}
sent, err := smsgateway.Send(ctx, smsgateway.Request{
Scene: smsgateway.SceneLogin,
PhoneNumber: phone,
MsgContent: code,
})
if err != nil {
_, _ = g.Redis().Del(ctx, codeKey)
return gerror.Wrap(err, "短信发送失败,请稍后重试")
}
if !sent {
_, _ = g.Redis().Del(ctx, codeKey)
return gerror.New("短信网关未启用,无法发送验证码")
}
return nil
}
// smsLoginCheckUserStatus 登录前的账号状态校验,返回用户可读错误。
func smsLoginCheckUserStatus(user *entity.SysUser) error {
switch user.Status {
case consts.SysUserStatusDisable:
return gerror.NewCode(consts.CodeUserDisabled, "账号已禁用")
case consts.SysUserStatusLocked:
return gerror.NewCode(consts.CodeUserLocked, "账号已锁定")
case consts.SysUserStatusExpired:
return gerror.NewCode(consts.CodeUserExpired, "账号已过期")
case consts.SysUserStatusDeleted:
return gerror.NewCode(consts.CodeUserDeleted, "账号已删除")
}
return nil
}
// LoginByPhoneCode 手机验证码登录:校验验证码后按手机号(跨租户)定位账号并签发 token。
// 同一手机号命中多个账号时拒绝登录,引导使用账号密码登录(账号密码可结合租户选择)。
// 验证码通过 Lua compare-and-del 原子校验并消费,防并发重放;每手机号限 5 次尝试。
func (s *sSysAuth) LoginByPhoneCode(ctx context.Context, phone string, code string) (res *model.LoginUserOut, token string, err error) {
phone = strings.TrimSpace(phone)
code = strings.TrimSpace(code)
ip := g.RequestFromCtx(ctx).GetClientIp()
codeKey := smsLoginCodeKeyPrefix + phone
attemptKey := smsLoginAttemptKeyPrefix + phone
// 尝试次数限制:防暴力枚举;TTL 与验证码对齐,成功登录后清除。
attempts, err := g.Redis().Do(ctx, "INCR", attemptKey)
if err != nil {
return nil, "", gerror.Wrap(err, "校验登录尝试失败,请稍后重试")
}
if attempts.Int64() == 1 {
_, _ = g.Redis().Do(ctx, "EXPIRE", attemptKey, smsLoginCodeTTLSeconds)
}
if attempts.Int64() > smsLoginMaxAttempts {
return nil, "", gerror.New("尝试次数过多,请重新获取验证码")
}
// 原子校验并消费验证码(compare-and-delete),消费成功才继续登录。
consumed, err := g.Redis().Do(ctx, "EVAL", smsLoginConsumeScript, 1, codeKey, code)
if err != nil {
return nil, "", gerror.Wrap(err, "校验验证码失败,请稍后重试")
}
if consumed.Int64() != 1 {
return nil, "", gerror.New("验证码错误或已过期")
}
_, _ = g.Redis().Del(ctx, attemptKey)
users, err := service.SysUser().ListUsersByPhone(ctx, phone)
if err != nil {
return nil, "", err
}
if len(users) == 0 {
return nil, "", gerror.NewCode(consts.CodeUserNotFound, "账号不存在")
}
if len(users) > 1 {
return nil, "", gerror.New("该手机号绑定了多个账号,请使用账号密码登录")
}
user := users[0]
if err = smsLoginCheckUserStatus(user); err != nil {
return nil, "", err
}
logininfor := &model.SysLogininforAddModel{
TenantId: user.TenantId,
UserName: 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(),
}
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.LoginByPhoneCode 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@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"time"
"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
}
+138
View File
@@ -0,0 +1,138 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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)
_, err = db.Data(param).OmitNil().Where(dao.SysClient.Columns().Id, param.Id).Update()
if err != nil {
return
}
output = &model.SysClientStatusModel{
Id: param.Id,
}
return
}
+292
View File
@@ -0,0 +1,292 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolesync"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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 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("配置名称不能为空")
}
if param.ConfigKey == rolepolicy.TemplateConfigKey {
if !contexts.IsSuperAdmin(ctx) || contexts.GetTenantId(ctx) != consts.DefaultSystemTenantCode {
return nil, errors.New("仅平台超级管理员可配置代理模板")
}
if _, err := rolepolicy.ValidateTemplate(ctx, param.ConfigValue); err != nil {
return nil, err
}
count, err := dao.SysConfig.Ctx(ctx).Where("tenant_id", consts.DefaultSystemTenantCode).Where("config_key", rolepolicy.TemplateConfigKey).Count()
if err != nil {
return nil, err
}
if count > 0 {
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()
var lastInsertId int64
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var insertErr error
lastInsertId, insertErr = m.Ctx(ctx).Data(data).InsertAndGetId()
if insertErr != nil {
return insertErr
}
if param.ConfigKey == rolepolicy.TemplateConfigKey {
template, resolveErr := rolepolicy.Template(ctx)
if resolveErr != nil {
return resolveErr
}
_ = template
return nil
}
return nil
})
if err != nil {
return nil, err
}
if param.ConfigKey == rolepolicy.TemplateConfigKey {
template, resolveErr := rolepolicy.Template(ctx)
if resolveErr != nil {
return nil, resolveErr
}
if err = rolesync.EnqueueRole(ctx, template.RoleId, contexts.GetUserId(ctx)); 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不能为空")
}
var current *entity.SysConfig
if err := s.Model(ctx).Where("config_id", param.ConfigId).Scan(&current); err != nil {
return nil, err
}
if current == nil {
return nil, errors.New("参数不存在")
}
nextKey, nextValue := current.ConfigKey, current.ConfigValue
if param.ConfigKey != nil {
nextKey = *param.ConfigKey
}
if param.ConfigValue != nil {
nextValue = *param.ConfigValue
}
if current.ConfigKey == rolepolicy.TemplateConfigKey || nextKey == rolepolicy.TemplateConfigKey {
if !contexts.IsSuperAdmin(ctx) || current.TenantId != consts.DefaultSystemTenantCode {
return nil, errors.New("仅平台超级管理员可配置代理模板")
}
if current.ConfigKey != nextKey {
return nil, errors.New("代理模板参数键不允许更改,请修改角色ID")
}
nextTemplate, err := rolepolicy.ValidateTemplate(ctx, nextValue)
if err != nil {
return nil, err
}
previousTemplate, previousErr := rolepolicy.ValidateTemplate(ctx, current.ConfigValue)
if previousErr == nil && previousTemplate.DeptId != nextTemplate.DeptId {
return nil, errors.New("模板角色必须属于同一机构,跨机构切换需要迁移下级授权")
}
}
data := &do.SysConfig{}
gconv.Struct(param, data)
data.UpdatedBy = contexts.GetUserId(ctx)
data.UpdatedAt = gtime.Now()
data.ConfigId = nil
m := s.Model(ctx)
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if _, updateErr := m.Ctx(ctx).Data(data).Where(dao.SysConfig.Columns().ConfigId, param.ConfigId).Update(); updateErr != nil {
return updateErr
}
if nextKey == rolepolicy.TemplateConfigKey {
template, resolveErr := rolepolicy.Template(ctx)
if resolveErr != nil {
return resolveErr
}
return rolesync.EnqueueRole(ctx, template.RoleId, contexts.GetUserId(ctx))
}
return nil
})
if err != nil {
return nil, err
}
if nextKey == rolepolicy.TemplateConfigKey {
template, resolveErr := rolepolicy.Template(ctx)
if resolveErr != nil {
return nil, resolveErr
}
if err = rolesync.EnqueueRole(ctx, template.RoleId, contexts.GetUserId(ctx)); err != nil {
return nil, err
}
}
output = &model.SysConfigEditModel{
ConfigId: param.ConfigId,
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysConfigUpdate, param.ConfigId, contexts.GetTenantId(ctx), nextKey)
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("请选择要删除的配置")
}
count, checkErr := dao.SysConfig.Ctx(ctx).WhereIn("config_id", param.ConfigIds).Where("tenant_id", consts.DefaultSystemTenantCode).Where("config_key", rolepolicy.TemplateConfigKey).Count()
if checkErr != nil {
return nil, checkErr
}
if count > 0 {
return nil, errors.New("代理模板参数不能删除,请通过修改角色ID切换模板")
}
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
}
+681
View File
@@ -0,0 +1,681 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"fmt"
"sort"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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...)
}
// GetDeptList 获取部门列表
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
}
// GetDeptById 获取部门详情
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, "登录已过期")
}
mod := l.ModelQuery(ctx).Where(dao.SysDept.Columns().DeptId, id)
mod = mod.FieldsPrefix(dao.SysDept.Table(), model.SysDeptViewModel{})
err = mod.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,
DeptType: dept.DeptType,
}
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
}
// GetDeptFullTree 组织管理页专用的完整部门树。
//
// 与 GetDeptTree 的区别:
// 1. 支持按组织名称模糊搜索、按状态/类型筛选(复用 GetDeptList 的过滤能力),
// 而 GetDeptTree 只支持 deptType 一个维度;
// 2. 节点携带 SysDeptListModel 的全部字段(含 deptCategory/status/createdAt),
// 供表格展示。
//
// 同样由后端一次建好嵌套 children 返回,前端无需再开 treeConfig.transform。
func (l *sSysDept) GetDeptFullTree(ctx context.Context, query model.SysDeptFullTreeParam) (items []*model.SysDeptFullTreeModel, err error) {
depts, _, err := l.GetDeptList(ctx, query.SysDeptListParam)
if err != nil {
return nil, err
}
// 带过滤条件时,命中的节点其父级可能被 WhereLike/Where 滤掉。
// 补齐这些祖先节点,使搜索结果仍呈现完整的层级链路(与前端
// filterDeptTree 的「命中节点保留整棵子树」语义一致),
// 否则用户看到的是一个parentId 悬空的孤立行,看不出它归属谁。
depts, err = l.withDeptAncestors(ctx, depts)
if err != nil {
return nil, err
}
return buildSysDeptFullTree(depts), nil
}
// withDeptAncestors 为列表中的每个节点补齐缺失的祖先,返回补齐后的完整列表。
//
// 输入通常是过滤后的结果集,节点可能缺失父级。做法是逐轮收集
// 「父级 id 不在当前集合中」的节点,按 id 批量补查,直到没有新节点补入为止。
// 补入的祖先沿用原有排序(order_num、dept_id),与 GetDeptList 一致。
func (l *sSysDept) withDeptAncestors(ctx context.Context, depts []*model.SysDeptListModel) ([]*model.SysDeptListModel, error) {
if len(depts) == 0 {
return depts, nil
}
present := make(map[int64]struct{}, len(depts))
for _, d := range depts {
if d != nil {
present[d.DeptId] = struct{}{}
}
}
merged := make([]*model.SysDeptListModel, 0, len(depts))
merged = append(merged, depts...)
// 每轮收集缺失父级 → 批量补查;轮数受部门层级深度约束(通常 2~3 轮)。
for {
missingIds := make([]int64, 0)
for _, d := range depts {
if d == nil || d.ParentId == 0 {
continue
}
if _, ok := present[d.ParentId]; !ok {
missingIds = append(missingIds, d.ParentId)
}
}
if len(missingIds) == 0 {
break
}
ancestors, err := l.GetDeptListByIds(ctx, missingIds)
if err != nil {
return nil, err
}
if len(ancestors) == 0 {
// 父级已不存在(脏数据或跨租户不可见),到此为止,避免死循环。
break
}
// 去重后并入;下一轮继续向上补。
fresh := make([]*model.SysDeptListModel, 0, len(ancestors))
for _, a := range ancestors {
if a == nil {
continue
}
if _, ok := present[a.DeptId]; ok {
continue
}
present[a.DeptId] = struct{}{}
fresh = append(fresh, a)
}
if len(fresh) == 0 {
break
}
merged = append(merged, fresh...)
depts = merged
}
// 补入的祖先是按「缺失父级 id 批次」查回来的,追加在尾部会打乱原有
// order_num / dept_id 排序,而 buildSysDeptFullTree 依赖入参顺序来保证
// 同层节点的展示次序,因此这里统一重排一次。
sort.SliceStable(merged, func(i, j int) bool {
if merged[i].OrderNum != merged[j].OrderNum {
return merged[i].OrderNum < merged[j].OrderNum
}
return merged[i].DeptId < merged[j].DeptId
})
return merged, nil
}
// buildSysDeptFullTree 把部门列表组装成嵌套树。
//
// 处理「可见根」:ParentId == 0 的顶层部门,以及 ParentId 指向本批次
// 中不存在的部门(例如按状态/名称过滤后父级被滤掉)—— 后者若丢弃会
// 导致整棵子树不可见,因此提升为根节点。逻辑与 buildSysMenuFullTree 一致。
func buildSysDeptFullTree(depts []*model.SysDeptListModel) []*model.SysDeptFullTreeModel {
if len(depts) == 0 {
return make([]*model.SysDeptFullTreeModel, 0)
}
// 保持入参顺序(查询已按 order_num、dept_id 排序),同时建立 id 索引。
nodes := make([]*model.SysDeptFullTreeModel, 0, len(depts))
nodeMaps := make(map[int64]*model.SysDeptFullTreeModel, len(depts))
for _, item := range depts {
if item == nil {
continue
}
node := &model.SysDeptFullTreeModel{
SysDeptListModel: *item,
Children: make([]*model.SysDeptFullTreeModel, 0),
}
nodes = append(nodes, node)
nodeMaps[item.DeptId] = node
}
roots := make([]*model.SysDeptFullTreeModel, 0, len(depts))
for _, node := range nodes {
if node.ParentId == 0 {
roots = append(roots, node)
continue
}
if parent, ok := nodeMaps[node.ParentId]; ok {
parent.Children = append(parent.Children, node)
continue
}
// 父级被过滤掉:提升为可见根,保证其子树仍可访问。
roots = append(roots, node)
}
return roots
}
// GetDeptTree 获取部门树
func (l *sSysDept) GetDeptTree(ctx context.Context, query model.SysDeptTreeParam) (items []*model.SysDeptTreeModel, err error) {
depts, _, err := l.GetDeptList(ctx, model.SysDeptListParam{})
if err != nil {
return nil, err
}
if len(depts) == 0 {
return make([]*model.SysDeptTreeModel, 0), nil
}
// 可见根:ParentId=0 或 ParentId 指向不在列表中的部门(孤立节点)
deptIdSet := make(map[int64]struct{}, len(depts))
for _, d := range depts {
deptIdSet[d.DeptId] = struct{}{}
}
rootDepts := make([]*model.SysDeptListModel, 0, len(depts))
for _, d := range depts {
if d.ParentId == 0 {
rootDepts = append(rootDepts, d)
continue
}
if _, ok := deptIdSet[d.ParentId]; !ok {
rootDepts = append(rootDepts, d)
}
}
items = make([]*model.SysDeptTreeModel, 0)
// 不带过滤:完整树
if query.DeptType == nil || *query.DeptType == "" {
for _, root := range rootDepts {
rootItem := l.deptTreeItem(root, fmt.Sprintf("%d", root.DeptId))
rootItem.Children, err = l.DeptTree(ctx, rootItem, depts, rootItem.Ancestors)
if err != nil {
return nil, err
}
items = append(items, rootItem)
}
return items, nil
}
// DeptType=0:根部门的直接子(按类型过滤)
if *query.DeptType == "0" {
for _, root := range rootDepts {
for _, dept := range depts {
if dept.ParentId == root.DeptId && dept.DeptType == 0 {
items = append(items, l.deptTreeItem(dept, fmt.Sprintf("%d,%d", root.DeptId, dept.DeptId)))
}
}
}
return items, nil
}
// DeptType=1:公司类型树
if *query.DeptType == "1" {
companyDepts := make([]*model.SysDeptListModel, 0)
for _, dept := range depts {
if dept.DeptType == 1 {
companyDepts = append(companyDepts, dept)
}
}
// 公司类型的可见根:ParentId=0 或 ParentId 不在 companyDepts 中
companyIdSet := make(map[int64]struct{}, len(companyDepts))
for _, d := range companyDepts {
companyIdSet[d.DeptId] = struct{}{}
}
companyRoots := make([]*model.SysDeptListModel, 0, len(companyDepts))
for _, d := range companyDepts {
if d.ParentId == 0 {
companyRoots = append(companyRoots, d)
continue
}
if _, ok := companyIdSet[d.ParentId]; !ok {
companyRoots = append(companyRoots, d)
}
}
// rootMap:原 rootDepts 的索引,用于补 ancestors
rootMap := make(map[int64]*model.SysDeptListModel, len(rootDepts))
for _, root := range rootDepts {
rootMap[root.DeptId] = root
}
// 公司子树递归
var buildCompanySubTree func(parentDept *model.SysDeptTreeModel, ancestors string) ([]*model.SysDeptTreeModel, error)
buildCompanySubTree = func(parentDept *model.SysDeptTreeModel, ancestors string) ([]*model.SysDeptTreeModel, error) {
data := make([]*model.SysDeptTreeModel, 0)
for _, dept := range companyDepts {
if dept.ParentId == parentDept.DeptId {
item := l.deptTreeItem(dept, ancestors+","+fmt.Sprintf("%d", dept.DeptId))
children, err := buildCompanySubTree(item, item.Ancestors)
if err != nil {
return nil, err
}
item.Children = children
data = append(data, item)
}
}
return data, nil
}
for _, root := range companyRoots {
ancestors := fmt.Sprintf("%d", root.DeptId)
if visibleRoot, ok := rootMap[root.ParentId]; ok {
ancestors = fmt.Sprintf("%d,%d", visibleRoot.DeptId, root.DeptId)
}
rootItem := l.deptTreeItem(root, ancestors)
children, err := buildCompanySubTree(rootItem, rootItem.Ancestors)
if err != nil {
return nil, err
}
rootItem.Children = children
items = append(items, rootItem)
}
return items, nil
}
return make([]*model.SysDeptTreeModel, 0), nil
}
// deptTreeItem 构建部门树项
func (l *sSysDept) deptTreeItem(dept *model.SysDeptListModel, ancestors string) *model.SysDeptTreeModel {
return &model.SysDeptTreeModel{
DeptId: dept.DeptId,
ParentId: dept.ParentId,
DeptName: dept.DeptName,
Key: fmt.Sprintf("%d", dept.DeptId),
Children: nil,
Ancestors: ancestors,
DeptType: dept.DeptType,
}
}
// 递归构建结构
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
}
// AddDept 添加部门
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
}
// EditDept 编辑部门
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 && *dept.ParentId != 0 {
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
}
// DeleteDept 删除部门
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 = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
err = service.AgentEnterprise().Delete(ctx, &model.TAgentEnterpriseDeleteParam{
DeptId: dept.DeptId,
})
if err != nil {
g.Log().Errorf(ctx, "sSysDept.DeleteDept Delete err: %v, deptId:%d, Input:%+v", err, dept.DeptId, dept)
return err
}
_, err = l.Model(ctx).Where(dao.SysDept.Columns().DeptId, dept.DeptId).Delete()
if err != nil {
return err
}
return nil
})
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
}
// GetParentIDAncestors 获取部门祖先
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
}
// GetDeptListByIds 根据部门ID列表获取部门列表
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,225 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"strings"
"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
}
+383
View File
@@ -0,0 +1,383 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @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-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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
result, err := c.Model(ctx).Data(jobAdd).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
result, err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobUpdate.JobId).Update(jobUpdate)
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
result, err := c.Model(ctx).Where(dao.SysJob.Columns().JobId, jobUpdate.JobId).Update(jobUpdate)
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
result, err := c.Model(ctx).WhereIn(dao.SysJob.Columns().JobId, jobDelete.JobIds).Update(map[string]interface{}{
dao.SysJob.Columns().DeletedAt: jobDelete.DeletedAt,
dao.SysJob.Columns().DeletedBy: jobDelete.DeletedBy})
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@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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(logininfor).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
}
+508
View File
@@ -0,0 +1,508 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"fmt"
v1 "git.5iot.com/iotlcm-public/iotlcm-admin-svr/api/system/v1"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"slices"
"sort"
"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"
)
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)
if err == nil {
placeDocumentCenterInMenuList(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)
if err == nil {
placeDocumentCenterInMenuList(data)
}
return
}
func placeDocumentCenterInMenuList(menus []*model.SysMenuListModel) {
order := 33
for _, menu := range menus {
if menu.ParentId == 0 && menu.Path == "batchExecutionLog" {
order = menu.OrderNum + 1
break
}
}
for _, menu := range menus {
if menu.MenuId == 2153 && menu.Path == "docs" && menu.MenuType == consts.SysMenuTypeDir && menu.ParentId != 0 {
menu.ParentId = 0
menu.OrderNum = order
}
}
sort.SliceStable(menus, func(i, j int) bool { return menus[i].OrderNum < menus[j].OrderNum })
}
// GetTenantMenuTree 获取租户菜单树。
//
// 与 GetTenantMenu 使用完全相同的租户隔离与筛选条件,区别只在于返回结构:
// 这里由后端一次性建好嵌套 children,前端可直接交给 vxe-table 渲染,
// 无需再依赖 treeConfig.transform 在浏览器主线程把扁平列表二次转成树。
//
// 建树采用「id 索引 + 单次遍历」的实现(O(n)),不使用递归逐层过滤(O(n²)),
// 因为菜单量级已达数百行(其中绝大多数是 F 按钮),递归过滤会有可观开销。
func (l *sSysMenu) GetTenantMenuTree(ctx context.Context, query *model.SysMenuListParam) (items []*model.SysMenuFullTreeModel, err error) {
list, _, err := l.GetTenantMenu(ctx, query)
if err != nil {
return nil, err
}
return buildSysMenuFullTree(list), nil
}
// buildSysMenuFullTree 把扁平菜单列表组装成嵌套树。
//
// 处理两种「可见根」:
// 1. ParentId == 0 的顶层菜单;
// 2. ParentId 指向本批次中不存在的菜单(例如按状态/名称过滤后父级被滤掉),
// 这类节点若丢弃会导致整棵子树不可见,因此提升为根节点。
func buildSysMenuFullTree(list []*model.SysMenuListModel) []*model.SysMenuFullTreeModel {
if len(list) == 0 {
return make([]*model.SysMenuFullTreeModel, 0)
}
// 保持入参顺序(查询已按 order_num 排序),同时建立 id 索引。
nodes := make([]*model.SysMenuFullTreeModel, 0, len(list))
nodeMaps := make(map[int64]*model.SysMenuFullTreeModel, len(list))
for _, item := range list {
if item == nil {
continue
}
node := &model.SysMenuFullTreeModel{
SysMenuListModel: *item,
Children: make([]*model.SysMenuFullTreeModel, 0),
}
nodes = append(nodes, node)
nodeMaps[item.MenuId] = node
}
roots := make([]*model.SysMenuFullTreeModel, 0)
for _, node := range nodes {
if node.ParentId == 0 {
roots = append(roots, node)
continue
}
if parent, ok := nodeMaps[node.ParentId]; ok {
parent.Children = append(parent.Children, node)
continue
}
// 父级不在本批次内:提升为根,避免子树整体丢失。
roots = append(roots, node)
}
return roots
}
// 构建树结构
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) {
assignedRoleIDs, err := service.SysUser().GetUserRoleIds(ctx, contexts.GetUserId(ctx))
if err != nil {
return nil, err
}
roleIds := make([]int64, 0)
roleIds = append(roleIds, assignedRoleIDs...)
g.Log().Infof(ctx, "sSysMenu.GetUserMenu roleIds: %v", roleIds)
var validRoles []*entity.SysRole
if len(roleIds) > 0 {
if err := dao.SysRole.Ctx(ctx).Where("tenant_id", contexts.GetTenantId(ctx)).WhereIn("role_id", roleIds).Where("status", "0").Scan(&validRoles); err != nil {
return nil, err
}
}
roleIds = nil
for _, role := range validRoles {
roleIds = append(roleIds, role.RoleId)
}
if contexts.IsSuperAdmin(ctx) && contexts.GetTenantId(ctx) == consts.DefaultSystemTenantCode && slices.Contains(roleIds, consts.SuperAdminRoleId) {
err = l.ModelQuery(ctx).Where(dao.SysMenu.Columns().Status, consts.SysMenuStatusNormal).
Order(dao.SysMenu.Columns().OrderNum).Scan(&data)
return
}
roleIds = slices.DeleteFunc(roleIds, func(roleID int64) bool {
return roleID == consts.SuperAdminRoleId
})
// 获取角色菜单列表
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)
}
menuIds, err = rolepolicy.RestrictMenus(ctx, contexts.GetTenantId(ctx), contexts.GetDeptId(ctx), menuIds)
if err != nil {
return nil, err
}
if len(menuIds) == 0 {
return []*entity.SysMenu{}, nil
}
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)
menuIdSet := make(map[int64]struct{}, len(menuList))
for _, menu := range menuList {
menuIdSet[menu.MenuId] = struct{}{}
}
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
}
isRootOrphan := parentMenu == nil && menu.ParentId != 0
if isRootOrphan {
_, isRootOrphan = menuIdSet[menu.ParentId]
isRootOrphan = !isRootOrphan
}
if menu.ParentId == pId || isRootOrphan {
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: normalizeMenuAccessCodes(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
}
menuList = placeDocumentCenterAfterExecutionLog(menuList)
// 构建树结构
data, err = l.BuildUserMenuTree(ctx, nil, menuList, "")
if err != nil {
return nil, err
}
return
}
func placeDocumentCenterAfterExecutionLog(menuList []*entity.SysMenu) []*entity.SysMenu {
menus := append([]*entity.SysMenu(nil), menuList...)
order := 33
for _, menu := range menus {
if menu.ParentId == 0 && menu.Path == "batchExecutionLog" {
order = menu.OrderNum + 1
break
}
}
for index, menu := range menus {
if menu.Path != "docs" || menu.MenuName != "文档中心" || menu.MenuType != consts.SysMenuTypeDir || menu.ParentId == 0 {
continue
}
copyOfMenu := *menu
copyOfMenu.ParentId = 0
copyOfMenu.Level = 1
copyOfMenu.OrderNum = order
menus[index] = &copyOfMenu
}
return menus
}
func normalizeMenuAccessCodes(perms string) []string {
items := strings.Split(perms, ",")
codes := make([]string, 0, len(items))
seen := make(map[string]struct{}, len(items))
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if !strings.HasPrefix(item, consts.SysCheckPermissionPrefix) {
item = consts.SysCheckPermissionMenuPrefix + item
}
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
codes = append(codes, item)
}
return codes
}
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)
_, err = l.Model(ctx).Where(dao.SysMenu.Columns().MenuId, menu.MenuId).Data(menu).OmitNil().Update(menu)
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)
re, err := l.Model(ctx).Data(menu).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,53 @@
package system
import (
"context"
"testing"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
)
func TestDocumentCenterAppearsAfterExecutionLog(t *testing.T) {
menuList := []*entity.SysMenu{
{MenuId: 1, MenuName: "系统管理", Path: "system", MenuType: "M", OrderNum: 900},
{MenuId: 1836, MenuName: "执行日志", Path: "batchExecutionLog", MenuType: "C", OrderNum: 32},
{MenuId: 2153, MenuName: "文档中心", ParentId: 1, Path: "docs", MenuType: "M", OrderNum: 13},
{MenuId: 2155, MenuName: "Web 使用手册", ParentId: 2153, Path: "https://admin.iotlcm.example.com/docs/manual/web/", MenuType: "C", IsFrame: 0, OrderNum: 1},
{MenuId: 2154, MenuName: "OpenAPI 文档", ParentId: 2153, Path: "https://admin.iotlcm.example.com/docs/openapi/v3/", MenuType: "C", IsFrame: 0, OrderNum: 2},
}
menus, err := NewSysMenu().BuildUserMenuTree(context.Background(), nil, placeDocumentCenterAfterExecutionLog(menuList), "")
if err != nil {
t.Fatal(err)
}
if len(menus) != 3 || menus[0].Id != 1836 || menus[1].Id != 2153 || menus[2].Id != 1 {
t.Fatalf("unexpected root menu order: %+v", menus)
}
if len(menus[1].Children) != 2 || menus[1].Children[0].Id != 2155 || menus[1].Children[1].Id != 2154 {
t.Fatalf("document links were not preserved: %+v", menus[1].Children)
}
if menuList[2].ParentId != 1 || menuList[2].OrderNum != 13 {
t.Fatal("original menu record was mutated")
}
}
func TestDocumentCenterMenuListHierarchy(t *testing.T) {
menus := []*model.SysMenuListModel{
{MenuId: 1, MenuName: "system", ParentId: 0, Path: "system", OrderNum: 900},
{MenuId: 2153, MenuName: "docs", ParentId: 1, Path: "docs", MenuType: "M", OrderNum: 13, Perms: "docs:menu"},
{MenuId: 2155, ParentId: 2153, Path: "https://admin.iotlcm.example.com/docs/manual/web/", OrderNum: 1},
{MenuId: 2154, ParentId: 2153, Path: "https://admin.iotlcm.example.com/docs/openapi/v3/", OrderNum: 2},
{MenuId: 1836, ParentId: 0, Path: "batchExecutionLog", OrderNum: 32},
}
placeDocumentCenterInMenuList(menus)
if menus[3].MenuId != 2153 || menus[2].MenuId != 1836 || menus[3].ParentId != 0 || menus[3].OrderNum != 33 || menus[3].Perms != "docs:menu" {
t.Fatalf("document center must follow execution log without losing permissions: %+v", menus)
}
if menus[0].ParentId != 2153 || menus[1].ParentId != 2153 {
t.Fatalf("document links must remain children: %+v", menus)
}
items := buildSysMenuFullTree(menus)
if len(items) != 3 || items[1].MenuId != 2153 || len(items[1].Children) != 2 {
t.Fatalf("unexpected root hierarchy: %+v", items)
}
}
+350
View File
@@ -0,0 +1,350 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"encoding/json"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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, userIds *gset.IntSet, param *entity.SysNotice) (err error) {
sysNotifyUserList := make([]*model.SysNoticeUserEditParam, 0)
for _, userId := range userIds.Slice() {
sysNotifyUserList = append(sysNotifyUserList, &model.SysNoticeUserEditParam{
NoticeId: param.NoticeId,
NoticeTitle: param.NoticeTitle,
NoticeType: param.NoticeType,
NoticeContent: string(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 = g.DB().Model(dao.SysNoticeUser.Table()).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()
g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
id, err := s.Model(ctx).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, userIds, &entityData)
if err != nil {
return err
}
return
})
return
}
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()
g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
m := s.Model(ctx)
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 = g.DB().Model(dao.SysNoticeUser.Table()).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, userIds, &entityData)
if err != nil {
return err
}
return
})
return
}
func (s *sSysNotice) Delete(ctx context.Context, param *model.SysNoticeDeleteParam) (err error) {
g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
_, err = s.Model(ctx).WhereIn(dao.SysNotice.Columns().NoticeId, param.NoticeIds).Delete()
if err != nil {
return err
}
// 删除SysNoticeUser
_, err = g.DB().Model(dao.SysNoticeUser.Table()).WhereIn(dao.SysNoticeUser.Columns().NoticeId, param.NoticeIds).Delete()
if err != nil {
return err
}
return
})
return
}
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,193 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package system
import (
"context"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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()
if _, err = s.Model(ctx).
Fields(model.SysNoticeUserUpdateFields{}).
WherePri(in.Id).Data(in).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()
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
Fields(model.SysNoticeUserInsertFields{}).
Data(in).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) {
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.SysNoticeUser.Columns().Status: in.Status,
dao.SysNoticeUser.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
err = gerror.Wrap(err, "更新用户通知公告表状态失败,请稍后重试!")
return
}
return
}
// Read 已读
func (s *sSysNoticeUser) Read(ctx context.Context, in *model.SysNoticeUserReadParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Ids).Data(g.Map{
dao.SysNoticeUser.Columns().IsRead: 1,
dao.SysNoticeUser.Columns().UpdatedBy: contexts.GetUserId(ctx),
dao.SysNoticeUser.Columns().UpdatedAt: gtime.Now(),
}).Update(); err != nil {
err = gerror.Wrap(err, "更新用户通知公告表状态失败,请稍后重试!")
return
}
return
}
@@ -0,0 +1,159 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/mcache"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility"
"strings"
"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 entity.SysOperLog) (err error) {
_, err = dao.SysOperLog.Ctx(ctx).Data(data).Save()
if err != nil {
g.Log().Error(ctx, "sSysOperLog.RealWrite error:%v,data:%+v", err, data)
}
return
}
+981
View File
@@ -0,0 +1,981 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/hash"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/uuid32"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"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 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.OssId == 0 {
return nil, errors.New("文件不存在")
}
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(g.Map{
dao.SysOss.Columns().TenantId: contexts.GetTenantId(ctx),
dao.SysOss.Columns().FileName: result.Name,
dao.SysOss.Columns().OriginalName: result.OriginalName,
dao.SysOss.Columns().FileSuffix: result.Ext,
dao.SysOss.Columns().Path: result.Path,
dao.SysOss.Columns().Url: result.FullPath,
dao.SysOss.Columns().CreatedDept: contexts.GetDeptId(ctx),
dao.SysOss.Columns().CreatedAt: gtime.Now(),
dao.SysOss.Columns().CreatedBy: contexts.GetUserId(ctx),
dao.SysOss.Columns().UpdatedAt: gtime.Now(),
dao.SysOss.Columns().UpdatedBy: contexts.GetUserId(ctx),
dao.SysOss.Columns().Service: "local",
dao.SysOss.Columns().Md5: result.Md5,
dao.SysOss.Columns().FileSize: result.Size,
dao.SysOss.Columns().FileCrc16: result.Crc16,
dao.SysOss.Columns().FileSum: result.Sum16,
}).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) UploadWithBizName(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
}
if err = s.CheckType(ctx, param.FileType, file); err != nil {
return
}
if err = s.CheckSize(ctx, param.FileType, file); err != nil {
return
}
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
}
}
var result model.UploadResponse
for i := 0; i < 3; i++ {
fileName := makeBizUploadFileName(file.Filename)
result, err = s.UploadLocalWithFileName(ctx, file, param.NewFileType, fileName, param.SubDirName, param.NotAddDate)
if err == nil {
break
}
if !errors.Is(err, os.ErrExist) {
return nil, err
}
}
if err != nil {
return nil, err
}
id, err := s.Model(ctx).Data(g.Map{
dao.SysOss.Columns().TenantId: contexts.GetTenantId(ctx),
dao.SysOss.Columns().FileName: result.Name,
dao.SysOss.Columns().OriginalName: result.OriginalName,
dao.SysOss.Columns().FileSuffix: result.Ext,
dao.SysOss.Columns().Path: result.Path,
dao.SysOss.Columns().Url: result.FullPath,
dao.SysOss.Columns().CreatedDept: contexts.GetDeptId(ctx),
dao.SysOss.Columns().CreatedAt: gtime.Now(),
dao.SysOss.Columns().CreatedBy: contexts.GetUserId(ctx),
dao.SysOss.Columns().UpdatedAt: gtime.Now(),
dao.SysOss.Columns().UpdatedBy: contexts.GetUserId(ctx),
dao.SysOss.Columns().Service: "local",
dao.SysOss.Columns().Md5: result.Md5,
dao.SysOss.Columns().FileSize: result.Size,
dao.SysOss.Columns().FileCrc16: result.Crc16,
dao.SysOss.Columns().FileSum: result.Sum16,
}).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(g.Map{
dao.SysOss.Columns().TenantId: contexts.GetTenantId(ctx),
dao.SysOss.Columns().FileName: result.Name,
dao.SysOss.Columns().OriginalName: result.OriginalName,
dao.SysOss.Columns().FileSuffix: result.Ext,
dao.SysOss.Columns().Path: result.Path,
dao.SysOss.Columns().Url: result.FullPath,
dao.SysOss.Columns().CreatedDept: contexts.GetDeptId(ctx),
dao.SysOss.Columns().CreatedAt: gtime.Now(),
dao.SysOss.Columns().CreatedBy: contexts.GetUserId(ctx),
dao.SysOss.Columns().UpdatedAt: gtime.Now(),
dao.SysOss.Columns().UpdatedBy: contexts.GetUserId(ctx),
dao.SysOss.Columns().Service: "local",
dao.SysOss.Columns().Md5: result.Md5,
dao.SysOss.Columns().FileSize: result.Size,
dao.SysOss.Columns().FileCrc16: result.Crc16,
dao.SysOss.Columns().FileSum: result.Sum16,
}).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(g.Map{
dao.SysOss.Columns().TenantId: contexts.GetTenantId(ctx),
dao.SysOss.Columns().FileName: result.Name,
dao.SysOss.Columns().OriginalName: result.OriginalName,
dao.SysOss.Columns().FileSuffix: result.Ext,
dao.SysOss.Columns().Path: result.Path,
dao.SysOss.Columns().Url: result.FullPath,
dao.SysOss.Columns().CreatedDept: contexts.GetDeptId(ctx),
dao.SysOss.Columns().CreatedAt: gtime.Now(),
dao.SysOss.Columns().CreatedBy: contexts.GetUserId(ctx),
dao.SysOss.Columns().UpdatedAt: gtime.Now(),
dao.SysOss.Columns().UpdatedBy: contexts.GetUserId(ctx),
dao.SysOss.Columns().Service: "local",
dao.SysOss.Columns().Md5: result.Md5,
dao.SysOss.Columns().FileSize: result.Size,
dao.SysOss.Columns().FileCrc16: result.Crc16,
dao.SysOss.Columns().FileSum: result.Sum16,
}).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 := localUploadURL(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 := localUploadURL(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 := localUploadURL(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) UploadLocalWithFileName(ctx context.Context, file *ghttp.UploadFile, newFileType int, fileName string, subDirName string, notAddDate int) (result model.UploadResponse, err error) {
if file == nil {
err = errors.New("文件必须")
return
}
fileName = safeUploadFileName(fileName)
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
}
}
err = saveUploadFileToPath(file, fullDirPath, fileName)
if err != nil {
return
}
fullFilePath := fullDirPath + "/" + fileName
url := localUploadURL(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: gfile.Basename(file.Filename),
Name: fileName,
Type: file.Header.Get("Content-type"),
Ext: gfile.Ext(fileName),
Md5: md5,
Crc16: crc16,
Sum16: sum16,
}
return
}
func saveUploadFileToPath(file *ghttp.UploadFile, dirPath string, fileName string) error {
source, err := file.Open()
if err != nil {
return gerror.Wrap(err, "open upload file failed")
}
defer source.Close()
filePath := gfile.Join(dirPath, fileName)
target, err := gfile.OpenWithFlagPerm(filePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
return err
}
defer target.Close()
if _, err = io.Copy(target, source); err != nil {
return gerror.Wrapf(err, `copy upload file failed from "%s" to "%s"`, file.Filename, filePath)
}
return nil
}
func safeUploadFileName(fileName string) string {
fileName = strings.TrimSpace(gfile.Basename(fileName))
if fileName == "" {
fileName = "file"
}
for _, old := range []string{"\\", "/", ":", "*", "?", "\"", "<", ">", "|"} {
fileName = strings.ReplaceAll(fileName, old, "_")
}
return fileName
}
func makeBizUploadFileName(fileName string) string {
fileName = safeUploadFileName(fileName)
ext := gfile.Ext(fileName)
baseName := strings.TrimSuffix(fileName, ext)
if baseName == "" {
baseName = "file"
}
uid := uuid32.S()
if len(uid) > 8 {
uid = uid[:8]
}
return fmt.Sprintf("%s_%s_%s.xlsx", time.Now().Format("20060102150405"), baseName, uid)
}
func localUploadURL(ctx context.Context, filePath string) string {
filePath = strings.TrimSpace(filepath.ToSlash(filepath.Clean(filePath)))
if filePath == "" {
return ""
}
if configType, err := service.SysConfig().GetConfigByKey(ctx, consts.ConfigOssUrlPath); err == nil && configType != nil {
configValue := strings.TrimSpace(configType.ConfigValue)
if configValue != "" {
if strings.Contains(configValue, "|") {
split := strings.SplitN(configValue, "|", 2)
from := strings.TrimSpace(filepath.ToSlash(split[0]))
to := strings.TrimSpace(filepath.ToSlash(split[1]))
if suffix, ok := localUploadMappedSuffix(filePath, from); ok {
return joinUploadURL(to, suffix)
}
} else {
if suffix := localUploadRelativePath(filePath); suffix != "" {
return joinUploadURL(configValue, suffix)
}
return joinUploadURL(configValue, filePath)
}
}
}
if suffix := localUploadRelativePath(filePath); suffix != "" {
return joinUploadURL("/upload", suffix)
}
return filePath
}
func localUploadMappedSuffix(filePath string, from string) (string, bool) {
from = strings.Trim(strings.TrimSpace(filepath.ToSlash(from)), "/")
if from == "" {
return "", false
}
normalized := strings.TrimSpace(filepath.ToSlash(filePath))
marker := "/" + from + "/"
if idx := strings.Index(normalized, marker); idx >= 0 {
return strings.TrimPrefix(normalized[idx+len(marker):], "/"), true
}
prefix := from + "/"
if strings.HasPrefix(normalized, prefix) {
return strings.TrimPrefix(normalized[len(prefix):], "/"), true
}
if strings.Trim(normalized, "/") == from {
return "", true
}
return "", false
}
func joinUploadURL(base string, suffix string) string {
base = strings.TrimRight(strings.TrimSpace(filepath.ToSlash(base)), "/")
suffix = strings.TrimLeft(strings.TrimSpace(filepath.ToSlash(suffix)), "/")
if base == "" {
base = "/upload"
}
if suffix == "" {
return base
}
return base + "/" + suffix
}
func (s *sSysOss) DownloadLocal(ctx context.Context, file *entity.SysOss) (err error) {
if file == nil {
err = errors.New("文件必须")
return
}
finalPath, err := resolveLocalOssPath(ctx, file.Path)
if err != nil {
return err
}
r := ghttp.RequestFromCtx(ctx)
if r == nil {
err = gerror.New("ctx not http request")
return
}
r.Response.ServeFileDownload(finalPath)
r.Response.Status = http.StatusOK
r.Response.Flush()
return
}
func resolveLocalOssPath(ctx context.Context, storedPath string) (string, error) {
storedPath = strings.TrimSpace(storedPath)
if storedPath == "" {
return "", errors.New("文件路径为空")
}
candidates := localOssPathCandidates(ctx, storedPath)
seen := make(map[string]struct{}, len(candidates))
tried := make([]string, 0, len(candidates))
for _, candidate := range candidates {
candidate = strings.TrimSpace(candidate)
if candidate == "" {
continue
}
candidate = filepath.Clean(candidate)
if _, ok := seen[candidate]; ok {
continue
}
seen[candidate] = struct{}{}
tried = append(tried, candidate)
if info, statErr := os.Stat(candidate); statErr == nil && !info.IsDir() {
return candidate, nil
}
}
return "", gerror.Newf("本地文件不存在,请检查Web服务和Worker是否共用上传目录,原始路径: %s,尝试路径: %s", storedPath, strings.Join(tried, "; "))
}
func localOssPathCandidates(ctx context.Context, storedPath string) []string {
candidates := []string{storedPath}
if !filepath.IsAbs(storedPath) {
if absPath, err := filepath.Abs(storedPath); err == nil {
candidates = append(candidates, absPath)
}
}
uploadPath := strings.TrimSpace(g.Cfg().MustGet(ctx, "system.upload.local.path").String())
if uploadPath == "" {
return candidates
}
uploadRoots := localOssUploadRootCandidates(uploadPath)
suffixes := localOssRelativeSuffixes(storedPath)
for _, root := range uploadRoots {
if !filepath.IsAbs(storedPath) {
candidates = append(candidates, filepath.Join(root, storedPath))
}
for _, suffix := range suffixes {
candidates = append(candidates, filepath.Join(root, suffix))
}
}
return candidates
}
func localOssUploadRootCandidates(uploadPath string) []string {
roots := make([]string, 0, 4)
for _, root := range []string{
os.Getenv("IOTLCM_SERVER_UPLOAD_ROOT"),
os.Getenv("IOTLCM_UPLOAD_ROOT"),
uploadPath,
} {
root = strings.TrimSpace(root)
if root == "" {
continue
}
roots = appendLocalOssUploadRootCandidate(roots, root)
}
return roots
}
func appendLocalOssUploadRootCandidate(roots []string, root string) []string {
for _, candidate := range localOssEquivalentUploadRoots(root) {
if filepath.IsAbs(candidate) {
roots = appendUniqueLocalOssPath(roots, candidate)
continue
}
if absPath, err := filepath.Abs(candidate); err == nil {
roots = appendUniqueLocalOssPath(roots, absPath)
}
roots = appendUniqueLocalOssPath(roots, candidate)
}
return roots
}
func localOssEquivalentUploadRoots(root string) []string {
root = strings.TrimSpace(root)
if root == "" {
return nil
}
roots := []string{root}
normalized := filepath.ToSlash(filepath.Clean(root))
if normalized == "resource/upload" || strings.HasSuffix(normalized, "/resource/upload") {
prefix := strings.TrimSuffix(normalized, "resource/upload")
roots = appendUniqueLocalOssPath(roots, filepath.FromSlash(prefix+"upload"))
}
return roots
}
func appendUniqueLocalOssPath(values []string, value string) []string {
value = strings.TrimSpace(value)
if value == "" {
return values
}
cleaned := filepath.Clean(value)
for _, existing := range values {
if filepath.Clean(strings.TrimSpace(existing)) == cleaned {
return values
}
}
return append(values, value)
}
func localOssRelativeSuffixes(storedPath string) []string {
normalized := filepath.ToSlash(filepath.Clean(strings.TrimSpace(storedPath)))
markers := localUploadPathMarkers()
suffixes := make([]string, 0, len(markers))
for _, marker := range markers {
if idx := strings.Index(normalized, marker); idx >= 0 {
suffix := strings.TrimPrefix(normalized[idx+len(marker):], "/")
if suffix != "" {
suffixes = append(suffixes, filepath.FromSlash(suffix))
}
}
}
return suffixes
}
func localUploadRelativePath(filePath string) string {
normalized := filepath.ToSlash(filepath.Clean(strings.TrimSpace(filePath)))
for _, marker := range localUploadPathMarkers() {
if idx := strings.Index(normalized, marker); idx >= 0 {
return strings.TrimPrefix(normalized[idx+len(marker):], "/")
}
}
return ""
}
func localUploadPathMarkers() []string {
return []string{
"resource/upload/",
"/resource/upload/",
"/app/resource/upload/",
"/app/upload/",
"upload/",
"/upload/",
}
}
func (s *sSysOss) DeleteLocal(ctx context.Context, file *entity.SysOss) (err error) {
if file == nil {
err = errors.New("文件不存在")
return
}
filePath := file.Path
if !gfile.Exists(filePath) {
err = errors.New("文件不存在")
return
}
err = gfile.Remove(filePath)
if err != nil {
return
}
return
}
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@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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
}
+214
View File
@@ -0,0 +1,214 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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
}
+829
View File
@@ -0,0 +1,829 @@
package system
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility"
"io"
"strconv"
"strings"
"time"
"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 {
Timeout string `json:"timeout" dc:"请求超时"`
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
}
timeout, err := g.Cfg().Get(ctx, "wx.qrcode.timeout", "")
if err != nil {
return nil, err
}
return &SysQrcodeConfig{
Timeout: timeout.String(),
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().Timeout(utility.ExternalRequestTimeout(sysQrcodeConfig.Timeout))
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
}
defer response.Close()
if response.StatusCode != 200 {
g.Log().Errorf(ctx, "调用二维码API失败, 请求数据: %v, 响应数据: %v", request, response)
return nil, errors.New("调用二维码API失败")
}
responseBytes, err := io.ReadAll(response.Body)
if err != nil {
return nil, gerror.Wrap(err, "读取二维码服务响应失败")
}
responseData := string(responseBytes)
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,83 @@
package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestQrcodeAPISuccessAndBodyTimeout(t *testing.T) {
for _, scenario := range []string{"success", "header-timeout", "body-timeout", "partial-body-timeout"} {
t.Run(scenario, func(t *testing.T) {
release := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
_, _ = io.Copy(io.Discard, request.Body)
if scenario != "success" {
if scenario != "header-timeout" {
writer.Header().Set("Content-Length", "1024")
writer.WriteHeader(http.StatusOK)
if scenario == "partial-body-timeout" {
_, _ = writer.Write([]byte(`{"code":0,"data":{"ticket":"partial"}}`))
}
writer.(http.Flusher).Flush()
}
select {
case <-request.Context().Done():
case <-release:
}
return
}
_, _ = writer.Write([]byte(`{"code":0,"data":{"ticket":"test-ticket","expireTime":300,"qrUrl":"test-url"}}`))
}))
defer server.Close()
defer close(release)
started := time.Now()
result, err := callQrcodeApi(context.Background(), &SysQrcodeConfig{
ServerApi: server.URL, Timeout: "70ms", Token: "test-token", SceneIdLogin: 1,
}, true)
if scenario != "success" {
if err == nil || result != nil || time.Since(started) > time.Second {
t.Fatalf("stalled response was not rejected: result=%v error=%v", result, err)
}
} else if err != nil || result == nil || result.Ticket != "test-ticket" {
t.Fatalf("successful response changed: result=%v error=%v", result, err)
}
})
}
}
func TestUserCodeHTTPTimeoutAndStatus(t *testing.T) {
for _, scenario := range []string{"success", "rejected", "timeout"} {
t.Run(scenario, func(t *testing.T) {
release := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
_, _ = io.Copy(io.Discard, request.Body)
if scenario == "timeout" {
select {
case <-request.Context().Done():
case <-release:
}
return
}
if scenario == "rejected" {
writer.WriteHeader(http.StatusServiceUnavailable)
return
}
writer.WriteHeader(http.StatusAccepted)
}))
defer server.Close()
defer close(release)
started := time.Now()
err := callUserCodeHTTP(context.Background(), model.UserCodeHttpSenderConfig{
Url: server.URL, Timeout: "70ms", SuccessStatus: http.StatusAccepted,
}, "user@example.com", "123456")
if (err != nil) != (scenario != "success") || time.Since(started) > time.Second {
t.Fatalf("unexpected sender result: %v", err)
}
})
}
}
+601
View File
@@ -0,0 +1,601 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/mcache"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolesync"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility"
"slices"
"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.getRoleMenuRelations(ctx, param.RoleId)
if err != nil {
return nil, err
}
menuIdList := make([]int64, 0)
roleMenuDataScopes := make(map[int64]string)
for _, menu := range roleMenus {
menuIdList = append(menuIdList, menu.MenuId)
roleMenuDataScopes[menu.MenuId] = menu.DataScope
}
res.MenuIds = menuIdList
res.RoleMenuDataScopes = roleMenuDataScopes
// 获取角色部门
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) getRoleMenuRelations(ctx context.Context, id int64) (res []*entity.SysRoleMenu, err error) {
err = dao.SysRoleMenu.Ctx(ctx).
Where(dao.SysRoleMenu.Columns().TenantId, contexts.GetTenantId(ctx)).
Where(dao.SysRoleMenu.Columns().RoleId, id).Scan(&res)
return
}
// 获取角色菜单
func (s *sSysRole) GetRoleMenu(ctx context.Context, id int64) (res []*entity.SysMenu, err error) {
err = dao.SysRoleMenu.Ctx(ctx).
Where(dao.SysRoleMenu.Columns().TenantId, contexts.GetTenantId(ctx)).
Where(dao.SysRoleMenu.Columns().RoleId, id).Scan(&res)
return
}
// 获取角色部门
func (s *sSysRole) GetRoleDept(ctx context.Context, id int64) (res []*entity.SysDept, err error) {
err = dao.SysRoleDept.Ctx(ctx).
Where(dao.SysRoleDept.Columns().TenantId, contexts.GetTenantId(ctx)).
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{
TenantId: contexts.GetTenantId(ctx),
RoleId: consts.SuperAdminRoleId,
MenuId: menu.MenuId,
})
}
return res, nil
}
err = dao.SysRoleMenu.Ctx(ctx).
Where(dao.SysRoleMenu.Columns().TenantId, contexts.GetTenantId(ctx)).
WhereIn(dao.SysRoleMenu.Columns().RoleId, ids).Scan(&res)
return
}
// 新增角色
func (s *sSysRole) Add(ctx context.Context, param *model.SysRoleAddParam) (role *model.SysRoleAddModel, err error) {
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var saveErr error
role, saveErr = s.addRoleAtomic(ctx, param)
if saveErr != nil {
return saveErr
}
return nil
})
if err == nil && role != nil {
err = rolesync.EnqueueRole(ctx, role.RoleId, contexts.GetUserId(ctx))
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleCreate, role.RoleId)
}
return
}
func (s *sSysRole) addRoleAtomic(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)
if data.DataScope == nil || data.DataScope == "" {
data.DataScope = consts.SysRoleDataScopePersonal
}
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
}
role = &model.SysRoleAddModel{
RoleId: roleId,
}
// 新增角色菜单
err = s.RoleMenu(ctx, roleId, param.MenuIds, param.RoleMenuDataScopes)
if err != nil {
return role, err
}
return
}
// 编辑角色
func (s *sSysRole) Edit(ctx context.Context, param *model.SysRoleEditParam) (role *model.SysRoleEditModel, err error) {
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if _, err := tx.Ctx(ctx).Model(dao.SysRole.Table()).Where("role_id", param.RoleId).Where("tenant_id", contexts.GetTenantId(ctx)).Data(do.SysRole{UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return err
}
var saveErr error
role, saveErr = s.editRoleAtomic(ctx, param)
if saveErr != nil {
return saveErr
}
return nil
})
if err == nil {
err = rolesync.EnqueueRole(ctx, param.RoleId, contexts.GetUserId(ctx))
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, param.RoleId)
}
return
}
func (s *sSysRole) editRoleAtomic(ctx context.Context, param *model.SysRoleEditParam) (role *model.SysRoleEditModel, err error) {
role = &model.SysRoleEditModel{
RoleId: param.RoleId,
}
if err = s.ensureRolesNotInherited(ctx, []int64{param.RoleId}); err != nil {
return role, err
}
user := contexts.GetUser(ctx)
if user == nil {
return role, gerror.New("登录已过期")
}
template, templateErr := rolepolicy.Template(ctx)
if templateErr != nil {
return role, templateErr
}
if template != nil && template.RoleId == param.RoleId {
if !contexts.IsSuperAdmin(ctx) {
return role, gerror.New("仅平台超级管理员可修改代理模板")
}
if param.Status != nil && *param.Status != "0" {
return role, gerror.New("代理模板不能停用,请先切换模板角色")
}
if param.DeptId != nil && *param.DeptId != template.DeptId {
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
}
// 更新角色菜单
err = s.RoleMenu(ctx, param.RoleId, param.MenuIds, param.RoleMenuDataScopes)
if err != nil {
return role, err
}
return
}
// 删除角色
func (s *sSysRole) Delete(ctx context.Context, param *model.SysRoleDeleteParam) (role *model.SysRoleDeleteModel, err error) {
template, templateErr := rolepolicy.Template(ctx)
if templateErr != nil {
return nil, templateErr
}
if template != nil && (slices.Contains(param.RoleIds, template.RoleId) || param.RoleId == template.RoleId) {
return nil, gerror.New("代理模板不能删除,请先切换模板角色")
}
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("请选择要删除的角色")
}
if err = s.ensureRolesNotInherited(ctx, roleIds); err != nil {
return role, err
}
if err = s.ensureRolesHaveNoInheritedChildren(ctx, roleIds); err != nil {
return role, err
}
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)
clearRoleUsersAccessCache(ctx, roleIds...)
return
}
func (s *sSysRole) RoleMenu(ctx context.Context, roleId int64, menuIds []int64, dataScopes map[int64]string) (err error) {
template, templateErr := rolepolicy.Template(ctx)
if templateErr != nil {
return templateErr
}
if template != nil && template.RoleId == roleId {
for _, scope := range dataScopes {
if scope == consts.SysRoleDataScopeAll || scope == consts.SysRoleDataScopeCustom {
return gerror.New("模板按钮不允许全部或自定义数据范围")
}
}
}
tenantId := contexts.GetTenantId(ctx)
if !contexts.IsSuperAdmin(ctx) {
allowed, err := rolepolicy.RestrictMenus(ctx, tenantId, contexts.GetDeptId(ctx), menuIds)
if err != nil {
return err
}
for _, id := range menuIds {
if !slices.Contains(allowed, id) {
return gerror.New("角色授权超出机构或模板范围")
}
}
for _, scope := range dataScopes {
if scope == consts.SysRoleDataScopeAll || scope == consts.SysRoleDataScopeCustom {
return gerror.New("不允许授予全部或自定义数据范围")
}
}
}
// 查询角色菜单
roleMenu := make([]*entity.SysRoleMenu, 0)
err = dao.SysRoleMenu.Ctx(ctx).Where(dao.SysRoleMenu.Columns().TenantId, tenantId).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)
updateMenuIds := utility.ArrayRightDiff(menuIds, addMenuIds)
// 删除角色菜单
if len(delMenuIds) > 0 {
_, err = dao.SysRoleMenu.Ctx(ctx).Where(dao.SysRoleMenu.Columns().TenantId, tenantId).Where(dao.SysRoleMenu.Columns().RoleId, roleId).WhereIn(dao.SysRoleMenu.Columns().MenuId, delMenuIds).Delete()
if err != nil {
return err
}
}
// 新增角色菜单
if len(addMenuIds) > 0 {
addData := make([]*do.SysRoleMenu, 0)
for _, menuId := range addMenuIds {
addData = append(addData, &do.SysRoleMenu{
TenantId: tenantId,
RoleId: roleId,
MenuId: menuId,
DataScope: normalizeRoleMenuDataScope(dataScopes[menuId]),
})
}
_, err = dao.SysRoleMenu.Ctx(ctx).Data(addData).Insert()
if err != nil {
return err
}
}
for _, menuId := range updateMenuIds {
_, err = dao.SysRoleMenu.Ctx(ctx).
Where(dao.SysRoleMenu.Columns().TenantId, tenantId).
Where(dao.SysRoleMenu.Columns().RoleId, roleId).
Where(dao.SysRoleMenu.Columns().MenuId, menuId).
Data(do.SysRoleMenu{
DataScope: normalizeRoleMenuDataScope(dataScopes[menuId]),
}).Update()
if err != nil {
return err
}
}
clearRoleUsersAccessCache(ctx, roleId)
return
}
func normalizeRoleMenuDataScope(dataScope string) string {
switch dataScope {
case consts.SysRoleDataScopeAll,
consts.SysRoleDataScopeDept,
consts.SysRoleDataScopeDeptAndBelow,
consts.SysRoleDataScopePersonal,
consts.SysRoleDataScopeDeptAndBelowOrPersonal,
consts.SysRoleDataScopeOrgAndNextLevel,
consts.SysRoleDataScopeOrgNextLevel:
return dataScope
default:
return consts.SysRoleMenuDataScopeRole
}
}
// clearRoleUsersAccessCache removes stale permission caches after role changes.
func clearRoleUsersAccessCache(ctx context.Context, roleIds ...int64) {
if len(roleIds) == 0 {
return
}
relations := make([]*entity.SysUserRole, 0)
if err := dao.SysUserRole.Ctx(ctx).
Fields(dao.SysUserRole.Columns().UserId).
Where(dao.SysUserRole.Columns().TenantId, contexts.GetTenantId(ctx)).
WhereIn(dao.SysUserRole.Columns().RoleId, roleIds).
Scan(&relations); err != nil {
g.Log().Warningf(ctx, "clear role users access cache failed, roleIds:%v, err:%+v", roleIds, err)
return
}
seen := make(map[int64]struct{}, len(relations))
for _, relation := range relations {
if relation == nil || relation.UserId <= 0 {
continue
}
if _, ok := seen[relation.UserId]; ok {
continue
}
seen[relation.UserId] = struct{}{}
if err := mcache.RemoveUserAccessCodeList(ctx, relation.UserId); err != nil {
g.Log().Warningf(ctx, "remove user access code cache failed, userId:%d, err:%+v", relation.UserId, err)
}
if err := mcache.RemoveUserRoleDataAccessCodeList(ctx, relation.UserId); err != nil {
g.Log().Warningf(ctx, "remove user role data access code cache failed, userId:%d, err:%+v", relation.UserId, err)
}
}
}
// 自定义角色部门数据权限
func (s *sSysRole) RoleDept(ctx context.Context, roleId int64, deptIds []int64) (err error) {
tenantId := contexts.GetTenantId(ctx)
// 查询角色部门
roleDept := make([]*entity.SysRoleDept, 0)
err = dao.SysRoleDept.Ctx(ctx).Where(dao.SysRoleDept.Columns().TenantId, tenantId).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().TenantId, tenantId).Where(dao.SysRoleDept.Columns().RoleId, roleId).WhereIn(dao.SysRoleDept.Columns().DeptId, delDeptIds).Delete()
}
// 新增角色部门
if len(addDeptIds) > 0 {
addData := make([]map[string]interface{}, 0)
for _, deptId := range addDeptIds {
addData = append(addData, map[string]interface{}{
dao.SysRoleDept.Columns().TenantId: tenantId,
dao.SysRoleDept.Columns().RoleId: roleId,
dao.SysRoleDept.Columns().DeptId: deptId,
})
}
_, err = dao.SysRoleDept.Ctx(ctx).Data(addData).Insert()
}
if err == nil {
clearRoleUsersAccessCache(ctx, roleId)
}
return
}
// 编辑角色数据权限
func (s *sSysRole) EditRoleDataScope(ctx context.Context, model *model.SysRoleDataScopeEditParam) (err error) {
template, templateErr := rolepolicy.Template(ctx)
if templateErr != nil {
return templateErr
}
if !contexts.IsSuperAdmin(ctx) || (template != nil && template.RoleId == model.RoleId) {
if model.DataScope == consts.SysRoleDataScopeAll || model.DataScope == consts.SysRoleDataScopeCustom || model.DataScope == "" {
return gerror.New("不允许全部、自定义或空数据权限")
}
}
if err = s.ensureRolesNotInherited(ctx, []int64{model.RoleId}); err != nil {
return err
}
data := do.SysRole{}
gconv.Struct(model, &data)
data.UpdatedBy = contexts.GetUserId(ctx)
data.UpdatedAt = gtime.Now()
if template != nil && template.RoleId == model.RoleId && model.DataScope != consts.SysRoleDataScopeDeptAndBelow {
return gerror.New("代理模板数据范围必须为本机构及下级")
}
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if _, updateErr := s.Model(ctx).Data(data).Where(dao.SysRole.Columns().RoleId, model.RoleId).OmitNil().Update(); updateErr != nil {
return updateErr
}
if err := s.RoleDept(ctx, model.RoleId, model.DeptIds); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
if err = rolesync.EnqueueRole(ctx, model.RoleId, contexts.GetUserId(ctx)); err != nil {
return err
}
event.EventsInstance().Emit(ctx, consts.EventKeyDBSysRoleUpdate, model.RoleId)
return
}
func (s *sSysRole) ensureRolesNotInherited(ctx context.Context, roleIds []int64) error {
if len(roleIds) == 0 {
return nil
}
if contexts.IsSuperAdmin(ctx) {
return nil
}
count, err := g.Model(dao.SysRole.Table()+" r").
LeftJoin(dao.SysDept.Table()+" d", "d.dept_id = r.dept_id AND d.tenant_id = r.tenant_id").
WhereIn("r.role_id", roleIds).
Where("r.deleted_at IS NULL").
Where("d.deleted_at IS NULL").
Where("r.created_dept > 0").
Where("r.dept_id <> r.created_dept").
Where("d.parent_id = r.created_dept").
Where("r.tenant_id", contexts.GetTenantId(ctx)).
Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("上级下发角色由上级统一维护,当前组织只能查看和使用")
}
return nil
}
func (s *sSysRole) ensureRolesHaveNoInheritedChildren(ctx context.Context, roleIds []int64) error {
if len(roleIds) == 0 {
return nil
}
count, err := g.Model(dao.SysRole.Table()+" r").
LeftJoin(dao.SysDept.Table()+" child_dept", "child_dept.dept_id = r.dept_id AND child_dept.tenant_id = r.tenant_id").
Where("r.created_dept IN (SELECT dept_id FROM sys_role WHERE role_id IN (?) AND is_pass = 1 AND deleted_at IS NULL)", roleIds).
Where("r.deleted_at IS NULL").
Where("child_dept.deleted_at IS NULL").
Where("r.created_dept > 0").
Where("r.dept_id <> r.created_dept").
Where("child_dept.parent_id = r.created_dept").
Where("r.tenant_id", contexts.GetTenantId(ctx)).
Count()
if err != nil {
return err
}
if count > 0 {
return gerror.New("该角色已下发给下级,不能删除;可关闭后续下发或停用角色")
}
return nil
}
@@ -0,0 +1,83 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
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) {
g.DB().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("绑定信息已存在")
}
_, err = dao.SysSocial.Ctx(ctx).Data(social).OmitEmpty().Save()
return err
})
return err
}
+502
View File
@@ -0,0 +1,502 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"database/sql"
"errors"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/event"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility"
"slices"
"strings"
"text/template"
"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 := tx.Ctx(ctx).Model(dao.SysTenant.Table()).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 = tx.Ctx(ctx).Model(dao.SysDept.Table()).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 = tx.Ctx(ctx).Model(dao.SysRole.Table()).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{
TenantId: tenantId,
RoleId: roleId,
MenuId: menuId,
DataScope: consts.SysRoleMenuDataScopeRole,
})
}
_, err = tx.Ctx(ctx).Model(dao.SysRoleMenu.Table()).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 = tx.Ctx(ctx).Model(dao.SysUser.Table()).Data(dataUserInsert).OmitNil().InsertAndGetId()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.Add SysUser.Table().InsertAndGetId err: %v, data: %+v", err, dataUserInsert)
return err
}
// 修改租户信息
_, err = tx.Ctx(ctx).Model(dao.SysTenant.Table()).Data(g.Map{
dao.SysTenant.Columns().TenantId: tenantId,
dao.SysTenant.Columns().AdminRoleId: roleId,
dao.SysTenant.Columns().AdminDeptId: deptId,
dao.SysTenant.Columns().AdminUserId: userId,
}).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 = tx.Ctx(ctx).Model(dao.SysDept.Table()).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{
TenantId: tenantId,
UserId: userId,
RoleId: roleId,
})
_, err = tx.Ctx(ctx).Model(dao.SysUserRole.Table()).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) {
sql := `
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), '蓝色 skin-blue、绿色 skin-green、紫色 skin-purple、红色 skin-red、黄色 skin-yellow');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '用户管理-账号初始密码', 'sys.user.initPassword', '123456', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), '初始化密码 123456');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), '深色主题theme-dark,浅色主题theme-light');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), '是否开启注册用户功能(true开启,false关闭)');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', 'OSS预览列表资源开关', 'sys.oss.previewListResource', 'true', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), 'true:开启, false:关闭');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '上传文件类型', 'sys.oss.fileType', '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', 'Y', 103, 1, SYSDATE(), '');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '上传图片类型', 'sys.oss.imgType', 'jpg,jpeg,png,gif,webp', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), '');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '上传文件大小', 'sys.oss.fileSize', '20M', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), '');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '上传图片大小', 'sys.oss.imgSize', '5M', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), '');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '上传文件Url路径', 'sys.oss.urlPath', 'resource/upload|/upload', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), '\"http://ab.com/\" 或 \"/\" 或 \"resource/upload|http://ab.com/upload\"');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '是否启动在线用户强制退出', 'sys.online.forceLogout', 'true', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), 'true开启,false关闭');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '系统启用的监控通道', 'iot.default.channel.gb28181', '0', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), '');
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '报警资源匹配时间', 'iot.alarm.image.timesec', '10', 'N', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), NULL);
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '自主注册组织ID', 'iot.register.dept', '0', 'N', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), NULL);
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '自主注册角色ID', 'iot.register.role', '0', 'N', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), NULL);
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '系统默认MQTT渠道', 'iot.default.channel.mqtt', '0', 'N', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), NULL);
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '系统默认TCP渠道', 'iot.default.channel.tcp', '0', 'N', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), NULL);
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '系统默认UDP渠道', 'iot.default.channel.udp', '0', 'N', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), NULL);
INSERT INTO sys_config (tenant_id, config_name, config_key, config_value, config_type, created_dept, created_by, created_at, remark) VALUES ('{{.TenantId}}', '项目API默认地址', 'iot.project.api.default.addr', 'http://127.0.0.1:8510', 'Y', {{.CreatedDept}}, {{.CreatedBy}}, SYSDATE(), '');
`
tmpl, err := template.New("example").Parse(sql)
if err != nil {
panic(err)
}
data := struct {
TenantId string
CreatedBy int64
CreatedDept int64
}{
TenantId: tenantId,
CreatedBy: createdBy,
CreatedDept: createdDept,
}
var b strings.Builder
err = tmpl.Execute(&b, data)
if err != nil {
return nil, err
}
sql = b.String()
g.Log().Infof(ctx, "sSysTenant.batchInsertSysParam sql: %s", sql)
sqlArr := strings.Split(sql, ";")
ids = make([]int64, 0)
for _, sql := range sqlArr {
sql = strings.ReplaceAll(sql, "\n", "")
sql = strings.TrimLeft(sql, " ")
sql = strings.TrimRight(sql, " ")
if sql == "" {
continue
}
res, err := tx.Ctx(ctx).Exec(sql)
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.batchInsertSysParam tx.Ctx(ctx).Exec err: %v, sql: %s", err, sql)
return nil, err
}
lastInsertId, err := res.LastInsertId()
if err != nil {
g.Log().Errorf(ctx, "sSysTenant.batchInsertSysParam LastInsertId err: %v, sql: %s", err, sql)
return nil, err
}
ids = append(ids, lastInsertId)
}
g.Log().Infof(ctx, "sSysTenant.batchInsertSysParam ids: %v", ids)
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 := make([]*entity.SysRoleMenu, 0)
err = dao.SysRoleMenu.Ctx(ctx).
Where(dao.SysRoleMenu.Columns().TenantId, tenantId).
Where(dao.SysRoleMenu.Columns().RoleId, tenantInfo.AdminRoleId).
Scan(&roleMenuList)
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{
TenantId: tenantId,
RoleId: tenantInfo.AdminRoleId,
MenuId: gconv.Int64(menuId),
DataScope: consts.SysRoleMenuDataScopeRole,
})
}
}
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).Where(dao.SysRoleMenu.Columns().TenantId, tenantId).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@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,140 @@
// package system
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2025 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
package system
import (
"context"
"errors"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/do"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/request"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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) cleanupExpired(ctx context.Context, tenantId string) (err error) {
cols := dao.SysUserOnline.Columns()
var data []*model.SysUserOnlineViewModel
err = s.Model(ctx).
Where(cols.TenantId, tenantId).
WhereLT(cols.ExpireTime, gtime.Now()).
Scan(&data)
if err != nil {
return err
}
if len(data) == 0 {
return nil
}
ids := make([]int64, 0, len(data))
for _, v := range data {
ids = append(ids, v.OnlineId)
if v.Token != "" {
service.SysAuth().DeleteToken(ctx, v.Token)
}
}
_, err = s.Model(ctx).WhereIn(cols.OnlineId, ids).Delete()
return err
}
func (s *sSysUserOnline) List(ctx context.Context, query *model.SysUserOnlineListParam, page *request.PageInfo) (items []*model.SysUserOnlineListModel, total int, err error) {
tenantId := contexts.GetTenantId(ctx)
if err = s.cleanupExpired(ctx, tenantId); err != nil {
return nil, 0, err
}
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,206 @@
// Package gen
// @Link https://github.com/xiujiecn/xiu-admin
// @Copyright Copyright (c) 2026 LiXiujie
// @Author Lxj <li@xiujie.cn>
// @License https://github.com/xiujiecn/xiu-admin/blob/master/LICENSE
// @AutoGenerate Version
package system
import (
"context"
"fmt"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/contexts"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/handler"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/xgorm/hook"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/convert"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/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 sSysTTplatformSetting struct{}
func NewSysTTplatformSetting() *sSysTTplatformSetting {
return &sSysTTplatformSetting{}
}
func init() {
service.RegisterSysTTplatformSetting(NewSysTTplatformSetting())
}
// Model 二级域名站点个性化配置表ORM模型
func (s *sSysTTplatformSetting) 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.TTplatformSetting.Ctx(ctx), option...)
}
// List 获取二级域名站点个性化配置表列表
func (s *sSysTTplatformSetting) List(ctx context.Context, in *model.TTplatformSettingListParam) (list []*model.TTplatformSettingListModel, totalCount int, err error) {
mod := s.Model(ctx)
// 字段过滤
mod = mod.Fields(model.TTplatformSettingListModel{})
// 查询主键ID
if in.Id > 0 {
mod = mod.Where(dao.TTplatformSetting.Columns().Id, in.Id)
}
// 查询状态 0启用 1禁用
if in.Status > 0 {
mod = mod.Where(dao.TTplatformSetting.Columns().Status, in.Status)
}
// 查询创建时间
if len(in.CreatedAt) == 2 {
in.CreatedAt[1] = in.CreatedAt[1].EndOfDay()
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(dao.TTplatformSetting.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
}
// 分页
mod = mod.Page(in.Page, in.PageSize)
// 排序
mod = mod.OrderAsc(dao.TTplatformSetting.Columns().Sort).OrderDesc(dao.TTplatformSetting.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 *sSysTTplatformSetting) Export(ctx context.Context, in *model.TTplatformSettingListParam) (err error) {
list, totalCount, err := s.List(ctx, in)
if err != nil {
return
}
// 字段的排序是依据tags的字段顺序,如果你不想使用默认的排序方式,可以直接定义 tags = []string{"字段名称", "字段名称2", ...}
tags, err := convert.GetEntityDescTags(model.TTplatformSettingExportModel{})
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.TTplatformSettingExportModel
)
if err = gconv.Scan(list, &exports); err != nil {
return
}
err = excel.ExportByStructs(ctx, tags, exports, fileName, sheetName)
return
}
// Edit 修改/新增二级域名站点个性化配置表
func (s *sSysTTplatformSetting) Edit(ctx context.Context, in *model.TTplatformSettingEditParam) (err error) {
// 验证'FullDomain'唯一
if err = xgorm.IsUnique(ctx, &dao.TTplatformSetting, g.Map{dao.TTplatformSetting.Columns().FullDomain: in.FullDomain}, "完整域名已存在", in.Id); err != nil {
return
}
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
updatedBy := contexts.GetUserId(ctx)
in.UpdatedBy = &updatedBy
in.UpdatedAt = gtime.Now()
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
if _, err = s.Model(ctx).
Fields(model.TTplatformSettingUpdateFields{}).
WherePri(in.Id).Data(in).Update(); err != nil {
err = gerror.Wrap(err, "修改二级域名站点个性化配置表失败,请稍后重试!")
}
return
}
// 新增
tenantId := contexts.GetTenantId(ctx)
in.TenantId = &tenantId
createdDept := contexts.GetDeptId(ctx)
in.CreatedDept = &createdDept
createdBy := contexts.GetUserId(ctx)
in.CreatedBy = &createdBy
in.CreatedAt = gtime.Now()
if _, err = s.Model(ctx).
Fields(model.TTplatformSettingInsertFields{}).
Data(in).Insert(); err != nil {
err = gerror.Wrap(err, "新增二级域名站点个性化配置表失败,请稍后重试!")
}
return
})
}
// Delete 删除二级域名站点个性化配置表
func (s *sSysTTplatformSetting) Delete(ctx context.Context, in *model.TTplatformSettingDeleteParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Unscoped().Delete(); err != nil {
err = gerror.Wrap(err, "删除二级域名站点个性化配置表失败,请稍后重试!")
return
}
return
}
// MaxSort 获取二级域名站点个性化配置表最大排序
func (s *sSysTTplatformSetting) MaxSort(ctx context.Context, in *model.TTplatformSettingMaxSortParam) (res *model.TTplatformSettingMaxSortModel, err error) {
if err = dao.TTplatformSetting.Ctx(ctx).Fields(dao.TTplatformSetting.Columns().Sort).OrderDesc(dao.TTplatformSetting.Columns().Sort).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取二级域名站点个性化配置表最大排序,请稍后重试!")
return
}
if res == nil {
res = new(model.TTplatformSettingMaxSortModel)
}
res.Sort = res.Sort + 1
return
}
// View 获取二级域名站点个性化配置表指定信息
func (s *sSysTTplatformSetting) View(ctx context.Context, in *model.TTplatformSettingViewParam) (res *model.TTplatformSettingViewModel, err error) {
g.Log().Info(ctx, "sSysTTplatformSetting.View:", in)
if err = dao.TTplatformSetting.Ctx(ctx).Where(dao.TTplatformSetting.Columns().FullDomain, in.FullDomain).Hook(hook.MemberSummary).Scan(&res); err != nil {
err = gerror.Wrap(err, "获取二级域名站点个性化配置表信息,请稍后重试!")
return
}
return
}
// Status 更新二级域名站点个性化配置表状态
func (s *sSysTTplatformSetting) Status(ctx context.Context, in *model.TTplatformSettingStatusParam) (err error) {
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
dao.TTplatformSetting.Columns().Status: in.Status,
dao.TTplatformSetting.Columns().UpdatedBy: contexts.GetUserId(ctx),
}).Update(); err != nil {
err = gerror.Wrap(err, "更新二级域名站点个性化配置表状态失败,请稍后重试!")
return
}
return
}