This commit is contained in:
XuanLee-HEALER
2025-11-10 15:30:21 +08:00
parent 4716b82f29
commit df39693dff
43 changed files with 6864 additions and 5 deletions

View File

@@ -0,0 +1,146 @@
# Message Migrator 数据迁移工具
这个工具用于将指定项目的消息数据从源数据库迁移到目标数据库。
## 功能特性
- ✅ 按项目ID迁移数据
- ✅ 支持可选的UID替换
- ✅ 自动处理表之间的ID关联映射
- ✅ 使用事务保证数据一致性
- ✅ 按正确的依赖顺序迁移:`m_stream_messages``m_stream_contents``m_context_messages``m_task_messages`
## 涉及的表
1. `m_stream_messages` - 流式消息记录表(主表)
2. `m_stream_contents` - 流式消息内容详情及分段表
3. `m_context_messages` - Agent上下文消息记录表
4. `m_task_messages` - 任务聚合消息记录表
## 安装依赖
```bash
go get github.com/go-sql-driver/mysql
```
## 配置
在使用前,请修改 `main.go` 中的数据库连接配置:
```go
const (
// 源数据库 DSN 格式: user:password@tcp(host:port)/database?parseTime=true
SOURCE_DSN = "user:password@tcp(localhost:3306)/goalfymax_prod?parseTime=true"
// 目标数据库 DSN
TARGET_DSN = "user:password@tcp(localhost:3306)/goalfymax_archive?parseTime=true"
)
```
## 使用方法
### 基本用法保持原始UID
```bash
go run main.go -project <project_id>
```
示例:
```bash
go run main.go -project 123
```
### 替换UID
```bash
go run main.go -project <project_id> -uid <new_uid>
```
示例:
```bash
go run main.go -project 123 -uid 456
```
### 自定义数据库连接
```bash
go run main.go -project 123 \
-source "root:pass@tcp(source-host:3306)/goalfymax_prod?parseTime=true" \
-target "root:pass@tcp(target-host:3306)/goalfymax_archive?parseTime=true"
```
### 编译后使用
```bash
# 编译
go build -o message-migrator
# 运行
./message-migrator -project 123
./message-migrator -project 123 -uid 456
```
## 参数说明
| 参数 | 必需 | 说明 | 示例 |
| ---------- | ---- | ------------------------------------------- | --------------------------------------- |
| `-project` | 是 | 要迁移的项目ID | `-project 123` |
| `-uid` | 否 | 新的用户ID不指定则使用原始UID | `-uid 456` |
| `-source` | 否 | 源数据库DSN不指定则使用代码中的默认值 | `-source "user:pass@tcp(host:3306)/db"` |
| `-target` | 否 | 目标数据库DSN不指定则使用代码中的默认值 | `-target "user:pass@tcp(host:3306)/db"` |
## 迁移流程
1. **连接数据库**:连接到源数据库和目标数据库
2. **开启事务**:在两个数据库上分别开启事务
3. **迁移主表**:迁移 `m_stream_messages`记录旧ID到新ID的映射
4. **迁移关联表**
- 迁移 `m_stream_contents`,使用映射更新 `main_message_id`
- 迁移 `m_context_messages`,使用映射更新 `main_message_id`
- 迁移 `m_task_messages`,使用映射更新 `main_message_id`
5. **提交事务**:所有数据迁移成功后提交事务
## 注意事项
⚠️ **重要提示**
1. **备份数据**:在执行迁移前,请务必备份源数据库和目标数据库
2. **目标表存在**:工具不会创建表,请确保目标数据库中已存在所有需要的表结构
3. **事务一致性**:迁移过程使用事务,如果中途失败会自动回滚
4. **ID映射**:工具会自动处理 `main_message_id` 的映射关系,确保关联正确
5. **UID替换**:如果指定了 `-uid` 参数,所有表中的 `uid` 字段都会被替换为新值
6. **重复执行**:请注意,重复执行相同的迁移命令可能会导致数据重复
## 日志输出
工具会输出详细的迁移日志:
```
2025/11/07 10:00:00 Starting migration for project_id=123
2025/11/07 10:00:00 Will replace UID with: 456
2025/11/07 10:00:01 Step 1: Migrating m_stream_messages...
2025/11/07 10:00:02 Migrated 150 stream messages
2025/11/07 10:00:02 Step 2: Migrating m_stream_contents...
2025/11/07 10:00:03 Migrated 300 stream contents
2025/11/07 10:00:03 Step 3: Migrating m_context_messages...
2025/11/07 10:00:04 Migrated 200 context messages
2025/11/07 10:00:04 Step 4: Migrating m_task_messages...
2025/11/07 10:00:05 Migrated 180 task messages
2025/11/07 10:00:05 Migration completed successfully!
```
## 错误处理
如果迁移过程中遇到错误:
- 事务会自动回滚,不会产生部分数据
- 错误信息会显示在日志中
- 检查数据库连接、权限和表结构是否正确
## 性能建议
对于大量数据的迁移:
1. 在非高峰期执行
2. 确保数据库有足够的临时空间
3. 可以考虑先迁移到测试环境验证
4. 监控数据库性能指标

View File

@@ -0,0 +1,569 @@
package main
import (
"database/sql"
"flag"
"fmt"
"log"
"os"
"time"
_ "github.com/go-sql-driver/mysql"
)
// 配置常量 - 请根据实际情况修改
const (
// 源数据库 DSN 格式: user:password@tcp(host:port)/database?parseTime=true
SOURCE_DSN = "goalfymax_prod:X6cQDaOLOifFBOMq@tcp(goalfyagent-aurora-mysql-staging.cb2sq6y2mg93.us-west-2.rds.amazonaws.com:3306)/goalfymax_prod?charset=utf8mb4&parseTime=True&loc=UTC"
// 目标数据库 DSN
TARGET_DSN = "root:123456@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
)
type MigrationConfig struct {
ProjectID uint64
NewUID *uint64 // 如果为 nil则使用原始 UID
SourceDB *sql.DB
TargetDB *sql.DB
}
// StreamMessage 对应 m_stream_messages 表
type StreamMessage struct {
ID uint64
UID uint64
ProjectID uint64
SenderID sql.NullString
SenderAgentType sql.NullString
Type string
MessageTimestamp string
AgentGenMessageID sql.NullString
Content string
FormattedContent string
TaskID sql.NullString
TaskStatus sql.NullString
TurnID sql.NullInt32
AgentMessageID sql.NullString
Source string
Metadata sql.NullString
ReceivedAt string
CreatedAt string
Show int
}
func main() {
// 命令行参数
projectID := flag.Uint64("project", 0, "Project ID to migrate (required)")
newUID := flag.Uint64("uid", 0, "New UID for migrated data (optional, use 0 to keep original)")
sourceDSN := flag.String("source", SOURCE_DSN, "Source database DSN")
targetDSN := flag.String("target", TARGET_DSN, "Target database DSN")
flag.Parse()
if *projectID == 0 {
fmt.Println("Usage: go run main.go -project <project_id> [-uid <new_uid>] [-source <dsn>] [-target <dsn>]")
fmt.Println("\nExample:")
fmt.Println(" go run main.go -project 123")
fmt.Println(" go run main.go -project 123 -uid 456")
os.Exit(1)
}
// 连接数据库
sourceDB, err := sql.Open("mysql", *sourceDSN)
if err != nil {
log.Fatalf("Failed to connect to source database: %v", err)
}
defer sourceDB.Close()
targetDB, err := sql.Open("mysql", *targetDSN)
if err != nil {
log.Fatalf("Failed to connect to target database: %v", err)
}
defer targetDB.Close()
// 测试连接
if err := sourceDB.Ping(); err != nil {
log.Fatalf("Failed to ping source database: %v", err)
}
if err := targetDB.Ping(); err != nil {
log.Fatalf("Failed to ping target database: %v", err)
}
config := &MigrationConfig{
ProjectID: *projectID,
SourceDB: sourceDB,
TargetDB: targetDB,
}
if *newUID != 0 {
config.NewUID = newUID
}
log.Printf("Starting migration for project_id=%d", *projectID)
if config.NewUID != nil {
log.Printf("Will replace UID with: %d", *config.NewUID)
}
// 执行迁移
if err := migrateProject(config); err != nil {
log.Fatalf("Migration failed: %v", err)
}
log.Println("Migration completed successfully!")
}
func migrateProject(config *MigrationConfig) error {
// 开启事务
txSource, err := config.SourceDB.Begin()
if err != nil {
return fmt.Errorf("failed to begin source transaction: %w", err)
}
defer txSource.Rollback()
txTarget, err := config.TargetDB.Begin()
if err != nil {
return fmt.Errorf("failed to begin target transaction: %w", err)
}
defer txTarget.Rollback()
// 步骤0: 清除目标库中已存在的该项目数据
log.Println("Step 0: Cleaning existing data in target database...")
if err := cleanExistingData(txTarget, config); err != nil {
return fmt.Errorf("failed to clean existing data: %w", err)
}
log.Println(" Existing data cleaned")
// 步骤1: 迁移 m_stream_messages (主表)
log.Println("Step 1: Migrating m_stream_messages...")
oldToNewStreamMsgID, err := migrateStreamMessages(txSource, txTarget, config)
if err != nil {
return fmt.Errorf("failed to migrate stream messages: %w", err)
}
log.Printf(" Migrated %d stream messages", len(oldToNewStreamMsgID))
// 步骤2: 迁移 m_stream_contents (依赖 main_message_id)
log.Println("Step 2: Migrating m_stream_contents...")
count, err := migrateStreamContents(txSource, txTarget, config, oldToNewStreamMsgID)
if err != nil {
return fmt.Errorf("failed to migrate stream contents: %w", err)
}
log.Printf(" Migrated %d stream contents", count)
// 步骤3: 迁移 m_context_messages (依赖 main_message_id)
log.Println("Step 3: Migrating m_context_messages...")
count, err = migrateContextMessages(txSource, txTarget, config, oldToNewStreamMsgID)
if err != nil {
return fmt.Errorf("failed to migrate context messages: %w", err)
}
log.Printf(" Migrated %d context messages", count)
// 步骤4: 迁移 m_task_messages (依赖 main_message_id)
log.Println("Step 4: Migrating m_task_messages...")
count, err = migrateTaskMessages(txSource, txTarget, config, oldToNewStreamMsgID)
if err != nil {
return fmt.Errorf("failed to migrate task messages: %w", err)
}
log.Printf(" Migrated %d task messages", count)
// 提交事务
if err := txTarget.Commit(); err != nil {
return fmt.Errorf("failed to commit target transaction: %w", err)
}
if err := txSource.Commit(); err != nil {
return fmt.Errorf("failed to commit source transaction: %w", err)
}
return nil
}
// cleanExistingData 清除目标库中已存在的该项目数据
// 按照依赖关系逆序删除:先删除依赖表,最后删除主表
func cleanExistingData(txTarget *sql.Tx, config *MigrationConfig) error {
// 首先获取该项目在目标库中的所有 stream_message IDs
var streamMessageIDs []uint64
query := `SELECT id FROM m_stream_messages WHERE project_id = ?`
rows, err := txTarget.Query(query, config.ProjectID)
if err != nil {
return fmt.Errorf("failed to query existing stream messages: %w", err)
}
defer rows.Close()
for rows.Next() {
var id uint64
if err := rows.Scan(&id); err != nil {
return err
}
streamMessageIDs = append(streamMessageIDs, id)
}
if err := rows.Err(); err != nil {
return err
}
if len(streamMessageIDs) == 0 {
log.Println(" No existing data found for this project in target database")
return nil
}
log.Printf(" Found %d existing stream messages to clean", len(streamMessageIDs))
// 步骤1: 删除 m_task_messages (依赖 main_message_id)
result, err := txTarget.Exec(`DELETE FROM m_task_messages WHERE project_id = ?`, config.ProjectID)
if err != nil {
return fmt.Errorf("failed to delete task messages: %w", err)
}
if affected, _ := result.RowsAffected(); affected > 0 {
log.Printf(" Deleted %d task messages", affected)
}
// 步骤2: 删除 m_context_messages (依赖 main_message_id)
result, err = txTarget.Exec(`DELETE FROM m_context_messages WHERE project_id = ?`, config.ProjectID)
if err != nil {
return fmt.Errorf("failed to delete context messages: %w", err)
}
if affected, _ := result.RowsAffected(); affected > 0 {
log.Printf(" Deleted %d context messages", affected)
}
// 步骤3: 删除 m_stream_contents (依赖 main_message_id)
// 需要通过 main_message_id IN (...) 来删除
if len(streamMessageIDs) > 0 {
// 构建 IN 子句
query := `DELETE FROM m_stream_contents WHERE main_message_id IN (`
args := make([]interface{}, len(streamMessageIDs))
for i, id := range streamMessageIDs {
if i > 0 {
query += ","
}
query += "?"
args[i] = id
}
query += ")"
result, err = txTarget.Exec(query, args...)
if err != nil {
return fmt.Errorf("failed to delete stream contents: %w", err)
}
if affected, _ := result.RowsAffected(); affected > 0 {
log.Printf(" Deleted %d stream contents", affected)
}
}
// 步骤4: 删除 m_stream_messages (主表)
result, err = txTarget.Exec(`DELETE FROM m_stream_messages WHERE project_id = ?`, config.ProjectID)
if err != nil {
return fmt.Errorf("failed to delete stream messages: %w", err)
}
if affected, _ := result.RowsAffected(); affected > 0 {
log.Printf(" Deleted %d stream messages", affected)
}
return nil
}
// formatTimestamp 将时间字符串转换为 MySQL datetime 格式
// 输入可能是 "2025-11-07T02:37:23Z" 或已经是 "2025-11-07 02:37:23" 格式
func formatTimestamp(ts string) string {
// 尝试解析 ISO 8601 格式
if t, err := time.Parse(time.RFC3339, ts); err == nil {
return t.Format("2006-01-02 15:04:05")
}
// 尝试解析带纳秒的 ISO 8601 格式
if t, err := time.Parse(time.RFC3339Nano, ts); err == nil {
return t.Format("2006-01-02 15:04:05")
}
// 如果已经是正确格式或解析失败,返回原值
return ts
}
// migrateStreamMessages 迁移主消息表返回旧ID到新ID的映射
func migrateStreamMessages(txSource, txTarget *sql.Tx, config *MigrationConfig) (map[uint64]uint64, error) {
query := `SELECT id, uid, project_id, sender_id, sender_agent_type, type,
message_timestamp, agent_gen_message_id, content, formatted_content,
task_id, task_status, turn_id, agent_message_id, source, metadata,
received_at, created_at, ` + "`show`" + `
FROM m_stream_messages WHERE project_id = ?`
rows, err := txSource.Query(query, config.ProjectID)
if err != nil {
return nil, err
}
defer rows.Close()
oldToNewID := make(map[uint64]uint64)
insertQuery := `INSERT INTO m_stream_messages
(uid, project_id, sender_id, sender_agent_type, type, message_timestamp,
agent_gen_message_id, content, formatted_content, task_id, task_status,
turn_id, agent_message_id, source, metadata, received_at, created_at, ` + "`show`" + `)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
for rows.Next() {
var msg StreamMessage
err := rows.Scan(&msg.ID, &msg.UID, &msg.ProjectID, &msg.SenderID, &msg.SenderAgentType,
&msg.Type, &msg.MessageTimestamp, &msg.AgentGenMessageID, &msg.Content,
&msg.FormattedContent, &msg.TaskID, &msg.TaskStatus, &msg.TurnID,
&msg.AgentMessageID, &msg.Source, &msg.Metadata, &msg.ReceivedAt,
&msg.CreatedAt, &msg.Show)
if err != nil {
return nil, err
}
// 如果指定了新UID替换
uid := msg.UID
if config.NewUID != nil {
uid = *config.NewUID
}
// 转换时间戳格式
messageTimestamp := formatTimestamp(msg.MessageTimestamp)
receivedAt := formatTimestamp(msg.ReceivedAt)
createdAt := formatTimestamp(msg.CreatedAt)
result, err := txTarget.Exec(insertQuery,
uid, msg.ProjectID, msg.SenderID, msg.SenderAgentType, msg.Type,
messageTimestamp, msg.AgentGenMessageID, msg.Content,
msg.FormattedContent, msg.TaskID, msg.TaskStatus, msg.TurnID,
msg.AgentMessageID, msg.Source, msg.Metadata, receivedAt,
createdAt, msg.Show)
if err != nil {
return nil, err
}
newID, err := result.LastInsertId()
if err != nil {
return nil, err
}
oldToNewID[msg.ID] = uint64(newID)
}
return oldToNewID, rows.Err()
}
// migrateStreamContents 迁移流式内容表
func migrateStreamContents(txSource, txTarget *sql.Tx, config *MigrationConfig, idMap map[uint64]uint64) (int, error) {
query := `SELECT id, main_message_id, agent_gen_message_id, type, content,
formatted_content, metadata, message_timestamp, ` + "`show`" + `, received_at, created_at
FROM m_stream_contents
WHERE main_message_id IN (
SELECT id FROM m_stream_messages WHERE project_id = ?
)`
rows, err := txSource.Query(query, config.ProjectID)
if err != nil {
return 0, err
}
defer rows.Close()
insertQuery := `INSERT INTO m_stream_contents
(main_message_id, agent_gen_message_id, type, content, formatted_content,
metadata, message_timestamp, ` + "`show`" + `, received_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
count := 0
for rows.Next() {
var (
id uint64
mainMessageID uint64
agentGenMessageID sql.NullString
msgType string
content string
formattedContent string
metadata sql.NullString
messageTimestamp string
show int
receivedAt string
createdAt string
)
err := rows.Scan(&id, &mainMessageID, &agentGenMessageID, &msgType, &content,
&formattedContent, &metadata, &messageTimestamp, &show, &receivedAt, &createdAt)
if err != nil {
return count, err
}
// 映射新的 main_message_id
newMainMessageID, ok := idMap[mainMessageID]
if !ok {
log.Printf("Warning: main_message_id %d not found in mapping, skipping content id %d", mainMessageID, id)
continue
}
// 转换时间戳格式
messageTimestamp = formatTimestamp(messageTimestamp)
receivedAt = formatTimestamp(receivedAt)
createdAt = formatTimestamp(createdAt)
_, err = txTarget.Exec(insertQuery, newMainMessageID, agentGenMessageID, msgType,
content, formattedContent, metadata, messageTimestamp, show, receivedAt, createdAt)
if err != nil {
return count, err
}
count++
}
return count, rows.Err()
}
// migrateContextMessages 迁移上下文消息表
func migrateContextMessages(txSource, txTarget *sql.Tx, config *MigrationConfig, idMap map[uint64]uint64) (int, error) {
query := `SELECT id, main_message_id, ownership, uid, project_id, turn_id,
agent_message_id, sender_id, sender_agent_type, type, source, content,
formatted_content, metadata, message_timestamp, agent_gen_message_id,
` + "`show`" + `, received_at, created_at
FROM m_context_messages WHERE project_id = ?`
rows, err := txSource.Query(query, config.ProjectID)
if err != nil {
return 0, err
}
defer rows.Close()
insertQuery := `INSERT INTO m_context_messages
(main_message_id, ownership, uid, project_id, turn_id, agent_message_id,
sender_id, sender_agent_type, type, source, content, formatted_content,
metadata, message_timestamp, agent_gen_message_id, ` + "`show`" + `, received_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
count := 0
for rows.Next() {
var (
id uint64
mainMessageID uint64
ownership string
uid uint64
projectID uint64
turnID sql.NullInt32
agentMessageID sql.NullString
senderID sql.NullString
senderAgentType sql.NullString
msgType string
source string
content sql.NullString
formattedContent sql.NullString
metadata sql.NullString
messageTimestamp string
agentGenMessageID sql.NullString
show int
receivedAt string
createdAt string
)
err := rows.Scan(&id, &mainMessageID, &ownership, &uid, &projectID, &turnID,
&agentMessageID, &senderID, &senderAgentType, &msgType, &source, &content,
&formattedContent, &metadata, &messageTimestamp, &agentGenMessageID,
&show, &receivedAt, &createdAt)
if err != nil {
return count, err
}
// 映射新的 main_message_id
newMainMessageID, ok := idMap[mainMessageID]
if !ok {
log.Printf("Warning: main_message_id %d not found in mapping, skipping context message id %d", mainMessageID, id)
continue
}
// 如果指定了新UID替换
targetUID := uid
if config.NewUID != nil {
targetUID = *config.NewUID
}
// 转换时间戳格式
messageTimestamp = formatTimestamp(messageTimestamp)
receivedAt = formatTimestamp(receivedAt)
createdAt = formatTimestamp(createdAt)
_, err = txTarget.Exec(insertQuery, newMainMessageID, ownership, targetUID, projectID,
turnID, agentMessageID, senderID, senderAgentType, msgType, source, content,
formattedContent, metadata, messageTimestamp, agentGenMessageID, show,
receivedAt, createdAt)
if err != nil {
return count, err
}
count++
}
return count, rows.Err()
}
// migrateTaskMessages 迁移任务消息表
func migrateTaskMessages(txSource, txTarget *sql.Tx, config *MigrationConfig, idMap map[uint64]uint64) (int, error) {
query := `SELECT id, main_message_id, uid, project_id, sender_id, sender_agent_type,
type, message_timestamp, agent_gen_message_id, content, formatted_content,
task_status, turn_id, agent_message_id, source, metadata, ` + "`show`" + `,
received_at, created_at
FROM m_task_messages WHERE project_id = ?`
rows, err := txSource.Query(query, config.ProjectID)
if err != nil {
return 0, err
}
defer rows.Close()
insertQuery := `INSERT INTO m_task_messages
(main_message_id, uid, project_id, sender_id, sender_agent_type, type,
message_timestamp, agent_gen_message_id, content, formatted_content,
task_status, turn_id, agent_message_id, source, metadata, ` + "`show`" + `,
received_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
count := 0
for rows.Next() {
var (
id uint64
mainMessageID uint64
uid uint64
projectID uint64
senderID sql.NullString
senderAgentType sql.NullString
msgType string
messageTimestamp string
agentGenMessageID sql.NullString
content string
formattedContent string
taskStatus sql.NullString
turnID sql.NullInt32
agentMessageID sql.NullString
source sql.NullString
metadata sql.NullString
show int
receivedAt string
createdAt string
)
err := rows.Scan(&id, &mainMessageID, &uid, &projectID, &senderID, &senderAgentType,
&msgType, &messageTimestamp, &agentGenMessageID, &content, &formattedContent,
&taskStatus, &turnID, &agentMessageID, &source, &metadata, &show,
&receivedAt, &createdAt)
if err != nil {
return count, err
}
// 映射新的 main_message_id
newMainMessageID, ok := idMap[mainMessageID]
if !ok {
log.Printf("Warning: main_message_id %d not found in mapping, skipping task message id %d", mainMessageID, id)
continue
}
// 如果指定了新UID替换
targetUID := uid
if config.NewUID != nil {
targetUID = *config.NewUID
}
// 转换时间戳格式
messageTimestamp = formatTimestamp(messageTimestamp)
receivedAt = formatTimestamp(receivedAt)
createdAt = formatTimestamp(createdAt)
_, err = txTarget.Exec(insertQuery, newMainMessageID, targetUID, projectID, senderID,
senderAgentType, msgType, messageTimestamp, agentGenMessageID, content,
formattedContent, taskStatus, turnID, agentMessageID, source, metadata,
show, receivedAt, createdAt)
if err != nil {
return count, err
}
count++
}
return count, rows.Err()
}

Binary file not shown.

124
cmd/redis-pub-sub/main.go Normal file
View File

@@ -0,0 +1,124 @@
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"time"
"github.com/redis/go-redis/v9"
)
// 定义 Redis 配置和频道名称
const redisAddr = "localhost:6379" // 替换成您的 Redis 地址
const channelName = "stream:10"
// 全局 Redis 客户端
var rdb *redis.Client
// 定义命令行参数
var mode = flag.String("mode", "", "运行模式:必须是 'pub' (发布者) 或 'sub' (订阅者)")
// 初始化 Redis 客户端并检查连接
func initRedis() {
rdb = redis.NewClient(&redis.Options{
Addr: redisAddr,
Password: "", // 您的密码(如果没有则为空)
DB: 7, // 默认 DB
})
ctx := context.Background()
_, err := rdb.Ping(ctx).Result()
if err != nil {
log.Fatalf("无法连接到 Redis (%s): %v", redisAddr, err)
}
fmt.Println("成功连接到 Redis。")
}
// 订阅者逻辑
func runSubscriber() {
fmt.Println("--- 启动订阅者 (Subscriber) 模式 ---")
// 订阅频道
// Subscribes to the given channels.
pubsub := rdb.Subscribe(context.Background(), channelName)
defer pubsub.Close() // 确保在函数退出时关闭连接
// 接收并处理消息
fmt.Printf("已订阅频道: %s。等待消息...\n", channelName)
// 使用 Channel() 方法获取一个 Go Channel 来接收消息
ch := pubsub.Channel()
// 循环接收消息
for msg := range ch {
fmt.Printf("\n--- 收到消息 ---\n")
fmt.Printf("频道 (Channel): %s\n", msg.Channel)
fmt.Printf("消息 (Payload): %s\n", msg.Payload)
fmt.Printf("----------------\n")
// 添加退出逻辑
if msg.Payload == "exit" {
fmt.Println("\n收到 'exit' 消息,退出订阅循环。")
break
}
}
fmt.Println("订阅者程序退出。")
}
// 发布者逻辑
func runPublisher() {
fmt.Println("--- 启动发布者 (Publisher) 模式 ---")
messages := []string{
"Hello Redis Pub/Sub!",
"This is message two from publisher.",
"A third message is here.",
"exit", // 发送退出信号给订阅者
}
ctx := context.Background()
// 循环发布消息
for i, msg := range messages {
// Publish publishes message to channel.
// Result() 返回值是接收到消息的订阅者数量 (int64)
result, err := rdb.Publish(ctx, channelName, msg).Result()
if err != nil {
log.Fatalf("发布消息失败: %v", err)
}
fmt.Printf("发布消息 %d: '%s' 到频道 '%s',有 %d 个订阅者接收。\n",
i+1, msg, channelName, result)
// 每次发送后等待一小段时间
time.Sleep(500 * time.Millisecond)
}
fmt.Println("发布者程序完成。")
}
func main() {
// 解析命令行参数
flag.Parse()
// 检查模式参数
if *mode != "pub" && *mode != "sub" {
fmt.Println("错误:请指定有效的运行模式。")
flag.Usage()
os.Exit(1)
}
// 初始化 Redis
initRedis()
// 根据模式执行相应逻辑
if *mode == "sub" {
runSubscriber()
} else if *mode == "pub" {
runPublisher()
}
}

49
cmd/xb/main.go Normal file
View File

@@ -0,0 +1,49 @@
package main
import (
"fmt"
"log"
"time"
)
func TzStrToOffset(ianaZone string) (string, error) {
loc, err := time.LoadLocation(ianaZone)
if err != nil {
return "", fmt.Errorf("无法加载时区 %s: %v", ianaZone, err)
}
t := time.Date(2025, time.October, 28, 0, 0, 0, 0, loc)
_, offsetSeconds := t.Zone()
offset := time.Duration(offsetSeconds) * time.Second
// 格式化偏移量。time.Time.Format() 无法直接将 Location 格式化成 UTC+HH:MM
// 因此我们需要手动计算并格式化。
sign := "+"
if offsetSeconds < 0 {
sign = "-"
offset = -offset // 确保偏移量为正值进行计算
}
hours := int(offset.Hours())
minutes := int(offset.Minutes()) % 60
// 最终的格式化字符串
utcOffsetString := fmt.Sprintf("%s%02d%02d", sign, hours, minutes)
return utcOffsetString, nil
}
func main() {
ianaZone := "Asia/Shanghai"
ianaZone = "UTC"
utcOffsetString, err := TzStrToOffset(ianaZone)
if err != nil {
log.Fatalf("错误: %v", err)
}
fmt.Printf("IANA 时区: %s\n", ianaZone)
fmt.Printf("UTC 偏移量: %s\n", utcOffsetString)
}