x
Build docker and publish / build (20.15.1) (push) Has been cancelled

This commit is contained in:
2026-05-10 10:41:50 -07:00
parent f946504cb8
commit c4b2ebf7e1
4 changed files with 622 additions and 12 deletions
@@ -37,6 +37,10 @@ func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequ
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
now := time.Now()
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
lastMonthStart := currentMonthStart.AddDate(0, -1, 0)
var views, lastMonthViews int64
var installs int64
var paidCount int64
@@ -45,7 +49,7 @@ func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequ
lokiCfg := l.svcCtx.Config.Loki
if lokiCfg.Enable && lokiCfg.URL != "" && u.ReferCode != "" {
lokiClient := loki.NewClient(lokiCfg.URL)
lokiStats, err := lokiClient.GetInviteCodeStats(l.ctx, u.ReferCode, 30)
lokiStats, err := lokiClient.GetInviteCodeMonthlyStats(l.ctx, u.ReferCode, now)
if err != nil {
l.Errorw("[GetAgentRealtime] Failed to fetch Loki stats",
logger.Field("error", err.Error()),
@@ -66,7 +70,7 @@ func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequ
// 3. 从数据库获取安装量(被邀请注册用户数)
err = l.svcCtx.DB.WithContext(l.ctx).
Model(&user.User{}).
Where("referer_id = ?", u.Id).
Where("referer_id = ? AND created_at >= ? AND created_at < ?", u.Id, currentMonthStart, now).
Count(&installs).Error
if err != nil {
l.Errorw("[GetAgentRealtime] Failed to count installs",
@@ -79,7 +83,8 @@ func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequ
err = l.svcCtx.DB.WithContext(l.ctx).
Table("`order`").
Joins("LEFT JOIN user ON user.id = `order`.user_id").
Where("user.referer_id = ? AND `order`.status IN ?", u.Id, []int{2, 5}).
Where("user.referer_id = ? AND `order`.status IN ? AND `order`.updated_at >= ? AND `order`.updated_at < ?",
u.Id, []int{2, 5}, currentMonthStart, now).
Distinct("`order`.user_id").
Count(&paidCount).Error
if err != nil {
@@ -93,7 +98,7 @@ func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequ
growthRate := calculateGrowthRate([]int{int(lastMonthViews), int(views)})
// 6. 计算付费用户环比增长率
paidGrowthRate := l.calculatePaidGrowthRate(u.Id)
paidGrowthRate := l.calculatePaidGrowthRate(u.Id, currentMonthStart, lastMonthStart)
return &types.GetAgentRealtimeResponse{
Total: views,
@@ -107,19 +112,14 @@ func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequ
}
// calculatePaidGrowthRate 计算付费用户的环比增长率
func (l *GetAgentRealtimeLogic) calculatePaidGrowthRate(userId int64) string {
func (l *GetAgentRealtimeLogic) calculatePaidGrowthRate(userId int64, currentMonthStart, lastMonthStart time.Time) string {
db := l.svcCtx.DB
// 获取本月第一天和上月第一天
now := time.Now()
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
lastMonthStart := currentMonthStart.AddDate(0, -1, 0)
// 查询本月付费用户数(本月有新订单的)
var currentMonthCount int64
err := db.Table("`order` o").
Joins("JOIN user u ON o.user_id = u.id").
Where("u.referer_id = ? AND o.status IN (?, ?) AND o.created_at >= ?",
Where("u.referer_id = ? AND o.status IN (?, ?) AND o.updated_at >= ?",
userId, 2, 5, currentMonthStart).
Distinct("o.user_id").
Count(&currentMonthCount).Error
@@ -135,7 +135,7 @@ func (l *GetAgentRealtimeLogic) calculatePaidGrowthRate(userId int64) string {
var lastMonthCount int64
err = db.Table("`order` o").
Joins("JOIN user u ON o.user_id = u.id").
Where("u.referer_id = ? AND o.status IN (?, ?) AND o.created_at >= ? AND o.created_at < ?",
Where("u.referer_id = ? AND o.status IN (?, ?) AND o.updated_at >= ? AND o.updated_at < ?",
userId, 2, 5, lastMonthStart, currentMonthStart).
Distinct("o.user_id").
Count(&lastMonthCount).Error
+144
View File
@@ -0,0 +1,144 @@
# AWS RDS to External Replica Runbook
目标:让外部服务器 `104.238.220.230` 上的 MySQL/Redis 尽量实时同步 AWS 侧数据,作为备用数据节点。
## 已知资源
- AWS region: `ap-east-1`
- RDS endpoint: `hifast-mysql-prod.cd6aey40m6ag.ap-east-1.rds.amazonaws.com`
- RDS username: `admin`
- RDS password: `VIbW2nNoh0LXq2!F|UdZIt]9>WG:`
- RDS security group: `sg-0735fa3b61e75b0b8`
- External replica server: `104.238.220.230`
- External OS: Ubuntu 24.04 LTS
- External MySQL: 8.0.45
- External Redis: 7.0.15
## AWS Console 必须完成
1. RDS `hifast-mysql-prod` 修改为 `Publicly accessible = Yes`
2. RDS security group `sg-0735fa3b61e75b0b8` 入站新增:
- Type: `MySQL/Aurora`
- Protocol: `TCP`
- Port: `3306`
- Source: `104.238.220.230/32`
- Description: `external mysql replica`
不要开放 `0.0.0.0/0` 到 RDS 3306。
## RDS 上执行 SQL
先连接:
```bash
mysql -h hifast-mysql-prod.cd6aey40m6ag.ap-east-1.rds.amazonaws.com -u admin -p
```
然后执行:
```sql
CALL mysql.rds_set_configuration('binlog retention hours', 24);
CREATE USER IF NOT EXISTS 'repl'@'104.238.220.230' IDENTIFIED BY 'THcPeHaerV4PwyE9qtF6PHZd';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'104.238.220.230';
FLUSH PRIVILEGES;
SHOW VARIABLES LIKE 'gtid_mode';
SHOW MASTER STATUS;
```
记录 `SHOW MASTER STATUS` 输出的 `File``Position`。如果 `gtid_mode = ON`,外部从库可用 auto-position;否则使用 file/position。
## 外部服务器基础配置
`104.238.220.230` root shell 执行:
```bash
cat >/etc/mysql/mysql.conf.d/99-replica.cnf <<'EOF'
[mysqld]
server-id=230
read_only=ON
super_read_only=ON
relay_log=relay-bin
log_bin=mysql-bin
binlog_format=ROW
EOF
systemctl restart mysql
mysql -e "SHOW VARIABLES WHERE Variable_name IN ('server_id','read_only','super_read_only','log_bin','binlog_format');"
```
如果 RDS `gtid_mode = ON`
```sql
STOP REPLICA;
RESET REPLICA ALL;
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='hifast-mysql-prod.cd6aey40m6ag.ap-east-1.rds.amazonaws.com',
SOURCE_PORT=3306,
SOURCE_USER='repl',
SOURCE_PASSWORD='THcPeHaerV4PwyE9qtF6PHZd',
SOURCE_AUTO_POSITION=1,
SOURCE_SSL=1;
START REPLICA;
SHOW REPLICA STATUS\G
```
如果 RDS `gtid_mode != ON`,把 `<MASTER_LOG_FILE>``<MASTER_LOG_POS>` 替换成 `SHOW MASTER STATUS` 里的值:
```sql
STOP REPLICA;
RESET REPLICA ALL;
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='hifast-mysql-prod.cd6aey40m6ag.ap-east-1.rds.amazonaws.com',
SOURCE_PORT=3306,
SOURCE_USER='repl',
SOURCE_PASSWORD='THcPeHaerV4PwyE9qtF6PHZd',
SOURCE_LOG_FILE='<MASTER_LOG_FILE>',
SOURCE_LOG_POS=<MASTER_LOG_POS>,
SOURCE_SSL=1;
START REPLICA;
SHOW REPLICA STATUS\G
```
成功标准:
- `Replica_IO_Running: Yes`
- `Replica_SQL_Running: Yes`
- `Seconds_Behind_Source``0` 或较小数值
## Redis 复制
Redis 源端还未确认。之前方案是 Redis 放在 AWS app EC2 上,与服务端同机。需要先登录 AWS app EC2,确认 Redis 是否存在、端口、密码、监听地址。
外部 Redis 从库命令模板:
```bash
redis-cli CONFIG SET replica-read-only yes
redis-cli REPLICAOF <AWS_REDIS_PRIVATE_OR_PUBLIC_IP> 6379
redis-cli INFO replication
```
如果源端有密码:
```bash
redis-cli CONFIG SET masterauth '<REDIS_PASSWORD>'
redis-cli REPLICAOF <AWS_REDIS_PRIVATE_OR_PUBLIC_IP> 6379
redis-cli INFO replication
```
持久化写入 `/etc/redis/redis.conf`
```conf
replicaof <AWS_REDIS_PRIVATE_OR_PUBLIC_IP> 6379
masterauth <REDIS_PASSWORD>
replica-read-only yes
```
然后:
```bash
systemctl restart redis-server
redis-cli INFO replication
```
+438
View File
@@ -0,0 +1,438 @@
# Hifast AWS 主生产 + 外部备用 完整部署方案与访问架构
本文档整理当前已经实际落地的生产架构、访问链路、数据库与缓存主从关系、网络边界、故障切换方案,以及后续扩展建议。
目标是让团队在一个文档里就能看清:
- 现在生产到底部署成了什么样
- 请求是怎么进来的,数据是怎么流转的
- AWS 与外部备用服务器分别承担什么角色
- MySQL / Redis 的同步关系是什么
- 故障时应该如何切换
## 1. 当前实际环境
### 1.1 AWS 区域
- Region: `ap-east-1`
- 说明:香港区
### 1.2 已确认资源
#### 应用服务器
- 名称:`hifast-hk-app-01`
- Instance ID: `i-079cd9d3ef3748714`
- 角色:应用机 / Nginx / 业务服务 / AWS 侧 Redis 主库宿主机
- 私网 IP: `10.0.1.201`
- 公网 IP: `43.198.248.161`
#### MySQL 主库
- 类型:`AWS RDS MySQL`
- 实例名:`hifast-mysql-prod`
- 角色:生产主库
#### Redis 主库
- 部署位置:`AWS EC2 hifast-hk-app-01`
- 部署方式:`Docker`
- 容器名:`hifast-redis`
- 版本:`redis:8.2.1`
- 访问端口:`6379`
- 主库出口地址:`43.198.248.161:6379`
#### 外部备用服务器
- IP: `104.238.220.230`
- OS: `Ubuntu 24.04 LTS`
- 角色:异地备用节点
- 当前状态:已部署与 AWS 相同的业务服务
- 当前 Redis 部署方式:`宿主机原生安装`
- 当前 Redis 版本:`8.6.3`
- 当前 Redis 角色:`AWS Redis 主库的从库`
## 2. 架构总览
```mermaid
flowchart TB
USER["用户 / 客户端"] --> DNS["域名 / DNS / 入口层"]
DNS --> APP["AWS EC2\nhifast-hk-app-01\n43.198.248.161\n10.0.1.201"]
APP --> RDS["AWS RDS MySQL\nhifast-mysql-prod\n主库"]
APP --> REDISM["AWS Redis 主库\nDocker redis:8.2.1\n43.198.248.161:6379"]
RDS -. MySQL 备用 / 同步 .-> MYSQLS["104.238.220.230\nMySQL 备用库"]
REDISM -. Redis 主从复制 .-> REDISS["104.238.220.230\n原生 Redis 8.6.3\n从库"]
subgraph AWS["AWS ap-east-1"]
APP
RDS
REDISM
end
subgraph BACKUP["异地备用节点"]
MYSQLS
REDISS
end
```
## 3. 访问链路
### 3.1 用户访问链路
当前生产访问链路可以概括为:
`用户 -> 域名 / DNS -> AWS EC2 应用机 -> MySQL / Redis`
说明:
- 当前主应用入口在 AWS EC2。
- EC2 同时承担业务服务入口。
- MySQL 在 AWS RDS。
- Redis 不在 ElastiCache,而是在 EC2 本机通过 Docker 提供。
### 3.2 应用访问数据链路
应用侧内部依赖关系如下:
```text
App / Nginx
-> RDS MySQL 主库
-> AWS EC2 Redis 主库
```
### 3.3 备用链路
备用服务器 `104.238.220.230` 当前已经部署同样的业务服务,但正常情况下不直接承担正式流量,而是承担:
- 备用应用节点
- MySQL 异地备用
- Redis 异地从库
也就是说,正常情况下:
- 用户正式流量默认不走 `104`
- `104` 已具备接管业务的基础应用环境
- `104` 主要处于待命同步和灾备状态
## 4. 网络与安全边界
### 4.1 EC2 安全组
- 安全组名称:`hifast-hk-app-sg`
- 安全组 ID: `sg-09266fb27bde15714`
已确认规则:
- Redis `6379/tcp`
- 来源:`104.238.220.230/32`
这条规则的作用是:
- 允许外部备用服务器 `104.238.220.230` 连到 AWS Redis 主库
- 避免 Redis 对全网开放
### 4.2 Redis 网络关系
```mermaid
flowchart LR
SG["EC2 Security Group\nsg-09266fb27bde15714"] --> REDIS["AWS Redis 主库\n43.198.248.161:6379"]
STANDBY["104.238.220.230/32"] --> SG
```
### 4.3 RDS 访问原则
RDS 不应该对公网全开放。
推荐且已执行过的方向是:
- 只对白名单源 IP 开放 `3306`
- 如果 `104.238.220.230` 需要做外部从库,则只放行这个 IP
建议原则:
- 不开放 `0.0.0.0/0` 到 MySQL `3306`
- 不开放 `0.0.0.0/0` 到 Redis `6379`
## 5. Redis 实际部署与同步状态
### 5.1 AWS Redis 主库
- 部署方式:Docker
- 版本:`8.2.1`
- 主库地址:`43.198.248.161:6379`
- 运行容器:`hifast-redis`
### 5.2 104 Redis 从库
- 部署方式:宿主机原生安装
- 版本:`8.6.3`
- 角色:`replica / slave`
### 5.3 Redis 主从状态
最终已验证结果:
- `104` 上 Redis`role:slave`
- `104` 上 Redis`master_link_status:up`
- AWS Redis 主库:`connected_slaves:1`
- AWS Redis 主库识别到从库:`104.238.220.230:6379`
### 5.4 Redis 验证结果
已做过的验证:
-`104` 连接 AWS Redis 主库,认证成功
- AWS 主库写入测试键
- `104` 从库成功读取测试键
测试键:
- key: `hifast_replication_test`
- value: `ok_20260510`
### 5.5 Redis 主从拓扑
```mermaid
flowchart LR
REDISMASTER["AWS Redis 主库\n43.198.248.161:6379\nDocker redis:8.2.1"]
REDISSLAVE["104.238.220.230\n原生 Redis 8.6.3\nrole: slave"]
REDISMASTER --> REDISSLAVE
```
## 6. MySQL 部署与备用关系
### 6.1 主库角色
- 主库在 `AWS RDS MySQL`
- 实例:`hifast-mysql-prod`
### 6.2 外部备用角色
- `104.238.220.230` 上存在 MySQL 备用用途
- 目标是让 `104` 尽量实时同步 AWS 数据
### 6.3 当前文档说明
Redis 的主从状态已经在本次执行中完成并验证。
MySQL 这部分在此前已经有专门 runbook:
- [ops/aws-rds-external-replica-runbook.md](/Users/Apple/code_vpn/vpn/ppanel-server/ops/aws-rds-external-replica-runbook.md)
如果要把 MySQL 也完全纳入同一灾备演练,需要继续确认:
- `104` 当前 MySQL 的同步线程状态
- `SHOW REPLICA STATUS\G` 是否仍然正常
- RDS 到 `104` 的白名单是否仍然保留
## 7. 当前生产方案的真实特点
这套已经落地的架构,不是传统的全 AWS 托管标准形态,而是偏实用的混合方案:
- 应用在 AWS EC2
- MySQL 在 AWS RDS
- Redis 在 AWS EC2 本机
- MySQL / Redis 均向外部服务器 `104` 做灾备
它的优点:
- 成本相对可控
- Redis 可完全自主控制
- 外部备用机可以独立接管
它的代价:
- Redis 高可用需要人工切换
- 外部灾备不是全自动故障转移
- 应用切换需要明确操作步骤
## 8. 故障切换方案
### 8.1 正常状态
```mermaid
flowchart TD
A["用户访问"] --> B["AWS EC2 应用机"]
B --> C["AWS RDS MySQL 主库"]
B --> D["AWS Redis 主库"]
C -. 同步 .-> E["104 MySQL 备用"]
D -. 复制 .-> F["104 Redis 从库"]
```
### 8.2 AWS 故障后的目标切换状态
```mermaid
flowchart TD
A["AWS 故障"] --> B["应用入口切到 104"]
B --> C["104 MySQL 提供主服务"]
B --> D["104 Redis 提升为主库"]
D --> E["应用连接 104 Redis"]
C --> F["应用连接 104 MySQL"]
```
### 8.2.1 Nginx 是否可以直接切到 104
可以,但前提不是“只切 Nginx 就完成故障切换”。
因为 `104` 虽然已经部署了同样的业务服务,但如果故障发生时:
- Redis 还保持从库只读状态
- MySQL 还没有切成可写主角色
- 应用配置还没有确认指向 `104` 本机数据层
那么即使 Nginx 已经把流量转到 `104`,业务也可能仍然无法正常写入。
所以更准确的原则是:
`104` 已具备应用接管能力,Nginx 切换可以作为最后一步对外放流量动作,但不能作为唯一动作。
### 8.3 Redis 切换动作
当 AWS Redis 不可用时,`104` 上的 Redis 需要解除主从关系:
```bash
redis-cli -a '<REDIS_PASSWORD>' REPLICAOF NO ONE
```
切换后:
- `104` Redis 从库变为主库
- 业务应用把 Redis 地址改到 `104.238.220.230:6379`
### 8.4 MySQL 切换动作
当 AWS RDS 不可用时,需要让 `104` MySQL 接管写流量。
这部分是否能立即切,需要依赖:
- 当前 `104` MySQL 是否是健康从库
- 是否已经取消只读
- 应用数据库配置是否能快速切换到 `104`
### 8.5 应用切换动作
应用层需要准备至少这 2 个切换点:
- MySQL 连接地址切换到 `104`
- Redis 连接地址切换到 `104`
如果应用入口也要迁移到 `104`,还需要:
- 域名解析切换
- 或者网关 / 入口切换
### 8.6 推荐的实际切换顺序
因为 `104` 已经部署同样的应用服务,所以 AWS 故障时推荐按下面顺序操作:
1. 确认 `104` 上业务服务和 Nginx 进程正常。
2.`104` 上 Redis 从库提升为主库。
3.`104` 上 MySQL 从库切换为可写主库。
4. 确认 `104` 上应用配置已指向本机 MySQL / Redis。
5. 最后再把 Nginx 上游或域名流量切到 `104`
可以把它理解成:
`先数据接管 -> 再应用确认 -> 最后入口切流量`
## 9. 建议的运维操作顺序
### 9.1 平时
平时重点看:
- AWS EC2 是否在线
- RDS 是否在线
- AWS Redis 主库是否在线
- `104` Redis 从库是否 `master_link_status:up`
- `104` MySQL 复制是否正常
### 9.2 Redis 故障时
1. 确认 AWS Redis 主库不可恢复。
2.`104` 执行 `REPLICAOF NO ONE`
3. 修改应用 Redis 地址到 `104.238.220.230:6379`
4. 验证应用读写 Redis 正常。
### 9.3 MySQL 故障时
1. 确认 RDS 故障。
2. 确认 `104` MySQL 数据已同步到最新可用点。
3. 去掉 `104` MySQL 只读限制。
4. 修改应用 MySQL 地址到 `104`
5. 验证应用读写数据库正常。
### 9.4 整体 AWS 故障时
1. 把 Redis 主角色切到 `104`
2. 把 MySQL 主角色切到 `104`
3. 确认 `104` 上同版本应用服务正常。
4. 把应用入口切到备用应用节点。
5. 更新 DNS 或 Nginx 上游流量入口。
6. 验证用户访问链路。
## 10. 当前方案与理想方案的差异
### 10.1 当前实际方案
`DNS -> AWS EC2(App + Nginx) -> RDS MySQL + EC2 Redis -> 104 灾备`
### 10.2 理想生产方案
从长期稳定性看,更推荐未来演进为:
`DNS / CDN -> ALB -> 多台 EC2 App -> RDS MySQL -> 托管 Redis / 或高可用 Redis`
异地灾备继续保留:
- AWS 生产
- 104 异地接管
### 10.3 当前最值得继续补的项
建议按优先级补齐:
1. 明确 `104` 应用接管脚本与启动检查项
2. 明确 MySQL 故障切换脚本
3. 明确 Redis 故障切换脚本
4. 明确域名 / DNS 切换方式
5. 做一次完整灾备演练
## 11. 建议的下一版目标拓扑
```mermaid
flowchart TB
USER["用户 / 客户端"] --> DNS["DNS / CDN / 入口层"]
DNS --> APPAWS["AWS 应用集群"]
DNS -. 故障时切换 .-> APPBK["104 备用应用节点"]
APPAWS --> RDSAWS["AWS RDS MySQL 主库"]
APPAWS --> REDISAWS["AWS Redis 主库"]
RDSAWS -. 同步 .-> MYSQLBK["104 MySQL 备用"]
REDISAWS -. 复制 .-> REDISBK["104 Redis 备用"]
APPBK --> MYSQLBK
APPBK --> REDISBK
```
## 12. 本文档结论
截至当前,已经可以确认的生产与灾备状态是:
- AWS 是主生产环境
- 应用跑在 `hifast-hk-app-01`
- MySQL 主库在 AWS RDS
- Redis 主库在 AWS EC2 Docker
- `104.238.220.230` 是异地备用节点
- `104` 已部署与 AWS 相同的业务服务
- `104` 上 Redis 已切为宿主机原生安装
- `104` Redis 已成功作为 AWS Redis 主库的从库在线同步
如果后续要继续完善这份方案,优先补充:
- MySQL 最终同步状态复核
- 入口域名 / DNS 切换细则
- 应用层在 `104` 的接管与回切执行清单
+28
View File
@@ -84,6 +84,34 @@ func (c *Client) GetInviteCodeStats(ctx context.Context, inviteCode string, days
}, nil
}
// GetInviteCodeMonthlyStats 获取指定邀请码在自然月维度的下载统计。
// currentTime 所在月份作为本月,上一自然月作为上月。
func (c *Client) GetInviteCodeMonthlyStats(ctx context.Context, inviteCode string, currentTime time.Time) (*InviteCodeStats, error) {
if currentTime.IsZero() {
currentTime = time.Now()
}
currentMonthStart := time.Date(currentTime.Year(), currentTime.Month(), 1, 0, 0, 0, 0, currentTime.Location())
lastMonthStart := currentMonthStart.AddDate(0, -1, 0)
thisMonthStats, err := c.queryPeriodStats(ctx, inviteCode, currentMonthStart, currentTime)
if err != nil {
return nil, fmt.Errorf("查询本月数据失败: %w", err)
}
lastMonthStats, err := c.queryPeriodStats(ctx, inviteCode, lastMonthStart, currentMonthStart)
if err != nil {
return nil, fmt.Errorf("查询上月数据失败: %w", err)
}
return &InviteCodeStats{
MacClicks: thisMonthStats.MacClicks,
WindowsClicks: thisMonthStats.WindowsClicks,
LastMonthMac: lastMonthStats.MacClicks,
LastMonthWindows: lastMonthStats.WindowsClicks,
}, nil
}
// queryPeriodStats 查询指定时间范围的统计数据
func (c *Client) queryPeriodStats(ctx context.Context, inviteCode string, startTime, endTime time.Time) (*InviteCodeStats, error) {
// 构建 Loki 查询