项目结构更新

This commit is contained in:
JACKYMYPERSON
2025-09-15 11:09:22 +08:00
parent 2ab7614ea0
commit 8ccf028ae4
25 changed files with 608 additions and 17 deletions

View File

@@ -0,0 +1,203 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"go.etcd.io/etcd/client/v3"
)
// ServiceInstance 服务实例信息,增加更多可视化所需字段
type ServiceInstance struct {
IP string `json:"ip"`
Port int `json:"port"`
Service string `json:"service"`
Timestamp string `json:"timestamp"`
Status string `json:"status"` // 服务状态running/stopped等
Metadata map[string]string `json:"metadata"` // 额外元数据,如服务版本等
}
// 初始化etcd客户端
func initEtcdClient(endpoints []string) (*clientv3.Client, error) {
client, err := clientv3.New(clientv3.Config{
Endpoints: endpoints,
DialTimeout: 5 * time.Second,
})
if err != nil {
return nil, fmt.Errorf("初始化etcd客户端失败: %v", err)
}
return client, nil
}
// 注册服务到etcd - 显式注册,优化路径便于可视化
func registerService(ctx context.Context, client *clientv3.Client, serviceName, ip string, port int, ttl int64) (clientv3.LeaseID, func(), error) {
// 优化服务在etcd中的存储键使用更清晰的层次结构
// 这种结构在可视化面板中会以目录树形式展示
serviceKey := fmt.Sprintf("/microservices/%s/instances/%s:%d", serviceName, ip, port)
// 准备服务实例信息,包含更多可视化所需的元数据
instance := ServiceInstance{
IP: ip,
Port: port,
Service: serviceName,
Timestamp: time.Now().Format(time.RFC3339),
Status: "running",
Metadata: map[string]string{
"version": "1.0.0",
"protocol": "http",
"endpoint": "/send",
"lastCheck": time.Now().Format(time.RFC3339),
},
}
instanceData, err := json.MarshalIndent(instance, "", " ") // 格式化JSON可视化更友好
if err != nil {
return 0, nil, fmt.Errorf("序列化服务实例信息失败: %v", err)
}
// 创建租约
leaseResp, err := client.Grant(ctx, ttl)
if err != nil {
return 0, nil, fmt.Errorf("创建etcd租约失败: %v", err)
}
leaseID := leaseResp.ID
// 将服务信息写入etcd
_, err = client.Put(ctx, serviceKey, string(instanceData), clientv3.WithLease(leaseID))
if err != nil {
return 0, nil, fmt.Errorf("写入服务信息到etcd失败: %v", err)
}
log.Printf("服务已显式注册到etcd, 服务键: %s, 租约ID: %d", serviceKey, leaseID)
// 启动心跳保活
keepAliveChan, err := client.KeepAlive(ctx, leaseID)
if err != nil {
return 0, nil, fmt.Errorf("启动心跳保活失败: %v", err)
}
// 处理心跳响应定期更新元数据中的lastCheck时间
go func() {
for resp := range keepAliveChan {
log.Printf("服务心跳保活成功, 租约续期至 %d 秒后", resp.TTL)
// 定期更新服务最后检查时间每3次心跳更新一次
if time.Now().Unix()%3 == 0 {
instance.Metadata["lastCheck"] = time.Now().Format(time.RFC3339)
updatedData, _ := json.MarshalIndent(instance, "", " ")
client.Put(ctx, serviceKey, string(updatedData), clientv3.WithLease(leaseID))
}
}
log.Println("心跳保活通道已关闭, 服务可能需要重新注册")
}()
// 服务注销函数
unregister := func() {
// 更新服务状态为stopped
instance.Status = "stopped"
instance.Metadata["lastCheck"] = time.Now().Format(time.RFC3339)
updatedData, _ := json.MarshalIndent(instance, "", " ")
client.Put(ctx, serviceKey, string(updatedData), clientv3.WithLease(leaseID))
// 短暂延迟,确保状态更新被可视化面板捕获
time.Sleep(500 * time.Millisecond)
// 撤销租约
_, err := client.Revoke(ctx, leaseID)
if err != nil {
log.Printf("撤销租约失败: %v", err)
return
}
// 主动删除服务信息
_, err = client.Delete(ctx, serviceKey)
if err != nil {
log.Printf("删除服务信息失败: %v", err)
return
}
log.Printf("服务已从etcd注销, 服务键: %s", serviceKey)
}
return leaseID, unregister, nil
}
// 获取本地IP地址
func getLocalIP() (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String(), nil
}
}
}
return "", fmt.Errorf("无法获取本地IP地址")
}
// 消息发送处理函数
func sendMessageHandler(w http.ResponseWriter, r *http.Request) {
// 输出"发送消息成功"
fmt.Println("发送消息成功")
w.WriteHeader(http.StatusOK)
w.Write([]byte("发送消息成功"))
}
func main() {
// 服务配置
serviceName := "email"
servicePort := 8080
etcdEndpoints := []string{"http://127.0.0.1:2379"}
leaseTTL := int64(15) // 15秒租约
// 获取本地IP
localIP, err := getLocalIP()
if err != nil {
log.Fatalf("获取本地IP失败: %v", err)
}
// 初始化etcd客户端
etcdClient, err := initEtcdClient(etcdEndpoints)
if err != nil {
log.Fatalf("初始化etcd客户端失败: %v", err)
}
defer etcdClient.Close()
// 显式注册服务到etcd
ctx := context.Background()
_, unregister, err := registerService(ctx, etcdClient, serviceName, localIP, servicePort, leaseTTL)
if err != nil {
log.Fatalf("服务注册失败: %v", err)
}
defer unregister() // 程序退出时注销服务
// 设置HTTP路由处理消息发送请求
http.HandleFunc("/send", sendMessageHandler)
// 启动HTTP服务
go func() {
addr := fmt.Sprintf(":%d", servicePort)
log.Printf("email微服务已启动监听地址: %s", addr)
if err := http.ListenAndServe(addr, nil); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务启动失败: %v", err)
}
}()
// 等待退出信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
log.Println("收到退出信号,正在关闭服务...")
}

View File

@@ -0,0 +1 @@
package grpc

View File

@@ -0,0 +1,176 @@
package main
import (
"context"
"encoding/json"
"fmt"
"gorm.io/gorm"
"log"
"os"
"os/signal"
"syscall"
"time"
"toutoukan/init/databaseInit"
"toutoukan/model/good"
"toutoukan/model/usermodel/userOrder"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/consumer"
"github.com/apache/rocketmq-client-go/v2/primitive"
)
const (
colorRed = "\033[31m" // 红色
colorGreen = "\033[32m" // 绿色
colorYellow = "\033[33m" // 黄色
colorBlue = "\033[34m" // 蓝色
colorReset = "\033[0m" // 重置颜色
)
// 与生产者对应的消息结构体
func main() {
databaseInit.DbInit()
defer func() {
sqlDB, err := databaseInit.UserDB.DB()
if err != nil {
// 处理获取失败的情况
log.Printf("获取底层数据库连接失败: %v", err)
return
}
// 2. 调用 Close() 关闭连接池
if err := sqlDB.Close(); err != nil {
log.Printf("关闭数据库连接池失败: %v", err)
} else {
log.Println("数据库连接池已成功关闭")
}
}()
// 初始化消费者
c, err := rocketmq.NewPushConsumer(
consumer.WithNameServer([]string{"127.0.0.1:9876"}), // Namesrv 地址
consumer.WithGroupName("order_consumer_group"), // 消费者组名
// 消费模式广播模式BROADCASTING或集群模式CLUSTERING默认
consumer.WithConsumeFromWhere(consumer.ConsumeFromFirstOffset), // 从最早消息开始消费
)
if err != nil {
log.Fatalf("初始化消费者失败: %v", err)
}
// 订阅主题并注册消息处理函数
err = c.Subscribe(
"killgoods", // 要消费的主题(需与生产者一致)
consumer.MessageSelector{}, // 消息过滤规则(空表示全部消费)
func(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
// 处理消息的回调函数(每次收到消息会触发)
for _, msg := range msgs {
// 解析消息体
var orderMsg userOrder.OrderMessage
if err := json.Unmarshal(msg.Body, &orderMsg); err != nil {
log.Printf("解析消息失败: %v, 消息内容: %s", err, string(msg.Body))
// 消息格式错误属于不可重试错误(重试也会失败),返回成功但记录错误
return consumer.ConsumeSuccess, nil
}
log.Printf(
"收到消息: ID=%s, 订单ID=%s, 商品ID=%d, 用户ID=%d, 数量=%d",
msg.MsgId, orderMsg.OrderID, orderMsg.GoodsID, orderMsg.UserID, orderMsg.Quantity,
)
db := databaseInit.UserDB
if db == nil {
log.Printf("%sUserDB 未初始化,请先调用 InitUserDB()%s\n", colorRed, colorReset)
// 数据库未初始化属于可恢复错误(重启服务后可能解决),返回重试
return consumer.ConsumeRetryLater, fmt.Errorf("UserDB 未初始化")
}
fmt.Printf("%s用户请求处理完成%s\n", colorBlue, colorReset)
var goods good.Goods
tx := db.Begin()
if tx.Error != nil {
log.Printf("%s开启事务失败: %v%s\n", colorRed, tx.Error, colorReset)
// 事务开启失败可能是临时错误(如连接池满),返回重试
return consumer.ConsumeRetryLater, tx.Error
}
// 加行锁查询商品
// 执行库存扣减
result := tx.Model(&good.Goods{}).
Where("gid = ? AND stock > 0", orderMsg.GoodsID).
Update("stock", gorm.Expr("stock - ?", 1))
if result.Error != nil {
tx.Rollback()
log.Printf("%s扣减库存失败: %v%s\n", colorRed, result.Error, colorReset)
return consumer.ConsumeRetryLater, result.Error
}
// 获取实际受影响的行数
affected := result.RowsAffected
// 4. 判断库存是否充足(受影响行数=0表示库存不足
if affected == 0 {
tx.Rollback()
log.Printf("%s商品库存不足订单ID: %s商品GID: %d%s\n", colorYellow, orderMsg.OrderID, orderMsg.GoodsID, colorReset)
return consumer.ConsumeSuccess, nil
}
// 生成订单
userorder := good.Order{
OrderId: orderMsg.OrderID,
TradeTime: time.Now(),
}
result1 := tx.Where("order_id = ?", orderMsg.OrderID).FirstOrCreate(&userorder)
if result1.Error != nil {
tx.Rollback()
log.Printf("%s创建/查询订单失败: %v%s\n", colorRed, result1.Error, colorReset)
return consumer.ConsumeRetryLater, result.Error
}
// 若订单已存在(重复消费),直接跳过创建逻辑,继续提交事务
if result1.RowsAffected == 0 {
log.Printf("%s订单已存在重复消费订单ID: %s%s\n", colorYellow, orderMsg.OrderID, colorReset)
}
if err := tx.Create(&userorder).Error; err != nil {
tx.Rollback()
log.Printf("%s创建订单失败: %v%s\n", colorRed, err, colorReset)
return consumer.ConsumeRetryLater, err
}
// 提交事务
if err := tx.Commit().Error; err != nil {
tx.Rollback()
log.Printf("%s提交事务失败: %v%s\n", colorRed, err, colorReset)
// 事务提交失败可能是临时错误,返回重试
return consumer.ConsumeRetryLater, err
}
log.Printf("%s订单创建成功订单ID: %s剩余库存: %d%s\n", colorGreen, userorder.OrderId, goods.Stock-1, colorReset)
}
// 所有消息处理成功,返回成功
return consumer.ConsumeSuccess, nil
},
)
if err != nil {
log.Fatalf("订阅主题失败: %v", err)
}
// 启动消费者(会阻塞当前 goroutine
if err := c.Start(); err != nil {
log.Fatalf("启动消费者失败: %v", err)
}
defer c.Shutdown() // 退出时关闭消费者
// 等待中断信号(如 Ctrl+C保证程序持续运行
log.Println("消费者已启动,持续等待消息...")
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // 阻塞直到收到中断信号
log.Println("消费者正在退出...")
}

View File

@@ -0,0 +1,64 @@
package mqproducer
import (
"context"
"encoding/json"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/primitive"
"github.com/apache/rocketmq-client-go/v2/producer"
"log"
"sync"
"toutoukan/model/usermodel/userOrder"
)
var normalProducer rocketmq.Producer
var producerInitOnce sync.Once // 用于保证生产者只初始化一次
// 发送普通消息
func SendNormalMessage(OrderId string, GoodsID string, UserID int) error {
var initErr error
// 使用 sync.Once 保证生产者只初始化一次
producerInitOnce.Do(func() {
normalProducer, initErr = rocketmq.NewProducer(
producer.WithNameServer([]string{"127.0.0.1:9876"}), // RocketMQ namesrv 地址
producer.WithGroupName("order_producer_group"), // 生产者组名
)
if initErr != nil {
log.Fatalf("初始化普通生产者失败: %v", initErr)
return
}
if initErr = normalProducer.Start(); initErr != nil {
log.Fatalf("启动普通生产者失败: %v", initErr)
return
}
})
// 若初始化过程有错误,直接返回
if initErr != nil {
log.Printf("生产者初始化失败: %v", initErr)
return initErr
}
// 构造消息内容
msgData := userOrder.OrderMessage{
OrderID: OrderId,
GoodsID: GoodsID,
UserID: UserID,
Quantity: 1,
}
msgBody, _ := json.Marshal(msgData)
// 创建消息(指定主题和内容)
msg := primitive.NewMessage("killgoods", msgBody) // 主题需提前创建或开启自动创建
// 发送消息(同步发送,等待结果)
result, err := normalProducer.SendSync(context.Background(), msg)
if err != nil {
log.Printf("普通消息发送失败: %v", err)
return err
}
log.Printf("普通消息发送成功消息ID: %s, 队列: %d, 偏移量: %d",
result.MsgID, result.MessageQueue.QueueId, result.QueueOffset)
return nil
}

View File

@@ -0,0 +1,5 @@
package orderService
func OrderService() {
}

View File

@@ -0,0 +1,12 @@
package main
type PayOrder struct {
}
func (P *PayOrder) GenerateOrderID() string {
return ""
}
func PayOrderService() {
}

View File

@@ -0,0 +1,69 @@
// client.go
package main
import (
"context"
"log"
"time"
"go-micro.dev/v4"
"go-micro.dev/v4/client"
"go-micro.dev/v4/registry"
"go.etcd.io/etcd/client/v3"
)
type LoginRequest struct {
Username string
Password string
}
type LoginResponse struct {
Token string
}
func main() {
// 1. 创建 etcd 客户端
etcdClient, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal(err)
}
defer etcdClient.Close()
// 2. 创建注册中心
reg := registry.NewRegistry(
registry.Addrs("127.0.0.1:2379"),
)
// 3. 创建微服务客户端
service := micro.NewService(
micro.Registry(reg),
)
service.Init()
// 4. 创建 RPC 客户端
userService := micro.NewService(
micro.Name("user.client"),
micro.Registry(reg),
)
userService.Init()
// 5. 调用远程服务
req := client.NewRequest(
"user.service", // 服务名
"UserService.Login", // 方法名
&LoginRequest{ // 请求参数
Username: "admin",
Password: "123456",
},
)
rsp := &LoginResponse{}
if err := userService.Client().Call(context.Background(), req, rsp); err != nil {
log.Fatal("调用失败:", err)
}
log.Printf("登录成功Token: %s", rsp.Token)
}

View File

@@ -0,0 +1,76 @@
// server.go
package main
import (
"context"
"go-micro.dev/v4/errors"
"log"
"time"
"go-micro.dev/v4"
"go-micro.dev/v4/registry"
"go.etcd.io/etcd/client/v3"
)
// 定义请求和响应结构体
type LoginRequest struct {
Username string
Password string
}
type LoginResponse struct {
Token string
}
// UserService 实现
type UserService struct{}
func (u *UserService) Login(ctx context.Context, req *LoginRequest, rsp *LoginResponse) error {
if req.Username == "admin" && req.Password == "123456" {
rsp.Token = "abc123"
return nil // 返回 nil 表示成功
}
return errors.New("登录失败", "失败", 500) // 返回 error 表示失败
}
func main() {
// 1. 创建原生 etcd 客户端
etcdClient, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
})
if err != nil {
log.Fatal("创建 etcd 客户端失败:", err)
}
defer etcdClient.Close()
// 2. 创建自定义的 etcd 注册中心
reg := registry.NewRegistry(
registry.Addrs("127.0.0.1:2379"),
registry.Timeout(5*time.Second),
// 这里可以添加更多自定义配置
)
// 3. 创建微服务实例
service := micro.NewService(
micro.Name("user.service"),
micro.Registry(reg),
// 注入 etcd 客户端
micro.BeforeStart(func() error {
return etcdClient.Sync(context.Background())
}),
)
// 4. 初始化服务
service.Init()
// 5. 注册服务处理器
if err := micro.RegisterHandler(service.Server(), new(UserService)); err != nil {
log.Fatal("注册服务处理器失败:", err)
}
// 6. 启动服务
if err := service.Run(); err != nil {
log.Fatal("服务启动失败:", err)
}
}