完成大部分基础功能
This commit is contained in:
21
util/reflect_util/reflect.go
Normal file
21
util/reflect_util/reflect.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package reflect_util
|
||||
|
||||
import "reflect"
|
||||
|
||||
func GetFields(t reflect.Type) []reflect.StructField {
|
||||
num := t.NumField()
|
||||
fields := make([]reflect.StructField, 0, num)
|
||||
for i := 0; i < num; i++ {
|
||||
fields = append(fields, t.Field(i))
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func GetFieldValues(v reflect.Value) []reflect.Value {
|
||||
num := v.NumField()
|
||||
fields := make([]reflect.Value, 0, num)
|
||||
for i := 0; i < num; i++ {
|
||||
fields = append(fields, v.Field(i))
|
||||
}
|
||||
return fields
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-layout-sider-zero-width-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -88,4 +88,27 @@ class DBInbound {
|
||||
const inbound = this.toInbound();
|
||||
return inbound.genLink(address, this.remark);
|
||||
}
|
||||
}
|
||||
|
||||
class AllSetting {
|
||||
webListen = "";
|
||||
webPort = 65432;
|
||||
webCertFile = "";
|
||||
webKeyFile = "";
|
||||
webBasePath = "/";
|
||||
|
||||
xrayTemplateConfig = "";
|
||||
|
||||
timeLocation = "Asia/Shanghai";
|
||||
|
||||
constructor(data) {
|
||||
if (data == null) {
|
||||
return
|
||||
}
|
||||
ObjectUtil.cloneProps(this, data);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return ObjectUtil.equals(this, other);
|
||||
}
|
||||
}
|
||||
@@ -270,4 +270,18 @@ class ObjectUtil {
|
||||
return obj;
|
||||
}
|
||||
|
||||
static equals(a, b) {
|
||||
for (const key in a) {
|
||||
if (!a.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
if (!b.hasOwnProperty(key)) {
|
||||
return false;
|
||||
} else if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strconv"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/entity"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
@@ -17,6 +18,7 @@ type XUIController struct {
|
||||
|
||||
inboundService service.InboundService
|
||||
xrayService service.XrayService
|
||||
settingService service.SettingService
|
||||
|
||||
isNeedXrayRestart atomic.Bool
|
||||
}
|
||||
@@ -39,6 +41,8 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/inbound/del/:id", a.delInbound)
|
||||
g.POST("/inbound/update/:id", a.updateInbound)
|
||||
g.GET("/setting", a.setting)
|
||||
g.POST("/setting/all", a.getAllSetting)
|
||||
g.POST("/setting/update", a.updateSetting)
|
||||
}
|
||||
|
||||
func (a *XUIController) startTask() {
|
||||
@@ -128,3 +132,23 @@ func (a *XUIController) updateInbound(c *gin.Context) {
|
||||
a.isNeedXrayRestart.Store(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *XUIController) getAllSetting(c *gin.Context) {
|
||||
allSetting, err := a.settingService.GetAllSetting()
|
||||
if err != nil {
|
||||
jsonMsg(c, "获取设置", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, allSetting, nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) updateSetting(c *gin.Context) {
|
||||
allSetting := &entity.AllSetting{}
|
||||
err := c.ShouldBind(allSetting)
|
||||
if err != nil {
|
||||
jsonMsg(c, "修改设置", err)
|
||||
return
|
||||
}
|
||||
err = a.settingService.UpdateAllSetting(allSetting)
|
||||
jsonMsg(c, "修改设置", err)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
"x-ui/util/common"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
type Msg struct {
|
||||
Success bool `json:"success"`
|
||||
Msg string `json:"msg"`
|
||||
@@ -15,3 +25,55 @@ type Pager struct {
|
||||
Key string `json:"key"`
|
||||
List interface{} `json:"list"`
|
||||
}
|
||||
|
||||
type AllSetting struct {
|
||||
WebListen string `json:"webListen" form:"webListen"`
|
||||
WebPort int `json:"webPort" form:"webPort"`
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||
|
||||
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
||||
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
}
|
||||
|
||||
func (s *AllSetting) CheckValid() error {
|
||||
if s.WebListen != "" {
|
||||
ip := net.ParseIP(s.WebListen)
|
||||
if ip == nil {
|
||||
return common.NewError("web listen is not valid ip:", s.WebListen)
|
||||
}
|
||||
}
|
||||
|
||||
if s.WebPort <= 0 || s.WebPort > 65535 {
|
||||
return common.NewError("web port is not a valid port:", s.WebPort)
|
||||
}
|
||||
|
||||
if s.WebCertFile != "" || s.WebKeyFile != "" {
|
||||
_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
|
||||
if err != nil {
|
||||
return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.WebCertFile, s.WebKeyFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s.WebBasePath, "/") {
|
||||
return common.NewErrorf("web base path must start with '/' : <%v>", s.WebBasePath)
|
||||
}
|
||||
if !strings.HasSuffix(s.WebBasePath, "/") {
|
||||
return common.NewErrorf("web base path must end with '/' : <%v>", s.WebBasePath)
|
||||
}
|
||||
|
||||
xrayConfig := &xray.Config{}
|
||||
err := json.Unmarshal([]byte(s.XrayTemplateConfig), xrayConfig)
|
||||
if err != nil {
|
||||
return common.NewError("xray template config invalid:", err)
|
||||
}
|
||||
|
||||
_, err = time.LoadLocation(s.TimeLocation)
|
||||
if err != nil {
|
||||
return common.NewError("time location not exist:", s.TimeLocation)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}xui/inbounds">
|
||||
<a-icon type="user"></a-icon>
|
||||
<span>账号列表</span>
|
||||
<span>入站列表</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}xui/setting">
|
||||
<a-icon type="setting"></a-icon>
|
||||
|
||||
29
web/html/xui/component/setting.html
Normal file
29
web/html/xui/component/setting.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{{define "component/settingListItem"}}
|
||||
<a-list-item style="padding: 20px">
|
||||
<a-row>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta :title="title" :description="desc"/>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template v-if="type === 'text'">
|
||||
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
||||
</template>
|
||||
<template v-else-if="type === 'number'">
|
||||
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
||||
</template>
|
||||
<template v-else-if="type === 'textarea'">
|
||||
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 6, maxRows: 6 }"></a-textarea>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
{{end}}
|
||||
|
||||
{{define "component/setting"}}
|
||||
<script>
|
||||
Vue.component('setting-list-item', {
|
||||
props: ["type", "title", "desc", "value"],
|
||||
template: `{{template "component/settingListItem"}}`,
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -2,8 +2,10 @@
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-col-sm-24 {
|
||||
@@ -133,7 +135,7 @@
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
ip: location.hostname,
|
||||
siderDrawer,
|
||||
spinning: false,
|
||||
dbInbounds: [],
|
||||
searchKey: '',
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-col-sm-24 {
|
||||
@@ -273,6 +275,7 @@
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
status: new Status(),
|
||||
versionModal,
|
||||
spinning: false,
|
||||
|
||||
125
web/html/xui/setting.html
Normal file
125
web/html/xui/setting.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-col-sm-24 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-tabs-top-bar {
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak>
|
||||
{{ template "commonSider" . }}
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip="loading">
|
||||
<a-space direction="vertical">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">保存配置</a-button>
|
||||
<a-tabs default-active-key="1">
|
||||
<a-tab-pane key="1" tab="面板配置">
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<setting-list-item type="text" title="面板监听 IP" desc="默认留空监听所有 IP" v-model="allSetting.webListen"></setting-list-item>
|
||||
<setting-list-item type="number" title="面板监听端口" v-model.number="allSetting.webPort"></setting-list-item>
|
||||
<setting-list-item type="text" title="面板证书公钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾" v-model="allSetting.webBasePath"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="用户设置">
|
||||
<a-form style="background: white; padding: 20px">
|
||||
<a-form-item label="原用户名">
|
||||
<a-input></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="原密码">
|
||||
<a-input></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="新用户名">
|
||||
<a-input></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="新密码">
|
||||
<a-input></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab="xray 相关设置">
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab="其他设置">
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行" v-model="allSetting.timeLocation"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-space>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/setting"}}
|
||||
<script>
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
siderDrawer,
|
||||
spinning: false,
|
||||
oldAllSetting: new AllSetting(),
|
||||
allSetting: new AllSetting(),
|
||||
saveBtnDisable: true,
|
||||
},
|
||||
methods: {
|
||||
loading(spinning = true) {
|
||||
this.spinning = spinning;
|
||||
},
|
||||
async getAllSetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/all");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.oldAllSetting = new AllSetting(msg.obj);
|
||||
this.allSetting = new AllSetting(msg.obj);
|
||||
this.saveBtnDisable = true;
|
||||
}
|
||||
},
|
||||
async updateAllSetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
await this.getAllSetting();
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getAllSetting();
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(1000);
|
||||
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,22 +2,112 @@ package service
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/util/random"
|
||||
"x-ui/util/reflect_util"
|
||||
"x-ui/web/entity"
|
||||
)
|
||||
|
||||
//go:embed config.json
|
||||
var xrayTemplateConfig string
|
||||
|
||||
var defaultValueMap = map[string]string{
|
||||
"xrayTemplateConfig": xrayTemplateConfig,
|
||||
"webListen": "",
|
||||
"webPort": "65432",
|
||||
"webCertFile": "",
|
||||
"webKeyFile": "",
|
||||
"secret": random.Seq(32),
|
||||
"webBasePath": "/",
|
||||
"timeLocation": "Asia/Shanghai",
|
||||
}
|
||||
|
||||
type SettingService struct {
|
||||
}
|
||||
|
||||
func (s *SettingService) ClearSetting() error {
|
||||
func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
|
||||
db := database.GetDB()
|
||||
settings := make([]*model.Setting, 0)
|
||||
err := db.Model(model.Setting{}).Find(&settings).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allSetting := &entity.AllSetting{}
|
||||
t := reflect.TypeOf(allSetting).Elem()
|
||||
v := reflect.ValueOf(allSetting).Elem()
|
||||
fields := reflect_util.GetFields(t)
|
||||
|
||||
setSetting := func(key, value string) (err error) {
|
||||
defer func() {
|
||||
panicErr := recover()
|
||||
if panicErr != nil {
|
||||
err = errors.New(fmt.Sprint(panicErr))
|
||||
}
|
||||
}()
|
||||
|
||||
var found bool
|
||||
var field reflect.StructField
|
||||
for _, f := range fields {
|
||||
if f.Tag.Get("json") == key {
|
||||
field = f
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// 有些设置自动生成,不需要返回到前端给用户修改
|
||||
return nil
|
||||
}
|
||||
|
||||
fieldV := v.FieldByName(field.Name)
|
||||
switch t := fieldV.Interface().(type) {
|
||||
case int:
|
||||
n, err := strconv.ParseInt(value, 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fieldV.SetInt(n)
|
||||
case string:
|
||||
fieldV.SetString(value)
|
||||
default:
|
||||
return common.NewErrorf("unknown field %v type %v", key, t)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
keyMap := map[string]bool{}
|
||||
for _, setting := range settings {
|
||||
err := setSetting(setting.Key, setting.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyMap[setting.Key] = true
|
||||
}
|
||||
|
||||
for key, value := range defaultValueMap {
|
||||
if keyMap[key] {
|
||||
continue
|
||||
}
|
||||
err := setSetting(key, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return allSetting, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) ResetSettings() error {
|
||||
db := database.GetDB()
|
||||
return db.Delete(model.Setting{}).Error
|
||||
}
|
||||
@@ -48,18 +138,22 @@ func (s *SettingService) saveSetting(key string, value string) error {
|
||||
return db.Save(setting).Error
|
||||
}
|
||||
|
||||
func (s *SettingService) getString(key string, defaultValue string) (string, error) {
|
||||
func (s *SettingService) getString(key string) (string, error) {
|
||||
setting, err := s.getSetting(key)
|
||||
if database.IsNotFound(err) {
|
||||
return defaultValue, nil
|
||||
value, ok := defaultValueMap[key]
|
||||
if !ok {
|
||||
return "", common.NewErrorf("key <%v> not in defaultValueMap", key)
|
||||
}
|
||||
return value, nil
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) getInt(key string, defaultValue int) (int, error) {
|
||||
str, err := s.getString(key, strconv.Itoa(defaultValue))
|
||||
func (s *SettingService) getInt(key string) (int, error) {
|
||||
str, err := s.getString(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -67,29 +161,28 @@ func (s *SettingService) getInt(key string, defaultValue int) (int, error) {
|
||||
}
|
||||
|
||||
func (s *SettingService) GetXrayConfigTemplate() (string, error) {
|
||||
return s.getString("xray_template_config", xrayTemplateConfig)
|
||||
return s.getString("xrayTemplateConfig")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetListen() (string, error) {
|
||||
return s.getString("web_listen", "")
|
||||
return s.getString("webListen")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetPort() (int, error) {
|
||||
return s.getInt("web_port", 65432)
|
||||
return s.getInt("webPort")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetCertFile() (string, error) {
|
||||
return s.getString("web_cert_file", "")
|
||||
return s.getString("webCertFile")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetKeyFile() (string, error) {
|
||||
return s.getString("web_key_file", "")
|
||||
return s.getString("webKeyFile")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||
seq := random.Seq(32)
|
||||
secret, err := s.getString("secret", seq)
|
||||
if secret == seq {
|
||||
secret, err := s.getString("secret")
|
||||
if secret == defaultValueMap["secret"] {
|
||||
err := s.saveSetting("secret", secret)
|
||||
if err != nil {
|
||||
logger.Warning("save secret failed:", err)
|
||||
@@ -99,7 +192,7 @@ func (s *SettingService) GetSecret() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (s *SettingService) GetBasePath() (string, error) {
|
||||
basePath, err := s.getString("web_base_path", "/")
|
||||
basePath, err := s.getString("webBasePath")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -113,15 +206,36 @@ func (s *SettingService) GetBasePath() (string, error) {
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTimeLocation() (*time.Location, error) {
|
||||
defaultLocation := "Asia/Shanghai"
|
||||
l, err := s.getString("time_location", defaultLocation)
|
||||
l, err := s.getString("timeLocation")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
location, err := time.LoadLocation(l)
|
||||
if err != nil {
|
||||
defaultLocation := defaultValueMap["timeLocation"]
|
||||
logger.Errorf("location <%v> not exist, using default location: %v", l, defaultLocation)
|
||||
return time.LoadLocation(defaultLocation)
|
||||
}
|
||||
return location, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||
if err := allSetting.CheckValid(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(allSetting).Elem()
|
||||
t := reflect.TypeOf(allSetting).Elem()
|
||||
fields := reflect_util.GetFields(t)
|
||||
errs := make([]error, 0)
|
||||
for _, field := range fields {
|
||||
key := field.Tag.Get("json")
|
||||
fieldV := v.FieldByName(field.Name)
|
||||
value := fmt.Sprint(fieldV.Interface())
|
||||
err := s.saveSetting(key, value)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return common.Combine(errs...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user