21 Commits

Author SHA1 Message Date
7fe415de63 新增本地构建脚本 2026-02-27 16:44:31 +08:00
8e42fad8aa 修复修改密码时未验证手机号所属账号的问题 2026-02-27 16:28:28 +08:00
7a3c47f1d4 修复导致首次登录时注册失败的问题 2026-02-26 14:47:36 +08:00
dfbb3a9acc 管理员用户查询接口提供管理员详细信息 2026-01-09 18:52:48 +08:00
19fa8b381c 扩展令牌内省函数以支持多账号池 2026-01-09 17:22:02 +08:00
b7a9682552 实现管理员认领功能 2026-01-06 15:33:55 +08:00
f638baec64 实现基本管理员数据访问接口 2025-12-30 19:24:03 +08:00
1262a8dae4 重构认证授权模块,统一到 auth 包下 2025-12-29 10:18:01 +08:00
bf8f001a30 新增代理注册接口
Some checks failed
Docker / build (push) Has been cancelled
2025-12-22 17:31:31 +08:00
eac793becb 优化 sql 脚本,排除无关填充流程
Some checks failed
Docker / build (push) Has been cancelled
2025-12-20 18:47:21 +08:00
7bdbb7ddff 优化 Dockerfile
Some checks failed
Docker / build (push) Has been cancelled
2025-12-20 15:15:54 +08:00
c8fd4cf9ca 添加 otel 相关环境变量,优化部署配置
Some checks failed
Docker / build (push) Has been cancelled
2025-12-19 17:26:59 +08:00
2b190bd4e5 实现批次检查接口 & 修复白银接口 url 二次编码问题 2025-12-19 10:59:04 +08:00
8f2e71849f 实现用户咨询数据收集接口 2025-12-18 14:22:56 +08:00
0207720943 优化 model json 格式化注释 2025-12-15 14:48:30 +08:00
05fba68b3e 重构优化套餐数据结构,修复提取计数问题 2025-12-10 20:09:20 +08:00
c8c86081d9 修复删除空批次时的数组越界问题 2025-12-09 19:28:49 +08:00
983dbb4564 重构白银节点分配方式,使用手动接口精确配置节点 2025-12-08 17:59:40 +08:00
9e237be21e 修复通道注册失败问题 2025-12-05 18:57:52 +08:00
5649a03c47 优化通道关闭流程,只靠 batch id 索引通道 2025-12-05 18:31:30 +08:00
4a2dcabf58 优化全局 id 生成效率 2025-12-05 17:30:34 +08:00
70 changed files with 2795 additions and 1714 deletions

View File

@@ -2,7 +2,7 @@ name: Docker
on: on:
push: push:
branches: [ "main" ] branches: ["v*"]
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
@@ -10,14 +10,12 @@ env:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write packages: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4

16
.vscode/launch.json vendored
View File

@@ -1,16 +0,0 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "main",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/cmd/main",
"cwd": "${workspaceFolder}"
}
]
}

13
.zed/debug.json Normal file
View File

@@ -0,0 +1,13 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "debug main",
"adapter": "Delve",
"request": "launch",
"mode": "debug",
"program": "./cmd/main"
}
]

View File

@@ -1,35 +1,31 @@
# 第一阶段:构建 # 第一阶段:构建
FROM golang:1.24.0 AS builder FROM golang:1.25.3 AS builder
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /build WORKDIR /build
# 复制Go模块文件 ENV GOPROXY=https://goproxy.cn,direct
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
# 复制源代码
COPY . . COPY . .
RUN go build -ldflags '-w -s' -o bin/platform_linux_amd64 cmd/main/main.go
# 编译
RUN GOOS=linux GOARCH=amd64 go build -ldflags '-w -s' -o bin/platform_linux_amd64 cmd/main/main.go
# 第二阶段:运行环境 # 第二阶段:运行环境
FROM ubuntu:24.04 AS runner FROM alpine:3.23 AS runner
WORKDIR /app WORKDIR /app
RUN apt-get update && apt-get install -y ca-certificates ENV TZ=Asia/Shanghai
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache ca-certificates tzdata
# 从构建阶段复制编译好的二进制文件
COPY --from=builder /build/bin/platform_linux_amd64 /app/platform COPY --from=builder /build/bin/platform_linux_amd64 /app/platform
# 设置可执行权限
RUN chmod +x /app/platform RUN chmod +x /app/platform
# 声明暴露端口
EXPOSE 8080 EXPOSE 8080
# 启动平台服务
CMD ["/app/platform"] CMD ["/app/platform"]

View File

@@ -1,26 +1,20 @@
## TODO ## TODO
trade/create 性能问题,缩短事务时间,考虑其他方式实现可靠分布式事务 前端需要 token 化改造,以避免每次 basic 认证流程中 bcrypt 对比导致的性能对比
需要确认以下 ID.GenSerial 的分布式并发安全性 优化中间件,配置通用限速
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具 observe 部署,蓝狐部署
端口资源池的 gc 实现 ---
标准化生产环境 cors 配置 用反射实现环境变量解析,以简化函数签名
底层调用集成 otel
- redis
- gorm
- 三方接口
分离 task 的客户端支持多进程prefork 必要!) 分离 task 的客户端支持多进程prefork 必要!)
调整目录结构: 调整目录结构:
``` ```
- /core 核心概念
- /util 工具函数 - /util 工具函数
- /models 模型 - /models 模型
@@ -30,7 +24,7 @@ jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
- /services 服务层 - /services 服务层
- /auth 认证相关,特化服务 - /auth 认证相关,特化服务
- /app 应用相关,初始化日志,环境变量等 - /app 应用相关,初始化日志,环境变量,错误类型
- /http 协议层http 服务 - /http 协议层http 服务
- /cmd 主函数 - /cmd 主函数
@@ -39,12 +33,20 @@ cmd 调用 app, http 的初始化函数
http 调用 clients 的初始化函数 http 调用 clients 的初始化函数
``` ```
考虑一个方案限制接口请求速率,无侵入更好 ---
数据库转模型文件
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
慢速请求底层调用埋点监控
- redis
- gorm
- 三方接口
冷数据迁移方案 冷数据迁移方案
proxy 网关更新接口可以传输更结构化的数据,直接区分不同类型以加快更新速度
## 业务逻辑 ## 业务逻辑
### 订单关闭的几种方式 ### 订单关闭的几种方式
@@ -63,22 +65,20 @@ proxy 网关更新接口可以传输更结构化的数据,直接区分不同
### 节点分配与存储逻辑 ### 节点分配与存储逻辑
添加 提取
- 检查用户 ip 是否在白名单内
- 取用端口,不够则返回失败
- 将分配结果转写成配置发送到网关
- 保存通道信息和分配记录,其中通道信息以网关为主体,分配记录以用户为主体
- 添加异步任务,当时间结束后释放取用的端口并清空网关配置
删除: - 检查用户套餐与白名单
- 如果传入用户信息,检查要删除的连接是否属于该用户 - 选中代理
- 释放可用端口 - 找到当前可用端口最多的代理
- redis 脚本中检查,如果端口所属节点已下线则直接忽略 - 不考虑分割端口,不够加机器
- 提交清空配置到网关 - 获取可用端口
- 获取可用节点
- 生成批次号,提交到期释放任务
- 绑定节点与端口,保存到数据库
- 分别提交连接与配置请求
缩扩容 释放
- 通过调度任务实现缩扩容
- 每分钟检查一次全部配置,按代理分组 - 根据批次查出所有端口与相关节点
- 获取所有代理后备配置 - 分别提交断开与关闭请求
- 后备配置/当前配置 - 释放端口
- 当比例 < 1.5 或 > 3 时,重新更新为 2 倍

View File

@@ -61,6 +61,7 @@ func main() {
m.User{}, m.User{},
m.UserRole{}, m.UserRole{},
m.Whitelist{}, m.Whitelist{},
m.Inquiry{},
) )
g.Execute() g.Execute()
} }

View File

@@ -1,8 +1,8 @@
name: lanhu name: lanhu-platform
services: services:
postgres: postgres:
image: postgres:17 image: postgres:17.7
environment: environment:
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}

10
pkg/env/env.go vendored
View File

@@ -36,7 +36,10 @@ var (
RedisPort = "6379" RedisPort = "6379"
RedisPassword = "" RedisPassword = ""
BaiyinAddr = "http://103.139.212.110:9989" OtelHost string
OtelPort string
BaiyinCloudUrl string
BaiyinTokenUrl string BaiyinTokenUrl string
IdenCallbackUrl string IdenCallbackUrl string
@@ -115,7 +118,10 @@ func Init() {
errs = append(errs, parse(&RedisPort, "REDIS_PORT", true, nil)) errs = append(errs, parse(&RedisPort, "REDIS_PORT", true, nil))
errs = append(errs, parse(&RedisPassword, "REDIS_PASS", true, nil)) errs = append(errs, parse(&RedisPassword, "REDIS_PASS", true, nil))
errs = append(errs, parse(&BaiyinAddr, "BAIYIN_ADDR", true, nil)) errs = append(errs, parse(&OtelHost, "OTEL_HOST", true, nil))
errs = append(errs, parse(&OtelPort, "OTEL_PORT", true, nil))
errs = append(errs, parse(&BaiyinCloudUrl, "BAIYIN_CLOUD_URL", false, nil))
errs = append(errs, parse(&BaiyinTokenUrl, "BAIYIN_TOKEN_URL", false, nil)) errs = append(errs, parse(&BaiyinTokenUrl, "BAIYIN_TOKEN_URL", false, nil))
errs = append(errs, parse(&IdenCallbackUrl, "IDEN_CALLBACK_URL", false, nil)) errs = append(errs, parse(&IdenCallbackUrl, "IDEN_CALLBACK_URL", false, nil))

View File

@@ -25,6 +25,15 @@ func ElseTo[A any, B any](a *A, f func(A) B) *B {
} }
} }
// 三元表达式
func Ternary[T any](condition bool, trueValue T, falseValue T) T {
if condition {
return trueValue
} else {
return falseValue
}
}
// ==================== // ====================
// 指针 // 指针
// ==================== // ====================

16
publish.ps1 Normal file
View File

@@ -0,0 +1,16 @@
if (-not $args) {
Write-Error "需要指定版本号"
exit 1
}
$confrim = Read-Host "构建版本为 [platform:$($args[0])],是否继续?(y/n)"
if ($confrim -ne "y") {
Write-Host "已取消构建"
exit 0
}
docker build -t 43.226.58.254:53000/lanhu/platform:latest .
docker build -t 43.226.58.254:53000/lanhu/platform:$($args[0]) .
docker push 43.226.58.254:53000/lanhu/platform:latest
docker push 43.226.58.254:53000/lanhu/platform:$($args[0])

12
scripts/sql/fill.sql Normal file
View File

@@ -0,0 +1,12 @@
-- ====================
-- region 填充数据
-- ====================
insert into client (
client_id, client_secret, redirect_uri, spec, name, type
)
values (
'web', '$2a$10$Ss12mXQgpYyo1CKIZ3URouDm.Lc2KcYJzsvEK2PTIXlv6fHQht45a', '', 3, 'web', 1
);
-- endregion

View File

@@ -108,6 +108,76 @@ comment on column logs_user_bandwidth.time is '记录时间';
-- endregion -- endregion
-- ====================
-- region 系统信息
-- ====================
-- announcement
drop table if exists announcement cascade;
create table announcement (
id int generated by default as identity primary key,
title text not null,
content text,
type int not null default 1,
pin bool not null default false,
status int not null default 1,
sort int not null default 0,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
);
create index idx_announcement_type on announcement (type) where deleted_at is null;
create index idx_announcement_pin on announcement (pin) where deleted_at is null;
create index idx_announcement_created_at on announcement (created_at) where deleted_at is null;
-- announcement表字段注释
comment on table announcement is '公告表';
comment on column announcement.id is '公告ID';
comment on column announcement.title is '公告标题';
comment on column announcement.content is '公告内容';
comment on column announcement.type is '公告类型1-普通公告';
comment on column announcement.status is '公告状态0-禁用1-正常';
comment on column announcement.pin is '是否置顶';
comment on column announcement.sort is '公告排序';
comment on column announcement.created_at is '创建时间';
comment on column announcement.updated_at is '更新时间';
comment on column announcement.deleted_at is '删除时间';
-- inquiry
drop table if exists inquiry cascade;
create table inquiry (
id int generated by default as identity primary key,
company text,
name text,
phone text,
email text,
content text,
status int not null default 0,
remark text,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
);
create index idx_inquiry_phone on inquiry (phone) where deleted_at is null;
create index idx_inquiry_status on inquiry (status) where deleted_at is null;
create index idx_inquiry_created_at on inquiry (created_at) where deleted_at is null;
-- inquiry表字段注释
comment on table inquiry is '用户咨询表';
comment on column inquiry.id is '咨询ID';
comment on column inquiry.name is '联系人姓名';
comment on column inquiry.phone is '联系电话';
comment on column inquiry.email is '联系邮箱';
comment on column inquiry.company is '公司名称';
comment on column inquiry.content is '咨询内容';
comment on column inquiry.status is '处理状态0-待处理1-已处理';
comment on column inquiry.remark is '备注';
comment on column inquiry.created_at is '创建时间';
comment on column inquiry.updated_at is '更新时间';
comment on column inquiry.deleted_at is '删除时间';
-- endregion
-- ==================== -- ====================
-- region 管理员信息 -- region 管理员信息
-- ==================== -- ====================
@@ -177,37 +247,6 @@ comment on column admin_role.created_at is '创建时间';
comment on column admin_role.updated_at is '更新时间'; comment on column admin_role.updated_at is '更新时间';
comment on column admin_role.deleted_at is '删除时间'; comment on column admin_role.deleted_at is '删除时间';
-- announcement
drop table if exists announcement cascade;
create table announcement (
id int generated by default as identity primary key,
title text not null,
content text,
type int not null default 1,
pin bool not null default false,
status int not null default 1,
sort int not null default 0,
created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp,
deleted_at timestamptz
);
create index idx_announcement_type on announcement (type) where deleted_at is null;
create index idx_announcement_pin on announcement (pin) where deleted_at is null;
create index idx_announcement_created_at on announcement (created_at) where deleted_at is null;
-- announcement表字段注释
comment on table announcement is '公告表';
comment on column announcement.id is '公告ID';
comment on column announcement.title is '公告标题';
comment on column announcement.content is '公告内容';
comment on column announcement.type is '公告类型1-普通公告';
comment on column announcement.status is '公告状态0-禁用1-正常';
comment on column announcement.pin is '是否置顶';
comment on column announcement.sort is '公告排序';
comment on column announcement.created_at is '创建时间';
comment on column announcement.updated_at is '更新时间';
comment on column announcement.deleted_at is '删除时间';
-- endregion -- endregion
-- ==================== -- ====================
@@ -500,10 +539,11 @@ create table proxy (
version int not null, version int not null,
mac text not null, mac text not null,
ip inet not null, ip inet not null,
host text,
secret text, secret text,
type int not null, type int not null,
status int not null, status int not null,
meta jsonb not null, meta jsonb,
created_at timestamptz default current_timestamp, created_at timestamptz default current_timestamp,
updated_at timestamptz default current_timestamp, updated_at timestamptz default current_timestamp,
deleted_at timestamptz deleted_at timestamptz
@@ -518,8 +558,9 @@ comment on column proxy.id is '代理服务ID';
comment on column proxy.version is '代理服务版本'; comment on column proxy.version is '代理服务版本';
comment on column proxy.mac is '代理服务名称'; comment on column proxy.mac is '代理服务名称';
comment on column proxy.ip is '代理服务地址'; comment on column proxy.ip is '代理服务地址';
comment on column proxy.type is '代理服务类型1-自有2-白银'; comment on column proxy.host is '代理服务域名';
comment on column proxy.secret is '代理服务密钥'; comment on column proxy.secret is '代理服务密钥';
comment on column proxy.type is '代理服务类型1-自有2-白银';
comment on column proxy.status is '代理服务状态0-离线1-在线'; comment on column proxy.status is '代理服务状态0-离线1-在线';
comment on column proxy.meta is '代理服务元信息'; comment on column proxy.meta is '代理服务元信息';
comment on column proxy.created_at is '创建时间'; comment on column proxy.created_at is '创建时间';
@@ -600,8 +641,10 @@ create table channel (
resource_id int not null, resource_id int not null,
batch_no text not null, batch_no text not null,
proxy_id int not null, proxy_id int not null,
host text not null,
port int not null, port int not null,
edge_id int, edge_id int,
edge_ref text,
filter_isp int, filter_isp int,
filter_prov text, filter_prov text,
filter_city text, filter_city text,
@@ -626,8 +669,10 @@ comment on column channel.user_id is '用户ID';
comment on column channel.resource_id is '套餐ID'; comment on column channel.resource_id is '套餐ID';
comment on column channel.batch_no is '批次编号'; comment on column channel.batch_no is '批次编号';
comment on column channel.proxy_id is '代理ID'; comment on column channel.proxy_id is '代理ID';
comment on column channel.host is '代理主机(快照)';
comment on column channel.port is '代理端口'; comment on column channel.port is '代理端口';
comment on column channel.edge_id is '节点ID手动配置'; comment on column channel.edge_id is '节点ID手动配置';
comment on column channel.edge_ref is '外部节点引用用于索引没有ID的外部非受控节点';
comment on column channel.filter_isp is '运营商过滤(自动配置):参考 edge.isp'; comment on column channel.filter_isp is '运营商过滤(自动配置):参考 edge.isp';
comment on column channel.filter_prov is '省份过滤(自动配置)'; comment on column channel.filter_prov is '省份过滤(自动配置)';
comment on column channel.filter_city is '城市过滤(自动配置)'; comment on column channel.filter_city is '城市过滤(自动配置)';
@@ -707,14 +752,13 @@ drop table if exists resource_short cascade;
create table resource_short ( create table resource_short (
id int generated by default as identity primary key, id int generated by default as identity primary key,
resource_id int not null, resource_id int not null,
type int not null,
live int not null, live int not null,
expire timestamptz, type int not null,
quota int, quota int not null,
expire_at timestamptz,
used int not null default 0, used int not null default 0,
daily_limit int not null default 0, daily int not null default 0,
daily_used int not null default 0, last_at timestamptz
daily_last timestamptz
); );
create index idx_resource_short_resource_id on resource_short (resource_id); create index idx_resource_short_resource_id on resource_short (resource_id);
@@ -722,28 +766,26 @@ create index idx_resource_short_resource_id on resource_short (resource_id);
comment on table resource_short is '短效动态套餐表'; comment on table resource_short is '短效动态套餐表';
comment on column resource_short.id is 'ID'; comment on column resource_short.id is 'ID';
comment on column resource_short.resource_id is '套餐ID'; comment on column resource_short.resource_id is '套餐ID';
comment on column resource_short.type is '套餐类型1-包时2-包量';
comment on column resource_short.live is '可用时长(秒)'; comment on column resource_short.live is '可用时长(秒)';
comment on column resource_short.quota is '配额数'; comment on column resource_short.type is '套餐类型1-包时2-包';
comment on column resource_short.used is '已用数量'; comment on column resource_short.quota is '每日配额(包时)或总配额(包量)';
comment on column resource_short.expire is '过期时间'; comment on column resource_short.expire_at is '套餐过期时间,包时模式可用';
comment on column resource_short.daily_limit is '每日限制'; comment on column resource_short.used is '总用量';
comment on column resource_short.daily_used is '今日已用数'; comment on column resource_short.daily is '当日用';
comment on column resource_short.daily_last is '今日最后使用时间'; comment on column resource_short.last_at is '最后使用时间';
-- resource_long -- resource_long
drop table if exists resource_long cascade; drop table if exists resource_long cascade;
create table resource_long ( create table resource_long (
id int generated by default as identity primary key, id int generated by default as identity primary key,
resource_id int not null, resource_id int not null,
type int not null,
live int not null, live int not null,
expire timestamptz, type int not null,
quota int, quota int not null,
expire_at timestamptz,
used int not null default 0, used int not null default 0,
daily_limit int not null default 0, daily int not null default 0,
daily_used int not null default 0, last_at timestamptz
daily_last timestamptz
); );
create index idx_resource_long_resource_id on resource_long (resource_id); create index idx_resource_long_resource_id on resource_long (resource_id);
@@ -751,14 +793,13 @@ create index idx_resource_long_resource_id on resource_long (resource_id);
comment on table resource_long is '长效动态套餐表'; comment on table resource_long is '长效动态套餐表';
comment on column resource_long.id is 'ID'; comment on column resource_long.id is 'ID';
comment on column resource_long.resource_id is '套餐ID'; comment on column resource_long.resource_id is '套餐ID';
comment on column resource_long.live is '可用时长(小时)';
comment on column resource_long.type is '套餐类型1-包时2-包量'; comment on column resource_long.type is '套餐类型1-包时2-包量';
comment on column resource_long.live is '可用时长(天)'; comment on column resource_long.quota is '每日配额(包时)或总配额(包量)';
comment on column resource_long.quota is '配额数量'; comment on column resource_long.expire_at is '套餐过期时间,包时模式可用';
comment on column resource_long.used is '已用数'; comment on column resource_long.used is '总用';
comment on column resource_long.expire is '过期时间'; comment on column resource_long.daily is '当日用量';
comment on column resource_long.daily_limit is '每日限制'; comment on column resource_long.last_at is '最后使用时间';
comment on column resource_long.daily_used is '今日已用数量';
comment on column resource_long.daily_last is '今日最后使用时间';
-- endregion -- endregion
@@ -1018,16 +1059,3 @@ alter table coupon
add constraint fk_coupon_user_id foreign key (user_id) references "user" (id) on delete cascade; add constraint fk_coupon_user_id foreign key (user_id) references "user" (id) on delete cascade;
-- endregion -- endregion
-- ====================
-- region 填充数据
-- ====================
insert into client (
client_id, client_secret, redirect_uri, spec, name, type
)
values (
'web', '$2a$10$Ss12mXQgpYyo1CKIZ3URouDm.Lc2KcYJzsvEK2PTIXlv6fHQht45a', '', 3, 'web', 1
);
-- endregion

156
web/auth/account.go Normal file
View File

@@ -0,0 +1,156 @@
package auth
import (
"context"
"errors"
"log/slog"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
"golang.org/x/crypto/bcrypt"
)
func authClient(clientId string, clientSecrets ...string) (*m.Client, error) {
// 获取客户端信息
client, err := q.Client.
Where(
q.Client.ClientID.Eq(clientId),
q.Client.Status.Eq(1)).
Take()
if err != nil {
return nil, err
}
// 检查客户端密钥
if client.Spec == m.ClientSpecWeb || client.Spec == m.ClientSpecAPI {
if len(clientSecrets) == 0 {
return nil, errors.New("客户端密钥错误")
}
clientSecret := clientSecrets[0]
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
return nil, errors.New("客户端密钥错误")
}
}
// todo 查询客户端关联权限
// 组织授权信息(一次性请求)
return client, nil
}
func authUser(loginType PwdLoginType, username, password string) (user *m.User, err error) {
switch loginType {
case PwdLoginByPhone:
user, err = authUserBySms(q.Q, username, password)
if err != nil {
return nil, err
}
if user == nil {
user = &m.User{
Phone: username,
Username: u.P(username),
Status: m.UserStatusEnabled,
}
}
case PwdLoginByEmail:
user, err = authUserByEmail(q.Q, username, password)
if err != nil {
return nil, err
}
case PwdLoginByPassword:
user, err = authUserByPassword(q.Q, username, password)
if err != nil {
return nil, err
}
default:
return nil, ErrAuthorizeInvalidRequest
}
// 账户状态
if user.Status == m.UserStatusDisabled {
return nil, core.NewBizErr("账号已禁用")
}
return user, nil
}
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
// 验证验证码
err := s.Verifier.VerifySms(context.Background(), username, code)
if err != nil {
return nil, core.NewBizErr("短信认证失败", err)
}
// 查找用户
return tx.User.Where(tx.User.Phone.Eq(username)).Take()
}
func authUserByEmail(tx *q.Query, username, code string) (*m.User, error) {
return nil, core.NewServErr("邮箱登录不可用")
}
func authUserByPassword(tx *q.Query, username, password string) (*m.User, error) {
user, err := tx.User.
Where(tx.User.Phone.Eq(username)).
Or(tx.User.Email.Eq(username)).
Or(tx.User.Username.Eq(username)).
Take()
if err != nil {
slog.Debug("查找用户失败", "error", err)
return nil, core.NewBizErr("用户不存在或密码错误")
}
// 验证密码
if user.Password == nil || *user.Password == "" {
slog.Debug("用户未设置密码", "username", username)
return nil, core.NewBizErr("用户不存在或密码错误")
}
if bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)) != nil {
slog.Debug("密码验证失败", "username", username)
return nil, core.NewBizErr("用户不存在或密码错误")
}
return user, nil
}
func authAdmin(loginType PwdLoginType, username, password string) (admin *m.Admin, err error) {
switch loginType {
case PwdLoginByPhone, PwdLoginByEmail:
return nil, core.NewServErr("不支持的登录方式:" + string(loginType))
case PwdLoginByPassword:
admin, err = authAdminByPassword(q.Q, username, password)
if err != nil {
return nil, err
}
default:
return nil, ErrAuthorizeInvalidRequest
}
// 账户状态
if admin.Status == m.AdminStatusDisabled {
return nil, core.NewBizErr("账号已禁用")
}
return admin, nil
}
func authAdminByPassword(tx *q.Query, username, password string) (*m.Admin, error) {
admin, err := tx.Admin.Where(tx.Admin.Username.Eq(username)).Take()
if err != nil {
return nil, core.NewBizErr("账号不存在或密码错误")
}
// 验证密码
if admin.Password == "" {
return nil, core.NewBizErr("账号不存在或密码错误")
}
if bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(password)) != nil {
return nil, core.NewBizErr("账号不存在或密码错误")
}
return admin, nil
}

View File

@@ -6,10 +6,8 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"log/slog"
"platform/pkg/env" "platform/pkg/env"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core"
g "platform/web/globals" g "platform/web/globals"
"platform/web/globals/orm" "platform/web/globals/orm"
m "platform/web/models" m "platform/web/models"
@@ -22,67 +20,52 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type GrantType string // AuthorizeGet 授权端点
func AuthorizeGet(ctx *fiber.Ctx) error {
const ( // 检查请求
GrantAuthorizationCode = GrantType("authorization_code") // 授权码模式 req := new(AuthorizeGetReq)
GrantClientCredentials = GrantType("client_credentials") // 客户端凭证模式 if err := g.Validator.ParseQuery(ctx, req); err != nil {
GrantRefreshToken = GrantType("refresh_token") // 刷新令牌模式 return err
GrantPassword = GrantType("password") // 密码模式(私有扩展) }
)
type PasswordGrantType string // 检查客户端
client, err := authClient(req.ClientID)
if err != nil {
return err
}
const ( if client.RedirectURI == nil || *client.RedirectURI != req.RedirectURI {
GrantPasswordSecret = PasswordGrantType("password") // 账号密码 return errors.New("客户端重定向URI错误")
GrantPasswordPhone = PasswordGrantType("phone_code") // 手机验证码 }
GrantPasswordEmail = PasswordGrantType("email_code") // 邮箱验证码
)
type TokenReq struct { // todo 检查 scope
GrantType GrantType `json:"grant_type" form:"grant_type"`
ClientID string `json:"client_id" form:"client_id"` // 授权确认页面
ClientSecret string `json:"client_secret" form:"client_secret"` return nil
Scope string `json:"scope" form:"scope"`
GrantCodeData
GrantClientData
GrantRefreshData
GrantPasswordData
} }
type GrantCodeData struct { type AuthorizeGetReq struct {
Code string `json:"code" form:"code"` ResponseType string `json:"response_type" validate:"eq=code"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri"` ClientID string `json:"client_id" validate:"required"`
CodeVerifier string `json:"code_verifier" form:"code_verifier"` RedirectURI string `json:"redirect_uri" validate:"required"`
Scope string `json:"scope"`
State string `json:"state"`
} }
type GrantClientData struct { func AuthorizePost(ctx *fiber.Ctx) error {
// todo 解析用户授权的范围
return nil
} }
type GrantRefreshData struct { type AuthorizePostReq struct {
RefreshToken string `json:"refresh_token" form:"refresh_token"` Accept bool `json:"accept"`
} Scope string `json:"scope"`
type GrantPasswordData struct {
LoginType PasswordGrantType `json:"login_type" form:"login_type"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Remember bool `json:"remember" form:"remember"`
}
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope,omitempty"`
}
type TokenErrResp struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
} }
// Token 令牌端点
func Token(c *fiber.Ctx) error { func Token(c *fiber.Ctx) error {
now := time.Now() now := time.Now()
@@ -165,6 +148,75 @@ func Token(c *fiber.Ctx) error {
}) })
} }
type TokenReq struct {
GrantType GrantType `json:"grant_type" form:"grant_type"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
Scope string `json:"scope" form:"scope"`
GrantCodeData
GrantClientData
GrantRefreshData
GrantPasswordData
}
type GrantCodeData struct {
Code string `json:"code" form:"code"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
}
type GrantClientData struct {
}
type GrantRefreshData struct {
RefreshToken string `json:"refresh_token" form:"refresh_token"`
}
type GrantPasswordData struct {
LoginType PwdLoginType `json:"login_type" form:"login_type"`
LoginPool PwdLoginPool `json:"login_pool" form:"login_pool"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Remember bool `json:"remember" form:"remember"`
}
type GrantType string
const (
GrantAuthorizationCode = GrantType("authorization_code") // 授权码模式
GrantClientCredentials = GrantType("client_credentials") // 客户端凭证模式
GrantRefreshToken = GrantType("refresh_token") // 刷新令牌模式
GrantPassword = GrantType("password") // 密码模式(私有扩展)
)
type PwdLoginType string
const (
PwdLoginByPassword = PwdLoginType("password") // 账号密码
PwdLoginByPhone = PwdLoginType("phone_code") // 手机验证码
PwdLoginByEmail = PwdLoginType("email_code") // 邮箱验证码
)
type PwdLoginPool string
const (
PwdLoginAsUser = PwdLoginPool("user") // 用户池
PwdLoginAsAdmin = PwdLoginPool("admin") // 管理员池
)
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope,omitempty"`
}
type TokenErrResp struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
}
func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m.Session, error) { func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m.Session, error) {
// 检查 code 获取用户授权信息 // 检查 code 获取用户授权信息
@@ -226,7 +278,7 @@ func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second)) session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
} }
err = SaveSession(session) err = SaveSession(q.Q, session)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -249,7 +301,7 @@ func authClientCredential(c *fiber.Ctx, auth *AuthCtx, _ *TokenReq, now time.Tim
} }
// 保存会话 // 保存会话
err := SaveSession(session) err := SaveSession(q.Q, session)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -261,71 +313,85 @@ func authPassword(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m
ip, _ := orm.ParseInet(c.IP()) // 可空字段,忽略异常 ip, _ := orm.ParseInet(c.IP()) // 可空字段,忽略异常
ua := u.X(c.Get(fiber.HeaderUserAgent)) ua := u.X(c.Get(fiber.HeaderUserAgent))
// 分池认证
var err error
var user *m.User var user *m.User
err := q.Q.Transaction(func(tx *q.Query) (err error) { var admin *m.Admin
switch req.LoginType {
case GrantPasswordPhone: pool := req.LoginPool
user, err = authUserBySms(tx, req.Username, req.Password) if pool == "" {
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { pool = PwdLoginAsUser
return err
} }
if user == nil { switch pool {
case PwdLoginAsUser:
user, err = authUser(req.LoginType, req.Username, req.Password)
if err != nil {
if req.LoginType != PwdLoginByPhone || !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
// 手机号首次登录的自动创建用户
user = &m.User{ user = &m.User{
Phone: req.Username, Phone: req.Username,
Username: u.P(req.Username), Username: u.P(req.Username),
Status: m.UserStatusEnabled, Status: m.UserStatusEnabled,
} }
} }
case GrantPasswordEmail:
user, err = authUserByEmail(tx, req.Username, req.Password)
if err != nil {
return err
}
case GrantPasswordSecret:
user, err = authUserByPassword(tx, req.Username, req.Password)
if err != nil {
return err
}
default:
return ErrAuthorizeInvalidRequest
}
// 账户状态
if user.Status == m.UserStatusDisabled {
slog.Debug("账户状态异常", "username", req.Username, "status", user.Status)
return core.NewBizErr("账号无法登录")
}
// 更新用户的登录时间 // 更新用户的登录时间
user.LastLogin = u.P(time.Now()) user.LastLogin = u.P(time.Now())
user.LastLoginIP = ip user.LastLoginIP = ip
user.LastLoginUA = ua user.LastLoginUA = ua
if err := tx.User.Save(user); err != nil {
return err
}
return nil case PwdLoginAsAdmin:
}) admin, err = authAdmin(req.LoginType, req.Username, req.Password)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 更新管理员登录时间
admin.LastLogin = u.P(time.Now())
admin.LastLoginIP = ip
admin.LastLoginUA = ua
default:
return nil, ErrAuthorizeInvalidRequest
}
// 生成会话 // 生成会话
session := &m.Session{ session := &m.Session{
IP: ip, IP: ip,
UA: ua, UA: ua,
UserID: &user.ID,
ClientID: &auth.Client.ID, ClientID: &auth.Client.ID,
Scopes: u.X(req.Scope), Scopes: u.X(req.Scope),
AccessToken: uuid.NewString(), AccessToken: uuid.NewString(),
AccessTokenExpires: now.Add(time.Duration(env.SessionAccessExpire) * time.Second), AccessTokenExpires: now.Add(time.Duration(env.SessionAccessExpire) * time.Second),
} }
if req.Remember { if req.Remember {
session.RefreshToken = u.P(uuid.NewString()) session.RefreshToken = u.P(uuid.NewString())
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second)) session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
} }
err = SaveSession(session) // 保存用户更新和会话
err = q.Q.Transaction(func(tx *q.Query) error {
if user != nil {
if err := tx.User.Save(user); err != nil {
return err
}
session.UserID = &user.ID
}
if admin != nil {
if err := tx.Admin.Save(admin); err != nil {
return err
}
session.AdminID = &admin.ID
}
if err := SaveSession(tx, session); err != nil {
return err
}
return nil
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -353,7 +419,7 @@ func authRefreshToken(_ *fiber.Ctx, _ *AuthCtx, req *TokenReq, now time.Time) (*
} }
// 保存令牌 // 保存令牌
err = SaveSession(session) err = SaveSession(q.Q, session)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -394,12 +460,117 @@ func sendError(c *fiber.Ctx, err error, description ...string) error {
return err return err
} }
func Revoke() error { // Revoke 令牌撤销端点
func Revoke(ctx *fiber.Ctx) error {
_, err := GetAuthCtx(ctx).PermitUser()
if err != nil {
// 用户未登录
return nil
}
// 解析请求参数
req := new(RevokeReq)
if err := ctx.BodyParser(req); err != nil {
return err
}
// 删除会话
err = RemoveSession(ctx.Context(), req.AccessToken, req.RefreshToken)
if err != nil {
return err
}
return nil return nil
} }
func Introspect() error { type RevokeReq struct {
return nil AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// Introspect 令牌检查端点
func Introspect(ctx *fiber.Ctx) error {
authCtx := GetAuthCtx(ctx)
// 尝试验证用户权限
if _, err := authCtx.PermitUser(); err == nil {
return introspectUser(ctx, authCtx)
}
// 尝试验证管理员权限
if _, err := authCtx.PermitAdmin(); err == nil {
return introspectAdmin(ctx, authCtx)
}
return ErrAuthenticateForbidden
}
// introspectUser 获取并返回用户信息
func introspectUser(ctx *fiber.Ctx, authCtx *AuthCtx) error {
// 获取用户信息
profile, err := q.User.
Where(q.User.ID.Eq(authCtx.User.ID)).
Omit(q.User.DeletedAt).
Take()
if err != nil {
return err
}
// 检查用户是否设置了密码
hasPassword := false
if profile.Password != nil && *profile.Password != "" {
hasPassword = true
profile.Password = nil // 不返回密码
}
// 掩码敏感信息
if profile.Phone != "" {
profile.Phone = maskPhone(profile.Phone)
}
if profile.IDNo != nil && *profile.IDNo != "" {
profile.IDNo = u.P(maskIdNo(*profile.IDNo))
}
return ctx.JSON(struct {
m.User
HasPassword bool `json:"has_password"` // 是否设置了密码
}{*profile, hasPassword})
}
// introspectAdmin 获取并返回管理员信息
func introspectAdmin(ctx *fiber.Ctx, authCtx *AuthCtx) error {
// 获取管理员信息
profile, err := q.Admin.
Where(q.Admin.ID.Eq(authCtx.Admin.ID)).
Omit(q.Admin.DeletedAt).
Take()
if err != nil {
return err
}
// 不返回密码
profile.Password = ""
// 掩码敏感信息
if profile.Phone != nil && *profile.Phone != "" {
profile.Phone = u.P(maskPhone(*profile.Phone))
}
return ctx.JSON(profile)
}
func maskPhone(phone string) string {
if len(phone) < 11 {
return phone
}
return phone[:3] + "****" + phone[7:]
}
func maskIdNo(idNo string) string {
if len(idNo) < 18 {
return idNo
}
return idNo[:3] + "*********" + idNo[14:]
} }
type CodeContext struct { type CodeContext struct {

View File

@@ -6,15 +6,10 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
"strings" "strings"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
) )
func Authenticate() fiber.Handler { func Authenticate() fiber.Handler {
@@ -123,67 +118,3 @@ func authBasic(_ context.Context, token string) (*AuthCtx, error) {
Scopes: []string{}, Scopes: []string{},
}, nil }, nil
} }
func authClient(clientId, clientSecret string) (*m.Client, error) {
// 获取客户端信息
client, err := q.Client.
Where(
q.Client.ClientID.Eq(clientId),
q.Client.Status.Eq(1)).
Take()
if err != nil {
return nil, err
}
// 检查客户端密钥
if client.Spec == m.ClientSpecWeb || client.Spec == m.ClientSpecAPI {
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
return nil, errors.New("客户端密钥错误")
}
}
// todo 查询客户端关联权限
// 组织授权信息(一次性请求)
return client, nil
}
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
// 验证验证码
err := s.Verifier.VerifySms(context.Background(), username, code)
if err != nil {
return nil, core.NewBizErr("短信认证失败:%w", err)
}
// 查找用户
return tx.User.Where(tx.User.Phone.Eq(username)).Take()
}
func authUserByEmail(tx *q.Query, username, code string) (*m.User, error) {
return nil, core.NewServErr("邮箱登录不可用")
}
func authUserByPassword(tx *q.Query, username, password string) (*m.User, error) {
user, err := tx.User.
Where(tx.User.Phone.Eq(username)).
Or(tx.User.Email.Eq(username)).
Or(tx.User.Username.Eq(username)).
Take()
if err != nil {
slog.Debug("查找用户失败", "error", err)
return nil, core.NewBizErr("用户不存在或密码错误")
}
// 验证密码
if user.Password == nil || *user.Password == "" {
slog.Debug("用户未设置密码", "username", username)
return nil, core.NewBizErr("用户不存在或密码错误")
}
if bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)) != nil {
slog.Debug("密码验证失败", "username", username)
return nil, core.NewBizErr("用户不存在或密码错误")
}
return user, nil
}

View File

@@ -29,8 +29,8 @@ func FindSessionByRefresh(refreshToken string, now time.Time) (*m.Session, error
).First() ).First()
} }
func SaveSession(session *m.Session) error { func SaveSession(tx *q.Query, session *m.Session) error {
return q.Session.Save(session) return tx.Session.Save(session)
} }
func RemoveSession(ctx context.Context, accessToken string, refreshToken string) error { func RemoveSession(ctx context.Context, accessToken string, refreshToken string) error {

View File

@@ -19,7 +19,6 @@ type Err struct {
func (e *Err) Error() string { func (e *Err) Error() string {
if e.err != nil { if e.err != nil {
slog.Debug(fmt.Sprintf("%s: %s", e.msg, e.err.Error())) slog.Debug(fmt.Sprintf("%s: %s", e.msg, e.err.Error()))
return e.msg
} }
return e.msg return e.msg
} }

View File

@@ -1,5 +1,17 @@
package core package core
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"platform/pkg/env"
"platform/pkg/u"
"reflect"
"strconv"
"strings"
)
// PageReq 分页请求参数 // PageReq 分页请求参数
type PageReq struct { type PageReq struct {
RawPage int `json:"page"` RawPage int `json:"page"`
@@ -38,3 +50,83 @@ type PageResp struct {
Size int `json:"size"` Size int `json:"size"`
List any `json:"list"` List any `json:"list"`
} }
// Fetch 发送HTTP请求并返回响应
func Fetch(req *http.Request) (*http.Response, error) {
if env.DebugHttpDump {
str, err := httputil.DumpRequest(req, true)
if err != nil {
return nil, err
}
fmt.Printf("===== REQUEST ===== %s\n", req.URL)
fmt.Println(string(str))
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if env.DebugHttpDump {
str, err := httputil.DumpResponse(resp, true)
if err != nil {
return nil, err
}
fmt.Printf("===== RESPONSE ===== %s\n", req.URL)
fmt.Println(string(str))
}
return resp, nil
}
func Query(in any) url.Values {
out := url.Values{}
if in == nil {
return out
}
ref := reflect.ValueOf(in)
if ref.Kind() == reflect.Pointer {
ref = ref.Elem()
}
if ref.Kind() != reflect.Struct {
return out
}
for i := 0; i < ref.NumField(); i++ {
field := ref.Type().Field(i)
value := ref.Field(i)
if field.Type.Kind() == reflect.Pointer {
if value.IsNil() {
continue
}
value = value.Elem()
}
name := field.Name
tags := strings.Split(field.Tag.Get("query"), ",")
if len(tags) > 0 && tags[0] != "" {
name = tags[0]
}
switch value := value.Interface().(type) {
case string:
out.Add(name, value)
case int:
out.Add(name, strconv.Itoa(value))
case bool:
if tags[1] == "b2i" {
out.Add(name, u.Ternary(value, "1", "0"))
} else {
out.Add(name, strconv.FormatBool(value))
}
default:
out.Add(name, fmt.Sprintf("%v", value))
}
}
return out
}

View File

@@ -1,24 +1,11 @@
package events package events
import ( import (
"encoding/json"
"log/slog"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
) )
const RemoveChannel = "channel:remove" const RemoveChannel = "channel:remove"
type RemoveChannelData struct { func NewRemoveChannel(batch string) *asynq.Task {
Batch string `json:"batch"` return asynq.NewTask(RemoveChannel, []byte(batch))
IDs []int32 `json:"ids"`
}
func NewRemoveChannel(data RemoveChannelData) *asynq.Task {
bytes, err := json.Marshal(data)
if err != nil {
slog.Error("序列化删除通道任务失败", "error", err)
return nil
}
return asynq.NewTask(RemoveChannel, bytes)
} }

View File

@@ -10,6 +10,7 @@ import (
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"platform/pkg/env" "platform/pkg/env"
"platform/web/core"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -17,18 +18,12 @@ import (
// CloudClient 定义云服务接口 // CloudClient 定义云服务接口
type CloudClient interface { type CloudClient interface {
CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error) CloudEdges(param *CloudEdgesReq) (*CloudEdgesResp, error)
CloudConnect(param CloudConnectReq) error CloudConnect(param *CloudConnectReq) error
CloudDisconnect(param CloudDisconnectReq) (int, error) CloudDisconnect(param *CloudDisconnectReq) (int, error)
CloudAutoQuery() (CloudConnectResp, error) CloudAutoQuery() (CloudConnectResp, error)
} }
// GatewayClient 定义网关接口
type GatewayClient interface {
GatewayPortConfigs(params []PortConfigsReq) error
GatewayPortActive(param ...PortActiveReq) (map[string]PortData, error)
}
type cloud struct { type cloud struct {
url string url string
} }
@@ -37,59 +32,14 @@ var Cloud CloudClient
func initBaiyin() error { func initBaiyin() error {
Cloud = &cloud{ Cloud = &cloud{
url: env.BaiyinAddr, url: env.BaiyinCloudUrl,
} }
return nil return nil
} }
type AutoConfig struct { // cloud:/edges 筛选查询边缘节点
Province string `json:"province"` func (c *cloud) CloudEdges(param *CloudEdgesReq) (*CloudEdgesResp, error) {
City string `json:"city"` resp, err := c.requestCloud("GET", "/edges?"+core.Query(param).Encode(), "")
Isp string `json:"isp"`
Count int `json:"count"`
}
// region cloud:/edges
type CloudEdgesReq struct {
Province string
City string
Isp string
Offset int
Limit int
}
type CloudEdgesResp struct {
Edges []Edge `json:"edges"`
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
}
type Edge struct {
EdgesId int `json:"edges_id"`
Province string `json:"province"`
City string `json:"city"`
Isp string `json:"isp"`
Ip string `json:"ip"`
Rtt int `json:"rtt"`
PacketLoss int `json:"packet_loss"`
}
func (c *cloud) CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error) {
data := strings.Builder{}
data.WriteString("province=")
data.WriteString(param.Province)
data.WriteString("&city=")
data.WriteString(param.City)
data.WriteString("&isp=")
data.WriteString(param.Isp)
data.WriteString("&offset=")
data.WriteString(strconv.Itoa(param.Offset))
data.WriteString("&limit=")
data.WriteString(strconv.Itoa(param.Limit))
resp, err := c.requestCloud("GET", "/edges?"+data.String(), "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -115,17 +65,46 @@ func (c *cloud) CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error) {
return &result, nil return &result, nil
} }
// endregion type CloudEdgesReq struct {
Province *string `query:"province"`
// region cloud:/connect City *string `query:"city"`
Isp *string `query:"isp"`
type CloudConnectReq struct { Offset *int `query:"offset"`
Uuid string `json:"uuid"` Limit *int `query:"limit"`
Edge []string `json:"edge,omitempty"` NoRepeat *bool `query:"norepeat,b2i"`
AutoConfig []AutoConfig `json:"auto_config,omitempty"` NoDayRepeat *bool `query:"nodayrepeat,b2i"`
IpUnchangedTime *int `query:"ip_unchanged_time"` // 单位秒
ActiveTime *int `query:"active_time"` // 单位秒
// 排序方式,可选值:
// - create_time_asc 设备创建时间顺序
// - create_time_desc 设备创建时间倒序
// - ip_unchanged_time_asc ip持续没变化时间顺序
// - ip_unchanged_time_desc ip持续没变化时间倒序
// - active_time_asc 连续活跃时间顺序
// - active_time_desc 连续活跃时间倒序
// - rand 随机排序 (默认)
Sort *string `query:"sort"`
} }
func (c *cloud) CloudConnect(param CloudConnectReq) error { type CloudEdgesResp struct {
Edges []Edge `json:"edges"`
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
}
type Edge struct {
EdgeID string `json:"edge_id"`
Province string `json:"province"`
City string `json:"city"`
Isp string `json:"isp"`
Ip string `json:"ip"`
Rtt int `json:"rtt"`
PacketLoss int `json:"packet_loss"`
}
// cloud:/connect 连接边缘节点到网关
func (c *cloud) CloudConnect(param *CloudConnectReq) error {
data, err := json.Marshal(param) data, err := json.Marshal(param)
if err != nil { if err != nil {
return err return err
@@ -162,25 +141,21 @@ func (c *cloud) CloudConnect(param CloudConnectReq) error {
return nil return nil
} }
// endregion type CloudConnectReq struct {
// region cloud:/disconnect
type CloudDisconnectReq struct {
Uuid string `json:"uuid"` Uuid string `json:"uuid"`
Edge []string `json:"edge,omitempty"` Edge *[]string `json:"edge,omitempty"`
Config []Config `json:"auto_config,omitempty"` AutoConfig *[]AutoConfig `json:"auto_config,omitempty"`
} }
type Config struct { type AutoConfig struct {
Province string `json:"province"` Province string `json:"province"`
City string `json:"city"` City string `json:"city"`
Isp string `json:"isp"` Isp string `json:"isp"`
Count int `json:"count"` Count int `json:"count"`
Online bool `json:"online"`
} }
func (c *cloud) CloudDisconnect(param CloudDisconnectReq) (int, error) { // cloud:/disconnect 解除连接边缘节点到网关
func (c *cloud) CloudDisconnect(param *CloudDisconnectReq) (int, error) {
data, err := json.Marshal(param) data, err := json.Marshal(param)
if err != nil { if err != nil {
return 0, err return 0, err
@@ -217,12 +192,21 @@ func (c *cloud) CloudDisconnect(param CloudDisconnectReq) (int, error) {
return int(result["disconnected_edges"].(float64)), nil return int(result["disconnected_edges"].(float64)), nil
} }
// endregion type CloudDisconnectReq struct {
Uuid string `json:"uuid"`
Edge *[]string `json:"edge,omitempty"`
Config *[]Config `json:"auto_config,omitempty"`
}
// region cloud:/auto_query type Config struct {
Province string `json:"province"`
type CloudConnectResp map[string][]AutoConfig City string `json:"city"`
Isp string `json:"isp"`
Count int `json:"count"`
Online bool `json:"online"`
}
// cloud:/auto_query 自动连接配置查询
func (c *cloud) CloudAutoQuery() (CloudConnectResp, error) { func (c *cloud) CloudAutoQuery() (CloudConnectResp, error) {
resp, err := c.requestCloud("GET", "/auto_query", "") resp, err := c.requestCloud("GET", "/auto_query", "")
if err != nil { if err != nil {
@@ -250,7 +234,7 @@ func (c *cloud) CloudAutoQuery() (CloudConnectResp, error) {
return result, nil return result, nil
} }
// endregion type CloudConnectResp map[string][]AutoConfig
func (c *cloud) requestCloud(method string, url string, data string) (*http.Response, error) { func (c *cloud) requestCloud(method string, url string, data string) (*http.Response, error) {
@@ -263,7 +247,7 @@ func (c *cloud) requestCloud(method string, url string, data string) (*http.Resp
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
var resp *http.Response var resp *http.Response
for i := 0; i < 2; i++ { for i := range 2 {
token, err := c.token(i == 1) token, err := c.token(i == 1)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -304,7 +288,7 @@ func (c *cloud) requestCloud(method string, url string, data string) (*http.Resp
func (c *cloud) token(refresh bool) (string, error) { func (c *cloud) token(refresh bool) (string, error) {
// redis 获取令牌 // redis 获取令牌
if !refresh { if !refresh {
token, err := Redis.Get(context.Background(), "remote:token").Result() token, err := Redis.Get(context.Background(), BaiyinToken).Result()
if err == nil && token != "" { if err == nil && token != "" {
return token, nil return token, nil
} }
@@ -338,7 +322,7 @@ func (c *cloud) token(refresh bool) (string, error) {
var result map[string]any var result map[string]any
err = json.Unmarshal(body, &result) err = json.Unmarshal(body, &result)
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("解析响应 [%s] 失败: %w", string(body), err)
} }
if result["code"].(float64) != 1 { if result["code"].(float64) != 1 {
@@ -347,7 +331,7 @@ func (c *cloud) token(refresh bool) (string, error) {
// redis 设置令牌 // redis 设置令牌
token := result["token"].(string) token := result["token"].(string)
err = Redis.Set(context.Background(), "remote:token", token, 1*time.Hour).Err() err = Redis.Set(context.Background(), BaiyinToken, token, 1*time.Hour).Err()
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -355,6 +339,15 @@ func (c *cloud) token(refresh bool) (string, error) {
return token, nil return token, nil
} }
const BaiyinToken = "clients:baiyin:token"
// GatewayClient 定义网关接口
type GatewayClient interface {
GatewayPortConfigs(params []*PortConfigsReq) error
GatewayPortActive(param ...*PortActiveReq) (map[string]PortData, error)
GatewayEdge(params *GatewayEdgeReq) (map[string]GatewayEdgeInfo, error)
}
type gateway struct { type gateway struct {
url string url string
username string username string
@@ -373,6 +366,68 @@ func NewGateway(url, username, password string) GatewayClient {
return GatewayInitializer(url, username, password) return GatewayInitializer(url, username, password)
} }
type GatewayEdgeReq struct {
EdgeID *string `query:"edge_id"`
Province *string `query:"province"`
City *string `query:"city"`
Isp *string `query:"isp"`
Connected *bool `query:"connected"`
Assigned *bool `query:"assigned"`
GetRand *int `query:"getRand"`
IpUnchangedTimeStart *int `query:"ip_unchanged_time_start"`
IpUnchangedTimeEnd *int `query:"ip_unchanged_time_end"`
OnlineTimeStart *int `query:"online_time_start"`
OnlineTimeEnd *int `query:"online_time_end"`
Rtt *int `query:"rtt"`
MinRtt *int `query:"min_rtt"`
RttBaidu *int `query:"rtt_baidu"`
PacketLoss *int `query:"packet_loss"`
PacketLossBaidu *int `query:"packet_loss_baidu"`
IP *string `query:"ip"`
Limit *int `query:"limit"`
Offset *int `query:"offset"`
}
type GatewayEdgeResp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data map[string]GatewayEdgeInfo `json:"data"`
Total int `json:"total"`
}
type GatewayEdgeInfo struct {
IP string `json:"ip"`
Connected bool `json:"connected"`
Assigned bool `json:"assigned"`
AssignedTo string `json:"assignedto"`
PacketLoss int `json:"packet_loss"`
PacketLossBaidu int `json:"packet_loss_baidu"`
Rtt int `json:"rtt"`
RttBaidu int `json:"rtt_baidu"`
OfflineTime int `json:"offline_time"`
OnlineTime int `json:"online_time"`
IpUnchangedTime int `json:"ip_unchanged_time"`
}
func (c *gateway) GatewayEdge(req *GatewayEdgeReq) (map[string]GatewayEdgeInfo, error) {
resp, err := c.get("/edge", core.Query(req))
if err != nil {
return nil, fmt.Errorf("查询可用节点失败:%w", err)
}
defer resp.Body.Close()
body := new(GatewayEdgeResp)
if err = json.NewDecoder(resp.Body).Decode(body); err != nil {
return nil, fmt.Errorf("解析响应内容失败:%w", err)
}
if body.Code != 0 {
return nil, fmt.Errorf("接口业务响应异常: %d %s", body.Code, body.Msg)
}
return body.Data, nil
}
// region gateway:/port/configs // region gateway:/port/configs
type PortConfigsReq struct { type PortConfigsReq struct {
@@ -395,7 +450,7 @@ type AutoEdgeConfig struct {
PacketLoss int `json:"packet_loss,omitempty"` PacketLoss int `json:"packet_loss,omitempty"`
} }
func (c *gateway) GatewayPortConfigs(params []PortConfigsReq) error { func (c *gateway) GatewayPortConfigs(params []*PortConfigsReq) error {
if len(params) == 0 { if len(params) == 0 {
return errors.New("params is empty") return errors.New("params is empty")
} }
@@ -461,10 +516,10 @@ type PortData struct {
Userpass string `json:"userpass"` Userpass string `json:"userpass"`
} }
func (c *gateway) GatewayPortActive(param ...PortActiveReq) (map[string]PortData, error) { func (c *gateway) GatewayPortActive(param ...*PortActiveReq) (map[string]PortData, error) {
_param := PortActiveReq{} _param := PortActiveReq{}
if len(param) != 0 { if len(param) != 0 && param[0] != nil {
_param = param[0] _param = *param[0]
} }
path := strings.Builder{} path := strings.Builder{}
@@ -520,38 +575,33 @@ func (c *gateway) GatewayPortActive(param ...PortActiveReq) (map[string]PortData
// endregion // endregion
func (c *gateway) get(url string, params url.Values) (*http.Response, error) {
url = fmt.Sprintf("http://%s:%s@%s:9990%s?%s", c.username, c.password, c.url, url, params.Encode())
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败:%w", err)
}
res, err := core.Fetch(req)
if err != nil {
return nil, fmt.Errorf("获取数据失败:%w", err)
}
if res.StatusCode != http.StatusOK {
bytes, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("接口响应异常: %d %s", res.StatusCode, string(bytes))
}
return res, nil
}
func (c *gateway) requestGateway(method string, url string, data string) (*http.Response, error) { func (c *gateway) requestGateway(method string, url string, data string) (*http.Response, error) {
//goland:noinspection ALL
url = fmt.Sprintf("http://%s:%s@%s:9990%s", c.username, c.password, c.url, url) url = fmt.Sprintf("http://%s:%s@%s:9990%s", c.username, c.password, c.url, url)
req, err := http.NewRequest(method, url, strings.NewReader(data)) req, err := http.NewRequest(method, url, strings.NewReader(data))
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
if env.DebugHttpDump { return core.Fetch(req)
str, err := httputil.DumpRequest(req, true)
if err != nil {
return nil, err
}
fmt.Println("==============================")
fmt.Println(string(str))
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if env.DebugHttpDump {
str, err := httputil.DumpResponse(resp, true)
if err != nil {
return nil, err
}
fmt.Println("------------------------------")
fmt.Println(string(str))
}
return resp, nil
} }

View File

@@ -3,6 +3,7 @@ package globals
import ( import (
"context" "context"
"fmt" "fmt"
"platform/pkg/env"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
@@ -16,8 +17,13 @@ import (
var tp *trace.TracerProvider var tp *trace.TracerProvider
func initOtel(ctx context.Context) error { func initOtel(ctx context.Context) error {
if env.OtelHost == "" || env.OtelPort == "" {
return nil
}
addr := env.OtelHost + ":" + env.OtelPort
exporter, err := otlptracegrpc.New(ctx, exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("localhost:4317"), otlptracegrpc.WithEndpoint(addr),
otlptracegrpc.WithInsecure(), otlptracegrpc.WithInsecure(),
) )
if err != nil { if err != nil {

View File

@@ -1,97 +0,0 @@
package handlers
import (
"platform/pkg/u"
auth2 "platform/web/auth"
m "platform/web/models"
q "platform/web/queries"
"github.com/gofiber/fiber/v2"
)
// region /revoke
type RevokeReq struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
func Revoke(c *fiber.Ctx) error {
_, err := auth2.GetAuthCtx(c).PermitUser()
if err != nil {
// 用户未登录
return nil
}
// 解析请求参数
req := new(RevokeReq)
if err := c.BodyParser(req); err != nil {
return err
}
// 删除会话
err = auth2.RemoveSession(c.Context(), req.AccessToken, req.RefreshToken)
if err != nil {
return err
}
return nil
}
// endregion
// region /profile
type IntrospectResp struct {
m.User
HasPassword bool `json:"has_password"` // 是否设置了密码
}
func Introspect(c *fiber.Ctx) error {
// 验证权限
authCtx, err := auth2.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
// 获取用户信息
profile, err := q.User.
Where(q.User.ID.Eq(authCtx.User.ID)).
Omit(q.User.DeletedAt).
Take()
if err != nil {
return err
}
// 检查用户是否设置了密码
hasPassword := false
if profile.Password != nil && *profile.Password != "" {
hasPassword = true
profile.Password = nil // 不返回密码
}
// 掩码敏感信息
if profile.Phone != "" {
profile.Phone = maskPhone(profile.Phone)
}
if profile.IDNo != nil && *profile.IDNo != "" {
profile.IDNo = u.P(maskIdNo(*profile.IDNo))
}
return c.JSON(IntrospectResp{*profile, hasPassword})
}
func maskPhone(phone string) string {
if len(phone) < 11 {
return phone
}
return phone[:3] + "****" + phone[7:]
}
func maskIdNo(idNo string) string {
if len(idNo) < 18 {
return idNo
}
return idNo[:3] + "*********" + idNo[14:]
}
// endregion

79
web/handlers/batch.go Normal file
View File

@@ -0,0 +1,79 @@
package handlers
import (
"platform/web/auth"
"platform/web/core"
c "platform/web/core"
g "platform/web/globals"
q "platform/web/queries"
"time"
"github.com/gofiber/fiber/v2"
)
// PageResourceBatch 分页查询套餐提取记录
func PageResourceBatch(ctx *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.GetAuthCtx(ctx).PermitUser()
if err != nil {
return err
}
// 解析请求参数
req := new(PageResourceBatchReq)
if err := g.Validator.ParseBody(ctx, req); err != nil {
return err
}
// 查询批次
conds := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(authCtx.User.ID))
if req.TimeStart != nil {
conds.Where(q.LogsUserUsage.Time.Gte(*req.TimeStart))
}
if req.TimeEnd != nil {
conds.Where(q.LogsUserUsage.Time.Lte(*req.TimeEnd))
}
list, total, err := q.LogsUserUsage.Where(conds).
Order(q.LogsUserUsage.Time.Desc()).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return core.NewBizErr("获取数据失败", err)
}
// 返回数据
return ctx.JSON(c.PageResp{
Total: int(total),
List: list,
Page: req.GetPage(),
Size: req.GetSize(),
})
}
type PageResourceBatchReq struct {
c.PageReq
TimeStart *time.Time `json:"time_start"`
TimeEnd *time.Time `json:"time_end"`
}
// PageBatchByAdmin 分页查询所有提取记录
func PageBatchByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
req := new(struct{ core.PageReq })
if err = g.Validator.ParseBody(c, req); err != nil {
return err
}
list, total, err := q.LogsUserUsage.FindByPage(req.GetOffset(), req.GetLimit())
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}

View File

@@ -3,20 +3,40 @@ package handlers
import ( import (
"platform/web/auth" "platform/web/auth"
"platform/web/core" "platform/web/core"
g "platform/web/globals"
q "platform/web/queries" q "platform/web/queries"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// region ListBill // PageBillByAdmin 分页查询全部账单
func PageBillByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
type ListBillReq struct { // 解析请求参数
core.PageReq req := new(core.PageReq)
BillNo *string `json:"bill_no"` if err := g.Validator.ParseBody(c, req); err != nil {
Type *int `json:"type"` return err
CreateAfter *time.Time `json:"create_after"` }
CreateBefore *time.Time `json:"create_before"`
// 查询用户列表
list, total, err := q.Bill.FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
} }
// ListBill 获取账单列表 // ListBill 获取账单列表
@@ -79,4 +99,10 @@ func ListBill(c *fiber.Ctx) error {
}) })
} }
// endregion type ListBillReq struct {
core.PageReq
BillNo *string `json:"bill_no"`
Type *int `json:"type"`
CreateAfter *time.Time `json:"create_after"`
CreateBefore *time.Time `json:"create_before"`
}

View File

@@ -14,15 +14,36 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// region ListChannels // PageChannelsByAdmin 分页查询所有通道
func PageChannelsByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
type ListChannelsReq struct { // 解析请求参数
core.PageReq req := new(core.PageReq)
AuthType s.ChannelAuthType `json:"auth_type"` if err := g.Validator.ParseBody(c, req); err != nil {
ExpireAfter *time.Time `json:"expire_after"` return err
ExpireBefore *time.Time `json:"expire_before"` }
// 查询通道列表
list, total, err := q.Channel.FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
} }
// 分页查询当前用户通道
func ListChannels(c *fiber.Ctx) error { func ListChannels(c *fiber.Ctx) error {
// 检查权限 // 检查权限
authContext, err := auth.GetAuthCtx(c).PermitUser() authContext, err := auth.GetAuthCtx(c).PermitUser()
@@ -57,7 +78,6 @@ func ListChannels(c *fiber.Ctx) error {
// 查询数据 // 查询数据
channels, err := q.Channel. channels, err := q.Channel.
Preload(q.Channel.Proxy).
Where(cond). Where(cond).
Order(q.Channel.CreatedAt.Desc()). Order(q.Channel.CreatedAt.Desc()).
Offset(req.GetOffset()). Offset(req.GetOffset()).
@@ -87,28 +107,14 @@ func ListChannels(c *fiber.Ctx) error {
}) })
} }
// endregion type ListChannelsReq struct {
core.PageReq
// region CreateChannel AuthType s.ChannelAuthType `json:"auth_type"`
ExpireAfter *time.Time `json:"expire_after"`
type CreateChannelReq struct { ExpireBefore *time.Time `json:"expire_before"`
ResourceId int32 `json:"resource_id" validate:"required"`
AuthType s.ChannelAuthType `json:"auth_type" validate:"required"`
Protocol int `json:"protocol" validate:"required"`
Count int `json:"count" validate:"required"`
Prov *string `json:"prov"`
City *string `json:"city"`
Isp *int `json:"isp"`
}
type CreateChannelRespItem struct {
Proto int `json:"-"`
Host string `json:"host"`
Port uint16 `json:"port"`
Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"`
} }
// 创建新通道
func CreateChannel(c *fiber.Ctx) error { func CreateChannel(c *fiber.Ctx) error {
// 解析参数 // 解析参数
@@ -144,7 +150,7 @@ func CreateChannel(c *fiber.Ctx) error {
for i, channel := range result { for i, channel := range result {
resp[i] = &CreateChannelRespItem{ resp[i] = &CreateChannelRespItem{
Proto: req.Protocol, Proto: req.Protocol,
Host: channel.Proxy.IP.String(), Host: channel.Host,
Port: channel.Port, Port: channel.Port,
} }
if req.AuthType == s.ChannelAuthTypePass { if req.AuthType == s.ChannelAuthTypePass {
@@ -155,17 +161,25 @@ func CreateChannel(c *fiber.Ctx) error {
return c.JSON(resp) return c.JSON(resp)
} }
type CreateChannelResultType string type CreateChannelReq struct {
ResourceId int32 `json:"resource_id" validate:"required"`
// endregion AuthType s.ChannelAuthType `json:"auth_type" validate:"required"`
Protocol int `json:"protocol" validate:"required"`
// region RemoveChannels Count int `json:"count" validate:"required"`
Prov *string `json:"prov"`
type RemoveChannelsReq struct { City *string `json:"city"`
Batch string `json:"batch" validate:"required"` Isp *int `json:"isp"`
Ids []int32 `json:"ids" validate:"required"`
} }
type CreateChannelRespItem struct {
Proto int `json:"-"`
Host string `json:"host"`
Port uint16 `json:"port"`
Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"`
}
// RemoveChannels 删除通道
func RemoveChannels(c *fiber.Ctx) error { func RemoveChannels(c *fiber.Ctx) error {
// 检查权限 // 检查权限
_, err := auth.GetAuthCtx(c).PermitOfficialClient() _, err := auth.GetAuthCtx(c).PermitOfficialClient()
@@ -180,7 +194,7 @@ func RemoveChannels(c *fiber.Ctx) error {
} }
// 删除通道 // 删除通道
err = s.Channel.RemoveChannels(req.Batch, req.Ids) err = s.Channel.RemoveChannels(req.Batch)
if err != nil { if err != nil {
return err return err
} }
@@ -188,4 +202,6 @@ func RemoveChannels(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK) return c.SendStatus(fiber.StatusOK)
} }
// endregion type RemoveChannelsReq struct {
Batch string `json:"batch" validate:"required"`
}

48
web/handlers/inquiry.go Normal file
View File

@@ -0,0 +1,48 @@
package handlers
import (
"platform/pkg/u"
"platform/web/core"
g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries"
"github.com/gofiber/fiber/v2"
)
// region CreateInquiry
type CreateInquiryRequest struct {
Company string `json:"company" validate:"omitempty,max=200"`
Name string `json:"name" validate:"required,max=100"`
Phone string `json:"phone" validate:"required,max=20"`
Email string `json:"email" validate:"omitempty,email,max=100"`
Content string `json:"content" validate:"required,max=1000"`
}
func CreateInquiry(c *fiber.Ctx) error {
// 解析请求参数
req := new(CreateInquiryRequest)
err := g.Validator.ParseBody(c, req)
if err != nil {
return err
}
// 创建咨询记录
err = q.Inquiry.Create(&m.Inquiry{
Company: u.X(req.Company),
Name: u.X(req.Name),
Phone: u.X(req.Phone),
Email: u.X(req.Email),
Content: u.X(req.Content),
Status: m.InquiryStatusPending,
})
if err != nil {
return core.NewServErr("提交咨询失败", err)
}
return c.SendStatus(fiber.StatusNoContent)
}
// endregion

View File

@@ -1,10 +1,11 @@
package handlers package handlers
import ( import (
"log/slog"
"net/netip" "net/netip"
"platform/pkg/env" "platform/pkg/env"
g "platform/web/globals" "platform/web/auth"
"platform/web/core"
"platform/web/globals"
s "platform/web/services" s "platform/web/services"
"time" "time"
@@ -16,37 +17,48 @@ func DebugRegisterProxyBaiYin(c *fiber.Ctx) error {
return fiber.ErrNotFound return fiber.ErrNotFound
} }
ok, err := g.Redis.SetNX(c.Context(), "debug:channel:register:127.0.0.1", true, 0).Result() err := s.Proxy.RegisterBaiyin("1a:2b:3c:4d:5e:6f", netip.AddrFrom4([4]byte{127, 0, 0, 1}), "test", "test")
if err != nil { if err != nil {
return err return core.NewServErr("注册失败", err)
}
slog.Info("注册代理", "ok", ok)
if !ok {
return fiber.ErrConflict
}
err = s.Proxy.RegisterBaiyin("1a:2b:3c:4d:5e:6f", netip.AddrFrom4([4]byte{127, 0, 0, 1}), "test", "test")
if err != nil {
return err
} }
return nil return nil
} }
// region 报告上线 // 注册白银代理网关
func ProxyRegisterBaiYin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
if err != nil {
return err
}
type ProxyReportOnlineReq struct { req := new(RegisterProxyBaiyinReq)
err = globals.Validator.ParseBody(c, req)
if err != nil {
return err
}
addr, err := netip.ParseAddr(req.IP)
if err != nil {
return core.NewServErr("IP地址格式错误", err)
}
err = s.Proxy.RegisterBaiyin(req.Name, addr, req.Username, req.Password)
if err != nil {
return core.NewServErr("注册失败", err)
}
return nil
}
type RegisterProxyBaiyinReq struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Version int `json:"version" validate:"required"` IP string `json:"ip" validate:"required"`
} Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
type ProxyReportOnlineResp struct {
Id int32 `json:"id"`
Secret string `json:"secret"`
Permits []*ProxyPermit `json:"permits"`
Edges []*ProxyEdge `json:"edges"`
} }
// region 报告上线
func ProxyReportOnline(c *fiber.Ctx) (err error) { func ProxyReportOnline(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{ return c.JSON(map[string]any{
"error": "接口暂不可用", "error": "接口暂不可用",
@@ -150,12 +162,19 @@ func ProxyReportOnline(c *fiber.Ctx) (err error) {
// }) // })
} }
// region 报告下线 type ProxyReportOnlineReq struct {
Name string `json:"name" validate:"required"`
type ProxyReportOfflineReq struct { Version int `json:"version" validate:"required"`
Id int32 `json:"id" validate:"required"`
} }
type ProxyReportOnlineResp struct {
Id int32 `json:"id"`
Secret string `json:"secret"`
Permits []*ProxyPermit `json:"permits"`
Edges []*ProxyEdge `json:"edges"`
}
// region 报告下线
func ProxyReportOffline(c *fiber.Ctx) (err error) { func ProxyReportOffline(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{ return c.JSON(map[string]any{
"error": "接口暂不可用", "error": "接口暂不可用",
@@ -193,13 +212,11 @@ func ProxyReportOffline(c *fiber.Ctx) (err error) {
// return nil // return nil
} }
// region 报告更新 type ProxyReportOfflineReq struct {
type ProxyReportUpdateReq struct {
Id int32 `json:"id" validate:"required"` Id int32 `json:"id" validate:"required"`
Edges []*ProxyEdge `json:"edges" validate:"required"`
} }
// region 报告更新
func ProxyReportUpdate(c *fiber.Ctx) (err error) { func ProxyReportUpdate(c *fiber.Ctx) (err error) {
return c.JSON(map[string]any{ return c.JSON(map[string]any{
"error": "接口暂不可用", "error": "接口暂不可用",
@@ -358,6 +375,11 @@ func ProxyReportUpdate(c *fiber.Ctx) (err error) {
// return nil // return nil
} }
type ProxyReportUpdateReq struct {
Id int32 `json:"id" validate:"required"`
Edges []*ProxyEdge `json:"edges" validate:"required"`
}
type ProxyPermit struct { type ProxyPermit struct {
Id int32 `json:"id"` Id int32 `json:"id"`
Expire time.Time `json:"expire"` Expire time.Time `json:"expire"`

View File

@@ -15,18 +15,8 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
type ListResourceShortReq struct { // PageResourceShort 分页查询当前用户短效套餐
core.PageReq func PageResourceShort(c *fiber.Ctx) error {
ResourceNo *string `json:"resource_no"`
Active *bool `json:"active"`
Type *int `json:"type"`
CreateAfter *time.Time `json:"create_after"`
CreateBefore *time.Time `json:"create_before"`
ExpireAfter *time.Time `json:"expire_after"`
ExpireBefore *time.Time `json:"expire_before"`
}
func ListResourceShort(c *fiber.Ctx) error {
// 检查权限 // 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser() authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil { if err != nil {
@@ -34,7 +24,7 @@ func ListResourceShort(c *fiber.Ctx) error {
} }
// 解析请求参数 // 解析请求参数
req := new(ListResourceShortReq) req := new(PageResourceShortReq)
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return err return err
} }
@@ -60,10 +50,10 @@ func ListResourceShort(c *fiber.Ctx) error {
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore)) do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
} }
if req.ExpireAfter != nil { if req.ExpireAfter != nil {
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Expire.Gte(*req.ExpireAfter)) do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Gte(*req.ExpireAfter))
} }
if req.ExpireBefore != nil { if req.ExpireBefore != nil {
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Expire.Lte(*req.ExpireBefore)) do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Lte(*req.ExpireBefore))
} }
resource, err := q.Resource.Where(do). resource, err := q.Resource.Where(do).
@@ -96,7 +86,7 @@ func ListResourceShort(c *fiber.Ctx) error {
}) })
} }
type ListResourceLongReq struct { type PageResourceShortReq struct {
core.PageReq core.PageReq
ResourceNo *string `json:"resource_no"` ResourceNo *string `json:"resource_no"`
Active *bool `json:"active"` Active *bool `json:"active"`
@@ -107,7 +97,8 @@ type ListResourceLongReq struct {
ExpireBefore *time.Time `json:"expire_before"` ExpireBefore *time.Time `json:"expire_before"`
} }
func ListResourceLong(c *fiber.Ctx) error { // PageResourceLong 分页查询当前用户长效套餐
func PageResourceLong(c *fiber.Ctx) error {
// 检查权限 // 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser() authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil { if err != nil {
@@ -115,7 +106,7 @@ func ListResourceLong(c *fiber.Ctx) error {
} }
// 解析请求参数 // 解析请求参数
req := new(ListResourceLongReq) req := new(PageResourceLongReq)
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return err return err
} }
@@ -141,10 +132,10 @@ func ListResourceLong(c *fiber.Ctx) error {
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore)) do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
} }
if req.ExpireAfter != nil { if req.ExpireAfter != nil {
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Expire.Gte(*req.ExpireAfter)) do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Gte(*req.ExpireAfter))
} }
if req.ExpireBefore != nil { if req.ExpireBefore != nil {
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Expire.Lte(*req.ExpireBefore)) do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Lte(*req.ExpireBefore))
} }
resource, err := q.Resource.Where(do). resource, err := q.Resource.Where(do).
@@ -177,9 +168,68 @@ func ListResourceLong(c *fiber.Ctx) error {
}) })
} }
type AllResourceReq struct { type PageResourceLongReq struct {
core.PageReq
ResourceNo *string `json:"resource_no"`
Active *bool `json:"active"`
Type *int `json:"type"`
CreateAfter *time.Time `json:"create_after"`
CreateBefore *time.Time `json:"create_before"`
ExpireAfter *time.Time `json:"expire_after"`
ExpireBefore *time.Time `json:"expire_before"`
} }
// PageResourceShortByAdmin 分页查询全部短效套餐
func PageResourceShortByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
req := new(struct{ core.PageReq })
if err = g.Validator.ParseBody(c, req); err != nil {
return err
}
list, total, err := q.Resource.
LeftJoin(q.ResourceShort, q.ResourceShort.ResourceID.EqCol(q.Resource.ID)).
Where(q.Resource.Type.Eq(int(m.ResourceTypeShort))).
FindByPage(req.GetOffset(), req.GetLimit())
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
// PageResourceLongByAdmin 分页查询全部短效套餐
func PageResourceLongByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
req := new(struct{ core.PageReq })
if err = g.Validator.ParseBody(c, req); err != nil {
return err
}
list, total, err := q.Resource.
LeftJoin(q.ResourceLong, q.ResourceLong.ResourceID.EqCol(q.Resource.ID)).
Where(q.Resource.Type.Eq(int(m.ResourceTypeLong))).
FindByPage(req.GetOffset(), req.GetLimit())
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
// AllActiveResource 所有可用套餐
func AllActiveResource(c *fiber.Ctx) error { func AllActiveResource(c *fiber.Ctx) error {
// 检查权限 // 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser() authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -204,10 +254,10 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Type.Eq(int(m.ResourceTypeShort)), q.Resource.Type.Eq(int(m.ResourceTypeShort)),
q.ResourceShort.As(q.Resource.Short.Name()).Where( q.ResourceShort.As(q.Resource.Short.Name()).Where(
short.Type.Eq(int(m.ResourceModeTime)), short.Type.Eq(int(m.ResourceModeTime)),
short.Expire.Gte(now), short.ExpireAt.Gte(now),
q.ResourceShort.As(q.Resource.Short.Name()). q.ResourceShort.As(q.Resource.Short.Name()).
Where(short.DailyLast.Lt(u.Today())). Where(short.LastAt.Lt(u.Today())).
Or(short.DailyLimit.GtCol(short.DailyUsed)), Or(short.Quota.GtCol(short.Daily)),
).Or( ).Or(
short.Type.Eq(int(m.ResourceModeQuota)), short.Type.Eq(int(m.ResourceModeQuota)),
short.Quota.GtCol(short.Used), short.Quota.GtCol(short.Used),
@@ -216,10 +266,10 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Type.Eq(int(m.ResourceTypeLong)), q.Resource.Type.Eq(int(m.ResourceTypeLong)),
q.ResourceLong.As(q.Resource.Long.Name()).Where( q.ResourceLong.As(q.Resource.Long.Name()).Where(
long.Type.Eq(int(m.ResourceModeTime)), long.Type.Eq(int(m.ResourceModeTime)),
long.Expire.Gte(now), long.ExpireAt.Gte(now),
q.ResourceLong.As(q.Resource.Long.Name()). q.ResourceLong.As(q.Resource.Long.Name()).
Where(long.DailyLast.Lt(u.Today())). Where(long.LastAt.Lt(u.Today())).
Or(long.DailyLimit.GtCol(long.DailyUsed)), Or(long.Quota.GtCol(long.Daily)),
).Or( ).Or(
long.Type.Eq(int(m.ResourceModeQuota)), long.Type.Eq(int(m.ResourceModeQuota)),
long.Quota.GtCol(long.Used), long.Quota.GtCol(long.Used),
@@ -235,23 +285,10 @@ func AllActiveResource(c *fiber.Ctx) error {
return c.JSON(resources) return c.JSON(resources)
} }
type StatisticPersonalResp struct { type AllResourceReq struct {
Short StatisticShort `json:"short"`
Long StatisticLong `json:"long"`
}
type StatisticShort struct {
ResourceCount int `json:"resource_count"`
ResourceQuotaSum int `json:"resource_quota_sum"`
ResourceDailyFreeSum int `json:"resource_daily_free_sum"`
}
type StatisticLong struct {
ResourceCount int `json:"resource_count"`
ResourceQuotaSum int `json:"resource_quota_sum"`
ResourceDailyFreeSum int `json:"resource_daily_free_sum"`
} }
// StatisticResourceFree 统计每日可用
func StatisticResourceFree(c *fiber.Ctx) error { func StatisticResourceFree(c *fiber.Ctx) error {
// 检查权限 // 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser() authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -282,39 +319,39 @@ func StatisticResourceFree(c *fiber.Ctx) error {
// 短效包量 // 短效包量
case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeQuota: case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeQuota:
if u.Z(resource.Short.Quota) > resource.Short.Used { if resource.Short.Quota > resource.Short.Used {
shortCount++ shortCount++
shortQuotaSum += int(u.Z(resource.Short.Quota) - resource.Short.Used) shortQuotaSum += int(resource.Short.Quota - resource.Short.Used)
} }
// 长效包量 // 长效包量
case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeQuota: case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeQuota:
if u.Z(resource.Long.Quota) > resource.Long.Used { if resource.Long.Quota > resource.Long.Used {
longCount++ longCount++
longQuotaSum += int(u.Z(resource.Long.Quota) - resource.Long.Used) longQuotaSum += int(resource.Long.Quota - resource.Long.Used)
} }
// 短效包时 // 短效包时
case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeTime: case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeTime:
if time.Time(*resource.Short.Expire).After(time.Now()) { if time.Time(*resource.Short.ExpireAt).After(time.Now()) {
if resource.Short.DailyLast == nil || u.IsToday(time.Time(*resource.Short.DailyLast)) == false { if resource.Short.LastAt == nil || u.IsToday(time.Time(*resource.Short.LastAt)) == false {
shortCount++ shortCount++
shortDailyFreeSum += int(resource.Short.DailyLimit) shortDailyFreeSum += int(resource.Short.Quota)
} else if resource.Short.DailyLimit > resource.Short.DailyUsed { } else if resource.Short.Quota > resource.Short.Daily {
shortCount++ shortCount++
shortDailyFreeSum += int(resource.Short.DailyLimit - resource.Short.DailyUsed) shortDailyFreeSum += int(resource.Short.Quota - resource.Short.Daily)
} }
} }
// 长效包时 // 长效包时
case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeTime: case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeTime:
if time.Time(*resource.Long.Expire).After(time.Now()) { if time.Time(*resource.Long.ExpireAt).After(time.Now()) {
if resource.Long.DailyLast == nil || u.IsToday(time.Time(*resource.Long.DailyLast)) == false { if resource.Long.LastAt == nil || u.IsToday(time.Time(*resource.Long.LastAt)) == false {
longCount++ longCount++
longDailyFreeSum += int(resource.Long.DailyLimit) longDailyFreeSum += int(resource.Long.Quota)
} else if resource.Long.DailyLimit > resource.Long.DailyUsed { } else if resource.Long.Quota > resource.Long.Daily {
longCount++ longCount++
longDailyFreeSum += int(resource.Long.DailyLimit - resource.Long.DailyUsed) longDailyFreeSum += int(resource.Long.Quota - resource.Long.Daily)
} }
} }
} }
@@ -334,17 +371,24 @@ func StatisticResourceFree(c *fiber.Ctx) error {
}) })
} }
type StatisticResourceUsageReq struct { type StatisticPersonalResp struct {
ResourceNo *string `json:"resource_no"` Short StatisticShort `json:"short"`
TimeAfter *time.Time `json:"time_after"` Long StatisticLong `json:"long"`
TimeBefore *time.Time `json:"time_before"`
} }
type StatisticResourceUsageResp []struct { type StatisticShort struct {
Date time.Time `json:"date"` ResourceCount int `json:"resource_count"`
Count int `json:"count"` ResourceQuotaSum int `json:"resource_quota_sum"`
ResourceDailyFreeSum int `json:"resource_daily_free_sum"`
} }
type StatisticLong struct {
ResourceCount int `json:"resource_count"`
ResourceQuotaSum int `json:"resource_quota_sum"`
ResourceDailyFreeSum int `json:"resource_daily_free_sum"`
}
// StatisticResourceUsage 统计每日用量
func StatisticResourceUsage(c *fiber.Ctx) error { func StatisticResourceUsage(c *fiber.Ctx) error {
// 检查权限 // 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser() authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -359,21 +403,10 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
} }
// 统计套餐提取数量 // 统计套餐提取数量
do := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(authCtx.User.ID)) do := q.LogsUserUsage.Where(
if req.ResourceNo != nil && *req.ResourceNo != "" { q.LogsUserUsage.UserID.Eq(authCtx.User.ID),
var resourceID int32 )
err := q.Resource.
Where(
q.Resource.UserID.Eq(authCtx.User.ID),
q.Resource.ResourceNo.Eq(*req.ResourceNo),
).
Select(q.Resource.ID).
Scan(&resourceID)
if err != nil {
return err
}
do.Where(q.LogsUserUsage.ResourceID.Eq(resourceID))
}
if req.TimeAfter != nil { if req.TimeAfter != nil {
do.Where(q.LogsUserUsage.Time.Gte(*req.TimeAfter)) do.Where(q.LogsUserUsage.Time.Gte(*req.TimeAfter))
} }
@@ -389,7 +422,7 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
). ).
Where(do). Where(do).
Group( Group(
field.NewUnsafeFieldRaw("date_trunc('day', time)"), field.NewField("", "date"),
). ).
Order( Order(
field.NewField("", "date").Desc(), field.NewField("", "date").Desc(),
@@ -402,10 +435,17 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
return c.JSON(data) return c.JSON(data)
} }
type CreateResourceReq struct { type StatisticResourceUsageReq struct {
*s.CreateResourceData TimeAfter *time.Time `json:"time_start"`
TimeBefore *time.Time `json:"time_end"`
} }
type StatisticResourceUsageResp []struct {
Date time.Time `json:"date"`
Count int `json:"count"`
}
// CreateResource 创建套餐
func CreateResource(c *fiber.Ctx) error { func CreateResource(c *fiber.Ctx) error {
// 检查权限 // 检查权限
@@ -429,6 +469,11 @@ func CreateResource(c *fiber.Ctx) error {
return nil return nil
} }
type CreateResourceReq struct {
*s.CreateResourceData
}
// ResourcePrice 套餐价格
func ResourcePrice(c *fiber.Ctx) error { func ResourcePrice(c *fiber.Ctx) error {
// 检查权限 // 检查权限
_, err := auth.GetAuthCtx(c).PermitSecretClient() _, err := auth.GetAuthCtx(c).PermitSecretClient()
@@ -439,11 +484,25 @@ func ResourcePrice(c *fiber.Ctx) error {
// 解析请求参数 // 解析请求参数
var req = new(CreateResourceReq) var req = new(CreateResourceReq)
if err := g.Validator.ParseBody(c, req); err != nil { if err := g.Validator.ParseBody(c, req); err != nil {
return err return core.NewBizErr("接口参数解析异常", err)
} }
// 获取套餐价格 // 获取套餐价格
return c.JSON(fiber.Map{ amount, err := req.GetAmount()
"price": req.GetAmount().StringFixed(2), if err != nil {
return err
}
// 计算折扣
return c.JSON(ResourcePriceResp{
Price: amount.StringFixed(2),
Discounted: 1,
DiscountedPrice: amount.StringFixed(2),
}) })
} }
type ResourcePriceResp struct {
Price string `json:"price"`
Discounted float32 `json:"discounted"`
DiscountedPrice string `json:"discounted_price"`
}

View File

@@ -9,26 +9,44 @@ import (
"platform/web/core" "platform/web/core"
g "platform/web/globals" g "platform/web/globals"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries"
s "platform/web/services" s "platform/web/services"
"reflect"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
type TradeCreateReq struct { // PageTradeByAdmin 分页查询所有订单
s.CreateTradeData func PageTradeByAdmin(c *fiber.Ctx) error {
Type m.TradeType `json:"type" validate:"required"` // 检查权限
Resource *s.CreateResourceData `json:"resource,omitempty"` _, err := auth.GetAuthCtx(c).PermitAdmin()
Recharge *s.RechargeProductInfo `json:"recharge,omitempty"` if err != nil {
} return err
}
type TradeCreateResp struct {
PayUrl string `json:"pay_url"` // 解析请求参数
TradeNo string `json:"trade_no"` req := new(core.PageReq)
if err := g.Validator.ParseBody(c, req); err != nil {
return err
}
// 查询用户列表
list, total, err := q.Trade.FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
} }
// 创建订单
func TradeCreate(c *fiber.Ctx) error { func TradeCreate(c *fiber.Ctx) error {
// 检查权限 // 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser() authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -68,10 +86,19 @@ func TradeCreate(c *fiber.Ctx) error {
}) })
} }
type TradeCompleteReq struct { type TradeCreateReq struct {
s.ModifyTradeData s.CreateTradeData
Type m.TradeType `json:"type" validate:"required"`
Resource *s.CreateResourceData `json:"resource,omitempty"`
Recharge *s.RechargeProductInfo `json:"recharge,omitempty"`
} }
type TradeCreateResp struct {
PayUrl string `json:"pay_url"`
TradeNo string `json:"trade_no"`
}
// 完成订单
func TradeComplete(c *fiber.Ctx) error { func TradeComplete(c *fiber.Ctx) error {
// 检查权限 // 检查权限
_, err := auth.GetAuthCtx(c).PermitUser() _, err := auth.GetAuthCtx(c).PermitUser()
@@ -94,10 +121,11 @@ func TradeComplete(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent) return c.SendStatus(fiber.StatusNoContent)
} }
type TradeCancelReq struct { type TradeCompleteReq struct {
s.ModifyTradeData s.ModifyTradeData
} }
// 取消订单
func TradeCancel(c *fiber.Ctx) error { func TradeCancel(c *fiber.Ctx) error {
// 检查权限 // 检查权限
_, err := auth.GetAuthCtx(c).PermitUser() _, err := auth.GetAuthCtx(c).PermitUser()
@@ -121,10 +149,11 @@ func TradeCancel(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent) return c.SendStatus(fiber.StatusNoContent)
} }
type TradeCheckReq struct { type TradeCancelReq struct {
s.ModifyTradeData s.ModifyTradeData
} }
// 检查订单
func TradeCheck(c *fiber.Ctx) error { func TradeCheck(c *fiber.Ctx) error {
// 解析请求参数 // 解析请求参数
req := new(TradeCheckReq) req := new(TradeCheckReq)
@@ -154,9 +183,9 @@ func TradeCheck(c *fiber.Ctx) error {
slog.Error("写入订单状态失败", "trade_no", req.TradeNo, "error", err) slog.Error("写入订单状态失败", "trade_no", req.TradeNo, "error", err)
return return
} }
err = w.Flush() err = w.Flush()
if err != nil { if err != nil {
slog.Error("刷新缓冲区失败", "trade_no", req.TradeNo, "error", err, "errType", reflect.TypeOf(err))
return return
} }
@@ -171,3 +200,7 @@ func TradeCheck(c *fiber.Ctx) error {
return nil return nil
} }
type TradeCheckReq struct {
s.ModifyTradeData
}

View File

@@ -2,6 +2,8 @@ package handlers
import ( import (
"platform/web/auth" "platform/web/auth"
"platform/web/core"
g "platform/web/globals"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
s "platform/web/services" s "platform/web/services"
@@ -10,15 +12,81 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
// region /update // 分页获取用户
func PageUserByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
type UpdateUserReq struct { // 解析请求参数
Username string `json:"username" validate:"omitempty,min=3,max=20"` req := new(core.PageReq)
Email string `json:"email" validate:"omitempty,email"` if err := g.Validator.ParseBody(c, req); err != nil {
ContactQQ string `json:"contact_qq" validate:"omitempty,qq"` return err
ContactWechat string `json:"contact_wechat" validate:"omitempty,wechat"` }
// 查询用户列表
users, total, err := q.User.
Preload(q.User.Admin).
Omit(q.User.Password).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
for _, user := range users {
if user.Admin != nil {
user.Admin = &m.Admin{
Name: user.Admin.Name,
}
}
}
// 返回结果
return c.JSON(core.PageResp{
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
List: users,
})
} }
// 绑定管理员
func BindAdmin(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
// 解析请求参数
req := new(struct {
UserID int `json:"user_id" validate:"required"`
})
if err := g.Validator.ParseBody(c, req); err != nil {
return err
}
// 更新用户信息
result, err := q.User.Where(
q.User.ID.Eq(int32(req.UserID)),
q.User.AdminID.IsNull(),
).UpdateColumnSimple(
q.User.AdminID.Value(authCtx.Admin.ID),
)
if err != nil {
return err
}
if result.RowsAffected == 0 {
return core.NewBizErr("用户已绑定管理员")
}
// 返回结果
return c.SendStatus(fiber.StatusNoContent)
}
// 更新用户
func UpdateUser(c *fiber.Ctx) error { func UpdateUser(c *fiber.Ctx) error {
// 检查权限 // 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser() authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -49,15 +117,14 @@ func UpdateUser(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent) return c.SendStatus(fiber.StatusNoContent)
} }
// endregion type UpdateUserReq struct {
// region /update/account
type UpdateAccountReq struct {
Username string `json:"username" validate:"omitempty,min=3,max=20"` Username string `json:"username" validate:"omitempty,min=3,max=20"`
Password string `json:"password" validate:"omitempty,min=6,max=20"` Email string `json:"email" validate:"omitempty,email"`
ContactQQ string `json:"contact_qq" validate:"omitempty,qq"`
ContactWechat string `json:"contact_wechat" validate:"omitempty,wechat"`
} }
// 更新账号信息
func UpdateAccount(c *fiber.Ctx) error { func UpdateAccount(c *fiber.Ctx) error {
// 检查权限 // 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser() authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -86,16 +153,12 @@ func UpdateAccount(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent) return c.SendStatus(fiber.StatusNoContent)
} }
// endregion type UpdateAccountReq struct {
Username string `json:"username" validate:"omitempty,min=3,max=20"`
// region /update/password Password string `json:"password" validate:"omitempty,min=6,max=20"`
type UpdatePasswordReq struct {
Phone string `json:"phone"`
Code string `json:"code"`
Password string `json:"password"`
} }
// 更新账号密码
func UpdatePassword(c *fiber.Ctx) error { func UpdatePassword(c *fiber.Ctx) error {
// 检查权限 // 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser() authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -109,8 +172,13 @@ func UpdatePassword(c *fiber.Ctx) error {
return err return err
} }
// 验证手机号
if req.Phone != authCtx.User.Phone {
return fiber.NewError(fiber.StatusBadRequest, "手机号码不正确")
}
// 验证手机令牌 // 验证手机令牌
if req.Phone == "" || req.Code == "" { if req.Code == "" {
return fiber.NewError(fiber.StatusBadRequest, "手机号码和验证码不能为空") return fiber.NewError(fiber.StatusBadRequest, "手机号码和验证码不能为空")
} }
err = s.Verifier.VerifySms(c.Context(), req.Phone, req.Code) err = s.Verifier.VerifySms(c.Context(), req.Phone, req.Code)
@@ -135,4 +203,8 @@ func UpdatePassword(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent) return c.SendStatus(fiber.StatusNoContent)
} }
// endregion type UpdatePasswordReq struct {
Phone string `json:"phone"`
Code string `json:"code"`
Password string `json:"password"`
}

View File

@@ -20,6 +20,14 @@ func ApplyMiddlewares(app *fiber.App) {
EnableStackTrace: true, EnableStackTrace: true,
})) }))
// cors
app.Use(cors.New(cors.Config{
AllowCredentials: true,
AllowOriginsFunc: func(origin string) bool {
return true
},
}))
// logger // logger
app.Use(logger.New(logger.Config{ app.Use(logger.New(logger.Config{
Next: func(c *fiber.Ctx) bool { Next: func(c *fiber.Ctx) bool {
@@ -38,9 +46,6 @@ func ApplyMiddlewares(app *fiber.App) {
}, },
})) }))
// cors
app.Use(cors.New())
// authenticate // authenticate
app.Use(auth.Authenticate()) app.Use(auth.Authenticate())
} }

View File

@@ -12,14 +12,14 @@ type Admin struct {
core.Model core.Model
Username string `json:"username" gorm:"column:username"` // 用户名 Username string `json:"username" gorm:"column:username"` // 用户名
Password string `json:"password" gorm:"column:password"` // 密码 Password string `json:"password" gorm:"column:password"` // 密码
Name *string `json:"name" gorm:"column:name"` // 真实姓名 Name *string `json:"name,omitempty" gorm:"column:name"` // 真实姓名
Avatar *string `json:"avatar" gorm:"column:avatar"` // 头像URL Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL
Phone *string `json:"phone" gorm:"column:phone"` // 手机号码 Phone *string `json:"phone,omitempty" gorm:"column:phone"` // 手机号码
Email *string `json:"email" gorm:"column:email"` // 邮箱 Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱
Status AdminStatus `json:"status" gorm:"column:status"` // 状态0-禁用1-正常 Status AdminStatus `json:"status" gorm:"column:status"` // 状态0-禁用1-正常
LastLogin *time.Time `json:"last_login" gorm:"column:last_login"` // 最后登录时间 LastLogin *time.Time `json:"last_login,omitempty" gorm:"column:last_login"` // 最后登录时间
LastLoginIP *orm.Inet `json:"last_login_ip" gorm:"column:last_login_ip"` // 最后登录地址 LastLoginIP *orm.Inet `json:"last_login_ip,omitempty" gorm:"column:last_login_ip"` // 最后登录地址
LastLoginUA *string `json:"last_login_ua" gorm:"column:last_login_ua"` // 最后登录代理 LastLoginUA *string `json:"last_login_ua,omitempty" gorm:"column:last_login_ua"` // 最后登录代理
} }
// AdminStatus 管理员状态枚举 // AdminStatus 管理员状态枚举

View File

@@ -8,7 +8,7 @@ import (
type AdminRole struct { type AdminRole struct {
core.Model core.Model
Name string `json:"name" gorm:"column:name"` // 角色名称 Name string `json:"name" gorm:"column:name"` // 角色名称
Description *string `json:"description" gorm:"column:description"` // 角色描述 Description *string `json:"description,omitempty" gorm:"column:description"` // 角色描述
Active bool `json:"active" gorm:"column:active"` // 是否激活 Active bool `json:"active" gorm:"column:active"` // 是否激活
Sort int32 `json:"sort" gorm:"column:sort"` // 排序 Sort int32 `json:"sort" gorm:"column:sort"` // 排序
} }

View File

@@ -8,7 +8,7 @@ import (
type Announcement struct { type Announcement struct {
core.Model core.Model
Title string `json:"title" gorm:"column:title"` // 公告标题 Title string `json:"title" gorm:"column:title"` // 公告标题
Content *string `json:"content" gorm:"column:content"` // 公告内容 Content *string `json:"content,omitempty" gorm:"column:content"` // 公告内容
Type AnnouncementType `json:"type" gorm:"column:type"` // 公告类型1-普通公告 Type AnnouncementType `json:"type" gorm:"column:type"` // 公告类型1-普通公告
Pin bool `json:"pin" gorm:"column:pin"` // 是否置顶 Pin bool `json:"pin" gorm:"column:pin"` // 是否置顶
Status AnnouncementStatus `json:"status" gorm:"column:status"` // 公告状态0-禁用1-正常 Status AnnouncementStatus `json:"status" gorm:"column:status"` // 公告状态0-禁用1-正常

View File

@@ -10,18 +10,18 @@ import (
type Bill struct { type Bill struct {
core.Model core.Model
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
TradeID *int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID TradeID *int32 `json:"trade_id,omitempty" gorm:"column:trade_id"` // 订单ID
ResourceID *int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID ResourceID *int32 `json:"resource_id,omitempty" gorm:"column:resource_id"` // 套餐ID
RefundID *int32 `json:"refund_id" gorm:"column:refund_id"` // 退款ID RefundID *int32 `json:"refund_id,omitempty" gorm:"column:refund_id"` // 退款ID
BillNo string `json:"bill_no" gorm:"column:bill_no"` // 易读账单号 BillNo string `json:"bill_no" gorm:"column:bill_no"` // 易读账单号
Info *string `json:"info" gorm:"column:info"` // 产品可读信息 Info *string `json:"info,omitempty" gorm:"column:info"` // 产品可读信息
Type BillType `json:"type" gorm:"column:type"` // 账单类型1-消费2-退款3-充值 Type BillType `json:"type" gorm:"column:type"` // 账单类型1-消费2-退款3-充值
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 账单金额 Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 账单金额
User *User `json:"user" gorm:"foreignKey:UserID"` User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Trade *Trade `json:"trade" gorm:"foreignKey:TradeID"` Trade *Trade `json:"trade,omitempty" gorm:"foreignKey:TradeID"`
Resource *Resource `json:"resource" gorm:"foreignKey:ResourceID"` Resource *Resource `json:"resource,omitempty" gorm:"foreignKey:ResourceID"`
Refund *Refund `json:"refund" gorm:"foreignKey:RefundID"` Refund *Refund `json:"refund,omitempty" gorm:"foreignKey:RefundID"`
} }
// BillType 账单类型枚举 // BillType 账单类型枚举

View File

@@ -11,21 +11,23 @@ type Channel struct {
core.Model core.Model
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
ProxyID int32 `json:"proxy_id" gorm:"column:proxy_id"` // 代理ID
BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号 BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号
ProxyID int32 `json:"proxy_id" gorm:"column:proxy_id"` // 代理ID
Host string `json:"host" gorm:"column:host"` // 代理主机
Port uint16 `json:"port" gorm:"column:port"` // 代理端口 Port uint16 `json:"port" gorm:"column:port"` // 代理端口
EdgeID *int32 `json:"edge_id" gorm:"column:edge_id"` // 节点ID手动配置 EdgeID *int32 `json:"edge_id,omitempty" gorm:"column:edge_id"` // 节点ID手动配置
FilterISP *EdgeISP `json:"filter_isp" gorm:"column:filter_isp"` // 运营商过滤(自动配置):参考 edge.isp EdgeRef *string `json:"edge_ref,omitempty" gorm:"column:edge_ref"` // 外部节点引用用于索引没有ID的外部非受控节点
FilterProv *string `json:"filter_prov" gorm:"column:filter_prov"` // 省份过滤(自动配置) FilterISP *EdgeISP `json:"filter_isp,omitempty" gorm:"column:filter_isp"` // 运营商过滤(自动配置):参考 edge.isp
FilterCity *string `json:"filter_city" gorm:"column:filter_city"` // 城市过滤(自动配置) FilterProv *string `json:"filter_prov,omitempty" gorm:"column:filter_prov"` // 省份过滤(自动配置)
IP *orm.Inet `json:"ip" gorm:"column:ip"` // 节点地址 FilterCity *string `json:"filter_city,omitempty" gorm:"column:filter_city"` // 城市过滤(自动配置)
Whitelists *string `json:"whitelists" gorm:"column:whitelists"` // IP白名单逗号分隔 IP *orm.Inet `json:"ip,omitempty" gorm:"column:ip"` // 节点地址
Username *string `json:"username" gorm:"column:username"` // 用户名 Whitelists *string `json:"whitelists,omitempty" gorm:"column:whitelists"` // IP白名单逗号分隔
Password *string `json:"password" gorm:"column:password"` // 密码 Username *string `json:"username,omitempty" gorm:"column:username"` // 用户名
Password *string `json:"password,omitempty" gorm:"column:password"` // 密码
ExpiredAt time.Time `json:"expired_at" gorm:"column:expired_at"` // 过期时间 ExpiredAt time.Time `json:"expired_at" gorm:"column:expired_at"` // 过期时间
User User `json:"user" gorm:"foreignKey:UserID"` User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Resource Resource `json:"resource" gorm:"foreignKey:ResourceID"` Resource *Resource `json:"resource,omitempty" gorm:"foreignKey:ResourceID"`
Proxy Proxy `json:"proxy" gorm:"foreignKey:ProxyID"` Proxy *Proxy `json:"proxy,omitempty" gorm:"foreignKey:ProxyID"`
Edge *Edge `json:"edge" gorm:"foreignKey:EdgeID"` Edge *Edge `json:"edge,omitempty" gorm:"foreignKey:EdgeID"`
} }

View File

@@ -9,10 +9,10 @@ type Client struct {
core.Model core.Model
ClientID string `json:"client_id" gorm:"column:client_id"` // OAuth2客户端标识符 ClientID string `json:"client_id" gorm:"column:client_id"` // OAuth2客户端标识符
ClientSecret string `json:"client_secret" gorm:"column:client_secret"` // OAuth2客户端密钥 ClientSecret string `json:"client_secret" gorm:"column:client_secret"` // OAuth2客户端密钥
RedirectURI *string `json:"redirect_uri" gorm:"column:redirect_uri"` // OAuth2 重定向URI RedirectURI *string `json:"redirect_uri,omitempty" gorm:"column:redirect_uri"` // OAuth2 重定向URI
Spec ClientSpec `json:"spec" gorm:"column:spec"` // 安全规范1-native2-browser3-web4-api Spec ClientSpec `json:"spec" gorm:"column:spec"` // 安全规范1-native2-browser3-web4-api
Name string `json:"name" gorm:"column:name"` // 名称 Name string `json:"name" gorm:"column:name"` // 名称
Icon *string `json:"icon" gorm:"column:icon"` // 图标URL Icon *string `json:"icon,omitempty" gorm:"column:icon"` // 图标URL
Status ClientStatus `json:"status" gorm:"column:status"` // 状态0-禁用1-正常 Status ClientStatus `json:"status" gorm:"column:status"` // 状态0-禁用1-正常
Type ClientType `json:"type" gorm:"column:type"` // 类型0-普通1-官方 Type ClientType `json:"type" gorm:"column:type"` // 类型0-普通1-官方
} }

View File

@@ -10,13 +10,13 @@ import (
// Coupon 优惠券表 // Coupon 优惠券表
type Coupon struct { type Coupon struct {
core.Model core.Model
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
Code string `json:"code" gorm:"column:code"` // 优惠券代码 Code string `json:"code" gorm:"column:code"` // 优惠券代码
Remark *string `json:"remark" gorm:"column:remark"` // 优惠券备注 Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 优惠券备注
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 优惠券金额 Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 优惠券金额
MinAmount decimal.Decimal `json:"min_amount" gorm:"column:min_amount"` // 最低消费金额 MinAmount decimal.Decimal `json:"min_amount" gorm:"column:min_amount"` // 最低消费金额
Status CouponStatus `json:"status" gorm:"column:status"` // 优惠券状态0-未使用1-已使用2-已过期 Status CouponStatus `json:"status" gorm:"column:status"` // 优惠券状态0-未使用1-已使用2-已过期
ExpireAt *time.Time `json:"expire_at" gorm:"column:expire_at"` // 过期时间 ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 过期时间
} }
// CouponStatus 优惠券状态枚举 // CouponStatus 优惠券状态枚举

25
web/models/inquiry.go Normal file
View File

@@ -0,0 +1,25 @@
package models
import (
"platform/web/core"
)
// Inquiry 用户咨询表
type Inquiry struct {
core.Model
Company *string `json:"company,omitempty" gorm:"column:company"` // 公司名称
Name *string `json:"name,omitempty" gorm:"column:name"` // 联系人姓名
Phone *string `json:"phone,omitempty" gorm:"column:phone"` // 联系电话
Email *string `json:"email,omitempty" gorm:"column:email"` // 联系邮箱
Content *string `json:"content,omitempty" gorm:"column:content"` // 咨询内容
Status InquiryStatus `json:"status" gorm:"column:status"` // 处理状态0-待处理1-已处理
Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 备注
}
// InquiryStatus 咨询处理状态枚举
type InquiryStatus int
const (
InquiryStatusPending InquiryStatus = 0 // 待处理
InquiryStatusProcessed InquiryStatus = 1 // 已处理
)

View File

@@ -13,10 +13,10 @@ type LogsLogin struct {
GrantType GrantType `json:"grant_type" gorm:"column:grant_type"` // 授权类型 GrantType GrantType `json:"grant_type" gorm:"column:grant_type"` // 授权类型
PasswordType PasswordType `json:"password_type" gorm:"column:password_type"` // 密码模式子授权类型 PasswordType PasswordType `json:"password_type" gorm:"column:password_type"` // 密码模式子授权类型
Success bool `json:"success" gorm:"column:success"` // 登录是否成功 Success bool `json:"success" gorm:"column:success"` // 登录是否成功
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
Time time.Time `json:"time" gorm:"column:time"` // 登录时间 Time time.Time `json:"time" gorm:"column:time"` // 登录时间
User *User `json:"user" gorm:"foreignKey:UserID"` User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
} }
// GrantType 授权类型枚举 // GrantType 授权类型枚举

View File

@@ -10,15 +10,15 @@ type LogsRequest struct {
ID int32 `json:"id" gorm:"column:id"` // 访问日志ID ID int32 `json:"id" gorm:"column:id"` // 访问日志ID
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址 IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
UA string `json:"ua" gorm:"column:ua"` // 用户代理 UA string `json:"ua" gorm:"column:ua"` // 用户代理
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
ClientID *int32 `json:"client_id" gorm:"column:client_id"` // 客户端ID ClientID *int32 `json:"client_id,omitempty" gorm:"column:client_id"` // 客户端ID
Method string `json:"method" gorm:"column:method"` // 请求方法 Method string `json:"method" gorm:"column:method"` // 请求方法
Path string `json:"path" gorm:"column:path"` // 请求路径 Path string `json:"path" gorm:"column:path"` // 请求路径
Status int16 `json:"status" gorm:"column:status"` // 响应状态码 Status int16 `json:"status" gorm:"column:status"` // 响应状态码
Error *string `json:"error" gorm:"column:error"` // 错误信息 Error *string `json:"error,omitempty" gorm:"column:error"` // 错误信息
Time time.Time `json:"time" gorm:"column:time"` // 请求时间 Time time.Time `json:"time" gorm:"column:time"` // 请求时间
Latency string `json:"latency" gorm:"column:latency"` // 请求延迟 Latency string `json:"latency" gorm:"column:latency"` // 请求延迟
User *User `json:"user" gorm:"foreignKey:UserID"` User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Client *Client `json:"client" gorm:"foreignKey:ClientID"` Client *Client `json:"client,omitempty" gorm:"foreignKey:ClientID"`
} }

View File

@@ -12,9 +12,9 @@ type LogsUserUsage struct {
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号 BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号
Count int32 `json:"count" gorm:"column:count"` // 数量 Count int32 `json:"count" gorm:"column:count"` // 数量
Prov *string `json:"prov" gorm:"column:prov"` // 省份 Prov *string `json:"prov,omitempty" gorm:"column:prov"` // 省份
City *string `json:"city" gorm:"column:city"` // 城市 City *string `json:"city,omitempty" gorm:"column:city"` // 城市
ISP *string `json:"isp" gorm:"column:isp"` // 运营商 ISP *string `json:"isp,omitempty" gorm:"column:isp"` // 运营商
IP orm.Inet `json:"ip" gorm:"column:ip"` // IP地址 IP orm.Inet `json:"ip" gorm:"column:ip"` // IP地址
Time time.Time `json:"time" gorm:"column:time"` // 提取时间 Time time.Time `json:"time" gorm:"column:time"` // 提取时间
} }

View File

@@ -5,10 +5,10 @@ import "platform/web/core"
// Permission 权限表 // Permission 权限表
type Permission struct { type Permission struct {
core.Model core.Model
ParentID *int32 `json:"parent_id" gorm:"column:parent_id"` // 父权限ID ParentID *int32 `json:"parent_id,omitempty" gorm:"column:parent_id"` // 父权限ID
Name string `json:"name" gorm:"column:name"` // 权限名称 Name string `json:"name" gorm:"column:name"` // 权限名称
Description *string `json:"description" gorm:"column:description"` // 权限描述 Description *string `json:"description,omitempty" gorm:"column:description"` // 权限描述
Parent *Permission `json:"parent" gorm:"foreignKey:ParentID"` Parent *Permission `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
Children []*Permission `json:"children" gorm:"foreignKey:ParentID"` Children []*Permission `json:"children,omitempty" gorm:"foreignKey:ParentID"`
} }

View File

@@ -9,7 +9,7 @@ type Product struct {
core.Model core.Model
Code string `json:"code" gorm:"column:code"` // 产品代码 Code string `json:"code" gorm:"column:code"` // 产品代码
Name string `json:"name" gorm:"column:name"` // 产品名称 Name string `json:"name" gorm:"column:name"` // 产品名称
Description *string `json:"description" gorm:"column:description"` // 产品描述 Description *string `json:"description,omitempty" gorm:"column:description"` // 产品描述
Sort int32 `json:"sort" gorm:"column:sort"` // 排序 Sort int32 `json:"sort" gorm:"column:sort"` // 排序
Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态0-禁用1-正常 Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态0-禁用1-正常
} }

View File

@@ -13,12 +13,13 @@ type Proxy struct {
Version int32 `json:"version" gorm:"column:version"` // 代理服务版本 Version int32 `json:"version" gorm:"column:version"` // 代理服务版本
Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称 Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址 IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址
Secret *string `json:"secret" gorm:"column:secret"` // 代理服务密钥 Host *string `json:"host,omitempty" gorm:"column:host"` // 代理服务域名
Secret *string `json:"secret,omitempty" gorm:"column:secret"` // 代理服务密钥
Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型1-自有2-白银 Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型1-自有2-白银
Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态0-离线1-在线 Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态0-离线1-在线
Meta *datatypes.JSONType[any] `json:"meta" gorm:"column:meta"` // 代理服务元信息 Meta *datatypes.JSONType[any] `json:"meta,omitempty" gorm:"column:meta"` // 代理服务元信息
Channels []Channel `json:"channels" gorm:"foreignkey:ProxyID"` Channels []Channel `json:"channels,omitempty" gorm:"foreignkey:ProxyID"`
} }
// ProxyType 代理服务类型枚举 // ProxyType 代理服务类型枚举

View File

@@ -10,9 +10,9 @@ import (
type Refund struct { type Refund struct {
core.Model core.Model
TradeID int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID TradeID int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID
ProductID *int32 `json:"product_id" gorm:"column:product_id"` // 产品ID ProductID *int32 `json:"product_id,omitempty" gorm:"column:product_id"` // 产品ID
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 退款金额 Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 退款金额
Reason *string `json:"reason" gorm:"column:reason"` // 退款原因 Reason *string `json:"reason,omitempty" gorm:"column:reason"` // 退款原因
Status RefundStatus `json:"status" gorm:"column:status"` // 退款状态0-待处理1-已退款2-已拒绝 Status RefundStatus `json:"status" gorm:"column:status"` // 退款状态0-待处理1-已退款2-已拒绝
} }

View File

@@ -8,13 +8,13 @@ import (
type Resource struct { type Resource struct {
core.Model core.Model
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
ResourceNo *string `json:"resource_no" gorm:"column:resource_no"` // 套餐编号 ResourceNo *string `json:"resource_no,omitempty" gorm:"column:resource_no"` // 套餐编号
Active bool `json:"active" gorm:"column:active"` // 套餐状态 Active bool `json:"active" gorm:"column:active"` // 套餐状态
Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型1-短效动态2-长效动态 Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型1-短效动态2-长效动态
User User `json:"user" gorm:"foreignKey:UserID"` User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Short *ResourceShort `json:"short" gorm:"foreignKey:ResourceID"` Short *ResourceShort `json:"short,omitempty" gorm:"foreignKey:ResourceID"`
Long *ResourceLong `json:"long" gorm:"foreignKey:ResourceID"` Long *ResourceLong `json:"long,omitempty" gorm:"foreignKey:ResourceID"`
} }
// ResourceType 套餐类型枚举 // ResourceType 套餐类型枚举

View File

@@ -8,12 +8,11 @@ import (
type ResourceLong struct { type ResourceLong struct {
ID int32 `json:"id" gorm:"column:id"` // ID ID int32 `json:"id" gorm:"column:id"` // ID
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
Live int32 `json:"live" gorm:"column:live"` // 可用时长(小时)
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型1-包时2-包量 Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型1-包时2-包量
Live int32 `json:"live" gorm:"column:live"` // 可用时长(天) Quota int32 `json:"quota" gorm:"column:quota"` // 每日配额(包时)或总配额(包量)
Expire *time.Time `json:"expire" gorm:"column:expire"` // 过期时间 ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 套餐过期时间,包时模式可用
Quota *int32 `json:"quota" gorm:"column:quota"` // 配额数 Used int32 `json:"used" gorm:"column:used"` // 总用
Used int32 `json:"used" gorm:"column:used"` // 已用数 Daily int32 `json:"daily" gorm:"column:daily"` // 当日用
DailyLimit int32 `json:"daily_limit" gorm:"column:daily_limit"` // 每日限制 LastAt *time.Time `json:"last_at,omitempty" gorm:"column:last_at"` // 最后使用时间
DailyUsed int32 `json:"daily_used" gorm:"column:daily_used"` // 今日已用数量
DailyLast *time.Time `json:"daily_last" gorm:"column:daily_last"` // 今日最后使用时间
} }

View File

@@ -8,12 +8,11 @@ import (
type ResourceShort struct { type ResourceShort struct {
ID int32 `json:"id" gorm:"column:id"` // ID ID int32 `json:"id" gorm:"column:id"` // ID
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型1-包时2-包量
Live int32 `json:"live" gorm:"column:live"` // 可用时长(秒) Live int32 `json:"live" gorm:"column:live"` // 可用时长(秒)
Expire *time.Time `json:"expire" gorm:"column:expire"` // 过期时间 Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型1-包时2-包量
Quota *int32 `json:"quota" gorm:"column:quota"` // 配额数量 Quota int32 `json:"quota" gorm:"column:quota"` // 每日配额(包时)或总配额(包量)
Used int32 `json:"used" gorm:"column:used"` // 已用数量 ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 套餐过期时间,包时模式可用
DailyLimit int32 `json:"daily_limit" gorm:"column:daily_limit"` // 每日限制 Used int32 `json:"used" gorm:"column:used"` // 总用量
DailyUsed int32 `json:"daily_used" gorm:"column:daily_used"` // 今日已用数 Daily int32 `json:"daily" gorm:"column:daily"` // 当日用
DailyLast *time.Time `json:"daily_last" gorm:"column:daily_last"` // 今日最后使用时间 LastAt *time.Time `json:"last_at,omitempty" gorm:"column:last_at"` // 最后使用时间
} }

View File

@@ -9,18 +9,18 @@ import (
// Session 会话表 // Session 会话表
type Session struct { type Session struct {
core.Model core.Model
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
AdminID *int32 `json:"admin_id" gorm:"column:admin_id"` // 管理员ID AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID
ClientID *int32 `json:"client_id" gorm:"column:client_id"` // 客户端ID ClientID *int32 `json:"client_id,omitempty" gorm:"column:client_id"` // 客户端ID
IP *orm.Inet `json:"ip" gorm:"column:ip"` // IP地址 IP *orm.Inet `json:"ip,omitempty" gorm:"column:ip"` // IP地址
UA *string `json:"ua" gorm:"column:ua"` // 用户代理 UA *string `json:"ua,omitempty" gorm:"column:ua"` // 用户代理
AccessToken string `json:"access_token" gorm:"column:access_token"` // 访问令牌 AccessToken string `json:"access_token" gorm:"column:access_token"` // 访问令牌
AccessTokenExpires time.Time `json:"access_token_expires" gorm:"column:access_token_expires"` // 访问令牌过期时间 AccessTokenExpires time.Time `json:"access_token_expires" gorm:"column:access_token_expires"` // 访问令牌过期时间
RefreshToken *string `json:"refresh_token" gorm:"column:refresh_token"` // 刷新令牌 RefreshToken *string `json:"refresh_token,omitempty" gorm:"column:refresh_token"` // 刷新令牌
RefreshTokenExpires *time.Time `json:"refresh_token_expires" gorm:"column:refresh_token_expires"` // 刷新令牌过期时间 RefreshTokenExpires *time.Time `json:"refresh_token_expires,omitempty" gorm:"column:refresh_token_expires"` // 刷新令牌过期时间
Scopes *string `json:"scopes" gorm:"column:scopes"` // 权限范围 Scopes *string `json:"scopes,omitempty" gorm:"column:scopes"` // 权限范围
User *User `json:"user" gorm:"foreignKey:UserID"` User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Admin *Admin `json:"admin" gorm:"foreignKey:AdminID"` Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"`
Client *Client `json:"client" gorm:"foreignKey:ClientID;belongsTo:ID"` Client *Client `json:"client,omitempty" gorm:"foreignKey:ClientID;belongsTo:ID"`
} }

View File

@@ -12,20 +12,20 @@ type Trade struct {
core.Model core.Model
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
InnerNo string `json:"inner_no" gorm:"column:inner_no"` // 内部订单号 InnerNo string `json:"inner_no" gorm:"column:inner_no"` // 内部订单号
OuterNo *string `json:"outer_no" gorm:"column:outer_no"` // 外部订单号 OuterNo *string `json:"outer_no,omitempty" gorm:"column:outer_no"` // 外部订单号
Type TradeType `json:"type" gorm:"column:type"` // 订单类型1-购买产品2-充值余额 Type TradeType `json:"type" gorm:"column:type"` // 订单类型1-购买产品2-充值余额
Subject string `json:"subject" gorm:"column:subject"` // 订单主题 Subject string `json:"subject" gorm:"column:subject"` // 订单主题
Remark *string `json:"remark" gorm:"column:remark"` // 订单备注 Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 订单备注
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 订单总金额 Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 订单总金额
Payment decimal.Decimal `json:"payment" gorm:"column:payment"` // 实际支付金额 Payment decimal.Decimal `json:"payment" gorm:"column:payment"` // 实际支付金额
Method TradeMethod `json:"method" gorm:"column:method"` // 支付方式1-支付宝2-微信3-商福通4-商福通渠道支付宝5-商福通渠道微信 Method TradeMethod `json:"method" gorm:"column:method"` // 支付方式1-支付宝2-微信3-商福通4-商福通渠道支付宝5-商福通渠道微信
Platform TradePlatform `json:"platform" gorm:"column:platform"` // 支付平台1-电脑网站2-手机网站 Platform TradePlatform `json:"platform" gorm:"column:platform"` // 支付平台1-电脑网站2-手机网站
Acquirer *TradeAcquirer `json:"acquirer" gorm:"column:acquirer"` // 收单机构1-支付宝2-微信3-银联 Acquirer *TradeAcquirer `json:"acquirer,omitempty" gorm:"column:acquirer"` // 收单机构1-支付宝2-微信3-银联
Status TradeStatus `json:"status" gorm:"column:status"` // 订单状态0-待支付1-已支付2-已取消 Status TradeStatus `json:"status" gorm:"column:status"` // 订单状态0-待支付1-已支付2-已取消
Refunded bool `json:"refunded" gorm:"column:refunded"` // 是否已退款 Refunded bool `json:"refunded" gorm:"column:refunded"` // 是否已退款
PaymentURL *string `json:"payment_url" gorm:"column:payment_url"` // 支付链接 PaymentURL *string `json:"payment_url,omitempty" gorm:"column:payment_url"` // 支付链接
CompletedAt *time.Time `json:"completed_at" gorm:"column:completed_at"` // 支付时间 CompletedAt *time.Time `json:"completed_at,omitempty" gorm:"column:completed_at"` // 支付时间
CanceledAt *time.Time `json:"canceled_at" gorm:"column:canceled_at"` // 取消时间 CanceledAt *time.Time `json:"canceled_at,omitempty" gorm:"column:canceled_at"` // 取消时间
} }
// TradeType 订单类型枚举 // TradeType 订单类型枚举

View File

@@ -11,25 +11,25 @@ import (
// User 用户表 // User 用户表
type User struct { type User struct {
core.Model core.Model
AdminID *int32 `json:"admin_id" gorm:"column:admin_id"` // 管理员ID AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID
Phone string `json:"phone" gorm:"column:phone"` // 手机号码 Phone string `json:"phone" gorm:"column:phone"` // 手机号码
Username *string `json:"username" gorm:"column:username"` // 用户名 Username *string `json:"username,omitempty" gorm:"column:username"` // 用户名
Email *string `json:"email" gorm:"column:email"` // 邮箱 Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱
Password *string `json:"password" gorm:"column:password"` // 用户密码 Password *string `json:"password,omitempty" gorm:"column:password"` // 用户密码
Name *string `json:"name" gorm:"column:name"` // 真实姓名 Name *string `json:"name,omitempty" gorm:"column:name"` // 真实姓名
Avatar *string `json:"avatar" gorm:"column:avatar"` // 头像URL Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL
Status UserStatus `json:"status" gorm:"column:status"` // 用户状态0-禁用1-正常 Status UserStatus `json:"status" gorm:"column:status"` // 用户状态0-禁用1-正常
Balance decimal.Decimal `json:"balance" gorm:"column:balance"` // 账户余额 Balance decimal.Decimal `json:"balance" gorm:"column:balance"` // 账户余额
IDType UserIDType `json:"id_type" gorm:"column:id_type"` // 认证类型0-未认证1-个人认证2-企业认证 IDType UserIDType `json:"id_type" gorm:"column:id_type"` // 认证类型0-未认证1-个人认证2-企业认证
IDNo *string `json:"id_no" gorm:"column:id_no"` // 身份证号或营业执照号 IDNo *string `json:"id_no,omitempty" gorm:"column:id_no"` // 身份证号或营业执照号
IDToken *string `json:"id_token" gorm:"column:id_token"` // 身份验证标识 IDToken *string `json:"id_token,omitempty" gorm:"column:id_token"` // 身份验证标识
ContactQQ *string `json:"contact_qq" gorm:"column:contact_qq"` // QQ联系方式 ContactQQ *string `json:"contact_qq,omitempty" gorm:"column:contact_qq"` // QQ联系方式
ContactWechat *string `json:"contact_wechat" gorm:"column:contact_wechat"` // 微信联系方式 ContactWechat *string `json:"contact_wechat,omitempty" gorm:"column:contact_wechat"` // 微信联系方式
LastLogin *time.Time `json:"last_login" gorm:"column:last_login"` // 最后登录时间 LastLogin *time.Time `json:"last_login,omitempty" gorm:"column:last_login"` // 最后登录时间
LastLoginIP *orm.Inet `json:"last_login_ip" gorm:"column:last_login_ip"` // 最后登录地址 LastLoginIP *orm.Inet `json:"last_login_ip,omitempty" gorm:"column:last_login_ip"` // 最后登录地址
LastLoginUA *string `json:"last_login_ua" gorm:"column:last_login_ua"` // 最后登录代理 LastLoginUA *string `json:"last_login_ua,omitempty" gorm:"column:last_login_ua"` // 最后登录代理
Admin Admin `json:"admin" gorm:"foreignKey:AdminID"` Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"`
} }
// UserStatus 用户状态枚举 // UserStatus 用户状态枚举

View File

@@ -8,7 +8,7 @@ import (
type UserRole struct { type UserRole struct {
core.Model core.Model
Name string `json:"name" gorm:"column:name"` // 角色名称 Name string `json:"name" gorm:"column:name"` // 角色名称
Description *string `json:"description" gorm:"column:description"` // 角色描述 Description *string `json:"description,omitempty" gorm:"column:description"` // 角色描述
Active bool `json:"active" gorm:"column:active"` // 是否激活 Active bool `json:"active" gorm:"column:active"` // 是否激活
Sort int32 `json:"sort" gorm:"column:sort"` // 排序 Sort int32 `json:"sort" gorm:"column:sort"` // 排序
} }

View File

@@ -10,5 +10,5 @@ type Whitelist struct {
core.Model core.Model
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址 IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
Remark *string `json:"remark" gorm:"column:remark"` // 备注 Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 备注
} }

View File

@@ -33,10 +33,12 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
_channel.DeletedAt = field.NewField(tableName, "deleted_at") _channel.DeletedAt = field.NewField(tableName, "deleted_at")
_channel.UserID = field.NewInt32(tableName, "user_id") _channel.UserID = field.NewInt32(tableName, "user_id")
_channel.ResourceID = field.NewInt32(tableName, "resource_id") _channel.ResourceID = field.NewInt32(tableName, "resource_id")
_channel.ProxyID = field.NewInt32(tableName, "proxy_id")
_channel.BatchNo = field.NewString(tableName, "batch_no") _channel.BatchNo = field.NewString(tableName, "batch_no")
_channel.ProxyID = field.NewInt32(tableName, "proxy_id")
_channel.Host = field.NewString(tableName, "host")
_channel.Port = field.NewUint16(tableName, "port") _channel.Port = field.NewUint16(tableName, "port")
_channel.EdgeID = field.NewInt32(tableName, "edge_id") _channel.EdgeID = field.NewInt32(tableName, "edge_id")
_channel.EdgeRef = field.NewString(tableName, "edge_ref")
_channel.FilterISP = field.NewInt(tableName, "filter_isp") _channel.FilterISP = field.NewInt(tableName, "filter_isp")
_channel.FilterProv = field.NewString(tableName, "filter_prov") _channel.FilterProv = field.NewString(tableName, "filter_prov")
_channel.FilterCity = field.NewString(tableName, "filter_city") _channel.FilterCity = field.NewString(tableName, "filter_city")
@@ -141,10 +143,12 @@ type channel struct {
DeletedAt field.Field DeletedAt field.Field
UserID field.Int32 UserID field.Int32
ResourceID field.Int32 ResourceID field.Int32
ProxyID field.Int32
BatchNo field.String BatchNo field.String
ProxyID field.Int32
Host field.String
Port field.Uint16 Port field.Uint16
EdgeID field.Int32 EdgeID field.Int32
EdgeRef field.String
FilterISP field.Int FilterISP field.Int
FilterProv field.String FilterProv field.String
FilterCity field.String FilterCity field.String
@@ -182,10 +186,12 @@ func (c *channel) updateTableName(table string) *channel {
c.DeletedAt = field.NewField(table, "deleted_at") c.DeletedAt = field.NewField(table, "deleted_at")
c.UserID = field.NewInt32(table, "user_id") c.UserID = field.NewInt32(table, "user_id")
c.ResourceID = field.NewInt32(table, "resource_id") c.ResourceID = field.NewInt32(table, "resource_id")
c.ProxyID = field.NewInt32(table, "proxy_id")
c.BatchNo = field.NewString(table, "batch_no") c.BatchNo = field.NewString(table, "batch_no")
c.ProxyID = field.NewInt32(table, "proxy_id")
c.Host = field.NewString(table, "host")
c.Port = field.NewUint16(table, "port") c.Port = field.NewUint16(table, "port")
c.EdgeID = field.NewInt32(table, "edge_id") c.EdgeID = field.NewInt32(table, "edge_id")
c.EdgeRef = field.NewString(table, "edge_ref")
c.FilterISP = field.NewInt(table, "filter_isp") c.FilterISP = field.NewInt(table, "filter_isp")
c.FilterProv = field.NewString(table, "filter_prov") c.FilterProv = field.NewString(table, "filter_prov")
c.FilterCity = field.NewString(table, "filter_city") c.FilterCity = field.NewString(table, "filter_city")
@@ -210,17 +216,19 @@ func (c *channel) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (c *channel) fillFieldMap() { func (c *channel) fillFieldMap() {
c.fieldMap = make(map[string]field.Expr, 22) c.fieldMap = make(map[string]field.Expr, 24)
c.fieldMap["id"] = c.ID c.fieldMap["id"] = c.ID
c.fieldMap["created_at"] = c.CreatedAt c.fieldMap["created_at"] = c.CreatedAt
c.fieldMap["updated_at"] = c.UpdatedAt c.fieldMap["updated_at"] = c.UpdatedAt
c.fieldMap["deleted_at"] = c.DeletedAt c.fieldMap["deleted_at"] = c.DeletedAt
c.fieldMap["user_id"] = c.UserID c.fieldMap["user_id"] = c.UserID
c.fieldMap["resource_id"] = c.ResourceID c.fieldMap["resource_id"] = c.ResourceID
c.fieldMap["proxy_id"] = c.ProxyID
c.fieldMap["batch_no"] = c.BatchNo c.fieldMap["batch_no"] = c.BatchNo
c.fieldMap["proxy_id"] = c.ProxyID
c.fieldMap["host"] = c.Host
c.fieldMap["port"] = c.Port c.fieldMap["port"] = c.Port
c.fieldMap["edge_id"] = c.EdgeID c.fieldMap["edge_id"] = c.EdgeID
c.fieldMap["edge_ref"] = c.EdgeRef
c.fieldMap["filter_isp"] = c.FilterISP c.fieldMap["filter_isp"] = c.FilterISP
c.fieldMap["filter_prov"] = c.FilterProv c.fieldMap["filter_prov"] = c.FilterProv
c.fieldMap["filter_city"] = c.FilterCity c.fieldMap["filter_city"] = c.FilterCity

View File

@@ -25,6 +25,7 @@ var (
Client *client Client *client
Coupon *coupon Coupon *coupon
Edge *edge Edge *edge
Inquiry *inquiry
LinkAdminRole *linkAdminRole LinkAdminRole *linkAdminRole
LinkAdminRolePermission *linkAdminRolePermission LinkAdminRolePermission *linkAdminRolePermission
LinkClientPermission *linkClientPermission LinkClientPermission *linkClientPermission
@@ -58,6 +59,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
Client = &Q.Client Client = &Q.Client
Coupon = &Q.Coupon Coupon = &Q.Coupon
Edge = &Q.Edge Edge = &Q.Edge
Inquiry = &Q.Inquiry
LinkAdminRole = &Q.LinkAdminRole LinkAdminRole = &Q.LinkAdminRole
LinkAdminRolePermission = &Q.LinkAdminRolePermission LinkAdminRolePermission = &Q.LinkAdminRolePermission
LinkClientPermission = &Q.LinkClientPermission LinkClientPermission = &Q.LinkClientPermission
@@ -92,6 +94,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
Client: newClient(db, opts...), Client: newClient(db, opts...),
Coupon: newCoupon(db, opts...), Coupon: newCoupon(db, opts...),
Edge: newEdge(db, opts...), Edge: newEdge(db, opts...),
Inquiry: newInquiry(db, opts...),
LinkAdminRole: newLinkAdminRole(db, opts...), LinkAdminRole: newLinkAdminRole(db, opts...),
LinkAdminRolePermission: newLinkAdminRolePermission(db, opts...), LinkAdminRolePermission: newLinkAdminRolePermission(db, opts...),
LinkClientPermission: newLinkClientPermission(db, opts...), LinkClientPermission: newLinkClientPermission(db, opts...),
@@ -127,6 +130,7 @@ type Query struct {
Client client Client client
Coupon coupon Coupon coupon
Edge edge Edge edge
Inquiry inquiry
LinkAdminRole linkAdminRole LinkAdminRole linkAdminRole
LinkAdminRolePermission linkAdminRolePermission LinkAdminRolePermission linkAdminRolePermission
LinkClientPermission linkClientPermission LinkClientPermission linkClientPermission
@@ -163,6 +167,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
Client: q.Client.clone(db), Client: q.Client.clone(db),
Coupon: q.Coupon.clone(db), Coupon: q.Coupon.clone(db),
Edge: q.Edge.clone(db), Edge: q.Edge.clone(db),
Inquiry: q.Inquiry.clone(db),
LinkAdminRole: q.LinkAdminRole.clone(db), LinkAdminRole: q.LinkAdminRole.clone(db),
LinkAdminRolePermission: q.LinkAdminRolePermission.clone(db), LinkAdminRolePermission: q.LinkAdminRolePermission.clone(db),
LinkClientPermission: q.LinkClientPermission.clone(db), LinkClientPermission: q.LinkClientPermission.clone(db),
@@ -206,6 +211,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
Client: q.Client.replaceDB(db), Client: q.Client.replaceDB(db),
Coupon: q.Coupon.replaceDB(db), Coupon: q.Coupon.replaceDB(db),
Edge: q.Edge.replaceDB(db), Edge: q.Edge.replaceDB(db),
Inquiry: q.Inquiry.replaceDB(db),
LinkAdminRole: q.LinkAdminRole.replaceDB(db), LinkAdminRole: q.LinkAdminRole.replaceDB(db),
LinkAdminRolePermission: q.LinkAdminRolePermission.replaceDB(db), LinkAdminRolePermission: q.LinkAdminRolePermission.replaceDB(db),
LinkClientPermission: q.LinkClientPermission.replaceDB(db), LinkClientPermission: q.LinkClientPermission.replaceDB(db),
@@ -239,6 +245,7 @@ type queryCtx struct {
Client *clientDo Client *clientDo
Coupon *couponDo Coupon *couponDo
Edge *edgeDo Edge *edgeDo
Inquiry *inquiryDo
LinkAdminRole *linkAdminRoleDo LinkAdminRole *linkAdminRoleDo
LinkAdminRolePermission *linkAdminRolePermissionDo LinkAdminRolePermission *linkAdminRolePermissionDo
LinkClientPermission *linkClientPermissionDo LinkClientPermission *linkClientPermissionDo
@@ -272,6 +279,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
Client: q.Client.WithContext(ctx), Client: q.Client.WithContext(ctx),
Coupon: q.Coupon.WithContext(ctx), Coupon: q.Coupon.WithContext(ctx),
Edge: q.Edge.WithContext(ctx), Edge: q.Edge.WithContext(ctx),
Inquiry: q.Inquiry.WithContext(ctx),
LinkAdminRole: q.LinkAdminRole.WithContext(ctx), LinkAdminRole: q.LinkAdminRole.WithContext(ctx),
LinkAdminRolePermission: q.LinkAdminRolePermission.WithContext(ctx), LinkAdminRolePermission: q.LinkAdminRolePermission.WithContext(ctx),
LinkClientPermission: q.LinkClientPermission.WithContext(ctx), LinkClientPermission: q.LinkClientPermission.WithContext(ctx),

359
web/queries/inquiry.gen.go Normal file
View File

@@ -0,0 +1,359 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package queries
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/plugin/dbresolver"
"platform/web/models"
)
func newInquiry(db *gorm.DB, opts ...gen.DOOption) inquiry {
_inquiry := inquiry{}
_inquiry.inquiryDo.UseDB(db, opts...)
_inquiry.inquiryDo.UseModel(&models.Inquiry{})
tableName := _inquiry.inquiryDo.TableName()
_inquiry.ALL = field.NewAsterisk(tableName)
_inquiry.ID = field.NewInt32(tableName, "id")
_inquiry.CreatedAt = field.NewTime(tableName, "created_at")
_inquiry.UpdatedAt = field.NewTime(tableName, "updated_at")
_inquiry.DeletedAt = field.NewField(tableName, "deleted_at")
_inquiry.Company = field.NewString(tableName, "company")
_inquiry.Name = field.NewString(tableName, "name")
_inquiry.Phone = field.NewString(tableName, "phone")
_inquiry.Email = field.NewString(tableName, "email")
_inquiry.Content = field.NewString(tableName, "content")
_inquiry.Status = field.NewInt(tableName, "status")
_inquiry.Remark = field.NewString(tableName, "remark")
_inquiry.fillFieldMap()
return _inquiry
}
type inquiry struct {
inquiryDo
ALL field.Asterisk
ID field.Int32
CreatedAt field.Time
UpdatedAt field.Time
DeletedAt field.Field
Company field.String
Name field.String
Phone field.String
Email field.String
Content field.String
Status field.Int
Remark field.String
fieldMap map[string]field.Expr
}
func (i inquiry) Table(newTableName string) *inquiry {
i.inquiryDo.UseTable(newTableName)
return i.updateTableName(newTableName)
}
func (i inquiry) As(alias string) *inquiry {
i.inquiryDo.DO = *(i.inquiryDo.As(alias).(*gen.DO))
return i.updateTableName(alias)
}
func (i *inquiry) updateTableName(table string) *inquiry {
i.ALL = field.NewAsterisk(table)
i.ID = field.NewInt32(table, "id")
i.CreatedAt = field.NewTime(table, "created_at")
i.UpdatedAt = field.NewTime(table, "updated_at")
i.DeletedAt = field.NewField(table, "deleted_at")
i.Company = field.NewString(table, "company")
i.Name = field.NewString(table, "name")
i.Phone = field.NewString(table, "phone")
i.Email = field.NewString(table, "email")
i.Content = field.NewString(table, "content")
i.Status = field.NewInt(table, "status")
i.Remark = field.NewString(table, "remark")
i.fillFieldMap()
return i
}
func (i *inquiry) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
_f, ok := i.fieldMap[fieldName]
if !ok || _f == nil {
return nil, false
}
_oe, ok := _f.(field.OrderExpr)
return _oe, ok
}
func (i *inquiry) fillFieldMap() {
i.fieldMap = make(map[string]field.Expr, 11)
i.fieldMap["id"] = i.ID
i.fieldMap["created_at"] = i.CreatedAt
i.fieldMap["updated_at"] = i.UpdatedAt
i.fieldMap["deleted_at"] = i.DeletedAt
i.fieldMap["company"] = i.Company
i.fieldMap["name"] = i.Name
i.fieldMap["phone"] = i.Phone
i.fieldMap["email"] = i.Email
i.fieldMap["content"] = i.Content
i.fieldMap["status"] = i.Status
i.fieldMap["remark"] = i.Remark
}
func (i inquiry) clone(db *gorm.DB) inquiry {
i.inquiryDo.ReplaceConnPool(db.Statement.ConnPool)
return i
}
func (i inquiry) replaceDB(db *gorm.DB) inquiry {
i.inquiryDo.ReplaceDB(db)
return i
}
type inquiryDo struct{ gen.DO }
func (i inquiryDo) Debug() *inquiryDo {
return i.withDO(i.DO.Debug())
}
func (i inquiryDo) WithContext(ctx context.Context) *inquiryDo {
return i.withDO(i.DO.WithContext(ctx))
}
func (i inquiryDo) ReadDB() *inquiryDo {
return i.Clauses(dbresolver.Read)
}
func (i inquiryDo) WriteDB() *inquiryDo {
return i.Clauses(dbresolver.Write)
}
func (i inquiryDo) Session(config *gorm.Session) *inquiryDo {
return i.withDO(i.DO.Session(config))
}
func (i inquiryDo) Clauses(conds ...clause.Expression) *inquiryDo {
return i.withDO(i.DO.Clauses(conds...))
}
func (i inquiryDo) Returning(value interface{}, columns ...string) *inquiryDo {
return i.withDO(i.DO.Returning(value, columns...))
}
func (i inquiryDo) Not(conds ...gen.Condition) *inquiryDo {
return i.withDO(i.DO.Not(conds...))
}
func (i inquiryDo) Or(conds ...gen.Condition) *inquiryDo {
return i.withDO(i.DO.Or(conds...))
}
func (i inquiryDo) Select(conds ...field.Expr) *inquiryDo {
return i.withDO(i.DO.Select(conds...))
}
func (i inquiryDo) Where(conds ...gen.Condition) *inquiryDo {
return i.withDO(i.DO.Where(conds...))
}
func (i inquiryDo) Order(conds ...field.Expr) *inquiryDo {
return i.withDO(i.DO.Order(conds...))
}
func (i inquiryDo) Distinct(cols ...field.Expr) *inquiryDo {
return i.withDO(i.DO.Distinct(cols...))
}
func (i inquiryDo) Omit(cols ...field.Expr) *inquiryDo {
return i.withDO(i.DO.Omit(cols...))
}
func (i inquiryDo) Join(table schema.Tabler, on ...field.Expr) *inquiryDo {
return i.withDO(i.DO.Join(table, on...))
}
func (i inquiryDo) LeftJoin(table schema.Tabler, on ...field.Expr) *inquiryDo {
return i.withDO(i.DO.LeftJoin(table, on...))
}
func (i inquiryDo) RightJoin(table schema.Tabler, on ...field.Expr) *inquiryDo {
return i.withDO(i.DO.RightJoin(table, on...))
}
func (i inquiryDo) Group(cols ...field.Expr) *inquiryDo {
return i.withDO(i.DO.Group(cols...))
}
func (i inquiryDo) Having(conds ...gen.Condition) *inquiryDo {
return i.withDO(i.DO.Having(conds...))
}
func (i inquiryDo) Limit(limit int) *inquiryDo {
return i.withDO(i.DO.Limit(limit))
}
func (i inquiryDo) Offset(offset int) *inquiryDo {
return i.withDO(i.DO.Offset(offset))
}
func (i inquiryDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *inquiryDo {
return i.withDO(i.DO.Scopes(funcs...))
}
func (i inquiryDo) Unscoped() *inquiryDo {
return i.withDO(i.DO.Unscoped())
}
func (i inquiryDo) Create(values ...*models.Inquiry) error {
if len(values) == 0 {
return nil
}
return i.DO.Create(values)
}
func (i inquiryDo) CreateInBatches(values []*models.Inquiry, batchSize int) error {
return i.DO.CreateInBatches(values, batchSize)
}
// Save : !!! underlying implementation is different with GORM
// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)
func (i inquiryDo) Save(values ...*models.Inquiry) error {
if len(values) == 0 {
return nil
}
return i.DO.Save(values)
}
func (i inquiryDo) First() (*models.Inquiry, error) {
if result, err := i.DO.First(); err != nil {
return nil, err
} else {
return result.(*models.Inquiry), nil
}
}
func (i inquiryDo) Take() (*models.Inquiry, error) {
if result, err := i.DO.Take(); err != nil {
return nil, err
} else {
return result.(*models.Inquiry), nil
}
}
func (i inquiryDo) Last() (*models.Inquiry, error) {
if result, err := i.DO.Last(); err != nil {
return nil, err
} else {
return result.(*models.Inquiry), nil
}
}
func (i inquiryDo) Find() ([]*models.Inquiry, error) {
result, err := i.DO.Find()
return result.([]*models.Inquiry), err
}
func (i inquiryDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.Inquiry, err error) {
buf := make([]*models.Inquiry, 0, batchSize)
err = i.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error {
defer func() { results = append(results, buf...) }()
return fc(tx, batch)
})
return results, err
}
func (i inquiryDo) FindInBatches(result *[]*models.Inquiry, batchSize int, fc func(tx gen.Dao, batch int) error) error {
return i.DO.FindInBatches(result, batchSize, fc)
}
func (i inquiryDo) Attrs(attrs ...field.AssignExpr) *inquiryDo {
return i.withDO(i.DO.Attrs(attrs...))
}
func (i inquiryDo) Assign(attrs ...field.AssignExpr) *inquiryDo {
return i.withDO(i.DO.Assign(attrs...))
}
func (i inquiryDo) Joins(fields ...field.RelationField) *inquiryDo {
for _, _f := range fields {
i = *i.withDO(i.DO.Joins(_f))
}
return &i
}
func (i inquiryDo) Preload(fields ...field.RelationField) *inquiryDo {
for _, _f := range fields {
i = *i.withDO(i.DO.Preload(_f))
}
return &i
}
func (i inquiryDo) FirstOrInit() (*models.Inquiry, error) {
if result, err := i.DO.FirstOrInit(); err != nil {
return nil, err
} else {
return result.(*models.Inquiry), nil
}
}
func (i inquiryDo) FirstOrCreate() (*models.Inquiry, error) {
if result, err := i.DO.FirstOrCreate(); err != nil {
return nil, err
} else {
return result.(*models.Inquiry), nil
}
}
func (i inquiryDo) FindByPage(offset int, limit int) (result []*models.Inquiry, count int64, err error) {
result, err = i.Offset(offset).Limit(limit).Find()
if err != nil {
return
}
if size := len(result); 0 < limit && 0 < size && size < limit {
count = int64(size + offset)
return
}
count, err = i.Offset(-1).Limit(-1).Count()
return
}
func (i inquiryDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) {
count, err = i.Count()
if err != nil {
return
}
err = i.Offset(offset).Limit(limit).Scan(result)
return
}
func (i inquiryDo) Scan(result interface{}) (err error) {
return i.DO.Scan(result)
}
func (i inquiryDo) Delete(models ...*models.Inquiry) (result gen.ResultInfo, err error) {
return i.DO.Delete(models)
}
func (i *inquiryDo) withDO(do gen.Dao) *inquiryDo {
i.DO = *do.(*gen.DO)
return i
}

View File

@@ -34,6 +34,7 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
_proxy.Version = field.NewInt32(tableName, "version") _proxy.Version = field.NewInt32(tableName, "version")
_proxy.Mac = field.NewString(tableName, "mac") _proxy.Mac = field.NewString(tableName, "mac")
_proxy.IP = field.NewField(tableName, "ip") _proxy.IP = field.NewField(tableName, "ip")
_proxy.Host = field.NewString(tableName, "host")
_proxy.Secret = field.NewString(tableName, "secret") _proxy.Secret = field.NewString(tableName, "secret")
_proxy.Type = field.NewInt(tableName, "type") _proxy.Type = field.NewInt(tableName, "type")
_proxy.Status = field.NewInt(tableName, "status") _proxy.Status = field.NewInt(tableName, "status")
@@ -120,6 +121,7 @@ type proxy struct {
Version field.Int32 Version field.Int32
Mac field.String Mac field.String
IP field.Field IP field.Field
Host field.String
Secret field.String Secret field.String
Type field.Int Type field.Int
Status field.Int Status field.Int
@@ -148,6 +150,7 @@ func (p *proxy) updateTableName(table string) *proxy {
p.Version = field.NewInt32(table, "version") p.Version = field.NewInt32(table, "version")
p.Mac = field.NewString(table, "mac") p.Mac = field.NewString(table, "mac")
p.IP = field.NewField(table, "ip") p.IP = field.NewField(table, "ip")
p.Host = field.NewString(table, "host")
p.Secret = field.NewString(table, "secret") p.Secret = field.NewString(table, "secret")
p.Type = field.NewInt(table, "type") p.Type = field.NewInt(table, "type")
p.Status = field.NewInt(table, "status") p.Status = field.NewInt(table, "status")
@@ -168,7 +171,7 @@ func (p *proxy) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (p *proxy) fillFieldMap() { func (p *proxy) fillFieldMap() {
p.fieldMap = make(map[string]field.Expr, 12) p.fieldMap = make(map[string]field.Expr, 13)
p.fieldMap["id"] = p.ID p.fieldMap["id"] = p.ID
p.fieldMap["created_at"] = p.CreatedAt p.fieldMap["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt p.fieldMap["updated_at"] = p.UpdatedAt
@@ -176,6 +179,7 @@ func (p *proxy) fillFieldMap() {
p.fieldMap["version"] = p.Version p.fieldMap["version"] = p.Version
p.fieldMap["mac"] = p.Mac p.fieldMap["mac"] = p.Mac
p.fieldMap["ip"] = p.IP p.fieldMap["ip"] = p.IP
p.fieldMap["host"] = p.Host
p.fieldMap["secret"] = p.Secret p.fieldMap["secret"] = p.Secret
p.fieldMap["type"] = p.Type p.fieldMap["type"] = p.Type
p.fieldMap["status"] = p.Status p.fieldMap["status"] = p.Status

View File

@@ -29,14 +29,13 @@ func newResourceLong(db *gorm.DB, opts ...gen.DOOption) resourceLong {
_resourceLong.ALL = field.NewAsterisk(tableName) _resourceLong.ALL = field.NewAsterisk(tableName)
_resourceLong.ID = field.NewInt32(tableName, "id") _resourceLong.ID = field.NewInt32(tableName, "id")
_resourceLong.ResourceID = field.NewInt32(tableName, "resource_id") _resourceLong.ResourceID = field.NewInt32(tableName, "resource_id")
_resourceLong.Type = field.NewInt(tableName, "type")
_resourceLong.Live = field.NewInt32(tableName, "live") _resourceLong.Live = field.NewInt32(tableName, "live")
_resourceLong.Expire = field.NewTime(tableName, "expire") _resourceLong.Type = field.NewInt(tableName, "type")
_resourceLong.Quota = field.NewInt32(tableName, "quota") _resourceLong.Quota = field.NewInt32(tableName, "quota")
_resourceLong.ExpireAt = field.NewTime(tableName, "expire_at")
_resourceLong.Used = field.NewInt32(tableName, "used") _resourceLong.Used = field.NewInt32(tableName, "used")
_resourceLong.DailyLimit = field.NewInt32(tableName, "daily_limit") _resourceLong.Daily = field.NewInt32(tableName, "daily")
_resourceLong.DailyUsed = field.NewInt32(tableName, "daily_used") _resourceLong.LastAt = field.NewTime(tableName, "last_at")
_resourceLong.DailyLast = field.NewTime(tableName, "daily_last")
_resourceLong.fillFieldMap() _resourceLong.fillFieldMap()
@@ -49,14 +48,13 @@ type resourceLong struct {
ALL field.Asterisk ALL field.Asterisk
ID field.Int32 ID field.Int32
ResourceID field.Int32 ResourceID field.Int32
Type field.Int
Live field.Int32 Live field.Int32
Expire field.Time Type field.Int
Quota field.Int32 Quota field.Int32
ExpireAt field.Time
Used field.Int32 Used field.Int32
DailyLimit field.Int32 Daily field.Int32
DailyUsed field.Int32 LastAt field.Time
DailyLast field.Time
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@@ -75,14 +73,13 @@ func (r *resourceLong) updateTableName(table string) *resourceLong {
r.ALL = field.NewAsterisk(table) r.ALL = field.NewAsterisk(table)
r.ID = field.NewInt32(table, "id") r.ID = field.NewInt32(table, "id")
r.ResourceID = field.NewInt32(table, "resource_id") r.ResourceID = field.NewInt32(table, "resource_id")
r.Type = field.NewInt(table, "type")
r.Live = field.NewInt32(table, "live") r.Live = field.NewInt32(table, "live")
r.Expire = field.NewTime(table, "expire") r.Type = field.NewInt(table, "type")
r.Quota = field.NewInt32(table, "quota") r.Quota = field.NewInt32(table, "quota")
r.ExpireAt = field.NewTime(table, "expire_at")
r.Used = field.NewInt32(table, "used") r.Used = field.NewInt32(table, "used")
r.DailyLimit = field.NewInt32(table, "daily_limit") r.Daily = field.NewInt32(table, "daily")
r.DailyUsed = field.NewInt32(table, "daily_used") r.LastAt = field.NewTime(table, "last_at")
r.DailyLast = field.NewTime(table, "daily_last")
r.fillFieldMap() r.fillFieldMap()
@@ -99,17 +96,16 @@ func (r *resourceLong) GetFieldByName(fieldName string) (field.OrderExpr, bool)
} }
func (r *resourceLong) fillFieldMap() { func (r *resourceLong) fillFieldMap() {
r.fieldMap = make(map[string]field.Expr, 10) r.fieldMap = make(map[string]field.Expr, 9)
r.fieldMap["id"] = r.ID r.fieldMap["id"] = r.ID
r.fieldMap["resource_id"] = r.ResourceID r.fieldMap["resource_id"] = r.ResourceID
r.fieldMap["type"] = r.Type
r.fieldMap["live"] = r.Live r.fieldMap["live"] = r.Live
r.fieldMap["expire"] = r.Expire r.fieldMap["type"] = r.Type
r.fieldMap["quota"] = r.Quota r.fieldMap["quota"] = r.Quota
r.fieldMap["expire_at"] = r.ExpireAt
r.fieldMap["used"] = r.Used r.fieldMap["used"] = r.Used
r.fieldMap["daily_limit"] = r.DailyLimit r.fieldMap["daily"] = r.Daily
r.fieldMap["daily_used"] = r.DailyUsed r.fieldMap["last_at"] = r.LastAt
r.fieldMap["daily_last"] = r.DailyLast
} }
func (r resourceLong) clone(db *gorm.DB) resourceLong { func (r resourceLong) clone(db *gorm.DB) resourceLong {

View File

@@ -29,14 +29,13 @@ func newResourceShort(db *gorm.DB, opts ...gen.DOOption) resourceShort {
_resourceShort.ALL = field.NewAsterisk(tableName) _resourceShort.ALL = field.NewAsterisk(tableName)
_resourceShort.ID = field.NewInt32(tableName, "id") _resourceShort.ID = field.NewInt32(tableName, "id")
_resourceShort.ResourceID = field.NewInt32(tableName, "resource_id") _resourceShort.ResourceID = field.NewInt32(tableName, "resource_id")
_resourceShort.Type = field.NewInt(tableName, "type")
_resourceShort.Live = field.NewInt32(tableName, "live") _resourceShort.Live = field.NewInt32(tableName, "live")
_resourceShort.Expire = field.NewTime(tableName, "expire") _resourceShort.Type = field.NewInt(tableName, "type")
_resourceShort.Quota = field.NewInt32(tableName, "quota") _resourceShort.Quota = field.NewInt32(tableName, "quota")
_resourceShort.ExpireAt = field.NewTime(tableName, "expire_at")
_resourceShort.Used = field.NewInt32(tableName, "used") _resourceShort.Used = field.NewInt32(tableName, "used")
_resourceShort.DailyLimit = field.NewInt32(tableName, "daily_limit") _resourceShort.Daily = field.NewInt32(tableName, "daily")
_resourceShort.DailyUsed = field.NewInt32(tableName, "daily_used") _resourceShort.LastAt = field.NewTime(tableName, "last_at")
_resourceShort.DailyLast = field.NewTime(tableName, "daily_last")
_resourceShort.fillFieldMap() _resourceShort.fillFieldMap()
@@ -49,14 +48,13 @@ type resourceShort struct {
ALL field.Asterisk ALL field.Asterisk
ID field.Int32 ID field.Int32
ResourceID field.Int32 ResourceID field.Int32
Type field.Int
Live field.Int32 Live field.Int32
Expire field.Time Type field.Int
Quota field.Int32 Quota field.Int32
ExpireAt field.Time
Used field.Int32 Used field.Int32
DailyLimit field.Int32 Daily field.Int32
DailyUsed field.Int32 LastAt field.Time
DailyLast field.Time
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@@ -75,14 +73,13 @@ func (r *resourceShort) updateTableName(table string) *resourceShort {
r.ALL = field.NewAsterisk(table) r.ALL = field.NewAsterisk(table)
r.ID = field.NewInt32(table, "id") r.ID = field.NewInt32(table, "id")
r.ResourceID = field.NewInt32(table, "resource_id") r.ResourceID = field.NewInt32(table, "resource_id")
r.Type = field.NewInt(table, "type")
r.Live = field.NewInt32(table, "live") r.Live = field.NewInt32(table, "live")
r.Expire = field.NewTime(table, "expire") r.Type = field.NewInt(table, "type")
r.Quota = field.NewInt32(table, "quota") r.Quota = field.NewInt32(table, "quota")
r.ExpireAt = field.NewTime(table, "expire_at")
r.Used = field.NewInt32(table, "used") r.Used = field.NewInt32(table, "used")
r.DailyLimit = field.NewInt32(table, "daily_limit") r.Daily = field.NewInt32(table, "daily")
r.DailyUsed = field.NewInt32(table, "daily_used") r.LastAt = field.NewTime(table, "last_at")
r.DailyLast = field.NewTime(table, "daily_last")
r.fillFieldMap() r.fillFieldMap()
@@ -99,17 +96,16 @@ func (r *resourceShort) GetFieldByName(fieldName string) (field.OrderExpr, bool)
} }
func (r *resourceShort) fillFieldMap() { func (r *resourceShort) fillFieldMap() {
r.fieldMap = make(map[string]field.Expr, 10) r.fieldMap = make(map[string]field.Expr, 9)
r.fieldMap["id"] = r.ID r.fieldMap["id"] = r.ID
r.fieldMap["resource_id"] = r.ResourceID r.fieldMap["resource_id"] = r.ResourceID
r.fieldMap["type"] = r.Type
r.fieldMap["live"] = r.Live r.fieldMap["live"] = r.Live
r.fieldMap["expire"] = r.Expire r.fieldMap["type"] = r.Type
r.fieldMap["quota"] = r.Quota r.fieldMap["quota"] = r.Quota
r.fieldMap["expire_at"] = r.ExpireAt
r.fieldMap["used"] = r.Used r.fieldMap["used"] = r.Used
r.fieldMap["daily_limit"] = r.DailyLimit r.fieldMap["daily"] = r.Daily
r.fieldMap["daily_used"] = r.DailyUsed r.fieldMap["last_at"] = r.LastAt
r.fieldMap["daily_last"] = r.DailyLast
} }
func (r resourceShort) clone(db *gorm.DB) resourceShort { func (r resourceShort) clone(db *gorm.DB) resourceShort {

View File

@@ -1,6 +1,7 @@
package web package web
import ( import (
"platform/pkg/env"
auth2 "platform/web/auth" auth2 "platform/web/auth"
"platform/web/handlers" "platform/web/handlers"
@@ -9,12 +10,30 @@ import (
func ApplyRouters(app *fiber.App) { func ApplyRouters(app *fiber.App) {
api := app.Group("/api") api := app.Group("/api")
userRouter(api)
adminRouter(api)
// 回调
callbacks := app.Group("/callback")
callbacks.Get("/identify", handlers.IdentifyCallbackNew)
// 临时
if env.RunMode == env.RunModeDev {
debug := app.Group("/debug")
debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin)
}
}
// 用户接口路由
func userRouter(api fiber.Router) {
// 认证 // 认证
auth := api.Group("/auth") auth := api.Group("/auth")
auth.Get("/authorize", auth2.AuthorizeGet)
auth.Post("/authorize", auth2.AuthorizePost)
auth.Post("/token", auth2.Token) auth.Post("/token", auth2.Token)
auth.Post("/revoke", handlers.Revoke) auth.Post("/revoke", auth2.Revoke)
auth.Post("/introspect", handlers.Introspect) auth.Post("/introspect", auth2.Introspect)
auth.Post("/verify/sms", handlers.SmsCode) auth.Post("/verify/sms", handlers.SmsCode)
// 用户 // 用户
@@ -34,12 +53,16 @@ func ApplyRouters(app *fiber.App) {
// 套餐 // 套餐
resource := api.Group("/resource") resource := api.Group("/resource")
resource.Post("/all", handlers.AllActiveResource) resource.Post("/all", handlers.AllActiveResource)
resource.Post("/list/short", handlers.ListResourceShort) resource.Post("/list/short", handlers.PageResourceShort)
resource.Post("/list/long", handlers.ListResourceLong) resource.Post("/list/long", handlers.PageResourceLong)
resource.Post("/statistics/free", handlers.StatisticResourceFree)
resource.Post("/statistics/usage", handlers.StatisticResourceUsage)
resource.Post("/create", handlers.CreateResource) resource.Post("/create", handlers.CreateResource)
resource.Post("/price", handlers.ResourcePrice) resource.Post("/price", handlers.ResourcePrice)
resource.Post("/statistics/free", handlers.StatisticResourceFree)
resource.Post("/statistics/usage", handlers.StatisticResourceUsage)
// 批次
batch := api.Group("/batch")
batch.Post("/page", handlers.PageResourceBatch)
// 通道 // 通道
channel := api.Group("/channel") channel := api.Group("/channel")
@@ -67,17 +90,45 @@ func ApplyRouters(app *fiber.App) {
proxy.Post("/online", handlers.ProxyReportOnline) proxy.Post("/online", handlers.ProxyReportOnline)
proxy.Post("/offline", handlers.ProxyReportOffline) proxy.Post("/offline", handlers.ProxyReportOffline)
proxy.Post("/update", handlers.ProxyReportUpdate) proxy.Post("/update", handlers.ProxyReportUpdate)
proxy.Post("/register/baidyin", handlers.ProxyRegisterBaiYin)
// 节点 // 节点
edge := api.Group("/edge") edge := api.Group("/edge")
edge.Post("/assign", handlers.AssignEdge) edge.Post("/assign", handlers.AssignEdge)
edge.Post("/all", handlers.AllEdgesAvailable) edge.Post("/all", handlers.AllEdgesAvailable)
// 临时 // 前台
debug := app.Group("/debug") inquiry := api.Group("/inquiry")
debug.Get("/sms/:phone", handlers.DebugGetSmsCode) inquiry.Post("/create", handlers.CreateInquiry)
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin) }
callbacks := app.Group("/callback") // 管理员接口路由
callbacks.Get("/identify", handlers.IdentifyCallbackNew) func adminRouter(api fiber.Router) {
api = api.Group("/admin")
// user 用户
var user = api.Group("/user")
user.Post("/page", handlers.PageUserByAdmin)
user.Post("/bind", handlers.BindAdmin)
// resource 套餐
var resource = api.Group("/resource")
resource.Post("/short/page", handlers.PageResourceShortByAdmin)
resource.Post("/long/page", handlers.PageResourceLongByAdmin)
// batch 批次
var usage = api.Group("batch")
usage.Post("/page", handlers.PageBatchByAdmin)
// channel 通道
var channel = api.Group("/channel")
channel.Post("/page", handlers.PageChannelsByAdmin)
// trade 交易
var trade = api.Group("trade")
trade.Post("/page", handlers.PageTradeByAdmin)
// bill 账单
var bill = api.Group("/bill")
bill.Post("/page", handlers.PageBillByAdmin)
} }

View File

@@ -2,23 +2,42 @@ package services
import ( import (
"context" "context"
"errors"
"fmt"
"math/rand/v2" "math/rand/v2"
"net/netip" "net/netip"
"platform/pkg/u"
"platform/web/core" "platform/web/core"
g "platform/web/globals" g "platform/web/globals"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"strconv"
"time" "time"
"github.com/redis/go-redis/v9"
"gorm.io/gen/field" "gorm.io/gen/field"
) )
var Channel ChannelService = &channelBaiyinService{}
// 通道服务 // 通道服务
type ChannelService interface { var Channel = &channelServer{
provider: &channelBaiyinProvider{},
}
type ChannelServiceProvider interface {
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error)
RemoveChannels(batch string, ids []int32) error RemoveChannels(batch string) error
}
type channelServer struct {
provider ChannelServiceProvider
}
func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter...)
}
func (s *channelServer) RemoveChannels(batch string) error {
return s.provider.RemoveChannels(batch)
} }
// 授权方式 // 授权方式
@@ -49,7 +68,7 @@ func genPassPair() (string, string) {
} }
// 查找资源 // 查找资源
func findResource(resourceId int32) (*ResourceView, error) { func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
resource, err := q.Resource. resource, err := q.Resource.
Preload(field.Associations). Preload(field.Associations).
Where( Where(
@@ -60,60 +79,48 @@ func findResource(resourceId int32) (*ResourceView, error) {
if err != nil { if err != nil {
return nil, ErrResourceNotExist return nil, ErrResourceNotExist
} }
if resource.User == nil {
return nil, ErrResourceNotExist
}
var info = &ResourceView{ var info = &ResourceView{
Id: resource.ID, Id: resource.ID,
User: *resource.User,
Active: resource.Active, Active: resource.Active,
Type: resource.Type, Type: resource.Type,
User: resource.User,
} }
switch resource.Type { switch resource.Type {
case m.ResourceTypeShort: case m.ResourceTypeShort:
var sub = resource.Short var sub = resource.Short
var dailyLast = time.Time{} info.ShortId = &sub.ID
if sub.DailyLast != nil { info.ExpireAt = sub.ExpireAt
dailyLast = time.Time(*sub.DailyLast)
}
var expire = time.Time{}
if sub.Expire != nil {
expire = time.Time(*sub.Expire)
}
var quota int32
if sub.Quota != nil {
quota = *sub.Quota
}
info.Mode = sub.Type
info.Live = time.Duration(sub.Live) * time.Second info.Live = time.Duration(sub.Live) * time.Second
info.DailyLimit = sub.DailyLimit info.Mode = sub.Type
info.DailyUsed = sub.DailyUsed info.Quota = sub.Quota
info.DailyLast = dailyLast
info.Expire = expire
info.Quota = quota
info.Used = sub.Used info.Used = sub.Used
info.Daily = sub.Daily
info.LastAt = sub.LastAt
if sub.LastAt != nil && u.IsSameDate(*sub.LastAt, now) {
info.Today = int(sub.Daily)
}
case m.ResourceTypeLong: case m.ResourceTypeLong:
var sub = resource.Long var sub = resource.Long
var dailyLast = time.Time{} info.LongId = &sub.ID
if sub.DailyLast != nil { info.ExpireAt = sub.ExpireAt
dailyLast = time.Time(*sub.DailyLast) info.Live = time.Duration(sub.Live) * time.Hour
}
var expire = time.Time{}
if sub.Expire != nil {
expire = time.Time(*sub.Expire)
}
var quota int32
if sub.Quota != nil {
quota = *sub.Quota
}
info.Mode = sub.Type info.Mode = sub.Type
info.Live = time.Duration(sub.Live) * time.Hour * 24 info.Quota = sub.Quota
info.DailyLimit = sub.DailyLimit
info.DailyUsed = sub.DailyUsed
info.DailyLast = dailyLast
info.Expire = expire
info.Quota = quota
info.Used = sub.Used info.Used = sub.Used
info.Daily = sub.Daily
info.LastAt = sub.LastAt
if sub.LastAt != nil && u.IsSameDate(*sub.LastAt, now) {
info.Today = int(sub.Daily)
}
}
if info.Mode == m.ResourceModeTime && info.ExpireAt == nil {
return nil, errors.New("检查套餐获取时间失败")
} }
return info, nil return info, nil
@@ -122,47 +129,137 @@ func findResource(resourceId int32) (*ResourceView, error) {
// ResourceView 套餐数据的简化视图,便于直接获取主要数据 // ResourceView 套餐数据的简化视图,便于直接获取主要数据
type ResourceView struct { type ResourceView struct {
Id int32 Id int32
User m.User
Active bool Active bool
Type m.ResourceType Type m.ResourceType
Mode m.ResourceMode ShortId *int32
LongId *int32
Live time.Duration Live time.Duration
DailyLimit int32 Mode m.ResourceMode
DailyUsed int32
DailyLast time.Time
Quota int32 Quota int32
ExpireAt *time.Time
Used int32 Used int32
Expire time.Time Daily int32
User m.User LastAt *time.Time
Today int // 今日用量
}
// 检查用户是否可提取
func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*ResourceView, []string, error) {
if count > 400 {
return nil, nil, core.NewBizErr("单次最多提取 400 个")
}
// 获取用户套餐
resource, err := findResource(resourceId, now)
if err != nil {
return nil, nil, err
}
// 检查用户
user := resource.User
if user.IDToken == nil || *user.IDToken == "" {
return nil, nil, core.NewBizErr("账号未实名")
}
// 获取用户白名单并检查用户 ip 地址
whitelists, err := q.Whitelist.Where(
q.Whitelist.UserID.Eq(user.ID),
).Find()
if err != nil {
return nil, nil, err
}
ips := make([]string, len(whitelists))
pass := false
for i, item := range whitelists {
ips[i] = item.IP.String()
if item.IP.Addr == source {
pass = true
}
}
if !pass {
return nil, nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String()))
}
// 检查套餐使用情况
switch resource.Mode {
default:
return nil, nil, core.NewBizErr("不支持的套餐模式")
// 包时
case m.ResourceModeTime:
// 检查过期时间
if resource.ExpireAt.Before(now) {
return nil, nil, ErrResourceExpired
}
// 检查每日限额
if count+resource.Today > int(resource.Quota) {
return nil, nil, ErrResourceDailyLimit
}
// 包量
case m.ResourceModeQuota:
// 检查可用配额
if int(resource.Quota)-int(resource.Used) < count {
return nil, nil, ErrResourceExhausted
}
}
return resource, ips, nil
} }
var ( var (
allChansKey = "channel:all"
freeChansKey = "channel:free" freeChansKey = "channel:free"
usedChansKey = "channel:used" usedChansKey = "channel:used"
) )
// 扩容通道
func regChans(proxy int32, chans []netip.AddrPort) error {
strs := make([]any, len(chans))
for i, ch := range chans {
strs[i] = ch.String()
}
key := freeChansKey + ":" + strconv.Itoa(int(proxy))
err := g.Redis.SAdd(context.Background(), key, strs...).Err()
if err != nil {
return fmt.Errorf("扩容通道失败: %w", err)
}
return nil
}
// 缩容通道
func remChans(proxy int32) error {
key := freeChansKey + ":" + strconv.Itoa(int(proxy))
err := g.Redis.SRem(context.Background(), key).Err()
if err != nil {
return fmt.Errorf("缩容通道失败: %w", err)
}
return nil
}
// 取用通道 // 取用通道
func lockChans(batch string, count int, expire time.Time) ([]netip.AddrPort, error) { func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) {
chans, err := g.Redis.Eval( pid := strconv.Itoa(int(proxy))
chans, err := RedisScriptLockChans.Run(
context.Background(), context.Background(),
RedisScriptLockChans, g.Redis,
[]string{ []string{
freeChansKey, freeChansKey + ":" + pid,
usedChansKey, usedChansKey + ":" + pid + ":" + batch,
usedChansKey + ":" + batch,
}, },
count, count,
expire.Unix(),
).StringSlice() ).StringSlice()
if err != nil { if err != nil {
return nil, core.NewBizErr("获取通道失败", err) return nil, fmt.Errorf("获取通道失败: %w", err)
} }
addrs := make([]netip.AddrPort, len(chans)) addrs := make([]netip.AddrPort, len(chans))
for i, ch := range chans { for i, ch := range chans {
addr, err := netip.ParseAddrPort(ch) addr, err := netip.ParseAddrPort(ch)
if err != nil { if err != nil {
return nil, core.NewServErr("解析通道数据失败", err) return nil, fmt.Errorf("解析通道数据失败: %w", err)
} }
addrs[i] = addr addrs[i] = addr
} }
@@ -170,41 +267,31 @@ func lockChans(batch string, count int, expire time.Time) ([]netip.AddrPort, err
return addrs, nil return addrs, nil
} }
var RedisScriptLockChans = ` var RedisScriptLockChans = redis.NewScript(`
local free_key = KEYS[1] local free_key = KEYS[1]
local used_key = KEYS[2] local batch_key = KEYS[2]
local batch_key = KEYS[3]
local count = tonumber(ARGV[1]) local count = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])
if redis.call("SCARD", free_key) < count then if redis.call("SCARD", free_key) < count then
return nil return nil
end end
local ports = redis.call("SPOP", free_key, count) local ports = redis.call("SPOP", free_key, count)
redis.call("ZADD", used_key, expire, batch_key)
redis.call("RPUSH", batch_key, unpack(ports)) redis.call("RPUSH", batch_key, unpack(ports))
return ports return ports
` `)
// 归还通道 // 归还通道
func freeChans(batch string, chans []string) error { func freeChans(proxy int32, batch string) error {
values := make([]any, len(chans)) pid := strconv.Itoa(int(proxy))
for i, ch := range chans { err := RedisScriptFreeChans.Run(
values[i] = ch
}
err := g.Redis.Eval(
context.Background(), context.Background(),
RedisScriptFreeChans, g.Redis,
[]string{ []string{
freeChansKey, freeChansKey + ":" + pid,
usedChansKey, usedChansKey + ":" + pid + ":" + batch,
usedChansKey + ":" + batch,
allChansKey,
}, },
values...,
).Err() ).Err()
if err != nil { if err != nil {
return core.NewBizErr("释放通道失败", err) return core.NewBizErr("释放通道失败", err)
@@ -213,88 +300,19 @@ func freeChans(batch string, chans []string) error {
return nil return nil
} }
var RedisScriptFreeChans = ` var RedisScriptFreeChans = redis.NewScript(`
local free_key = KEYS[1] local free_key = KEYS[1]
local used_key = KEYS[2] local batch_key = KEYS[2]
local batch_key = KEYS[3]
local all_key = KEYS[4]
local chans = ARGV
local count = 0 local chans = redis.call("LRANGE", batch_key, 0, -1)
for i, chan in ipairs(chans) do
if redis.call("SISMEMBER", all_key, chan) == 1 then
redis.call("SADD", free_key, chan)
count = count + 1
end
end
redis.call("ZREM", used_key, batch_key)
redis.call("DEL", batch_key) redis.call("DEL", batch_key)
return count if redis.call("EXISTS", free_key) == 1 then
` redis.call("SADD", free_key, unpack(chans))
end
// 扩容通道
func addChans(chans []netip.AddrPort) error {
strs := make([]string, len(chans))
for i, ch := range chans {
strs[i] = ch.String()
}
err := g.Redis.Eval(
context.Background(),
RedisScriptAddChans,
[]string{
freeChansKey,
allChansKey,
},
strs,
).Err()
if err != nil {
return core.NewBizErr("扩容通道失败", err)
}
return nil
}
var RedisScriptAddChans = `
local free_key = KEYS[1]
local all_key = KEYS[2]
local chans = ARGV
redis.call("SADD", free_key, unpack(chans))
redis.call("SADD", all_key, unpack(chans))
return 1 return 1
` `)
// 缩容通道
func removeChans(chans []string) error {
err := g.Redis.Eval(
context.Background(),
RedisScriptRemoveChans,
[]string{
freeChansKey,
allChansKey,
},
chans,
).Err()
if err != nil {
return core.NewBizErr("缩容通道失败", err)
}
return nil
}
var RedisScriptRemoveChans = `
local free_key = KEYS[1]
local all_key = KEYS[2]
local chans = ARGV
redis.call("SREM", free_key, unpack(chans))
redis.call("SREM", all_key, unpack(chans))
return 1
`
// 错误信息 // 错误信息
var ( var (

View File

@@ -1,7 +1,6 @@
package services package services
import ( import (
"database/sql/driver"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -18,16 +17,13 @@ import (
"time" "time"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
"gorm.io/gen"
"gorm.io/gen/field" "gorm.io/gen/field"
) )
type channelBaiyinService struct{} type channelBaiyinProvider struct{}
func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
if count > 400 {
return nil, core.NewBizErr("单次最多提取 400 个")
}
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
var filter *EdgeFilter = nil var filter *EdgeFilter = nil
if len(edgeFilter) > 0 { if len(edgeFilter) > 0 {
filter = &edgeFilter[0] filter = &edgeFilter[0]
@@ -36,111 +32,180 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
now := time.Now() now := time.Now()
batch := ID.GenReadable("bat") batch := ID.GenReadable("bat")
// 获取用户套餐 // 检查并获取套餐与白名单
resource, err := findResource(resourceId) resource, whitelists, err := ensure(now, source, resourceId, count)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 检查用户
user := resource.User user := resource.User
if user.IDToken == nil || *user.IDToken == "" {
return nil, core.NewBizErr("账号未实名")
}
// 获取用户白名单并检查用户 ip 地址
whitelists, err := q.Whitelist.Where(
q.Whitelist.UserID.Eq(user.ID),
).Find()
if err != nil {
return nil, err
}
whitelistIPs := make([]string, len(whitelists))
pass := false
for i, item := range whitelists {
whitelistIPs[i] = item.IP.String()
if item.IP.Addr == source {
pass = true
}
}
if !pass {
return nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String()))
}
// 检查套餐使用情况
switch resource.Mode {
default:
return nil, core.NewBizErr("不支持的套餐模式")
// 包时
case m.ResourceModeTime:
// 检查过期时间
if resource.Expire.Before(now) {
return nil, ErrResourceExpired
}
// 检查每日限额
used := 0
if now.Format("2006-01-02") == resource.DailyLast.Format("2006-01-02") {
used = int(resource.DailyUsed)
}
excess := used+count > int(resource.DailyLimit)
if excess {
return nil, ErrResourceDailyLimit
}
// 包量
case m.ResourceModeQuota:
// 检查可用配额
if int(resource.Quota)-int(resource.Used) < count {
return nil, ErrResourceExhausted
}
}
expire := now.Add(resource.Live) expire := now.Add(resource.Live)
// 获取可用通道 // 选择代理
chans, err := lockChans(batch, count, expire) proxyResult := struct {
if err != nil { m.Proxy
return nil, err Count int
} }{}
err = q.Proxy.
// 获取对应代理 LeftJoin(q.Channel, q.Channel.ProxyID.EqCol(q.Proxy.ID), q.Channel.ExpiredAt.Gt(now)).
ips := make([]driver.Valuer, 0) Select(q.Proxy.ALL, field.NewUnsafeFieldRaw("10000 - count(*)").As("count")).
findProxy := make(map[orm.Inet]*m.Proxy) Where(
for _, ch := range chans {
ip := orm.Inet{Addr: ch.Addr()}
if _, ok := findProxy[ip]; !ok {
ips = append(ips, ip)
findProxy[ip] = nil
}
}
proxies, err := q.Proxy.Where(
q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)), q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)),
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)), q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
q.Proxy.IP.In(ips...), ).
).Find() Group(q.Proxy.ID).
Order(field.NewField("", "count")).
Limit(1).Scan(&proxyResult)
if err != nil { if err != nil {
return nil, core.NewBizErr("获取代理失败", err) return nil, core.NewBizErr("获取可用代理失败", err)
}
if proxyResult.Count < count {
return nil, core.NewBizErr("无可用主机,请稍后再试")
}
proxy := proxyResult.Proxy
// 获取可用通道
chans, err := lockChans(proxy.ID, batch, count)
if err != nil {
return nil, core.NewBizErr("无可用通道,请稍后再试", err)
} }
groups := make(map[*m.Proxy][]*m.Channel) // 获取可用节点
for _, proxy := range proxies { edgesResp, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{
findProxy[proxy.IP] = proxy Province: filter.Prov,
groups[proxy] = make([]*m.Channel, 0) City: filter.City,
Isp: u.X(filter.Isp.String()),
Limit: &count,
NoRepeat: u.P(true),
NoDayRepeat: u.P(true),
ActiveTime: u.P(3600),
IpUnchangedTime: u.P(3600),
Sort: u.P("ip_unchanged_time_asc"),
})
if err != nil {
return nil, core.NewBizErr("获取可用节点失败", err)
} }
if edgesResp.Total != count && len(edgesResp.Edges) != count {
return nil, core.NewBizErr("地区可用节点数量不足 [%s, %s] [%s]")
}
edges := edgesResp.Edges
// 准备通道数据 // 准备通道数据
actions := make([]*m.LogsUserUsage, len(chans)) channels := make([]*m.Channel, count)
channels := make([]*m.Channel, len(chans)) chanConfigs := make([]*g.PortConfigsReq, count)
for i, ch := range chans { edgeConfigs := make([]string, count)
for i := range count {
ch := chans[i]
edge := edges[i]
if err != nil { if err != nil {
return nil, core.NewBizErr("解析通道地址失败", err) return nil, core.NewBizErr("解析通道地址失败", err)
} }
// 使用记录 // 通道数据
actions[i] = &m.LogsUserUsage{ channels[i] = &m.Channel{
UserID: user.ID,
ResourceID: resourceId,
BatchNo: batch,
ProxyID: proxy.ID,
Host: u.Else(proxy.Host, proxy.IP.String()),
Port: ch.Port(),
EdgeRef: u.P(edge.EdgeID),
FilterISP: filter.Isp,
FilterProv: filter.Prov,
FilterCity: filter.City,
ExpiredAt: expire,
}
// 通道配置数据
chanConfigs[i] = &g.PortConfigsReq{
Port: int(ch.Port()),
Status: true,
Edge: &[]string{edge.EdgeID},
}
// 白名单模式
if authWhitelist {
channels[i].Whitelists = u.P(strings.Join(whitelists, ","))
chanConfigs[i].Whitelist = &whitelists
}
// 密码模式
if authPassword {
username, password := genPassPair()
channels[i].Username = &username
channels[i].Password = &password
chanConfigs[i].Userpass = u.P(username + ":" + password)
}
// 连接配置数据
edgeConfigs[i] = edge.EdgeID
}
// 提交异步任务关闭通道
_, err = g.Asynq.Enqueue(
e.NewRemoveChannel(batch),
asynq.ProcessAt(expire),
)
if err != nil {
return nil, core.NewServErr("提交关闭通道任务失败", err)
}
// 保存数据
err = q.Q.Transaction(func(q *q.Query) error {
var rs gen.ResultInfo
// 根据套餐类型和模式更新使用记录
isShortType := resource.Type == m.ResourceTypeShort
isLongType := resource.Type == m.ResourceTypeLong
switch {
case isShortType:
rs, err = q.ResourceShort.
Where(
q.ResourceShort.ID.Eq(*resource.ShortId),
q.ResourceShort.Used.Eq(resource.Used),
q.ResourceShort.Daily.Eq(resource.Daily),
).
UpdateSimple(
q.ResourceShort.Used.Add(int32(count)),
q.ResourceShort.Daily.Value(int32(resource.Today+count)),
q.ResourceShort.LastAt.Value(now),
)
case isLongType:
rs, err = q.ResourceLong.Debug().
Where(
q.ResourceLong.ID.Eq(*resource.LongId),
q.ResourceLong.Used.Eq(resource.Used),
q.ResourceLong.Daily.Eq(resource.Daily),
).
UpdateSimple(
q.ResourceLong.Used.Add(int32(count)),
q.ResourceLong.Daily.Value(int32(resource.Today+count)),
q.ResourceLong.LastAt.Value(now),
)
default:
return core.NewServErr("套餐类型不正确,无法更新", nil)
}
if err != nil {
return core.NewServErr("更新套餐使用记录失败", err)
}
if rs.RowsAffected == 0 {
return core.NewServErr("套餐使用记录不存在")
}
// 保存通道
err = q.Channel.
Omit(field.AssociationFields).
Create(channels...)
if err != nil {
return core.NewServErr("保存通道失败", err)
}
// 保存提取记录
err = q.LogsUserUsage.Create(&m.LogsUserUsage{
UserID: user.ID, UserID: user.ID,
ResourceID: resourceId, ResourceID: resourceId,
BatchNo: batch, BatchNo: batch,
@@ -150,86 +215,7 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
City: filter.City, City: filter.City,
IP: orm.Inet{Addr: source}, IP: orm.Inet{Addr: source},
Time: now, Time: now,
} })
// 通道数据
inet := orm.Inet{Addr: ch.Addr()}
channels[i] = &m.Channel{
UserID: user.ID,
ResourceID: resourceId,
BatchNo: batch,
ProxyID: findProxy[inet].ID,
Port: ch.Port(),
FilterISP: filter.Isp,
FilterProv: filter.Prov,
FilterCity: filter.City,
ExpiredAt: expire,
Proxy: *findProxy[inet],
}
if authWhitelist {
channels[i].Whitelists = u.P(strings.Join(whitelistIPs, ","))
}
if authPassword {
username, password := genPassPair()
channels[i].Username = &username
channels[i].Password = &password
}
// 关联代理
proxy := findProxy[inet]
groups[proxy] = append(groups[proxy], channels[i])
}
// 保存数据
err = q.Q.Transaction(func(q *q.Query) error {
// 更新套餐用量
used := int32(count)
if u.IsSameDate(now, resource.DailyLast) {
used += resource.DailyUsed
}
switch resource.Type {
case m.ResourceTypeShort:
_, err = q.ResourceShort.
Where(
q.ResourceShort.ResourceID.Eq(resource.Id),
q.ResourceShort.Used.Eq(resource.Used),
q.ResourceShort.DailyUsed.Eq(resource.DailyUsed),
q.ResourceShort.DailyLast.Eq(resource.DailyLast),
).
UpdateSimple(
q.ResourceShort.Used.Add(int32(count)),
q.ResourceShort.DailyUsed.Value(used),
q.ResourceShort.DailyLast.Value(now),
)
case m.ResourceTypeLong:
_, err = q.ResourceLong.
Where(
q.ResourceLong.ResourceID.Eq(resource.Id),
q.ResourceLong.Used.Eq(resource.Used),
q.ResourceLong.DailyUsed.Eq(resource.DailyUsed),
q.ResourceLong.DailyLast.Eq(resource.DailyLast),
).
UpdateSimple(
q.ResourceLong.Used.Add(int32(count)),
q.ResourceLong.DailyUsed.Value(used),
q.ResourceLong.DailyLast.Value(now),
)
}
if err != nil {
return core.NewServErr("更新套餐使用记录失败", err)
}
// 保存通道和分配记录
err = q.Channel.
Omit(field.AssociationFields).
Create(channels...)
if err != nil {
return core.NewServErr("保存通道失败", err)
}
err = q.LogsUserUsage.Create(actions...)
if err != nil { if err != nil {
return core.NewServErr("保存用户使用记录失败", err) return core.NewServErr("保存用户使用记录失败", err)
} }
@@ -240,118 +226,100 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
return nil, err return nil, err
} }
// 提交异步任务关闭通道
_, err = g.Asynq.Enqueue(
e.NewRemoveChannel(e.RemoveChannelData{
Batch: batch,
IDs: core.GetIDs(channels),
}),
asynq.ProcessAt(expire),
)
if err != nil {
return nil, core.NewServErr("提交关闭通道任务失败", err)
}
// 提交配置 // 提交配置
for proxy, chanels := range groups {
secret := strings.Split(u.Z(proxy.Secret), ":") secret := strings.Split(u.Z(proxy.Secret), ":")
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1]) gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
configs := make([]g.PortConfigsReq, len(chanels))
for i, channel := range chanels {
configs[i] = g.PortConfigsReq{
Port: int(channel.Port),
Status: true,
AutoEdgeConfig: &g.AutoEdgeConfig{
Isp: channel.FilterISP.String(),
Province: u.Z(channel.FilterProv),
City: u.Z(channel.FilterCity),
},
}
if authWhitelist {
configs[i].Whitelist = &whitelistIPs
}
if authPassword {
configs[i].Userpass = u.P(fmt.Sprintf("%s:%s", *channel.Username, *channel.Password))
}
}
if env.DebugExternalChange { if env.DebugExternalChange {
err := gateway.GatewayPortConfigs(configs)
// 连接节点到网关
err = g.Cloud.CloudConnect(&g.CloudConnectReq{
Uuid: proxy.Mac,
Edge: &edgeConfigs,
})
if err != nil {
return nil, core.NewServErr("连接云平台失败", err)
}
// 启用网关代理通道
err = gateway.GatewayPortConfigs(chanConfigs)
if err != nil { if err != nil {
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err) return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
} }
} else { } else {
bytes, _ := json.Marshal(configs) slog.Debug("提交代理端口配置", "proxy", proxy.IP.String())
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "config", string(bytes)) for _, item := range chanConfigs {
str, _ := json.Marshal(item)
fmt.Println(string(str))
} }
} }
return channels, nil return channels, nil
} }
func (s *channelBaiyinService) RemoveChannels(batch string, ids []int32) error { func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
start := time.Now() start := time.Now()
// 获取连接数据 // 获取连接数据
channels, err := q.Channel. channels, err := q.Channel.Where(q.Channel.BatchNo.Eq(batch)).Find()
Preload(q.Channel.Proxy).
Where(q.Channel.ID.In(ids...)).
Find()
if err != nil { if err != nil {
return core.NewServErr("获取通道数据失败", err) return core.NewServErr(fmt.Sprintf("获取通道数据失败batch%s", batch), err)
} }
if len(channels) != len(ids) { if len(channels) == 0 {
return core.NewServErr("获取通道数据不完整", err) slog.Warn(fmt.Sprintf("未找到通道数据batch%s", batch))
return nil
} }
proxies := make(map[string]*m.Proxy, len(channels)) proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(channels[0].ProxyID)).Take()
groups := make(map[string][]*m.Channel, len(channels))
chans := make([]string, len(channels))
for i, channel := range channels {
ip := channel.Proxy.IP.String()
groups[ip] = append(groups[ip], channel)
proxies[ip] = &channel.Proxy
chans[i] = fmt.Sprintf("%s:%d", ip, channel.Port)
}
addrs := make([]netip.AddrPort, len(channels))
for i, channel := range channels {
addrs[i] = netip.AddrPortFrom(channel.Proxy.IP.Addr, channel.Port)
}
// 释放端口
err = freeChans(batch, chans)
if err != nil { if err != nil {
return err return core.NewServErr(fmt.Sprintf("获取代理数据失败batch%s", batch), err)
} }
// 清空配置 // 准备配置数据
for ip, channels := range groups { edgeConfigs := make([]string, len(channels))
proxy := proxies[ip] configs := make([]*g.PortConfigsReq, len(channels))
secret := strings.Split(*proxy.Secret, ":")
gateway := g.NewGateway(ip, secret[0], secret[1])
configs := make([]g.PortConfigsReq, len(channels))
for i, channel := range channels { for i, channel := range channels {
configs[i] = g.PortConfigsReq{ if channel.EdgeRef != nil {
edgeConfigs[i] = *channel.EdgeRef
} else {
slog.Warn(fmt.Sprintf("通道 %d 没有保存节点引用", channel.ID))
}
configs[i] = &g.PortConfigsReq{
Status: false, Status: false,
Port: int(channel.Port), Port: int(channel.Port),
Edge: &[]string{}, Edge: &[]string{},
} }
} }
// 提交配置
if env.DebugExternalChange { if env.DebugExternalChange {
// 断开节点连接
g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
Uuid: proxy.Mac,
Edge: &edgeConfigs,
})
// 清空通道配置
secret := strings.Split(*proxy.Secret, ":")
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
err := gateway.GatewayPortConfigs(configs) err := gateway.GatewayPortConfigs(configs)
if err != nil { if err != nil {
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err) return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
} }
} else { } else {
bytes, _ := json.Marshal(configs) slog.Debug("清除代理端口配置", "proxy", proxy.IP)
slog.Debug("清除代理端口配置", "proxy", ip, "config", string(bytes)) for _, item := range configs {
str, _ := json.Marshal(item)
fmt.Println(string(str))
} }
} }
// 释放端口
err = freeChans(proxy.ID, batch)
if err != nil {
return err
}
slog.Debug("清除代理端口配置", "time", time.Since(start).String()) slog.Debug("清除代理端口配置", "time", time.Since(start).String())
return nil return nil
} }

View File

@@ -44,48 +44,28 @@ var (
) )
func (s *IdService) GenSerial() (string, error) { func (s *IdService) GenSerial() (string, error) {
var ctx = context.Background()
// 构造Redis键
now := time.Now().Unix() now := time.Now().Unix()
key := idSerialKey(now)
// 使用Redis事务确保原子操作 // 脚本实现原子操作
var sequence int64 script := redis.NewScript(`
err := g.Redis.Watch(ctx, func(tx *redis.Tx) error { local current = tonumber(redis.call('GET', KEYS[1])) or 0
if current >= tonumber(ARGV[1]) then
return redis.error_reply('sequence overflow')
end
// 获取当前序列号 local sequence = current + 1
currentVal, err := tx.Get(ctx, key).Int64() redis.call('SET', KEYS[1], sequence, 'EX', ARGV[2])
if err != nil && !errors.Is(err, redis.Nil) {
return err
}
if errors.Is(err, redis.Nil) { return sequence
currentVal = 0 `)
} sequence, err := script.Run(context.Background(), g.Redis, []string{idSerialKey(now)}, maxSequence, redisTTL).Int64()
sequence = currentVal + 1
// 检查序列号是否溢出
if sequence > maxSequence {
return ErrSequenceOverflow
}
// 将更新后的序列号保存回Redis设置5秒过期时间
pipe := tx.Pipeline()
pipe.Set(ctx, key, sequence, redisTTL*time.Second)
_, err = pipe.Exec(ctx)
return err
}, key)
if err != nil { if err != nil {
return "", err return "", err
} }
// 组装最终ID // 组装最终ID
id := uint64((now << timestampShift) | sequence) id := uint64((now << timestampShift) | sequence)
return strconv.FormatUint(id, 10), nil
idStr := strconv.FormatUint(id, 10)
return idStr, nil
} }
// ParseSerial 解析ID返回其组成部分 // ParseSerial 解析ID返回其组成部分
@@ -98,7 +78,7 @@ func (s *IdService) ParseSerial(id uint64) (timestamp int64, sequence int64) {
// idSerialKey 根据时间戳生成Redis键 // idSerialKey 根据时间戳生成Redis键
func idSerialKey(timestamp int64) string { func idSerialKey(timestamp int64) string {
return fmt.Sprintf("global:id:serial:%d", timestamp) return fmt.Sprintf("id:serial:%d", timestamp)
} }
// endregion // endregion

View File

@@ -31,28 +31,29 @@ func (s *proxyService) AllProxies(proxyType m.ProxyType, channels bool) ([]*m.Pr
} }
// RegisterBaiyin 注册新代理服务 // RegisterBaiyin 注册新代理服务
func (s *proxyService) RegisterBaiyin(Mac string, IP netip.Addr, username, password string) error { func (s *proxyService) RegisterBaiyin(Name string, IP netip.Addr, username, password string) error {
// 保存代理信息
proxy := &m.Proxy{
Version: 0,
Mac: Name,
IP: orm.Inet{Addr: IP},
Secret: u.P(fmt.Sprintf("%s:%s", username, password)),
Type: m.ProxyTypeBaiYin,
Status: m.ProxyStatusOnline,
}
if err := q.Proxy.Create(proxy); err != nil {
return core.NewServErr("保存通道数据失败")
}
// 添加可用通道到 redis // 添加可用通道到 redis
chans := make([]netip.AddrPort, 10000) chans := make([]netip.AddrPort, 10000)
for i := range 10000 { for i := range 10000 {
chans[i] = netip.AddrPortFrom(IP, uint16(i+10000)) chans[i] = netip.AddrPortFrom(IP, uint16(i+10000))
} }
err := addChans(chans) err := regChans(proxy.ID, chans)
if err != nil { if err != nil {
return core.NewServErr("添加通道失败") return core.NewServErr("添加通道失败", err)
}
// 保存代理信息
if err := q.Proxy.Create(&m.Proxy{
Version: 0,
Mac: Mac,
IP: orm.Inet{Addr: IP},
Secret: u.P(fmt.Sprintf("%s:%s", username, password)),
Type: m.ProxyTypeBaiYin,
Status: m.ProxyStatusOnline,
}); err != nil {
return core.NewServErr("保存通道数据失败")
} }
return nil return nil

View File

@@ -2,10 +2,10 @@ package services
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"platform/pkg/u" "platform/pkg/u"
"platform/web/core" "platform/web/core"
g "platform/web/globals"
m "platform/web/models" m "platform/web/models"
q "platform/web/queries" q "platform/web/queries"
"time" "time"
@@ -18,8 +18,7 @@ var Resource = &resourceService{}
type resourceService struct{} type resourceService struct{}
func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data *CreateResourceData) error { func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data *CreateResourceData) error {
return g.Redsync.WithLock(userBalanceKey(uid), func() error {
return q.Q.Transaction(func(q *q.Query) error {
// 找到用户 // 找到用户
user, err := q.User. user, err := q.User.
Where(q.User.ID.Eq(uid)). Where(q.User.ID.Eq(uid)).
@@ -29,15 +28,24 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data
} }
// 检查余额 // 检查余额
var amount = user.Balance.Sub(data.GetAmount()) amount, err := data.GetAmount()
if amount.IsNegative() { if err != nil {
return err
}
newBalance := user.Balance.Sub(amount)
if newBalance.IsNegative() {
return ErrBalanceNotEnough return ErrBalanceNotEnough
} }
return q.Q.Transaction(func(q *q.Query) error {
// 更新用户余额 // 更新用户余额
_, err = q.User. _, err = q.User.
Where(q.User.ID.Eq(uid), q.User.Balance.Eq(user.Balance)). Where(
UpdateSimple(q.User.Balance.Value(amount)) q.User.ID.Eq(uid),
q.User.Balance.Eq(user.Balance),
).
UpdateSimple(q.User.Balance.Value(newBalance))
if err != nil { if err != nil {
return core.NewServErr("更新用户余额失败", err) return core.NewServErr("更新用户余额失败", err)
} }
@@ -49,17 +57,27 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data
} }
// 生成账单 // 生成账单
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource)) subject, err := data.GetSubject()
if err != nil {
return err
}
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource))
if err != nil { if err != nil {
return core.NewServErr("生成账单失败", err) return core.NewServErr("生成账单失败", err)
} }
return nil return nil
}) })
})
} }
func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *CreateResourceData, trade *m.Trade) error { func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *CreateResourceData, trade *m.Trade) error { // 检查交易
if trade == nil {
return core.NewBizErr("交易数据不能为空")
}
if trade.Status != m.TradeStatusSuccess {
return core.NewBizErr("交易状态不正确")
}
return q.Q.Transaction(func(q *q.Query) error { return q.Q.Transaction(func(q *q.Query) error {
// 保存套餐 // 保存套餐
@@ -69,7 +87,15 @@ func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *
} }
// 生成账单 // 生成账单
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource, trade)) subject, err := data.GetSubject()
if err != nil {
return err
}
amount, err := data.GetAmount()
if err != nil {
return err
}
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource, trade))
if err != nil { if err != nil {
return core.NewServErr("生成账单失败", err) return core.NewServErr("生成账单失败", err)
} }
@@ -95,13 +121,17 @@ func createResource(q *q.Query, uid int32, now time.Time, data *CreateResourceDa
if short == nil { if short == nil {
return nil, core.NewBizErr("短效套餐数据不能为空") return nil, core.NewBizErr("短效套餐数据不能为空")
} }
var duration = time.Duration(short.Expire) * 24 * time.Hour
resource.Short = &m.ResourceShort{ resource.Short = &m.ResourceShort{
Type: short.Mode,
Live: short.Live, Live: short.Live,
Quota: &short.Quota, Type: short.Mode,
Expire: u.P(now.Add(duration)), Quota: short.Quota,
DailyLimit: short.DailyLimit, }
if short.Mode == m.ResourceModeTime {
if short.Expire == nil {
return nil, core.NewBizErr("包时套餐过期时间不能为空")
}
var duration = time.Duration(*short.Expire) * 24 * time.Hour
resource.Short.ExpireAt = u.P(now.Add(duration))
} }
// 长效套餐 // 长效套餐
@@ -110,13 +140,17 @@ func createResource(q *q.Query, uid int32, now time.Time, data *CreateResourceDa
if long == nil { if long == nil {
return nil, core.NewBizErr("长效套餐数据不能为空") return nil, core.NewBizErr("长效套餐数据不能为空")
} }
var duration = time.Duration(long.Expire) * 24 * time.Hour
resource.Long = &m.ResourceLong{ resource.Long = &m.ResourceLong{
Type: long.Mode,
Live: long.Live, Live: long.Live,
Quota: &long.Quota, Type: long.Mode,
Expire: u.P(now.Add(duration)), Quota: long.Quota,
DailyLimit: long.DailyLimit, }
if long.Mode == m.ResourceModeTime {
if long.Expire == nil {
return nil, core.NewBizErr("包时套餐过期时间不能为空")
}
var duration = time.Duration(*long.Expire) * 24 * time.Hour
resource.Long.ExpireAt = u.P(now.Add(duration))
} }
default: default:
return nil, core.NewBizErr("不支持的套餐类型") return nil, core.NewBizErr("不支持的套餐类型")
@@ -139,20 +173,18 @@ type CreateResourceData struct {
type CreateShortResourceData struct { type CreateShortResourceData struct {
Live int32 `json:"live" validate:"required,min=180"` Live int32 `json:"live" validate:"required,min=180"`
Mode m.ResourceMode `json:"mode" validate:"required"` Mode m.ResourceMode `json:"mode" validate:"required"`
Expire int32 `json:"expire"` Quota int32 `json:"quota" validate:"required"`
DailyLimit int32 `json:"daily_limit" validate:"min=2000"` Expire *int32 `json:"expire"`
Quota int32 `json:"quota" validate:"min=10000"`
name string name string
price *decimal.Decimal price *decimal.Decimal
} }
type CreateLongResourceData struct { type CreateLongResourceData struct {
Live int32 `json:"live" validate:"required,oneof=1 4 8 12 24"` Live int32 `json:"live" validate:"required"`
Mode m.ResourceMode `json:"mode" validate:"required,oneof=1 2"` Mode m.ResourceMode `json:"mode" validate:"required"`
Expire int32 `json:"expire"` Quota int32 `json:"quota" validate:"required"`
DailyLimit int32 `json:"daily_limit" validate:"min=100"` Expire *int32 `json:"expire"`
Quota int32 `json:"quota" validate:"min=500"`
name string name string
price *decimal.Decimal price *decimal.Decimal
@@ -162,24 +194,28 @@ func (c *CreateResourceData) GetType() m.TradeType {
return m.TradeTypePurchase return m.TradeTypePurchase
} }
func (c *CreateResourceData) GetSubject() string { func (c *CreateResourceData) GetSubject() (string, error) {
switch c.Type { switch {
case m.ResourceTypeShort: default:
return "", errors.New("无效的套餐类型")
case c.Type == m.ResourceTypeShort && c.Short != nil:
return c.Short.GetSubject() return c.Short.GetSubject()
case m.ResourceTypeLong: case c.Type == m.ResourceTypeLong && c.Long != nil:
return c.Long.GetSubject() return c.Long.GetSubject()
} }
panic("类型对应的数据为空")
} }
func (c *CreateResourceData) GetAmount() decimal.Decimal { func (c *CreateResourceData) GetAmount() (decimal.Decimal, error) {
switch c.Type { switch {
case m.ResourceTypeShort: default:
return decimal.Zero, errors.New("无效的套餐类型")
case c.Type == m.ResourceTypeShort && c.Short != nil:
return c.Short.GetAmount() return c.Short.GetAmount()
case m.ResourceTypeLong: case c.Type == m.ResourceTypeLong && c.Long != nil:
return c.Long.GetAmount() return c.Long.GetAmount()
} }
panic("类型对应的数据为空")
} }
func (c *CreateResourceData) Serialize() (string, error) { func (c *CreateResourceData) Serialize() (string, error) {
@@ -191,27 +227,37 @@ func (c *CreateResourceData) Deserialize(str string) error {
return json.Unmarshal([]byte(str), c) return json.Unmarshal([]byte(str), c)
} }
func (data *CreateShortResourceData) GetSubject() string { func (data *CreateShortResourceData) GetSubject() (string, error) {
if data.name == "" { if data.name == "" {
var mode string var mode string
switch data.Mode { switch data.Mode {
case 1: default:
return "", errors.New("无效的套餐模式")
case m.ResourceModeTime:
mode = "包时" mode = "包时"
case 2: case m.ResourceModeQuota:
mode = "包量" mode = "包量"
} }
data.name = fmt.Sprintf("短效动态%s %v 分钟", mode, data.Live/60) data.name = fmt.Sprintf("短效动态%s %v 分钟", mode, data.Live/60)
} }
return data.name return data.name, nil
} }
func (data *CreateShortResourceData) GetAmount() decimal.Decimal { func (data *CreateShortResourceData) GetAmount() (decimal.Decimal, error) {
if data.price == nil { if data.price == nil {
var factor int32 var factor int32
switch data.Mode { switch data.Mode {
case 1: default:
factor = data.DailyLimit * data.Expire return decimal.Zero, errors.New("无效的套餐模式")
case 2:
case m.ResourceModeTime:
if data.Expire == nil {
return decimal.Zero, errors.New("包时套餐过期时间不能为空")
}
factor = data.Quota * *data.Expire
case m.ResourceModeQuota:
factor = data.Quota factor = data.Quota
} }
@@ -223,38 +269,53 @@ func (data *CreateShortResourceData) GetAmount() decimal.Decimal {
var dec = decimal.Decimal{}. var dec = decimal.Decimal{}.
Add(decimal.NewFromInt32(base * factor)). Add(decimal.NewFromInt32(base * factor)).
Div(decimal.NewFromInt(30000)) Div(decimal.NewFromInt(30000))
if dec.IsZero() {
return decimal.Zero, errors.New("计算金额错误")
}
data.price = &dec data.price = &dec
} }
return *data.price return *data.price, nil
} }
func (data *CreateLongResourceData) GetSubject() string { func (data *CreateLongResourceData) GetSubject() (string, error) {
if data.name == "" { if data.name == "" {
var mode string var mode string
switch data.Mode { switch data.Mode {
case 1: default:
return "", errors.New("无效的套餐模式")
case m.ResourceModeTime:
mode = "包时" mode = "包时"
case 2: case m.ResourceModeQuota:
mode = "包量" mode = "包量"
} }
data.name = fmt.Sprintf("长效动态%s %d 小时", mode, data.Live) data.name = fmt.Sprintf("长效动态%s %d 小时", mode, data.Live)
} }
return data.name return data.name, nil
} }
func (data *CreateLongResourceData) GetAmount() decimal.Decimal { func (data *CreateLongResourceData) GetAmount() (decimal.Decimal, error) {
if data.price == nil { if data.price == nil {
var factor int32 = 0 var factor int32 = 0
switch data.Mode { switch data.Mode {
default:
return decimal.Zero, errors.New("无效的套餐模式")
case m.ResourceModeTime: case m.ResourceModeTime:
factor = data.Expire * data.DailyLimit if data.Expire == nil {
return decimal.Zero, errors.New("包时套餐过期时间不能为空")
}
factor = *data.Expire * data.Quota
case m.ResourceModeQuota: case m.ResourceModeQuota:
factor = data.Quota factor = data.Quota
} }
var base int32 var base int32
switch data.Live { switch data.Live {
default:
return decimal.Zero, errors.New("无效的套餐时长")
case 1: case 1:
base = 30 base = 30
case 4: case 4:
@@ -271,11 +332,16 @@ func (data *CreateLongResourceData) GetAmount() decimal.Decimal {
var dec = decimal.Decimal{}. var dec = decimal.Decimal{}.
Add(decimal.NewFromInt32(base * factor)). Add(decimal.NewFromInt32(base * factor)).
Div(decimal.NewFromInt(100)) Div(decimal.NewFromInt(100))
if dec.IsZero() {
return decimal.Zero, errors.New("计算金额错误")
}
data.price = &dec data.price = &dec
} }
return *data.price return *data.price, nil
} }
// 交易后创建套餐
type ResourceOnTradeComplete struct{} type ResourceOnTradeComplete struct{}
func (r ResourceOnTradeComplete) Check(t m.TradeType) (ProductInfo, bool) { func (r ResourceOnTradeComplete) Check(t m.TradeType) (ProductInfo, bool) {
@@ -289,6 +355,7 @@ func (r ResourceOnTradeComplete) OnTradeComplete(info ProductInfo, trade *m.Trad
return Resource.CreateResourceByTrade(trade.UserID, time.Time(*trade.CompletedAt), info.(*CreateResourceData), trade) return Resource.CreateResourceByTrade(trade.UserID, time.Time(*trade.CompletedAt), info.(*CreateResourceData), trade)
} }
// 服务错误
type ResourceServiceErr string type ResourceServiceErr string
func (e ResourceServiceErr) Error() string { func (e ResourceServiceErr) Error() string {

View File

@@ -35,12 +35,18 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
platform := data.Platform platform := data.Platform
method := data.Method method := data.Method
tType := data.Product.GetType() tType := data.Product.GetType()
subject := data.Product.GetSubject()
amount := data.Product.GetAmount()
expire := time.Now().Add(30 * time.Minute) expire := time.Now().Add(30 * time.Minute)
subject, err := data.Product.GetSubject()
if err != nil {
return nil, err
}
amount, err := data.Product.GetAmount()
if err != nil {
return nil, err
}
// 实际支付金额,只在创建真实订单时使用 // 实际支付金额,只在创建真实订单时使用
var amountReal = data.Product.GetAmount() amountReal := amount
if env.RunMode == env.RunModeDev { if env.RunMode == env.RunModeDev {
amountReal = decimal.NewFromFloat(0.01) amountReal = decimal.NewFromFloat(0.01)
} }
@@ -60,7 +66,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
return nil, err return nil, err
} }
var expireAt = time.Time(u.Z(coupon.ExpireAt)) expireAt := time.Time(u.Z(coupon.ExpireAt))
if !expireAt.IsZero() && expireAt.Before(now) { if !expireAt.IsZero() && expireAt.Before(now) {
_, err = q.Coupon. _, err = q.Coupon.
Where(q.Coupon.ID.Eq(coupon.ID)). Where(q.Coupon.ID.Eq(coupon.ID)).
@@ -99,7 +105,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
} }
// 生成订单号 // 生成订单号
var tradeNo, err = ID.GenSerial() tradeNo, err := ID.GenSerial()
if err != nil { if err != nil {
return nil, core.NewServErr("生成订单号失败", err) return nil, core.NewServErr("生成订单号失败", err)
} }
@@ -692,8 +698,8 @@ type OnTradeCompletedData struct {
type ProductInfo interface { type ProductInfo interface {
GetType() m.TradeType GetType() m.TradeType
GetSubject() string GetSubject() (string, error)
GetAmount() decimal.Decimal GetAmount() (decimal.Decimal, error)
Serialize() (string, error) Serialize() (string, error)
Deserialize(str string) error Deserialize(str string) error
} }

View File

@@ -25,7 +25,15 @@ func (s *userService) UpdateBalanceByTrade(uid int32, info *RechargeProductInfo,
} }
// 生成账单 // 生成账单
err = q.Bill.Create(newForRecharge(uid, Bill.GenNo(), info.GetSubject(), info.GetAmount(), trade)) subject, err := info.GetSubject()
if err != nil {
return err
}
amount, err := info.GetAmount()
if err != nil {
return err
}
err = q.Bill.Create(newForRecharge(uid, Bill.GenNo(), subject, amount, trade))
if err != nil { if err != nil {
return core.NewServErr("生成账单失败", err) return core.NewServErr("生成账单失败", err)
} }
@@ -39,23 +47,25 @@ func (s *userService) UpdateBalanceByTrade(uid int32, info *RechargeProductInfo,
return nil return nil
} }
func updateBalance(q *q.Query, uid int32, info *RechargeProductInfo) (err error) { func updateBalance(q *q.Query, uid int32, info *RechargeProductInfo) error {
// 更新余额
user, err := q.User. user, err := q.User.
Where(q.User.ID.Eq(uid)).Take() Where(q.User.ID.Eq(uid)).Take()
if err != nil { if err != nil {
return core.NewServErr("查询用户失败", err) return core.NewServErr("查询用户失败", err)
} }
var amount = user.Balance.Add(info.GetAmount()) amount, err := info.GetAmount()
if amount.IsNegative() { if err != nil {
return err
}
balance := user.Balance.Add(amount)
if balance.IsNegative() {
return core.NewServErr("用户余额不足") return core.NewServErr("用户余额不足")
} }
_, err = q.User. _, err = q.User.
Where(q.User.ID.Eq(user.ID)). Where(q.User.ID.Eq(user.ID)).
UpdateSimple(q.User.Balance.Value(amount)) UpdateSimple(q.User.Balance.Value(balance))
if err != nil { if err != nil {
return core.NewServErr("更新用户余额失败", err) return core.NewServErr("更新用户余额失败", err)
} }
@@ -75,12 +85,13 @@ func (r *RechargeProductInfo) GetType() m.TradeType {
return m.TradeTypeRecharge return m.TradeTypeRecharge
} }
func (r *RechargeProductInfo) GetSubject() string { func (r *RechargeProductInfo) GetSubject() (string, error) {
return fmt.Sprintf("账户充值 - %s元", r.GetAmount().StringFixed(2)) amount, _ := r.GetAmount()
return fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)), nil
} }
func (r *RechargeProductInfo) GetAmount() decimal.Decimal { func (r *RechargeProductInfo) GetAmount() (decimal.Decimal, error) {
return decimal.NewFromInt(int64(r.Amount)).Div(decimal.NewFromInt(100)) return decimal.NewFromInt(int64(r.Amount)).Div(decimal.NewFromInt(100)), nil
} }
func (r *RechargeProductInfo) Serialize() (string, error) { func (r *RechargeProductInfo) Serialize() (string, error) {

View File

@@ -5,19 +5,11 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/events" "platform/web/events"
e "platform/web/events"
g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services" s "platform/web/services"
"strings"
"time" "time"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
"gorm.io/datatypes"
) )
func HandleCompleteTrade(_ context.Context, task *asynq.Task) (err error) { func HandleCompleteTrade(_ context.Context, task *asynq.Task) (err error) {
@@ -45,112 +37,10 @@ func HandleCompleteTrade(_ context.Context, task *asynq.Task) (err error) {
} }
func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) { func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
data := new(e.RemoveChannelData) batch := string(task.Payload())
err = json.Unmarshal(task.Payload(), data) err = s.Channel.RemoveChannels(batch)
if err != nil {
return fmt.Errorf("解析任务参数失败: %w", err)
}
err = s.Channel.RemoveChannels(data.Batch, data.IDs)
if err != nil { if err != nil {
return fmt.Errorf("删除通道失败: %w", err) return fmt.Errorf("删除通道失败: %w", err)
} }
return nil
}
func HandleFlushGateway(_ context.Context, task *asynq.Task) error {
start := time.Now()
defer func() {
duration := time.Since(start)
if duration > time.Second {
slog.Warn("更新代理后备配置耗时过长", "time", duration.String())
}
}()
// 获取所有网关:配置组
proxies, err := s.Proxy.AllProxies(m.ProxyTypeBaiYin, true)
if err != nil {
return fmt.Errorf("获取网关失败: %w", err)
}
for _, proxy := range proxies {
// 获取当前后备配置
locals := map[string]int{}
for _, channel := range proxy.Channels {
isp := channel.FilterISP.String()
prov := u.Z(channel.FilterProv)
city := u.Z(channel.FilterCity)
locals[fmt.Sprintf("%s:%s:%s", isp, prov, city)]++
}
// 获取之前的后备配置
remotes := map[string]int{}
if proxy.Meta != nil {
meta, ok := proxy.Meta.Data().([]any)
if !ok {
return fmt.Errorf("解析网关数据失败: %T", proxy.Meta.Data())
}
for _, rawM := range meta {
m, ok := rawM.(map[string]any)
if !ok {
return fmt.Errorf("解析网关数据失败: %T", rawM)
}
remotes[fmt.Sprintf("%s:%s:%s", m["isp"], m["province"], m["city"])] = int(m["count"].(float64))
}
}
// 检查是否需要更新
pass := true
for k, local := range locals {
remote, ok := remotes[k]
if !ok {
pass = false
} else {
local, remote := float64(local), float64(remote)
if remote < local*1.5 || remote > local*3 {
pass = false
}
}
}
if pass {
continue
}
// 更新后备配置
configs := make([]g.AutoConfig, 0)
for k, local := range locals {
arr := strings.Split(k, ":")
isp, prov, city := arr[0], arr[1], arr[2]
configs = append(configs, g.AutoConfig{
Isp: isp,
Province: prov,
City: city,
Count: local * 2,
})
}
if env.DebugExternalChange {
err := g.Cloud.CloudConnect(g.CloudConnectReq{
Uuid: proxy.Mac,
AutoConfig: configs,
})
if err != nil {
slog.Error("提交代理后备配置失败", "error", err)
}
} else {
bytes, _ := json.Marshal(configs)
slog.Debug("更新代理后备配置", "proxy", proxy.IP.String(), "config", string(bytes))
}
_, err := q.Proxy.
Where(q.Proxy.ID.Eq(proxy.ID)).
UpdateSimple(q.Proxy.Meta.Value(datatypes.NewJSONType(configs)))
if err != nil {
slog.Error("更新代理后备配置失败", "error", err)
}
}
return nil return nil
} }

View File

@@ -6,7 +6,7 @@ import (
"log/slog" "log/slog"
_ "net/http/pprof" _ "net/http/pprof"
"platform/web/events" "platform/web/events"
base "platform/web/globals" deps "platform/web/globals"
"platform/web/tasks" "platform/web/tasks"
"time" "time"
@@ -19,12 +19,12 @@ func RunApp(pCtx context.Context) error {
g, ctx := errgroup.WithContext(pCtx) g, ctx := errgroup.WithContext(pCtx)
// 初始化依赖 // 初始化依赖
err := base.Init(ctx) err := deps.Init(ctx)
if err != nil { if err != nil {
return fmt.Errorf("初始化依赖失败: %w", err) return fmt.Errorf("初始化依赖失败: %w", err)
} }
defer func() { defer func() {
err := base.Close() err := deps.Close()
if err != nil { if err != nil {
slog.Error("关闭依赖失败", "error", err) slog.Error("关闭依赖失败", "error", err)
} }
@@ -39,10 +39,6 @@ func RunApp(pCtx context.Context) error {
return RunTask(ctx) return RunTask(ctx)
}) })
g.Go(func() error {
return RunSchedule(ctx)
})
return g.Wait() return g.Wait()
} }
@@ -76,30 +72,9 @@ func RunWeb(ctx context.Context) error {
return nil return nil
} }
func RunSchedule(ctx context.Context) error {
var scheduler = asynq.NewSchedulerFromRedisClient(base.Redis, &asynq.SchedulerOpts{
Location: time.Local,
})
scheduler.Register("@every 5s", events.NewFlushGateway(5*time.Second))
// 停止服务
go func() {
<-ctx.Done()
scheduler.Shutdown()
}()
// 启动服务
err := scheduler.Run()
if err != nil {
return fmt.Errorf("调度服务运行失败: %w", err)
}
return nil
}
func RunTask(ctx context.Context) error { func RunTask(ctx context.Context) error {
var server = asynq.NewServerFromRedisClient(base.Redis, asynq.Config{ var server = asynq.NewServerFromRedisClient(deps.Redis, asynq.Config{
ShutdownTimeout: 5 * time.Second,
ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) { ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) {
slog.Error("任务执行失败", "task", task.Type(), "error", err) slog.Error("任务执行失败", "task", task.Type(), "error", err)
}), }),
@@ -108,7 +83,6 @@ func RunTask(ctx context.Context) error {
var mux = asynq.NewServeMux() var mux = asynq.NewServeMux()
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel) mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
mux.HandleFunc(events.CompleteTrade, tasks.HandleCompleteTrade) mux.HandleFunc(events.CompleteTrade, tasks.HandleCompleteTrade)
mux.HandleFunc(events.FlushGateway, tasks.HandleFlushGateway)
// 停止服务 // 停止服务
go func() { go func() {