Compare commits
10 Commits
3f24fba1ae
...
c8fd4cf9ca
| Author | SHA1 | Date | |
|---|---|---|---|
| c8fd4cf9ca | |||
| 2b190bd4e5 | |||
| 8f2e71849f | |||
| 0207720943 | |||
| 05fba68b3e | |||
| c8c86081d9 | |||
| 983dbb4564 | |||
| 9e237be21e | |||
| 5649a03c47 | |||
| 4a2dcabf58 |
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
@@ -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
13
.zed/debug.json
Normal 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"
|
||||
}
|
||||
]
|
||||
65
README.md
65
README.md
@@ -1,26 +1,20 @@
|
||||
## TODO
|
||||
|
||||
trade/create 性能问题,缩短事务时间,考虑其他方式实现可靠分布式事务
|
||||
前端需要 token 化改造,以避免每次 basic 认证流程中 bcrypt 对比导致的性能对比
|
||||
|
||||
需要确认以下 ID.GenSerial 的分布式并发安全性
|
||||
优化中间件,配置通用限速
|
||||
|
||||
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
|
||||
observe 部署,蓝狐部署
|
||||
|
||||
端口资源池的 gc 实现
|
||||
---
|
||||
|
||||
标准化生产环境 cors 配置
|
||||
|
||||
底层调用集成 otel
|
||||
- redis
|
||||
- gorm
|
||||
- 三方接口
|
||||
用反射实现环境变量解析,以简化函数签名
|
||||
|
||||
分离 task 的客户端,支持多进程(prefork 必要!)
|
||||
|
||||
调整目录结构:
|
||||
|
||||
```
|
||||
- /core 核心概念
|
||||
- /util 工具函数
|
||||
|
||||
- /models 模型
|
||||
@@ -30,7 +24,7 @@ jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
|
||||
- /services 服务层
|
||||
- /auth 认证相关,特化服务
|
||||
|
||||
- /app 应用相关,初始化日志,环境变量等
|
||||
- /app 应用相关,初始化日志,环境变量,错误类型等
|
||||
- /http 协议层,http 服务
|
||||
- /cmd 主函数
|
||||
|
||||
@@ -38,13 +32,20 @@ jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
|
||||
cmd 调用 app, http 的初始化函数
|
||||
http 调用 clients 的初始化函数
|
||||
```
|
||||
|
||||
考虑一个方案限制接口请求速率,无侵入更好
|
||||
|
||||
---
|
||||
|
||||
数据库转模型文件
|
||||
|
||||
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
|
||||
|
||||
慢速请求底层调用埋点监控
|
||||
- redis
|
||||
- gorm
|
||||
- 三方接口
|
||||
|
||||
冷数据迁移方案
|
||||
|
||||
proxy 网关更新接口可以传输更结构化的数据,直接区分不同类型以加快更新速度
|
||||
|
||||
## 业务逻辑
|
||||
|
||||
### 订单关闭的几种方式
|
||||
@@ -63,22 +64,18 @@ proxy 网关更新接口可以传输更结构化的数据,直接区分不同
|
||||
|
||||
### 节点分配与存储逻辑
|
||||
|
||||
添加:
|
||||
- 检查用户 ip 是否在白名单内
|
||||
- 取用端口,不够则返回失败
|
||||
- 将分配结果转写成配置发送到网关
|
||||
- 保存通道信息和分配记录,其中通道信息以网关为主体,分配记录以用户为主体
|
||||
- 添加异步任务,当时间结束后释放取用的端口并清空网关配置
|
||||
提取:
|
||||
- 检查用户套餐与白名单
|
||||
- 选中代理
|
||||
- 找到当前可用端口最多的代理
|
||||
- 不考虑分割端口,不够加机器
|
||||
- 获取可用端口
|
||||
- 获取可用节点
|
||||
- 生成批次号,提交到期释放任务
|
||||
- 绑定节点与端口,保存到数据库
|
||||
- 分别提交连接与配置请求
|
||||
|
||||
删除:
|
||||
- 如果传入用户信息,检查要删除的连接是否属于该用户
|
||||
- 释放可用端口
|
||||
- redis 脚本中检查,如果端口所属节点已下线则直接忽略
|
||||
- 提交清空配置到网关
|
||||
|
||||
缩扩容:
|
||||
- 通过调度任务实现缩扩容
|
||||
- 每分钟检查一次全部配置,按代理分组
|
||||
- 获取所有代理后备配置
|
||||
- 后备配置/当前配置
|
||||
- 当比例 < 1.5 或 > 3 时,重新更新为 2 倍
|
||||
释放:
|
||||
- 根据批次查出所有端口与相关节点
|
||||
- 分别提交断开与关闭请求
|
||||
- 释放端口
|
||||
|
||||
@@ -61,6 +61,7 @@ func main() {
|
||||
m.User{},
|
||||
m.UserRole{},
|
||||
m.Whitelist{},
|
||||
m.Inquiry{},
|
||||
)
|
||||
g.Execute()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ name: lanhu
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
image: postgres:17.7
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
|
||||
10
pkg/env/env.go
vendored
10
pkg/env/env.go
vendored
@@ -36,7 +36,10 @@ var (
|
||||
RedisPort = "6379"
|
||||
RedisPassword = ""
|
||||
|
||||
BaiyinAddr = "http://103.139.212.110:9989"
|
||||
OtelHost string
|
||||
OtelPort string
|
||||
|
||||
BaiyinCloudUrl string
|
||||
BaiyinTokenUrl string
|
||||
|
||||
IdenCallbackUrl string
|
||||
@@ -115,7 +118,10 @@ func Init() {
|
||||
errs = append(errs, parse(&RedisPort, "REDIS_PORT", 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(&IdenCallbackUrl, "IDEN_CALLBACK_URL", false, nil))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
// 指针
|
||||
// ====================
|
||||
|
||||
@@ -108,6 +108,76 @@ comment on column logs_user_bandwidth.time is '记录时间';
|
||||
|
||||
-- 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 管理员信息
|
||||
-- ====================
|
||||
@@ -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.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
|
||||
|
||||
-- ====================
|
||||
@@ -497,13 +536,14 @@ comment on column link_client_permission.permission_id is '权限ID';
|
||||
drop table if exists proxy cascade;
|
||||
create table proxy (
|
||||
id int generated by default as identity primary key,
|
||||
version int not null,
|
||||
mac text not null,
|
||||
ip inet not null,
|
||||
version int not null,
|
||||
mac text not null,
|
||||
ip inet not null,
|
||||
host text,
|
||||
secret text,
|
||||
type int not null,
|
||||
status int not null,
|
||||
meta jsonb not null,
|
||||
type int not null,
|
||||
status int not null,
|
||||
meta jsonb,
|
||||
created_at timestamptz default current_timestamp,
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
@@ -518,8 +558,9 @@ comment on column proxy.id is '代理服务ID';
|
||||
comment on column proxy.version is '代理服务版本';
|
||||
comment on column proxy.mac 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.type is '代理服务类型:1-自有,2-白银';
|
||||
comment on column proxy.status is '代理服务状态:0-离线,1-在线';
|
||||
comment on column proxy.meta is '代理服务元信息';
|
||||
comment on column proxy.created_at is '创建时间';
|
||||
@@ -600,8 +641,10 @@ create table channel (
|
||||
resource_id int not null,
|
||||
batch_no text not null,
|
||||
proxy_id int not null,
|
||||
host text not null,
|
||||
port int not null,
|
||||
edge_id int,
|
||||
edge_ref text,
|
||||
filter_isp int,
|
||||
filter_prov 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.batch_no is '批次编号';
|
||||
comment on column channel.proxy_id is '代理ID';
|
||||
comment on column channel.host is '代理主机(快照)';
|
||||
comment on column channel.port is '代理端口';
|
||||
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_prov is '省份过滤(自动配置)';
|
||||
comment on column channel.filter_city is '城市过滤(自动配置)';
|
||||
@@ -707,14 +752,13 @@ drop table if exists resource_short cascade;
|
||||
create table resource_short (
|
||||
id int generated by default as identity primary key,
|
||||
resource_id int not null,
|
||||
type int not null,
|
||||
live int not null,
|
||||
expire timestamptz,
|
||||
quota int,
|
||||
type int not null,
|
||||
quota int not null,
|
||||
expire_at timestamptz,
|
||||
used int not null default 0,
|
||||
daily_limit int not null default 0,
|
||||
daily_used int not null default 0,
|
||||
daily_last timestamptz
|
||||
daily int not null default 0,
|
||||
last_at timestamptz
|
||||
);
|
||||
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 column resource_short.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.quota is '配额数量';
|
||||
comment on column resource_short.used is '已用数量';
|
||||
comment on column resource_short.expire is '过期时间';
|
||||
comment on column resource_short.daily_limit is '每日限制';
|
||||
comment on column resource_short.daily_used is '今日已用数量';
|
||||
comment on column resource_short.daily_last is '今日最后使用时间';
|
||||
comment on column resource_short.type is '套餐类型:1-包时,2-包量';
|
||||
comment on column resource_short.quota is '每日配额(包时)或总配额(包量)';
|
||||
comment on column resource_short.expire_at is '套餐过期时间,包时模式可用';
|
||||
comment on column resource_short.used is '总用量';
|
||||
comment on column resource_short.daily is '当日用量';
|
||||
comment on column resource_short.last_at is '最后使用时间';
|
||||
|
||||
-- resource_long
|
||||
drop table if exists resource_long cascade;
|
||||
create table resource_long (
|
||||
id int generated by default as identity primary key,
|
||||
resource_id int not null,
|
||||
type int not null,
|
||||
live int not null,
|
||||
expire timestamptz,
|
||||
quota int,
|
||||
type int not null,
|
||||
quota int not null,
|
||||
expire_at timestamptz,
|
||||
used int not null default 0,
|
||||
daily_limit int not null default 0,
|
||||
daily_used int not null default 0,
|
||||
daily_last timestamptz
|
||||
daily int not null default 0,
|
||||
last_at timestamptz
|
||||
);
|
||||
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 column resource_long.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.live is '可用时长(天)';
|
||||
comment on column resource_long.quota is '配额数量';
|
||||
comment on column resource_long.used is '已用数量';
|
||||
comment on column resource_long.expire is '过期时间';
|
||||
comment on column resource_long.daily_limit is '每日限制';
|
||||
comment on column resource_long.daily_used is '今日已用数量';
|
||||
comment on column resource_long.daily_last 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.daily is '当日用量';
|
||||
comment on column resource_long.last_at is '最后使用时间';
|
||||
|
||||
-- endregion
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ type Err struct {
|
||||
func (e *Err) Error() string {
|
||||
if e.err != nil {
|
||||
slog.Debug(fmt.Sprintf("%s: %s", e.msg, e.err.Error()))
|
||||
return e.msg
|
||||
}
|
||||
return e.msg
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PageReq 分页请求参数
|
||||
type PageReq struct {
|
||||
RawPage int `json:"page"`
|
||||
@@ -38,3 +50,83 @@ type PageResp struct {
|
||||
Size int `json:"size"`
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
const RemoveChannel = "channel:remove"
|
||||
|
||||
type RemoveChannelData struct {
|
||||
Batch string `json:"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)
|
||||
func NewRemoveChannel(batch string) *asynq.Task {
|
||||
return asynq.NewTask(RemoveChannel, []byte(batch))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"platform/pkg/env"
|
||||
"platform/web/core"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -17,18 +18,12 @@ import (
|
||||
|
||||
// CloudClient 定义云服务接口
|
||||
type CloudClient interface {
|
||||
CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error)
|
||||
CloudConnect(param CloudConnectReq) error
|
||||
CloudDisconnect(param CloudDisconnectReq) (int, error)
|
||||
CloudEdges(param *CloudEdgesReq) (*CloudEdgesResp, error)
|
||||
CloudConnect(param *CloudConnectReq) error
|
||||
CloudDisconnect(param *CloudDisconnectReq) (int, error)
|
||||
CloudAutoQuery() (CloudConnectResp, error)
|
||||
}
|
||||
|
||||
// GatewayClient 定义网关接口
|
||||
type GatewayClient interface {
|
||||
GatewayPortConfigs(params []PortConfigsReq) error
|
||||
GatewayPortActive(param ...PortActiveReq) (map[string]PortData, error)
|
||||
}
|
||||
|
||||
type cloud struct {
|
||||
url string
|
||||
}
|
||||
@@ -37,59 +32,14 @@ var Cloud CloudClient
|
||||
|
||||
func initBaiyin() error {
|
||||
Cloud = &cloud{
|
||||
url: env.BaiyinAddr,
|
||||
url: env.BaiyinCloudUrl,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AutoConfig struct {
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
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(), "")
|
||||
// cloud:/edges 筛选查询边缘节点
|
||||
func (c *cloud) CloudEdges(param *CloudEdgesReq) (*CloudEdgesResp, error) {
|
||||
resp, err := c.requestCloud("GET", "/edges?"+core.Query(param).Encode(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -115,17 +65,46 @@ func (c *cloud) CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error) {
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region cloud:/connect
|
||||
|
||||
type CloudConnectReq struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Edge []string `json:"edge,omitempty"`
|
||||
AutoConfig []AutoConfig `json:"auto_config,omitempty"`
|
||||
type CloudEdgesReq struct {
|
||||
Province *string `query:"province"`
|
||||
City *string `query:"city"`
|
||||
Isp *string `query:"isp"`
|
||||
Offset *int `query:"offset"`
|
||||
Limit *int `query:"limit"`
|
||||
NoRepeat *bool `query:"norepeat,b2i"`
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -162,25 +141,21 @@ func (c *cloud) CloudConnect(param CloudConnectReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region cloud:/disconnect
|
||||
|
||||
type CloudDisconnectReq struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Edge []string `json:"edge,omitempty"`
|
||||
Config []Config `json:"auto_config,omitempty"`
|
||||
type CloudConnectReq struct {
|
||||
Uuid string `json:"uuid"`
|
||||
Edge *[]string `json:"edge,omitempty"`
|
||||
AutoConfig *[]AutoConfig `json:"auto_config,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
type AutoConfig struct {
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
Isp string `json:"isp"`
|
||||
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)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -217,12 +192,21 @@ func (c *cloud) CloudDisconnect(param CloudDisconnectReq) (int, error) {
|
||||
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 CloudConnectResp map[string][]AutoConfig
|
||||
type Config struct {
|
||||
Province string `json:"province"`
|
||||
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) {
|
||||
resp, err := c.requestCloud("GET", "/auto_query", "")
|
||||
if err != nil {
|
||||
@@ -250,7 +234,7 @@ func (c *cloud) CloudAutoQuery() (CloudConnectResp, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// endregion
|
||||
type CloudConnectResp map[string][]AutoConfig
|
||||
|
||||
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")
|
||||
|
||||
var resp *http.Response
|
||||
for i := 0; i < 2; i++ {
|
||||
for i := range 2 {
|
||||
token, err := c.token(i == 1)
|
||||
if err != nil {
|
||||
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) {
|
||||
// redis 获取令牌
|
||||
if !refresh {
|
||||
token, err := Redis.Get(context.Background(), "remote:token").Result()
|
||||
token, err := Redis.Get(context.Background(), BaiyinToken).Result()
|
||||
if err == nil && token != "" {
|
||||
return token, nil
|
||||
}
|
||||
@@ -338,7 +322,7 @@ func (c *cloud) token(refresh bool) (string, error) {
|
||||
var result map[string]any
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("解析响应 [%s] 失败: %w", string(body), err)
|
||||
}
|
||||
|
||||
if result["code"].(float64) != 1 {
|
||||
@@ -347,7 +331,7 @@ func (c *cloud) token(refresh bool) (string, error) {
|
||||
|
||||
// redis 设置令牌
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
@@ -355,6 +339,15 @@ func (c *cloud) token(refresh bool) (string, error) {
|
||||
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 {
|
||||
url string
|
||||
username string
|
||||
@@ -373,6 +366,68 @@ func NewGateway(url, username, password string) GatewayClient {
|
||||
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
|
||||
|
||||
type PortConfigsReq struct {
|
||||
@@ -395,7 +450,7 @@ type AutoEdgeConfig struct {
|
||||
PacketLoss int `json:"packet_loss,omitempty"`
|
||||
}
|
||||
|
||||
func (c *gateway) GatewayPortConfigs(params []PortConfigsReq) error {
|
||||
func (c *gateway) GatewayPortConfigs(params []*PortConfigsReq) error {
|
||||
if len(params) == 0 {
|
||||
return errors.New("params is empty")
|
||||
}
|
||||
@@ -461,10 +516,10 @@ type PortData struct {
|
||||
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{}
|
||||
if len(param) != 0 {
|
||||
_param = param[0]
|
||||
if len(param) != 0 && param[0] != nil {
|
||||
_param = *param[0]
|
||||
}
|
||||
|
||||
path := strings.Builder{}
|
||||
@@ -520,38 +575,33 @@ func (c *gateway) GatewayPortActive(param ...PortActiveReq) (map[string]PortData
|
||||
|
||||
// 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) {
|
||||
//goland:noinspection ALL
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
if env.DebugHttpDump {
|
||||
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
|
||||
return core.Fetch(req)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package globals
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"platform/pkg/env"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
@@ -16,8 +17,13 @@ import (
|
||||
var tp *trace.TracerProvider
|
||||
|
||||
func initOtel(ctx context.Context) error {
|
||||
if env.OtelHost == "" || env.OtelPort == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
addr := env.OtelHost + ":" + env.OtelPort
|
||||
exporter, err := otlptracegrpc.New(ctx,
|
||||
otlptracegrpc.WithEndpoint("localhost:4317"),
|
||||
otlptracegrpc.WithEndpoint(addr),
|
||||
otlptracegrpc.WithInsecure(),
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
57
web/handlers/batch.go
Normal file
57
web/handlers/batch.go
Normal file
@@ -0,0 +1,57 @@
|
||||
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"`
|
||||
}
|
||||
@@ -57,7 +57,6 @@ func ListChannels(c *fiber.Ctx) error {
|
||||
|
||||
// 查询数据
|
||||
channels, err := q.Channel.
|
||||
Preload(q.Channel.Proxy).
|
||||
Where(cond).
|
||||
Order(q.Channel.CreatedAt.Desc()).
|
||||
Offset(req.GetOffset()).
|
||||
@@ -144,7 +143,7 @@ func CreateChannel(c *fiber.Ctx) error {
|
||||
for i, channel := range result {
|
||||
resp[i] = &CreateChannelRespItem{
|
||||
Proto: req.Protocol,
|
||||
Host: channel.Proxy.IP.String(),
|
||||
Host: channel.Host,
|
||||
Port: channel.Port,
|
||||
}
|
||||
if req.AuthType == s.ChannelAuthTypePass {
|
||||
@@ -162,8 +161,7 @@ type CreateChannelResultType string
|
||||
// region RemoveChannels
|
||||
|
||||
type RemoveChannelsReq struct {
|
||||
Batch string `json:"batch" validate:"required"`
|
||||
Ids []int32 `json:"ids" validate:"required"`
|
||||
Batch string `json:"batch" validate:"required"`
|
||||
}
|
||||
|
||||
func RemoveChannels(c *fiber.Ctx) error {
|
||||
@@ -180,7 +178,7 @@ func RemoveChannels(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 删除通道
|
||||
err = s.Channel.RemoveChannels(req.Batch, req.Ids)
|
||||
err = s.Channel.RemoveChannels(req.Batch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
48
web/handlers/inquiry.go
Normal file
48
web/handlers/inquiry.go
Normal 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
|
||||
@@ -1,10 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"platform/pkg/env"
|
||||
g "platform/web/globals"
|
||||
"platform/web/core"
|
||||
s "platform/web/services"
|
||||
"time"
|
||||
|
||||
@@ -16,18 +15,9 @@ func DebugRegisterProxyBaiYin(c *fiber.Ctx) error {
|
||||
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 {
|
||||
return 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 core.NewServErr("注册失败", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -15,17 +15,7 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ListResourceShortReq 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"`
|
||||
}
|
||||
|
||||
// ListResourceShort 分页短效套餐
|
||||
func ListResourceShort(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -60,10 +50,10 @@ func ListResourceShort(c *fiber.Ctx) error {
|
||||
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
|
||||
}
|
||||
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 {
|
||||
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).
|
||||
@@ -96,6 +86,88 @@ func ListResourceShort(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
type ListResourceShortReq 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"`
|
||||
}
|
||||
|
||||
// ListResourceLong 分页长效套餐
|
||||
func ListResourceLong(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(ListResourceLongReq)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 查询套餐列表
|
||||
do := q.Resource.Where(
|
||||
q.Resource.UserID.Eq(authCtx.User.ID),
|
||||
q.Resource.Type.Eq(int(m.ResourceTypeLong)),
|
||||
)
|
||||
if req.ResourceNo != nil && *req.ResourceNo != "" {
|
||||
do.Where(q.Resource.ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.Active != nil {
|
||||
do.Where(q.Resource.Active.Is(*req.Active))
|
||||
}
|
||||
if req.Type != nil {
|
||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Type.Eq(int(*req.Type)))
|
||||
}
|
||||
if req.CreateAfter != nil {
|
||||
do.Where(q.Resource.CreatedAt.Gte(*req.CreateAfter))
|
||||
}
|
||||
if req.CreateBefore != nil {
|
||||
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
|
||||
}
|
||||
if req.ExpireAfter != nil {
|
||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Gte(*req.ExpireAfter))
|
||||
}
|
||||
if req.ExpireBefore != nil {
|
||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Lte(*req.ExpireBefore))
|
||||
}
|
||||
|
||||
resource, err := q.Resource.Where(do).
|
||||
Joins(q.Resource.Long).
|
||||
Order(q.Resource.CreatedAt.Desc()).
|
||||
Offset(req.GetOffset()).
|
||||
Limit(req.GetLimit()).
|
||||
Find()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var total int64
|
||||
if len(resource) < req.GetLimit() {
|
||||
total = int64(len(resource) + req.GetOffset())
|
||||
} else {
|
||||
total, err = q.Resource.
|
||||
Where(do).
|
||||
Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
List: resource,
|
||||
})
|
||||
}
|
||||
|
||||
type ListResourceLongReq struct {
|
||||
core.PageReq
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
@@ -107,79 +179,7 @@ type ListResourceLongReq struct {
|
||||
ExpireBefore *time.Time `json:"expire_before"`
|
||||
}
|
||||
|
||||
func ListResourceLong(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析请求参数
|
||||
req := new(ListResourceLongReq)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 查询套餐列表
|
||||
do := q.Resource.Where(
|
||||
q.Resource.UserID.Eq(authCtx.User.ID),
|
||||
q.Resource.Type.Eq(int(m.ResourceTypeLong)),
|
||||
)
|
||||
if req.ResourceNo != nil && *req.ResourceNo != "" {
|
||||
do.Where(q.Resource.ResourceNo.Eq(*req.ResourceNo))
|
||||
}
|
||||
if req.Active != nil {
|
||||
do.Where(q.Resource.Active.Is(*req.Active))
|
||||
}
|
||||
if req.Type != nil {
|
||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Type.Eq(int(*req.Type)))
|
||||
}
|
||||
if req.CreateAfter != nil {
|
||||
do.Where(q.Resource.CreatedAt.Gte(*req.CreateAfter))
|
||||
}
|
||||
if req.CreateBefore != nil {
|
||||
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
|
||||
}
|
||||
if req.ExpireAfter != nil {
|
||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Expire.Gte(*req.ExpireAfter))
|
||||
}
|
||||
if req.ExpireBefore != nil {
|
||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Expire.Lte(*req.ExpireBefore))
|
||||
}
|
||||
|
||||
resource, err := q.Resource.Where(do).
|
||||
Joins(q.Resource.Long).
|
||||
Order(q.Resource.CreatedAt.Desc()).
|
||||
Offset(req.GetOffset()).
|
||||
Limit(req.GetLimit()).
|
||||
Find()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var total int64
|
||||
if len(resource) < req.GetLimit() {
|
||||
total = int64(len(resource) + req.GetOffset())
|
||||
} else {
|
||||
total, err = q.Resource.
|
||||
Where(do).
|
||||
Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(core.PageResp{
|
||||
Total: int(total),
|
||||
Page: req.GetPage(),
|
||||
Size: req.GetSize(),
|
||||
List: resource,
|
||||
})
|
||||
}
|
||||
|
||||
type AllResourceReq struct {
|
||||
}
|
||||
|
||||
// AllActiveResource 所有可用套餐
|
||||
func AllActiveResource(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -204,10 +204,10 @@ func AllActiveResource(c *fiber.Ctx) error {
|
||||
q.Resource.Type.Eq(int(m.ResourceTypeShort)),
|
||||
q.ResourceShort.As(q.Resource.Short.Name()).Where(
|
||||
short.Type.Eq(int(m.ResourceModeTime)),
|
||||
short.Expire.Gte(now),
|
||||
short.ExpireAt.Gte(now),
|
||||
q.ResourceShort.As(q.Resource.Short.Name()).
|
||||
Where(short.DailyLast.Lt(u.Today())).
|
||||
Or(short.DailyLimit.GtCol(short.DailyUsed)),
|
||||
Where(short.LastAt.Lt(u.Today())).
|
||||
Or(short.Quota.GtCol(short.Daily)),
|
||||
).Or(
|
||||
short.Type.Eq(int(m.ResourceModeQuota)),
|
||||
short.Quota.GtCol(short.Used),
|
||||
@@ -216,10 +216,10 @@ func AllActiveResource(c *fiber.Ctx) error {
|
||||
q.Resource.Type.Eq(int(m.ResourceTypeLong)),
|
||||
q.ResourceLong.As(q.Resource.Long.Name()).Where(
|
||||
long.Type.Eq(int(m.ResourceModeTime)),
|
||||
long.Expire.Gte(now),
|
||||
long.ExpireAt.Gte(now),
|
||||
q.ResourceLong.As(q.Resource.Long.Name()).
|
||||
Where(long.DailyLast.Lt(u.Today())).
|
||||
Or(long.DailyLimit.GtCol(long.DailyUsed)),
|
||||
Where(long.LastAt.Lt(u.Today())).
|
||||
Or(long.Quota.GtCol(long.Daily)),
|
||||
).Or(
|
||||
long.Type.Eq(int(m.ResourceModeQuota)),
|
||||
long.Quota.GtCol(long.Used),
|
||||
@@ -235,23 +235,10 @@ func AllActiveResource(c *fiber.Ctx) error {
|
||||
return c.JSON(resources)
|
||||
}
|
||||
|
||||
type StatisticPersonalResp 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"`
|
||||
type AllResourceReq struct {
|
||||
}
|
||||
|
||||
// StatisticResourceFree 统计每日可用
|
||||
func StatisticResourceFree(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -282,39 +269,39 @@ func StatisticResourceFree(c *fiber.Ctx) error {
|
||||
|
||||
// 短效包量
|
||||
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++
|
||||
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:
|
||||
if u.Z(resource.Long.Quota) > resource.Long.Used {
|
||||
if resource.Long.Quota > resource.Long.Used {
|
||||
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:
|
||||
if time.Time(*resource.Short.Expire).After(time.Now()) {
|
||||
if resource.Short.DailyLast == nil || u.IsToday(time.Time(*resource.Short.DailyLast)) == false {
|
||||
if time.Time(*resource.Short.ExpireAt).After(time.Now()) {
|
||||
if resource.Short.LastAt == nil || u.IsToday(time.Time(*resource.Short.LastAt)) == false {
|
||||
shortCount++
|
||||
shortDailyFreeSum += int(resource.Short.DailyLimit)
|
||||
} else if resource.Short.DailyLimit > resource.Short.DailyUsed {
|
||||
shortDailyFreeSum += int(resource.Short.Quota)
|
||||
} else if resource.Short.Quota > resource.Short.Daily {
|
||||
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:
|
||||
if time.Time(*resource.Long.Expire).After(time.Now()) {
|
||||
if resource.Long.DailyLast == nil || u.IsToday(time.Time(*resource.Long.DailyLast)) == false {
|
||||
if time.Time(*resource.Long.ExpireAt).After(time.Now()) {
|
||||
if resource.Long.LastAt == nil || u.IsToday(time.Time(*resource.Long.LastAt)) == false {
|
||||
longCount++
|
||||
longDailyFreeSum += int(resource.Long.DailyLimit)
|
||||
} else if resource.Long.DailyLimit > resource.Long.DailyUsed {
|
||||
longDailyFreeSum += int(resource.Long.Quota)
|
||||
} else if resource.Long.Quota > resource.Long.Daily {
|
||||
longCount++
|
||||
longDailyFreeSum += int(resource.Long.DailyLimit - resource.Long.DailyUsed)
|
||||
longDailyFreeSum += int(resource.Long.Quota - resource.Long.Daily)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,17 +321,24 @@ func StatisticResourceFree(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
type StatisticResourceUsageReq struct {
|
||||
ResourceNo *string `json:"resource_no"`
|
||||
TimeAfter *time.Time `json:"time_after"`
|
||||
TimeBefore *time.Time `json:"time_before"`
|
||||
type StatisticPersonalResp struct {
|
||||
Short StatisticShort `json:"short"`
|
||||
Long StatisticLong `json:"long"`
|
||||
}
|
||||
|
||||
type StatisticResourceUsageResp []struct {
|
||||
Date time.Time `json:"date"`
|
||||
Count int `json:"count"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// StatisticResourceUsage 统计每日用量
|
||||
func StatisticResourceUsage(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||
@@ -359,21 +353,10 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// 统计套餐提取数量
|
||||
do := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(authCtx.User.ID))
|
||||
if req.ResourceNo != nil && *req.ResourceNo != "" {
|
||||
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))
|
||||
}
|
||||
do := q.LogsUserUsage.Where(
|
||||
q.LogsUserUsage.UserID.Eq(authCtx.User.ID),
|
||||
)
|
||||
|
||||
if req.TimeAfter != nil {
|
||||
do.Where(q.LogsUserUsage.Time.Gte(*req.TimeAfter))
|
||||
}
|
||||
@@ -389,7 +372,7 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
|
||||
).
|
||||
Where(do).
|
||||
Group(
|
||||
field.NewUnsafeFieldRaw("date_trunc('day', time)"),
|
||||
field.NewField("", "date"),
|
||||
).
|
||||
Order(
|
||||
field.NewField("", "date").Desc(),
|
||||
@@ -402,10 +385,17 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
type CreateResourceReq struct {
|
||||
*s.CreateResourceData
|
||||
type StatisticResourceUsageReq struct {
|
||||
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 {
|
||||
|
||||
// 检查权限
|
||||
@@ -429,6 +419,11 @@ func CreateResource(c *fiber.Ctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreateResourceReq struct {
|
||||
*s.CreateResourceData
|
||||
}
|
||||
|
||||
// ResourcePrice 套餐价格
|
||||
func ResourcePrice(c *fiber.Ctx) error {
|
||||
// 检查权限
|
||||
_, err := auth.GetAuthCtx(c).PermitSecretClient()
|
||||
@@ -439,11 +434,25 @@ func ResourcePrice(c *fiber.Ctx) error {
|
||||
// 解析请求参数
|
||||
var req = new(CreateResourceReq)
|
||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||
return err
|
||||
return core.NewBizErr("接口参数解析异常", err)
|
||||
}
|
||||
|
||||
// 获取套餐价格
|
||||
return c.JSON(fiber.Map{
|
||||
"price": req.GetAmount().StringFixed(2),
|
||||
amount, err := req.GetAmount()
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
g "platform/web/globals"
|
||||
m "platform/web/models"
|
||||
s "platform/web/services"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -154,9 +153,9 @@ func TradeCheck(c *fiber.Ctx) error {
|
||||
slog.Error("写入订单状态失败", "trade_no", req.TradeNo, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = w.Flush()
|
||||
if err != nil {
|
||||
slog.Error("刷新缓冲区失败", "trade_no", req.TradeNo, "error", err, "errType", reflect.TypeOf(err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/gofiber/contrib/otelfiber/v2"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
@@ -38,9 +37,6 @@ func ApplyMiddlewares(app *fiber.App) {
|
||||
},
|
||||
}))
|
||||
|
||||
// cors
|
||||
app.Use(cors.New())
|
||||
|
||||
// authenticate
|
||||
app.Use(auth.Authenticate())
|
||||
}
|
||||
|
||||
@@ -10,16 +10,16 @@ import (
|
||||
// Admin 管理员表
|
||||
type Admin struct {
|
||||
core.Model
|
||||
Username string `json:"username" gorm:"column:username"` // 用户名
|
||||
Password string `json:"password" gorm:"column:password"` // 密码
|
||||
Name *string `json:"name" gorm:"column:name"` // 真实姓名
|
||||
Avatar *string `json:"avatar" gorm:"column:avatar"` // 头像URL
|
||||
Phone *string `json:"phone" gorm:"column:phone"` // 手机号码
|
||||
Email *string `json:"email" gorm:"column:email"` // 邮箱
|
||||
Status AdminStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
||||
LastLogin *time.Time `json:"last_login" gorm:"column:last_login"` // 最后登录时间
|
||||
LastLoginIP *orm.Inet `json:"last_login_ip" gorm:"column:last_login_ip"` // 最后登录地址
|
||||
LastLoginUA *string `json:"last_login_ua" gorm:"column:last_login_ua"` // 最后登录代理
|
||||
Username string `json:"username" gorm:"column:username"` // 用户名
|
||||
Password string `json:"password" gorm:"column:password"` // 密码
|
||||
Name *string `json:"name,omitempty" gorm:"column:name"` // 真实姓名
|
||||
Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL
|
||||
Phone *string `json:"phone,omitempty" gorm:"column:phone"` // 手机号码
|
||||
Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱
|
||||
Status AdminStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
||||
LastLogin *time.Time `json:"last_login,omitempty" gorm:"column:last_login"` // 最后登录时间
|
||||
LastLoginIP *orm.Inet `json:"last_login_ip,omitempty" gorm:"column:last_login_ip"` // 最后登录地址
|
||||
LastLoginUA *string `json:"last_login_ua,omitempty" gorm:"column:last_login_ua"` // 最后登录代理
|
||||
}
|
||||
|
||||
// AdminStatus 管理员状态枚举
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
// AdminRole 管理员角色表
|
||||
type AdminRole struct {
|
||||
core.Model
|
||||
Name string `json:"name" gorm:"column:name"` // 角色名称
|
||||
Description *string `json:"description" gorm:"column:description"` // 角色描述
|
||||
Active bool `json:"active" gorm:"column:active"` // 是否激活
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
Name string `json:"name" gorm:"column:name"` // 角色名称
|
||||
Description *string `json:"description,omitempty" gorm:"column:description"` // 角色描述
|
||||
Active bool `json:"active" gorm:"column:active"` // 是否激活
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
// Announcement 公告表
|
||||
type Announcement struct {
|
||||
core.Model
|
||||
Title string `json:"title" gorm:"column:title"` // 公告标题
|
||||
Content *string `json:"content" gorm:"column:content"` // 公告内容
|
||||
Type AnnouncementType `json:"type" gorm:"column:type"` // 公告类型:1-普通公告
|
||||
Pin bool `json:"pin" gorm:"column:pin"` // 是否置顶
|
||||
Status AnnouncementStatus `json:"status" gorm:"column:status"` // 公告状态:0-禁用,1-正常
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 公告排序
|
||||
Title string `json:"title" gorm:"column:title"` // 公告标题
|
||||
Content *string `json:"content,omitempty" gorm:"column:content"` // 公告内容
|
||||
Type AnnouncementType `json:"type" gorm:"column:type"` // 公告类型:1-普通公告
|
||||
Pin bool `json:"pin" gorm:"column:pin"` // 是否置顶
|
||||
Status AnnouncementStatus `json:"status" gorm:"column:status"` // 公告状态:0-禁用,1-正常
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 公告排序
|
||||
}
|
||||
|
||||
// AnnouncementType 公告类型枚举
|
||||
|
||||
@@ -9,19 +9,19 @@ import (
|
||||
// Bill 账单表
|
||||
type Bill struct {
|
||||
core.Model
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
TradeID *int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID
|
||||
ResourceID *int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
RefundID *int32 `json:"refund_id" gorm:"column:refund_id"` // 退款ID
|
||||
BillNo string `json:"bill_no" gorm:"column:bill_no"` // 易读账单号
|
||||
Info *string `json:"info" gorm:"column:info"` // 产品可读信息
|
||||
Type BillType `json:"type" gorm:"column:type"` // 账单类型:1-消费,2-退款,3-充值
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 账单金额
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
TradeID *int32 `json:"trade_id,omitempty" gorm:"column:trade_id"` // 订单ID
|
||||
ResourceID *int32 `json:"resource_id,omitempty" gorm:"column:resource_id"` // 套餐ID
|
||||
RefundID *int32 `json:"refund_id,omitempty" gorm:"column:refund_id"` // 退款ID
|
||||
BillNo string `json:"bill_no" gorm:"column:bill_no"` // 易读账单号
|
||||
Info *string `json:"info,omitempty" gorm:"column:info"` // 产品可读信息
|
||||
Type BillType `json:"type" gorm:"column:type"` // 账单类型:1-消费,2-退款,3-充值
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 账单金额
|
||||
|
||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Trade *Trade `json:"trade" gorm:"foreignKey:TradeID"`
|
||||
Resource *Resource `json:"resource" gorm:"foreignKey:ResourceID"`
|
||||
Refund *Refund `json:"refund" gorm:"foreignKey:RefundID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Trade *Trade `json:"trade,omitempty" gorm:"foreignKey:TradeID"`
|
||||
Resource *Resource `json:"resource,omitempty" gorm:"foreignKey:ResourceID"`
|
||||
Refund *Refund `json:"refund,omitempty" gorm:"foreignKey:RefundID"`
|
||||
}
|
||||
|
||||
// BillType 账单类型枚举
|
||||
|
||||
@@ -9,23 +9,25 @@ import (
|
||||
// Channel 通道表
|
||||
type Channel struct {
|
||||
core.Model
|
||||
UserID int32 `json:"user_id" gorm:"column:user_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"` // 批次编号
|
||||
Port uint16 `json:"port" gorm:"column:port"` // 代理端口
|
||||
EdgeID *int32 `json:"edge_id" gorm:"column:edge_id"` // 节点ID(手动配置)
|
||||
FilterISP *EdgeISP `json:"filter_isp" gorm:"column:filter_isp"` // 运营商过滤(自动配置):参考 edge.isp
|
||||
FilterProv *string `json:"filter_prov" gorm:"column:filter_prov"` // 省份过滤(自动配置)
|
||||
FilterCity *string `json:"filter_city" gorm:"column:filter_city"` // 城市过滤(自动配置)
|
||||
IP *orm.Inet `json:"ip" gorm:"column:ip"` // 节点地址
|
||||
Whitelists *string `json:"whitelists" gorm:"column:whitelists"` // IP白名单,逗号分隔
|
||||
Username *string `json:"username" gorm:"column:username"` // 用户名
|
||||
Password *string `json:"password" gorm:"column:password"` // 密码
|
||||
ExpiredAt time.Time `json:"expired_at" gorm:"column:expired_at"` // 过期时间
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
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"` // 代理端口
|
||||
EdgeID *int32 `json:"edge_id,omitempty" gorm:"column:edge_id"` // 节点ID(手动配置)
|
||||
EdgeRef *string `json:"edge_ref,omitempty" gorm:"column:edge_ref"` // 外部节点引用,用于索引没有ID的外部非受控节点
|
||||
FilterISP *EdgeISP `json:"filter_isp,omitempty" gorm:"column:filter_isp"` // 运营商过滤(自动配置):参考 edge.isp
|
||||
FilterProv *string `json:"filter_prov,omitempty" gorm:"column:filter_prov"` // 省份过滤(自动配置)
|
||||
FilterCity *string `json:"filter_city,omitempty" gorm:"column:filter_city"` // 城市过滤(自动配置)
|
||||
IP *orm.Inet `json:"ip,omitempty" gorm:"column:ip"` // 节点地址
|
||||
Whitelists *string `json:"whitelists,omitempty" gorm:"column:whitelists"` // IP白名单,逗号分隔
|
||||
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"` // 过期时间
|
||||
|
||||
User User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Resource Resource `json:"resource" gorm:"foreignKey:ResourceID"`
|
||||
Proxy Proxy `json:"proxy" gorm:"foreignKey:ProxyID"`
|
||||
Edge *Edge `json:"edge" gorm:"foreignKey:EdgeID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Resource *Resource `json:"resource,omitempty" gorm:"foreignKey:ResourceID"`
|
||||
Proxy *Proxy `json:"proxy,omitempty" gorm:"foreignKey:ProxyID"`
|
||||
Edge *Edge `json:"edge,omitempty" gorm:"foreignKey:EdgeID"`
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
// Client 客户端表
|
||||
type Client struct {
|
||||
core.Model
|
||||
ClientID string `json:"client_id" gorm:"column:client_id"` // OAuth2客户端标识符
|
||||
ClientSecret string `json:"client_secret" gorm:"column:client_secret"` // OAuth2客户端密钥
|
||||
RedirectURI *string `json:"redirect_uri" gorm:"column:redirect_uri"` // OAuth2 重定向URI
|
||||
Spec ClientSpec `json:"spec" gorm:"column:spec"` // 安全规范:1-native,2-browser,3-web,4-api
|
||||
Name string `json:"name" gorm:"column:name"` // 名称
|
||||
Icon *string `json:"icon" gorm:"column:icon"` // 图标URL
|
||||
Status ClientStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
||||
Type ClientType `json:"type" gorm:"column:type"` // 类型:0-普通,1-官方
|
||||
ClientID string `json:"client_id" gorm:"column:client_id"` // OAuth2客户端标识符
|
||||
ClientSecret string `json:"client_secret" gorm:"column:client_secret"` // OAuth2客户端密钥
|
||||
RedirectURI *string `json:"redirect_uri,omitempty" gorm:"column:redirect_uri"` // OAuth2 重定向URI
|
||||
Spec ClientSpec `json:"spec" gorm:"column:spec"` // 安全规范:1-native,2-browser,3-web,4-api
|
||||
Name string `json:"name" gorm:"column:name"` // 名称
|
||||
Icon *string `json:"icon,omitempty" gorm:"column:icon"` // 图标URL
|
||||
Status ClientStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
||||
Type ClientType `json:"type" gorm:"column:type"` // 类型:0-普通,1-官方
|
||||
}
|
||||
|
||||
// ClientSpec 客户端安全规范枚举
|
||||
|
||||
@@ -10,13 +10,13 @@ import (
|
||||
// Coupon 优惠券表
|
||||
type Coupon struct {
|
||||
core.Model
|
||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
Code string `json:"code" gorm:"column:code"` // 优惠券代码
|
||||
Remark *string `json:"remark" gorm:"column:remark"` // 优惠券备注
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 优惠券金额
|
||||
MinAmount decimal.Decimal `json:"min_amount" gorm:"column:min_amount"` // 最低消费金额
|
||||
Status CouponStatus `json:"status" gorm:"column:status"` // 优惠券状态:0-未使用,1-已使用,2-已过期
|
||||
ExpireAt *time.Time `json:"expire_at" gorm:"column:expire_at"` // 过期时间
|
||||
UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
|
||||
Code string `json:"code" gorm:"column:code"` // 优惠券代码
|
||||
Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 优惠券备注
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 优惠券金额
|
||||
MinAmount decimal.Decimal `json:"min_amount" gorm:"column:min_amount"` // 最低消费金额
|
||||
Status CouponStatus `json:"status" gorm:"column:status"` // 优惠券状态:0-未使用,1-已使用,2-已过期
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 过期时间
|
||||
}
|
||||
|
||||
// CouponStatus 优惠券状态枚举
|
||||
|
||||
25
web/models/inquiry.go
Normal file
25
web/models/inquiry.go
Normal 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 // 已处理
|
||||
)
|
||||
@@ -13,10 +13,10 @@ type LogsLogin struct {
|
||||
GrantType GrantType `json:"grant_type" gorm:"column:grant_type"` // 授权类型
|
||||
PasswordType PasswordType `json:"password_type" gorm:"column:password_type"` // 密码模式子授权类型
|
||||
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"` // 登录时间
|
||||
|
||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// GrantType 授权类型枚举
|
||||
|
||||
@@ -7,18 +7,18 @@ import (
|
||||
|
||||
// LogsRequest 访问日志表
|
||||
type LogsRequest struct {
|
||||
ID int32 `json:"id" gorm:"column:id"` // 访问日志ID
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
|
||||
UA string `json:"ua" gorm:"column:ua"` // 用户代理
|
||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
ClientID *int32 `json:"client_id" gorm:"column:client_id"` // 客户端ID
|
||||
Method string `json:"method" gorm:"column:method"` // 请求方法
|
||||
Path string `json:"path" gorm:"column:path"` // 请求路径
|
||||
Status int16 `json:"status" gorm:"column:status"` // 响应状态码
|
||||
Error *string `json:"error" gorm:"column:error"` // 错误信息
|
||||
Time time.Time `json:"time" gorm:"column:time"` // 请求时间
|
||||
Latency string `json:"latency" gorm:"column:latency"` // 请求延迟
|
||||
ID int32 `json:"id" gorm:"column:id"` // 访问日志ID
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
|
||||
UA string `json:"ua" gorm:"column:ua"` // 用户代理
|
||||
UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
|
||||
ClientID *int32 `json:"client_id,omitempty" gorm:"column:client_id"` // 客户端ID
|
||||
Method string `json:"method" gorm:"column:method"` // 请求方法
|
||||
Path string `json:"path" gorm:"column:path"` // 请求路径
|
||||
Status int16 `json:"status" gorm:"column:status"` // 响应状态码
|
||||
Error *string `json:"error,omitempty" gorm:"column:error"` // 错误信息
|
||||
Time time.Time `json:"time" gorm:"column:time"` // 请求时间
|
||||
Latency string `json:"latency" gorm:"column:latency"` // 请求延迟
|
||||
|
||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Client *Client `json:"client" gorm:"foreignKey:ClientID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Client *Client `json:"client,omitempty" gorm:"foreignKey:ClientID"`
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ type LogsUserUsage struct {
|
||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||
BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号
|
||||
Count int32 `json:"count" gorm:"column:count"` // 数量
|
||||
Prov *string `json:"prov" gorm:"column:prov"` // 省份
|
||||
City *string `json:"city" gorm:"column:city"` // 城市
|
||||
ISP *string `json:"isp" gorm:"column:isp"` // 运营商
|
||||
Prov *string `json:"prov,omitempty" gorm:"column:prov"` // 省份
|
||||
City *string `json:"city,omitempty" gorm:"column:city"` // 城市
|
||||
ISP *string `json:"isp,omitempty" gorm:"column:isp"` // 运营商
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip"` // IP地址
|
||||
Time time.Time `json:"time" gorm:"column:time"` // 提取时间
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import "platform/web/core"
|
||||
// Permission 权限表
|
||||
type Permission struct {
|
||||
core.Model
|
||||
ParentID *int32 `json:"parent_id" gorm:"column:parent_id"` // 父权限ID
|
||||
Name string `json:"name" gorm:"column:name"` // 权限名称
|
||||
Description *string `json:"description" gorm:"column:description"` // 权限描述
|
||||
ParentID *int32 `json:"parent_id,omitempty" gorm:"column:parent_id"` // 父权限ID
|
||||
Name string `json:"name" gorm:"column:name"` // 权限名称
|
||||
Description *string `json:"description,omitempty" gorm:"column:description"` // 权限描述
|
||||
|
||||
Parent *Permission `json:"parent" gorm:"foreignKey:ParentID"`
|
||||
Children []*Permission `json:"children" gorm:"foreignKey:ParentID"`
|
||||
Parent *Permission `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
|
||||
Children []*Permission `json:"children,omitempty" gorm:"foreignKey:ParentID"`
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
// Product 产品表
|
||||
type Product struct {
|
||||
core.Model
|
||||
Code string `json:"code" gorm:"column:code"` // 产品代码
|
||||
Name string `json:"name" gorm:"column:name"` // 产品名称
|
||||
Description *string `json:"description" gorm:"column:description"` // 产品描述
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态:0-禁用,1-正常
|
||||
Code string `json:"code" gorm:"column:code"` // 产品代码
|
||||
Name string `json:"name" gorm:"column:name"` // 产品名称
|
||||
Description *string `json:"description,omitempty" gorm:"column:description"` // 产品描述
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态:0-禁用,1-正常
|
||||
}
|
||||
|
||||
// ProductStatus 产品状态枚举
|
||||
|
||||
@@ -10,15 +10,16 @@ import (
|
||||
// Proxy 代理服务表
|
||||
type Proxy struct {
|
||||
core.Model
|
||||
Version int32 `json:"version" gorm:"column:version"` // 代理服务版本
|
||||
Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址
|
||||
Secret *string `json:"secret" gorm:"column:secret"` // 代理服务密钥
|
||||
Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型:1-自有,2-白银
|
||||
Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态:0-离线,1-在线
|
||||
Meta *datatypes.JSONType[any] `json:"meta" gorm:"column:meta"` // 代理服务元信息
|
||||
Version int32 `json:"version" gorm:"column:version"` // 代理服务版本
|
||||
Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址
|
||||
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-白银
|
||||
Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态:0-离线,1-在线
|
||||
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 代理服务类型枚举
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
// Refund 退款记录表
|
||||
type Refund struct {
|
||||
core.Model
|
||||
TradeID int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID
|
||||
ProductID *int32 `json:"product_id" gorm:"column:product_id"` // 产品ID
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 退款金额
|
||||
Reason *string `json:"reason" gorm:"column:reason"` // 退款原因
|
||||
Status RefundStatus `json:"status" gorm:"column:status"` // 退款状态:0-待处理,1-已退款,2-已拒绝
|
||||
TradeID int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID
|
||||
ProductID *int32 `json:"product_id,omitempty" gorm:"column:product_id"` // 产品ID
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 退款金额
|
||||
Reason *string `json:"reason,omitempty" gorm:"column:reason"` // 退款原因
|
||||
Status RefundStatus `json:"status" gorm:"column:status"` // 退款状态:0-待处理,1-已退款,2-已拒绝
|
||||
}
|
||||
|
||||
// RefundStatus 退款状态枚举
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
// Resource 套餐表
|
||||
type Resource struct {
|
||||
core.Model
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
ResourceNo *string `json:"resource_no" gorm:"column:resource_no"` // 套餐编号
|
||||
Active bool `json:"active" gorm:"column:active"` // 套餐状态
|
||||
Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型:1-短效动态,2-长效动态
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
ResourceNo *string `json:"resource_no,omitempty" gorm:"column:resource_no"` // 套餐编号
|
||||
Active bool `json:"active" gorm:"column:active"` // 套餐状态
|
||||
Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型:1-短效动态,2-长效动态
|
||||
|
||||
User User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Short *ResourceShort `json:"short" gorm:"foreignKey:ResourceID"`
|
||||
Long *ResourceLong `json:"long" gorm:"foreignKey:ResourceID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Short *ResourceShort `json:"short,omitempty" gorm:"foreignKey:ResourceID"`
|
||||
Long *ResourceLong `json:"long,omitempty" gorm:"foreignKey:ResourceID"`
|
||||
}
|
||||
|
||||
// ResourceType 套餐类型枚举
|
||||
|
||||
@@ -6,14 +6,13 @@ import (
|
||||
|
||||
// ResourceLong 长效动态套餐表
|
||||
type ResourceLong struct {
|
||||
ID int32 `json:"id" gorm:"column: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"` // 可用时长(天)
|
||||
Expire *time.Time `json:"expire" gorm:"column:expire"` // 过期时间
|
||||
Quota *int32 `json:"quota" gorm:"column:quota"` // 配额数量
|
||||
Used int32 `json:"used" gorm:"column:used"` // 已用数量
|
||||
DailyLimit int32 `json:"daily_limit" gorm:"column:daily_limit"` // 每日限制
|
||||
DailyUsed int32 `json:"daily_used" gorm:"column:daily_used"` // 今日已用数量
|
||||
DailyLast *time.Time `json:"daily_last" gorm:"column:daily_last"` // 今日最后使用时间
|
||||
ID int32 `json:"id" gorm:"column: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-包量
|
||||
Quota int32 `json:"quota" gorm:"column:quota"` // 每日配额(包时)或总配额(包量)
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 套餐过期时间,包时模式可用
|
||||
Used int32 `json:"used" gorm:"column:used"` // 总用量
|
||||
Daily int32 `json:"daily" gorm:"column:daily"` // 当日用量
|
||||
LastAt *time.Time `json:"last_at,omitempty" gorm:"column:last_at"` // 最后使用时间
|
||||
}
|
||||
|
||||
@@ -6,14 +6,13 @@ import (
|
||||
|
||||
// ResourceShort 短效动态套餐表
|
||||
type ResourceShort struct {
|
||||
ID int32 `json:"id" gorm:"column: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"` // 可用时长(秒)
|
||||
Expire *time.Time `json:"expire" gorm:"column:expire"` // 过期时间
|
||||
Quota *int32 `json:"quota" gorm:"column:quota"` // 配额数量
|
||||
Used int32 `json:"used" gorm:"column:used"` // 已用数量
|
||||
DailyLimit int32 `json:"daily_limit" gorm:"column:daily_limit"` // 每日限制
|
||||
DailyUsed int32 `json:"daily_used" gorm:"column:daily_used"` // 今日已用数量
|
||||
DailyLast *time.Time `json:"daily_last" gorm:"column:daily_last"` // 今日最后使用时间
|
||||
ID int32 `json:"id" gorm:"column: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-包量
|
||||
Quota int32 `json:"quota" gorm:"column:quota"` // 每日配额(包时)或总配额(包量)
|
||||
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 套餐过期时间,包时模式可用
|
||||
Used int32 `json:"used" gorm:"column:used"` // 总用量
|
||||
Daily int32 `json:"daily" gorm:"column:daily"` // 当日用量
|
||||
LastAt *time.Time `json:"last_at,omitempty" gorm:"column:last_at"` // 最后使用时间
|
||||
}
|
||||
|
||||
@@ -9,18 +9,18 @@ import (
|
||||
// Session 会话表
|
||||
type Session struct {
|
||||
core.Model
|
||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
AdminID *int32 `json:"admin_id" gorm:"column:admin_id"` // 管理员ID
|
||||
ClientID *int32 `json:"client_id" gorm:"column:client_id"` // 客户端ID
|
||||
IP *orm.Inet `json:"ip" gorm:"column:ip"` // IP地址
|
||||
UA *string `json:"ua" gorm:"column:ua"` // 用户代理
|
||||
AccessToken string `json:"access_token" gorm:"column:access_token"` // 访问令牌
|
||||
AccessTokenExpires time.Time `json:"access_token_expires" gorm:"column:access_token_expires"` // 访问令牌过期时间
|
||||
RefreshToken *string `json:"refresh_token" gorm:"column:refresh_token"` // 刷新令牌
|
||||
RefreshTokenExpires *time.Time `json:"refresh_token_expires" gorm:"column:refresh_token_expires"` // 刷新令牌过期时间
|
||||
Scopes *string `json:"scopes" gorm:"column:scopes"` // 权限范围
|
||||
UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
|
||||
AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID
|
||||
ClientID *int32 `json:"client_id,omitempty" gorm:"column:client_id"` // 客户端ID
|
||||
IP *orm.Inet `json:"ip,omitempty" gorm:"column:ip"` // IP地址
|
||||
UA *string `json:"ua,omitempty" gorm:"column:ua"` // 用户代理
|
||||
AccessToken string `json:"access_token" gorm:"column:access_token"` // 访问令牌
|
||||
AccessTokenExpires time.Time `json:"access_token_expires" gorm:"column:access_token_expires"` // 访问令牌过期时间
|
||||
RefreshToken *string `json:"refresh_token,omitempty" gorm:"column:refresh_token"` // 刷新令牌
|
||||
RefreshTokenExpires *time.Time `json:"refresh_token_expires,omitempty" gorm:"column:refresh_token_expires"` // 刷新令牌过期时间
|
||||
Scopes *string `json:"scopes,omitempty" gorm:"column:scopes"` // 权限范围
|
||||
|
||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
||||
Admin *Admin `json:"admin" gorm:"foreignKey:AdminID"`
|
||||
Client *Client `json:"client" gorm:"foreignKey:ClientID;belongsTo:ID"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"`
|
||||
Client *Client `json:"client,omitempty" gorm:"foreignKey:ClientID;belongsTo:ID"`
|
||||
}
|
||||
|
||||
@@ -10,22 +10,22 @@ import (
|
||||
// Trade 订单表
|
||||
type Trade struct {
|
||||
core.Model
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
InnerNo string `json:"inner_no" gorm:"column:inner_no"` // 内部订单号
|
||||
OuterNo *string `json:"outer_no" gorm:"column:outer_no"` // 外部订单号
|
||||
Type TradeType `json:"type" gorm:"column:type"` // 订单类型:1-购买产品,2-充值余额
|
||||
Subject string `json:"subject" gorm:"column:subject"` // 订单主题
|
||||
Remark *string `json:"remark" gorm:"column:remark"` // 订单备注
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 订单总金额
|
||||
Payment decimal.Decimal `json:"payment" gorm:"column:payment"` // 实际支付金额
|
||||
Method TradeMethod `json:"method" gorm:"column:method"` // 支付方式:1-支付宝,2-微信,3-商福通,4-商福通渠道支付宝,5-商福通渠道微信
|
||||
Platform TradePlatform `json:"platform" gorm:"column:platform"` // 支付平台:1-电脑网站,2-手机网站
|
||||
Acquirer *TradeAcquirer `json:"acquirer" gorm:"column:acquirer"` // 收单机构:1-支付宝,2-微信,3-银联
|
||||
Status TradeStatus `json:"status" gorm:"column:status"` // 订单状态:0-待支付,1-已支付,2-已取消
|
||||
Refunded bool `json:"refunded" gorm:"column:refunded"` // 是否已退款
|
||||
PaymentURL *string `json:"payment_url" gorm:"column:payment_url"` // 支付链接
|
||||
CompletedAt *time.Time `json:"completed_at" gorm:"column:completed_at"` // 支付时间
|
||||
CanceledAt *time.Time `json:"canceled_at" gorm:"column:canceled_at"` // 取消时间
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
InnerNo string `json:"inner_no" gorm:"column:inner_no"` // 内部订单号
|
||||
OuterNo *string `json:"outer_no,omitempty" gorm:"column:outer_no"` // 外部订单号
|
||||
Type TradeType `json:"type" gorm:"column:type"` // 订单类型:1-购买产品,2-充值余额
|
||||
Subject string `json:"subject" gorm:"column:subject"` // 订单主题
|
||||
Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 订单备注
|
||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 订单总金额
|
||||
Payment decimal.Decimal `json:"payment" gorm:"column:payment"` // 实际支付金额
|
||||
Method TradeMethod `json:"method" gorm:"column:method"` // 支付方式:1-支付宝,2-微信,3-商福通,4-商福通渠道支付宝,5-商福通渠道微信
|
||||
Platform TradePlatform `json:"platform" gorm:"column:platform"` // 支付平台:1-电脑网站,2-手机网站
|
||||
Acquirer *TradeAcquirer `json:"acquirer,omitempty" gorm:"column:acquirer"` // 收单机构:1-支付宝,2-微信,3-银联
|
||||
Status TradeStatus `json:"status" gorm:"column:status"` // 订单状态:0-待支付,1-已支付,2-已取消
|
||||
Refunded bool `json:"refunded" gorm:"column:refunded"` // 是否已退款
|
||||
PaymentURL *string `json:"payment_url,omitempty" gorm:"column:payment_url"` // 支付链接
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" gorm:"column:completed_at"` // 支付时间
|
||||
CanceledAt *time.Time `json:"canceled_at,omitempty" gorm:"column:canceled_at"` // 取消时间
|
||||
}
|
||||
|
||||
// TradeType 订单类型枚举
|
||||
|
||||
@@ -11,25 +11,25 @@ import (
|
||||
// User 用户表
|
||||
type User struct {
|
||||
core.Model
|
||||
AdminID *int32 `json:"admin_id" gorm:"column:admin_id"` // 管理员ID
|
||||
Phone string `json:"phone" gorm:"column:phone"` // 手机号码
|
||||
Username *string `json:"username" gorm:"column:username"` // 用户名
|
||||
Email *string `json:"email" gorm:"column:email"` // 邮箱
|
||||
Password *string `json:"password" gorm:"column:password"` // 用户密码
|
||||
Name *string `json:"name" gorm:"column:name"` // 真实姓名
|
||||
Avatar *string `json:"avatar" gorm:"column:avatar"` // 头像URL
|
||||
Status UserStatus `json:"status" gorm:"column:status"` // 用户状态:0-禁用,1-正常
|
||||
Balance decimal.Decimal `json:"balance" gorm:"column:balance"` // 账户余额
|
||||
IDType UserIDType `json:"id_type" gorm:"column:id_type"` // 认证类型:0-未认证,1-个人认证,2-企业认证
|
||||
IDNo *string `json:"id_no" gorm:"column:id_no"` // 身份证号或营业执照号
|
||||
IDToken *string `json:"id_token" gorm:"column:id_token"` // 身份验证标识
|
||||
ContactQQ *string `json:"contact_qq" gorm:"column:contact_qq"` // QQ联系方式
|
||||
ContactWechat *string `json:"contact_wechat" gorm:"column:contact_wechat"` // 微信联系方式
|
||||
LastLogin *time.Time `json:"last_login" gorm:"column:last_login"` // 最后登录时间
|
||||
LastLoginIP *orm.Inet `json:"last_login_ip" gorm:"column:last_login_ip"` // 最后登录地址
|
||||
LastLoginUA *string `json:"last_login_ua" gorm:"column:last_login_ua"` // 最后登录代理
|
||||
AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID
|
||||
Phone string `json:"phone" gorm:"column:phone"` // 手机号码
|
||||
Username *string `json:"username,omitempty" gorm:"column:username"` // 用户名
|
||||
Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱
|
||||
Password *string `json:"password,omitempty" gorm:"column:password"` // 用户密码
|
||||
Name *string `json:"name,omitempty" gorm:"column:name"` // 真实姓名
|
||||
Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL
|
||||
Status UserStatus `json:"status" gorm:"column:status"` // 用户状态:0-禁用,1-正常
|
||||
Balance decimal.Decimal `json:"balance" gorm:"column:balance"` // 账户余额
|
||||
IDType UserIDType `json:"id_type" gorm:"column:id_type"` // 认证类型:0-未认证,1-个人认证,2-企业认证
|
||||
IDNo *string `json:"id_no,omitempty" gorm:"column:id_no"` // 身份证号或营业执照号
|
||||
IDToken *string `json:"id_token,omitempty" gorm:"column:id_token"` // 身份验证标识
|
||||
ContactQQ *string `json:"contact_qq,omitempty" gorm:"column:contact_qq"` // QQ联系方式
|
||||
ContactWechat *string `json:"contact_wechat,omitempty" gorm:"column:contact_wechat"` // 微信联系方式
|
||||
LastLogin *time.Time `json:"last_login,omitempty" gorm:"column:last_login"` // 最后登录时间
|
||||
LastLoginIP *orm.Inet `json:"last_login_ip,omitempty" gorm:"column:last_login_ip"` // 最后登录地址
|
||||
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 用户状态枚举
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
// UserRole 用户角色表
|
||||
type UserRole struct {
|
||||
core.Model
|
||||
Name string `json:"name" gorm:"column:name"` // 角色名称
|
||||
Description *string `json:"description" gorm:"column:description"` // 角色描述
|
||||
Active bool `json:"active" gorm:"column:active"` // 是否激活
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
Name string `json:"name" gorm:"column:name"` // 角色名称
|
||||
Description *string `json:"description,omitempty" gorm:"column:description"` // 角色描述
|
||||
Active bool `json:"active" gorm:"column:active"` // 是否激活
|
||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
// Whitelist 白名单表
|
||||
type Whitelist struct {
|
||||
core.Model
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
|
||||
Remark *string `json:"remark" gorm:"column:remark"` // 备注
|
||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
|
||||
Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 备注
|
||||
}
|
||||
|
||||
@@ -33,10 +33,12 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
|
||||
_channel.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||
_channel.UserID = field.NewInt32(tableName, "user_id")
|
||||
_channel.ResourceID = field.NewInt32(tableName, "resource_id")
|
||||
_channel.ProxyID = field.NewInt32(tableName, "proxy_id")
|
||||
_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.EdgeID = field.NewInt32(tableName, "edge_id")
|
||||
_channel.EdgeRef = field.NewString(tableName, "edge_ref")
|
||||
_channel.FilterISP = field.NewInt(tableName, "filter_isp")
|
||||
_channel.FilterProv = field.NewString(tableName, "filter_prov")
|
||||
_channel.FilterCity = field.NewString(tableName, "filter_city")
|
||||
@@ -141,10 +143,12 @@ type channel struct {
|
||||
DeletedAt field.Field
|
||||
UserID field.Int32
|
||||
ResourceID field.Int32
|
||||
ProxyID field.Int32
|
||||
BatchNo field.String
|
||||
ProxyID field.Int32
|
||||
Host field.String
|
||||
Port field.Uint16
|
||||
EdgeID field.Int32
|
||||
EdgeRef field.String
|
||||
FilterISP field.Int
|
||||
FilterProv field.String
|
||||
FilterCity field.String
|
||||
@@ -182,10 +186,12 @@ func (c *channel) updateTableName(table string) *channel {
|
||||
c.DeletedAt = field.NewField(table, "deleted_at")
|
||||
c.UserID = field.NewInt32(table, "user_id")
|
||||
c.ResourceID = field.NewInt32(table, "resource_id")
|
||||
c.ProxyID = field.NewInt32(table, "proxy_id")
|
||||
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.EdgeID = field.NewInt32(table, "edge_id")
|
||||
c.EdgeRef = field.NewString(table, "edge_ref")
|
||||
c.FilterISP = field.NewInt(table, "filter_isp")
|
||||
c.FilterProv = field.NewString(table, "filter_prov")
|
||||
c.FilterCity = field.NewString(table, "filter_city")
|
||||
@@ -210,17 +216,19 @@ func (c *channel) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
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["created_at"] = c.CreatedAt
|
||||
c.fieldMap["updated_at"] = c.UpdatedAt
|
||||
c.fieldMap["deleted_at"] = c.DeletedAt
|
||||
c.fieldMap["user_id"] = c.UserID
|
||||
c.fieldMap["resource_id"] = c.ResourceID
|
||||
c.fieldMap["proxy_id"] = c.ProxyID
|
||||
c.fieldMap["batch_no"] = c.BatchNo
|
||||
c.fieldMap["proxy_id"] = c.ProxyID
|
||||
c.fieldMap["host"] = c.Host
|
||||
c.fieldMap["port"] = c.Port
|
||||
c.fieldMap["edge_id"] = c.EdgeID
|
||||
c.fieldMap["edge_ref"] = c.EdgeRef
|
||||
c.fieldMap["filter_isp"] = c.FilterISP
|
||||
c.fieldMap["filter_prov"] = c.FilterProv
|
||||
c.fieldMap["filter_city"] = c.FilterCity
|
||||
|
||||
@@ -25,6 +25,7 @@ var (
|
||||
Client *client
|
||||
Coupon *coupon
|
||||
Edge *edge
|
||||
Inquiry *inquiry
|
||||
LinkAdminRole *linkAdminRole
|
||||
LinkAdminRolePermission *linkAdminRolePermission
|
||||
LinkClientPermission *linkClientPermission
|
||||
@@ -58,6 +59,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
||||
Client = &Q.Client
|
||||
Coupon = &Q.Coupon
|
||||
Edge = &Q.Edge
|
||||
Inquiry = &Q.Inquiry
|
||||
LinkAdminRole = &Q.LinkAdminRole
|
||||
LinkAdminRolePermission = &Q.LinkAdminRolePermission
|
||||
LinkClientPermission = &Q.LinkClientPermission
|
||||
@@ -92,6 +94,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
||||
Client: newClient(db, opts...),
|
||||
Coupon: newCoupon(db, opts...),
|
||||
Edge: newEdge(db, opts...),
|
||||
Inquiry: newInquiry(db, opts...),
|
||||
LinkAdminRole: newLinkAdminRole(db, opts...),
|
||||
LinkAdminRolePermission: newLinkAdminRolePermission(db, opts...),
|
||||
LinkClientPermission: newLinkClientPermission(db, opts...),
|
||||
@@ -127,6 +130,7 @@ type Query struct {
|
||||
Client client
|
||||
Coupon coupon
|
||||
Edge edge
|
||||
Inquiry inquiry
|
||||
LinkAdminRole linkAdminRole
|
||||
LinkAdminRolePermission linkAdminRolePermission
|
||||
LinkClientPermission linkClientPermission
|
||||
@@ -163,6 +167,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
||||
Client: q.Client.clone(db),
|
||||
Coupon: q.Coupon.clone(db),
|
||||
Edge: q.Edge.clone(db),
|
||||
Inquiry: q.Inquiry.clone(db),
|
||||
LinkAdminRole: q.LinkAdminRole.clone(db),
|
||||
LinkAdminRolePermission: q.LinkAdminRolePermission.clone(db),
|
||||
LinkClientPermission: q.LinkClientPermission.clone(db),
|
||||
@@ -206,6 +211,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
||||
Client: q.Client.replaceDB(db),
|
||||
Coupon: q.Coupon.replaceDB(db),
|
||||
Edge: q.Edge.replaceDB(db),
|
||||
Inquiry: q.Inquiry.replaceDB(db),
|
||||
LinkAdminRole: q.LinkAdminRole.replaceDB(db),
|
||||
LinkAdminRolePermission: q.LinkAdminRolePermission.replaceDB(db),
|
||||
LinkClientPermission: q.LinkClientPermission.replaceDB(db),
|
||||
@@ -239,6 +245,7 @@ type queryCtx struct {
|
||||
Client *clientDo
|
||||
Coupon *couponDo
|
||||
Edge *edgeDo
|
||||
Inquiry *inquiryDo
|
||||
LinkAdminRole *linkAdminRoleDo
|
||||
LinkAdminRolePermission *linkAdminRolePermissionDo
|
||||
LinkClientPermission *linkClientPermissionDo
|
||||
@@ -272,6 +279,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
||||
Client: q.Client.WithContext(ctx),
|
||||
Coupon: q.Coupon.WithContext(ctx),
|
||||
Edge: q.Edge.WithContext(ctx),
|
||||
Inquiry: q.Inquiry.WithContext(ctx),
|
||||
LinkAdminRole: q.LinkAdminRole.WithContext(ctx),
|
||||
LinkAdminRolePermission: q.LinkAdminRolePermission.WithContext(ctx),
|
||||
LinkClientPermission: q.LinkClientPermission.WithContext(ctx),
|
||||
|
||||
359
web/queries/inquiry.gen.go
Normal file
359
web/queries/inquiry.gen.go
Normal 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
|
||||
}
|
||||
@@ -34,6 +34,7 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
|
||||
_proxy.Version = field.NewInt32(tableName, "version")
|
||||
_proxy.Mac = field.NewString(tableName, "mac")
|
||||
_proxy.IP = field.NewField(tableName, "ip")
|
||||
_proxy.Host = field.NewString(tableName, "host")
|
||||
_proxy.Secret = field.NewString(tableName, "secret")
|
||||
_proxy.Type = field.NewInt(tableName, "type")
|
||||
_proxy.Status = field.NewInt(tableName, "status")
|
||||
@@ -120,6 +121,7 @@ type proxy struct {
|
||||
Version field.Int32
|
||||
Mac field.String
|
||||
IP field.Field
|
||||
Host field.String
|
||||
Secret field.String
|
||||
Type field.Int
|
||||
Status field.Int
|
||||
@@ -148,6 +150,7 @@ func (p *proxy) updateTableName(table string) *proxy {
|
||||
p.Version = field.NewInt32(table, "version")
|
||||
p.Mac = field.NewString(table, "mac")
|
||||
p.IP = field.NewField(table, "ip")
|
||||
p.Host = field.NewString(table, "host")
|
||||
p.Secret = field.NewString(table, "secret")
|
||||
p.Type = field.NewInt(table, "type")
|
||||
p.Status = field.NewInt(table, "status")
|
||||
@@ -168,7 +171,7 @@ func (p *proxy) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
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["created_at"] = p.CreatedAt
|
||||
p.fieldMap["updated_at"] = p.UpdatedAt
|
||||
@@ -176,6 +179,7 @@ func (p *proxy) fillFieldMap() {
|
||||
p.fieldMap["version"] = p.Version
|
||||
p.fieldMap["mac"] = p.Mac
|
||||
p.fieldMap["ip"] = p.IP
|
||||
p.fieldMap["host"] = p.Host
|
||||
p.fieldMap["secret"] = p.Secret
|
||||
p.fieldMap["type"] = p.Type
|
||||
p.fieldMap["status"] = p.Status
|
||||
|
||||
@@ -29,14 +29,13 @@ func newResourceLong(db *gorm.DB, opts ...gen.DOOption) resourceLong {
|
||||
_resourceLong.ALL = field.NewAsterisk(tableName)
|
||||
_resourceLong.ID = field.NewInt32(tableName, "id")
|
||||
_resourceLong.ResourceID = field.NewInt32(tableName, "resource_id")
|
||||
_resourceLong.Type = field.NewInt(tableName, "type")
|
||||
_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.ExpireAt = field.NewTime(tableName, "expire_at")
|
||||
_resourceLong.Used = field.NewInt32(tableName, "used")
|
||||
_resourceLong.DailyLimit = field.NewInt32(tableName, "daily_limit")
|
||||
_resourceLong.DailyUsed = field.NewInt32(tableName, "daily_used")
|
||||
_resourceLong.DailyLast = field.NewTime(tableName, "daily_last")
|
||||
_resourceLong.Daily = field.NewInt32(tableName, "daily")
|
||||
_resourceLong.LastAt = field.NewTime(tableName, "last_at")
|
||||
|
||||
_resourceLong.fillFieldMap()
|
||||
|
||||
@@ -49,14 +48,13 @@ type resourceLong struct {
|
||||
ALL field.Asterisk
|
||||
ID field.Int32
|
||||
ResourceID field.Int32
|
||||
Type field.Int
|
||||
Live field.Int32
|
||||
Expire field.Time
|
||||
Type field.Int
|
||||
Quota field.Int32
|
||||
ExpireAt field.Time
|
||||
Used field.Int32
|
||||
DailyLimit field.Int32
|
||||
DailyUsed field.Int32
|
||||
DailyLast field.Time
|
||||
Daily field.Int32
|
||||
LastAt field.Time
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@@ -75,14 +73,13 @@ func (r *resourceLong) updateTableName(table string) *resourceLong {
|
||||
r.ALL = field.NewAsterisk(table)
|
||||
r.ID = field.NewInt32(table, "id")
|
||||
r.ResourceID = field.NewInt32(table, "resource_id")
|
||||
r.Type = field.NewInt(table, "type")
|
||||
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.ExpireAt = field.NewTime(table, "expire_at")
|
||||
r.Used = field.NewInt32(table, "used")
|
||||
r.DailyLimit = field.NewInt32(table, "daily_limit")
|
||||
r.DailyUsed = field.NewInt32(table, "daily_used")
|
||||
r.DailyLast = field.NewTime(table, "daily_last")
|
||||
r.Daily = field.NewInt32(table, "daily")
|
||||
r.LastAt = field.NewTime(table, "last_at")
|
||||
|
||||
r.fillFieldMap()
|
||||
|
||||
@@ -99,17 +96,16 @@ func (r *resourceLong) GetFieldByName(fieldName string) (field.OrderExpr, bool)
|
||||
}
|
||||
|
||||
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["resource_id"] = r.ResourceID
|
||||
r.fieldMap["type"] = r.Type
|
||||
r.fieldMap["live"] = r.Live
|
||||
r.fieldMap["expire"] = r.Expire
|
||||
r.fieldMap["type"] = r.Type
|
||||
r.fieldMap["quota"] = r.Quota
|
||||
r.fieldMap["expire_at"] = r.ExpireAt
|
||||
r.fieldMap["used"] = r.Used
|
||||
r.fieldMap["daily_limit"] = r.DailyLimit
|
||||
r.fieldMap["daily_used"] = r.DailyUsed
|
||||
r.fieldMap["daily_last"] = r.DailyLast
|
||||
r.fieldMap["daily"] = r.Daily
|
||||
r.fieldMap["last_at"] = r.LastAt
|
||||
}
|
||||
|
||||
func (r resourceLong) clone(db *gorm.DB) resourceLong {
|
||||
|
||||
@@ -29,14 +29,13 @@ func newResourceShort(db *gorm.DB, opts ...gen.DOOption) resourceShort {
|
||||
_resourceShort.ALL = field.NewAsterisk(tableName)
|
||||
_resourceShort.ID = field.NewInt32(tableName, "id")
|
||||
_resourceShort.ResourceID = field.NewInt32(tableName, "resource_id")
|
||||
_resourceShort.Type = field.NewInt(tableName, "type")
|
||||
_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.ExpireAt = field.NewTime(tableName, "expire_at")
|
||||
_resourceShort.Used = field.NewInt32(tableName, "used")
|
||||
_resourceShort.DailyLimit = field.NewInt32(tableName, "daily_limit")
|
||||
_resourceShort.DailyUsed = field.NewInt32(tableName, "daily_used")
|
||||
_resourceShort.DailyLast = field.NewTime(tableName, "daily_last")
|
||||
_resourceShort.Daily = field.NewInt32(tableName, "daily")
|
||||
_resourceShort.LastAt = field.NewTime(tableName, "last_at")
|
||||
|
||||
_resourceShort.fillFieldMap()
|
||||
|
||||
@@ -49,14 +48,13 @@ type resourceShort struct {
|
||||
ALL field.Asterisk
|
||||
ID field.Int32
|
||||
ResourceID field.Int32
|
||||
Type field.Int
|
||||
Live field.Int32
|
||||
Expire field.Time
|
||||
Type field.Int
|
||||
Quota field.Int32
|
||||
ExpireAt field.Time
|
||||
Used field.Int32
|
||||
DailyLimit field.Int32
|
||||
DailyUsed field.Int32
|
||||
DailyLast field.Time
|
||||
Daily field.Int32
|
||||
LastAt field.Time
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@@ -75,14 +73,13 @@ func (r *resourceShort) updateTableName(table string) *resourceShort {
|
||||
r.ALL = field.NewAsterisk(table)
|
||||
r.ID = field.NewInt32(table, "id")
|
||||
r.ResourceID = field.NewInt32(table, "resource_id")
|
||||
r.Type = field.NewInt(table, "type")
|
||||
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.ExpireAt = field.NewTime(table, "expire_at")
|
||||
r.Used = field.NewInt32(table, "used")
|
||||
r.DailyLimit = field.NewInt32(table, "daily_limit")
|
||||
r.DailyUsed = field.NewInt32(table, "daily_used")
|
||||
r.DailyLast = field.NewTime(table, "daily_last")
|
||||
r.Daily = field.NewInt32(table, "daily")
|
||||
r.LastAt = field.NewTime(table, "last_at")
|
||||
|
||||
r.fillFieldMap()
|
||||
|
||||
@@ -99,17 +96,16 @@ func (r *resourceShort) GetFieldByName(fieldName string) (field.OrderExpr, bool)
|
||||
}
|
||||
|
||||
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["resource_id"] = r.ResourceID
|
||||
r.fieldMap["type"] = r.Type
|
||||
r.fieldMap["live"] = r.Live
|
||||
r.fieldMap["expire"] = r.Expire
|
||||
r.fieldMap["type"] = r.Type
|
||||
r.fieldMap["quota"] = r.Quota
|
||||
r.fieldMap["expire_at"] = r.ExpireAt
|
||||
r.fieldMap["used"] = r.Used
|
||||
r.fieldMap["daily_limit"] = r.DailyLimit
|
||||
r.fieldMap["daily_used"] = r.DailyUsed
|
||||
r.fieldMap["daily_last"] = r.DailyLast
|
||||
r.fieldMap["daily"] = r.Daily
|
||||
r.fieldMap["last_at"] = r.LastAt
|
||||
}
|
||||
|
||||
func (r resourceShort) clone(db *gorm.DB) resourceShort {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"platform/pkg/env"
|
||||
auth2 "platform/web/auth"
|
||||
"platform/web/handlers"
|
||||
|
||||
@@ -36,10 +37,14 @@ func ApplyRouters(app *fiber.App) {
|
||||
resource.Post("/all", handlers.AllActiveResource)
|
||||
resource.Post("/list/short", handlers.ListResourceShort)
|
||||
resource.Post("/list/long", handlers.ListResourceLong)
|
||||
resource.Post("/statistics/free", handlers.StatisticResourceFree)
|
||||
resource.Post("/statistics/usage", handlers.StatisticResourceUsage)
|
||||
resource.Post("/create", handlers.CreateResource)
|
||||
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")
|
||||
@@ -73,11 +78,18 @@ func ApplyRouters(app *fiber.App) {
|
||||
edge.Post("/assign", handlers.AssignEdge)
|
||||
edge.Post("/all", handlers.AllEdgesAvailable)
|
||||
|
||||
// 临时
|
||||
debug := app.Group("/debug")
|
||||
debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
|
||||
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin)
|
||||
// 其他系统接口
|
||||
inquiry := api.Group("/inquiry")
|
||||
inquiry.Post("/create", handlers.CreateInquiry)
|
||||
|
||||
// 回调
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,42 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net/netip"
|
||||
"platform/pkg/u"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"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)
|
||||
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.
|
||||
Preload(field.Associations).
|
||||
Where(
|
||||
@@ -60,60 +79,48 @@ func findResource(resourceId int32) (*ResourceView, error) {
|
||||
if err != nil {
|
||||
return nil, ErrResourceNotExist
|
||||
}
|
||||
|
||||
if resource.User == nil {
|
||||
return nil, ErrResourceNotExist
|
||||
}
|
||||
var info = &ResourceView{
|
||||
Id: resource.ID,
|
||||
User: *resource.User,
|
||||
Active: resource.Active,
|
||||
Type: resource.Type,
|
||||
User: resource.User,
|
||||
}
|
||||
|
||||
switch resource.Type {
|
||||
case m.ResourceTypeShort:
|
||||
var sub = resource.Short
|
||||
var dailyLast = time.Time{}
|
||||
if sub.DailyLast != nil {
|
||||
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.ShortId = &sub.ID
|
||||
info.ExpireAt = sub.ExpireAt
|
||||
info.Live = time.Duration(sub.Live) * time.Second
|
||||
info.DailyLimit = sub.DailyLimit
|
||||
info.DailyUsed = sub.DailyUsed
|
||||
info.DailyLast = dailyLast
|
||||
info.Expire = expire
|
||||
info.Quota = quota
|
||||
info.Mode = sub.Type
|
||||
info.Quota = sub.Quota
|
||||
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:
|
||||
var sub = resource.Long
|
||||
var dailyLast = time.Time{}
|
||||
if sub.DailyLast != nil {
|
||||
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.LongId = &sub.ID
|
||||
info.ExpireAt = sub.ExpireAt
|
||||
info.Live = time.Duration(sub.Live) * time.Hour
|
||||
info.Mode = sub.Type
|
||||
info.Live = time.Duration(sub.Live) * time.Hour * 24
|
||||
info.DailyLimit = sub.DailyLimit
|
||||
info.DailyUsed = sub.DailyUsed
|
||||
info.DailyLast = dailyLast
|
||||
info.Expire = expire
|
||||
info.Quota = quota
|
||||
info.Quota = sub.Quota
|
||||
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
|
||||
@@ -121,48 +128,138 @@ func findResource(resourceId int32) (*ResourceView, error) {
|
||||
|
||||
// ResourceView 套餐数据的简化视图,便于直接获取主要数据
|
||||
type ResourceView struct {
|
||||
Id int32
|
||||
Active bool
|
||||
Type m.ResourceType
|
||||
Mode m.ResourceMode
|
||||
Live time.Duration
|
||||
DailyLimit int32
|
||||
DailyUsed int32
|
||||
DailyLast time.Time
|
||||
Quota int32
|
||||
Used int32
|
||||
Expire time.Time
|
||||
User m.User
|
||||
Id int32
|
||||
User m.User
|
||||
Active bool
|
||||
Type m.ResourceType
|
||||
ShortId *int32
|
||||
LongId *int32
|
||||
Live time.Duration
|
||||
Mode m.ResourceMode
|
||||
Quota int32
|
||||
ExpireAt *time.Time
|
||||
Used int32
|
||||
Daily int32
|
||||
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 (
|
||||
allChansKey = "channel:all"
|
||||
freeChansKey = "channel:free"
|
||||
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) {
|
||||
chans, err := g.Redis.Eval(
|
||||
func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) {
|
||||
pid := strconv.Itoa(int(proxy))
|
||||
chans, err := RedisScriptLockChans.Run(
|
||||
context.Background(),
|
||||
RedisScriptLockChans,
|
||||
g.Redis,
|
||||
[]string{
|
||||
freeChansKey,
|
||||
usedChansKey,
|
||||
usedChansKey + ":" + batch,
|
||||
freeChansKey + ":" + pid,
|
||||
usedChansKey + ":" + pid + ":" + batch,
|
||||
},
|
||||
count,
|
||||
expire.Unix(),
|
||||
).StringSlice()
|
||||
if err != nil {
|
||||
return nil, core.NewBizErr("获取通道失败", err)
|
||||
return nil, fmt.Errorf("获取通道失败: %w", err)
|
||||
}
|
||||
|
||||
addrs := make([]netip.AddrPort, len(chans))
|
||||
for i, ch := range chans {
|
||||
addr, err := netip.ParseAddrPort(ch)
|
||||
if err != nil {
|
||||
return nil, core.NewServErr("解析通道数据失败", err)
|
||||
return nil, fmt.Errorf("解析通道数据失败: %w", err)
|
||||
}
|
||||
addrs[i] = addr
|
||||
}
|
||||
@@ -170,41 +267,31 @@ func lockChans(batch string, count int, expire time.Time) ([]netip.AddrPort, err
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
var RedisScriptLockChans = `
|
||||
var RedisScriptLockChans = redis.NewScript(`
|
||||
local free_key = KEYS[1]
|
||||
local used_key = KEYS[2]
|
||||
local batch_key = KEYS[3]
|
||||
local batch_key = KEYS[2]
|
||||
local count = tonumber(ARGV[1])
|
||||
local expire = tonumber(ARGV[2])
|
||||
|
||||
if redis.call("SCARD", free_key) < count then
|
||||
return nil
|
||||
end
|
||||
|
||||
local ports = redis.call("SPOP", free_key, count)
|
||||
redis.call("ZADD", used_key, expire, batch_key)
|
||||
redis.call("RPUSH", batch_key, unpack(ports))
|
||||
|
||||
return ports
|
||||
`
|
||||
`)
|
||||
|
||||
// 归还通道
|
||||
func freeChans(batch string, chans []string) error {
|
||||
values := make([]any, len(chans))
|
||||
for i, ch := range chans {
|
||||
values[i] = ch
|
||||
}
|
||||
|
||||
err := g.Redis.Eval(
|
||||
func freeChans(proxy int32, batch string) error {
|
||||
pid := strconv.Itoa(int(proxy))
|
||||
err := RedisScriptFreeChans.Run(
|
||||
context.Background(),
|
||||
RedisScriptFreeChans,
|
||||
g.Redis,
|
||||
[]string{
|
||||
freeChansKey,
|
||||
usedChansKey,
|
||||
usedChansKey + ":" + batch,
|
||||
allChansKey,
|
||||
freeChansKey + ":" + pid,
|
||||
usedChansKey + ":" + pid + ":" + batch,
|
||||
},
|
||||
values...,
|
||||
).Err()
|
||||
if err != nil {
|
||||
return core.NewBizErr("释放通道失败", err)
|
||||
@@ -213,88 +300,19 @@ func freeChans(batch string, chans []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var RedisScriptFreeChans = `
|
||||
var RedisScriptFreeChans = redis.NewScript(`
|
||||
local free_key = KEYS[1]
|
||||
local used_key = KEYS[2]
|
||||
local batch_key = KEYS[3]
|
||||
local all_key = KEYS[4]
|
||||
local chans = ARGV
|
||||
local batch_key = KEYS[2]
|
||||
|
||||
local count = 0
|
||||
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)
|
||||
local chans = redis.call("LRANGE", batch_key, 0, -1)
|
||||
redis.call("DEL", batch_key)
|
||||
|
||||
return count
|
||||
`
|
||||
|
||||
// 扩容通道
|
||||
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))
|
||||
if redis.call("EXISTS", free_key) == 1 then
|
||||
redis.call("SADD", free_key, unpack(chans))
|
||||
end
|
||||
|
||||
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 (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -18,16 +17,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"gorm.io/gen"
|
||||
"gorm.io/gen/field"
|
||||
)
|
||||
|
||||
type channelBaiyinService 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 个")
|
||||
}
|
||||
type channelBaiyinProvider struct{}
|
||||
|
||||
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
|
||||
var filter *EdgeFilter = nil
|
||||
if len(edgeFilter) > 0 {
|
||||
filter = &edgeFilter[0]
|
||||
@@ -36,111 +32,180 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
|
||||
now := time.Now()
|
||||
batch := ID.GenReadable("bat")
|
||||
|
||||
// 获取用户套餐
|
||||
resource, err := findResource(resourceId)
|
||||
// 检查并获取套餐与白名单
|
||||
resource, whitelists, err := ensure(now, source, resourceId, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查用户
|
||||
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)
|
||||
|
||||
// 选择代理
|
||||
proxyResult := struct {
|
||||
m.Proxy
|
||||
Count int
|
||||
}{}
|
||||
err = q.Proxy.
|
||||
LeftJoin(q.Channel, q.Channel.ProxyID.EqCol(q.Proxy.ID), q.Channel.ExpiredAt.Gt(now)).
|
||||
Select(q.Proxy.ALL, field.NewUnsafeFieldRaw("10000 - count(*)").As("count")).
|
||||
Where(
|
||||
q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)),
|
||||
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
|
||||
).
|
||||
Group(q.Proxy.ID).
|
||||
Order(field.NewField("", "count")).
|
||||
Limit(1).Scan(&proxyResult)
|
||||
if err != nil {
|
||||
return nil, core.NewBizErr("获取可用代理失败", err)
|
||||
}
|
||||
if proxyResult.Count < count {
|
||||
return nil, core.NewBizErr("无可用主机,请稍后再试")
|
||||
}
|
||||
proxy := proxyResult.Proxy
|
||||
|
||||
// 获取可用通道
|
||||
chans, err := lockChans(batch, count, expire)
|
||||
chans, err := lockChans(proxy.ID, batch, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, core.NewBizErr("无可用通道,请稍后再试", err)
|
||||
}
|
||||
|
||||
// 获取对应代理
|
||||
ips := make([]driver.Valuer, 0)
|
||||
findProxy := make(map[orm.Inet]*m.Proxy)
|
||||
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.Status.Eq(int(m.ProxyStatusOnline)),
|
||||
q.Proxy.IP.In(ips...),
|
||||
).Find()
|
||||
// 获取可用节点
|
||||
edgesResp, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{
|
||||
Province: filter.Prov,
|
||||
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)
|
||||
return nil, core.NewBizErr("获取可用节点失败", err)
|
||||
}
|
||||
|
||||
groups := make(map[*m.Proxy][]*m.Channel)
|
||||
for _, proxy := range proxies {
|
||||
findProxy[proxy.IP] = proxy
|
||||
groups[proxy] = make([]*m.Channel, 0)
|
||||
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, len(chans))
|
||||
for i, ch := range chans {
|
||||
channels := make([]*m.Channel, count)
|
||||
chanConfigs := make([]*g.PortConfigsReq, count)
|
||||
edgeConfigs := make([]string, count)
|
||||
for i := range count {
|
||||
ch := chans[i]
|
||||
edge := edges[i]
|
||||
|
||||
if err != nil {
|
||||
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,
|
||||
ResourceID: resourceId,
|
||||
BatchNo: batch,
|
||||
@@ -150,86 +215,7 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
|
||||
City: filter.City,
|
||||
IP: orm.Inet{Addr: source},
|
||||
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 {
|
||||
return core.NewServErr("保存用户使用记录失败", err)
|
||||
}
|
||||
@@ -240,118 +226,100 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
|
||||
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), ":")
|
||||
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
|
||||
secret := strings.Split(u.Z(proxy.Secret), ":")
|
||||
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
|
||||
if env.DebugExternalChange {
|
||||
|
||||
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))
|
||||
}
|
||||
// 连接节点到网关
|
||||
err = g.Cloud.CloudConnect(&g.CloudConnectReq{
|
||||
Uuid: proxy.Mac,
|
||||
Edge: &edgeConfigs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, core.NewServErr("连接云平台失败", err)
|
||||
}
|
||||
|
||||
if env.DebugExternalChange {
|
||||
err := gateway.GatewayPortConfigs(configs)
|
||||
if err != nil {
|
||||
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
|
||||
}
|
||||
} else {
|
||||
bytes, _ := json.Marshal(configs)
|
||||
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "config", string(bytes))
|
||||
// 启用网关代理通道
|
||||
err = gateway.GatewayPortConfigs(chanConfigs)
|
||||
if err != nil {
|
||||
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
|
||||
}
|
||||
} else {
|
||||
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String())
|
||||
for _, item := range chanConfigs {
|
||||
str, _ := json.Marshal(item)
|
||||
fmt.Println(string(str))
|
||||
}
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (s *channelBaiyinService) RemoveChannels(batch string, ids []int32) error {
|
||||
func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
|
||||
start := time.Now()
|
||||
|
||||
// 获取连接数据
|
||||
channels, err := q.Channel.
|
||||
Preload(q.Channel.Proxy).
|
||||
Where(q.Channel.ID.In(ids...)).
|
||||
Find()
|
||||
channels, err := q.Channel.Where(q.Channel.BatchNo.Eq(batch)).Find()
|
||||
if err != nil {
|
||||
return core.NewServErr("获取通道数据失败", err)
|
||||
return core.NewServErr(fmt.Sprintf("获取通道数据失败,batch:%s", batch), err)
|
||||
}
|
||||
if len(channels) != len(ids) {
|
||||
return core.NewServErr("获取通道数据不完整", err)
|
||||
if len(channels) == 0 {
|
||||
slog.Warn(fmt.Sprintf("未找到通道数据,batch:%s", batch))
|
||||
return nil
|
||||
}
|
||||
|
||||
proxies := make(map[string]*m.Proxy, len(channels))
|
||||
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)
|
||||
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(channels[0].ProxyID)).Take()
|
||||
if err != nil {
|
||||
return core.NewServErr(fmt.Sprintf("获取代理数据失败,batch:%s", batch), err)
|
||||
}
|
||||
|
||||
addrs := make([]netip.AddrPort, len(channels))
|
||||
// 准备配置数据
|
||||
edgeConfigs := make([]string, len(channels))
|
||||
configs := make([]*g.PortConfigsReq, len(channels))
|
||||
for i, channel := range channels {
|
||||
addrs[i] = netip.AddrPortFrom(channel.Proxy.IP.Addr, channel.Port)
|
||||
if channel.EdgeRef != nil {
|
||||
edgeConfigs[i] = *channel.EdgeRef
|
||||
} else {
|
||||
slog.Warn(fmt.Sprintf("通道 %d 没有保存节点引用", channel.ID))
|
||||
}
|
||||
|
||||
configs[i] = &g.PortConfigsReq{
|
||||
Status: false,
|
||||
Port: int(channel.Port),
|
||||
Edge: &[]string{},
|
||||
}
|
||||
}
|
||||
|
||||
// 提交配置
|
||||
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)
|
||||
if err != nil {
|
||||
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
||||
}
|
||||
} else {
|
||||
slog.Debug("清除代理端口配置", "proxy", proxy.IP)
|
||||
for _, item := range configs {
|
||||
str, _ := json.Marshal(item)
|
||||
fmt.Println(string(str))
|
||||
}
|
||||
}
|
||||
|
||||
// 释放端口
|
||||
err = freeChans(batch, chans)
|
||||
err = freeChans(proxy.ID, batch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清空配置
|
||||
for ip, channels := range groups {
|
||||
proxy := proxies[ip]
|
||||
secret := strings.Split(*proxy.Secret, ":")
|
||||
gateway := g.NewGateway(ip, secret[0], secret[1])
|
||||
|
||||
configs := make([]g.PortConfigsReq, len(channels))
|
||||
for i, channel := range channels {
|
||||
configs[i] = g.PortConfigsReq{
|
||||
Status: false,
|
||||
Port: int(channel.Port),
|
||||
Edge: &[]string{},
|
||||
}
|
||||
}
|
||||
|
||||
if env.DebugExternalChange {
|
||||
err := gateway.GatewayPortConfigs(configs)
|
||||
if err != nil {
|
||||
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
||||
}
|
||||
} else {
|
||||
bytes, _ := json.Marshal(configs)
|
||||
slog.Debug("清除代理端口配置", "proxy", ip, "config", string(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
slog.Debug("清除代理端口配置", "time", time.Since(start).String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -44,48 +44,28 @@ var (
|
||||
)
|
||||
|
||||
func (s *IdService) GenSerial() (string, error) {
|
||||
var ctx = context.Background()
|
||||
|
||||
// 构造Redis键
|
||||
now := time.Now().Unix()
|
||||
key := idSerialKey(now)
|
||||
|
||||
// 使用Redis事务确保原子操作
|
||||
var sequence int64
|
||||
err := g.Redis.Watch(ctx, func(tx *redis.Tx) error {
|
||||
// 脚本实现原子操作
|
||||
script := redis.NewScript(`
|
||||
local current = tonumber(redis.call('GET', KEYS[1])) or 0
|
||||
if current >= tonumber(ARGV[1]) then
|
||||
return redis.error_reply('sequence overflow')
|
||||
end
|
||||
|
||||
// 获取当前序列号
|
||||
currentVal, err := tx.Get(ctx, key).Int64()
|
||||
if err != nil && !errors.Is(err, redis.Nil) {
|
||||
return err
|
||||
}
|
||||
local sequence = current + 1
|
||||
redis.call('SET', KEYS[1], sequence, 'EX', ARGV[2])
|
||||
|
||||
if errors.Is(err, redis.Nil) {
|
||||
currentVal = 0
|
||||
}
|
||||
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)
|
||||
return sequence
|
||||
`)
|
||||
sequence, err := script.Run(context.Background(), g.Redis, []string{idSerialKey(now)}, maxSequence, redisTTL).Int64()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 组装最终ID
|
||||
id := uint64((now << timestampShift) | sequence)
|
||||
|
||||
idStr := strconv.FormatUint(id, 10)
|
||||
|
||||
return idStr, nil
|
||||
return strconv.FormatUint(id, 10), nil
|
||||
}
|
||||
|
||||
// ParseSerial 解析ID,返回其组成部分
|
||||
@@ -98,7 +78,7 @@ func (s *IdService) ParseSerial(id uint64) (timestamp int64, sequence int64) {
|
||||
|
||||
// idSerialKey 根据时间戳生成Redis键
|
||||
func idSerialKey(timestamp int64) string {
|
||||
return fmt.Sprintf("global:id:serial:%d", timestamp)
|
||||
return fmt.Sprintf("id:serial:%d", timestamp)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -33,28 +33,29 @@ func (s *proxyService) AllProxies(proxyType m.ProxyType, channels bool) ([]*m.Pr
|
||||
// RegisterBaiyin 注册新代理服务
|
||||
func (s *proxyService) RegisterBaiyin(Mac string, IP netip.Addr, username, password string) error {
|
||||
|
||||
// 添加可用通道到 redis
|
||||
chans := make([]netip.AddrPort, 10000)
|
||||
for i := range 10000 {
|
||||
chans[i] = netip.AddrPortFrom(IP, uint16(i+10000))
|
||||
}
|
||||
err := addChans(chans)
|
||||
if err != nil {
|
||||
return core.NewServErr("添加通道失败")
|
||||
}
|
||||
|
||||
// 保存代理信息
|
||||
if err := q.Proxy.Create(&m.Proxy{
|
||||
proxy := &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 {
|
||||
}
|
||||
if err := q.Proxy.Create(proxy); err != nil {
|
||||
return core.NewServErr("保存通道数据失败")
|
||||
}
|
||||
|
||||
// 添加可用通道到 redis
|
||||
chans := make([]netip.AddrPort, 10000)
|
||||
for i := range 10000 {
|
||||
chans[i] = netip.AddrPortFrom(IP, uint16(i+10000))
|
||||
}
|
||||
err := regChans(proxy.ID, chans)
|
||||
if err != nil {
|
||||
return core.NewServErr("添加通道失败", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"platform/pkg/u"
|
||||
"platform/web/core"
|
||||
g "platform/web/globals"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
"time"
|
||||
@@ -18,48 +18,66 @@ var Resource = &resourceService{}
|
||||
type resourceService struct{}
|
||||
|
||||
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.
|
||||
Where(q.User.ID.Eq(uid)).
|
||||
Take()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查余额
|
||||
var amount = user.Balance.Sub(data.GetAmount())
|
||||
if amount.IsNegative() {
|
||||
return ErrBalanceNotEnough
|
||||
}
|
||||
// 找到用户
|
||||
user, err := q.User.
|
||||
Where(q.User.ID.Eq(uid)).
|
||||
Take()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新用户余额
|
||||
_, err = q.User.
|
||||
Where(q.User.ID.Eq(uid), q.User.Balance.Eq(user.Balance)).
|
||||
UpdateSimple(q.User.Balance.Value(amount))
|
||||
if err != nil {
|
||||
return core.NewServErr("更新用户余额失败", err)
|
||||
}
|
||||
// 检查余额
|
||||
amount, err := data.GetAmount()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newBalance := user.Balance.Sub(amount)
|
||||
if newBalance.IsNegative() {
|
||||
return ErrBalanceNotEnough
|
||||
}
|
||||
|
||||
// 保存套餐
|
||||
resource, err := createResource(q, uid, now, data)
|
||||
if err != nil {
|
||||
return core.NewServErr("创建套餐失败", err)
|
||||
}
|
||||
return q.Q.Transaction(func(q *q.Query) error {
|
||||
|
||||
// 生成账单
|
||||
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource))
|
||||
if err != nil {
|
||||
return core.NewServErr("生成账单失败", err)
|
||||
}
|
||||
// 更新用户余额
|
||||
_, err = q.User.
|
||||
Where(
|
||||
q.User.ID.Eq(uid),
|
||||
q.User.Balance.Eq(user.Balance),
|
||||
).
|
||||
UpdateSimple(q.User.Balance.Value(newBalance))
|
||||
if err != nil {
|
||||
return core.NewServErr("更新用户余额失败", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
// 保存套餐
|
||||
resource, err := createResource(q, uid, now, data)
|
||||
if err != nil {
|
||||
return core.NewServErr("创建套餐失败", err)
|
||||
}
|
||||
|
||||
// 生成账单
|
||||
subject, err := data.GetSubject()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource))
|
||||
if err != nil {
|
||||
return core.NewServErr("生成账单失败", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
// 保存套餐
|
||||
@@ -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 {
|
||||
return core.NewServErr("生成账单失败", err)
|
||||
}
|
||||
@@ -95,13 +121,17 @@ func createResource(q *q.Query, uid int32, now time.Time, data *CreateResourceDa
|
||||
if short == nil {
|
||||
return nil, core.NewBizErr("短效套餐数据不能为空")
|
||||
}
|
||||
var duration = time.Duration(short.Expire) * 24 * time.Hour
|
||||
resource.Short = &m.ResourceShort{
|
||||
Type: short.Mode,
|
||||
Live: short.Live,
|
||||
Quota: &short.Quota,
|
||||
Expire: u.P(now.Add(duration)),
|
||||
DailyLimit: short.DailyLimit,
|
||||
Live: short.Live,
|
||||
Type: short.Mode,
|
||||
Quota: short.Quota,
|
||||
}
|
||||
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 {
|
||||
return nil, core.NewBizErr("长效套餐数据不能为空")
|
||||
}
|
||||
var duration = time.Duration(long.Expire) * 24 * time.Hour
|
||||
resource.Long = &m.ResourceLong{
|
||||
Type: long.Mode,
|
||||
Live: long.Live,
|
||||
Quota: &long.Quota,
|
||||
Expire: u.P(now.Add(duration)),
|
||||
DailyLimit: long.DailyLimit,
|
||||
Live: long.Live,
|
||||
Type: long.Mode,
|
||||
Quota: long.Quota,
|
||||
}
|
||||
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:
|
||||
return nil, core.NewBizErr("不支持的套餐类型")
|
||||
@@ -137,22 +171,20 @@ type CreateResourceData struct {
|
||||
}
|
||||
|
||||
type CreateShortResourceData struct {
|
||||
Live int32 `json:"live" validate:"required,min=180"`
|
||||
Mode m.ResourceMode `json:"mode" validate:"required"`
|
||||
Expire int32 `json:"expire"`
|
||||
DailyLimit int32 `json:"daily_limit" validate:"min=2000"`
|
||||
Quota int32 `json:"quota" validate:"min=10000"`
|
||||
Live int32 `json:"live" validate:"required,min=180"`
|
||||
Mode m.ResourceMode `json:"mode" validate:"required"`
|
||||
Quota int32 `json:"quota" validate:"required"`
|
||||
Expire *int32 `json:"expire"`
|
||||
|
||||
name string
|
||||
price *decimal.Decimal
|
||||
}
|
||||
|
||||
type CreateLongResourceData struct {
|
||||
Live int32 `json:"live" validate:"required,oneof=1 4 8 12 24"`
|
||||
Mode m.ResourceMode `json:"mode" validate:"required,oneof=1 2"`
|
||||
Expire int32 `json:"expire"`
|
||||
DailyLimit int32 `json:"daily_limit" validate:"min=100"`
|
||||
Quota int32 `json:"quota" validate:"min=500"`
|
||||
Live int32 `json:"live" validate:"required"`
|
||||
Mode m.ResourceMode `json:"mode" validate:"required"`
|
||||
Quota int32 `json:"quota" validate:"required"`
|
||||
Expire *int32 `json:"expire"`
|
||||
|
||||
name string
|
||||
price *decimal.Decimal
|
||||
@@ -162,24 +194,28 @@ func (c *CreateResourceData) GetType() m.TradeType {
|
||||
return m.TradeTypePurchase
|
||||
}
|
||||
|
||||
func (c *CreateResourceData) GetSubject() string {
|
||||
switch c.Type {
|
||||
case m.ResourceTypeShort:
|
||||
func (c *CreateResourceData) GetSubject() (string, error) {
|
||||
switch {
|
||||
default:
|
||||
return "", errors.New("无效的套餐类型")
|
||||
|
||||
case c.Type == m.ResourceTypeShort && c.Short != nil:
|
||||
return c.Short.GetSubject()
|
||||
case m.ResourceTypeLong:
|
||||
case c.Type == m.ResourceTypeLong && c.Long != nil:
|
||||
return c.Long.GetSubject()
|
||||
}
|
||||
panic("类型对应的数据为空")
|
||||
}
|
||||
|
||||
func (c *CreateResourceData) GetAmount() decimal.Decimal {
|
||||
switch c.Type {
|
||||
case m.ResourceTypeShort:
|
||||
func (c *CreateResourceData) GetAmount() (decimal.Decimal, error) {
|
||||
switch {
|
||||
default:
|
||||
return decimal.Zero, errors.New("无效的套餐类型")
|
||||
|
||||
case c.Type == m.ResourceTypeShort && c.Short != nil:
|
||||
return c.Short.GetAmount()
|
||||
case m.ResourceTypeLong:
|
||||
case c.Type == m.ResourceTypeLong && c.Long != nil:
|
||||
return c.Long.GetAmount()
|
||||
}
|
||||
panic("类型对应的数据为空")
|
||||
}
|
||||
|
||||
func (c *CreateResourceData) Serialize() (string, error) {
|
||||
@@ -191,27 +227,37 @@ func (c *CreateResourceData) Deserialize(str string) error {
|
||||
return json.Unmarshal([]byte(str), c)
|
||||
}
|
||||
|
||||
func (data *CreateShortResourceData) GetSubject() string {
|
||||
func (data *CreateShortResourceData) GetSubject() (string, error) {
|
||||
if data.name == "" {
|
||||
var mode string
|
||||
switch data.Mode {
|
||||
case 1:
|
||||
default:
|
||||
return "", errors.New("无效的套餐模式")
|
||||
|
||||
case m.ResourceModeTime:
|
||||
mode = "包时"
|
||||
case 2:
|
||||
case m.ResourceModeQuota:
|
||||
mode = "包量"
|
||||
}
|
||||
|
||||
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 {
|
||||
var factor int32
|
||||
switch data.Mode {
|
||||
case 1:
|
||||
factor = data.DailyLimit * data.Expire
|
||||
case 2:
|
||||
default:
|
||||
return decimal.Zero, errors.New("无效的套餐模式")
|
||||
|
||||
case m.ResourceModeTime:
|
||||
if data.Expire == nil {
|
||||
return decimal.Zero, errors.New("包时套餐过期时间不能为空")
|
||||
}
|
||||
factor = data.Quota * *data.Expire
|
||||
case m.ResourceModeQuota:
|
||||
factor = data.Quota
|
||||
}
|
||||
|
||||
@@ -223,38 +269,53 @@ func (data *CreateShortResourceData) GetAmount() decimal.Decimal {
|
||||
var dec = decimal.Decimal{}.
|
||||
Add(decimal.NewFromInt32(base * factor)).
|
||||
Div(decimal.NewFromInt(30000))
|
||||
if dec.IsZero() {
|
||||
return decimal.Zero, errors.New("计算金额错误")
|
||||
}
|
||||
|
||||
data.price = &dec
|
||||
}
|
||||
return *data.price
|
||||
return *data.price, nil
|
||||
}
|
||||
|
||||
func (data *CreateLongResourceData) GetSubject() string {
|
||||
func (data *CreateLongResourceData) GetSubject() (string, error) {
|
||||
if data.name == "" {
|
||||
var mode string
|
||||
switch data.Mode {
|
||||
case 1:
|
||||
default:
|
||||
return "", errors.New("无效的套餐模式")
|
||||
|
||||
case m.ResourceModeTime:
|
||||
mode = "包时"
|
||||
case 2:
|
||||
case m.ResourceModeQuota:
|
||||
mode = "包量"
|
||||
}
|
||||
|
||||
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 {
|
||||
var factor int32 = 0
|
||||
switch data.Mode {
|
||||
default:
|
||||
return decimal.Zero, errors.New("无效的套餐模式")
|
||||
|
||||
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:
|
||||
factor = data.Quota
|
||||
}
|
||||
|
||||
var base int32
|
||||
switch data.Live {
|
||||
default:
|
||||
return decimal.Zero, errors.New("无效的套餐时长")
|
||||
case 1:
|
||||
base = 30
|
||||
case 4:
|
||||
@@ -271,11 +332,16 @@ func (data *CreateLongResourceData) GetAmount() decimal.Decimal {
|
||||
var dec = decimal.Decimal{}.
|
||||
Add(decimal.NewFromInt32(base * factor)).
|
||||
Div(decimal.NewFromInt(100))
|
||||
if dec.IsZero() {
|
||||
return decimal.Zero, errors.New("计算金额错误")
|
||||
}
|
||||
|
||||
data.price = &dec
|
||||
}
|
||||
return *data.price
|
||||
return *data.price, nil
|
||||
}
|
||||
|
||||
// 交易后创建套餐
|
||||
type ResourceOnTradeComplete struct{}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 服务错误
|
||||
type ResourceServiceErr string
|
||||
|
||||
func (e ResourceServiceErr) Error() string {
|
||||
|
||||
@@ -35,12 +35,18 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
|
||||
platform := data.Platform
|
||||
method := data.Method
|
||||
tType := data.Product.GetType()
|
||||
subject := data.Product.GetSubject()
|
||||
amount := data.Product.GetAmount()
|
||||
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 {
|
||||
amountReal = decimal.NewFromFloat(0.01)
|
||||
}
|
||||
@@ -60,7 +66,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var expireAt = time.Time(u.Z(coupon.ExpireAt))
|
||||
expireAt := time.Time(u.Z(coupon.ExpireAt))
|
||||
if !expireAt.IsZero() && expireAt.Before(now) {
|
||||
_, err = q.Coupon.
|
||||
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 {
|
||||
return nil, core.NewServErr("生成订单号失败", err)
|
||||
}
|
||||
@@ -692,8 +698,8 @@ type OnTradeCompletedData struct {
|
||||
|
||||
type ProductInfo interface {
|
||||
GetType() m.TradeType
|
||||
GetSubject() string
|
||||
GetAmount() decimal.Decimal
|
||||
GetSubject() (string, error)
|
||||
GetAmount() (decimal.Decimal, error)
|
||||
Serialize() (string, error)
|
||||
Deserialize(str string) error
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
return core.NewServErr("生成账单失败", err)
|
||||
}
|
||||
@@ -39,23 +47,25 @@ func (s *userService) UpdateBalanceByTrade(uid int32, info *RechargeProductInfo,
|
||||
|
||||
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.
|
||||
Where(q.User.ID.Eq(uid)).Take()
|
||||
if err != nil {
|
||||
return core.NewServErr("查询用户失败", err)
|
||||
}
|
||||
|
||||
var amount = user.Balance.Add(info.GetAmount())
|
||||
if amount.IsNegative() {
|
||||
amount, err := info.GetAmount()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
balance := user.Balance.Add(amount)
|
||||
if balance.IsNegative() {
|
||||
return core.NewServErr("用户余额不足")
|
||||
}
|
||||
|
||||
_, err = q.User.
|
||||
Where(q.User.ID.Eq(user.ID)).
|
||||
UpdateSimple(q.User.Balance.Value(amount))
|
||||
UpdateSimple(q.User.Balance.Value(balance))
|
||||
if err != nil {
|
||||
return core.NewServErr("更新用户余额失败", err)
|
||||
}
|
||||
@@ -75,12 +85,13 @@ func (r *RechargeProductInfo) GetType() m.TradeType {
|
||||
return m.TradeTypeRecharge
|
||||
}
|
||||
|
||||
func (r *RechargeProductInfo) GetSubject() string {
|
||||
return fmt.Sprintf("账户充值 - %s元", r.GetAmount().StringFixed(2))
|
||||
func (r *RechargeProductInfo) GetSubject() (string, error) {
|
||||
amount, _ := r.GetAmount()
|
||||
return fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)), nil
|
||||
}
|
||||
|
||||
func (r *RechargeProductInfo) GetAmount() decimal.Decimal {
|
||||
return decimal.NewFromInt(int64(r.Amount)).Div(decimal.NewFromInt(100))
|
||||
func (r *RechargeProductInfo) GetAmount() (decimal.Decimal, error) {
|
||||
return decimal.NewFromInt(int64(r.Amount)).Div(decimal.NewFromInt(100)), nil
|
||||
}
|
||||
|
||||
func (r *RechargeProductInfo) Serialize() (string, error) {
|
||||
|
||||
@@ -5,19 +5,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"platform/pkg/env"
|
||||
"platform/pkg/u"
|
||||
"platform/web/events"
|
||||
e "platform/web/events"
|
||||
g "platform/web/globals"
|
||||
m "platform/web/models"
|
||||
q "platform/web/queries"
|
||||
s "platform/web/services"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
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) {
|
||||
data := new(e.RemoveChannelData)
|
||||
err = json.Unmarshal(task.Payload(), data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析任务参数失败: %w", err)
|
||||
}
|
||||
|
||||
err = s.Channel.RemoveChannels(data.Batch, data.IDs)
|
||||
batch := string(task.Payload())
|
||||
err = s.Channel.RemoveChannels(batch)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
36
web/web.go
36
web/web.go
@@ -6,7 +6,7 @@ import (
|
||||
"log/slog"
|
||||
_ "net/http/pprof"
|
||||
"platform/web/events"
|
||||
base "platform/web/globals"
|
||||
deps "platform/web/globals"
|
||||
"platform/web/tasks"
|
||||
"time"
|
||||
|
||||
@@ -19,12 +19,12 @@ func RunApp(pCtx context.Context) error {
|
||||
g, ctx := errgroup.WithContext(pCtx)
|
||||
|
||||
// 初始化依赖
|
||||
err := base.Init(ctx)
|
||||
err := deps.Init(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化依赖失败: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err := base.Close()
|
||||
err := deps.Close()
|
||||
if err != nil {
|
||||
slog.Error("关闭依赖失败", "error", err)
|
||||
}
|
||||
@@ -39,10 +39,6 @@ func RunApp(pCtx context.Context) error {
|
||||
return RunTask(ctx)
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
return RunSchedule(ctx)
|
||||
})
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
@@ -76,30 +72,9 @@ func RunWeb(ctx context.Context) error {
|
||||
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 {
|
||||
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) {
|
||||
slog.Error("任务执行失败", "task", task.Type(), "error", err)
|
||||
}),
|
||||
@@ -108,7 +83,6 @@ func RunTask(ctx context.Context) error {
|
||||
var mux = asynq.NewServeMux()
|
||||
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
|
||||
mux.HandleFunc(events.CompleteTrade, tasks.HandleCompleteTrade)
|
||||
mux.HandleFunc(events.FlushGateway, tasks.HandleFlushGateway)
|
||||
|
||||
// 停止服务
|
||||
go func() {
|
||||
|
||||
Reference in New Issue
Block a user