diff --git a/common/params/email_send.go b/common/params/email_send.go new file mode 100644 index 0000000..0bbe7f4 --- /dev/null +++ b/common/params/email_send.go @@ -0,0 +1,7 @@ +package params + +type EmailSend struct { + Email string `json:"email" binding:"required"` + AccessKeyId string `json:"access_key_id" example:"LTAI5tBzohLQvNcEh3HZjnWi"` + AccessKeySecret string `json:"access_key_secret" example:"XMyTwsKBjubwTMHqUVyPZCYvQFuXZA"` +} diff --git a/common/params/sms_send.go b/common/params/sms_send.go new file mode 100644 index 0000000..d215678 --- /dev/null +++ b/common/params/sms_send.go @@ -0,0 +1,9 @@ +package params + +type SmsSend struct { + SendType int `json:"send_type" binding:"required"` + Phone string `json:"phone" binding:"required"` + AccessKeyId string `json:"access_key_id" example:"LTAI5tBzohLQvNcEh3HZjnWi"` + AccessKeySecret string `json:"access_key_secret" example:"XMyTwsKBjubwTMHqUVyPZCYvQFuXZA"` + RegionId string `json:"region_id" example:"ap-southeast-1"` +} diff --git a/common/public/err_msg.go b/common/public/err_msg.go new file mode 100644 index 0000000..80359e8 --- /dev/null +++ b/common/public/err_msg.go @@ -0,0 +1,50 @@ +package public + +import ( + "net/http" + "project/common/response" +) + +const ( + UnknownCode = -1 + RespCodeErrorAuthInvalid = -4 + RespCodeErrorAuthTimeout = -2 + RespCodeErrorAuthOtherLogin = -3 + RespCodeErrorAuthRedisNoFind = -5 + RespCodeSuccess = 0 // success + CustomErrCode = UnknownCode + RespCodeErrorParam = 1 + RespCodeErrorCustom = 2 + RespCodeErrorUnknown = 3 + RespCodeErrorAuthTokenCheckFail = 4 + RespCodeErrorAuthNoPermission = 5 + RespCodeErrorBadRequest = http.StatusBadRequest + + RespCodeErrorSave = 10000 // 10002 + RespCodeErrorAdd = 10001 + RespCodeErrorDel = 10003 + RespCodeErrorDataNoFind = 10004 + RespCodeErrorDecodeFail = 10005 // 10006 + RespCodeErrorFind = 10008 + RespCodeErrorNoPermission = 11000 // 50001 + RespCodeErrorBusiness = RespCodeErrorCustom + RespCodeErrorAccountInvalid = 20001 +) + +func init() { + response.CodeMsgMap[RespCodeSuccess] = "success" + response.CodeMsgMap[RespCodeErrorParam] = "参数错误" + response.CodeMsgMap[RespCodeErrorUnknown] = "fail" + + // 401 token 错误占位 + response.CodeMsgMap[RespCodeErrorBadRequest] = "token error." + + // 10000+ + response.CodeMsgMap[RespCodeErrorSave] = "保存失败" + response.CodeMsgMap[RespCodeErrorAdd] = "新增失败" + response.CodeMsgMap[RespCodeErrorDel] = "删除失败" + response.CodeMsgMap[RespCodeErrorFind] = "查询失败" + response.CodeMsgMap[RespCodeErrorDataNoFind] = "无数据" + response.CodeMsgMap[RespCodeErrorDecodeFail] = "数据解析失败" + response.CodeMsgMap[RespCodeErrorNoPermission] = "无权限" +} diff --git a/common/request/common.go b/common/request/common.go new file mode 100644 index 0000000..959cbf1 --- /dev/null +++ b/common/request/common.go @@ -0,0 +1,42 @@ +package request + +import ( + "errors" + "unsafe" +) + +const ( + defaultMemory = 32 << 20 + formTagName = "form" +) + +type Unmarshaler interface { + UnmarshalForm(v string) error +} + +type SliceUnmarshaler interface { + UnmarshalForm(v []string) error +} + +var ( + Validate = func(obj interface{}) error { return nil } + + JsonEnableDecoderUseNumber = false + JsonEnableDecoderDisallowUnknownFields = false +) + +var ( + ErrUnknownType = errors.New("unknown type") + ErrMapSlicesToStringsType = errors.New("cannot convert to map slices of strings") + ErrMapToStringsType = errors.New("cannot convert to map of strings") +) + +// StringToBytes converts string to byte slice without a memory allocation. +func StringToBytes(s string) []byte { + return *(*[]byte)(unsafe.Pointer( + &struct { + string + Cap int + }{s, len(s)}, + )) +} diff --git a/common/request/common_parser.go b/common/request/common_parser.go new file mode 100644 index 0000000..7130634 --- /dev/null +++ b/common/request/common_parser.go @@ -0,0 +1,357 @@ +package request + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + "time" +) + +var ( + _EmptyField = reflect.StructField{} +) + +type _SetOptions struct { + isDefaultExists bool + defaultValue string +} + +type _Setter interface { + TrySet(value reflect.Value, field reflect.StructField, key string, opt _SetOptions) (isSetted bool, err error) +} + +func head(str, sep string) (head string, tail string) { + idx := strings.Index(str, sep) + if idx < 0 { + return str, "" + } + return str[:idx], str[idx+len(sep):] +} + +func _ValueSet(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt _SetOptions) (isSetted bool, err error) { + vs, ok := form[tagValue] + if !ok && !opt.isDefaultExists { + return false, nil + } + + switch value.Kind() { + case reflect.Slice: + if !ok { + vs = []string{opt.defaultValue} + } + if ok, err = _SliceUnmarshaler(value, vs); ok { + return true, err + } + return true, _SetSlice(vs, value, field) + case reflect.Array: + if !ok { + vs = []string{opt.defaultValue} + } + if ok, err = _SliceUnmarshaler(value, vs); ok { + return true, err + } + if len(vs) != value.Len() { + return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String()) + } + return true, _SetArray(vs, value, field) + default: + var val string + if !ok { + val = opt.defaultValue + } + + if len(vs) > 0 { + val = vs[0] + } + return true, _SetValue(val, value, field) + } +} + +func _TryToSetValue(value reflect.Value, field reflect.StructField, setter _Setter, tag string) (bool, error) { + var tagValue string + var setOpt _SetOptions + + tagValue = field.Tag.Get(tag) + tagValue, opts := head(tagValue, ",") + + if tagValue == "" { // default value is FieldName + tagValue = field.Name + } + if tagValue == "" { // when field is "_EmptyField" variable + return false, nil + } + + var opt string + for len(opts) > 0 { + opt, opts = head(opts, ",") + + if k, v := head(opt, "="); k == "default" { + setOpt.isDefaultExists = true + setOpt.defaultValue = v + } + } + + return setter.TrySet(value, field, tagValue, setOpt) +} + +func _Mapping(value reflect.Value, field reflect.StructField, setter _Setter, tag string) (bool, error) { + if field.Tag.Get(tag) == "-" { // just ignoring this field + return false, nil + } + + var vKind = value.Kind() + + if vKind == reflect.Ptr { + var isNew bool + vPtr := value + if value.IsNil() { + isNew = true + vPtr = reflect.New(value.Type().Elem()) + } + isSetted, err := _Mapping(vPtr.Elem(), field, setter, tag) + if err != nil { + return false, err + } + if isNew && isSetted { + value.Set(vPtr) + } + return isSetted, nil + } + + if vKind != reflect.Struct || !field.Anonymous { + ok, err := _TryToSetValue(value, field, setter, tag) + if err != nil { + return false, err + } + if ok { + return true, nil + } + } + + if vKind == reflect.Struct { + tValue := value.Type() + + var isSetted bool + for i := 0; i < value.NumField(); i++ { + sf := tValue.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { // unexported + continue + } + ok, err := _Mapping(value.Field(i), tValue.Field(i), setter, tag) + if err != nil { + return false, err + } + isSetted = isSetted || ok + } + return isSetted, nil + } + return false, nil +} + +func _SetIntField(val string, bitSize int, value reflect.Value) error { + if val == "" { + val = "0" + } + intVal, err := strconv.ParseInt(val, 10, bitSize) + if err == nil { + value.SetInt(intVal) + } + return err +} + +func _SetUintField(val string, bitSize int, value reflect.Value) error { + if val == "" { + val = "0" + } + uintVal, err := strconv.ParseUint(val, 10, bitSize) + if err == nil { + value.SetUint(uintVal) + } + return err +} + +func _SetBoolField(val string, value reflect.Value) error { + if val == "" { + val = "false" + } + boolVal, err := strconv.ParseBool(val) + if err == nil { + value.SetBool(boolVal) + } + return err +} + +func _SetFloatField(val string, bitSize int, value reflect.Value) error { + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, bitSize) + if err == nil { + value.SetFloat(floatVal) + } + return err +} + +func _SetTimeField(val string, structField reflect.StructField, value reflect.Value) error { + timeFormat := structField.Tag.Get("time_format") + if timeFormat == "" { + timeFormat = time.RFC3339 + } + + switch tf := strings.ToLower(timeFormat); tf { + case "unix", "unixnano": + tv, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return err + } + + d := time.Duration(1) + if tf == "unixnano" { + d = time.Second + } + + t := time.Unix(tv/int64(d), tv%int64(d)) + value.Set(reflect.ValueOf(t)) + return nil + + } + + if val == "" { + value.Set(reflect.ValueOf(time.Time{})) + return nil + } + + l := time.Local + if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC { + l = time.UTC + } + + if locTag := structField.Tag.Get("time_location"); locTag != "" { + loc, err := time.LoadLocation(locTag) + if err != nil { + return err + } + l = loc + } + + t, err := time.ParseInLocation(timeFormat, val, l) + if err != nil { + return err + } + + value.Set(reflect.ValueOf(t)) + return nil +} + +func _SetTimeDuration(val string, value reflect.Value) error { + d, err := time.ParseDuration(val) + if err != nil { + return err + } + value.Set(reflect.ValueOf(d)) + return nil +} + +func _SetArray(vals []string, value reflect.Value, field reflect.StructField) error { + for i, s := range vals { + err := _SetValue(s, value.Index(i), field) + if err != nil { + return err + } + } + return nil +} + +func _SetSlice(vals []string, value reflect.Value, field reflect.StructField) error { + slice := reflect.MakeSlice(value.Type(), len(vals), len(vals)) + err := _SetArray(vals, slice, field) + if err != nil { + return err + } + value.Set(slice) + return nil +} + +func _SliceUnmarshaler(v reflect.Value, vs []string) (bool, error) { + if v.Kind() != reflect.Pointer && v.Type().Name() != "" && v.CanAddr() { + v = v.Addr() + } + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(SliceUnmarshaler); ok { + return true, u.UnmarshalForm(vs) + } + if u, ok := v.Interface().(Unmarshaler); ok { + var val string + if len(vs) > 0 { + val = vs[0] + } + return true, u.UnmarshalForm(val) + } + } + return false, nil +} + +func _ValueUnmarshaler(v reflect.Value) Unmarshaler { + if v.Kind() != reflect.Pointer && v.Type().Name() != "" && v.CanAddr() { + v = v.Addr() + } + if v.Type().NumMethod() > 0 && v.CanInterface() { + if u, ok := v.Interface().(Unmarshaler); ok { + return u + } + } + return nil +} + +func _SetValue(val string, value reflect.Value, field reflect.StructField) error { + if u := _ValueUnmarshaler(value); u != nil { + return u.UnmarshalForm(val) + } + switch value.Kind() { + case reflect.Int: + return _SetIntField(val, 0, value) + case reflect.Int8: + return _SetIntField(val, 8, value) + case reflect.Int16: + return _SetIntField(val, 16, value) + case reflect.Int32: + return _SetIntField(val, 32, value) + case reflect.Int64: + switch value.Interface().(type) { + case time.Duration: + return _SetTimeDuration(val, value) + } + return _SetIntField(val, 64, value) + case reflect.Uint: + return _SetUintField(val, 0, value) + case reflect.Uint8: + return _SetUintField(val, 8, value) + case reflect.Uint16: + return _SetUintField(val, 16, value) + case reflect.Uint32: + return _SetUintField(val, 32, value) + case reflect.Uint64: + return _SetUintField(val, 64, value) + case reflect.Bool: + return _SetBoolField(val, value) + case reflect.Float32: + return _SetFloatField(val, 32, value) + case reflect.Float64: + return _SetFloatField(val, 64, value) + case reflect.String: + value.SetString(val) + case reflect.Struct: + switch value.Interface().(type) { + case time.Time: + return _SetTimeField(val, field, value) + } + return json.Unmarshal(StringToBytes(val), value.Addr().Interface()) + case reflect.Map: + return json.Unmarshal(StringToBytes(val), value.Addr().Interface()) + default: + return ErrUnknownType + } + return nil +} diff --git a/common/request/form.go b/common/request/form.go new file mode 100644 index 0000000..ad83411 --- /dev/null +++ b/common/request/form.go @@ -0,0 +1,75 @@ +package request + +import ( + "net/http" + "reflect" +) + +type _FormSource map[string][]string + +func (form _FormSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt _SetOptions) (isSetted bool, err error) { + return _ValueSet(value, field, form, tagValue, opt) +} + +type FormBind struct { +} + +func (FormBind) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + if err := req.ParseMultipartForm(defaultMemory); err != nil { + if err != http.ErrNotMultipart { + return err + } + } + if err := _FormMap(formTagName, obj, req.PostForm); err != nil { + return err + } + return Validate(obj) +} + +func _FormParse(ptr interface{}, form map[string][]string) error { + el := reflect.TypeOf(ptr).Elem() + + if el.Kind() == reflect.Slice { + ptrMap, ok := ptr.(map[string][]string) + if !ok { + return ErrMapSlicesToStringsType + } + for k, v := range form { + ptrMap[k] = v + } + + return nil + } + + ptrMap, ok := ptr.(map[string]string) + if !ok { + return ErrMapToStringsType + } + for k, v := range form { + ptrMap[k] = v[len(v)-1] // pick last + } + + return nil +} + +func _FormMap(tag string, ptr interface{}, form map[string][]string) error { + ptrVal := reflect.ValueOf(ptr) + var pointed interface{} + if ptrVal.Kind() == reflect.Ptr { + ptrVal = ptrVal.Elem() + pointed = ptrVal.Interface() + } + if ptrVal.Kind() == reflect.Map && + ptrVal.Type().Key().Kind() == reflect.String { + if pointed != nil { + ptr = pointed + } + return _FormParse(ptr, form) + } + + _, err := _Mapping(reflect.ValueOf(ptr), _EmptyField, _FormSource(form), tag) + return err +} diff --git a/common/request/header.go b/common/request/header.go new file mode 100644 index 0000000..37a337a --- /dev/null +++ b/common/request/header.go @@ -0,0 +1,22 @@ +package request + +import ( + "net/http" + "net/textproto" + "reflect" +) + +type _HeaderSource map[string][]string + +func (hs _HeaderSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt _SetOptions) (isSetted bool, err error) { + return _ValueSet(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt) +} + +type HeaderBind struct{} + +func (HeaderBind) Bind(req *http.Request, obj interface{}) error { + if _, err := _Mapping(reflect.ValueOf(obj), _EmptyField, _HeaderSource(req.Header), "header"); err != nil { + return err + } + return Validate(obj) +} diff --git a/common/request/json.go b/common/request/json.go new file mode 100644 index 0000000..6d53d2f --- /dev/null +++ b/common/request/json.go @@ -0,0 +1,31 @@ +package request + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +type JsonBind struct{} + +func (JsonBind) Bind(req *http.Request, obj interface{}) error { + if req == nil || req.Body == nil { + return fmt.Errorf("invalid request") + } + return _JsonDecode(req.Body, obj) +} + +func _JsonDecode(r io.Reader, obj interface{}) error { + decoder := json.NewDecoder(r) + if JsonEnableDecoderUseNumber { + decoder.UseNumber() + } + if JsonEnableDecoderDisallowUnknownFields { + decoder.DisallowUnknownFields() + } + if err := decoder.Decode(obj); err != nil { + return err + } + return Validate(obj) +} diff --git a/common/request/multipart_form.go b/common/request/multipart_form.go new file mode 100644 index 0000000..45beb1d --- /dev/null +++ b/common/request/multipart_form.go @@ -0,0 +1,74 @@ +package request + +import ( + "errors" + "mime/multipart" + "net/http" + "reflect" +) + +type _MultipartRequest http.Request + +func (r *_MultipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt _SetOptions) (isSetted bool, err error) { + if files := r.MultipartForm.File[key]; len(files) != 0 { + return _MultipartFormFileSet(value, field, files) + } + return _ValueSet(value, field, r.MultipartForm.Value, key, opt) +} + +type FormMultipartBind struct{} + +func (FormMultipartBind) Name() string { + return "multipart/form-data" +} + +func (FormMultipartBind) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseMultipartForm(defaultMemory); err != nil { + return err + } + if _, err := _Mapping(reflect.ValueOf(obj), _EmptyField, (*_MultipartRequest)(req), "form"); err != nil { + return err + } + return Validate(obj) +} + +func _MultipartFormFileSet(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) { + switch value.Kind() { + case reflect.Ptr: + switch value.Interface().(type) { + case *multipart.FileHeader: + value.Set(reflect.ValueOf(files[0])) + return true, nil + } + case reflect.Struct: + switch value.Interface().(type) { + case multipart.FileHeader: + value.Set(reflect.ValueOf(*files[0])) + return true, nil + } + case reflect.Slice: + slice := reflect.MakeSlice(value.Type(), len(files), len(files)) + isSetted, err = _SetArrayOfMultipartFormFiles(slice, field, files) + if err != nil || !isSetted { + return isSetted, err + } + value.Set(slice) + return true, nil + case reflect.Array: + return _SetArrayOfMultipartFormFiles(value, field, files) + } + return false, errors.New("unsupported field type for multipart.FileHeader") +} + +func _SetArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) { + if value.Len() != len(files) { + return false, errors.New("unsupported len of array for []*multipart.FileHeader") + } + for i := range files { + setted, err := _MultipartFormFileSet(value.Index(i), field, files[i:i+1]) + if err != nil || !setted { + return setted, err + } + } + return true, nil +} diff --git a/common/request/query.go b/common/request/query.go new file mode 100644 index 0000000..d7c14c5 --- /dev/null +++ b/common/request/query.go @@ -0,0 +1,14 @@ +package request + +import "net/http" + +type QueryBind struct { +} + +func (QueryBind) Bind(req *http.Request, obj interface{}) error { + values := req.URL.Query() + if err := _FormMap(formTagName, obj, values); err != nil { + return err + } + return Validate(obj) +} diff --git a/common/response/response.go b/common/response/response.go new file mode 100644 index 0000000..fda3591 --- /dev/null +++ b/common/response/response.go @@ -0,0 +1,137 @@ +package response + +import "fmt" + +type Error interface { + error + Code() int +} + +type ErrorDefault struct { + ErrMsg string + ErrCode int +} + +func (v *ErrorDefault) Error() string { + return v.ErrMsg +} + +func (v *ErrorDefault) Code() int { + return v.ErrCode +} + +type Response interface { + SetErrCode(int) Response + SetCustomError(Error) Response + SetErrMsg(int, string, ...interface{}) Response + SetData(interface{}) Response + SetDataMsg(interface{}, string, ...interface{}) Response + SetMeta(interface{}) Response + AddError(...error) Response +} + +func SetCode(v *int, code int) { + *v = code +} + +func SetData(v *interface{}, data interface{}) { + *v = data +} + +func SetMsg(v *string, msg string, msgArgs ...interface{}) { + if len(msgArgs) > 0 { + *v = fmt.Sprintf(msg, msgArgs...) + } else { + *v = msg + } +} + +func SetMeta(v *interface{}, meta interface{}) { + if ShowMore { + *v = meta + } +} + +func SetErrCode(c *int, m *string, code int) { + SetCode(c, code) + if msg, ok := CodeMsgMap[code]; ok { + *m = msg + } else { + *m = "fail" + } +} + +func SetError(c *[]string, errs ...error) { + if ShowMore { + if *c != nil { + nLen := 4 + if len(errs) > nLen { + nLen = len(errs) + } + *c = make([]string, 0, nLen) + } + for _, err := range errs { + if err != nil { + *c = append(*c, err.Error()) + } + } + } +} + +type _Default struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` + Meta interface{} `json:"meta,omitempty"` + Errors []string `json:"errors,omitempty"` +} + +var ( + CodeMsgMap = map[int]string{} + ShowMore = true +) + +func (r *_Default) SetErrCode(code int) Response { + SetErrCode(&r.Code, &r.Msg, code) + return r +} + +func (r *_Default) SetCustomError(err Error) Response { + SetCode(&r.Code, err.Code()) + return r._SetMsg(err.Error()) +} + +func (r *_Default) SetErrMsg(code int, msg string, msgArgs ...interface{}) Response { + SetCode(&r.Code, code) + return r._SetMsg(msg, msgArgs...) +} + +func (r *_Default) _SetMsg(msg string, msgArgs ...interface{}) Response { + SetMsg(&r.Msg, msg, msgArgs...) + return r +} + +func (r *_Default) SetData(v interface{}) Response { + SetData(&r.Data, v) + return r +} + +func (r *_Default) SetDataMsg(v interface{}, msg string, msgArgs ...interface{}) Response { + SetData(&r.Data, v) + SetMsg(&r.Msg, msg, msgArgs...) + return r +} + +func (r *_Default) SetMeta(v interface{}) Response { + SetMeta(&r.Meta, v) + return r +} + +func (r *_Default) AddError(errs ...error) Response { + SetError(&r.Errors, errs...) + return r +} + +func New() Response { + return &_Default{} +} diff --git a/config.yaml b/config.yaml new file mode 100755 index 0000000..848e383 --- /dev/null +++ b/config.yaml @@ -0,0 +1,20 @@ +listen: 8889 +runmode: local # 服务运行模式 debug/release +logs: + dir: ../log # 文件保存路径 + file: go-contest-api # 文件名称,实际会保存为{filename}+{datetime} + level: 3 # 日志等级:0-error,1-warning,2-info,3-debug + savefile: false # 是否保存为文件,置为false会输出到标准输出 +mysql: # 数据库信息 + addr: 192.168.2.10:3306 + user: quentin + password: sEK1BhVnYCzg6HvCgYuu + db: ns9 + charset: utf8mb4 + max_open: 500 + max_idle: 5 +#redis: +# addr: 120.25.84.150:6380 +# password: 123456 +# db: 0 +# poolsize: 5 diff --git a/config/app_ser/app.go b/config/app_ser/app.go new file mode 100644 index 0000000..4b873e1 --- /dev/null +++ b/config/app_ser/app.go @@ -0,0 +1,287 @@ +package app_ser + +import ( + "errors" + "fmt" + "github.com/urfave/cli/v2" + "math/rand" + "os" + "os/signal" + "project/config/logger" + "runtime" + "strings" + "sync" + "syscall" + "time" +) + +const ( + // Env + EnvLocal = iota // 本地 + EnvDebug // 测试 + EnvProd // 线上 + + CliAppEnv = "app-env" + CliAppLogMore = "app-log-more" +) + +var ( + _EnvDevStr = "local" // 本地 + _EnvTestStr = "debug" // 测试 + _EnvProdStr = "release" // 线上 +) + +var ( + _EnvMap = map[int][2]string{ + EnvLocal: {_EnvDevStr, "本地环境"}, + EnvDebug: {_EnvTestStr, "测试环境"}, + EnvProd: {_EnvProdStr, "线上环境"}, + } +) + +type _EnvFlag int + +func (v _EnvFlag) Usage() string { + ret := make([]string, 0, len(_EnvMap)) + for _, k := range []int{EnvLocal, EnvDebug, EnvProd} { + s := _EnvMap[k] + ret = append(ret, strings.Join(s[:], "-")) + } + return "env(" + strings.Join(ret, ", ") + ")" +} + +func (v *_EnvFlag) Action(_ *cli.Context, sv string) error { + match := false + for ek, ev := range _EnvMap { + if sv == ev[0] { + *v, match = _EnvFlag(ek), true + break + } + } + if match != true { + return errors.New("Env Is Invalid: " + sv) + } + return nil +} + +type AppConfig struct { + env _EnvFlag + version string + execDir string + logMore bool +} + +func (v *AppConfig) Env() int { + return int(v.env) +} + +func (v *AppConfig) EnvDesc() string { + return _EnvMap[int(v.env)][0] +} + +func (v *AppConfig) SetEnv(e int) bool { + if _, ok := _EnvMap[int(v.env)]; ok { + v.env = _EnvFlag(e) + return true + } + return false +} + +func (v *AppConfig) ExecDir() string { + return v.execDir +} + +func (v *AppConfig) Version() string { + return v.version +} + +func (v *AppConfig) LogMore() bool { + return v.logMore +} + +type Component interface { + CliFlags() []cli.Flag + Init(logger.Interface, *AppConfig, *cli.Context) error + Run() error + Close() error +} + +type Service interface { + CliFlags() []cli.Flag + Init(*AppConfig, *cli.Context) (logger.Interface, error) + Run(logger.Interface, *AppConfig) error + Close() +} + +type Application struct { + Component map[string]Component + Log logger.Interface + Cfg AppConfig + Ser Service +} + +func (app *Application) closeComponent() { + if app.Component != nil && len(app.Component) > 0 { + wg := sync.WaitGroup{} + wg.Add(len(app.Component)) + for k := range app.Component { + go func(v Component) { + defer wg.Done() + _ = v.Close() + }(app.Component[k]) + } + + wg.Wait() + } +} + +func (app *Application) shutdown() { + defer func() { + fmt.Println("Application Had Exit") + os.Exit(0) + }() + fmt.Println("Application To Exit") +} + +func (app *Application) catchSignal() { + c := make(chan os.Signal) + signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) + + for s := range c { + switch s { + case syscall.SIGHUP: + fallthrough + case syscall.SIGINT, syscall.SIGTERM: + app.closeComponent() + app.Ser.Close() + app.shutdown() + } + } +} + +func (app *Application) runComponent() error { + if app.Component != nil && len(app.Component) > 0 { + wg := sync.WaitGroup{} + lock := sync.Mutex{} + var errList []error + + wg.Add(len(app.Component)) + for k := range app.Component { + go func(v Component) { + defer wg.Done() + err := v.Run() + if err != nil { + lock.Lock() + if errList == nil { + errList = make([]error, 0) + } + errList = append(errList, err) + lock.Unlock() + } + }(app.Component[k]) + } + + wg.Wait() + if len(errList) > 0 { + errStr := make([]string, 0, len(errList)) + for k := range errList { + errStr = append(errStr, errList[k].Error()) + } + return errors.New("Component Run Error:" + strings.Join(errStr, ",")) + } + } + return nil +} + +func (app *Application) run() error { + var err error = nil + + runtime.GOMAXPROCS(runtime.NumCPU()) + go app.catchSignal() + + // 初始化随机种子 + rand.Seed(time.Now().UnixNano()) + + // 启动服务 + err = app.runComponent() + if err != nil { + return err + } + + return app.Ser.Run(app.Log, &app.Cfg) +} + +func (app *Application) Start(cmd *cli.App, args []string, service Service) { + if cmd == nil { + cmd = cli.NewApp() + cmd.Version = "1.0" + cmd.Name = "ApplicationWeb" + cmd.Usage = "ApplicationWeb Server" + } + + app.Ser = service + + // app + cfs := make([][]cli.Flag, 0, len(app.Component)+2) + cfs = append(cfs, []cli.Flag{ + // base + &cli.StringFlag{Name: CliAppEnv, Value: _EnvDevStr, Usage: app.Cfg.env.Usage(), Action: app.Cfg.env.Action}, + &cli.BoolFlag{Name: CliAppLogMore, Value: false, Usage: "log more", Destination: &app.Cfg.logMore}, + }) + fsLen := len(cfs[0]) + + // service + afs := app.Ser.CliFlags() + if len(afs) > 0 { + cfs = append(cfs, afs) + fsLen += len(afs) + } + + // component + for k := range app.Component { + v := app.Component[k].CliFlags() + if len(v) > 0 { + cfs = append(cfs, v) + fsLen += len(v) + } + } + + // cli merge + fs := make([]cli.Flag, 0, fsLen) + for _, v := range cfs { + fs = append(fs, v...) + } + + // 执行命令行 + cmd.Flags = fs + cmd.Action = func(ctx *cli.Context) error { + var err error = nil + app.Cfg.execDir, err = os.Getwd() + if err == nil { + // 用命令行初始化配置 + app.Log, err = app.Ser.Init(&app.Cfg, ctx) + if err == nil { + // 组件初始化 + for k := range app.Component { + if err = app.Component[k].Init(app.Log, &app.Cfg, ctx); err != nil { + err = errors.New("Component Init Error: " + err.Error()) + break + } + } + // 启动程序 + if err == nil { + err = app.run() + } + } + } else { + err = errors.New("dir is invalid: " + app.Cfg.execDir) + } + return err + } + + err := cmd.Run(args) + if err != nil { + fmt.Printf("Init Error: %s", err.Error()) + os.Exit(1) + } +} diff --git a/config/app_ser/common.go b/config/app_ser/common.go new file mode 100644 index 0000000..ded35d1 --- /dev/null +++ b/config/app_ser/common.go @@ -0,0 +1,66 @@ +package app_ser + +import ( + "fmt" + "github.com/urfave/cli/v2" + "gopkg.in/yaml.v3" + "io/ioutil" + "os" + "project/config/logger" +) + +type BasicService struct { + RunBefore, InitBefore func() error +} + +func (c *BasicService) ParserFile(cfg interface{}) error { + // 解析文件 + execDir, err := os.Getwd() + if nil != err { + return fmt.Errorf("load config (err: %s)\n", err.Error()) + } + fPath := fmt.Sprintf("%s%c%s", execDir, os.PathSeparator, "config.yaml") + fmt.Printf("load config (path: %s)\n", fPath) + fp, err := os.Open(fPath) + if nil != err { + return fmt.Errorf("load config (err: %s)\n", err.Error()) + } + defer fp.Close() + d, err := ioutil.ReadAll(fp) + if nil != err { + return fmt.Errorf("load config (err: %s)\n", err.Error()) + } + // 解析配置 + if err = yaml.Unmarshal(d, cfg); err != nil { + return fmt.Errorf("parser config failed! %s", err.Error()) + } + return nil +} + +func (c *BasicService) Init(runMode string, cfg *AppConfig, ctx *cli.Context) (logger.Interface, error) { + // 设置环境 + if !ctx.IsSet(CliAppEnv) { + for k := range _EnvMap { + if _EnvMap[k][0] == runMode { + cfg.SetEnv(k) + break + } + } + } + + // 初始化日志 + v := &logger.Console{} + switch cfg.Env() { + case EnvLocal: + v.Init(logger.Info, true, os.Stdout, os.Stderr) + case EnvDebug, EnvProd: + v.Init(logger.Warn, false, os.Stdout, os.Stderr) + } + + if c.InitBefore != nil { + if err := c.InitBefore(); err != nil { + return nil, err + } + } + return v, nil +} diff --git a/config/app_ser/component/mysql.go b/config/app_ser/component/mysql.go new file mode 100644 index 0000000..bb9e115 --- /dev/null +++ b/config/app_ser/component/mysql.go @@ -0,0 +1,109 @@ +package component + +import ( + "fmt" + "github.com/urfave/cli/v2" + "gorm.io/gorm/logger" + "project/config" + "project/config/app_ser" + glogger "project/config/logger" + "strings" + "time" +) + +type MySQL struct { + Cfg config.ComponentMysql + Clts map[string]*MySQLClient + Log MysqlLogger +} + +func (c *MySQL) SetLogLevel(v int) { + // log leve + nVal := logger.Error + switch v { + case glogger.Info: + nVal = logger.Info + case glogger.Warn: + nVal = logger.Warn + case glogger.Error: + nVal = logger.Error + } + c.Log.level = nVal +} + +func (c *MySQL) CliFlags() []cli.Flag { + fs := make([][]cli.Flag, 0, len(c.Clts)+1) + fs = append(fs, []cli.Flag{ + &cli.IntFlag{Name: "mysql-threshold", Usage: "mysql slow threshold", Value: 300, Destination: &c.Cfg.SlowThreshold}, + &cli.IntFlag{Name: "mysql-op-timeout", Usage: "mysql operation timeout", Value: 15, Destination: &c.Cfg.OpTimeout}, + }) + for name, clt := range c.Clts { + fs = append(fs, clt.CliFlags(name)) + } + l := 0 + for k := range fs { + l += len(fs[k]) + } + ret := make([]cli.Flag, 0, l) + for k := range fs { + ret = append(ret, fs[k]...) + } + return ret +} + +func (c *MySQL) Init(l glogger.Interface, cfg *app_ser.AppConfig, _ *cli.Context) error { + // 设置日志等级 + c.Log.SetLogger(l) + c.Log.slowThreshold = time.Duration(c.Cfg.SlowThreshold) * time.Millisecond + switch cfg.Env() { + case app_ser.EnvLocal: + c.SetLogLevel(glogger.Info) + case app_ser.EnvDebug: + c.SetLogLevel(glogger.Warn) + case app_ser.EnvProd: + c.SetLogLevel(glogger.Error) + } + + // 打印链接信息 + for k, v := range c.Clts { + msg := fmt.Sprintf("Init MySQL (%s) config", k) + v.Init(cfg.Env(), c) + if config.PrintfDSN { + var slave string + cCfg := v.Config() + if len(cCfg.Slave) > 0 { + ss := make([]string, 0, len(cCfg.Slave)) + for sk := range cCfg.Slave { + ss = append(ss, cCfg.Slave[sk].GetPrintDSN()) + } + slave = "[" + strings.Join(ss, ";") + "]" + } + + msg = fmt.Sprintf("%s (%s%s)", msg, cCfg.Master.GetPrintDSN(), slave) + } + c.Log.log.WarnForce(msg) + } + return nil +} + +func (c *MySQL) Run() error { + for name := range c.Clts { + if err := c.LoadOne(name); err != nil { + return err + } + } + return nil +} + +func (c *MySQL) LoadOne(name string) (err error) { + if clt, ok := c.Clts[name]; ok { + err = clt.Load(name) + } else { + err = fmt.Errorf("MySQL (%s) config no find", name) + } + return err +} + +func (c *MySQL) Close() error { + return nil +} diff --git a/config/app_ser/component/mysql_client.go b/config/app_ser/component/mysql_client.go new file mode 100644 index 0000000..92225a3 --- /dev/null +++ b/config/app_ser/component/mysql_client.go @@ -0,0 +1,107 @@ +package component + +import ( + "database/sql" + "fmt" + "github.com/urfave/cli/v2" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/schema" + "gorm.io/plugin/dbresolver" + "project/config" + "strings" +) + +type MySQLClient struct { + DB *gorm.DB + Cfg config.EntityMysql + ref *MySQL +} + +func (c *MySQLClient) Ref() *MySQL { + return c.ref +} + +func (c *MySQLClient) CliFlags(name string) []cli.Flag { + return []cli.Flag{ + &cli.IntFlag{ + Name: fmt.Sprintf("mysql-%s-open-limit", name), + Usage: fmt.Sprintf("mysql(%s) max open limit", name), + Action: func(_ *cli.Context, v int) error { c.Cfg.MaxOpen = v; return nil }, + }, + &cli.IntFlag{ + Name: fmt.Sprintf("mysql-%s-idle-limit", name), + Usage: fmt.Sprintf("mysql(%s) idle open limit", name), + Action: func(_ *cli.Context, v int) error { c.Cfg.MaxIdle = v; return nil }, + }, + &cli.StringFlag{ + Name: fmt.Sprintf("mysql-%s-dsn", name), + Usage: fmt.Sprintf("mysql(%s) DSN (format: `{{user_name}}:{{password}}@tcp({{host}}:{{port}})/{{db_name}}?{{connect_options}}`)", name), + Action: func(_ *cli.Context, val string) (err error) { + dsn := strings.Split(val, ";") + if err = c.Cfg.Master.ParserDSN(dsn[0]); err == nil { + if len(dsn) > 1 { + c.Cfg.Slave = make([]config.ConnectMysql, 0, len(dsn)-1) + for i, l := 1, len(dsn); i < l; i++ { + v := config.ConnectMysql{} + if err = v.ParserDSN(dsn[i]); err != nil { + break + } + c.Cfg.Slave = append(c.Cfg.Slave, v) + } + } + } + if err != nil { + err = fmt.Errorf("Mysql DSN is invalid(%s)", err.Error()) + } + return err + }, + }, + } +} + +func (c *MySQLClient) Init(env int, cc *MySQL) { + c.ref = cc +} + +func (c *MySQLClient) Config() *config.EntityMysql { + return &c.Cfg +} + +func (c *MySQLClient) Load(name string) (err error) { + mastDB := mysql.Open(c.Cfg.Master.GetConnectDSN()) + gormDB, err := gorm.Open(mastDB, &gorm.Config{ + SkipDefaultTransaction: true, + Logger: &c.ref.Log, + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, + }, + }) + if err == nil { + if len(c.Cfg.Slave) > 0 { + slaveDBs := make([]gorm.Dialector, 0, len(c.Cfg.Slave)) + for k := range c.Cfg.Slave { + slaveDBs = append(slaveDBs, mysql.Open(c.Cfg.Slave[k].GetConnectDSN())) + } + readWritePlugin := dbresolver.Register(dbresolver.Config{ + Sources: []gorm.Dialector{mastDB}, + Replicas: slaveDBs, + Policy: dbresolver.RandomPolicy{}, + }) + readWritePlugin.SetMaxOpenConns(c.Cfg.MaxOpen) + readWritePlugin.SetMaxIdleConns(c.Cfg.MaxIdle) + err = gormDB.Use(readWritePlugin) + } else { + var sqlDB *sql.DB + if sqlDB, err = gormDB.DB(); err == nil { + sqlDB.SetMaxOpenConns(c.Cfg.MaxOpen) + sqlDB.SetMaxIdleConns(c.Cfg.MaxIdle) + } + } + } + if err != nil { + gormDB, err = nil, fmt.Errorf("Load Mysql (%s) Failed (Error: %s) ", name, err.Error()) + } + c.DB = gormDB + return err +} diff --git a/config/app_ser/component/mysql_log.go b/config/app_ser/component/mysql_log.go new file mode 100644 index 0000000..62401b8 --- /dev/null +++ b/config/app_ser/component/mysql_log.go @@ -0,0 +1,82 @@ +package component + +import ( + "context" + "errors" + "fmt" + "gorm.io/gorm/logger" + "gorm.io/gorm/utils" + glogger "project/config/logger" + "time" +) + +const ( + _TraceStr = "\n\t%s\n\t[%.3fms] [rows:%v] %s" + _TraceErrStr = "\n\t%s %s\n\t[%.3fms] [rows:%v] %s" + _TraceWarnStr = "\n\t%s %s\n\t[%.3fms] [rows:%v] %s" +) + +type MysqlLogger struct { + log glogger.Interface + level logger.LogLevel + slowThreshold time.Duration +} + +func (l *MysqlLogger) SetLogger(log glogger.Interface) { + l.log = log +} + +func (l *MysqlLogger) LogMode(level logger.LogLevel) logger.Interface { + l.level = level + return l +} + +func (l *MysqlLogger) Info(_ context.Context, msg string, data ...interface{}) { + if l.level >= logger.Info { + l.log.Info(msg, append([]interface{}{utils.FileWithLineNum()}, data...)...) + } +} + +func (l *MysqlLogger) Warn(_ context.Context, msg string, data ...interface{}) { + if l.level >= logger.Warn { + l.log.Warn(msg, append([]interface{}{utils.FileWithLineNum()}, data...)...) + } +} + +func (l *MysqlLogger) Error(_ context.Context, msg string, data ...interface{}) { + if l.level >= logger.Error { + l.log.Error(msg, append([]interface{}{utils.FileWithLineNum()}, data...)...) + } +} + +func (l *MysqlLogger) Trace(_ context.Context, begin time.Time, fc func() (string, int64), err error) { + if l.level <= logger.Silent { + return + } + + elapsed := time.Since(begin) + switch { + case err != nil && l.level >= logger.Error && !errors.Is(err, logger.ErrRecordNotFound): + sql, rows := fc() + if rows == -1 { + l.log.Error(_TraceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, "-", sql) + } else { + l.log.Error(_TraceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, rows, sql) + } + case elapsed > l.slowThreshold && l.slowThreshold != 0 && l.level >= logger.Warn: + sql, rows := fc() + slowLog := fmt.Sprintf("SLOW SQL >= %v", l.slowThreshold) + if rows == -1 { + l.log.Warn(_TraceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, "-", sql) + } else { + l.log.Warn(_TraceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql) + } + case l.level == logger.Info: + sql, rows := fc() + if rows == -1 { + l.log.Info(_TraceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, "-", sql) + } else { + l.log.Info(_TraceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql) + } + } +} diff --git a/config/app_ser/component/redis.go b/config/app_ser/component/redis.go new file mode 100644 index 0000000..c2d2cbc --- /dev/null +++ b/config/app_ser/component/redis.go @@ -0,0 +1,74 @@ +package component + +import ( + "fmt" + "github.com/urfave/cli/v2" + "project/config" + "project/config/app_ser" + "project/config/logger" +) + +type Redis struct { + Cfg config.ComponentRedis + Clts map[string]*RedisClient + Log logger.Interface +} + +func (c *Redis) CliFlags() []cli.Flag { + fs := make([][]cli.Flag, 0, len(c.Clts)+1) + fs = append(fs, []cli.Flag{ + &cli.IntFlag{Name: "redis-sr-timeout", Usage: "redis connect socket read timeout", Value: 15, Destination: &c.Cfg.MaxReadTime}, + &cli.IntFlag{Name: "redis-sw-timeout", Usage: "redis connect socket write timeout", Value: 15, Destination: &c.Cfg.MaxWriteTime}, + &cli.IntFlag{Name: "redis-cr-timeout", Usage: "redis client request timeout", Value: 15, Destination: &c.Cfg.MaxRequestTime}, + &cli.IntFlag{Name: "redis-check-idle-time", Usage: "redis idle timeout", Value: 30, Destination: &c.Cfg.IdleCheckTime}, + &cli.IntFlag{Name: "redis-threshold", Usage: "redis slow request threshold", Value: 100, Destination: &c.Cfg.SlowThreshold}, + }) + for name, clt := range c.Clts { + fs = append(fs, clt.CliFlags(name)) + } + l := 0 + for k := range fs { + l += len(fs[k]) + } + ret := make([]cli.Flag, 0, l) + for k := range fs { + ret = append(ret, fs[k]...) + } + return ret +} + +func (c *Redis) Init(l logger.Interface, cfg *app_ser.AppConfig, _ *cli.Context) error { + c.Log = l + // 打印链接信息 + for k, v := range c.Clts { + v.Init(cfg.Env(), c) + msg := fmt.Sprintf("Init Redis (%s) config", k) + if config.PrintfDSN { + msg = fmt.Sprintf("%s (%s/%d)", msg, v.Config().Addr, v.Config().DB) + } + l.WarnForce(msg) + } + return nil +} + +func (c *Redis) Run() (err error) { + for name := range c.Clts { + if err = c.LoadOne(name); err != nil { + break + } + } + return err +} + +func (c *Redis) LoadOne(name string) (err error) { + if clt, ok := c.Clts[name]; ok { + err = clt.Load(name) + } else { + err = fmt.Errorf("Redis (%s) config no find", name) + } + return err +} + +func (c *Redis) Close() error { + return nil +} diff --git a/config/app_ser/component/redis_client.go b/config/app_ser/component/redis_client.go new file mode 100644 index 0000000..e7685a5 --- /dev/null +++ b/config/app_ser/component/redis_client.go @@ -0,0 +1,102 @@ +package component + +import ( + "bytes" + "context" + "fmt" + "github.com/go-redis/redis/v8" + "github.com/urfave/cli/v2" + "net/url" + "project/config" + "strconv" + "strings" + "time" +) + +type RedisClient struct { + DB *redis.Client + Cfg config.EntityRedis + ref *Redis +} + +func (c *RedisClient) Ref() *Redis { + return c.ref +} + +func (c *RedisClient) CliFlags(name string) []cli.Flag { + return []cli.Flag{ + &cli.IntFlag{ + Name: fmt.Sprintf("redis-%s-open-limit", name), + Usage: fmt.Sprintf("redis(%s) max open limit", name), + Action: func(_ *cli.Context, v int) error { c.Cfg.PoolSize = v; return nil }, + }, + &cli.StringFlag{ + Name: fmt.Sprintf("redis-%s-dsn", name), + Usage: fmt.Sprintf("redis(%s) DSN (format: `user:{{password}}@{{ip}}:{{port}}/{{db}}`)", name), + Action: func(_ *cli.Context, dsn string) error { + pv, err := url.Parse("redis://" + dsn) + if err == nil { + c.Cfg.Addr = pv.Host + c.Cfg.Pwd, _ = pv.User.Password() + c.Cfg.DB, _ = strconv.Atoi(strings.Trim(pv.Path, "/")) + } + if err != nil { + return fmt.Errorf("redis DSN is invalid(%s)[url parse faild err_msg:%s]", dsn, err.Error()) + } + return nil + }, + }, + } +} + +func (c *RedisClient) Init(env int, cc *Redis) { + c.ref = cc +} + +func (c *RedisClient) Config() *config.EntityRedis { + return &c.Cfg +} + +func (c *RedisClient) Load(name string) error { + c.DB = redis.NewClient(&redis.Options{ + Addr: c.Cfg.Addr, + Password: c.Cfg.Pwd, + DB: c.Cfg.DB, + PoolSize: c.Cfg.PoolSize, + MinIdleConns: c.Cfg.PoolSize/3 + 1, + IdleTimeout: time.Second * time.Duration(c.ref.Cfg.IdleCheckTime), + ReadTimeout: time.Second * time.Duration(c.ref.Cfg.MaxReadTime), + WriteTimeout: time.Second * time.Duration(c.ref.Cfg.MaxWriteTime), + }) + if _, err := c.DB.Ping(context.Background()).Result(); err != nil { + c.DB = nil + return fmt.Errorf("Load Redis (%s) Failed (Ping-Error: %s) ", name, err.Error()) + } + return nil +} + +func (c *RedisClient) ApiLog(args [][2]interface{}) func() { + bTm := time.Now() + return func() { + spendTime := time.Now().Sub(bTm) / time.Millisecond + var handler func(string, ...interface{}) + //if spendTime > time.Duration(c.ref.Cfg.SlowThreshold) { // 查询超过阈值 + // handler = c.ref.Log + //} else if c.ref.Log.Level() == logger.Info { + // handler = c.ref.Log.InfoForce + //} + if handler != nil { + var format bytes.Buffer + format.WriteString("Redis[%d ms]: ") + + fArgs := make([]interface{}, 0, 2+len(args)*2) + fArgs = append(fArgs, spendTime) + + for i, l := 0, len(args); i < l; i++ { + format.WriteString("\n\t%s: %+v") + fArgs = append(fArgs, args[0][0], args[0][1]) + } + handler(format.String(), fArgs...) + } + } +} diff --git a/config/app_ser/web.go b/config/app_ser/web.go new file mode 100644 index 0000000..ad6850e --- /dev/null +++ b/config/app_ser/web.go @@ -0,0 +1,65 @@ +package app_ser + +import ( + "fmt" + "github.com/urfave/cli/v2" + "net/http" + "project/config/logger" +) + +const ( + CliWebIP = "web-ip" + CliWebPort = "web-port" +) + +type WebService struct { + BasicService + Cfg struct { + IP string `yaml:"ip"` // 监听端口 + Port int `yaml:"listen"` // 监听端口 + RunMode string `yaml:"runmode"` // 服务运行模式 + } + Handler http.Handler +} + +func (c *WebService) CliFlags() []cli.Flag { + return []cli.Flag{ + // web + &cli.StringFlag{ + Name: CliWebIP, + Usage: "web service ip", + Action: func(_ *cli.Context, s string) error { c.Cfg.IP = s; return nil }, + }, + &cli.IntFlag{ + Name: CliWebPort, + Usage: "web service port", + Action: func(_ *cli.Context, s int) error { c.Cfg.Port = s; return nil }, + }, + } +} + +func (c *WebService) Init(cfg *AppConfig, ctx *cli.Context) (logger.Interface, error) { + return c.BasicService.Init(c.Cfg.RunMode, cfg, ctx) +} + +func (c *WebService) Run(log logger.Interface, cfg *AppConfig) error { + if c.RunBefore != nil { + if err := c.RunBefore(); err != nil { + return err + } + } + if c.Cfg.IP == "" { + log.WarnForce("Listening Server [[[ Run-Mode: %s ]]] http://127.0.0.1:%d\n", cfg.EnvDesc(), c.Cfg.Port) + } else { + log.WarnForce("Listening Server [[[ Run-Mode: %s ]]] http://%s:%d\n", cfg.EnvDesc(), c.Cfg.IP, c.Cfg.Port) + } + + srv := &http.Server{ + Addr: fmt.Sprintf("%s:%d", c.Cfg.IP, c.Cfg.Port), + Handler: c.Handler, + } + srv.SetKeepAlivesEnabled(true) + return srv.ListenAndServe() +} + +func (c *WebService) Close() {} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..471c87c --- /dev/null +++ b/config/config.go @@ -0,0 +1,7 @@ +package config + +import "project/config/app_ser" + +var ( + AppEnv *app_ser.AppConfig +) diff --git a/config/define.go b/config/define.go new file mode 100644 index 0000000..96065e0 --- /dev/null +++ b/config/define.go @@ -0,0 +1,138 @@ +package config + +import ( + "fmt" + "github.com/go-sql-driver/mysql" + "net/url" + "strings" +) + +var ( + PrintfDSN = true +) + +type FileCfg struct { + Listen *int `yaml:"listen"` // 监听端口 + RunMode *string `yaml:"runmode"` // 服务运行模式 + Mysql *EntityMysql `yaml:"mysql"` // + Redis *EntityRedis `yaml:"redis"` // redis +} + +type EntityMysql struct { + MaxOpen int `yaml:"max_open"` + MaxIdle int `yaml:"max_idle"` + Master ConnectMysql `yaml:",inline"` + Slave []ConnectMysql `yaml:"slave"` +} + +type ComponentMysql struct { + SlowThreshold int + OpTimeout int +} + +type ConnectMysql struct { + Addr string `yaml:"addr"` + UserName string `yaml:"user"` + Password string `yaml:"password"` + Database string `yaml:"db"` + Charset string `yaml:"charset"` + Options string `yaml:"options"` + initOpt bool +} + +type EntityRedis struct { + ConnectRedis `yaml:",inline"` + PoolSize int `yaml:"poolsize"` +} + +// redis + +type ConnectRedis struct { + Addr string `yaml:"addr"` + Pwd string `yaml:"password"` + DB int `yaml:"db"` +} + +type ComponentRedis struct { + IdleCheckTime, + MaxReadTime, + MaxWriteTime, + MaxRequestTime, + SlowThreshold int +} + +func (c *ConnectMysql) GetPrintDSN() string { + if !c.initOpt { + var params map[string]string + var optValues []string + + if c.Options != "" { + optValues = strings.Split(c.Options, "&") + params = make(map[string]string, len(optValues)) + for _, v := range optValues { + v = strings.Trim(v, " ") + if v != "" { + kv := strings.Split(v, "=") + if len(kv) > 1 { + params[strings.Trim(kv[0], " ")] = strings.Trim(kv[1], " ") + } else { + params[strings.Trim(kv[0], " ")] = "" + } + } + } + } + c._setOption(params) + } + return fmt.Sprintf("mysql://%s/%s?%s", c.Addr, c.Database, c.Options) +} + +func (c *ConnectMysql) ParserDSN(val string) error { + cfg, err := mysql.ParseDSN(val) + if err != nil { + return err + } + + var params map[string]string + u := strings.Split(val, "?") + if len(u) > 1 { + if q, err := url.ParseQuery(u[1]); err == nil { + params = make(map[string]string, len(q)) + for k := range q { + params[k] = q.Get(k) + } + } + } + + *c = ConnectMysql{} + c.Addr, c.Database, c.UserName, c.Password = cfg.Addr, cfg.DBName, cfg.User, cfg.Passwd + c._setOption(params) + return nil +} + +func (c *ConnectMysql) _setOption(params map[string]string) { + if params == nil { + params = make(map[string]string) + } + //if _, ok := params["parseTime"]; !ok { + // params["parseTime"] = "true" + //} + //if _, ok := params["loc"]; !ok { + // params["loc"] = "Local" + //} + if _, ok := params["charset"]; !ok { + if c.Charset == "" { + c.Charset = "utf8mb4" + } + params["charset"] = c.Charset + } + optValues := make([]string, 0, len(params)) + for k := range params { + optValues = append(optValues, k+"="+params[k]) + } + c.Options = strings.Join(optValues, "&") + c.initOpt = true +} + +func (c *ConnectMysql) GetConnectDSN() string { + return fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", c.UserName, c.Password, c.Addr, c.Database, c.Options) +} diff --git a/config/logger/common.go b/config/logger/common.go new file mode 100644 index 0000000..f24559c --- /dev/null +++ b/config/logger/common.go @@ -0,0 +1,23 @@ +package logger + +const ( + Debug = iota + Info + Warn + Error +) + +type Interface interface { + Level() int + SetLevel(int) int + + Debug(format string, v ...interface{}) + Info(format string, v ...interface{}) + Warn(format string, v ...interface{}) + Error(format string, v ...interface{}) + + DebugForce(format string, v ...interface{}) + InfoForce(format string, v ...interface{}) + WarnForce(format string, v ...interface{}) + ErrorForce(format string, v ...interface{}) +} diff --git a/config/logger/console.go b/config/logger/console.go new file mode 100644 index 0000000..d32901c --- /dev/null +++ b/config/logger/console.go @@ -0,0 +1,100 @@ +package logger + +import ( + "bytes" + "fmt" + "io" + "log" + "runtime" +) + +const ( + // Colors + _LogColorReset = "\033[0m" + _LogColorRedBold = "\033[1;31m" + _LogColorGreenBold = "\033[1;32m" + _LogColorYellowBold = "\033[1;33m" + _LogColorBlueBold = "\033[1;34m" + _LogColorMagentaBold = "\033[1;35m" + _LogColorCyanBold = "\033[1;36m" + _LogColorWhiteBold = "\033[1;37m" +) + +type Console struct { + level int + + stdOutput *log.Logger + errOutput *log.Logger + + debugStr, infoStr, warnStr, errStr string +} + +func (output *Console) Level() int { + return output.level +} +func (output *Console) SetLevel(level int) int { + old := output.level + output.level = level + return old +} +func (output *Console) Init(level int, colorful bool, normal, err io.Writer) { + output.level = level + output.stdOutput = log.New(normal, "", log.Ldate|log.Lmicroseconds) + output.errOutput = log.New(err, "", log.Ldate|log.Lmicroseconds) + + if colorful { + output.debugStr = _LogColorWhiteBold + "[Debug] " + _LogColorReset + output.infoStr = _LogColorBlueBold + "[Info] " + _LogColorReset + output.warnStr = _LogColorMagentaBold + "[Warn] " + _LogColorReset + output.errStr = _LogColorRedBold + "[Error] " + _LogColorReset + } else { + output.infoStr = "[Debug] " + output.infoStr = "[Info] " + output.warnStr = "[Warn] " + output.errStr = "[Error] " + } +} + +func (output *Console) Debug(format string, v ...interface{}) { + if Debug >= output.level { + output.DebugForce(format, v...) + } +} +func (output *Console) Info(format string, v ...interface{}) { + if Info >= output.level { + output.InfoForce(format, v...) + } +} +func (output *Console) Warn(format string, v ...interface{}) { + if Warn >= output.level { + output.WarnForce(format, v...) + } +} +func (output *Console) Error(format string, v ...interface{}) { + if Error >= output.level { + output.ErrorForce(format, v...) + } +} + +func (output *Console) DebugForce(format string, v ...interface{}) { + var buff bytes.Buffer + buff.WriteString(output.debugStr) + buff.WriteString(format) + output.stdOutput.Printf(buff.String(), v...) +} +func (output *Console) InfoForce(format string, v ...interface{}) { + var buff bytes.Buffer + buff.WriteString(output.infoStr) + buff.WriteString(format) + output.stdOutput.Printf(buff.String(), v...) +} +func (output *Console) WarnForce(format string, v ...interface{}) { + var buff bytes.Buffer + buff.WriteString(output.warnStr) + buff.WriteString(format) + output.stdOutput.Printf(buff.String(), v...) +} +func (output *Console) ErrorForce(format string, v ...interface{}) { + _, file, line, _ := runtime.Caller(2) + output.errOutput.Printf(fmt.Sprintf("%s%s\n\tfile: %s:%d", output.errStr, format, file, line), v...) +} diff --git a/config/logger/null.go b/config/logger/null.go new file mode 100644 index 0000000..41f1d46 --- /dev/null +++ b/config/logger/null.go @@ -0,0 +1,18 @@ +package logger + +// ToNull + +type ToNull int + +func (output ToNull) Level() int { return int(output) } +func (output *ToNull) SetLevel(v int) int { *output = ToNull(v); return int(*output) } + +func (output ToNull) Debug(string, ...interface{}) {} +func (output ToNull) Info(string, ...interface{}) {} +func (output ToNull) Warn(string, ...interface{}) {} +func (output ToNull) Error(string, ...interface{}) {} + +func (output ToNull) DebugForce(string, ...interface{}) {} +func (output ToNull) InfoForce(string, ...interface{}) {} +func (output ToNull) WarnForce(string, ...interface{}) {} +func (output ToNull) ErrorForce(string, ...interface{}) {} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..14bcfe5 --- /dev/null +++ b/go.mod @@ -0,0 +1,54 @@ +module project + +go 1.23.3 + +require ( + github.com/gin-contrib/pprof v1.5.3 + github.com/gin-gonic/gin v1.10.0 + github.com/go-redis/redis/v8 v8.11.5 + github.com/go-sql-driver/mysql v1.9.2 + github.com/urfave/cli/v2 v2.27.6 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/mysql v1.5.7 + gorm.io/gorm v1.26.0 + gorm.io/plugin/dbresolver v1.6.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 // indirect + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/arch v0.16.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..eb6e413 --- /dev/null +++ b/go.sum @@ -0,0 +1,189 @@ +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/pprof v1.5.3 h1:Bj5SxJ3kQDVez/s/+f9+meedJIqLS+xlkIVDe/lcvgM= +github.com/gin-contrib/pprof v1.5.3/go.mod h1:0+LQSZ4SLO0B6+2n6JBzaEygpTBxe/nI+YEYpfQQ6xY= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= +github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= +golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs= +gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +gorm.io/plugin/dbresolver v1.6.0 h1:XvKDeOtTn1EIX6s4SrKpEH82q0gXVemhYjbYZFGFVcw= +gorm.io/plugin/dbresolver v1.6.0/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/handler/api/email_send.go b/internal/handler/api/email_send.go new file mode 100644 index 0000000..28231e9 --- /dev/null +++ b/internal/handler/api/email_send.go @@ -0,0 +1,71 @@ +package api + +import ( + "fmt" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" + "github.com/gin-gonic/gin" + "project/common/params" + "project/common/public" + "project/router/web" +) + +func EmailXSend(c *gin.Context) { + result := web.NewResponse() + defer web.SendResponse(c, result)() + + pm := params.EmailSend{} + err := c.ShouldBindJSON(&pm) + if err != nil { + result.SetErrCode(public.RespCodeErrorParam).AddError(err) + return + } + // 初始化客户端 + credentialsProvider := credentials.NewStaticAKCredentialsProvider(pm.AccessKeyId, pm.AccessKeySecret) + client, err := sdk.NewClientWithOptions( + "cn-hangzhou", // 区域 ID,例如 cn-hangzhou + sdk.NewConfig(), // 请求配置 + credentialsProvider, // + ) + if err != nil { + panic(err) + } + + // 创建请求 + request := requests.NewCommonRequest() + request.Method = "POST" + request.Scheme = "https" + request.Domain = "dm.aliyuncs.com" // 邮件推送服务域名 + request.Version = "2015-11-23" // API 版本 + request.ApiName = "SingleSendMail" // 接口名称 + + // 设置请求参数 + request.QueryParams["RegionId"] = "cn-hangzhou" + request.QueryParams["AccountName"] = "sender@your-domain.com" // 发件人地址(需验证) + request.QueryParams["AddressType"] = "1" // 0: 随机账号; 1: 发件人地址 + request.QueryParams["ReplyToAddress"] = "true" // 是否允许回复 + request.QueryParams["ToAddress"] = "recipient@example.com" // 收件人地址 + request.QueryParams["Subject"] = "Test Email from Aliyun" // 邮件主题 + request.QueryParams["HtmlBody"] = "

Hello

This is a test email .

" // HTML 邮件正文 + // request.QueryParams["TextBody"] = "This is a test email." // 纯文本正文(可选,优先使用 HtmlBody) + + // 发送请求 + res, err := client.ProcessCommonRequest(request) + if err != nil { + panic(err) + } + + // 输出响应 + //fmt.Println(res.GetHttpContentString()) + // 4. 处理响应 + if res.IsSuccess() { + result.SetData(fmt.Sprintf("发送失败: %s", res.GetHttpContentString())) + return + } else { + result.SetErrMsg( + public.RespCodeErrorUnknown, + fmt.Sprintf("发送失败: %s", res.GetHttpContentString())).AddError() + return + } +} diff --git a/internal/handler/api/sms_send.go b/internal/handler/api/sms_send.go new file mode 100644 index 0000000..cbb5ea5 --- /dev/null +++ b/internal/handler/api/sms_send.go @@ -0,0 +1,49 @@ +package api + +import ( + "fmt" + "github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi" + "github.com/gin-gonic/gin" + "project/common/params" + "project/common/public" + "project/router/web" +) + +func SmsXSend(c *gin.Context) { + result := web.NewResponse() + defer web.SendResponse(c, result)() + + pm := params.SmsSend{} + err := c.ShouldBindJSON(&pm) + if err != nil { + result.SetErrCode(public.RespCodeErrorParam).AddError(err) + return + } + client, err := dysmsapi.NewClientWithAccessKey(pm.RegionId, pm.AccessKeyId, pm.AccessKeySecret) + if err != nil { + fmt.Printf("初始化客户端失败: %v\n", err) + return + } + // 2. 设置请求参数 + request := dysmsapi.CreateSendMessageToGlobeRequest() + request.To = "目标手机号码" // 国际号码格式,例如:+85212345678 + request.Message = "1234" // 短信内容 + //request.From = "你的短信签名" // 短信签名 + + // 3. 发送短信 + res, err := client.SendMessageToGlobe(request) + if err != nil { + fmt.Printf("发送短信失败: %v\n", err) + return + } + + // 4. 处理响应 + if res.IsSuccess() { + fmt.Println("短信发送成功:", res.MessageId) + } else { + result.SetErrMsg(public.RespCodeErrorUnknown, fmt.Sprintf("短信发送失败: %s", res.ResponseDescription)).AddError(err) + return + } + + result.SetData(nil) +} diff --git a/internal/models/common.go b/internal/models/common.go new file mode 100644 index 0000000..b1be5c8 --- /dev/null +++ b/internal/models/common.go @@ -0,0 +1,64 @@ +package mysql + +import ( + "fmt" + "project/config/app_ser/component" + + "gorm.io/gorm" +) + +var ( + Ns9 = component.MySQLClient{} +) + +type Model interface { + TableName() string + DB() *gorm.DB +} + +func GetNs9DB() *gorm.DB { + return Ns9.DB +} +func GetById(m Model, id int) error { + tx := m.DB().Where("id=?", id).Take(m) + return tx.Error +} + +func Add(m Model) error { + tx := m.DB().Create(m) + return tx.Error +} + +func Save(m Model, id int, columns []string) error { + tx := m.DB().Select(columns) + tx.Where("id=?", id).Updates(m) + return tx.Error +} + +func OrderBySQL(orderBy string, order int, isNullBottom bool) string { + if orderBy != "" { + if isNullBottom { + orderBy = fmt.Sprintf("IF(%s IS NULL, 0, 1) desc, %s", orderBy, orderBy) + } + if order == 0 { // 为0倒序,否则升序 + return fmt.Sprintf("%s desc", orderBy) + } else { + return fmt.Sprintf("%s asc ", orderBy) + } + } + return "" +} + +func GetOrderByStr(by string, order int, isNullBottom bool) string { + if by != "" { + if isNullBottom { + by = fmt.Sprintf("IF(%s IS NULL, 0, 1) desc, %s", by, by) + } + if order == 1 { // 为0倒序,否则升序 + return fmt.Sprintf("%s desc ", by) + } else { + return fmt.Sprintf("%s asc ", by) + } + } + return "" +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..fa10d1f --- /dev/null +++ b/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" + "os" + "project/config" + "project/config/app_ser" + "project/config/app_ser/component" + mysql "project/internal/models" + "project/router" +) + +func main() { + app := app_ser.Application{} + + gin.SetMode(gin.ReleaseMode) + webRouter := gin.Default() + + //cRedis := component.Redis{ + // Clts: map[string]*component.RedisClient{"default": &redis.Default}, + //} + + var cMysql component.MySQL + cMysql = component.MySQL{ + Clts: map[string]*component.MySQLClient{"ns9": &mysql.Ns9}, + } + + // 关联配置 + ser := &app_ser.WebService{ + BasicService: app_ser.BasicService{ + InitBefore: func() error { + config.PrintfDSN = true + return nil + }, + RunBefore: func() (err error) { + // 慢查询阈值 + cMysql.Cfg.SlowThreshold = 500 + + // 初始化 + //logs.Instance = app.Log + config.AppEnv = &app.Cfg + + // init local cache + //if err = cache.Init(); err != nil { + // return err + //} + + // 初始化 + router.Router(webRouter) + return nil + }, + }, + Handler: webRouter, + } + + // 解析配置 + if err := ser.ParserFile(&config.FileCfg{ + Listen: &ser.Cfg.Port, + RunMode: &ser.Cfg.RunMode, + Mysql: &mysql.Ns9.Cfg, + //Redis: &redis.Default.Cfg, + }); err != nil { + fmt.Printf("Init Error: %s", err.Error()) + os.Exit(1) + } + + // 运行程序 + app.Component = map[string]app_ser.Component{ + "mysql": &cMysql, + //"redis": &cRedis, + //"cron": &cache.Default, + } + app.Start(nil, os.Args, ser) +} diff --git a/router/middleware/api-log.go b/router/middleware/api-log.go new file mode 100644 index 0000000..3eaf0fc --- /dev/null +++ b/router/middleware/api-log.go @@ -0,0 +1,19 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +func LogApiReq(c *gin.Context) { + //if config.AppEnv.Env() != gsf.EnvLocal { + // start := time.Now() + // c.Next() + // url := "contest::" + c.Request.URL.Path + // timeData := time.Since(start) + // date := time.Now().Format("20060102-15") + // sec := int(timeData / time.Second) + // _ = redis.ApiLog(url, date, sec) + //} else { + c.Next() + //} +} diff --git a/router/middleware/options.go b/router/middleware/options.go new file mode 100644 index 0000000..7f51b18 --- /dev/null +++ b/router/middleware/options.go @@ -0,0 +1,20 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +func OPTIONS(c *gin.Context) { + //// 跨域 + //if config.AppEnv.Env() == gsf.EnvLocal { + // c.Writer.Header().Set("Access-Control-Allow-Headers", "*") + // c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + //} + //// POST options 预请求 + //if c.Request.Method == "OPTIONS" { + // c.AbortWithStatus(204) + // return + //} + + c.Next() +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..68d8590 --- /dev/null +++ b/router/router.go @@ -0,0 +1,18 @@ +package router + +import ( + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" + "project/router/middleware" + "project/router/uri" +) + +func Router(router *gin.Engine) { + + pprof.Register(router) // 性能 + + router.Use(middleware.OPTIONS) + router.Use(middleware.LogApiReq) + + uri.InitAuthRouter(router.Group("/")) +} diff --git a/router/uri/auth_web.go b/router/uri/auth_web.go new file mode 100644 index 0000000..e4e1eb8 --- /dev/null +++ b/router/uri/auth_web.go @@ -0,0 +1,17 @@ +package uri + +import ( + "github.com/gin-gonic/gin" + "project/internal/handler/api" +) + +func authWeb(router *gin.RouterGroup) { + { + router.POST("/sms/send", api.SmsXSend) // 发送短信 + router.POST("/email/send", api.EmailXSend) // 发送邮件 + } +} + +func InitAuthRouter(router *gin.RouterGroup) { + authWeb(router.Group("/")) +} diff --git a/router/web/request.go b/router/web/request.go new file mode 100644 index 0000000..45e9d56 --- /dev/null +++ b/router/web/request.go @@ -0,0 +1,52 @@ +package web + +import ( + "github.com/gin-gonic/gin/binding" + "project/common/request" +) + +func init() { + request.Validate = binding.Validator.ValidateStruct +} + +var ( + BindForm = _FormBind{} + BindMultipartForm = _FormMultipartBind{} + BindQuery = _QueryBind{} + BindHead = _HeadHead{} +) + +/** +**************************************************/ + +type _FormBind struct { + request.FormBind +} + +func (_FormBind) Name() string { + return "custom_form" +} + +type _QueryBind struct { + request.QueryBind +} + +func (_QueryBind) Name() string { + return "custom_query" +} + +type _FormMultipartBind struct { + request.FormMultipartBind +} + +func (_FormMultipartBind) Name() string { + return "custom_form_multipart" +} + +type _HeadHead struct { + request.HeaderBind +} + +func (_HeadHead) Name() string { + return "custom_head" +} diff --git a/router/web/response.go b/router/web/response.go new file mode 100644 index 0000000..66bcb62 --- /dev/null +++ b/router/web/response.go @@ -0,0 +1,67 @@ +package web + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "project/common/response" +) + +type _Default struct { + Code int `json:"error_code"` + Msg string `json:"msg"` + Data interface{} `json:"data"` + Meta interface{} `json:"meta,omitempty"` + Errors []string `json:"errors,omitempty"` +} + +func (r *_Default) SetCustomError(err response.Error) response.Response { + response.SetCode(&r.Code, err.Code()) + return r._SetMsg(err.Error()) +} + +func (r *_Default) SetErrCode(code int) response.Response { + response.SetErrCode(&r.Code, &r.Msg, code) + return r +} + +func (r *_Default) SetErrMsg(code int, msg string, msgArgs ...interface{}) response.Response { + response.SetCode(&r.Code, code) + return r._SetMsg(msg, msgArgs...) +} + +func (r *_Default) _SetMsg(msg string, msgArgs ...interface{}) response.Response { + response.SetMsg(&r.Msg, msg, msgArgs...) + return r +} + +func (r *_Default) SetData(v interface{}) response.Response { + response.SetData(&r.Data, v) + return r +} + +func (r *_Default) SetDataMsg(v interface{}, msg string, msgArgs ...interface{}) response.Response { + response.SetData(&r.Data, v) + response.SetMsg(&r.Msg, msg, msgArgs...) + return r +} + +func (r *_Default) SetMeta(v interface{}) response.Response { + response.SetMeta(&r.Meta, v) + return r +} + +func (r *_Default) AddError(errs ...error) response.Response { + response.SetError(&r.Errors, errs...) + return r +} + +func NewResponse() response.Response { + return &_Default{} +} + +func SendResponse(c *gin.Context, req response.Response) func() { + return func() { + c.JSON(http.StatusOK, req) + } +}