feat(ik3cloud): 初始化项目基础配置与部门模块
- 添加 .gitignore 忽略规则 - 实现应用配置加载与管理逻辑 - 添加默认配置文件 app.ini - 配置 Gitea CI/CD 工作流用于构建和部署 - 实现金蝶云客户端初始化功能 - 添加 RPC 插件支持 Consul 注册中心 - 实现部门数据获取及树形结构处理逻辑 - 添加通用工具函数库 - 初始化 Go 模块依赖管理 - 创建 Dockerfile 用于服务容器化部署
This commit is contained in:
39
app/lib/ik3cloud/client.go
Normal file
39
app/lib/ik3cloud/client.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package ik3cloud
|
||||
|
||||
import (
|
||||
"github.com/deep-project/kingdee"
|
||||
"github.com/deep-project/kingdee/adapters"
|
||||
client2 "github.com/deep-project/kingdee/pkg/client"
|
||||
)
|
||||
|
||||
var Client = &client{}
|
||||
|
||||
type client struct {
|
||||
config *Config
|
||||
*client2.Client
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Host string // 接口地址
|
||||
AccountId string // 账套ID
|
||||
Username string // 用户名
|
||||
AppId string // 应用ID
|
||||
AppSecret string // 应用秘钥
|
||||
LanguageId string // 语言ID
|
||||
}
|
||||
|
||||
func InitClient(config *Config) (*client, error) {
|
||||
var err error
|
||||
Client.Client, err = kingdee.New(config.Host, &adapters.LoginBySign{
|
||||
AccountID: config.AccountId,
|
||||
Username: config.Username,
|
||||
AppID: config.AppId,
|
||||
AppSecret: config.AppSecret,
|
||||
LanguageID: config.LanguageId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
Client.config = config
|
||||
return Client, nil
|
||||
}
|
||||
152
app/lib/logger/logger.go
Normal file
152
app/lib/logger/logger.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
const (
|
||||
// DebugLevel logs are typically voluminous, and are usually disabled in
|
||||
// production.
|
||||
DebugLevel = zapcore.DebugLevel
|
||||
// InfoLevel is the default logging priority.
|
||||
InfoLevel = zapcore.InfoLevel
|
||||
// WarnLevel logs are more important than Info, but don't need individual
|
||||
// human review.
|
||||
WarnLevel = zapcore.WarnLevel
|
||||
// ErrorLevel logs are high-priority. If an application is running smoothly,
|
||||
// it shouldn't generate any error-level logs.
|
||||
ErrorLevel = zapcore.ErrorLevel
|
||||
// DPanicLevel logs are particularly important errors. In development the
|
||||
// logger panics after writing the message.
|
||||
DPanicLevel = zapcore.DPanicLevel
|
||||
// PanicLevel logs a message, then panics.
|
||||
PanicLevel = zapcore.PanicLevel
|
||||
// FatalLevel logs a message, then calls os.Exit(1).
|
||||
FatalLevel = zapcore.FatalLevel
|
||||
)
|
||||
|
||||
var (
|
||||
Logger = &logger{}
|
||||
|
||||
LowercaseLevelEncoder = zapcore.LowercaseLevelEncoder
|
||||
LowercaseColorLevelEncoder = zapcore.LowercaseColorLevelEncoder
|
||||
CapitalLevelEncoder = zapcore.CapitalLevelEncoder
|
||||
CapitalColorLevelEncoder = zapcore.CapitalColorLevelEncoder
|
||||
)
|
||||
|
||||
type logger struct {
|
||||
*zap.Logger
|
||||
Config *LoggerConfig
|
||||
hasShowLine bool
|
||||
}
|
||||
|
||||
type LoggerConfig struct {
|
||||
Levels []zapcore.Level `json:"level"`
|
||||
ShowLine bool `json:"showLine"`
|
||||
LogInConsole bool `json:"logInConsole"`
|
||||
Format string `json:"format"`
|
||||
EncodeLevel zapcore.LevelEncoder `json:"encodeLevel"`
|
||||
Prefix string `json:"prefix"`
|
||||
Writer func(config *LoggerConfig, filename string, level zapcore.Level) io.Writer
|
||||
}
|
||||
|
||||
// InitLogger @Title 初始化日志工具
|
||||
func InitLogger(config *LoggerConfig) *logger {
|
||||
Logger.Config = config
|
||||
|
||||
Logger.Logger = zap.New(zapcore.NewTee(
|
||||
Logger.getEncoderCore("debug", zapcore.DebugLevel),
|
||||
Logger.getEncoderCore("info", zapcore.InfoLevel),
|
||||
Logger.getEncoderCore("Warn", zapcore.WarnLevel),
|
||||
Logger.getEncoderCore("error", zapcore.ErrorLevel),
|
||||
), zap.AddCaller())
|
||||
if config.ShowLine {
|
||||
Logger.Logger = Logger.Logger.WithOptions(zap.AddCaller())
|
||||
}
|
||||
return Logger
|
||||
}
|
||||
|
||||
// getEncoderConfig 获取zapcore.EncoderConfig
|
||||
func (l *logger) getEncoderConfig() (config zapcore.EncoderConfig) {
|
||||
config = zapcore.EncoderConfig{
|
||||
MessageKey: "message",
|
||||
LevelKey: "level",
|
||||
TimeKey: "time",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: l.Config.EncodeLevel,
|
||||
EncodeTime: l.CustomTimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.FullCallerEncoder,
|
||||
}
|
||||
if config.EncodeLevel == nil {
|
||||
config.EncodeLevel = zapcore.LowercaseLevelEncoder
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// getEncoder 获取zapcore.Encoder
|
||||
func (l *logger) getEncoder() zapcore.Encoder {
|
||||
if l.Config.Format == "json" {
|
||||
return zapcore.NewJSONEncoder(l.getEncoderConfig())
|
||||
}
|
||||
return zapcore.NewConsoleEncoder(l.getEncoderConfig())
|
||||
}
|
||||
|
||||
// getEncoderCore 获取Encoder的zapcore.Core
|
||||
func (l *logger) getEncoderCore(filename string, level zapcore.Level) (core zapcore.Core) {
|
||||
return zapcore.NewCore(l.getEncoder(), l.getWriteSyncer(filename, level), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
||||
return l.inLevel(level) && lvl >= level
|
||||
}))
|
||||
}
|
||||
|
||||
func (l *logger) inLevel(level zapcore.Level) bool {
|
||||
for _, cLevel := range l.Config.Levels {
|
||||
if cLevel == level {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CustomTimeEncoder 自定义日志输出时间格式
|
||||
func (l *logger) CustomTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format(l.Config.Prefix + "2006/01/02 - 15:04:05.000"))
|
||||
}
|
||||
|
||||
// @author: [piexlmax](https://github.com/piexlmax)
|
||||
// @function: PathExists
|
||||
// @description: 文件目录是否存在
|
||||
// @param: path string
|
||||
// @return: bool, error
|
||||
func (l *logger) pathExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (l *logger) getWriteSyncer(filename string, level zapcore.Level) zapcore.WriteSyncer {
|
||||
var fileWriter io.Writer
|
||||
// 日志切割
|
||||
if l.Config.Writer != nil {
|
||||
fileWriter = l.Config.Writer(l.Config, filename, level)
|
||||
} else {
|
||||
return zapcore.AddSync(os.Stdout)
|
||||
}
|
||||
if l.Config.LogInConsole && !l.hasShowLine && l.inLevel(level) {
|
||||
l.hasShowLine = true
|
||||
return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(fileWriter))
|
||||
}
|
||||
return zapcore.AddSync(fileWriter)
|
||||
}
|
||||
291
app/lib/rpcplugin/consul.go
Normal file
291
app/lib/rpcplugin/consul.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package rpcplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
metrics "github.com/rcrowley/go-metrics"
|
||||
"github.com/rpcxio/libkv"
|
||||
"github.com/rpcxio/libkv/store"
|
||||
"github.com/rpcxio/libkv/store/consul"
|
||||
"github.com/smallnest/rpcx/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
consul.Register()
|
||||
}
|
||||
|
||||
// ConsulRegisterPlugin implements consul registry.
|
||||
type ConsulRegisterPlugin struct {
|
||||
// service address, for example, tcp@127.0.0.1:8972, quic@127.0.0.1:1234
|
||||
ServiceAddress string
|
||||
// consul addresses
|
||||
ConsulServers []string
|
||||
// base path for rpcx server, for example com/example/rpcx
|
||||
BasePath string
|
||||
Metrics metrics.Registry
|
||||
// Registered services
|
||||
Services []string
|
||||
metasLock sync.RWMutex
|
||||
metas map[string]string
|
||||
UpdateInterval time.Duration
|
||||
|
||||
Options *store.Config
|
||||
kv store.Store
|
||||
|
||||
dying chan struct{}
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Start starts to connect consul cluster
|
||||
func (p *ConsulRegisterPlugin) Start() error {
|
||||
if p.done == nil {
|
||||
p.done = make(chan struct{})
|
||||
}
|
||||
if p.dying == nil {
|
||||
p.dying = make(chan struct{})
|
||||
}
|
||||
|
||||
if p.kv == nil {
|
||||
kv, err := libkv.NewStore(store.CONSUL, p.ConsulServers, p.Options)
|
||||
if err != nil {
|
||||
log.Errorf("cannot create consul registry: %v", err)
|
||||
close(p.done)
|
||||
return err
|
||||
}
|
||||
p.kv = kv
|
||||
}
|
||||
|
||||
if p.BasePath[0] == '/' {
|
||||
p.BasePath = p.BasePath[1:]
|
||||
}
|
||||
|
||||
err := p.kv.Put(p.BasePath, []byte("rpcx_path"), &store.WriteOptions{IsDir: true})
|
||||
if err != nil {
|
||||
log.Errorf("cannot create consul path %s: %v", p.BasePath, err)
|
||||
close(p.done)
|
||||
return err
|
||||
}
|
||||
|
||||
if p.UpdateInterval > 0 {
|
||||
go func() {
|
||||
ticker := time.NewTicker(p.UpdateInterval)
|
||||
|
||||
defer ticker.Stop()
|
||||
defer p.kv.Close()
|
||||
|
||||
// refresh service TTL
|
||||
for {
|
||||
select {
|
||||
case <-p.dying:
|
||||
close(p.done)
|
||||
return
|
||||
case <-ticker.C:
|
||||
extra := make(map[string]string)
|
||||
if p.Metrics != nil {
|
||||
extra["calls"] = fmt.Sprintf("%.2f", metrics.GetOrRegisterMeter("calls", p.Metrics).RateMean())
|
||||
extra["connections"] = fmt.Sprintf("%.2f", metrics.GetOrRegisterMeter("connections", p.Metrics).RateMean())
|
||||
}
|
||||
|
||||
//set this same metrics for all services at this server
|
||||
for _, name := range p.Services {
|
||||
nodePath := fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress)
|
||||
kvPaire, err := p.kv.Get(nodePath)
|
||||
if err != nil {
|
||||
log.Warnf("can't get data of node: %s, will re-create, because of %v", nodePath, err.Error())
|
||||
|
||||
p.metasLock.RLock()
|
||||
meta := p.metas[name]
|
||||
p.metasLock.RUnlock()
|
||||
|
||||
err = p.kv.Put(nodePath, []byte(meta), &store.WriteOptions{TTL: p.UpdateInterval * 2})
|
||||
if err != nil {
|
||||
log.Errorf("cannot re-create consul path %s: %v", nodePath, err)
|
||||
}
|
||||
} else {
|
||||
v, _ := url.ParseQuery(string(kvPaire.Value))
|
||||
for key, value := range extra {
|
||||
v.Set(key, value)
|
||||
}
|
||||
_ = p.kv.Put(nodePath, []byte(v.Encode()), &store.WriteOptions{TTL: p.UpdateInterval * 2})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop unregister all services.
|
||||
func (p *ConsulRegisterPlugin) Stop() error {
|
||||
if p.kv == nil {
|
||||
kv, err := libkv.NewStore(store.CONSUL, p.ConsulServers, p.Options)
|
||||
if err != nil {
|
||||
log.Errorf("cannot create consul registry: %v", err)
|
||||
return err
|
||||
}
|
||||
p.kv = kv
|
||||
}
|
||||
|
||||
if p.BasePath[0] == '/' {
|
||||
p.BasePath = p.BasePath[1:]
|
||||
}
|
||||
|
||||
for _, name := range p.Services {
|
||||
nodePath := fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress)
|
||||
exist, err := p.kv.Exists(nodePath)
|
||||
if err != nil {
|
||||
log.Errorf("cannot delete path %s: %v", nodePath, err)
|
||||
continue
|
||||
}
|
||||
if exist {
|
||||
_ = p.kv.Delete(nodePath)
|
||||
log.Infof("delete path %s", nodePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
close(p.dying)
|
||||
<-p.done
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleConnAccept handles connections from clients
|
||||
func (p *ConsulRegisterPlugin) HandleConnAccept(conn net.Conn) (net.Conn, bool) {
|
||||
if p.Metrics != nil {
|
||||
metrics.GetOrRegisterMeter("connections", p.Metrics).Mark(1)
|
||||
}
|
||||
return conn, true
|
||||
}
|
||||
|
||||
// PreCall handles rpc call from clients
|
||||
func (p *ConsulRegisterPlugin) PreCall(_ context.Context, _, _ string, args interface{}) (interface{}, error) {
|
||||
if p.Metrics != nil {
|
||||
metrics.GetOrRegisterMeter("calls", p.Metrics).Mark(1)
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// Register handles registering event.
|
||||
// this service is registered at BASE/serviceName/thisIpAddress node
|
||||
func (p *ConsulRegisterPlugin) Register(name string, rcvr interface{}, metadata string) (err error) {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
err = errors.New("Register service `name` can't be empty")
|
||||
return
|
||||
}
|
||||
|
||||
if p.kv == nil {
|
||||
consul.Register()
|
||||
kv, err := libkv.NewStore(store.CONSUL, p.ConsulServers, nil)
|
||||
if err != nil {
|
||||
log.Errorf("cannot create consul registry: %v", err)
|
||||
return err
|
||||
}
|
||||
p.kv = kv
|
||||
}
|
||||
|
||||
if p.BasePath[0] == '/' {
|
||||
p.BasePath = p.BasePath[1:]
|
||||
}
|
||||
err = p.kv.Put(p.BasePath, []byte("rpcx_path"), &store.WriteOptions{IsDir: true})
|
||||
if err != nil {
|
||||
log.Errorf("cannot create consul path %s: %v", p.BasePath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
//nodePath := fmt.Sprintf("%s/%s", p.BasePath, name)
|
||||
//err = p.kv.Put(nodePath, []byte(name), &store.WriteOptions{IsDir: true})
|
||||
//if err != nil {
|
||||
// log.Errorf("cannot create consul path %s: %v", nodePath, err)
|
||||
// return err
|
||||
//}
|
||||
|
||||
nodePath := fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress)
|
||||
err = p.kv.Put(nodePath, []byte(metadata), &store.WriteOptions{TTL: p.UpdateInterval * 2})
|
||||
if err != nil {
|
||||
log.Errorf("cannot create consul path %s: %v", nodePath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
p.Services = append(p.Services, name)
|
||||
|
||||
p.metasLock.Lock()
|
||||
if p.metas == nil {
|
||||
p.metas = make(map[string]string)
|
||||
}
|
||||
p.metas[name] = metadata
|
||||
p.metasLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ConsulRegisterPlugin) RegisterFunction(serviceName, fname string, fn interface{}, metadata string) error {
|
||||
return p.Register(serviceName, fn, metadata)
|
||||
}
|
||||
|
||||
func (p *ConsulRegisterPlugin) Unregister(name string) (err error) {
|
||||
if len(p.Services) == 0 {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(name) == "" {
|
||||
err = errors.New("Unregister service `name` can't be empty")
|
||||
return
|
||||
}
|
||||
|
||||
if p.kv == nil {
|
||||
consul.Register()
|
||||
kv, err := libkv.NewStore(store.CONSUL, p.ConsulServers, nil)
|
||||
if err != nil {
|
||||
log.Errorf("cannot create consul registry: %v", err)
|
||||
return err
|
||||
}
|
||||
p.kv = kv
|
||||
}
|
||||
|
||||
if p.BasePath[0] == '/' {
|
||||
p.BasePath = p.BasePath[1:]
|
||||
}
|
||||
err = p.kv.Put(p.BasePath, []byte("rpcx_path"), &store.WriteOptions{IsDir: true})
|
||||
if err != nil {
|
||||
log.Errorf("cannot create consul path %s: %v", p.BasePath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
//nodePath := fmt.Sprintf("%s/%s", p.BasePath, name)
|
||||
//
|
||||
//err = p.kv.Put(nodePath, []byte(name), &store.WriteOptions{IsDir: true})
|
||||
//if err != nil {
|
||||
// log.Errorf("cannot create consul path %s: %v", nodePath, err)
|
||||
// return err
|
||||
//}
|
||||
|
||||
nodePath := fmt.Sprintf("%s/%s/%s", p.BasePath, name, p.ServiceAddress)
|
||||
|
||||
err = p.kv.Delete(nodePath)
|
||||
if err != nil {
|
||||
log.Errorf("cannot remove consul path %s: %v", nodePath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var services = make([]string, 0, len(p.Services)-1)
|
||||
for _, s := range p.Services {
|
||||
if s != name {
|
||||
services = append(services, s)
|
||||
}
|
||||
}
|
||||
p.Services = services
|
||||
|
||||
p.metasLock.Lock()
|
||||
if p.metas == nil {
|
||||
p.metas = make(map[string]string)
|
||||
}
|
||||
delete(p.metas, name)
|
||||
p.metasLock.Unlock()
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user