Compare commits

...

64 Commits

Author SHA1 Message Date
6ffe49ff5b feat 初始化 2026-10-29 11:28:42 +08:00
09c1df8a22 feat 初始化 2026-10-29 10:12:22 +08:00
332d812347 feat 修改包路径 2026-10-28 16:21:44 +08:00
c39cea8656 feat 修改包路径 2026-10-28 15:58:42 +08:00
9439be8a60 feat 自定义问答 2026-03-11 20:17:58 +08:00
558677bc3a feat 包命名规范 2026-02-11 14:48:21 +08:00
0ca8a0ccbb feat 基础模块拆分为雪花和日志模块 2026-02-07 13:13:02 +08:00
5a0f4b71d4 feat 模块删除bind函数 2026-02-06 22:51:47 +08:00
44ebbe4449 feat 废弃jwt 2026-02-06 22:22:43 +08:00
5dc5391b07 feat sn 改成 int64 2026-01-30 11:53:20 +08:00
427fca7ed1 feat kafka 改版 2 2026-01-30 10:53:00 +08:00
a2251b9cb7 feat kafka 改版 1 2026-01-29 16:20:29 +08:00
be7b922cbf feat qgdzs协议 2026-01-28 10:13:22 +08:00
a93b5fd1aa feat kafka 2026-01-27 17:14:05 +08:00
7271181501 feat 协议 2026-01-26 01:02:04 +08:00
f22d22cbdc feat tracer 2026-01-25 17:16:51 +08:00
81e422d304 feat kafka 2026-01-24 22:46:15 +08:00
b45df9c4ee feat kafka 2026-01-24 21:46:14 +08:00
4d661d0813 feat kafka 2026-01-23 21:22:58 +08:00
1edebb439c feat 默认模块 2026-01-20 12:07:18 +08:00
a47557920c feat grpc模块 2026-01-19 10:53:13 +08:00
22d48542a8 feat 服务发现模块 2026-01-18 00:06:58 +08:00
3de703eb83 feat 自定义拦截器 2026-01-16 23:50:14 +08:00
d944729ad0 feat Prometheus 2026-01-16 22:34:17 +08:00
e59d106700 feat 链路追踪 2026-01-16 22:01:35 +08:00
7cb057fc91 feat 协议 2026-01-14 19:03:46 +08:00
7aaa54403b feat 协议 2026-01-14 18:50:48 +08:00
da4b2dff49 feat 协议 2026-01-14 18:45:56 +08:00
695f5e7b04 feat 删除-拦截器取usn 2026-01-14 18:09:22 +08:00
a2025e8901 feat 拦截器取usn 2026-01-14 16:00:26 +08:00
53d4e4513e feat 协议 2026-01-14 15:32:42 +08:00
8e2d96844e feat 协议 2026-01-14 15:27:26 +08:00
9518f3da5f feat grpc客户端整理 2026-01-14 14:44:49 +08:00
29d5444082 feat 协议 2026-01-14 14:33:22 +08:00
1c90e21050 feat 错误码 2026-01-14 14:21:41 +08:00
6aefdaf650 feat 协议 2026-01-14 12:01:42 +08:00
34b2e45e3d feat 协议 2026-01-13 17:54:15 +08:00
7812a3c669 feat 协议 2026-01-13 09:46:17 +08:00
faf46e2bba feat 协议 2026-01-13 00:09:30 +08:00
b678307d61 feat 协议 2026-01-12 20:32:58 +08:00
d502d615fe feat 协议 2026-01-12 16:57:32 +08:00
8e4dc3e339 feat 拆出qgdzs 2026-01-12 16:53:40 +08:00
b1e7d33940 feat 增加grpc客户端 2026-01-12 16:22:58 +08:00
375b41c245 feat 定义服务 2026-01-12 16:10:03 +08:00
479282ccfd feat 协议 2026-01-12 15:57:05 +08:00
d433cd03a9 feat sc改名ss 2026-01-12 12:06:22 +08:00
ec291d4332 feat ss改名rs 2026-01-12 11:53:11 +08:00
8c09b125a4 feat ss改名rs 2026-01-12 11:46:51 +08:00
0be254c6a2 feat 协议 2026-01-10 23:30:08 +08:00
015e333b88 feat usn sid 改成string 2026-01-10 16:13:19 +08:00
8c6614578b feat usn sid 改成string 2026-01-10 16:02:23 +08:00
e209ec64af feat 协议 2026-01-10 15:33:11 +08:00
4b8db63ba4 feat 协议 2026-01-10 10:44:06 +08:00
a73797b3bc feat 协议 2026-01-10 10:11:25 +08:00
f312ac4f3c feat 协议 2026-01-10 10:04:10 +08:00
c5d30adafd feat 协议 2026-01-10 00:21:53 +08:00
4d8501c3a9 feat 协议 2026-01-09 21:31:28 +08:00
f381510a0d feat 协议 2026-01-09 14:08:41 +08:00
69e82ec0fe feat redis 增加 HDel 2026-01-08 22:45:40 +08:00
e02e56781e feat 修改model名称 2026-01-07 14:10:47 +08:00
97c7813a24 feat 排队 2026-01-06 18:36:17 +08:00
6523820cf6 feat 单元测试 2026-01-05 00:32:24 +08:00
3ea3a3ac6d feat 初次提交 2026-01-03 14:26:09 +08:00
18eb946934 .gitignore 2026-01-03 14:17:26 +08:00
78 changed files with 5300 additions and 1 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.idea
*/.idea

23
PORTS.md Normal file
View File

@@ -0,0 +1,23 @@
# Ports
端口分配记录
---
gateway 18500-18599
---
third-party 18600-18699
---
scene 18700-18799
---
user 18800-18899
---
post 18900-18999
---
notice 19000-19099
---
admin 19200-19299
---
timer 19400-19499
---
qgdzs 1 // TODO
---

190
README.md
View File

@@ -1,3 +1,193 @@
# common # common
公共代码集合 公共代码集合
---
## ✅ 微服务十大核心原则(实战导向)
> 这些不是理论教条,而是无数团队踩坑后总结出的“生存法则”。
---
### 1. **单一职责 & 高内聚(Single Responsibility)**
- 每个微服务应围绕 **一个明确的业务能力**(如“用户管理”、“支付”、“通知”)。
- ❌ 不要按技术分层拆服务(如“所有 DB 服务”、“所有 API 服务”)。
- ✅ 建议用 **DDD(领域驱动设计)** 划分限界上下文(Bounded Context)。
> 📌 口诀:**“一个服务,一个事”**
---
### 2. **自治性(Autonomy)**
- 服务应能 **独立开发、测试、部署、扩展、故障恢复**。
- 依赖越少越好;跨服务调用必须有 **降级、熔断、超时** 机制。
- 使用 **契约测试(Contract Testing)** 保证接口兼容(如 Pact)。
> 🔧 工具推荐:
> - gRPC + Protobuf(强契约)
> - OpenAPI(REST)
> - WireMock / Pact(契约测试)
---
### 3. **数据私有(Database per Service)** ← 你已掌握!
- 每个服务独占数据存储,**禁止跨服务直连数据库**。
- 跨服务数据交互靠:
- **同步**:API(gRPC/HTTP)
- **异步**:事件(Kafka / RabbitMQ / Pulsar)
> 💡 技巧:用 **事件溯源(Event Sourcing) + CQRS** 构建复杂查询视图。
---
### 4. **无状态(Stateless)**
- 服务实例本身不保存会话或状态(状态存 Redis / DB / Cookie)。
- 好处:**水平扩展容易**,K8s 可随意扩缩容。
> ⚠️ 例外:批处理、流计算等有状态场景可用 StatefulSet,但要谨慎。
---
### 5. **可观测性(Observability)三件套**
微服务没有可观测性 = 盲人开车!
| 组件 | 作用 | 工具推荐 |
|------|------|--------|
| **Logging** | 记录结构化日志 | Zap / Logrus + Loki / ELK |
| **Metrics** | 监控性能指标 | Prometheus + Grafana |
| Assistant | 追踪请求链路 | Jaeger / Zipkin / OpenTelemetry |
> ✅ 必做:
> - 所有日志带 `trace_id`
> - 所有 gRPC 调用自动埋点
> - 关键路径加自定义指标(如“登录成功率”)
---
### 6. **弹性设计(Resilience)**
网络不可靠!必须假设:
- 其他服务会宕机
- 网络会延迟或丢包
✅ 必备机制:
- **超时(Timeout)**:每个调用设上限(如 1s)
- **重试(Retry)**:带退避策略(指数退避)
- **熔断(Circuit Breaker)**:失败太多就短路(如 Hystrix / Sentinel)
- **限流(Rate Limiting)**:保护自己不被压垮
- **降级(Fallback)**:返回默认值或缓存数据
> 🛠 Go 推荐库:
> - `go-resiliency`
> - `sony/gobreaker`
> - 自研中间件(结合 context + retry)
---
### 7. **API 优先(API-First Design)**
- 先定义 `.proto`(gRPC)或 OpenAPI(REST),再写代码。
- 客户端 SDK 自动生成(如 `protoc-gen-go`)。
- 版本管理:**向后兼容优先**,避免频繁改接口。
> 📁 目录建议:
> ```
> api/
> ├── user/v1/user.proto
> └── order/v1/order.proto
> ```
---
### 8. **基础设施即代码(IaC) & GitOps**
- 数据库、K8s、网关配置全部代码化(Terraform / Helm / Kustomize)。
- 所有变更通过 Git PR 触发自动化部署。
- 环境一致性:dev / staging / prod 配置几乎相同。
> ✅ 好处:**“一键重建整个环境”**
---
### 9. **安全内建(Security by Default)**
- **mTLS**:服务间通信加密(Istio / Linkerd)
- **RBAC**:基于角色的访问控制
- **敏感信息**:用 Vault / K8s Secret 管理,不硬编码
- **输入校验**:所有 API 参数严格验证(如 `validator.v10`)
> 🔒 原则:**“零信任网络”** —— 不信任任何内部流量。
---
### 10. **演进式设计(Evolutionary Architecture)**
- 不追求“一次性完美架构”
- 用 **Strangler Fig Pattern(绞杀者模式)** 逐步替换旧系统
- 定期做 **架构重构** 和 **技术债清理**
> 🌱 微服务是“种出来的”,不是“画出来的”。
---
## 🧱 推荐架构分层(Go 微服务)
```
user-service/
├── api/ # .proto 文件(对外契约)
├── cmd/ # main 入口
├── internal/
│ ├── handler/ # gRPC/HTTP handlers(薄层)
│ ├── service/ # 业务逻辑(核心)
│ ├── repository/ # 数据访问(接口 + mysql 实现)
│ └── model/ # 领域模型(与 DB 解耦)
├── pkg/ # 可共享工具(谨慎!)
├── deploy/ # K8s / Docker 配置
└── go.mod
```
> 🔑 关键:`internal/` 禁止被外部 import,强制通过 API 交互。
---
## 🚫 微服务常见反模式(务必避开!)
| 反模式 | 后果 |
|-------|------|
| 分布式单体 | 失去微服务所有优势 |
| 共享数据库 | 强耦合,无法独立演进 |
| 同步调用链过长(A→B→C→D) | 故障放大,雪崩风险 |
| 无监控无告警 | 出问题找不到原因 |
| 手动部署 | 发布慢,易出错 |
---
## ✅ 最后建议:从小做起,逐步完善
1. **先保证“能跑”**:一个服务 + 独立 DB + gRPC API
2. **加上可观测性**:日志 + Metrics + Tracing
3. **加上弹性机制**:超时 + 重试
4. **加上 CI/CD**:自动化测试 & 部署
5. **再考虑事件驱动、CQRS、Service Mesh...**
> 🌟 **好的微服务架构不是设计出来的,而是在约束和实践中演化出来的。**
---
# 增加服务器步骤
代码
1. go module 命名:`git.hlsq.asia/alex/service-${服务名}`
2. `deploy、config` 修改
common
1. 在 `discover\common\define.go` 定义服务发现
2. 在 `net\grpc\grpcclient` 实现客户端发现
public
1. 在 `Proto\RpcServer\sources` 定义gRPC服务
db
1. 创建独立db,命名为`${服务名}_db`
quickly
1. `MySQL Model` 增加数据库配置
gateway
1. 在 `internal\net\httpgateway\router.go` 定义路由

79
config/config.go Normal file
View File

@@ -0,0 +1,79 @@
package config
type AppConfig struct {
Name string `yaml:"name"`
}
type LogConfig struct {
Debug bool `yaml:"debug"`
MaxSize int32 `yaml:"maxSize"`
MaxBackups int32 `yaml:"maxBackups"`
MaxAge int32 `yaml:"maxAge"`
Level string `yaml:"level"`
}
type MetricConfig struct {
Prometheus *struct {
Address string `yaml:"address"`
Port int32 `yaml:"port"`
} `yaml:"prometheus"`
Jaeger *struct {
Endpoint string `yaml:"endpoint"`
SamplingRate float64 `yaml:"samplingRate"`
} `yaml:"jaeger"`
}
type DBConfig struct {
Etcd *EtcdConfig `yaml:"etcd"`
MySQL map[string]*MySQLConfig `yaml:"mysql"`
Mongo map[string]*MongoConfig `yaml:"mongo"`
Redis *RedisConfig `yaml:"redis"`
Kafka *KafkaConfig `yaml:"kafka"`
}
type EtcdConfig struct {
Endpoints []string `yaml:"endpoints"`
}
type MySQLConfig struct {
Dsn string `yaml:"dsn"`
MaxOpenConn int32 `yaml:"maxOpenConn"`
MaxIdleConn int32 `yaml:"maxIdleConn"`
ConnMaxLifetimeSec int32 `yaml:"connMaxLifetimeSec"`
ConnMaxIdleTimeSec int32 `yaml:"connMaxIdleTimeSec"`
LogLevel string `yaml:"logLevel"`
}
type MongoConfig struct {
URI string `yaml:"uri"`
}
type RedisConfig struct {
Addr string `yaml:"addr"`
Password string `yaml:"password"`
DB int `yaml:"db"`
}
type KafkaConfig struct {
Brokers []string `yaml:"brokers"`
}
type ServeConfig struct {
Grpc *GrpcConfig `yaml:"grpc"`
Socket *struct {
Web *AddressConfig `yaml:"web"`
Raw *AddressConfig `yaml:"raw"`
} `yaml:"socket"`
Http *AddressConfig `yaml:"http"`
}
type GrpcConfig struct {
Address string `yaml:"address"`
Port int32 `yaml:"port"`
TTL int64 `yaml:"ttl"`
}
type AddressConfig struct {
Address string `yaml:"address"`
Port int32 `yaml:"port"`
}

83
config/loader.go Normal file
View File

@@ -0,0 +1,83 @@
package config
import (
"encoding/json"
"fmt"
"github.com/spf13/viper"
"os"
"strings"
"sync"
)
const (
envConfigPrefix = "XH_G"
)
var (
currentEnv EnvType // 当前环境变量
currentEnvOnce sync.Once
currentCommit string // 当前提交的CommitID
currentCommitOnce sync.Once
)
type EnvType string
const (
EnvDev EnvType = "dev"
EnvTest EnvType = "test"
EnvProd EnvType = "prod"
)
// LoadConfig 加载并返回应用配置
func LoadConfig[T any](configDir string, configPtr *T) (*T, error) {
v := viper.New()
v.SetEnvPrefix(envConfigPrefix)
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
env := v.GetString("env")
if env == "" {
env = "dev"
}
v.SetConfigName(fmt.Sprintf("config.%s", strings.ToLower(env)))
v.AddConfigPath(configDir)
v.SetConfigType("yaml")
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
if err := v.Unmarshal(&configPtr); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
marshal, _ := json.Marshal(configPtr)
fmt.Printf("Configuration loading completed: %v\n", string(marshal))
return configPtr, nil
}
// Env 获取环境
func Env() EnvType {
currentEnvOnce.Do(func() {
if v, ok := os.LookupEnv(envConfigPrefix + "_ENV"); ok {
currentEnv = EnvType(v)
} else {
currentEnv = EnvDev
}
})
return currentEnv
}
// Commit 获取提交ID
func Commit() string {
currentCommitOnce.Do(func() {
if v, ok := os.LookupEnv(envConfigPrefix + "_COMMIT"); ok {
currentCommit = v
} else {
currentCommit = "unknown"
}
})
return currentCommit
}

80
db/etcd/client.go Normal file
View File

@@ -0,0 +1,80 @@
package etcd
import (
"context"
"git.hlsq.asia/alex/service-common/config"
"go.etcd.io/etcd/client/v3"
"time"
)
var instance *Client
type Client struct {
cli *clientv3.Client
}
// Init 初始化
func Init(cfg *config.EtcdConfig) error {
client, err := clientv3.New(clientv3.Config{
Endpoints: cfg.Endpoints,
DialTimeout: 5 * time.Second,
})
instance = &Client{
cli: client,
}
return err
}
// Close 关闭
func Close() error {
if instance != nil && instance.cli != nil {
return instance.cli.Close()
}
return nil
}
func GetClient() *Client {
return instance
}
func (c *Client) Raw() *clientv3.Client {
return c.cli
}
// Get 获取数据
func (c *Client) Get(key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return c.cli.Get(ctx, key, opts...)
}
// Put 创建数据
func (c *Client) Put(key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return c.cli.Put(ctx, key, val, opts...)
}
// Grant 创建租约
func (c *Client) Grant(ttl int64) (*clientv3.LeaseGrantResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return c.cli.Grant(ctx, ttl)
}
// KeepAlive 保活租约
func (c *Client) KeepAlive(id clientv3.LeaseID) (<-chan *clientv3.LeaseKeepAliveResponse, error) {
return c.cli.KeepAlive(context.Background(), id)
}
// Revoke 撤销租约
func (c *Client) Revoke(id clientv3.LeaseID) (*clientv3.LeaseRevokeResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return c.cli.Revoke(ctx, id)
}
// Watch 监听数据
func (c *Client) Watch(key string, opts ...clientv3.OpOption) clientv3.WatchChan {
return c.cli.Watch(context.Background(), key, opts...)
}

41
db/kafka/carrier.go Normal file
View File

@@ -0,0 +1,41 @@
package kafka
import (
"context"
"github.com/IBM/sarama"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
type Carrier struct {
}
func NewCarrier() *Carrier {
return &Carrier{}
}
func (c *Carrier) Inject(ctx context.Context) []sarama.RecordHeader {
headers := make([]sarama.RecordHeader, 0)
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, carrier)
for k, v := range carrier {
headers = append(headers, sarama.RecordHeader{Key: []byte(k), Value: []byte(v)})
}
return headers
}
func (c *Carrier) ExtractConsumer(headers []*sarama.RecordHeader) context.Context {
carrier := propagation.MapCarrier{}
for _, header := range headers {
carrier[string(header.Key)] = string(header.Value)
}
return otel.GetTextMapPropagator().Extract(context.Background(), carrier)
}
func (c *Carrier) ExtractProducer(headers []sarama.RecordHeader) context.Context {
carrier := propagation.MapCarrier{}
for _, header := range headers {
carrier[string(header.Key)] = string(header.Value)
}
return otel.GetTextMapPropagator().Extract(context.Background(), carrier)
}

60
db/kafka/client.go Normal file
View File

@@ -0,0 +1,60 @@
package kafka
import (
"git.hlsq.asia/alex/service-common/config"
"github.com/IBM/sarama"
"time"
)
var client *Client
type Client struct {
producer sarama.AsyncProducer
consumer sarama.ConsumerGroup
}
func Init(cfg *config.KafkaConfig, appName string) error {
producer, err := getAsyncProducer(cfg)
if err != nil {
return err
}
consumer, err := getConsumer(cfg, appName)
if err != nil {
return err
}
client = &Client{
producer: producer,
consumer: consumer,
}
go producerError()
go consumerError()
return nil
}
func getAsyncProducer(cfg *config.KafkaConfig) (sarama.AsyncProducer, error) {
conf := sarama.NewConfig()
conf.Producer.RequiredAcks = sarama.WaitForAll
conf.Producer.Return.Errors = true
conf.Producer.Retry.Max = 5
conf.Producer.Retry.Backoff = 100 * time.Millisecond
return sarama.NewAsyncProducer(cfg.Brokers, conf)
}
func getConsumer(cfg *config.KafkaConfig, appName string) (sarama.ConsumerGroup, error) {
conf := sarama.NewConfig()
conf.Consumer.Return.Errors = true
conf.Consumer.Group.Session.Timeout = 10 * time.Second
conf.Consumer.Offsets.AutoCommit.Enable = true
conf.Consumer.Offsets.Initial = sarama.OffsetOldest
return sarama.NewConsumerGroup(cfg.Brokers, appName, conf)
}
func Close() error {
if client != nil && client.producer != nil {
_ = client.producer.Close()
}
if client != nil && client.consumer != nil {
_ = client.consumer.Close()
}
return nil
}

147
db/kafka/consumer.go Normal file
View File

@@ -0,0 +1,147 @@
package kafka
import (
"context"
"encoding/json"
"errors"
"fmt"
"git.hlsq.asia/alex/service-common/log"
"git.hlsq.asia/alex/service-common/utils"
"github.com/IBM/sarama"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
otelcodes "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"reflect"
"sync"
"time"
)
func NewConsumer() *Consumer {
ctx, cancel := context.WithCancel(context.Background())
return &Consumer{
ctx: ctx,
cancel: cancel,
}
}
type Consumer struct {
ctx context.Context
cancel context.CancelFunc
}
func (c *Consumer) Consume(t []Topic) {
for {
topicArr := make([]string, 0)
handlerMap := make(map[string]*sync.Pool)
for _, v := range t {
topicArr = append(topicArr, v.Name())
typ := reflect.ValueOf(v).Elem().Type()
handlerMap[v.Name()] = &sync.Pool{New: func() interface{} {
return reflect.New(typ).Interface()
}}
}
err := client.consumer.Consume(c.ctx, topicArr, &handler{
handler: handlerMap,
})
if errors.Is(err, context.Canceled) || errors.Is(c.ctx.Err(), context.Canceled) {
return
}
time.Sleep(time.Second)
}
}
func (c *Consumer) Stop() error {
c.cancel()
return client.consumer.Close()
}
type Handler func(context.Context, []byte) error
type handler struct {
handler map[string]*sync.Pool
}
func (h *handler) Setup(_ sarama.ConsumerGroupSession) error {
return nil
}
func (h *handler) Cleanup(sess sarama.ConsumerGroupSession) error {
sess.Commit()
return nil
}
func (h *handler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for {
select {
case message, ok := <-claim.Messages():
if ok {
ctx := NewCarrier().ExtractConsumer(message.Headers)
_, span := otel.Tracer("common.db.kafka").Start(ctx, "kafka.consume")
pool := h.handler[message.Topic]
if pool == nil {
span.SetStatus(otelcodes.Error, "handler not found")
span.End()
return utils.ErrorsWrap(errors.New("handler not found"))
}
span.SetAttributes(attribute.String("kafka.topic", message.Topic))
span.SetAttributes(attribute.String("kafka.message", string(message.Value)))
newCb := pool.Get().(Topic)
if err := json.Unmarshal(message.Value, newCb); err != nil {
releaseTopicObject(pool, newCb)
span.SetStatus(otelcodes.Error, "handler json.Unmarshal error")
span.End()
return utils.ErrorsWrap(err, "handler json.Unmarshal error")
}
if err := func() (err error) {
defer func() {
if e := recover(); e != nil {
span.SetStatus(otelcodes.Error, fmt.Sprintf("%v", e))
span.End()
err = fmt.Errorf("kafka handler panic: %v", e)
}
}()
if err = newCb.OnMessage(ctx); err != nil {
if stack, ok := err.(interface{ StackTrace() string }); ok {
span.AddEvent("Stack Trace", trace.WithAttributes(
attribute.String("stack.trace", fmt.Sprintf("%v", stack.StackTrace())),
))
}
span.SetStatus(otelcodes.Error, err.Error())
span.End()
log.Errorf("kafka handler error: %v", err)
err = utils.ErrorsWrap(err, "kafka handler error")
}
return
}(); err != nil {
releaseTopicObject(pool, newCb)
return err
}
sess.MarkMessage(message, "")
releaseTopicObject(pool, newCb)
span.End()
}
case <-sess.Context().Done():
log.Warnf("kafka session done, reason: %v", sess.Context().Err())
sess.Commit()
return sess.Context().Err()
}
}
}
// 回收Topic对象
func releaseTopicObject(pool *sync.Pool, obj Topic) {
v := reflect.ValueOf(obj).Elem()
v.Set(reflect.Zero(v.Type()))
pool.Put(obj)
}
func consumerError() {
for err := range client.consumer.Errors() {
log.Errorf("kafka consumer error: %v", err)
}
}

48
db/kafka/producer.go Normal file
View File

@@ -0,0 +1,48 @@
package kafka
import (
"context"
"encoding/json"
"errors"
"github.com/IBM/sarama"
"go.opentelemetry.io/otel"
otelcodes "go.opentelemetry.io/otel/codes"
"strconv"
)
var ProduceTimeoutError = errors.New("kafka producer timeout")
func NewProducer(key int64) *Producer {
return &Producer{
key: key,
}
}
type Producer struct {
key int64
}
func (c *Producer) Produce(ctx context.Context, data Topic) error {
marshal, _ := json.Marshal(data)
msg := &sarama.ProducerMessage{
Topic: data.Name(),
Key: sarama.StringEncoder(strconv.FormatInt(c.key, 10)),
Value: sarama.ByteEncoder(marshal),
Headers: NewCarrier().Inject(ctx),
}
select {
case client.producer.Input() <- msg:
return nil
case <-ctx.Done():
return ProduceTimeoutError
}
}
func producerError() {
for err := range client.producer.Errors() {
ctx := NewCarrier().ExtractProducer(err.Msg.Headers)
_, span := otel.Tracer("common.db.kafka").Start(ctx, "kafka.producer.error")
span.SetStatus(otelcodes.Error, err.Error())
span.End()
}
}

8
db/kafka/topic.go Normal file
View File

@@ -0,0 +1,8 @@
package kafka
import "context"
type Topic interface {
Name() string
OnMessage(context.Context) error
}

94
db/mongo/client.go Normal file
View File

@@ -0,0 +1,94 @@
package mongo
import (
"context"
"git.hlsq.asia/alex/service-common/config"
"git.hlsq.asia/alex/service-common/log"
"go.mongodb.org/mongo-driver/event"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
"time"
)
// Colors
const (
Reset = "\033[0m"
BrightGreen = "\033[92m"
)
type DBName string
var clients = make(map[DBName]*mongo.Client)
func Init(cfg map[string]*config.MongoConfig) error {
for name, oneConfig := range cfg {
if client, err := initOneClient(oneConfig); err != nil {
return err
} else {
clients[DBName(name)] = client
}
}
return nil
}
func initOneClient(cfg *config.MongoConfig) (*mongo.Client, error) {
opts := options.Client().
ApplyURI(cfg.URI).
SetMonitor(&event.CommandMonitor{
Started: func(ctx context.Context, startedEvent *event.CommandStartedEvent) {
log.Infof(BrightGreen+"[MongoDB] Started, Command: %v"+Reset, startedEvent.Command.String())
},
Failed: func(ctx context.Context, failedEvent *event.CommandFailedEvent) {
log.Errorf(BrightGreen+"[MongoDB] [%v] Failed, Failure: %v"+Reset, failedEvent.Duration, failedEvent.Failure)
},
})
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, opts)
if err != nil {
return nil, err
}
if err = client.Ping(ctx, readpref.Primary()); err != nil {
_ = client.Disconnect(context.Background())
return nil, err
}
return client, nil
}
// GetClient 返回 mongo.Client,你可以通过 .Database("xxx") 获取具体数据库
func GetClient(dbName DBName) *mongo.Client {
if c, ok := clients[dbName]; ok {
return c
}
log.Errorf("mongo client %s not found", dbName)
return nil
}
// GetDB 是便捷方法,直接返回 *mongo.Database(假设 dbName 对应数据库名)
// 如果你的配置中 dbName 和实际数据库名一致,可这样用
func GetDB(dbName DBName) *mongo.Database {
client := GetClient(dbName)
if client == nil {
return nil
}
// 假设配置中的 key 就是数据库名;若需分离,可在 config 中加字段
return client.Database(string(dbName))
}
func Close() error {
if clients == nil {
return nil
}
for name, client := range clients {
if client != nil {
if err := client.Disconnect(context.Background()); err != nil {
log.Errorf("close mongo client %s error: %v", name, err)
}
}
}
return nil
}

85
db/mysql/client.go Normal file
View File

@@ -0,0 +1,85 @@
package mysql
import (
"git.hlsq.asia/alex/service-common/config"
"git.hlsq.asia/alex/service-common/log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"time"
)
type DBName string
var dbs = make(map[DBName]*gorm.DB)
func Init(cfg map[string]*config.MySQLConfig) error {
for name, oneConfig := range cfg {
if db, err := initOneDB(oneConfig); err != nil {
return err
} else {
dbs[DBName(name)] = db
}
}
return nil
}
func initOneDB(cfg *config.MySQLConfig) (*gorm.DB, error) {
db, err := gorm.Open(
mysql.Open(cfg.Dsn),
&gorm.Config{
TranslateError: true,
Logger: NewGormLogger(parseLogLevel(cfg.LogLevel)),
},
)
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(int(cfg.MaxOpenConn))
sqlDB.SetMaxIdleConns(int(cfg.MaxIdleConn))
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetimeSec) * time.Second)
sqlDB.SetConnMaxIdleTime(time.Duration(cfg.ConnMaxIdleTimeSec) * time.Second)
return db, nil
}
func parseLogLevel(logLevel string) logger.LogLevel {
switch logLevel {
case "silent":
return logger.Silent
case "error":
return logger.Error
case "warn":
return logger.Warn
case "info":
return logger.Info
}
return logger.Info
}
func GetDB(dbName DBName) *gorm.DB {
if q, ok := dbs[dbName]; ok {
return q
}
log.Errorf("db %s not found", dbName)
return nil
}
func Close() error {
if dbs == nil {
return nil
}
for name, db := range dbs {
if sqlDB, _ := db.DB(); sqlDB != nil {
if err := sqlDB.Close(); err != nil {
log.Errorf("close db %s error: %v", name, err)
}
}
}
return nil
}

84
db/mysql/log.go Normal file
View File

@@ -0,0 +1,84 @@
package mysql
import (
"context"
"errors"
"fmt"
"git.hlsq.asia/alex/service-common/log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.uber.org/zap"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"time"
)
// Colors
const (
Reset = "\033[0m"
Blue = "\033[34m"
)
type GormLogger struct {
logLevel logger.LogLevel
logger *zap.SugaredLogger
}
func NewGormLogger(level logger.LogLevel) *GormLogger {
return &GormLogger{
logLevel: level,
logger: log.GetLogger().
WithOptions(zap.AddCallerSkip(5)).
Named("GORM"),
}
}
func (l *GormLogger) LogMode(level logger.LogLevel) logger.Interface {
newLogger := *l
newLogger.logLevel = level
return &newLogger
}
func (l *GormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Info {
l.logger.Infof(Blue+msg+Reset, data...)
}
}
func (l *GormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Warn {
l.logger.Warnf(Blue+msg+Reset, data...)
}
}
func (l *GormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= logger.Error {
l.logger.Errorf(Blue+msg+Reset, data...)
}
}
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
if l.logLevel <= logger.Silent {
return
}
elapsed := time.Since(begin)
sql, rows := fc()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
l.logger.Errorf(Blue+"[SLOW SQL] [%v] [rows:%v] [error: %v] %v"+Reset, elapsed, rows, err.Error(), sql)
l.SlowLog(ctx, elapsed, sql)
} else if elapsed > 50*time.Millisecond {
l.logger.Warnf(Blue+"[SLOW SQL] [%v] [rows:%v] %v"+Reset, elapsed, rows, sql)
l.SlowLog(ctx, elapsed, sql)
} else {
l.logger.Infof(Blue+"[%v] [rows:%v] %v"+Reset, elapsed, rows, sql)
}
}
func (l *GormLogger) SlowLog(ctx context.Context, elapsed time.Duration, sql string) {
_, span := otel.Tracer("common.db.mysql").Start(ctx, "mysql.slow")
defer span.End()
span.SetAttributes(attribute.String("elapsed", fmt.Sprintf("%v", elapsed)))
span.SetAttributes(attribute.String("sql", sql))
}

58
db/redis/cache_client.go Normal file
View File

@@ -0,0 +1,58 @@
package redis
import (
"context"
"encoding/json"
"github.com/panjf2000/gnet/v2/pkg/logging"
"github.com/redis/go-redis/v9"
"time"
)
var cacheInstance *CacheClient
type CacheClient struct {
cli *redis.Client
logger logging.Logger
}
func GetCacheClient() *CacheClient {
return cacheInstance
}
func (c *CacheClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) {
bytes, err := json.Marshal(value)
if err != nil {
c.logger.Errorf("Set json.Marshal error: %v, key: %v, value: %v", err, key, value)
return
}
if err = c.cli.Set(ctx, key, bytes, expiration).Err(); err != nil {
c.logger.Errorf("Set redis.Set error: %v, key: %v, value: %v", err, key, value)
}
}
func (c *CacheClient) Del(ctx context.Context, keys ...string) {
if err := c.cli.Del(ctx, keys...).Err(); err != nil {
c.logger.Errorf("Set redis.Del error: %v, keys: %v", err, keys)
}
}
// Get 获取数据
func (c *CacheClient) Get(ctx context.Context, key string, dst interface{}) bool {
data, err := c.cli.Get(ctx, key).Bytes()
if err != nil {
return false
}
if err = json.Unmarshal(data, dst); err != nil {
c.logger.Errorf("Get json.Unmarshal error: %v, key: %v", err, key)
return false
}
return true
}
func (c *CacheClient) Exists(ctx context.Context, key ...string) bool {
n, err := c.cli.Exists(ctx, key...).Result()
if err != nil {
return false
}
return n > 0
}

45
db/redis/client.go Normal file
View File

@@ -0,0 +1,45 @@
package redis
import (
"context"
"git.hlsq.asia/alex/service-common/config"
"git.hlsq.asia/alex/service-common/log"
"github.com/redis/go-redis/v9"
)
var instance *Client
type Client struct {
*redis.Client
}
func Init(cfg *config.RedisConfig) error {
client := redis.NewClient(&redis.Options{
Addr: cfg.Addr,
Password: cfg.Password,
DB: cfg.DB,
})
instance = &Client{
Client: client,
}
cacheInstance = &CacheClient{
cli: client,
}
if logger := log.GetLogger(); logger != nil {
cacheInstance.logger = logger.Named("CACHE")
}
_, err := client.Ping(context.Background()).Result()
return err
}
func Close() error {
if instance != nil && instance.Client != nil {
return instance.Client.Close()
}
return nil
}
func GetClient() *Client {
return instance
}

54
discover/common/define.go Normal file
View File

@@ -0,0 +1,54 @@
package common
type ListenerType int
const (
ListenerTypeNewServer = 1 // 服务启动
ListenerTypeCloseServer = 2 // 服务关闭
ListenerTypeNewInstance = 3 // 副本启动
ListenerTypeCloseInstance = 4 // 副本关闭
)
var (
KeyDiscover = "/discover"
KeyDiscoverService = KeyDiscover + "/service"
KeyDiscoverInstance = KeyDiscover + "/instance"
)
var (
KeyDiscoverServiceNameGateway = "01-gateway" // 网关服
KeyDiscoverServiceNameScene = "02-scene" // 场景服
KeyDiscoverServiceNameUser = "03-user" // 用户中心
KeyDiscoverServiceNameThirdParty = "04-third-party" // 三方服
KeyDiscoverServiceNamePost = "05-post" // 推送服
KeyDiscoverServiceNameNotice = "06-notice" // 公告服
KeyDiscoverServiceNameAdmin = "07-admin" // 后台服
KeyDiscoverServiceNameTimer = "08-timer" // 定时任务服(只起一个)
KeyDiscoverServiceNameQgdzs = "09-qgdzs" // 奇怪的知识
)
var (
KeyDiscoverGateway = KeyDiscoverService + "/" + KeyDiscoverServiceNameGateway // 网关服
KeyDiscoverScene = KeyDiscoverService + "/" + KeyDiscoverServiceNameScene // 场景服
KeyDiscoverUser = KeyDiscoverService + "/" + KeyDiscoverServiceNameUser // 用户中心
KeyDiscoverThirdParty = KeyDiscoverService + "/" + KeyDiscoverServiceNameThirdParty // 三方服
KeyDiscoverPost = KeyDiscoverService + "/" + KeyDiscoverServiceNamePost // 推送服
KeyDiscoverNotice = KeyDiscoverService + "/" + KeyDiscoverServiceNameNotice // 公告服
KeyDiscoverAdmin = KeyDiscoverService + "/" + KeyDiscoverServiceNameAdmin // 后台服
KeyDiscoverTimer = KeyDiscoverService + "/" + KeyDiscoverServiceNameTimer // 定时任务服(只起一个)
KeyDiscoverQgdzs = KeyDiscoverService + "/" + KeyDiscoverServiceNameQgdzs // 奇怪的知识
)
// ServiceProvider 服务提供者
type ServiceProvider struct {
Target string
SID int64
Addr string
}
// InstanceProvider 副本提供者
type InstanceProvider struct {
InstanceID int // 副本ID
UniqueNo int64 // 副本唯一编号
SID int64
}

25
discover/common/tool.go Normal file
View File

@@ -0,0 +1,25 @@
package common
import (
"git.hlsq.asia/alex/service-common/db/etcd"
"git.hlsq.asia/alex/service-common/log"
clientv3 "go.etcd.io/etcd/client/v3"
)
// NewLeaseAndKeepAlive 创建租约并保活
func NewLeaseAndKeepAlive(ttl int64) (clientv3.LeaseID, error) {
lease, err := etcd.GetClient().Grant(ttl)
if err != nil {
return 0, err
}
chKeepAlive, err := etcd.GetClient().KeepAlive(lease.ID)
if err != nil {
return 0, err
}
go func(leaseID clientv3.LeaseID) {
for range chKeepAlive {
}
log.Warnf("Lease %x expired or revoked", leaseID)
}(lease.ID)
return lease.ID, nil
}

81
discover/instance.go Normal file
View File

@@ -0,0 +1,81 @@
package discover
import (
"fmt"
"git.hlsq.asia/alex/service-common/db/etcd"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/log"
"git.hlsq.asia/alex/service-common/utils"
clientv3 "go.etcd.io/etcd/client/v3"
"sync"
)
// 大量读少量写的情况下,读写锁比同步Map更高效
var (
instanceMU = sync.RWMutex{}
instanceM = make(map[int64]int64) // [uniqueNo]sid
instanceLeaseM = make(map[int64]clientv3.LeaseID) // [uniqueNo]
)
func init() {
RegisterListener(common.ListenerTypeNewInstance, onInstanceStart)
RegisterListener(common.ListenerTypeCloseInstance, onInstanceStop)
}
// FindInstanceByUniqueNo 根据唯一标识查询副本
func FindInstanceByUniqueNo(uniqueNO int64) (sid int64) {
instanceMU.RLock()
defer instanceMU.RUnlock()
if c, ok := instanceM[uniqueNO]; ok {
return c
}
return
}
// RegisterInstance 注册副本
func RegisterInstance(sid int64, instanceID int32, uniqueNo int64, ttl int64) error {
serverMU.Lock()
defer serverMU.Unlock()
leaseID, err := common.NewLeaseAndKeepAlive(ttl)
if err != nil {
return err
}
key := fmt.Sprintf("%v/%v/%v", common.KeyDiscoverInstance, instanceID, uniqueNo)
_, err = etcd.GetClient().Put(key, utils.Int64ToString(sid), clientv3.WithLease(leaseID))
if err != nil {
return err
}
instanceLeaseM[uniqueNo] = leaseID
return nil
}
// UnRegisterInstance 解注册副本
func UnRegisterInstance(uniqueNo int64) {
serverMU.Lock()
defer serverMU.Unlock()
if leaseID, ok := instanceLeaseM[uniqueNo]; ok {
_, err := etcd.GetClient().Revoke(leaseID)
if err != nil {
log.Errorf("UnRegisterInstance err: %v", err)
}
delete(instanceLeaseM, uniqueNo)
}
}
// 某个副本启动了
func onInstanceStart(data any) {
if provider, ok := data.(*common.InstanceProvider); ok {
instanceMU.Lock()
defer instanceMU.Unlock()
instanceM[provider.UniqueNo] = provider.SID
}
}
// 某个副本关闭了
func onInstanceStop(data any) {
if provider, ok := data.(*common.InstanceProvider); ok {
instanceMU.Lock()
defer instanceMU.Unlock()
delete(instanceM, provider.UniqueNo)
}
}

132
discover/listener.go Normal file
View File

@@ -0,0 +1,132 @@
package discover
import (
"context"
"fmt"
"git.hlsq.asia/alex/service-common/db/etcd"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/log"
"git.hlsq.asia/alex/service-common/utils"
"go.etcd.io/etcd/api/v3/mvccpb"
clientv3 "go.etcd.io/etcd/client/v3"
"strconv"
"strings"
"sync"
)
var (
wg = &sync.WaitGroup{}
listenerMU = &sync.RWMutex{}
listener = make(map[common.ListenerType][]func(data any))
stopFunc context.CancelFunc
)
// RegisterListener 注册服务变动监听
func RegisterListener(t common.ListenerType, cb func(data any)) {
listenerMU.Lock()
defer listenerMU.Unlock()
arr := listener[t]
if arr == nil {
arr = make([]func(data any), 0)
}
arr = append(arr, cb)
listener[t] = arr
}
// 根据类型触发回调
func onCBByType(t common.ListenerType, data any) {
listenerMU.RLock()
defer listenerMU.RUnlock()
for _, f := range listener[t] {
f(data)
}
}
func Listen(ready *sync.WaitGroup) {
var stopCtx context.Context
stopCtx, stopFunc = context.WithCancel(context.Background())
wg.Add(1)
go func() {
defer wg.Done()
log.Infof(fmt.Sprintf("Discover start listen..."))
// 服务
serviceAll, _ := etcd.GetClient().Get(common.KeyDiscoverService, clientv3.WithPrefix())
for _, kv := range serviceAll.Kvs {
onServerChange(clientv3.EventTypePut, string(kv.Key), string(kv.Value))
}
chService := etcd.GetClient().Watch(common.KeyDiscoverService, clientv3.WithPrefix(), clientv3.WithRev(serviceAll.Header.Revision+1))
// 副本
instanceAll, _ := etcd.GetClient().Get(common.KeyDiscoverInstance, clientv3.WithPrefix())
for _, kv := range instanceAll.Kvs {
onInstanceChange(clientv3.EventTypePut, string(kv.Key), string(kv.Value), nil)
}
chInstance := etcd.GetClient().Watch(common.KeyDiscoverScene, clientv3.WithPrefix(), clientv3.WithRev(instanceAll.Header.Revision+1), clientv3.WithPrevKV())
// 准备好了
ready.Done()
for {
select {
case msg := <-chService:
for _, event := range msg.Events {
onServerChange(event.Type, string(event.Kv.Key), string(event.Kv.Value))
}
case msg := <-chInstance:
for _, event := range msg.Events {
onInstanceChange(event.Type, string(event.Kv.Key), string(event.Kv.Value), event.PrevKv)
}
case <-stopCtx.Done():
return
}
}
}()
}
// 服务发生变化
func onServerChange(t mvccpb.Event_EventType, key, value string) {
split := strings.Split(key, "/")
if len(split) != 5 {
return
}
switch t {
case clientv3.EventTypePut:
onCBByType(common.ListenerTypeNewServer, &common.ServiceProvider{
Target: common.KeyDiscoverService + "/" + split[3],
SID: utils.StringToInt64(split[4]),
Addr: value,
})
case clientv3.EventTypeDelete:
onCBByType(common.ListenerTypeCloseServer, &common.ServiceProvider{
Target: common.KeyDiscoverService + "/" + split[3],
SID: utils.StringToInt64(split[4]),
})
}
}
// 副本发生变化
func onInstanceChange(t mvccpb.Event_EventType, key, value string, preKv *mvccpb.KeyValue) {
split := strings.Split(key, "/")
if len(split) != 5 {
return
}
instanceID, _ := strconv.Atoi(split[3])
switch t {
case clientv3.EventTypePut:
onCBByType(common.ListenerTypeNewInstance, &common.InstanceProvider{
InstanceID: instanceID,
UniqueNo: utils.StringToInt64(split[4]),
SID: utils.StringToInt64(value),
})
case clientv3.EventTypeDelete:
onCBByType(common.ListenerTypeCloseInstance, &common.InstanceProvider{
InstanceID: instanceID,
UniqueNo: utils.StringToInt64(split[4]),
SID: utils.StringToInt64(string(preKv.Value)),
})
}
}
func Close() {
if stopFunc != nil {
stopFunc()
wg.Wait()
}
}

109
discover/server.go Normal file
View File

@@ -0,0 +1,109 @@
package discover
import (
"fmt"
"git.hlsq.asia/alex/service-common/db/etcd"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/log"
"git.hlsq.asia/alex/service-common/net/grpc/grpcconn"
clientv3 "go.etcd.io/etcd/client/v3"
"google.golang.org/grpc"
"sync"
)
// 大量读少量写的情况下,读写锁比同步Map更高效
var (
serverMU = sync.RWMutex{}
conn = make(map[string]*grpcconn.GrpcConnectionMgr)
serverLeaseM = make(map[int64]clientv3.LeaseID)
)
func init() {
RegisterListener(common.ListenerTypeNewServer, onServerStart)
RegisterListener(common.ListenerTypeCloseServer, onServerStop)
}
// FindServer 根据SID或随机查找服务
func FindServer(target string, sid ...int64) (*grpc.ClientConn, error) {
serverMU.RLock()
defer serverMU.RUnlock()
if v, ok := conn[target]; ok {
return v.Load(sid...)
}
return nil, fmt.Errorf("cannot find server")
}
func FindServerAll(target string) map[int64]*grpc.ClientConn {
serverMU.RLock()
defer serverMU.RUnlock()
if v, ok := conn[target]; ok {
return v.LoadAll()
}
return make(map[int64]*grpc.ClientConn)
}
func FindServerAllSid(target string) []int64 {
serverMU.RLock()
defer serverMU.RUnlock()
if v, ok := conn[target]; ok {
return v.LoadAllSid()
}
return make([]int64, 0)
}
// RegisterGrpcServer 注册服务提供者
func RegisterGrpcServer(target string, sid int64, addr string, ttl int64) error {
serverMU.Lock()
defer serverMU.Unlock()
leaseID, err := common.NewLeaseAndKeepAlive(ttl)
if err != nil {
return err
}
_, err = etcd.GetClient().Put(fmt.Sprintf("%v/%v", target, sid), addr, clientv3.WithLease(leaseID))
if err != nil {
return err
}
serverLeaseM[sid] = leaseID
return nil
}
// UnRegisterGrpcServer 解注册服务提供者
func UnRegisterGrpcServer(sid int64) {
serverMU.Lock()
defer serverMU.Unlock()
if leaseID, ok := serverLeaseM[sid]; ok {
_, err := etcd.GetClient().Revoke(leaseID)
if err != nil {
log.Errorf("server.go UnRegisterGrpcServer err: %v", err)
}
delete(serverLeaseM, sid)
}
}
// 某个服务启动了
func onServerStart(data any) {
if provider, ok := data.(*common.ServiceProvider); ok {
serverMU.Lock()
defer serverMU.Unlock()
if v, ok := conn[provider.Target]; ok {
v.Store(provider.SID, provider.Addr)
} else {
mgr := grpcconn.NewGrpcConnectionMgr()
mgr.Store(provider.SID, provider.Addr)
conn[provider.Target] = mgr
}
}
}
// 某个服务关闭了
func onServerStop(data any) {
if provider, ok := data.(*common.ServiceProvider); ok {
serverMU.Lock()
defer serverMU.Unlock()
if v, ok := conn[provider.Target]; ok {
if v.Delete(provider.SID) == 0 {
delete(conn, provider.Target)
}
}
}
}

131
go.mod Normal file
View File

@@ -0,0 +1,131 @@
module git.hlsq.asia/alex/service-common
go 1.24.0
require (
git.hlsq.asia/alex/proto v0.0.0-20261029023453-16bb20cbcd2e
github.com/IBM/sarama v1.46.3
github.com/gin-gonic/gin v1.11.0
github.com/gobwas/ws v1.4.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/panjf2000/gnet/v2 v2.9.7
github.com/prometheus/client_golang v1.20.5
github.com/redis/go-redis/v9 v9.10.0
github.com/shopspring/decimal v1.4.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/sony/sonyflake/v2 v2.2.0
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
go.etcd.io/etcd/api/v3 v3.6.1
go.etcd.io/etcd/client/v3 v3.6.1
go.mongodb.org/mongo-driver v1.17.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0
go.opentelemetry.io/otel/sdk v1.39.0
go.opentelemetry.io/otel/trace v1.39.0
go.uber.org/zap v1.27.0
google.golang.org/grpc v1.77.0
google.golang.org/protobuf v1.36.11
gorm.io/driver/mysql v1.6.0
gorm.io/gen v0.3.27
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/eapache/go-resiliency v1.7.0 // indirect
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect
github.com/eapache/queue v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // 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.27.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // 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/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/panjf2000/ants/v2 v2.11.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

347
go.sum Normal file
View File

@@ -0,0 +1,347 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.hlsq.asia/alex/proto v0.0.0-20261029023453-16bb20cbcd2e h1:MCBfc5To7T6EuAWGJpr0U40BzJcnw9udJHOUWVr/qAE=
git.hlsq.asia/alex/proto v0.0.0-20261029023453-16bb20cbcd2e/go.mod h1:f7oMNC/o7AzT+SLUw5DKHcURu55e/sAHLjtO7in1OCA=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/IBM/sarama v1.46.3 h1:njRsX6jNlnR+ClJ8XmkO+CM4unbrNr/2vB5KK6UA+IE=
github.com/IBM/sarama v1.46.3/go.mod h1:GTUYiF9DMOZVe3FwyGT+dtSPceGFIgA+sPc5u6CBwko=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=
github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
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.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
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/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
github.com/panjf2000/gnet/v2 v2.9.7 h1:6zW7Jl3oAfXwSuh1PxHLndoL2MQRWx0AJR6aaQjxUgA=
github.com/panjf2000/gnet/v2 v2.9.7/go.mod h1:WQTxDWYuQ/hz3eccH0FN32IVuvZ19HewEWx0l62fx7E=
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/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/sony/sonyflake/v2 v2.2.0 h1:wSzEoewlWnUtc3SZX/MpT8zsWTuAnjwrprUYfuPl9Jg=
github.com/sony/sonyflake/v2 v2.2.0/go.mod h1:09EcfmR846JLupbkgVfzp8QtQwJ+Y8e69VVayHdawzg=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/etcd/api/v3 v3.6.1 h1:yJ9WlDih9HT457QPuHt/TH/XtsdN2tubyxyQHSHPsEo=
go.etcd.io/etcd/api/v3 v3.6.1/go.mod h1:lnfuqoGsXMlZdTJlact3IB56o3bWp1DIlXPIGKRArto=
go.etcd.io/etcd/client/pkg/v3 v3.6.1 h1:CxDVv8ggphmamrXM4Of8aCC8QHzDM4tGcVr9p2BSoGk=
go.etcd.io/etcd/client/pkg/v3 v3.6.1/go.mod h1:aTkCp+6ixcVTZmrJGa7/Mc5nMNs59PEgBbq+HCmWyMc=
go.etcd.io/etcd/client/v3 v3.6.1 h1:KelkcizJGsskUXlsxjVrSmINvMMga0VWwFF0tSPGEP0=
go.etcd.io/etcd/client/v3 v3.6.1/go.mod h1:fCbPUdjWNLfx1A6ATo9syUmFVxqHH9bCnPLBZmnLmMY=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
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.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/gen v0.3.27 h1:ziocAFLpE7e0g4Rum69pGfB9S6DweTxK8gAun7cU8as=
gorm.io/gen v0.3.27/go.mod h1:9zquz2xD1f3Eb/eHq4oLn2z6vDVvQlCY5S3uMBLv4EA=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

23
log/level.go Normal file
View File

@@ -0,0 +1,23 @@
package log
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var logLevelMap = map[string]zapcore.Level{
"panic": zap.DPanicLevel,
"fatal": zap.FatalLevel,
"error": zap.ErrorLevel,
"warn": zap.WarnLevel,
"info": zap.InfoLevel,
"debug": zap.DebugLevel,
}
// GetLogLevel get the logLevel from logLevelName
func GetLogLevel(logLevelName string) zapcore.Level {
if v, ok := logLevelMap[logLevelName]; ok {
return v
}
return zap.DebugLevel
}

66
log/log.go Normal file
View File

@@ -0,0 +1,66 @@
package log
import "go.uber.org/zap"
var globalLogger *zap.SugaredLogger
// SetLogger 设置日志记录器
func SetLogger(logger *zap.SugaredLogger) {
if logger == nil {
return
}
globalLogger = logger
}
func GetLogger() *zap.SugaredLogger {
return globalLogger
}
// Debugf 打印调试模板日志
func Debugf(format string, a ...interface{}) {
if globalLogger != nil {
globalLogger.Debugf(format, a...)
}
}
// Infof 打印信息模板日志
func Infof(format string, a ...interface{}) {
if globalLogger != nil {
globalLogger.Infof(format, a...)
}
}
// Warnf 打印警告模板日志
func Warnf(format string, a ...interface{}) {
if globalLogger != nil {
globalLogger.Warnf(format, a...)
}
}
// Errorf 打印错误模板日志
func Errorf(format string, a ...interface{}) {
if globalLogger != nil {
globalLogger.Errorf(format, a...)
}
}
// Panicf 打印Panic模板日志
func Panicf(format string, a ...interface{}) {
if globalLogger != nil {
globalLogger.Panicf(format, a...)
}
}
// Fatalf 打印致命错误模板日志
func Fatalf(format string, a ...interface{}) {
if globalLogger != nil {
globalLogger.Fatalf(format, a...)
}
}
// Close 关闭日志
func Close() {
if globalLogger != nil {
_ = globalLogger.Sync()
}
}

64
log/log_zap.go Normal file
View File

@@ -0,0 +1,64 @@
package log
import (
"git.hlsq.asia/alex/service-common/config"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"os"
)
func Init(cfg *config.LogConfig) {
// 格式配置
jsonConfig := zapcore.EncoderConfig{
MessageKey: "M",
LevelKey: "L",
TimeKey: "T",
NameKey: "N",
CallerKey: "C",
FunctionKey: zapcore.OmitKey,
StacktraceKey: "S",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.TimeEncoderOfLayout("01-02 15:04:05.000"),
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeName: func(loggerName string, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString("[" + loggerName + "]")
},
ConsoleSeparator: " ",
}
// 日志输出到控制台和文件
writeSyncer := []zapcore.WriteSyncer{zapcore.AddSync(os.Stdout)}
if !cfg.Debug {
writeSyncer = append(writeSyncer, zapcore.AddSync(&lumberjack.Logger{
Filename: "./logs/log.log", // 日志文件位置
MaxSize: int(cfg.MaxSize), // 最大文件大小(MB)
MaxBackups: int(cfg.MaxBackups), // 保留旧文件的最大个数
MaxAge: int(cfg.MaxAge), // 保留旧文件的最大天数
Compress: false, // 是否压缩/归档旧文件
LocalTime: true,
}))
}
var encoder zapcore.Encoder
if cfg.Debug {
encoder = zapcore.NewConsoleEncoder(jsonConfig)
} else {
encoder = zapcore.NewJSONEncoder(jsonConfig)
}
logger := zap.New(zapcore.NewCore(
encoder,
zapcore.NewMultiWriteSyncer(writeSyncer...),
zap.NewAtomicLevelAt(GetLogLevel(cfg.Level)),
))
if cfg.Debug {
logger = logger.WithOptions(
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
)
}
SetLogger(logger.Sugar())
}

71
module/db.go Normal file
View File

@@ -0,0 +1,71 @@
package module
import (
"git.hlsq.asia/alex/service-common/config"
"git.hlsq.asia/alex/service-common/db/etcd"
"git.hlsq.asia/alex/service-common/db/kafka"
"git.hlsq.asia/alex/service-common/db/mongo"
"git.hlsq.asia/alex/service-common/db/mysql"
"git.hlsq.asia/alex/service-common/db/redis"
"git.hlsq.asia/alex/service-common/log"
)
// DB 数据库模块
type DB struct {
DefaultModule
Cfg *config.DBConfig
AppName string
}
func (m *DB) Init() error {
// ETCD
if m.Cfg.Etcd != nil {
if err := etcd.Init(m.Cfg.Etcd); err != nil {
return err
}
}
// MYSQL
if m.Cfg.MySQL != nil {
if err := mysql.Init(m.Cfg.MySQL); err != nil {
return err
}
}
// REDIS
if m.Cfg.Redis != nil {
if err := redis.Init(m.Cfg.Redis); err != nil {
return err
}
}
// KAFKA
if m.Cfg.Kafka != nil {
if err := kafka.Init(m.Cfg.Kafka, m.AppName); err != nil {
return err
}
}
// MONGO
if m.Cfg.Mongo != nil {
if err := mongo.Init(m.Cfg.Mongo); err != nil {
return err
}
}
return nil
}
func (m *DB) Stop() error {
if err := etcd.Close(); err != nil {
log.Errorf("close etcd failed: %v", err)
}
if err := mysql.Close(); err != nil {
log.Errorf("close mysql failed: %v", err)
}
if err := redis.Close(); err != nil {
log.Errorf("close redis failed: %v", err)
}
if err := kafka.Close(); err != nil {
log.Errorf("close kafka failed: %v", err)
}
if err := mongo.Close(); err != nil {
log.Errorf("close mongo failed: %v", err)
}
return nil
}

21
module/discover.go Normal file
View File

@@ -0,0 +1,21 @@
package module
import (
"git.hlsq.asia/alex/service-common/discover"
"sync"
)
// Discover 服务发现模块
type Discover struct {
DefaultModule
}
func (m *Discover) Start(ready *sync.WaitGroup) error {
discover.Listen(ready)
return nil
}
func (m *Discover) Stop() error {
discover.Close()
return nil
}

27
module/grpc.go Normal file
View File

@@ -0,0 +1,27 @@
package module
import (
"git.hlsq.asia/alex/service-common/net/grpc/service"
"sync"
)
// Grpc Grpc模块
type Grpc struct {
DefaultModule
Server service.IService
}
func (m *Grpc) Start(ready *sync.WaitGroup) error {
m.Server.Init(ready)
return nil
}
func (m *Grpc) AfterStart() error {
m.Server.SetReady()
return nil
}
func (m *Grpc) Stop() error {
m.Server.Close()
return nil
}

17
module/log.go Normal file
View File

@@ -0,0 +1,17 @@
package module
import (
"git.hlsq.asia/alex/service-common/config"
"git.hlsq.asia/alex/service-common/log"
)
// Log 日志模块
type Log struct {
DefaultModule
Cfg *config.LogConfig
}
func (m *Log) Init() error {
log.Init(m.Cfg)
return nil
}

32
module/module.go Normal file
View File

@@ -0,0 +1,32 @@
package module
import "sync"
// 重点!!!每个模块需要保证同步执行
type Module interface {
Init() error // 初始化
Start(ready *sync.WaitGroup) error // 启动
AfterStart() error // 启动之后
Stop() error // 停止
}
type DefaultModule struct {
}
func (m *DefaultModule) Init() error {
return nil
}
func (m *DefaultModule) Start(ready *sync.WaitGroup) error {
ready.Done()
return nil
}
func (m *DefaultModule) AfterStart() error {
return nil
}
func (m *DefaultModule) Stop() error {
return nil
}

50
module/prometheus.go Normal file
View File

@@ -0,0 +1,50 @@
package module
import (
"context"
"errors"
"fmt"
"git.hlsq.asia/alex/service-common/config"
"git.hlsq.asia/alex/service-common/log"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
"sync"
)
// Prometheus 普罗米修斯模块
type Prometheus struct {
DefaultModule
Cfg *config.MetricConfig
wg *sync.WaitGroup
server *http.Server
}
func (m *Prometheus) Init() error {
m.wg = &sync.WaitGroup{}
return nil
}
func (m *Prometheus) Start(ready *sync.WaitGroup) error {
m.wg.Add(1)
go func() {
defer m.wg.Done()
m.server = &http.Server{
Addr: fmt.Sprintf("%v:%v", m.Cfg.Prometheus.Address, m.Cfg.Prometheus.Port),
Handler: promhttp.Handler(),
}
ready.Done()
if err := m.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Errorf("prometheus server failed: %v", err.Error())
}
log.Infof("prometheus server stop.")
}()
return nil
}
func (m *Prometheus) Stop() error {
if err := m.server.Shutdown(context.Background()); err != nil {
log.Errorf("stop prometheus server failed: %v", err)
}
m.wg.Wait()
return nil
}

25
module/server_status.go Normal file
View File

@@ -0,0 +1,25 @@
package module
import (
"git.hlsq.asia/alex/service-common/server"
"sync"
)
type ServerStatus struct {
DefaultModule
}
func (s *ServerStatus) Init() error {
server.InitStatus()
return nil
}
func (s *ServerStatus) Start(ready *sync.WaitGroup) error {
server.ListenStatus(ready)
return nil
}
func (s *ServerStatus) Stop() error {
server.CloseStatus()
return nil
}

View File

@@ -0,0 +1,25 @@
package module
import (
"git.hlsq.asia/alex/service-common/server"
"sync"
)
type ServerWhitelist struct {
DefaultModule
}
func (s *ServerWhitelist) Init() error {
server.InitWhitelist()
return nil
}
func (s *ServerWhitelist) Start(ready *sync.WaitGroup) error {
server.ListenWhitelist(ready)
return nil
}
func (s *ServerWhitelist) Stop() error {
server.CloseWhitelist()
return nil
}

64
module/snowflake.go Normal file
View File

@@ -0,0 +1,64 @@
package module
import (
"context"
"errors"
"fmt"
"git.hlsq.asia/alex/service-common/db/etcd"
"git.hlsq.asia/alex/service-common/utils"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"math/rand"
)
// Snowflake 雪花模块
type Snowflake struct {
DefaultModule
snowflakeSession *concurrency.Session
}
func (m *Snowflake) Init() error {
node, session, err := acquire(context.Background(), 1, 1000)
if err != nil {
return err
}
m.snowflakeSession = session
utils.InitSnowflake(node)
return nil
}
func (m *Snowflake) Stop() error {
_ = m.snowflakeSession.Close()
return nil
}
func acquire(ctx context.Context, min, max int) (int64, *concurrency.Session, error) {
nums := rand.Perm(max - min + 1)
for i := range nums {
nums[i] += min
}
for _, n := range nums {
key := fmt.Sprintf("/node/num/%d", n)
session, err := concurrency.NewSession(
etcd.GetClient().Raw(),
concurrency.WithContext(ctx),
)
if err != nil {
return 0, nil, utils.ErrorsWrap(fmt.Errorf("etcd NewSession error: %v", err))
}
txnResp, _ := etcd.GetClient().Raw().Txn(ctx).
If(clientv3.Compare(clientv3.CreateRevision(key), "=", 0)).
Then(clientv3.OpPut(key, "", clientv3.WithLease(session.Lease()))).
Commit()
if txnResp.Succeeded {
return int64(n), session, nil
} else {
_ = session.Close()
}
}
return 0, nil, utils.ErrorsWrap(errors.New("etcd num empty"), "acquire error")
}

57
module/tracer.go Normal file
View File

@@ -0,0 +1,57 @@
package module
import (
"context"
"git.hlsq.asia/alex/service-common/config"
"git.hlsq.asia/alex/service-common/log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
// Tracer 链路追踪模块
type Tracer struct {
DefaultModule
Cfg *config.MetricConfig
ServiceName string
tp *sdktrace.TracerProvider
}
func (m *Tracer) Init() error {
exporter, err := otlptracegrpc.New(
context.Background(),
otlptracegrpc.WithInsecure(),
otlptracegrpc.WithEndpoint(m.Cfg.Jaeger.Endpoint),
)
if err != nil {
return err
}
samplingRate := 1.0
if 0 <= m.Cfg.Jaeger.SamplingRate && m.Cfg.Jaeger.SamplingRate <= 1 {
samplingRate = m.Cfg.Jaeger.SamplingRate
}
m.tp = sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(m.ServiceName),
)),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(samplingRate))),
)
otel.SetTracerProvider(m.tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
return nil
}
func (m *Tracer) Stop() error {
if m.tp != nil {
if err := m.tp.Shutdown(context.Background()); err != nil {
log.Errorf("stop tracer provider failed: %v", err)
}
}
return nil
}

View File

@@ -0,0 +1,23 @@
package grpcclient
import (
"git.hlsq.asia/alex/proto/proto/rs/rspb"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/net/grpc/resolver"
"google.golang.org/grpc"
)
func AdminNewClient(sid ...int64) (rspb.AdminClient, error) {
var c *grpc.ClientConn
var err error
if len(sid) == 0 {
c, err = resolver.GetGrpcClientConn("etcd:///" + common.KeyDiscoverServiceNameAdmin)
} else {
c, err = discover.FindServer(common.KeyDiscoverAdmin, sid...)
}
if err != nil {
return nil, err
}
return rspb.NewAdminClient(c), nil
}

View File

@@ -0,0 +1,32 @@
package grpcclient
import (
"git.hlsq.asia/alex/proto/proto/rs/rspb"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/net/grpc/resolver"
"google.golang.org/grpc"
)
func GatewayNewClient(sid ...int64) (rspb.GatewayClient, error) {
var c *grpc.ClientConn
var err error
if len(sid) == 0 {
c, err = resolver.GetGrpcClientConn("etcd:///" + common.KeyDiscoverServiceNameGateway)
} else {
c, err = discover.FindServer(common.KeyDiscoverGateway, sid...)
}
if err != nil {
return nil, err
}
return rspb.NewGatewayClient(c), nil
}
func GatewayNewBroadcastClient() map[int64]rspb.GatewayClient {
clientM := make(map[int64]rspb.GatewayClient)
connM := discover.FindServerAll(common.KeyDiscoverGateway)
for sid, conn := range connM {
clientM[sid] = rspb.NewGatewayClient(conn)
}
return clientM
}

View File

@@ -0,0 +1,23 @@
package grpcclient
import (
"git.hlsq.asia/alex/proto/proto/rs/rspb"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/net/grpc/resolver"
"google.golang.org/grpc"
)
func NoticeNewClient(sid ...int64) (rspb.NoticeClient, error) {
var c *grpc.ClientConn
var err error
if len(sid) == 0 {
c, err = resolver.GetGrpcClientConn("etcd:///" + common.KeyDiscoverServiceNameNotice)
} else {
c, err = discover.FindServer(common.KeyDiscoverNotice, sid...)
}
if err != nil {
return nil, err
}
return rspb.NewNoticeClient(c), nil
}

View File

@@ -0,0 +1,23 @@
package grpcclient
import (
"git.hlsq.asia/alex/proto/proto/rs/rspb"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/net/grpc/resolver"
"google.golang.org/grpc"
)
func PostNewClient(sid ...int64) (rspb.PostClient, error) {
var c *grpc.ClientConn
var err error
if len(sid) == 0 {
c, err = resolver.GetGrpcClientConn("etcd:///" + common.KeyDiscoverServiceNamePost)
} else {
c, err = discover.FindServer(common.KeyDiscoverPost, sid...)
}
if err != nil {
return nil, err
}
return rspb.NewPostClient(c), nil
}

View File

@@ -0,0 +1,23 @@
package grpcclient
import (
"git.hlsq.asia/alex/proto/proto/rs/rspb"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/net/grpc/resolver"
"google.golang.org/grpc"
)
func QgdzsNewClient(sid ...int64) (rspb.QgdzsClient, error) {
var c *grpc.ClientConn
var err error
if len(sid) == 0 {
c, err = resolver.GetGrpcClientConn("etcd:///" + common.KeyDiscoverServiceNameQgdzs)
} else {
c, err = discover.FindServer(common.KeyDiscoverQgdzs, sid...)
}
if err != nil {
return nil, err
}
return rspb.NewQgdzsClient(c), nil
}

View File

@@ -0,0 +1,43 @@
package grpcclient
import (
"fmt"
"git.hlsq.asia/alex/proto/proto/rs/rspb"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/net/grpc/resolver"
"google.golang.org/grpc"
)
// SceneNewClient 根据SID查找场景服,一般不用这个函数
func SceneNewClient(sid ...int64) (rspb.SceneClient, error) {
var c *grpc.ClientConn
var err error
if len(sid) == 0 {
c, err = resolver.GetGrpcClientConn("etcd:///" + common.KeyDiscoverServiceNameScene)
} else {
c, err = discover.FindServer(common.KeyDiscoverScene, sid...)
}
if err != nil {
return nil, err
}
return rspb.NewSceneClient(c), nil
}
// SceneInstanceNewClient 根据副本唯一编号查找场景服
func SceneInstanceNewClient(uniqueNO int64) (rspb.SceneClient, error) {
sid := discover.FindInstanceByUniqueNo(uniqueNO)
if sid == 0 {
return nil, fmt.Errorf("instance not found: %v", uniqueNO)
}
return SceneNewClient(sid)
}
func SceneNewBroadcastClient() map[int64]rspb.SceneClient {
clientM := make(map[int64]rspb.SceneClient)
connM := discover.FindServerAll(common.KeyDiscoverScene)
for sid, conn := range connM {
clientM[sid] = rspb.NewSceneClient(conn)
}
return clientM
}

View File

@@ -0,0 +1,23 @@
package grpcclient
import (
"git.hlsq.asia/alex/proto/proto/rs/rspb"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/net/grpc/resolver"
"google.golang.org/grpc"
)
func ThirdPartyNewClient(sid ...int64) (rspb.ThirdPartyClient, error) {
var c *grpc.ClientConn
var err error
if len(sid) == 0 {
c, err = resolver.GetGrpcClientConn("etcd:///" + common.KeyDiscoverServiceNameThirdParty)
} else {
c, err = discover.FindServer(common.KeyDiscoverThirdParty, sid...)
}
if err != nil {
return nil, err
}
return rspb.NewThirdPartyClient(c), nil
}

View File

@@ -0,0 +1,23 @@
package grpcclient
import (
"git.hlsq.asia/alex/proto/proto/rs/rspb"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/net/grpc/resolver"
"google.golang.org/grpc"
)
func TimerNewClient(sid ...int64) (rspb.TimerClient, error) {
var c *grpc.ClientConn
var err error
if len(sid) == 0 {
c, err = resolver.GetGrpcClientConn("etcd:///" + common.KeyDiscoverServiceNameTimer)
} else {
c, err = discover.FindServer(common.KeyDiscoverTimer, sid...)
}
if err != nil {
return nil, err
}
return rspb.NewTimerClient(c), nil
}

View File

@@ -0,0 +1,23 @@
package grpcclient
import (
"git.hlsq.asia/alex/proto/proto/rs/rspb"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/net/grpc/resolver"
"google.golang.org/grpc"
)
func UserNewClient(sid ...int64) (rspb.UserClient, error) {
var c *grpc.ClientConn
var err error
if len(sid) == 0 {
c, err = resolver.GetGrpcClientConn("etcd:///" + common.KeyDiscoverServiceNameUser)
} else {
c, err = discover.FindServer(common.KeyDiscoverUser, sid...)
}
if err != nil {
return nil, err
}
return rspb.NewUserClient(c), nil
}

View File

@@ -0,0 +1,126 @@
package grpcclient
import (
"context"
"fmt"
"git.hlsq.asia/alex/proto/proto/rs/rspb"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/log"
"git.hlsq.asia/alex/service-common/utils/locator"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
"sync"
)
type GatewayFun int
const (
FunToClient GatewayFun = iota
FunBroadcastClient
FunBroadcastChannel
)
var gatewayServer sync.Map // map[string]*gatewayStream
type gatewayStream struct {
mu sync.Mutex
stream grpc.ClientStream
}
func findGatewayBySID(sid int64, fun GatewayFun) (*gatewayStream, error) {
key := gatewayKey(sid, fun)
if v, ok := gatewayServer.Load(key); ok {
return v.(*gatewayStream), nil
}
client, err := GatewayNewClient(sid)
if err != nil {
log.Errorf("findGatewayBySID cannot find client: %v", err)
return nil, err
}
var stream grpc.ClientStream
switch fun {
case FunToClient:
stream, err = client.InnerToClient(context.Background())
case FunBroadcastClient:
stream, err = client.InnerBroadcastClient(context.Background())
case FunBroadcastChannel:
stream, err = client.InnerBroadcastChannel(context.Background())
}
if err != nil {
log.Errorf("findGatewayBySID %v err: %v, sid: %v", fun, err, sid)
return nil, err
}
ss := &gatewayStream{stream: stream}
if actual, loaded := gatewayServer.LoadOrStore(key, ss); loaded {
go func() { _ = stream.CloseSend() }()
return actual.(*gatewayStream), nil
}
return ss, nil
}
func sendMessageToGateway(sid int64, fun GatewayFun, msg proto.Message, re ...bool) error {
ss, err := findGatewayBySID(sid, fun)
if err != nil {
return err
}
ss.mu.Lock()
err = ss.stream.SendMsg(msg)
ss.mu.Unlock()
if err != nil {
key := gatewayKey(sid, fun)
if v, ok := gatewayServer.Load(key); ok && v == ss {
gatewayServer.Delete(key)
_ = ss.stream.CloseSend()
}
// 如果没有标识本次是重试的,就重试一次(默认重试)
if re == nil || !re[0] {
return sendMessageToGateway(sid, fun, msg, true)
}
return err
}
return nil
}
// SendMessageToClient 单播消息给客户端
func SendMessageToClient(ctx context.Context, usn int64, msg proto.Message, platform ...rspb.PlatformType) error {
if platform == nil || len(platform) == 0 {
return nil
}
for _, pf := range platform {
if gatewaySID := locator.NewLocator(ctx, usn, int32(pf)).LocateGateway(); gatewaySID > 0 {
return sendMessageToGateway(gatewaySID, FunToClient, msg)
}
}
return nil
}
// BroadcastMessageToClient 广播消息给客户端
func BroadcastMessageToClient(msg *rspb.InnerBroadcastClientReq) error {
return broadcastMessageToGateway(FunBroadcastClient, msg)
}
// BroadcastMessageToChannel 广播消息到频道(加入频道的能收到,不区分平台)
func BroadcastMessageToChannel(msg *rspb.InnerBroadcastChannelReq) error {
return broadcastMessageToGateway(FunBroadcastChannel, msg)
}
func broadcastMessageToGateway(fun GatewayFun, msg proto.Message) error {
sidArr := discover.FindServerAllSid(common.KeyDiscoverGateway)
for _, sid := range sidArr {
if err := sendMessageToGateway(sid, fun, msg); err != nil {
return err
}
}
return nil
}
func gatewayKey(sid int64, fun GatewayFun) string {
return fmt.Sprintf("%v-%v", sid, fun)
}

View File

@@ -0,0 +1,83 @@
package grpcclient
import (
"context"
"fmt"
"git.hlsq.asia/alex/service-common/log"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
"sync"
)
type SceneFun int
const (
FunAction SceneFun = iota
)
var sceneServer sync.Map // map[string]*sceneStream
type sceneStream struct {
mu sync.Mutex
stream grpc.ClientStream
}
func findSceneBySID(sid int64, fun SceneFun) (*sceneStream, error) {
key := sceneKey(sid, fun)
if v, ok := sceneServer.Load(key); ok {
return v.(*sceneStream), nil
}
client, err := SceneNewClient(sid)
if err != nil {
log.Errorf("findSceneBySID cannot find client: %v", err)
return nil, err
}
var stream grpc.ClientStream
switch fun {
case FunAction:
stream, err = client.Action(context.Background())
}
if err != nil {
log.Errorf("findSceneBySID %v err: %v, sid: %v", fun, err, sid)
return nil, err
}
ss := &sceneStream{stream: stream}
if actual, loaded := sceneServer.LoadOrStore(key, ss); loaded {
go func() { _ = stream.CloseSend() }()
return actual.(*sceneStream), nil
}
return ss, nil
}
func sendMessageToScene(sid int64, fun SceneFun, msg proto.Message, re ...bool) error {
ss, err := findSceneBySID(sid, fun)
if err != nil {
return err
}
ss.mu.Lock()
err = ss.stream.SendMsg(msg)
ss.mu.Unlock()
if err != nil {
key := sceneKey(sid, fun)
if v, ok := sceneServer.Load(key); ok && v == ss {
sceneServer.Delete(key)
_ = ss.stream.CloseSend()
}
// 如果没有标识本次是重试的,就重试一次(默认重试)
if re == nil || !re[0] {
return sendMessageToScene(sid, fun, msg, true)
}
return err
}
return nil
}
func sceneKey(sid int64, fun SceneFun) string {
return fmt.Sprintf("%v-%v", sid, fun)
}

57
net/grpc/grpcconn/conn.go Normal file
View File

@@ -0,0 +1,57 @@
package grpcconn
import (
"context"
"git.hlsq.asia/alex/service-common/log"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/metadata"
"strconv"
"time"
)
type GrpcConnection struct {
sid int64
conn *grpc.ClientConn
}
func NewGrpcConnection(sid int64, address string) (*GrpcConnection, error) {
p := &GrpcConnection{
sid: sid,
}
conn, err := grpc.NewClient(
address,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
grpc.WithUnaryInterceptor(sidInterceptor(sid)),
grpc.WithKeepaliveParams(
keepalive.ClientParameters{
Time: 30 * time.Second, // 保活探测包发送的时间间隔
Timeout: 10 * time.Second, // 保活探测包的超时时间
PermitWithoutStream: true,
},
),
)
if err != nil {
log.Errorf("create grpc err: %v, sid: %v, addr: %v", err, sid, address)
return nil, err
}
p.conn = conn
return p, nil
}
func (g *GrpcConnection) GetConnection() *grpc.ClientConn {
return g.conn
}
func sidInterceptor(sid int64) grpc.UnaryClientInterceptor {
sidStr := strconv.Itoa(int(sid))
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx = metadata.AppendToOutgoingContext(ctx,
"X-Sid", sidStr,
)
return invoker(ctx, method, req, reply, cc, opts...)
}
}

View File

@@ -0,0 +1,70 @@
package grpcconn
import (
"fmt"
"git.hlsq.asia/alex/service-common/log"
"google.golang.org/grpc"
"math/rand"
)
type GrpcConnectionMgr struct {
poolM map[int64]*GrpcConnection
poolS []*GrpcConnection
}
func NewGrpcConnectionMgr() *GrpcConnectionMgr {
return &GrpcConnectionMgr{
poolM: make(map[int64]*GrpcConnection),
poolS: make([]*GrpcConnection, 0),
}
}
func (p *GrpcConnectionMgr) Store(sid int64, addr string) {
pool, err := NewGrpcConnection(sid, addr)
if err != nil {
log.Errorf("create grpc err: %v, sid: %v, addr: %v", err, sid, addr)
return
}
p.poolM[sid] = pool
p.poolS = append(p.poolS, pool)
}
func (p *GrpcConnectionMgr) Delete(sid int64) int {
delete(p.poolM, sid)
for i, pool := range p.poolS {
if pool.sid == sid {
p.poolS = append(p.poolS[:i], p.poolS[i+1:]...)
break
}
}
return len(p.poolS)
}
func (p *GrpcConnectionMgr) Load(sid ...int64) (*grpc.ClientConn, error) {
var pool *GrpcConnection
if len(sid) > 0 && sid[0] != 0 {
pool = p.poolM[sid[0]]
} else {
pool = p.poolS[rand.Intn(len(p.poolS))]
}
if pool == nil {
return nil, fmt.Errorf("cannot find connection")
}
return pool.GetConnection(), nil
}
func (p *GrpcConnectionMgr) LoadAll() map[int64]*grpc.ClientConn {
sidM := make(map[int64]*grpc.ClientConn)
for sid, pool := range p.poolM {
sidM[sid] = pool.GetConnection()
}
return sidM
}
func (p *GrpcConnectionMgr) LoadAllSid() []int64 {
sidArr := make([]int64, 0)
for sid := range p.poolM {
sidArr = append(sidArr, sid)
}
return sidArr
}

42
net/grpc/resolver/conn.go Normal file
View File

@@ -0,0 +1,42 @@
package resolver
import (
"git.hlsq.asia/alex/service-common/log"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/keepalive"
"time"
)
const serviceConfig = `{
"loadBalancingConfig": [
{
"round_robin": {}
}
],
"healthCheckConfig": {
"serviceName": ""
}
}`
func NewGrpcConnection(target string) (*grpc.ClientConn, error) {
cc, err := grpc.NewClient(
target,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
grpc.WithDefaultServiceConfig(serviceConfig),
grpc.WithKeepaliveParams(
keepalive.ClientParameters{
Time: 30 * time.Second, // 保活探测包发送的时间间隔
Timeout: 10 * time.Second, // 保活探测包的超时时间
PermitWithoutStream: true,
},
),
)
if err != nil {
log.Errorf("create grpc err: %v, target: %v", err, target)
return nil, err
}
return cc, nil
}

View File

@@ -0,0 +1,84 @@
package resolver
import (
"context"
"git.hlsq.asia/alex/service-common/db/etcd"
"git.hlsq.asia/alex/service-common/discover/common"
"git.hlsq.asia/alex/service-common/log"
"go.etcd.io/etcd/client/v3"
"google.golang.org/grpc/resolver"
"strings"
)
const etcdSchema = "etcd"
func init() {
resolver.Register(&etcdBuilder{})
}
type etcdBuilder struct{}
func (*etcdBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
serviceName := strings.TrimPrefix(target.URL.Path, "/")
r := &etcdResolver{
serviceName: serviceName,
cc: cc,
}
r.start()
return r, nil
}
func (*etcdBuilder) Scheme() string { return etcdSchema }
type etcdResolver struct {
serviceName string
cc resolver.ClientConn
ctx context.Context
cancel context.CancelFunc
}
func (r *etcdResolver) start() {
r.ctx, r.cancel = context.WithCancel(context.Background())
r.update()
go r.watch()
}
func (r *etcdResolver) getPrefix() string {
return common.KeyDiscoverService + "/" + r.serviceName
}
func (r *etcdResolver) update() {
resp, err := etcd.GetClient().Get(r.getPrefix(), clientv3.WithPrefix())
if err != nil {
log.Errorf("etcd resolver get error: %v", err)
return
}
var addrArray []resolver.Address
for _, kv := range resp.Kvs {
addr := string(kv.Value)
if addr != "" {
addrArray = append(addrArray, resolver.Address{Addr: addr})
}
}
_ = r.cc.UpdateState(resolver.State{Addresses: addrArray})
}
func (r *etcdResolver) watch() {
watchCh := etcd.GetClient().Watch(r.getPrefix(), clientv3.WithPrefix())
for w := range watchCh {
if w.Err() != nil {
continue
}
r.update()
}
}
func (r *etcdResolver) ResolveNow(resolver.ResolveNowOptions) {
r.update()
}
func (r *etcdResolver) Close() {
r.cancel()
}

View File

@@ -0,0 +1,35 @@
package resolver
import (
"google.golang.org/grpc"
"sync"
)
var (
mu = sync.RWMutex{}
conn = make(map[string]*grpc.ClientConn)
)
func GetGrpcClientConn(target string) (*grpc.ClientConn, error) {
mu.RLock()
if c, ok := conn[target]; ok && c != nil {
mu.RUnlock()
return c, nil
}
mu.RUnlock()
mu.Lock()
defer mu.Unlock()
if c, ok := conn[target]; ok && c != nil {
return c, nil
}
newConn, err := NewGrpcConnection(target)
if err != nil {
return nil, err
}
conn[target] = newConn
return newConn, nil
}

View File

@@ -0,0 +1,103 @@
package service
import (
"context"
"encoding/json"
"fmt"
"git.hlsq.asia/alex/proto/proto/rs/rscommon"
"git.hlsq.asia/alex/service-common/log"
"git.hlsq.asia/alex/service-common/net/http/httpresp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
otelcodes "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"strconv"
"time"
)
func (s *Base) RecoveryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
defer func() {
if r := recover(); r != nil {
log.Errorf("service Panic: %v", r)
err = status.Error(codes.Internal, fmt.Sprintf("Internal server error: %v", r))
}
}()
return handler(ctx, req)
}
func (s *Base) CheckSID(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
if md, ok := metadata.FromIncomingContext(ctx); ok {
sidArr := md.Get("X-Sid")
if len(sidArr) > 0 {
if strconv.Itoa(int(s.SID)) != sidArr[0] {
err = status.Error(codes.Internal, "SID mismatch")
return
}
}
}
return handler(ctx, req)
}
func (s *Base) LoggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
usn := ""
if md, ok := metadata.FromIncomingContext(ctx); ok {
usnArr := md.Get("X-Usn")
if len(usnArr) > 0 {
usn = usnArr[0]
}
}
_, span := otel.Tracer("grpc.interceptor.logging").Start(ctx, info.FullMethod)
defer span.End()
reqStr, _ := json.Marshal(req)
reqString := fmt.Sprintf("[usn:%v] ↓↓↓ method: %s, Request: %v", usn, info.FullMethod, string(reqStr))
span.SetAttributes(attribute.String("rpc.req", reqString))
span.SetAttributes(attribute.String("usn", usn))
log.Infof(reqString)
start := time.Now()
resp, err = handler(ctx, req)
respStr, _ := json.Marshal(resp)
respString := fmt.Sprintf("[usn:%v] ↑↑↑ method: %s, Duration: %v, Response: %v, Error: %v", usn, info.FullMethod, time.Since(start), string(respStr), err)
span.SetAttributes(attribute.String("rpc.resp", respString))
log.Infof(respString)
if err != nil {
if stack, ok := err.(interface{ StackTrace() string }); ok {
span.AddEvent("Stack Trace", trace.WithAttributes(
attribute.String("stack.trace", fmt.Sprintf("%v", stack.StackTrace())),
))
if original, ok := err.(interface{ Original() error }); ok {
if wrap, ok := original.Original().(interface{ Wrap() error }); ok {
err = wrap.Wrap()
} else {
err = checkGRPCError(original.Original())
}
}
} else if wrap, ok := err.(interface{ Wrap() error }); ok {
err = wrap.Wrap()
} else {
err = checkGRPCError(err)
}
span.SetStatus(otelcodes.Error, err.Error())
}
return
}
func checkGRPCError(err error) error {
if original, ok := err.(interface{ GRPCStatus() *status.Status }); ok {
details := original.GRPCStatus().Details()
if len(details) > 0 {
if ei, ok := details[0].(*rscommon.ErrorInfo); ok {
return httpresp.NewCode(int(ei.Code), ei.Msg).Wrap()
}
}
}
return err
}

106
net/grpc/service/service.go Normal file
View File

@@ -0,0 +1,106 @@
package service
import (
"fmt"
"git.hlsq.asia/alex/service-common/config"
"git.hlsq.asia/alex/service-common/discover"
"git.hlsq.asia/alex/service-common/log"
"git.hlsq.asia/alex/service-common/utils"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/keepalive"
"net"
"sync"
"time"
)
type IService interface {
Init(ready *sync.WaitGroup)
SetReady()
Close()
}
type Base struct {
Target string
ServiceName string
SID int64
Serve *grpc.Server
Cfg *config.GrpcConfig
OnCustomGrpcServerOption func() []grpc.ServerOption
OnInit func(serve *grpc.Server)
OnClose func()
wg *sync.WaitGroup
healthcheck *health.Server
}
func (s *Base) Init(ready *sync.WaitGroup) {
s.wg = &sync.WaitGroup{}
s.wg.Add(1)
s.SID = utils.SnowflakeInt64()
go func() {
defer s.wg.Done()
defer s.OnClose()
defer discover.UnRegisterGrpcServer(s.SID)
// 监听端口
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", s.Cfg.Port))
if err != nil {
log.Errorf("%v ListenPort err: %v", s.Target, err)
return
}
options := []grpc.ServerOption{
grpc.ChainUnaryInterceptor(
s.RecoveryInterceptor,
s.CheckSID,
s.LoggingInterceptor,
),
grpc.StatsHandler(otelgrpc.NewServerHandler()),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 20 * time.Second,
PermitWithoutStream: true,
}),
}
options = append(options, s.OnCustomGrpcServerOption()...)
s.Serve = grpc.NewServer(options...)
s.OnInit(s.Serve)
// 健康检查
s.healthcheck = health.NewServer()
s.healthcheck.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
grpc_health_v1.RegisterHealthServer(s.Serve, s.healthcheck)
// 服务注册
if err = discover.RegisterGrpcServer(s.Target, s.SID, fmt.Sprintf("%v:%d", s.Cfg.Address, s.Cfg.Port), s.Cfg.TTL); err != nil {
log.Errorf("%v RegisterGrpcServer err: %v", s.Target, err)
return
}
// 准备好了
ready.Done()
log.Infof("%v registered grpc sid: %v", s.Target, s.SID)
if err = s.Serve.Serve(lis); err != nil {
log.Errorf("%v Serve err: %v", s.Target, err)
return
}
log.Infof("%v server stop.", s.Target)
}()
}
func (s *Base) SetReady() {
if s.healthcheck != nil {
s.healthcheck.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
}
}
func (s *Base) Close() {
if s.healthcheck != nil {
s.healthcheck.SetServingStatus("", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
}
if s.Serve != nil {
s.Serve.GracefulStop()
s.wg.Wait()
}
}

122
net/http/httpresp/code.go Normal file
View File

@@ -0,0 +1,122 @@
package httpresp
import (
"git.hlsq.asia/alex/proto/proto/rs/rscommon"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
OK = NewCode(0, "OK")
Failed = NewCode(1, "Failed")
TokenInvalid = NewCode(2, "Token Invalid")
MethodNotAllowed = NewCode(3, "Method Not Allowed")
NotFound = NewCode(4, "Not Found")
TokenForbidden = NewCode(5, "Token Forbidden")
AdminNotExist = NewCode(6, "Admin Not Exist")
ServerMaintenance = NewCode(7, "Server Maintenance")
RequestTooFast = NewCode(8, "Request too fast") // 请求太频繁了
ParamError = NewCode(1001, "参数错误")
PageQueryError = NewCode(1002, "page query param error")
PhoneCodeError = NewCode(1003, "Phone or code error") // 手机验证码错误
PhoneHasBeenBind = NewCode(1004, "Phone number has been bind") // 手机号已经被绑定
WeChatHasBeenBind = NewCode(1005, "WeChat has been bind") // 微信已经被绑定
BindTicketError = NewCode(1006, "Bind ticket error") // 登录绑定凭证无效
RoundIsEnd = NewCode(2001, "Round is end")
RoundNotEnd = NewCode(2002, "Round not end")
NameEmpty = NewCode(1002, "名称不能为空")
NameDuplicate = NewCode(1003, "名称或编号不能重复")
ListEmpty = NewCode(1004, "列表不能为空")
RepeatCommit = NewCode(1005, "请勿重复提交")
// ----------------user 10000-11000--------------------
UserNotFound = NewCode(10001, "User not found")
CharNickNameTooLong = NewCode(10002, "Character nickname too long")
CharNickNameIllegal = NewCode(10003, "Character nickname illegal")
CharNickNameEmpty = NewCode(10004, "Character nickname empty")
CharModelEmpty = NewCode(10005, "Character model empty")
CharModelNotExist = NewCode(10006, "Character model not exist")
CharAvatarEmpty = NewCode(10007, "Character avatar empty")
CharAvatarNotExist = NewCode(10008, "Character avatar not exist")
CharSexInvalid = NewCode(10009, "Character sex invalid")
RewardAlreadyBindError = NewCode(10010, "Reward already bind") // 奖励已绑定
// ----------------SCENE 10010-10100--------------------
SceneRoomNumberGenError = NewCode(10010, "Room number gen error")
RoomNotExistsError = NewCode(10011, "Room not exists error")
RoomPlayerUpperLimit = NewCode(10012, "Room player upper limit") // 房间人数满了
RoomStatusNotAllowEnter = NewCode(10013, "Room status not allow enter") // 房间状态不允许中途进入
PlayerAlreadyInRoom = NewCode(10014, "Player already in room") //玩家已经在房间中
PlayerHandicapNotMeet = NewCode(10015, "Player handicap not meet") //玩家等级不足
NoAvailableRoom = NewCode(10016, "No available room") // 没有符合的房间
SceneRoomDescriptionWidthError = NewCode(10017, "Room description width more than error")
// ----------------court-physical 11001-12000--------------------
CourtPhysicalNotFind = NewCode(11001, "没有找到线下球场配置")
CourtPhysicalNotFindCompetitionl = NewCode(11002, "没有找到比赛")
CourtPhysicalCompetitionlNotPermission = NewCode(11003, "你不是房主无权编辑")
CourtPhysicalCompetitionlNotHole = NewCode(11004, "球场配置错误,没有洞的数据")
CourtPhysicalCompetitionlPlayerUpperLimit = NewCode(11005, "比赛球员已达上限")
CourtPhysicalCompetitionlPlayerIsRoom = NewCode(11006, "用户已在房间")
CourtPhysicalCompetitionlStart = NewCode(11007, "比赛已开始")
CourtPhysicalCompetitionlPlayerJoinFailed = NewCode(11008, "用户加入比赛失败")
CourtPhysicalCompetitionlNotRemovedHomeowner = NewCode(11009, "不能移除房主")
CourtPhysicalCompetitionlNotFindPlayer = NewCode(11010, "房间没有该用户")
CourtPhysicalCompetitionlNotGamePlayer = NewCode(11011, "该用户不是球员")
CourtPhysicalCompetitionlNotIn = NewCode(11012, "该用户不在本局比赛")
CourtPhysicalCompetitionlStartFail = NewCode(11013, "比赛开始初始化数据失败")
CourtPhysicalCompetitionlEnd = NewCode(11014, "比赛已结束")
CourtPhysicalCompetitionlNotStart = NewCode(11015, "比赛未开始")
CourtPhysicalCompetitionlPKFail = NewCode(11016, "PK玩法配置错误")
CourtPhysicalCompetitionlPKTigerNot3Player = NewCode(11017, "PK玩法配置错误:Tiger玩法参赛选手需≥3")
CourtPhysicalCompetitionlPKTigerNotTiger = NewCode(11018, "PK玩法配置错误:Tiger玩法不能没有老虎")
CourtPhysicalCompetitionlPKTigerNotHole = NewCode(11019, "PK玩法配置错误:Tiger玩法至少要选择一个球洞")
CourtPhysicalCompetitionlPKTigerEnhanceFail = NewCode(11020, "PK玩法配置错误:Tiger玩法比洞才有受让杆")
CourtPhysicalCompetitionlPKTigerTopHoleFail = NewCode(11021, "PK玩法配置错误:Tiger玩法比洞才有顶洞和收顶洞")
CourtPhysicalCompetitionlPKRyderCupNot4Player = NewCode(11022, "PK玩法配置错误:莱德杯玩法需4名参赛选手")
CourtPhysicalCompetitionlPKRyderCupLeastFormat = NewCode(11023, "PK玩法配置错误:莱德杯玩法至少要选择一个赛制")
CourtPhysicalCompetitionlPKRyderCupHoleFail = NewCode(11024, "PK玩法配置错误:莱德杯玩法球洞配置冲突")
CourtPhysicalCompetitionlPKRyderCupTeamFail = NewCode(11025, "PK玩法配置错误:莱德杯玩法两队人数不对")
CourtPhysicalCompetitionlNameIllegal = NewCode(11026, "比赛昵称存在违禁词")
CourtPhysicalCompetitionlLinkError = NewCode(11027, "生成微信链接错误")
// 锦标赛 ----------------court 12001-13000--------------------
CourtTournamentNotInvited = NewCode(12002, "仅限受邀用户参赛") // 未在邀请名单内
CourtTournamentHandicapNotMeet = NewCode(12003, "差点不符合参赛要求") // 无有效差点或超出要求区间
CourtTournamentRoundAlreadyStarted = NewCode(12004, "本轮比赛已开始") // 并发重复开赛(竞态)
CourtTournamentRoundNotInProgress = NewCode(12005, "轮次未开始,不能开赛") // 轮次不是进行中
CourtTournamentJoinEnded = NewCode(12006, "已淘汰,无法继续比赛") // 报名已结束(已淘汰)
CourtTournamentPracticeNotAllowed = NewCode(12007, "该锦标赛未开放练习") // 未配置练习次数
CourtTournamentPracticeEnded = NewCode(12008, "比赛已结束,不允许练习") // 决赛结束/已完成/已取消
CourtTournamentPracticeTimesNotEnough = NewCode(12009, "练习次数已用完") // 已练习次数达上限
CourtTournamentShelfStatusNotAllowed = NewCode(12010, "该锦标赛不允许下架") // 该锦标赛不允许下架
)
type Code struct {
code int
error string
}
func NewCode(code int, error string) *Code {
return &Code{
code: code,
error: error,
}
}
func (c *Code) Code() int {
return c.code
}
func (c *Code) Error() string {
return c.error
}
func (c *Code) Wrap() error {
st := status.New(codes.Unknown, c.Error())
st, _ = st.WithDetails(&rscommon.ErrorInfo{
Code: int32(c.Code()),
Msg: c.Error(),
})
return st.Err()
}

View File

@@ -0,0 +1,51 @@
package httpresp
import (
"github.com/gin-gonic/gin"
"net/http"
)
type RespJsonData struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
func Success(data interface{}) *RespJsonData {
return &RespJsonData{
Code: OK.Code(),
Msg: OK.Error(),
Data: data,
}
}
func Error(code *Code) *RespJsonData {
return &RespJsonData{
Code: code.Code(),
Msg: code.Error(),
}
}
func JsonOK(c *gin.Context, data *RespJsonData) {
c.JSON(http.StatusOK, data)
}
func JsonBadRequest(c *gin.Context) {
c.JSON(http.StatusBadRequest, Error(ParamError))
}
func JsonMethodNotAllowed(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, Error(MethodNotAllowed))
}
func JsonNotFound(c *gin.Context) {
c.JSON(http.StatusNotFound, Error(NotFound))
}
func JsonUnauthorized(c *gin.Context) {
c.JSON(http.StatusOK, Error(TokenInvalid))
}
func JsonMaintenance(c *gin.Context) {
c.JSON(http.StatusOK, Error(ServerMaintenance))
}

43
net/socket/server.go Normal file
View File

@@ -0,0 +1,43 @@
package socket
type Action int
const (
// None indicates that no action should occur following an event.
None Action = iota
// Close closes the connection.
Close
// Shutdown shutdowns the engine.
Shutdown
)
type OpCode byte
const (
OpContinuation OpCode = 0x0
OpText OpCode = 0x1
OpBinary OpCode = 0x2
OpClose OpCode = 0x8
OpPing OpCode = 0x9
OpPong OpCode = 0xa
)
// ISocketServer 由应用层实现
type ISocketServer interface {
OnOpen(ISocketConn) ([]byte, Action) // 开启连接
OnHandShake(ISocketConn) Action // 开始握手
OnMessage(ISocketConn, []byte) Action // 收到消息
OnClose(ISocketConn, error) Action // 关闭连接
OnTick() Action // 定时调用(由网络层决定)
}
// ISocketConn 由网络层实现
type ISocketConn interface {
GetParam(key string) interface{}
SetParam(key string, values interface{})
RemoteAddr() string
Ping() error
Write(data []byte) error
Close() error
IsClose() bool
}

View File

@@ -0,0 +1,181 @@
package websocket
import (
"bytes"
"context"
"fmt"
"git.hlsq.asia/alex/service-common/log"
"git.hlsq.asia/alex/service-common/net/socket"
"github.com/panjf2000/gnet/v2"
"github.com/panjf2000/gnet/v2/pkg/logging"
"sync"
"time"
)
// WSServer 实现GNet库接口
type WSServer struct {
eng gnet.Engine
i socket.ISocketServer
logger logging.Logger // 日志
upgradeTimeout time.Duration // 升级超时时间
unUpgradeConn sync.Map
}
func NewWSServer(i socket.ISocketServer, logger logging.Logger, upgradeTimeout time.Duration) *WSServer {
if i == nil {
return nil
}
return &WSServer{
i: i,
logger: logger,
upgradeTimeout: upgradeTimeout,
unUpgradeConn: sync.Map{},
}
}
func (s *WSServer) Run(addr string, multicore bool, numEventLoop int, tcpNoDelay gnet.TCPSocketOpt, readBufferCap, writeBufferCap int, lockOSThread bool, reusePort, ticker bool) error {
return gnet.Run(
s,
addr,
gnet.WithMulticore(multicore),
gnet.WithNumEventLoop(numEventLoop),
gnet.WithTCPNoDelay(tcpNoDelay),
gnet.WithReadBufferCap(readBufferCap),
gnet.WithWriteBufferCap(writeBufferCap),
gnet.WithLockOSThread(lockOSThread),
gnet.WithReusePort(reusePort),
gnet.WithTicker(ticker),
gnet.WithLogger(s.logger),
)
}
func (s *WSServer) Stop() error {
return s.eng.Stop(context.Background())
}
func (s *WSServer) OnBoot(eng gnet.Engine) gnet.Action {
s.eng = eng
return gnet.None
}
func (s *WSServer) OnShutdown(_ gnet.Engine) {
}
func (s *WSServer) OnOpen(c gnet.Conn) ([]byte, gnet.Action) {
ws := &WSConn{
Conn: c,
isUpgrade: false,
openTime: time.Now().Unix(),
buf: bytes.Buffer{},
logger: log.GetLogger().Named(fmt.Sprintf("CONN %v", c.RemoteAddr().String())),
wsMessageBuf: wsMessageBuf{
curHeader: nil,
cachedBuf: bytes.Buffer{},
},
param: make(map[string]interface{}),
remoteAddr: c.RemoteAddr().String(),
}
c.SetContext(ws)
s.unUpgradeConn.Store(c.RemoteAddr().String(), ws)
d, a := s.i.OnOpen(ws)
return d, (gnet.Action)(a)
}
// OnClose fires when a connection has been closed.
// The parameter err is the last known connection error.
func (s *WSServer) OnClose(c gnet.Conn, err error) (action gnet.Action) {
s.unUpgradeConn.Delete(c.RemoteAddr().String())
ws, ok := c.Context().(*WSConn)
if ok {
ws.isClose = true
ws.logger.Infof("Connection close, err: %v", err)
return gnet.Action(s.i.OnClose(ws, err))
}
return
}
// OnTraffic fires when a local socket receives data from the peer.
func (s *WSServer) OnTraffic(c gnet.Conn) gnet.Action {
ws := c.Context().(*WSConn)
if action := ws.readBytesBuf(c); action != gnet.None {
return action
}
if !ws.isUpgrade {
data, action := ws.upgrade()
if len(data) > 0 {
s.unUpgradeConn.Delete(c.RemoteAddr().String())
action = gnet.Action(s.i.OnHandShake(ws))
if action == gnet.None {
if err := ws.Conn.AsyncWrite(data, nil); err != nil {
ws.logger.Errorf("OnTraffic upgrade AsyncWrite err: %v", err)
if err = ws.Close(); err != nil {
ws.logger.Errorf("OnTraffic upgrade Close error: %v", err)
}
}
}
}
return action
}
if msg, err := ws.readWsMessages(); err != nil {
ws.logger.Errorf("read ws messages err: %v", err)
return gnet.Close
} else if msg != nil {
for _, m := range msg {
if socket.OpCode(m.OpCode) == socket.OpClose {
return gnet.Close
}
if socket.OpCode(m.OpCode) == socket.OpPing || socket.OpCode(m.OpCode) == socket.OpPong {
continue
}
a := s.i.OnMessage(ws, m.Payload)
if gnet.Action(a) != gnet.None {
return gnet.Action(a)
}
}
}
return gnet.None
}
// OnTick fires immediately after the engine starts and will fire again
// following the duration specified by the delay return value.
func (s *WSServer) OnTick() (delay time.Duration, action gnet.Action) {
now := time.Now().Unix()
delConn := make([]string, 0)
s.unUpgradeConn.Range(func(key, value interface{}) bool {
k, ok := key.(string)
if !ok {
return true
}
v, ok := value.(*WSConn)
if !ok {
return true
}
if now-v.openTime >= int64(s.upgradeTimeout.Seconds()) {
delConn = append(delConn, k)
}
return true
})
for _, k := range delConn {
wsConn, _ := s.unUpgradeConn.LoadAndDelete(k)
if wsConn == nil {
continue
}
v, ok := wsConn.(*WSConn)
if !ok {
continue
}
if err := v.Close(); err != nil {
v.logger.Errorf("upgrade ws time out close socket error: %v", err)
}
}
delay = 5 * time.Second
action = gnet.Action(s.i.OnTick())
return
}

View File

@@ -0,0 +1,203 @@
package websocket
import (
"bufio"
"bytes"
"errors"
"io"
"net/url"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
"github.com/panjf2000/gnet/v2"
"github.com/panjf2000/gnet/v2/pkg/logging"
)
// WSConn 实现ISocketConn接口
type WSConn struct {
gnet.Conn
buf bytes.Buffer
logger logging.Logger
isUpgrade bool // 是否已经升级
upgradeResp []byte // 升级响应
isClose bool
param map[string]interface{}
openTime int64 // 开启连接的时间
remoteAddr string // 远程IP地址
wsMessageBuf
}
type wsMessageBuf struct {
curHeader *ws.Header
cachedBuf bytes.Buffer
}
type readWrite struct {
io.Reader
io.Writer
}
func (w *WSConn) readBytesBuf(c gnet.Conn) gnet.Action {
size := c.InboundBuffered()
if size <= 0 {
return gnet.None
}
buf := make([]byte, size)
read, err := c.Read(buf)
if err != nil {
if w.logger != nil {
w.logger.Errorf("ws read bytes buf error: %v", err)
}
return gnet.Close
}
if read < size {
if w.logger != nil {
w.logger.Errorf("read bytes len err! size: %d read: %d", size, read)
}
return gnet.Close
}
w.buf.Write(buf)
return gnet.None
}
func (w *WSConn) upgrade() (data []byte, action gnet.Action) {
buf := &w.buf
tmpReader := bytes.NewReader(buf.Bytes())
oldLen := tmpReader.Len()
result := &bytes.Buffer{}
tempWriter := bufio.NewWriter(result)
var err error = nil
up := ws.Upgrader{
OnRequest: w.OnRequest,
}
_, err = up.Upgrade(readWrite{tmpReader, tempWriter})
skipN := oldLen - tmpReader.Len()
if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { //数据不完整
return
}
buf.Next(skipN)
if w.logger != nil {
w.logger.Errorf("ws upgrade err: %v", err.Error())
}
action = gnet.Close
return
}
buf.Next(skipN)
//if w.logger != nil {
// w.logger.Infof("ws upgrade success conn upgrade websocket protocol!")
//}
_ = tempWriter.Flush()
data = result.Bytes()
w.isUpgrade = true
return
}
func (w *WSConn) readWsMessages() (messages []wsutil.Message, err error) {
in := &w.buf
for {
if w.curHeader == nil {
if in.Len() < ws.MinHeaderSize { //头长度至少是2
return
}
var head ws.Header
//有可能不完整,构建新的 reader 读取 head 读取成功才实际对 in 进行读操作
tmpReader := bytes.NewReader(in.Bytes())
oldLen := tmpReader.Len()
head, err = ws.ReadHeader(tmpReader)
skipN := oldLen - tmpReader.Len()
if err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { //数据不完整
return messages, nil
}
in.Next(skipN)
return nil, err
}
_, err = io.CopyN(&w.cachedBuf, in, int64(skipN))
if err != nil {
return
}
w.curHeader = &head
}
dataLen := (int)(w.curHeader.Length)
if dataLen > 0 {
if in.Len() >= dataLen {
_, err = io.CopyN(&w.cachedBuf, in, int64(dataLen))
if err != nil {
return
}
} else { //数据不完整
if w.logger != nil {
w.logger.Debugf("ws read ws message incomplete data ,%v,%v", in.Len(), dataLen)
}
return
}
}
if w.curHeader.Fin { //当前 header 已经是一个完整消息
messages, err = wsutil.ReadClientMessage(&w.cachedBuf, messages)
if err != nil {
return nil, err
}
w.cachedBuf.Reset()
}
w.curHeader = nil
}
}
func (w *WSConn) OnRequest(u []byte) error {
parsedURL, err := url.Parse(string(u))
if err != nil {
return err
}
for key, value := range parsedURL.Query() {
w.SetParam(key, value[0])
}
return nil
}
func (w *WSConn) GetParam(key string) interface{} {
return w.param[key]
}
func (w *WSConn) SetParam(key string, values interface{}) {
w.param[key] = values
}
func (w *WSConn) RemoteAddr() string {
return w.remoteAddr
}
func (w *WSConn) Write(data []byte) error {
return w.write(data, ws.OpBinary)
}
func (w *WSConn) Ping() (err error) {
return w.write(make([]byte, 0), ws.OpPing)
}
func (w *WSConn) Close() (err error) {
defer func(Conn gnet.Conn) {
err = Conn.Close()
}(w.Conn)
return w.write(make([]byte, 0), ws.OpClose)
}
func (w *WSConn) IsClose() bool {
return w.isClose
}
func (w *WSConn) write(data []byte, opCode ws.OpCode) error {
if w.isClose {
return errors.New("connection has close")
}
buf := bytes.Buffer{}
if err := wsutil.WriteServerMessage(&buf, opCode, data); err != nil {
return err
}
return w.Conn.AsyncWrite(buf.Bytes(), nil)
}

143
server/status.go Normal file
View File

@@ -0,0 +1,143 @@
package server
import (
"context"
"git.hlsq.asia/alex/service-common/db/etcd"
"git.hlsq.asia/alex/service-common/log"
"go.etcd.io/etcd/api/v3/mvccpb"
clientv3 "go.etcd.io/etcd/client/v3"
"sync"
)
var (
statusWg = &sync.WaitGroup{}
statusStopFunc context.CancelFunc
)
const (
serverStatusKey = "/server/status" // 状态key
)
type StatusType string
const (
StatusInit StatusType = "init" // 初始化状态
StatusRunning StatusType = "running" // 运行中
StatusMaintenance StatusType = "maintenance" // 维护中
StatusWhitelist StatusType = "whitelist" // 白名单模式
)
var serverStatus *Status
type Status struct {
etcdClient *etcd.Client
status StatusType
mu sync.RWMutex
callbacks []func(status StatusType)
}
func InitStatus() {
serverStatus = &Status{
etcdClient: etcd.GetClient(),
status: StatusInit,
mu: sync.RWMutex{},
}
}
func GetServerStatus() *Status {
return serverStatus
}
func ListenStatus(ready *sync.WaitGroup) {
var stopCtx context.Context
stopCtx, statusStopFunc = context.WithCancel(context.Background())
statusWg.Add(1)
go func(stopCtx context.Context) {
defer statusWg.Done()
// 监听状态变化
serverStatusResp, _ := serverStatus.etcdClient.Get(serverStatusKey)
for _, kv := range serverStatusResp.Kvs {
serverStatus.onStatusChange(clientv3.EventTypePut, string(kv.Key), string(kv.Value))
}
serverStatusWatchChan := serverStatus.etcdClient.Watch(serverStatusKey,
clientv3.WithRev(serverStatusResp.Header.Revision+1))
ready.Done()
for {
select {
case msg := <-serverStatusWatchChan:
for _, event := range msg.Events {
serverStatus.onStatusChange(event.Type, string(event.Kv.Key), string(event.Kv.Value))
}
case <-stopCtx.Done():
return
}
}
}(stopCtx)
return
}
func CloseStatus() {
if statusStopFunc != nil {
statusStopFunc()
statusWg.Wait()
}
}
func (s *Status) GetStatus() StatusType {
s.mu.RLock()
defer s.mu.RUnlock()
return s.status
}
func (s *Status) CanAccess(usn int64) bool {
var status StatusType
s.mu.RLock()
status = s.status
s.mu.RUnlock()
switch status {
case StatusRunning:
return true
case StatusMaintenance:
return false
case StatusWhitelist:
return GetServerWhitelist().CheckWhitelist(usn)
default:
return false
}
}
func (s *Status) RegisterChangeCallback(fc func(status StatusType)) {
s.mu.Lock()
defer s.mu.Unlock()
s.callbacks = append(s.callbacks, fc)
}
func (s *Status) SetStatus(status string) {
_, err := s.etcdClient.Put(serverStatusKey, status)
if err != nil {
log.Errorf("server status set failed: %v", err)
return
}
return
}
func (s *Status) onStatusChange(t mvccpb.Event_EventType, key, value string) {
if key != serverStatusKey {
return
}
switch t {
case clientv3.EventTypePut:
s.mu.Lock()
s.status = StatusType(value)
s.mu.Unlock()
for _, fc := range s.callbacks {
fc(StatusType(value))
}
}
}

215
server/whitelist.go Normal file
View File

@@ -0,0 +1,215 @@
package server
import (
"context"
"encoding/json"
"git.hlsq.asia/alex/service-common/db/etcd"
"git.hlsq.asia/alex/service-common/log"
"go.etcd.io/etcd/api/v3/mvccpb"
clientv3 "go.etcd.io/etcd/client/v3"
"sync"
)
var (
whiteListenWg = &sync.WaitGroup{}
whiteListenStopFunc context.CancelFunc
)
const (
serverWhitelistKey = "/server/whitelist" // 白名单列表key
serverFactoryWhitelistKey = "/server/factory_whitelist" // 工厂白名单列表key
)
const whitelistUsnList = "usn_list" // usn白名单列表字段名
type Whitelist struct {
etcdClient *etcd.Client
mu sync.RWMutex
whitelist map[int64]struct{}
factoryWhitelist map[int64]struct{}
}
var serverWhitelist *Whitelist
func InitWhitelist() {
serverWhitelist = &Whitelist{
etcdClient: etcd.GetClient(),
mu: sync.RWMutex{},
whitelist: make(map[int64]struct{}),
factoryWhitelist: make(map[int64]struct{}),
}
}
func ListenWhitelist(ready *sync.WaitGroup) {
var stopCtx context.Context
stopCtx, whiteListenStopFunc = context.WithCancel(context.Background())
whiteListenWg.Add(1)
go func(stopCtx context.Context) {
defer whiteListenWg.Done()
// 监听工厂白名单变化
serverFactoryWhitelistResp, _ := serverWhitelist.etcdClient.Get(serverFactoryWhitelistKey)
for _, kv := range serverFactoryWhitelistResp.Kvs {
serverWhitelist.onFactoryWhitelistChange(clientv3.EventTypePut, string(kv.Key), string(kv.Value))
}
serverFactoryWhitelistWatchChan := serverWhitelist.etcdClient.Watch(serverFactoryWhitelistKey,
clientv3.WithRev(serverFactoryWhitelistResp.Header.Revision+1))
// 监听白名单变化
serverWhitelistResp, _ := serverWhitelist.etcdClient.Get(serverWhitelistKey)
for _, kv := range serverWhitelistResp.Kvs {
serverWhitelist.onWhitelistChange(clientv3.EventTypePut, string(kv.Key), string(kv.Value))
}
serverWhitelistWatchChan := serverWhitelist.etcdClient.Watch(serverWhitelistKey,
clientv3.WithRev(serverWhitelistResp.Header.Revision+1))
ready.Done()
for {
select {
case msg := <-serverFactoryWhitelistWatchChan:
for _, event := range msg.Events {
serverWhitelist.onFactoryWhitelistChange(event.Type, string(event.Kv.Key), string(event.Kv.Value))
}
case msg := <-serverWhitelistWatchChan:
for _, event := range msg.Events {
serverWhitelist.onWhitelistChange(event.Type, string(event.Kv.Key), string(event.Kv.Value))
}
case <-stopCtx.Done():
return
}
}
}(stopCtx)
return
}
func CloseWhitelist() {
if whiteListenStopFunc != nil {
whiteListenStopFunc()
whiteListenWg.Wait()
}
}
func GetServerWhitelist() *Whitelist {
return serverWhitelist
}
func (s *Whitelist) GetWhitelist() map[int64]struct{} {
s.mu.RLock()
defer s.mu.RUnlock()
return s.whitelist
}
func (s *Whitelist) SetWhitelist(whitelist []int64) {
white := make(map[string]interface{})
white[whitelistUsnList] = whitelist
whiteStr, err := json.Marshal(white)
if err != nil {
log.Errorf("server whitelist set failed: %v", err)
return
}
_, err = s.etcdClient.Put(serverWhitelistKey, string(whiteStr))
if err != nil {
log.Errorf("server whitelist set failed: %v", err)
return
}
}
func (s *Whitelist) CheckWhitelist(usn int64) bool {
s.mu.RLock()
defer s.mu.RUnlock()
_, ok := s.whitelist[usn]
return ok
}
func (s *Whitelist) GetFactoryWhitelist() map[int64]struct{} {
s.mu.RLock()
defer s.mu.RUnlock()
return s.factoryWhitelist
}
func (s *Whitelist) SetFactoryWhitelist(whitelist []int64) {
white := make(map[string]interface{})
white[whitelistUsnList] = whitelist
whiteStr, err := json.Marshal(white)
if err != nil {
log.Errorf("server factory whitelist set failed: %v", err)
return
}
_, err = s.etcdClient.Put(serverFactoryWhitelistKey, string(whiteStr))
if err != nil {
log.Errorf("server factory whitelist set failed: %v", err)
return
}
}
func (s *Whitelist) CheckFactoryWhitelist(usn int64) bool {
s.mu.RLock()
defer s.mu.RUnlock()
_, ok := s.factoryWhitelist[usn]
return ok
}
func (s *Whitelist) onFactoryWhitelistChange(t mvccpb.Event_EventType, key, value string) {
if key != serverFactoryWhitelistKey {
return
}
switch t {
case clientv3.EventTypePut:
s.mu.Lock()
defer s.mu.Unlock()
userSns := s.handlerWhitelistChange(value)
s.factoryWhitelist = make(map[int64]struct{})
for _, usn := range userSns {
s.factoryWhitelist[usn] = struct{}{}
}
}
}
func (s *Whitelist) onWhitelistChange(t mvccpb.Event_EventType, key, value string) {
if key != serverWhitelistKey {
return
}
switch t {
case clientv3.EventTypePut:
s.mu.Lock()
defer s.mu.Unlock()
userSns := s.handlerWhitelistChange(value)
s.whitelist = make(map[int64]struct{})
for _, usn := range userSns {
s.whitelist[usn] = struct{}{}
}
}
}
func (s *Whitelist) handlerWhitelistChange(value string) []int64 {
userSns := make([]int64, 0)
white := make(map[string]interface{})
if err := json.Unmarshal([]byte(value), &white); err != nil {
log.Errorf("server whitelist set failed: %v", err)
return userSns
}
if usnList, ok := white[whitelistUsnList].([]interface{}); ok {
for _, usn := range usnList {
userSns = append(userSns, int64(usn.(float64)))
}
}
return userSns
}

37
template/sms/lianlu.go Normal file
View File

@@ -0,0 +1,37 @@
package sms
//
//import "git.hlsq.asia/alex/proto/topic"
//
//type CountryCode string
//
//const (
// CountryCodeChina CountryCode = "+86" // 中国
// CountryCodeHongKong CountryCode = "+852" // 中国香港
// CountryCodeMacau CountryCode = "+853" // 中国澳门
// CountryCodeTaiwan CountryCode = "+886" // 中国台湾
// CountryCodeJapan CountryCode = "+81" // 日本
// CountryCodeSouthKorea CountryCode = "+82" // 韩国
//)
//
//const (
// TemplateCodeVerificationCodeChinese topic.SmsTemplateCode = "70630138" // 验证码(中文)
// TemplateCodeVerificationCodeEnglish topic.SmsTemplateCode = "54236" // 验证码(英文)
// TemplateCodeVerificationCodeJapanese topic.SmsTemplateCode = "54270" // 验证码(日文)
// TemplateCodeVerificationCodeKorean topic.SmsTemplateCode = "54271" // 验证码(韩文)
// TemplateCodeVerificationCodeChineseTraditional topic.SmsTemplateCode = "54272" // 验证码(繁体中文)
//)
//
//func GetTemplateCodeVerificationCodeByCountryCode(countryCode CountryCode) topic.SmsTemplateCode {
// switch countryCode {
// case CountryCodeChina:
// return TemplateCodeVerificationCodeChinese
// case CountryCodeHongKong, CountryCodeMacau, CountryCodeTaiwan:
// return TemplateCodeVerificationCodeChineseTraditional
// case CountryCodeJapan:
// return TemplateCodeVerificationCodeJapanese
// case CountryCodeSouthKorea:
// return TemplateCodeVerificationCodeKorean
// }
// return TemplateCodeVerificationCodeEnglish
//}

36
template/sms/zhongtian.go Normal file
View File

@@ -0,0 +1,36 @@
package sms
import "git.hlsq.asia/alex/proto/topic"
type CountryCode string
const (
CountryCodeChina CountryCode = "+86" // 中国
CountryCodeHongKong CountryCode = "+852" // 中国香港
CountryCodeMacau CountryCode = "+853" // 中国澳门
CountryCodeTaiwan CountryCode = "+886" // 中国台湾
CountryCodeJapan CountryCode = "+81" // 日本
CountryCodeSouthKorea CountryCode = "+82" // 韩国
)
const (
TemplateCodeVerificationCodeChinese topic.SmsTemplateCode = "【巢云互动】您的验证码是:%v,该验证码5分钟内有效,请勿泄漏于他人!" // 验证码(中文)
TemplateCodeVerificationCodeEnglish topic.SmsTemplateCode = "[NEXQUB] Your verification code is %v. It is valid for 5 minutes. Please do not share it with anyone." // 验证码(英文)
TemplateCodeVerificationCodeJapanese topic.SmsTemplateCode = "[NEXQUB] 認証コード:%v 有効期限5分。このコードを他人に教えないでください。" // 验证码(日文)
TemplateCodeVerificationCodeKorean topic.SmsTemplateCode = "[NEXQUB] 인증번호 %v 유효시간 5분. 타인과 공유하지 마세요." // 验证码(韩文)
TemplateCodeVerificationCodeChineseTraditional topic.SmsTemplateCode = "[NEXQUB] 您的驗證碼是:%v,5分鐘內有效,請勿向他人提供。" // 验证码(繁体中文)
)
func GetTemplateCodeVerificationCodeByCountryCode(countryCode CountryCode) topic.SmsTemplateCode {
switch countryCode {
case CountryCodeChina:
return TemplateCodeVerificationCodeChinese
case CountryCodeHongKong, CountryCodeMacau, CountryCodeTaiwan:
return TemplateCodeVerificationCodeChineseTraditional
case CountryCodeJapan:
return TemplateCodeVerificationCodeJapanese
case CountryCodeSouthKorea:
return TemplateCodeVerificationCodeKorean
}
return TemplateCodeVerificationCodeEnglish
}

13
utils/channel.go Normal file
View File

@@ -0,0 +1,13 @@
package utils
import (
"fmt"
"git.hlsq.asia/alex/proto/proto/ss/sspb"
"strings"
)
// GetChannelName 获得频道名称
func GetChannelName(t sspb.ChannelType, sn int64) string {
s := sspb.ChannelType_name[int32(t)]
return fmt.Sprintf("%v-%v", strings.TrimPrefix(s, "CHANNEL_TYPE_"), sn)
}

72
utils/error.go Normal file
View File

@@ -0,0 +1,72 @@
package utils
import (
"errors"
"fmt"
"runtime"
"strconv"
"strings"
)
type WrapErrors struct {
original error
message string
trace []string
}
func (e *WrapErrors) Original() error {
return e.original
}
func (e *WrapErrors) Error() string {
return e.message
}
func (e *WrapErrors) StackTrace() string {
if e.trace == nil {
return ""
}
return strings.Join(e.trace, "\n")
}
func ErrorsWrap(err error, message ...string) error {
if err == nil {
return nil
}
var e *WrapErrors
if !errors.As(err, &e) {
e = &WrapErrors{
original: err,
}
}
if len(message) > 0 {
e.message = fmt.Sprintf("%v: %v", message[0], err.Error())
} else {
e.message = err.Error()
}
e.trace = append(e.trace, getErrorPath())
return e
}
func ErrorsWrapF(err error, message string, a ...any) error {
if err == nil {
return nil
}
var e *WrapErrors
if !errors.As(err, &e) {
e = &WrapErrors{
original: err,
}
}
e.message = fmt.Sprintf("%v: %v", fmt.Sprintf(message, a...), err.Error())
e.trace = append(e.trace, getErrorPath())
return e
}
func getErrorPath() string {
_, file, line, _ := runtime.Caller(2)
if file == "" {
return "unknown"
}
return file + ":" + strconv.Itoa(line)
}

71
utils/jwt.go Normal file
View File

@@ -0,0 +1,71 @@
package utils
import (
"errors"
"github.com/golang-jwt/jwt/v5"
"time"
)
type Claims struct {
USN int64 `json:"usn"`
jwt.RegisteredClaims
}
func GenToken(usn int64, secret string, expires time.Duration) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{
USN: usn,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expires)),
},
})
return token.SignedString([]byte(secret))
}
func ParseToken(tokenString string, secret string) (*Claims, error) {
if tokenString == "" {
return nil, errors.New("invalid token")
}
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(_ *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
type PhoneClaims struct {
Phone string `json:"phone"`
jwt.RegisteredClaims
}
func GenPhoneToken(phone string, secret string, expires time.Duration) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, PhoneClaims{
Phone: phone,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expires)),
},
})
return token.SignedString([]byte(secret))
}
func ParsePhoneToken(tokenString string, secret string) (*PhoneClaims, error) {
if tokenString == "" {
return nil, errors.New("invalid token")
}
claims := &PhoneClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(_ *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}

82
utils/locator/locator.go Normal file
View File

@@ -0,0 +1,82 @@
package locator
import (
"context"
"fmt"
"git.hlsq.asia/alex/service-common/db/redis"
"git.hlsq.asia/alex/service-common/log"
)
// 玩家定位器
const (
KeyGatewayInfo = "gateway:info:%v"
HFieldInfoGatewaySID = "gateway_sid_%v"
)
var (
scriptBindGatewaySha1 string
scriptBindGateway = `
local gateway_key = KEYS[1]
local field_key = ARGV[1]
local new_sid = ARGV[2]
local existing_sid = redis.call("HGET", gateway_key, field_key)
if existing_sid then
return 0
end
redis.call("HSET", gateway_key, field_key, new_sid)
return 1
`
)
func NewLocator(ctx context.Context, usn int64, platform int32) *Locator {
return &Locator{
ctx: ctx,
usn: usn,
platform: platform,
}
}
type Locator struct {
ctx context.Context
usn int64
platform int32
}
// BindGateway 绑定网关
func (l *Locator) BindGateway(gatewaySID int64) bool {
if scriptBindGatewaySha1 == "" {
scriptBindGatewaySha1 = redis.GetClient().ScriptLoad(l.ctx, scriptBindGateway).Val()
}
result, err := redis.GetClient().EvalSha(
l.ctx,
scriptBindGatewaySha1,
[]string{fmt.Sprintf(KeyGatewayInfo, l.usn)},
fmt.Sprintf(HFieldInfoGatewaySID, l.platform),
gatewaySID,
).Int64()
if err != nil {
return false
}
if result == 0 {
return false
}
return true
}
// UnbindGateway 解绑网关
func (l *Locator) UnbindGateway() {
err := redis.GetClient().HDel(l.ctx, fmt.Sprintf(KeyGatewayInfo, l.usn), fmt.Sprintf(HFieldInfoGatewaySID, l.platform)).Err()
if err != nil {
log.Errorf("UnbindGateway Failed %v-%v, err: %v", l.usn, l.platform, err)
}
}
// LocateGateway 定位网关
func (l *Locator) LocateGateway() int64 {
sid, _ := redis.GetClient().HGet(l.ctx, fmt.Sprintf(KeyGatewayInfo, l.usn), fmt.Sprintf(HFieldInfoGatewaySID, l.platform)).Int64()
return sid
}

112
utils/number.go Normal file
View File

@@ -0,0 +1,112 @@
package utils
import (
"github.com/shopspring/decimal"
"math/rand"
"strconv"
)
// RandInt 生成 [min, max] 范围内的随机整数
func RandInt(min, max int) int {
if min > max {
min, max = max, min
}
return rand.Intn(max-min+1) + min
}
// StringToInt64 converts a string to int64, returns 0 if failed
func StringToInt64(s string) int64 {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0
}
return i
}
// Int64ToString converts int64 to string
func Int64ToString(i int64) string {
return strconv.FormatInt(i, 10)
}
// AddFloat64 对浮点数求和
func AddFloat64(places int32, number ...float64) float64 {
if len(number) == 0 {
return 0
}
result := decimal.NewFromFloat(0)
for _, n := range number {
result = result.Add(decimal.NewFromFloat(n))
}
return result.Round(places).InexactFloat64()
}
// SubFloat64 对浮点数求差
func SubFloat64(places int32, number ...float64) float64 {
if len(number) == 0 {
return 0
}
result := decimal.NewFromFloat(number[0])
for i := 1; i < len(number); i++ {
result = result.Sub(decimal.NewFromFloat(number[i]))
}
return result.Round(places).InexactFloat64()
}
// MulFloat64 对浮点数求积
func MulFloat64(places int32, number ...float64) float64 {
if len(number) == 0 {
return 0
}
result := decimal.NewFromFloat(1)
for _, n := range number {
result = result.Mul(decimal.NewFromFloat(n))
}
return result.Round(places).InexactFloat64()
}
// DivFloat64 对浮点数求商
func DivFloat64(places int32, number ...float64) float64 {
if len(number) == 0 {
return 0
}
result := decimal.NewFromFloat(number[0])
for i := 1; i < len(number); i++ {
if number[i] == 0 {
return 0 // 避免除以零
}
result = result.Div(decimal.NewFromFloat(number[i]))
}
return result.Round(places).InexactFloat64()
}
// MaxInt32 返回两个 int32 中的较大值
func MaxInt32(a, b int32) int32 {
if a > b {
return a
}
return b
}
// MinInt32 返回两个 int32 中的较小值
func MinInt32(a, b int32) int32 {
if a < b {
return a
}
return b
}
// MaxFloat64 返回两个 float64 中的较大值
func MaxFloat64(a, b float64) float64 {
if a > b {
return a
}
return b
}
// MinFloat64 返回两个 float64 中的较小值
func MinFloat64(a, b float64) float64 {
if a < b {
return a
}
return b
}

62
utils/page.go Normal file
View File

@@ -0,0 +1,62 @@
package utils
import (
"math"
"git.hlsq.asia/alex/proto/proto/rs/rscommon"
"gorm.io/gen/field"
)
func PageGetOffSet(page *rscommon.PageArgs) int {
if page == nil || page.PageSize <= 0 {
return 0
}
return int((page.PageIndex - 1) * page.PageSize)
}
func CalcTotalPage(totalCount, pageSize int32) int32 {
if pageSize <= 0 {
return 0
}
return int32(math.Ceil(float64(totalCount) / float64(pageSize)))
}
func BuildPageInfoReply(page *rscommon.PageArgs, totalCount int32) *rscommon.PageInfoReply {
if page == nil {
page = &rscommon.PageArgs{PageIndex: 1, PageSize: 10}
}
if page.PageSize <= 0 {
page.PageSize = 10
}
if page.PageIndex <= 0 {
page.PageIndex = 1
}
if totalCount <= 0 {
totalCount = 0
return &rscommon.PageInfoReply{
PageIndex: page.PageIndex,
PageSize: page.PageSize,
TotalCount: totalCount,
}
}
return &rscommon.PageInfoReply{
PageIndex: page.PageIndex,
PageSize: page.PageSize,
TotalCount: totalCount,
}
}
func GetOrderInfo(orderList []*rscommon.OrderInfo, getField func(field string) field.OrderExpr) []field.Expr {
orders := make([]field.Expr, 0, len(orderList))
for _, order := range orderList {
expr := getField(order.Field)
if expr != nil {
if order.Asc {
orders = append(orders, expr.Asc())
} else {
orders = append(orders, expr.Desc())
}
}
}
return orders
}

33
utils/pool_object.go Normal file
View File

@@ -0,0 +1,33 @@
package utils
import (
"sync"
)
type ObjectPool[T any] struct {
pool sync.Pool
}
func NewObjectPool[T any]() *ObjectPool[T] {
return &ObjectPool[T]{
pool: sync.Pool{
New: func() any {
return new(T)
},
},
}
}
func (p *ObjectPool[T]) Get() *T {
return p.pool.Get().(*T)
}
func (p *ObjectPool[T]) Put(obj *T) {
*obj = zero[T]()
p.pool.Put(obj)
}
func zero[T any]() T {
var z T
return z
}

47
utils/pool_worker.go Normal file
View File

@@ -0,0 +1,47 @@
package utils
import (
"git.hlsq.asia/alex/service-common/log"
"sync"
)
// DefaultMaxWorkers 默认并发数
const DefaultMaxWorkers = 32
// WorkerPool 并发执行一批任务。
func WorkerPool(tasks []interface{}, taskFunc func(interface{}), maxWorkers ...int) {
if len(tasks) == 0 {
return
}
workers := DefaultMaxWorkers
if len(maxWorkers) > 0 && maxWorkers[0] > 0 {
workers = maxWorkers[0]
}
if workers > len(tasks) {
workers = len(tasks)
}
jobs := make(chan interface{}, len(tasks))
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
defer func() {
if e := recover(); e != nil {
log.Errorf("WorkerPool panic: %v", e)
}
}()
for task := range jobs {
taskFunc(task)
}
}()
}
for _, task := range tasks {
jobs <- task
}
close(jobs)
wg.Wait()
}

19
utils/qr.go Normal file
View File

@@ -0,0 +1,19 @@
package utils
import (
"encoding/base64"
"github.com/skip2/go-qrcode"
)
func GetQRCodeIO(content string, size ...int) string {
s := 260 // 写死尺寸 260*260
if len(size) > 0 {
s = size[0]
}
png, err := qrcode.Encode(content, qrcode.High, s)
if err != nil {
return ""
}
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
}

59
utils/replay.go Normal file
View File

@@ -0,0 +1,59 @@
package utils
import (
"context"
"fmt"
"git.hlsq.asia/alex/service-common/db/redis"
"git.hlsq.asia/alex/service-common/net/http/httpresp"
"google.golang.org/grpc/metadata"
"time"
)
const (
replayTTL = 2 * time.Minute // 防重放缓存有效期
lockTTL = 10 * time.Second // 分布式锁有效期
)
// WrapPreventReplay 防止重放请求
func WrapPreventReplay[T any](ctx context.Context, usn int64, fun func() (T, error)) (T, error) {
var zero T
key := ""
if md, ok := metadata.FromIncomingContext(ctx); ok {
ridArr := md.Get("Request-Id")
if len(ridArr) == 0 || ridArr[0] == "" {
return fun()
}
key = fmt.Sprintf("cache:replay:%v:%v", usn, ridArr[0])
} else {
return fun()
}
// 先查缓存,命中直接返回
if val := getReplayCache[T](ctx, key); val != nil {
return *val, nil
}
// 尝试抢分布式锁,抢到就执行,没抢到报错
lockKey := fmt.Sprintf("%s:lock", key)
locked, err := redis.GetClient().SetNX(ctx, lockKey, "1", lockTTL).Result()
if err == nil && locked {
defer redis.GetClient().Del(ctx, lockKey)
result, err := fun()
// 这里报错了是没有缓存的
if err != nil {
return zero, err
}
redis.GetCacheClient().Set(ctx, key, result, replayTTL)
return result, nil
}
return zero, httpresp.RequestTooFast
}
// getReplayCache 从 Redis 读取重放缓存
func getReplayCache[T any](ctx context.Context, key string) *T {
var dst T
if redis.GetCacheClient().Get(ctx, key, &dst) {
return &dst
}
return nil
}

69
utils/session.go Normal file
View File

@@ -0,0 +1,69 @@
package utils
import (
"context"
"google.golang.org/grpc/metadata"
"strconv"
"strings"
)
type UserSession struct {
USN int64 `json:"usn" redis:"usn"`
IP string `json:"ip" redis:"ip"`
UserAgent string `json:"ua" redis:"ua"`
AccessToken string `json:"at" redis:"at"`
RefreshToken string `json:"rt" redis:"rt"`
}
func (us *UserSession) GetUsnKey() string {
return "usn"
}
func (us *UserSession) GetAccessTokenKey() string {
return "at"
}
func (us *UserSession) GetRefreshTokenKey() string {
return "rt"
}
// Deprecated: 请使用 ShouldGetUsn 替代,示例:
// usn := utils.ShouldGetUsn(ctx)
// 不需要判断 usn 是否为 0,只有不鉴权接口的 usn 才可能是 0。
func ShouldBindUsn(ctx context.Context, usn *int64) bool {
if md, ok := metadata.FromIncomingContext(ctx); ok {
usnArr := md.Get("X-Usn")
if len(usnArr) == 0 || usnArr[0] == "" {
return false
}
*usn = StringToInt64(usnArr[0])
}
return *usn != 0
}
func ShouldGetUsn(ctx context.Context) int64 {
if md, ok := metadata.FromIncomingContext(ctx); ok {
usnArr := md.Get("X-Usn")
if len(usnArr) == 0 || usnArr[0] == "" {
return 0
}
return StringToInt64(usnArr[0])
}
return 0
}
func GetPlatformAndUsn(token string) (int32, int64, bool) {
str := strings.Split(token, ":")
if len(str) != 3 {
return 0, 0, false
}
platform, err := strconv.Atoi(str[0])
if err != nil {
return 0, 0, false
}
usn, err := strconv.Atoi(str[1])
if err != nil {
return 0, 0, false
}
return int32(platform), int64(usn), true
}

30
utils/snowflake.go Normal file
View File

@@ -0,0 +1,30 @@
package utils
import (
"github.com/sony/sonyflake/v2"
"log"
)
var instance *sonyflake.Sonyflake
func InitSnowflake(node int64) *sonyflake.Sonyflake {
n, err := sonyflake.New(sonyflake.Settings{
BitsMachineID: 10,
MachineID: func() (int, error) {
return int(node), nil
},
})
if err != nil {
log.Fatalf("create snowflake's node failed: %v", err)
}
instance = n
return instance
}
func SnowflakeInt64() int64 {
if instance == nil {
log.Fatal("snowflake's not init")
}
id, _ := instance.NextID()
return id
}

49
utils/string.go Normal file
View File

@@ -0,0 +1,49 @@
package utils
import (
"crypto/md5"
"encoding/hex"
"math/rand"
)
// GetMD5Encode 获取Md5编码
func GetMD5Encode(data string) string {
h := md5.New()
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
const (
StringChars1 = "0123456789"
StringChars2 = "abcdefghijklmnopqrstuvwxyz"
StringChars3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
StringChars4 = "0123456789abcdefghijklmnopqrstuvwxyz"
StringChars5 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
StringChars6 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
StringChars7 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
// RandomString 获取随机字符串
func RandomString(chars string, length int8) string {
result := make([]byte, length)
for i := int8(0); i < length; i++ {
result[i] = chars[rand.Intn(len(chars))]
if i == 0 && result[i] == '0' {
i--
}
}
return string(result)
}
// GetDisplayWidth 获取字符串长度
func GetDisplayWidth(text string) int {
width := 0
for _, r := range text {
if r <= 0x7F {
width += 1 // ASCII,半角
} else {
width += 2 // CJK、日文假名等,全角
}
}
return width
}

34
utils/test.go Normal file
View File

@@ -0,0 +1,34 @@
package utils
import (
"bytes"
"encoding/json"
"git.hlsq.asia/alex/service-common/net/http/httpresp"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
"net/http/httptest"
)
// 给测试用例提供一些通用函数
// CreateTestContext 创建测试用例的上下文
func CreateTestContext(method, path string, body interface{}) (*httptest.ResponseRecorder, *gin.Context) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
var buf bytes.Buffer
if body != nil {
_ = json.NewEncoder(&buf).Encode(body)
}
req := httptest.NewRequest(method, path, &buf)
req.Header.Set("Content-Type", "application/json")
c.Request = req
return w, c
}
// AssertResponse 断言返回值
func AssertResponse(ts *suite.Suite, w *httptest.ResponseRecorder, httpCode int, code *httpresp.Code) {
ts.Assert().Equal(httpCode, w.Code)
var response map[string]interface{}
ts.Assert().NoError(json.Unmarshal(w.Body.Bytes(), &response))
ts.Assert().Equal(float64(code.Code()), response["code"])
}

26
utils/time.go Normal file
View File

@@ -0,0 +1,26 @@
package utils
import (
"math"
"time"
)
// CalcDaysDiff 计算两个日期相差多少天
func CalcDaysDiff(one, two time.Time) int32 {
oneZero := time.Date(
one.Year(),
one.Month(),
one.Day(),
0, 0, 0, 0,
time.Local,
)
twoZero := time.Date(
two.Year(),
two.Month(),
two.Day(),
0, 0, 0, 0,
time.Local,
)
// 计算相差天数
return int32(math.Abs(oneZero.Sub(twoZero).Hours() / 24))
}