support composite feature that server supported just by configure file

pull/62/head
alimy 3 years ago
parent 6efbf08638
commit 89dd735e61

@ -5,28 +5,33 @@ App: # APP基础设置项
DefaultContextTimeout: 60
DefaultPageSize: 10
MaxPageSize: 100
SmsJuheKey:
SmsJuheTplID:
SmsJuheTplVal: "#code#=%d&#m#=%d"
AlipayAppID:
AlipayPrivateKey:
Runtime: # App运行时功能调节
DisablePhoneVerify: False # 禁止绑定手机号码时验证短信验证码为true时任意验证码都可以通过验证
Server: # 服务设置
RunMode: debug
HttpIp: 0.0.0.0
HttpPort: 8008
ReadTimeout: 60
WriteTimeout: 60
Log: # 日志
LogType: zinc # 可选file或zinc
LogFileSavePath: storage/logs
LogFileName: app
LogFileExt: .log
LogZincHost: http://127.0.0.1:4080/es/_bulk
LogZincIndex: paopao-log
LogZincUser: admin
LogZincPassword: admin
Features:
Default: ["Sms", "Alipay", "Zinc", "MySQL", "Redis", "AliOSS", "LoggerZinc"]
Develop: ["Zinc", "MySQL", "AliOSS", "LoggerFile"]
Slim: ["Zinc", "MySQL", "Redis", "AliOSS", "LoggerFile"]
Sms: "SmsJuhe"
SmsJuhe:
Key:
TplID:
TplVal: "#code#=%d&#m#=%d"
Alipay:
AppID:
PrivateKey:
LoggerFile: # 使用File写日志
SavePath: storage/logs
FileName: app
FileExt: .log
LoggerZinc: # 使用Zinc写日志
Host: http://127.0.0.1:4080/es/_bulk
Index: paopao-log
User: admin
Password: admin
JWT: # 鉴权加密
Secret: 18a6413dc4fe394c66345ebe501b2f26
Issuer: paopao-api
@ -36,14 +41,18 @@ Search: # 搜索配置
ZincIndex: paopao-data
ZincUser: admin
ZincPassword: admin
Storage: # 阿里云OSS存储配置
AliossAccessKeyID:
AliossAccessKeySecret:
AliossEndpoint:
AliossBucket:
AliossDomain:
Database: # 数据库
DBType: mysql
Zinc: # Zinc搜索配置
Host: http://127.0.0.1:4080
Index: paopao-data
User: admin
Password: admin
AliOSS: # 阿里云OSS存储配置
AccessKeyID:
AccessKeySecret:
Endpoint:
Bucket:
Domain:
MySQL: # MySQL数据库
Username: root # 填写你的数据库账号
Password: root # 填写你的数据库密码
Host: 127.0.0.1:3306

@ -8,15 +8,26 @@ import (
)
var (
Features *setting.FeaturesSettingS
ServerSetting *setting.ServerSettingS
AppSetting *setting.AppSettingS
RuntimeSetting *setting.RuntimeSettingS
DatabaseSetting *setting.DatabaseSettingS
MySQLSetting *setting.MySQLSettingS
RedisSetting *setting.RedisSettingS
SearchSetting *setting.SearchSettingS
AliossSetting *setting.AliossSettingS
SmsJuheSetting *setting.SmsJuheSettings
AlipaySetting *setting.AlipaySettingS
ZincSetting *setting.ZincSettingS
AliOSSSetting *setting.AliOSSSettingS
JWTSetting *setting.JWTSettingS
LoggerSetting *setting.LoggerSettingS
LoggerFileSetting *setting.LoggerFileSettingS
LoggerZincSetting *setting.LoggerZincSettingS
Logger *logrus.Logger
Mutex *sync.Mutex
)
func Cfg(key string) (string, bool) {
return Features.Cfg(key)
}
func CfgIf(expression string) bool {
return Features.CfgIf(expression)
}

@ -27,7 +27,7 @@ func init() {
if err != nil {
log.Fatalf("init.setupDBEngine err: %v", err)
}
client := zinc.NewClient(global.SearchSetting)
client := zinc.NewClient(global.ZincSetting)
service.Initialize(global.DBEngine, client)
}
@ -37,40 +37,22 @@ func setupSetting() error {
return err
}
err = setting.ReadSection("Server", &global.ServerSetting)
if err != nil {
return err
}
err = setting.ReadSection("App", &global.AppSetting)
if err != nil {
return err
}
err = setting.ReadSection("Runtime", &global.RuntimeSetting)
if err != nil {
return err
}
err = setting.ReadSection("Log", &global.LoggerSetting)
if err != nil {
return err
}
err = setting.ReadSection("Database", &global.DatabaseSetting)
if err != nil {
return err
}
err = setting.ReadSection("Search", &global.SearchSetting)
if err != nil {
return err
}
err = setting.ReadSection("Redis", &global.RedisSetting)
if err != nil {
return err
}
err = setting.ReadSection("JWT", &global.JWTSetting)
if err != nil {
return err
global.Features = setting.FeaturesFrom("Features")
objects := map[string]interface{}{
"App": &global.AppSetting,
"Server": &global.ServerSetting,
"Alipay": &global.AlipaySetting,
"SmsJuhe": &global.SmsJuheSetting,
"LoggerFile": &global.LoggerFileSetting,
"LoggerZinc": &global.LoggerZincSetting,
"MySQL": &global.MySQLSetting,
"Zinc": &global.ZincSetting,
"Redis": &global.RedisSetting,
"JWT": &global.JWTSetting,
"AliOSS": &global.AliOSSSetting,
}
err = setting.ReadSection("Storage", &global.AliossSetting)
if err != nil {
if err = setting.Unmarshal(objects); err != nil {
return err
}
@ -82,7 +64,7 @@ func setupSetting() error {
}
func setupLogger() error {
logger, err := logger.New(global.LoggerSetting)
logger, err := logger.New()
if err != nil {
return err
}
@ -91,9 +73,10 @@ func setupLogger() error {
return nil
}
// setupDBEngine 暂时只支持MySQL
func setupDBEngine() error {
var err error
global.DBEngine, err = model.NewDBEngine(global.DatabaseSetting)
global.DBEngine, err = model.NewDBEngine(global.MySQLSetting)
if err != nil {
return err
}

@ -127,9 +127,9 @@ func (d *dataServant) SendPhoneCaptcha(phone string) error {
resp, err := client.R().
SetFormData(map[string]string{
"mobile": phone,
"tpl_id": global.AppSetting.SmsJuheTplID,
"tpl_value": fmt.Sprintf(global.AppSetting.SmsJuheTplVal, captcha, m),
"key": global.AppSetting.SmsJuheKey,
"tpl_id": global.SmsJuheSetting.TplID,
"tpl_value": fmt.Sprintf(global.SmsJuheSetting.TplVal, captcha, m),
"key": global.SmsJuheSetting.Key,
}).Post(gateway)
if err != nil {
return err

@ -25,7 +25,7 @@ type Model struct {
type ConditionsT map[string]interface{}
func NewDBEngine(databaseSetting *setting.DatabaseSettingS) (*gorm.DB, error) {
func NewDBEngine(databaseSetting *setting.MySQLSettingS) (*gorm.DB, error) {
newLogger := logger.New(
global.Logger, // io writer日志输出的目标前缀和日志包含的内容
logger.Config{

@ -97,14 +97,14 @@ func UploadAttachment(c *gin.Context) {
randomPath := uuid.Must(uuid.NewV4()).String()
ossSavePath := uploadType + "/" + GeneratePath(randomPath[:8]) + "/" + randomPath[9:] + fileExt
client, err := oss.New(global.AliossSetting.AliossEndpoint, global.AliossSetting.AliossAccessKeyID, global.AliossSetting.AliossAccessKeySecret)
client, err := oss.New(global.AliOSSSetting.Endpoint, global.AliOSSSetting.AccessKeyID, global.AliOSSSetting.AccessKeySecret)
if err != nil {
global.Logger.Errorf("oss.New err: %v", err)
response.ToErrorResponse(errcode.FileUploadFailed)
return
}
bucket, err := client.Bucket(global.AliossSetting.AliossBucket)
bucket, err := client.Bucket(global.AliOSSSetting.Bucket)
if err != nil {
global.Logger.Errorf("client.Bucket err: %v", err)
response.ToErrorResponse(errcode.FileUploadFailed)
@ -121,7 +121,7 @@ func UploadAttachment(c *gin.Context) {
// 构造附件Model
attachment := &model.Attachment{
FileSize: fileHeader.Size,
Content: "https://" + global.AliossSetting.AliossDomain + "/" + ossSavePath,
Content: "https://" + global.AliOSSSetting.Domain + "/" + ossSavePath,
}
if userID, exists := c.Get("UID"); exists {
@ -256,14 +256,14 @@ func DownloadAttachment(c *gin.Context) {
}
// 开始构造下载地址
client, err := oss.New(global.AliossSetting.AliossEndpoint, global.AliossSetting.AliossAccessKeyID, global.AliossSetting.AliossAccessKeySecret)
client, err := oss.New(global.AliOSSSetting.Endpoint, global.AliOSSSetting.AccessKeyID, global.AliOSSSetting.AccessKeySecret)
if err != nil {
global.Logger.Errorf("oss.New err: %v", err)
response.ToErrorResponse(errcode.DownloadReqError)
return
}
bucket, err := client.Bucket(global.AliossSetting.AliossBucket)
bucket, err := client.Bucket(global.AliOSSSetting.Bucket)
if err != nil {
global.Logger.Errorf("client.Bucket err: %v", err)
response.ToErrorResponse(errcode.DownloadReqError)
@ -271,7 +271,7 @@ func DownloadAttachment(c *gin.Context) {
}
// 签名
objectKey := strings.Replace(content.Content, "https://"+global.AliossSetting.AliossDomain+"/", "", -1)
objectKey := strings.Replace(content.Content, "https://"+global.AliOSSSetting.Domain+"/", "", -1)
signedURL, err := bucket.SignURL(objectKey, oss.HTTPGet, 60)
if err != nil {
global.Logger.Errorf("client.SignURL err: %v", err)

@ -204,7 +204,7 @@ func ChangeAvatar(c *gin.Context) {
user = u.(*model.User)
}
if strings.Index(param.Avatar, "https://"+global.AliossSetting.AliossDomain) != 0 {
if strings.Index(param.Avatar, "https://"+global.AliOSSSetting.Domain) != 0 {
response.ToErrorResponse(errcode.InvalidParams)
return
}
@ -238,14 +238,11 @@ func BindUserPhone(c *gin.Context) {
return
}
// 验证短信验证码
if !global.RuntimeSetting.DisablePhoneVerify {
if err := service.CheckPhoneCaptcha(param.Phone, param.Captcha); err != nil {
global.Logger.Errorf("service.CheckPhoneCaptcha err: %v\n", err)
response.ToErrorResponse(err)
return
}
}
// 执行绑定
user.Phone = param.Phone
@ -383,7 +380,7 @@ func GetUserRechargeLink(c *gin.Context) {
}
client, err := alipay.New(global.AppSetting.AlipayAppID, global.AppSetting.AlipayPrivateKey, true)
client, err := alipay.New(global.AlipaySetting.AppID, global.AlipaySetting.PrivateKey, true)
// 将 key 的验证调整到初始化阶段
if err != nil {
global.Logger.Errorf("alipay.New err: %v\n", err)
@ -462,7 +459,7 @@ func AlipayNotify(c *gin.Context) {
response := app.NewResponse(c)
c.Request.ParseForm()
aliClient, err := alipay.New(global.AppSetting.AlipayAppID, global.AppSetting.AlipayPrivateKey, true)
aliClient, err := alipay.New(global.AlipaySetting.AppID, global.AlipaySetting.PrivateKey, true)
// 将 key 的验证调整到初始化阶段
if err != nil {
global.Logger.Errorf("alipay.New err: %v\n", err)

@ -116,7 +116,7 @@ func CreatePostComment(ctx *gin.Context, userID int64, param CommentCreationReq)
for _, item := range param.Contents {
// 检查附件是否是本站资源
if item.Type == model.CONTENT_TYPE_IMAGE || item.Type == model.CONTENT_TYPE_VIDEO || item.Type == model.CONTENT_TYPE_ATTACHMENT {
if strings.Index(item.Content, "https://"+global.AliossSetting.AliossDomain) != 0 {
if strings.Index(item.Content, "https://"+global.AliOSSSetting.Domain) != 0 {
continue
}
}

@ -68,7 +68,7 @@ func (p *PostContentItem) Check() error {
// 检查附件是否是本站资源
if p.Type == model.CONTENT_TYPE_IMAGE || p.Type == model.CONTENT_TYPE_VIDEO || p.Type == model.
CONTENT_TYPE_ATTACHMENT {
if strings.Index(p.Content, "https://"+global.AliossSetting.AliossDomain) != 0 {
if strings.Index(p.Content, "https://"+global.AliOSSSetting.Domain) != 0 {
return fmt.Errorf("附件非本站资源")
}
}
@ -377,7 +377,7 @@ func GetPostCount(conditions *model.ConditionsT) (int64, error) {
}
func GetPostListFromSearch(q *core.QueryT, offset, limit int) ([]*model.PostFormated, int64, error) {
queryResult, err := ds.QueryAll(q, global.SearchSetting.ZincIndex, offset, limit)
queryResult, err := ds.QueryAll(q, global.ZincSetting.Index, offset, limit)
if err != nil {
return nil, 0, err
}
@ -391,7 +391,7 @@ func GetPostListFromSearch(q *core.QueryT, offset, limit int) ([]*model.PostForm
}
func GetPostListFromSearchByQuery(query string, offset, limit int) ([]*model.PostFormated, int64, error) {
queryResult, err := ds.QuerySearch(global.SearchSetting.ZincIndex, query, offset, limit)
queryResult, err := ds.QuerySearch(global.ZincSetting.Index, query, offset, limit)
if err != nil {
return nil, 0, err
}
@ -405,7 +405,7 @@ func GetPostListFromSearchByQuery(query string, offset, limit int) ([]*model.Pos
}
func PushPostToSearch(post *model.Post) {
indexName := global.SearchSetting.ZincIndex
indexName := global.ZincSetting.Index
postFormated := post.Format()
postFormated.User = &model.UserFormated{
@ -456,7 +456,7 @@ func PushPostToSearch(post *model.Post) {
}
func DeleteSearchPost(post *model.Post) error {
indexName := global.SearchSetting.ZincIndex
indexName := global.ZincSetting.Index
return ds.DelDoc(indexName, fmt.Sprintf("%d", post.ID))
}
@ -469,7 +469,7 @@ func PushPostsToSearch(c *gin.Context) {
pages := math.Ceil(float64(totalRows) / float64(splitNum))
nums := int(pages)
indexName := global.SearchSetting.ZincIndex
indexName := global.ZincSetting.Index
// 创建索引
ds.CreateSearchIndex(indexName)

@ -1,6 +1,7 @@
package service
import (
"github.com/rocboss/paopao-ce/global"
"github.com/rocboss/paopao-ce/internal/core"
"github.com/rocboss/paopao-ce/internal/dao"
"github.com/rocboss/paopao-ce/pkg/zinc"
@ -9,8 +10,10 @@ import (
var (
ds core.DataService
DisablePhoneVerify bool
)
func Initialize(engine *gorm.DB, client *zinc.ZincClient) {
ds = dao.NewDataService(engine, client)
DisablePhoneVerify = !global.CfgIf("Sms")
}

@ -136,6 +136,11 @@ func CheckPassword(password string) error {
// CheckPhoneCaptcha 验证手机验证码
func CheckPhoneCaptcha(phone, captcha string) *errcode.Error {
// 如果禁止phone verify 则允许通过任意验证码
if DisablePhoneVerify {
return nil
}
c, err := ds.GetLatestPhoneCaptcha(phone)
if err != nil {
return errcode.ErrorPhoneCaptcha

@ -7,7 +7,6 @@ import (
"time"
"github.com/rocboss/paopao-ce/global"
"github.com/rocboss/paopao-ce/pkg/setting"
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
"gopkg.in/resty.v1"
@ -31,7 +30,7 @@ type ZincLogHook struct {
func (hook *ZincLogHook) Fire(entry *logrus.Entry) error {
index := &ZincLogIndex{
Index: map[string]string{
"_index": global.LoggerSetting.LogZincIndex,
"_index": global.LoggerZincSetting.Index,
},
}
indexBytes, _ := json.Marshal(index)
@ -49,9 +48,9 @@ func (hook *ZincLogHook) Fire(entry *logrus.Entry) error {
if _, err := client.SetDisableWarn(true).R().
SetHeader("Content-Type", "application/json").
SetBasicAuth(global.LoggerSetting.LogZincUser, global.LoggerSetting.LogZincPassword).
SetBasicAuth(global.LoggerZincSetting.User, global.LoggerZincSetting.Password).
SetBody(logStr).
Post(global.LoggerSetting.LogZincHost); err != nil {
Post(global.LoggerZincSetting.Host); err != nil {
fmt.Println(err.Error())
}
@ -62,19 +61,19 @@ func (hook *ZincLogHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func New(s *setting.LoggerSettingS) (*logrus.Logger, error) {
func New() (*logrus.Logger, error) {
log := logrus.New()
log.Formatter = &logrus.JSONFormatter{}
switch s.LogType {
case setting.LogFileType:
if global.CfgIf("LoggerFile") {
s := global.LoggerFileSetting
log.Out = &lumberjack.Logger{
Filename: s.LogFileSavePath + "/" + s.LogFileName + s.LogFileExt,
Filename: s.SavePath + "/" + s.FileName + s.FileExt,
MaxSize: 600,
MaxAge: 10,
LocalTime: true,
}
case setting.LogZincType:
} else if global.CfgIf("LoggerZinc") {
log.Out = io.Discard
log.AddHook(&ZincLogHook{})
}

@ -0,0 +1,41 @@
package setting
import (
"testing"
)
func TestUseDefault(t *testing.T) {
suites := map[string][]string{
"default": {"Sms", "Alipay", "Zinc", "MySQL", "Redis", "AliOSS", "LogZinc"},
"develop": {"Zinc", "MySQL", "AliOSS", "LogFile"},
"slim": {"Zinc", "MySQL", "Redis", "AliOSS", "LogFile"},
}
kv := map[string]string{
"sms": "SmsJuhe",
}
features := newFeatures(suites, kv)
for _, data := range []struct {
key string
expect string
exist bool
}{
{"Sms", "SmsJuhe", true},
{"Alipay", "", true},
{"Zinc", "", true},
{"Redis", "", true},
{"Database", "", false},
} {
if v, ok := features.Cfg(data.key); ok != data.exist || v != data.expect {
t.Errorf("key: %s expect: %s exist: %t got v: %s ok: %t", data.key, data.expect, data.exist, v, ok)
}
}
for exp, res := range map[string]bool{
"Sms": true,
"Sms = SmsJuhe": true,
"SmsJuhe": false,
} {
if ok := features.CfgIf(exp); res != ok {
t.Errorf("CfgIf(%s) want %t got %t", exp, res, ok)
}
}
}

@ -1,6 +1,7 @@
package setting
import (
"strings"
"time"
"github.com/spf13/viper"
@ -16,6 +17,19 @@ type LogType string
const LogFileType LogType = "file"
const LogZincType LogType = "zinc"
type LoggerFileSettingS struct {
SavePath string
FileName string
FileExt string
}
type LoggerZincSettingS struct {
Host string
Index string
User string
Password string
}
type LoggerSettingS struct {
LogType LogType
LogFileSavePath string
@ -44,22 +58,30 @@ type AppSettingS struct {
MaxPageSize int
IsShastaTestnet bool
TronApiKeys []string
SmsJuheKey string
SmsJuheTplID string
SmsJuheTplVal string
AlipayAppID string
AlipayPrivateKey string
}
type RuntimeSettingS struct {
DisablePhoneVerify bool
type AlipaySettingS struct {
AppID string
PrivateKey string
}
type SmsJuheSettings struct {
Key string
TplID string
TplVal string
}
type SearchSettingS struct {
ZincHost string
ZincIndex string
ZincUser string
ZincPassword string
type FeaturesSettingS struct {
kv map[string]string
suites map[string][]string
features map[string]string
}
type ZincSettingS struct {
Host string
Index string
User string
Password string
}
type DatabaseSettingS struct {
@ -75,6 +97,20 @@ type DatabaseSettingS struct {
MaxIdleConns int
MaxOpenConns int
}
type MySQLSettingS struct {
UserName string
Password string
Host string
DBName string
TablePrefix string
Charset string
ParseTime bool
LogLevel logger.LogLevel
MaxIdleConns int
MaxOpenConns int
}
type AliossSettingS struct {
AliossAccessKeyID string
AliossAccessKeySecret string
@ -83,6 +119,14 @@ type AliossSettingS struct {
AliossDomain string
}
type AliOSSSettingS struct {
AccessKeyID string
AccessKeySecret string
Endpoint string
Bucket string
Domain string
}
type RedisSettingS struct {
Host string
Password string
@ -117,3 +161,92 @@ func (s *Setting) ReadSection(k string, v interface{}) error {
return nil
}
func (s *Setting) Unmarshal(objects map[string]interface{}) error {
for k, v := range objects {
err := s.vp.UnmarshalKey(k, v)
if err != nil {
return err
}
}
return nil
}
func (s *Setting) FeaturesFrom(k string) *FeaturesSettingS {
sub := s.vp.Sub(k)
keys := sub.AllKeys()
suites := make(map[string][]string)
kv := make(map[string]string, len(keys))
for _, key := range sub.AllKeys() {
val := sub.Get(key)
switch v := val.(type) {
case string:
kv[key] = v
case []interface{}:
suites[key] = sub.GetStringSlice(key)
}
}
return newFeatures(suites, kv)
}
func newFeatures(suites map[string][]string, kv map[string]string) *FeaturesSettingS {
features := &FeaturesSettingS{
suites: suites,
kv: kv,
}
features.UseDefault()
return features
}
func (f *FeaturesSettingS) UseDefault() {
defaultSuite := f.suites["default"]
f.Use(defaultSuite, true)
}
func (f *FeaturesSettingS) Use(suite []string, noDefault bool) error {
if noDefault {
f.features = make(map[string]string)
}
features := f.flatFeatures(suite)
for _, feature := range features {
feature = strings.ToLower(feature)
f.features[feature] = f.kv[feature]
}
return nil
}
func (f *FeaturesSettingS) flatFeatures(suite []string) []string {
features := make([]string, 0, len(suite)+10)
for s := suite[:]; len(s) > 0; s = s[:len(s)-1] {
if items, exist := f.suites[s[0]]; exist {
s = append(s, items...)
}
features = append(features, s[0])
s[0] = s[len(s)-1]
}
return features
}
// Cfg get value by key if exsit
func (f *FeaturesSettingS) Cfg(key string) (string, bool) {
key = strings.ToLower(key)
value, exist := f.features[key]
return value, exist
}
// CfgIf check expression is true. if expression just have a string like
// `Sms` is mean `Sms` whether define in suite feature settings. expression like
// `Sms = SmsJuhe` is mean whether `Sms` define in suite feature settings and value
// is `SmsJuhe``
func (f *FeaturesSettingS) CfgIf(expression string) bool {
kv := strings.Split(expression, "=")
key := strings.Trim(strings.ToLower(kv[0]), " ")
v, ok := f.features[key]
if len(kv) == 2 && ok && strings.Trim(kv[1], " ") == v {
return true
} else if len(kv) == 1 && ok {
return true
}
return false
}

@ -71,12 +71,12 @@ type HitItem struct {
}
// NewClient 获取ZincClient新实例
func NewClient(conf *setting.SearchSettingS) *ZincClient {
func NewClient(conf *setting.ZincSettingS) *ZincClient {
return &ZincClient{
ZincClientConfig: &ZincClientConfig{
ZincHost: conf.ZincHost,
ZincUser: conf.ZincUser,
ZincPassword: conf.ZincPassword,
ZincHost: conf.Host,
ZincUser: conf.User,
ZincPassword: conf.Password,
},
}
}

Loading…
Cancel
Save