配置设置

dev
truthhun 2 years ago
parent 7c166b0393
commit 5cfe9eb906

@ -0,0 +1,50 @@
syntax = "proto3";
import "google/protobuf/timestamp.proto";
import "gogoproto/gogo.proto";
// import "validate/validate.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
package api.v1;
option go_package = "moredoc/api/v1;v1";
option java_multiple_files = true;
option java_package = "api.v1";
message Config {
int64 id = 1;
string label = 2;
string name = 3;
string value = 4;
string placeholder = 5;
string input_type = 6;
string category = 7;
int32 sort = 8;
string options = 9;
google.protobuf.Timestamp created_at = 10 [ (gogoproto.stdtime) = true ];
google.protobuf.Timestamp updated_at = 11 [ (gogoproto.stdtime) = true ];
}
message ListConfigRequest {
repeated string category = 1;
}
message Configs { repeated Config config = 1; }
service ConfigAPI {
// UpdateConfig
rpc UpdateConfig(Configs) returns (google.protobuf.Empty) {
option (google.api.http) = {
put : '/api/v1/config',
body : '*',
};
}
// ListConfig
rpc ListConfig(ListConfigRequest) returns (Configs) {
option (google.api.http) = {
get : '/api/v1/config/list',
};
}
}

@ -0,0 +1,78 @@
package biz
import (
"context"
"fmt"
pb "moredoc/api/v1"
"moredoc/middleware/auth"
"moredoc/model"
"moredoc/util"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
type ConfigAPIService struct {
pb.UnimplementedConfigAPIServer
dbModel *model.DBModel
logger *zap.Logger
}
func NewConfigAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *ConfigAPIService) {
return &ConfigAPIService{dbModel: dbModel, logger: logger.Named("ConfigAPIService")}
}
func (s *ConfigAPIService) checkPermission(ctx context.Context) (userClaims *auth.UserClaims, err error) {
return checkGRPCPermission(s.dbModel, ctx)
}
// UpdateConfig 更新配置
func (s *ConfigAPIService) UpdateConfig(ctx context.Context, req *pb.Configs) (*emptypb.Empty, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
var cfgs []*model.Config
err = util.CopyStruct(req.Config, &cfgs)
if err != nil {
s.logger.Error("util.CopyStruct", zap.Any("req", req), zap.Any("cfgs", cfgs), zap.Error(err))
fmt.Println(err.Error())
}
err = s.dbModel.UpdateConfigs(cfgs, "value")
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &emptypb.Empty{}, nil
}
// ListConfig 查询配置
func (s *ConfigAPIService) ListConfig(ctx context.Context, req *pb.ListConfigRequest) (*pb.Configs, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
opt := &model.OptionGetConfigList{
QueryIn: map[string][]interface{}{
"category": util.Slice2Interface(req.Category),
},
}
configs, err := s.dbModel.GetConfigList(opt)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
var pbConfigs []*pb.Config
util.CopyStruct(&configs, &pbConfigs)
s.logger.Debug("ListConfig", zap.Any("req", req), zap.Any("configs", configs), zap.Any("pbConfigs", pbConfigs))
return &pb.Configs{Config: pbConfigs}, nil
}

@ -1,10 +1,8 @@
package model
import (
"fmt"
"moredoc/util/captcha"
"strconv"
"strings"
"time"
jsoniter "github.com/json-iterator/go"
@ -23,8 +21,6 @@ const (
ConfigCategoryEmail = "email"
// ConfigCategoryCaptcha 验证码配置:是否启用验证码、验证码有效期、验证码长度、验证码类型等
ConfigCategoryCaptcha = "captcha"
// ConfigCategoryJWT JWT配置JWT有效期、JWT加密密钥等
ConfigCategoryJWT = "jwt"
// ConfigCategorySecurity 安全配置项
ConfigCategorySecurity = "security"
)
@ -37,27 +33,12 @@ type Config struct {
Placeholder string `form:"placeholder" json:"placeholder,omitempty" gorm:"column:placeholder;type:varchar(255);size:255;comment:提示信息;"`
InputType string `form:"input_type" json:"input_type,omitempty" gorm:"column:input_type;type:varchar(32);size:32;default:text;comment:输入类型;"`
Category string `form:"category" json:"category,omitempty" gorm:"column:category;type:varchar(32);size:32;index:name_category,unique;index:category;comment:所属类别;"`
Sort int `form:"sort" json:"sort,omitempty" gorm:"column:sort;type:int(11);size:11;default:0;comment:同一category下的排序;"`
Sort int `form:"sort" json:"sort,omitempty" gorm:"column:sort;type:int(11);size:11;default:0;comment:同一category下的排序,这里按顺序排序,值越小越靠前;"`
Options string `form:"options" json:"options,omitempty" gorm:"column:options;type:text;comment:针对checkbox等的枚举值;"`
CreatedAt *time.Time `form:"created_at" json:"created_at,omitempty" gorm:"column:created_at;type:datetime;comment:创建时间;"`
UpdatedAt *time.Time `form:"updated_at" json:"updated_at,omitempty" gorm:"column:updated_at;type:datetime;comment:更新时间;"`
}
// 这里是proto文件中的结构体可以根据需要删除或者调整
//message Config {
// int64 id = 1;
// string label = 2;
// string name = 3;
// string value = 4;
// int32 placeholder = 5;
// int32 input_type = 6;
// string category = 7;
// int32 sort = 8;
// string options = 9;
// google.protobuf.Timestamp created_at = 10 [ (gogoproto.stdtime) = true ];
// google.protobuf.Timestamp updated_at = 11 [ (gogoproto.stdtime) = true ];
//}
func (Config) TableName() string {
return tablePrefix + "config"
}
@ -89,6 +70,33 @@ func (m *DBModel) UpdateConfig(config *Config, updateFields ...string) (err erro
return
}
// UpdateConfigs 配置项批量更新
// TODO: value值为6个*的,需要特殊处理
func (m *DBModel) UpdateConfigs(configs []*Config, updateFields ...string) (err error) {
sess := m.db.Begin()
defer func() {
if err != nil {
sess.Rollback()
} else {
sess.Commit()
}
}()
tableName := Config{}.TableName()
updateFields = m.FilterValidFields(tableName, updateFields...)
if len(updateFields) == 0 {
updateFields = m.GetTableFields(tableName)
}
for _, config := range configs {
if err = sess.Select(updateFields).Save(config).Error; err != nil {
m.logger.Error("UpdateConfigs", zap.Error(err))
return
}
}
return
}
// GetConfig 根据id获取Config
func (m *DBModel) GetConfig(id interface{}, fields ...string) (config Config, err error) {
db := m.db
@ -124,89 +132,15 @@ func (m *DBModel) GetConfigByNameCategory(name string, category string, fields .
}
type OptionGetConfigList struct {
Page int
Size int
WithCount bool // 是否返回总数
Ids []interface{} // id列表
SelectFields []string // 查询字段
QueryRange map[string][2]interface{} // map[field][]{min,max}
QueryIn map[string][]interface{} // map[field][]{value1,value2,...}
QueryLike map[string][]interface{} // map[field][]{value1,value2,...}
Sort []string
SelectFields []string // 查询字段
QueryIn map[string][]interface{} // map[field][]{value1,value2,...}
}
// GetConfigList 获取Config列表
func (m *DBModel) GetConfigList(opt OptionGetConfigList) (configList []Config, total int64, err error) {
func (m *DBModel) GetConfigList(opt *OptionGetConfigList) (configList []Config, err error) {
db := m.db.Model(&Config{})
for field, rangeValue := range opt.QueryRange {
fields := m.FilterValidFields(Config{}.TableName(), field)
if len(fields) == 0 {
continue
}
if rangeValue[0] != nil {
db = db.Where(fmt.Sprintf("%s >= ?", field), rangeValue[0])
}
if rangeValue[1] != nil {
db = db.Where(fmt.Sprintf("%s <= ?", field), rangeValue[1])
}
}
for field, values := range opt.QueryIn {
fields := m.FilterValidFields(Config{}.TableName(), field)
if len(fields) == 0 {
continue
}
db = db.Where(fmt.Sprintf("%s in (?)", field), values)
}
for field, values := range opt.QueryLike {
fields := m.FilterValidFields(Config{}.TableName(), field)
if len(fields) == 0 {
continue
}
db = db.Where(strings.TrimSuffix(fmt.Sprintf(strings.Join(make([]string, len(values)+1), "%s like ? or"), field), "or"), values...)
}
if len(opt.Ids) > 0 {
db = db.Where("id in (?)", opt.Ids)
}
if opt.WithCount {
err = db.Count(&total).Error
if err != nil {
m.logger.Error("GetConfigList", zap.Error(err))
return
}
}
opt.SelectFields = m.FilterValidFields(Config{}.TableName(), opt.SelectFields...)
if len(opt.SelectFields) > 0 {
db = db.Select(opt.SelectFields)
}
if len(opt.Sort) > 0 {
var sorts []string
for _, sort := range opt.Sort {
slice := strings.Split(sort, " ")
if len(m.FilterValidFields(Config{}.TableName(), slice[0])) == 0 {
continue
}
if len(slice) == 2 {
sorts = append(sorts, fmt.Sprintf("%s %s", slice[0], slice[1]))
} else {
sorts = append(sorts, fmt.Sprintf("%s desc", slice[0]))
}
}
if len(sorts) > 0 {
db = db.Order(strings.Join(sorts, ","))
}
}
db = db.Offset((opt.Page - 1) * opt.Size).Limit(opt.Size)
err = db.Find(&configList).Error
db = m.generateQueryIn(db, Config{}.TableName(), opt.QueryIn)
err = db.Order("sort asc").Find(&configList).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigList", zap.Error(err))
}
@ -390,13 +324,13 @@ func (m *DBModel) initConfig() (err error) {
{Category: ConfigCategoryCaptcha, Name: ConfigCaptchaType, Label: "验证码类型", Value: "digit", Placeholder: "请选择验证码类型,默认为数字", InputType: "select", Sort: 16, Options: captcha.CaptchaTypeOptions},
// 安全配置项
{Category: ConfigCategorySecurity, Name: ConfigSecurityIsClose, Label: "是否关闭网站", Value: "false", Placeholder: "请选择是否关闭网站", InputType: "swith", Sort: 17, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableRegister, Label: "是否允许注册", Value: "true", Placeholder: "请选择是否允许用户注册", InputType: "swith", Sort: 18, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaLogin, Label: "是否开启登录验证码", Value: "true", Placeholder: "请选择是否开启登录验证码", InputType: "swith", Sort: 19, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaRegister, Label: "是否开启注册验证码", Value: "true", Placeholder: "请选择是否开启注册验证码", InputType: "swith", Sort: 20, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaComment, Label: "是否开启评论验证码", Value: "true", Placeholder: "请选择是否开启评论验证码", InputType: "swith", Sort: 21, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaFindPassword, Label: "是否开启找回密码验证码", Value: "true", Placeholder: "请选择是否开启找回密码验证码", InputType: "swith", Sort: 22, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaUpload, Label: "是否开启文档上传验证码", Value: "true", Placeholder: "请选择是否开启文档上传验证码", InputType: "swith", Sort: 23, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityIsClose, Label: "是否关闭网站", Value: "false", Placeholder: "请选择是否关闭网站", InputType: "switch", Sort: 17, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableRegister, Label: "是否允许注册", Value: "true", Placeholder: "请选择是否允许用户注册", InputType: "switch", Sort: 18, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaLogin, Label: "是否开启登录验证码", Value: "true", Placeholder: "请选择是否开启登录验证码", InputType: "switch", Sort: 19, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaRegister, Label: "是否开启注册验证码", Value: "true", Placeholder: "请选择是否开启注册验证码", InputType: "switch", Sort: 20, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaComment, Label: "是否开启评论验证码", Value: "true", Placeholder: "请选择是否开启评论验证码", InputType: "switch", Sort: 21, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaFindPassword, Label: "是否开启找回密码验证码", Value: "true", Placeholder: "请选择是否开启找回密码验证码", InputType: "switch", Sort: 22, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaUpload, Label: "是否开启文档上传验证码", Value: "true", Placeholder: "请选择是否开启文档上传验证码", InputType: "switch", Sort: 23, Options: ""},
}
for _, cfg := range cfgs {

@ -68,5 +68,14 @@ func RegisterGRPCService(dbModel *model.DBModel, logger *zap.Logger, endpoint st
return
}
// Config API接口服务
configAPIService := biz.NewConfigAPIService(dbModel, logger)
v1.RegisterConfigAPIServer(grpcServer, configAPIService)
err = v1.RegisterConfigAPIHandlerFromEndpoint(context.Background(), gwmux, endpoint, dialOpts)
if err != nil {
logger.Error("RegisterConfigAPIHandlerFromEndpoint", zap.Error(err))
return
}
return
}

@ -13,9 +13,12 @@ import (
)
// CopyStruct 拷贝。注意只能拷贝相同类型的结构体且结构体中有json标签
func CopyStruct(srcPtr, dstPtr interface{}) {
bytes, _ := jsoniter.Marshal(srcPtr)
jsoniter.Unmarshal(bytes, dstPtr)
func CopyStruct(srcPtr, dstPtr interface{}) (err error) {
bytes, err := jsoniter.Marshal(srcPtr)
if err != nil {
return err
}
return jsoniter.Unmarshal(bytes, dstPtr)
}
// GetGRPCRemoteIP 获取用户IP

@ -0,0 +1,19 @@
import service from '~/utils/request'
export const updateConfig = (data) => {
return service({
url: '/api/v1/config',
method: 'put',
data,
})
}
export const listConfig = (params) => {
return service({
url: '/api/v1/config/list',
method: 'get',
params,
})
}

@ -0,0 +1,129 @@
<template>
<div class="com-form-config">
<el-form
ref="formConfig"
label-position="top"
label-width="80px"
:model="{}"
>
<el-form-item
v-for="(item, index) in configs"
:key="'cfg-' + item.id"
:label="item.label + '' + item.placeholder + ''"
>
<el-input-number
v-if="item.input_type === 'number'"
v-model="configs[index]['value']"
clearable
:min="0"
:placeholder="item.placeholder"
:step="1"
></el-input-number>
<el-input
v-else-if="item.input_type === 'textarea'"
v-model="configs[index]['value']"
type="textarea"
:placeholder="item.placeholder"
rows="5"
></el-input>
<el-select
v-else-if="item.input_type === 'select'"
v-model="configs[index]['value']"
>
<el-option
v-for="option in item.options.split('\n')"
:key="'option-' + option"
:label="option.split(':')[1]"
:value="option.split(':')[0]"
></el-option>
</el-select>
<el-switch
v-else-if="item.input_type === 'switch'"
v-model="configs[index]['value']"
active-color="#13ce66"
inactive-color="#ff4949"
active-text="是"
inactive-text="否"
:active-value="'true'"
:inactive-value="'false'"
>
</el-switch>
<el-input
v-else
v-model="configs[index]['value']"
:placeholder="item.placeholder"
clearable
></el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
class="btn-block"
icon="el-icon-check"
:loading="loading"
@click="onSubmit"
>提交</el-button
>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { updateConfig } from '~/api/config'
export default {
name: 'FormConfig',
props: {
initConfigs: {
type: Array,
default: () => {
return []
},
},
},
data() {
return {
loading: false,
configs: [],
}
},
watch: {
initConfigs: {
handler(val) {
this.configs = val
},
immediate: true,
},
},
created() {
this.configs = this.initConfigs
},
methods: {
async onSubmit() {
this.loading = true
const configs = []
this.configs.forEach((item) => {
// valuevalue
let value = ''
try {
value = item.value.toString()
} catch (error) {}
configs.push({ ...item, value })
})
const res = await updateConfig({ config: configs })
if (res.status === 200) {
this.$message.success('配置更新成功')
} else {
this.$message.error('配置更新失败')
}
this.loading = false
},
},
}
</script>
<style lang="scss">
.com-form-config {
.el-form-item__label {
padding-bottom: 0;
}
}
</style>

@ -1,10 +1,69 @@
<template>
<div>{{ $route.name }}</div>
<el-card shadow="never">
<el-tabs v-model="activeName" type="card" @tab-click="handleClick">
<el-tab-pane
v-for="item in categories"
:key="'category-' + item.value"
:label="item.label"
:name="item.value"
>
</el-tab-pane>
</el-tabs>
<FormConfig :init-configs="configs" />
</el-card>
</template>
<script>
import { listConfig } from '~/api/config'
import FormConfig from '~/components/FormConfig.vue'
export default {
components: { FormConfig },
layout: 'admin',
created() {},
data() {
return {
activeName: 'system',
configs: [],
categories: [
{
label: '系统配置',
value: 'system',
},
// {
// label: '',
// value: 'user',
// },
// {
// label: '',
// value: 'email',
// },
{
label: '验证码配置',
value: 'captcha',
},
{
label: '安全配置',
value: 'security',
},
],
}
},
created() {
this.loadConfig()
},
methods: {
handleClick(tab) {
this.activeName = tab.name
this.loadConfig()
},
async loadConfig() {
const res = await listConfig({ category: [this.activeName] })
if (res.status === 200) {
this.configs = res.data.config || []
} else {
this.configs = []
this.$message.error(res.data.message)
}
},
},
}
</script>

Loading…
Cancel
Save