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
@@ -0,0 +1,307 @@
package middleware
import (
"context"
"errors"
"net/http"
"slices"
"strings"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts"
"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/mcache"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/rolepolicy"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/packed/response"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/service"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility"
"git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility/queue"
"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/util/gmeta"
)
type sMiddleware struct{}
func New() *sMiddleware {
return &sMiddleware{}
}
func init() {
service.RegisterMiddleware(New())
}
func getContentType(r *ghttp.Request) (contentType string) {
contentType = r.Response.Header().Get("Content-Type")
if contentType != "" {
return
}
mime := gmeta.Get(r.GetHandlerResponse(), "mime").String()
if mime == "" {
contentType = consts.HTTPContentTypeJson
} else {
contentType = mime
}
return
}
func (m *sMiddleware) ResponseHandler(r *ghttp.Request) {
r.Middleware.Next()
if r.Response.BufferLength() > 0 {
return
}
if contexts.IsWebSocket(r.GetCtx()) {
return
}
if disposition := r.Response.Header().Get("Content-Disposition"); strings.Contains(strings.ToLower(disposition), "attachment") {
r.Response.Status = http.StatusOK
return
}
contentType := getContentType(r)
if contentType == consts.HTTPContentTypeExcel {
r.Response.Status = http.StatusOK
return
}
err := r.GetError()
res := r.GetHandlerResponse()
var code gcode.Code = gcode.CodeOK
if err != nil {
code = gerror.Code(err)
if code == gcode.CodeNil {
code = gcode.CodeInternalError
}
var channelTestErr *model.TSimChannelsTestError
if errors.As(err, &channelTestErr) {
response.JsonExit(r, code.Code(), err.Error(), &model.TSimChannelsTestOutput{
GatewayResponseRaw: channelTestErr.GatewayResponseRaw,
})
}
response.JsonExit(r, code.Code(), err.Error())
} else {
response.Json(r, code.Code(), "", res)
}
}
func (s *sMiddleware) Ctx(r *ghttp.Request) {
ctx := r.GetCtx()
r.SetCtx(r.GetNeverDoneCtx())
data, err := service.SysAuth().GetCurrentUser(ctx)
if err != nil {
r.Middleware.Next()
return
}
if data != nil {
contextModel := new(model.Context)
contextModel.User = &model.Identity{
BaseClaims: data.BaseClaims,
}
contexts.Init(r, contextModel)
}
r.Middleware.Next()
}
func (s *sMiddleware) CORS(r *ghttp.Request) {
r.SetCtx(r.GetNeverDoneCtx())
if contexts.IsWebSocket(r.GetCtx()) {
r.Middleware.Next()
return
}
corsOptions := r.Response.DefaultCORSOptions()
corsConfig := g.Cfg().MustGet(context.Background(), "server.allowedDomains").Strings()
if len(corsConfig) == 0 {
r.Response.CORSDefault()
} else {
corsOptions.AllowDomain = corsConfig
r.Response.CORS(corsOptions)
}
r.Middleware.Next()
}
func (s *sMiddleware) Auth(r *ghttp.Request) {
var (
ctx = r.Context()
method = r.Method
path = ""
)
if r.URL != nil {
path = r.URL.Path
}
if s.IsExceptLogin(ctx, path) {
r.Middleware.Next()
return
}
userId := contexts.GetUserId(ctx)
tenantId := contexts.GetTenantId(ctx)
g.Log().Debugf(ctx, "sMiddleware.Auth userId: %d tenantId: %s method: %s path: %s, ================>>", userId, tenantId, method, path)
if userId == 0 {
g.Log().Error(ctx, "sMiddleware.Auth userId is 0", "path", path, "clientIp", r.GetClientIp(), "User-Agent", utility.GetUserAgent(ctx), "Authorization", r.Header.Get("Authorization"))
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
return
}
if contexts.IsWebSocket(ctx) {
r.Middleware.Next()
return
}
forceLogout, err := mcache.GetSystemConfig(ctx, consts.ConfigOnlineForceLogout, consts.ConfigOnlineForceLogoutFalse)
if err != nil {
g.Log().Errorf(ctx, "sMiddleware.Auth GetSystemConfig error: %v", err)
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
return
}
if forceLogout == consts.ConfigOnlineForceLogoutTrue {
uuid := contexts.GetUserUuid(ctx)
token, err := bcache.GetSysAuthTokenReject(ctx, userId, uuid)
if err != nil {
g.Log().Errorf(ctx, "sMiddleware.Auth GetSysAuthTokenReject error: %v", err)
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
return
}
if token != "" {
g.Log().Infof(ctx, "sMiddleware.Auth token is reject, userId: %d, uuid: %s, token: %s", userId, uuid, token)
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
return
}
}
if contexts.IsSuperAdmin(r.GetCtx()) && tenantId == consts.DefaultSystemTenantCode {
isAdmin, checkErr := rolepolicy.CurrentSuperAdmin(ctx, userId)
if checkErr != nil {
g.Log().Errorf(ctx, "verify super admin failed: %v", checkErr)
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
return
}
if isAdmin {
r.Middleware.Next()
return
}
}
if contexts.IsSuperAdmin(ctx) {
user := contexts.GetUser(ctx)
user.Roles = slices.DeleteFunc(user.Roles, func(role model.Role) bool {
return role.RoleId == consts.SuperAdminRoleId
})
}
serveHandler := r.GetServeHandler()
userAccessCodeList, menuRoleDataAccessCodeList, err := service.SysAuth().GetUserAccessCodeList(ctx, userId)
if err != nil {
g.Log().Errorf(ctx, "sMiddleware.Auth GetUserAccessCodeList error: %v", err)
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), consts.CodeLoginExpired.Message())
return
}
if serveHandler != nil {
g.Log().Infof(ctx, "sMiddleware.Auth serveHandler GetMetaTag(x-check-permission): %+v", serveHandler.GetMetaTag("x-check-permission"))
accessCode := serveHandler.GetMetaTag("x-check-permission")
if accessCode != "" {
accessCodeList := strings.Split(accessCode, ",")
hasPermission := false
for _, code := range accessCodeList {
if strings.HasPrefix(code, consts.SysCheckPermissionPrefix) {
accessCodeItemList := strings.Split(code, "&")
hasItem := true
for _, item := range accessCodeItemList {
if !hasAccessCode(userAccessCodeList, item) {
g.Log().Errorf(ctx, "sMiddleware.Auth userAccessCodeList not contains item: %v, userAccessCodeList: %v", item, userAccessCodeList)
hasItem = false
break
}
for _, roleDataAccessCode := range menuRoleDataAccessCodeList {
roleDataAccessCodeItemList := strings.Split(roleDataAccessCode, "|")
if len(roleDataAccessCodeItemList) > 0 && matchAccessCode(roleDataAccessCodeItemList[0], item) {
g.Log().Debugf(ctx, "sMiddleware.Auth roleDataAccessCodeItemList: %v", roleDataAccessCodeItemList)
if len(roleDataAccessCodeItemList) == 3 {
roleId := roleDataAccessCodeItemList[1]
roleDataScope := roleDataAccessCodeItemList[2]
contexts.SetDataValue(ctx, "currentRoleId", roleId)
contexts.SetDataValue(ctx, "currentRoleDataScope", roleDataScope)
g.Log().Debugf(ctx, "sMiddleware.Auth currentRoleId: %s, currentRoleDataScope: %s", roleId, roleDataScope)
}
break
}
}
}
if hasItem {
hasPermission = true
break
}
}
}
if !hasPermission {
response.JsonExit(r, consts.CodeNoPermission.Code(), consts.CodeNoPermission.Message())
return
}
}
}
r.Middleware.Next()
}
func matchAccessCode(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
}
func hasAccessCode(userAccessCodeList []string, requiredCode string) bool {
for _, userCode := range userAccessCodeList {
if matchAccessCode(userCode, requiredCode) {
return true
}
}
return false
}
func (s *sMiddleware) IsExceptLogin(ctx context.Context, path string) bool {
pathList := g.Cfg().MustGet(ctx, "router.exceptLogin").Strings()
return slices.Contains(pathList, path)
}
func (s *sMiddleware) OperationLog(r *ghttp.Request) {
r.Middleware.Next()
ctx := r.Context()
enabledDBOperationLog := g.Cfg().MustGet(ctx, "server.enabledDBOperationLog").Bool()
if !enabledDBOperationLog {
return
}
// Replay accepts only a Run ID. Rejecting extra request fields at the
// controller protects the Worker; redact the generic operation log as an
// additional guard against clients submitting card/token data anyway.
if strings.HasPrefix(r.URL.Path, "/api/v1/iot/fullSyncRuns/replay") {
return
}
data, err := service.SysOperLog().AnalysisLog(ctx)
if err != nil {
g.Log().Errorf(context.TODO(), "sMiddleware.OperationLog error:%v", err)
return
}
queue.Push(r.GetCtx(), consts.QueueSysOptLog, data)
}
func (s *sMiddleware) ResponseHandlerRaw(r *ghttp.Request) {
r.Middleware.Next()
if r.Response.BufferLength() > 0 {
g.Log().Infof(r.Context(), "ResponseHandler r.Response.BufferLength() %d buf:%s", r.Response.BufferLength(), r.Response.BufferString())
return
}
res := r.GetHandlerResponse()
if r.GetError() != nil {
r.Response.WriteJson(res)
r.ExitAll()
} else {
r.Response.WriteJson(res)
}
}