diff --git a/frontend/package.json b/frontend/package.json index b3305c21..953aa527 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", - "@vueuse/core": "^12.7.0", + "@vueuse/core": "^12.8.2", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-search": "^0.15.0", "@xterm/addon-web-links": "^0.11.0", @@ -59,9 +59,9 @@ "eslint": "^8.35.0", "eslint-plugin-vue": "^9.31.0", "prettier": "^3.2.5", - "sass": "^1.85.0", - "typescript": "^5.7.3", - "vite": "^6.2.0", + "sass": "^1.85.1", + "typescript": "^5.8.2", + "vite": "^6.2.1", "vite-plugin-progress": "0.0.7", "vue-eslint-parser": "^9.4.3" }, diff --git a/frontend/src/views/ops/component/ResourceOpPanel.vue b/frontend/src/views/ops/component/ResourceOpPanel.vue new file mode 100644 index 00000000..36de9700 --- /dev/null +++ b/frontend/src/views/ops/component/ResourceOpPanel.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/frontend/src/views/ops/db/SqlExec.vue b/frontend/src/views/ops/db/SqlExec.vue index 78e8b266..ab26eaaa 100644 --- a/frontend/src/views/ops/db/SqlExec.vue +++ b/frontend/src/views/ops/db/SqlExec.vue @@ -1,7 +1,7 @@ - + + + import('./component/table/DbTableOp.vue')); const DbSqlEditor = defineAsyncComponent(() => import('./component/sqleditor/DbSqlEditor.vue')); diff --git a/frontend/src/views/ops/db/component/table/DbTablesOp.vue b/frontend/src/views/ops/db/component/table/DbTablesOp.vue index c381d65a..0eb5014c 100644 --- a/frontend/src/views/ops/db/component/table/DbTablesOp.vue +++ b/frontend/src/views/ops/db/component/table/DbTablesOp.vue @@ -89,7 +89,7 @@ - + diff --git a/frontend/src/views/ops/machine/MachineList.vue b/frontend/src/views/ops/machine/MachineList.vue index c1ea4c3d..e4780ff2 100644 --- a/frontend/src/views/ops/machine/MachineList.vue +++ b/frontend/src/views/ops/machine/MachineList.vue @@ -317,7 +317,7 @@ const searchItems = [ const columns = [ TableColumn.new('tags[0].tagPath', 'tag.relateTag').isSlot('tagPath').setAddWidth(20), TableColumn.new('name', 'common.name'), - TableColumn.new('ipPort', 'Ip:Port').isSlot().setAddWidth(50), + TableColumn.new('ipPort', 'Ip:Port').isSlot().setAddWidth(55), TableColumn.new('authCerts[0].username', 'machine.acName').isSlot('authCert').setAddWidth(10), TableColumn.new('status', 'common.status').isSlot().setAddWidth(5), TableColumn.new('stat', 'machine.runningStat').isSlot().setAddWidth(55), diff --git a/frontend/src/views/ops/machine/MachineOp.vue b/frontend/src/views/ops/machine/MachineOp.vue index 3b678dd4..b4daa474 100644 --- a/frontend/src/views/ops/machine/MachineOp.vue +++ b/frontend/src/views/ops/machine/MachineOp.vue @@ -1,8 +1,7 @@ - + + @@ -171,7 +170,6 @@ import { hasPerms } from '@/components/auth/auth'; import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum'; import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag'; import TagTree from '../component/TagTree.vue'; -import { Pane, Splitpanes } from 'splitpanes'; import { ContextmenuItem } from '@/components/contextmenu/index'; import TerminalBody from '@/components/terminal/TerminalBody.vue'; import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common'; @@ -183,6 +181,7 @@ import { useAutoOpenResource } from '@/store/autoOpenResource'; import { storeToRefs } from 'pinia'; import EnumValue from '@/common/Enum'; import { useI18n } from 'vue-i18n'; +import ResourceOpPanel from '../component/ResourceOpPanel.vue'; // 组件 const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue')); diff --git a/frontend/src/views/ops/mongo/MongoDataOp.vue b/frontend/src/views/ops/mongo/MongoDataOp.vue index eff2d8ae..c3d397f8 100644 --- a/frontend/src/views/ops/mongo/MongoDataOp.vue +++ b/frontend/src/views/ops/mongo/MongoDataOp.vue @@ -1,7 +1,7 @@ - + + @@ -177,11 +177,11 @@ import TagTree from '../component/TagTree.vue'; import { formatByteSize } from '@/common/utils/format'; import { TagResourceTypeEnum } from '@/common/commonEnum'; import { sleep } from '@/common/utils/loading'; -import { Splitpanes, Pane } from 'splitpanes'; import { useAutoOpenResource } from '@/store/autoOpenResource'; import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n'; import { useI18nDeleteSuccessMsg, useI18nSaveSuccessMsg } from '@/hooks/useI18n'; +import ResourceOpPanel from '../component/ResourceOpPanel.vue'; const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue')); diff --git a/frontend/src/views/ops/redis/DataOperation.vue b/frontend/src/views/ops/redis/DataOperation.vue index 427ca9b2..02862beb 100644 --- a/frontend/src/views/ops/redis/DataOperation.vue +++ b/frontend/src/views/ops/redis/DataOperation.vue @@ -1,7 +1,7 @@ - -
- - - - - - - - - - - - + +
@@ -204,6 +208,7 @@ import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n'; import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n'; import { Rules } from '@/common/rule'; +import ResourceOpPanel from '../component/ResourceOpPanel.vue'; const KeyDetail = defineAsyncComponent(() => import('./KeyDetail.vue')); diff --git a/server/go.mod b/server/go.mod index c5113a66..62632c46 100644 --- a/server/go.mod +++ b/server/go.mod @@ -32,9 +32,9 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/veops/go-ansiterm v0.0.5 go.mongodb.org/mongo-driver v1.16.0 // mongo - golang.org/x/crypto v0.35.0 // ssh + golang.org/x/crypto v0.36.0 // ssh golang.org/x/oauth2 v0.26.0 - golang.org/x/sync v0.11.0 + golang.org/x/sync v0.12.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 // gorm @@ -93,8 +93,8 @@ require ( golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/image v0.23.0 // indirect golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect google.golang.org/protobuf v1.34.1 // indirect modernc.org/libc v1.22.5 // indirect modernc.org/mathutil v1.5.0 // indirect diff --git a/server/internal/db/application/db_data_sync.go b/server/internal/db/application/db_data_sync.go index c1e71a41..6264e830 100644 --- a/server/internal/db/application/db_data_sync.go +++ b/server/internal/db/application/db_data_sync.go @@ -159,7 +159,7 @@ func (app *dataSyncAppImpl) RunCronJob(ctx context.Context, id uint64) error { break } } - return errorx.NewBiz("get column data type... ignore~") + return dbi.NewStopWalkQueryError("get column data type... ignore~") }) updSql = fmt.Sprintf("and %s > %s", task.UpdField, updFieldDataType.DataType.SQLValue(task.UpdFieldVal)) diff --git a/server/internal/db/application/db_sql_exec.go b/server/internal/db/application/db_sql_exec.go index 62e14884..dbc4855c 100644 --- a/server/internal/db/application/db_sql_exec.go +++ b/server/internal/db/application/db_sql_exec.go @@ -333,7 +333,6 @@ func (d *dbSqlExecAppImpl) saveSqlExecLog(dbSqlExecRecord *entity.DbSqlExec, res func (d *dbSqlExecAppImpl) doSelect(ctx context.Context, sqlExecParam *sqlExecParam) (*dto.DbSqlExecRes, error) { maxCount := config.GetDbms().MaxResultSet - selectStmt := sqlExecParam.Stmt selectSql := sqlExecParam.Sql sqlExecParam.SqlExecRecord.Type = entity.DbSqlExecTypeQuery @@ -343,49 +342,7 @@ func (d *dbSqlExecAppImpl) doSelect(ctx context.Context, sqlExecParam *sqlExecPa } } - if selectStmt != nil { - needCheckLimit := false - var limit *sqlstmt.Limit - switch stmt := selectStmt.(type) { - case *sqlstmt.SimpleSelectStmt: - qs := stmt.QuerySpecification - limit = qs.Limit - if qs.SelectElements != nil && (qs.SelectElements.Star != "" || len(qs.SelectElements.Elements) > 1) { - needCheckLimit = true - } - case *sqlstmt.UnionSelectStmt: - limit = stmt.Limit - selectSql = selectStmt.GetText() - needCheckLimit = true - } - - // 如果配置为0,则不校验分页参数 - if needCheckLimit && maxCount != 0 { - if limit == nil { - return nil, errorx.NewBizI(ctx, imsg.ErrNoLimitStmt) - } - if limit.RowCount > maxCount { - return nil, errorx.NewBizI(ctx, imsg.ErrLimitInvalid, "count", maxCount) - } - } - } else { - if maxCount != 0 { - if !strings.Contains(selectSql, "limit") && - // 兼容oracle rownum分页 - !strings.Contains(selectSql, "rownum") && - // 兼容mssql offset分页 - !strings.Contains(selectSql, "offset") && - // 兼容mssql top 分页 with result as ({query sql}) select top 100 * from result - !strings.Contains(selectSql, " top ") { - // 判断是不是count语句 - if !strings.Contains(selectSql, "count(") { - return nil, errorx.NewBizI(ctx, imsg.ErrNoLimitStmt) - } - } - } - } - - return d.doQuery(ctx, sqlExecParam.DbConn, selectSql) + return d.doQuery(ctx, sqlExecParam.DbConn, selectSql, maxCount) } func (d *dbSqlExecAppImpl) doOtherRead(ctx context.Context, sqlExecParam *sqlExecParam) (*dto.DbSqlExecRes, error) { @@ -398,7 +355,7 @@ func (d *dbSqlExecAppImpl) doOtherRead(ctx context.Context, sqlExecParam *sqlExe } } - return d.doQuery(ctx, sqlExecParam.DbConn, selectSql) + return d.doQuery(ctx, sqlExecParam.DbConn, selectSql, 0) } func (d *dbSqlExecAppImpl) doExecDDL(ctx context.Context, sqlExecParam *sqlExecParam) (*dto.DbSqlExecRes, error) { @@ -588,11 +545,23 @@ func (d *dbSqlExecAppImpl) doInsert(ctx context.Context, sqlExecParam *sqlExecPa return d.doExec(ctx, sqlExecParam.DbConn, sqlExecParam.Sql) } -func (d *dbSqlExecAppImpl) doQuery(ctx context.Context, dbConn *dbi.DbConn, sql string) (*dto.DbSqlExecRes, error) { - cols, res, err := dbConn.QueryContext(ctx, sql) +func (d *dbSqlExecAppImpl) doQuery(ctx context.Context, dbConn *dbi.DbConn, sql string, maxRows int) (*dto.DbSqlExecRes, error) { + res := make([]map[string]any, 0, 16) + nowRows := 0 + cols, err := dbConn.WalkQueryRows(ctx, sql, func(row map[string]any, columns []*dbi.QueryColumn) error { + nowRows++ + // 超过指定的最大查询记录数,则停止查询 + if maxRows != 0 && nowRows > maxRows { + return dbi.NewStopWalkQueryError(fmt.Sprintf("exceed the maximum number of query records %d", maxRows)) + } + res = append(res, row) + return nil + }) + if err != nil { return nil, err } + return &dto.DbSqlExecRes{ Sql: sql, Columns: cols, diff --git a/server/internal/db/dbm/dbi/conn.go b/server/internal/db/dbm/dbi/conn.go index 8de5fb60..f69bcf6c 100644 --- a/server/internal/db/dbm/dbi/conn.go +++ b/server/internal/db/dbm/dbi/conn.go @@ -70,11 +70,7 @@ func (d *DbConn) QueryContext(ctx context.Context, querySql string, args ...any) return nil }, args...) - if err != nil { - return nil, nil, wrapSqlError(err) - } - - return cols, result, nil + return cols, result, err } // 将查询结果映射至struct,可具体参考sqlx库 @@ -95,7 +91,15 @@ func (d *DbConn) Query2Struct(execSql string, dest any) error { // WalkQueryRows 游标方式遍历查询结果集, walkFn返回error不为nil, 则跳出遍历并取消查询 func (d *DbConn) WalkQueryRows(ctx context.Context, querySql string, walkFn WalkQueryRowsFunc, args ...any) ([]*QueryColumn, error) { - return d.walkQueryRows(ctx, querySql, walkFn, args...) + if qcs, err := d.walkQueryRows(ctx, querySql, walkFn, args...); err != nil { + // 如果是手动停止 则默认返回当前已遍历查询的数据即可 + if _, ok := err.(*StopWalkQueryError); ok { + return qcs, nil + } + return qcs, wrapSqlError(err) + } else { + return qcs, nil + } } // WalkTableRows 游标方式遍历指定表的结果集, walkFn返回error不为nil, 则跳出遍历并取消查询 @@ -242,3 +246,18 @@ func wrapSqlError(err error) error { } return err } + +// StopWalkQueryError 自定义的停止遍历查询错误类型 +type StopWalkQueryError struct { + Reason string +} + +// Error 实现 error 接口 +func (e *StopWalkQueryError) Error() string { + return fmt.Sprintf("stop walk query: %s", e.Reason) +} + +// NewStopWalkQueryError 创建一个带有reason的StopWalkQueryError +func NewStopWalkQueryError(reason string) *StopWalkQueryError { + return &StopWalkQueryError{Reason: reason} +} diff --git a/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql b/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql index 2c31d0a2..d47e4e07 100644 --- a/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql +++ b/server/internal/db/dbm/dbi/metasql/pgsql_meta.sql @@ -13,26 +13,31 @@ order by n.nspname --------------------------------------- --PGSQL_TABLE_INFO 表详细信息 -SELECT - c.relname AS "tableName", - obj_description(c.oid) AS "tableComment", - pg_total_relation_size(c.oid) AS "dataLength", - pg_indexes_size(c.oid) AS "indexLength", - psut.n_live_tup AS "tableRows" +SELECT DISTINCT + c.relname AS "tableName", + COALESCE(b.description, '') AS "tableComment", + pg_total_relation_size(c.oid) AS "dataLength", + pg_indexes_size(c.oid) AS "indexLength", + psut.n_live_tup AS "tableRows" FROM - pg_class c -JOIN - pg_namespace n ON c.relnamespace = n.oid -JOIN - pg_stat_user_tables psut ON psut.relid = c.oid + pg_class c + LEFT JOIN pg_description b ON c.oid = b.objoid AND b.objsubid = 0 + JOIN pg_stat_user_tables psut ON psut.relid = c.oid WHERE - has_table_privilege(c.oid, 'SELECT') - and n.nspname = current_schema() - {{if .tableNames}} - and c.relname in ({{.tableNames}}) - {{end}} + c.relkind = 'r' + AND c.relnamespace = ( + SELECT + oid + FROM + pg_namespace + WHERE + nspname = current_schema() + {{if .tableNames}} + and c.relname in ({{.tableNames}}) + {{end}} + ) ORDER BY - c.relname; + c.relname; --------------------------------------- --PGSQL_INDEX_INFO 表索引信息 SELECT a.indexname AS "indexName", diff --git a/server/internal/db/domain/entity/db.go b/server/internal/db/domain/entity/db.go index 541d379d..e8545dfe 100644 --- a/server/internal/db/domain/entity/db.go +++ b/server/internal/db/domain/entity/db.go @@ -7,7 +7,7 @@ import ( type Db struct { model.Model - Code string `json:"code" gorm:"size:32;not null;index:idx_code"` + Code string `json:"code" gorm:"size:32;not null;index:idx_db_code"` Name string `json:"name" gorm:"size:255;not null;"` GetDatabaseMode DbGetDatabaseMode `json:"getDatabaseMode" gorm:"comment:库名获取方式(-1.实时获取、1.指定库名)"` // 获取数据库方式 Database string `json:"database" gorm:"size:2000;"` diff --git a/server/internal/db/imsg/en.go b/server/internal/db/imsg/en.go index 23fad90b..b6c3e75d 100644 --- a/server/internal/db/imsg/en.go +++ b/server/internal/db/imsg/en.go @@ -23,8 +23,6 @@ var En = map[i18n.MsgId]string{ ErrExistRunFailSql: "There is an execution error in sql", ErrNeedSubmitWorkTicket: "This operation needs to submit a work ticket for approval", - ErrNoLimitStmt: "Please complete the paging information before executing", - ErrLimitInvalid: "The number of query result sets should be less than the {{.count}} number configured by the system", // db transfer LogDtsSave: "dts - Save data transfer task", diff --git a/server/internal/db/imsg/imsg.go b/server/internal/db/imsg/imsg.go index b93f9ef4..6517798d 100644 --- a/server/internal/db/imsg/imsg.go +++ b/server/internal/db/imsg/imsg.go @@ -33,8 +33,6 @@ const ( ErrExistRunFailSql ErrNeedSubmitWorkTicket - ErrNoLimitStmt - ErrLimitInvalid // db transfer LogDtsSave diff --git a/server/internal/db/imsg/zh_cn.go b/server/internal/db/imsg/zh_cn.go index 5c4983e8..d3d94bdf 100644 --- a/server/internal/db/imsg/zh_cn.go +++ b/server/internal/db/imsg/zh_cn.go @@ -23,8 +23,6 @@ var Zh_CN = map[i18n.MsgId]string{ ErrExistRunFailSql: "存在执行错误的sql", ErrNeedSubmitWorkTicket: "该操作需要提交工单审批执行", - ErrNoLimitStmt: "请完善分页信息后执行", - ErrLimitInvalid: "查询结果集数需小于系统配置的{{.count}}条", // db transfer LogDtsSave: "dts-保存数据迁移任务", diff --git a/server/internal/flow/domain/entity/procdef.go b/server/internal/flow/domain/entity/procdef.go index 1d0a110e..dbfa40e1 100644 --- a/server/internal/flow/domain/entity/procdef.go +++ b/server/internal/flow/domain/entity/procdef.go @@ -47,7 +47,11 @@ func (p *Procdef) MatchCondition(bizType string, param map[string]any) bool { return true } - res := stringx.TemplateResolve(*p.Condition, collx.Kvs("bizType", bizType, "param", param)) + res, err := stringx.TemplateResolve(*p.Condition, collx.Kvs("bizType", bizType, "param", param)) + if err != nil { + logx.ErrorTrace("parse condition error", err.Error()) + return true + } return strings.TrimSpace(res) == "1" } diff --git a/server/internal/tag/application/resouce_auth_cert.go b/server/internal/tag/application/resouce_auth_cert.go index 971bbf11..0d49cb09 100644 --- a/server/internal/tag/application/resouce_auth_cert.go +++ b/server/internal/tag/application/resouce_auth_cert.go @@ -316,6 +316,12 @@ func (r *resourceAuthCertAppImpl) addAuthCert(ctx context.Context, rac *entity.R return errorx.NewBizI(ctx, imsg.ErrAcNameExist, "acName", rac.Name) } } + if rac.Type == 0 { + rac.Type = entity.AuthCertTypePrivate + } + if rac.CiphertextType == 0 { + rac.CiphertextType = entity.AuthCertCiphertextTypePassword + } // 公共凭证 if rac.Type == entity.AuthCertTypePublic { diff --git a/server/internal/tag/domain/entity/tag_tree.go b/server/internal/tag/domain/entity/tag_tree.go index d2760012..80c4863a 100644 --- a/server/internal/tag/domain/entity/tag_tree.go +++ b/server/internal/tag/domain/entity/tag_tree.go @@ -14,11 +14,11 @@ import ( type TagTree struct { model.Model - Type TagType `json:"type" gorm:"not null;default:-1;comment:类型: -1.普通标签; 1机器 2db 3redis 4mongo"` // 类型: -1.普通标签; 其他值则为对应的资源类型 - Code string `json:"code" gorm:"not null;size:50;comment:标识符"` // 标识编码, 若类型不为-1,则为对应资源编码 - CodePath string `json:"codePath" gorm:"not null;size:800;comment:标识符路径"` // 标识路径,tag1/tag2/tagType1|tagCode/tagType2|yyycode/,非普通标签类型段含有标签类型 - Name string `json:"name" gorm:"size:50;comment:名称"` // 名称 - Remark string `json:"remark" gorm:"size:255;"` // 备注说明 + Type TagType `json:"type" gorm:"not null;default:-1;comment:类型: -1.普通标签; 1机器 2db 3redis 4mongo"` // 类型: -1.普通标签; 其他值则为对应的资源类型 + Code string `json:"code" gorm:"not null;size:50;index:idx_tag_code;comment:标识符"` // 标识编码, 若类型不为-1,则为对应资源编码 + CodePath string `json:"codePath" gorm:"not null;size:800;index:idx_tag_code_path,length:255;comment:标识符路径"` // 标识路径,tag1/tag2/tagType1|tagCode/tagType2|yyycode/,非普通标签类型段含有标签类型 + Name string `json:"name" gorm:"size:50;comment:名称"` // 名称 + Remark string `json:"remark" gorm:"size:255;"` // 备注说明 } type TagType int8 diff --git a/server/migration/migrations/init.go b/server/migration/migrations/init.go index f7f63ad4..fd36f782 100644 --- a/server/migration/migrations/init.go +++ b/server/migration/migrations/init.go @@ -157,6 +157,15 @@ func initRole(tx *gorm.DB) error { role.Creator = "admin" role.Modifier = "admin" + roleResource := &sysentity.RoleResource{ + RoleId: role.Id, + ResourceId: 1, + CreateTime: &now, + CreatorId: 1, + Creator: "admin", + } + + tx.Create(roleResource) return tx.Create(role).Error } diff --git a/server/pkg/utils/stringx/stringx.go b/server/pkg/utils/stringx/stringx.go index 2d6cf127..e551daab 100644 --- a/server/pkg/utils/stringx/stringx.go +++ b/server/pkg/utils/stringx/stringx.go @@ -7,14 +7,6 @@ import ( "unicode/utf8" ) -// 逻辑空字符串(由于gorm更新结构体只更新非零值,所以使用该值最为逻辑空字符串,方便更新结构体) -const LogicEmptyStr = "-" - -// 是否为逻辑上空字符串 -func IsLogicEmpty(str string) bool { - return str == "" || str == LogicEmptyStr -} - // 可判断中文 func Len(str string) int { return len([]rune(str)) @@ -89,15 +81,18 @@ func UnicodeIndex(str, substr string) int { } // 字符串模板解析 -func TemplateResolve(temp string, data any) string { - t, _ := template.New("string-temp").Parse(temp) +func TemplateResolve(temp string, data any) (string, error) { + t, err := template.New("string-temp").Parse(temp) + if err != nil { + return "", err + } var tmplBytes bytes.Buffer - err := t.Execute(&tmplBytes, data) + err = t.Execute(&tmplBytes, data) if err != nil { - panic(err) + return "", err } - return tmplBytes.String() + return tmplBytes.String(), nil } func ReverStrTemplate(temp, str string, res map[string]any) { diff --git a/server/pkg/utils/structx/structx_test.go b/server/pkg/utils/structx/structx_test.go index 0bba00a2..2ed59c68 100644 --- a/server/pkg/utils/structx/structx_test.go +++ b/server/pkg/utils/structx/structx_test.go @@ -190,7 +190,7 @@ func TestTemplateResolve(t *testing.T) { d := make(map[string]string) d["Name"] = "黄先生" d["Age"] = "23jlfdsjf" - resolve := stringx.TemplateResolve("{{.Name}} is name, and {{.Age}} is age", d) + resolve, _ := stringx.TemplateResolve("{{.Name}} is name, and {{.Age}} is age", d) fmt.Println(resolve) }