feat: 新增数据库导出功能&其他小优化

This commit is contained in:
meilin.huang
2022-06-30 16:42:25 +08:00
parent 64b49dae2e
commit fe8cd93c78
11 changed files with 234 additions and 31 deletions

View File

@@ -30,7 +30,7 @@ import { useStore } from '@/store/index.ts';
export default defineComponent({ export default defineComponent({
name: 'layoutBreadcrumbSearch', name: 'layoutBreadcrumbSearch',
setup() { setup() {
const layoutMenuAutocompleteRef = ref(); const layoutMenuAutocompleteRef: any = ref(null);
const store = useStore(); const store = useStore();
const router = useRouter(); const router = useRouter();
const state: any = reactive({ const state: any = reactive({
@@ -68,7 +68,6 @@ export default defineComponent({
// 初始化菜单数据 // 初始化菜单数据
const initTageView = () => { const initTageView = () => {
if (state.tagsViewList.length > 0) return false; if (state.tagsViewList.length > 0) return false;
console.log(getRoutes(store.state.routesList.routesList));
getRoutes(store.state.routesList.routesList).map((v: any) => { getRoutes(store.state.routesList.routesList).map((v: any) => {
if (!v.meta.isHide) { if (!v.meta.isHide) {
state.tagsViewList.push({ ...v }); state.tagsViewList.push({ ...v });

View File

@@ -72,6 +72,32 @@
<el-dialog width="75%" :title="`${db} 表信息`" :before-close="closeTableInfo" v-model="tableInfoDialog.visible"> <el-dialog width="75%" :title="`${db} 表信息`" :before-close="closeTableInfo" v-model="tableInfoDialog.visible">
<el-row class="mb10"> <el-row class="mb10">
<el-popover v-model:visible="showDumpInfo" :width="470" placement="right">
<template #reference>
<el-button class="ml5" type="success" size="small" @click="showDumpInfo = !showDumpInfo">导出</el-button>
</template>
<el-form-item label="导出内容: ">
<el-radio-group v-model="dumpInfo.type">
<el-radio :label="1" size="small">结构</el-radio>
<el-radio :label="2" size="small">数据</el-radio>
<el-radio :label="3" size="small">结构数据</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="导出表: ">
<el-table @selection-change="handleDumpTableSelectionChange" max-height="300" size="small" :data="tableInfoDialog.infos">
<el-table-column type="selection" width="45" />
<el-table-column property="tableName" label="表名" min-width="150" show-overflow-tooltip> </el-table-column>
<el-table-column property="tableComment" label="备注" min-width="150" show-overflow-tooltip></el-table-column>
</el-table>
</el-form-item>
<div style="text-align: right">
<el-button @click="showDumpInfo = false" size="small">取消</el-button>
<el-button @click="dump(db)" type="success" size="small">确定</el-button>
</div>
</el-popover>
<el-button type="primary" size="small" @click="tableCreateDialog.visible = true">创建表</el-button> <el-button type="primary" size="small" @click="tableCreateDialog.visible = true">创建表</el-button>
</el-row> </el-row>
<el-table v-loading="tableInfoDialog.loading" border stripe :data="tableInfoDialog.infos" size="small"> <el-table v-loading="tableInfoDialog.loading" border stripe :data="tableInfoDialog.infos" size="small">
@@ -226,6 +252,10 @@ import { dbApi } from './api';
import enums from './enums'; import enums from './enums';
import { projectApi } from '../project/api.ts'; import { projectApi } from '../project/api.ts';
import SqlExecBox from './component/SqlExecBox.ts'; import SqlExecBox from './component/SqlExecBox.ts';
import config from '@/common/config';
import { getSession } from '@/common/utils/storage';
import { isTrue } from '@/common/assert';
export default defineComponent({ export default defineComponent({
name: 'DbList', name: 'DbList',
components: { components: {
@@ -255,6 +285,13 @@ export default defineComponent({
}, },
datas: [], datas: [],
total: 0, total: 0,
showDumpInfo: false,
dumpInfo: {
id: 0,
db: '',
type: 3,
tables: [],
},
// sql执行记录弹框 // sql执行记录弹框
sqlExecLogDialog: { sqlExecLogDialog: {
title: '', title: '',
@@ -392,6 +429,29 @@ export default defineComponent({
searchSqlExecLog(); searchSqlExecLog();
}; };
/**
* 选择导出数据库表
*/
const handleDumpTableSelectionChange = (vals: any) => {
state.dumpInfo.tables = vals.map((x: any) => x.tableName);
};
/**
* 数据库信息导出
*/
const dump = (db: string) => {
isTrue(state.dumpInfo.tables.length > 0, '请选择要导出的表');
const a = document.createElement('a');
a.setAttribute(
'href',
`${config.baseApiUrl}/dbs/${state.dbId}/dump?db=${db}&type=${state.dumpInfo.type}&tables=${state.dumpInfo.tables.join(
','
)}&token=${getSession('token')}`
);
a.click();
state.showDumpInfo = false;
};
const onShowRollbackSql = async (sqlExecLog: any) => { const onShowRollbackSql = async (sqlExecLog: any) => {
const columns = await dbApi.columnMetadata.request({ id: sqlExecLog.dbId, db: sqlExecLog.db, tableName: sqlExecLog.table }); const columns = await dbApi.columnMetadata.request({ id: sqlExecLog.dbId, db: sqlExecLog.db, tableName: sqlExecLog.table });
const primaryKey = columns[0].columnName; const primaryKey = columns[0].columnName;
@@ -447,6 +507,7 @@ export default defineComponent({
}; };
const closeTableInfo = () => { const closeTableInfo = () => {
state.showDumpInfo = false;
state.tableInfoDialog.visible = false; state.tableInfoDialog.visible = false;
state.tableInfoDialog.infos = []; state.tableInfoDialog.infos = [];
}; };
@@ -516,6 +577,8 @@ export default defineComponent({
valChange, valChange,
deleteDb, deleteDb,
onShowSqlExec, onShowSqlExec,
handleDumpTableSelectionChange,
dump,
onBeforeCloseSqlExecDialog, onBeforeCloseSqlExecDialog,
handleSqlExecPageChange, handleSqlExecPageChange,
searchSqlExecLog, searchSqlExecLog,

View File

@@ -927,7 +927,7 @@ export default defineComponent({
return; return;
} }
// 转为字符串比较,可能存在数字等 // 转为字符串比较,可能存在数字等
let text = row[property] + ''; let text = (row[property] ? row[property] : '') + '';
let div = cell.children[0]; let div = cell.children[0];
if (div) { if (div) {
let input = document.createElement('input'); let input = document.createElement('input');

View File

@@ -5,6 +5,7 @@ export const dbApi = {
dbs: Api.create("/dbs", 'get'), dbs: Api.create("/dbs", 'get'),
saveDb: Api.create("/dbs", 'post'), saveDb: Api.create("/dbs", 'post'),
deleteDb: Api.create("/dbs/{id}", 'delete'), deleteDb: Api.create("/dbs/{id}", 'delete'),
dumpDb: Api.create("/dbs/{id}/dump", 'post'),
tableInfos: Api.create("/dbs/{id}/t-infos", 'get'), tableInfos: Api.create("/dbs/{id}/t-infos", 'get'),
tableIndex: Api.create("/dbs/{id}/t-index", 'get'), tableIndex: Api.create("/dbs/{id}/t-index", 'get'),
tableDdl: Api.create("/dbs/{id}/t-create-ddl", 'get'), tableDdl: Api.create("/dbs/{id}/t-create-ddl", 'get'),

View File

@@ -2,7 +2,7 @@
<div> <div>
<el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px"> <el-dialog title="待执行SQL" v-model="dialogVisible" :show-close="false" width="600px">
<codemirror height="350px" class="codesql" ref="cmEditor" language="sql" v-model="sqlValue" :options="cmOptions" /> <codemirror height="350px" class="codesql" ref="cmEditor" language="sql" v-model="sqlValue" :options="cmOptions" />
<el-input v-model="remark" placeholder="请输入执行备注" class="mt5" /> <el-input ref="remarkInputRef" v-model="remark" placeholder="请输入执行备注" class="mt5" />
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="cancel"> </el-button> <el-button @click="cancel"> </el-button>
@@ -14,9 +14,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { toRefs, reactive, defineComponent } from 'vue'; import { toRefs, ref, nextTick, reactive, defineComponent } from 'vue';
import { dbApi } from '../api'; import { dbApi } from '../api';
import { ElDialog, ElButton, ElInput, ElMessage } from 'element-plus'; import { ElDialog, ElButton, ElInput, ElMessage, InputInstance } from 'element-plus';
// import base style // import base style
import 'codemirror/lib/codemirror.css'; import 'codemirror/lib/codemirror.css';
// 引入主题后还需要在 options 中指定主题才会生效 // 引入主题后还需要在 options 中指定主题才会生效
@@ -50,6 +50,7 @@ export default defineComponent({
}, },
}, },
setup(props: any) { setup(props: any) {
const remarkInputRef = ref<InputInstance>();
const state = reactive({ const state = reactive({
dialogVisible: false, dialogVisible: false,
sqlValue: '', sqlValue: '',
@@ -133,10 +134,14 @@ export default defineComponent({
state.dbId = props.dbId; state.dbId = props.dbId;
state.db = props.db; state.db = props.db;
state.dialogVisible = true; state.dialogVisible = true;
nextTick(() => {
remarkInputRef.value?.focus();
});
}; };
return { return {
...toRefs(state), ...toRefs(state),
remarkInputRef,
open, open,
runSql, runSql,
cancel, cancel,

View File

@@ -1,10 +1,10 @@
module mayfly-go module mayfly-go
go 1.17 go 1.18
require ( require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible // jwt github.com/dgrijalva/jwt-go v3.2.0+incompatible // jwt
github.com/gin-gonic/gin v1.7.7 github.com/gin-gonic/gin v1.8.1
github.com/go-redis/redis v6.15.9+incompatible github.com/go-redis/redis v6.15.9+incompatible
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/mojocn/base64Captcha v1.3.5 // github.com/mojocn/base64Captcha v1.3.5 //
@@ -27,8 +27,8 @@ require (
github.com/go-playground/validator/v10 v10.10.1 // indirect github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect github.com/go-stack/stack v1.8.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.1 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.4 // indirect github.com/jinzhu/now v1.1.4 // indirect
@@ -41,6 +41,7 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.18.1 // indirect github.com/onsi/gomega v1.18.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
@@ -48,9 +49,10 @@ require (
github.com/xdg-go/stringprep v1.0.2 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect golang.org/x/image v0.0.0-20220302094943-723b81ca9867 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

View File

@@ -2,7 +2,7 @@ package api
import ( import (
"fmt" "fmt"
"io/ioutil" "io"
"mayfly-go/internal/devops/api/form" "mayfly-go/internal/devops/api/form"
"mayfly-go/internal/devops/api/vo" "mayfly-go/internal/devops/api/vo"
"mayfly-go/internal/devops/application" "mayfly-go/internal/devops/application"
@@ -16,8 +16,10 @@ import (
"mayfly-go/pkg/ws" "mayfly-go/pkg/ws"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/xwb1989/sqlparser"
) )
type Db struct { type Db struct {
@@ -27,6 +29,8 @@ type Db struct {
ProjectApp application.Project ProjectApp application.Project
} }
const DEFAULT_COLUMN_SIZE = 500
// @router /api/dbs [get] // @router /api/dbs [get]
func (d *Db) Dbs(rc *ctx.ReqCtx) { func (d *Db) Dbs(rc *ctx.ReqCtx) {
g := rc.GinCtx g := rc.GinCtx
@@ -91,7 +95,7 @@ func (d *Db) ExecSql(rc *ctx.ReqCtx) {
rc.ReqParam = fmt.Sprintf("db: %d:%s | sql: %s", id, db, sql) rc.ReqParam = fmt.Sprintf("db: %d:%s | sql: %s", id, db, sql)
biz.NotEmpty(sql, "sql不能为空") biz.NotEmpty(sql, "sql不能为空")
if strings.HasPrefix(sql, "SELECT") || strings.HasPrefix(sql, "select") || strings.HasPrefix(sql, "show") { if strings.HasPrefix(sql, "SELECT") || strings.HasPrefix(sql, "select") || strings.HasPrefix(sql, "show") || strings.HasPrefix(sql, "explain") {
colNames, res, err := dbInstance.SelectData(sql) colNames, res, err := dbInstance.SelectData(sql)
biz.ErrIsNilAppendErr(err, "查询失败: %s") biz.ErrIsNilAppendErr(err, "查询失败: %s")
colAndRes := make(map[string]interface{}) colAndRes := make(map[string]interface{})
@@ -128,12 +132,8 @@ func (d *Db) ExecSqlFile(rc *ctx.ReqCtx) {
fileheader, err := g.FormFile("file") fileheader, err := g.FormFile("file")
biz.ErrIsNilAppendErr(err, "读取sql文件失败: %s") biz.ErrIsNilAppendErr(err, "读取sql文件失败: %s")
// 读取sql文件并根据;切割sql语句
file, _ := fileheader.Open() file, _ := fileheader.Open()
filename := fileheader.Filename filename := fileheader.Filename
bytes, _ := ioutil.ReadAll(file)
sqlContent := string(bytes)
sqls := strings.Split(sqlContent, ";")
dbId, db := GetIdAndDb(g) dbId, db := GetIdAndDb(g)
go func() { go func() {
@@ -153,12 +153,14 @@ func (d *Db) ExecSqlFile(rc *ctx.ReqCtx) {
biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, db.ProjectId), "%s") biz.ErrIsNilAppendErr(d.ProjectApp.CanAccess(rc.LoginAccount.Id, db.ProjectId), "%s")
for _, sql := range sqls { tokens := sqlparser.NewTokenizer(file)
sql = strings.Trim(sql, " ") for {
if sql == "" || sql == "\n" { stmt, err := sqlparser.ParseNext(tokens)
continue if err == io.EOF {
break
} }
_, err := db.Exec(sql) sql := sqlparser.String(stmt)
_, err = db.Exec(sql)
if err != nil { if err != nil {
d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrMsg("sql脚本执行失败", fmt.Sprintf("[%s]%s执行失败: [%s]", filename, dbInfo, err.Error()))) d.MsgApp.CreateAndSend(rc.LoginAccount, ws.ErrMsg("sql脚本执行失败", fmt.Sprintf("[%s]%s执行失败: [%s]", filename, dbInfo, err.Error())))
return return
@@ -168,6 +170,88 @@ func (d *Db) ExecSqlFile(rc *ctx.ReqCtx) {
}() }()
} }
// 数据库dump
func (d *Db) DumpSql(rc *ctx.ReqCtx) {
g := rc.GinCtx
dbId, db := GetIdAndDb(g)
dumpType := g.Query("type")
tablesStr := g.Query("tables")
biz.NotEmpty(tablesStr, "请选择要导出的表")
tables := strings.Split(tablesStr, ",")
// 是否需要导出表结构
needStruct := dumpType == "1" || dumpType == "3"
// 是否需要导出数据
needData := dumpType == "2" || dumpType == "3"
now := time.Now()
filename := fmt.Sprintf("%s.%s.sql", db, now.Format("200601021504"))
g.Header("Content-Type", "application/octet-stream")
g.Header("Content-Disposition", "attachment; filename="+filename)
rc.ReqParam = fmt.Sprintf("数据库id: %d -- %s", dbId, db)
dbInstance := d.DbApp.GetDbInstance(dbId, db)
writer := g.Writer
writer.WriteString("-- ----------------------------")
writer.WriteString("\n-- 导出平台: mayfly-go")
writer.WriteString(fmt.Sprintf("\n-- 导出时间: %s ", now.Format("2006-01-02 15:04:05")))
writer.WriteString(fmt.Sprintf("\n-- 导出数据库: %s ", db))
writer.WriteString("\n-- ----------------------------\n")
for _, table := range tables {
if needStruct {
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表结构: %s \n-- ----------------------------\n", table))
writer.WriteString(fmt.Sprintf("DROP TABLE IF EXISTS `%s`;\n", table))
writer.WriteString(dbInstance.GetCreateTableDdl(table)[0]["Create Table"].(string) + ";\n")
}
if !needData {
continue
}
writer.WriteString(fmt.Sprintf("\n-- ----------------------------\n-- 表记录: %s \n-- ----------------------------\n", table))
writer.WriteString("BEGIN;\n")
countSql := fmt.Sprintf("SELECT COUNT(*) count FROM %s", table)
_, countRes, _ := dbInstance.SelectData(countSql)
// 查询出所有列信息总数,手动分页获取所有数据
maCount := int(countRes[0]["count"].(int64))
// 计算需要查询的页数
pageNum := maCount / DEFAULT_COLUMN_SIZE
if maCount%DEFAULT_COLUMN_SIZE > 0 {
pageNum++
}
sqlTmp := "SELECT * FROM %s LIMIT %d, %d"
for index := 0; index < pageNum; index++ {
sql := fmt.Sprintf(sqlTmp, table, index*DEFAULT_COLUMN_SIZE, DEFAULT_COLUMN_SIZE)
columns, result, _ := dbInstance.SelectData(sql)
insertSql := "INSERT INTO `%s` VALUES (%s);\n"
for _, res := range result {
var values []string
for _, column := range columns {
value := res[column]
if value == nil {
values = append(values, "NULL")
continue
}
strValue, ok := value.(string)
if ok {
values = append(values, fmt.Sprintf("%#v", strValue))
} else {
values = append(values, utils.ToString(value))
}
}
writer.WriteString(fmt.Sprintf(insertSql, table, strings.Join(values, ", ")))
}
}
writer.WriteString("COMMIT;\n")
}
rc.NoRes = true
}
// @router /api/db/:dbId/t-metadata [get] // @router /api/db/:dbId/t-metadata [get]
func (d *Db) TableMA(rc *ctx.ReqCtx) { func (d *Db) TableMA(rc *ctx.ReqCtx) {
dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx)) dbi := d.DbApp.GetDbInstance(GetIdAndDb(rc.GinCtx))

View File

@@ -223,8 +223,9 @@ func (d *DbInstance) SelectData(execSql string) ([]string, []map[string]interfac
execSql = strings.Trim(execSql, " ") execSql = strings.Trim(execSql, " ")
isSelect := strings.HasPrefix(execSql, "SELECT") || strings.HasPrefix(execSql, "select") isSelect := strings.HasPrefix(execSql, "SELECT") || strings.HasPrefix(execSql, "select")
isShow := strings.HasPrefix(execSql, "show") isShow := strings.HasPrefix(execSql, "show")
isExplain := strings.HasPrefix(execSql, "explain")
if !isSelect && !isShow { if !isSelect && !isShow && !isExplain {
return nil, nil, errors.New("该sql非查询语句") return nil, nil, errors.New("该sql非查询语句")
} }
// 没加limit则默认限制50条 // 没加limit则默认限制50条
@@ -272,14 +273,13 @@ func (d *DbInstance) SelectData(execSql string) ([]string, []map[string]interfac
colName := colType.Name() colName := colType.Name()
// 字段类型名 // 字段类型名
colScanType := colType.ScanType().Name() colScanType := colType.ScanType().Name()
// 如果是密码字段,则脱敏显示
if colName == "password" {
v = []byte("******")
}
if isFirst { if isFirst {
colNames = append(colNames, colName) colNames = append(colNames, colName)
} }
if v == nil {
rowData[colName] = nil
continue
}
// 这里把[]byte数据转成string // 这里把[]byte数据转成string
stringV := string(v) stringV := string(v)
if stringV == "" { if stringV == "" {

View File

@@ -61,6 +61,12 @@ func InitDbRouter(router *gin.RouterGroup) {
rc.Handle(d.ExecSqlFile) rc.Handle(d.ExecSqlFile)
}) })
db.GET(":dbId/dump", func(g *gin.Context) {
ctx.NewReqCtxWithGin(g).
WithLog(ctx.NewLogInfo("Sql文件dump")).
Handle(d.DumpSql)
})
db.GET(":dbId/t-metadata", func(c *gin.Context) { db.GET(":dbId/t-metadata", func(c *gin.Context) {
ctx.NewReqCtxWithGin(c).Handle(d.TableMA) ctx.NewReqCtxWithGin(c).Handle(d.TableMA)
}) })

View File

@@ -16,7 +16,6 @@ type HandlerFunc func(*ReqCtx)
type ReqCtx struct { type ReqCtx struct {
GinCtx *gin.Context // gin context GinCtx *gin.Context // gin context
// NeedToken bool // 是否需要token
RequiredPermission *Permission // 需要的权限信息默认为nil需要校验token RequiredPermission *Permission // 需要的权限信息默认为nil需要校验token
LoginAccount *model.LoginAccount // 登录账号信息只有校验token后才会有值 LoginAccount *model.LoginAccount // 登录账号信息只有校验token后才会有值
@@ -26,7 +25,7 @@ type ReqCtx struct {
Err interface{} // 请求错误 Err interface{} // 请求错误
timed int64 // 执行时间 timed int64 // 执行时间
noRes bool // 无需返回结果,即文件下载等 NoRes bool // 无需返回结果,即文件下载等
} }
func (rc *ReqCtx) Handle(handler HandlerFunc) { func (rc *ReqCtx) Handle(handler HandlerFunc) {
@@ -55,13 +54,13 @@ func (rc *ReqCtx) Handle(handler HandlerFunc) {
begin := time.Now() begin := time.Now()
handler(rc) handler(rc)
rc.timed = time.Now().Sub(begin).Milliseconds() rc.timed = time.Now().Sub(begin).Milliseconds()
if !rc.noRes { if !rc.NoRes {
ginx.SuccessRes(ginCtx, rc.ResData) ginx.SuccessRes(ginCtx, rc.ResData)
} }
} }
func (rc *ReqCtx) Download(reader io.Reader, filename string) { func (rc *ReqCtx) Download(reader io.Reader, filename string) {
rc.noRes = true rc.NoRes = true
ginx.Download(rc.GinCtx, reader, filename) ginx.Download(rc.GinCtx, reader, filename)
} }

View File

@@ -2,6 +2,8 @@ package utils
import ( import (
"bytes" "bytes"
"encoding/json"
"strconv"
"strings" "strings"
"text/template" "text/template"
) )
@@ -107,3 +109,45 @@ func ReverStrTemplate(temp, str string, res map[string]interface{}) {
ReverStrTemplate(next, StrTrim(SubString(str, UnicodeIndex(str, value)+StrLen(value), StrLen(str))), res) ReverStrTemplate(next, StrTrim(SubString(str, UnicodeIndex(str, value)+StrLen(value), StrLen(str))), res)
} }
} }
func ToString(value interface{}) string {
// interface 转 string
var key string
if value == nil {
return key
}
switch it := value.(type) {
case float64:
return strconv.FormatFloat(it, 'f', -1, 64)
case float32:
return strconv.FormatFloat(float64(it), 'f', -1, 64)
case int:
return strconv.Itoa(it)
case uint:
return strconv.Itoa(int(it))
case int8:
return strconv.Itoa(int(it))
case uint8:
return strconv.Itoa(int(it))
case int16:
return strconv.Itoa(int(it))
case uint16:
return strconv.Itoa(int(it))
case int32:
return strconv.Itoa(int(it))
case uint32:
return strconv.Itoa(int(it))
case int64:
return strconv.FormatInt(it, 10)
case uint64:
return strconv.FormatUint(it, 10)
case string:
return it
case []byte:
return string(value.([]byte))
default:
newValue, _ := json.Marshal(value)
return string(newValue)
}
}