feat(ik3cloud): 初始化项目基础配置与部门模块

- 添加 .gitignore 忽略规则
- 实现应用配置加载与管理逻辑
- 添加默认配置文件 app.ini
- 配置 Gitea CI/CD 工作流用于构建和部署
- 实现金蝶云客户端初始化功能
- 添加 RPC 插件支持 Consul 注册中心
- 实现部门数据获取及树形结构处理逻辑
- 添加通用工具函数库
- 初始化 Go 模块依赖管理
- 创建 Dockerfile 用于服务容器化部署
This commit is contained in:
2025-11-19 14:05:23 +08:00
commit 9968887665
23 changed files with 2016 additions and 0 deletions

View 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
View 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
View 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
}