chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。 - app/:uni-app 小程序前端(基于 unibest,MIT) - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID - 内网后端地址 → admin.iotlcm.example.com - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明) - server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0) - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件) - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化 - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位) - 依赖号码尾部的断言同步更新 - 补 server/LICENSE(Apache-2.0) - 移除内部资产:CI 触发器 - 补根 README,LF 行尾 + .gitattributes 已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
// Package addons
|
||||
// @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 addons
|
||||
|
||||
import (
|
||||
"context"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
)
|
||||
|
||||
func ModuleSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
output = make([]*genmodel.SelectItemModel, 0)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package appidentity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type TerminalUserConfig struct {
|
||||
DeptId int64
|
||||
RoleId int64
|
||||
}
|
||||
|
||||
func IsTerminalUser(ctx context.Context, user *entity.SysUser) (bool, error) {
|
||||
if user == nil || user.UserId <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
cfg, err := LoadTerminalUserConfig(ctx, user.TenantId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if cfg == nil {
|
||||
return false, nil
|
||||
}
|
||||
hasRole, err := userHasRole(ctx, user.UserId, cfg.RoleId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return hasRole, nil
|
||||
}
|
||||
|
||||
func IsTerminalUserId(ctx context.Context, userId int64) (bool, error) {
|
||||
if userId <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
var user *entity.SysUser
|
||||
if err := dao.SysUser.Ctx(ctx).
|
||||
Where(dao.SysUser.Columns().UserId, userId).
|
||||
WhereNull(dao.SysUser.Columns().DeletedAt).
|
||||
Limit(1).
|
||||
Scan(&user); err != nil {
|
||||
return false, gerror.Wrap(err, "查询小程序用户身份失败")
|
||||
}
|
||||
if user == nil || user.UserId <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
return IsTerminalUser(ctx, user)
|
||||
}
|
||||
|
||||
func LoadTerminalUserConfig(ctx context.Context, tenantId string) (*TerminalUserConfig, error) {
|
||||
tenantId = strings.TrimSpace(tenantId)
|
||||
if tenantId == "" {
|
||||
tenantId = consts.DefaultSystemTenantCode
|
||||
}
|
||||
deptValue, err := tenantConfigValue(ctx, tenantId, consts.ConfigKeyAppRegisterDeptId)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取小程序所属组织配置失败")
|
||||
}
|
||||
roleValue, err := tenantConfigValue(ctx, tenantId, consts.ConfigKeyAppRegisterRoleId)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取小程序角色配置失败")
|
||||
}
|
||||
deptValue = strings.TrimSpace(deptValue)
|
||||
roleValue = strings.TrimSpace(roleValue)
|
||||
if roleValue == "" {
|
||||
return nil, nil
|
||||
}
|
||||
roleId, err := positiveConfigId(roleValue, "小程序角色ID")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deptId := int64(0)
|
||||
if deptValue != "" {
|
||||
deptId, err = positiveConfigId(deptValue, "小程序所属组织ID")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &TerminalUserConfig{
|
||||
DeptId: deptId,
|
||||
RoleId: roleId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func tenantConfigValue(ctx context.Context, tenantId string, key string) (string, error) {
|
||||
var config *entity.SysConfig
|
||||
if err := dao.SysConfig.Ctx(ctx).
|
||||
Where(dao.SysConfig.Columns().TenantId, tenantId).
|
||||
Where(dao.SysConfig.Columns().ConfigKey, key).
|
||||
OrderDesc(dao.SysConfig.Columns().ConfigId).
|
||||
Limit(1).
|
||||
Scan(&config); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if config == nil || config.ConfigId == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return strings.TrimSpace(config.ConfigValue), nil
|
||||
}
|
||||
|
||||
func positiveConfigId(value string, label string) (int64, error) {
|
||||
id, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, gerror.Newf("%s配置不正确", label)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func userHasRole(ctx context.Context, userId int64, roleId int64) (bool, error) {
|
||||
if userId <= 0 || roleId <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
count, err := dao.SysUserRole.Ctx(ctx).
|
||||
Where(dao.SysUserRole.Columns().UserId, userId).
|
||||
Where(dao.SysUserRole.Columns().RoleId, roleId).
|
||||
Count()
|
||||
if err != nil {
|
||||
return false, gerror.Wrap(err, "校验小程序角色失败")
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func userInTerminalDept(ctx context.Context, user *entity.SysUser, terminalDeptId int64) (bool, error) {
|
||||
if user == nil || user.DeptId <= 0 || terminalDeptId <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
if user.DeptId == terminalDeptId {
|
||||
return true, nil
|
||||
}
|
||||
deptIds, err := deptAndSubIds(ctx, user.TenantId, terminalDeptId)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, deptId := range deptIds {
|
||||
if deptId == user.DeptId {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func deptAndSubIds(ctx context.Context, tenantId string, rootDeptId int64) ([]int64, error) {
|
||||
if rootDeptId <= 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
tenantId = strings.TrimSpace(tenantId)
|
||||
if tenantId == "" {
|
||||
tenantId = consts.DefaultSystemTenantCode
|
||||
}
|
||||
|
||||
seen := map[int64]struct{}{
|
||||
rootDeptId: {},
|
||||
}
|
||||
result := []int64{rootDeptId}
|
||||
level := []int64{rootDeptId}
|
||||
|
||||
for len(level) > 0 {
|
||||
var rows []*entity.SysDept
|
||||
err := dao.SysDept.Ctx(ctx).
|
||||
Fields(dao.SysDept.Columns().DeptId).
|
||||
Where(dao.SysDept.Columns().TenantId, tenantId).
|
||||
WhereIn(dao.SysDept.Columns().ParentId, level).
|
||||
WhereNull(dao.SysDept.Columns().DeletedAt).
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "查询小程序所属组织下级失败")
|
||||
}
|
||||
|
||||
nextLevel := make([]int64, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row == nil || row.DeptId <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[row.DeptId]; ok {
|
||||
continue
|
||||
}
|
||||
seen[row.DeptId] = struct{}{}
|
||||
result = append(result, row.DeptId)
|
||||
nextLevel = append(nextLevel, row.DeptId)
|
||||
}
|
||||
level = nextLevel
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package batchimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type PendingOptions struct {
|
||||
PrepareRows func(ctx context.Context, rows []Row) error
|
||||
}
|
||||
|
||||
func CreatePendingUpload(ctx context.Context, req SubmitRequest, p Processor, options PendingOptions) (*Result, error) {
|
||||
cfg := p.Config()
|
||||
if req.File == nil {
|
||||
return nil, gerror.New("请选择上传文件")
|
||||
}
|
||||
if err := service.SysOss().CheckType(ctx, consts.SysUploadFileTypeFile, req.File); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.File.Size > cfg.MaxFileSize {
|
||||
return nil, gerror.Newf("上传文件大小超过限制,最大为%dM", cfg.MaxFileSize>>20)
|
||||
}
|
||||
|
||||
batchNo, err := NextBatchNoContext(ctx, cfg.BatchNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.BatchNo = batchNo
|
||||
content, err := readUploadContent(req.File)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
importOss, err := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{
|
||||
Content: content,
|
||||
FileName: batchNo + "-import.xlsx",
|
||||
NewFileType: consts.SysUploadNewFileTypeUserFile,
|
||||
SubDirName: cfg.ImportDir,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "保存原始Excel失败")
|
||||
}
|
||||
|
||||
rows, err := p.ParseRows(ctx, importOss.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return createPending(ctx, &req, p, rows, importOss, options)
|
||||
}
|
||||
|
||||
func CreatePendingRows(ctx context.Context, req SubmitRequest, p Processor, rows []Row, options PendingOptions) (*Result, error) {
|
||||
batchNo, err := NextBatchNoContext(ctx, p.Config().BatchNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.BatchNo = batchNo
|
||||
return createPending(ctx, &req, p, rows, nil, options)
|
||||
}
|
||||
|
||||
func createPending(ctx context.Context, req *SubmitRequest, p Processor, rows []Row, importOss *model.SysOssSaveContentModel, options PendingOptions) (*Result, error) {
|
||||
if len(rows) == 0 {
|
||||
return nil, gerror.New("导入数据为空")
|
||||
}
|
||||
if err := p.ValidateRows(ctx, rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if options.PrepareRows != nil {
|
||||
if err := options.PrepareRows(ctx, rows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
result := &Result{
|
||||
ImportStatus: consts.ImportStatusSuccess,
|
||||
BatchNo: req.BatchNo,
|
||||
BatchType: p.Config().BatchType,
|
||||
TotalRows: len(rows),
|
||||
}
|
||||
if importOss != nil {
|
||||
result.ImportFileOssId = importOss.OssId
|
||||
result.ImportFileUrl = importOss.Url
|
||||
}
|
||||
|
||||
if err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
sourceBatchNo := strings.TrimSpace(req.SourceBatchNo)
|
||||
if sourceBatchNo == "" {
|
||||
sourceBatchNo = req.BatchNo
|
||||
}
|
||||
if _, err := dao.BatchMain.Ctx(ctx).TX(tx).Data(do.BatchMain{
|
||||
BatchNo: req.BatchNo,
|
||||
OrderSn: "",
|
||||
BatchType: p.Config().BatchType,
|
||||
BatchStatus: consts.BatchAsyncStatusPending,
|
||||
BatchAsyncStatus: consts.BatchAsyncStatusPending,
|
||||
SourceType: req.SourceType,
|
||||
SourceBatchNo: sourceBatchNo,
|
||||
IsCrossDept: 0,
|
||||
TargetDeptId: req.TargetDeptId,
|
||||
DeptId: req.Operator.DeptId,
|
||||
TotalCount: len(rows),
|
||||
ValidCount: 0,
|
||||
SuccessCount: 0,
|
||||
FailCount: 0,
|
||||
RunResult: consts.BatchRunResultPending,
|
||||
FailReason: "",
|
||||
BatchInfo: p.BuildBatchInfo(ctx, *req, result, importOss, nil, 0),
|
||||
TenantId: req.Operator.TenantId,
|
||||
CreatedDept: req.Operator.DeptId,
|
||||
CreatedBy: req.Operator.UserId,
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "写入待处理批次失败")
|
||||
}
|
||||
|
||||
details := make([]do.BatchDetail, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
details = append(details, do.BatchDetail{
|
||||
BatchNo: req.BatchNo,
|
||||
BatchType: p.Config().BatchType,
|
||||
DetailStatus: consts.BatchDetailStatusPending,
|
||||
BlankOutStatus: consts.BlankOutStatusNo,
|
||||
RowNo: row.GetRowNo(),
|
||||
ItemId: row.GetItemID(),
|
||||
ErrorCode: "",
|
||||
ErrorReason: strings.Join(row.GetErrorReasons(), ";"),
|
||||
Snapshot: row.Snapshot(),
|
||||
TenantId: req.Operator.TenantId,
|
||||
CreatedDept: req.Operator.DeptId,
|
||||
CreatedBy: req.Operator.UserId,
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
})
|
||||
}
|
||||
_, err := dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert()
|
||||
return gerror.Wrap(err, "写入待处理批次明细失败")
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func SaveAsyncResultFiles(
|
||||
ctx context.Context,
|
||||
req SubmitRequest,
|
||||
p Processor,
|
||||
result *Result,
|
||||
rows []Row,
|
||||
importOss *model.SysOssSaveContentModel,
|
||||
startTime, endTime *gtime.Time,
|
||||
) (*model.SysOssSaveContentModel, int64, error) {
|
||||
resultOss, downloadId, err := saveResultExcelAndDownload(ctx, req, p, result, rows, importOss, startTime, endTime)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
result.ResultDownloadId = downloadId
|
||||
result.ResultFileOssId = resultOss.OssId
|
||||
result.ResultFileUrl = resultOss.Url
|
||||
return resultOss, downloadId, nil
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package batchimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
const (
|
||||
batchNoPrefixLength = 3
|
||||
batchNoSequenceLength = 3
|
||||
batchNoSequenceMax = 999
|
||||
batchNoSequenceTTL = 48 * time.Hour
|
||||
batchNoSequenceRedisPrefix = "iotlcm:batch:no:seq:"
|
||||
randomNoSuffixLength = 8
|
||||
randomNoAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
||||
)
|
||||
|
||||
func NextBatchNo(prefix string) (string, error) {
|
||||
return NextBatchNoContext(context.Background(), prefix)
|
||||
}
|
||||
|
||||
func NextBatchNoContext(ctx context.Context, prefix string) (string, error) {
|
||||
prefix, err := normalizeBatchNoPrefix(prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
now := time.Now()
|
||||
seq, err := nextDailyBatchSequence(ctx, now)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return formatBatchNo(prefix, now, seq)
|
||||
}
|
||||
|
||||
func NextRandomNo(prefix string) (string, error) {
|
||||
prefix = strings.TrimSpace(strings.ToUpper(prefix))
|
||||
if prefix == "" {
|
||||
return "", gerror.New("编号前缀不能为空")
|
||||
}
|
||||
|
||||
random, err := randomNoSuffix(randomNoSuffixLength)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s", prefix, time.Now().Format("20060102150405"), random), nil
|
||||
}
|
||||
|
||||
func normalizeBatchNoPrefix(prefix string) (string, error) {
|
||||
prefix = strings.TrimSpace(strings.ToUpper(prefix))
|
||||
if prefix == "" {
|
||||
return "", gerror.New("批次号前缀不能为空")
|
||||
}
|
||||
if len(prefix) != batchNoPrefixLength {
|
||||
return "", gerror.Newf("批次号前缀必须为%d位", batchNoPrefixLength)
|
||||
}
|
||||
return prefix, nil
|
||||
}
|
||||
|
||||
func nextDailyBatchSequence(ctx context.Context, now time.Time) (int64, error) {
|
||||
key := batchNoSequenceKey(now)
|
||||
script := `
|
||||
local v = redis.call("INCR", KEYS[1])
|
||||
if v == 1 then
|
||||
redis.call("EXPIRE", KEYS[1], ARGV[1])
|
||||
end
|
||||
return v`
|
||||
res, err := g.Redis().Do(ctx, "EVAL", script, 1, key, int(batchNoSequenceTTL.Seconds()))
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "生成批次号序号失败")
|
||||
}
|
||||
seq := res.Int64()
|
||||
if seq <= 0 {
|
||||
return 0, gerror.New("生成批次号序号失败")
|
||||
}
|
||||
if seq > batchNoSequenceMax {
|
||||
return 0, gerror.Newf("当日批次号序号超过%03d", batchNoSequenceMax)
|
||||
}
|
||||
return seq, nil
|
||||
}
|
||||
|
||||
func batchNoSequenceKey(now time.Time) string {
|
||||
return batchNoSequenceRedisPrefix + now.Format("20060102")
|
||||
}
|
||||
|
||||
func formatBatchNo(prefix string, now time.Time, seq int64) (string, error) {
|
||||
if seq <= 0 || seq > batchNoSequenceMax {
|
||||
return "", gerror.Newf("批次号序号必须在1-%03d之间", batchNoSequenceMax)
|
||||
}
|
||||
return fmt.Sprintf("%s%s%0*d", prefix, now.Format("20060102150405"), batchNoSequenceLength, seq), nil
|
||||
}
|
||||
|
||||
func randomNoSuffix(length int) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", gerror.New("随机码长度必须大于0")
|
||||
}
|
||||
|
||||
buf := make([]byte, length)
|
||||
randomBytes := make([]byte, length)
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
return "", gerror.Wrap(err, "生成随机码失败")
|
||||
}
|
||||
|
||||
alphabetLen := byte(len(randomNoAlphabet))
|
||||
for i, b := range randomBytes {
|
||||
buf[i] = randomNoAlphabet[b%alphabetLen]
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package batchimport
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNormalizeBatchNoPrefix(t *testing.T) {
|
||||
prefix, err := normalizeBatchNoPrefix(" inb ")
|
||||
if err != nil {
|
||||
t.Fatalf("normalize prefix: %v", err)
|
||||
}
|
||||
if prefix != "INB" {
|
||||
t.Fatalf("unexpected prefix: %s", prefix)
|
||||
}
|
||||
|
||||
if _, err = normalizeBatchNoPrefix("IN"); err == nil {
|
||||
t.Fatal("expected error for non-three-character prefix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatBatchNo(t *testing.T) {
|
||||
now := time.Date(2026, 8, 10, 12, 34, 56, 789000000, time.Local)
|
||||
batchNo, err := formatBatchNo("REN", now, 7)
|
||||
if err != nil {
|
||||
t.Fatalf("format batch no: %v", err)
|
||||
}
|
||||
if batchNo != "REN20260810123456007" {
|
||||
t.Fatalf("unexpected batch no: %s", batchNo)
|
||||
}
|
||||
if len(batchNo) != 20 {
|
||||
t.Fatalf("unexpected batch no length: %d", len(batchNo))
|
||||
}
|
||||
if strings.Contains(batchNo, "789") {
|
||||
t.Fatalf("batch no should not include milliseconds: %s", batchNo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatBatchNoRejectsOverflowSequence(t *testing.T) {
|
||||
_, err := formatBatchNo("REN", time.Now(), 1000)
|
||||
if err == nil {
|
||||
t.Fatal("expected overflow sequence error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
package batchimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/hash"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/iot_excelize"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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/gtime"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func SubmitAndRun(ctx context.Context, req SubmitRequest, p Processor) (*Result, error) {
|
||||
cfg := p.Config()
|
||||
if req.File == nil {
|
||||
return nil, gerror.New("请选择上传文件")
|
||||
}
|
||||
if err := service.SysOss().CheckType(ctx, consts.SysUploadFileTypeFile, req.File); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.File.Size > cfg.MaxFileSize {
|
||||
return nil, gerror.Newf("上传文件大小超过限制,最大为%dM", cfg.MaxFileSize>>20)
|
||||
}
|
||||
|
||||
batchNo, err := NextBatchNoContext(ctx, cfg.BatchNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.BatchNo = batchNo
|
||||
result := &Result{
|
||||
ImportStatus: consts.ImportStatusFailed,
|
||||
BatchNo: batchNo,
|
||||
}
|
||||
|
||||
content, err := readUploadContent(req.File)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
importOss, err := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{
|
||||
Content: content,
|
||||
FileName: batchNo + "-import.xlsx",
|
||||
NewFileType: consts.SysUploadNewFileTypeUserFile,
|
||||
SubDirName: cfg.ImportDir,
|
||||
NotAddDate: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "保存原始Excel失败")
|
||||
}
|
||||
result.ImportFileOssId = importOss.OssId
|
||||
result.ImportFileUrl = importOss.Url
|
||||
|
||||
rows, err := p.ParseRows(ctx, importOss.Path)
|
||||
if err != nil {
|
||||
result.FailReason = userFacingFailReason(err)
|
||||
if err = archiveFailedBatchAfterImport(ctx, req, p, result, nil, importOss); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
result.TotalRows = len(rows)
|
||||
|
||||
if err = p.ValidateRows(ctx, rows); err != nil {
|
||||
for _, row := range rows {
|
||||
row.AddErrorReason(userFacingFailReason(err))
|
||||
}
|
||||
}
|
||||
fillResultCounts(result, rows)
|
||||
|
||||
startTime := gtime.Now()
|
||||
endTime := gtime.Now()
|
||||
if err = saveBatchAndExecute(ctx, &req, p, result, rows, importOss, startTime, endTime); err != nil {
|
||||
markRowsFailed(result, rows, userFacingFailReason(err))
|
||||
if err = saveBatchAndExecute(ctx, &req, p, result, rows, importOss, startTime, endTime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result.ExecutionResult = req.ExecutionResult
|
||||
|
||||
resultOss, downloadId, err := saveResultExcelAndDownload(ctx, req, p, result, rows, importOss, startTime, endTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.ResultDownloadId = downloadId
|
||||
result.ResultFileOssId = resultOss.OssId
|
||||
result.ResultFileUrl = resultOss.Url
|
||||
|
||||
if err = updateBatchResultFileInfo(ctx, req, p, result, importOss, resultOss, downloadId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func RunRows(ctx context.Context, req SubmitRequest, p Processor, rows []Row, sourceName string) (*Result, error) {
|
||||
cfg := p.Config()
|
||||
batchNo, err := NextBatchNoContext(ctx, cfg.BatchNoPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.BatchNo = batchNo
|
||||
result := &Result{
|
||||
ImportStatus: consts.ImportStatusFailed,
|
||||
BatchNo: batchNo,
|
||||
TotalRows: len(rows),
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
result.FailReason = "导入数据为空"
|
||||
} else if err = p.ValidateRows(ctx, rows); err != nil {
|
||||
for _, row := range rows {
|
||||
row.AddErrorReason(userFacingFailReason(err))
|
||||
}
|
||||
}
|
||||
fillResultCounts(result, rows)
|
||||
if result.FailReason != "" {
|
||||
result.ImportStatus = consts.ImportStatusFailed
|
||||
result.InsertedRows = 0
|
||||
}
|
||||
|
||||
startTime := gtime.Now()
|
||||
endTime := gtime.Now()
|
||||
importOss := &model.SysOssSaveContentModel{SysOss: &entity.SysOss{}}
|
||||
if sourceName != "" {
|
||||
importOss.OriginalName = sourceName
|
||||
}
|
||||
if err = saveBatchAndExecute(ctx, &req, p, result, rows, importOss, startTime, endTime); err != nil {
|
||||
markRowsFailed(result, rows, userFacingFailReason(err))
|
||||
if err = saveBatchAndExecute(ctx, &req, p, result, rows, importOss, startTime, endTime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result.ExecutionResult = req.ExecutionResult
|
||||
|
||||
resultOss, downloadId, err := saveResultExcelAndDownload(ctx, req, p, result, rows, importOss, startTime, endTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.ResultDownloadId = downloadId
|
||||
result.ResultFileOssId = resultOss.OssId
|
||||
result.ResultFileUrl = resultOss.Url
|
||||
|
||||
if err = updateBatchResultFileInfo(ctx, req, p, result, importOss, resultOss, downloadId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func archiveFailedBatchAfterImport(ctx context.Context, req SubmitRequest, p Processor, result *Result, rows []Row, importOss *model.SysOssSaveContentModel) error {
|
||||
result.ImportStatus = consts.ImportStatusFailed
|
||||
result.InsertedRows = 0
|
||||
if result.FailReason == "" {
|
||||
result.FailReason = "导入失败"
|
||||
}
|
||||
|
||||
startTime := gtime.Now()
|
||||
endTime := gtime.Now()
|
||||
if err := saveBatchAndExecute(ctx, &req, p, result, rows, importOss, startTime, endTime); err != nil {
|
||||
return err
|
||||
}
|
||||
result.ExecutionResult = req.ExecutionResult
|
||||
|
||||
resultOss, downloadId, err := saveResultExcelAndDownload(ctx, req, p, result, rows, importOss, startTime, endTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.ResultDownloadId = downloadId
|
||||
result.ResultFileOssId = resultOss.OssId
|
||||
result.ResultFileUrl = resultOss.Url
|
||||
|
||||
return updateBatchResultFileInfo(ctx, req, p, result, importOss, resultOss, downloadId)
|
||||
}
|
||||
|
||||
func readUploadContent(file *ghttp.UploadFile) ([]byte, error) {
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "打开上传文件失败")
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "读取上传文件失败")
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func fillResultCounts(result *Result, rows []Row) {
|
||||
failCount := 0
|
||||
for _, row := range rows {
|
||||
if len(row.GetErrorReasons()) > 0 {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
result.ErrorRows = failCount
|
||||
result.ValidRows = result.TotalRows - failCount
|
||||
if result.ValidRows < 0 {
|
||||
result.ValidRows = 0
|
||||
}
|
||||
if failCount == 0 {
|
||||
result.ImportStatus = consts.ImportStatusSuccess
|
||||
result.InsertedRows = len(rows)
|
||||
return
|
||||
}
|
||||
result.ImportStatus = consts.ImportStatusFailed
|
||||
result.InsertedRows = 0
|
||||
}
|
||||
|
||||
func markRowsFailed(result *Result, rows []Row, reason string) {
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
if reason == "" {
|
||||
reason = "业务处理失败"
|
||||
}
|
||||
for _, row := range rows {
|
||||
row.AddErrorReason(reason)
|
||||
}
|
||||
result.ImportStatus = consts.ImportStatusFailed
|
||||
result.FailReason = reason
|
||||
result.TotalRows = len(rows)
|
||||
result.ValidRows = 0
|
||||
result.ErrorRows = len(rows)
|
||||
result.InsertedRows = 0
|
||||
}
|
||||
|
||||
// saveBatchAndExecute writes stock data only when all rows pass validation.
|
||||
// Failed validation keeps the batch and result file, but skips business inserts.
|
||||
func saveBatchAndExecute(ctx context.Context, req *SubmitRequest, p Processor, result *Result, rows []Row, importOss *model.SysOssSaveContentModel, startTime, endTime *gtime.Time) error {
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
now := gtime.Now()
|
||||
failReason := result.FailReason
|
||||
if failReason == "" && result.ErrorRows > 0 {
|
||||
failReason = "存在校验失败数据,本次未写入"
|
||||
}
|
||||
|
||||
batchType := batchTypeByImportStatus(req, p, result.ImportStatus)
|
||||
result.BatchType = batchType
|
||||
if result.ImportStatus == consts.ImportStatusSuccess && len(rows) > 0 && req.BeforeSave != nil {
|
||||
if err := req.BeforeSave(ctx, tx, req, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sourceBatchNo := req.SourceBatchNo
|
||||
if sourceBatchNo == "" {
|
||||
sourceBatchNo = result.BatchNo
|
||||
}
|
||||
|
||||
if _, err := dao.BatchMain.Ctx(ctx).TX(tx).Data(do.BatchMain{
|
||||
BatchNo: result.BatchNo,
|
||||
OrderSn: "",
|
||||
BatchType: batchType,
|
||||
BatchStatus: batchStatusByImportStatus(result.ImportStatus),
|
||||
BatchAsyncStatus: batchStatusByImportStatus(result.ImportStatus),
|
||||
SourceType: req.SourceType,
|
||||
SourceBatchNo: sourceBatchNo,
|
||||
IsCrossDept: 0,
|
||||
TargetDeptId: req.TargetDeptId,
|
||||
DeptId: req.Operator.DeptId,
|
||||
TotalCount: result.TotalRows,
|
||||
ValidCount: result.ValidRows,
|
||||
SuccessCount: result.InsertedRows,
|
||||
FailCount: result.ErrorRows,
|
||||
RunResult: runResultByImportStatus(result.ImportStatus),
|
||||
FailReason: failReason,
|
||||
BatchInfo: p.BuildBatchInfo(ctx, *req, result, importOss, nil, 0),
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
TenantId: req.Operator.TenantId,
|
||||
CreatedDept: req.Operator.DeptId,
|
||||
CreatedBy: req.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "写入批次主表失败")
|
||||
}
|
||||
|
||||
details := make([]do.BatchDetail, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
details = append(details, do.BatchDetail{
|
||||
BatchNo: result.BatchNo,
|
||||
BatchType: batchType,
|
||||
DetailStatus: detailStatus(row),
|
||||
BlankOutStatus: consts.BlankOutStatusNo,
|
||||
RowNo: row.GetRowNo(),
|
||||
ItemId: row.GetItemID(),
|
||||
ErrorCode: "",
|
||||
ErrorReason: strings.Join(row.GetErrorReasons(), ";"),
|
||||
Snapshot: row.Snapshot(),
|
||||
TenantId: req.Operator.TenantId,
|
||||
CreatedDept: req.Operator.DeptId,
|
||||
CreatedBy: req.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
if len(details) > 0 {
|
||||
if _, err := dao.BatchDetail.Ctx(ctx).TX(tx).Data(details).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "写入批次明细失败")
|
||||
}
|
||||
}
|
||||
|
||||
if result.ImportStatus == consts.ImportStatusSuccess && len(rows) > 0 && !req.SkipExecute {
|
||||
if err := p.Execute(ctx, tx, req, rows); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func saveResultExcelAndDownload(ctx context.Context, req SubmitRequest, p Processor, result *Result, rows []Row, importOss *model.SysOssSaveContentModel, startTime, endTime *gtime.Time) (*model.SysOssSaveContentModel, int64, error) {
|
||||
cfg := p.Config()
|
||||
resultRows := make([]iot_excelize.BatchResultRow, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
resultRows = append(resultRows, iot_excelize.BatchResultRow{
|
||||
RowNo: row.GetRowNo(),
|
||||
ItemID: row.GetItemID(),
|
||||
Status: displayRowStatus(row),
|
||||
ErrorReason: strings.Join(row.GetErrorReasons(), ";"),
|
||||
})
|
||||
}
|
||||
|
||||
importName := ""
|
||||
importOssId := ""
|
||||
if importOss != nil {
|
||||
importName = importOss.OriginalName
|
||||
if importOss.OssId > 0 {
|
||||
importOssId = fmt.Sprintf("%d", importOss.OssId)
|
||||
}
|
||||
}
|
||||
|
||||
infos := []iot_excelize.BatchInfoItem{
|
||||
{Field: "批次号", Value: result.BatchNo},
|
||||
{Field: "批次类型", Value: taskTypeName(p)},
|
||||
{Field: "处理结果", Value: displayImportStatus(result.ImportStatus)},
|
||||
{Field: "总行数", Value: fmt.Sprintf("%d", result.TotalRows)},
|
||||
{Field: "校验通过行数", Value: fmt.Sprintf("%d", result.ValidRows)},
|
||||
{Field: "失败行数", Value: fmt.Sprintf("%d", result.ErrorRows)},
|
||||
{Field: "实际写入行数", Value: fmt.Sprintf("%d", result.InsertedRows)},
|
||||
{Field: "原始文件名", Value: importName},
|
||||
{Field: "原始文件OSS ID", Value: importOssId},
|
||||
{Field: "结果文件OSS ID", Value: ""},
|
||||
{Field: "操作人", Value: fmt.Sprintf("%d", req.Operator.UserId)},
|
||||
{Field: "开始时间", Value: formatGTime(startTime)},
|
||||
{Field: "结束时间", Value: formatGTime(endTime)},
|
||||
{Field: "备注", Value: resultRemark(result)},
|
||||
}
|
||||
|
||||
f, err := iot_excelize.BuildBatchResultExcel(resultRows, infos)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
reader, err := iot_excelize.ToReadSeeker(f)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
content, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
resultOss, err := service.SysOss().SaveContent(ctx, &model.SysOssSaveContentParam{
|
||||
Content: content,
|
||||
FileName: result.BatchNo + "-result.xlsx",
|
||||
NewFileType: consts.SysUploadNewFileTypeUserFile,
|
||||
SubDirName: cfg.ResultDir,
|
||||
NotAddDate: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "保存结果Excel失败")
|
||||
}
|
||||
if err = fillResultOssId(ctx, req, resultOss); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
downloadId, err := dao.TResourceBatchDownload.Ctx(ctx).Data(do.TResourceBatchDownload{
|
||||
BatchNo: result.BatchNo,
|
||||
TaskName: cfg.TaskName,
|
||||
ResourceType: consts.ResourceTypeExcel,
|
||||
ExportScene: cfg.ExportScene,
|
||||
TotalCount: result.TotalRows,
|
||||
SuccessCount: result.InsertedRows,
|
||||
FailCount: result.ErrorRows,
|
||||
DownloadStatus: consts.DownloadStatusGenerated,
|
||||
OssId: resultOss.OssId,
|
||||
FileName: resultOss.FileName,
|
||||
FileExt: resultOss.FileSuffix,
|
||||
FileUrl: resultOss.Url,
|
||||
FilePath: resultOss.Path,
|
||||
FileSize: resultOss.FileSize,
|
||||
FileMd5: resultOss.Md5,
|
||||
DownloadCount: 0,
|
||||
MaxDownloadCount: 0,
|
||||
DeptId: req.Operator.DeptId,
|
||||
StartTime: startTime,
|
||||
FinishTime: now,
|
||||
TenantId: req.Operator.TenantId,
|
||||
CreatedDept: req.Operator.DeptId,
|
||||
CreatedBy: req.Operator.UserId,
|
||||
CreatedAt: now,
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
UpdatedAt: now,
|
||||
}).InsertAndGetId()
|
||||
if err != nil {
|
||||
return nil, 0, gerror.Wrap(err, "写入结果下载任务失败")
|
||||
}
|
||||
|
||||
return resultOss, downloadId, nil
|
||||
}
|
||||
|
||||
func fillResultOssId(ctx context.Context, req SubmitRequest, resultOss *model.SysOssSaveContentModel) error {
|
||||
f, err := excelize.OpenFile(resultOss.Path)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "打开结果Excel失败")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
const batchInfoSheet = "批次信息"
|
||||
rows, err := f.GetRows(batchInfoSheet)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取结果Excel批次信息失败")
|
||||
}
|
||||
|
||||
found := false
|
||||
for i, row := range rows {
|
||||
if len(row) == 0 || strings.TrimSpace(row[0]) != "结果文件OSS ID" {
|
||||
continue
|
||||
}
|
||||
cell, err := excelize.CoordinatesToCellName(2, i+1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.SetCellStr(batchInfoSheet, cell, fmt.Sprintf("%d", resultOss.OssId)); err != nil {
|
||||
return err
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
return gerror.New("结果Excel缺少结果文件OSS ID字段")
|
||||
}
|
||||
if err = f.SaveAs(resultOss.Path); err != nil {
|
||||
return gerror.Wrap(err, "回写结果Excel OSS ID失败")
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(resultOss.Path)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取结果Excel文件信息失败")
|
||||
}
|
||||
md5, _ := gmd5.EncryptFile(resultOss.Path)
|
||||
crc16 := hash.Crc16ChecksumFile(resultOss.Path)
|
||||
sum16 := hash.Sum16ChecksumFile(resultOss.Path)
|
||||
|
||||
resultOss.FileSize = int(fileInfo.Size())
|
||||
resultOss.Md5 = md5
|
||||
resultOss.FileCrc16 = int(crc16)
|
||||
resultOss.FileSum = int(sum16)
|
||||
|
||||
_, err = dao.SysOss.Ctx(ctx).
|
||||
Where(dao.SysOss.Columns().OssId, resultOss.OssId).
|
||||
Data(do.SysOss{
|
||||
Md5: resultOss.Md5,
|
||||
FileSize: resultOss.FileSize,
|
||||
FileCrc16: resultOss.FileCrc16,
|
||||
FileSum: resultOss.FileSum,
|
||||
UpdatedAt: gtime.Now(),
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新结果Excel文件信息失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateBatchResultFileInfo(ctx context.Context, req SubmitRequest, p Processor, result *Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) error {
|
||||
_, err := dao.BatchMain.Ctx(ctx).
|
||||
Where(dao.BatchMain.Columns().BatchNo, result.BatchNo).
|
||||
Data(do.BatchMain{
|
||||
BatchInfo: p.BuildBatchInfo(ctx, req, result, importOss, resultOss, downloadId),
|
||||
UpdatedBy: req.Operator.UserId,
|
||||
UpdatedAt: gtime.Now(),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新批次结果文件信息失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func batchStatusByImportStatus(importStatus string) int {
|
||||
if importStatus == consts.ImportStatusSuccess {
|
||||
return consts.BatchAsyncStatusSuccess
|
||||
}
|
||||
return consts.BatchAsyncStatusFailed
|
||||
}
|
||||
|
||||
func runResultByImportStatus(importStatus string) int {
|
||||
if importStatus == consts.ImportStatusSuccess {
|
||||
return consts.BatchRunResultSuccess
|
||||
}
|
||||
return consts.BatchRunResultFailed
|
||||
}
|
||||
|
||||
func batchTypeByImportStatus(req *SubmitRequest, p Processor, importStatus string) int {
|
||||
if req == nil {
|
||||
return p.Config().BatchType
|
||||
}
|
||||
if importStatus != consts.ImportStatusSuccess && req.FailureBatchType > 0 {
|
||||
return req.FailureBatchType
|
||||
}
|
||||
if req.BatchType > 0 {
|
||||
return req.BatchType
|
||||
}
|
||||
return p.Config().BatchType
|
||||
}
|
||||
|
||||
func detailStatus(row Row) int {
|
||||
if len(row.GetErrorReasons()) > 0 {
|
||||
return consts.BatchDetailStatusFailed
|
||||
}
|
||||
return consts.BatchDetailStatusSuccess
|
||||
}
|
||||
|
||||
func displayRowStatus(row Row) string {
|
||||
if len(row.GetErrorReasons()) > 0 {
|
||||
return "失败"
|
||||
}
|
||||
return "成功"
|
||||
}
|
||||
|
||||
func displayImportStatus(status string) string {
|
||||
if status == consts.ImportStatusSuccess {
|
||||
return "成功"
|
||||
}
|
||||
return "失败"
|
||||
}
|
||||
|
||||
func resultRemark(result *Result) string {
|
||||
if result != nil && result.ImportStatus == consts.ImportStatusSuccess {
|
||||
return "导入成功"
|
||||
}
|
||||
if result != nil && result.FailReason != "" {
|
||||
return result.FailReason
|
||||
}
|
||||
return "存在校验失败数据,本次未写入"
|
||||
}
|
||||
|
||||
func userFacingFailReason(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
reason := strings.TrimSpace(err.Error())
|
||||
if reason == "" {
|
||||
return "导入失败"
|
||||
}
|
||||
const maxLen = 500
|
||||
if len([]rune(reason)) <= maxLen {
|
||||
return reason
|
||||
}
|
||||
runes := []rune(reason)
|
||||
return string(runes[:maxLen])
|
||||
}
|
||||
|
||||
func taskTypeName(p Processor) string {
|
||||
taskName := p.Config().TaskName
|
||||
return strings.TrimSuffix(taskName, "结果")
|
||||
}
|
||||
|
||||
func formatGTime(t *gtime.Time) string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
return t.Format("Y-m-d H:i:s")
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package batchimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type SubmitRequest struct {
|
||||
File *ghttp.UploadFile
|
||||
BizParams any
|
||||
SourceType int
|
||||
SourceBatchNo string
|
||||
TargetDeptId int64
|
||||
Operator OperatorContext
|
||||
BatchNo string
|
||||
BatchType int
|
||||
FailureBatchType int
|
||||
SkipExecute bool
|
||||
BeforeSave func(ctx context.Context, tx gdb.TX, req *SubmitRequest, rows []Row) error
|
||||
ExecutionResult any
|
||||
}
|
||||
|
||||
type OperatorContext struct {
|
||||
TenantId string
|
||||
DeptId int64
|
||||
UserId int64
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
ImportStatus string
|
||||
BatchNo string
|
||||
BatchType int
|
||||
FailReason string
|
||||
ImportFileOssId int64
|
||||
ImportFileUrl string
|
||||
TotalRows int
|
||||
ValidRows int
|
||||
ErrorRows int
|
||||
InsertedRows int
|
||||
ResultDownloadId int64
|
||||
ResultFileOssId int64
|
||||
ResultFileUrl string
|
||||
ExecutionResult any
|
||||
}
|
||||
|
||||
type Row interface {
|
||||
GetRowNo() int
|
||||
GetItemID() string
|
||||
GetErrorReasons() []string
|
||||
AddErrorReason(reason string)
|
||||
Snapshot() *gjson.Json
|
||||
}
|
||||
|
||||
type ProcessorConfig struct {
|
||||
Code string
|
||||
BatchType int
|
||||
BatchNoPrefix string
|
||||
TaskName string
|
||||
ExportScene int
|
||||
ImportDir string
|
||||
ResultDir string
|
||||
MaxFileSize int64
|
||||
TemplateHeaders []string
|
||||
RequiredHeaders []string
|
||||
}
|
||||
|
||||
type Processor interface {
|
||||
Config() ProcessorConfig
|
||||
ParseRows(ctx context.Context, filePath string) ([]Row, error)
|
||||
ValidateRows(ctx context.Context, rows []Row) error
|
||||
Execute(ctx context.Context, tx gdb.TX, req *SubmitRequest, rows []Row) error
|
||||
BuildBatchInfo(ctx context.Context, req SubmitRequest, result *Result, importOss *model.SysOssSaveContentModel, resultOss *model.SysOssSaveContentModel, downloadId int64) *gjson.Json
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
BatchNo string `json:"batchNo"`
|
||||
BatchType int `json:"batchType"`
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Package bcache
|
||||
// @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 bcache
|
||||
@@ -0,0 +1,77 @@
|
||||
// Package bcache
|
||||
// @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 bcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/cache"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// 设置系统认证token缓存
|
||||
func SetSysAuthToken(ctx context.Context, userId int64, uuid string, token string, timeout time.Duration) (err error) {
|
||||
key := fmt.Sprintf(consts.KeySysAuthToken, userId, uuid)
|
||||
err = cache.Instance().Set(ctx, key, token, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取系统认证token缓存
|
||||
func GetSysAuthToken(ctx context.Context, userId int64, uuid string) (token string, err error) {
|
||||
key := fmt.Sprintf(consts.KeySysAuthToken, userId, uuid)
|
||||
t, err := cache.Instance().Get(ctx, key)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "bcache.GetSysAuthToken Get failed,error: %v, userId: %d, uuid: %s, key: %s", err, userId, uuid, key)
|
||||
return "", err
|
||||
}
|
||||
return t.String(), nil
|
||||
}
|
||||
|
||||
// 删除系统认证token缓存
|
||||
func DelSysAuthToken(ctx context.Context, userId int64, uuid string) (err error) {
|
||||
key := fmt.Sprintf(consts.KeySysAuthToken, userId, uuid)
|
||||
_, err = cache.Instance().Remove(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 设置系统认证token拒绝缓存
|
||||
func SetSysAuthTokenReject(ctx context.Context, userId int64, uuid string, token string, timeout time.Duration) (err error) {
|
||||
key := fmt.Sprintf(consts.KeySysAuthTokenReject, userId, uuid)
|
||||
err = cache.Instance().Set(ctx, key, token, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取系统认证token拒绝缓存
|
||||
func GetSysAuthTokenReject(ctx context.Context, userId int64, uuid string) (token string, err error) {
|
||||
key := fmt.Sprintf(consts.KeySysAuthTokenReject, userId, uuid)
|
||||
t, err := cache.Instance().Get(ctx, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return t.String(), nil
|
||||
}
|
||||
|
||||
// 删除系统认证token拒绝缓存
|
||||
func DelSysAuthTokenReject(ctx context.Context, userId int64, uuid string) (err error) {
|
||||
key := fmt.Sprintf(consts.KeySysAuthTokenReject, userId, uuid)
|
||||
_, err = cache.Instance().Remove(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Package cache
|
||||
// @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 cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/cache/file"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
)
|
||||
|
||||
// cache 缓存驱动
|
||||
var cacheVal *gcache.Cache
|
||||
|
||||
// Instance 缓存实例
|
||||
func Instance() *gcache.Cache {
|
||||
if cacheVal == nil {
|
||||
panic("cache uninitialized.")
|
||||
}
|
||||
return cacheVal
|
||||
}
|
||||
|
||||
// SetAdapter 设置缓存适配器
|
||||
func SetAdapter(ctx context.Context) {
|
||||
var cacheAdapter gcache.Adapter
|
||||
adapter := g.Cfg().MustGet(ctx, "cache.adapter").String()
|
||||
fileDir := g.Cfg().MustGet(ctx, "cache.fileDir").String()
|
||||
|
||||
switch adapter {
|
||||
case "redis":
|
||||
cacheAdapter = gcache.NewAdapterRedis(g.Redis())
|
||||
case "file":
|
||||
if fileDir == "" {
|
||||
g.Log().Fatal(ctx, "file path must be configured for file caching.")
|
||||
return
|
||||
}
|
||||
|
||||
if !gfile.Exists(fileDir) {
|
||||
if err := gfile.Mkdir(fileDir); err != nil {
|
||||
g.Log().Fatalf(ctx, "failed to create the cache directory. procedure, err:%+v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
cacheAdapter = file.NewAdapterFile(fileDir)
|
||||
default:
|
||||
cacheAdapter = gcache.NewAdapterMemory()
|
||||
}
|
||||
|
||||
// 数据库缓存,默认和通用缓冲驱动一致,如果你不想使用默认的,可以自行调整
|
||||
g.DB().GetCache().SetAdapter(cacheAdapter)
|
||||
|
||||
// 通用缓存
|
||||
cacheVal = gcache.New()
|
||||
cacheVal.SetAdapter(cacheAdapter)
|
||||
}
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
// Package file
|
||||
// @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 file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type (
|
||||
// AdapterFile gcache适配器使用文件的实现
|
||||
AdapterFile struct {
|
||||
dir string
|
||||
mutex sync.RWMutex // 添加互斥锁以支持并发操作
|
||||
}
|
||||
|
||||
fileContent struct {
|
||||
Duration int64 `json:"duration"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
const perm = 0o666 //设置文件所有者、同用户组成员和其他用户都可以读写该文件,但都不能执。
|
||||
|
||||
var (
|
||||
CacheExpiredErr = errors.New("cache expired")
|
||||
)
|
||||
|
||||
// NewAdapterFile creates and returns a new memory cache object.
|
||||
func NewAdapterFile(dir string) gcache.Adapter {
|
||||
return &AdapterFile{
|
||||
dir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Set(ctx context.Context, key interface{}, value interface{}, lifeTime time.Duration) (err error) {
|
||||
c.mutex.Lock() // 加锁
|
||||
defer c.mutex.Unlock() // 解锁
|
||||
fileKey := gconv.String(key)
|
||||
if value == nil || lifeTime < 0 {
|
||||
return c.Delete(fileKey)
|
||||
}
|
||||
return c.Save(fileKey, gconv.String(value), lifeTime)
|
||||
}
|
||||
|
||||
// SetMap 批量设置多个键值对,并且每个键值对的有效期是一样的
|
||||
func (c *AdapterFile) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) (err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
for key, value := range data {
|
||||
fileKey := gconv.String(key)
|
||||
if err = c.Save(fileKey, gconv.String(value), duration); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetIfNotExist 在指定的键不存在时设置其值
|
||||
func (c *AdapterFile) SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
fileKey := gconv.String(key)
|
||||
if c.Has(fileKey) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
err = c.Save(fileKey, gconv.String(value), duration)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// SetIfNotExistFunc 在指定的键不存在时,通过一个函数来生成值并设置它
|
||||
func (c *AdapterFile) SetIfNotExistFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
fileKey := gconv.String(key)
|
||||
if c.Has(fileKey) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
value, err := f(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = c.Save(fileKey, gconv.String(value), duration)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// SetIfNotExistFuncLock 与 SetIfNotExistFunc 类似,但它在调用生成值的函数时提供了额外的锁机制,以避免在生成值期间的并发问题。
|
||||
func (c *AdapterFile) SetIfNotExistFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
fileKey := gconv.String(key)
|
||||
if c.Has(fileKey) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 在这里加锁是为了确保在值生成期间不会有并发的写入操作
|
||||
value, err := f(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
err = c.Save(fileKey, gconv.String(value), duration)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Get(ctx context.Context, key interface{}) (*gvar.Var, error) {
|
||||
fetch, err := c.Fetch(gconv.String(key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gvar.New(fetch), nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) {
|
||||
result, err = c.Get(ctx, key)
|
||||
if err != nil && !errors.Is(err, CacheExpiredErr) {
|
||||
return nil, err
|
||||
}
|
||||
if result.IsNil() {
|
||||
return gvar.New(value), c.Set(ctx, key, value, duration)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetOrSetFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
|
||||
v, err := c.Get(ctx, key)
|
||||
if err != nil && !errors.Is(err, CacheExpiredErr) {
|
||||
return nil, err
|
||||
}
|
||||
if v.IsNil() {
|
||||
value, err := f(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return gvar.New(value), c.Set(ctx, key, value, duration)
|
||||
} else {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetOrSetFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
|
||||
return c.GetOrSetFunc(ctx, key, f, duration)
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Contains(ctx context.Context, key interface{}) (bool, error) {
|
||||
return c.Has(gconv.String(key)), nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Size(ctx context.Context) (size int, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Data 获取所有缓存的键值对
|
||||
func (c *AdapterFile) Data(ctx context.Context) (data map[interface{}]interface{}, err error) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
// 初始化返回的数据结构
|
||||
data = make(map[interface{}]interface{})
|
||||
|
||||
// 读取文件目录中的所有缓存文件
|
||||
files, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 遍历缓存文件,提取数据
|
||||
for _, file := range files {
|
||||
content, err := c.read(file.Name())
|
||||
if err != nil {
|
||||
continue // 忽略无法读取的文件
|
||||
}
|
||||
data[file.Name()] = content.Data
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Keys 获取所有缓存的键
|
||||
func (c *AdapterFile) Keys(ctx context.Context) (keys []interface{}, err error) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
files, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys = make([]interface{}, 0, len(files))
|
||||
for _, file := range files {
|
||||
keys = append(keys, file.Name())
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// Values 获取所有缓存的值
|
||||
func (c *AdapterFile) Values(ctx context.Context) (values []interface{}, err error) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
files, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
values = make([]interface{}, 0, len(files))
|
||||
for _, file := range files {
|
||||
content, err := c.read(file.Name())
|
||||
if err != nil {
|
||||
continue // 忽略无法读取的文件
|
||||
}
|
||||
values = append(values, content.Data)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// Update 更新一个指定键的缓存值,如果这个键不存在,则返回错误
|
||||
func (c *AdapterFile) Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
fileKey := gconv.String(key)
|
||||
content, err := c.read(fileKey)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if content == nil {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
oldValue = gvar.New(content.Data)
|
||||
err = c.Save(fileKey, gconv.String(value), time.Duration(content.Duration)*time.Second)
|
||||
return oldValue, true, err
|
||||
}
|
||||
|
||||
func (c *AdapterFile) UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
|
||||
var (
|
||||
v *gvar.Var
|
||||
oldTTL int64
|
||||
fileKey = gconv.String(key)
|
||||
)
|
||||
// TTL.
|
||||
expire, err := c.GetExpire(ctx, fileKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
oldTTL = int64(expire)
|
||||
if oldTTL == -2 {
|
||||
// It does not exist.
|
||||
return
|
||||
}
|
||||
oldDuration = time.Duration(oldTTL) * time.Second
|
||||
// DEL.
|
||||
if duration < 0 {
|
||||
err = c.Delete(fileKey)
|
||||
return
|
||||
}
|
||||
v, err = c.Get(ctx, fileKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = c.Set(ctx, fileKey, v.Val(), duration)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AdapterFile) GetExpire(ctx context.Context, key interface{}) (time.Duration, error) {
|
||||
content, err := c.read(gconv.String(key))
|
||||
if err != nil {
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
if content.Duration <= time.Now().Unix() {
|
||||
return -1, nil
|
||||
}
|
||||
return time.Duration(time.Now().Unix()-content.Duration) * time.Second, nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) {
|
||||
if len(keys) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// Retrieves the last key value.
|
||||
if lastValue, err = c.Get(ctx, gconv.String(keys[len(keys)-1])); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Deletes all given keys.
|
||||
err = c.DeleteMulti(gconv.Strings(keys)...)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Clear(ctx context.Context) error {
|
||||
return c.Flush()
|
||||
}
|
||||
|
||||
func (c *AdapterFile) Close(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *AdapterFile) createName(key string) string {
|
||||
h := sha256.New()
|
||||
_, _ = h.Write([]byte(key))
|
||||
hash := hex.EncodeToString(h.Sum(nil))
|
||||
return filepath.Join(c.dir, fmt.Sprintf("%s.cache", hash))
|
||||
}
|
||||
|
||||
func (c *AdapterFile) read(key string) (*fileContent, error) {
|
||||
rp := gfile.RealPath(c.createName(key))
|
||||
if rp == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
value, err := os.ReadFile(rp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := &fileContent{}
|
||||
if err := json.Unmarshal(value, content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if content.Duration == 0 {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
if content.Duration <= time.Now().Unix() {
|
||||
_ = c.Delete(key)
|
||||
return nil, CacheExpiredErr
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Has checks if the cached key exists into the File storage
|
||||
func (c *AdapterFile) Has(key string) bool {
|
||||
fc, err := c.read(key)
|
||||
return err == nil && fc != nil
|
||||
}
|
||||
|
||||
// Delete the cached key from File storage
|
||||
func (c *AdapterFile) Delete(key string) error {
|
||||
_, err := os.Stat(c.createName(key))
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(c.createName(key))
|
||||
}
|
||||
|
||||
// DeleteMulti the cached key from File storage
|
||||
func (c *AdapterFile) DeleteMulti(keys ...string) (err error) {
|
||||
for _, key := range keys {
|
||||
if err = c.Delete(key); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch retrieves the cached value from key of the File storage
|
||||
func (c *AdapterFile) Fetch(key string) (interface{}, error) {
|
||||
content, err := c.read(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if content == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return content.Data, nil
|
||||
}
|
||||
|
||||
// FetchMulti retrieve multiple cached values from keys of the File storage
|
||||
func (c *AdapterFile) FetchMulti(keys []string) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for _, key := range keys {
|
||||
if value, err := c.Fetch(key); err == nil {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Flush removes all cached keys of the File storage
|
||||
func (c *AdapterFile) Flush() error {
|
||||
dir, err := os.Open(c.dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = dir.Close()
|
||||
}()
|
||||
|
||||
names, _ := dir.Readdirnames(-1)
|
||||
|
||||
for _, name := range names {
|
||||
_ = os.Remove(filepath.Join(c.dir, name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save a value in File storage by key
|
||||
func (c *AdapterFile) Save(key string, value string, lifeTime time.Duration) error {
|
||||
duration := int64(0)
|
||||
|
||||
if lifeTime > 0 {
|
||||
duration = time.Now().Unix() + int64(lifeTime.Seconds())
|
||||
}
|
||||
|
||||
content := &fileContent{duration, value}
|
||||
|
||||
data, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(c.createName(key), data, perm)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// Package contexts
|
||||
// @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 contexts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"slices"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type ContextKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var contextKey = &ContextKey{
|
||||
name: consts.ContextKey,
|
||||
}
|
||||
|
||||
// Init初始化上下文对象指针到上下文对象中,以便后续的请求流程中可以修改。
|
||||
func Init(r *ghttp.Request, customCtx *model.Context) {
|
||||
r.SetCtxVar(contextKey, customCtx)
|
||||
}
|
||||
|
||||
// Set设置本地上下文
|
||||
func Set(ctx context.Context, customCtx *model.Context) context.Context {
|
||||
ctx = context.WithValue(ctx, contextKey, customCtx)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Get 获取上下文变量;
|
||||
func Get(ctx context.Context) *model.Context {
|
||||
customCtx, ok := ctx.Value(contextKey).(*model.Context)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return customCtx
|
||||
}
|
||||
|
||||
// Set 设置上下文变量
|
||||
func SetUser(ctx context.Context, user *model.Identity) {
|
||||
customCtx := Get(ctx)
|
||||
customCtx.User = user
|
||||
}
|
||||
|
||||
// GetUser 获取上下文变量中的用户信息
|
||||
func GetUser(ctx context.Context) *model.Identity {
|
||||
customCtx := Get(ctx)
|
||||
if customCtx == nil {
|
||||
return nil
|
||||
}
|
||||
return customCtx.User
|
||||
}
|
||||
|
||||
// GetUserId 获取上下文变量中的用户ID
|
||||
func GetUserId(ctx context.Context) int64 {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
return user.ID
|
||||
}
|
||||
|
||||
// GetUserUuid 获取上下文变量中的用户UUID
|
||||
func GetUserUuid(ctx context.Context) string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
return user.UUID
|
||||
}
|
||||
|
||||
// GetUserName 获取上下文变量中的用户名
|
||||
func GetUserName(ctx context.Context) string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
return user.Username
|
||||
}
|
||||
|
||||
// GetNickName 获取上下文变量中的用户昵称
|
||||
func GetNickName(ctx context.Context) string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
return user.NickName
|
||||
}
|
||||
|
||||
// GetDeptId 获取上下文变量中的部门ID
|
||||
func GetDeptId(ctx context.Context) int64 {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
return user.DeptId
|
||||
}
|
||||
|
||||
// GetTenantId 获取上下文变量中的租户ID
|
||||
func GetTenantId(ctx context.Context) string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
return user.TenantId
|
||||
}
|
||||
|
||||
// GetRoles 获取上下文变量中的角色列表
|
||||
func GetRoles(ctx context.Context) []model.Role {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
return user.Roles
|
||||
}
|
||||
|
||||
// GetLoginAt 获取上下文变量中的登录时间
|
||||
func GetLoginAt(ctx context.Context) int64 {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
return user.LoginAt
|
||||
}
|
||||
|
||||
// IsSystemTenant 判断是否是系统租户
|
||||
func IsSystemTenant(ctx context.Context) bool {
|
||||
tenantId := GetTenantId(ctx)
|
||||
return tenantId == consts.DefaultSystemTenantCode
|
||||
}
|
||||
|
||||
// IsSuperAdmin 判断是否是超级管理员角色
|
||||
func IsSuperAdmin(ctx context.Context) bool {
|
||||
roles := GetRoles(ctx)
|
||||
return slices.ContainsFunc(roles, func(role model.Role) bool {
|
||||
return role.RoleId == consts.SuperAdminRoleId
|
||||
})
|
||||
}
|
||||
func IsWebSocket(ctx context.Context) bool {
|
||||
customCtx := Get(ctx)
|
||||
if customCtx == nil {
|
||||
return false
|
||||
}
|
||||
return customCtx.IsWebSocket
|
||||
}
|
||||
func SetIsWebSocket(ctx context.Context, isWebSocket bool) {
|
||||
customCtx := Get(ctx)
|
||||
if customCtx == nil {
|
||||
return
|
||||
}
|
||||
customCtx.IsWebSocket = isWebSocket
|
||||
g.Log().Debugf(ctx, "SetIsWebSocket: %v", isWebSocket)
|
||||
}
|
||||
|
||||
// GetAccessCodeList 获取上下文变量中的访问码列表
|
||||
func GetAccessCodeList(ctx context.Context) []string {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
if len(user.AccessCodeList) == 0 {
|
||||
return nil
|
||||
}
|
||||
return user.AccessCodeList
|
||||
}
|
||||
|
||||
func GetDataValue(ctx context.Context, key string) interface{} {
|
||||
customCtx := Get(ctx)
|
||||
if customCtx == nil {
|
||||
return nil
|
||||
}
|
||||
if customCtx.Data == nil {
|
||||
return nil
|
||||
}
|
||||
value, ok := customCtx.Data[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func SetDataValue(ctx context.Context, key string, value interface{}) {
|
||||
customCtx := Get(ctx)
|
||||
if customCtx == nil {
|
||||
return
|
||||
}
|
||||
if customCtx.Data == nil {
|
||||
customCtx.Data = make(g.Map)
|
||||
}
|
||||
customCtx.Data[key] = value
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Package event
|
||||
// @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 event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type EventCallbackFunc func(ctx context.Context, eventKey string, args ...interface{})
|
||||
|
||||
type sEvent struct {
|
||||
sync.Mutex
|
||||
eventList map[string][]EventCallbackFunc // 事件列表 [事件Key]:回调函数
|
||||
}
|
||||
|
||||
var (
|
||||
instance *sEvent
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// EventsInstance 事件实例
|
||||
func EventsInstance() *sEvent {
|
||||
once.Do(func() {
|
||||
instance = &sEvent{
|
||||
eventList: make(map[string][]EventCallbackFunc),
|
||||
}
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
// Register 注册事件
|
||||
func (e *sEvent) Register(eventKey interface{}, callback EventCallbackFunc) {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
eventKeyStr, ok1 := eventKey.(string)
|
||||
eventKeyList, ok2 := eventKey.([]string)
|
||||
if !ok1 && !ok2 {
|
||||
return
|
||||
}
|
||||
if ok1 {
|
||||
e.eventList[eventKeyStr] = append(e.eventList[eventKeyStr], callback)
|
||||
} else {
|
||||
for _, eventKey := range eventKeyList {
|
||||
e.eventList[eventKey] = append(e.eventList[eventKey], callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call 回调事件
|
||||
func (e *sEvent) Call(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if events, ok := e.eventList[eventKey]; ok {
|
||||
for _, f := range events {
|
||||
f(ctx, eventKey, args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit 触发事件
|
||||
func (e *sEvent) Emit(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
e.Call(ctx, eventKey, args...)
|
||||
}
|
||||
|
||||
// Remove 删除事件
|
||||
func (e *sEvent) Remove(eventKey string) {
|
||||
delete(e.eventList, eventKey)
|
||||
}
|
||||
|
||||
// Clear 清空事件列表
|
||||
func (e *sEvent) Clear() {
|
||||
e.eventList = make(map[string][]EventCallbackFunc)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Package mcache
|
||||
// @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 mcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gcache"
|
||||
)
|
||||
|
||||
// cache 缓存驱动
|
||||
var (
|
||||
mcache *gcache.Cache
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
// Instance 缓存实例
|
||||
func Instance() *gcache.Cache {
|
||||
if mcache == nil {
|
||||
mu.Lock()
|
||||
if mcache == nil {
|
||||
mcache = gcache.NewWithAdapter(gcache.NewAdapterMemory())
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
return mcache
|
||||
}
|
||||
|
||||
func Set(ctx context.Context, key string, value interface{}, timeout time.Duration) error {
|
||||
return Instance().Set(ctx, key, value, timeout)
|
||||
}
|
||||
|
||||
func Get(ctx context.Context, key string) (interface{}, error) {
|
||||
return Instance().Get(ctx, key)
|
||||
}
|
||||
|
||||
func Remove(ctx context.Context, key string) error {
|
||||
_, err := Instance().Remove(ctx, key)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Package mcache
|
||||
// @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 mcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func init() {
|
||||
event.EventsInstance().Register(consts.EventKeyUserLogout, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) == 0 {
|
||||
return
|
||||
}
|
||||
userId := args[0].(int64)
|
||||
RemoveUserAccessCodeList(ctx, userId)
|
||||
g.Log().Infof(ctx, "mcache_access_list.go EventKeyUserLogout userId: %d", userId)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func GetUserAccessCodeList(ctx context.Context, userId int64) ([]string, []string, error) {
|
||||
accessCodeList, err := Instance().Get(ctx, fmt.Sprintf(consts.MemCacheUserAccessCodeList, userId))
|
||||
menuRoleDataAccessCodeList, err := Instance().Get(ctx, fmt.Sprintf(consts.MemCacheUserRoleDataAccessCodeList, userId))
|
||||
if err != nil || accessCodeList == nil {
|
||||
accessCodeCurrList, menuRoleDataAccessCodeList, err := service.SysAuth().GetUserAccessCodeList(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
accessCodeCurrList = ensureCurrentUserAccessCode(accessCodeCurrList)
|
||||
if len(accessCodeCurrList) == 0 {
|
||||
accessCodeCurrList = []string{"null"}
|
||||
}
|
||||
Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserAccessCodeList, userId), accessCodeCurrList, time.Hour*24)
|
||||
Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserRoleDataAccessCodeList, userId), menuRoleDataAccessCodeList, time.Hour*24)
|
||||
return accessCodeCurrList, menuRoleDataAccessCodeList, nil
|
||||
}
|
||||
return ensureCurrentUserAccessCode(accessCodeList.Strings()), menuRoleDataAccessCodeList.Strings(), nil
|
||||
}
|
||||
|
||||
func ensureCurrentUserAccessCode(accessCodeList []string) []string {
|
||||
for _, code := range accessCodeList {
|
||||
if code == consts.SysCheckPermissionCurrentUser {
|
||||
return accessCodeList
|
||||
}
|
||||
}
|
||||
return append(accessCodeList, consts.SysCheckPermissionCurrentUser)
|
||||
}
|
||||
|
||||
func SetUserAccessCodeList(ctx context.Context, userId int64, accessCodeList []string) error {
|
||||
return Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserAccessCodeList, userId), accessCodeList, time.Hour*24)
|
||||
}
|
||||
|
||||
func SetUserRoleDataAccessCodeList(ctx context.Context, userId int64, roleDataAccessCodeList []string) error {
|
||||
return Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserRoleDataAccessCodeList, userId), roleDataAccessCodeList, time.Hour*24)
|
||||
}
|
||||
|
||||
func RemoveUserRoleDataAccessCodeList(ctx context.Context, userId int64) error {
|
||||
_, err := Instance().Remove(ctx, fmt.Sprintf(consts.MemCacheUserRoleDataAccessCodeList, userId))
|
||||
return err
|
||||
}
|
||||
|
||||
func RemoveUserAccessCodeList(ctx context.Context, userId int64) error {
|
||||
_, err := Instance().Remove(ctx, fmt.Sprintf(consts.MemCacheUserAccessCodeList, userId))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Package mcache
|
||||
// @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 mcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
event.EventsInstance().Register([]string{consts.EventKeyDBSysConfigUpdate, consts.EventKeyDBSysConfigCreate, consts.EventKeyDBSysConfigDelete}, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) < 1 {
|
||||
return
|
||||
}
|
||||
// tenantId := args[0].(string)
|
||||
Instance().Remove(ctx, getKey(ctx))
|
||||
})
|
||||
}
|
||||
func getKey(ctx context.Context) string {
|
||||
return fmt.Sprintf(consts.MemCacheSystemConfig, contexts.GetTenantId(ctx))
|
||||
}
|
||||
|
||||
func GetSystemConfig(ctx context.Context, key string, defaultValue string) (string, error) {
|
||||
mapV, err := Instance().Get(ctx, getKey(ctx))
|
||||
if err != nil {
|
||||
data, _, err := service.SysConfig().List(ctx, &model.SysConfigListParam{
|
||||
PageInfo: request.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 9999,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return defaultValue, err
|
||||
}
|
||||
mapConfig := make(map[string]string)
|
||||
for _, item := range data {
|
||||
mapConfig[item.ConfigKey] = item.ConfigValue
|
||||
}
|
||||
Instance().Set(ctx, getKey(ctx), mapConfig, time.Hour*24)
|
||||
return mapConfig[key], nil
|
||||
}
|
||||
mapConfig := mapV.MapStrStr()
|
||||
if v, ok := mapConfig[key]; ok {
|
||||
return v, nil
|
||||
}
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func SetSystemConfig(ctx context.Context, key string, value string) error {
|
||||
mapV, err := Instance().Get(ctx, getKey(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mapConfig := mapV.MapStrStr()
|
||||
mapConfig[key] = value
|
||||
return Instance().Set(ctx, getKey(ctx), mapConfig, time.Hour*24)
|
||||
}
|
||||
|
||||
func RemoveSystemConfig(ctx context.Context, key string) error {
|
||||
mapV, err := Instance().Get(ctx, getKey(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mapConfig := mapV.MapStrStr()
|
||||
delete(mapConfig, key)
|
||||
return Instance().Set(ctx, getKey(ctx), mapConfig, time.Hour*24)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Package mcache
|
||||
// @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 mcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
event.EventsInstance().Register([]string{consts.EventKeyDBSysDeptUpdate,
|
||||
consts.EventKeyDBSysDeptCreate,
|
||||
consts.EventKeyDBSysDeptDelete}, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) == 0 {
|
||||
return
|
||||
}
|
||||
deptId := args[0].(int64)
|
||||
RemoveDeptName(ctx, deptId)
|
||||
})
|
||||
}
|
||||
|
||||
func GetDeptName(ctx context.Context, deptId int64) (string, error) {
|
||||
deptName, err := Instance().Get(ctx, fmt.Sprintf(consts.MemCacheDeptName, deptId))
|
||||
if err != nil {
|
||||
dept, err := service.SysDept().GetDeptById(ctx, deptId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
Instance().Set(ctx, fmt.Sprintf(consts.MemCacheDeptName, deptId), dept.DeptName, time.Hour*24)
|
||||
return dept.DeptName, nil
|
||||
}
|
||||
return deptName.String(), nil
|
||||
}
|
||||
|
||||
func SetDeptName(ctx context.Context, deptId int64, deptName string) error {
|
||||
return Instance().Set(ctx, fmt.Sprintf(consts.MemCacheDeptName, deptId), deptName, time.Hour*24)
|
||||
}
|
||||
|
||||
func RemoveDeptName(ctx context.Context, deptId int64) error {
|
||||
_, err := Instance().Remove(ctx, fmt.Sprintf(consts.MemCacheDeptName, deptId))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Package mcache
|
||||
// @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 mcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/event"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
func init() {
|
||||
event.EventsInstance().Register(consts.EventKeyUserUpdate, func(ctx context.Context, eventKey string, args ...interface{}) {
|
||||
if len(args) == 0 {
|
||||
return
|
||||
}
|
||||
userId := args[0].(int64)
|
||||
RemoveUserInfo(ctx, userId)
|
||||
})
|
||||
}
|
||||
|
||||
func GetUserInfo(ctx context.Context, userId int64) (*model.SysUserMiniModel, error) {
|
||||
userInfo, err := Instance().Get(ctx, fmt.Sprintf(consts.MemCacheUserInfo, userId))
|
||||
if err != nil {
|
||||
user, err := service.SysUser().GetUserById(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
miniUser := &model.SysUserMiniModel{}
|
||||
gconv.Struct(miniUser, user)
|
||||
Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserInfo, userId), miniUser, time.Hour*24)
|
||||
return miniUser, nil
|
||||
}
|
||||
miniUser := &model.SysUserMiniModel{}
|
||||
err = userInfo.Struct(miniUser)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return miniUser, nil
|
||||
}
|
||||
|
||||
func SetUserInfo(ctx context.Context, userId int64, userInfo *model.SysUserMiniModel) error {
|
||||
return Instance().Set(ctx, fmt.Sprintf(consts.MemCacheUserInfo, userId), userInfo, time.Hour*24)
|
||||
}
|
||||
|
||||
func RemoveUserInfo(ctx context.Context, userId int64) error {
|
||||
_, err := Instance().Remove(ctx, fmt.Sprintf(consts.MemCacheUserInfo, userId))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Enabled bool
|
||||
Dsn string
|
||||
}
|
||||
|
||||
var (
|
||||
conn *amqp.Connection
|
||||
connMu sync.Mutex
|
||||
)
|
||||
|
||||
func GetConfig(ctx context.Context) Config {
|
||||
return Config{
|
||||
Enabled: g.Cfg().MustGet(ctx, "rabbitmq.enabled", false).Bool(),
|
||||
Dsn: g.Cfg().MustGet(ctx, "rabbitmq.dsn", "").String(),
|
||||
}
|
||||
}
|
||||
|
||||
func Enabled(ctx context.Context) bool {
|
||||
cfg := GetConfig(ctx)
|
||||
return cfg.Enabled && cfg.Dsn != ""
|
||||
}
|
||||
|
||||
func Publish(ctx context.Context, queue string, data any) error {
|
||||
body, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "序列化 RabbitMQ 消息失败")
|
||||
}
|
||||
return PublishBytes(ctx, queue, body)
|
||||
}
|
||||
|
||||
func PublishBytes(ctx context.Context, queue string, body []byte) error {
|
||||
if queue == "" {
|
||||
return gerror.New("RabbitMQ 队列名不能为空")
|
||||
}
|
||||
ch, err := channel(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ch.Close()
|
||||
|
||||
if _, err = ch.QueueDeclare(queue, true, false, false, false, nil); err != nil {
|
||||
return gerror.Wrapf(err, "声明 RabbitMQ 队列失败: %s", queue)
|
||||
}
|
||||
return ch.PublishWithContext(ctx, "", queue, false, false, amqp.Publishing{
|
||||
ContentType: "application/json",
|
||||
DeliveryMode: amqp.Persistent,
|
||||
Timestamp: time.Now(),
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
|
||||
func channel(ctx context.Context) (*amqp.Channel, error) {
|
||||
c, err := connection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch, err := c.Channel()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "打开 RabbitMQ Channel 失败")
|
||||
}
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func connection(ctx context.Context) (*amqp.Connection, error) {
|
||||
cfg := GetConfig(ctx)
|
||||
if !cfg.Enabled {
|
||||
return nil, gerror.New("RabbitMQ 未启用")
|
||||
}
|
||||
if cfg.Dsn == "" {
|
||||
return nil, gerror.New("RabbitMQ DSN 未配置")
|
||||
}
|
||||
|
||||
connMu.Lock()
|
||||
defer connMu.Unlock()
|
||||
if conn != nil && !conn.IsClosed() {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
c, err := amqp.Dial(cfg.Dsn)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "连接 RabbitMQ 失败")
|
||||
}
|
||||
conn = c
|
||||
return conn, nil
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Package websocket
|
||||
// @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 websocket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32"
|
||||
|
||||
"github.com/gogf/gf/v2/container/garray"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
// 用户连接超时时间
|
||||
heartbeatExpirationTime = 6 * 60
|
||||
)
|
||||
|
||||
// 用户登录
|
||||
type login struct {
|
||||
UserId uint64
|
||||
Client *Client
|
||||
}
|
||||
|
||||
// GetKey 读取客户端数据
|
||||
func (l *login) GetKey() (key string) {
|
||||
key = GetUserKey(l.UserId)
|
||||
return
|
||||
}
|
||||
|
||||
// Client 客户端连接
|
||||
type Client struct {
|
||||
Addr string // 客户端地址
|
||||
ID string // 连接唯一标识
|
||||
Socket *websocket.Conn // 用户连接
|
||||
Send chan *WResponse // 待发送的数据
|
||||
SendClose bool // 发送是否关闭
|
||||
UserId uint64 // 用户ID,用户登录以后才有
|
||||
FirstTime uint64 // 首次连接事件
|
||||
HeartbeatTime uint64 // 用户上次心跳时间
|
||||
LoginTime uint64 // 登录时间 登录以后才有
|
||||
isApp bool // 是否是app
|
||||
tags *garray.StrArray // 标签
|
||||
Ctx context.Context // 上下文
|
||||
closeSignal chan struct{} // 关闭信号
|
||||
}
|
||||
|
||||
// NewClient 初始化
|
||||
func NewClient(addr string, socket *websocket.Conn, firstTime uint64, ctx context.Context) (client *Client) {
|
||||
client = &Client{
|
||||
Addr: addr,
|
||||
ID: uuid32.New(),
|
||||
Socket: socket,
|
||||
Send: make(chan *WResponse, 100),
|
||||
SendClose: false,
|
||||
FirstTime: firstTime,
|
||||
HeartbeatTime: firstTime,
|
||||
Ctx: ctx,
|
||||
closeSignal: make(chan struct{}, 1),
|
||||
tags: garray.NewStrArray(true),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 读取客户端数据
|
||||
func (c *Client) read() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("write stop", string(debug.Stack()), r)
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
c.close()
|
||||
}()
|
||||
|
||||
for {
|
||||
_, message, err := c.Socket.ReadMessage()
|
||||
if err != nil {
|
||||
g.Log().Info(c.Ctx, "websocket Client.read failed.", err)
|
||||
return
|
||||
}
|
||||
// 处理程序
|
||||
// fmt.Println(message)
|
||||
ProcessData(c, message)
|
||||
}
|
||||
}
|
||||
|
||||
// 向客户端写数据
|
||||
func (c *Client) write() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("write stop", string(debug.Stack()), r)
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
clientManager.Unregister <- c
|
||||
_ = c.Socket.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-c.closeSignal:
|
||||
g.Log().Infof(mctx, "websocket client exit, user:%+v", c.UserId)
|
||||
return
|
||||
case message, ok := <-c.Send:
|
||||
if !ok {
|
||||
g.Log().Info(c.Ctx, "websocket Client.write failed.")
|
||||
// 发送数据错误 关闭连接
|
||||
return
|
||||
}
|
||||
_ = c.Socket.WriteJSON(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendMsg 发送数据
|
||||
func (c *Client) SendMsg(msg *WResponse) {
|
||||
if c == nil || c.SendClose {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("SendMsg stop:", r, string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
c.Send <- msg
|
||||
}
|
||||
|
||||
// Heartbeat 心跳更新
|
||||
func (c *Client) Heartbeat(currentTime uint64) {
|
||||
c.HeartbeatTime = currentTime
|
||||
}
|
||||
|
||||
// IsHeartbeatTimeout 心跳是否超时
|
||||
func (c *Client) IsHeartbeatTimeout(currentTime uint64) (timeout bool) {
|
||||
if c.HeartbeatTime+heartbeatExpirationTime <= currentTime {
|
||||
timeout = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭客户端
|
||||
func (c *Client) close() {
|
||||
g.Log().Info(c.Ctx, "websocket Client.close")
|
||||
if c.SendClose {
|
||||
return
|
||||
}
|
||||
c.SendClose = true
|
||||
c.closeSignal <- struct{}{}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
// Package websocket
|
||||
// @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 websocket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcron"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ClientManager 客户端管理
|
||||
type ClientManager struct {
|
||||
Clients map[*Client]bool // 全部的连接
|
||||
ClientsLock sync.RWMutex // 读写锁
|
||||
Users map[string]*Client // 登录的用户 // uuid
|
||||
UserLock sync.RWMutex // 读写锁
|
||||
Register chan *Client // 连接连接处理
|
||||
Login chan *login // 用户登录处理
|
||||
Unregister chan *Client // 断开连接处理程序
|
||||
Broadcast chan *WResponse // 广播 向全部成员发送数据
|
||||
ClientBroadcast chan *ClientWResponse // 广播 向某个客户端发送数据
|
||||
TagBroadcast chan *TagWResponse // 广播 向某个标签成员发送数据
|
||||
UserBroadcast chan *UserWResponse // 广播 向某个用户的所有链接发送数据
|
||||
TagCallbackMap map[string]func(client *Client, tag string) // 注册标签回调
|
||||
closeSignal chan struct{} // 关闭信号
|
||||
TagCountMap map[string]int // 标签map
|
||||
TagLock sync.RWMutex // 标签读写锁
|
||||
}
|
||||
|
||||
func NewClientManager() (clientManager *ClientManager) {
|
||||
clientManager = &ClientManager{
|
||||
Clients: make(map[*Client]bool),
|
||||
Users: make(map[string]*Client),
|
||||
Register: make(chan *Client, 1000),
|
||||
Unregister: make(chan *Client, 1000),
|
||||
Broadcast: make(chan *WResponse, 1000),
|
||||
TagBroadcast: make(chan *TagWResponse, 1000),
|
||||
UserBroadcast: make(chan *UserWResponse, 1000),
|
||||
TagCallbackMap: make(map[string]func(client *Client, tag string)),
|
||||
closeSignal: make(chan struct{}, 1),
|
||||
TagCountMap: make(map[string]int),
|
||||
TagLock: sync.RWMutex{},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserKey 获取用户key
|
||||
func GetUserKey(userId uint64) (key string) {
|
||||
key = fmt.Sprintf("%s_%d", "ws", userId)
|
||||
return
|
||||
}
|
||||
|
||||
// InClient 客户端是否存在
|
||||
func (manager *ClientManager) InClient(client *Client) (ok bool) {
|
||||
manager.ClientsLock.RLock()
|
||||
defer manager.ClientsLock.RUnlock()
|
||||
_, ok = manager.Clients[client]
|
||||
return
|
||||
}
|
||||
|
||||
// GetClients 获取所有客户端
|
||||
func (manager *ClientManager) GetClients() (clients map[*Client]bool) {
|
||||
clients = make(map[*Client]bool)
|
||||
manager.ClientsRange(func(client *Client, value bool) (result bool) {
|
||||
clients[client] = value
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ClientsRange 遍历
|
||||
func (manager *ClientManager) ClientsRange(f func(client *Client, value bool) bool) {
|
||||
manager.ClientsLock.RLock()
|
||||
defer func() {
|
||||
manager.ClientsLock.RUnlock()
|
||||
}()
|
||||
for key, value := range manager.Clients {
|
||||
r := f(key, value)
|
||||
if !r {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetClientsLen 获取客户端总数
|
||||
func (manager *ClientManager) GetClientsLen() (clientsLen int) {
|
||||
clientsLen = len(manager.Clients)
|
||||
return
|
||||
}
|
||||
|
||||
// AddClients 添加客户端
|
||||
func (manager *ClientManager) AddClients(client *Client) {
|
||||
manager.ClientsLock.Lock()
|
||||
defer manager.ClientsLock.Unlock()
|
||||
manager.Clients[client] = true
|
||||
}
|
||||
|
||||
// DelClients 删除客户端
|
||||
func (manager *ClientManager) DelClients(client *Client) {
|
||||
manager.ClientsLock.Lock()
|
||||
defer manager.ClientsLock.Unlock()
|
||||
if _, ok := manager.Clients[client]; ok {
|
||||
// 删除标签
|
||||
client.tags.Iterator(func(k int, name string) bool {
|
||||
DelTagCount(name)
|
||||
return true
|
||||
})
|
||||
delete(manager.Clients, client)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserClient 获取用户的连接
|
||||
func (manager *ClientManager) GetUserClient(userId uint64) (client *Client) {
|
||||
manager.UserLock.RLock()
|
||||
defer manager.UserLock.RUnlock()
|
||||
userKey := GetUserKey(userId)
|
||||
if value, ok := manager.Users[userKey]; ok {
|
||||
client = value
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AddUsers 添加用户
|
||||
func (manager *ClientManager) AddUsers(key string, client *Client) {
|
||||
manager.UserLock.Lock()
|
||||
defer manager.UserLock.Unlock()
|
||||
manager.Users[key] = client
|
||||
}
|
||||
|
||||
// DelUsers 删除用户
|
||||
func (manager *ClientManager) DelUsers(client *Client) (result bool) {
|
||||
manager.UserLock.Lock()
|
||||
defer manager.UserLock.Unlock()
|
||||
key := GetUserKey(client.UserId)
|
||||
if value, ok := manager.Users[key]; ok {
|
||||
// 判断是否为相同的用户
|
||||
if value.Addr != client.Addr {
|
||||
return
|
||||
}
|
||||
delete(manager.Users, key)
|
||||
result = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetUsersLen 已登录用户数
|
||||
func (manager *ClientManager) GetUsersLen() (userLen int) {
|
||||
userLen = len(manager.Users)
|
||||
return
|
||||
}
|
||||
|
||||
// EventRegister 用户建立连接事件
|
||||
func (manager *ClientManager) EventRegister(client *Client) {
|
||||
manager.AddClients(client)
|
||||
//发送当前客户端标识
|
||||
client.SendMsg(&WResponse{Event: "connected", Data: g.Map{
|
||||
"ID": client.ID,
|
||||
}})
|
||||
if client.UserId > 0 {
|
||||
manager.AddUsers(GetUserKey(client.UserId), client)
|
||||
}
|
||||
g.Log().Info(client.Ctx, "websocket EventRegister.", client.UserId)
|
||||
}
|
||||
|
||||
// EventLogin 用户登录事件
|
||||
func (manager *ClientManager) EventLogin(login *login) {
|
||||
client := login.Client
|
||||
if manager.InClient(client) {
|
||||
userKey := login.GetKey()
|
||||
manager.AddUsers(userKey, login.Client)
|
||||
}
|
||||
}
|
||||
|
||||
// EventUnregister 用户断开连接事件
|
||||
func (manager *ClientManager) EventUnregister(client *Client) {
|
||||
g.Log().Info(client.Ctx, "websocket EventUnregister.", client.UserId)
|
||||
manager.DelClients(client)
|
||||
// 删除用户连接
|
||||
deleteResult := manager.DelUsers(client)
|
||||
if !deleteResult {
|
||||
// 不是当前连接的客户端
|
||||
return
|
||||
}
|
||||
// 关闭 chan
|
||||
// close(client.Send)
|
||||
client.close()
|
||||
}
|
||||
|
||||
// ClearTimeoutConnections 定时清理超时连接
|
||||
func (manager *ClientManager) clearTimeoutConnections() {
|
||||
currentTime := uint64(gtime.Now().Unix())
|
||||
clients := clientManager.GetClients()
|
||||
for client := range clients {
|
||||
if client.IsHeartbeatTimeout(currentTime) {
|
||||
//fmt.Println("心跳时间超时 关闭连接", client.Addr, client.UserId, client.LoginTime, client.HeartbeatTime)
|
||||
_ = client.Socket.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WebsocketPing 心跳处理
|
||||
func (manager *ClientManager) ping(ctx context.Context) {
|
||||
//定时任务,发送心跳包
|
||||
_, _ = gcron.Add(ctx, "0 */1 * * * *", func(ctx context.Context) {
|
||||
res := &WResponse{
|
||||
Event: Ping,
|
||||
Data: g.Map{},
|
||||
}
|
||||
SendToAll(res)
|
||||
})
|
||||
// 定时任务,清理超时连接
|
||||
_, _ = gcron.Add(ctx, "*/30 * * * * *", func(ctx context.Context) {
|
||||
manager.clearTimeoutConnections()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 管道处理程序
|
||||
func (manager *ClientManager) start() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
g.Log().Warningf(mctx, "websocket start recover:%+v, stack:%+v", r, string(debug.Stack()))
|
||||
return
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case conn := <-manager.Register:
|
||||
// 建立连接事件
|
||||
manager.EventRegister(conn)
|
||||
|
||||
case login := <-manager.Login:
|
||||
// 用户登录
|
||||
manager.EventLogin(login)
|
||||
|
||||
case conn := <-manager.Unregister:
|
||||
// 断开连接事件
|
||||
manager.EventUnregister(conn)
|
||||
|
||||
case message := <-manager.Broadcast:
|
||||
// 全部客户端广播事件
|
||||
clients := manager.GetClients()
|
||||
for conn := range clients {
|
||||
conn.SendMsg(message)
|
||||
}
|
||||
case message := <-manager.TagBroadcast:
|
||||
// 标签广播事件
|
||||
clients := manager.GetClients()
|
||||
// find := false
|
||||
for conn := range clients {
|
||||
if conn.tags.Contains(message.Tag) {
|
||||
conn.SendMsg(message.WResponse)
|
||||
// find = true
|
||||
}
|
||||
}
|
||||
// g.Log().Debugf(context.TODO(), "client_manager.go 标签广播事件 find:%v msg:%v", find, message)
|
||||
case message := <-manager.UserBroadcast:
|
||||
// 用户广播事件
|
||||
clients := manager.GetClients()
|
||||
for conn := range clients {
|
||||
if conn.UserId == message.UserID {
|
||||
conn.SendMsg(message.WResponse)
|
||||
}
|
||||
}
|
||||
case message := <-manager.ClientBroadcast:
|
||||
// 单个客户端广播事件
|
||||
clients := manager.GetClients()
|
||||
for conn := range clients {
|
||||
if conn.ID == message.ID {
|
||||
conn.SendMsg(message.WResponse)
|
||||
}
|
||||
}
|
||||
case <-manager.closeSignal:
|
||||
g.Log().Debug(mctx, "websocket closeSignal exit.")
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// SendToAll 发送全部客户端
|
||||
func SendToAll(response *WResponse) {
|
||||
clientManager.Broadcast <- response
|
||||
}
|
||||
|
||||
// SendToClientID 发送单个客户端
|
||||
func SendToClientID(id string, response *WResponse) {
|
||||
clientRes := &ClientWResponse{
|
||||
ID: id,
|
||||
WResponse: response,
|
||||
}
|
||||
clientManager.ClientBroadcast <- clientRes
|
||||
}
|
||||
|
||||
// SendToUser 发送单个用户
|
||||
func SendToUser(userID uint64, response *WResponse) {
|
||||
userRes := &UserWResponse{
|
||||
UserID: userID,
|
||||
WResponse: response,
|
||||
}
|
||||
clientManager.UserBroadcast <- userRes
|
||||
// g.Log().Debugf(context.Background(), "websocket SendToUser userID:%v response:%+v", userID, response)
|
||||
}
|
||||
|
||||
// SendToTag 发送某个标签
|
||||
func SendToTag(tag string, response *WResponse) {
|
||||
count := GetTagCount(tag)
|
||||
if count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
tagRes := &TagWResponse{
|
||||
Tag: tag,
|
||||
WResponse: response,
|
||||
}
|
||||
clientManager.TagBroadcast <- tagRes
|
||||
}
|
||||
|
||||
// RegisterTagCallback 注册标签回调
|
||||
func RegisterTagCallback(tag string, callback func(client *Client, tag string)) {
|
||||
clientManager.TagLock.Lock()
|
||||
defer clientManager.TagLock.Unlock()
|
||||
clientManager.TagCallbackMap[tag] = callback
|
||||
}
|
||||
|
||||
// UnregisterTagCallback 注销标签回调
|
||||
func UnregisterTagCallback(tag string) {
|
||||
clientManager.TagLock.Lock()
|
||||
defer clientManager.TagLock.Unlock()
|
||||
delete(clientManager.TagCallbackMap, tag)
|
||||
}
|
||||
|
||||
// 添加标签订阅计数
|
||||
func AddTagCount(tag string) {
|
||||
clientManager.TagLock.Lock()
|
||||
defer clientManager.TagLock.Unlock()
|
||||
if _, ok := clientManager.TagCountMap[tag]; !ok {
|
||||
clientManager.TagCountMap[tag] = 1
|
||||
} else {
|
||||
clientManager.TagCountMap[tag]++
|
||||
}
|
||||
}
|
||||
|
||||
// 删除标签订阅计数
|
||||
func DelTagCount(tag string) {
|
||||
clientManager.TagLock.Lock()
|
||||
defer clientManager.TagLock.Unlock()
|
||||
count, ok := clientManager.TagCountMap[tag]
|
||||
if ok {
|
||||
if count > 0 {
|
||||
clientManager.TagCountMap[tag]--
|
||||
} else {
|
||||
delete(clientManager.TagCountMap, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetTagCount 获取标签订阅计数
|
||||
func GetTagCount(tag string) (count int) {
|
||||
clientManager.TagLock.RLock()
|
||||
defer clientManager.TagLock.RUnlock()
|
||||
count, ok := clientManager.TagCountMap[tag]
|
||||
if ok {
|
||||
return count
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 变量Tag Count
|
||||
func RangeTagCount(callback func(tag string, count int)) {
|
||||
clientManager.TagLock.RLock()
|
||||
defer clientManager.TagLock.RUnlock()
|
||||
for tag, count := range clientManager.TagCountMap {
|
||||
callback(tag, count)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Package websocket
|
||||
// @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 websocket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// LoginController 用户登录
|
||||
func LoginController(client *Client, req *request) {
|
||||
l := &WsLoginDataReq{}
|
||||
gconv.Struct(req.Data, &l)
|
||||
userId := gconv.Uint64(l.UserID)
|
||||
client.UserId = userId
|
||||
// 用户登录
|
||||
login := &login{
|
||||
UserId: userId,
|
||||
Client: client,
|
||||
}
|
||||
clientManager.Login <- login
|
||||
|
||||
client.SendMsg(&WResponse{
|
||||
Event: Login,
|
||||
Data: "success",
|
||||
})
|
||||
}
|
||||
|
||||
func IsAppController(client *Client) {
|
||||
client.isApp = true
|
||||
}
|
||||
|
||||
// JoinController 加入
|
||||
func JoinController(client *Client, req *request) {
|
||||
name := gconv.String(req.Data["name"])
|
||||
|
||||
if !client.tags.Contains(name) {
|
||||
client.tags.Append(name)
|
||||
for k, v := range clientManager.TagCallbackMap {
|
||||
if strings.HasPrefix(name, k) {
|
||||
if v != nil {
|
||||
v(client, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
AddTagCount(name)
|
||||
}
|
||||
client.SendMsg(&WResponse{
|
||||
Event: Join,
|
||||
Data: client.tags.Slice(),
|
||||
})
|
||||
}
|
||||
|
||||
// QuitController 退出
|
||||
func QuitController(client *Client, req *request) {
|
||||
name := gconv.String(req.Data["name"])
|
||||
if client.tags.Contains(name) {
|
||||
client.tags.RemoveValue(name)
|
||||
DelTagCount(name)
|
||||
}
|
||||
client.SendMsg(&WResponse{
|
||||
Event: Quit,
|
||||
Data: client.tags.Slice(),
|
||||
})
|
||||
}
|
||||
|
||||
func PingController(client *Client) {
|
||||
currentTime := uint64(gtime.Now().Unix())
|
||||
client.Heartbeat(currentTime)
|
||||
}
|
||||
|
||||
// JoinsController 加入
|
||||
func JoinsController(client *Client, req *request) {
|
||||
if req == nil || req.Data == nil {
|
||||
fmt.Println("websocket.JoinsController req is nil or req.Data is nil")
|
||||
return
|
||||
}
|
||||
// fmt.Println("websocket.JoinsController req.Data", req.Data)
|
||||
names, ok := req.Data["names"]
|
||||
if !ok || names == nil {
|
||||
fmt.Println("websocket.JoinsController names is nil or names is empty")
|
||||
return
|
||||
}
|
||||
// fmt.Println("websocket.JoinsController names", names)
|
||||
for _, name := range gconv.Strings(names) {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if !client.tags.Contains(name) {
|
||||
client.tags.Append(name)
|
||||
for k, v := range clientManager.TagCallbackMap {
|
||||
if strings.HasPrefix(name, k) {
|
||||
if v != nil {
|
||||
v(client, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
AddTagCount(name)
|
||||
}
|
||||
}
|
||||
client.SendMsg(&WResponse{
|
||||
Event: Joins,
|
||||
Data: client.tags.Slice(),
|
||||
})
|
||||
}
|
||||
|
||||
// QuitController 退出
|
||||
func QuitsController(client *Client, req *request) {
|
||||
for _, name := range gconv.Strings(req.Data["names"]) {
|
||||
if client.tags.Contains(name) {
|
||||
client.tags.RemoveValue(name)
|
||||
DelTagCount(name)
|
||||
}
|
||||
}
|
||||
client.SendMsg(&WResponse{
|
||||
Event: Quits,
|
||||
Data: client.tags.Slice(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Package websocket
|
||||
// @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 websocket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"net/http"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
mctx = gctx.GetInitCtx() // 上下文
|
||||
clientManager = NewClientManager() // 管理者
|
||||
)
|
||||
var upGrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func StartWebSocket(ctx context.Context) {
|
||||
g.Log().Info(ctx, "启动:WebSocket")
|
||||
go clientManager.start()
|
||||
go clientManager.ping(ctx)
|
||||
}
|
||||
|
||||
func StopWebSocket(ctx context.Context) {
|
||||
g.Log().Info(ctx, "停止:WebSocket")
|
||||
clientManager.closeSignal <- struct{}{}
|
||||
}
|
||||
|
||||
func WsPage(r *ghttp.Request) {
|
||||
g.Log().Infof(r.GetCtx(), "websocket.WsPage Begin. UserID:%v ", contexts.GetUserId(r.GetCtx()))
|
||||
// r.Response.Header().Set("Content-Type", "text/event-stream")
|
||||
contexts.SetIsWebSocket(r.GetCtx(), true)
|
||||
conn, err := upGrader.Upgrade(r.Response.Writer, r.Request, nil)
|
||||
if err != nil {
|
||||
r.Response.Write(err.Error())
|
||||
return
|
||||
}
|
||||
currentTime := uint64(gtime.Now().Unix())
|
||||
client := NewClient(conn.RemoteAddr().String(), conn, currentTime, r.GetCtx())
|
||||
|
||||
client.UserId = uint64(contexts.GetUserId(r.GetCtx()))
|
||||
go client.read()
|
||||
go client.write()
|
||||
|
||||
// 用户连接事件
|
||||
clientManager.Register <- client
|
||||
|
||||
g.Log().Infof(r.GetCtx(), "websocket.WsPage End. UserID:%v", client.UserId)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Package websocket
|
||||
// @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 websocket
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// 当前输入对象
|
||||
type request struct {
|
||||
Event string `json:"e"` //事件名称
|
||||
Data g.Map `json:"d"` //数据
|
||||
}
|
||||
|
||||
// WResponse 输出对象
|
||||
type WResponse struct {
|
||||
Event string `json:"e"` //事件名称
|
||||
Data interface{} `json:"d"` //数据
|
||||
}
|
||||
|
||||
type TagWResponse struct {
|
||||
Tag string
|
||||
WResponse *WResponse
|
||||
}
|
||||
|
||||
type UserWResponse struct {
|
||||
UserID uint64
|
||||
WResponse *WResponse
|
||||
}
|
||||
|
||||
type ClientWResponse struct {
|
||||
ID string
|
||||
WResponse *WResponse
|
||||
}
|
||||
|
||||
type WsLoginDataReq struct {
|
||||
UserID uint `json:"userId" `
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Package websocket
|
||||
// @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 websocket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const (
|
||||
Error = "error"
|
||||
Login = "login"
|
||||
Join = "join"
|
||||
Quit = "quit"
|
||||
IsApp = "is_app"
|
||||
Ping = "ping"
|
||||
Joins = "joins"
|
||||
Quits = "quits"
|
||||
)
|
||||
|
||||
// ProcessData 处理数据
|
||||
func ProcessData(client *Client, message []byte) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("websocket.ProcessData 处理数据 stop", r, "message=", message)
|
||||
}
|
||||
}()
|
||||
request := &request{}
|
||||
err := gconv.Struct(message, request)
|
||||
if err != nil {
|
||||
fmt.Println("websocket.ProcessData 数据解析失败:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("websocket.ProcessData request", request)
|
||||
switch request.Event {
|
||||
case Login:
|
||||
LoginController(client, request)
|
||||
break
|
||||
case Join:
|
||||
JoinController(client, request)
|
||||
break
|
||||
case Quit:
|
||||
QuitController(client, request)
|
||||
break
|
||||
case IsApp:
|
||||
IsAppController(client)
|
||||
break
|
||||
case Ping:
|
||||
PingController(client)
|
||||
break
|
||||
case Joins:
|
||||
JoinsController(client, request)
|
||||
break
|
||||
case Quits:
|
||||
QuitsController(client, request)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
// Package worker
|
||||
// @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 worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
IsAggregate bool `json:"isAggregate"`
|
||||
Group string `json:"group"`
|
||||
Uid string `json:"uid"`
|
||||
Payload []byte `json:"payload"`
|
||||
}
|
||||
|
||||
type AggregatorOptions struct {
|
||||
GroupGracePeriod int // 组的优雅延迟时间(秒)
|
||||
GroupMaxDelay int // 组的最大延迟时间(秒)
|
||||
GroupMaxSize int // 组的最大大小
|
||||
}
|
||||
|
||||
type WorkerOptions struct {
|
||||
group string //任务处理器的组名
|
||||
redisAddr string //redis地址
|
||||
redisUser string //redis用户名
|
||||
redisPassword string //redis密码
|
||||
redisDB int //redis数据库
|
||||
handler func(ctx context.Context, p Payload) error //任务处理函数
|
||||
maxRetry int //任务最大重试次数
|
||||
clearArchived int //清除已归档任务的时间间隔
|
||||
timeout int //任务超时时间
|
||||
redisPeriodKey string //redis周期任务键
|
||||
aggregator *AggregatorOptions //聚合器选项
|
||||
}
|
||||
|
||||
func GetDefaultWorkerOptions(ops *WorkerOptions) *WorkerOptions {
|
||||
if ops != nil {
|
||||
return ops
|
||||
}
|
||||
addr := g.Cfg().MustGet(context.Background(), "queue.asynq.redis.address").String()
|
||||
pass := g.Cfg().MustGet(context.Background(), "queue.asynq.redis.pass").String()
|
||||
db := g.Cfg().MustGet(context.Background(), "queue.asynq.redis.db").Int()
|
||||
user := g.Cfg().MustGet(context.Background(), "queue.asynq.redis.user").String()
|
||||
if user == "" {
|
||||
user = "default"
|
||||
}
|
||||
return &WorkerOptions{
|
||||
group: "task",
|
||||
redisAddr: addr,
|
||||
redisPassword: pass,
|
||||
redisDB: db,
|
||||
redisUser: user,
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerGroup(group string) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).group = group
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerRedisAddr(addr string) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).redisAddr = addr
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerRedisUser(user string) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).redisUser = user
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerRedisPassword(pass string) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).redisPassword = pass
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerRedisDB(db int) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).redisDB = db
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerHandler(handler func(ctx context.Context, p Payload) error) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).handler = handler
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerMaxRetry(maxRetry int) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).maxRetry = maxRetry
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerClearArchived(clearArchived int) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).clearArchived = clearArchived
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerTimeout(timeout int) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerRedisPeriodKey(redisPeriodKey string) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).redisPeriodKey = redisPeriodKey
|
||||
}
|
||||
}
|
||||
|
||||
func WithWorkerAggregator(aggregator *AggregatorOptions) func(*WorkerOptions) {
|
||||
return func(o *WorkerOptions) {
|
||||
GetDefaultWorkerOptions(o).aggregator = aggregator
|
||||
}
|
||||
}
|
||||
|
||||
type TaskOptions struct {
|
||||
uid string
|
||||
group string
|
||||
payload []byte
|
||||
expr string // 仅周期任务
|
||||
in *time.Duration // 仅一次性任务 当前时间的延迟
|
||||
at *time.Time // 仅一次性任务 指定时间
|
||||
now bool // 仅一次性任务 当前时间
|
||||
retention int // 仅一次性任务 任务保留时间 在这个时间内任务会被保留为已完成状态,过了这个时间后,任务将被自动删除。
|
||||
replace bool // 仅一次性任务 是否替换已存在的任务
|
||||
ctx context.Context // 仅一次性任务
|
||||
maxRetry int
|
||||
timeout int
|
||||
}
|
||||
|
||||
func GetDefaultTaskOptions(opt *TaskOptions) *TaskOptions {
|
||||
if opt != nil {
|
||||
return opt
|
||||
}
|
||||
return &TaskOptions{
|
||||
group: "group",
|
||||
maxRetry: 3,
|
||||
timeout: 120,
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskUid(uid string) func(*TaskOptions) {
|
||||
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).uid = uid
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskGroup(group string) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).group = group
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskPayload(payload []byte) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).payload = payload
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskExpr(expr string) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).expr = expr
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskIn(in *time.Duration) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).in = in
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskAt(at *time.Time) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).at = at
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskNow(now bool) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).now = now
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskRetention(retention int) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).retention = retention
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskReplace(replace bool) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).replace = replace
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskCtx(ctx context.Context) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskMaxRetry(maxRetry int) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).maxRetry = maxRetry
|
||||
}
|
||||
}
|
||||
|
||||
func WithTaskTimeout(timeout int) func(*TaskOptions) {
|
||||
return func(o *TaskOptions) {
|
||||
GetDefaultTaskOptions(o).timeout = timeout
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// package worker
|
||||
// @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 worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type Queue struct {
|
||||
topic string
|
||||
w *Worker
|
||||
}
|
||||
|
||||
type Process interface {
|
||||
GetTopic() string // 获取消费主题
|
||||
Handle(ctx context.Context, p Payload) (err error) // 处理过程的方法
|
||||
GetAggregator() *AggregatorOptions // 获取聚合器配置,返回nil不使用聚合
|
||||
}
|
||||
|
||||
func RegisterQueueProcess(p Process) (q *Queue) {
|
||||
q = &Queue{
|
||||
topic: p.GetTopic(),
|
||||
w: New(WithWorkerGroup(p.GetTopic()), WithWorkerHandler(p.Handle), WithWorkerAggregator(p.GetAggregator())),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (q *Queue) Push(ctx context.Context, topic string, data []byte, timeout int) (err error) {
|
||||
err = q.w.Once(
|
||||
WithTaskUid(uuid32.New()),
|
||||
WithTaskGroup(topic),
|
||||
WithTaskPayload(data),
|
||||
WithTaskTimeout(timeout),
|
||||
)
|
||||
if err != nil {
|
||||
g.Log().Debug(ctx, "Run Queue TaskWorker %s Error: %v", topic, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *Queue) Stop() {
|
||||
if q.w != nil {
|
||||
q.w.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
type Scheduled struct {
|
||||
topic string
|
||||
w *Worker
|
||||
}
|
||||
|
||||
func RegisterScheduledProcess(p Process) (q *Queue) {
|
||||
q = &Queue{
|
||||
topic: p.GetTopic(),
|
||||
w: New(WithWorkerGroup(p.GetTopic()), WithWorkerHandler(p.Handle), WithWorkerAggregator(p.GetAggregator())),
|
||||
}
|
||||
return
|
||||
}
|
||||
func (s *Scheduled) Cron(ctx context.Context, topic, cronExpr string, data []byte) (entryID string, err error) {
|
||||
s.topic = topic
|
||||
entryID, err = s.w.Cron(
|
||||
1,
|
||||
WithTaskUid(uuid32.S()),
|
||||
WithTaskGroup(topic),
|
||||
WithTaskExpr(cronExpr),
|
||||
WithTaskPayload(data),
|
||||
)
|
||||
if err != nil {
|
||||
g.Log().Debug(ctx, "Run Cron TaskWorker %s Error: %v", topic, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Scheduled) Remove(entryID string) (err error) {
|
||||
err = s.w.RemoveCron(entryID)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
// package worker
|
||||
// @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 worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/uuid32"
|
||||
|
||||
"github.com/dromara/carbon/v2"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/xiujiecn/asynq"
|
||||
)
|
||||
|
||||
type Worker struct {
|
||||
ops *WorkerOptions
|
||||
client *asynq.Client
|
||||
inspector *asynq.Inspector
|
||||
scheduler *asynq.Scheduler
|
||||
svr *asynq.Server
|
||||
Error error
|
||||
}
|
||||
|
||||
type periodTask struct {
|
||||
Expr string `json:"expr"` // asynq
|
||||
Group string `json:"group"`
|
||||
Uid string `json:"uid"`
|
||||
Payload []byte `json:"payload"`
|
||||
Next int64 `json:"next"` // next schedule unix timestamp
|
||||
Processed int64 `json:"processed"` // run times
|
||||
MaxRetry int `json:"maxRetry"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
|
||||
func (p *periodTask) String() (str string) {
|
||||
bs, _ := json.Marshal(p)
|
||||
str = string(bs)
|
||||
return
|
||||
}
|
||||
|
||||
func (p *periodTask) FromString(str string) {
|
||||
err := json.Unmarshal([]byte(str), p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func aggregate(group string, tasks []*asynq.Task) *asynq.Task {
|
||||
// g.Log().Infof(context.Background(), "server/internal/library/worker/worker.go aggregate group %s, tasks %d", group, len(tasks))
|
||||
list := make([][]byte, 0)
|
||||
for _, t := range tasks {
|
||||
list = append(list, t.Payload())
|
||||
}
|
||||
payload, _ := json.Marshal(list)
|
||||
return asynq.NewTask(group+".aggregate", payload, asynq.TaskID(uuid32.S()))
|
||||
}
|
||||
|
||||
func New(options ...func(*WorkerOptions)) *Worker {
|
||||
ops := GetDefaultWorkerOptions(nil)
|
||||
for _, op := range options {
|
||||
op(ops)
|
||||
}
|
||||
|
||||
rs := asynq.RedisClientOpt{
|
||||
Addr: ops.redisAddr,
|
||||
Password: ops.redisPassword,
|
||||
DB: ops.redisDB,
|
||||
}
|
||||
|
||||
w := &Worker{}
|
||||
if ops.redisAddr == "" {
|
||||
w.Error = errors.New("redisAddr is nil")
|
||||
return w
|
||||
}
|
||||
w.ops = ops
|
||||
w.client = asynq.NewClient(rs)
|
||||
w.inspector = asynq.NewInspector(rs)
|
||||
if ops.redisPeriodKey != "" {
|
||||
w.scheduler = asynq.NewScheduler(rs, &asynq.SchedulerOpts{
|
||||
Location: time.Now().Location(),
|
||||
LogLevel: asynq.LogLevel(g.Cfg().MustGet(context.Background(), "queue.asynq.logLevel").Int()), // 日志级别 DebugLevel 1, InfoLevel 2, WarnLevel 3, ErrorLevel 4, FatalLevel 5
|
||||
})
|
||||
go func() {
|
||||
if err := w.scheduler.Start(); err != nil {
|
||||
g.Log().Error(context.Background(), "server/internal/library/worker/worker.go New 启动调度器失败", err)
|
||||
panic(err)
|
||||
}
|
||||
g.Log().Info(context.Background(), "server/internal/library/worker/worker.go New 启动调度器成功", "group", w.ops.group)
|
||||
}()
|
||||
}
|
||||
conf := asynq.Config{
|
||||
Concurrency: g.Cfg().MustGet(context.Background(), "queue.asynq.concurrency").Int(), // 最大同时执行的任务数量
|
||||
Queues: map[string]int{ops.group: 10}, // 队列名称和优先级
|
||||
LogLevel: asynq.LogLevel(g.Cfg().MustGet(context.Background(), "queue.asynq.logLevel").Int()), // 日志级别 DebugLevel 1, InfoLevel 2, WarnLevel 3, ErrorLevel 4, FatalLevel 5
|
||||
}
|
||||
if w.ops.aggregator != nil {
|
||||
conf.GroupAggregator = asynq.GroupAggregatorFunc(aggregate)
|
||||
conf.GroupGracePeriod = time.Duration(w.ops.aggregator.GroupGracePeriod) * time.Second
|
||||
conf.GroupMaxDelay = time.Duration(w.ops.aggregator.GroupMaxDelay) * time.Second
|
||||
conf.GroupMaxSize = w.ops.aggregator.GroupMaxSize
|
||||
g.Log().Infof(context.Background(), "server/internal/library/worker/worker.go New 聚合器配置 group %s, gracePeriod %d, maxDelay %d, maxSize %d",
|
||||
w.ops.group, w.ops.aggregator.GroupGracePeriod, w.ops.aggregator.GroupMaxDelay, w.ops.aggregator.GroupMaxSize)
|
||||
}
|
||||
svr := asynq.NewServer(rs, conf)
|
||||
h := &taskHandlerBase{w: w}
|
||||
|
||||
mux := asynq.NewServeMux()
|
||||
if w.ops.aggregator != nil {
|
||||
mux.HandleFunc(w.ops.group+".aggregate", h.ProcessTask)
|
||||
} else {
|
||||
mux.HandleFunc(w.ops.group+".once", h.ProcessTask)
|
||||
mux.HandleFunc(w.ops.group+".cron", h.ProcessTask)
|
||||
}
|
||||
|
||||
if err := svr.Start(mux); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
w.svr = svr
|
||||
if w.ops.clearArchived > 0 {
|
||||
go func() {
|
||||
time.Sleep(time.Duration(w.ops.clearArchived) * time.Second)
|
||||
w.clearArchived()
|
||||
}()
|
||||
}
|
||||
g.Log().Debug(context.Background(), "server/internal/library/worker/worker.go New", "group", w.ops.group)
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *Worker) Once(options ...func(*TaskOptions)) (err error) {
|
||||
ops := GetDefaultTaskOptions(nil)
|
||||
for _, op := range options {
|
||||
op(ops)
|
||||
}
|
||||
if ops.uid == "" {
|
||||
err = errors.New("uid is nil")
|
||||
return
|
||||
}
|
||||
t := asynq.NewTask(ops.group+".once", ops.payload, asynq.TaskID(ops.uid), asynq.Group(ops.group))
|
||||
taskOpts := []asynq.Option{
|
||||
asynq.Queue(w.ops.group),
|
||||
asynq.MaxRetry(w.ops.maxRetry),
|
||||
asynq.Timeout(time.Duration(ops.timeout) * time.Second),
|
||||
}
|
||||
if ops.maxRetry > 0 {
|
||||
taskOpts = append(taskOpts, asynq.MaxRetry(ops.maxRetry))
|
||||
}
|
||||
if ops.retention > 0 {
|
||||
taskOpts = append(taskOpts, asynq.Retention(time.Duration(ops.retention)*time.Second))
|
||||
} else {
|
||||
taskOpts = append(taskOpts, asynq.Retention(time.Duration(w.ops.timeout)*time.Second))
|
||||
}
|
||||
if ops.in != nil {
|
||||
taskOpts = append(taskOpts, asynq.ProcessIn(*ops.in))
|
||||
} else if ops.at != nil {
|
||||
taskOpts = append(taskOpts, asynq.ProcessAt(*ops.at))
|
||||
} else if ops.now {
|
||||
taskOpts = append(taskOpts, asynq.ProcessIn(time.Millisecond))
|
||||
}
|
||||
_, err = w.client.Enqueue(t, taskOpts...)
|
||||
if ops.replace && errors.Is(err, asynq.ErrTaskIDConflict) {
|
||||
// 错误表示由于给定的任务ID已经存在,因此无法将该任务加入队列。这可能是因为任务已经存在,或者任务ID冲突。
|
||||
err = w.inspector.DeleteTask(w.ops.group, ops.uid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = w.client.Enqueue(t, taskOpts...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Cron 注册一个cron任务, 如果concurrent为0,则不限制并发,否则限制并发
|
||||
func (w *Worker) Cron(concurrent int, options ...func(*TaskOptions)) (entryID string, err error) {
|
||||
ops := GetDefaultTaskOptions(nil)
|
||||
for _, op := range options {
|
||||
op(ops)
|
||||
}
|
||||
if ops.expr == "" {
|
||||
err = errors.New("expr is nil")
|
||||
return
|
||||
}
|
||||
if concurrent == 0 {
|
||||
// 不限制并发, 不传递任务id,任务ID随机,可并发
|
||||
entryID, err = w.scheduler.Register(ops.expr, asynq.NewTask(ops.group+".cron", ops.payload), asynq.Queue(w.ops.group))
|
||||
} else {
|
||||
// 限制并发, 传递任务id,任务ID固定,不可并发
|
||||
entryID, err = w.scheduler.Register(ops.expr, asynq.NewTask(ops.group+".cron", ops.payload, asynq.TaskID(ops.uid)), asynq.Queue(w.ops.group))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
g.Log().Debug(context.Background(), "server/internal/library/worker/worker.go Cron", "group", w.ops.group, "entryID", entryID, "expr", ops.expr, "concurrent", concurrent)
|
||||
return
|
||||
}
|
||||
|
||||
func (w *Worker) Remove(uid string) (err error) {
|
||||
entries, err := w.inspector.SchedulerEntries()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
payload := string(entry.Task.Payload())
|
||||
g.Log().Debug(context.Background(), "server/internal/library/worker/worker.go Remove", "group", w.ops.group, "entryID", entry.ID, "payload", payload)
|
||||
if strings.Contains(string(entry.Task.Payload()), "\""+uid+"\"") {
|
||||
g.Log().Debug(context.Background(), "strings.Contains(string(entry.Task.Payload()), uid) ")
|
||||
err = w.scheduler.Unregister(entry.ID)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (w *Worker) GetTaskInfo(uid string) (task *asynq.TaskInfo, err error) {
|
||||
task, err = w.inspector.GetTaskInfo(w.ops.group, uid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func (w *Worker) RemoveCron(entryID string) (err error) {
|
||||
err = w.scheduler.Unregister(entryID)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取带有默认超时的上下文
|
||||
func (w *Worker) getDefaultTimeoutCtx() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), time.Duration(w.ops.timeout)*time.Second)
|
||||
}
|
||||
|
||||
// 清除已归档任务
|
||||
func (w *Worker) clearArchived() {
|
||||
list, err := w.inspector.ListArchivedTasks(w.ops.group, asynq.Page(1), asynq.PageSize(100))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// ctx := w.getDefaultTimeoutCtx()
|
||||
for _, item := range list {
|
||||
last := carbon.CreateFromStdTime(item.LastFailedAt)
|
||||
if !last.IsZero() && item.Retried < item.MaxRetry {
|
||||
continue
|
||||
}
|
||||
uid := item.ID
|
||||
var flag bool
|
||||
if strings.HasSuffix(item.Type, ".cron") {
|
||||
if carbon.Now().Gt(last.AddMinutes(5)) {
|
||||
flag = true
|
||||
}
|
||||
} else {
|
||||
if carbon.Now().Gt(last.AddMinutes(5)) {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
if flag {
|
||||
err := w.inspector.DeleteTask(w.ops.group, uid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type taskHandlerBase struct {
|
||||
w *Worker
|
||||
}
|
||||
|
||||
func (h *taskHandlerBase) ProcessTask(ctx context.Context, task *asynq.Task) (err error) {
|
||||
var isAggregate bool
|
||||
if strings.Contains(task.Type(), ".aggregate") {
|
||||
isAggregate = true
|
||||
}
|
||||
group := strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(task.Type(), ".aggregate"), ".once"), ".cron")
|
||||
if h.w.ops.handler == nil {
|
||||
g.Log().Errorf(ctx, "server/internal/library/worker/worker.go ProcessTask handler is nil, group: %s, uid: %s, payload: %s", group, task.ResultWriter().TaskID(), string(task.Payload()))
|
||||
return errors.New("handler is nil")
|
||||
}
|
||||
err = h.w.ops.handler(ctx, Payload{
|
||||
IsAggregate: isAggregate,
|
||||
Group: group,
|
||||
Uid: task.ResultWriter().TaskID(),
|
||||
Payload: task.Payload(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (w *Worker) Stop() {
|
||||
if w.scheduler != nil {
|
||||
w.scheduler.Shutdown()
|
||||
}
|
||||
if w.client != nil {
|
||||
w.client.Close()
|
||||
}
|
||||
if w.inspector != nil {
|
||||
w.inspector.Close()
|
||||
}
|
||||
if w.svr != nil {
|
||||
w.svr.Shutdown()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// package genconsts
|
||||
// @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 genconsts
|
||||
|
||||
// 生成类型值定义
|
||||
const (
|
||||
GenCodesTypeCurd = 0
|
||||
GenCodesTypeTree = 1
|
||||
)
|
||||
|
||||
// 生成类型标签映射
|
||||
var GenCodesTypeNameMap = map[int]string{
|
||||
GenCodesTypeCurd: "增删改查列表",
|
||||
GenCodesTypeTree: "关系树列表",
|
||||
}
|
||||
|
||||
// 生成类型配置映射
|
||||
var GenCodesTypeConfMap = map[int]string{
|
||||
GenCodesTypeCurd: "crud",
|
||||
GenCodesTypeTree: "tree",
|
||||
}
|
||||
|
||||
// 生成代码状态
|
||||
const (
|
||||
GenCodesStatusOk = "0" // 生成成功
|
||||
GenCodesStatusWait = "1" // 未生成
|
||||
GenCodesStatusFail = "2" // 生成失败
|
||||
)
|
||||
|
||||
var GenCodesStatusNameMap = map[string]string{
|
||||
GenCodesStatusOk: "生成成功",
|
||||
GenCodesStatusWait: "未生成",
|
||||
GenCodesStatusFail: "生成失败",
|
||||
}
|
||||
|
||||
// 生成代码关联表方式
|
||||
const (
|
||||
GenCodesJoinLeft = 1 // 左关联
|
||||
GenCodesJoinRight = 2 // 右关联
|
||||
GenCodesJoinInner = 3 // 内关联
|
||||
)
|
||||
|
||||
var GenCodesJoinNameMap = map[int]string{
|
||||
GenCodesJoinLeft: "左关联",
|
||||
GenCodesJoinRight: "右关联",
|
||||
GenCodesJoinInner: "内关联",
|
||||
}
|
||||
|
||||
var GenCodesJoinLinkMap = map[int]string{
|
||||
GenCodesJoinLeft: "LeftJoin",
|
||||
GenCodesJoinRight: "RightJoin",
|
||||
GenCodesJoinInner: "InnerJoin",
|
||||
}
|
||||
|
||||
// 生成代码的生成方式
|
||||
const (
|
||||
GenCodesBuildMethCreate = 1 // 创建
|
||||
GenCodesBuildMethCover = 2 // 覆盖
|
||||
GenCodesBuildMethSkip = 3 // 跳过
|
||||
GenCodesBuildIgnore = 4 // 不生成
|
||||
)
|
||||
|
||||
var GenCodesBuildMethNameMap = map[int]string{
|
||||
GenCodesBuildMethCreate: "创建文件",
|
||||
GenCodesBuildMethCover: "强制覆盖",
|
||||
GenCodesBuildMethSkip: "已存在跳过",
|
||||
GenCodesBuildIgnore: "不生成",
|
||||
}
|
||||
|
||||
const (
|
||||
GenCodesIndexPK = "PRI" // 主键索引
|
||||
GenCodesIndexUNI = "UNI" // 唯一索引
|
||||
)
|
||||
|
||||
const (
|
||||
GenCodesTreeStyleTypeNormal = 1 // 普通树表格
|
||||
GenCodesTreeStyleTypeOption = 2 // 选项式树表
|
||||
)
|
||||
|
||||
var GenCodesTreeStyleTypeNameMap = map[int]string{
|
||||
GenCodesTreeStyleTypeNormal: "普通树表格",
|
||||
GenCodesTreeStyleTypeOption: "选项式树表",
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// package genconsts
|
||||
// @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 genconsts
|
||||
|
||||
import "github.com/gogf/gf/v2/util/gconv"
|
||||
|
||||
// 配置数据类型
|
||||
const (
|
||||
ConfigTypeString = "string"
|
||||
ConfigTypeInt = "int"
|
||||
ConfigTypeInt8 = "int8"
|
||||
ConfigTypeInt16 = "int16"
|
||||
ConfigTypeInt32 = "int32"
|
||||
ConfigTypeInt64 = "int64"
|
||||
ConfigTypeUint = "uint"
|
||||
ConfigTypeUint8 = "uint8"
|
||||
ConfigTypeUint16 = "uint16"
|
||||
ConfigTypeUint32 = "uint32"
|
||||
ConfigTypeUint64 = "uint64"
|
||||
ConfigTypeFloat32 = "float32"
|
||||
ConfigTypeFloat64 = "float64"
|
||||
ConfigTypeBool = "bool"
|
||||
ConfigTypeDate = "date"
|
||||
ConfigTypeDateTime = "datetime"
|
||||
ConfigTypeSliceString = "[]string"
|
||||
ConfigTypeSliceInt = "[]int"
|
||||
ConfigTypeSliceInt64 = "[]int64"
|
||||
)
|
||||
|
||||
var ConfigTypes = []string{ConfigTypeString,
|
||||
ConfigTypeInt, ConfigTypeInt8, ConfigTypeInt16, ConfigTypeInt32, ConfigTypeInt64,
|
||||
ConfigTypeUint, ConfigTypeUint8, ConfigTypeUint16, ConfigTypeUint32, ConfigTypeUint64,
|
||||
ConfigTypeFloat32, ConfigTypeFloat64,
|
||||
ConfigTypeBool,
|
||||
ConfigTypeDate, ConfigTypeDateTime,
|
||||
ConfigTypeSliceString, ConfigTypeSliceInt, ConfigTypeSliceInt64,
|
||||
}
|
||||
|
||||
// ConvType 类型转换
|
||||
func ConvType(val interface{}, t string) interface{} {
|
||||
switch t {
|
||||
case ConfigTypeString:
|
||||
val = gconv.String(val)
|
||||
case ConfigTypeInt:
|
||||
val = gconv.Int(val)
|
||||
case ConfigTypeInt8:
|
||||
val = gconv.Int8(val)
|
||||
case ConfigTypeInt16:
|
||||
val = gconv.Int16(val)
|
||||
case ConfigTypeInt32:
|
||||
val = gconv.Int32(val)
|
||||
case ConfigTypeInt64:
|
||||
val = gconv.Int64(val)
|
||||
case ConfigTypeUint:
|
||||
val = gconv.Uint(val)
|
||||
case ConfigTypeUint8:
|
||||
val = gconv.Uint8(val)
|
||||
case ConfigTypeUint16:
|
||||
val = gconv.Uint16(val)
|
||||
case ConfigTypeUint32:
|
||||
val = gconv.Uint32(val)
|
||||
case ConfigTypeUint64:
|
||||
val = gconv.Uint64(val)
|
||||
case ConfigTypeFloat32:
|
||||
val = gconv.Float32(val)
|
||||
case ConfigTypeFloat64:
|
||||
val = gconv.Float64(val)
|
||||
case ConfigTypeBool:
|
||||
val = gconv.Bool(val)
|
||||
case ConfigTypeDate:
|
||||
val = gconv.Time(val, "Y-m-d")
|
||||
case ConfigTypeDateTime:
|
||||
val = gconv.Time(val, "Y-m-d H:i:s")
|
||||
case ConfigTypeSliceInt:
|
||||
val = gconv.SliceInt(val)
|
||||
case ConfigTypeSliceInt64:
|
||||
val = gconv.SliceInt64(val)
|
||||
case ConfigTypeSliceString:
|
||||
val = gconv.SliceStr(val)
|
||||
default:
|
||||
val = gconv.String(val)
|
||||
}
|
||||
return val
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// package gendao
|
||||
// @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 gendao
|
||||
|
||||
import (
|
||||
"context"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcfg"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
var (
|
||||
daoConfigList = make([]*genmodel.GenDaoConfig, 0)
|
||||
cliConfigPath = `hack/config.yaml`
|
||||
cliConfig *gcfg.Config
|
||||
defaultGenServiceConfig = genmodel.GenServiceConfig{
|
||||
SrcFolder: "internal/logic",
|
||||
DstFolder: "internal/service",
|
||||
DstFileNameCase: "Snake",
|
||||
StPattern: `s([A-Z]\w+)`,
|
||||
// gf gen service -l currently has a Windows path handling issue that
|
||||
// can remove all generated service files. Keep generation portable.
|
||||
Clear: false,
|
||||
}
|
||||
serviceConfig = genmodel.GenServiceConfig{}
|
||||
)
|
||||
|
||||
func GetDaoConfig(ctx context.Context, group string) (output *genmodel.GenDaoConfig, err error) {
|
||||
for _, v := range daoConfigList {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if v.Group == group {
|
||||
output = v
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Init(ctx context.Context) (err error) {
|
||||
adapter, err := gcfg.NewAdapterFile(cliConfigPath)
|
||||
if err != nil {
|
||||
g.Log().Fatalf(ctx, "get cli configuration file:%v, err:%+v", cliConfigPath, err)
|
||||
return
|
||||
}
|
||||
cliConfig = gcfg.NewWithAdapter(adapter)
|
||||
|
||||
cliConfig.MustGet(ctx, "gfcli.gen.dao").Scan(&daoConfigList)
|
||||
cliConfig.MustGet(ctx, "gfcli.gen.service").Scan(&serviceConfig)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func GetServiceConfig() *genmodel.GenServiceConfig {
|
||||
conf := &genmodel.GenServiceConfig{}
|
||||
_ = gconv.Scan(defaultGenServiceConfig, &conf)
|
||||
_ = gconv.Scan(serviceConfig, &conf)
|
||||
return conf
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// package genmodel
|
||||
// @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 genmodel
|
||||
|
||||
import "github.com/gogf/gf/v2/encoding/gjson"
|
||||
|
||||
type SelectItemModel struct {
|
||||
Value interface{} `json:"value"` // 选择项值
|
||||
Label string `json:"label"` // 选择项标签
|
||||
Name string `json:"name"` // 选择项名称
|
||||
Disabled bool `json:"disabled"` // 是否禁用
|
||||
}
|
||||
|
||||
// 生成类型选择项
|
||||
type SelectGenTypeModel struct {
|
||||
SelectItemModel
|
||||
Templates []*SelectGenTemplateModel `json:"templates"` // 选择项模板
|
||||
}
|
||||
|
||||
// 生成模板选择项
|
||||
type SelectGenTemplateModel struct {
|
||||
SelectItemModel
|
||||
IsAddon bool `json:"isAddon"` // 是否为插件
|
||||
}
|
||||
|
||||
type SelectsModel struct {
|
||||
GenType []*SelectGenTypeModel `json:"genType"` // 生成类型 0:增删改查列表 1:关系树列表
|
||||
Db []*SelectItemModel `json:"db"` // 数据库选择项
|
||||
Status []*SelectItemModel `json:"status"` // 状态选择项 0:生成成功 1:未生产 2:生成失败
|
||||
LinkMode []*SelectItemModel `json:"linkMode"` // 链接模式选择项 1左关联 2右关联 3内关联
|
||||
BuildMethod []*SelectItemModel `json:"buildMethod"` // 构建标志选择项 1:创建文件 2强制覆盖 3已存在跳过 4不生成
|
||||
FormMode []*SelectItemModel `json:"formMode"` // 表单项类型选择项
|
||||
FormRole []*SelectItemModel `json:"formRole"` // 表单项校验规则选择项
|
||||
DictMode []*SelectItemModel `json:"dictMode"` // 字典模式选择项
|
||||
WhereMode []*SelectItemModel `json:"whereMode"` // 查询条件选择项
|
||||
Addons []*SelectItemModel `json:"addons"` // 插件选择项
|
||||
TableAlign []*SelectItemModel `json:"tableAlign"` // 表格对齐方式选择项
|
||||
TreeStyleType []*SelectItemModel `json:"treeStyleType"` // 树形结构选择项
|
||||
}
|
||||
|
||||
// GenCodesColumn 生成表字段属性
|
||||
type GenCodesColumn struct {
|
||||
// 表属性
|
||||
Id int64 `json:"id" dc:"序号"`
|
||||
Name string `json:"name" dc:"字段列名"`
|
||||
Dc string `json:"dc" dc:"字段描述"`
|
||||
DataType string `json:"dataType" dc:"字段类型"`
|
||||
SqlType string `json:"sqlType" dc:"物理类型"`
|
||||
Length int64 `json:"length" dc:"字段长度"`
|
||||
IsAllowNull string `json:"isAllowNull" dc:"是否允许为空"`
|
||||
DefaultValue interface{} `json:"defaultValue" dc:"默认值"`
|
||||
Index string `json:"index" dc:"索引"`
|
||||
Extra string `json:"extra" dc:"额外选项"`
|
||||
// 自定义生成属性
|
||||
// Alias string `json:"alias" dc:"字段别名"`
|
||||
GoName string `json:"goName" dc:"Go属性"`
|
||||
GoType string `json:"goType" dc:"Go类型"`
|
||||
TsName string `json:"tsName" dc:"Ts属性"`
|
||||
TsType string `json:"tsType" dc:"Ts类型"`
|
||||
IsList bool `json:"isList" dc:"列表"`
|
||||
IsExport bool `json:"isExport" dc:"导出"`
|
||||
IsSort bool `json:"isSort" dc:"排序"`
|
||||
IsQuery bool `json:"isQuery" dc:"查询"`
|
||||
QueryWhere string `json:"queryWhere" dc:"查询条件"`
|
||||
IsEdit bool `json:"isEdit" dc:"编辑"`
|
||||
Required bool `json:"required" dc:"必填"`
|
||||
Unique bool `json:"unique" dc:"唯一性"`
|
||||
FormMode string `json:"formMode" dc:"表单组件"`
|
||||
FormRole string `json:"formRole" dc:"表单验证"`
|
||||
FormGridSpan int `json:"formGridSpan" dc:"栅格占位数量"`
|
||||
DictType string `json:"dictType" dc:"字典类型ID"`
|
||||
Align string `json:"align" dc:"排列方式"`
|
||||
Width int64 `json:"width" dc:"列宽"`
|
||||
}
|
||||
|
||||
type GenCodesTableSelectParam struct {
|
||||
DbGroup string `json:"dbGroup"`
|
||||
}
|
||||
|
||||
type GenCodesTableSelectModel struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
Name string `json:"name"`
|
||||
DaoName string `json:"daoName" dc:"orm模型名称"`
|
||||
DefVarName string `json:"defVarName" dc:"默认实体名称"`
|
||||
DefAlias string `json:"defAlias" dc:"默认关联表别名"`
|
||||
DefTableComment string `json:"defTableComment" dc:"默认菜单名称"`
|
||||
}
|
||||
|
||||
type GenCodesColumnSelectParam struct {
|
||||
DbGroup string `json:"dbGroup"`
|
||||
TableName string `json:"tableName"`
|
||||
}
|
||||
|
||||
type GenCodesColumnSelectModel struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type GenCodesColumnListParam struct {
|
||||
DbGroup string `json:"dbGroup" dc:"数据库配置名称"`
|
||||
TableName string `json:"tableName" dc:"表名称"`
|
||||
IsLink int64 `json:"isLink" dc:"是否是关联表"`
|
||||
Alias string `json:"alias" dc:"关联表别名"`
|
||||
}
|
||||
|
||||
type GenCodesColumnListModel struct {
|
||||
GenCodesColumn
|
||||
}
|
||||
|
||||
type GenCodesPreviewParam struct {
|
||||
TableId int64 `json:"tableId" orm:"table_id" description:"生成ID"`
|
||||
GenType uint `json:"genType" orm:"gen_type" description:"生成类型"`
|
||||
GenTemplate int `json:"genTemplate" orm:"gen_template" description:"生成模板"`
|
||||
VarName string `json:"varName" orm:"var_name" description:"实体命名"`
|
||||
Options *gjson.Json `json:"options" orm:"options" description:"配置选项"`
|
||||
DbName string `json:"dbName" orm:"db_name" description:"数据库名称"`
|
||||
TableName string `json:"tableName" orm:"table_name" description:"主表名称"`
|
||||
TableComment string `json:"tableComment" orm:"table_comment" description:"主表注释"`
|
||||
DaoName string `json:"daoName" orm:"dao_name" description:"主表dao模型"`
|
||||
MasterColumns *gjson.Json `json:"masterColumns" orm:"master_columns" description:"主表字段"`
|
||||
AddonName string `json:"addonName" orm:"addon_name" description:"插件名称"`
|
||||
Status int `json:"status" orm:"status" description:"生成状态"`
|
||||
}
|
||||
|
||||
// GenFile 生成文件配置
|
||||
type GenFile struct {
|
||||
Content string `json:"content" dc:"页面内容"`
|
||||
Path string `json:"path" dc:"生成路径"`
|
||||
Meth int `json:"meth" dc:"生成方式"`
|
||||
Required bool `json:"required" dc:"是否是必要构建文件"`
|
||||
}
|
||||
|
||||
type GenCodesPreviewModel struct {
|
||||
Config *GenCodesConfig `json:"config"`
|
||||
Views map[string]*GenFile `json:"views" dc:"页面"`
|
||||
}
|
||||
|
||||
type GenCodesBuildParam = GenCodesPreviewParam
|
||||
|
||||
type GenCodesBuildModel struct {
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// package genmodel
|
||||
// @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 genmodel
|
||||
|
||||
import (
|
||||
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
|
||||
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// 模板配置 config/config.yaml xgen.application.crud.templates
|
||||
type GenCodesConfigCrudTemplate struct {
|
||||
Group string `name:"group" json:"group"`
|
||||
IsAddon bool `name:"isAddon" json:"isAddon"`
|
||||
MasterPackage string `name:"masterPackage" json:"masterPackage"`
|
||||
TemplatePath string `name:"templatePath" json:"templatePath"`
|
||||
ApiPath string `name:"apiPath" json:"apiPath"`
|
||||
InputPath string `name:"inputPath" json:"inputPath"`
|
||||
ControllerPath string `name:"controllerPath" json:"controllerPath"`
|
||||
LogicPath string `name:"logicPath" json:"logicPath"`
|
||||
RouterPath string `name:"routerPath" json:"routerPath"`
|
||||
SqlPath string `name:"sqlPath" json:"sqlPath"`
|
||||
WebApiPath string `name:"webApiPath" json:"webApiPath"`
|
||||
WebViewsPath string `name:"webViewsPath" json:"webViewsPath"`
|
||||
}
|
||||
type GenCodesConfigTreeTemplate struct {
|
||||
Group string `json:"group"`
|
||||
TemplatePath string `json:"templatePath"`
|
||||
}
|
||||
|
||||
type GenCodesConfigBuildAddon struct {
|
||||
SrcPath string `json:"srcPath"`
|
||||
WebApiPath string `json:"webApiPath"`
|
||||
WebViewsPath string `json:"webViewsPath"`
|
||||
}
|
||||
|
||||
// 工具生成配置 hack/config.yaml gfcli.gen.dao
|
||||
type GenDaoConfig struct {
|
||||
Group string `name:"group" json:"group"`
|
||||
Link string `name:"link" json:"link"`
|
||||
DbName string `name:"dbName" json:"dbName"`
|
||||
TableName string `name:"tableName" json:"tableName"`
|
||||
JsonCase string `name:"jsonCase" json:"jsonCase"`
|
||||
StdTime bool `name:"stdTime" json:"stdTime"`
|
||||
GJsonSupport bool `name:"gJsonSupport" json:"gJsonSupport"`
|
||||
}
|
||||
|
||||
// 工具生成配置 hack/config.yaml gfcli.gen.service
|
||||
type GenServiceConfig struct {
|
||||
SrcFolder string `name:"srcFolder"`
|
||||
DstFolder string `name:"dstFolder"`
|
||||
DstFileNameCase string `name:"dstFileNameCase"`
|
||||
WatchFile string `name:"watchFile" `
|
||||
StPattern string `name:"stPattern" `
|
||||
Packages []string `name:"packages" `
|
||||
ImportPrefix string `name:"importPrefix" `
|
||||
Clear bool `name:"clear" `
|
||||
}
|
||||
|
||||
// 模板配置 config/config.yaml xgen
|
||||
type GenCodesConfig struct {
|
||||
AllowedIPs []string `json:"allowedIPs"`
|
||||
Application struct {
|
||||
Crud struct {
|
||||
Templates []*GenCodesConfigCrudTemplate `json:"templates"`
|
||||
} `json:"crud"`
|
||||
Tree struct {
|
||||
Templates []*GenCodesConfigTreeTemplate `json:"templates"`
|
||||
} `json:"tree"`
|
||||
} `json:"application"`
|
||||
Delimiters []string `json:"delimiters"`
|
||||
DevPath string `json:"devPath"`
|
||||
DisableTables []string `json:"disableTables"`
|
||||
SelectDbs []string `json:"selectDbs"`
|
||||
Addon *GenCodesConfigBuildAddon `json:"addon"`
|
||||
}
|
||||
|
||||
// ConvType 类型转换
|
||||
func ConvType(val interface{}, t string) interface{} {
|
||||
switch t {
|
||||
case genconsts.ConfigTypeString:
|
||||
val = gconv.String(val)
|
||||
case genconsts.ConfigTypeInt:
|
||||
val = gconv.Int(val)
|
||||
case genconsts.ConfigTypeInt8:
|
||||
val = gconv.Int8(val)
|
||||
case genconsts.ConfigTypeInt16:
|
||||
val = gconv.Int16(val)
|
||||
case genconsts.ConfigTypeInt32:
|
||||
val = gconv.Int32(val)
|
||||
case genconsts.ConfigTypeInt64:
|
||||
val = gconv.Int64(val)
|
||||
case genconsts.ConfigTypeUint:
|
||||
val = gconv.Uint(val)
|
||||
case genconsts.ConfigTypeUint8:
|
||||
val = gconv.Uint8(val)
|
||||
case genconsts.ConfigTypeUint16:
|
||||
val = gconv.Uint16(val)
|
||||
case genconsts.ConfigTypeUint32:
|
||||
val = gconv.Uint32(val)
|
||||
case genconsts.ConfigTypeUint64:
|
||||
val = gconv.Uint64(val)
|
||||
case genconsts.ConfigTypeFloat32:
|
||||
val = gconv.Float32(val)
|
||||
case genconsts.ConfigTypeFloat64:
|
||||
val = gconv.Float64(val)
|
||||
case genconsts.ConfigTypeBool:
|
||||
val = gconv.Bool(val)
|
||||
case genconsts.ConfigTypeDate:
|
||||
val = gconv.Time(val, "Y-m-d")
|
||||
case genconsts.ConfigTypeDateTime:
|
||||
val = gconv.Time(val, "Y-m-d H:i:s")
|
||||
case genconsts.ConfigTypeSliceInt:
|
||||
val = gconv.SliceInt(val)
|
||||
case genconsts.ConfigTypeSliceInt64:
|
||||
val = gconv.SliceInt64(val)
|
||||
case genconsts.ConfigTypeSliceString:
|
||||
val = gconv.SliceStr(val)
|
||||
default:
|
||||
val = gconv.String(val)
|
||||
}
|
||||
return val
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// package genmodel
|
||||
// @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 genmodel
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gview"
|
||||
)
|
||||
|
||||
// ImportModel 导包 - model.ts
|
||||
type ImportModel struct {
|
||||
NaiveUI []string
|
||||
UtilsIs []string
|
||||
UtilsUrl []string
|
||||
UtilsDate []string
|
||||
UtilsValidate []string
|
||||
UtilsIndex []string
|
||||
}
|
||||
|
||||
// CurdStep 生成选项
|
||||
type CurdStep struct {
|
||||
HasMaxSort bool // 最大排序
|
||||
HasAdd bool // 表单添加
|
||||
HasBatchDel bool // 批量删除
|
||||
HasExport bool // 表格导出
|
||||
HasNotFilterAuth bool // 不过滤认证权限
|
||||
HasEdit bool // 表单编辑
|
||||
HasDel bool // 删除
|
||||
HasView bool // 查看详情
|
||||
HasStatus bool // 修改状态
|
||||
HasSwitch bool // 数值开关
|
||||
HasCheck bool // 勾选列
|
||||
HasMenu bool // 菜单权限
|
||||
IsTreeTable bool // 树型列表
|
||||
IsOptionTreeTable bool // 选项式树型列表
|
||||
HasRules bool // 表单验证规则
|
||||
HasRulesValidator bool // 表单验证器
|
||||
HasSearchForm bool // 列表搜索
|
||||
HasDict bool // 字典
|
||||
HasFuncDict bool // 注册方法字典
|
||||
HasQueryMemberSummary bool // 查询用户摘要
|
||||
HasHookMemberSummary bool // hook用户摘要
|
||||
ImportModel ImportModel // 公用导包 - model.ts
|
||||
ActionColumnWidth int64 // 列表操作栏宽度
|
||||
IsAddon bool // 是否是插件
|
||||
IsEditModal bool // 新增修改模态框模式
|
||||
}
|
||||
|
||||
type FuncDict struct {
|
||||
ValueColumn string // 选项值
|
||||
LabelColumn string // 选项名称
|
||||
Value *GenCodesColumnListModel
|
||||
Label *GenCodesColumnListModel
|
||||
}
|
||||
|
||||
// PresetStep 预设生成流程参数
|
||||
type PresetStep struct {
|
||||
FormGridCols int `json:"formGridCols" dc:"表单显示的栅格数量"`
|
||||
}
|
||||
|
||||
// OptionsTree 树形结构选项
|
||||
type OptionsTree struct {
|
||||
TitleColumn string `json:"titleColumn"`
|
||||
PidColumn string `json:"pidColumn"`
|
||||
LevelColumn string `json:"levelColumn"`
|
||||
TreeColumn string `json:"treeColumn"`
|
||||
StyleType int `json:"styleType"`
|
||||
TitleField *GenCodesColumnListModel
|
||||
PidField *GenCodesColumnListModel
|
||||
LevelField *GenCodesColumnListModel
|
||||
TreeField *GenCodesColumnListModel
|
||||
HasTreePath bool
|
||||
}
|
||||
|
||||
// CurdOptionsMenu 菜单选项
|
||||
type CurdOptionsMenu struct {
|
||||
Icon string `json:"icon"`
|
||||
Pid int `json:"pid"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// CurdOptionsJoin 关联表选项
|
||||
type CurdOptionsJoin struct {
|
||||
Uuid string `json:"uuid"`
|
||||
LinkTable string `json:"linkTable"`
|
||||
Alias string `json:"alias"`
|
||||
LinkMode int `json:"linkMode"`
|
||||
Field string `json:"field"`
|
||||
MasterField string `json:"masterField"`
|
||||
DaoName string `json:"daoName"`
|
||||
Columns []*GenCodesColumnListModel `json:"columns"`
|
||||
}
|
||||
|
||||
type OptionsSchemasField struct {
|
||||
Field string
|
||||
Type string
|
||||
}
|
||||
|
||||
type CurdOptionsDict struct {
|
||||
Has bool
|
||||
Types []string
|
||||
Schemas []*OptionsSchemasField
|
||||
}
|
||||
|
||||
// CurdOptions 生成选项
|
||||
type CurdOptions struct {
|
||||
AutoOps []string `json:"autoOps"`
|
||||
ColumnOps []string `json:"columnOps"`
|
||||
HeadOps []string `json:"headOps"`
|
||||
Join []*CurdOptionsJoin `json:"join"`
|
||||
Menu *CurdOptionsMenu `json:"menu"`
|
||||
Tree *OptionsTree `json:"tree"`
|
||||
TemplateGroup string `json:"templateGroup"`
|
||||
ApiPrefix string `json:"apiPrefix"`
|
||||
ImportWebApi string `json:"importWebApi"`
|
||||
FuncDict *FuncDict `json:"funcDict"`
|
||||
PresetStep *PresetStep `json:"presetStep"`
|
||||
Step *CurdStep // 转换后的流程控制条件
|
||||
DictOps CurdOptionsDict // 字典选项
|
||||
DictMap g.Map // 字典选项 -> 字段映射关系
|
||||
}
|
||||
|
||||
type CurdPreviewParam struct {
|
||||
In *GenCodesPreviewParam // 提交参数
|
||||
DaoConfig *GenDaoConfig // 生成dao配置
|
||||
Config *GenCodesConfig // 生成配置
|
||||
View *gview.View // 视图模板
|
||||
Content *GenCodesPreviewModel // 页面代码
|
||||
MasterFields []*GenCodesColumnListModel // 主表字段属性
|
||||
Pk *GenCodesColumnListModel // 主键属性
|
||||
Options *CurdOptions // 生成选项
|
||||
}
|
||||
|
||||
type CurdBuildEvent map[string]func(ctx context.Context) (err error)
|
||||
|
||||
type CurdBuildParam struct {
|
||||
PreviewIn *CurdPreviewParam // 预览参数
|
||||
BeforeEvent CurdBuildEvent // 前置事件
|
||||
AfterEvent CurdBuildEvent // 后置事件
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
"context"
|
||||
gendao "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_dao"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
func getJsonTagFromCase(str, caseStr string) string {
|
||||
switch gstr.ToLower(caseStr) {
|
||||
case gstr.ToLower("Camel"):
|
||||
return gstr.CaseCamel(str)
|
||||
|
||||
case gstr.ToLower("CamelLower"):
|
||||
return gstr.CaseCamelLower(str)
|
||||
|
||||
case gstr.ToLower("Kebab"):
|
||||
return gstr.CaseKebab(str)
|
||||
|
||||
case gstr.ToLower("KebabScreaming"):
|
||||
return gstr.CaseKebabScreaming(str)
|
||||
|
||||
case gstr.ToLower("Snake"):
|
||||
return gstr.CaseSnake(str)
|
||||
|
||||
case gstr.ToLower("SnakeFirstUpper"):
|
||||
return gstr.CaseSnakeFirstUpper(str)
|
||||
|
||||
case gstr.ToLower("SnakeScreaming"):
|
||||
return gstr.CaseSnakeScreaming(str)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue interface{}) (string, error) {
|
||||
var (
|
||||
typeName string
|
||||
typePattern string
|
||||
)
|
||||
match, _ := gregex.MatchString(`(.+?)\((.+)\)`, fieldType)
|
||||
if len(match) == 3 {
|
||||
typeName = gstr.Trim(match[1])
|
||||
typePattern = gstr.Trim(match[2])
|
||||
} else {
|
||||
typeName = gstr.Split(fieldType, " ")[0]
|
||||
}
|
||||
typeName = strings.ToLower(typeName)
|
||||
switch typeName {
|
||||
case
|
||||
"binary",
|
||||
"varbinary",
|
||||
"blob",
|
||||
"tinyblob",
|
||||
"mediumblob",
|
||||
"longblob":
|
||||
return string(gdb.LocalTypeBytes), nil
|
||||
|
||||
case
|
||||
"int",
|
||||
"tinyint",
|
||||
"small_int",
|
||||
"smallint",
|
||||
"medium_int",
|
||||
"mediumint",
|
||||
"serial":
|
||||
if gstr.ContainsI(fieldType, "unsigned") {
|
||||
return string(gdb.LocalTypeUint), nil
|
||||
}
|
||||
return string(gdb.LocalTypeInt), nil
|
||||
|
||||
case
|
||||
"big_int",
|
||||
"bigint",
|
||||
"bigserial":
|
||||
if gstr.ContainsI(fieldType, "unsigned") {
|
||||
return string(gdb.LocalTypeUint64), nil
|
||||
}
|
||||
return string(gdb.LocalTypeInt64), nil
|
||||
|
||||
case
|
||||
"real":
|
||||
return string(gdb.LocalTypeFloat32), nil
|
||||
|
||||
case
|
||||
"float",
|
||||
"double",
|
||||
"decimal",
|
||||
"money",
|
||||
"numeric",
|
||||
"smallmoney":
|
||||
return string(gdb.LocalTypeFloat64), nil
|
||||
|
||||
case
|
||||
"bit":
|
||||
// It is suggested using bit(1) as boolean.
|
||||
if typePattern == "1" {
|
||||
return string(gdb.LocalTypeBool), nil
|
||||
}
|
||||
s := gconv.String(fieldValue)
|
||||
// mssql is true|false string.
|
||||
if strings.EqualFold(s, "true") || strings.EqualFold(s, "false") {
|
||||
return string(gdb.LocalTypeBool), nil
|
||||
}
|
||||
if gstr.ContainsI(fieldType, "unsigned") {
|
||||
return string(gdb.LocalTypeUint64Bytes), nil
|
||||
}
|
||||
return string(gdb.LocalTypeInt64Bytes), nil
|
||||
|
||||
case
|
||||
"bool":
|
||||
return string(gdb.LocalTypeBool), nil
|
||||
|
||||
case
|
||||
"date":
|
||||
return string(gdb.LocalTypeDate), nil
|
||||
|
||||
case
|
||||
"datetime",
|
||||
"timestamp",
|
||||
"timestamptz":
|
||||
return string(gdb.LocalTypeDatetime), nil
|
||||
|
||||
case
|
||||
"json":
|
||||
return string(gdb.LocalTypeJson), nil
|
||||
|
||||
case
|
||||
"jsonb":
|
||||
return string(gdb.LocalTypeJsonb), nil
|
||||
|
||||
default:
|
||||
// Auto-detect field type, using key match.
|
||||
switch {
|
||||
case strings.Contains(typeName, "text") || strings.Contains(typeName, "char") || strings.Contains(typeName, "character"):
|
||||
return string(gdb.LocalTypeString), nil
|
||||
|
||||
case strings.Contains(typeName, "float") || strings.Contains(typeName, "double") || strings.Contains(typeName, "numeric"):
|
||||
return string(gdb.LocalTypeFloat64), nil
|
||||
|
||||
case strings.Contains(typeName, "bool"):
|
||||
return string(gdb.LocalTypeBool), nil
|
||||
|
||||
case strings.Contains(typeName, "binary") || strings.Contains(typeName, "blob"):
|
||||
return string(gdb.LocalTypeBytes), nil
|
||||
|
||||
case strings.Contains(typeName, "int"):
|
||||
if gstr.ContainsI(fieldType, "unsigned") {
|
||||
return string(gdb.LocalTypeUint), nil
|
||||
}
|
||||
return string(gdb.LocalTypeInt), nil
|
||||
|
||||
case strings.Contains(typeName, "time"):
|
||||
return string(gdb.LocalTypeDatetime), nil
|
||||
|
||||
case strings.Contains(typeName, "date"):
|
||||
return string(gdb.LocalTypeDatetime), nil
|
||||
|
||||
default:
|
||||
return string(gdb.LocalTypeString), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GenGotype 生成字段的go类型
|
||||
func GenGotype(ctx context.Context, field *genmodel.GenCodesColumnListModel, in *genmodel.GenDaoConfig) (goName, typeName, tsName string, tsType string) {
|
||||
var err error
|
||||
tsName = getJsonTagFromCase(field.Name, in.JsonCase)
|
||||
goName = gstr.CaseCamel(field.Name)
|
||||
|
||||
typeName, err = CheckLocalTypeForField(ctx, field.DataType, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch gdb.LocalType(typeName) {
|
||||
case gdb.LocalTypeDate, gdb.LocalTypeDatetime:
|
||||
if in.StdTime {
|
||||
typeName = "time.Time"
|
||||
} else {
|
||||
typeName = "*gtime.Time"
|
||||
}
|
||||
|
||||
case gdb.LocalTypeInt64Bytes:
|
||||
typeName = "int64"
|
||||
|
||||
case gdb.LocalTypeUint64Bytes:
|
||||
typeName = "uint64"
|
||||
|
||||
// Special type handle.
|
||||
case gdb.LocalTypeJson, gdb.LocalTypeJsonb:
|
||||
if in.GJsonSupport {
|
||||
typeName = "*gjson.Json"
|
||||
} else {
|
||||
typeName = "string"
|
||||
}
|
||||
}
|
||||
|
||||
tsType = ShiftMap[typeName]
|
||||
return
|
||||
}
|
||||
|
||||
func CustomLinkAttributes(ctx context.Context, alias string, field *genmodel.GenCodesColumnListModel, in *genmodel.GenDaoConfig) {
|
||||
field.GoName, field.GoType, field.TsName, field.TsType = GenGotype(ctx, field, in)
|
||||
|
||||
field.GoName = gstr.UcFirst(alias + field.GoName)
|
||||
field.TsName = gstr.LcFirst(field.GoName)
|
||||
|
||||
setDefaultQueryWhere(field)
|
||||
setDefaultValue(field)
|
||||
}
|
||||
|
||||
func CustomAttributes(ctx context.Context, field *genmodel.GenCodesColumnListModel, in *genmodel.GenDaoConfig) {
|
||||
field.GoName, field.GoType, field.TsName, field.TsType = GenGotype(ctx, field, in)
|
||||
setDefault(field)
|
||||
}
|
||||
|
||||
func TableColumnList(ctx context.Context, params *genmodel.GenCodesColumnListParam) (output []*genmodel.GenCodesColumnListModel, err error) {
|
||||
sql := "select ORDINAL_POSITION as `id`, COLUMN_NAME as `name`, COLUMN_COMMENT as `dc`, DATA_TYPE as `dataType`, COLUMN_TYPE as `sqlType`, CHARACTER_MAXIMUM_LENGTH as `length`, IS_NULLABLE as `isAllowNull`, COLUMN_DEFAULT as `defaultValue`, COLUMN_KEY as `index`, EXTRA as `extra` from information_schema.COLUMNS where TABLE_SCHEMA = ? and TABLE_NAME = ? ORDER BY `id` ASC"
|
||||
dbConfig := g.DB(params.DbGroup).GetConfig()
|
||||
daoConfig, err := gendao.GetDaoConfig(ctx, params.DbGroup)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
output = make([]*genmodel.GenCodesColumnListModel, 0)
|
||||
err = g.DB(params.DbGroup).Ctx(ctx).Raw(sql, dbConfig.Name, params.TableName).Scan(&output)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(output) == 0 {
|
||||
return
|
||||
}
|
||||
for _, v := range output {
|
||||
if params.IsLink == 1 {
|
||||
CustomLinkAttributes(ctx, params.Alias, v, daoConfig)
|
||||
} else {
|
||||
CustomAttributes(ctx, v, daoConfig)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
// 默认表单组件映射 Ts -> 表单组件
|
||||
var defaultFormModeMap = map[string]string{
|
||||
TsTypeString: FMInput,
|
||||
TsTypeNumber: FMInputNumber,
|
||||
TsTypeBoolean: FMInputNumber,
|
||||
TsTypeArray: FMInputDynamic,
|
||||
TsTypeTuple: FMInputDynamic,
|
||||
TsTypeAny: FMInput,
|
||||
}
|
||||
|
||||
var defaultEditFields = map[string]bool{
|
||||
"id": false,
|
||||
"created_dept": false,
|
||||
"created_by": false,
|
||||
"created_at": false,
|
||||
"updated_by": false,
|
||||
"updated_at": false,
|
||||
"deleted_by": false,
|
||||
"deleted_at": false,
|
||||
}
|
||||
|
||||
var defaultEditSwitch = map[string]bool{
|
||||
"id": false,
|
||||
"level": false,
|
||||
"tree": false,
|
||||
"created_by": false,
|
||||
"updated_by": false,
|
||||
"created_dept": false,
|
||||
"created_at": false,
|
||||
"updated_at": false,
|
||||
"deleted_at": false,
|
||||
}
|
||||
|
||||
var defaultListSwitch = map[string]bool{
|
||||
"level": false,
|
||||
"tree": false,
|
||||
"deleted_at": false,
|
||||
}
|
||||
|
||||
var defaultExportSwitch = map[string]bool{
|
||||
"level": false,
|
||||
"tree": false,
|
||||
"deleted_at": false,
|
||||
}
|
||||
|
||||
var defaultQuerySwitch = map[string]bool{
|
||||
"level": false,
|
||||
"tree": false,
|
||||
"deleted_at": false,
|
||||
}
|
||||
|
||||
var defaultSort = map[string]bool{
|
||||
"id": true,
|
||||
"sort": true,
|
||||
}
|
||||
|
||||
var defaultTreeFields = []string{"pid", "level", "tree"}
|
||||
|
||||
// 默认表单验证映射 物理类型命名识别
|
||||
var defaultFormRoleMap = map[string]string{
|
||||
"mobile": FRPhone,
|
||||
"qq": FRQq,
|
||||
"email": FREmail,
|
||||
"id_card": FRIdCard,
|
||||
"bank_card": FRBankCard,
|
||||
"password": FRPassword,
|
||||
"pass": FRPassword,
|
||||
"price": FRAmount,
|
||||
}
|
||||
|
||||
// 默认查询条件映射 go类型识别
|
||||
var defaultWhereModeMap = map[string]string{
|
||||
GoTypeString: WMLike,
|
||||
GoTypeDate: WMEq,
|
||||
GoTypeDatetime: WMEq,
|
||||
GoTypeInt: WMEq,
|
||||
GoTypeUint: WMEq,
|
||||
GoTypeInt64: WMEq,
|
||||
GoTypeUint64: WMEq,
|
||||
GoTypeIntSlice: WMIn,
|
||||
GoTypeInt64Slice: WMIn,
|
||||
GoTypeUint64Slice: WMIn,
|
||||
GoTypeFloat32: WMEq,
|
||||
GoTypeFloat64: WMEq,
|
||||
GoTypeBytes: WMEq,
|
||||
GoTypeTime: WMEq,
|
||||
GoTypeGTime: WMEq,
|
||||
GoTypeJson: WMJsonContains,
|
||||
}
|
||||
|
||||
// IsIndexPK 是否是主键
|
||||
func IsIndexPK(index string) bool {
|
||||
return gstr.ToUpper(index) == gstr.ToUpper(genconsts.GenCodesIndexPK)
|
||||
}
|
||||
|
||||
// IsIndexUNI 是否是唯一索引
|
||||
func IsIndexUNI(index string) bool {
|
||||
return gstr.ToUpper(index) == gstr.ToUpper(genconsts.GenCodesIndexUNI)
|
||||
}
|
||||
|
||||
// setDefault 设置默认属性
|
||||
func setDefault(field *genmodel.GenCodesColumnListModel) {
|
||||
setDefaultEdit(field)
|
||||
|
||||
setDefaultFormMode(field)
|
||||
|
||||
setDefaultFormRole(field)
|
||||
|
||||
setDefaultDictType(field)
|
||||
|
||||
setDefaultList(field)
|
||||
|
||||
setDefaultExport(field)
|
||||
|
||||
setDefaultQuery(field)
|
||||
|
||||
setDefaultQueryWhere(field)
|
||||
|
||||
setDefaultValue(field)
|
||||
|
||||
if field.IsAllowNull == "NO" {
|
||||
field.Required = true
|
||||
}
|
||||
|
||||
if IsIndexUNI(field.Index) {
|
||||
field.Unique = true
|
||||
}
|
||||
|
||||
if df, ok := defaultSort[field.Name]; ok {
|
||||
field.IsSort = df
|
||||
}
|
||||
|
||||
if field.Dc == "" {
|
||||
field.Dc = field.Name
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultEdit 设置默认编辑
|
||||
func setDefaultEdit(field *genmodel.GenCodesColumnListModel) {
|
||||
field.IsEdit = true
|
||||
|
||||
if IsIndexPK(field.Index) {
|
||||
field.IsEdit = false
|
||||
return
|
||||
}
|
||||
|
||||
if df, ok := defaultEditFields[field.Name]; ok {
|
||||
field.IsEdit = df
|
||||
}
|
||||
|
||||
if df, ok := defaultEditSwitch[field.Name]; ok {
|
||||
field.IsEdit = df
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultFormMode 设置默认表单组件
|
||||
func setDefaultFormMode(field *genmodel.GenCodesColumnListModel) {
|
||||
field.FormMode = FMInput
|
||||
if df, ok := defaultFormModeMap[field.TsType]; ok {
|
||||
field.FormMode = df
|
||||
}
|
||||
|
||||
if gstr.HasSuffix(field.GoName, "Status") && IsNumberType(field.GoType) {
|
||||
field.FormMode = FMSelect
|
||||
return
|
||||
}
|
||||
|
||||
if field.GoName == "CreatedAt" {
|
||||
field.FormMode = FMDateRange
|
||||
return
|
||||
}
|
||||
|
||||
if (field.GoName == "ProvinceId" || field.GoName == "CityId") && IsNumberType(field.GoType) {
|
||||
field.FormMode = FMCitySelector
|
||||
return
|
||||
}
|
||||
|
||||
if field.DataType == "datetime" || field.DataType == "timestamp" || field.DataType == "timestamptz" {
|
||||
field.FormMode = FMTime
|
||||
return
|
||||
}
|
||||
|
||||
if field.DataType == "date" {
|
||||
field.FormMode = FMDate
|
||||
return
|
||||
}
|
||||
|
||||
if field.GoType == GoTypeString && field.Length >= 256 && field.Length <= 512 {
|
||||
field.FormMode = FMInputTextarea
|
||||
return
|
||||
}
|
||||
|
||||
if field.GoType == GoTypeString && field.Length > 512 {
|
||||
field.FormMode = FMInputEditor
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultFormRole 设置默认表单验证
|
||||
func setDefaultFormRole(field *genmodel.GenCodesColumnListModel) {
|
||||
field.FormRole = FRNone
|
||||
|
||||
switch field.GoType {
|
||||
case GoTypeUint, GoTypeUint64:
|
||||
field.FormRole = FRNum
|
||||
return
|
||||
}
|
||||
|
||||
if df, ok := defaultFormRoleMap[field.Name]; ok {
|
||||
field.FormRole = df
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultDictType 设置默认字典类型
|
||||
func setDefaultDictType(field *genmodel.GenCodesColumnListModel) {
|
||||
if gstr.HasSuffix(field.GoName, "Status") && IsNumberType(field.GoType) {
|
||||
field.DictType = "sys_common_status" // 默认系统状态ID
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultList 设置默认列表
|
||||
func setDefaultList(field *genmodel.GenCodesColumnListModel) {
|
||||
field.IsList = true
|
||||
switch field.GoType {
|
||||
case GoTypeIntSlice, GoTypeInt64Slice, GoTypeUint64Slice, GoTypeBytes, GoTypeJson:
|
||||
field.IsList = false
|
||||
return
|
||||
}
|
||||
|
||||
if field.Length >= 500 {
|
||||
field.IsList = false
|
||||
return
|
||||
}
|
||||
|
||||
if df, ok := defaultListSwitch[field.Name]; ok {
|
||||
field.IsList = df
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultExport 设置默认导出
|
||||
func setDefaultExport(field *genmodel.GenCodesColumnListModel) {
|
||||
field.IsExport = true
|
||||
switch field.GoType {
|
||||
case GoTypeIntSlice, GoTypeInt64Slice, GoTypeUint64Slice, GoTypeBytes, GoTypeJson:
|
||||
field.IsExport = false
|
||||
return
|
||||
}
|
||||
|
||||
if field.Length >= 500 {
|
||||
field.IsExport = false
|
||||
return
|
||||
}
|
||||
|
||||
if df, ok := defaultExportSwitch[field.Name]; ok {
|
||||
field.IsExport = df
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultQuery 设置默认查询
|
||||
func setDefaultQuery(field *genmodel.GenCodesColumnListModel) {
|
||||
field.IsQuery = false
|
||||
|
||||
if IsIndexPK(field.Index) {
|
||||
field.IsQuery = true
|
||||
return
|
||||
}
|
||||
|
||||
if gstr.HasSuffix(field.GoName, "Status") && IsNumberType(field.GoType) {
|
||||
field.IsQuery = true
|
||||
return
|
||||
}
|
||||
|
||||
if field.GoName == "CreatedAt" {
|
||||
field.IsQuery = true
|
||||
return
|
||||
}
|
||||
|
||||
if df, ok := defaultQuerySwitch[field.Name]; ok {
|
||||
field.IsQuery = df
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultQueryWhere 设置默认查询条件
|
||||
func setDefaultQueryWhere(field *genmodel.GenCodesColumnListModel) {
|
||||
field.QueryWhere = WMEq
|
||||
|
||||
if field.GoName == "CreatedAt" {
|
||||
field.QueryWhere = WMBetween
|
||||
return
|
||||
}
|
||||
|
||||
if field.Length >= 500 {
|
||||
field.QueryWhere = WMLikeAll
|
||||
return
|
||||
}
|
||||
|
||||
if df, ok := defaultWhereModeMap[field.GoType]; ok {
|
||||
field.QueryWhere = df
|
||||
}
|
||||
}
|
||||
|
||||
// setDefaultValue 设置默认value
|
||||
func setDefaultValue(field *genmodel.GenCodesColumnListModel) {
|
||||
var value interface{}
|
||||
if field.DefaultValue == nil {
|
||||
switch field.GoType {
|
||||
case GoTypeString, GoTypeBytes, GoTypeDate, GoTypeDatetime, GoTypeTime, GoTypeGTime:
|
||||
value = ""
|
||||
case GoTypeIntSlice, GoTypeInt64Slice, GoTypeUint64Slice, GoTypeJson:
|
||||
value = nil
|
||||
case GoTypeInt, GoTypeUint, GoTypeInt64, GoTypeUint64:
|
||||
value = 0
|
||||
case GoTypeBool:
|
||||
value = false
|
||||
}
|
||||
} else {
|
||||
value = genconsts.ConvType(field.DefaultValue, field.GoType)
|
||||
}
|
||||
|
||||
// 时间类型不做默认值处理
|
||||
if field.GoType == GoTypeGTime {
|
||||
value = ""
|
||||
}
|
||||
|
||||
field.DefaultValue = value
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
// 表单组件
|
||||
const (
|
||||
FMInput = "Input" // 文本输入
|
||||
FMInputNumber = "InputNumber" // 数字输入
|
||||
FMInputTextarea = "InputTextarea" // 文本域
|
||||
FMInputEditor = "InputEditor" // 富文本
|
||||
FMInputDynamic = "InputDynamic" // 动态键值对
|
||||
FMDate = "Date" // 日期选择(Y-M-D)
|
||||
FMDateRange = "DateRange" // 日期范围选择
|
||||
FMTime = "Time" // 时间选择(Y-M-D H:i:s)
|
||||
FMTimeRange = "TimeRange" // 时间范围选择
|
||||
FMRadio = "Radio" // 单选按钮
|
||||
FMCheckbox = "Checkbox" // 复选按钮
|
||||
FMSelect = "Select" // 单选下拉框
|
||||
FMSelectMultiple = "SelectMultiple" // 多选下拉框
|
||||
FMTreeSelect = "TreeSelect" // 树型选择
|
||||
FMCascader = "Cascader" // 级联选择
|
||||
FMUploadImage = "UploadImage" // 单图上传
|
||||
FMUploadImages = "UploadImages" // 多图上传
|
||||
FMUploadFile = "UploadFile" // 单文件上传
|
||||
FMUploadFiles = "UploadFiles" // 多文件上传
|
||||
FMSwitch = "Switch" // 开关
|
||||
FMRate = "Rate" // 评分
|
||||
FMCitySelector = "CitySelector" // 省市区选择
|
||||
FMPidTreeSelect = "PidTreeSelect" // 树型上级选择,树表生成专用
|
||||
)
|
||||
|
||||
var FMs = []string{
|
||||
FMInput, FMInputNumber, FMInputTextarea, FMInputEditor, FMInputDynamic,
|
||||
FMDate, FMDateRange, FMTime, FMTimeRange,
|
||||
FMRadio, FMCheckbox, FMSelect, FMSelectMultiple, FMTreeSelect, FMCascader,
|
||||
FMUploadImage, FMUploadImages, FMUploadFile, FMUploadFiles,
|
||||
FMSwitch,
|
||||
FMRate,
|
||||
FMCitySelector,
|
||||
}
|
||||
|
||||
var FMMap = map[string]string{
|
||||
FMInput: "文本输入",
|
||||
FMInputNumber: "数字输入",
|
||||
FMInputTextarea: "文本域",
|
||||
FMInputEditor: "富文本",
|
||||
FMInputDynamic: "动态键值对",
|
||||
FMDate: "日期选择(Y-M-D)",
|
||||
FMDateRange: "日期范围选择",
|
||||
FMTime: "时间选择(Y-M-D H:i:s)",
|
||||
FMTimeRange: "时间范围选择",
|
||||
FMRadio: "单选按钮",
|
||||
FMCheckbox: "复选按钮",
|
||||
FMSelect: "单选下拉框",
|
||||
FMSelectMultiple: "多选下拉框",
|
||||
FMTreeSelect: "树型选择",
|
||||
FMCascader: "级联选择",
|
||||
FMUploadImage: "单图上传",
|
||||
FMUploadImages: "多图上传",
|
||||
FMUploadFile: "单文件上传",
|
||||
FMUploadFiles: "多文件上传",
|
||||
FMSwitch: "开关",
|
||||
FMRate: "评分",
|
||||
FMCitySelector: "省市区选择",
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
const (
|
||||
FRNone = "none"
|
||||
FRIp = "ip"
|
||||
FRPercentage = "percentage"
|
||||
FRTel = "tel"
|
||||
FRPhone = "phone"
|
||||
FRQq = "qq"
|
||||
FREmail = "email"
|
||||
FRIdCard = "idCard"
|
||||
FRNum = "num"
|
||||
FRBankCard = "bankCard"
|
||||
FRWeibo = "weibo"
|
||||
FRUserName = "userName"
|
||||
FRAccount = "account"
|
||||
FRPassword = "password"
|
||||
FRAmount = "amount"
|
||||
)
|
||||
|
||||
var FRMap = map[string]string{
|
||||
FRNone: "不验证",
|
||||
FRIp: "Ipv4或Ipv6",
|
||||
FRPercentage: "0-100百分比",
|
||||
FRTel: "固话格式",
|
||||
FRPhone: "手机号",
|
||||
FRQq: "QQ号码",
|
||||
FREmail: "邮箱",
|
||||
FRIdCard: "身份证",
|
||||
FRNum: "非零正整数",
|
||||
FRBankCard: "银行卡",
|
||||
FRWeibo: "微博号",
|
||||
FRUserName: "用户名",
|
||||
FRAccount: "账号",
|
||||
FRPassword: "密码",
|
||||
FRAmount: "金额",
|
||||
}
|
||||
|
||||
// 查询条件
|
||||
const (
|
||||
WMEq = "=" // =
|
||||
WMNeq = "!=" // !=
|
||||
WMGt = ">" // >
|
||||
WMGte = ">=" // >=
|
||||
WMLt = "<" // <
|
||||
WMLte = "<=" // <=
|
||||
WMIn = "IN" // IN (...)
|
||||
WMNotIn = "NOT IN" // NOT IN (...)
|
||||
WMBetween = "BETWEEN" // BETWEEN
|
||||
WMNotBetween = "NOT BETWEEN" // NOT BETWEEN
|
||||
WMLike = "LIKE" // LIKE
|
||||
WMLikeAll = "LIKE %...%" // LIKE %...%
|
||||
WMNotLike = "NOT LIKE" // NOT LIKE
|
||||
WMJsonContains = "JSON_CONTAINS(doc, val)" // JSON_CONTAINS(json_doc, val[, path]) // 判断是否包含某个json值
|
||||
)
|
||||
|
||||
var WMs = []string{WMEq,
|
||||
WMNeq, WMGt, WMGte, WMLt, WMLte,
|
||||
WMIn, WMNotIn,
|
||||
WMBetween, WMNotBetween,
|
||||
WMLike, WMLikeAll, WMNotLike,
|
||||
WMJsonContains,
|
||||
}
|
||||
|
||||
// 表格列的排序方式
|
||||
const (
|
||||
TableAlignLeft = "left"
|
||||
TableAlignRight = "right"
|
||||
TableAlignCenter = "center"
|
||||
)
|
||||
|
||||
var TableAligns = []string{TableAlignLeft, TableAlignRight, TableAlignCenter}
|
||||
|
||||
var TableAlignMap = map[string]string{
|
||||
TableAlignLeft: "居左",
|
||||
TableAlignRight: "居右",
|
||||
TableAlignCenter: "居中",
|
||||
}
|
||||
|
||||
// 是否是数字类型
|
||||
func IsNumberType(goType string) bool {
|
||||
switch goType {
|
||||
case GoTypeInt, GoTypeUint, GoTypeInt64, GoTypeUint64:
|
||||
return true
|
||||
case GoTypeFloat32, GoTypeFloat64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSelectFormModel 是否是选择器组件
|
||||
func IsSelectFormModel(FM string) bool {
|
||||
switch FM {
|
||||
case FMRadio, FMCheckbox, FMSelect, FMSelectMultiple, FMCitySelector, FMTreeSelect, FMCascader:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 字段映射关系
|
||||
|
||||
// go类型
|
||||
const (
|
||||
GoTypeString = "string"
|
||||
GoTypeDate = "date"
|
||||
GoTypeDatetime = "datetime"
|
||||
GoTypeInt = "int"
|
||||
GoTypeUint = "uint"
|
||||
GoTypeInt64 = "int64"
|
||||
GoTypeUint64 = "uint64"
|
||||
GoTypeIntSlice = "[]int"
|
||||
GoTypeInt64Slice = "[]int64"
|
||||
GoTypeUint64Slice = "[]uint64"
|
||||
GoTypeFloat32 = "float32"
|
||||
GoTypeFloat64 = "float64"
|
||||
GoTypeBytes = "[]byte"
|
||||
GoTypeBool = "bool"
|
||||
GoTypeTime = "time.Time"
|
||||
GoTypeGTime = "*gtime.Time"
|
||||
GoTypeJson = "*gjson.Json"
|
||||
)
|
||||
|
||||
var GoTypeNameMap = map[string]string{
|
||||
GoTypeString: GoTypeString,
|
||||
GoTypeDate: GoTypeDate,
|
||||
GoTypeDatetime: GoTypeDatetime,
|
||||
GoTypeInt: GoTypeInt,
|
||||
GoTypeUint: GoTypeUint,
|
||||
GoTypeInt64: GoTypeInt64,
|
||||
GoTypeUint64: GoTypeUint64,
|
||||
GoTypeIntSlice: GoTypeIntSlice,
|
||||
GoTypeInt64Slice: GoTypeInt64Slice,
|
||||
GoTypeUint64Slice: GoTypeUint64Slice,
|
||||
GoTypeFloat32: GoTypeFloat32,
|
||||
GoTypeFloat64: GoTypeFloat64,
|
||||
GoTypeBytes: GoTypeBytes,
|
||||
GoTypeBool: GoTypeBool,
|
||||
GoTypeTime: GoTypeTime,
|
||||
GoTypeGTime: GoTypeGTime,
|
||||
GoTypeJson: GoTypeJson,
|
||||
}
|
||||
|
||||
// ts类型
|
||||
const (
|
||||
TsTypeString = "string"
|
||||
TsTypeNumber = "number"
|
||||
TsTypeBoolean = "boolean"
|
||||
TsTypeArray = "array"
|
||||
TsTypeTuple = "tuple"
|
||||
TsTypeAny = "any"
|
||||
)
|
||||
|
||||
var TsTypeNameMap = map[string]string{
|
||||
TsTypeString: TsTypeString,
|
||||
TsTypeNumber: TsTypeNumber,
|
||||
TsTypeBoolean: TsTypeBoolean,
|
||||
TsTypeArray: TsTypeArray,
|
||||
TsTypeTuple: TsTypeTuple,
|
||||
TsTypeAny: TsTypeAny,
|
||||
}
|
||||
|
||||
// ShiftMap Go -> Ts 类型转换
|
||||
var ShiftMap = map[string]string{
|
||||
GoTypeString: TsTypeString,
|
||||
GoTypeDate: TsTypeString,
|
||||
GoTypeDatetime: TsTypeString,
|
||||
GoTypeInt: TsTypeNumber,
|
||||
GoTypeUint: TsTypeNumber,
|
||||
GoTypeInt64: TsTypeNumber,
|
||||
GoTypeUint64: TsTypeNumber,
|
||||
GoTypeIntSlice: TsTypeArray,
|
||||
GoTypeInt64Slice: TsTypeArray,
|
||||
GoTypeUint64Slice: TsTypeArray,
|
||||
GoTypeFloat32: TsTypeNumber,
|
||||
GoTypeFloat64: TsTypeNumber,
|
||||
GoTypeBytes: TsTypeString,
|
||||
GoTypeBool: TsTypeBoolean,
|
||||
GoTypeTime: TsTypeString,
|
||||
GoTypeGTime: TsTypeString,
|
||||
GoTypeJson: TsTypeAny,
|
||||
}
|
||||
|
||||
func HasColumn(fields []*genmodel.GenCodesColumnListModel, column string) bool {
|
||||
for _, field := range fields {
|
||||
if field.GoName == column {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HasColumnWithFormMode(fields []*genmodel.GenCodesColumnListModel, formMode string) bool {
|
||||
for _, field := range fields {
|
||||
if field.FormMode == formMode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HasMaxSort(fields []*genmodel.GenCodesColumnListModel) bool {
|
||||
return HasColumn(fields, "Sort")
|
||||
}
|
||||
|
||||
func HasStatus(headOps []string, fields []*genmodel.GenCodesColumnListModel) bool {
|
||||
if !gstr.InArray(headOps, "status") {
|
||||
return false
|
||||
}
|
||||
return HasColumn(fields, "Status")
|
||||
}
|
||||
|
||||
func HasSwitch(fields []*genmodel.GenCodesColumnListModel) bool {
|
||||
return HasColumnWithFormMode(fields, FMSwitch)
|
||||
}
|
||||
|
||||
func HasHookMemberSummary(fields []*genmodel.GenCodesColumnListModel) bool {
|
||||
for _, field := range fields {
|
||||
if IsMemberSummaryField(field.Name) {
|
||||
if field.IsList {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func HasQueryMemberSummary(fields []*genmodel.GenCodesColumnListModel) bool {
|
||||
for _, field := range fields {
|
||||
if IsMemberSummaryField(field.Name) {
|
||||
if field.IsQuery {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsMemberSummaryField(name string) bool {
|
||||
switch name {
|
||||
case "created_by", "updated_by", "deleted_by":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ReviseFields 校正字段值,兼容版本升级前的老数据格式
|
||||
func ReviseFields(fields []*genmodel.GenCodesColumnListModel) []*genmodel.GenCodesColumnListModel {
|
||||
for _, field := range fields {
|
||||
if !gstr.InArray(TableAligns, field.Align) {
|
||||
field.Align = TableAlignLeft
|
||||
}
|
||||
|
||||
if field.Width < 1 {
|
||||
field.Width = -1
|
||||
}
|
||||
if field.Width > 2000 {
|
||||
field.Width = 2000
|
||||
}
|
||||
|
||||
if field.FormGridSpan < 1 {
|
||||
field.FormGridSpan = 1
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
@@ -0,0 +1,993 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/tree"
|
||||
version "git.5iot.com/iotlcm-public/iotlcm-app/utility/version"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"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/gfile"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/os/gview"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
var Curd = gCurd{}
|
||||
|
||||
type gCurd struct{}
|
||||
|
||||
func (l *gCurd) DoBuild(ctx context.Context, in *genmodel.CurdBuildParam) (err error) {
|
||||
st := gtime.Now()
|
||||
preview, err := l.DoPreview(ctx, in.PreviewIn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
db, err := g.DB().Open(ParseDBConfigNodeLink(&gdb.ConfigNode{Link: in.PreviewIn.DaoConfig.Link}))
|
||||
if err != nil {
|
||||
err = gerror.Newf("连接数据库失败,请检查配置文件[server/hack/config.yaml]数据库配置是否正确!err:%v", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
defer db.Close()
|
||||
if err = db.Ping(); err != nil {
|
||||
err = gerror.Newf("数据库访问异常,请检查配置文件[server/hack/config.yaml]数据库配置是否正确!err:%v", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 前置操作
|
||||
if len(in.BeforeEvent) > 0 {
|
||||
for name, f := range in.BeforeEvent {
|
||||
if gstr.InArray(in.PreviewIn.Options.AutoOps, name) {
|
||||
if err = f(ctx); err != nil {
|
||||
return gerror.Newf("in doBuild operation beforeEvent to '%s' failed:%v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理sql文件
|
||||
handleSqlFile := func(vi *genmodel.GenFile) (err error) {
|
||||
// 无需生成
|
||||
if vi.Meth != genconsts.GenCodesBuildMethCreate && vi.Meth != genconsts.GenCodesBuildMethCover {
|
||||
return
|
||||
}
|
||||
|
||||
if err = gfile.PutContents(vi.Path, strings.TrimSpace(vi.Content)); err != nil {
|
||||
return gerror.Newf("writing content to '%s' failed: %v", vi.Path, err)
|
||||
}
|
||||
|
||||
// 导入失败,将sql文件删除
|
||||
if err = ImportSql(ctx, vi.Path); err != nil {
|
||||
_ = gfile.Remove(vi.Path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 将sql文件提取出来优先处理
|
||||
// sql执行过程出错是高概率事件,后期在执行前要进行预效验,尽量减少在执行过程中出错的可能性
|
||||
sqlGenFile, ok := preview.Views["source.sql"]
|
||||
if ok {
|
||||
delete(preview.Views, "source.sql")
|
||||
if err = handleSqlFile(sqlGenFile); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, vi := range preview.Views {
|
||||
// 无需生成
|
||||
if vi.Meth != genconsts.GenCodesBuildMethCreate && vi.Meth != genconsts.GenCodesBuildMethCover {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = gfile.PutContents(vi.Path, strings.TrimSpace(vi.Content)); err != nil {
|
||||
return gerror.Newf("writing content to '%s' failed: %v", vi.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 后置操作
|
||||
if len(in.AfterEvent) > 0 {
|
||||
for name, f := range in.AfterEvent {
|
||||
if gstr.InArray(in.PreviewIn.Options.AutoOps, name) {
|
||||
if err = f(ctx); err != nil {
|
||||
return gerror.Newf("in doBuild operation afterEvent to '%s' failed:%v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
g.Log().Debugf(ctx, "generate code operation completed, %vms", gtime.Now().Sub(st).Milliseconds())
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) DoPreview(ctx context.Context, in *genmodel.CurdPreviewParam) (res *genmodel.GenCodesPreviewModel, err error) {
|
||||
// 初始化
|
||||
if err = l.initInput(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 加载模板
|
||||
if err = l.loadView(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateApiContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateInputContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateControllerContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateLogicContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateRouterContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateWebApiContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateWebModelContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateWebIndexContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateWebEditContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateWebViewContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = l.generateSqlContent(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
in.Content.Config = in.Config
|
||||
res = in.Content
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) initInput(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
in.Content = new(genmodel.GenCodesPreviewModel)
|
||||
in.Content.Views = make(map[string]*genmodel.GenFile)
|
||||
|
||||
// 初始化生成选项
|
||||
if err = initOptions(in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化表字段配置
|
||||
if err = initTableField(ctx, in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化树表
|
||||
if err = initTableTree(in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化生成选型
|
||||
initStep(in)
|
||||
|
||||
// 初始化方法字典
|
||||
if err = initFuncDict(in); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化生成模板
|
||||
if err = initTemplate(in); err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
func initOptions(in *genmodel.CurdPreviewParam) (err error) {
|
||||
if err = in.In.Options.Scan(&in.Options); err != nil {
|
||||
return
|
||||
}
|
||||
in.Options.DictMap = make(g.Map)
|
||||
return
|
||||
}
|
||||
|
||||
func initTableField(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
// 加载主表配置
|
||||
if err = in.In.MasterColumns.Scan(&in.MasterFields); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(in.MasterFields) == 0 {
|
||||
if in.MasterFields, err = TableColumnList(ctx, &genmodel.GenCodesColumnListParam{DbGroup: in.In.DbName, TableName: in.In.TableName}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 主键属性
|
||||
in.Pk = getPkField(in)
|
||||
if in.Pk == nil {
|
||||
return gerror.New("initInput no primary key is set in the table!")
|
||||
}
|
||||
|
||||
in.MasterFields = ReviseFields(in.MasterFields)
|
||||
|
||||
// 检查表命名
|
||||
var names = []string{in.In.DaoName}
|
||||
for _, v := range in.Options.Join {
|
||||
v.Columns = ReviseFields(v.Columns)
|
||||
names = append(names, v.DaoName)
|
||||
g.Log().Infof(ctx, "initTableField in.Options.Join: %+v", v)
|
||||
}
|
||||
if err = CheckIllegalName("数据库表名", names...); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = CheckIllegalName("实体命名", in.In.VarName); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// getPkField 获取主键
|
||||
func getPkField(in *genmodel.CurdPreviewParam) *genmodel.GenCodesColumnListModel {
|
||||
if len(in.MasterFields) == 0 {
|
||||
panic("getPkField masterFields uninitialized.")
|
||||
}
|
||||
for _, field := range in.MasterFields {
|
||||
if IsIndexPK(field.Index) {
|
||||
return field
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initTableTree(in *genmodel.CurdPreviewParam) (err error) {
|
||||
// 检查树表字段
|
||||
if in.In.GenType == genconsts.GenCodesTypeTree {
|
||||
if err = CheckTreeTableFields(in.MasterFields); err != nil {
|
||||
return err
|
||||
}
|
||||
if in.Options.Tree == nil {
|
||||
in.Options.Tree = &genmodel.OptionsTree{}
|
||||
}
|
||||
|
||||
if in.Options.Tree.PidColumn == "" {
|
||||
in.Options.Tree.PidColumn = "parent_id"
|
||||
}
|
||||
if in.Options.Tree.TitleColumn == "" {
|
||||
if field := inferTreeTitleField(in.MasterFields, in.Options.Tree.PidColumn); field != nil {
|
||||
in.Options.Tree.TitleColumn = field.Name
|
||||
}
|
||||
}
|
||||
|
||||
hasTitle := false
|
||||
hasPid := false
|
||||
hasLevel := false
|
||||
hasTree := false
|
||||
for _, field := range in.MasterFields {
|
||||
if in.Options.Tree.TitleColumn == field.Name {
|
||||
in.Options.Tree.TitleField = field
|
||||
hasTitle = true
|
||||
}
|
||||
if in.Options.Tree.PidColumn == field.Name {
|
||||
in.Options.Tree.PidField = field
|
||||
hasPid = true
|
||||
}
|
||||
if in.Options.Tree.LevelColumn != "" && in.Options.Tree.LevelColumn == field.Name {
|
||||
in.Options.Tree.LevelField = field
|
||||
hasLevel = true
|
||||
}
|
||||
if in.Options.Tree.TreeColumn != "" && in.Options.Tree.TreeColumn == field.Name {
|
||||
in.Options.Tree.TreeField = field
|
||||
hasTree = true
|
||||
}
|
||||
}
|
||||
if !hasTitle {
|
||||
err = gerror.New("请选择一个有效的树名称字段")
|
||||
return
|
||||
}
|
||||
if !hasPid {
|
||||
err = gerror.New("请选择一个有效的父级字段")
|
||||
return
|
||||
}
|
||||
in.Options.Tree.HasTreePath = hasLevel && hasTree
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func inferTreeTitleField(fields []*genmodel.GenCodesColumnListModel, pidColumn string) *genmodel.GenCodesColumnListModel {
|
||||
preferredNames := []string{"tree_name", "name", "title", "label", "menu_name", "dept_name", "category_name"}
|
||||
for _, name := range preferredNames {
|
||||
for _, field := range fields {
|
||||
if field.Name == name {
|
||||
return field
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, field := range fields {
|
||||
if IsIndexPK(field.Index) || field.Name == pidColumn || field.Name == "tenant_id" {
|
||||
continue
|
||||
}
|
||||
goType := strings.TrimPrefix(field.GoType, "*")
|
||||
if goType == "string" {
|
||||
return field
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckTreeTableFields 检查树表字段
|
||||
func CheckTreeTableFields(columns []*genmodel.GenCodesColumnListModel) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func initStep(in *genmodel.CurdPreviewParam) {
|
||||
in.Options.Step = new(genmodel.CurdStep)
|
||||
in.Options.Step.HasMaxSort = HasMaxSort(in.MasterFields)
|
||||
in.Options.Step.HasAdd = gstr.InArray(in.Options.HeadOps, "add")
|
||||
in.Options.Step.HasBatchDel = gstr.InArray(in.Options.HeadOps, "batchDel") && gstr.InArray(in.Options.ColumnOps, "check")
|
||||
in.Options.Step.HasExport = gstr.InArray(in.Options.HeadOps, "export")
|
||||
in.Options.Step.HasNotFilterAuth = gstr.InArray(in.Options.ColumnOps, "notFilterAuth")
|
||||
in.Options.Step.HasEdit = gstr.InArray(in.Options.ColumnOps, "edit")
|
||||
in.Options.Step.HasDel = gstr.InArray(in.Options.ColumnOps, "del")
|
||||
in.Options.Step.HasView = gstr.InArray(in.Options.ColumnOps, "view")
|
||||
in.Options.Step.HasStatus = HasStatus(in.Options.ColumnOps, in.MasterFields)
|
||||
in.Options.Step.HasSwitch = HasSwitch(in.MasterFields)
|
||||
in.Options.Step.HasCheck = gstr.InArray(in.Options.ColumnOps, "check")
|
||||
in.Options.Step.HasMenu = gstr.InArray(in.Options.AutoOps, "genMenuPermissions")
|
||||
in.Options.Step.HasQueryMemberSummary = HasQueryMemberSummary(in.MasterFields)
|
||||
in.Options.Step.HasHookMemberSummary = HasHookMemberSummary(in.MasterFields)
|
||||
in.Options.Step.IsTreeTable = in.In.GenType == genconsts.GenCodesTypeTree
|
||||
if in.Options.Step.IsTreeTable {
|
||||
in.Options.Step.IsOptionTreeTable = in.Options.Tree.StyleType == genconsts.GenCodesTreeStyleTypeOption
|
||||
}
|
||||
in.Options.Step.HasFuncDict = gstr.InArray(in.Options.AutoOps, "genFuncDict")
|
||||
in.Options.Step.IsAddon = in.Config.Application.Crud.Templates[in.In.GenTemplate].IsAddon
|
||||
if in.Options.PresetStep.FormGridCols < 1 {
|
||||
in.Options.PresetStep.FormGridCols = 1
|
||||
}
|
||||
in.Options.Step.IsEditModal = gstr.InArray(in.Options.AutoOps, "genEditModal")
|
||||
}
|
||||
|
||||
func initFuncDict(in *genmodel.CurdPreviewParam) (err error) {
|
||||
g.Log().Infof(context.Background(), "server/internal/library/xgen/gen_curd.go initFuncDict in.Options.Step.HasFuncDict:%v in.Options.FuncDict:%+v", in.Options.Step.HasFuncDict, in.Options.FuncDict)
|
||||
if !in.Options.Step.HasFuncDict || in.Options.FuncDict == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(in.Options.FuncDict.LabelColumn) == 0 || len(in.Options.FuncDict.ValueColumn) == 0 {
|
||||
err = gerror.New("生成字典选项必须设置选项值和选项名称")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, field := range in.MasterFields {
|
||||
if field.Name == in.Options.FuncDict.ValueColumn {
|
||||
in.Options.FuncDict.Value = field
|
||||
}
|
||||
|
||||
if field.Name == in.Options.FuncDict.LabelColumn {
|
||||
in.Options.FuncDict.Label = field
|
||||
}
|
||||
}
|
||||
g.Log().Infof(context.Background(), "server/internal/library/xgen/gen_curd.go initFuncDict in.Options.Step.HasFuncDict:%v in.Options.FuncDict:%+v", in.Options.Step.HasFuncDict, in.Options.FuncDict)
|
||||
return
|
||||
}
|
||||
|
||||
func initTemplate(in *genmodel.CurdPreviewParam) (err error) {
|
||||
if len(in.Config.Application.Crud.Templates)-1 < in.In.GenTemplate {
|
||||
return gerror.New("没有找到生成模板的配置,请检查!")
|
||||
}
|
||||
|
||||
// api前缀
|
||||
apiPrefix := gstr.LcFirst(in.In.VarName)
|
||||
if in.Config.Application.Crud.Templates[in.In.GenTemplate].IsAddon {
|
||||
apiPrefix = in.In.AddonName + "/" + apiPrefix
|
||||
}
|
||||
in.Options.ApiPrefix = apiPrefix
|
||||
|
||||
if err = checkCurdPath(in.Config.Application.Crud.Templates[in.In.GenTemplate], in.In.AddonName); err != nil {
|
||||
return
|
||||
}
|
||||
in.Options.TemplateGroup = in.Config.Application.Crud.Templates[in.In.GenTemplate].MasterPackage
|
||||
return
|
||||
}
|
||||
|
||||
func checkCurdPath(temp *genmodel.GenCodesConfigCrudTemplate, addonName string) (err error) {
|
||||
if temp == nil {
|
||||
return gerror.New("生成模板配置不能为空")
|
||||
}
|
||||
|
||||
if temp.IsAddon {
|
||||
temp.TemplatePath = gstr.Replace(temp.TemplatePath, "{$name}", addonName)
|
||||
temp.ApiPath = gstr.Replace(temp.ApiPath, "{$name}", addonName)
|
||||
temp.InputPath = gstr.Replace(temp.InputPath, "{$name}", addonName)
|
||||
temp.ControllerPath = gstr.Replace(temp.ControllerPath, "{$name}", addonName)
|
||||
temp.LogicPath = gstr.Replace(temp.LogicPath, "{$name}", addonName)
|
||||
temp.RouterPath = gstr.Replace(temp.RouterPath, "{$name}", addonName)
|
||||
temp.SqlPath = gstr.Replace(temp.SqlPath, "{$name}", addonName)
|
||||
temp.WebApiPath = gstr.Replace(temp.WebApiPath, "{$name}", addonName)
|
||||
temp.WebViewsPath = gstr.Replace(temp.WebViewsPath, "{$name}", addonName)
|
||||
}
|
||||
|
||||
tip := `生成模板配置参数'%s'路径不存在,请先创建路径:%s`
|
||||
|
||||
if !gfile.Exists(temp.TemplatePath) {
|
||||
return gerror.Newf(tip, "TemplatePath", temp.TemplatePath)
|
||||
}
|
||||
if !gfile.Exists(temp.ApiPath) {
|
||||
return gerror.Newf(tip, "ApiPath", temp.ApiPath)
|
||||
}
|
||||
if !gfile.Exists(temp.InputPath) {
|
||||
return gerror.Newf(tip, "InputPath", temp.InputPath)
|
||||
}
|
||||
if !gfile.Exists(temp.ControllerPath) {
|
||||
return gerror.Newf(tip, "ControllerPath", temp.ControllerPath)
|
||||
}
|
||||
if !gfile.Exists(temp.LogicPath) {
|
||||
return gerror.Newf(tip, "LogicPath", temp.LogicPath)
|
||||
}
|
||||
if !gfile.Exists(temp.RouterPath) {
|
||||
return gerror.Newf(tip, "RouterPath", temp.RouterPath)
|
||||
}
|
||||
if !gfile.Exists(temp.SqlPath) {
|
||||
return gerror.Newf(tip, "SqlPath", temp.SqlPath)
|
||||
}
|
||||
if !gfile.Exists(temp.WebApiPath) {
|
||||
return gerror.Newf(tip, "WebApiPath", temp.WebApiPath)
|
||||
}
|
||||
if !gfile.Exists(temp.WebViewsPath) {
|
||||
return gerror.Newf(tip, "WebViewsPath", temp.WebViewsPath)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) loadView(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
temp := in.Config.Application.Crud.Templates[in.In.GenTemplate]
|
||||
view := gview.New()
|
||||
err = view.SetConfigWithMap(g.Map{
|
||||
"Paths": temp.TemplatePath,
|
||||
"Delimiters": in.Config.Delimiters,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := gtime.Now()
|
||||
view.BindFuncMap(g.Map{
|
||||
"NowYear": now.Year, // 当前年
|
||||
"ToLower": strings.ToLower, // 全部小写
|
||||
"LcFirst": gstr.LcFirst, // 首字母小写
|
||||
"UcFirst": gstr.UcFirst, // 首字母大写
|
||||
"ToTSArray": ToTSArray, // 转为ts数组格式
|
||||
"DictToTSArray": DictToTSArray, // 转为ts数组格式
|
||||
})
|
||||
|
||||
if err = l.generateWebModelDictOptions(ctx, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
modName, err := GetModName(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
importApi := gstr.Replace(temp.ApiPath, "./", modName+"/") + "/" + strings.ToLower(in.In.VarName) + "/v1"
|
||||
importInput := gstr.Replace(temp.InputPath, "./", modName+"/")
|
||||
importController := gstr.Replace(temp.ControllerPath, "./", modName+"/")
|
||||
importService := "git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
if temp.IsAddon {
|
||||
importService = "git.5iot.com/iotlcm-public/iotlcm-app/addons/" + in.In.AddonName + "/service"
|
||||
}
|
||||
|
||||
in.Options.ImportWebApi = "#/api/gen/" + gstr.LcFirst(in.In.VarName)
|
||||
if temp.IsAddon {
|
||||
in.Options.ImportWebApi = "#/api/gen/addons/" + in.In.AddonName + "/" + gstr.LcFirst(in.In.VarName)
|
||||
}
|
||||
|
||||
componentPrefix := gstr.LcFirst(in.In.VarName)
|
||||
if temp.IsAddon {
|
||||
componentPrefix = "addons/" + in.In.AddonName + "/" + componentPrefix
|
||||
}
|
||||
|
||||
nowTime := now.Format("Y-m-d H:i:s")
|
||||
view.Assigns(gview.Params{
|
||||
"templateGroup": in.Options.TemplateGroup, // 生成模板分组名称
|
||||
"servFunName": l.parseServFunName(in.Options.TemplateGroup, in.In.VarName), // 业务服务名称
|
||||
"nowTime": nowTime, // 当前时间
|
||||
"version": runtime.Version(), // GO 版本
|
||||
"xjVersion": version.BuildVersion, // XJ 版本
|
||||
"varName": in.In.VarName, // 实体名称
|
||||
"tableComment": in.In.TableComment, // 对外名称
|
||||
"daoName": in.In.DaoName, // ORM模型
|
||||
"masterFields": in.MasterFields, // 主表字段
|
||||
"pk": in.Pk, // 主键属性
|
||||
"options": in.Options, // 提交选项
|
||||
"dictOptions": in.Options.DictOps, // web字典选项
|
||||
"importApi": importApi, // 导入goApi包
|
||||
"importInput": importInput, // 导入input包
|
||||
"importController": importController, // 导入控制器包
|
||||
"importService": importService, // 导入业务服务
|
||||
"importWebApi": in.Options.ImportWebApi, // 导入webApi
|
||||
"apiPrefix": in.Options.ApiPrefix, // api前缀
|
||||
"componentPrefix": componentPrefix, // vue子组件前缀
|
||||
})
|
||||
|
||||
in.View = view
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateApiContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "api.go"
|
||||
tplData = g.Map{}
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content, err = FormatGo(ctx, name, genFile.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].ApiPath, strings.ToLower(in.In.VarName), "v1", strings.ToLower(in.In.VarName)+".go")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethSkip
|
||||
}
|
||||
|
||||
genFile.Required = true
|
||||
|
||||
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCover
|
||||
}
|
||||
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateInputContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "input.go"
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
|
||||
tplData, err := l.inputTplData(ctx, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content, err = FormatGo(ctx, name, genFile.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].InputPath, CamelCaseToUnderline(in.In.VarName)+".go")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethSkip
|
||||
}
|
||||
genFile.Required = true
|
||||
|
||||
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCover
|
||||
}
|
||||
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateControllerContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "controller.go"
|
||||
tplData = g.Map{}
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content, err = FormatGo(ctx, name, genFile.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].ControllerPath, CamelCaseToUnderline(in.In.VarName)+".go")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethSkip
|
||||
}
|
||||
genFile.Required = true
|
||||
|
||||
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCover
|
||||
}
|
||||
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateLogicContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "logic.go"
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
|
||||
tplData, err := l.logicTplData(ctx, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content, err = FormatGo(ctx, name, genFile.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].LogicPath, CamelCaseToUnderline(in.In.VarName)+".go")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethSkip
|
||||
}
|
||||
genFile.Required = true
|
||||
|
||||
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCover
|
||||
}
|
||||
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateRouterContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "router.go"
|
||||
tplData = g.Map{}
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content, err = FormatGo(ctx, name, genFile.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].RouterPath, CamelCaseToUnderline(in.In.VarName)+".go")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethSkip
|
||||
}
|
||||
genFile.Required = true
|
||||
|
||||
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCover
|
||||
}
|
||||
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateWebApiContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "web.api.ts"
|
||||
tplData = g.Map{}
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content = FormatTs(genFile.Content)
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebApiPath, gstr.LcFirst(in.In.VarName), "index.ts")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethSkip
|
||||
}
|
||||
genFile.Required = true
|
||||
|
||||
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCover
|
||||
}
|
||||
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateWebModelContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "web.model.ts"
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
|
||||
tplData, err := l.webModelTplData(ctx, in)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
genFile.Content = FormatTs(genFile.Content)
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "model.tsx")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCover
|
||||
}
|
||||
genFile.Required = true
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateWebIndexContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "web.index.vue"
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
|
||||
tplData, err := l.webIndexTplData(ctx, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content = FormatVue(genFile.Content)
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "index.vue")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethSkip
|
||||
}
|
||||
genFile.Required = true
|
||||
|
||||
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCover
|
||||
}
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateWebEditContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "web.edit.vue"
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
|
||||
tplData, err := l.webEditTplData(ctx, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content = FormatVue(genFile.Content)
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "edit.vue")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethSkip
|
||||
}
|
||||
genFile.Required = true
|
||||
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCover
|
||||
}
|
||||
|
||||
if !in.Options.Step.HasEdit {
|
||||
genFile.Meth = genconsts.GenCodesBuildIgnore
|
||||
genFile.Required = false
|
||||
}
|
||||
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateWebViewContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "web.view.vue"
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
|
||||
tplData, err := l.webViewTplData(ctx, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
genFile.Content = FormatVue(genFile.Content)
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].WebViewsPath, gstr.LcFirst(in.In.VarName), "view.vue")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethSkip
|
||||
}
|
||||
genFile.Required = true
|
||||
|
||||
if genFile.Meth == genconsts.GenCodesBuildMethSkip && gstr.InArray(in.Options.AutoOps, "forcedCover") {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCover
|
||||
}
|
||||
|
||||
if !in.Options.Step.HasView {
|
||||
genFile.Meth = genconsts.GenCodesBuildIgnore
|
||||
genFile.Required = false
|
||||
}
|
||||
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateSqlContent(ctx context.Context, in *genmodel.CurdPreviewParam) (err error) {
|
||||
var (
|
||||
name = "source.sql"
|
||||
config = g.DB("default").GetConfig()
|
||||
tplData = g.Map{
|
||||
"dbName": config.Name,
|
||||
"menuTable": config.Prefix + "sys_menu",
|
||||
"roleMenuTable": config.Prefix + "sys_role_menu",
|
||||
"mainComponent": "BasicLayout",
|
||||
}
|
||||
genFile = new(genmodel.GenFile)
|
||||
)
|
||||
|
||||
menus, err := service.SysMenu().GetFastList(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tplData["dirPid"], tplData["dirLevel"], tplData["dirTree"], err = xgorm.AutoUpdateTree(ctx, &dao.SysMenu, 0, int64(in.Options.Menu.Pid), &xgorm.TreeFiledOption{
|
||||
IdField: "menu_id",
|
||||
PidField: "parent_id",
|
||||
LevelField: "level",
|
||||
TreeField: "tree",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tplData["listLevel"] = tplData["dirLevel"].(int) + 1
|
||||
tplData["btnLevel"] = tplData["dirLevel"].(int) + 2
|
||||
tplData["sortLevel"] = tplData["dirLevel"].(int) + 3
|
||||
|
||||
pageRedirect := ""
|
||||
if in.Options.Menu.Pid > 0 {
|
||||
tplData["mainComponent"] = "ParentLayout"
|
||||
menu, ok := menus[int64(in.Options.Menu.Pid)]
|
||||
if !ok {
|
||||
err = gerror.New("选择的上级菜单不存在")
|
||||
return
|
||||
}
|
||||
for _, id := range tree.GetIds(menu.Tree) {
|
||||
if v, ok2 := menus[id]; ok2 {
|
||||
if !gstr.HasSuffix(pageRedirect, "/") && !gstr.HasPrefix(v.Path, "/") {
|
||||
pageRedirect += "/"
|
||||
}
|
||||
pageRedirect += v.Path
|
||||
}
|
||||
}
|
||||
|
||||
if !gstr.HasSuffix(pageRedirect, "/") && !gstr.HasPrefix(menu.Path, "/") {
|
||||
pageRedirect += "/"
|
||||
}
|
||||
pageRedirect += menu.Path
|
||||
}
|
||||
pageRedirect += "/" + gstr.LcFirst(in.In.VarName) + "/index"
|
||||
tplData["pageRedirect"] = pageRedirect
|
||||
|
||||
genFile.Path = utility.MergeAbs(in.Config.Application.Crud.Templates[in.In.GenTemplate].SqlPath, CamelCaseToUnderline(in.In.VarName)+"_menu.sql")
|
||||
genFile.Meth = genconsts.GenCodesBuildMethCreate
|
||||
if gfile.Exists(genFile.Path) {
|
||||
genFile.Meth = genconsts.GenCodesBuildMethSkip
|
||||
}
|
||||
genFile.Required = true
|
||||
|
||||
if !in.Options.Step.HasMenu {
|
||||
genFile.Meth = genconsts.GenCodesBuildIgnore
|
||||
genFile.Required = false
|
||||
}
|
||||
|
||||
// 需要生成时,检查菜单命名是否存在
|
||||
if genFile.Meth == genconsts.GenCodesBuildMethCreate {
|
||||
menuNamePrefix := gstr.LcFirst(in.In.VarName)
|
||||
menuNames := []string{menuNamePrefix, menuNamePrefix + "Index"}
|
||||
if in.Options.Step.HasEdit {
|
||||
menuNames = append(menuNames, menuNamePrefix+"Edit")
|
||||
menuNames = append(menuNames, menuNamePrefix+"View")
|
||||
}
|
||||
if in.Options.Step.HasView {
|
||||
menuNames = append(menuNames, menuNamePrefix+"View")
|
||||
}
|
||||
if in.Options.Step.HasMaxSort {
|
||||
menuNames = append(menuNames, menuNamePrefix+"MaxSort")
|
||||
}
|
||||
if in.Options.Step.HasDel {
|
||||
menuNames = append(menuNames, menuNamePrefix+"Delete")
|
||||
}
|
||||
if in.Options.Step.HasStatus {
|
||||
menuNames = append(menuNames, menuNamePrefix+"Status")
|
||||
}
|
||||
if in.Options.Step.HasSwitch {
|
||||
menuNames = append(menuNames, menuNamePrefix+"Switch")
|
||||
}
|
||||
if in.Options.Step.HasExport {
|
||||
menuNames = append(menuNames, menuNamePrefix+"Export")
|
||||
}
|
||||
if in.Options.Step.IsTreeTable {
|
||||
menuNames = append(menuNames, menuNamePrefix+"TreeOption")
|
||||
}
|
||||
|
||||
menuNames = utility.UniqueSlice(menuNames)
|
||||
|
||||
hasMenus, err := service.SysMenu().Model(ctx).Fields("path").WhereIn("path", menuNames).Array()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(hasMenus) > 0 {
|
||||
err = gerror.Newf("要生成的菜单中有已存在的路由别名,请检查并删除:%v", strings.Join(gvar.New(hasMenus).Strings(), `、`))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tplData["generatePath"] = genFile.Path
|
||||
genFile.Content, err = in.View.Parse(ctx, name+".template", tplData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
in.Content.Views[name] = genFile
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
"strings"
|
||||
|
||||
"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/text/gstr"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
const (
|
||||
InputTypeListInp = 1 // 列表输入
|
||||
InputTypeListModel = 2 // 列表输出
|
||||
InputTypeExportModel = 3 // 列表导出
|
||||
InputTypeEditInpValidator = 4 // 添加&编辑验证器
|
||||
InputTypeUpdateFields = 5 // 编辑修改过滤字段
|
||||
InputTypeInsertFields = 6 // 编辑新增过滤字段
|
||||
InputTypeTreeOptionFields = 7 // 关系树查询字段
|
||||
InputTypeEditParamFields = 8 // 编辑参数过滤字段
|
||||
EditInpValidatorGenerally = "if err := g.Validator().Rules(\"%s\").Data(in.%s).Messages(\"%s\").Run(ctx); err != nil {\n\t\treturn err.Current()\n\t}\n"
|
||||
)
|
||||
|
||||
func (l *gCurd) inputTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) {
|
||||
data = make(g.Map)
|
||||
data["listInpColumns"] = l.genInputListColumns(ctx, in, InputTypeListInp)
|
||||
data["listModelColumns"] = l.genInputListColumns(ctx, in, InputTypeListModel)
|
||||
data["exportModelColumns"] = l.genInputListColumns(ctx, in, InputTypeExportModel)
|
||||
data["editInpValidator"] = l.genInputListColumns(ctx, in, InputTypeEditInpValidator)
|
||||
data["updateFieldsColumns"] = l.genInputListColumns(ctx, in, InputTypeUpdateFields)
|
||||
data["insertFieldsColumns"] = l.genInputListColumns(ctx, in, InputTypeInsertFields)
|
||||
data["editParamFieldsColumns"] = l.genInputListColumns(ctx, in, InputTypeEditParamFields)
|
||||
data["viewModelColumns"] = l.genInputViewColumns(ctx, in)
|
||||
if in.Options.Step.IsTreeTable {
|
||||
data["treeOptionFields"] = l.genInputListColumns(ctx, in, InputTypeTreeOptionFields)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) genInputViewColumns(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
|
||||
index := 0
|
||||
array := make([][]string, 1000)
|
||||
// 主表
|
||||
for _, field := range in.MasterFields {
|
||||
// 查询用户摘要
|
||||
if field.IsList && in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) {
|
||||
tagKey := "`"
|
||||
descriptionTag := gstr.Replace(formatComment(field.Dc)+"摘要信息", `"`, `\"`)
|
||||
result := []string{" #" + field.GoName + "Summa"}
|
||||
result = append(result, " #*hook.MemberSumma")
|
||||
result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName+"Summa"))
|
||||
result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag))
|
||||
array[index] = result
|
||||
index++
|
||||
}
|
||||
}
|
||||
table := tablewriter.NewTable(buffer)
|
||||
|
||||
table.Options(tablewriter.WithRendition(tw.Rendition{
|
||||
Borders: tw.Border{
|
||||
Top: tw.Fail,
|
||||
Bottom: tw.Fail,
|
||||
Left: tw.Success,
|
||||
Right: tw.Success,
|
||||
},
|
||||
Settings: tw.Settings{
|
||||
Lines: tw.Lines{
|
||||
ShowTop: tw.Fail,
|
||||
ShowBottom: tw.Fail,
|
||||
ShowHeaderLine: tw.Fail,
|
||||
ShowFooterLine: tw.Fail,
|
||||
},
|
||||
},
|
||||
}), tablewriter.WithRowConfig(tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{
|
||||
AutoWrap: tw.WrapNone,
|
||||
},
|
||||
}), tablewriter.WithSymbols(tw.NewSymbols(tw.StyleDefault)))
|
||||
|
||||
// tw.SetBorder(false)
|
||||
// tw.SetRowLine(false)
|
||||
// tw.SetAutoWrapText(false)
|
||||
// tw.SetColumnSeparator("")
|
||||
table.Bulk(array)
|
||||
table.Render()
|
||||
stContent := buffer.String()
|
||||
// Let's do this hack of table writer for indent!
|
||||
stContent = gstr.Replace(stContent, "│#", " ")
|
||||
stContent = gstr.Replace(stContent, "│", " ")
|
||||
stContent = gstr.Replace(stContent, " #", " ")
|
||||
stContent = gstr.Replace(stContent, "` ", "`")
|
||||
stContent = gstr.Replace(stContent, "``", "")
|
||||
stContent = removeEndWrap(stContent)
|
||||
|
||||
buffer.Reset()
|
||||
buffer.WriteString(stContent)
|
||||
return "\tentity." + in.In.DaoName + "\n" + buffer.String()
|
||||
}
|
||||
|
||||
func (l *gCurd) genInputListColumns(ctx context.Context, in *genmodel.CurdPreviewParam, inputType int) string {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
index := 0
|
||||
array := make([][]string, 1000)
|
||||
// 主表
|
||||
for _, field := range in.MasterFields {
|
||||
row := l.genStructFieldDefinition(in, field, inputType, true)
|
||||
if row == nil {
|
||||
continue
|
||||
}
|
||||
array[index] = row
|
||||
index++
|
||||
|
||||
switch inputType {
|
||||
case InputTypeListModel:
|
||||
// 查询用户摘要
|
||||
if field.IsList && in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) {
|
||||
tagKey := "`"
|
||||
descriptionTag := gstr.Replace(formatComment(field.Dc)+"摘要信息", `"`, `\"`)
|
||||
result := []string{" #" + field.GoName + "Summa"}
|
||||
result = append(result, " #*hook.MemberSumma")
|
||||
result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName+"Summa"))
|
||||
result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag))
|
||||
array[index] = result
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关联表
|
||||
if len(in.Options.Join) > 0 {
|
||||
for _, v := range in.Options.Join {
|
||||
if !isEffectiveJoin(v) {
|
||||
g.Log().Infof(ctx, "genInputListColumns isEffectiveJoin: %+v", v)
|
||||
continue
|
||||
}
|
||||
g.Log().Infof(ctx, "genInputListColumns v: %+v", v)
|
||||
for _, field := range v.Columns {
|
||||
row := l.genStructFieldDefinition(in, field, inputType, false)
|
||||
g.Log().Infof(ctx, "genInputListColumns field: %+v row: %+v", field, row)
|
||||
if row != nil {
|
||||
array[index] = row
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
g.Log().Infof(ctx, "genInputListColumns in.Options.Join: %+v", in.Options.Join)
|
||||
|
||||
table := tablewriter.NewWriter(buffer)
|
||||
table.Options(tablewriter.WithRendition(tw.Rendition{
|
||||
Borders: tw.Border{
|
||||
Top: tw.Fail,
|
||||
Bottom: tw.Fail,
|
||||
Left: tw.Success,
|
||||
Right: tw.Success,
|
||||
},
|
||||
Settings: tw.Settings{
|
||||
Lines: tw.Lines{
|
||||
ShowTop: tw.Fail,
|
||||
ShowBottom: tw.Fail,
|
||||
ShowHeaderLine: tw.Fail,
|
||||
ShowFooterLine: tw.Fail,
|
||||
},
|
||||
},
|
||||
}), tablewriter.WithRowConfig(tw.CellConfig{
|
||||
Formatting: tw.CellFormatting{
|
||||
AutoWrap: tw.WrapNone,
|
||||
},
|
||||
}), tablewriter.WithSymbols(tw.NewSymbols(tw.StyleDefault)))
|
||||
// tw.SetBorder(false)
|
||||
// tw.SetRowLine(false)
|
||||
// tw.SetAutoWrapText(false)
|
||||
// tw.SetColumnSeparator("")
|
||||
table.Bulk(array)
|
||||
table.Render()
|
||||
stContent := buffer.String()
|
||||
// Let's do this hack of table writer for indent!
|
||||
stContent = gstr.Replace(stContent, "│#", " ")
|
||||
stContent = gstr.Replace(stContent, "│", " ")
|
||||
stContent = gstr.Replace(stContent, " #", " ")
|
||||
stContent = gstr.Replace(stContent, "` ", "`")
|
||||
stContent = gstr.Replace(stContent, "``", "")
|
||||
stContent = removeEndWrap(stContent)
|
||||
|
||||
buffer.Reset()
|
||||
buffer.WriteString(stContent)
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// genStructFieldForModel gens and returns the attribute definition for specified field.
|
||||
func (l *gCurd) genStructFieldDefinition(in *genmodel.CurdPreviewParam, field *genmodel.GenCodesColumnListModel, inputType int, isMaster bool) []string {
|
||||
var (
|
||||
tagKey = "`"
|
||||
result = []string{" #" + field.GoName}
|
||||
descriptionTag = gstr.Replace(formatComment(field.Dc), `"`, `\"`)
|
||||
)
|
||||
|
||||
addResult := func() []string {
|
||||
isTreeManagedField := in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn
|
||||
isTreeManagedField = isTreeManagedField || (in.Options.Step.IsTreeTable && in.Options.Tree.HasTreePath && (field.Name == in.Options.Tree.LevelColumn || field.Name == in.Options.Tree.TreeColumn))
|
||||
if inputType == InputTypeEditParamFields && !IsIndexPK(field.Index) && !isTreeManagedField && !strings.HasPrefix(field.GoType, "*") {
|
||||
result = append(result, " #*"+field.GoType)
|
||||
} else {
|
||||
result = append(result, " #"+field.GoType)
|
||||
}
|
||||
result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName))
|
||||
result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag))
|
||||
return result
|
||||
}
|
||||
|
||||
isUpdateOrInsertInputFields := func() bool {
|
||||
if !isMaster {
|
||||
return field.IsEdit
|
||||
}
|
||||
|
||||
if field.IsEdit {
|
||||
return true
|
||||
}
|
||||
|
||||
// 树表内部维护字段
|
||||
if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn {
|
||||
return true
|
||||
}
|
||||
if in.Options.Step.IsTreeTable && in.Options.Tree.HasTreePath && (field.Name == in.Options.Tree.LevelColumn || field.Name == in.Options.Tree.TreeColumn) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 修改人
|
||||
if inputType == InputTypeUpdateFields && field.GoName == "UpdatedBy" {
|
||||
return true
|
||||
}
|
||||
if inputType == InputTypeUpdateFields && field.GoName == "UpdatedAt" {
|
||||
return true
|
||||
}
|
||||
// 创建人
|
||||
if inputType == InputTypeInsertFields && field.GoName == "CreatedBy" {
|
||||
return true
|
||||
}
|
||||
if inputType == InputTypeInsertFields && field.GoName == "CreatedAt" {
|
||||
return true
|
||||
}
|
||||
if inputType == InputTypeInsertFields && field.GoName == "CreatedDept" {
|
||||
return true
|
||||
}
|
||||
if inputType == InputTypeInsertFields && field.GoName == "TenantId" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
isQuery := false
|
||||
|
||||
switch inputType {
|
||||
case InputTypeListInp:
|
||||
if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn {
|
||||
isQuery = true
|
||||
field.QueryWhere = WMEq
|
||||
}
|
||||
if !field.IsQuery && !isQuery {
|
||||
return nil
|
||||
}
|
||||
|
||||
if field.QueryWhere == WMBetween {
|
||||
result = append(result, " #[]"+field.GoType)
|
||||
} else {
|
||||
// 查询用户摘要时,固定接收字符串类型
|
||||
if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) {
|
||||
result = append(result, " #string")
|
||||
} else {
|
||||
result = append(result, " #"+field.GoType)
|
||||
}
|
||||
}
|
||||
result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, field.TsName))
|
||||
result = append(result, " #"+fmt.Sprintf(`dc:"%s"`+tagKey, descriptionTag))
|
||||
|
||||
case InputTypeListModel:
|
||||
// 主表的主键
|
||||
if IsIndexPK(field.Index) && isMaster {
|
||||
addResult()
|
||||
// 树表的pid字段
|
||||
} else if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn {
|
||||
addResult()
|
||||
} else if field.IsList {
|
||||
addResult()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case InputTypeExportModel:
|
||||
if !field.IsExport {
|
||||
return nil
|
||||
}
|
||||
addResult()
|
||||
case InputTypeEditInpValidator:
|
||||
if !field.IsEdit {
|
||||
return nil
|
||||
}
|
||||
if !field.Required && (field.FormRole == "none" || field.FormRole == "") {
|
||||
return nil
|
||||
}
|
||||
rule := "// 验证" + field.Dc + "\n"
|
||||
if field.Required && (field.FormRole == FRNone || field.FormRole == "") {
|
||||
field.FormRole = "required"
|
||||
}
|
||||
if err, s := makeValidatorFunc(field); err != nil {
|
||||
return nil
|
||||
} else {
|
||||
rule += s
|
||||
}
|
||||
result = []string{rule}
|
||||
case InputTypeUpdateFields:
|
||||
if !isUpdateOrInsertInputFields() {
|
||||
return nil
|
||||
}
|
||||
addResult()
|
||||
case InputTypeInsertFields:
|
||||
if !isUpdateOrInsertInputFields() {
|
||||
return nil
|
||||
}
|
||||
addResult()
|
||||
case InputTypeTreeOptionFields:
|
||||
if IsIndexPK(field.Index) {
|
||||
return addResult()
|
||||
}
|
||||
if field.Name == in.Options.Tree.PidColumn {
|
||||
return addResult()
|
||||
}
|
||||
if in.Options.Tree.TitleColumn == field.Name {
|
||||
return addResult()
|
||||
}
|
||||
return nil
|
||||
case InputTypeEditParamFields:
|
||||
return addResult()
|
||||
default:
|
||||
panic("inputType is invalid")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func makeValidatorFunc(field *genmodel.GenCodesColumnListModel) (err error, rule string) {
|
||||
if field.FormRole == "required" {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "required", field.GoName, field.Dc+"不能为空")
|
||||
} else if field.FormRole == FRIp {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "ip", field.GoName, field.Dc+"必须为IPV4或IPV6")
|
||||
} else if field.FormRole == FRPercentage {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "min:0|max:100", field.GoName, field.Dc+"必须0-100之间")
|
||||
} else if field.FormRole == FRTel {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "telephone", field.GoName, field.Dc+"不是座机号码")
|
||||
} else if field.FormRole == FRPhone {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "phone", field.GoName, field.Dc+"不是手机号码")
|
||||
} else if field.FormRole == FRQq {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "qq", field.GoName, field.Dc+"不是QQ号码")
|
||||
} else if field.FormRole == FREmail {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "email", field.GoName, field.Dc+"不是邮箱地址")
|
||||
} else if field.FormRole == FRIdCard {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "resident-id", field.GoName, field.Dc+"不是身份证号码")
|
||||
} else if field.FormRole == FRNum {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "min:1", field.GoName, field.Dc+"必须大于0")
|
||||
} else if field.FormRole == FRBankCard {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "bank-card", field.GoName, field.Dc+"不是银行卡号")
|
||||
} else if field.FormRole == FRWeibo {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^[0-9a-zA-Z\\u4e00-\\u9fa5_-]*$", field.GoName, field.Dc+"不是微博号")
|
||||
} else if field.FormRole == FRUserName {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^[0-9a-zA-Z]{6,16}$", field.GoName, field.Dc+"必须为6-16位由字母和数字组成")
|
||||
} else if field.FormRole == FRAccount {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^[\\w_\\d]{6,16}$", field.GoName, field.Dc+"必须为6-16位由字母、数字或下划线组成")
|
||||
} else if field.FormRole == FRPassword {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,18}$", field.GoName, field.Dc+"必须包含6-18为字母和数字")
|
||||
} else if field.FormRole == FRAmount {
|
||||
rule = fmt.Sprintf(EditInpValidatorGenerally, "regex:(^[0-9]{1,10}$)|(^[0-9]{1,10}[\\\\.]{1}[0-9]{1,2}$)", field.GoName, field.Dc+"最多允许输入10位整数及2位小数")
|
||||
} else {
|
||||
err = gerror.New("not support")
|
||||
}
|
||||
|
||||
// 生成验证字典
|
||||
if field.DictType != "" {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
valueType gdb.Value
|
||||
resultValue gdb.Result
|
||||
)
|
||||
valueType, err = dao.SysDictType.Ctx(ctx).Fields(dao.SysDictType.Columns().DictType).Where(dao.SysDictType.Columns().DictId, field.DictType).Value()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resultValue, err = dao.SysDictData.Ctx(context.Background()).Fields(dao.SysDictData.Columns().DictValue).Where(dao.SysDictData.Columns().DictType, valueType.String()).All()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resultValue.Len() > 0 {
|
||||
names := make([]string, 0)
|
||||
for _, item := range resultValue {
|
||||
names = append(names, item["value"].String())
|
||||
}
|
||||
dictRule := "in:" + strings.Join(names, ",")
|
||||
rule += fmt.Sprintf(EditInpValidatorGenerally, dictRule, field.GoName, field.Dc+"值不正确")
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
const (
|
||||
LogicWhereComments = "\n\t// 查询%s\n"
|
||||
LogicWhereNoSupport = "\t// TODO 暂不支持生成[ %s ]查询方式,请自行补充此处代码!"
|
||||
LogicEditUpdate = "\t\tdata := do.%s{}\n\t\tgconv.Struct(in, &data)\n\t\tdata.%s = nil\n\t\tif _, err = s.Model(ctx%s).TX(tx).\n\t\t\tFields(%sin.%sUpdateFields{}).\n\t\t\tWherePri(in.%s).Data(data).Update(); err != nil {\n\t\t\terr = gerror.Wrap(err, \"修改%s失败,请稍后重试!\")\n\t\t}\n\t\treturn"
|
||||
LogicEditInsert = "\t\tdata := do.%s{}\n\t\tgconv.Struct(in, &data)\n\t\tdata.%s = nil\n\t\tif _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).TX(tx).\n\t\t\tFields(%sin.%sInsertFields{}).\n\t\t\tData(data).Insert(); err != nil {\n\t\t\terr = gerror.Wrap(err, \"新增%s失败,请稍后重试!\")\n\t\t}"
|
||||
LogicEditUnique = "\t// 验证'%s'唯一\n\tif err = xgorm.IsUnique(ctx, &dao.%s, g.Map{dao.%s.Columns().%s: in.%s}, \"%s已存在\", in.%s); err != nil {\n\t\treturn\n\t}\n"
|
||||
LogicSwitchUpdate = "%s"
|
||||
LogicStatusUpdate = "do.%s{\n\t\tStatus: in.Status,\n%s}"
|
||||
LogicDeletedUpdate = "do.%s{\n%s}"
|
||||
)
|
||||
|
||||
func (l *gCurd) logicTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) {
|
||||
g.Log().Infof(ctx, "logicTplData in.Options.Join: %+v", in.Options.Join)
|
||||
data = make(g.Map)
|
||||
data["listWhere"] = l.genLogicListWhere(ctx, in)
|
||||
data["listJoin"] = l.genLogicListJoin(ctx, in)
|
||||
data["listFields"] = l.genLogicListFields(ctx, in)
|
||||
data["listOrder"] = l.genLogicListOrder(ctx, in)
|
||||
data["edit"] = l.genLogicEdit(ctx, in)
|
||||
data["switchFields"] = l.genLogicSwitchFields(ctx, in)
|
||||
data["switchUpdate"] = l.genLogicSwitchUpdate(ctx, in)
|
||||
data["statusUpdate"] = l.genLogicStatusUpdate(ctx, in)
|
||||
data["deletedUpdate"] = l.genLogicDeletedUpdate(ctx, in)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) genLogicDeletedUpdate(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
hasDeletedAt := false
|
||||
var update string
|
||||
for _, field := range in.MasterFields {
|
||||
switch field.GoName {
|
||||
case "DeletedBy":
|
||||
update += "\t\tDeletedBy: contexts.GetUserId(ctx),\n"
|
||||
case "DeletedAt":
|
||||
update += "\t\tDeletedAt: gtime.Now(),\n"
|
||||
hasDeletedAt = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasDeletedAt {
|
||||
return ""
|
||||
}
|
||||
|
||||
update += "\t"
|
||||
return fmt.Sprintf(LogicDeletedUpdate, in.In.DaoName, update)
|
||||
}
|
||||
|
||||
func (l *gCurd) genLogicStatusUpdate(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
var update string
|
||||
for _, field := range in.MasterFields {
|
||||
switch field.GoName {
|
||||
case "UpdatedBy":
|
||||
update += "\t\tUpdatedBy: contexts.GetUserId(ctx),\n"
|
||||
case "UpdatedAt":
|
||||
update += "\t\tUpdatedAt: gtime.Now(),\n"
|
||||
}
|
||||
}
|
||||
|
||||
update += "\t"
|
||||
return fmt.Sprintf(LogicStatusUpdate, in.In.DaoName, update)
|
||||
}
|
||||
|
||||
func (l *gCurd) genLogicSwitchUpdate(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
buffer.WriteString("\tvar data do." + in.In.DaoName + "\n")
|
||||
buffer.WriteString("\tswitch in.Key {\n")
|
||||
for _, field := range in.MasterFields {
|
||||
if field.FormMode != FMSwitch {
|
||||
continue
|
||||
}
|
||||
buffer.WriteString("\tcase \"" + field.Name + "\":\n")
|
||||
buffer.WriteString("\t\tdata." + field.GoName + " = " + switchValueExpression(field.GoType) + "\n")
|
||||
}
|
||||
buffer.WriteString("\tdefault:\n")
|
||||
buffer.WriteString("\t\terr = gerror.New(\"开关键名不在白名单\")\n")
|
||||
buffer.WriteString("\t\treturn\n")
|
||||
buffer.WriteString("\t}\n")
|
||||
for _, field := range in.MasterFields {
|
||||
switch field.GoName {
|
||||
case "UpdatedBy":
|
||||
buffer.WriteString("\tdata.UpdatedBy = contexts.GetUserId(ctx)\n")
|
||||
case "UpdatedAt":
|
||||
buffer.WriteString("\tdata.UpdatedAt = gtime.Now()\n")
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(LogicSwitchUpdate, buffer.String())
|
||||
}
|
||||
|
||||
func switchValueExpression(goType string) string {
|
||||
switch goType {
|
||||
case GoTypeBool:
|
||||
return "gconv.Bool(in.Value)"
|
||||
case GoTypeInt:
|
||||
return "gconv.Int(in.Value)"
|
||||
case GoTypeUint:
|
||||
return "gconv.Uint(in.Value)"
|
||||
case GoTypeUint64:
|
||||
return "gconv.Uint64(in.Value)"
|
||||
case GoTypeInt64:
|
||||
return "gconv.Int64(in.Value)"
|
||||
case GoTypeFloat32:
|
||||
return "gconv.Float32(in.Value)"
|
||||
case GoTypeFloat64:
|
||||
return "gconv.Float64(in.Value)"
|
||||
case GoTypeString:
|
||||
return "gconv.String(in.Value)"
|
||||
default:
|
||||
return "gconv.Int(in.Value)"
|
||||
}
|
||||
}
|
||||
|
||||
func (l *gCurd) genLogicSwitchFields(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if in.Options.Step.HasSwitch {
|
||||
for _, field := range in.MasterFields {
|
||||
if field.FormMode == FMSwitch {
|
||||
buffer.WriteString("\t\tdao." + in.In.DaoName + ".Columns()." + field.GoName + ",\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (l *gCurd) genLogicEdit(ctx context.Context, in *genmodel.CurdPreviewParam) g.Map {
|
||||
var (
|
||||
data = make(g.Map)
|
||||
updateBuffer = bytes.NewBuffer(nil)
|
||||
insertBuffer = bytes.NewBuffer(nil)
|
||||
uniqueBuffer = bytes.NewBuffer(nil)
|
||||
)
|
||||
|
||||
for _, field := range in.MasterFields {
|
||||
if field.GoName == "TenantId" {
|
||||
insertBuffer.WriteString("\t\ttenantId := contexts.GetTenantId(ctx)\n\t\tin.TenantId = &tenantId\n")
|
||||
}
|
||||
if field.GoName == "CreatedDept" {
|
||||
insertBuffer.WriteString("\t\tin.CreatedDept = contexts.GetDeptId(ctx)\n")
|
||||
}
|
||||
if field.GoName == "CreatedAt" {
|
||||
insertBuffer.WriteString("\t\tin.CreatedAt = gtime.Now()\n")
|
||||
}
|
||||
if field.GoName == "CreatedBy" {
|
||||
insertBuffer.WriteString("\tin.CreatedBy = contexts.GetUserId(ctx)\n")
|
||||
}
|
||||
if field.GoName == "UpdatedBy" {
|
||||
updateBuffer.WriteString("\t\tin.UpdatedBy = contexts.GetUserId(ctx)\n")
|
||||
}
|
||||
if field.GoName == "UpdatedAt" {
|
||||
updateBuffer.WriteString("\t\tin.UpdatedAt = gtime.Now()\n")
|
||||
}
|
||||
|
||||
if field.Unique {
|
||||
uniqueBuffer.WriteString(fmt.Sprintf(LogicEditUnique, field.GoName, in.In.DaoName, in.In.DaoName, field.GoName, field.GoName, field.Dc, in.Pk.GoName))
|
||||
}
|
||||
}
|
||||
|
||||
notFilterAuth := ""
|
||||
if gstr.InArray(in.Options.ColumnOps, "notFilterAuth") {
|
||||
notFilterAuth = ", &handler.Option{FilterAuth: false}"
|
||||
}
|
||||
|
||||
updateBuffer.WriteString(fmt.Sprintf(LogicEditUpdate, in.In.DaoName, in.Pk.GoName, notFilterAuth, in.Options.TemplateGroup, in.In.VarName, in.Pk.GoName, in.In.TableComment))
|
||||
insertBuffer.WriteString(fmt.Sprintf(LogicEditInsert, in.In.DaoName, in.Pk.GoName, in.Options.TemplateGroup, in.In.VarName, in.In.TableComment))
|
||||
|
||||
data["update"] = updateBuffer.String()
|
||||
data["insert"] = insertBuffer.String()
|
||||
data["unique"] = uniqueBuffer.String()
|
||||
return data
|
||||
}
|
||||
|
||||
func (l *gCurd) genLogicListOrder(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
statement := ""
|
||||
if hasEffectiveJoins(in.Options.Join) {
|
||||
statement = "dao." + in.In.DaoName + ".Table() + \".\" +"
|
||||
}
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
if in.Options.Step.HasMaxSort {
|
||||
buffer.WriteString("OrderAsc(" + statement + "dao." + in.In.DaoName + ".Columns().Sort).")
|
||||
}
|
||||
buffer.WriteString("OrderDesc(" + statement + "dao." + in.In.DaoName + ".Columns()." + in.Pk.GoName + ")")
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (l *gCurd) genLogicListJoin(ctx context.Context, in *genmodel.CurdPreviewParam) (link string) {
|
||||
connector := `"="`
|
||||
if hasEffectiveJoins(in.Options.Join) {
|
||||
linkBuffer := bytes.NewBuffer(nil)
|
||||
for _, join := range in.Options.Join {
|
||||
if isEffectiveJoin(join) {
|
||||
linkBuffer.WriteString("\tmod = mod." + genconsts.GenCodesJoinLinkMap[join.LinkMode] + "OnFields(dao." + join.DaoName + ".Table(), dao." + in.In.DaoName + ".Columns()." + gstr.CaseCamel(join.MasterField) + "," + connector + ", dao." + join.DaoName + ".Columns()." + gstr.CaseCamel(join.Field) + ")\n")
|
||||
}
|
||||
}
|
||||
link = linkBuffer.String()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) genLogicListFields(ctx context.Context, in *genmodel.CurdPreviewParam) (fields string) {
|
||||
selectBuffer := bytes.NewBuffer(nil)
|
||||
if hasEffectiveJoins(in.Options.Join) {
|
||||
selectBuffer.WriteString("mod = mod.FieldsPrefix(dao." + in.In.DaoName + ".Table(), " + in.Options.TemplateGroup + "in." + in.In.VarName + "ListModel{})\n")
|
||||
for _, join := range in.Options.Join {
|
||||
if isEffectiveJoin(join) {
|
||||
selectBuffer.WriteString("mod = mod.Fields(xgorm.JoinFields(ctx, " + in.Options.TemplateGroup + "in." + in.In.VarName + "ListModel{}, &dao." + join.DaoName + ", \"" + join.Alias + "\"))\n")
|
||||
}
|
||||
}
|
||||
fields = selectBuffer.String()
|
||||
} else {
|
||||
fields = fmt.Sprintf("mod = mod.Fields(%sin.%sListModel{})", in.Options.TemplateGroup, in.In.VarName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) genLogicListWhere(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
|
||||
// 主表
|
||||
l.genLogicListWhereEach(buffer, in, in.MasterFields, in.In.DaoName, "")
|
||||
// 关联表
|
||||
g.Log().Infof(ctx, "genLogicListWhere in.Options.Join: %+v", in.Options.Join)
|
||||
if hasEffectiveJoins(in.Options.Join) {
|
||||
for _, v := range in.Options.Join {
|
||||
if isEffectiveJoin(v) {
|
||||
l.genLogicListWhereEach(buffer, in, v.Columns, v.DaoName, v.Alias)
|
||||
}
|
||||
}
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (l *gCurd) genLogicListWhereEach(buffer *bytes.Buffer, in *genmodel.CurdPreviewParam, fields []*genmodel.GenCodesColumnListModel, daoName string, alias string) {
|
||||
isLink := false
|
||||
if alias != "" {
|
||||
alias = `"` + alias + `."+`
|
||||
isLink = true
|
||||
}
|
||||
|
||||
tablePrefix := ""
|
||||
wherePrefix := "Where"
|
||||
if isLink {
|
||||
wherePrefix = "WherePrefix"
|
||||
tablePrefix = "dao." + daoName + ".Table(), "
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
isQuery := false
|
||||
// 树表查询上级
|
||||
if in.Options.Step.IsTreeTable && field.Name == in.Options.Tree.PidColumn {
|
||||
isQuery = true
|
||||
field.QueryWhere = WMEq
|
||||
}
|
||||
|
||||
if (!field.IsQuery && !isQuery) || field.QueryWhere == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
buffer.WriteString(fmt.Sprintf(LogicWhereComments, field.Dc))
|
||||
|
||||
var (
|
||||
linkMode string
|
||||
whereTag string
|
||||
columnName string
|
||||
)
|
||||
|
||||
// 查询用户摘要
|
||||
if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) {
|
||||
servicePackName := "service"
|
||||
if in.Options.Step.IsAddon {
|
||||
servicePackName = "isc"
|
||||
}
|
||||
buffer.WriteString(fmt.Sprintf("if in.%v != \"\" {\n\t\t\t\tids, err := %v.AdminMember().GetIdsByKeyword(ctx, in.%v)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, 0, err\n\t\t\t\t}\n\t\t\t\tmod = mod.WhereIn(dao.%v.Columns().%v, ids)\n\t\t\t}\n", field.GoName, servicePackName, field.GoName, in.In.DaoName, field.GoName))
|
||||
continue
|
||||
}
|
||||
|
||||
if IsNumberType(field.GoType) {
|
||||
linkMode = `in.` + field.GoName + ` > 0`
|
||||
} else if field.GoType == GoTypeGTime {
|
||||
linkMode = `in.` + field.GoName + ` != nil`
|
||||
} else if field.GoType == GoTypeJson {
|
||||
linkMode = `!in.` + field.GoName + `.IsNil()`
|
||||
} else {
|
||||
linkMode = `in.` + field.GoName + ` != ""`
|
||||
}
|
||||
|
||||
if field.QueryWhere == WMBetween || field.QueryWhere == WMNotBetween {
|
||||
linkMode = `len(in.` + field.GoName + `) == 2`
|
||||
}
|
||||
|
||||
// 如果是关联表重新转换字段
|
||||
columnName = field.GoName
|
||||
if isLink {
|
||||
columnName = gstr.CaseCamel(field.Name)
|
||||
}
|
||||
if field.FormMode == FMDateRange {
|
||||
tmp := "\tif " + linkMode + " {\n\t\tin." + field.GoName + "[1] = in." + field.GoName + "[1].EndOfDay()\n\t}\n"
|
||||
buffer.WriteString(tmp)
|
||||
}
|
||||
|
||||
switch field.QueryWhere {
|
||||
case WMEq:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
|
||||
case WMNeq:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Not(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
|
||||
case WMGt:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "GT(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
|
||||
case WMGte:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "GTE(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
|
||||
case WMLt:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "LT(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
|
||||
case WMLte:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "LTE(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
|
||||
case WMIn:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "In(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
|
||||
case WMNotIn:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "NotIn(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
|
||||
case WMBetween:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Between(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + "[0], in." + field.GoName + "[1])\n\t}"
|
||||
case WMNotBetween:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "NotBetween(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + "[0], in." + field.GoName + "[1])\n\t}"
|
||||
case WMLike:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Like(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
|
||||
case WMLikeAll:
|
||||
val := `"%"+in.` + field.GoName + `+"%"`
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "Like(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", " + val + ")\n\t}"
|
||||
case WMNotLike:
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "NotLike(" + tablePrefix + "dao." + daoName + ".Columns()." + columnName + ", in." + field.GoName + ")\n\t}"
|
||||
case WMJsonContains:
|
||||
val := tablePrefix + `"JSON_CONTAINS("+dao.` + daoName + `.Columns().` + columnName + `+",?)", in.` + field.GoName
|
||||
whereTag = "\tif " + linkMode + " {\n\t\tmod = mod." + wherePrefix + "(" + val + ")\n\t}"
|
||||
|
||||
default:
|
||||
buffer.WriteString(fmt.Sprintf(LogicWhereNoSupport, field.QueryWhere))
|
||||
}
|
||||
|
||||
buffer.WriteString(whereTag + "\n")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
"strconv"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type StateItem struct {
|
||||
Name string
|
||||
DefaultValue interface{}
|
||||
Dc string
|
||||
}
|
||||
|
||||
func (l *gCurd) webModelTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) {
|
||||
data = make(g.Map)
|
||||
data["stateItems"] = l.genWebModelStateItems(ctx, in)
|
||||
data["rules"] = l.genWebModelRules(ctx, in)
|
||||
data["formSchema"] = l.genWebModelFormSchema(ctx, in)
|
||||
if data["columns"], err = l.genWebModelColumns(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data["columnsInterface"], err = l.genWebModelColumnsInterface(ctx, in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data["viewSchema"] = l.genWebModelViewSchema(ctx, in)
|
||||
data["editSchema"] = l.genWebModelEditSchema(ctx, in)
|
||||
// 根据表单生成情况,按需导包
|
||||
data["import"], data["const"] = l.genWebModelImport(ctx, in)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelImport(ctx context.Context, in *genmodel.CurdPreviewParam) (string, string) {
|
||||
importBuffer := bytes.NewBuffer(nil)
|
||||
constBuffer := bytes.NewBuffer(nil)
|
||||
|
||||
importBuffer.WriteString(`import { h, ref } from 'vue';
|
||||
import { Tag } from 'ant-design-vue';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
import type { DescItem } from '#/components/description';
|
||||
`)
|
||||
|
||||
// 导入基础组件
|
||||
if len(in.Options.Step.ImportModel.NaiveUI) > 0 {
|
||||
importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.NaiveUI) + " from 'ant-design-vue';\n")
|
||||
}
|
||||
|
||||
importBuffer.WriteString("import { cloneDeep } from 'lodash-es';\n")
|
||||
|
||||
// 导入表单搜索
|
||||
if in.Options.Step.HasSearchForm {
|
||||
importBuffer.WriteString("import type { VbenFormSchema } from '@vben/common-ui';\n")
|
||||
}
|
||||
|
||||
// 导入工具类
|
||||
if len(in.Options.Step.ImportModel.UtilsIs) > 0 {
|
||||
importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsIs) + " from '#/utils/is';\n")
|
||||
}
|
||||
|
||||
if len(in.Options.Step.ImportModel.UtilsUrl) > 0 {
|
||||
importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsUrl) + " from '#/utils/urlUtils';\n")
|
||||
}
|
||||
|
||||
if len(in.Options.Step.ImportModel.UtilsDate) > 0 {
|
||||
// importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsDate) + " from '@/utils/dateUtil';\n")
|
||||
importBuffer.WriteString("import dayjs from 'dayjs';\n")
|
||||
}
|
||||
|
||||
if in.Options.Step.HasRulesValidator {
|
||||
importBuffer.WriteString("import { z } from '@vben/common-ui';\n")
|
||||
}
|
||||
|
||||
if len(in.Options.Step.ImportModel.UtilsIndex) > 0 {
|
||||
importBuffer.WriteString("import " + ImportWebMethod(in.Options.Step.ImportModel.UtilsIndex) + " from '#/utils';\n")
|
||||
}
|
||||
|
||||
// 导入api
|
||||
var importApiMethod []string
|
||||
if in.Options.Step.HasSwitch {
|
||||
importApiMethod = append(importApiMethod, "Switch")
|
||||
}
|
||||
if in.Options.Step.IsTreeTable {
|
||||
importApiMethod = append(importApiMethod, "TreeOption")
|
||||
}
|
||||
if len(importApiMethod) > 0 {
|
||||
importBuffer.WriteString("import " + ImportWebMethod(importApiMethod) + " from '" + in.Options.ImportWebApi + "';\n")
|
||||
}
|
||||
|
||||
// 导入字典选项
|
||||
if in.Options.DictOps.Has {
|
||||
importBuffer.WriteString(`import { DictEnum } from '@vben/constants';
|
||||
import { getDictOptions } from '#/utils/dict';`)
|
||||
constBuffer.WriteString("\n")
|
||||
}
|
||||
|
||||
if in.Options.Step.HasSwitch {
|
||||
importBuffer.WriteString("import { usePermission } from '@/hooks/web/usePermission';\n")
|
||||
constBuffer.WriteString("const { hasPermission } = usePermission();\n")
|
||||
constBuffer.WriteString("const $message = window['$message'];\n")
|
||||
}
|
||||
|
||||
return importBuffer.String(), constBuffer.String()
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelStateItems(ctx context.Context, in *genmodel.CurdPreviewParam) (items []*StateItem) {
|
||||
for _, field := range in.MasterFields {
|
||||
var value = webModelDefaultValue(field)
|
||||
|
||||
// 选项组件默认值调整
|
||||
if gconv.Int(value) == 0 && IsSelectFormModel(field.FormMode) {
|
||||
value = "null"
|
||||
}
|
||||
|
||||
if field.Name == "status" {
|
||||
value = 0
|
||||
}
|
||||
if field.FormMode == FMSwitch {
|
||||
value = 0
|
||||
}
|
||||
if field.FormMode == FMInputDynamic {
|
||||
value = "[]"
|
||||
}
|
||||
items = append(items, &StateItem{
|
||||
Name: field.TsName,
|
||||
DefaultValue: value,
|
||||
Dc: field.Dc,
|
||||
})
|
||||
|
||||
// 查询用户摘要
|
||||
if field.IsList && in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) {
|
||||
items = append(items, &StateItem{
|
||||
Name: field.TsName + "Summa?: null | MemberSumma",
|
||||
DefaultValue: "null",
|
||||
Dc: field.Dc + "摘要信息",
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func webModelDefaultValue(field *genmodel.GenCodesColumnListModel) interface{} {
|
||||
value := field.DefaultValue
|
||||
if value == nil {
|
||||
return "null"
|
||||
}
|
||||
|
||||
if gconv.String(value) == "NULL" {
|
||||
return "null"
|
||||
}
|
||||
|
||||
switch field.GoType {
|
||||
case GoTypeString, GoTypeBytes, GoTypeDate, GoTypeDatetime, GoTypeTime, GoTypeGTime:
|
||||
return strconv.Quote(gconv.String(value))
|
||||
default:
|
||||
if value == "" {
|
||||
return "null"
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelDictOptions(ctx context.Context, in *genmodel.CurdPreviewParam) error {
|
||||
type DictType struct {
|
||||
DictType string `json:"dictType"`
|
||||
DictName string `json:"dictName"`
|
||||
}
|
||||
|
||||
var (
|
||||
dictTypeIds []string
|
||||
dictTypeList []*DictType
|
||||
builtinDictTypeIds []int64
|
||||
builtinDictTypeList []*DictType
|
||||
)
|
||||
|
||||
for _, field := range in.MasterFields {
|
||||
if field.DictType != "" {
|
||||
dictTypeIds = append(dictTypeIds, field.DictType)
|
||||
}
|
||||
|
||||
// if field.DictType < 0 {
|
||||
// builtinDictTypeIds = append(builtinDictTypeIds, field.DictType)
|
||||
// }
|
||||
}
|
||||
|
||||
dictTypeIds = utility.UniqueSlice(dictTypeIds)
|
||||
builtinDictTypeIds = utility.UniqueSlice(builtinDictTypeIds)
|
||||
|
||||
if len(dictTypeIds) == 0 && len(builtinDictTypeIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(dictTypeIds) > 0 {
|
||||
err := g.Model("sys_dict_type").Ctx(ctx).
|
||||
Fields("dict_type", "dict_name").
|
||||
WhereIn("dict_type", dictTypeIds).
|
||||
Scan(&dictTypeList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if len(builtinDictTypeIds) > 0 {
|
||||
// for _, id := range builtinDictTypeIds {
|
||||
// typ, err := dict.GetTypeById(ctx, id)
|
||||
// if err != nil && !errors.Is(err, dict.NotExistKeyError) {
|
||||
// return err
|
||||
// }
|
||||
// if len(typ) > 0 {
|
||||
// row := new(DictType)
|
||||
// row.Id = id
|
||||
// row.Type = typ
|
||||
// builtinDictTypeList = append(builtinDictTypeList, row)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if len(dictTypeList) == 0 && len(builtinDictTypeList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(builtinDictTypeList) > 0 {
|
||||
dictTypeList = append(dictTypeList, builtinDictTypeList...)
|
||||
}
|
||||
|
||||
in.Options.DictOps.Has = true
|
||||
|
||||
for _, v := range dictTypeList {
|
||||
// 字段映射字典
|
||||
for _, field := range in.MasterFields {
|
||||
if field.DictType != "" && v.DictType == field.DictType {
|
||||
in.Options.DictMap[field.TsName] = v.DictType
|
||||
in.Options.DictOps.Schemas = append(in.Options.DictOps.Schemas, &genmodel.OptionsSchemasField{
|
||||
Field: field.TsName,
|
||||
Type: v.DictType,
|
||||
})
|
||||
}
|
||||
}
|
||||
in.Options.DictOps.Types = append(in.Options.DictOps.Types, v.DictName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelRules(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
// buffer := bytes.NewBuffer(nil)
|
||||
// buffer.WriteString("export const rules = {\n")
|
||||
// for _, field := range in.MasterFields {
|
||||
// if !field.IsEdit || (!field.Required && (field.FormRole == "" || field.FormRole == FRNone)) {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// in.Options.Step.HasRules = true
|
||||
// if field.FormRole == "" || field.FormRole == FRNone || field.FormRole == "required" {
|
||||
// buffer.WriteString(fmt.Sprintf(" %s: {\n required: %v,\n trigger: ['blur', 'input'],\n type: '%s',\n message: '请输入%s',\n },\n", field.TsName, field.Required, field.TsType, field.Dc))
|
||||
// } else {
|
||||
// in.Options.Step.HasRulesValidator = true
|
||||
// buffer.WriteString(fmt.Sprintf(" %s: {\n required: %v,\n trigger: ['blur', 'input'],\n type: '%s',\n validator: validate.%v,\n },\n", field.TsName, field.Required, field.TsType, field.FormRole))
|
||||
// }
|
||||
// }
|
||||
// buffer.WriteString("};\n")
|
||||
// return buffer.String()
|
||||
return ""
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelFormSchema(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
buffer.WriteString("export const querySchema: VbenFormSchema[] = [\n")
|
||||
|
||||
// 主表
|
||||
l.genWebModelFormSchemaEach(buffer, in.MasterFields, in, false)
|
||||
|
||||
// 关联表
|
||||
if len(in.Options.Join) > 0 {
|
||||
for _, v := range in.Options.Join {
|
||||
if !isEffectiveJoin(v) {
|
||||
continue
|
||||
}
|
||||
l.genWebModelFormSchemaEach(buffer, v.Columns, in, false)
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteString("];\n")
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// genWebDictOption 生产字典选项
|
||||
func (l *gCurd) genWebDictOption(typ any) string {
|
||||
return fmt.Sprintf(`getDictOptions('%v')`, typ)
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelFormSchemaEach(buffer *bytes.Buffer, fields []*genmodel.GenCodesColumnListModel, in *genmodel.CurdPreviewParam, isEdit bool) {
|
||||
for _, field := range fields {
|
||||
if isEdit {
|
||||
if !field.IsEdit && field.Name != in.Pk.Name {
|
||||
continue
|
||||
}
|
||||
if field.Name == in.Pk.Name {
|
||||
buffer.WriteString(fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
dependencies: { show: () => false, triggerFields: [''], },
|
||||
componentProps: {
|
||||
placeholder: '',
|
||||
onUpdateValue: (e: any) => {
|
||||
console.log(e);
|
||||
},
|
||||
},
|
||||
formItemClass: 'col-span-%d',
|
||||
},`, field.TsName, "Input", field.Dc, field.FormGridSpan))
|
||||
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if !field.IsQuery {
|
||||
continue
|
||||
}
|
||||
}
|
||||
rules := "null"
|
||||
if in.Options.Step.IsTreeTable && isEdit && field.Name == in.Options.Tree.PidColumn {
|
||||
field.FormMode = FMPidTreeSelect
|
||||
}
|
||||
if field.IsEdit {
|
||||
if field.Required {
|
||||
if field.FormMode == FMInputNumber {
|
||||
rules = fmt.Sprintf("z.number({required_error: '请输入%s', invalid_type_error: '无效数字'})", field.Dc)
|
||||
in.Options.Step.HasRulesValidator = true
|
||||
} else if field.FormMode == FMSelect || field.FormMode == FMSelectMultiple || field.FormMode == FMCheckbox || field.FormMode == FMDate || field.FormMode == FMDateRange || field.FormMode == FMTime || field.FormMode == FMTimeRange {
|
||||
rules = "'selectRequired'"
|
||||
} else {
|
||||
rules = "'required'"
|
||||
}
|
||||
}
|
||||
}
|
||||
if field.FormRole != "" {
|
||||
in.Options.Step.HasRulesValidator = true
|
||||
if field.FormRole == FRNum {
|
||||
rules = fmt.Sprintf("z.number({required_error: '请输入%s', invalid_type_error: '无效数字'} )", field.Dc)
|
||||
}
|
||||
if field.FormRole == FRIp {
|
||||
rules = "z.string().ip({message: '无效IP格式'})"
|
||||
}
|
||||
if field.FormRole == FRPhone {
|
||||
rules = "z.string().regex(/^1[3-9]\\d{9}$/, '无效手机号格式')"
|
||||
}
|
||||
if field.FormRole == FRTel {
|
||||
rules = "z.string().regex(/^(?:\\+?\\d{1,3}[-.\\s]?)?(?:$?\\d{1,4}$?[-.\\s]?)?\\d{1,4}[-.\\s]?\\d{1,4}[-.\\s]?\\d{1,9}$/, '无效手机号格式')"
|
||||
}
|
||||
if field.FormRole == FRPercentage {
|
||||
rules = fmt.Sprintf("z.number({required_error: '请输入%s', invalid_type_error: '无效百分比'}).min(0).max(100)", field.Dc)
|
||||
}
|
||||
if field.FormRole == FRQq {
|
||||
rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效QQ号格式')"
|
||||
}
|
||||
if field.FormRole == FRAmount {
|
||||
rules = "z.number({required_error: '必填', invalid_type_error: '无效金额'} ).min(0)"
|
||||
}
|
||||
if field.FormRole == FRBankCard {
|
||||
rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效银行卡号格式')"
|
||||
}
|
||||
if field.FormRole == FRWeibo {
|
||||
rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效微博号格式')"
|
||||
}
|
||||
if field.FormRole == FRUserName {
|
||||
rules = "z.string().regex(/^[a-zA-Z0-9_]+$/, '无效用户名格式')"
|
||||
}
|
||||
if field.FormRole == FRPassword {
|
||||
rules = "z.string().regex(/^[a-zA-Z0-9_]+$/, '无效密码格式')"
|
||||
}
|
||||
if field.FormRole == FRAccount {
|
||||
rules = "z.string().regex(/^[a-zA-Z0-9_]+$/, '无效账号格式')"
|
||||
}
|
||||
if field.FormRole == FRIdCard {
|
||||
rules = "z.string().regex(/^[1-9]\\d{4,}$/, '无效身份证号格式')"
|
||||
}
|
||||
if field.FormRole == FREmail {
|
||||
rules = "z.string().email('无效邮箱格式')"
|
||||
}
|
||||
}
|
||||
|
||||
in.Options.Step.HasSearchForm = true
|
||||
|
||||
// 查询用户摘要
|
||||
if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) {
|
||||
buffer.WriteString(fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
componentProps: {
|
||||
placeholder: '请输入ID|用户名|姓名|手机号',
|
||||
onUpdateValue: (e: any) => {
|
||||
console.log(e);
|
||||
},
|
||||
},
|
||||
rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},`, field.TsName, "Input", field.Dc, rules, field.FormGridSpan))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
defaultComponent = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
componentProps: {
|
||||
placeholder: '请输入%s',
|
||||
onUpdateValue: (e: any) => {
|
||||
console.log(e);
|
||||
},
|
||||
},
|
||||
rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},`, field.TsName, "Input", field.Dc, field.Dc, rules, field.FormGridSpan)
|
||||
component string
|
||||
)
|
||||
|
||||
// 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况
|
||||
switch field.FormMode {
|
||||
case FMInput, FMInputTextarea, FMInputEditor:
|
||||
component = defaultComponent
|
||||
|
||||
case FMInputNumber:
|
||||
component = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
componentProps: {
|
||||
placeholder: '请输入%s',
|
||||
onUpdateValue: (e: any) => {
|
||||
console.log(e);
|
||||
},
|
||||
}, rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},`, field.TsName, "InputNumber", field.Dc, field.Dc, rules, field.FormGridSpan)
|
||||
case FMDate:
|
||||
component = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
componentProps: {
|
||||
type: '%s',
|
||||
clearable: true,
|
||||
shortcuts: %s,
|
||||
onUpdateValue: (e: any) => {
|
||||
console.log(e);
|
||||
},
|
||||
}, rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},`, field.TsName, "DatePicker", field.Dc, "date", "'FMDate'", rules, field.FormGridSpan)
|
||||
in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defShortcuts")
|
||||
|
||||
case FMDateRange:
|
||||
component = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
componentProps: {
|
||||
type: '%s',
|
||||
clearable: true,
|
||||
valueFormat: %s,
|
||||
onUpdateValue: (e: any) => {
|
||||
console.log(e);
|
||||
},
|
||||
}, rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},`, field.TsName, "RangePicker", field.Dc, "datetimerange", "'FMDateRange'", rules, field.FormGridSpan)
|
||||
in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defRangeShortcuts")
|
||||
|
||||
// in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defRangeShortcuts")
|
||||
|
||||
case FMTime:
|
||||
component = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
componentProps: {
|
||||
type: '%s',
|
||||
clearable: true,
|
||||
showTime: true,
|
||||
shortcuts: %s,
|
||||
onUpdateValue: (e: any) => {
|
||||
console.log(e);
|
||||
},
|
||||
}, rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},`, field.TsName, "DatePicker", field.Dc, "datetime", "'FMTime'", rules, field.FormGridSpan)
|
||||
in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defShortcuts")
|
||||
|
||||
case FMTimeRange:
|
||||
component = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
componentProps: {
|
||||
type: '%s',
|
||||
clearable: true,
|
||||
shortcuts: %s,
|
||||
showTime: true,
|
||||
onUpdateValue: (e: any) => {
|
||||
console.log(e);
|
||||
},
|
||||
}, rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},`, field.TsName, "RangePicker", field.Dc, "datetimerange", "'FMTimeRange'", rules, field.FormGridSpan)
|
||||
|
||||
in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "defRangeShortcuts")
|
||||
|
||||
case FMSwitch:
|
||||
fallthrough
|
||||
case FMRadio:
|
||||
component = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
componentProps: {
|
||||
options: %v
|
||||
},
|
||||
rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},
|
||||
`, field.TsName, "RadioGroup", field.Dc, l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan)
|
||||
|
||||
case FMCheckbox:
|
||||
component = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
componentProps: {
|
||||
placeholder: '请选择%s',
|
||||
options: %v
|
||||
},
|
||||
rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},
|
||||
`, field.TsName, "CheckboxGroup", field.Dc, field.Dc,
|
||||
l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan)
|
||||
|
||||
case FMSelect:
|
||||
component = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
defaultValue: null,
|
||||
componentProps: {
|
||||
placeholder: '请选择%s',
|
||||
options: %v,
|
||||
onUpdateValue: (e: any) => {
|
||||
console.log(e);
|
||||
},
|
||||
},
|
||||
rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},
|
||||
`, field.TsName, "Select", field.Dc, field.Dc, l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan)
|
||||
|
||||
case FMSelectMultiple:
|
||||
component = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
defaultValue: null,
|
||||
componentProps: {
|
||||
mode: 'multiple',
|
||||
placeholder: '请选择%s',
|
||||
options: %v,
|
||||
onUpdateValue: (e: any) => {
|
||||
console.log(e);
|
||||
},
|
||||
},
|
||||
rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},
|
||||
`, field.TsName, "Select", field.Dc, field.Dc, l.genWebDictOption(in.Options.DictMap[field.TsName]), rules, field.FormGridSpan)
|
||||
|
||||
case FMPidTreeSelect:
|
||||
component = fmt.Sprintf(` {
|
||||
fieldName: '%s',
|
||||
component: '%s',
|
||||
label: '%s',
|
||||
defaultValue: 0,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
fieldNames: { label: '%s', value: '%s' },
|
||||
showSearch: true,
|
||||
treeData: [],
|
||||
treeDefaultExpandAll: true,
|
||||
treeLine: { showLeafIcon: false },
|
||||
treeNodeFilterProp: '%s',
|
||||
},
|
||||
rules:%v,
|
||||
formItemClass: 'col-span-%d',
|
||||
},
|
||||
`, field.TsName, "TreeSelect", field.Dc, in.Options.Tree.TitleField.TsName, in.Pk.TsName, in.Options.Tree.TitleField.TsName, rules, field.FormGridSpan)
|
||||
|
||||
default:
|
||||
component = defaultComponent
|
||||
}
|
||||
|
||||
buffer.WriteString(component)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelColumns(ctx context.Context, in *genmodel.CurdPreviewParam) (string, error) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
buffer.WriteString("export const columns: VxeGridProps['columns'] = [\n")
|
||||
|
||||
// 主表
|
||||
if err := l.genWebModelColumnsEach(buffer, in, in.MasterFields); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 关联表
|
||||
if len(in.Options.Join) > 0 {
|
||||
for _, v := range in.Options.Join {
|
||||
if !isEffectiveJoin(v) {
|
||||
continue
|
||||
}
|
||||
if err := l.genWebModelColumnsEach(buffer, in, v.Columns); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.WriteString(" { title: '操作', width: 120, slots: { default: 'action' } },\n")
|
||||
buffer.WriteString("];\n")
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelColumnsInterface(ctx context.Context, in *genmodel.CurdPreviewParam) (string, error) {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
buffer.WriteString("export interface RowType {\n")
|
||||
|
||||
// 主表
|
||||
for _, field := range in.MasterFields {
|
||||
buffer.WriteString(fmt.Sprintf(" %s: %s;\n", field.TsName, field.TsType))
|
||||
}
|
||||
|
||||
// 关联表
|
||||
if len(in.Options.Join) > 0 {
|
||||
for _, v := range in.Options.Join {
|
||||
if !isEffectiveJoin(v) {
|
||||
continue
|
||||
}
|
||||
for _, field := range v.Columns {
|
||||
buffer.WriteString(fmt.Sprintf(" %s: %s;\n", field.TsName, field.TsType))
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.WriteString("};\n")
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelViewSchema(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
buffer.WriteString("export const viewSchema: DescItem[] = [\n")
|
||||
|
||||
// 主表
|
||||
for _, field := range in.MasterFields {
|
||||
l.genWebModelViewSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in)
|
||||
}
|
||||
|
||||
// 关联表
|
||||
if len(in.Options.Join) > 0 {
|
||||
for _, v := range in.Options.Join {
|
||||
if !isEffectiveJoin(v) {
|
||||
continue
|
||||
}
|
||||
for _, field := range v.Columns {
|
||||
l.genWebModelViewSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in)
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.WriteString("];\n")
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelEditSchema(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
buffer.WriteString("export const editSchema: VbenFormSchema[] = [\n")
|
||||
|
||||
// 主表
|
||||
for _, field := range in.MasterFields {
|
||||
|
||||
l.genWebModelFormSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in, true)
|
||||
}
|
||||
|
||||
// 关联表
|
||||
if len(in.Options.Join) > 0 {
|
||||
for _, v := range in.Options.Join {
|
||||
if !isEffectiveJoin(v) {
|
||||
continue
|
||||
}
|
||||
for _, field := range v.Columns {
|
||||
l.genWebModelFormSchemaEach(buffer, []*genmodel.GenCodesColumnListModel{field}, in, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer.WriteString("];\n")
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelColumnsEach(buffer *bytes.Buffer, in *genmodel.CurdPreviewParam, fields []*genmodel.GenCodesColumnListModel) (err error) {
|
||||
for _, field := range fields {
|
||||
if !field.IsList {
|
||||
continue
|
||||
}
|
||||
|
||||
if field.Name == in.Pk.Name {
|
||||
buffer.WriteString(fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n type: 'checkbox',\n },\n", field.Dc, field.TsName, field.Align, field.Width))
|
||||
continue
|
||||
}
|
||||
var (
|
||||
defaultComponent = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n },\n", field.Dc, field.TsName, field.Align, field.Width)
|
||||
component string
|
||||
)
|
||||
if in.Options.Step.IsTreeTable && in.Options.Tree.TitleColumn == field.Name {
|
||||
defaultComponent = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n treeNode: true,\n },\n", field.Dc, field.TsName, field.Align, field.Width)
|
||||
}
|
||||
|
||||
// 查询用户摘要
|
||||
if in.Options.Step.HasHookMemberSummary && IsMemberSummaryField(field.Name) {
|
||||
buffer.WriteString(fmt.Sprintf(" {\n title: '%v',\n field: '%v',\n align: '%v',\n width: %v,\n slots: {\n default: ({ row }) => {\n return renderPopoverMemberSumma(row.%vSumma);\n },\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName))
|
||||
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, []string{"renderPopoverMemberSumma", "type MemberSumma"}...)
|
||||
continue
|
||||
}
|
||||
|
||||
// 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况
|
||||
switch field.FormMode {
|
||||
case FMDate:
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return formatDateTime(row.%s);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)
|
||||
in.Options.Step.ImportModel.UtilsDate = append(in.Options.Step.ImportModel.UtilsDate, "formatToDate")
|
||||
|
||||
case FMRadio:
|
||||
fallthrough
|
||||
case FMSelect:
|
||||
|
||||
if g.IsEmpty(in.Options.DictMap[field.TsName]) {
|
||||
g.Log().Infof(context.Background(), "server/internal/library/xgen/gen_curd_model.go genWebModelColumnsEach in.Options.DictMap:%+v", in.Options.DictMap)
|
||||
err = gerror.Newf("设置单选下拉框选项时,必须选择字典类型,字段名称:%v", field.Name)
|
||||
return
|
||||
}
|
||||
|
||||
component = fmt.Sprintf(` {
|
||||
title: '%s', field: '%s', align: '%v', width: %v,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.%v, '%v');
|
||||
}
|
||||
},
|
||||
},
|
||||
`,
|
||||
field.Dc, field.TsName, field.Align, field.Width, field.TsName, in.Options.DictMap[field.TsName])
|
||||
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDict")
|
||||
|
||||
case FMSelectMultiple:
|
||||
if g.IsEmpty(in.Options.DictMap[field.TsName]) {
|
||||
err = gerror.Newf("设置多选下拉框选项时,必须选择字典类型,字段名称:%v", field.Name)
|
||||
return
|
||||
}
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n if (isNullObject(row.%s) || !isArray(row.%s)) {\n return ``;\n }\n return row.%s.map((tagKey) => {\n return renderOptionTag('%s', row.tagKey)\n });\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName, field.TsName, field.TsName, in.Options.DictMap[field.TsName])
|
||||
in.Options.Step.ImportModel.NaiveUI = append(in.Options.Step.ImportModel.NaiveUI, "NTag")
|
||||
in.Options.Step.ImportModel.UtilsIs = append(in.Options.Step.ImportModel.UtilsIs, "isNullObject")
|
||||
|
||||
case FMUploadImage:
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderImage(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)
|
||||
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderImage")
|
||||
|
||||
case FMUploadImages:
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderImageGroup(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)
|
||||
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderImageGroup")
|
||||
|
||||
case FMUploadFile:
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderFile(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)
|
||||
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderFile")
|
||||
|
||||
case FMUploadFiles:
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return renderFileGroup(row.%v);\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, field.TsName)
|
||||
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderFileGroup")
|
||||
|
||||
case FMSwitch:
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return h(%s, {\n value: row.%s === 1,\n checked: '开启',\n unchecked: '关闭',\n disabled: !hasPermission(['%s']),\n onUpdateValue: function (e) {\n console.log('onUpdateValue e:' + JSON.stringify(e));\n row.%s = e ? 1 : 0;\n Switch({ %s: row.%s, key: '%s', value: row.%s }).then((_res) => {\n $message.success('操作成功');\n });\n },\n });\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, "NSwitch", field.TsName, "/"+in.Options.ApiPrefix+"/switch", field.TsName, in.Pk.TsName, in.Pk.TsName, CamelCaseToUnderline(field.TsName), field.TsName)
|
||||
in.Options.Step.ImportModel.NaiveUI = append(in.Options.Step.ImportModel.NaiveUI, "NSwitch")
|
||||
|
||||
case FMRate:
|
||||
component = fmt.Sprintf(" {\n title: '%s',\n field: '%s',\n align: '%v',\n width: %v,\n render(row: State) {\n return h(%s, {\n allowHalf: true,\n readonly: true,\n defaultValue: row.%s,\n });\n },\n },\n", field.Dc, field.TsName, field.Align, field.Width, "NRate", field.TsName)
|
||||
in.Options.Step.ImportModel.NaiveUI = append(in.Options.Step.ImportModel.NaiveUI, "NRate")
|
||||
|
||||
default:
|
||||
component = defaultComponent
|
||||
}
|
||||
|
||||
buffer.WriteString(component)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebModelViewSchemaEach(buffer *bytes.Buffer, fields []*genmodel.GenCodesColumnListModel, in *genmodel.CurdPreviewParam) {
|
||||
for _, field := range fields {
|
||||
|
||||
if field.Name == in.Pk.Name {
|
||||
tmp := fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
|
||||
buffer.WriteString(tmp)
|
||||
continue
|
||||
}
|
||||
|
||||
// 查询用户摘要
|
||||
if field.IsQuery && in.Options.Step.HasQueryMemberSummary && IsMemberSummaryField(field.Name) {
|
||||
tmp := fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
|
||||
buffer.WriteString(tmp)
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
defaultComponent = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
|
||||
component string
|
||||
)
|
||||
|
||||
// 这里根据编辑表单组件来进行推断,如果没有则使用默认input,这可能会导致和查询条件所需参数不符的情况
|
||||
switch field.FormMode {
|
||||
case FMInput, FMInputTextarea, FMInputEditor:
|
||||
component = defaultComponent
|
||||
|
||||
case FMInputNumber:
|
||||
component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
|
||||
|
||||
case FMDate:
|
||||
component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
|
||||
|
||||
case FMDateRange:
|
||||
component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
|
||||
|
||||
case FMTime:
|
||||
component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
|
||||
|
||||
case FMTimeRange:
|
||||
component = fmt.Sprintf(" { field: '%s', label: '%s'},\n", field.TsName, field.Dc)
|
||||
|
||||
case FMSwitch:
|
||||
fallthrough
|
||||
case FMRadio:
|
||||
component = fmt.Sprintf(` {
|
||||
field: '%s',
|
||||
label: '%s',
|
||||
render(row: any) {
|
||||
return renderDict(row.%s, '%v');
|
||||
},
|
||||
},
|
||||
`, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
|
||||
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDict")
|
||||
|
||||
case FMCheckbox:
|
||||
component = fmt.Sprintf(` {
|
||||
field: '%s',
|
||||
label: '%s',
|
||||
render(row: any) {
|
||||
return renderDictTags(row.%s, '%v');
|
||||
},
|
||||
},
|
||||
`, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
|
||||
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDictTags")
|
||||
|
||||
case FMSelect:
|
||||
component = fmt.Sprintf(` {
|
||||
field: '%s',
|
||||
label: '%s',
|
||||
render(row: any) {
|
||||
return renderDict(row.%s, '%v');
|
||||
},
|
||||
},
|
||||
`, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
|
||||
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDict")
|
||||
|
||||
case FMSelectMultiple:
|
||||
component = fmt.Sprintf(` {
|
||||
field: '%s',
|
||||
label: '%s',
|
||||
render(row: any) {
|
||||
return renderDictTags(row.%s, '%v');
|
||||
},
|
||||
},
|
||||
`, field.TsName, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
|
||||
in.Options.Step.ImportModel.UtilsIndex = append(in.Options.Step.ImportModel.UtilsIndex, "renderDictTags")
|
||||
|
||||
default:
|
||||
component = defaultComponent
|
||||
}
|
||||
|
||||
buffer.WriteString(component)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
func (l *gCurd) webEditTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) {
|
||||
data = make(g.Map)
|
||||
data["formItem"] = l.genWebEditFormItem(ctx, in)
|
||||
data["script"] = l.genWebEditScript(ctx, in)
|
||||
data["isEditModal"] = in.Options.Step.IsEditModal
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebEditFormItem(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
for _, field := range in.MasterFields {
|
||||
if !field.IsEdit {
|
||||
continue
|
||||
}
|
||||
|
||||
if IsIndexPK(field.Index) {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
defaultComponent = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<Input placeholder="请输入%s" v-model:value="formValue.%s" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName)
|
||||
component string
|
||||
)
|
||||
|
||||
if in.Options.Step.IsTreeTable && IsPidName(field.Name) {
|
||||
field.FormMode = FMPidTreeSelect
|
||||
}
|
||||
|
||||
switch field.FormMode {
|
||||
case FMInput:
|
||||
component = defaultComponent
|
||||
|
||||
case FMInputNumber:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<InputNumber class="w-full" placeholder="请输入%s" v-model:value="formValue.%s" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName)
|
||||
|
||||
case FMInputTextarea:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<Textarea placeholder="请输入%s" v-model:value="formValue.%s" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName)
|
||||
|
||||
case FMInputEditor:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<RichTextarea style="height: 450px" v-model="formValue.%s" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
|
||||
|
||||
case FMInputDynamic:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<Space direction="vertical" class="w-full">
|
||||
<Space v-for="(item, index) in formValue.%s" :key="index">
|
||||
<Input v-model:value="item.key" placeholder="键名" />
|
||||
<Input v-model:value="item.value" placeholder="键值" />
|
||||
</Space>
|
||||
</Space>
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
|
||||
|
||||
case FMDate:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<DatePicker v-model:value="formValue.%s" class="w-full" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
|
||||
|
||||
case FMTime:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<DatePicker v-model:value="formValue.%s" show-time class="w-full" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
|
||||
|
||||
case FMRadio:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<RadioGroup
|
||||
v-model:value="formValue.%s"
|
||||
:options="getDictOptions('%s')"
|
||||
option-type="button"
|
||||
button-style="solid"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, in.Options.DictMap[field.TsName])
|
||||
|
||||
case FMCheckbox:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<CheckboxGroup v-model:value="formValue.%s">
|
||||
<Space>
|
||||
<Checkbox
|
||||
v-for="item in getDictOptions('%s')"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</CheckboxGroup>
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, in.Options.DictMap[field.TsName])
|
||||
|
||||
case FMSelect:
|
||||
if in.Options.DictMap[field.TsName] != nil {
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<Select v-model:value="formValue.%s" :options="getDictOptions('%s')" placeholder="请选择%s" class="w-full" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, in.Options.DictMap[field.TsName], field.Dc)
|
||||
} else {
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<Select v-model:value="formValue.%s" placeholder="请选择%s" class="w-full" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, field.Dc)
|
||||
}
|
||||
|
||||
case FMSelectMultiple:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<Select v-model:value="formValue.%s" mode="multiple" :options="getDictOptions('%s')" placeholder="请选择%s" class="w-full" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, in.Options.DictMap[field.TsName], field.Dc)
|
||||
|
||||
case FMUploadImage:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<ImageUpload :max-count="1" v-model:value="formValue.%s" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
|
||||
|
||||
case FMUploadImages:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<ImageUpload :max-count="10" v-model:value="formValue.%s" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
|
||||
|
||||
case FMUploadFile:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<FileUpload :max-count="1" v-model:value="formValue.%s" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
|
||||
|
||||
case FMUploadFiles:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<FileUpload :max-count="10" v-model:value="formValue.%s" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
|
||||
|
||||
case FMSwitch:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<Switch v-model:checked="formValue.%s" :checked-value="1" :un-checked-value="0" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
|
||||
|
||||
case FMRate:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<Rate allow-half v-model:value="formValue.%s" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName)
|
||||
|
||||
case FMCitySelector:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%s" name="%s">
|
||||
<Cascader v-model:value="formValue.%s" placeholder="请选择%s" class="w-full" />
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.TsName, field.Dc)
|
||||
|
||||
case FMPidTreeSelect:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%v" name="pid">
|
||||
<TreeSelect
|
||||
v-model:value="formValue.pid"
|
||||
:tree-data="treeOption"
|
||||
:field-names="{ label: '%v', value: '%v', children: 'children' }"
|
||||
allow-clear
|
||||
show-search
|
||||
tree-default-expand-all
|
||||
:tree-node-filter-prop="'%v'"
|
||||
class="w-full"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, in.Options.Tree.TitleField.TsName, in.Pk.TsName, in.Options.Tree.TitleField.TsName)
|
||||
|
||||
case FMTreeSelect:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%v" name="%v">
|
||||
<TreeSelect
|
||||
placeholder="请选择%v"
|
||||
v-model:value="formValue.%v"
|
||||
:tree-data="[{ title: 'AA', value: 1, children: [{ title: 'BB', value: 2 }] }]"
|
||||
allow-clear
|
||||
show-search
|
||||
tree-default-expand-all
|
||||
class="w-full"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName)
|
||||
|
||||
case FMCascader:
|
||||
component = fmt.Sprintf(`<Col :span="%d">
|
||||
<FormItem label="%v" name="%v">
|
||||
<Cascader
|
||||
placeholder="请选择%v"
|
||||
v-model:value="formValue.%v"
|
||||
:options="[{ label: 'AA', value: 1, children: [{ label: 'BB', value: 2 }] }]"
|
||||
allow-clear
|
||||
show-search
|
||||
class="w-full"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>`, field.FormGridSpan, field.Dc, field.TsName, field.Dc, field.TsName)
|
||||
|
||||
default:
|
||||
component = defaultComponent
|
||||
}
|
||||
|
||||
buffer.WriteString(component + "\n\n")
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (l *gCurd) genWebEditScript(ctx context.Context, in *genmodel.CurdPreviewParam) g.Map {
|
||||
var (
|
||||
data = make(g.Map)
|
||||
importBuffer = bytes.NewBuffer(nil)
|
||||
antdUI []string
|
||||
)
|
||||
|
||||
importBuffer.WriteString(" import { ref, computed } from 'vue';\n")
|
||||
|
||||
// 导入字典
|
||||
if in.Options.DictOps.Has {
|
||||
importBuffer.WriteString(" import { getDictOptions } from '#/utils/dict';\n")
|
||||
}
|
||||
|
||||
// 导入api
|
||||
var importApiMethod = []string{"Edit", "View"}
|
||||
if in.Options.Step.HasMaxSort {
|
||||
importApiMethod = append(importApiMethod, "MaxSort")
|
||||
}
|
||||
importBuffer.WriteString(" import " + ImportWebMethod(importApiMethod) + " from '" + in.Options.ImportWebApi + "';\n")
|
||||
|
||||
// 导入model
|
||||
var importModelMethod = []string{"State", "newState"}
|
||||
if in.Options.Step.IsTreeTable {
|
||||
importModelMethod = append(importModelMethod, []string{"treeOption", "loadTreeOption"}...)
|
||||
}
|
||||
|
||||
if in.Options.Step.HasRules {
|
||||
importModelMethod = append(importModelMethod, "rules")
|
||||
}
|
||||
importBuffer.WriteString(" import " + ImportWebMethod(importModelMethod) + " from './model';\n")
|
||||
|
||||
for _, field := range in.MasterFields {
|
||||
if !field.IsEdit {
|
||||
continue
|
||||
}
|
||||
switch field.FormMode {
|
||||
case FMInput, FMInputTextarea, FMInputDynamic:
|
||||
antdUI = append(antdUI, "Col", "FormItem", "Input", "Space")
|
||||
case FMInputNumber:
|
||||
antdUI = append(antdUI, "Col", "FormItem", "InputNumber")
|
||||
case FMDate, FMDateRange, FMTime, FMTimeRange:
|
||||
antdUI = append(antdUI, "Col", "FormItem", "DatePicker")
|
||||
case FMInputEditor:
|
||||
if !gstr.Contains(importBuffer.String(), `import { Tinymce as RichTextarea }`) {
|
||||
importBuffer.WriteString(" import { Tinymce as RichTextarea } from '#/components/tinymce';\n")
|
||||
}
|
||||
antdUI = append(antdUI, "Col", "FormItem")
|
||||
case FMUploadImage, FMUploadImages:
|
||||
if !gstr.Contains(importBuffer.String(), `import { ImageUpload }`) {
|
||||
importBuffer.WriteString(" import { ImageUpload } from '#/components/upload';\n")
|
||||
}
|
||||
antdUI = append(antdUI, "Col", "FormItem")
|
||||
case FMUploadFile, FMUploadFiles:
|
||||
if !gstr.Contains(importBuffer.String(), `import { FileUpload }`) {
|
||||
importBuffer.WriteString(" import { FileUpload } from '#/components/upload';\n")
|
||||
}
|
||||
antdUI = append(antdUI, "Col", "FormItem")
|
||||
case FMRadio:
|
||||
antdUI = append(antdUI, "Col", "FormItem", "RadioGroup")
|
||||
case FMCheckbox:
|
||||
antdUI = append(antdUI, "Col", "FormItem", "CheckboxGroup", "Checkbox", "Space")
|
||||
case FMSelect, FMSelectMultiple:
|
||||
antdUI = append(antdUI, "Col", "FormItem", "Select")
|
||||
case FMSwitch:
|
||||
antdUI = append(antdUI, "Col", "FormItem", "Switch")
|
||||
case FMRate:
|
||||
antdUI = append(antdUI, "Col", "FormItem", "Rate")
|
||||
case FMCitySelector, FMCascader:
|
||||
antdUI = append(antdUI, "Col", "FormItem", "Cascader")
|
||||
case FMPidTreeSelect, FMTreeSelect:
|
||||
antdUI = append(antdUI, "Col", "FormItem", "TreeSelect")
|
||||
}
|
||||
}
|
||||
|
||||
antdUI = utility.UniqueSlice(antdUI)
|
||||
if len(antdUI) > 0 {
|
||||
importBuffer.WriteString(" import " + ImportWebMethod(antdUI) + " from 'ant-design-vue';\n")
|
||||
}
|
||||
|
||||
data["import"] = importBuffer.String()
|
||||
data["setup"] = ""
|
||||
if in.Options.PresetStep != nil {
|
||||
data["formGridSpan"] = in.Options.PresetStep.FormGridCols
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func (l *gCurd) webIndexTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (g.Map, error) {
|
||||
var (
|
||||
data = make(g.Map)
|
||||
importBuffer = bytes.NewBuffer(nil)
|
||||
importVueMethod = []string{"h", "reactive", "ref", "computed"}
|
||||
importApiMethod = []string{"List"}
|
||||
importModelMethod = []string{"columns", "querySchema", "type RowType"}
|
||||
importUtilsMethod []string
|
||||
importIcons []string
|
||||
actionWidth int64 = 72
|
||||
)
|
||||
|
||||
// 添加 MdiPlus, MdiDelete,MdiExport
|
||||
if in.Options.Step.HasAdd {
|
||||
importIcons = append(importIcons, "MdiPlus")
|
||||
}
|
||||
|
||||
// 编辑
|
||||
if in.Options.Step.HasEdit {
|
||||
in.Options.Step.ActionColumnWidth += actionWidth
|
||||
if in.Options.Step.IsTreeTable && !in.Options.Step.IsOptionTreeTable {
|
||||
in.Options.Step.ActionColumnWidth += actionWidth
|
||||
}
|
||||
if in.Options.Step.IsOptionTreeTable {
|
||||
importIcons = append(importIcons, "MdiEdit")
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
if in.Options.Step.HasExport {
|
||||
importIcons = append(importIcons, "MdiExport")
|
||||
importApiMethod = append(importApiMethod, "Export")
|
||||
}
|
||||
|
||||
// 删除
|
||||
if in.Options.Step.HasDel {
|
||||
importApiMethod = append(importApiMethod, "Delete")
|
||||
in.Options.Step.ActionColumnWidth += actionWidth
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
if in.Options.Step.HasBatchDel {
|
||||
importIcons = append(importIcons, "MdiDelete")
|
||||
importApiMethod = append(importApiMethod, "Delete")
|
||||
}
|
||||
|
||||
// 修改状态
|
||||
if in.Options.Step.HasStatus {
|
||||
importApiMethod = append(importApiMethod, "Status")
|
||||
in.Options.Step.ActionColumnWidth += actionWidth
|
||||
}
|
||||
|
||||
// 更多
|
||||
// 查看详情
|
||||
if in.Options.Step.HasView {
|
||||
in.Options.Step.ActionColumnWidth += actionWidth
|
||||
}
|
||||
|
||||
// 展开树
|
||||
if in.Options.Step.IsTreeTable {
|
||||
// importIcons = append(importIcons, "MdiExport")
|
||||
}
|
||||
|
||||
// 存在字典数据选项
|
||||
if in.Options.DictOps.Has {
|
||||
importVueMethod = append(importVueMethod, "onMounted")
|
||||
// importModelMethod = append(importModelMethod, "loadOptions")
|
||||
}
|
||||
|
||||
// 选项式树表
|
||||
if in.Options.Step.IsOptionTreeTable {
|
||||
importVueMethod = append(importVueMethod, "onMounted")
|
||||
// importIcons = append(importIcons, []string{"FormOutlined", "SearchOutlined"}...)
|
||||
importModelMethod = append(importModelMethod, []string{"loadTreeOption", "treeOption", "State"}...)
|
||||
}
|
||||
|
||||
// 操作按钮宽度最小值
|
||||
if in.Options.Step.ActionColumnWidth > 0 && in.Options.Step.ActionColumnWidth < actionWidth*2 {
|
||||
in.Options.Step.ActionColumnWidth = 100
|
||||
}
|
||||
|
||||
// 导入基础包
|
||||
importBuffer.WriteString(" import " + ImportWebMethod(importVueMethod) + " from 'vue';\n")
|
||||
importBuffer.WriteString(" import { Button, message,Tag, Modal, Popconfirm,Switch } from 'ant-design-vue';\n")
|
||||
importBuffer.WriteString(" import type { VbenFormProps } from '#/adapter/form';\n")
|
||||
importBuffer.WriteString(" import type { VxeTableGridOptions, VxeGridListeners } from '#/adapter/vxe-table';\n")
|
||||
importBuffer.WriteString(" import type { DeepPartial } from '@vben/types';\n")
|
||||
importBuffer.WriteString(" import { getVxePopupContainer } from '@vben/utils';\n")
|
||||
importBuffer.WriteString(" import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';\n")
|
||||
importBuffer.WriteString(" import { useVbenVxeGrid } from '#/adapter/vxe-table';\n")
|
||||
importBuffer.WriteString(" import { AccessControl, useAccess } from '@vben/access';\n")
|
||||
importBuffer.WriteString(" const { hasAccessByCodes } = useAccess();\n")
|
||||
if in.Options.Step.HasExport {
|
||||
importBuffer.WriteString(" import { commonDownloadExcel } from '#/utils/file/download';\n")
|
||||
}
|
||||
|
||||
// 导入字典
|
||||
if in.Options.DictOps.Has {
|
||||
// importBuffer.WriteString(" import { useDictStore } from '@/store/modules/dict';\n")
|
||||
}
|
||||
|
||||
// 导入api
|
||||
importBuffer.WriteString(" import " + ImportWebMethod(importApiMethod) + " from '" + in.Options.ImportWebApi + "';\n")
|
||||
|
||||
// 导入icons
|
||||
if len(importIcons) > 0 {
|
||||
importBuffer.WriteString(" import " + ImportWebMethod(importIcons) + " from '@vben/icons';\n")
|
||||
}
|
||||
|
||||
// 导入model
|
||||
if in.Options.Step.IsTreeTable {
|
||||
importModelMethod = append(importModelMethod, "newState")
|
||||
}
|
||||
importBuffer.WriteString(" import " + ImportWebMethod(importModelMethod) + " from './model';\n")
|
||||
|
||||
// 导入utils
|
||||
if len(importUtilsMethod) > 0 {
|
||||
// importBuffer.WriteString(" import " + ImportWebMethod(importUtilsMethod) + " from '@/utils/xjadmin';\n")
|
||||
}
|
||||
|
||||
// 导入edit组件
|
||||
if in.Options.Step.HasEdit {
|
||||
importBuffer.WriteString(" import editDrawer from './edit.vue';\n")
|
||||
}
|
||||
|
||||
// 导入view组件
|
||||
if in.Options.Step.HasView {
|
||||
importBuffer.WriteString(" import viewDrawer from './view.vue';\n")
|
||||
}
|
||||
|
||||
// 没有需要查询的字段则隐藏搜索表单
|
||||
isSearchForm := false
|
||||
for _, field := range in.MasterFields {
|
||||
if field.IsQuery {
|
||||
isSearchForm = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isSearchForm {
|
||||
if len(in.Options.Join) > 0 {
|
||||
LoopOut:
|
||||
for _, v := range in.Options.Join {
|
||||
for _, column := range v.Columns {
|
||||
if column.IsQuery {
|
||||
isSearchForm = true
|
||||
break LoopOut
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
data["isSearchForm"] = isSearchForm
|
||||
data["import"] = importBuffer.String()
|
||||
data["isEditModal"] = in.Options.Step.IsEditModal
|
||||
return data, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
"context"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func (l *gCurd) generateWebModelDictOptions(ctx context.Context, in *genmodel.CurdPreviewParam) error {
|
||||
type DictType struct {
|
||||
DictType string `json:"dictType"`
|
||||
DictName string `json:"dictName"`
|
||||
}
|
||||
|
||||
var (
|
||||
dictTypeIds []string
|
||||
dictTypeList []*DictType
|
||||
builtinDictTypeIds []int64
|
||||
builtinDictTypeList []*DictType
|
||||
)
|
||||
|
||||
for _, field := range in.MasterFields {
|
||||
if field.DictType != "" {
|
||||
dictTypeIds = append(dictTypeIds, field.DictType)
|
||||
g.Log().Debugf(context.Background(), "curd_generate_web_model.go generateWebModelDictOptions in.masterFields:%+v", field)
|
||||
}
|
||||
|
||||
// if field.DictType < 0 {
|
||||
// builtinDictTypeIds = append(builtinDictTypeIds, field.DictType)
|
||||
// }
|
||||
}
|
||||
g.Log().Debugf(context.Background(), "curd_generate_web_model.go generateWebModelDictOptions dictTypeIds:%+v", dictTypeIds)
|
||||
dictTypeIds = utility.UniqueSlice(dictTypeIds)
|
||||
builtinDictTypeIds = utility.UniqueSlice(builtinDictTypeIds)
|
||||
g.Log().Debugf(context.Background(), "curd_generate_web_model.go generateWebModelDictOptions dictTypeIds:%+v", dictTypeIds)
|
||||
// g.Log().Debugf(context.Background(), "curd_generate_web_model.go generateWebModelDictOptions builtinDictTypeIds:%+v", builtinDictTypeIds)
|
||||
if len(dictTypeIds) == 0 && len(builtinDictTypeIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(dictTypeIds) > 0 {
|
||||
err := g.Model("sys_dict_type").Ctx(ctx).
|
||||
Fields("dict_type", "dict_name").
|
||||
WhereIn("dict_type", dictTypeIds).
|
||||
Scan(&dictTypeList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
g.Log().Debugf(context.Background(), "curd_generate_web_model.go generateWebModelDictOptions dictTypeList:%+v", dictTypeList)
|
||||
// if len(builtinDictTypeIds) > 0 {
|
||||
// for _, id := range builtinDictTypeIds {
|
||||
// typ, err := dict.GetTypeById(ctx, id)
|
||||
// if err != nil && !errors.Is(err, dict.NotExistKeyError) {
|
||||
// return err
|
||||
// }
|
||||
// if len(typ) > 0 {
|
||||
// row := new(DictType)
|
||||
// row.Id = id
|
||||
// row.Type = typ
|
||||
// builtinDictTypeList = append(builtinDictTypeList, row)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if len(dictTypeList) == 0 && len(builtinDictTypeList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(builtinDictTypeList) > 0 {
|
||||
dictTypeList = append(dictTypeList, builtinDictTypeList...)
|
||||
}
|
||||
|
||||
in.Options.DictOps.Has = true
|
||||
|
||||
for _, v := range dictTypeList {
|
||||
// 字段映射字典
|
||||
for _, field := range in.MasterFields {
|
||||
if field.DictType != "" && v.DictType == field.DictType {
|
||||
in.Options.DictMap[field.TsName] = v.DictType
|
||||
in.Options.DictOps.Schemas = append(in.Options.DictOps.Schemas, &genmodel.OptionsSchemasField{
|
||||
Field: field.TsName,
|
||||
Type: v.DictType,
|
||||
})
|
||||
}
|
||||
}
|
||||
in.Options.DictOps.Types = append(in.Options.DictOps.Types, v.DictName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func (l *gCurd) webViewTplData(ctx context.Context, in *genmodel.CurdPreviewParam) (data g.Map, err error) {
|
||||
data = make(g.Map)
|
||||
data["item"] = l.generateWebViewItem(ctx, in)
|
||||
return
|
||||
}
|
||||
|
||||
func (l *gCurd) generateWebViewItem(ctx context.Context, in *genmodel.CurdPreviewParam) string {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
for _, field := range in.MasterFields {
|
||||
if !field.IsEdit {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
defaultComponent = fmt.Sprintf(`<DescriptionsItem label="%s">{{ formValue.%s }}</DescriptionsItem>`, field.Dc, field.TsName)
|
||||
component string
|
||||
)
|
||||
|
||||
switch field.FormMode {
|
||||
case FMInputTextarea, FMInputEditor:
|
||||
component = fmt.Sprintf(`<DescriptionsItem label="%s">
|
||||
<span v-html="formValue.%s"></span>
|
||||
</DescriptionsItem>`, field.Dc, field.TsName)
|
||||
|
||||
case FMInputDynamic:
|
||||
component = defaultComponent
|
||||
|
||||
case FMDate:
|
||||
component = defaultComponent
|
||||
|
||||
case FMTime:
|
||||
component = defaultComponent
|
||||
|
||||
case FMRadio, FMSelect:
|
||||
component = fmt.Sprintf(`<DescriptionsItem label="%s">
|
||||
<component :is="renderDict(String(formValue.%s), '%s')" />
|
||||
</DescriptionsItem>`, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
|
||||
|
||||
case FMCheckbox, FMSelectMultiple:
|
||||
component = fmt.Sprintf(`<DescriptionsItem label="%s">
|
||||
<component :is="renderDictTags(formValue.%s, '%s')" />
|
||||
</DescriptionsItem>`, field.Dc, field.TsName, in.Options.DictMap[field.TsName])
|
||||
|
||||
case FMUploadImage:
|
||||
component = fmt.Sprintf(`<DescriptionsItem label="%s">
|
||||
<Image style="margin-left: 10px; height: 100px; width: 100px" :src="formValue.%s" />
|
||||
</DescriptionsItem>`, field.Dc, field.TsName)
|
||||
|
||||
case FMUploadImages:
|
||||
component = fmt.Sprintf(`<DescriptionsItem label="%s">
|
||||
<Image.PreviewGroup>
|
||||
<Space>
|
||||
<Image
|
||||
v-for="(item, key) in formValue.%s"
|
||||
:key="key"
|
||||
style="margin-left: 10px; height: 100px; width: 100px"
|
||||
:src="item"
|
||||
/>
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
</DescriptionsItem>`, field.Dc, field.TsName)
|
||||
|
||||
case FMUploadFile:
|
||||
component = fmt.Sprintf(`<DescriptionsItem label="%s">
|
||||
<div class="upload-card" v-show="formValue.%s !== ''" @click="download(formValue.%s)">
|
||||
<div class="upload-card-item" style="height: 100px; width: 100px">
|
||||
<div class="upload-card-item-info">
|
||||
<div class="img-box">
|
||||
<Avatar :style="fileAvatarCSS">{{ getFileExt(formValue.%s) }}</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DescriptionsItem>`, field.Dc, field.TsName, field.TsName, field.TsName)
|
||||
|
||||
case FMUploadFiles:
|
||||
component = fmt.Sprintf(`<DescriptionsItem label="%s">
|
||||
<div class="upload-card">
|
||||
<Space :size="0">
|
||||
<div
|
||||
class="upload-card-item"
|
||||
style="height: 100px; width: 100px"
|
||||
v-for="(item, key) in formValue.%s"
|
||||
:key="key"
|
||||
>
|
||||
<div class="upload-card-item-info">
|
||||
<div class="img-box">
|
||||
<Avatar :style="fileAvatarCSS" @click="download(item)">{{
|
||||
getFileExt(item)
|
||||
}}</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
</DescriptionsItem>`, field.Dc, field.TsName)
|
||||
|
||||
case FMSwitch:
|
||||
component = fmt.Sprintf(`<DescriptionsItem label="%s">
|
||||
<Switch :checked="formValue.%s === 1" disabled />
|
||||
</DescriptionsItem>`, field.Dc, field.TsName)
|
||||
|
||||
case FMRate:
|
||||
component = fmt.Sprintf(`<DescriptionsItem label="%s">
|
||||
<Rate :value="formValue.%s" disabled allow-half />
|
||||
</DescriptionsItem>`, field.Dc, field.TsName)
|
||||
|
||||
default:
|
||||
component = defaultComponent
|
||||
}
|
||||
|
||||
buffer.WriteString(" " + component + "\n\n")
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
// package genview
|
||||
// @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 genview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/mcache"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_view/gohtml"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
version "git.5iot.com/iotlcm-public/iotlcm-app/utility/version"
|
||||
|
||||
"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/gfile"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"golang.org/x/tools/imports"
|
||||
)
|
||||
|
||||
func replaceEmptyLinesWithSpace(input string) string {
|
||||
re := regexp.MustCompile(`\n\s*\n`)
|
||||
result := re.ReplaceAllString(input, "\n\n")
|
||||
return result
|
||||
}
|
||||
|
||||
func FormatTs(code string) string {
|
||||
code = replaceEmptyLinesWithSpace(code)
|
||||
return code + "\n"
|
||||
}
|
||||
|
||||
func ToTSArray(vs []string) string {
|
||||
formattedStrings := make([]string, len(vs))
|
||||
for i, str := range vs {
|
||||
formattedStrings[i] = fmt.Sprintf("'%s'", str)
|
||||
}
|
||||
return fmt.Sprintf("[%s]", strings.Join(formattedStrings, ", "))
|
||||
}
|
||||
|
||||
func DictToTSArray(vs []string) string {
|
||||
formattedStrings := make([]string, len(vs))
|
||||
formattedObjStrings := make([]string, len(vs))
|
||||
for i, str := range vs {
|
||||
formattedStrings[i] = fmt.Sprintf("'%s'", str)
|
||||
formattedObjStrings[i] = fmt.Sprintf("dict_%s", str)
|
||||
}
|
||||
return fmt.Sprintf("[%s],[%s]", strings.Join(formattedStrings, ", "), strings.Join(formattedObjStrings, ", "))
|
||||
}
|
||||
|
||||
func RemoveSlice[K comparable](src []K, sub K) []K {
|
||||
for k, v := range src {
|
||||
if v == sub {
|
||||
copy(src[k:], src[k+1:])
|
||||
return src[:len(src)-1]
|
||||
}
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
// CheckIllegalName 检查命名是否合理
|
||||
func CheckIllegalName(errPrefix string, names ...string) (err error) {
|
||||
reg, _ := regexp.Compile("^[a-z_][a-z0-9_]*$")
|
||||
for _, name := range names {
|
||||
name = strings.ToLower(name)
|
||||
match := reg.MatchString(name)
|
||||
if !match {
|
||||
err = gerror.Newf("%v存在格式不正确,必须全部小写且由字母、数字和下划线组成:%v", errPrefix, name)
|
||||
return
|
||||
}
|
||||
if strings.HasSuffix(name, "test") {
|
||||
err = gerror.Newf("%v当中不能以`test`结尾:%v", errPrefix, name)
|
||||
return
|
||||
}
|
||||
if StartsWithDigit(name) {
|
||||
err = gerror.Newf("%v当中不能以阿拉伯数字开头:%v", errPrefix, name)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
func StartsWithDigit(s string) bool {
|
||||
r := []rune(s)
|
||||
if len(r) > 0 {
|
||||
return unicode.IsDigit(r[0])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetModName 获取主包名
|
||||
func GetModName(ctx context.Context) (modName string, err error) {
|
||||
if !gfile.Exists("go.mod") {
|
||||
err = gerror.New("go.mod does not exist in current working directory")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
goModContent = gfile.GetContents("go.mod")
|
||||
match, _ = gregex.MatchString(`^module\s+(.+)\s*`, goModContent)
|
||||
)
|
||||
|
||||
if len(match) > 1 {
|
||||
modName = gstr.Trim(match[1])
|
||||
} else {
|
||||
err = gerror.New("module name does not found in go.mod")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// parseServFunName 解析业务服务名称
|
||||
func (l *gCurd) parseServFunName(templateGroup, varName string) string {
|
||||
templateGroup = gstr.UcFirst(templateGroup)
|
||||
if gstr.HasPrefix(varName, templateGroup) && varName != templateGroup {
|
||||
return varName
|
||||
}
|
||||
return templateGroup + varName
|
||||
}
|
||||
|
||||
func GetTempGeneratePath(ctx context.Context) string {
|
||||
return gfile.Abs(gfile.Temp() + "/tmp_xjgen/" + version.AppName(ctx))
|
||||
}
|
||||
|
||||
func FormatGo(ctx context.Context, name, code string) (string, error) {
|
||||
path := GetTempGeneratePath(ctx) + "/" + name
|
||||
if err := gfile.PutContents(path, code); err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := imports.Process(path, []byte(code), nil)
|
||||
if err != nil {
|
||||
err = gerror.Newf(`FormatGo error format "%s" go files: %v`, path, err)
|
||||
return "", err
|
||||
}
|
||||
return string(res), nil
|
||||
}
|
||||
func formatComment(comment string) string {
|
||||
comment = gstr.ReplaceByArray(comment, g.SliceStr{
|
||||
"\n", " ",
|
||||
"\r", " ",
|
||||
})
|
||||
comment = gstr.Replace(comment, `\n`, " ")
|
||||
comment = gstr.Trim(comment)
|
||||
return comment
|
||||
}
|
||||
|
||||
func removeEndWrap(comment string) string {
|
||||
if len(comment) > 2 && comment[len(comment)-2:] == " \n" {
|
||||
comment = comment[:len(comment)-2]
|
||||
}
|
||||
return comment
|
||||
}
|
||||
func isEffectiveJoin(join *genmodel.CurdOptionsJoin) bool {
|
||||
return join.Alias != "" && join.Field != "" && join.LinkTable != "" && join.MasterField != "" && join.DaoName != "" && join.LinkMode > 0
|
||||
}
|
||||
|
||||
func IsPidName(name string) bool {
|
||||
return name == "pid"
|
||||
}
|
||||
|
||||
// CamelCaseToUnderline 驼峰单词转下划线单词
|
||||
func CamelCaseToUnderline(s string) string {
|
||||
var output []rune
|
||||
for i, r := range s {
|
||||
if i == 0 {
|
||||
output = append(output, unicode.ToLower(r))
|
||||
} else {
|
||||
if unicode.IsUpper(r) {
|
||||
output = append(output, '_')
|
||||
}
|
||||
|
||||
output = append(output, unicode.ToLower(r))
|
||||
}
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// hasEffectiveJoin 存在有效的关联表
|
||||
func hasEffectiveJoins(joins []*genmodel.CurdOptionsJoin) bool {
|
||||
for _, join := range joins {
|
||||
if isEffectiveJoin(join) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ImportWebMethod 导入前端方法
|
||||
func ImportWebMethod(vs []string) string {
|
||||
vs = utility.UniqueSlice(vs)
|
||||
str := "{ " + strings.Join(vs, ", ") + " }"
|
||||
str = strings.TrimSuffix(str, ", ")
|
||||
return str
|
||||
}
|
||||
|
||||
func FormatVue(code string) string {
|
||||
endTag := `</template>`
|
||||
vueLen := gstr.PosR(code, endTag)
|
||||
vueCode := code[:vueLen+len(endTag)]
|
||||
tsCode := code[vueLen+len(endTag):]
|
||||
vueCode = gohtml.Format(vueCode)
|
||||
tsCode = FormatTs(tsCode)
|
||||
return vueCode + tsCode
|
||||
}
|
||||
|
||||
// ImportSql 导出sql文件
|
||||
func ImportSql(ctx context.Context, path string) error {
|
||||
rows, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
beforeMaxMenuId, _ := dao.SysMenu.Ctx(ctx).Max(dao.SysMenu.Columns().MenuId)
|
||||
sqlArr := strings.Split(string(rows), "\n")
|
||||
for _, sql := range sqlArr {
|
||||
sql = strings.TrimSpace(sql)
|
||||
if sql == "" || strings.HasPrefix(sql, "--") {
|
||||
continue
|
||||
}
|
||||
g.Log().Infof(ctx, "views.ImportSql sql:%v", sql)
|
||||
exec, err := g.DB().Exec(ctx, sql)
|
||||
g.Log().Infof(ctx, "views.ImportSql sql:%v, exec:%+v, err:%+v", sql, exec, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = syncGeneratedMenus(ctx, gconv.Int64(beforeMaxMenuId)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncGeneratedMenus(ctx context.Context, beforeMaxMenuId int64) error {
|
||||
var menuIds []int64
|
||||
if err := dao.SysMenu.Ctx(ctx).
|
||||
Fields(dao.SysMenu.Columns().MenuId).
|
||||
WhereGT(dao.SysMenu.Columns().MenuId, beforeMaxMenuId).
|
||||
OrderAsc(dao.SysMenu.Columns().MenuId).
|
||||
Scan(&menuIds); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(menuIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, menuId := range menuIds {
|
||||
if err := service.MemDBSysMenu().LoadData(ctx, menuId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var roleIds []int64
|
||||
if err := dao.SysRoleMenu.Ctx(ctx).
|
||||
Fields(dao.SysRoleMenu.Columns().RoleId).
|
||||
WhereIn(dao.SysRoleMenu.Columns().MenuId, menuIds).
|
||||
Group(dao.SysRoleMenu.Columns().RoleId).
|
||||
Scan(&roleIds); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(roleIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var userIds []int64
|
||||
if err := dao.SysUserRole.Ctx(ctx).
|
||||
Fields(dao.SysUserRole.Columns().UserId).
|
||||
WhereIn(dao.SysUserRole.Columns().RoleId, roleIds).
|
||||
Group(dao.SysUserRole.Columns().UserId).
|
||||
Scan(&userIds); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, userId := range userIds {
|
||||
_ = mcache.RemoveUserAccessCodeList(ctx, userId)
|
||||
_ = mcache.RemoveUserRoleDataAccessCodeList(ctx, userId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseDBConfigNodeLink 解析数据库连接配置
|
||||
func ParseDBConfigNodeLink(node *gdb.ConfigNode) *gdb.ConfigNode {
|
||||
const linkPattern = `(\w+):([\w\-\$]*):(.*?)@(\w+?)\((.+?)\)/{0,1}([^\?]*)\?{0,1}(.*)`
|
||||
const defaultCharset = `utf8`
|
||||
const defaultProtocol = `tcp`
|
||||
|
||||
var match []string
|
||||
if node.Link != "" {
|
||||
match, _ = gregex.MatchString(linkPattern, node.Link)
|
||||
if len(match) > 5 {
|
||||
node.Type = match[1]
|
||||
node.User = match[2]
|
||||
node.Pass = match[3]
|
||||
node.Protocol = match[4]
|
||||
array := gstr.Split(match[5], ":")
|
||||
if len(array) == 2 && node.Protocol != "file" {
|
||||
node.Host = array[0]
|
||||
node.Port = array[1]
|
||||
node.Name = match[6]
|
||||
} else {
|
||||
node.Name = match[5]
|
||||
}
|
||||
if len(match) > 6 && match[7] != "" {
|
||||
node.Extra = match[7]
|
||||
}
|
||||
node.Link = ""
|
||||
}
|
||||
}
|
||||
if node.Extra != "" {
|
||||
if m, _ := gstr.Parse(node.Extra); len(m) > 0 {
|
||||
_ = gconv.Struct(m, &node)
|
||||
}
|
||||
}
|
||||
// Default value checks.
|
||||
if node.Charset == "" {
|
||||
node.Charset = defaultCharset
|
||||
}
|
||||
if node.Protocol == "" {
|
||||
node.Protocol = defaultProtocol
|
||||
}
|
||||
return node
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// package gohtml
|
||||
// @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 gohtml
|
||||
|
||||
const (
|
||||
defaultIndentString = " "
|
||||
startIndent = 0
|
||||
defaultLastElement = "</html>"
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
// package genview
|
||||
// @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 gohtml
|
||||
@@ -0,0 +1,12 @@
|
||||
// package gohtml
|
||||
// @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 gohtml
|
||||
|
||||
// An element represents an HTML element.
|
||||
type element interface {
|
||||
isInline() bool
|
||||
write(*formattedBuffer, bool) bool
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// package gohtml
|
||||
// @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 gohtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Format parses the input HTML string, formats it and returns the result.
|
||||
func Format(s string) string {
|
||||
return parse(strings.NewReader(s)).html()
|
||||
}
|
||||
|
||||
// FormatBytes parses input HTML as bytes, formats it and returns the result.
|
||||
func FormatBytes(b []byte) []byte {
|
||||
return parse(bytes.NewReader(b)).bytes()
|
||||
}
|
||||
|
||||
// Format parses the input HTML string, formats it and returns the result with line no.
|
||||
func FormatWithLineNo(s string) string {
|
||||
return AddLineNo(Format(s))
|
||||
}
|
||||
|
||||
func AddLineNo(s string) string {
|
||||
lines := strings.Split(s, "\n")
|
||||
maxLineNoStrLen := len(strconv.Itoa(len(lines)))
|
||||
bf := &bytes.Buffer{}
|
||||
for i, line := range lines {
|
||||
lineNoStr := strconv.Itoa(i + 1)
|
||||
if i > 0 {
|
||||
bf.WriteString("\n")
|
||||
}
|
||||
bf.WriteString(strings.Repeat(" ", maxLineNoStrLen-len(lineNoStr)) + lineNoStr + " " + line)
|
||||
}
|
||||
return bf.String()
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// package gohtml
|
||||
// @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 gohtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Column to wrap lines to (disabled by default)
|
||||
var LineWrapColumn = 0
|
||||
|
||||
// Maxmimum characters a long word can extend past LineWrapColumn without wrapping
|
||||
var LineWrapMaxSpillover = 5
|
||||
|
||||
// An htmlDocument represents an HTML document.
|
||||
type htmlDocument struct {
|
||||
elements []element
|
||||
}
|
||||
|
||||
// html generates an HTML source code and returns it.
|
||||
func (htmlDoc *htmlDocument) html() string {
|
||||
str := string(htmlDoc.bytes())
|
||||
str = replaceMultipleNewlinesWithSpaceAndTabs(str)
|
||||
return str
|
||||
}
|
||||
|
||||
func replaceMultipleNewlinesWithSpaceAndTabs(input string) string {
|
||||
re := regexp.MustCompile(`\n\s*\n+`)
|
||||
formattedString := re.ReplaceAllString(input, "\n")
|
||||
return formattedString
|
||||
}
|
||||
|
||||
// bytes reads from htmlDocument's internal array of elements and returns HTML source code
|
||||
func (htmlDoc *htmlDocument) bytes() []byte {
|
||||
bf := &formattedBuffer{
|
||||
buffer: &bytes.Buffer{},
|
||||
|
||||
lineWrapColumn: LineWrapColumn,
|
||||
lineWrapMaxSpillover: LineWrapMaxSpillover,
|
||||
|
||||
indentString: defaultIndentString,
|
||||
indentLevel: startIndent,
|
||||
}
|
||||
|
||||
isPreviousNodeInline := true
|
||||
for _, child := range htmlDoc.elements {
|
||||
isPreviousNodeInline = child.write(bf, isPreviousNodeInline)
|
||||
}
|
||||
return bf.buffer.Bytes()
|
||||
}
|
||||
|
||||
// append appends an element to the htmlDocument.
|
||||
func (htmlDoc *htmlDocument) append(e element) {
|
||||
htmlDoc.elements = append(htmlDoc.elements, e)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// package gohtml
|
||||
// @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 gohtml
|
||||
|
||||
import (
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// parse parses a stirng and converts it into an html.
|
||||
func parse(r io.Reader) *htmlDocument {
|
||||
htmlDoc := &htmlDocument{}
|
||||
tokenizer := html.NewTokenizer(r)
|
||||
for {
|
||||
if errorToken, _, _ := parseToken(tokenizer, htmlDoc, nil); errorToken {
|
||||
break
|
||||
}
|
||||
}
|
||||
return htmlDoc
|
||||
}
|
||||
|
||||
// Function that identifies which tags will be treated as containing preformatted
|
||||
// content. Such tags will have the formatting of all its contents preserved
|
||||
// unchanged.
|
||||
// The opening tag html.Token is passed to this function.
|
||||
// By default, only <pre> and <textarea> tags are considered preformatted.
|
||||
var IsPreformatted = func(token html.Token) bool {
|
||||
return token.Data == "pre" || token.Data == "textarea"
|
||||
}
|
||||
|
||||
func parseToken(tokenizer *html.Tokenizer, htmlDoc *htmlDocument, parent *tagElement) (bool, bool, string) {
|
||||
tokenType := tokenizer.Next()
|
||||
raw := string(tokenizer.Raw())
|
||||
switch tokenType {
|
||||
case html.ErrorToken:
|
||||
return true, false, ""
|
||||
case html.TextToken:
|
||||
text := string(tokenizer.Raw())
|
||||
if strings.TrimSpace(text) == "" && (parent == nil || !parent.isRaw) {
|
||||
break
|
||||
}
|
||||
textElement := &textElement{text: text, parent: parent}
|
||||
appendElement(htmlDoc, parent, textElement)
|
||||
case html.StartTagToken:
|
||||
raw := string(tokenizer.Raw())
|
||||
token := tokenizer.Token()
|
||||
tagElement := &tagElement{
|
||||
tagName: string(token.Data),
|
||||
startTagRaw: raw,
|
||||
isRaw: IsPreformatted(token) || (parent != nil && parent.isRaw),
|
||||
parent: parent,
|
||||
}
|
||||
appendElement(htmlDoc, parent, tagElement)
|
||||
for {
|
||||
errorToken, parentEnded, unsetEndTag := parseToken(tokenizer, htmlDoc, tagElement)
|
||||
if errorToken {
|
||||
return true, false, ""
|
||||
}
|
||||
if parentEnded {
|
||||
if unsetEndTag != "" {
|
||||
return false, false, unsetEndTag
|
||||
}
|
||||
break
|
||||
}
|
||||
if unsetEndTag != "" {
|
||||
tagName := setEndTagRaw(tokenizer, tagElement, unsetEndTag)
|
||||
return false, false, tagName
|
||||
}
|
||||
}
|
||||
case html.EndTagToken:
|
||||
tagName := setEndTagRaw(tokenizer, parent, getTagName(tokenizer))
|
||||
return false, true, tagName
|
||||
case html.DoctypeToken, html.CommentToken:
|
||||
tagElement := &tagElement{
|
||||
tagName: getTagName(tokenizer),
|
||||
startTagRaw: string(tokenizer.Raw()),
|
||||
isRaw: parent != nil && parent.isRaw,
|
||||
parent: parent,
|
||||
}
|
||||
appendElement(htmlDoc, parent, tagElement)
|
||||
case html.SelfClosingTagToken:
|
||||
tagElement := &tagElement{
|
||||
tagName: getTagName(tokenizer),
|
||||
startTagRaw: raw,
|
||||
isRaw: parent != nil && parent.isRaw,
|
||||
parent: parent,
|
||||
}
|
||||
appendElement(htmlDoc, parent, tagElement)
|
||||
}
|
||||
return false, false, ""
|
||||
}
|
||||
|
||||
// appendElement appends the element to the htmlDocument or parent tagElement.
|
||||
func appendElement(htmlDoc *htmlDocument, parent *tagElement, e element) {
|
||||
if parent != nil {
|
||||
parent.appendChild(e)
|
||||
} else {
|
||||
htmlDoc.append(e)
|
||||
}
|
||||
}
|
||||
|
||||
// getTagName gets a tagName from tokenizer.
|
||||
func getTagName(tokenizer *html.Tokenizer) string {
|
||||
tagName, _ := tokenizer.TagName()
|
||||
return string(tagName)
|
||||
}
|
||||
|
||||
// setEndTagRaw sets an endTagRaw to the parent.
|
||||
func setEndTagRaw(_ *html.Tokenizer, parent *tagElement, tagName string) string {
|
||||
if parent != nil && parent.tagName == tagName {
|
||||
parent.endTagRaw = `</` + fMustCompile(parent.startTagRaw) + `>` // string(tokenizer.Raw())
|
||||
return ""
|
||||
}
|
||||
return tagName
|
||||
}
|
||||
|
||||
func fMustCompile(input string) (result string) {
|
||||
re := regexp.MustCompile(`<([A-Za-z-]+)[\s\S]*?>`)
|
||||
match := re.FindStringSubmatch(input)
|
||||
|
||||
if len(match) > 1 {
|
||||
result = match[1]
|
||||
} else {
|
||||
result = input
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// package gohtml
|
||||
// @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 gohtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// A tagElement represents a tag element of an HTML document.
|
||||
type tagElement struct {
|
||||
tagName string
|
||||
startTagRaw string
|
||||
endTagRaw string
|
||||
|
||||
parent *tagElement
|
||||
children []element
|
||||
|
||||
isRaw bool
|
||||
isChildrenInlineCache *bool
|
||||
}
|
||||
|
||||
// Enable condensing a tag with only inline children onto a single line, or
|
||||
// completely inlining it with sibling nodes.
|
||||
// Tags to be treated as inline can be set in `InlineTags`.
|
||||
// Only inline tags will be completely inlined, while other condensable tags
|
||||
// will be given their own dedicated (single) line.
|
||||
var Condense bool
|
||||
|
||||
// Tags that are considered inline tags.
|
||||
// Note: Text nodes are always considered to be inline
|
||||
var InlineTags = map[string]bool{
|
||||
"a": true,
|
||||
"code": true,
|
||||
"em": true,
|
||||
"span": true,
|
||||
"strong": true,
|
||||
}
|
||||
|
||||
// Maximum length of an opening inline tag before it's un-inlined
|
||||
var InlineTagMaxLength = 40
|
||||
|
||||
func (e *tagElement) isInline() bool {
|
||||
if e.isRaw || !InlineTags[e.tagName] || len(e.startTagRaw) > InlineTagMaxLength {
|
||||
return false
|
||||
}
|
||||
return e.isChildrenInline()
|
||||
}
|
||||
|
||||
func (e *tagElement) isChildrenInline() bool {
|
||||
if !Condense {
|
||||
return false
|
||||
}
|
||||
if e.isChildrenInlineCache != nil {
|
||||
return *e.isChildrenInlineCache
|
||||
}
|
||||
|
||||
isInline := true
|
||||
for _, child := range e.children {
|
||||
isInline = isInline && child.isInline()
|
||||
}
|
||||
|
||||
e.isChildrenInlineCache = &isInline
|
||||
return isInline
|
||||
}
|
||||
|
||||
// write writes a tag to the buffer.
|
||||
func (e *tagElement) write(bf *formattedBuffer, isPreviousNodeInline bool) bool {
|
||||
if e.isRaw {
|
||||
if e.parent != nil && !e.parent.isRaw {
|
||||
bf.writeLineFeed()
|
||||
bf.writeIndent()
|
||||
bf.rawMode = true
|
||||
defer func() {
|
||||
bf.rawMode = false
|
||||
}()
|
||||
}
|
||||
bf.writeToken(e.startTagRaw, formatterTokenType_Tag)
|
||||
for _, child := range e.children {
|
||||
child.write(bf, true)
|
||||
}
|
||||
bf.writeToken(e.endTagRaw, formatterTokenType_Tag)
|
||||
return false
|
||||
}
|
||||
|
||||
if e.isChildrenInline() && (e.endTagRaw != "" || e.isInline()) {
|
||||
// Write the condensed output to a separate buffer, in case it doesn't work out
|
||||
condensedBuffer := *bf
|
||||
condensedBuffer.buffer = &bytes.Buffer{}
|
||||
|
||||
if bf.buffer.Len() > 0 && (!isPreviousNodeInline || !e.isInline()) {
|
||||
condensedBuffer.writeLineFeed()
|
||||
}
|
||||
condensedBuffer.writeToken(e.startTagRaw, formatterTokenType_Tag)
|
||||
if !isPreviousNodeInline && e.endTagRaw != "" {
|
||||
condensedBuffer.indentLevel++
|
||||
}
|
||||
|
||||
for _, child := range e.children {
|
||||
child.write(&condensedBuffer, true)
|
||||
}
|
||||
if e.endTagRaw != "" {
|
||||
condensedBuffer.writeToken(e.endTagRaw, formatterTokenType_Tag)
|
||||
if !isPreviousNodeInline {
|
||||
condensedBuffer.indentLevel--
|
||||
}
|
||||
}
|
||||
|
||||
if e.isInline() || bytes.ContainsAny(condensedBuffer.buffer.Bytes()[1:], "\n") {
|
||||
// If we're an inline tag, or there were no newlines were in the buffer,
|
||||
// replace the original with the condensed version
|
||||
condensedBuffer.buffer = bytes.NewBuffer(bytes.Join([][]byte{
|
||||
bf.buffer.Bytes(), condensedBuffer.buffer.Bytes(),
|
||||
}, []byte{}))
|
||||
*bf = condensedBuffer
|
||||
|
||||
return e.isInline()
|
||||
}
|
||||
}
|
||||
|
||||
if bf.buffer.Len() > 0 {
|
||||
bf.writeLineFeed()
|
||||
}
|
||||
bf.writeToken(e.startTagRaw, formatterTokenType_Tag)
|
||||
if e.endTagRaw != "" {
|
||||
bf.indentLevel++
|
||||
}
|
||||
|
||||
isPreviousNodeInline = false
|
||||
for _, child := range e.children {
|
||||
isPreviousNodeInline = child.write(bf, isPreviousNodeInline)
|
||||
}
|
||||
|
||||
if e.endTagRaw != "" {
|
||||
if len(e.children) > 0 {
|
||||
bf.writeLineFeed()
|
||||
}
|
||||
bf.indentLevel--
|
||||
bf.writeToken(e.endTagRaw, formatterTokenType_Tag)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// appendChild append an element to the element's children.
|
||||
func (e *tagElement) appendChild(child element) {
|
||||
e.children = append(e.children, child)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// package gohtml
|
||||
// @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 gohtml
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A textElement represents a text element of an HTML document.
|
||||
type textElement struct {
|
||||
text string
|
||||
parent *tagElement
|
||||
}
|
||||
|
||||
func (e *textElement) isInline() bool {
|
||||
// Text nodes are always considered to be inline
|
||||
return true
|
||||
}
|
||||
|
||||
// write writes a text to the buffer.
|
||||
func (e *textElement) write(bf *formattedBuffer, isPreviousNodeInline bool) bool {
|
||||
text := unifyLineFeed(e.text)
|
||||
if e.parent != nil && e.parent.isRaw {
|
||||
bf.writeToken(text, formatterTokenType_Text)
|
||||
return true
|
||||
}
|
||||
|
||||
if !isPreviousNodeInline {
|
||||
bf.writeLineFeed()
|
||||
}
|
||||
|
||||
// Collapse leading and trailing spaces
|
||||
text = regexp.MustCompile(`^\s+|\s+$`).ReplaceAllString(text, " ")
|
||||
lines := strings.Split(text, "\n")
|
||||
for l, line := range lines {
|
||||
if l > 0 {
|
||||
bf.writeLineFeed()
|
||||
}
|
||||
for _, word := range strings.Split(line, " ") {
|
||||
bf.writeToken(word, formatterTokenType_Text)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// package gohtml
|
||||
// @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 gohtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type formatterTokenType int
|
||||
|
||||
const (
|
||||
formatterTokenType_Nothing formatterTokenType = iota
|
||||
formatterTokenType_Tag
|
||||
formatterTokenType_Text
|
||||
)
|
||||
|
||||
type formattedBuffer struct {
|
||||
buffer *bytes.Buffer
|
||||
rawMode bool
|
||||
|
||||
indentString string
|
||||
indentLevel int
|
||||
|
||||
lineWrapColumn int
|
||||
lineWrapMaxSpillover int
|
||||
|
||||
curLineLength int
|
||||
prevTokenType formatterTokenType
|
||||
}
|
||||
|
||||
func (bf *formattedBuffer) writeLineFeed() {
|
||||
if !bf.rawMode {
|
||||
// Strip trailing newlines
|
||||
bf.buffer = bytes.NewBuffer(bytes.TrimRightFunc(
|
||||
bf.buffer.Bytes(),
|
||||
func(r rune) bool {
|
||||
return r != '\n' && unicode.IsSpace(r)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
bf.buffer.WriteString("\n")
|
||||
bf.curLineLength = 0
|
||||
bf.prevTokenType = formatterTokenType_Nothing
|
||||
}
|
||||
|
||||
func (bf *formattedBuffer) writeIndent() {
|
||||
bf.buffer.WriteString(strings.Repeat(bf.indentString, bf.indentLevel))
|
||||
bf.curLineLength += len(bf.indentString) * bf.indentLevel
|
||||
}
|
||||
|
||||
func (bf *formattedBuffer) writeToken(token string, kind formatterTokenType) {
|
||||
if bf.rawMode {
|
||||
bf.buffer.WriteString(token)
|
||||
bf.curLineLength += len(token)
|
||||
return
|
||||
}
|
||||
|
||||
if bf.prevTokenType == formatterTokenType_Nothing && strings.TrimSpace(token) == "" {
|
||||
// It's a whitespace token, but we already have indentation which functions
|
||||
// the same, so we ignore it
|
||||
return
|
||||
}
|
||||
|
||||
toWrite := token
|
||||
if kind == formatterTokenType_Text && bf.prevTokenType == formatterTokenType_Text {
|
||||
toWrite = " " + token
|
||||
}
|
||||
|
||||
if bf.prevTokenType != formatterTokenType_Nothing && bf.lineWrapColumn > 0 {
|
||||
switch {
|
||||
case bf.curLineLength > bf.lineWrapColumn:
|
||||
// Current line is too long
|
||||
fallthrough
|
||||
|
||||
case bf.curLineLength+len(toWrite) > bf.lineWrapColumn+bf.lineWrapMaxSpillover:
|
||||
// Current line + new token is too long even with allowed spillover
|
||||
fallthrough
|
||||
|
||||
case bf.curLineLength+len(toWrite) > bf.lineWrapColumn &&
|
||||
bf.curLineLength > bf.lineWrapColumn-bf.lineWrapMaxSpillover:
|
||||
// Current line + new token is too long and doesn't quality for spillover
|
||||
|
||||
bf.writeLineFeed()
|
||||
bf.writeToken(token, kind)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if bf.curLineLength == 0 {
|
||||
bf.writeIndent()
|
||||
}
|
||||
bf.buffer.WriteString(toWrite)
|
||||
bf.curLineLength += len(toWrite)
|
||||
bf.prevTokenType = kind
|
||||
}
|
||||
|
||||
// unifyLineFeed unifies line feeds.
|
||||
func unifyLineFeed(s string) string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", "\n"), "\r", "\n")
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// package gohtml
|
||||
// @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 gohtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// A Writer represents a formatted HTML source codes writer.
|
||||
type Writer struct {
|
||||
writer io.Writer
|
||||
lastElement string
|
||||
bf *bytes.Buffer
|
||||
}
|
||||
|
||||
// SetLastElement set the lastElement to the Writer.
|
||||
func (wr *Writer) SetLastElement(lastElement string) *Writer {
|
||||
wr.lastElement = lastElement
|
||||
return wr
|
||||
}
|
||||
|
||||
// Write writes the parameter.
|
||||
func (wr *Writer) Write(p []byte) (n int, err error) {
|
||||
n, _ = wr.bf.Write(p) // (*bytes.Buffer).Write never produces an error
|
||||
if bytes.HasSuffix(p, []byte(wr.lastElement)) {
|
||||
_, err = wr.writer.Write([]byte(Format(wr.bf.String()) + "\n"))
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// NewWriter generates a Writer and returns it.
|
||||
func NewWriter(wr io.Writer) *Writer {
|
||||
return &Writer{writer: wr, lastElement: defaultLastElement, bf: &bytes.Buffer{}}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
// package xgen
|
||||
// @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 xgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/addons"
|
||||
genconsts "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_consts"
|
||||
gendao "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_dao"
|
||||
genmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_model"
|
||||
genview "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_view"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/request"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"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/gproc"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
const gfCliVersion = "v2.9.6"
|
||||
|
||||
func gfCliCommand(args ...string) string {
|
||||
return "go run github.com/gogf/gf/cmd/gf/v2@" + gfCliVersion + " " + strings.Join(args, " ")
|
||||
}
|
||||
|
||||
func GenTypeSelects(ctx context.Context) (output []*genmodel.SelectGenTypeModel, err error) {
|
||||
output = make([]*genmodel.SelectGenTypeModel, 0)
|
||||
for k, v := range genconsts.GenCodesTypeNameMap {
|
||||
item := &genmodel.SelectGenTypeModel{
|
||||
SelectItemModel: genmodel.SelectItemModel{
|
||||
Value: k,
|
||||
Name: v,
|
||||
Label: v,
|
||||
Disabled: false,
|
||||
},
|
||||
Templates: make([]*genmodel.SelectGenTemplateModel, 0),
|
||||
}
|
||||
confName, ok := genconsts.GenCodesTypeConfMap[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
confTemplates := make([]*genmodel.GenCodesConfigCrudTemplate, 0)
|
||||
err = g.Cfg().MustGet(ctx, "xgen.application."+confName+".templates").Scan(&confTemplates)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(confTemplates) == 0 && confName != "crud" {
|
||||
err = g.Cfg().MustGet(ctx, "xgen.application.crud.templates").Scan(&confTemplates)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for index, v := range confTemplates {
|
||||
item.Templates = append(item.Templates, &genmodel.SelectGenTemplateModel{
|
||||
SelectItemModel: genmodel.SelectItemModel{
|
||||
Value: index,
|
||||
Label: v.Group,
|
||||
Name: v.Group,
|
||||
Disabled: false,
|
||||
},
|
||||
IsAddon: v.IsAddon,
|
||||
})
|
||||
}
|
||||
sort.Slice(item.Templates, func(i, j int) bool {
|
||||
return item.Templates[i].Value.(int) < item.Templates[j].Value.(int)
|
||||
})
|
||||
|
||||
output = append(output, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
func DbSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
output = make([]*genmodel.SelectItemModel, 0)
|
||||
dbs := g.Cfg().MustGet(ctx, "xgen.selectDbs")
|
||||
if len(dbs.Strings()) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range dbs.Strings() {
|
||||
output = append(output, &genmodel.SelectItemModel{
|
||||
Value: v,
|
||||
Label: v,
|
||||
Name: v,
|
||||
Disabled: false,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func StatusSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
output = make([]*genmodel.SelectItemModel, 0)
|
||||
for k, v := range genconsts.GenCodesStatusNameMap {
|
||||
output = append(output, &genmodel.SelectItemModel{
|
||||
Value: k,
|
||||
Label: v,
|
||||
Name: v,
|
||||
Disabled: false,
|
||||
})
|
||||
}
|
||||
sort.Slice(output, func(i, j int) bool {
|
||||
return output[i].Value.(string) < output[j].Value.(string)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func LinkModeSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
output = make([]*genmodel.SelectItemModel, 0)
|
||||
for k, v := range genconsts.GenCodesJoinNameMap {
|
||||
output = append(output, &genmodel.SelectItemModel{
|
||||
Value: k,
|
||||
Label: v,
|
||||
Name: v,
|
||||
Disabled: false,
|
||||
})
|
||||
}
|
||||
sort.Slice(output, func(i, j int) bool {
|
||||
return output[i].Value.(int) < output[j].Value.(int)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func BuildMethodSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
output = make([]*genmodel.SelectItemModel, 0)
|
||||
for k, v := range genconsts.GenCodesBuildMethNameMap {
|
||||
output = append(output, &genmodel.SelectItemModel{
|
||||
Value: k,
|
||||
Label: v,
|
||||
Name: v,
|
||||
Disabled: false,
|
||||
})
|
||||
}
|
||||
sort.Slice(output, func(i, j int) bool {
|
||||
return output[i].Value.(int) < output[j].Value.(int)
|
||||
})
|
||||
return
|
||||
}
|
||||
func FormModeSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
output = make([]*genmodel.SelectItemModel, 0)
|
||||
for _, v := range genview.FMs {
|
||||
output = append(output, &genmodel.SelectItemModel{
|
||||
Value: v,
|
||||
Label: genview.FMMap[v],
|
||||
Name: genview.FMMap[v],
|
||||
Disabled: false,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
func FormRoleSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
output = make([]*genmodel.SelectItemModel, 0)
|
||||
for k, v := range genview.FRMap {
|
||||
output = append(output, &genmodel.SelectItemModel{
|
||||
Value: k,
|
||||
Label: v,
|
||||
Name: v,
|
||||
Disabled: false,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func DictModeSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
dictMode, _, err := service.SysDictType().List(ctx, &model.SysDictTypeListParam{
|
||||
PageInfo: request.PageInfo{
|
||||
Page: 1,
|
||||
PageSize: 2000,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, v := range dictMode {
|
||||
output = append(output, &genmodel.SelectItemModel{
|
||||
Value: v.DictType,
|
||||
Label: v.DictName + "(" + v.DictType + ")",
|
||||
Name: v.DictName,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func WhereModeSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
output = make([]*genmodel.SelectItemModel, 0)
|
||||
for _, v := range genview.WMs {
|
||||
output = append(output, &genmodel.SelectItemModel{
|
||||
Value: v,
|
||||
Label: v,
|
||||
Name: v,
|
||||
Disabled: false,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TableAlignSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
output = make([]*genmodel.SelectItemModel, 0)
|
||||
for _, v := range genview.TableAligns {
|
||||
output = append(output, &genmodel.SelectItemModel{
|
||||
Value: v,
|
||||
Label: genview.TableAlignMap[v],
|
||||
Name: genview.TableAlignMap[v],
|
||||
Disabled: false,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TreeStyleTypeSelects(ctx context.Context) (output []*genmodel.SelectItemModel, err error) {
|
||||
output = make([]*genmodel.SelectItemModel, 0)
|
||||
for k, v := range genconsts.GenCodesTreeStyleTypeNameMap {
|
||||
output = append(output, &genmodel.SelectItemModel{
|
||||
Value: k,
|
||||
Label: v,
|
||||
Name: v,
|
||||
Disabled: false,
|
||||
})
|
||||
}
|
||||
sort.Slice(output, func(i, j int) bool {
|
||||
return output[i].Value.(int) < output[j].Value.(int)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func GenCodesSelects(ctx context.Context) (output *genmodel.SelectsModel, err error) {
|
||||
output = &genmodel.SelectsModel{}
|
||||
output.GenType, err = GenTypeSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.Db, err = DbSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.Status, err = StatusSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.LinkMode, err = LinkModeSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.BuildMethod, err = BuildMethodSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.FormMode, err = FormModeSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.WhereMode, err = WhereModeSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.FormRole, err = FormRoleSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.DictMode, err = DictModeSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.Addons, err = addons.ModuleSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.TableAlign, err = TableAlignSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
output.TreeStyleType, err = TreeStyleTypeSelects(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GenCodesTableSelect(ctx context.Context, params *genmodel.GenCodesTableSelectParam) (output []*genmodel.GenCodesTableSelectModel, err error) {
|
||||
if params == nil || params.DbGroup == "" {
|
||||
return
|
||||
}
|
||||
sql := "SELECT TABLE_NAME as value, TABLE_COMMENT as label FROM information_schema.`TABLES` WHERE TABLE_SCHEMA = ?"
|
||||
config := g.DB(params.DbGroup).GetConfig()
|
||||
disableTables := g.Cfg().MustGet(ctx, "xgen.disableTables").Strings()
|
||||
|
||||
lists := make([]*genmodel.GenCodesTableSelectModel, 0)
|
||||
err = g.DB(params.DbGroup).Ctx(ctx).Raw(sql, config.Name).Scan(&lists)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
patternStr := `addon_(\w+)_`
|
||||
repStr := ``
|
||||
for _, v := range lists {
|
||||
if gstr.InArray(disableTables, v.Value) {
|
||||
continue
|
||||
}
|
||||
newValue := v.Value
|
||||
if config.Prefix != "" {
|
||||
newValue = gstr.SubStrFromEx(v.Value, config.Prefix)
|
||||
}
|
||||
// g.Log().Infof(ctx, "server/internal/library/xgen/xgen_curd.go GenCodesTableSelect newValue:%v config.Prefix:%v", newValue, config.Prefix)
|
||||
if newValue == "" {
|
||||
continue
|
||||
}
|
||||
// 插件移除掉插件表前缀
|
||||
bt, err := gregex.Replace(patternStr, []byte(repStr), []byte(newValue))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// g.Log().Infof(ctx, "server/internal/library/xgen/xgen_curd.go GenCodesTableSelect newValue:%v string(bt):%v", newValue, string(bt))
|
||||
|
||||
row := v
|
||||
row.DefTableComment = v.Label
|
||||
row.DaoName = gstr.CaseCamel(newValue)
|
||||
row.DefVarName = gstr.CaseCamel(string(bt))
|
||||
row.DefAlias = gstr.CaseCamelLower(newValue)
|
||||
row.Name = fmt.Sprintf("%s (%s)", v.Value, v.Label)
|
||||
row.Label = row.Name
|
||||
output = append(output, row)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GenCodesColumnSelect(ctx context.Context, params *genmodel.GenCodesColumnSelectParam) (output []*genmodel.GenCodesColumnSelectModel, err error) {
|
||||
sql := "SELECT COLUMN_NAME as value, COLUMN_COMMENT as label FROM information_schema.`COLUMNS` WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?"
|
||||
dbConfig := g.DB(params.DbGroup).GetConfig()
|
||||
|
||||
lists := make([]*genmodel.GenCodesColumnSelectModel, 0)
|
||||
err = g.DB(params.DbGroup).Ctx(ctx).Raw(sql, dbConfig.Name, params.TableName).Scan(&lists)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(lists) == 0 {
|
||||
err = gerror.New("表不存在")
|
||||
return
|
||||
}
|
||||
for _, v := range lists {
|
||||
output = append(output, &genmodel.GenCodesColumnSelectModel{
|
||||
Value: v.Value,
|
||||
Label: v.Label,
|
||||
Name: fmt.Sprintf("%s (%s)", v.Value, v.Label),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GenCodesColumnList(ctx context.Context, params *genmodel.GenCodesColumnListParam) (output []*genmodel.GenCodesColumnListModel, err error) {
|
||||
output, err = genview.TableColumnList(ctx, params)
|
||||
return
|
||||
}
|
||||
|
||||
// GetLoadGenerate 获取本地生成配置
|
||||
func GetLoadGenerate(ctx context.Context) (conf *genmodel.GenCodesConfig, err error) {
|
||||
err = g.Cfg().MustGet(ctx, "xgen").Scan(&conf)
|
||||
return
|
||||
}
|
||||
|
||||
func GenCodesPreview(ctx context.Context, params *genmodel.GenCodesPreviewParam) (output *genmodel.GenCodesPreviewModel, err error) {
|
||||
|
||||
genConfig, err := GetLoadGenerate(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
daoConfig, err := gendao.GetDaoConfig(ctx, params.DbName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch params.GenType {
|
||||
case genconsts.GenCodesTypeCurd, genconsts.GenCodesTypeTree:
|
||||
return genview.Curd.DoPreview(ctx, &genmodel.CurdPreviewParam{
|
||||
In: params,
|
||||
DaoConfig: daoConfig,
|
||||
Config: genConfig,
|
||||
})
|
||||
default:
|
||||
err = gerror.Newf("生成类型暂不支持!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Build 提交生成
|
||||
func GenCodesBuild(ctx context.Context, params *genmodel.GenCodesBuildParam) (output *genmodel.GenCodesBuildModel, err error) {
|
||||
output = &genmodel.GenCodesBuildModel{}
|
||||
genConfig, err := GetLoadGenerate(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
daoConfig, err := gendao.GetDaoConfig(ctx, params.DbName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch params.GenType {
|
||||
case genconsts.GenCodesTypeCurd, genconsts.GenCodesTypeTree:
|
||||
pin := params
|
||||
|
||||
return output, genview.Curd.DoBuild(ctx, &genmodel.CurdBuildParam{
|
||||
PreviewIn: &genmodel.CurdPreviewParam{
|
||||
In: pin,
|
||||
DaoConfig: daoConfig,
|
||||
Config: genConfig,
|
||||
},
|
||||
BeforeEvent: genmodel.CurdBuildEvent{"runDao": Dao},
|
||||
AfterEvent: genmodel.CurdBuildEvent{"runService": func(ctx context.Context) (err error) {
|
||||
cfg := gendao.GetServiceConfig()
|
||||
|
||||
// 插件模块,切换到插件下运行gen service
|
||||
if genConfig.Application.Crud.Templates[pin.GenTemplate].IsAddon {
|
||||
// 依然使用配置中的参数,只是将生成路径指向插件模块路径
|
||||
cfg.SrcFolder = "addons/" + pin.AddonName + "/logic"
|
||||
cfg.DstFolder = "addons/" + pin.AddonName + "/service"
|
||||
}
|
||||
err = ServiceWithCfg(ctx, cfg)
|
||||
return
|
||||
}},
|
||||
})
|
||||
default:
|
||||
err = gerror.Newf("生成类型暂不支持!")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceWithCfg 生成业务接口
|
||||
func ServiceWithCfg(ctx context.Context, cfg ...*genmodel.GenServiceConfig) (err error) {
|
||||
c := gendao.GetServiceConfig()
|
||||
if len(cfg) > 0 {
|
||||
c = cfg[0]
|
||||
}
|
||||
cmd := gfCliCommand("gen", "service")
|
||||
|
||||
if c.SrcFolder != "" {
|
||||
cmd += ` -s ` + c.SrcFolder
|
||||
}
|
||||
if c.DstFolder != "" {
|
||||
cmd += ` -d ` + c.DstFolder
|
||||
}
|
||||
if c.DstFileNameCase != "" {
|
||||
cmd += ` -f ` + c.DstFileNameCase
|
||||
}
|
||||
if c.WatchFile != "" {
|
||||
cmd += ` -w ` + c.WatchFile
|
||||
}
|
||||
if c.StPattern != "" {
|
||||
cmd += ` -a ` + c.StPattern
|
||||
}
|
||||
// Do not pass "-l" here. GoFrame v2.9.6 can mis-handle relative paths
|
||||
// on Windows and remove the generated service files it just created.
|
||||
if c.Clear {
|
||||
g.Log().Warning(ctx, "gf gen service clear is disabled on Windows to avoid deleting generated files")
|
||||
}
|
||||
if c.ImportPrefix != "" {
|
||||
cmd += ` -i ` + c.ImportPrefix
|
||||
}
|
||||
if c.Packages != nil {
|
||||
cmd += ` -p ` + strings.Join(c.Packages, ",")
|
||||
}
|
||||
r, err := gproc.ShellExec(gctx.New(), cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
g.Log().Info(ctx, "生成业务接口 Stdout", r)
|
||||
return
|
||||
}
|
||||
|
||||
// Dao 生成数据库实体
|
||||
func Dao(ctx context.Context) (err error) {
|
||||
cmd := gfCliCommand("gen", "dao")
|
||||
r, err := gproc.ShellExec(gctx.New(), cmd)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "生成数据库实体 Stderr", r)
|
||||
return err
|
||||
}
|
||||
g.Log().Info(ctx, "生成数据库实体 Stdout", r)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// package xgen
|
||||
// @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 xgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
gendao "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgen/gen_dao"
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) (err error) {
|
||||
err = gendao.Init(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// package xgorm
|
||||
// @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 xgorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
"slices"
|
||||
|
||||
"reflect"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
var (
|
||||
descTags = []string{"description", "dc", "json"} // 实体描述标签
|
||||
fieldTags = []string{"json"} // 实体字段名称映射
|
||||
)
|
||||
|
||||
// GetFieldsToSlice 获取dao实例中的所有字段
|
||||
func GetFieldsToSlice(ctx context.Context, dao daoInstance) ([]string, error) {
|
||||
fields, err := dao.Ctx(ctx).TableFields(dao.Table())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return nil, gerror.New("field not found")
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for _, field := range fields {
|
||||
keys = append(keys, field.Name)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// GetPkField 获取dao实例中的主键名称
|
||||
func GetPkField(ctx context.Context, dao daoInstance) (string, error) {
|
||||
fields, err := dao.Ctx(ctx).TableFields(dao.Table())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return "", gerror.New("field not found")
|
||||
}
|
||||
|
||||
for _, field := range fields {
|
||||
if field.Key == "PRI" {
|
||||
return field.Name, nil
|
||||
}
|
||||
}
|
||||
return "", gerror.New("no primary key")
|
||||
}
|
||||
|
||||
// IsUnique 是否唯一
|
||||
func IsUnique(ctx context.Context, dao daoInstance, where g.Map, message string, pkId ...interface{}) error {
|
||||
if len(where) == 0 {
|
||||
return gerror.New("where condition cannot be empty")
|
||||
}
|
||||
|
||||
m := dao.Ctx(ctx).Where(where)
|
||||
if len(pkId) > 0 {
|
||||
field, err := GetPkField(ctx, dao)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m = m.WhereNot(field, pkId[0])
|
||||
}
|
||||
|
||||
count, err := m.Count(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
if message == "" {
|
||||
for k := range where {
|
||||
message = fmt.Sprintf("in the table:%s, %v not uniqued", dao.Table(), where[k])
|
||||
break
|
||||
}
|
||||
}
|
||||
return gerror.New(message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func JoinFields(ctx context.Context, entity interface{}, dao daoInstance, as string) (fs string) {
|
||||
entityFs, err := GetEntityFieldTags(entity)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "JoinFields err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(entityFs) == 0 {
|
||||
g.Log().Errorf(ctx, "JoinFields entityFs is empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
fields, err := dao.Ctx(ctx).TableFields(dao.Table())
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "JoinFields err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var columns []string
|
||||
for _, v := range entityFs {
|
||||
if !gstr.HasPrefix(v, as) {
|
||||
g.Log().Infof(ctx, "JoinFields gstr.HasPrefix: %+v, %+v", v, as)
|
||||
continue
|
||||
}
|
||||
g.Log().Infof(ctx, "JoinFields gstr.HasPrefix2: %+v, %+v", v, as)
|
||||
field := gstr.CaseSnakeFirstUpper(gstr.StrEx(v, as))
|
||||
if _, ok := fields[field]; ok {
|
||||
columns = append(columns, fmt.Sprintf("`%s`.`%s` as `%s`", dao.Table(), field, v))
|
||||
}
|
||||
g.Log().Infof(ctx, "JoinFields field: %+v fields: %+v", field, fields)
|
||||
}
|
||||
|
||||
if len(columns) > 0 {
|
||||
g.Log().Infof(ctx, "JoinFields columns: %+v", columns)
|
||||
return gstr.Implode(",", utility.UniqueSlice(columns))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetEntityFieldTags 获取实体中的字段名称
|
||||
func GetEntityFieldTags(entity interface{}) (tags []string, err error) {
|
||||
var formRef = reflect.TypeOf(entity)
|
||||
for i := 0; i < formRef.NumField(); i++ {
|
||||
field := formRef.Field(i)
|
||||
if field.Type.Kind() == reflect.Struct {
|
||||
addTags, err := reflectTag(field.Type, fieldTags, []string{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, addTags...)
|
||||
continue
|
||||
}
|
||||
tags = append(tags, reflectTagName(field, fieldTags, true))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// reflectTag 层级递增解析tag
|
||||
func reflectTag(reflectType reflect.Type, filterTags []string, tags []string) ([]string, error) {
|
||||
if reflectType.Kind() == reflect.Ptr {
|
||||
return nil, gerror.Newf("reflect type do not support reflect.Ptr yet, reflectType:%+v", reflectType)
|
||||
}
|
||||
if reflectType.Kind() != reflect.Struct {
|
||||
return nil, nil
|
||||
}
|
||||
for i := 0; i < reflectType.NumField(); i++ {
|
||||
tag := reflectTagName(reflectType.Field(i), filterTags, false)
|
||||
if tag == "" {
|
||||
addTags, err := reflectTag(reflectType.Field(i).Type, filterTags, tags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, addTags...)
|
||||
continue
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// reflectTagName 解析实体中的描述标签,优先级:description > dc > json > Name
|
||||
func reflectTagName(field reflect.StructField, filterTags []string, isDef bool) string {
|
||||
if slices.Contains(filterTags, "description") {
|
||||
if description, ok := field.Tag.Lookup("description"); ok && description != "" {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
if slices.Contains(filterTags, "dc") {
|
||||
if dc, ok := field.Tag.Lookup("dc"); ok && dc != "" {
|
||||
return dc
|
||||
}
|
||||
}
|
||||
|
||||
if slices.Contains(filterTags, "json") {
|
||||
if jsonName, ok := field.Tag.Lookup("json"); ok && jsonName != "" {
|
||||
return jsonName
|
||||
}
|
||||
}
|
||||
|
||||
if !isDef {
|
||||
return ""
|
||||
}
|
||||
return field.Name
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// package xgorm
|
||||
// @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 xgorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
gormmodel "git.5iot.com/iotlcm-public/iotlcm-app/internal/library/xgorm/gorm_model"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/tree"
|
||||
"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/util/gconv"
|
||||
)
|
||||
|
||||
const (
|
||||
idDefaulField = "id"
|
||||
pidDefaulField = "pid"
|
||||
levelDefaulField = "level"
|
||||
treeDefaulField = "tree"
|
||||
)
|
||||
|
||||
type TreeFiledOption struct {
|
||||
IdField string
|
||||
PidField string
|
||||
LevelField string
|
||||
TreeField string
|
||||
}
|
||||
|
||||
type daoInstance interface {
|
||||
Table() string
|
||||
Ctx(ctx context.Context) *gdb.Model
|
||||
}
|
||||
|
||||
func CustomTreeFiledToDefault(old map[string]interface{}, option *TreeFiledOption) *gormmodel.DefaultTree {
|
||||
if option == nil {
|
||||
option = &TreeFiledOption{
|
||||
IdField: idDefaulField,
|
||||
PidField: pidDefaulField,
|
||||
LevelField: levelDefaulField,
|
||||
TreeField: treeDefaulField,
|
||||
}
|
||||
}
|
||||
pd := &gormmodel.DefaultTree{}
|
||||
if id, ok := old[option.IdField]; ok {
|
||||
pd.Id = gconv.Int64(id)
|
||||
}
|
||||
if pid, ok := old[option.PidField]; ok {
|
||||
pd.Pid = gconv.Int64(pid)
|
||||
}
|
||||
if level, ok := old[option.LevelField]; ok {
|
||||
pd.Level = gconv.Int(level)
|
||||
}
|
||||
if tree, ok := old[option.TreeField]; ok {
|
||||
pd.Tree = gconv.String(tree)
|
||||
}
|
||||
return pd
|
||||
}
|
||||
|
||||
// AutoUpdateTree 自动更新关系树
|
||||
func AutoUpdateTree(ctx context.Context, dao daoInstance, id, pid int64, option *TreeFiledOption) (newPid int64, newLevel int, newTree string, err error) {
|
||||
if option == nil {
|
||||
option = &TreeFiledOption{
|
||||
IdField: idDefaulField,
|
||||
PidField: pidDefaulField,
|
||||
LevelField: levelDefaulField,
|
||||
TreeField: treeDefaulField,
|
||||
}
|
||||
}
|
||||
if err = CheckTreeTable(ctx, dao, option); err != nil {
|
||||
return
|
||||
}
|
||||
if pid <= 0 {
|
||||
newPid = 0
|
||||
newLevel = 1
|
||||
newTree = ""
|
||||
} else {
|
||||
var pdMap map[string]interface{}
|
||||
row, err := dao.Ctx(ctx).Fields(option.IdField, option.PidField, option.LevelField, option.TreeField).WherePri(pid).One()
|
||||
if err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
if row != nil {
|
||||
pdMap = row.Map()
|
||||
}
|
||||
|
||||
if len(pdMap) == 0 {
|
||||
return 0, 0, "", gerror.New("未查询到树表上级信息,请检查!")
|
||||
}
|
||||
pd := CustomTreeFiledToDefault(pdMap, option)
|
||||
|
||||
if id > 0 && slices.Contains(tree.GetIds(pd.Tree), id) {
|
||||
return 0, 0, "", gerror.New("上级不能设为自己当前的子级!")
|
||||
}
|
||||
|
||||
newPid = pid
|
||||
newLevel = pd.Level + 1
|
||||
newTree = tree.GenLabel(pd.Tree, pid)
|
||||
}
|
||||
|
||||
if id > 0 {
|
||||
if pid == id {
|
||||
return 0, 0, "", gerror.New("上级不能是自己!")
|
||||
}
|
||||
|
||||
var models *gormmodel.DefaultTree
|
||||
if err = dao.Ctx(ctx).WherePri(id).Scan(&models); err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
|
||||
if models == nil {
|
||||
return 0, 0, "", gerror.New("树表信息不存在,请检查!")
|
||||
}
|
||||
// g.Log().Infof(ctx, "models: %+v, pid: %v, newPid: %v, newLevel: %v, newTree: %v", models, pid, newPid, newLevel, newTree)
|
||||
// 上级发生变化时,遍历修改其所有的下级关系树
|
||||
if models.Pid != pid {
|
||||
if err = updateChildrenTree(ctx, dao, models.Id, newLevel, newTree, option); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CheckTreeTable 检查树表
|
||||
func CheckTreeTable(ctx context.Context, dao daoInstance, option *TreeFiledOption) (err error) {
|
||||
fields, err := GetFieldsToSlice(ctx, dao)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !slices.Contains(fields, option.PidField) {
|
||||
return gerror.New("树表必须包含`pid`字段")
|
||||
}
|
||||
|
||||
if !slices.Contains(fields, option.LevelField) {
|
||||
return gerror.New("树表必须包含`level`字段")
|
||||
}
|
||||
|
||||
if !slices.Contains(fields, option.TreeField) {
|
||||
return gerror.New("树表必须包含`tree`字段")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// updateChildrenTree 更新下级关系树
|
||||
func updateChildrenTree(ctx context.Context, dao daoInstance, pid int64, pLevel int, pTree string, option *TreeFiledOption) (err error) {
|
||||
var list []*gormmodel.DefaultTree
|
||||
// var pdMaps []map[string]interface{}
|
||||
pdMaps, err := dao.Ctx(ctx).Fields(option.IdField, option.PidField, option.LevelField, option.TreeField).Where(option.PidField, pid).All()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range pdMaps {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
pb := CustomTreeFiledToDefault(v.Map(), option)
|
||||
if pb.Id == 0 {
|
||||
g.Log().Warningf(ctx, "updateChildrenTree pb.Id is 0, pb: %+v, option: %+v, v: %+v, pdMaps: %+v", pb, option, v, pdMaps)
|
||||
continue
|
||||
}
|
||||
// g.Log().Debugf(ctx, "updateChildrenTree pb: %+v", pb)
|
||||
list = append(list, pb)
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
newLevel := pLevel + 1
|
||||
newTree := tree.GenLabel(pTree, pid)
|
||||
|
||||
var updateIds []int64
|
||||
for _, v := range list {
|
||||
updateIds = append(updateIds, v.Id)
|
||||
if err = updateChildrenTree(ctx, dao, v.Id, newLevel, newTree, option); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(updateIds) > 0 {
|
||||
update := g.Map{
|
||||
option.LevelField: newLevel,
|
||||
option.TreeField: newTree,
|
||||
}
|
||||
_, err = dao.Ctx(ctx).WhereIn(option.IdField, updateIds).Data(update).Update()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// package gormmodel
|
||||
// @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 gormmodel
|
||||
|
||||
// DefaultTree 默认树表字段
|
||||
type DefaultTree struct {
|
||||
Id int64 `json:"id" description:"ID"`
|
||||
Pid int64 `json:"pid" description:"父ID"`
|
||||
Level int `json:"level" description:"关系树等级"`
|
||||
Tree string `json:"tree" description:"关系树"`
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// package handler
|
||||
// @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 handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/convert"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// FilterAuth 过滤数据权限
|
||||
// 通过上下文中的用户角色权限和表中是否含有需要过滤的字段附加查询条件
|
||||
func FilterAuth(m *gdb.Model) *gdb.Model {
|
||||
var (
|
||||
ctx = m.GetCtx()
|
||||
fields = convert.EscapeFieldsToSlice(m.GetFieldsStr())
|
||||
)
|
||||
|
||||
// 超管拥有全部权限
|
||||
if contexts.IsSuperAdmin(ctx) {
|
||||
return m
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user := contexts.GetUser(ctx)
|
||||
if user == nil {
|
||||
return m
|
||||
}
|
||||
if len(user.Roles) == 0 {
|
||||
return m
|
||||
}
|
||||
|
||||
deptIds := make([]int64, 0)
|
||||
userIds := make([]int64, 0)
|
||||
// 是否进入了非「全部数据」的受限范围;解析结果为空时必须拒绝,不能漏过滤。
|
||||
hasRestrictedScope := false
|
||||
currentRoleId := ""
|
||||
currentRoleDataScope := ""
|
||||
currentRoleIdIf := contexts.GetDataValue(ctx, "currentRoleId")
|
||||
if currentRoleIdIf != nil {
|
||||
currentRoleId = currentRoleIdIf.(string)
|
||||
}
|
||||
currentRoleDataScopeIf := contexts.GetDataValue(ctx, "currentRoleDataScope")
|
||||
if currentRoleDataScopeIf != nil {
|
||||
currentRoleDataScope = currentRoleDataScopeIf.(string)
|
||||
}
|
||||
if len(currentRoleDataScope) > 0 && len(currentRoleId) > 0 {
|
||||
dataScope := currentRoleDataScope
|
||||
roleId := gconv.Int64(currentRoleId)
|
||||
g.Log().Infof(ctx, "uid:%d, currentRoleId: %d, currentRoleDataScope: %s", user.ID, roleId, dataScope)
|
||||
if dataScope == consts.SysRoleDataScopeAll { // 角色数据范围: 1全部数据权限
|
||||
return m
|
||||
}
|
||||
hasRestrictedScope = true
|
||||
if dataScope == consts.SysRoleDataScopeCustom { // 角色数据范围: 2自定数据权限
|
||||
var roleDepts []*entity.SysRoleDept
|
||||
err := g.Model("sys_role_dept").Where("tenant_id", contexts.GetTenantId(ctx)).Where("role_id", roleId).Scan(&roleDepts)
|
||||
if err != nil {
|
||||
g.Log().Panicf(ctx, "failed to get role dept data err:%+v", err)
|
||||
}
|
||||
for _, roleDept := range roleDepts {
|
||||
deptIds = append(deptIds, roleDept.DeptId)
|
||||
}
|
||||
} else if dataScope == consts.SysRoleDataScopeDept { // 角色数据范围: 3本部门数据权限
|
||||
deptIds = append(deptIds, user.DeptId)
|
||||
} else if dataScope == consts.SysRoleDataScopeDeptAndBelow { // 角色数据范围: 4本部门及以下数据权限
|
||||
deptIds = append(deptIds, GetDeptAndSub(ctx, user.DeptId)...)
|
||||
} else if dataScope == consts.SysRoleDataScopePersonal { // 角色数据范围: 5仅本人数据权限
|
||||
userIds = append(userIds, user.ID)
|
||||
} else if dataScope == consts.SysRoleDataScopeDeptAndBelowOrPersonal { // 角色数据范围: 6本部门及以下或本人数据权限
|
||||
deptIds = append(deptIds, GetDeptAndSub(ctx, user.DeptId)...)
|
||||
userIds = append(userIds, user.ID)
|
||||
} else if dataScope == consts.SysRoleDataScopeOrgAndNextLevel { // 角色数据范围: 7本组织及本组织下一级数据权限
|
||||
deptIds = append(deptIds, GetOrgAndNextLevelDeptIds(ctx, user.DeptId)...)
|
||||
} else if dataScope == consts.SysRoleDataScopeOrgNextLevel { // 角色数据范围: 8本组织下一级数据权限
|
||||
deptIds = append(deptIds, GetOrgNextLevelDeptIds(ctx, user.DeptId)...)
|
||||
}
|
||||
} else {
|
||||
for _, role := range user.Roles {
|
||||
dataScope := role.DataScope
|
||||
roleId := role.RoleId
|
||||
if dataScope == consts.SysRoleDataScopeAll { // 角色数据范围: 1全部数据权限
|
||||
return m
|
||||
} else if dataScope == consts.SysRoleDataScopeCustom { // 角色数据范围: 2自定数据权限
|
||||
hasRestrictedScope = true
|
||||
var roleDepts []*entity.SysRoleDept
|
||||
err := g.Model("sys_role_dept").Where("tenant_id", contexts.GetTenantId(ctx)).Where("role_id", roleId).Scan(&roleDepts)
|
||||
if err != nil {
|
||||
g.Log().Panicf(ctx, "failed to get role dept data err:%+v", err)
|
||||
}
|
||||
for _, roleDept := range roleDepts {
|
||||
deptIds = append(deptIds, roleDept.DeptId)
|
||||
}
|
||||
} else if dataScope == consts.SysRoleDataScopeDept { // 角色数据范围: 3本部门数据权限
|
||||
hasRestrictedScope = true
|
||||
deptIds = append(deptIds, user.DeptId)
|
||||
} else if dataScope == consts.SysRoleDataScopeDeptAndBelow { // 角色数据范围: 4本部门及以下数据权限
|
||||
hasRestrictedScope = true
|
||||
deptIds = append(deptIds, GetDeptAndSub(ctx, user.DeptId)...)
|
||||
} else if dataScope == consts.SysRoleDataScopePersonal { // 角色数据范围: 5仅本人数据权限
|
||||
hasRestrictedScope = true
|
||||
userIds = append(userIds, user.ID)
|
||||
} else if dataScope == consts.SysRoleDataScopeDeptAndBelowOrPersonal { // 角色数据范围: 6本部门及以下或本人数据权限
|
||||
hasRestrictedScope = true
|
||||
deptIds = append(deptIds, GetDeptAndSub(ctx, user.DeptId)...)
|
||||
userIds = append(userIds, user.ID)
|
||||
} else if dataScope == consts.SysRoleDataScopeOrgAndNextLevel { // 角色数据范围: 7本组织及本组织下一级数据权限
|
||||
hasRestrictedScope = true
|
||||
deptIds = append(deptIds, GetOrgAndNextLevelDeptIds(ctx, user.DeptId)...)
|
||||
} else if dataScope == consts.SysRoleDataScopeOrgNextLevel { // 角色数据范围: 8本组织下一级数据权限
|
||||
hasRestrictedScope = true
|
||||
deptIds = append(deptIds, GetOrgNextLevelDeptIds(ctx, user.DeptId)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deptFilterField := ""
|
||||
if gstr.InArray(fields, "dept_id") {
|
||||
deptFilterField = "dept_id"
|
||||
} else if gstr.InArray(fields, "created_dept") {
|
||||
deptFilterField = "created_dept"
|
||||
}
|
||||
userFilterField := ""
|
||||
if gstr.InArray(fields, "user_id") {
|
||||
userFilterField = "user_id"
|
||||
} else if gstr.InArray(fields, "created_by") {
|
||||
userFilterField = "created_by"
|
||||
}
|
||||
|
||||
if len(deptIds) > 0 && len(userIds) > 0 {
|
||||
if deptFilterField != "" && userFilterField != "" {
|
||||
m = m.Where(deptFilterField+" IN (?) OR "+userFilterField+" IN (?)", deptIds, userIds)
|
||||
} else if deptFilterField != "" {
|
||||
m = m.WhereIn(deptFilterField, deptIds)
|
||||
} else if userFilterField != "" {
|
||||
m = m.WhereIn(userFilterField, userIds)
|
||||
}
|
||||
} else if len(deptIds) > 0 && deptFilterField != "" {
|
||||
m = m.WhereIn(deptFilterField, deptIds)
|
||||
} else if len(userIds) > 0 && userFilterField != "" {
|
||||
m = m.WhereIn(userFilterField, userIds)
|
||||
} else if hasRestrictedScope && (deptFilterField != "" || userFilterField != "") {
|
||||
// 例如:范围8无下级部门、范围2未配自定义部门 -> 可见集合为空,禁止放行全表。
|
||||
g.Log().Warningf(ctx, "FilterAuth empty scope result, deny all data. uid:%d, deptIds:%v, userIds:%v", user.ID, deptIds, userIds)
|
||||
m = m.Where("1 = 0")
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// GetParentCompanyDeptId 获取用户所属上级公司组织ID
|
||||
func GetParentCompanyDeptId(ctx context.Context, deptId int64) int64 {
|
||||
if deptId <= 0 {
|
||||
return deptId
|
||||
}
|
||||
var dept entity.SysDept
|
||||
err := g.Model("sys_dept").Where("dept_id", deptId).Scan(&dept)
|
||||
if err != nil || dept.DeptId == 0 {
|
||||
return deptId
|
||||
}
|
||||
if dept.ParentId == 0 || dept.DeptType == 1 {
|
||||
return dept.DeptId
|
||||
}
|
||||
return GetParentCompanyDeptId(ctx, dept.ParentId)
|
||||
}
|
||||
|
||||
// GetDeptDirectSub 获取指定部门的直接下级部门ID
|
||||
func GetDeptDirectSub(ctx context.Context, deptId int64) (ids []int64) {
|
||||
array, err := g.Model("sys_dept").
|
||||
Where("parent_id", deptId).
|
||||
Fields("dept_id").
|
||||
Array()
|
||||
if err != nil {
|
||||
g.Log().Panicf(ctx, "GetDeptDirectSub err:%+v", err)
|
||||
return
|
||||
}
|
||||
for _, v := range array {
|
||||
ids = append(ids, v.Int64())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetOrgAndNextLevelDeptIds 获取本组织及本组织下一级数据权限范围:所属公司组织及直接下级
|
||||
func GetOrgAndNextLevelDeptIds(ctx context.Context, deptId int64) (ids []int64) {
|
||||
orgDeptId := GetParentCompanyDeptId(ctx, deptId)
|
||||
ids = append(ids, orgDeptId)
|
||||
ids = append(ids, GetDeptDirectSub(ctx, orgDeptId)...)
|
||||
return
|
||||
}
|
||||
|
||||
// GetOrgNextLevelDeptIds 获取本组织下一级数据权限范围:所属公司组织的直接下级
|
||||
func GetOrgNextLevelDeptIds(ctx context.Context, deptId int64) (ids []int64) {
|
||||
orgDeptId := GetParentCompanyDeptId(ctx, deptId)
|
||||
return GetDeptDirectSub(ctx, orgDeptId)
|
||||
}
|
||||
|
||||
// GetDeptAndSub 获取指定部门的所有下级,含本部门
|
||||
func GetDeptAndSub(ctx context.Context, deptId int64) (ids []int64) {
|
||||
array, err := g.Model("sys_dept").
|
||||
WhereLike("ancestors", "%,"+fmt.Sprintf("%d", deptId)+",%").
|
||||
Fields("dept_id").
|
||||
Array()
|
||||
if err != nil {
|
||||
g.Log().Panicf(ctx, "GetDeptAndSub err:%+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range array {
|
||||
ids = append(ids, v.Int64())
|
||||
}
|
||||
|
||||
ids = append(ids, deptId)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// package handler
|
||||
// @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 handler
|
||||
|
||||
import "github.com/gogf/gf/v2/database/gdb"
|
||||
|
||||
// ForceCache 强制缓存
|
||||
func ForceCache(m *gdb.Model) *gdb.Model {
|
||||
return m.Cache(gdb.CacheOption{Duration: 0, Force: true})
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// package handler
|
||||
// @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 handler
|
||||
|
||||
import "github.com/gogf/gf/v2/database/gdb"
|
||||
|
||||
// Option 预处理选项
|
||||
type Option struct {
|
||||
FilterAuth bool // 过滤权限
|
||||
ForceCache bool // 强制缓存
|
||||
FilterTenant bool // 过滤多租户权限
|
||||
}
|
||||
|
||||
// DefaultOption 默认预处理选项
|
||||
var DefaultOption = &Option{
|
||||
FilterAuth: true,
|
||||
FilterTenant: true,
|
||||
}
|
||||
|
||||
func Model(m *gdb.Model, opt ...*Option) *gdb.Model {
|
||||
var option *Option
|
||||
if len(opt) > 0 {
|
||||
option = opt[0]
|
||||
} else {
|
||||
option = DefaultOption
|
||||
}
|
||||
if option.FilterAuth {
|
||||
m = m.Handler(FilterAuth)
|
||||
}
|
||||
if option.ForceCache {
|
||||
m = m.Handler(ForceCache)
|
||||
}
|
||||
if option.FilterTenant {
|
||||
m = m.Handler(FilterTenant)
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// package handler
|
||||
// @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 handler
|
||||
|
||||
import (
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/internal/library/contexts"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility/convert"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
// FilterTenant 过滤多租户数据权限
|
||||
func FilterTenant(m *gdb.Model) *gdb.Model {
|
||||
var (
|
||||
ctx = m.GetCtx()
|
||||
fields = convert.EscapeFieldsToSlice(m.GetFieldsStr())
|
||||
)
|
||||
// 如果包含tenant_id字段,则过滤多租户数据权限
|
||||
if gstr.InArray(fields, "tenant_id") {
|
||||
m = m.Where("tenant_id", contexts.GetTenantId(ctx))
|
||||
}
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// package hook
|
||||
// @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 hook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.5iot.com/iotlcm-public/iotlcm-app/utility"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type MemberSumma struct {
|
||||
UserId int64 `json:"userId" orm:"user_id" description:"用户ID"`
|
||||
UserName string `json:"userName" orm:"user_name" description:"用户账号"`
|
||||
NickName string `json:"nickName" orm:"nick_name" description:"用户昵称"`
|
||||
Avatar string `json:"avatar" orm:"avatar" description:"头像地址"`
|
||||
TenantId string `json:"tenantId" orm:"tenant_id" description:"租户编号"`
|
||||
DeptId int64 `json:"deptId" orm:"dept_id" description:"部门ID"`
|
||||
}
|
||||
|
||||
// MemberSummary 操作人摘要信息
|
||||
var MemberSummary = gdb.HookHandler{
|
||||
Select: func(ctx context.Context, in *gdb.HookSelectInput) (result gdb.Result, err error) {
|
||||
result, err = in.Next(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
createdByIds []int64
|
||||
updatedByIds []int64
|
||||
deletedByIds []int64
|
||||
memberIds []int64
|
||||
)
|
||||
|
||||
for _, record := range result {
|
||||
if record["created_by"].Int64() > 0 {
|
||||
createdByIds = append(createdByIds, record["created_by"].Int64())
|
||||
}
|
||||
if record["updated_by"].Int64() > 0 {
|
||||
updatedByIds = append(updatedByIds, record["updated_by"].Int64())
|
||||
}
|
||||
if record["deleted_by"].Int64() > 0 {
|
||||
deletedByIds = append(deletedByIds, record["deleted_by"].Int64())
|
||||
}
|
||||
if record["user_id"].Int64() > 0 {
|
||||
memberIds = append(memberIds, record["user_id"].Int64())
|
||||
}
|
||||
}
|
||||
|
||||
memberIds = append(memberIds, createdByIds...)
|
||||
memberIds = append(memberIds, updatedByIds...)
|
||||
memberIds = append(memberIds, deletedByIds...)
|
||||
memberIds = utility.UniqueSlice(memberIds)
|
||||
if len(memberIds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var members []*MemberSumma
|
||||
if err = g.Model("sys_user").Ctx(ctx).WhereIn("user_id", memberIds).Scan(&members); err != nil {
|
||||
g.Log().Errorf(ctx, "[MemberSummary] 查询用户信息失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(members) == 0 {
|
||||
g.Log().Warningf(ctx, "[MemberSummary] 没有找到任何用户信息,用户IDs: %v", memberIds)
|
||||
return
|
||||
}
|
||||
|
||||
findMember := func(id *gvar.Var) *MemberSumma {
|
||||
userId := id.Int64()
|
||||
for _, v := range members {
|
||||
if v.UserId == userId {
|
||||
return v
|
||||
}
|
||||
}
|
||||
// 只在找不到用户时记录警告(避免日志过多)
|
||||
g.Log().Warningf(ctx, "[MemberSummary] 未找到用户ID: %d", userId)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, record := range result {
|
||||
if record["created_by"].Int64() > 0 {
|
||||
member := findMember(record["created_by"])
|
||||
if member != nil {
|
||||
record["createdBySumma"] = gvar.New(member)
|
||||
}
|
||||
}
|
||||
|
||||
if record["updated_by"].Int64() > 0 {
|
||||
member := findMember(record["updated_by"])
|
||||
if member != nil {
|
||||
record["updatedBySumma"] = gvar.New(member)
|
||||
}
|
||||
}
|
||||
|
||||
if record["deleted_by"].Int64() > 0 {
|
||||
member := findMember(record["deleted_by"])
|
||||
if member != nil {
|
||||
record["deletedBySumma"] = gvar.New(member)
|
||||
}
|
||||
}
|
||||
|
||||
if record["user_id"].Int64() > 0 {
|
||||
member := findMember(record["user_id"])
|
||||
if member != nil {
|
||||
record["memberBySumma"] = gvar.New(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user