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

338
app/common/function.go Normal file
View File

@@ -0,0 +1,338 @@
package common
import (
"bytes"
"crypto/md5"
"encoding/hex"
"io"
"math/rand"
"net/http"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"time"
"github.com/shopspring/decimal"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)
// @Title 随机数种子
func init() {
// 随机数种子
rand.Seed(time.Now().UnixNano())
}
// MD5 @Title md5加密
func MD5(str string) string {
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}
// RandStr @Title 生成随机数
func RandStr(n int, str ...string) string {
s := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
if len(str) > 0 {
s = str[0]
}
res := ""
for i := 0; i < n; i++ {
res += string(s[rand.Intn(len(s))])
}
return res
}
// InArray @Title 是否在数组中
func InArray[T comparable](need T, array []T) bool {
for _, item := range array {
if item == need {
return true
}
}
return false
}
// CreateDateDir 根据当前日期来创建文件夹
func CreateDateDir(Path string) string {
folderName := time.Now().Format("20060102")
folderPath := filepath.Join(Path, folderName)
if _, err := os.Stat(folderPath); os.IsNotExist(err) {
os.MkdirAll(folderPath, 0755)
}
return folderName
}
// DownloadFormUrl @Title 下载文件
func DownloadFormUrl(src string, filename string) error {
res, err := http.Get(src)
if err != nil {
return err
}
defer res.Body.Close()
// 获得get请求响应的reader对象
body, err := io.ReadAll(res.Body)
if err != nil {
return err
}
out, err := os.Create(filename)
if err != nil {
return err
}
if _, err := io.Copy(out, bytes.NewReader(body)); err != nil {
return err
}
return nil
}
// Utf8ToGbk @Title utf8转gbk
func Utf8ToGbk(source string) string {
result, _, _ := transform.String(simplifiedchinese.GBK.NewEncoder(), source)
return result
}
// GbkToUtf8 @Title gbk转utf8
func GbkToUtf8(source string) string {
result, _, _ := transform.String(simplifiedchinese.GBK.NewDecoder(), source)
return result
}
// GetPassword @Title 密码加密
func GetPassword(password, salt string) string {
return MD5(salt + MD5(password+salt))
}
// IsPhone @Title 是否手机号
func IsPhone(phone string) bool {
return regexp.MustCompile(`^1\d{10}$`).MatchString(phone)
}
// Equal @TITLE 数据比较
func Equal[T any](a, b T, fun func(a, b T) bool) bool {
return fun(a, b)
}
// Ptr @TITLE 返回指针
func Ptr[T any](data T) *T {
return &data
}
// Split @TITLE 字符串分隔
func Split(s, sep string) []string {
if s == "" {
return nil
}
return strings.Split(s, sep)
}
// PtrData @TITLE 指针数据
func PtrData[T any](data *T) T {
if data != nil {
return *data
}
return *new(T)
}
// PtrDecimal @TITLE Decimal比较数据
func PtrDecimal(a *decimal.Decimal, b *decimal.Decimal) bool {
if a == b {
return true
}
if a == nil || b == nil {
return false
}
return a.Cmp(*b) == 0
}
// PtrCmp @TITLE 客户比较数据引用比较
func PtrCmp[T comparable](a *T, b *T) bool {
if a == b {
return true
}
if a == nil || b == nil {
return false
}
return *a == *b
}
// DefaultData @TITLE 反射创建默认值
func DefaultData(data any) any {
var value reflect.Value
if reflect.TypeOf(data).Kind() != reflect.Ptr {
value = reflect.ValueOf(&data).Elem()
} else {
value = reflect.ValueOf(data).Elem()
}
if value.Kind() == reflect.Interface {
value = reflect.New(value.Elem().Type()).Elem()
}
labels := map[string]string{}
typeOf := value.Type()
for i := 0; i < typeOf.NumField(); i++ {
field := typeOf.Field(i)
name := field.Name
if field.IsExported() {
if !field.Anonymous {
labels[name] = field.Tag.Get("label")
} else {
name = ""
}
if field.Type.Kind() == reflect.Struct {
_, valueLabels := DefaultValue(value.Field(i), name)
for valueKey, valueLabel := range valueLabels {
labels[valueKey] = valueLabel
}
}
if field.Type.Kind() == reflect.Slice {
newValue := reflect.New(field.Type.Elem()).Elem()
defaultValue, valueLabels := DefaultValue(newValue, name)
for valueKey, valueLabel := range valueLabels {
labels[valueKey] = valueLabel
}
value.Field(i).Set(reflect.Append(value.Field(i), defaultValue))
}
}
}
return map[string]interface{}{
"Data": value.Interface(),
"Label": labels,
}
}
func DefaultValue(value reflect.Value, name string) (reflect.Value, map[string]string) {
typeOf := value.Type()
labels := map[string]string{}
if typeOf.Kind() == reflect.Struct {
for i := 0; i < typeOf.NumField(); i++ {
field := typeOf.Field(i)
fieldName := field.Name
if field.IsExported() {
if !field.Anonymous {
labels[strings.Trim(name+"."+fieldName, ".")] = field.Tag.Get("label")
} else {
fieldName = ""
}
labels[strings.Trim(name+"."+fieldName, ".")] = field.Tag.Get("label")
if field.Type.Kind() == reflect.Struct {
_, valueLabels := DefaultValue(value.Field(i), fieldName)
for valueKey, valueLabel := range valueLabels {
labels[strings.Trim(name+"."+valueKey, ".")] = valueLabel
}
}
if field.Type.Kind() == reflect.Slice {
newValue := reflect.New(field.Type.Elem()).Elem()
defaultValue, valueLabels := DefaultValue(newValue, fieldName)
for valueKey, valueLabel := range valueLabels {
labels[strings.Trim(name+"."+valueKey, ".")] = valueLabel
}
value.Field(i).Set(reflect.Append(value.Field(i), defaultValue))
}
}
}
}
return value, labels
}
func wrapIfStruct[T any](data T) T {
v := reflect.ValueOf(data)
if v.Kind() == reflect.Struct {
ptr := reflect.New(v.Type())
ptr.Elem().Set(v)
return ptr.Interface().(T)
}
return data
}
func DefaultSlice[T any](data T) T {
data = wrapIfStruct(data)
value := reflect.ValueOf(data).Elem()
typeOf := value.Type()
switch typeOf.Kind() {
case reflect.Struct:
DefaultSliceValue(value)
case reflect.Slice:
if value.IsNil() {
value.Set(reflect.MakeSlice(value.Type(), 0, 0))
}
for j := 0; j < value.Len(); j++ {
DefaultSliceValue(value.Index(j))
}
}
if reflect.TypeOf(data).Kind() != reflect.Ptr {
return value.Interface().(T)
} else {
return value.Addr().Interface().(T)
}
}
func DefaultSliceValue(value reflect.Value) reflect.Value {
typeOf := value.Type()
if typeOf.Kind() == reflect.Struct {
for i := 0; i < typeOf.NumField(); i++ {
DefaultSliceType(value.Field(i))
}
}
return value
}
func DefaultSliceType(value reflect.Value) reflect.Value {
switch value.Type().Kind() {
case reflect.Ptr:
if !value.IsNil() {
DefaultSliceType(value.Elem())
} else {
valueType := value.Type().Elem()
if valueType.Kind() == reflect.Slice {
slice := reflect.MakeSlice(valueType, 0, 0)
ptr := reflect.New(valueType)
ptr.Elem().Set(slice)
value.Set(ptr)
}
}
case reflect.Struct:
DefaultSliceValue(value)
case reflect.Slice:
if value.IsNil() {
if value.CanSet() {
value.Set(reflect.MakeSlice(value.Type(), 0, 0))
}
}
for j := 0; j < value.Len(); j++ {
DefaultSliceValue(value.Index(j))
}
}
return value
}
// Unique @Title 去重
func Unique[T comparable](array []T) (result []T) {
for _, item := range array {
if !InArray(item, result) {
result = append(result, item)
}
}
return
}
// InsertSorted @TITLE array插入排序
func InsertSorted[T any](array []T, value T, comp func(int) bool) []T {
i, j := 0, len(array)
for i < j {
h := int(uint(i+j) >> 1) // avoid overflow when computing h
// i ≤ h < j
if !comp(h) {
i = h + 1 // preserves f(i-1) == false
} else {
j = h // preserves f(j) == true
}
}
// 插入:保持有序
array = append(array, value) // 增加一个空位
copy(array[i+1:], array[i:]) // 后移
array[i] = value // 插入值
return array
}

90
app/config/app.go Normal file
View File

@@ -0,0 +1,90 @@
package config
import (
"fmt"
"log"
"os"
"path/filepath"
"gopkg.in/ini.v1"
)
var Config = &App{}
type App struct {
iniLoader *ini.File
initConfig *IniConfig
Debug bool `ini:"debug"`
Log Log `ini:"log"`
Server Server `ini:"server"`
Ik3cloud Ik3cloud `ini:"ik3cloud"`
Page Page `ini:"page"`
}
type IniConfig struct {
ConfigPath string
ConfigName string
RunModelName string
RunModel string
RunModelErrAllow bool // 允许不使用拓展配置文件
}
// InitConfig @TITLE 初始化配置
func InitConfig(config *IniConfig) {
if config.ConfigName == "" {
config.ConfigName = "app.ini"
}
// 读取配置文件
load, err := ini.Load(filepath.Join(config.ConfigPath, config.ConfigName))
if err != nil {
log.Fatal("配置文件读取错误err:", err)
return
}
// 环境配置
if config.RunModel == "" {
config.RunModel = os.Getenv("RunModel")
if config.RunModel == "" {
config.RunModel = load.Section("").Key("defaultModel").String()
if config.RunModel == "" {
config.RunModel = "test"
}
}
}
if config.RunModelName == "" {
config.RunModelName = fmt.Sprintf("app.%s.ini", config.RunModel)
}
if err := load.Append(filepath.Join(config.ConfigPath, config.RunModelName)); err != nil {
if config.RunModelErrAllow {
log.Println("环境配置文件读取错误err:", err)
} else {
log.Fatal("环境配置文件读取错误err:", err)
}
}
// 运行配置
load.Append(filepath.Join(config.ConfigPath, "app.run.ini"))
// 映射配置
if err := load.MapTo(Config); err != nil {
log.Fatal("配置文件映射错误err:", err)
return
}
Config.iniLoader = load
Config.initConfig = config
}
// SetConfig @Title 写入配置
func (a *App) SetConfig(section, name, value string) error {
runConfig := filepath.Join(Config.initConfig.ConfigPath, "app.run.ini")
load, err := ini.LooseLoad(runConfig)
if err != nil {
return err
}
load.Section(section).Key(name).SetValue(value)
load.SaveTo(runConfig)
Config.iniLoader.Reload()
Config.iniLoader.MapTo(Config)
return nil
}
// GetRunModel @Title 获取RunModel
func (a *App) GetRunModel() string {
return a.initConfig.RunModel
}

11
app/config/ik3cloud.go Normal file
View File

@@ -0,0 +1,11 @@
package config
type Ik3cloud struct {
Host string `ini:"host"` // 接口地址
AccountId string `ini:"accountId"` // 账套ID
Username string `ini:"username"` // 用户名
AppId string `ini:"appId"` // 应用ID
AppSecret string `ini:"appSecret"` // 应用秘钥
LanguageId string `ini:"languageId"` // 语言ID
OrganizationNumber string `ini:"organizationNumber"` // 组织编码
}

12
app/config/log.go Normal file
View File

@@ -0,0 +1,12 @@
package config
type Log struct {
Director string `ini:"director"`
Levels []int64 `ini:"levels"`
ShowLine bool `ini:"showLine"`
LogInConsole bool `ini:"logInConsole"`
Prefix string `ini:"prefix"`
MaxAge int `ini:"maxAge"`
MaxSize int `ini:"maxSize"`
MaxBackups int `ini:"maxBackups"`
}

7
app/config/page.go Normal file
View File

@@ -0,0 +1,7 @@
package config
type Page struct {
MaxLimit int `ini:"maxLimit"`
MinLimit int `ini:"minLimit"`
DefaultLimit int `ini:"defaultLimit"`
}

14
app/config/server.go Normal file
View File

@@ -0,0 +1,14 @@
package config
type Server struct {
RegistryServer []string `ini:"registryServer"`
Addr string `ini:"addr"`
Host string `ini:"host"`
Port uint `ini:"port"`
BasePath string `ini:"basePath"`
FilePath string `ini:"filePath"`
WorkerId int64 `ini:"workerId"`
Segment string `ini:"segment"`
YzmExpiry int `ini:"yzmExpiry"`
YzmRate int `ini:"yzmRate"`
}

View File

@@ -0,0 +1,17 @@
package controller
import (
"context"
"ik3cloud/app/logic"
"git.kumo.work/shama/service/ik3cloud"
)
type Department struct {
}
// All @TITLE 获取部门
func (d *Department) All(ctx context.Context, args int, reply *[]ik3cloud.DepartmentItem) (err error) {
*reply, err = logic.DepartmentLogic.All()
return err
}

21
app/controller/staff.go Normal file
View File

@@ -0,0 +1,21 @@
package controller
import (
"context"
"ik3cloud/app/logic"
"git.kumo.work/shama/service/ik3cloud"
)
type Staff struct {
}
// List @TITLE 员工列表
func (s *Staff) List(ctx context.Context, args ik3cloud.ArgsStaffList, reply *ik3cloud.ReplyStaffList) error {
list, total, err := logic.StaffLogic.List(args.Page, args.Search)
*reply = ik3cloud.ReplyStaffList{
List: list,
Total: total,
}
return err
}

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
}

68
app/logic/department.go Normal file
View File

@@ -0,0 +1,68 @@
package logic
import (
"encoding/json"
"ik3cloud/app/common"
"ik3cloud/app/config"
"ik3cloud/app/lib/ik3cloud"
ik3cloud2 "git.kumo.work/shama/service/ik3cloud"
)
var DepartmentLogic = &departmentLogic{}
type departmentLogic struct {
}
// All @TITLE 全部部门
func (d *departmentLogic) All() (result []ik3cloud2.DepartmentItem, err error) {
raw, err := ik3cloud.Client.ExecuteBillQuery(map[string]interface{}{
"FormId": "BD_Department",
"FieldKeys": "FNumber,FName,FParentID.FNumber,FParentID.FName",
"FilterString": []map[string]interface{}{{
"FieldName": "FUseOrgId.FNumber",
"Compare": "67",
"Value": config.Config.Ik3cloud.OrganizationNumber,
"Left": "",
"Right": "",
"Logic": "",
}},
})
if err != nil {
return
}
var data [][]*string
if err = json.Unmarshal(raw, &data); err != nil {
return
}
for _, item := range data {
result = append(result, ik3cloud2.DepartmentItem{
Number: item[0],
Name: item[1],
ParentNumber: item[2],
ParentName: item[3],
})
}
return d.getTree(result), nil
}
// @Title 获取菜单树
func (d *departmentLogic) getTree(datas []ik3cloud2.DepartmentItem) (result []ik3cloud2.DepartmentItem) {
result = []ik3cloud2.DepartmentItem{}
for i, item := range datas {
if item.ParentNumber != nil {
for index, value := range datas {
if common.PtrCmp(item.ParentNumber, value.Number) {
datas[index].Children = append(datas[index].Children, &datas[i])
break
}
}
}
}
for _, item := range datas {
if item.ParentNumber != nil {
result = append(result, item)
}
}
return
}

101
app/logic/staff.go Normal file
View File

@@ -0,0 +1,101 @@
package logic
import (
"encoding/json"
"ik3cloud/app/common"
"ik3cloud/app/config"
"ik3cloud/app/lib/ik3cloud"
ik3cloud2 "git.kumo.work/shama/service/ik3cloud"
"git.kumo.work/shama/service/lib/bean"
)
var StaffLogic = &staffLogic{}
type staffLogic struct {
}
// List @TITLE 员工列表
func (s *staffLogic) List(page bean.Page, search ik3cloud2.StaffSearch) (list []ik3cloud2.StaffItem, total int64, err error) {
where := []map[string]interface{}{
{
"FieldName": "FCreateOrgId.FNumber",
"Compare": "67",
"Value": config.Config.Ik3cloud.OrganizationNumber,
"Left": "",
"Right": "",
"Logic": 0,
},
}
{
// 搜索
// 状态
if search.Status > 0 {
status := "B"
if search.Status == 1 {
status = "A"
}
where = append(where, map[string]interface{}{
"FieldName": "FForbidStatus",
"Compare": "105",
"Value": status,
"Left": "",
"Right": "",
"Logic": 0,
})
}
// 姓名
if search.Name != "" {
where = append(where, map[string]interface{}{
"FieldName": "FName",
"Compare": "17",
"Value": search.Name,
"Left": "",
"Right": "",
"Logic": 0,
})
}
// 手机号
if search.Phone != "" {
where = append(where, map[string]interface{}{
"FieldName": "FBaseProperty",
"Compare": "17",
"Value": search.Phone,
"Left": "",
"Right": "",
"Logic": 0,
})
}
}
raw, err := ik3cloud.Client.ExecuteBillQuery(map[string]interface{}{
"FormId": "BD_Empinfo",
"FieldKeys": "FNumber,FName,FParentID.FNumber,FParentID.FName",
"FilterString": where,
"StartRow": page.GetStart(),
"Limit": page.GetLimit(),
})
if err != nil {
return
}
var data [][]*string
if err = json.Unmarshal(raw, &data); err != nil {
return
}
for _, item := range data {
status := "禁用"
if common.PtrCmp(item[5], common.Ptr("A")) {
status = "启用"
}
list = append(list, ik3cloud2.StaffItem{
Number: item[0],
Name: item[1],
Position: item[2],
Phone: item[3],
Mobile: item[4],
Status: common.Ptr(status),
})
}
return
}

19
app/router/router.go Normal file
View File

@@ -0,0 +1,19 @@
package router
import (
"ik3cloud/app/controller"
"github.com/smallnest/rpcx/server"
)
// SetRouter @TITLE 配置路由
func SetRouter(server *server.Server) {
// 员工
if err := server.RegisterName("staff", &controller.Staff{}, ""); err != nil {
return
}
// 部门
if err := server.RegisterName("department", &controller.Department{}, ""); err != nil {
return
}
}