Compare commits
21 Commits
3f24fba1ae
...
v1.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fe415de63 | |||
| 8e42fad8aa | |||
| 7a3c47f1d4 | |||
| dfbb3a9acc | |||
| 19fa8b381c | |||
| b7a9682552 | |||
| f638baec64 | |||
| 1262a8dae4 | |||
| bf8f001a30 | |||
| eac793becb | |||
| 7bdbb7ddff | |||
| c8fd4cf9ca | |||
| 2b190bd4e5 | |||
| 8f2e71849f | |||
| 0207720943 | |||
| 05fba68b3e | |||
| c8c86081d9 | |||
| 983dbb4564 | |||
| 9e237be21e | |||
| 5649a03c47 | |||
| 4a2dcabf58 |
@@ -2,7 +2,7 @@ name: Docker
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: ["v*"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
@@ -10,14 +10,12 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
16
.vscode/launch.json
vendored
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
28
Dockerfile
28
Dockerfile
@@ -1,35 +1,31 @@
|
|||||||
# 第一阶段:构建
|
# 第一阶段:构建
|
||||||
FROM golang:1.24.0 AS builder
|
FROM golang:1.25.3 AS builder
|
||||||
|
|
||||||
ENV GOPROXY=https://goproxy.cn,direct
|
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# 复制Go模块文件
|
ENV GOPROXY=https://goproxy.cn,direct
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
ENV GOOS=linux
|
||||||
|
ENV GOARCH=amd64
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# 复制源代码
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN go build -ldflags '-w -s' -o bin/platform_linux_amd64 cmd/main/main.go
|
||||||
# 编译
|
|
||||||
RUN GOOS=linux GOARCH=amd64 go build -ldflags '-w -s' -o bin/platform_linux_amd64 cmd/main/main.go
|
|
||||||
|
|
||||||
# 第二阶段:运行环境
|
# 第二阶段:运行环境
|
||||||
FROM ubuntu:24.04 AS runner
|
FROM alpine:3.23 AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y ca-certificates
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
# 从构建阶段复制编译好的二进制文件
|
|
||||||
COPY --from=builder /build/bin/platform_linux_amd64 /app/platform
|
COPY --from=builder /build/bin/platform_linux_amd64 /app/platform
|
||||||
|
|
||||||
# 设置可执行权限
|
|
||||||
RUN chmod +x /app/platform
|
RUN chmod +x /app/platform
|
||||||
|
|
||||||
# 声明暴露端口
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# 启动平台服务
|
|
||||||
CMD ["/app/platform"]
|
CMD ["/app/platform"]
|
||||||
64
README.md
64
README.md
@@ -1,26 +1,20 @@
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
trade/create 性能问题,缩短事务时间,考虑其他方式实现可靠分布式事务
|
前端需要 token 化改造,以避免每次 basic 认证流程中 bcrypt 对比导致的性能对比
|
||||||
|
|
||||||
需要确认以下 ID.GenSerial 的分布式并发安全性
|
优化中间件,配置通用限速
|
||||||
|
|
||||||
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
|
observe 部署,蓝狐部署
|
||||||
|
|
||||||
端口资源池的 gc 实现
|
---
|
||||||
|
|
||||||
标准化生产环境 cors 配置
|
用反射实现环境变量解析,以简化函数签名
|
||||||
|
|
||||||
底层调用集成 otel
|
|
||||||
- redis
|
|
||||||
- gorm
|
|
||||||
- 三方接口
|
|
||||||
|
|
||||||
分离 task 的客户端,支持多进程(prefork 必要!)
|
分离 task 的客户端,支持多进程(prefork 必要!)
|
||||||
|
|
||||||
调整目录结构:
|
调整目录结构:
|
||||||
|
|
||||||
```
|
```
|
||||||
- /core 核心概念
|
|
||||||
- /util 工具函数
|
- /util 工具函数
|
||||||
|
|
||||||
- /models 模型
|
- /models 模型
|
||||||
@@ -30,7 +24,7 @@ jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
|
|||||||
- /services 服务层
|
- /services 服务层
|
||||||
- /auth 认证相关,特化服务
|
- /auth 认证相关,特化服务
|
||||||
|
|
||||||
- /app 应用相关,初始化日志,环境变量等
|
- /app 应用相关,初始化日志,环境变量,错误类型等
|
||||||
- /http 协议层,http 服务
|
- /http 协议层,http 服务
|
||||||
- /cmd 主函数
|
- /cmd 主函数
|
||||||
|
|
||||||
@@ -39,12 +33,20 @@ cmd 调用 app, http 的初始化函数
|
|||||||
http 调用 clients 的初始化函数
|
http 调用 clients 的初始化函数
|
||||||
```
|
```
|
||||||
|
|
||||||
考虑一个方案限制接口请求速率,无侵入更好
|
---
|
||||||
|
|
||||||
|
数据库转模型文件
|
||||||
|
|
||||||
|
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
|
||||||
|
|
||||||
|
慢速请求底层调用埋点监控
|
||||||
|
|
||||||
|
- redis
|
||||||
|
- gorm
|
||||||
|
- 三方接口
|
||||||
|
|
||||||
冷数据迁移方案
|
冷数据迁移方案
|
||||||
|
|
||||||
proxy 网关更新接口可以传输更结构化的数据,直接区分不同类型以加快更新速度
|
|
||||||
|
|
||||||
## 业务逻辑
|
## 业务逻辑
|
||||||
|
|
||||||
### 订单关闭的几种方式
|
### 订单关闭的几种方式
|
||||||
@@ -63,22 +65,20 @@ proxy 网关更新接口可以传输更结构化的数据,直接区分不同
|
|||||||
|
|
||||||
### 节点分配与存储逻辑
|
### 节点分配与存储逻辑
|
||||||
|
|
||||||
添加:
|
提取:
|
||||||
- 检查用户 ip 是否在白名单内
|
|
||||||
- 取用端口,不够则返回失败
|
|
||||||
- 将分配结果转写成配置发送到网关
|
|
||||||
- 保存通道信息和分配记录,其中通道信息以网关为主体,分配记录以用户为主体
|
|
||||||
- 添加异步任务,当时间结束后释放取用的端口并清空网关配置
|
|
||||||
|
|
||||||
删除:
|
- 检查用户套餐与白名单
|
||||||
- 如果传入用户信息,检查要删除的连接是否属于该用户
|
- 选中代理
|
||||||
- 释放可用端口
|
- 找到当前可用端口最多的代理
|
||||||
- redis 脚本中检查,如果端口所属节点已下线则直接忽略
|
- 不考虑分割端口,不够加机器
|
||||||
- 提交清空配置到网关
|
- 获取可用端口
|
||||||
|
- 获取可用节点
|
||||||
|
- 生成批次号,提交到期释放任务
|
||||||
|
- 绑定节点与端口,保存到数据库
|
||||||
|
- 分别提交连接与配置请求
|
||||||
|
|
||||||
缩扩容:
|
释放:
|
||||||
- 通过调度任务实现缩扩容
|
|
||||||
- 每分钟检查一次全部配置,按代理分组
|
- 根据批次查出所有端口与相关节点
|
||||||
- 获取所有代理后备配置
|
- 分别提交断开与关闭请求
|
||||||
- 后备配置/当前配置
|
- 释放端口
|
||||||
- 当比例 < 1.5 或 > 3 时,重新更新为 2 倍
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ func main() {
|
|||||||
m.User{},
|
m.User{},
|
||||||
m.UserRole{},
|
m.UserRole{},
|
||||||
m.Whitelist{},
|
m.Whitelist{},
|
||||||
|
m.Inquiry{},
|
||||||
)
|
)
|
||||||
g.Execute()
|
g.Execute()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
name: lanhu
|
name: lanhu-platform
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:17
|
image: postgres:17.7
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
|||||||
10
pkg/env/env.go
vendored
10
pkg/env/env.go
vendored
@@ -36,7 +36,10 @@ var (
|
|||||||
RedisPort = "6379"
|
RedisPort = "6379"
|
||||||
RedisPassword = ""
|
RedisPassword = ""
|
||||||
|
|
||||||
BaiyinAddr = "http://103.139.212.110:9989"
|
OtelHost string
|
||||||
|
OtelPort string
|
||||||
|
|
||||||
|
BaiyinCloudUrl string
|
||||||
BaiyinTokenUrl string
|
BaiyinTokenUrl string
|
||||||
|
|
||||||
IdenCallbackUrl string
|
IdenCallbackUrl string
|
||||||
@@ -115,7 +118,10 @@ func Init() {
|
|||||||
errs = append(errs, parse(&RedisPort, "REDIS_PORT", true, nil))
|
errs = append(errs, parse(&RedisPort, "REDIS_PORT", true, nil))
|
||||||
errs = append(errs, parse(&RedisPassword, "REDIS_PASS", true, nil))
|
errs = append(errs, parse(&RedisPassword, "REDIS_PASS", true, nil))
|
||||||
|
|
||||||
errs = append(errs, parse(&BaiyinAddr, "BAIYIN_ADDR", true, nil))
|
errs = append(errs, parse(&OtelHost, "OTEL_HOST", true, nil))
|
||||||
|
errs = append(errs, parse(&OtelPort, "OTEL_PORT", true, nil))
|
||||||
|
|
||||||
|
errs = append(errs, parse(&BaiyinCloudUrl, "BAIYIN_CLOUD_URL", false, nil))
|
||||||
errs = append(errs, parse(&BaiyinTokenUrl, "BAIYIN_TOKEN_URL", false, nil))
|
errs = append(errs, parse(&BaiyinTokenUrl, "BAIYIN_TOKEN_URL", false, nil))
|
||||||
|
|
||||||
errs = append(errs, parse(&IdenCallbackUrl, "IDEN_CALLBACK_URL", false, nil))
|
errs = append(errs, parse(&IdenCallbackUrl, "IDEN_CALLBACK_URL", false, nil))
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ func ElseTo[A any, B any](a *A, f func(A) B) *B {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 三元表达式
|
||||||
|
func Ternary[T any](condition bool, trueValue T, falseValue T) T {
|
||||||
|
if condition {
|
||||||
|
return trueValue
|
||||||
|
} else {
|
||||||
|
return falseValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ====================
|
// ====================
|
||||||
// 指针
|
// 指针
|
||||||
// ====================
|
// ====================
|
||||||
|
|||||||
16
publish.ps1
Normal file
16
publish.ps1
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
if (-not $args) {
|
||||||
|
Write-Error "需要指定版本号"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$confrim = Read-Host "构建版本为 [platform:$($args[0])],是否继续?(y/n)"
|
||||||
|
if ($confrim -ne "y") {
|
||||||
|
Write-Host "已取消构建"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
docker build -t 43.226.58.254:53000/lanhu/platform:latest .
|
||||||
|
docker build -t 43.226.58.254:53000/lanhu/platform:$($args[0]) .
|
||||||
|
|
||||||
|
docker push 43.226.58.254:53000/lanhu/platform:latest
|
||||||
|
docker push 43.226.58.254:53000/lanhu/platform:$($args[0])
|
||||||
12
scripts/sql/fill.sql
Normal file
12
scripts/sql/fill.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-- ====================
|
||||||
|
-- region 填充数据
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
insert into client (
|
||||||
|
client_id, client_secret, redirect_uri, spec, name, type
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
'web', '$2a$10$Ss12mXQgpYyo1CKIZ3URouDm.Lc2KcYJzsvEK2PTIXlv6fHQht45a', '', 3, 'web', 1
|
||||||
|
);
|
||||||
|
|
||||||
|
-- endregion
|
||||||
@@ -108,6 +108,76 @@ comment on column logs_user_bandwidth.time is '记录时间';
|
|||||||
|
|
||||||
-- endregion
|
-- endregion
|
||||||
|
|
||||||
|
-- ====================
|
||||||
|
-- region 系统信息
|
||||||
|
-- ====================
|
||||||
|
|
||||||
|
-- announcement
|
||||||
|
drop table if exists announcement cascade;
|
||||||
|
create table announcement (
|
||||||
|
id int generated by default as identity primary key,
|
||||||
|
title text not null,
|
||||||
|
content text,
|
||||||
|
type int not null default 1,
|
||||||
|
pin bool not null default false,
|
||||||
|
status int not null default 1,
|
||||||
|
sort int not null default 0,
|
||||||
|
created_at timestamptz default current_timestamp,
|
||||||
|
updated_at timestamptz default current_timestamp,
|
||||||
|
deleted_at timestamptz
|
||||||
|
);
|
||||||
|
create index idx_announcement_type on announcement (type) where deleted_at is null;
|
||||||
|
create index idx_announcement_pin on announcement (pin) where deleted_at is null;
|
||||||
|
create index idx_announcement_created_at on announcement (created_at) where deleted_at is null;
|
||||||
|
|
||||||
|
-- announcement表字段注释
|
||||||
|
comment on table announcement is '公告表';
|
||||||
|
comment on column announcement.id is '公告ID';
|
||||||
|
comment on column announcement.title is '公告标题';
|
||||||
|
comment on column announcement.content is '公告内容';
|
||||||
|
comment on column announcement.type is '公告类型:1-普通公告';
|
||||||
|
comment on column announcement.status is '公告状态:0-禁用,1-正常';
|
||||||
|
comment on column announcement.pin is '是否置顶';
|
||||||
|
comment on column announcement.sort is '公告排序';
|
||||||
|
comment on column announcement.created_at is '创建时间';
|
||||||
|
comment on column announcement.updated_at is '更新时间';
|
||||||
|
comment on column announcement.deleted_at is '删除时间';
|
||||||
|
|
||||||
|
-- inquiry
|
||||||
|
drop table if exists inquiry cascade;
|
||||||
|
create table inquiry (
|
||||||
|
id int generated by default as identity primary key,
|
||||||
|
company text,
|
||||||
|
name text,
|
||||||
|
phone text,
|
||||||
|
email text,
|
||||||
|
content text,
|
||||||
|
status int not null default 0,
|
||||||
|
remark text,
|
||||||
|
created_at timestamptz default current_timestamp,
|
||||||
|
updated_at timestamptz default current_timestamp,
|
||||||
|
deleted_at timestamptz
|
||||||
|
);
|
||||||
|
create index idx_inquiry_phone on inquiry (phone) where deleted_at is null;
|
||||||
|
create index idx_inquiry_status on inquiry (status) where deleted_at is null;
|
||||||
|
create index idx_inquiry_created_at on inquiry (created_at) where deleted_at is null;
|
||||||
|
|
||||||
|
-- inquiry表字段注释
|
||||||
|
comment on table inquiry is '用户咨询表';
|
||||||
|
comment on column inquiry.id is '咨询ID';
|
||||||
|
comment on column inquiry.name is '联系人姓名';
|
||||||
|
comment on column inquiry.phone is '联系电话';
|
||||||
|
comment on column inquiry.email is '联系邮箱';
|
||||||
|
comment on column inquiry.company is '公司名称';
|
||||||
|
comment on column inquiry.content is '咨询内容';
|
||||||
|
comment on column inquiry.status is '处理状态:0-待处理,1-已处理';
|
||||||
|
comment on column inquiry.remark is '备注';
|
||||||
|
comment on column inquiry.created_at is '创建时间';
|
||||||
|
comment on column inquiry.updated_at is '更新时间';
|
||||||
|
comment on column inquiry.deleted_at is '删除时间';
|
||||||
|
|
||||||
|
-- endregion
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
-- region 管理员信息
|
-- region 管理员信息
|
||||||
-- ====================
|
-- ====================
|
||||||
@@ -177,37 +247,6 @@ comment on column admin_role.created_at is '创建时间';
|
|||||||
comment on column admin_role.updated_at is '更新时间';
|
comment on column admin_role.updated_at is '更新时间';
|
||||||
comment on column admin_role.deleted_at is '删除时间';
|
comment on column admin_role.deleted_at is '删除时间';
|
||||||
|
|
||||||
-- announcement
|
|
||||||
drop table if exists announcement cascade;
|
|
||||||
create table announcement (
|
|
||||||
id int generated by default as identity primary key,
|
|
||||||
title text not null,
|
|
||||||
content text,
|
|
||||||
type int not null default 1,
|
|
||||||
pin bool not null default false,
|
|
||||||
status int not null default 1,
|
|
||||||
sort int not null default 0,
|
|
||||||
created_at timestamptz default current_timestamp,
|
|
||||||
updated_at timestamptz default current_timestamp,
|
|
||||||
deleted_at timestamptz
|
|
||||||
);
|
|
||||||
create index idx_announcement_type on announcement (type) where deleted_at is null;
|
|
||||||
create index idx_announcement_pin on announcement (pin) where deleted_at is null;
|
|
||||||
create index idx_announcement_created_at on announcement (created_at) where deleted_at is null;
|
|
||||||
|
|
||||||
-- announcement表字段注释
|
|
||||||
comment on table announcement is '公告表';
|
|
||||||
comment on column announcement.id is '公告ID';
|
|
||||||
comment on column announcement.title is '公告标题';
|
|
||||||
comment on column announcement.content is '公告内容';
|
|
||||||
comment on column announcement.type is '公告类型:1-普通公告';
|
|
||||||
comment on column announcement.status is '公告状态:0-禁用,1-正常';
|
|
||||||
comment on column announcement.pin is '是否置顶';
|
|
||||||
comment on column announcement.sort is '公告排序';
|
|
||||||
comment on column announcement.created_at is '创建时间';
|
|
||||||
comment on column announcement.updated_at is '更新时间';
|
|
||||||
comment on column announcement.deleted_at is '删除时间';
|
|
||||||
|
|
||||||
-- endregion
|
-- endregion
|
||||||
|
|
||||||
-- ====================
|
-- ====================
|
||||||
@@ -500,10 +539,11 @@ create table proxy (
|
|||||||
version int not null,
|
version int not null,
|
||||||
mac text not null,
|
mac text not null,
|
||||||
ip inet not null,
|
ip inet not null,
|
||||||
|
host text,
|
||||||
secret text,
|
secret text,
|
||||||
type int not null,
|
type int not null,
|
||||||
status int not null,
|
status int not null,
|
||||||
meta jsonb not null,
|
meta jsonb,
|
||||||
created_at timestamptz default current_timestamp,
|
created_at timestamptz default current_timestamp,
|
||||||
updated_at timestamptz default current_timestamp,
|
updated_at timestamptz default current_timestamp,
|
||||||
deleted_at timestamptz
|
deleted_at timestamptz
|
||||||
@@ -518,8 +558,9 @@ comment on column proxy.id is '代理服务ID';
|
|||||||
comment on column proxy.version is '代理服务版本';
|
comment on column proxy.version is '代理服务版本';
|
||||||
comment on column proxy.mac is '代理服务名称';
|
comment on column proxy.mac is '代理服务名称';
|
||||||
comment on column proxy.ip is '代理服务地址';
|
comment on column proxy.ip is '代理服务地址';
|
||||||
comment on column proxy.type is '代理服务类型:1-自有,2-白银';
|
comment on column proxy.host is '代理服务域名';
|
||||||
comment on column proxy.secret is '代理服务密钥';
|
comment on column proxy.secret is '代理服务密钥';
|
||||||
|
comment on column proxy.type is '代理服务类型:1-自有,2-白银';
|
||||||
comment on column proxy.status is '代理服务状态:0-离线,1-在线';
|
comment on column proxy.status is '代理服务状态:0-离线,1-在线';
|
||||||
comment on column proxy.meta is '代理服务元信息';
|
comment on column proxy.meta is '代理服务元信息';
|
||||||
comment on column proxy.created_at is '创建时间';
|
comment on column proxy.created_at is '创建时间';
|
||||||
@@ -600,8 +641,10 @@ create table channel (
|
|||||||
resource_id int not null,
|
resource_id int not null,
|
||||||
batch_no text not null,
|
batch_no text not null,
|
||||||
proxy_id int not null,
|
proxy_id int not null,
|
||||||
|
host text not null,
|
||||||
port int not null,
|
port int not null,
|
||||||
edge_id int,
|
edge_id int,
|
||||||
|
edge_ref text,
|
||||||
filter_isp int,
|
filter_isp int,
|
||||||
filter_prov text,
|
filter_prov text,
|
||||||
filter_city text,
|
filter_city text,
|
||||||
@@ -626,8 +669,10 @@ comment on column channel.user_id is '用户ID';
|
|||||||
comment on column channel.resource_id is '套餐ID';
|
comment on column channel.resource_id is '套餐ID';
|
||||||
comment on column channel.batch_no is '批次编号';
|
comment on column channel.batch_no is '批次编号';
|
||||||
comment on column channel.proxy_id is '代理ID';
|
comment on column channel.proxy_id is '代理ID';
|
||||||
|
comment on column channel.host is '代理主机(快照)';
|
||||||
comment on column channel.port is '代理端口';
|
comment on column channel.port is '代理端口';
|
||||||
comment on column channel.edge_id is '节点ID(手动配置)';
|
comment on column channel.edge_id is '节点ID(手动配置)';
|
||||||
|
comment on column channel.edge_ref is '外部节点引用,用于索引没有ID的外部非受控节点';
|
||||||
comment on column channel.filter_isp is '运营商过滤(自动配置):参考 edge.isp';
|
comment on column channel.filter_isp is '运营商过滤(自动配置):参考 edge.isp';
|
||||||
comment on column channel.filter_prov is '省份过滤(自动配置)';
|
comment on column channel.filter_prov is '省份过滤(自动配置)';
|
||||||
comment on column channel.filter_city is '城市过滤(自动配置)';
|
comment on column channel.filter_city is '城市过滤(自动配置)';
|
||||||
@@ -707,14 +752,13 @@ drop table if exists resource_short cascade;
|
|||||||
create table resource_short (
|
create table resource_short (
|
||||||
id int generated by default as identity primary key,
|
id int generated by default as identity primary key,
|
||||||
resource_id int not null,
|
resource_id int not null,
|
||||||
type int not null,
|
|
||||||
live int not null,
|
live int not null,
|
||||||
expire timestamptz,
|
type int not null,
|
||||||
quota int,
|
quota int not null,
|
||||||
|
expire_at timestamptz,
|
||||||
used int not null default 0,
|
used int not null default 0,
|
||||||
daily_limit int not null default 0,
|
daily int not null default 0,
|
||||||
daily_used int not null default 0,
|
last_at timestamptz
|
||||||
daily_last timestamptz
|
|
||||||
);
|
);
|
||||||
create index idx_resource_short_resource_id on resource_short (resource_id);
|
create index idx_resource_short_resource_id on resource_short (resource_id);
|
||||||
|
|
||||||
@@ -722,28 +766,26 @@ create index idx_resource_short_resource_id on resource_short (resource_id);
|
|||||||
comment on table resource_short is '短效动态套餐表';
|
comment on table resource_short is '短效动态套餐表';
|
||||||
comment on column resource_short.id is 'ID';
|
comment on column resource_short.id is 'ID';
|
||||||
comment on column resource_short.resource_id is '套餐ID';
|
comment on column resource_short.resource_id is '套餐ID';
|
||||||
comment on column resource_short.type is '套餐类型:1-包时,2-包量';
|
|
||||||
comment on column resource_short.live is '可用时长(秒)';
|
comment on column resource_short.live is '可用时长(秒)';
|
||||||
comment on column resource_short.quota is '配额数量';
|
comment on column resource_short.type is '套餐类型:1-包时,2-包量';
|
||||||
comment on column resource_short.used is '已用数量';
|
comment on column resource_short.quota is '每日配额(包时)或总配额(包量)';
|
||||||
comment on column resource_short.expire is '过期时间';
|
comment on column resource_short.expire_at is '套餐过期时间,包时模式可用';
|
||||||
comment on column resource_short.daily_limit is '每日限制';
|
comment on column resource_short.used is '总用量';
|
||||||
comment on column resource_short.daily_used is '今日已用数量';
|
comment on column resource_short.daily is '当日用量';
|
||||||
comment on column resource_short.daily_last is '今日最后使用时间';
|
comment on column resource_short.last_at is '最后使用时间';
|
||||||
|
|
||||||
-- resource_long
|
-- resource_long
|
||||||
drop table if exists resource_long cascade;
|
drop table if exists resource_long cascade;
|
||||||
create table resource_long (
|
create table resource_long (
|
||||||
id int generated by default as identity primary key,
|
id int generated by default as identity primary key,
|
||||||
resource_id int not null,
|
resource_id int not null,
|
||||||
type int not null,
|
|
||||||
live int not null,
|
live int not null,
|
||||||
expire timestamptz,
|
type int not null,
|
||||||
quota int,
|
quota int not null,
|
||||||
|
expire_at timestamptz,
|
||||||
used int not null default 0,
|
used int not null default 0,
|
||||||
daily_limit int not null default 0,
|
daily int not null default 0,
|
||||||
daily_used int not null default 0,
|
last_at timestamptz
|
||||||
daily_last timestamptz
|
|
||||||
);
|
);
|
||||||
create index idx_resource_long_resource_id on resource_long (resource_id);
|
create index idx_resource_long_resource_id on resource_long (resource_id);
|
||||||
|
|
||||||
@@ -751,14 +793,13 @@ create index idx_resource_long_resource_id on resource_long (resource_id);
|
|||||||
comment on table resource_long is '长效动态套餐表';
|
comment on table resource_long is '长效动态套餐表';
|
||||||
comment on column resource_long.id is 'ID';
|
comment on column resource_long.id is 'ID';
|
||||||
comment on column resource_long.resource_id is '套餐ID';
|
comment on column resource_long.resource_id is '套餐ID';
|
||||||
|
comment on column resource_long.live is '可用时长(小时)';
|
||||||
comment on column resource_long.type is '套餐类型:1-包时,2-包量';
|
comment on column resource_long.type is '套餐类型:1-包时,2-包量';
|
||||||
comment on column resource_long.live is '可用时长(天)';
|
comment on column resource_long.quota is '每日配额(包时)或总配额(包量)';
|
||||||
comment on column resource_long.quota is '配额数量';
|
comment on column resource_long.expire_at is '套餐过期时间,包时模式可用';
|
||||||
comment on column resource_long.used is '已用数量';
|
comment on column resource_long.used is '总用量';
|
||||||
comment on column resource_long.expire is '过期时间';
|
comment on column resource_long.daily is '当日用量';
|
||||||
comment on column resource_long.daily_limit is '每日限制';
|
comment on column resource_long.last_at is '最后使用时间';
|
||||||
comment on column resource_long.daily_used is '今日已用数量';
|
|
||||||
comment on column resource_long.daily_last is '今日最后使用时间';
|
|
||||||
|
|
||||||
-- endregion
|
-- endregion
|
||||||
|
|
||||||
@@ -1018,16 +1059,3 @@ alter table coupon
|
|||||||
add constraint fk_coupon_user_id foreign key (user_id) references "user" (id) on delete cascade;
|
add constraint fk_coupon_user_id foreign key (user_id) references "user" (id) on delete cascade;
|
||||||
|
|
||||||
-- endregion
|
-- endregion
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- region 填充数据
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
insert into client (
|
|
||||||
client_id, client_secret, redirect_uri, spec, name, type
|
|
||||||
)
|
|
||||||
values (
|
|
||||||
'web', '$2a$10$Ss12mXQgpYyo1CKIZ3URouDm.Lc2KcYJzsvEK2PTIXlv6fHQht45a', '', 3, 'web', 1
|
|
||||||
);
|
|
||||||
|
|
||||||
-- endregion
|
|
||||||
|
|||||||
156
web/auth/account.go
Normal file
156
web/auth/account.go
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"platform/pkg/u"
|
||||||
|
"platform/web/core"
|
||||||
|
m "platform/web/models"
|
||||||
|
q "platform/web/queries"
|
||||||
|
s "platform/web/services"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func authClient(clientId string, clientSecrets ...string) (*m.Client, error) {
|
||||||
|
|
||||||
|
// 获取客户端信息
|
||||||
|
client, err := q.Client.
|
||||||
|
Where(
|
||||||
|
q.Client.ClientID.Eq(clientId),
|
||||||
|
q.Client.Status.Eq(1)).
|
||||||
|
Take()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查客户端密钥
|
||||||
|
if client.Spec == m.ClientSpecWeb || client.Spec == m.ClientSpecAPI {
|
||||||
|
if len(clientSecrets) == 0 {
|
||||||
|
return nil, errors.New("客户端密钥错误")
|
||||||
|
}
|
||||||
|
clientSecret := clientSecrets[0]
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
|
||||||
|
return nil, errors.New("客户端密钥错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo 查询客户端关联权限
|
||||||
|
|
||||||
|
// 组织授权信息(一次性请求)
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authUser(loginType PwdLoginType, username, password string) (user *m.User, err error) {
|
||||||
|
switch loginType {
|
||||||
|
case PwdLoginByPhone:
|
||||||
|
user, err = authUserBySms(q.Q, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
user = &m.User{
|
||||||
|
Phone: username,
|
||||||
|
Username: u.P(username),
|
||||||
|
Status: m.UserStatusEnabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case PwdLoginByEmail:
|
||||||
|
user, err = authUserByEmail(q.Q, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case PwdLoginByPassword:
|
||||||
|
user, err = authUserByPassword(q.Q, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, ErrAuthorizeInvalidRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账户状态
|
||||||
|
if user.Status == m.UserStatusDisabled {
|
||||||
|
return nil, core.NewBizErr("账号已禁用")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
|
||||||
|
// 验证验证码
|
||||||
|
err := s.Verifier.VerifySms(context.Background(), username, code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewBizErr("短信认证失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
return tx.User.Where(tx.User.Phone.Eq(username)).Take()
|
||||||
|
}
|
||||||
|
|
||||||
|
func authUserByEmail(tx *q.Query, username, code string) (*m.User, error) {
|
||||||
|
return nil, core.NewServErr("邮箱登录不可用")
|
||||||
|
}
|
||||||
|
|
||||||
|
func authUserByPassword(tx *q.Query, username, password string) (*m.User, error) {
|
||||||
|
user, err := tx.User.
|
||||||
|
Where(tx.User.Phone.Eq(username)).
|
||||||
|
Or(tx.User.Email.Eq(username)).
|
||||||
|
Or(tx.User.Username.Eq(username)).
|
||||||
|
Take()
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug("查找用户失败", "error", err)
|
||||||
|
return nil, core.NewBizErr("用户不存在或密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
if user.Password == nil || *user.Password == "" {
|
||||||
|
slog.Debug("用户未设置密码", "username", username)
|
||||||
|
return nil, core.NewBizErr("用户不存在或密码错误")
|
||||||
|
}
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)) != nil {
|
||||||
|
slog.Debug("密码验证失败", "username", username)
|
||||||
|
return nil, core.NewBizErr("用户不存在或密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authAdmin(loginType PwdLoginType, username, password string) (admin *m.Admin, err error) {
|
||||||
|
switch loginType {
|
||||||
|
case PwdLoginByPhone, PwdLoginByEmail:
|
||||||
|
return nil, core.NewServErr("不支持的登录方式:" + string(loginType))
|
||||||
|
case PwdLoginByPassword:
|
||||||
|
admin, err = authAdminByPassword(q.Q, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, ErrAuthorizeInvalidRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账户状态
|
||||||
|
if admin.Status == m.AdminStatusDisabled {
|
||||||
|
return nil, core.NewBizErr("账号已禁用")
|
||||||
|
}
|
||||||
|
|
||||||
|
return admin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authAdminByPassword(tx *q.Query, username, password string) (*m.Admin, error) {
|
||||||
|
admin, err := tx.Admin.Where(tx.Admin.Username.Eq(username)).Take()
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewBizErr("账号不存在或密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
if admin.Password == "" {
|
||||||
|
return nil, core.NewBizErr("账号不存在或密码错误")
|
||||||
|
}
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(password)) != nil {
|
||||||
|
return nil, core.NewBizErr("账号不存在或密码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
return admin, nil
|
||||||
|
}
|
||||||
@@ -6,10 +6,8 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
|
||||||
"platform/pkg/env"
|
"platform/pkg/env"
|
||||||
"platform/pkg/u"
|
"platform/pkg/u"
|
||||||
"platform/web/core"
|
|
||||||
g "platform/web/globals"
|
g "platform/web/globals"
|
||||||
"platform/web/globals/orm"
|
"platform/web/globals/orm"
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
@@ -22,67 +20,52 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GrantType string
|
// AuthorizeGet 授权端点
|
||||||
|
func AuthorizeGet(ctx *fiber.Ctx) error {
|
||||||
|
|
||||||
const (
|
// 检查请求
|
||||||
GrantAuthorizationCode = GrantType("authorization_code") // 授权码模式
|
req := new(AuthorizeGetReq)
|
||||||
GrantClientCredentials = GrantType("client_credentials") // 客户端凭证模式
|
if err := g.Validator.ParseQuery(ctx, req); err != nil {
|
||||||
GrantRefreshToken = GrantType("refresh_token") // 刷新令牌模式
|
return err
|
||||||
GrantPassword = GrantType("password") // 密码模式(私有扩展)
|
}
|
||||||
)
|
|
||||||
|
|
||||||
type PasswordGrantType string
|
// 检查客户端
|
||||||
|
client, err := authClient(req.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
if client.RedirectURI == nil || *client.RedirectURI != req.RedirectURI {
|
||||||
GrantPasswordSecret = PasswordGrantType("password") // 账号密码
|
return errors.New("客户端重定向URI错误")
|
||||||
GrantPasswordPhone = PasswordGrantType("phone_code") // 手机验证码
|
}
|
||||||
GrantPasswordEmail = PasswordGrantType("email_code") // 邮箱验证码
|
|
||||||
)
|
|
||||||
|
|
||||||
type TokenReq struct {
|
// todo 检查 scope
|
||||||
GrantType GrantType `json:"grant_type" form:"grant_type"`
|
|
||||||
ClientID string `json:"client_id" form:"client_id"`
|
// 授权确认页面
|
||||||
ClientSecret string `json:"client_secret" form:"client_secret"`
|
return nil
|
||||||
Scope string `json:"scope" form:"scope"`
|
|
||||||
GrantCodeData
|
|
||||||
GrantClientData
|
|
||||||
GrantRefreshData
|
|
||||||
GrantPasswordData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GrantCodeData struct {
|
type AuthorizeGetReq struct {
|
||||||
Code string `json:"code" form:"code"`
|
ResponseType string `json:"response_type" validate:"eq=code"`
|
||||||
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
|
ClientID string `json:"client_id" validate:"required"`
|
||||||
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
|
RedirectURI string `json:"redirect_uri" validate:"required"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
State string `json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GrantClientData struct {
|
func AuthorizePost(ctx *fiber.Ctx) error {
|
||||||
|
|
||||||
|
// todo 解析用户授权的范围
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type GrantRefreshData struct {
|
type AuthorizePostReq struct {
|
||||||
RefreshToken string `json:"refresh_token" form:"refresh_token"`
|
Accept bool `json:"accept"`
|
||||||
}
|
Scope string `json:"scope"`
|
||||||
|
|
||||||
type GrantPasswordData struct {
|
|
||||||
LoginType PasswordGrantType `json:"login_type" form:"login_type"`
|
|
||||||
Username string `json:"username" form:"username"`
|
|
||||||
Password string `json:"password" form:"password"`
|
|
||||||
Remember bool `json:"remember" form:"remember"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenResp struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token,omitempty"`
|
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
Scope string `json:"scope,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenErrResp struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
Description string `json:"error_description,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Token 令牌端点
|
||||||
func Token(c *fiber.Ctx) error {
|
func Token(c *fiber.Ctx) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
@@ -165,6 +148,75 @@ func Token(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TokenReq struct {
|
||||||
|
GrantType GrantType `json:"grant_type" form:"grant_type"`
|
||||||
|
ClientID string `json:"client_id" form:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret" form:"client_secret"`
|
||||||
|
Scope string `json:"scope" form:"scope"`
|
||||||
|
GrantCodeData
|
||||||
|
GrantClientData
|
||||||
|
GrantRefreshData
|
||||||
|
GrantPasswordData
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantCodeData struct {
|
||||||
|
Code string `json:"code" form:"code"`
|
||||||
|
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
|
||||||
|
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantClientData struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantRefreshData struct {
|
||||||
|
RefreshToken string `json:"refresh_token" form:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantPasswordData struct {
|
||||||
|
LoginType PwdLoginType `json:"login_type" form:"login_type"`
|
||||||
|
LoginPool PwdLoginPool `json:"login_pool" form:"login_pool"`
|
||||||
|
Username string `json:"username" form:"username"`
|
||||||
|
Password string `json:"password" form:"password"`
|
||||||
|
Remember bool `json:"remember" form:"remember"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GrantType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GrantAuthorizationCode = GrantType("authorization_code") // 授权码模式
|
||||||
|
GrantClientCredentials = GrantType("client_credentials") // 客户端凭证模式
|
||||||
|
GrantRefreshToken = GrantType("refresh_token") // 刷新令牌模式
|
||||||
|
GrantPassword = GrantType("password") // 密码模式(私有扩展)
|
||||||
|
)
|
||||||
|
|
||||||
|
type PwdLoginType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PwdLoginByPassword = PwdLoginType("password") // 账号密码
|
||||||
|
PwdLoginByPhone = PwdLoginType("phone_code") // 手机验证码
|
||||||
|
PwdLoginByEmail = PwdLoginType("email_code") // 邮箱验证码
|
||||||
|
)
|
||||||
|
|
||||||
|
type PwdLoginPool string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PwdLoginAsUser = PwdLoginPool("user") // 用户池
|
||||||
|
PwdLoginAsAdmin = PwdLoginPool("admin") // 管理员池
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenResp struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenErrResp struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Description string `json:"error_description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m.Session, error) {
|
func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m.Session, error) {
|
||||||
|
|
||||||
// 检查 code 获取用户授权信息
|
// 检查 code 获取用户授权信息
|
||||||
@@ -226,7 +278,7 @@ func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.
|
|||||||
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
|
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = SaveSession(session)
|
err = SaveSession(q.Q, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -249,7 +301,7 @@ func authClientCredential(c *fiber.Ctx, auth *AuthCtx, _ *TokenReq, now time.Tim
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 保存会话
|
// 保存会话
|
||||||
err := SaveSession(session)
|
err := SaveSession(q.Q, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -261,71 +313,85 @@ func authPassword(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m
|
|||||||
ip, _ := orm.ParseInet(c.IP()) // 可空字段,忽略异常
|
ip, _ := orm.ParseInet(c.IP()) // 可空字段,忽略异常
|
||||||
ua := u.X(c.Get(fiber.HeaderUserAgent))
|
ua := u.X(c.Get(fiber.HeaderUserAgent))
|
||||||
|
|
||||||
|
// 分池认证
|
||||||
|
var err error
|
||||||
var user *m.User
|
var user *m.User
|
||||||
err := q.Q.Transaction(func(tx *q.Query) (err error) {
|
var admin *m.Admin
|
||||||
switch req.LoginType {
|
|
||||||
case GrantPasswordPhone:
|
pool := req.LoginPool
|
||||||
user, err = authUserBySms(tx, req.Username, req.Password)
|
if pool == "" {
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
pool = PwdLoginAsUser
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if user == nil {
|
switch pool {
|
||||||
|
case PwdLoginAsUser:
|
||||||
|
user, err = authUser(req.LoginType, req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
if req.LoginType != PwdLoginByPhone || !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号首次登录的自动创建用户
|
||||||
user = &m.User{
|
user = &m.User{
|
||||||
Phone: req.Username,
|
Phone: req.Username,
|
||||||
Username: u.P(req.Username),
|
Username: u.P(req.Username),
|
||||||
Status: m.UserStatusEnabled,
|
Status: m.UserStatusEnabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case GrantPasswordEmail:
|
|
||||||
user, err = authUserByEmail(tx, req.Username, req.Password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case GrantPasswordSecret:
|
|
||||||
user, err = authUserByPassword(tx, req.Username, req.Password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return ErrAuthorizeInvalidRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
// 账户状态
|
|
||||||
if user.Status == m.UserStatusDisabled {
|
|
||||||
slog.Debug("账户状态异常", "username", req.Username, "status", user.Status)
|
|
||||||
return core.NewBizErr("账号无法登录")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户的登录时间
|
// 更新用户的登录时间
|
||||||
user.LastLogin = u.P(time.Now())
|
user.LastLogin = u.P(time.Now())
|
||||||
user.LastLoginIP = ip
|
user.LastLoginIP = ip
|
||||||
user.LastLoginUA = ua
|
user.LastLoginUA = ua
|
||||||
if err := tx.User.Save(user); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
case PwdLoginAsAdmin:
|
||||||
})
|
admin, err = authAdmin(req.LoginType, req.Username, req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新管理员登录时间
|
||||||
|
admin.LastLogin = u.P(time.Now())
|
||||||
|
admin.LastLoginIP = ip
|
||||||
|
admin.LastLoginUA = ua
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, ErrAuthorizeInvalidRequest
|
||||||
|
}
|
||||||
|
|
||||||
// 生成会话
|
// 生成会话
|
||||||
session := &m.Session{
|
session := &m.Session{
|
||||||
IP: ip,
|
IP: ip,
|
||||||
UA: ua,
|
UA: ua,
|
||||||
UserID: &user.ID,
|
|
||||||
ClientID: &auth.Client.ID,
|
ClientID: &auth.Client.ID,
|
||||||
Scopes: u.X(req.Scope),
|
Scopes: u.X(req.Scope),
|
||||||
AccessToken: uuid.NewString(),
|
AccessToken: uuid.NewString(),
|
||||||
AccessTokenExpires: now.Add(time.Duration(env.SessionAccessExpire) * time.Second),
|
AccessTokenExpires: now.Add(time.Duration(env.SessionAccessExpire) * time.Second),
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Remember {
|
if req.Remember {
|
||||||
session.RefreshToken = u.P(uuid.NewString())
|
session.RefreshToken = u.P(uuid.NewString())
|
||||||
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
|
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = SaveSession(session)
|
// 保存用户更新和会话
|
||||||
|
err = q.Q.Transaction(func(tx *q.Query) error {
|
||||||
|
if user != nil {
|
||||||
|
if err := tx.User.Save(user); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
session.UserID = &user.ID
|
||||||
|
}
|
||||||
|
if admin != nil {
|
||||||
|
if err := tx.Admin.Save(admin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
session.AdminID = &admin.ID
|
||||||
|
}
|
||||||
|
if err := SaveSession(tx, session); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -353,7 +419,7 @@ func authRefreshToken(_ *fiber.Ctx, _ *AuthCtx, req *TokenReq, now time.Time) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 保存令牌
|
// 保存令牌
|
||||||
err = SaveSession(session)
|
err = SaveSession(q.Q, session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -394,12 +460,117 @@ func sendError(c *fiber.Ctx, err error, description ...string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Revoke() error {
|
// Revoke 令牌撤销端点
|
||||||
|
func Revoke(ctx *fiber.Ctx) error {
|
||||||
|
_, err := GetAuthCtx(ctx).PermitUser()
|
||||||
|
if err != nil {
|
||||||
|
// 用户未登录
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析请求参数
|
||||||
|
req := new(RevokeReq)
|
||||||
|
if err := ctx.BodyParser(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除会话
|
||||||
|
err = RemoveSession(ctx.Context(), req.AccessToken, req.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Introspect() error {
|
type RevokeReq struct {
|
||||||
return nil
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Introspect 令牌检查端点
|
||||||
|
func Introspect(ctx *fiber.Ctx) error {
|
||||||
|
authCtx := GetAuthCtx(ctx)
|
||||||
|
|
||||||
|
// 尝试验证用户权限
|
||||||
|
if _, err := authCtx.PermitUser(); err == nil {
|
||||||
|
return introspectUser(ctx, authCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试验证管理员权限
|
||||||
|
if _, err := authCtx.PermitAdmin(); err == nil {
|
||||||
|
return introspectAdmin(ctx, authCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrAuthenticateForbidden
|
||||||
|
}
|
||||||
|
|
||||||
|
// introspectUser 获取并返回用户信息
|
||||||
|
func introspectUser(ctx *fiber.Ctx, authCtx *AuthCtx) error {
|
||||||
|
// 获取用户信息
|
||||||
|
profile, err := q.User.
|
||||||
|
Where(q.User.ID.Eq(authCtx.User.ID)).
|
||||||
|
Omit(q.User.DeletedAt).
|
||||||
|
Take()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否设置了密码
|
||||||
|
hasPassword := false
|
||||||
|
if profile.Password != nil && *profile.Password != "" {
|
||||||
|
hasPassword = true
|
||||||
|
profile.Password = nil // 不返回密码
|
||||||
|
}
|
||||||
|
|
||||||
|
// 掩码敏感信息
|
||||||
|
if profile.Phone != "" {
|
||||||
|
profile.Phone = maskPhone(profile.Phone)
|
||||||
|
}
|
||||||
|
if profile.IDNo != nil && *profile.IDNo != "" {
|
||||||
|
profile.IDNo = u.P(maskIdNo(*profile.IDNo))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.JSON(struct {
|
||||||
|
m.User
|
||||||
|
HasPassword bool `json:"has_password"` // 是否设置了密码
|
||||||
|
}{*profile, hasPassword})
|
||||||
|
}
|
||||||
|
|
||||||
|
// introspectAdmin 获取并返回管理员信息
|
||||||
|
func introspectAdmin(ctx *fiber.Ctx, authCtx *AuthCtx) error {
|
||||||
|
// 获取管理员信息
|
||||||
|
profile, err := q.Admin.
|
||||||
|
Where(q.Admin.ID.Eq(authCtx.Admin.ID)).
|
||||||
|
Omit(q.Admin.DeletedAt).
|
||||||
|
Take()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不返回密码
|
||||||
|
profile.Password = ""
|
||||||
|
|
||||||
|
// 掩码敏感信息
|
||||||
|
if profile.Phone != nil && *profile.Phone != "" {
|
||||||
|
profile.Phone = u.P(maskPhone(*profile.Phone))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.JSON(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskPhone(phone string) string {
|
||||||
|
if len(phone) < 11 {
|
||||||
|
return phone
|
||||||
|
}
|
||||||
|
return phone[:3] + "****" + phone[7:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskIdNo(idNo string) string {
|
||||||
|
if len(idNo) < 18 {
|
||||||
|
return idNo
|
||||||
|
}
|
||||||
|
return idNo[:3] + "*********" + idNo[14:]
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodeContext struct {
|
type CodeContext struct {
|
||||||
@@ -6,15 +6,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"platform/web/core"
|
|
||||||
m "platform/web/models"
|
|
||||||
q "platform/web/queries"
|
|
||||||
s "platform/web/services"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Authenticate() fiber.Handler {
|
func Authenticate() fiber.Handler {
|
||||||
@@ -123,67 +118,3 @@ func authBasic(_ context.Context, token string) (*AuthCtx, error) {
|
|||||||
Scopes: []string{},
|
Scopes: []string{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func authClient(clientId, clientSecret string) (*m.Client, error) {
|
|
||||||
|
|
||||||
// 获取客户端信息
|
|
||||||
client, err := q.Client.
|
|
||||||
Where(
|
|
||||||
q.Client.ClientID.Eq(clientId),
|
|
||||||
q.Client.Status.Eq(1)).
|
|
||||||
Take()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查客户端密钥
|
|
||||||
if client.Spec == m.ClientSpecWeb || client.Spec == m.ClientSpecAPI {
|
|
||||||
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
|
|
||||||
return nil, errors.New("客户端密钥错误")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo 查询客户端关联权限
|
|
||||||
|
|
||||||
// 组织授权信息(一次性请求)
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
|
|
||||||
// 验证验证码
|
|
||||||
err := s.Verifier.VerifySms(context.Background(), username, code)
|
|
||||||
if err != nil {
|
|
||||||
return nil, core.NewBizErr("短信认证失败:%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找用户
|
|
||||||
return tx.User.Where(tx.User.Phone.Eq(username)).Take()
|
|
||||||
}
|
|
||||||
|
|
||||||
func authUserByEmail(tx *q.Query, username, code string) (*m.User, error) {
|
|
||||||
return nil, core.NewServErr("邮箱登录不可用")
|
|
||||||
}
|
|
||||||
|
|
||||||
func authUserByPassword(tx *q.Query, username, password string) (*m.User, error) {
|
|
||||||
user, err := tx.User.
|
|
||||||
Where(tx.User.Phone.Eq(username)).
|
|
||||||
Or(tx.User.Email.Eq(username)).
|
|
||||||
Or(tx.User.Username.Eq(username)).
|
|
||||||
Take()
|
|
||||||
if err != nil {
|
|
||||||
slog.Debug("查找用户失败", "error", err)
|
|
||||||
return nil, core.NewBizErr("用户不存在或密码错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证密码
|
|
||||||
if user.Password == nil || *user.Password == "" {
|
|
||||||
slog.Debug("用户未设置密码", "username", username)
|
|
||||||
return nil, core.NewBizErr("用户不存在或密码错误")
|
|
||||||
}
|
|
||||||
if bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)) != nil {
|
|
||||||
slog.Debug("密码验证失败", "username", username)
|
|
||||||
return nil, core.NewBizErr("用户不存在或密码错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
@@ -29,8 +29,8 @@ func FindSessionByRefresh(refreshToken string, now time.Time) (*m.Session, error
|
|||||||
).First()
|
).First()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveSession(session *m.Session) error {
|
func SaveSession(tx *q.Query, session *m.Session) error {
|
||||||
return q.Session.Save(session)
|
return tx.Session.Save(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RemoveSession(ctx context.Context, accessToken string, refreshToken string) error {
|
func RemoveSession(ctx context.Context, accessToken string, refreshToken string) error {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ type Err struct {
|
|||||||
func (e *Err) Error() string {
|
func (e *Err) Error() string {
|
||||||
if e.err != nil {
|
if e.err != nil {
|
||||||
slog.Debug(fmt.Sprintf("%s: %s", e.msg, e.err.Error()))
|
slog.Debug(fmt.Sprintf("%s: %s", e.msg, e.err.Error()))
|
||||||
return e.msg
|
|
||||||
}
|
}
|
||||||
return e.msg
|
return e.msg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"platform/pkg/env"
|
||||||
|
"platform/pkg/u"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// PageReq 分页请求参数
|
// PageReq 分页请求参数
|
||||||
type PageReq struct {
|
type PageReq struct {
|
||||||
RawPage int `json:"page"`
|
RawPage int `json:"page"`
|
||||||
@@ -38,3 +50,83 @@ type PageResp struct {
|
|||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
List any `json:"list"`
|
List any `json:"list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch 发送HTTP请求并返回响应
|
||||||
|
func Fetch(req *http.Request) (*http.Response, error) {
|
||||||
|
if env.DebugHttpDump {
|
||||||
|
str, err := httputil.DumpRequest(req, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Printf("===== REQUEST ===== %s\n", req.URL)
|
||||||
|
fmt.Println(string(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if env.DebugHttpDump {
|
||||||
|
str, err := httputil.DumpResponse(resp, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Printf("===== RESPONSE ===== %s\n", req.URL)
|
||||||
|
fmt.Println(string(str))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Query(in any) url.Values {
|
||||||
|
out := url.Values{}
|
||||||
|
|
||||||
|
if in == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
ref := reflect.ValueOf(in)
|
||||||
|
if ref.Kind() == reflect.Pointer {
|
||||||
|
ref = ref.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if ref.Kind() != reflect.Struct {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < ref.NumField(); i++ {
|
||||||
|
field := ref.Type().Field(i)
|
||||||
|
value := ref.Field(i)
|
||||||
|
|
||||||
|
if field.Type.Kind() == reflect.Pointer {
|
||||||
|
if value.IsNil() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value = value.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
name := field.Name
|
||||||
|
tags := strings.Split(field.Tag.Get("query"), ",")
|
||||||
|
if len(tags) > 0 && tags[0] != "" {
|
||||||
|
name = tags[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.Interface().(type) {
|
||||||
|
case string:
|
||||||
|
out.Add(name, value)
|
||||||
|
case int:
|
||||||
|
out.Add(name, strconv.Itoa(value))
|
||||||
|
case bool:
|
||||||
|
if tags[1] == "b2i" {
|
||||||
|
out.Add(name, u.Ternary(value, "1", "0"))
|
||||||
|
} else {
|
||||||
|
out.Add(name, strconv.FormatBool(value))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
out.Add(name, fmt.Sprintf("%v", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +1,11 @@
|
|||||||
package events
|
package events
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
)
|
)
|
||||||
|
|
||||||
const RemoveChannel = "channel:remove"
|
const RemoveChannel = "channel:remove"
|
||||||
|
|
||||||
type RemoveChannelData struct {
|
func NewRemoveChannel(batch string) *asynq.Task {
|
||||||
Batch string `json:"batch"`
|
return asynq.NewTask(RemoveChannel, []byte(batch))
|
||||||
IDs []int32 `json:"ids"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRemoveChannel(data RemoveChannelData) *asynq.Task {
|
|
||||||
bytes, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("序列化删除通道任务失败", "error", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return asynq.NewTask(RemoveChannel, bytes)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"platform/pkg/env"
|
"platform/pkg/env"
|
||||||
|
"platform/web/core"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,18 +18,12 @@ import (
|
|||||||
|
|
||||||
// CloudClient 定义云服务接口
|
// CloudClient 定义云服务接口
|
||||||
type CloudClient interface {
|
type CloudClient interface {
|
||||||
CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error)
|
CloudEdges(param *CloudEdgesReq) (*CloudEdgesResp, error)
|
||||||
CloudConnect(param CloudConnectReq) error
|
CloudConnect(param *CloudConnectReq) error
|
||||||
CloudDisconnect(param CloudDisconnectReq) (int, error)
|
CloudDisconnect(param *CloudDisconnectReq) (int, error)
|
||||||
CloudAutoQuery() (CloudConnectResp, error)
|
CloudAutoQuery() (CloudConnectResp, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GatewayClient 定义网关接口
|
|
||||||
type GatewayClient interface {
|
|
||||||
GatewayPortConfigs(params []PortConfigsReq) error
|
|
||||||
GatewayPortActive(param ...PortActiveReq) (map[string]PortData, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type cloud struct {
|
type cloud struct {
|
||||||
url string
|
url string
|
||||||
}
|
}
|
||||||
@@ -37,59 +32,14 @@ var Cloud CloudClient
|
|||||||
|
|
||||||
func initBaiyin() error {
|
func initBaiyin() error {
|
||||||
Cloud = &cloud{
|
Cloud = &cloud{
|
||||||
url: env.BaiyinAddr,
|
url: env.BaiyinCloudUrl,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type AutoConfig struct {
|
// cloud:/edges 筛选查询边缘节点
|
||||||
Province string `json:"province"`
|
func (c *cloud) CloudEdges(param *CloudEdgesReq) (*CloudEdgesResp, error) {
|
||||||
City string `json:"city"`
|
resp, err := c.requestCloud("GET", "/edges?"+core.Query(param).Encode(), "")
|
||||||
Isp string `json:"isp"`
|
|
||||||
Count int `json:"count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// region cloud:/edges
|
|
||||||
|
|
||||||
type CloudEdgesReq struct {
|
|
||||||
Province string
|
|
||||||
City string
|
|
||||||
Isp string
|
|
||||||
Offset int
|
|
||||||
Limit int
|
|
||||||
}
|
|
||||||
|
|
||||||
type CloudEdgesResp struct {
|
|
||||||
Edges []Edge `json:"edges"`
|
|
||||||
Total int `json:"total"`
|
|
||||||
Offset int `json:"offset"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Edge struct {
|
|
||||||
EdgesId int `json:"edges_id"`
|
|
||||||
Province string `json:"province"`
|
|
||||||
City string `json:"city"`
|
|
||||||
Isp string `json:"isp"`
|
|
||||||
Ip string `json:"ip"`
|
|
||||||
Rtt int `json:"rtt"`
|
|
||||||
PacketLoss int `json:"packet_loss"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cloud) CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error) {
|
|
||||||
data := strings.Builder{}
|
|
||||||
data.WriteString("province=")
|
|
||||||
data.WriteString(param.Province)
|
|
||||||
data.WriteString("&city=")
|
|
||||||
data.WriteString(param.City)
|
|
||||||
data.WriteString("&isp=")
|
|
||||||
data.WriteString(param.Isp)
|
|
||||||
data.WriteString("&offset=")
|
|
||||||
data.WriteString(strconv.Itoa(param.Offset))
|
|
||||||
data.WriteString("&limit=")
|
|
||||||
data.WriteString(strconv.Itoa(param.Limit))
|
|
||||||
|
|
||||||
resp, err := c.requestCloud("GET", "/edges?"+data.String(), "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -115,17 +65,46 @@ func (c *cloud) CloudEdges(param CloudEdgesReq) (*CloudEdgesResp, error) {
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
type CloudEdgesReq struct {
|
||||||
|
Province *string `query:"province"`
|
||||||
// region cloud:/connect
|
City *string `query:"city"`
|
||||||
|
Isp *string `query:"isp"`
|
||||||
type CloudConnectReq struct {
|
Offset *int `query:"offset"`
|
||||||
Uuid string `json:"uuid"`
|
Limit *int `query:"limit"`
|
||||||
Edge []string `json:"edge,omitempty"`
|
NoRepeat *bool `query:"norepeat,b2i"`
|
||||||
AutoConfig []AutoConfig `json:"auto_config,omitempty"`
|
NoDayRepeat *bool `query:"nodayrepeat,b2i"`
|
||||||
|
IpUnchangedTime *int `query:"ip_unchanged_time"` // 单位秒
|
||||||
|
ActiveTime *int `query:"active_time"` // 单位秒
|
||||||
|
// 排序方式,可选值:
|
||||||
|
// - create_time_asc 设备创建时间顺序
|
||||||
|
// - create_time_desc 设备创建时间倒序
|
||||||
|
// - ip_unchanged_time_asc ip持续没变化时间顺序
|
||||||
|
// - ip_unchanged_time_desc ip持续没变化时间倒序
|
||||||
|
// - active_time_asc 连续活跃时间顺序
|
||||||
|
// - active_time_desc 连续活跃时间倒序
|
||||||
|
// - rand 随机排序 (默认)
|
||||||
|
Sort *string `query:"sort"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cloud) CloudConnect(param CloudConnectReq) error {
|
type CloudEdgesResp struct {
|
||||||
|
Edges []Edge `json:"edges"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Edge struct {
|
||||||
|
EdgeID string `json:"edge_id"`
|
||||||
|
Province string `json:"province"`
|
||||||
|
City string `json:"city"`
|
||||||
|
Isp string `json:"isp"`
|
||||||
|
Ip string `json:"ip"`
|
||||||
|
Rtt int `json:"rtt"`
|
||||||
|
PacketLoss int `json:"packet_loss"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// cloud:/connect 连接边缘节点到网关
|
||||||
|
func (c *cloud) CloudConnect(param *CloudConnectReq) error {
|
||||||
data, err := json.Marshal(param)
|
data, err := json.Marshal(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -162,25 +141,21 @@ func (c *cloud) CloudConnect(param CloudConnectReq) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
type CloudConnectReq struct {
|
||||||
|
|
||||||
// region cloud:/disconnect
|
|
||||||
|
|
||||||
type CloudDisconnectReq struct {
|
|
||||||
Uuid string `json:"uuid"`
|
Uuid string `json:"uuid"`
|
||||||
Edge []string `json:"edge,omitempty"`
|
Edge *[]string `json:"edge,omitempty"`
|
||||||
Config []Config `json:"auto_config,omitempty"`
|
AutoConfig *[]AutoConfig `json:"auto_config,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type AutoConfig struct {
|
||||||
Province string `json:"province"`
|
Province string `json:"province"`
|
||||||
City string `json:"city"`
|
City string `json:"city"`
|
||||||
Isp string `json:"isp"`
|
Isp string `json:"isp"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Online bool `json:"online"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *cloud) CloudDisconnect(param CloudDisconnectReq) (int, error) {
|
// cloud:/disconnect 解除连接边缘节点到网关
|
||||||
|
func (c *cloud) CloudDisconnect(param *CloudDisconnectReq) (int, error) {
|
||||||
data, err := json.Marshal(param)
|
data, err := json.Marshal(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -217,12 +192,21 @@ func (c *cloud) CloudDisconnect(param CloudDisconnectReq) (int, error) {
|
|||||||
return int(result["disconnected_edges"].(float64)), nil
|
return int(result["disconnected_edges"].(float64)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
type CloudDisconnectReq struct {
|
||||||
|
Uuid string `json:"uuid"`
|
||||||
|
Edge *[]string `json:"edge,omitempty"`
|
||||||
|
Config *[]Config `json:"auto_config,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// region cloud:/auto_query
|
type Config struct {
|
||||||
|
Province string `json:"province"`
|
||||||
type CloudConnectResp map[string][]AutoConfig
|
City string `json:"city"`
|
||||||
|
Isp string `json:"isp"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Online bool `json:"online"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// cloud:/auto_query 自动连接配置查询
|
||||||
func (c *cloud) CloudAutoQuery() (CloudConnectResp, error) {
|
func (c *cloud) CloudAutoQuery() (CloudConnectResp, error) {
|
||||||
resp, err := c.requestCloud("GET", "/auto_query", "")
|
resp, err := c.requestCloud("GET", "/auto_query", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -250,7 +234,7 @@ func (c *cloud) CloudAutoQuery() (CloudConnectResp, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
type CloudConnectResp map[string][]AutoConfig
|
||||||
|
|
||||||
func (c *cloud) requestCloud(method string, url string, data string) (*http.Response, error) {
|
func (c *cloud) requestCloud(method string, url string, data string) (*http.Response, error) {
|
||||||
|
|
||||||
@@ -263,7 +247,7 @@ func (c *cloud) requestCloud(method string, url string, data string) (*http.Resp
|
|||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
for i := 0; i < 2; i++ {
|
for i := range 2 {
|
||||||
token, err := c.token(i == 1)
|
token, err := c.token(i == 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -304,7 +288,7 @@ func (c *cloud) requestCloud(method string, url string, data string) (*http.Resp
|
|||||||
func (c *cloud) token(refresh bool) (string, error) {
|
func (c *cloud) token(refresh bool) (string, error) {
|
||||||
// redis 获取令牌
|
// redis 获取令牌
|
||||||
if !refresh {
|
if !refresh {
|
||||||
token, err := Redis.Get(context.Background(), "remote:token").Result()
|
token, err := Redis.Get(context.Background(), BaiyinToken).Result()
|
||||||
if err == nil && token != "" {
|
if err == nil && token != "" {
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
@@ -338,7 +322,7 @@ func (c *cloud) token(refresh bool) (string, error) {
|
|||||||
var result map[string]any
|
var result map[string]any
|
||||||
err = json.Unmarshal(body, &result)
|
err = json.Unmarshal(body, &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("解析响应 [%s] 失败: %w", string(body), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result["code"].(float64) != 1 {
|
if result["code"].(float64) != 1 {
|
||||||
@@ -347,7 +331,7 @@ func (c *cloud) token(refresh bool) (string, error) {
|
|||||||
|
|
||||||
// redis 设置令牌
|
// redis 设置令牌
|
||||||
token := result["token"].(string)
|
token := result["token"].(string)
|
||||||
err = Redis.Set(context.Background(), "remote:token", token, 1*time.Hour).Err()
|
err = Redis.Set(context.Background(), BaiyinToken, token, 1*time.Hour).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -355,6 +339,15 @@ func (c *cloud) token(refresh bool) (string, error) {
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BaiyinToken = "clients:baiyin:token"
|
||||||
|
|
||||||
|
// GatewayClient 定义网关接口
|
||||||
|
type GatewayClient interface {
|
||||||
|
GatewayPortConfigs(params []*PortConfigsReq) error
|
||||||
|
GatewayPortActive(param ...*PortActiveReq) (map[string]PortData, error)
|
||||||
|
GatewayEdge(params *GatewayEdgeReq) (map[string]GatewayEdgeInfo, error)
|
||||||
|
}
|
||||||
|
|
||||||
type gateway struct {
|
type gateway struct {
|
||||||
url string
|
url string
|
||||||
username string
|
username string
|
||||||
@@ -373,6 +366,68 @@ func NewGateway(url, username, password string) GatewayClient {
|
|||||||
return GatewayInitializer(url, username, password)
|
return GatewayInitializer(url, username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GatewayEdgeReq struct {
|
||||||
|
EdgeID *string `query:"edge_id"`
|
||||||
|
Province *string `query:"province"`
|
||||||
|
City *string `query:"city"`
|
||||||
|
Isp *string `query:"isp"`
|
||||||
|
Connected *bool `query:"connected"`
|
||||||
|
Assigned *bool `query:"assigned"`
|
||||||
|
GetRand *int `query:"getRand"`
|
||||||
|
IpUnchangedTimeStart *int `query:"ip_unchanged_time_start"`
|
||||||
|
IpUnchangedTimeEnd *int `query:"ip_unchanged_time_end"`
|
||||||
|
OnlineTimeStart *int `query:"online_time_start"`
|
||||||
|
OnlineTimeEnd *int `query:"online_time_end"`
|
||||||
|
Rtt *int `query:"rtt"`
|
||||||
|
MinRtt *int `query:"min_rtt"`
|
||||||
|
RttBaidu *int `query:"rtt_baidu"`
|
||||||
|
PacketLoss *int `query:"packet_loss"`
|
||||||
|
PacketLossBaidu *int `query:"packet_loss_baidu"`
|
||||||
|
IP *string `query:"ip"`
|
||||||
|
Limit *int `query:"limit"`
|
||||||
|
Offset *int `query:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GatewayEdgeResp struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data map[string]GatewayEdgeInfo `json:"data"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GatewayEdgeInfo struct {
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Connected bool `json:"connected"`
|
||||||
|
Assigned bool `json:"assigned"`
|
||||||
|
AssignedTo string `json:"assignedto"`
|
||||||
|
PacketLoss int `json:"packet_loss"`
|
||||||
|
PacketLossBaidu int `json:"packet_loss_baidu"`
|
||||||
|
Rtt int `json:"rtt"`
|
||||||
|
RttBaidu int `json:"rtt_baidu"`
|
||||||
|
OfflineTime int `json:"offline_time"`
|
||||||
|
OnlineTime int `json:"online_time"`
|
||||||
|
IpUnchangedTime int `json:"ip_unchanged_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *gateway) GatewayEdge(req *GatewayEdgeReq) (map[string]GatewayEdgeInfo, error) {
|
||||||
|
resp, err := c.get("/edge", core.Query(req))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("查询可用节点失败:%w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body := new(GatewayEdgeResp)
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(body); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析响应内容失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("接口业务响应异常: %d %s", body.Code, body.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return body.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
// region gateway:/port/configs
|
// region gateway:/port/configs
|
||||||
|
|
||||||
type PortConfigsReq struct {
|
type PortConfigsReq struct {
|
||||||
@@ -395,7 +450,7 @@ type AutoEdgeConfig struct {
|
|||||||
PacketLoss int `json:"packet_loss,omitempty"`
|
PacketLoss int `json:"packet_loss,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *gateway) GatewayPortConfigs(params []PortConfigsReq) error {
|
func (c *gateway) GatewayPortConfigs(params []*PortConfigsReq) error {
|
||||||
if len(params) == 0 {
|
if len(params) == 0 {
|
||||||
return errors.New("params is empty")
|
return errors.New("params is empty")
|
||||||
}
|
}
|
||||||
@@ -461,10 +516,10 @@ type PortData struct {
|
|||||||
Userpass string `json:"userpass"`
|
Userpass string `json:"userpass"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *gateway) GatewayPortActive(param ...PortActiveReq) (map[string]PortData, error) {
|
func (c *gateway) GatewayPortActive(param ...*PortActiveReq) (map[string]PortData, error) {
|
||||||
_param := PortActiveReq{}
|
_param := PortActiveReq{}
|
||||||
if len(param) != 0 {
|
if len(param) != 0 && param[0] != nil {
|
||||||
_param = param[0]
|
_param = *param[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
path := strings.Builder{}
|
path := strings.Builder{}
|
||||||
@@ -520,38 +575,33 @@ func (c *gateway) GatewayPortActive(param ...PortActiveReq) (map[string]PortData
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
func (c *gateway) get(url string, params url.Values) (*http.Response, error) {
|
||||||
|
url = fmt.Sprintf("http://%s:%s@%s:9990%s?%s", c.username, c.password, c.url, url, params.Encode())
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建请求失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := core.Fetch(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取数据失败:%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
bytes, _ := io.ReadAll(res.Body)
|
||||||
|
return nil, fmt.Errorf("接口响应异常: %d %s", res.StatusCode, string(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *gateway) requestGateway(method string, url string, data string) (*http.Response, error) {
|
func (c *gateway) requestGateway(method string, url string, data string) (*http.Response, error) {
|
||||||
//goland:noinspection ALL
|
|
||||||
url = fmt.Sprintf("http://%s:%s@%s:9990%s", c.username, c.password, c.url, url)
|
url = fmt.Sprintf("http://%s:%s@%s:9990%s", c.username, c.password, c.url, url)
|
||||||
req, err := http.NewRequest(method, url, strings.NewReader(data))
|
req, err := http.NewRequest(method, url, strings.NewReader(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if env.DebugHttpDump {
|
return core.Fetch(req)
|
||||||
str, err := httputil.DumpRequest(req, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fmt.Println("==============================")
|
|
||||||
fmt.Println(string(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if env.DebugHttpDump {
|
|
||||||
str, err := httputil.DumpResponse(resp, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fmt.Println("------------------------------")
|
|
||||||
fmt.Println(string(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package globals
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"platform/pkg/env"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
@@ -16,8 +17,13 @@ import (
|
|||||||
var tp *trace.TracerProvider
|
var tp *trace.TracerProvider
|
||||||
|
|
||||||
func initOtel(ctx context.Context) error {
|
func initOtel(ctx context.Context) error {
|
||||||
|
if env.OtelHost == "" || env.OtelPort == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := env.OtelHost + ":" + env.OtelPort
|
||||||
exporter, err := otlptracegrpc.New(ctx,
|
exporter, err := otlptracegrpc.New(ctx,
|
||||||
otlptracegrpc.WithEndpoint("localhost:4317"),
|
otlptracegrpc.WithEndpoint(addr),
|
||||||
otlptracegrpc.WithInsecure(),
|
otlptracegrpc.WithInsecure(),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"platform/pkg/u"
|
|
||||||
auth2 "platform/web/auth"
|
|
||||||
m "platform/web/models"
|
|
||||||
q "platform/web/queries"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// region /revoke
|
|
||||||
|
|
||||||
type RevokeReq struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Revoke(c *fiber.Ctx) error {
|
|
||||||
_, err := auth2.GetAuthCtx(c).PermitUser()
|
|
||||||
if err != nil {
|
|
||||||
// 用户未登录
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析请求参数
|
|
||||||
req := new(RevokeReq)
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除会话
|
|
||||||
err = auth2.RemoveSession(c.Context(), req.AccessToken, req.RefreshToken)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region /profile
|
|
||||||
|
|
||||||
type IntrospectResp struct {
|
|
||||||
m.User
|
|
||||||
HasPassword bool `json:"has_password"` // 是否设置了密码
|
|
||||||
}
|
|
||||||
|
|
||||||
func Introspect(c *fiber.Ctx) error {
|
|
||||||
// 验证权限
|
|
||||||
authCtx, err := auth2.GetAuthCtx(c).PermitUser()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
profile, err := q.User.
|
|
||||||
Where(q.User.ID.Eq(authCtx.User.ID)).
|
|
||||||
Omit(q.User.DeletedAt).
|
|
||||||
Take()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查用户是否设置了密码
|
|
||||||
hasPassword := false
|
|
||||||
if profile.Password != nil && *profile.Password != "" {
|
|
||||||
hasPassword = true
|
|
||||||
profile.Password = nil // 不返回密码
|
|
||||||
}
|
|
||||||
|
|
||||||
// 掩码敏感信息
|
|
||||||
if profile.Phone != "" {
|
|
||||||
profile.Phone = maskPhone(profile.Phone)
|
|
||||||
}
|
|
||||||
if profile.IDNo != nil && *profile.IDNo != "" {
|
|
||||||
profile.IDNo = u.P(maskIdNo(*profile.IDNo))
|
|
||||||
}
|
|
||||||
return c.JSON(IntrospectResp{*profile, hasPassword})
|
|
||||||
}
|
|
||||||
|
|
||||||
func maskPhone(phone string) string {
|
|
||||||
if len(phone) < 11 {
|
|
||||||
return phone
|
|
||||||
}
|
|
||||||
return phone[:3] + "****" + phone[7:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func maskIdNo(idNo string) string {
|
|
||||||
if len(idNo) < 18 {
|
|
||||||
return idNo
|
|
||||||
}
|
|
||||||
return idNo[:3] + "*********" + idNo[14:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
79
web/handlers/batch.go
Normal file
79
web/handlers/batch.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"platform/web/auth"
|
||||||
|
"platform/web/core"
|
||||||
|
c "platform/web/core"
|
||||||
|
g "platform/web/globals"
|
||||||
|
q "platform/web/queries"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PageResourceBatch 分页查询套餐提取记录
|
||||||
|
func PageResourceBatch(ctx *fiber.Ctx) error {
|
||||||
|
// 检查权限
|
||||||
|
authCtx, err := auth.GetAuthCtx(ctx).PermitUser()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析请求参数
|
||||||
|
req := new(PageResourceBatchReq)
|
||||||
|
if err := g.Validator.ParseBody(ctx, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询批次
|
||||||
|
conds := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(authCtx.User.ID))
|
||||||
|
if req.TimeStart != nil {
|
||||||
|
conds.Where(q.LogsUserUsage.Time.Gte(*req.TimeStart))
|
||||||
|
}
|
||||||
|
if req.TimeEnd != nil {
|
||||||
|
conds.Where(q.LogsUserUsage.Time.Lte(*req.TimeEnd))
|
||||||
|
}
|
||||||
|
|
||||||
|
list, total, err := q.LogsUserUsage.Where(conds).
|
||||||
|
Order(q.LogsUserUsage.Time.Desc()).
|
||||||
|
FindByPage(req.GetOffset(), req.GetLimit())
|
||||||
|
if err != nil {
|
||||||
|
return core.NewBizErr("获取数据失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回数据
|
||||||
|
return ctx.JSON(c.PageResp{
|
||||||
|
Total: int(total),
|
||||||
|
List: list,
|
||||||
|
Page: req.GetPage(),
|
||||||
|
Size: req.GetSize(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageResourceBatchReq struct {
|
||||||
|
c.PageReq
|
||||||
|
TimeStart *time.Time `json:"time_start"`
|
||||||
|
TimeEnd *time.Time `json:"time_end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageBatchByAdmin 分页查询所有提取记录
|
||||||
|
func PageBatchByAdmin(c *fiber.Ctx) error {
|
||||||
|
_, err := auth.GetAuthCtx(c).PermitAdmin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(struct{ core.PageReq })
|
||||||
|
if err = g.Validator.ParseBody(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
list, total, err := q.LogsUserUsage.FindByPage(req.GetOffset(), req.GetLimit())
|
||||||
|
|
||||||
|
return c.JSON(core.PageResp{
|
||||||
|
List: list,
|
||||||
|
Total: int(total),
|
||||||
|
Page: req.GetPage(),
|
||||||
|
Size: req.GetSize(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,20 +3,40 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"platform/web/auth"
|
"platform/web/auth"
|
||||||
"platform/web/core"
|
"platform/web/core"
|
||||||
|
g "platform/web/globals"
|
||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// region ListBill
|
// PageBillByAdmin 分页查询全部账单
|
||||||
|
func PageBillByAdmin(c *fiber.Ctx) error {
|
||||||
|
// 检查权限
|
||||||
|
_, err := auth.GetAuthCtx(c).PermitAdmin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type ListBillReq struct {
|
// 解析请求参数
|
||||||
core.PageReq
|
req := new(core.PageReq)
|
||||||
BillNo *string `json:"bill_no"`
|
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||||
Type *int `json:"type"`
|
return err
|
||||||
CreateAfter *time.Time `json:"create_after"`
|
}
|
||||||
CreateBefore *time.Time `json:"create_before"`
|
|
||||||
|
// 查询用户列表
|
||||||
|
list, total, err := q.Bill.FindByPage(req.GetOffset(), req.GetLimit())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回结果
|
||||||
|
return c.JSON(core.PageResp{
|
||||||
|
List: list,
|
||||||
|
Total: int(total),
|
||||||
|
Page: req.GetPage(),
|
||||||
|
Size: req.GetSize(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListBill 获取账单列表
|
// ListBill 获取账单列表
|
||||||
@@ -79,4 +99,10 @@ func ListBill(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
type ListBillReq struct {
|
||||||
|
core.PageReq
|
||||||
|
BillNo *string `json:"bill_no"`
|
||||||
|
Type *int `json:"type"`
|
||||||
|
CreateAfter *time.Time `json:"create_after"`
|
||||||
|
CreateBefore *time.Time `json:"create_before"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,15 +14,36 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// region ListChannels
|
// PageChannelsByAdmin 分页查询所有通道
|
||||||
|
func PageChannelsByAdmin(c *fiber.Ctx) error {
|
||||||
|
// 检查权限
|
||||||
|
_, err := auth.GetAuthCtx(c).PermitAdmin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type ListChannelsReq struct {
|
// 解析请求参数
|
||||||
core.PageReq
|
req := new(core.PageReq)
|
||||||
AuthType s.ChannelAuthType `json:"auth_type"`
|
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||||
ExpireAfter *time.Time `json:"expire_after"`
|
return err
|
||||||
ExpireBefore *time.Time `json:"expire_before"`
|
}
|
||||||
|
|
||||||
|
// 查询通道列表
|
||||||
|
list, total, err := q.Channel.FindByPage(req.GetOffset(), req.GetLimit())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回结果
|
||||||
|
return c.JSON(core.PageResp{
|
||||||
|
List: list,
|
||||||
|
Total: int(total),
|
||||||
|
Page: req.GetPage(),
|
||||||
|
Size: req.GetSize(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分页查询当前用户通道
|
||||||
func ListChannels(c *fiber.Ctx) error {
|
func ListChannels(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
authContext, err := auth.GetAuthCtx(c).PermitUser()
|
authContext, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
@@ -57,7 +78,6 @@ func ListChannels(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// 查询数据
|
// 查询数据
|
||||||
channels, err := q.Channel.
|
channels, err := q.Channel.
|
||||||
Preload(q.Channel.Proxy).
|
|
||||||
Where(cond).
|
Where(cond).
|
||||||
Order(q.Channel.CreatedAt.Desc()).
|
Order(q.Channel.CreatedAt.Desc()).
|
||||||
Offset(req.GetOffset()).
|
Offset(req.GetOffset()).
|
||||||
@@ -87,28 +107,14 @@ func ListChannels(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
type ListChannelsReq struct {
|
||||||
|
core.PageReq
|
||||||
// region CreateChannel
|
AuthType s.ChannelAuthType `json:"auth_type"`
|
||||||
|
ExpireAfter *time.Time `json:"expire_after"`
|
||||||
type CreateChannelReq struct {
|
ExpireBefore *time.Time `json:"expire_before"`
|
||||||
ResourceId int32 `json:"resource_id" validate:"required"`
|
|
||||||
AuthType s.ChannelAuthType `json:"auth_type" validate:"required"`
|
|
||||||
Protocol int `json:"protocol" validate:"required"`
|
|
||||||
Count int `json:"count" validate:"required"`
|
|
||||||
Prov *string `json:"prov"`
|
|
||||||
City *string `json:"city"`
|
|
||||||
Isp *int `json:"isp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateChannelRespItem struct {
|
|
||||||
Proto int `json:"-"`
|
|
||||||
Host string `json:"host"`
|
|
||||||
Port uint16 `json:"port"`
|
|
||||||
Username *string `json:"username,omitempty"`
|
|
||||||
Password *string `json:"password,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建新通道
|
||||||
func CreateChannel(c *fiber.Ctx) error {
|
func CreateChannel(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// 解析参数
|
// 解析参数
|
||||||
@@ -144,7 +150,7 @@ func CreateChannel(c *fiber.Ctx) error {
|
|||||||
for i, channel := range result {
|
for i, channel := range result {
|
||||||
resp[i] = &CreateChannelRespItem{
|
resp[i] = &CreateChannelRespItem{
|
||||||
Proto: req.Protocol,
|
Proto: req.Protocol,
|
||||||
Host: channel.Proxy.IP.String(),
|
Host: channel.Host,
|
||||||
Port: channel.Port,
|
Port: channel.Port,
|
||||||
}
|
}
|
||||||
if req.AuthType == s.ChannelAuthTypePass {
|
if req.AuthType == s.ChannelAuthTypePass {
|
||||||
@@ -155,17 +161,25 @@ func CreateChannel(c *fiber.Ctx) error {
|
|||||||
return c.JSON(resp)
|
return c.JSON(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateChannelResultType string
|
type CreateChannelReq struct {
|
||||||
|
ResourceId int32 `json:"resource_id" validate:"required"`
|
||||||
// endregion
|
AuthType s.ChannelAuthType `json:"auth_type" validate:"required"`
|
||||||
|
Protocol int `json:"protocol" validate:"required"`
|
||||||
// region RemoveChannels
|
Count int `json:"count" validate:"required"`
|
||||||
|
Prov *string `json:"prov"`
|
||||||
type RemoveChannelsReq struct {
|
City *string `json:"city"`
|
||||||
Batch string `json:"batch" validate:"required"`
|
Isp *int `json:"isp"`
|
||||||
Ids []int32 `json:"ids" validate:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateChannelRespItem struct {
|
||||||
|
Proto int `json:"-"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port uint16 `json:"port"`
|
||||||
|
Username *string `json:"username,omitempty"`
|
||||||
|
Password *string `json:"password,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveChannels 删除通道
|
||||||
func RemoveChannels(c *fiber.Ctx) error {
|
func RemoveChannels(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
|
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
|
||||||
@@ -180,7 +194,7 @@ func RemoveChannels(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 删除通道
|
// 删除通道
|
||||||
err = s.Channel.RemoveChannels(req.Batch, req.Ids)
|
err = s.Channel.RemoveChannels(req.Batch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -188,4 +202,6 @@ func RemoveChannels(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
type RemoveChannelsReq struct {
|
||||||
|
Batch string `json:"batch" validate:"required"`
|
||||||
|
}
|
||||||
|
|||||||
48
web/handlers/inquiry.go
Normal file
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,11 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"platform/pkg/env"
|
"platform/pkg/env"
|
||||||
g "platform/web/globals"
|
"platform/web/auth"
|
||||||
|
"platform/web/core"
|
||||||
|
"platform/web/globals"
|
||||||
s "platform/web/services"
|
s "platform/web/services"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,37 +17,48 @@ func DebugRegisterProxyBaiYin(c *fiber.Ctx) error {
|
|||||||
return fiber.ErrNotFound
|
return fiber.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := g.Redis.SetNX(c.Context(), "debug:channel:register:127.0.0.1", true, 0).Result()
|
err := s.Proxy.RegisterBaiyin("1a:2b:3c:4d:5e:6f", netip.AddrFrom4([4]byte{127, 0, 0, 1}), "test", "test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return core.NewServErr("注册失败", err)
|
||||||
}
|
|
||||||
slog.Info("注册代理", "ok", ok)
|
|
||||||
if !ok {
|
|
||||||
return fiber.ErrConflict
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Proxy.RegisterBaiyin("1a:2b:3c:4d:5e:6f", netip.AddrFrom4([4]byte{127, 0, 0, 1}), "test", "test")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// region 报告上线
|
// 注册白银代理网关
|
||||||
|
func ProxyRegisterBaiYin(c *fiber.Ctx) error {
|
||||||
|
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type ProxyReportOnlineReq struct {
|
req := new(RegisterProxyBaiyinReq)
|
||||||
|
err = globals.Validator.ParseBody(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := netip.ParseAddr(req.IP)
|
||||||
|
if err != nil {
|
||||||
|
return core.NewServErr("IP地址格式错误", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Proxy.RegisterBaiyin(req.Name, addr, req.Username, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
return core.NewServErr("注册失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterProxyBaiyinReq struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Version int `json:"version" validate:"required"`
|
IP string `json:"ip" validate:"required"`
|
||||||
}
|
Username string `json:"username" validate:"required"`
|
||||||
|
Password string `json:"password" validate:"required"`
|
||||||
type ProxyReportOnlineResp struct {
|
|
||||||
Id int32 `json:"id"`
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
Permits []*ProxyPermit `json:"permits"`
|
|
||||||
Edges []*ProxyEdge `json:"edges"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// region 报告上线
|
||||||
func ProxyReportOnline(c *fiber.Ctx) (err error) {
|
func ProxyReportOnline(c *fiber.Ctx) (err error) {
|
||||||
return c.JSON(map[string]any{
|
return c.JSON(map[string]any{
|
||||||
"error": "接口暂不可用",
|
"error": "接口暂不可用",
|
||||||
@@ -150,12 +162,19 @@ func ProxyReportOnline(c *fiber.Ctx) (err error) {
|
|||||||
// })
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
// region 报告下线
|
type ProxyReportOnlineReq struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
type ProxyReportOfflineReq struct {
|
Version int `json:"version" validate:"required"`
|
||||||
Id int32 `json:"id" validate:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProxyReportOnlineResp struct {
|
||||||
|
Id int32 `json:"id"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
Permits []*ProxyPermit `json:"permits"`
|
||||||
|
Edges []*ProxyEdge `json:"edges"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// region 报告下线
|
||||||
func ProxyReportOffline(c *fiber.Ctx) (err error) {
|
func ProxyReportOffline(c *fiber.Ctx) (err error) {
|
||||||
return c.JSON(map[string]any{
|
return c.JSON(map[string]any{
|
||||||
"error": "接口暂不可用",
|
"error": "接口暂不可用",
|
||||||
@@ -193,13 +212,11 @@ func ProxyReportOffline(c *fiber.Ctx) (err error) {
|
|||||||
// return nil
|
// return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// region 报告更新
|
type ProxyReportOfflineReq struct {
|
||||||
|
|
||||||
type ProxyReportUpdateReq struct {
|
|
||||||
Id int32 `json:"id" validate:"required"`
|
Id int32 `json:"id" validate:"required"`
|
||||||
Edges []*ProxyEdge `json:"edges" validate:"required"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// region 报告更新
|
||||||
func ProxyReportUpdate(c *fiber.Ctx) (err error) {
|
func ProxyReportUpdate(c *fiber.Ctx) (err error) {
|
||||||
return c.JSON(map[string]any{
|
return c.JSON(map[string]any{
|
||||||
"error": "接口暂不可用",
|
"error": "接口暂不可用",
|
||||||
@@ -358,6 +375,11 @@ func ProxyReportUpdate(c *fiber.Ctx) (err error) {
|
|||||||
// return nil
|
// return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProxyReportUpdateReq struct {
|
||||||
|
Id int32 `json:"id" validate:"required"`
|
||||||
|
Edges []*ProxyEdge `json:"edges" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProxyPermit struct {
|
type ProxyPermit struct {
|
||||||
Id int32 `json:"id"`
|
Id int32 `json:"id"`
|
||||||
Expire time.Time `json:"expire"`
|
Expire time.Time `json:"expire"`
|
||||||
|
|||||||
@@ -15,18 +15,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListResourceShortReq struct {
|
// PageResourceShort 分页查询当前用户短效套餐
|
||||||
core.PageReq
|
func PageResourceShort(c *fiber.Ctx) error {
|
||||||
ResourceNo *string `json:"resource_no"`
|
|
||||||
Active *bool `json:"active"`
|
|
||||||
Type *int `json:"type"`
|
|
||||||
CreateAfter *time.Time `json:"create_after"`
|
|
||||||
CreateBefore *time.Time `json:"create_before"`
|
|
||||||
ExpireAfter *time.Time `json:"expire_after"`
|
|
||||||
ExpireBefore *time.Time `json:"expire_before"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListResourceShort(c *fiber.Ctx) error {
|
|
||||||
// 检查权限
|
// 检查权限
|
||||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -34,7 +24,7 @@ func ListResourceShort(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析请求参数
|
// 解析请求参数
|
||||||
req := new(ListResourceShortReq)
|
req := new(PageResourceShortReq)
|
||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -60,10 +50,10 @@ func ListResourceShort(c *fiber.Ctx) error {
|
|||||||
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
|
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
|
||||||
}
|
}
|
||||||
if req.ExpireAfter != nil {
|
if req.ExpireAfter != nil {
|
||||||
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Expire.Gte(*req.ExpireAfter))
|
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Gte(*req.ExpireAfter))
|
||||||
}
|
}
|
||||||
if req.ExpireBefore != nil {
|
if req.ExpireBefore != nil {
|
||||||
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Expire.Lte(*req.ExpireBefore))
|
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Lte(*req.ExpireBefore))
|
||||||
}
|
}
|
||||||
|
|
||||||
resource, err := q.Resource.Where(do).
|
resource, err := q.Resource.Where(do).
|
||||||
@@ -96,7 +86,7 @@ func ListResourceShort(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListResourceLongReq struct {
|
type PageResourceShortReq struct {
|
||||||
core.PageReq
|
core.PageReq
|
||||||
ResourceNo *string `json:"resource_no"`
|
ResourceNo *string `json:"resource_no"`
|
||||||
Active *bool `json:"active"`
|
Active *bool `json:"active"`
|
||||||
@@ -107,7 +97,8 @@ type ListResourceLongReq struct {
|
|||||||
ExpireBefore *time.Time `json:"expire_before"`
|
ExpireBefore *time.Time `json:"expire_before"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListResourceLong(c *fiber.Ctx) error {
|
// PageResourceLong 分页查询当前用户长效套餐
|
||||||
|
func PageResourceLong(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -115,7 +106,7 @@ func ListResourceLong(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析请求参数
|
// 解析请求参数
|
||||||
req := new(ListResourceLongReq)
|
req := new(PageResourceLongReq)
|
||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -141,10 +132,10 @@ func ListResourceLong(c *fiber.Ctx) error {
|
|||||||
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
|
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
|
||||||
}
|
}
|
||||||
if req.ExpireAfter != nil {
|
if req.ExpireAfter != nil {
|
||||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Expire.Gte(*req.ExpireAfter))
|
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Gte(*req.ExpireAfter))
|
||||||
}
|
}
|
||||||
if req.ExpireBefore != nil {
|
if req.ExpireBefore != nil {
|
||||||
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Expire.Lte(*req.ExpireBefore))
|
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Lte(*req.ExpireBefore))
|
||||||
}
|
}
|
||||||
|
|
||||||
resource, err := q.Resource.Where(do).
|
resource, err := q.Resource.Where(do).
|
||||||
@@ -177,9 +168,68 @@ func ListResourceLong(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type AllResourceReq struct {
|
type PageResourceLongReq struct {
|
||||||
|
core.PageReq
|
||||||
|
ResourceNo *string `json:"resource_no"`
|
||||||
|
Active *bool `json:"active"`
|
||||||
|
Type *int `json:"type"`
|
||||||
|
CreateAfter *time.Time `json:"create_after"`
|
||||||
|
CreateBefore *time.Time `json:"create_before"`
|
||||||
|
ExpireAfter *time.Time `json:"expire_after"`
|
||||||
|
ExpireBefore *time.Time `json:"expire_before"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageResourceShortByAdmin 分页查询全部短效套餐
|
||||||
|
func PageResourceShortByAdmin(c *fiber.Ctx) error {
|
||||||
|
_, err := auth.GetAuthCtx(c).PermitAdmin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(struct{ core.PageReq })
|
||||||
|
if err = g.Validator.ParseBody(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
list, total, err := q.Resource.
|
||||||
|
LeftJoin(q.ResourceShort, q.ResourceShort.ResourceID.EqCol(q.Resource.ID)).
|
||||||
|
Where(q.Resource.Type.Eq(int(m.ResourceTypeShort))).
|
||||||
|
FindByPage(req.GetOffset(), req.GetLimit())
|
||||||
|
|
||||||
|
return c.JSON(core.PageResp{
|
||||||
|
List: list,
|
||||||
|
Total: int(total),
|
||||||
|
Page: req.GetPage(),
|
||||||
|
Size: req.GetSize(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageResourceLongByAdmin 分页查询全部短效套餐
|
||||||
|
func PageResourceLongByAdmin(c *fiber.Ctx) error {
|
||||||
|
_, err := auth.GetAuthCtx(c).PermitAdmin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(struct{ core.PageReq })
|
||||||
|
if err = g.Validator.ParseBody(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
list, total, err := q.Resource.
|
||||||
|
LeftJoin(q.ResourceLong, q.ResourceLong.ResourceID.EqCol(q.Resource.ID)).
|
||||||
|
Where(q.Resource.Type.Eq(int(m.ResourceTypeLong))).
|
||||||
|
FindByPage(req.GetOffset(), req.GetLimit())
|
||||||
|
|
||||||
|
return c.JSON(core.PageResp{
|
||||||
|
List: list,
|
||||||
|
Total: int(total),
|
||||||
|
Page: req.GetPage(),
|
||||||
|
Size: req.GetSize(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllActiveResource 所有可用套餐
|
||||||
func AllActiveResource(c *fiber.Ctx) error {
|
func AllActiveResource(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
@@ -204,10 +254,10 @@ func AllActiveResource(c *fiber.Ctx) error {
|
|||||||
q.Resource.Type.Eq(int(m.ResourceTypeShort)),
|
q.Resource.Type.Eq(int(m.ResourceTypeShort)),
|
||||||
q.ResourceShort.As(q.Resource.Short.Name()).Where(
|
q.ResourceShort.As(q.Resource.Short.Name()).Where(
|
||||||
short.Type.Eq(int(m.ResourceModeTime)),
|
short.Type.Eq(int(m.ResourceModeTime)),
|
||||||
short.Expire.Gte(now),
|
short.ExpireAt.Gte(now),
|
||||||
q.ResourceShort.As(q.Resource.Short.Name()).
|
q.ResourceShort.As(q.Resource.Short.Name()).
|
||||||
Where(short.DailyLast.Lt(u.Today())).
|
Where(short.LastAt.Lt(u.Today())).
|
||||||
Or(short.DailyLimit.GtCol(short.DailyUsed)),
|
Or(short.Quota.GtCol(short.Daily)),
|
||||||
).Or(
|
).Or(
|
||||||
short.Type.Eq(int(m.ResourceModeQuota)),
|
short.Type.Eq(int(m.ResourceModeQuota)),
|
||||||
short.Quota.GtCol(short.Used),
|
short.Quota.GtCol(short.Used),
|
||||||
@@ -216,10 +266,10 @@ func AllActiveResource(c *fiber.Ctx) error {
|
|||||||
q.Resource.Type.Eq(int(m.ResourceTypeLong)),
|
q.Resource.Type.Eq(int(m.ResourceTypeLong)),
|
||||||
q.ResourceLong.As(q.Resource.Long.Name()).Where(
|
q.ResourceLong.As(q.Resource.Long.Name()).Where(
|
||||||
long.Type.Eq(int(m.ResourceModeTime)),
|
long.Type.Eq(int(m.ResourceModeTime)),
|
||||||
long.Expire.Gte(now),
|
long.ExpireAt.Gte(now),
|
||||||
q.ResourceLong.As(q.Resource.Long.Name()).
|
q.ResourceLong.As(q.Resource.Long.Name()).
|
||||||
Where(long.DailyLast.Lt(u.Today())).
|
Where(long.LastAt.Lt(u.Today())).
|
||||||
Or(long.DailyLimit.GtCol(long.DailyUsed)),
|
Or(long.Quota.GtCol(long.Daily)),
|
||||||
).Or(
|
).Or(
|
||||||
long.Type.Eq(int(m.ResourceModeQuota)),
|
long.Type.Eq(int(m.ResourceModeQuota)),
|
||||||
long.Quota.GtCol(long.Used),
|
long.Quota.GtCol(long.Used),
|
||||||
@@ -235,23 +285,10 @@ func AllActiveResource(c *fiber.Ctx) error {
|
|||||||
return c.JSON(resources)
|
return c.JSON(resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatisticPersonalResp struct {
|
type AllResourceReq struct {
|
||||||
Short StatisticShort `json:"short"`
|
|
||||||
Long StatisticLong `json:"long"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatisticShort struct {
|
|
||||||
ResourceCount int `json:"resource_count"`
|
|
||||||
ResourceQuotaSum int `json:"resource_quota_sum"`
|
|
||||||
ResourceDailyFreeSum int `json:"resource_daily_free_sum"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatisticLong struct {
|
|
||||||
ResourceCount int `json:"resource_count"`
|
|
||||||
ResourceQuotaSum int `json:"resource_quota_sum"`
|
|
||||||
ResourceDailyFreeSum int `json:"resource_daily_free_sum"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StatisticResourceFree 统计每日可用
|
||||||
func StatisticResourceFree(c *fiber.Ctx) error {
|
func StatisticResourceFree(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
@@ -282,39 +319,39 @@ func StatisticResourceFree(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// 短效包量
|
// 短效包量
|
||||||
case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeQuota:
|
case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeQuota:
|
||||||
if u.Z(resource.Short.Quota) > resource.Short.Used {
|
if resource.Short.Quota > resource.Short.Used {
|
||||||
shortCount++
|
shortCount++
|
||||||
shortQuotaSum += int(u.Z(resource.Short.Quota) - resource.Short.Used)
|
shortQuotaSum += int(resource.Short.Quota - resource.Short.Used)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 长效包量
|
// 长效包量
|
||||||
case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeQuota:
|
case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeQuota:
|
||||||
if u.Z(resource.Long.Quota) > resource.Long.Used {
|
if resource.Long.Quota > resource.Long.Used {
|
||||||
longCount++
|
longCount++
|
||||||
longQuotaSum += int(u.Z(resource.Long.Quota) - resource.Long.Used)
|
longQuotaSum += int(resource.Long.Quota - resource.Long.Used)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 短效包时
|
// 短效包时
|
||||||
case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeTime:
|
case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeTime:
|
||||||
if time.Time(*resource.Short.Expire).After(time.Now()) {
|
if time.Time(*resource.Short.ExpireAt).After(time.Now()) {
|
||||||
if resource.Short.DailyLast == nil || u.IsToday(time.Time(*resource.Short.DailyLast)) == false {
|
if resource.Short.LastAt == nil || u.IsToday(time.Time(*resource.Short.LastAt)) == false {
|
||||||
shortCount++
|
shortCount++
|
||||||
shortDailyFreeSum += int(resource.Short.DailyLimit)
|
shortDailyFreeSum += int(resource.Short.Quota)
|
||||||
} else if resource.Short.DailyLimit > resource.Short.DailyUsed {
|
} else if resource.Short.Quota > resource.Short.Daily {
|
||||||
shortCount++
|
shortCount++
|
||||||
shortDailyFreeSum += int(resource.Short.DailyLimit - resource.Short.DailyUsed)
|
shortDailyFreeSum += int(resource.Short.Quota - resource.Short.Daily)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 长效包时
|
// 长效包时
|
||||||
case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeTime:
|
case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeTime:
|
||||||
if time.Time(*resource.Long.Expire).After(time.Now()) {
|
if time.Time(*resource.Long.ExpireAt).After(time.Now()) {
|
||||||
if resource.Long.DailyLast == nil || u.IsToday(time.Time(*resource.Long.DailyLast)) == false {
|
if resource.Long.LastAt == nil || u.IsToday(time.Time(*resource.Long.LastAt)) == false {
|
||||||
longCount++
|
longCount++
|
||||||
longDailyFreeSum += int(resource.Long.DailyLimit)
|
longDailyFreeSum += int(resource.Long.Quota)
|
||||||
} else if resource.Long.DailyLimit > resource.Long.DailyUsed {
|
} else if resource.Long.Quota > resource.Long.Daily {
|
||||||
longCount++
|
longCount++
|
||||||
longDailyFreeSum += int(resource.Long.DailyLimit - resource.Long.DailyUsed)
|
longDailyFreeSum += int(resource.Long.Quota - resource.Long.Daily)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,17 +371,24 @@ func StatisticResourceFree(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatisticResourceUsageReq struct {
|
type StatisticPersonalResp struct {
|
||||||
ResourceNo *string `json:"resource_no"`
|
Short StatisticShort `json:"short"`
|
||||||
TimeAfter *time.Time `json:"time_after"`
|
Long StatisticLong `json:"long"`
|
||||||
TimeBefore *time.Time `json:"time_before"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatisticResourceUsageResp []struct {
|
type StatisticShort struct {
|
||||||
Date time.Time `json:"date"`
|
ResourceCount int `json:"resource_count"`
|
||||||
Count int `json:"count"`
|
ResourceQuotaSum int `json:"resource_quota_sum"`
|
||||||
|
ResourceDailyFreeSum int `json:"resource_daily_free_sum"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StatisticLong struct {
|
||||||
|
ResourceCount int `json:"resource_count"`
|
||||||
|
ResourceQuotaSum int `json:"resource_quota_sum"`
|
||||||
|
ResourceDailyFreeSum int `json:"resource_daily_free_sum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatisticResourceUsage 统计每日用量
|
||||||
func StatisticResourceUsage(c *fiber.Ctx) error {
|
func StatisticResourceUsage(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
@@ -359,21 +403,10 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 统计套餐提取数量
|
// 统计套餐提取数量
|
||||||
do := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(authCtx.User.ID))
|
do := q.LogsUserUsage.Where(
|
||||||
if req.ResourceNo != nil && *req.ResourceNo != "" {
|
q.LogsUserUsage.UserID.Eq(authCtx.User.ID),
|
||||||
var resourceID int32
|
)
|
||||||
err := q.Resource.
|
|
||||||
Where(
|
|
||||||
q.Resource.UserID.Eq(authCtx.User.ID),
|
|
||||||
q.Resource.ResourceNo.Eq(*req.ResourceNo),
|
|
||||||
).
|
|
||||||
Select(q.Resource.ID).
|
|
||||||
Scan(&resourceID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
do.Where(q.LogsUserUsage.ResourceID.Eq(resourceID))
|
|
||||||
}
|
|
||||||
if req.TimeAfter != nil {
|
if req.TimeAfter != nil {
|
||||||
do.Where(q.LogsUserUsage.Time.Gte(*req.TimeAfter))
|
do.Where(q.LogsUserUsage.Time.Gte(*req.TimeAfter))
|
||||||
}
|
}
|
||||||
@@ -389,7 +422,7 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
|
|||||||
).
|
).
|
||||||
Where(do).
|
Where(do).
|
||||||
Group(
|
Group(
|
||||||
field.NewUnsafeFieldRaw("date_trunc('day', time)"),
|
field.NewField("", "date"),
|
||||||
).
|
).
|
||||||
Order(
|
Order(
|
||||||
field.NewField("", "date").Desc(),
|
field.NewField("", "date").Desc(),
|
||||||
@@ -402,10 +435,17 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
|
|||||||
return c.JSON(data)
|
return c.JSON(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateResourceReq struct {
|
type StatisticResourceUsageReq struct {
|
||||||
*s.CreateResourceData
|
TimeAfter *time.Time `json:"time_start"`
|
||||||
|
TimeBefore *time.Time `json:"time_end"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StatisticResourceUsageResp []struct {
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateResource 创建套餐
|
||||||
func CreateResource(c *fiber.Ctx) error {
|
func CreateResource(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// 检查权限
|
// 检查权限
|
||||||
@@ -429,6 +469,11 @@ func CreateResource(c *fiber.Ctx) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateResourceReq struct {
|
||||||
|
*s.CreateResourceData
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourcePrice 套餐价格
|
||||||
func ResourcePrice(c *fiber.Ctx) error {
|
func ResourcePrice(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
_, err := auth.GetAuthCtx(c).PermitSecretClient()
|
_, err := auth.GetAuthCtx(c).PermitSecretClient()
|
||||||
@@ -439,11 +484,25 @@ func ResourcePrice(c *fiber.Ctx) error {
|
|||||||
// 解析请求参数
|
// 解析请求参数
|
||||||
var req = new(CreateResourceReq)
|
var req = new(CreateResourceReq)
|
||||||
if err := g.Validator.ParseBody(c, req); err != nil {
|
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||||
return err
|
return core.NewBizErr("接口参数解析异常", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取套餐价格
|
// 获取套餐价格
|
||||||
return c.JSON(fiber.Map{
|
amount, err := req.GetAmount()
|
||||||
"price": req.GetAmount().StringFixed(2),
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算折扣
|
||||||
|
return c.JSON(ResourcePriceResp{
|
||||||
|
Price: amount.StringFixed(2),
|
||||||
|
Discounted: 1,
|
||||||
|
DiscountedPrice: amount.StringFixed(2),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResourcePriceResp struct {
|
||||||
|
Price string `json:"price"`
|
||||||
|
Discounted float32 `json:"discounted"`
|
||||||
|
DiscountedPrice string `json:"discounted_price"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,26 +9,44 @@ import (
|
|||||||
"platform/web/core"
|
"platform/web/core"
|
||||||
g "platform/web/globals"
|
g "platform/web/globals"
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
|
q "platform/web/queries"
|
||||||
s "platform/web/services"
|
s "platform/web/services"
|
||||||
"reflect"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TradeCreateReq struct {
|
// PageTradeByAdmin 分页查询所有订单
|
||||||
s.CreateTradeData
|
func PageTradeByAdmin(c *fiber.Ctx) error {
|
||||||
Type m.TradeType `json:"type" validate:"required"`
|
// 检查权限
|
||||||
Resource *s.CreateResourceData `json:"resource,omitempty"`
|
_, err := auth.GetAuthCtx(c).PermitAdmin()
|
||||||
Recharge *s.RechargeProductInfo `json:"recharge,omitempty"`
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
type TradeCreateResp struct {
|
|
||||||
PayUrl string `json:"pay_url"`
|
// 解析请求参数
|
||||||
TradeNo string `json:"trade_no"`
|
req := new(core.PageReq)
|
||||||
|
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用户列表
|
||||||
|
list, total, err := q.Trade.FindByPage(req.GetOffset(), req.GetLimit())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回结果
|
||||||
|
return c.JSON(core.PageResp{
|
||||||
|
List: list,
|
||||||
|
Total: int(total),
|
||||||
|
Page: req.GetPage(),
|
||||||
|
Size: req.GetSize(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
func TradeCreate(c *fiber.Ctx) error {
|
func TradeCreate(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
@@ -68,10 +86,19 @@ func TradeCreate(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type TradeCompleteReq struct {
|
type TradeCreateReq struct {
|
||||||
s.ModifyTradeData
|
s.CreateTradeData
|
||||||
|
Type m.TradeType `json:"type" validate:"required"`
|
||||||
|
Resource *s.CreateResourceData `json:"resource,omitempty"`
|
||||||
|
Recharge *s.RechargeProductInfo `json:"recharge,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TradeCreateResp struct {
|
||||||
|
PayUrl string `json:"pay_url"`
|
||||||
|
TradeNo string `json:"trade_no"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完成订单
|
||||||
func TradeComplete(c *fiber.Ctx) error {
|
func TradeComplete(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
_, err := auth.GetAuthCtx(c).PermitUser()
|
_, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
@@ -94,10 +121,11 @@ func TradeComplete(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(fiber.StatusNoContent)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TradeCancelReq struct {
|
type TradeCompleteReq struct {
|
||||||
s.ModifyTradeData
|
s.ModifyTradeData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 取消订单
|
||||||
func TradeCancel(c *fiber.Ctx) error {
|
func TradeCancel(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
_, err := auth.GetAuthCtx(c).PermitUser()
|
_, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
@@ -121,10 +149,11 @@ func TradeCancel(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(fiber.StatusNoContent)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TradeCheckReq struct {
|
type TradeCancelReq struct {
|
||||||
s.ModifyTradeData
|
s.ModifyTradeData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查订单
|
||||||
func TradeCheck(c *fiber.Ctx) error {
|
func TradeCheck(c *fiber.Ctx) error {
|
||||||
// 解析请求参数
|
// 解析请求参数
|
||||||
req := new(TradeCheckReq)
|
req := new(TradeCheckReq)
|
||||||
@@ -154,9 +183,9 @@ func TradeCheck(c *fiber.Ctx) error {
|
|||||||
slog.Error("写入订单状态失败", "trade_no", req.TradeNo, "error", err)
|
slog.Error("写入订单状态失败", "trade_no", req.TradeNo, "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = w.Flush()
|
err = w.Flush()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("刷新缓冲区失败", "trade_no", req.TradeNo, "error", err, "errType", reflect.TypeOf(err))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,3 +200,7 @@ func TradeCheck(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TradeCheckReq struct {
|
||||||
|
s.ModifyTradeData
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"platform/web/auth"
|
"platform/web/auth"
|
||||||
|
"platform/web/core"
|
||||||
|
g "platform/web/globals"
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
s "platform/web/services"
|
s "platform/web/services"
|
||||||
@@ -10,15 +12,81 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// region /update
|
// 分页获取用户
|
||||||
|
func PageUserByAdmin(c *fiber.Ctx) error {
|
||||||
|
// 检查权限
|
||||||
|
_, err := auth.GetAuthCtx(c).PermitAdmin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateUserReq struct {
|
// 解析请求参数
|
||||||
Username string `json:"username" validate:"omitempty,min=3,max=20"`
|
req := new(core.PageReq)
|
||||||
Email string `json:"email" validate:"omitempty,email"`
|
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||||
ContactQQ string `json:"contact_qq" validate:"omitempty,qq"`
|
return err
|
||||||
ContactWechat string `json:"contact_wechat" validate:"omitempty,wechat"`
|
}
|
||||||
|
|
||||||
|
// 查询用户列表
|
||||||
|
users, total, err := q.User.
|
||||||
|
Preload(q.User.Admin).
|
||||||
|
Omit(q.User.Password).
|
||||||
|
FindByPage(req.GetOffset(), req.GetLimit())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Admin != nil {
|
||||||
|
user.Admin = &m.Admin{
|
||||||
|
Name: user.Admin.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回结果
|
||||||
|
return c.JSON(core.PageResp{
|
||||||
|
Total: int(total),
|
||||||
|
Page: req.GetPage(),
|
||||||
|
Size: req.GetSize(),
|
||||||
|
List: users,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 绑定管理员
|
||||||
|
func BindAdmin(c *fiber.Ctx) error {
|
||||||
|
// 检查权限
|
||||||
|
authCtx, err := auth.GetAuthCtx(c).PermitAdmin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析请求参数
|
||||||
|
req := new(struct {
|
||||||
|
UserID int `json:"user_id" validate:"required"`
|
||||||
|
})
|
||||||
|
if err := g.Validator.ParseBody(c, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
result, err := q.User.Where(
|
||||||
|
q.User.ID.Eq(int32(req.UserID)),
|
||||||
|
q.User.AdminID.IsNull(),
|
||||||
|
).UpdateColumnSimple(
|
||||||
|
q.User.AdminID.Value(authCtx.Admin.ID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return core.NewBizErr("用户已绑定管理员")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回结果
|
||||||
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户
|
||||||
func UpdateUser(c *fiber.Ctx) error {
|
func UpdateUser(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
@@ -49,15 +117,14 @@ func UpdateUser(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(fiber.StatusNoContent)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
type UpdateUserReq struct {
|
||||||
|
|
||||||
// region /update/account
|
|
||||||
|
|
||||||
type UpdateAccountReq struct {
|
|
||||||
Username string `json:"username" validate:"omitempty,min=3,max=20"`
|
Username string `json:"username" validate:"omitempty,min=3,max=20"`
|
||||||
Password string `json:"password" validate:"omitempty,min=6,max=20"`
|
Email string `json:"email" validate:"omitempty,email"`
|
||||||
|
ContactQQ string `json:"contact_qq" validate:"omitempty,qq"`
|
||||||
|
ContactWechat string `json:"contact_wechat" validate:"omitempty,wechat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新账号信息
|
||||||
func UpdateAccount(c *fiber.Ctx) error {
|
func UpdateAccount(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
@@ -86,16 +153,12 @@ func UpdateAccount(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(fiber.StatusNoContent)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
type UpdateAccountReq struct {
|
||||||
|
Username string `json:"username" validate:"omitempty,min=3,max=20"`
|
||||||
// region /update/password
|
Password string `json:"password" validate:"omitempty,min=6,max=20"`
|
||||||
|
|
||||||
type UpdatePasswordReq struct {
|
|
||||||
Phone string `json:"phone"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新账号密码
|
||||||
func UpdatePassword(c *fiber.Ctx) error {
|
func UpdatePassword(c *fiber.Ctx) error {
|
||||||
// 检查权限
|
// 检查权限
|
||||||
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
authCtx, err := auth.GetAuthCtx(c).PermitUser()
|
||||||
@@ -109,8 +172,13 @@ func UpdatePassword(c *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证手机号
|
||||||
|
if req.Phone != authCtx.User.Phone {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "手机号码不正确")
|
||||||
|
}
|
||||||
|
|
||||||
// 验证手机令牌
|
// 验证手机令牌
|
||||||
if req.Phone == "" || req.Code == "" {
|
if req.Code == "" {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "手机号码和验证码不能为空")
|
return fiber.NewError(fiber.StatusBadRequest, "手机号码和验证码不能为空")
|
||||||
}
|
}
|
||||||
err = s.Verifier.VerifySms(c.Context(), req.Phone, req.Code)
|
err = s.Verifier.VerifySms(c.Context(), req.Phone, req.Code)
|
||||||
@@ -135,4 +203,8 @@ func UpdatePassword(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(fiber.StatusNoContent)
|
return c.SendStatus(fiber.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
type UpdatePasswordReq struct {
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ func ApplyMiddlewares(app *fiber.App) {
|
|||||||
EnableStackTrace: true,
|
EnableStackTrace: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// cors
|
||||||
|
app.Use(cors.New(cors.Config{
|
||||||
|
AllowCredentials: true,
|
||||||
|
AllowOriginsFunc: func(origin string) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
// logger
|
// logger
|
||||||
app.Use(logger.New(logger.Config{
|
app.Use(logger.New(logger.Config{
|
||||||
Next: func(c *fiber.Ctx) bool {
|
Next: func(c *fiber.Ctx) bool {
|
||||||
@@ -38,9 +46,6 @@ func ApplyMiddlewares(app *fiber.App) {
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// cors
|
|
||||||
app.Use(cors.New())
|
|
||||||
|
|
||||||
// authenticate
|
// authenticate
|
||||||
app.Use(auth.Authenticate())
|
app.Use(auth.Authenticate())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ type Admin struct {
|
|||||||
core.Model
|
core.Model
|
||||||
Username string `json:"username" gorm:"column:username"` // 用户名
|
Username string `json:"username" gorm:"column:username"` // 用户名
|
||||||
Password string `json:"password" gorm:"column:password"` // 密码
|
Password string `json:"password" gorm:"column:password"` // 密码
|
||||||
Name *string `json:"name" gorm:"column:name"` // 真实姓名
|
Name *string `json:"name,omitempty" gorm:"column:name"` // 真实姓名
|
||||||
Avatar *string `json:"avatar" gorm:"column:avatar"` // 头像URL
|
Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL
|
||||||
Phone *string `json:"phone" gorm:"column:phone"` // 手机号码
|
Phone *string `json:"phone,omitempty" gorm:"column:phone"` // 手机号码
|
||||||
Email *string `json:"email" gorm:"column:email"` // 邮箱
|
Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱
|
||||||
Status AdminStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
Status AdminStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
||||||
LastLogin *time.Time `json:"last_login" gorm:"column:last_login"` // 最后登录时间
|
LastLogin *time.Time `json:"last_login,omitempty" gorm:"column:last_login"` // 最后登录时间
|
||||||
LastLoginIP *orm.Inet `json:"last_login_ip" gorm:"column:last_login_ip"` // 最后登录地址
|
LastLoginIP *orm.Inet `json:"last_login_ip,omitempty" gorm:"column:last_login_ip"` // 最后登录地址
|
||||||
LastLoginUA *string `json:"last_login_ua" gorm:"column:last_login_ua"` // 最后登录代理
|
LastLoginUA *string `json:"last_login_ua,omitempty" gorm:"column:last_login_ua"` // 最后登录代理
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminStatus 管理员状态枚举
|
// AdminStatus 管理员状态枚举
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
type AdminRole struct {
|
type AdminRole struct {
|
||||||
core.Model
|
core.Model
|
||||||
Name string `json:"name" gorm:"column:name"` // 角色名称
|
Name string `json:"name" gorm:"column:name"` // 角色名称
|
||||||
Description *string `json:"description" gorm:"column:description"` // 角色描述
|
Description *string `json:"description,omitempty" gorm:"column:description"` // 角色描述
|
||||||
Active bool `json:"active" gorm:"column:active"` // 是否激活
|
Active bool `json:"active" gorm:"column:active"` // 是否激活
|
||||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
type Announcement struct {
|
type Announcement struct {
|
||||||
core.Model
|
core.Model
|
||||||
Title string `json:"title" gorm:"column:title"` // 公告标题
|
Title string `json:"title" gorm:"column:title"` // 公告标题
|
||||||
Content *string `json:"content" gorm:"column:content"` // 公告内容
|
Content *string `json:"content,omitempty" gorm:"column:content"` // 公告内容
|
||||||
Type AnnouncementType `json:"type" gorm:"column:type"` // 公告类型:1-普通公告
|
Type AnnouncementType `json:"type" gorm:"column:type"` // 公告类型:1-普通公告
|
||||||
Pin bool `json:"pin" gorm:"column:pin"` // 是否置顶
|
Pin bool `json:"pin" gorm:"column:pin"` // 是否置顶
|
||||||
Status AnnouncementStatus `json:"status" gorm:"column:status"` // 公告状态:0-禁用,1-正常
|
Status AnnouncementStatus `json:"status" gorm:"column:status"` // 公告状态:0-禁用,1-正常
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ import (
|
|||||||
type Bill struct {
|
type Bill struct {
|
||||||
core.Model
|
core.Model
|
||||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||||
TradeID *int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID
|
TradeID *int32 `json:"trade_id,omitempty" gorm:"column:trade_id"` // 订单ID
|
||||||
ResourceID *int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
ResourceID *int32 `json:"resource_id,omitempty" gorm:"column:resource_id"` // 套餐ID
|
||||||
RefundID *int32 `json:"refund_id" gorm:"column:refund_id"` // 退款ID
|
RefundID *int32 `json:"refund_id,omitempty" gorm:"column:refund_id"` // 退款ID
|
||||||
BillNo string `json:"bill_no" gorm:"column:bill_no"` // 易读账单号
|
BillNo string `json:"bill_no" gorm:"column:bill_no"` // 易读账单号
|
||||||
Info *string `json:"info" gorm:"column:info"` // 产品可读信息
|
Info *string `json:"info,omitempty" gorm:"column:info"` // 产品可读信息
|
||||||
Type BillType `json:"type" gorm:"column:type"` // 账单类型:1-消费,2-退款,3-充值
|
Type BillType `json:"type" gorm:"column:type"` // 账单类型:1-消费,2-退款,3-充值
|
||||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 账单金额
|
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 账单金额
|
||||||
|
|
||||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
Trade *Trade `json:"trade" gorm:"foreignKey:TradeID"`
|
Trade *Trade `json:"trade,omitempty" gorm:"foreignKey:TradeID"`
|
||||||
Resource *Resource `json:"resource" gorm:"foreignKey:ResourceID"`
|
Resource *Resource `json:"resource,omitempty" gorm:"foreignKey:ResourceID"`
|
||||||
Refund *Refund `json:"refund" gorm:"foreignKey:RefundID"`
|
Refund *Refund `json:"refund,omitempty" gorm:"foreignKey:RefundID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BillType 账单类型枚举
|
// BillType 账单类型枚举
|
||||||
|
|||||||
@@ -11,21 +11,23 @@ type Channel struct {
|
|||||||
core.Model
|
core.Model
|
||||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||||
ProxyID int32 `json:"proxy_id" gorm:"column:proxy_id"` // 代理ID
|
|
||||||
BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号
|
BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号
|
||||||
|
ProxyID int32 `json:"proxy_id" gorm:"column:proxy_id"` // 代理ID
|
||||||
|
Host string `json:"host" gorm:"column:host"` // 代理主机
|
||||||
Port uint16 `json:"port" gorm:"column:port"` // 代理端口
|
Port uint16 `json:"port" gorm:"column:port"` // 代理端口
|
||||||
EdgeID *int32 `json:"edge_id" gorm:"column:edge_id"` // 节点ID(手动配置)
|
EdgeID *int32 `json:"edge_id,omitempty" gorm:"column:edge_id"` // 节点ID(手动配置)
|
||||||
FilterISP *EdgeISP `json:"filter_isp" gorm:"column:filter_isp"` // 运营商过滤(自动配置):参考 edge.isp
|
EdgeRef *string `json:"edge_ref,omitempty" gorm:"column:edge_ref"` // 外部节点引用,用于索引没有ID的外部非受控节点
|
||||||
FilterProv *string `json:"filter_prov" gorm:"column:filter_prov"` // 省份过滤(自动配置)
|
FilterISP *EdgeISP `json:"filter_isp,omitempty" gorm:"column:filter_isp"` // 运营商过滤(自动配置):参考 edge.isp
|
||||||
FilterCity *string `json:"filter_city" gorm:"column:filter_city"` // 城市过滤(自动配置)
|
FilterProv *string `json:"filter_prov,omitempty" gorm:"column:filter_prov"` // 省份过滤(自动配置)
|
||||||
IP *orm.Inet `json:"ip" gorm:"column:ip"` // 节点地址
|
FilterCity *string `json:"filter_city,omitempty" gorm:"column:filter_city"` // 城市过滤(自动配置)
|
||||||
Whitelists *string `json:"whitelists" gorm:"column:whitelists"` // IP白名单,逗号分隔
|
IP *orm.Inet `json:"ip,omitempty" gorm:"column:ip"` // 节点地址
|
||||||
Username *string `json:"username" gorm:"column:username"` // 用户名
|
Whitelists *string `json:"whitelists,omitempty" gorm:"column:whitelists"` // IP白名单,逗号分隔
|
||||||
Password *string `json:"password" gorm:"column:password"` // 密码
|
Username *string `json:"username,omitempty" gorm:"column:username"` // 用户名
|
||||||
|
Password *string `json:"password,omitempty" gorm:"column:password"` // 密码
|
||||||
ExpiredAt time.Time `json:"expired_at" gorm:"column:expired_at"` // 过期时间
|
ExpiredAt time.Time `json:"expired_at" gorm:"column:expired_at"` // 过期时间
|
||||||
|
|
||||||
User User `json:"user" gorm:"foreignKey:UserID"`
|
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
Resource Resource `json:"resource" gorm:"foreignKey:ResourceID"`
|
Resource *Resource `json:"resource,omitempty" gorm:"foreignKey:ResourceID"`
|
||||||
Proxy Proxy `json:"proxy" gorm:"foreignKey:ProxyID"`
|
Proxy *Proxy `json:"proxy,omitempty" gorm:"foreignKey:ProxyID"`
|
||||||
Edge *Edge `json:"edge" gorm:"foreignKey:EdgeID"`
|
Edge *Edge `json:"edge,omitempty" gorm:"foreignKey:EdgeID"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ type Client struct {
|
|||||||
core.Model
|
core.Model
|
||||||
ClientID string `json:"client_id" gorm:"column:client_id"` // OAuth2客户端标识符
|
ClientID string `json:"client_id" gorm:"column:client_id"` // OAuth2客户端标识符
|
||||||
ClientSecret string `json:"client_secret" gorm:"column:client_secret"` // OAuth2客户端密钥
|
ClientSecret string `json:"client_secret" gorm:"column:client_secret"` // OAuth2客户端密钥
|
||||||
RedirectURI *string `json:"redirect_uri" gorm:"column:redirect_uri"` // OAuth2 重定向URI
|
RedirectURI *string `json:"redirect_uri,omitempty" gorm:"column:redirect_uri"` // OAuth2 重定向URI
|
||||||
Spec ClientSpec `json:"spec" gorm:"column:spec"` // 安全规范:1-native,2-browser,3-web,4-api
|
Spec ClientSpec `json:"spec" gorm:"column:spec"` // 安全规范:1-native,2-browser,3-web,4-api
|
||||||
Name string `json:"name" gorm:"column:name"` // 名称
|
Name string `json:"name" gorm:"column:name"` // 名称
|
||||||
Icon *string `json:"icon" gorm:"column:icon"` // 图标URL
|
Icon *string `json:"icon,omitempty" gorm:"column:icon"` // 图标URL
|
||||||
Status ClientStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
Status ClientStatus `json:"status" gorm:"column:status"` // 状态:0-禁用,1-正常
|
||||||
Type ClientType `json:"type" gorm:"column:type"` // 类型:0-普通,1-官方
|
Type ClientType `json:"type" gorm:"column:type"` // 类型:0-普通,1-官方
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import (
|
|||||||
// Coupon 优惠券表
|
// Coupon 优惠券表
|
||||||
type Coupon struct {
|
type Coupon struct {
|
||||||
core.Model
|
core.Model
|
||||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
|
||||||
Code string `json:"code" gorm:"column:code"` // 优惠券代码
|
Code string `json:"code" gorm:"column:code"` // 优惠券代码
|
||||||
Remark *string `json:"remark" gorm:"column:remark"` // 优惠券备注
|
Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 优惠券备注
|
||||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 优惠券金额
|
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 优惠券金额
|
||||||
MinAmount decimal.Decimal `json:"min_amount" gorm:"column:min_amount"` // 最低消费金额
|
MinAmount decimal.Decimal `json:"min_amount" gorm:"column:min_amount"` // 最低消费金额
|
||||||
Status CouponStatus `json:"status" gorm:"column:status"` // 优惠券状态:0-未使用,1-已使用,2-已过期
|
Status CouponStatus `json:"status" gorm:"column:status"` // 优惠券状态:0-未使用,1-已使用,2-已过期
|
||||||
ExpireAt *time.Time `json:"expire_at" gorm:"column:expire_at"` // 过期时间
|
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 过期时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// CouponStatus 优惠券状态枚举
|
// CouponStatus 优惠券状态枚举
|
||||||
|
|||||||
25
web/models/inquiry.go
Normal file
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"` // 授权类型
|
GrantType GrantType `json:"grant_type" gorm:"column:grant_type"` // 授权类型
|
||||||
PasswordType PasswordType `json:"password_type" gorm:"column:password_type"` // 密码模式子授权类型
|
PasswordType PasswordType `json:"password_type" gorm:"column:password_type"` // 密码模式子授权类型
|
||||||
Success bool `json:"success" gorm:"column:success"` // 登录是否成功
|
Success bool `json:"success" gorm:"column:success"` // 登录是否成功
|
||||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
|
||||||
Time time.Time `json:"time" gorm:"column:time"` // 登录时间
|
Time time.Time `json:"time" gorm:"column:time"` // 登录时间
|
||||||
|
|
||||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GrantType 授权类型枚举
|
// GrantType 授权类型枚举
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ type LogsRequest struct {
|
|||||||
ID int32 `json:"id" gorm:"column:id"` // 访问日志ID
|
ID int32 `json:"id" gorm:"column:id"` // 访问日志ID
|
||||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
|
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
|
||||||
UA string `json:"ua" gorm:"column:ua"` // 用户代理
|
UA string `json:"ua" gorm:"column:ua"` // 用户代理
|
||||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
|
||||||
ClientID *int32 `json:"client_id" gorm:"column:client_id"` // 客户端ID
|
ClientID *int32 `json:"client_id,omitempty" gorm:"column:client_id"` // 客户端ID
|
||||||
Method string `json:"method" gorm:"column:method"` // 请求方法
|
Method string `json:"method" gorm:"column:method"` // 请求方法
|
||||||
Path string `json:"path" gorm:"column:path"` // 请求路径
|
Path string `json:"path" gorm:"column:path"` // 请求路径
|
||||||
Status int16 `json:"status" gorm:"column:status"` // 响应状态码
|
Status int16 `json:"status" gorm:"column:status"` // 响应状态码
|
||||||
Error *string `json:"error" gorm:"column:error"` // 错误信息
|
Error *string `json:"error,omitempty" gorm:"column:error"` // 错误信息
|
||||||
Time time.Time `json:"time" gorm:"column:time"` // 请求时间
|
Time time.Time `json:"time" gorm:"column:time"` // 请求时间
|
||||||
Latency string `json:"latency" gorm:"column:latency"` // 请求延迟
|
Latency string `json:"latency" gorm:"column:latency"` // 请求延迟
|
||||||
|
|
||||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
Client *Client `json:"client" gorm:"foreignKey:ClientID"`
|
Client *Client `json:"client,omitempty" gorm:"foreignKey:ClientID"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ type LogsUserUsage struct {
|
|||||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||||
BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号
|
BatchNo string `json:"batch_no" gorm:"column:batch_no"` // 批次编号
|
||||||
Count int32 `json:"count" gorm:"column:count"` // 数量
|
Count int32 `json:"count" gorm:"column:count"` // 数量
|
||||||
Prov *string `json:"prov" gorm:"column:prov"` // 省份
|
Prov *string `json:"prov,omitempty" gorm:"column:prov"` // 省份
|
||||||
City *string `json:"city" gorm:"column:city"` // 城市
|
City *string `json:"city,omitempty" gorm:"column:city"` // 城市
|
||||||
ISP *string `json:"isp" gorm:"column:isp"` // 运营商
|
ISP *string `json:"isp,omitempty" gorm:"column:isp"` // 运营商
|
||||||
IP orm.Inet `json:"ip" gorm:"column:ip"` // IP地址
|
IP orm.Inet `json:"ip" gorm:"column:ip"` // IP地址
|
||||||
Time time.Time `json:"time" gorm:"column:time"` // 提取时间
|
Time time.Time `json:"time" gorm:"column:time"` // 提取时间
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import "platform/web/core"
|
|||||||
// Permission 权限表
|
// Permission 权限表
|
||||||
type Permission struct {
|
type Permission struct {
|
||||||
core.Model
|
core.Model
|
||||||
ParentID *int32 `json:"parent_id" gorm:"column:parent_id"` // 父权限ID
|
ParentID *int32 `json:"parent_id,omitempty" gorm:"column:parent_id"` // 父权限ID
|
||||||
Name string `json:"name" gorm:"column:name"` // 权限名称
|
Name string `json:"name" gorm:"column:name"` // 权限名称
|
||||||
Description *string `json:"description" gorm:"column:description"` // 权限描述
|
Description *string `json:"description,omitempty" gorm:"column:description"` // 权限描述
|
||||||
|
|
||||||
Parent *Permission `json:"parent" gorm:"foreignKey:ParentID"`
|
Parent *Permission `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
|
||||||
Children []*Permission `json:"children" gorm:"foreignKey:ParentID"`
|
Children []*Permission `json:"children,omitempty" gorm:"foreignKey:ParentID"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type Product struct {
|
|||||||
core.Model
|
core.Model
|
||||||
Code string `json:"code" gorm:"column:code"` // 产品代码
|
Code string `json:"code" gorm:"column:code"` // 产品代码
|
||||||
Name string `json:"name" gorm:"column:name"` // 产品名称
|
Name string `json:"name" gorm:"column:name"` // 产品名称
|
||||||
Description *string `json:"description" gorm:"column:description"` // 产品描述
|
Description *string `json:"description,omitempty" gorm:"column:description"` // 产品描述
|
||||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||||
Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态:0-禁用,1-正常
|
Status ProductStatus `json:"status" gorm:"column:status"` // 产品状态:0-禁用,1-正常
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ type Proxy struct {
|
|||||||
Version int32 `json:"version" gorm:"column:version"` // 代理服务版本
|
Version int32 `json:"version" gorm:"column:version"` // 代理服务版本
|
||||||
Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称
|
Mac string `json:"mac" gorm:"column:mac"` // 代理服务名称
|
||||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址
|
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // 代理服务地址
|
||||||
Secret *string `json:"secret" gorm:"column:secret"` // 代理服务密钥
|
Host *string `json:"host,omitempty" gorm:"column:host"` // 代理服务域名
|
||||||
|
Secret *string `json:"secret,omitempty" gorm:"column:secret"` // 代理服务密钥
|
||||||
Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型:1-自有,2-白银
|
Type ProxyType `json:"type" gorm:"column:type"` // 代理服务类型:1-自有,2-白银
|
||||||
Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态:0-离线,1-在线
|
Status ProxyStatus `json:"status" gorm:"column:status"` // 代理服务状态:0-离线,1-在线
|
||||||
Meta *datatypes.JSONType[any] `json:"meta" gorm:"column:meta"` // 代理服务元信息
|
Meta *datatypes.JSONType[any] `json:"meta,omitempty" gorm:"column:meta"` // 代理服务元信息
|
||||||
|
|
||||||
Channels []Channel `json:"channels" gorm:"foreignkey:ProxyID"`
|
Channels []Channel `json:"channels,omitempty" gorm:"foreignkey:ProxyID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyType 代理服务类型枚举
|
// ProxyType 代理服务类型枚举
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
type Refund struct {
|
type Refund struct {
|
||||||
core.Model
|
core.Model
|
||||||
TradeID int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID
|
TradeID int32 `json:"trade_id" gorm:"column:trade_id"` // 订单ID
|
||||||
ProductID *int32 `json:"product_id" gorm:"column:product_id"` // 产品ID
|
ProductID *int32 `json:"product_id,omitempty" gorm:"column:product_id"` // 产品ID
|
||||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 退款金额
|
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 退款金额
|
||||||
Reason *string `json:"reason" gorm:"column:reason"` // 退款原因
|
Reason *string `json:"reason,omitempty" gorm:"column:reason"` // 退款原因
|
||||||
Status RefundStatus `json:"status" gorm:"column:status"` // 退款状态:0-待处理,1-已退款,2-已拒绝
|
Status RefundStatus `json:"status" gorm:"column:status"` // 退款状态:0-待处理,1-已退款,2-已拒绝
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import (
|
|||||||
type Resource struct {
|
type Resource struct {
|
||||||
core.Model
|
core.Model
|
||||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||||
ResourceNo *string `json:"resource_no" gorm:"column:resource_no"` // 套餐编号
|
ResourceNo *string `json:"resource_no,omitempty" gorm:"column:resource_no"` // 套餐编号
|
||||||
Active bool `json:"active" gorm:"column:active"` // 套餐状态
|
Active bool `json:"active" gorm:"column:active"` // 套餐状态
|
||||||
Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型:1-短效动态,2-长效动态
|
Type ResourceType `json:"type" gorm:"column:type"` // 套餐类型:1-短效动态,2-长效动态
|
||||||
|
|
||||||
User User `json:"user" gorm:"foreignKey:UserID"`
|
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
Short *ResourceShort `json:"short" gorm:"foreignKey:ResourceID"`
|
Short *ResourceShort `json:"short,omitempty" gorm:"foreignKey:ResourceID"`
|
||||||
Long *ResourceLong `json:"long" gorm:"foreignKey:ResourceID"`
|
Long *ResourceLong `json:"long,omitempty" gorm:"foreignKey:ResourceID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceType 套餐类型枚举
|
// ResourceType 套餐类型枚举
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import (
|
|||||||
type ResourceLong struct {
|
type ResourceLong struct {
|
||||||
ID int32 `json:"id" gorm:"column:id"` // ID
|
ID int32 `json:"id" gorm:"column:id"` // ID
|
||||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||||
|
Live int32 `json:"live" gorm:"column:live"` // 可用时长(小时)
|
||||||
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型:1-包时,2-包量
|
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型:1-包时,2-包量
|
||||||
Live int32 `json:"live" gorm:"column:live"` // 可用时长(天)
|
Quota int32 `json:"quota" gorm:"column:quota"` // 每日配额(包时)或总配额(包量)
|
||||||
Expire *time.Time `json:"expire" gorm:"column:expire"` // 过期时间
|
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 套餐过期时间,包时模式可用
|
||||||
Quota *int32 `json:"quota" gorm:"column:quota"` // 配额数量
|
Used int32 `json:"used" gorm:"column:used"` // 总用量
|
||||||
Used int32 `json:"used" gorm:"column:used"` // 已用数量
|
Daily int32 `json:"daily" gorm:"column:daily"` // 当日用量
|
||||||
DailyLimit int32 `json:"daily_limit" gorm:"column:daily_limit"` // 每日限制
|
LastAt *time.Time `json:"last_at,omitempty" gorm:"column:last_at"` // 最后使用时间
|
||||||
DailyUsed int32 `json:"daily_used" gorm:"column:daily_used"` // 今日已用数量
|
|
||||||
DailyLast *time.Time `json:"daily_last" gorm:"column:daily_last"` // 今日最后使用时间
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import (
|
|||||||
type ResourceShort struct {
|
type ResourceShort struct {
|
||||||
ID int32 `json:"id" gorm:"column:id"` // ID
|
ID int32 `json:"id" gorm:"column:id"` // ID
|
||||||
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
|
||||||
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型:1-包时,2-包量
|
|
||||||
Live int32 `json:"live" gorm:"column:live"` // 可用时长(秒)
|
Live int32 `json:"live" gorm:"column:live"` // 可用时长(秒)
|
||||||
Expire *time.Time `json:"expire" gorm:"column:expire"` // 过期时间
|
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型:1-包时,2-包量
|
||||||
Quota *int32 `json:"quota" gorm:"column:quota"` // 配额数量
|
Quota int32 `json:"quota" gorm:"column:quota"` // 每日配额(包时)或总配额(包量)
|
||||||
Used int32 `json:"used" gorm:"column:used"` // 已用数量
|
ExpireAt *time.Time `json:"expire_at,omitempty" gorm:"column:expire_at"` // 套餐过期时间,包时模式可用
|
||||||
DailyLimit int32 `json:"daily_limit" gorm:"column:daily_limit"` // 每日限制
|
Used int32 `json:"used" gorm:"column:used"` // 总用量
|
||||||
DailyUsed int32 `json:"daily_used" gorm:"column:daily_used"` // 今日已用数量
|
Daily int32 `json:"daily" gorm:"column:daily"` // 当日用量
|
||||||
DailyLast *time.Time `json:"daily_last" gorm:"column:daily_last"` // 今日最后使用时间
|
LastAt *time.Time `json:"last_at,omitempty" gorm:"column:last_at"` // 最后使用时间
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,18 @@ import (
|
|||||||
// Session 会话表
|
// Session 会话表
|
||||||
type Session struct {
|
type Session struct {
|
||||||
core.Model
|
core.Model
|
||||||
UserID *int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
UserID *int32 `json:"user_id,omitempty" gorm:"column:user_id"` // 用户ID
|
||||||
AdminID *int32 `json:"admin_id" gorm:"column:admin_id"` // 管理员ID
|
AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID
|
||||||
ClientID *int32 `json:"client_id" gorm:"column:client_id"` // 客户端ID
|
ClientID *int32 `json:"client_id,omitempty" gorm:"column:client_id"` // 客户端ID
|
||||||
IP *orm.Inet `json:"ip" gorm:"column:ip"` // IP地址
|
IP *orm.Inet `json:"ip,omitempty" gorm:"column:ip"` // IP地址
|
||||||
UA *string `json:"ua" gorm:"column:ua"` // 用户代理
|
UA *string `json:"ua,omitempty" gorm:"column:ua"` // 用户代理
|
||||||
AccessToken string `json:"access_token" gorm:"column:access_token"` // 访问令牌
|
AccessToken string `json:"access_token" gorm:"column:access_token"` // 访问令牌
|
||||||
AccessTokenExpires time.Time `json:"access_token_expires" gorm:"column:access_token_expires"` // 访问令牌过期时间
|
AccessTokenExpires time.Time `json:"access_token_expires" gorm:"column:access_token_expires"` // 访问令牌过期时间
|
||||||
RefreshToken *string `json:"refresh_token" gorm:"column:refresh_token"` // 刷新令牌
|
RefreshToken *string `json:"refresh_token,omitempty" gorm:"column:refresh_token"` // 刷新令牌
|
||||||
RefreshTokenExpires *time.Time `json:"refresh_token_expires" gorm:"column:refresh_token_expires"` // 刷新令牌过期时间
|
RefreshTokenExpires *time.Time `json:"refresh_token_expires,omitempty" gorm:"column:refresh_token_expires"` // 刷新令牌过期时间
|
||||||
Scopes *string `json:"scopes" gorm:"column:scopes"` // 权限范围
|
Scopes *string `json:"scopes,omitempty" gorm:"column:scopes"` // 权限范围
|
||||||
|
|
||||||
User *User `json:"user" gorm:"foreignKey:UserID"`
|
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
Admin *Admin `json:"admin" gorm:"foreignKey:AdminID"`
|
Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"`
|
||||||
Client *Client `json:"client" gorm:"foreignKey:ClientID;belongsTo:ID"`
|
Client *Client `json:"client,omitempty" gorm:"foreignKey:ClientID;belongsTo:ID"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,20 @@ type Trade struct {
|
|||||||
core.Model
|
core.Model
|
||||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||||
InnerNo string `json:"inner_no" gorm:"column:inner_no"` // 内部订单号
|
InnerNo string `json:"inner_no" gorm:"column:inner_no"` // 内部订单号
|
||||||
OuterNo *string `json:"outer_no" gorm:"column:outer_no"` // 外部订单号
|
OuterNo *string `json:"outer_no,omitempty" gorm:"column:outer_no"` // 外部订单号
|
||||||
Type TradeType `json:"type" gorm:"column:type"` // 订单类型:1-购买产品,2-充值余额
|
Type TradeType `json:"type" gorm:"column:type"` // 订单类型:1-购买产品,2-充值余额
|
||||||
Subject string `json:"subject" gorm:"column:subject"` // 订单主题
|
Subject string `json:"subject" gorm:"column:subject"` // 订单主题
|
||||||
Remark *string `json:"remark" gorm:"column:remark"` // 订单备注
|
Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 订单备注
|
||||||
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 订单总金额
|
Amount decimal.Decimal `json:"amount" gorm:"column:amount"` // 订单总金额
|
||||||
Payment decimal.Decimal `json:"payment" gorm:"column:payment"` // 实际支付金额
|
Payment decimal.Decimal `json:"payment" gorm:"column:payment"` // 实际支付金额
|
||||||
Method TradeMethod `json:"method" gorm:"column:method"` // 支付方式:1-支付宝,2-微信,3-商福通,4-商福通渠道支付宝,5-商福通渠道微信
|
Method TradeMethod `json:"method" gorm:"column:method"` // 支付方式:1-支付宝,2-微信,3-商福通,4-商福通渠道支付宝,5-商福通渠道微信
|
||||||
Platform TradePlatform `json:"platform" gorm:"column:platform"` // 支付平台:1-电脑网站,2-手机网站
|
Platform TradePlatform `json:"platform" gorm:"column:platform"` // 支付平台:1-电脑网站,2-手机网站
|
||||||
Acquirer *TradeAcquirer `json:"acquirer" gorm:"column:acquirer"` // 收单机构:1-支付宝,2-微信,3-银联
|
Acquirer *TradeAcquirer `json:"acquirer,omitempty" gorm:"column:acquirer"` // 收单机构:1-支付宝,2-微信,3-银联
|
||||||
Status TradeStatus `json:"status" gorm:"column:status"` // 订单状态:0-待支付,1-已支付,2-已取消
|
Status TradeStatus `json:"status" gorm:"column:status"` // 订单状态:0-待支付,1-已支付,2-已取消
|
||||||
Refunded bool `json:"refunded" gorm:"column:refunded"` // 是否已退款
|
Refunded bool `json:"refunded" gorm:"column:refunded"` // 是否已退款
|
||||||
PaymentURL *string `json:"payment_url" gorm:"column:payment_url"` // 支付链接
|
PaymentURL *string `json:"payment_url,omitempty" gorm:"column:payment_url"` // 支付链接
|
||||||
CompletedAt *time.Time `json:"completed_at" gorm:"column:completed_at"` // 支付时间
|
CompletedAt *time.Time `json:"completed_at,omitempty" gorm:"column:completed_at"` // 支付时间
|
||||||
CanceledAt *time.Time `json:"canceled_at" gorm:"column:canceled_at"` // 取消时间
|
CanceledAt *time.Time `json:"canceled_at,omitempty" gorm:"column:canceled_at"` // 取消时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// TradeType 订单类型枚举
|
// TradeType 订单类型枚举
|
||||||
|
|||||||
@@ -11,25 +11,25 @@ import (
|
|||||||
// User 用户表
|
// User 用户表
|
||||||
type User struct {
|
type User struct {
|
||||||
core.Model
|
core.Model
|
||||||
AdminID *int32 `json:"admin_id" gorm:"column:admin_id"` // 管理员ID
|
AdminID *int32 `json:"admin_id,omitempty" gorm:"column:admin_id"` // 管理员ID
|
||||||
Phone string `json:"phone" gorm:"column:phone"` // 手机号码
|
Phone string `json:"phone" gorm:"column:phone"` // 手机号码
|
||||||
Username *string `json:"username" gorm:"column:username"` // 用户名
|
Username *string `json:"username,omitempty" gorm:"column:username"` // 用户名
|
||||||
Email *string `json:"email" gorm:"column:email"` // 邮箱
|
Email *string `json:"email,omitempty" gorm:"column:email"` // 邮箱
|
||||||
Password *string `json:"password" gorm:"column:password"` // 用户密码
|
Password *string `json:"password,omitempty" gorm:"column:password"` // 用户密码
|
||||||
Name *string `json:"name" gorm:"column:name"` // 真实姓名
|
Name *string `json:"name,omitempty" gorm:"column:name"` // 真实姓名
|
||||||
Avatar *string `json:"avatar" gorm:"column:avatar"` // 头像URL
|
Avatar *string `json:"avatar,omitempty" gorm:"column:avatar"` // 头像URL
|
||||||
Status UserStatus `json:"status" gorm:"column:status"` // 用户状态:0-禁用,1-正常
|
Status UserStatus `json:"status" gorm:"column:status"` // 用户状态:0-禁用,1-正常
|
||||||
Balance decimal.Decimal `json:"balance" gorm:"column:balance"` // 账户余额
|
Balance decimal.Decimal `json:"balance" gorm:"column:balance"` // 账户余额
|
||||||
IDType UserIDType `json:"id_type" gorm:"column:id_type"` // 认证类型:0-未认证,1-个人认证,2-企业认证
|
IDType UserIDType `json:"id_type" gorm:"column:id_type"` // 认证类型:0-未认证,1-个人认证,2-企业认证
|
||||||
IDNo *string `json:"id_no" gorm:"column:id_no"` // 身份证号或营业执照号
|
IDNo *string `json:"id_no,omitempty" gorm:"column:id_no"` // 身份证号或营业执照号
|
||||||
IDToken *string `json:"id_token" gorm:"column:id_token"` // 身份验证标识
|
IDToken *string `json:"id_token,omitempty" gorm:"column:id_token"` // 身份验证标识
|
||||||
ContactQQ *string `json:"contact_qq" gorm:"column:contact_qq"` // QQ联系方式
|
ContactQQ *string `json:"contact_qq,omitempty" gorm:"column:contact_qq"` // QQ联系方式
|
||||||
ContactWechat *string `json:"contact_wechat" gorm:"column:contact_wechat"` // 微信联系方式
|
ContactWechat *string `json:"contact_wechat,omitempty" gorm:"column:contact_wechat"` // 微信联系方式
|
||||||
LastLogin *time.Time `json:"last_login" gorm:"column:last_login"` // 最后登录时间
|
LastLogin *time.Time `json:"last_login,omitempty" gorm:"column:last_login"` // 最后登录时间
|
||||||
LastLoginIP *orm.Inet `json:"last_login_ip" gorm:"column:last_login_ip"` // 最后登录地址
|
LastLoginIP *orm.Inet `json:"last_login_ip,omitempty" gorm:"column:last_login_ip"` // 最后登录地址
|
||||||
LastLoginUA *string `json:"last_login_ua" gorm:"column:last_login_ua"` // 最后登录代理
|
LastLoginUA *string `json:"last_login_ua,omitempty" gorm:"column:last_login_ua"` // 最后登录代理
|
||||||
|
|
||||||
Admin Admin `json:"admin" gorm:"foreignKey:AdminID"`
|
Admin *Admin `json:"admin,omitempty" gorm:"foreignKey:AdminID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserStatus 用户状态枚举
|
// UserStatus 用户状态枚举
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
type UserRole struct {
|
type UserRole struct {
|
||||||
core.Model
|
core.Model
|
||||||
Name string `json:"name" gorm:"column:name"` // 角色名称
|
Name string `json:"name" gorm:"column:name"` // 角色名称
|
||||||
Description *string `json:"description" gorm:"column:description"` // 角色描述
|
Description *string `json:"description,omitempty" gorm:"column:description"` // 角色描述
|
||||||
Active bool `json:"active" gorm:"column:active"` // 是否激活
|
Active bool `json:"active" gorm:"column:active"` // 是否激活
|
||||||
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
Sort int32 `json:"sort" gorm:"column:sort"` // 排序
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ type Whitelist struct {
|
|||||||
core.Model
|
core.Model
|
||||||
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
UserID int32 `json:"user_id" gorm:"column:user_id"` // 用户ID
|
||||||
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
|
IP orm.Inet `json:"ip" gorm:"column:ip;not null"` // IP地址
|
||||||
Remark *string `json:"remark" gorm:"column:remark"` // 备注
|
Remark *string `json:"remark,omitempty" gorm:"column:remark"` // 备注
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
|
|||||||
_channel.DeletedAt = field.NewField(tableName, "deleted_at")
|
_channel.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||||
_channel.UserID = field.NewInt32(tableName, "user_id")
|
_channel.UserID = field.NewInt32(tableName, "user_id")
|
||||||
_channel.ResourceID = field.NewInt32(tableName, "resource_id")
|
_channel.ResourceID = field.NewInt32(tableName, "resource_id")
|
||||||
_channel.ProxyID = field.NewInt32(tableName, "proxy_id")
|
|
||||||
_channel.BatchNo = field.NewString(tableName, "batch_no")
|
_channel.BatchNo = field.NewString(tableName, "batch_no")
|
||||||
|
_channel.ProxyID = field.NewInt32(tableName, "proxy_id")
|
||||||
|
_channel.Host = field.NewString(tableName, "host")
|
||||||
_channel.Port = field.NewUint16(tableName, "port")
|
_channel.Port = field.NewUint16(tableName, "port")
|
||||||
_channel.EdgeID = field.NewInt32(tableName, "edge_id")
|
_channel.EdgeID = field.NewInt32(tableName, "edge_id")
|
||||||
|
_channel.EdgeRef = field.NewString(tableName, "edge_ref")
|
||||||
_channel.FilterISP = field.NewInt(tableName, "filter_isp")
|
_channel.FilterISP = field.NewInt(tableName, "filter_isp")
|
||||||
_channel.FilterProv = field.NewString(tableName, "filter_prov")
|
_channel.FilterProv = field.NewString(tableName, "filter_prov")
|
||||||
_channel.FilterCity = field.NewString(tableName, "filter_city")
|
_channel.FilterCity = field.NewString(tableName, "filter_city")
|
||||||
@@ -141,10 +143,12 @@ type channel struct {
|
|||||||
DeletedAt field.Field
|
DeletedAt field.Field
|
||||||
UserID field.Int32
|
UserID field.Int32
|
||||||
ResourceID field.Int32
|
ResourceID field.Int32
|
||||||
ProxyID field.Int32
|
|
||||||
BatchNo field.String
|
BatchNo field.String
|
||||||
|
ProxyID field.Int32
|
||||||
|
Host field.String
|
||||||
Port field.Uint16
|
Port field.Uint16
|
||||||
EdgeID field.Int32
|
EdgeID field.Int32
|
||||||
|
EdgeRef field.String
|
||||||
FilterISP field.Int
|
FilterISP field.Int
|
||||||
FilterProv field.String
|
FilterProv field.String
|
||||||
FilterCity field.String
|
FilterCity field.String
|
||||||
@@ -182,10 +186,12 @@ func (c *channel) updateTableName(table string) *channel {
|
|||||||
c.DeletedAt = field.NewField(table, "deleted_at")
|
c.DeletedAt = field.NewField(table, "deleted_at")
|
||||||
c.UserID = field.NewInt32(table, "user_id")
|
c.UserID = field.NewInt32(table, "user_id")
|
||||||
c.ResourceID = field.NewInt32(table, "resource_id")
|
c.ResourceID = field.NewInt32(table, "resource_id")
|
||||||
c.ProxyID = field.NewInt32(table, "proxy_id")
|
|
||||||
c.BatchNo = field.NewString(table, "batch_no")
|
c.BatchNo = field.NewString(table, "batch_no")
|
||||||
|
c.ProxyID = field.NewInt32(table, "proxy_id")
|
||||||
|
c.Host = field.NewString(table, "host")
|
||||||
c.Port = field.NewUint16(table, "port")
|
c.Port = field.NewUint16(table, "port")
|
||||||
c.EdgeID = field.NewInt32(table, "edge_id")
|
c.EdgeID = field.NewInt32(table, "edge_id")
|
||||||
|
c.EdgeRef = field.NewString(table, "edge_ref")
|
||||||
c.FilterISP = field.NewInt(table, "filter_isp")
|
c.FilterISP = field.NewInt(table, "filter_isp")
|
||||||
c.FilterProv = field.NewString(table, "filter_prov")
|
c.FilterProv = field.NewString(table, "filter_prov")
|
||||||
c.FilterCity = field.NewString(table, "filter_city")
|
c.FilterCity = field.NewString(table, "filter_city")
|
||||||
@@ -210,17 +216,19 @@ func (c *channel) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *channel) fillFieldMap() {
|
func (c *channel) fillFieldMap() {
|
||||||
c.fieldMap = make(map[string]field.Expr, 22)
|
c.fieldMap = make(map[string]field.Expr, 24)
|
||||||
c.fieldMap["id"] = c.ID
|
c.fieldMap["id"] = c.ID
|
||||||
c.fieldMap["created_at"] = c.CreatedAt
|
c.fieldMap["created_at"] = c.CreatedAt
|
||||||
c.fieldMap["updated_at"] = c.UpdatedAt
|
c.fieldMap["updated_at"] = c.UpdatedAt
|
||||||
c.fieldMap["deleted_at"] = c.DeletedAt
|
c.fieldMap["deleted_at"] = c.DeletedAt
|
||||||
c.fieldMap["user_id"] = c.UserID
|
c.fieldMap["user_id"] = c.UserID
|
||||||
c.fieldMap["resource_id"] = c.ResourceID
|
c.fieldMap["resource_id"] = c.ResourceID
|
||||||
c.fieldMap["proxy_id"] = c.ProxyID
|
|
||||||
c.fieldMap["batch_no"] = c.BatchNo
|
c.fieldMap["batch_no"] = c.BatchNo
|
||||||
|
c.fieldMap["proxy_id"] = c.ProxyID
|
||||||
|
c.fieldMap["host"] = c.Host
|
||||||
c.fieldMap["port"] = c.Port
|
c.fieldMap["port"] = c.Port
|
||||||
c.fieldMap["edge_id"] = c.EdgeID
|
c.fieldMap["edge_id"] = c.EdgeID
|
||||||
|
c.fieldMap["edge_ref"] = c.EdgeRef
|
||||||
c.fieldMap["filter_isp"] = c.FilterISP
|
c.fieldMap["filter_isp"] = c.FilterISP
|
||||||
c.fieldMap["filter_prov"] = c.FilterProv
|
c.fieldMap["filter_prov"] = c.FilterProv
|
||||||
c.fieldMap["filter_city"] = c.FilterCity
|
c.fieldMap["filter_city"] = c.FilterCity
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ var (
|
|||||||
Client *client
|
Client *client
|
||||||
Coupon *coupon
|
Coupon *coupon
|
||||||
Edge *edge
|
Edge *edge
|
||||||
|
Inquiry *inquiry
|
||||||
LinkAdminRole *linkAdminRole
|
LinkAdminRole *linkAdminRole
|
||||||
LinkAdminRolePermission *linkAdminRolePermission
|
LinkAdminRolePermission *linkAdminRolePermission
|
||||||
LinkClientPermission *linkClientPermission
|
LinkClientPermission *linkClientPermission
|
||||||
@@ -58,6 +59,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
|
|||||||
Client = &Q.Client
|
Client = &Q.Client
|
||||||
Coupon = &Q.Coupon
|
Coupon = &Q.Coupon
|
||||||
Edge = &Q.Edge
|
Edge = &Q.Edge
|
||||||
|
Inquiry = &Q.Inquiry
|
||||||
LinkAdminRole = &Q.LinkAdminRole
|
LinkAdminRole = &Q.LinkAdminRole
|
||||||
LinkAdminRolePermission = &Q.LinkAdminRolePermission
|
LinkAdminRolePermission = &Q.LinkAdminRolePermission
|
||||||
LinkClientPermission = &Q.LinkClientPermission
|
LinkClientPermission = &Q.LinkClientPermission
|
||||||
@@ -92,6 +94,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
|
|||||||
Client: newClient(db, opts...),
|
Client: newClient(db, opts...),
|
||||||
Coupon: newCoupon(db, opts...),
|
Coupon: newCoupon(db, opts...),
|
||||||
Edge: newEdge(db, opts...),
|
Edge: newEdge(db, opts...),
|
||||||
|
Inquiry: newInquiry(db, opts...),
|
||||||
LinkAdminRole: newLinkAdminRole(db, opts...),
|
LinkAdminRole: newLinkAdminRole(db, opts...),
|
||||||
LinkAdminRolePermission: newLinkAdminRolePermission(db, opts...),
|
LinkAdminRolePermission: newLinkAdminRolePermission(db, opts...),
|
||||||
LinkClientPermission: newLinkClientPermission(db, opts...),
|
LinkClientPermission: newLinkClientPermission(db, opts...),
|
||||||
@@ -127,6 +130,7 @@ type Query struct {
|
|||||||
Client client
|
Client client
|
||||||
Coupon coupon
|
Coupon coupon
|
||||||
Edge edge
|
Edge edge
|
||||||
|
Inquiry inquiry
|
||||||
LinkAdminRole linkAdminRole
|
LinkAdminRole linkAdminRole
|
||||||
LinkAdminRolePermission linkAdminRolePermission
|
LinkAdminRolePermission linkAdminRolePermission
|
||||||
LinkClientPermission linkClientPermission
|
LinkClientPermission linkClientPermission
|
||||||
@@ -163,6 +167,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
|
|||||||
Client: q.Client.clone(db),
|
Client: q.Client.clone(db),
|
||||||
Coupon: q.Coupon.clone(db),
|
Coupon: q.Coupon.clone(db),
|
||||||
Edge: q.Edge.clone(db),
|
Edge: q.Edge.clone(db),
|
||||||
|
Inquiry: q.Inquiry.clone(db),
|
||||||
LinkAdminRole: q.LinkAdminRole.clone(db),
|
LinkAdminRole: q.LinkAdminRole.clone(db),
|
||||||
LinkAdminRolePermission: q.LinkAdminRolePermission.clone(db),
|
LinkAdminRolePermission: q.LinkAdminRolePermission.clone(db),
|
||||||
LinkClientPermission: q.LinkClientPermission.clone(db),
|
LinkClientPermission: q.LinkClientPermission.clone(db),
|
||||||
@@ -206,6 +211,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
|
|||||||
Client: q.Client.replaceDB(db),
|
Client: q.Client.replaceDB(db),
|
||||||
Coupon: q.Coupon.replaceDB(db),
|
Coupon: q.Coupon.replaceDB(db),
|
||||||
Edge: q.Edge.replaceDB(db),
|
Edge: q.Edge.replaceDB(db),
|
||||||
|
Inquiry: q.Inquiry.replaceDB(db),
|
||||||
LinkAdminRole: q.LinkAdminRole.replaceDB(db),
|
LinkAdminRole: q.LinkAdminRole.replaceDB(db),
|
||||||
LinkAdminRolePermission: q.LinkAdminRolePermission.replaceDB(db),
|
LinkAdminRolePermission: q.LinkAdminRolePermission.replaceDB(db),
|
||||||
LinkClientPermission: q.LinkClientPermission.replaceDB(db),
|
LinkClientPermission: q.LinkClientPermission.replaceDB(db),
|
||||||
@@ -239,6 +245,7 @@ type queryCtx struct {
|
|||||||
Client *clientDo
|
Client *clientDo
|
||||||
Coupon *couponDo
|
Coupon *couponDo
|
||||||
Edge *edgeDo
|
Edge *edgeDo
|
||||||
|
Inquiry *inquiryDo
|
||||||
LinkAdminRole *linkAdminRoleDo
|
LinkAdminRole *linkAdminRoleDo
|
||||||
LinkAdminRolePermission *linkAdminRolePermissionDo
|
LinkAdminRolePermission *linkAdminRolePermissionDo
|
||||||
LinkClientPermission *linkClientPermissionDo
|
LinkClientPermission *linkClientPermissionDo
|
||||||
@@ -272,6 +279,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
|
|||||||
Client: q.Client.WithContext(ctx),
|
Client: q.Client.WithContext(ctx),
|
||||||
Coupon: q.Coupon.WithContext(ctx),
|
Coupon: q.Coupon.WithContext(ctx),
|
||||||
Edge: q.Edge.WithContext(ctx),
|
Edge: q.Edge.WithContext(ctx),
|
||||||
|
Inquiry: q.Inquiry.WithContext(ctx),
|
||||||
LinkAdminRole: q.LinkAdminRole.WithContext(ctx),
|
LinkAdminRole: q.LinkAdminRole.WithContext(ctx),
|
||||||
LinkAdminRolePermission: q.LinkAdminRolePermission.WithContext(ctx),
|
LinkAdminRolePermission: q.LinkAdminRolePermission.WithContext(ctx),
|
||||||
LinkClientPermission: q.LinkClientPermission.WithContext(ctx),
|
LinkClientPermission: q.LinkClientPermission.WithContext(ctx),
|
||||||
|
|||||||
359
web/queries/inquiry.gen.go
Normal file
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.Version = field.NewInt32(tableName, "version")
|
||||||
_proxy.Mac = field.NewString(tableName, "mac")
|
_proxy.Mac = field.NewString(tableName, "mac")
|
||||||
_proxy.IP = field.NewField(tableName, "ip")
|
_proxy.IP = field.NewField(tableName, "ip")
|
||||||
|
_proxy.Host = field.NewString(tableName, "host")
|
||||||
_proxy.Secret = field.NewString(tableName, "secret")
|
_proxy.Secret = field.NewString(tableName, "secret")
|
||||||
_proxy.Type = field.NewInt(tableName, "type")
|
_proxy.Type = field.NewInt(tableName, "type")
|
||||||
_proxy.Status = field.NewInt(tableName, "status")
|
_proxy.Status = field.NewInt(tableName, "status")
|
||||||
@@ -120,6 +121,7 @@ type proxy struct {
|
|||||||
Version field.Int32
|
Version field.Int32
|
||||||
Mac field.String
|
Mac field.String
|
||||||
IP field.Field
|
IP field.Field
|
||||||
|
Host field.String
|
||||||
Secret field.String
|
Secret field.String
|
||||||
Type field.Int
|
Type field.Int
|
||||||
Status field.Int
|
Status field.Int
|
||||||
@@ -148,6 +150,7 @@ func (p *proxy) updateTableName(table string) *proxy {
|
|||||||
p.Version = field.NewInt32(table, "version")
|
p.Version = field.NewInt32(table, "version")
|
||||||
p.Mac = field.NewString(table, "mac")
|
p.Mac = field.NewString(table, "mac")
|
||||||
p.IP = field.NewField(table, "ip")
|
p.IP = field.NewField(table, "ip")
|
||||||
|
p.Host = field.NewString(table, "host")
|
||||||
p.Secret = field.NewString(table, "secret")
|
p.Secret = field.NewString(table, "secret")
|
||||||
p.Type = field.NewInt(table, "type")
|
p.Type = field.NewInt(table, "type")
|
||||||
p.Status = field.NewInt(table, "status")
|
p.Status = field.NewInt(table, "status")
|
||||||
@@ -168,7 +171,7 @@ func (p *proxy) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *proxy) fillFieldMap() {
|
func (p *proxy) fillFieldMap() {
|
||||||
p.fieldMap = make(map[string]field.Expr, 12)
|
p.fieldMap = make(map[string]field.Expr, 13)
|
||||||
p.fieldMap["id"] = p.ID
|
p.fieldMap["id"] = p.ID
|
||||||
p.fieldMap["created_at"] = p.CreatedAt
|
p.fieldMap["created_at"] = p.CreatedAt
|
||||||
p.fieldMap["updated_at"] = p.UpdatedAt
|
p.fieldMap["updated_at"] = p.UpdatedAt
|
||||||
@@ -176,6 +179,7 @@ func (p *proxy) fillFieldMap() {
|
|||||||
p.fieldMap["version"] = p.Version
|
p.fieldMap["version"] = p.Version
|
||||||
p.fieldMap["mac"] = p.Mac
|
p.fieldMap["mac"] = p.Mac
|
||||||
p.fieldMap["ip"] = p.IP
|
p.fieldMap["ip"] = p.IP
|
||||||
|
p.fieldMap["host"] = p.Host
|
||||||
p.fieldMap["secret"] = p.Secret
|
p.fieldMap["secret"] = p.Secret
|
||||||
p.fieldMap["type"] = p.Type
|
p.fieldMap["type"] = p.Type
|
||||||
p.fieldMap["status"] = p.Status
|
p.fieldMap["status"] = p.Status
|
||||||
|
|||||||
@@ -29,14 +29,13 @@ func newResourceLong(db *gorm.DB, opts ...gen.DOOption) resourceLong {
|
|||||||
_resourceLong.ALL = field.NewAsterisk(tableName)
|
_resourceLong.ALL = field.NewAsterisk(tableName)
|
||||||
_resourceLong.ID = field.NewInt32(tableName, "id")
|
_resourceLong.ID = field.NewInt32(tableName, "id")
|
||||||
_resourceLong.ResourceID = field.NewInt32(tableName, "resource_id")
|
_resourceLong.ResourceID = field.NewInt32(tableName, "resource_id")
|
||||||
_resourceLong.Type = field.NewInt(tableName, "type")
|
|
||||||
_resourceLong.Live = field.NewInt32(tableName, "live")
|
_resourceLong.Live = field.NewInt32(tableName, "live")
|
||||||
_resourceLong.Expire = field.NewTime(tableName, "expire")
|
_resourceLong.Type = field.NewInt(tableName, "type")
|
||||||
_resourceLong.Quota = field.NewInt32(tableName, "quota")
|
_resourceLong.Quota = field.NewInt32(tableName, "quota")
|
||||||
|
_resourceLong.ExpireAt = field.NewTime(tableName, "expire_at")
|
||||||
_resourceLong.Used = field.NewInt32(tableName, "used")
|
_resourceLong.Used = field.NewInt32(tableName, "used")
|
||||||
_resourceLong.DailyLimit = field.NewInt32(tableName, "daily_limit")
|
_resourceLong.Daily = field.NewInt32(tableName, "daily")
|
||||||
_resourceLong.DailyUsed = field.NewInt32(tableName, "daily_used")
|
_resourceLong.LastAt = field.NewTime(tableName, "last_at")
|
||||||
_resourceLong.DailyLast = field.NewTime(tableName, "daily_last")
|
|
||||||
|
|
||||||
_resourceLong.fillFieldMap()
|
_resourceLong.fillFieldMap()
|
||||||
|
|
||||||
@@ -49,14 +48,13 @@ type resourceLong struct {
|
|||||||
ALL field.Asterisk
|
ALL field.Asterisk
|
||||||
ID field.Int32
|
ID field.Int32
|
||||||
ResourceID field.Int32
|
ResourceID field.Int32
|
||||||
Type field.Int
|
|
||||||
Live field.Int32
|
Live field.Int32
|
||||||
Expire field.Time
|
Type field.Int
|
||||||
Quota field.Int32
|
Quota field.Int32
|
||||||
|
ExpireAt field.Time
|
||||||
Used field.Int32
|
Used field.Int32
|
||||||
DailyLimit field.Int32
|
Daily field.Int32
|
||||||
DailyUsed field.Int32
|
LastAt field.Time
|
||||||
DailyLast field.Time
|
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
@@ -75,14 +73,13 @@ func (r *resourceLong) updateTableName(table string) *resourceLong {
|
|||||||
r.ALL = field.NewAsterisk(table)
|
r.ALL = field.NewAsterisk(table)
|
||||||
r.ID = field.NewInt32(table, "id")
|
r.ID = field.NewInt32(table, "id")
|
||||||
r.ResourceID = field.NewInt32(table, "resource_id")
|
r.ResourceID = field.NewInt32(table, "resource_id")
|
||||||
r.Type = field.NewInt(table, "type")
|
|
||||||
r.Live = field.NewInt32(table, "live")
|
r.Live = field.NewInt32(table, "live")
|
||||||
r.Expire = field.NewTime(table, "expire")
|
r.Type = field.NewInt(table, "type")
|
||||||
r.Quota = field.NewInt32(table, "quota")
|
r.Quota = field.NewInt32(table, "quota")
|
||||||
|
r.ExpireAt = field.NewTime(table, "expire_at")
|
||||||
r.Used = field.NewInt32(table, "used")
|
r.Used = field.NewInt32(table, "used")
|
||||||
r.DailyLimit = field.NewInt32(table, "daily_limit")
|
r.Daily = field.NewInt32(table, "daily")
|
||||||
r.DailyUsed = field.NewInt32(table, "daily_used")
|
r.LastAt = field.NewTime(table, "last_at")
|
||||||
r.DailyLast = field.NewTime(table, "daily_last")
|
|
||||||
|
|
||||||
r.fillFieldMap()
|
r.fillFieldMap()
|
||||||
|
|
||||||
@@ -99,17 +96,16 @@ func (r *resourceLong) GetFieldByName(fieldName string) (field.OrderExpr, bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *resourceLong) fillFieldMap() {
|
func (r *resourceLong) fillFieldMap() {
|
||||||
r.fieldMap = make(map[string]field.Expr, 10)
|
r.fieldMap = make(map[string]field.Expr, 9)
|
||||||
r.fieldMap["id"] = r.ID
|
r.fieldMap["id"] = r.ID
|
||||||
r.fieldMap["resource_id"] = r.ResourceID
|
r.fieldMap["resource_id"] = r.ResourceID
|
||||||
r.fieldMap["type"] = r.Type
|
|
||||||
r.fieldMap["live"] = r.Live
|
r.fieldMap["live"] = r.Live
|
||||||
r.fieldMap["expire"] = r.Expire
|
r.fieldMap["type"] = r.Type
|
||||||
r.fieldMap["quota"] = r.Quota
|
r.fieldMap["quota"] = r.Quota
|
||||||
|
r.fieldMap["expire_at"] = r.ExpireAt
|
||||||
r.fieldMap["used"] = r.Used
|
r.fieldMap["used"] = r.Used
|
||||||
r.fieldMap["daily_limit"] = r.DailyLimit
|
r.fieldMap["daily"] = r.Daily
|
||||||
r.fieldMap["daily_used"] = r.DailyUsed
|
r.fieldMap["last_at"] = r.LastAt
|
||||||
r.fieldMap["daily_last"] = r.DailyLast
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r resourceLong) clone(db *gorm.DB) resourceLong {
|
func (r resourceLong) clone(db *gorm.DB) resourceLong {
|
||||||
|
|||||||
@@ -29,14 +29,13 @@ func newResourceShort(db *gorm.DB, opts ...gen.DOOption) resourceShort {
|
|||||||
_resourceShort.ALL = field.NewAsterisk(tableName)
|
_resourceShort.ALL = field.NewAsterisk(tableName)
|
||||||
_resourceShort.ID = field.NewInt32(tableName, "id")
|
_resourceShort.ID = field.NewInt32(tableName, "id")
|
||||||
_resourceShort.ResourceID = field.NewInt32(tableName, "resource_id")
|
_resourceShort.ResourceID = field.NewInt32(tableName, "resource_id")
|
||||||
_resourceShort.Type = field.NewInt(tableName, "type")
|
|
||||||
_resourceShort.Live = field.NewInt32(tableName, "live")
|
_resourceShort.Live = field.NewInt32(tableName, "live")
|
||||||
_resourceShort.Expire = field.NewTime(tableName, "expire")
|
_resourceShort.Type = field.NewInt(tableName, "type")
|
||||||
_resourceShort.Quota = field.NewInt32(tableName, "quota")
|
_resourceShort.Quota = field.NewInt32(tableName, "quota")
|
||||||
|
_resourceShort.ExpireAt = field.NewTime(tableName, "expire_at")
|
||||||
_resourceShort.Used = field.NewInt32(tableName, "used")
|
_resourceShort.Used = field.NewInt32(tableName, "used")
|
||||||
_resourceShort.DailyLimit = field.NewInt32(tableName, "daily_limit")
|
_resourceShort.Daily = field.NewInt32(tableName, "daily")
|
||||||
_resourceShort.DailyUsed = field.NewInt32(tableName, "daily_used")
|
_resourceShort.LastAt = field.NewTime(tableName, "last_at")
|
||||||
_resourceShort.DailyLast = field.NewTime(tableName, "daily_last")
|
|
||||||
|
|
||||||
_resourceShort.fillFieldMap()
|
_resourceShort.fillFieldMap()
|
||||||
|
|
||||||
@@ -49,14 +48,13 @@ type resourceShort struct {
|
|||||||
ALL field.Asterisk
|
ALL field.Asterisk
|
||||||
ID field.Int32
|
ID field.Int32
|
||||||
ResourceID field.Int32
|
ResourceID field.Int32
|
||||||
Type field.Int
|
|
||||||
Live field.Int32
|
Live field.Int32
|
||||||
Expire field.Time
|
Type field.Int
|
||||||
Quota field.Int32
|
Quota field.Int32
|
||||||
|
ExpireAt field.Time
|
||||||
Used field.Int32
|
Used field.Int32
|
||||||
DailyLimit field.Int32
|
Daily field.Int32
|
||||||
DailyUsed field.Int32
|
LastAt field.Time
|
||||||
DailyLast field.Time
|
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
}
|
}
|
||||||
@@ -75,14 +73,13 @@ func (r *resourceShort) updateTableName(table string) *resourceShort {
|
|||||||
r.ALL = field.NewAsterisk(table)
|
r.ALL = field.NewAsterisk(table)
|
||||||
r.ID = field.NewInt32(table, "id")
|
r.ID = field.NewInt32(table, "id")
|
||||||
r.ResourceID = field.NewInt32(table, "resource_id")
|
r.ResourceID = field.NewInt32(table, "resource_id")
|
||||||
r.Type = field.NewInt(table, "type")
|
|
||||||
r.Live = field.NewInt32(table, "live")
|
r.Live = field.NewInt32(table, "live")
|
||||||
r.Expire = field.NewTime(table, "expire")
|
r.Type = field.NewInt(table, "type")
|
||||||
r.Quota = field.NewInt32(table, "quota")
|
r.Quota = field.NewInt32(table, "quota")
|
||||||
|
r.ExpireAt = field.NewTime(table, "expire_at")
|
||||||
r.Used = field.NewInt32(table, "used")
|
r.Used = field.NewInt32(table, "used")
|
||||||
r.DailyLimit = field.NewInt32(table, "daily_limit")
|
r.Daily = field.NewInt32(table, "daily")
|
||||||
r.DailyUsed = field.NewInt32(table, "daily_used")
|
r.LastAt = field.NewTime(table, "last_at")
|
||||||
r.DailyLast = field.NewTime(table, "daily_last")
|
|
||||||
|
|
||||||
r.fillFieldMap()
|
r.fillFieldMap()
|
||||||
|
|
||||||
@@ -99,17 +96,16 @@ func (r *resourceShort) GetFieldByName(fieldName string) (field.OrderExpr, bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *resourceShort) fillFieldMap() {
|
func (r *resourceShort) fillFieldMap() {
|
||||||
r.fieldMap = make(map[string]field.Expr, 10)
|
r.fieldMap = make(map[string]field.Expr, 9)
|
||||||
r.fieldMap["id"] = r.ID
|
r.fieldMap["id"] = r.ID
|
||||||
r.fieldMap["resource_id"] = r.ResourceID
|
r.fieldMap["resource_id"] = r.ResourceID
|
||||||
r.fieldMap["type"] = r.Type
|
|
||||||
r.fieldMap["live"] = r.Live
|
r.fieldMap["live"] = r.Live
|
||||||
r.fieldMap["expire"] = r.Expire
|
r.fieldMap["type"] = r.Type
|
||||||
r.fieldMap["quota"] = r.Quota
|
r.fieldMap["quota"] = r.Quota
|
||||||
|
r.fieldMap["expire_at"] = r.ExpireAt
|
||||||
r.fieldMap["used"] = r.Used
|
r.fieldMap["used"] = r.Used
|
||||||
r.fieldMap["daily_limit"] = r.DailyLimit
|
r.fieldMap["daily"] = r.Daily
|
||||||
r.fieldMap["daily_used"] = r.DailyUsed
|
r.fieldMap["last_at"] = r.LastAt
|
||||||
r.fieldMap["daily_last"] = r.DailyLast
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r resourceShort) clone(db *gorm.DB) resourceShort {
|
func (r resourceShort) clone(db *gorm.DB) resourceShort {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"platform/pkg/env"
|
||||||
auth2 "platform/web/auth"
|
auth2 "platform/web/auth"
|
||||||
"platform/web/handlers"
|
"platform/web/handlers"
|
||||||
|
|
||||||
@@ -9,12 +10,30 @@ import (
|
|||||||
|
|
||||||
func ApplyRouters(app *fiber.App) {
|
func ApplyRouters(app *fiber.App) {
|
||||||
api := app.Group("/api")
|
api := app.Group("/api")
|
||||||
|
userRouter(api)
|
||||||
|
adminRouter(api)
|
||||||
|
|
||||||
|
// 回调
|
||||||
|
callbacks := app.Group("/callback")
|
||||||
|
callbacks.Get("/identify", handlers.IdentifyCallbackNew)
|
||||||
|
|
||||||
|
// 临时
|
||||||
|
if env.RunMode == env.RunModeDev {
|
||||||
|
debug := app.Group("/debug")
|
||||||
|
debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
|
||||||
|
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户接口路由
|
||||||
|
func userRouter(api fiber.Router) {
|
||||||
// 认证
|
// 认证
|
||||||
auth := api.Group("/auth")
|
auth := api.Group("/auth")
|
||||||
|
auth.Get("/authorize", auth2.AuthorizeGet)
|
||||||
|
auth.Post("/authorize", auth2.AuthorizePost)
|
||||||
auth.Post("/token", auth2.Token)
|
auth.Post("/token", auth2.Token)
|
||||||
auth.Post("/revoke", handlers.Revoke)
|
auth.Post("/revoke", auth2.Revoke)
|
||||||
auth.Post("/introspect", handlers.Introspect)
|
auth.Post("/introspect", auth2.Introspect)
|
||||||
auth.Post("/verify/sms", handlers.SmsCode)
|
auth.Post("/verify/sms", handlers.SmsCode)
|
||||||
|
|
||||||
// 用户
|
// 用户
|
||||||
@@ -34,12 +53,16 @@ func ApplyRouters(app *fiber.App) {
|
|||||||
// 套餐
|
// 套餐
|
||||||
resource := api.Group("/resource")
|
resource := api.Group("/resource")
|
||||||
resource.Post("/all", handlers.AllActiveResource)
|
resource.Post("/all", handlers.AllActiveResource)
|
||||||
resource.Post("/list/short", handlers.ListResourceShort)
|
resource.Post("/list/short", handlers.PageResourceShort)
|
||||||
resource.Post("/list/long", handlers.ListResourceLong)
|
resource.Post("/list/long", handlers.PageResourceLong)
|
||||||
resource.Post("/statistics/free", handlers.StatisticResourceFree)
|
|
||||||
resource.Post("/statistics/usage", handlers.StatisticResourceUsage)
|
|
||||||
resource.Post("/create", handlers.CreateResource)
|
resource.Post("/create", handlers.CreateResource)
|
||||||
resource.Post("/price", handlers.ResourcePrice)
|
resource.Post("/price", handlers.ResourcePrice)
|
||||||
|
resource.Post("/statistics/free", handlers.StatisticResourceFree)
|
||||||
|
resource.Post("/statistics/usage", handlers.StatisticResourceUsage)
|
||||||
|
|
||||||
|
// 批次
|
||||||
|
batch := api.Group("/batch")
|
||||||
|
batch.Post("/page", handlers.PageResourceBatch)
|
||||||
|
|
||||||
// 通道
|
// 通道
|
||||||
channel := api.Group("/channel")
|
channel := api.Group("/channel")
|
||||||
@@ -67,17 +90,45 @@ func ApplyRouters(app *fiber.App) {
|
|||||||
proxy.Post("/online", handlers.ProxyReportOnline)
|
proxy.Post("/online", handlers.ProxyReportOnline)
|
||||||
proxy.Post("/offline", handlers.ProxyReportOffline)
|
proxy.Post("/offline", handlers.ProxyReportOffline)
|
||||||
proxy.Post("/update", handlers.ProxyReportUpdate)
|
proxy.Post("/update", handlers.ProxyReportUpdate)
|
||||||
|
proxy.Post("/register/baidyin", handlers.ProxyRegisterBaiYin)
|
||||||
|
|
||||||
// 节点
|
// 节点
|
||||||
edge := api.Group("/edge")
|
edge := api.Group("/edge")
|
||||||
edge.Post("/assign", handlers.AssignEdge)
|
edge.Post("/assign", handlers.AssignEdge)
|
||||||
edge.Post("/all", handlers.AllEdgesAvailable)
|
edge.Post("/all", handlers.AllEdgesAvailable)
|
||||||
|
|
||||||
// 临时
|
// 前台
|
||||||
debug := app.Group("/debug")
|
inquiry := api.Group("/inquiry")
|
||||||
debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
|
inquiry.Post("/create", handlers.CreateInquiry)
|
||||||
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin)
|
}
|
||||||
|
|
||||||
callbacks := app.Group("/callback")
|
// 管理员接口路由
|
||||||
callbacks.Get("/identify", handlers.IdentifyCallbackNew)
|
func adminRouter(api fiber.Router) {
|
||||||
|
api = api.Group("/admin")
|
||||||
|
|
||||||
|
// user 用户
|
||||||
|
var user = api.Group("/user")
|
||||||
|
user.Post("/page", handlers.PageUserByAdmin)
|
||||||
|
user.Post("/bind", handlers.BindAdmin)
|
||||||
|
|
||||||
|
// resource 套餐
|
||||||
|
var resource = api.Group("/resource")
|
||||||
|
resource.Post("/short/page", handlers.PageResourceShortByAdmin)
|
||||||
|
resource.Post("/long/page", handlers.PageResourceLongByAdmin)
|
||||||
|
|
||||||
|
// batch 批次
|
||||||
|
var usage = api.Group("batch")
|
||||||
|
usage.Post("/page", handlers.PageBatchByAdmin)
|
||||||
|
|
||||||
|
// channel 通道
|
||||||
|
var channel = api.Group("/channel")
|
||||||
|
channel.Post("/page", handlers.PageChannelsByAdmin)
|
||||||
|
|
||||||
|
// trade 交易
|
||||||
|
var trade = api.Group("trade")
|
||||||
|
trade.Post("/page", handlers.PageTradeByAdmin)
|
||||||
|
|
||||||
|
// bill 账单
|
||||||
|
var bill = api.Group("/bill")
|
||||||
|
bill.Post("/page", handlers.PageBillByAdmin)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,42 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"platform/pkg/u"
|
||||||
"platform/web/core"
|
"platform/web/core"
|
||||||
g "platform/web/globals"
|
g "platform/web/globals"
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"gorm.io/gen/field"
|
"gorm.io/gen/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Channel ChannelService = &channelBaiyinService{}
|
|
||||||
|
|
||||||
// 通道服务
|
// 通道服务
|
||||||
type ChannelService interface {
|
var Channel = &channelServer{
|
||||||
|
provider: &channelBaiyinProvider{},
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChannelServiceProvider interface {
|
||||||
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error)
|
CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error)
|
||||||
RemoveChannels(batch string, ids []int32) error
|
RemoveChannels(batch string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type channelServer struct {
|
||||||
|
provider ChannelServiceProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *channelServer) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
|
||||||
|
return s.provider.CreateChannels(source, resourceId, authWhitelist, authPassword, count, edgeFilter...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *channelServer) RemoveChannels(batch string) error {
|
||||||
|
return s.provider.RemoveChannels(batch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 授权方式
|
// 授权方式
|
||||||
@@ -49,7 +68,7 @@ func genPassPair() (string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查找资源
|
// 查找资源
|
||||||
func findResource(resourceId int32) (*ResourceView, error) {
|
func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
|
||||||
resource, err := q.Resource.
|
resource, err := q.Resource.
|
||||||
Preload(field.Associations).
|
Preload(field.Associations).
|
||||||
Where(
|
Where(
|
||||||
@@ -60,60 +79,48 @@ func findResource(resourceId int32) (*ResourceView, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrResourceNotExist
|
return nil, ErrResourceNotExist
|
||||||
}
|
}
|
||||||
|
if resource.User == nil {
|
||||||
|
return nil, ErrResourceNotExist
|
||||||
|
}
|
||||||
var info = &ResourceView{
|
var info = &ResourceView{
|
||||||
Id: resource.ID,
|
Id: resource.ID,
|
||||||
|
User: *resource.User,
|
||||||
Active: resource.Active,
|
Active: resource.Active,
|
||||||
Type: resource.Type,
|
Type: resource.Type,
|
||||||
User: resource.User,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch resource.Type {
|
switch resource.Type {
|
||||||
case m.ResourceTypeShort:
|
case m.ResourceTypeShort:
|
||||||
var sub = resource.Short
|
var sub = resource.Short
|
||||||
var dailyLast = time.Time{}
|
info.ShortId = &sub.ID
|
||||||
if sub.DailyLast != nil {
|
info.ExpireAt = sub.ExpireAt
|
||||||
dailyLast = time.Time(*sub.DailyLast)
|
|
||||||
}
|
|
||||||
var expire = time.Time{}
|
|
||||||
if sub.Expire != nil {
|
|
||||||
expire = time.Time(*sub.Expire)
|
|
||||||
}
|
|
||||||
var quota int32
|
|
||||||
if sub.Quota != nil {
|
|
||||||
quota = *sub.Quota
|
|
||||||
}
|
|
||||||
info.Mode = sub.Type
|
|
||||||
info.Live = time.Duration(sub.Live) * time.Second
|
info.Live = time.Duration(sub.Live) * time.Second
|
||||||
info.DailyLimit = sub.DailyLimit
|
info.Mode = sub.Type
|
||||||
info.DailyUsed = sub.DailyUsed
|
info.Quota = sub.Quota
|
||||||
info.DailyLast = dailyLast
|
|
||||||
info.Expire = expire
|
|
||||||
info.Quota = quota
|
|
||||||
info.Used = sub.Used
|
info.Used = sub.Used
|
||||||
|
info.Daily = sub.Daily
|
||||||
|
info.LastAt = sub.LastAt
|
||||||
|
if sub.LastAt != nil && u.IsSameDate(*sub.LastAt, now) {
|
||||||
|
info.Today = int(sub.Daily)
|
||||||
|
}
|
||||||
|
|
||||||
case m.ResourceTypeLong:
|
case m.ResourceTypeLong:
|
||||||
var sub = resource.Long
|
var sub = resource.Long
|
||||||
var dailyLast = time.Time{}
|
info.LongId = &sub.ID
|
||||||
if sub.DailyLast != nil {
|
info.ExpireAt = sub.ExpireAt
|
||||||
dailyLast = time.Time(*sub.DailyLast)
|
info.Live = time.Duration(sub.Live) * time.Hour
|
||||||
}
|
|
||||||
var expire = time.Time{}
|
|
||||||
if sub.Expire != nil {
|
|
||||||
expire = time.Time(*sub.Expire)
|
|
||||||
}
|
|
||||||
var quota int32
|
|
||||||
if sub.Quota != nil {
|
|
||||||
quota = *sub.Quota
|
|
||||||
}
|
|
||||||
info.Mode = sub.Type
|
info.Mode = sub.Type
|
||||||
info.Live = time.Duration(sub.Live) * time.Hour * 24
|
info.Quota = sub.Quota
|
||||||
info.DailyLimit = sub.DailyLimit
|
|
||||||
info.DailyUsed = sub.DailyUsed
|
|
||||||
info.DailyLast = dailyLast
|
|
||||||
info.Expire = expire
|
|
||||||
info.Quota = quota
|
|
||||||
info.Used = sub.Used
|
info.Used = sub.Used
|
||||||
|
info.Daily = sub.Daily
|
||||||
|
info.LastAt = sub.LastAt
|
||||||
|
if sub.LastAt != nil && u.IsSameDate(*sub.LastAt, now) {
|
||||||
|
info.Today = int(sub.Daily)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Mode == m.ResourceModeTime && info.ExpireAt == nil {
|
||||||
|
return nil, errors.New("检查套餐获取时间失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
return info, nil
|
return info, nil
|
||||||
@@ -122,47 +129,137 @@ func findResource(resourceId int32) (*ResourceView, error) {
|
|||||||
// ResourceView 套餐数据的简化视图,便于直接获取主要数据
|
// ResourceView 套餐数据的简化视图,便于直接获取主要数据
|
||||||
type ResourceView struct {
|
type ResourceView struct {
|
||||||
Id int32
|
Id int32
|
||||||
|
User m.User
|
||||||
Active bool
|
Active bool
|
||||||
Type m.ResourceType
|
Type m.ResourceType
|
||||||
Mode m.ResourceMode
|
ShortId *int32
|
||||||
|
LongId *int32
|
||||||
Live time.Duration
|
Live time.Duration
|
||||||
DailyLimit int32
|
Mode m.ResourceMode
|
||||||
DailyUsed int32
|
|
||||||
DailyLast time.Time
|
|
||||||
Quota int32
|
Quota int32
|
||||||
|
ExpireAt *time.Time
|
||||||
Used int32
|
Used int32
|
||||||
Expire time.Time
|
Daily int32
|
||||||
User m.User
|
LastAt *time.Time
|
||||||
|
Today int // 今日用量
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否可提取
|
||||||
|
func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*ResourceView, []string, error) {
|
||||||
|
if count > 400 {
|
||||||
|
return nil, nil, core.NewBizErr("单次最多提取 400 个")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户套餐
|
||||||
|
resource, err := findResource(resourceId, now)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户
|
||||||
|
user := resource.User
|
||||||
|
if user.IDToken == nil || *user.IDToken == "" {
|
||||||
|
return nil, nil, core.NewBizErr("账号未实名")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户白名单并检查用户 ip 地址
|
||||||
|
whitelists, err := q.Whitelist.Where(
|
||||||
|
q.Whitelist.UserID.Eq(user.ID),
|
||||||
|
).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ips := make([]string, len(whitelists))
|
||||||
|
pass := false
|
||||||
|
for i, item := range whitelists {
|
||||||
|
ips[i] = item.IP.String()
|
||||||
|
if item.IP.Addr == source {
|
||||||
|
pass = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pass {
|
||||||
|
return nil, nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查套餐使用情况
|
||||||
|
switch resource.Mode {
|
||||||
|
default:
|
||||||
|
return nil, nil, core.NewBizErr("不支持的套餐模式")
|
||||||
|
|
||||||
|
// 包时
|
||||||
|
case m.ResourceModeTime:
|
||||||
|
// 检查过期时间
|
||||||
|
if resource.ExpireAt.Before(now) {
|
||||||
|
return nil, nil, ErrResourceExpired
|
||||||
|
}
|
||||||
|
// 检查每日限额
|
||||||
|
if count+resource.Today > int(resource.Quota) {
|
||||||
|
return nil, nil, ErrResourceDailyLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包量
|
||||||
|
case m.ResourceModeQuota:
|
||||||
|
// 检查可用配额
|
||||||
|
if int(resource.Quota)-int(resource.Used) < count {
|
||||||
|
return nil, nil, ErrResourceExhausted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resource, ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
allChansKey = "channel:all"
|
|
||||||
freeChansKey = "channel:free"
|
freeChansKey = "channel:free"
|
||||||
usedChansKey = "channel:used"
|
usedChansKey = "channel:used"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 扩容通道
|
||||||
|
func regChans(proxy int32, chans []netip.AddrPort) error {
|
||||||
|
strs := make([]any, len(chans))
|
||||||
|
for i, ch := range chans {
|
||||||
|
strs[i] = ch.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
key := freeChansKey + ":" + strconv.Itoa(int(proxy))
|
||||||
|
err := g.Redis.SAdd(context.Background(), key, strs...).Err()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("扩容通道失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩容通道
|
||||||
|
func remChans(proxy int32) error {
|
||||||
|
key := freeChansKey + ":" + strconv.Itoa(int(proxy))
|
||||||
|
err := g.Redis.SRem(context.Background(), key).Err()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("缩容通道失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 取用通道
|
// 取用通道
|
||||||
func lockChans(batch string, count int, expire time.Time) ([]netip.AddrPort, error) {
|
func lockChans(proxy int32, batch string, count int) ([]netip.AddrPort, error) {
|
||||||
chans, err := g.Redis.Eval(
|
pid := strconv.Itoa(int(proxy))
|
||||||
|
chans, err := RedisScriptLockChans.Run(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
RedisScriptLockChans,
|
g.Redis,
|
||||||
[]string{
|
[]string{
|
||||||
freeChansKey,
|
freeChansKey + ":" + pid,
|
||||||
usedChansKey,
|
usedChansKey + ":" + pid + ":" + batch,
|
||||||
usedChansKey + ":" + batch,
|
|
||||||
},
|
},
|
||||||
count,
|
count,
|
||||||
expire.Unix(),
|
|
||||||
).StringSlice()
|
).StringSlice()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, core.NewBizErr("获取通道失败", err)
|
return nil, fmt.Errorf("获取通道失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs := make([]netip.AddrPort, len(chans))
|
addrs := make([]netip.AddrPort, len(chans))
|
||||||
for i, ch := range chans {
|
for i, ch := range chans {
|
||||||
addr, err := netip.ParseAddrPort(ch)
|
addr, err := netip.ParseAddrPort(ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, core.NewServErr("解析通道数据失败", err)
|
return nil, fmt.Errorf("解析通道数据失败: %w", err)
|
||||||
}
|
}
|
||||||
addrs[i] = addr
|
addrs[i] = addr
|
||||||
}
|
}
|
||||||
@@ -170,41 +267,31 @@ func lockChans(batch string, count int, expire time.Time) ([]netip.AddrPort, err
|
|||||||
return addrs, nil
|
return addrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var RedisScriptLockChans = `
|
var RedisScriptLockChans = redis.NewScript(`
|
||||||
local free_key = KEYS[1]
|
local free_key = KEYS[1]
|
||||||
local used_key = KEYS[2]
|
local batch_key = KEYS[2]
|
||||||
local batch_key = KEYS[3]
|
|
||||||
local count = tonumber(ARGV[1])
|
local count = tonumber(ARGV[1])
|
||||||
local expire = tonumber(ARGV[2])
|
|
||||||
|
|
||||||
if redis.call("SCARD", free_key) < count then
|
if redis.call("SCARD", free_key) < count then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local ports = redis.call("SPOP", free_key, count)
|
local ports = redis.call("SPOP", free_key, count)
|
||||||
redis.call("ZADD", used_key, expire, batch_key)
|
|
||||||
redis.call("RPUSH", batch_key, unpack(ports))
|
redis.call("RPUSH", batch_key, unpack(ports))
|
||||||
|
|
||||||
return ports
|
return ports
|
||||||
`
|
`)
|
||||||
|
|
||||||
// 归还通道
|
// 归还通道
|
||||||
func freeChans(batch string, chans []string) error {
|
func freeChans(proxy int32, batch string) error {
|
||||||
values := make([]any, len(chans))
|
pid := strconv.Itoa(int(proxy))
|
||||||
for i, ch := range chans {
|
err := RedisScriptFreeChans.Run(
|
||||||
values[i] = ch
|
|
||||||
}
|
|
||||||
|
|
||||||
err := g.Redis.Eval(
|
|
||||||
context.Background(),
|
context.Background(),
|
||||||
RedisScriptFreeChans,
|
g.Redis,
|
||||||
[]string{
|
[]string{
|
||||||
freeChansKey,
|
freeChansKey + ":" + pid,
|
||||||
usedChansKey,
|
usedChansKey + ":" + pid + ":" + batch,
|
||||||
usedChansKey + ":" + batch,
|
|
||||||
allChansKey,
|
|
||||||
},
|
},
|
||||||
values...,
|
|
||||||
).Err()
|
).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewBizErr("释放通道失败", err)
|
return core.NewBizErr("释放通道失败", err)
|
||||||
@@ -213,88 +300,19 @@ func freeChans(batch string, chans []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var RedisScriptFreeChans = `
|
var RedisScriptFreeChans = redis.NewScript(`
|
||||||
local free_key = KEYS[1]
|
local free_key = KEYS[1]
|
||||||
local used_key = KEYS[2]
|
local batch_key = KEYS[2]
|
||||||
local batch_key = KEYS[3]
|
|
||||||
local all_key = KEYS[4]
|
|
||||||
local chans = ARGV
|
|
||||||
|
|
||||||
local count = 0
|
local chans = redis.call("LRANGE", batch_key, 0, -1)
|
||||||
for i, chan in ipairs(chans) do
|
|
||||||
if redis.call("SISMEMBER", all_key, chan) == 1 then
|
|
||||||
redis.call("SADD", free_key, chan)
|
|
||||||
count = count + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
redis.call("ZREM", used_key, batch_key)
|
|
||||||
redis.call("DEL", batch_key)
|
redis.call("DEL", batch_key)
|
||||||
|
|
||||||
return count
|
if redis.call("EXISTS", free_key) == 1 then
|
||||||
`
|
redis.call("SADD", free_key, unpack(chans))
|
||||||
|
end
|
||||||
// 扩容通道
|
|
||||||
func addChans(chans []netip.AddrPort) error {
|
|
||||||
strs := make([]string, len(chans))
|
|
||||||
for i, ch := range chans {
|
|
||||||
strs[i] = ch.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
err := g.Redis.Eval(
|
|
||||||
context.Background(),
|
|
||||||
RedisScriptAddChans,
|
|
||||||
[]string{
|
|
||||||
freeChansKey,
|
|
||||||
allChansKey,
|
|
||||||
},
|
|
||||||
strs,
|
|
||||||
).Err()
|
|
||||||
if err != nil {
|
|
||||||
return core.NewBizErr("扩容通道失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var RedisScriptAddChans = `
|
|
||||||
local free_key = KEYS[1]
|
|
||||||
local all_key = KEYS[2]
|
|
||||||
local chans = ARGV
|
|
||||||
|
|
||||||
redis.call("SADD", free_key, unpack(chans))
|
|
||||||
redis.call("SADD", all_key, unpack(chans))
|
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
`
|
`)
|
||||||
|
|
||||||
// 缩容通道
|
|
||||||
func removeChans(chans []string) error {
|
|
||||||
err := g.Redis.Eval(
|
|
||||||
context.Background(),
|
|
||||||
RedisScriptRemoveChans,
|
|
||||||
[]string{
|
|
||||||
freeChansKey,
|
|
||||||
allChansKey,
|
|
||||||
},
|
|
||||||
chans,
|
|
||||||
).Err()
|
|
||||||
if err != nil {
|
|
||||||
return core.NewBizErr("缩容通道失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var RedisScriptRemoveChans = `
|
|
||||||
local free_key = KEYS[1]
|
|
||||||
local all_key = KEYS[2]
|
|
||||||
local chans = ARGV
|
|
||||||
|
|
||||||
redis.call("SREM", free_key, unpack(chans))
|
|
||||||
redis.call("SREM", all_key, unpack(chans))
|
|
||||||
|
|
||||||
return 1
|
|
||||||
`
|
|
||||||
|
|
||||||
// 错误信息
|
// 错误信息
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -18,16 +17,13 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
|
"gorm.io/gen"
|
||||||
"gorm.io/gen/field"
|
"gorm.io/gen/field"
|
||||||
)
|
)
|
||||||
|
|
||||||
type channelBaiyinService struct{}
|
type channelBaiyinProvider struct{}
|
||||||
|
|
||||||
func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
|
|
||||||
if count > 400 {
|
|
||||||
return nil, core.NewBizErr("单次最多提取 400 个")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
func (s *channelBaiyinProvider) CreateChannels(source netip.Addr, resourceId int32, authWhitelist bool, authPassword bool, count int, edgeFilter ...EdgeFilter) ([]*m.Channel, error) {
|
||||||
var filter *EdgeFilter = nil
|
var filter *EdgeFilter = nil
|
||||||
if len(edgeFilter) > 0 {
|
if len(edgeFilter) > 0 {
|
||||||
filter = &edgeFilter[0]
|
filter = &edgeFilter[0]
|
||||||
@@ -36,111 +32,180 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
batch := ID.GenReadable("bat")
|
batch := ID.GenReadable("bat")
|
||||||
|
|
||||||
// 获取用户套餐
|
// 检查并获取套餐与白名单
|
||||||
resource, err := findResource(resourceId)
|
resource, whitelists, err := ensure(now, source, resourceId, count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户
|
|
||||||
user := resource.User
|
user := resource.User
|
||||||
if user.IDToken == nil || *user.IDToken == "" {
|
|
||||||
return nil, core.NewBizErr("账号未实名")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户白名单并检查用户 ip 地址
|
|
||||||
whitelists, err := q.Whitelist.Where(
|
|
||||||
q.Whitelist.UserID.Eq(user.ID),
|
|
||||||
).Find()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
whitelistIPs := make([]string, len(whitelists))
|
|
||||||
pass := false
|
|
||||||
for i, item := range whitelists {
|
|
||||||
whitelistIPs[i] = item.IP.String()
|
|
||||||
if item.IP.Addr == source {
|
|
||||||
pass = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !pass {
|
|
||||||
return nil, core.NewBizErr(fmt.Sprintf("IP 地址 %s 不在白名单内", source.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查套餐使用情况
|
|
||||||
switch resource.Mode {
|
|
||||||
default:
|
|
||||||
return nil, core.NewBizErr("不支持的套餐模式")
|
|
||||||
|
|
||||||
// 包时
|
|
||||||
case m.ResourceModeTime:
|
|
||||||
// 检查过期时间
|
|
||||||
if resource.Expire.Before(now) {
|
|
||||||
return nil, ErrResourceExpired
|
|
||||||
}
|
|
||||||
// 检查每日限额
|
|
||||||
used := 0
|
|
||||||
if now.Format("2006-01-02") == resource.DailyLast.Format("2006-01-02") {
|
|
||||||
used = int(resource.DailyUsed)
|
|
||||||
}
|
|
||||||
excess := used+count > int(resource.DailyLimit)
|
|
||||||
if excess {
|
|
||||||
return nil, ErrResourceDailyLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
// 包量
|
|
||||||
case m.ResourceModeQuota:
|
|
||||||
// 检查可用配额
|
|
||||||
if int(resource.Quota)-int(resource.Used) < count {
|
|
||||||
return nil, ErrResourceExhausted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expire := now.Add(resource.Live)
|
expire := now.Add(resource.Live)
|
||||||
|
|
||||||
// 获取可用通道
|
// 选择代理
|
||||||
chans, err := lockChans(batch, count, expire)
|
proxyResult := struct {
|
||||||
if err != nil {
|
m.Proxy
|
||||||
return nil, err
|
Count int
|
||||||
}
|
}{}
|
||||||
|
err = q.Proxy.
|
||||||
// 获取对应代理
|
LeftJoin(q.Channel, q.Channel.ProxyID.EqCol(q.Proxy.ID), q.Channel.ExpiredAt.Gt(now)).
|
||||||
ips := make([]driver.Valuer, 0)
|
Select(q.Proxy.ALL, field.NewUnsafeFieldRaw("10000 - count(*)").As("count")).
|
||||||
findProxy := make(map[orm.Inet]*m.Proxy)
|
Where(
|
||||||
for _, ch := range chans {
|
|
||||||
ip := orm.Inet{Addr: ch.Addr()}
|
|
||||||
if _, ok := findProxy[ip]; !ok {
|
|
||||||
ips = append(ips, ip)
|
|
||||||
findProxy[ip] = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies, err := q.Proxy.Where(
|
|
||||||
q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)),
|
q.Proxy.Type.Eq(int(m.ProxyTypeBaiYin)),
|
||||||
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
|
q.Proxy.Status.Eq(int(m.ProxyStatusOnline)),
|
||||||
q.Proxy.IP.In(ips...),
|
).
|
||||||
).Find()
|
Group(q.Proxy.ID).
|
||||||
|
Order(field.NewField("", "count")).
|
||||||
|
Limit(1).Scan(&proxyResult)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, core.NewBizErr("获取代理失败", err)
|
return nil, core.NewBizErr("获取可用代理失败", err)
|
||||||
|
}
|
||||||
|
if proxyResult.Count < count {
|
||||||
|
return nil, core.NewBizErr("无可用主机,请稍后再试")
|
||||||
|
}
|
||||||
|
proxy := proxyResult.Proxy
|
||||||
|
|
||||||
|
// 获取可用通道
|
||||||
|
chans, err := lockChans(proxy.ID, batch, count)
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewBizErr("无可用通道,请稍后再试", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
groups := make(map[*m.Proxy][]*m.Channel)
|
// 获取可用节点
|
||||||
for _, proxy := range proxies {
|
edgesResp, err := g.Cloud.CloudEdges(&g.CloudEdgesReq{
|
||||||
findProxy[proxy.IP] = proxy
|
Province: filter.Prov,
|
||||||
groups[proxy] = make([]*m.Channel, 0)
|
City: filter.City,
|
||||||
|
Isp: u.X(filter.Isp.String()),
|
||||||
|
Limit: &count,
|
||||||
|
NoRepeat: u.P(true),
|
||||||
|
NoDayRepeat: u.P(true),
|
||||||
|
ActiveTime: u.P(3600),
|
||||||
|
IpUnchangedTime: u.P(3600),
|
||||||
|
Sort: u.P("ip_unchanged_time_asc"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewBizErr("获取可用节点失败", err)
|
||||||
}
|
}
|
||||||
|
if edgesResp.Total != count && len(edgesResp.Edges) != count {
|
||||||
|
return nil, core.NewBizErr("地区可用节点数量不足 [%s, %s] [%s]")
|
||||||
|
}
|
||||||
|
edges := edgesResp.Edges
|
||||||
|
|
||||||
// 准备通道数据
|
// 准备通道数据
|
||||||
actions := make([]*m.LogsUserUsage, len(chans))
|
channels := make([]*m.Channel, count)
|
||||||
channels := make([]*m.Channel, len(chans))
|
chanConfigs := make([]*g.PortConfigsReq, count)
|
||||||
for i, ch := range chans {
|
edgeConfigs := make([]string, count)
|
||||||
|
for i := range count {
|
||||||
|
ch := chans[i]
|
||||||
|
edge := edges[i]
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, core.NewBizErr("解析通道地址失败", err)
|
return nil, core.NewBizErr("解析通道地址失败", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用记录
|
// 通道数据
|
||||||
actions[i] = &m.LogsUserUsage{
|
channels[i] = &m.Channel{
|
||||||
|
UserID: user.ID,
|
||||||
|
ResourceID: resourceId,
|
||||||
|
BatchNo: batch,
|
||||||
|
ProxyID: proxy.ID,
|
||||||
|
Host: u.Else(proxy.Host, proxy.IP.String()),
|
||||||
|
Port: ch.Port(),
|
||||||
|
EdgeRef: u.P(edge.EdgeID),
|
||||||
|
FilterISP: filter.Isp,
|
||||||
|
FilterProv: filter.Prov,
|
||||||
|
FilterCity: filter.City,
|
||||||
|
ExpiredAt: expire,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通道配置数据
|
||||||
|
chanConfigs[i] = &g.PortConfigsReq{
|
||||||
|
Port: int(ch.Port()),
|
||||||
|
Status: true,
|
||||||
|
Edge: &[]string{edge.EdgeID},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 白名单模式
|
||||||
|
if authWhitelist {
|
||||||
|
channels[i].Whitelists = u.P(strings.Join(whitelists, ","))
|
||||||
|
chanConfigs[i].Whitelist = &whitelists
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码模式
|
||||||
|
if authPassword {
|
||||||
|
username, password := genPassPair()
|
||||||
|
channels[i].Username = &username
|
||||||
|
channels[i].Password = &password
|
||||||
|
chanConfigs[i].Userpass = u.P(username + ":" + password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接配置数据
|
||||||
|
edgeConfigs[i] = edge.EdgeID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交异步任务关闭通道
|
||||||
|
_, err = g.Asynq.Enqueue(
|
||||||
|
e.NewRemoveChannel(batch),
|
||||||
|
asynq.ProcessAt(expire),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewServErr("提交关闭通道任务失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存数据
|
||||||
|
err = q.Q.Transaction(func(q *q.Query) error {
|
||||||
|
var rs gen.ResultInfo
|
||||||
|
|
||||||
|
// 根据套餐类型和模式更新使用记录
|
||||||
|
isShortType := resource.Type == m.ResourceTypeShort
|
||||||
|
isLongType := resource.Type == m.ResourceTypeLong
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case isShortType:
|
||||||
|
rs, err = q.ResourceShort.
|
||||||
|
Where(
|
||||||
|
q.ResourceShort.ID.Eq(*resource.ShortId),
|
||||||
|
q.ResourceShort.Used.Eq(resource.Used),
|
||||||
|
q.ResourceShort.Daily.Eq(resource.Daily),
|
||||||
|
).
|
||||||
|
UpdateSimple(
|
||||||
|
q.ResourceShort.Used.Add(int32(count)),
|
||||||
|
q.ResourceShort.Daily.Value(int32(resource.Today+count)),
|
||||||
|
q.ResourceShort.LastAt.Value(now),
|
||||||
|
)
|
||||||
|
|
||||||
|
case isLongType:
|
||||||
|
rs, err = q.ResourceLong.Debug().
|
||||||
|
Where(
|
||||||
|
q.ResourceLong.ID.Eq(*resource.LongId),
|
||||||
|
q.ResourceLong.Used.Eq(resource.Used),
|
||||||
|
q.ResourceLong.Daily.Eq(resource.Daily),
|
||||||
|
).
|
||||||
|
UpdateSimple(
|
||||||
|
q.ResourceLong.Used.Add(int32(count)),
|
||||||
|
q.ResourceLong.Daily.Value(int32(resource.Today+count)),
|
||||||
|
q.ResourceLong.LastAt.Value(now),
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return core.NewServErr("套餐类型不正确,无法更新", nil)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return core.NewServErr("更新套餐使用记录失败", err)
|
||||||
|
}
|
||||||
|
if rs.RowsAffected == 0 {
|
||||||
|
return core.NewServErr("套餐使用记录不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存通道
|
||||||
|
err = q.Channel.
|
||||||
|
Omit(field.AssociationFields).
|
||||||
|
Create(channels...)
|
||||||
|
if err != nil {
|
||||||
|
return core.NewServErr("保存通道失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存提取记录
|
||||||
|
err = q.LogsUserUsage.Create(&m.LogsUserUsage{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
ResourceID: resourceId,
|
ResourceID: resourceId,
|
||||||
BatchNo: batch,
|
BatchNo: batch,
|
||||||
@@ -150,86 +215,7 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
|
|||||||
City: filter.City,
|
City: filter.City,
|
||||||
IP: orm.Inet{Addr: source},
|
IP: orm.Inet{Addr: source},
|
||||||
Time: now,
|
Time: now,
|
||||||
}
|
})
|
||||||
|
|
||||||
// 通道数据
|
|
||||||
inet := orm.Inet{Addr: ch.Addr()}
|
|
||||||
channels[i] = &m.Channel{
|
|
||||||
UserID: user.ID,
|
|
||||||
ResourceID: resourceId,
|
|
||||||
BatchNo: batch,
|
|
||||||
ProxyID: findProxy[inet].ID,
|
|
||||||
Port: ch.Port(),
|
|
||||||
FilterISP: filter.Isp,
|
|
||||||
FilterProv: filter.Prov,
|
|
||||||
FilterCity: filter.City,
|
|
||||||
ExpiredAt: expire,
|
|
||||||
Proxy: *findProxy[inet],
|
|
||||||
}
|
|
||||||
if authWhitelist {
|
|
||||||
channels[i].Whitelists = u.P(strings.Join(whitelistIPs, ","))
|
|
||||||
}
|
|
||||||
if authPassword {
|
|
||||||
username, password := genPassPair()
|
|
||||||
channels[i].Username = &username
|
|
||||||
channels[i].Password = &password
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关联代理
|
|
||||||
proxy := findProxy[inet]
|
|
||||||
groups[proxy] = append(groups[proxy], channels[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存数据
|
|
||||||
err = q.Q.Transaction(func(q *q.Query) error {
|
|
||||||
|
|
||||||
// 更新套餐用量
|
|
||||||
used := int32(count)
|
|
||||||
if u.IsSameDate(now, resource.DailyLast) {
|
|
||||||
used += resource.DailyUsed
|
|
||||||
}
|
|
||||||
|
|
||||||
switch resource.Type {
|
|
||||||
case m.ResourceTypeShort:
|
|
||||||
_, err = q.ResourceShort.
|
|
||||||
Where(
|
|
||||||
q.ResourceShort.ResourceID.Eq(resource.Id),
|
|
||||||
q.ResourceShort.Used.Eq(resource.Used),
|
|
||||||
q.ResourceShort.DailyUsed.Eq(resource.DailyUsed),
|
|
||||||
q.ResourceShort.DailyLast.Eq(resource.DailyLast),
|
|
||||||
).
|
|
||||||
UpdateSimple(
|
|
||||||
q.ResourceShort.Used.Add(int32(count)),
|
|
||||||
q.ResourceShort.DailyUsed.Value(used),
|
|
||||||
q.ResourceShort.DailyLast.Value(now),
|
|
||||||
)
|
|
||||||
case m.ResourceTypeLong:
|
|
||||||
_, err = q.ResourceLong.
|
|
||||||
Where(
|
|
||||||
q.ResourceLong.ResourceID.Eq(resource.Id),
|
|
||||||
q.ResourceLong.Used.Eq(resource.Used),
|
|
||||||
q.ResourceLong.DailyUsed.Eq(resource.DailyUsed),
|
|
||||||
q.ResourceLong.DailyLast.Eq(resource.DailyLast),
|
|
||||||
).
|
|
||||||
UpdateSimple(
|
|
||||||
q.ResourceLong.Used.Add(int32(count)),
|
|
||||||
q.ResourceLong.DailyUsed.Value(used),
|
|
||||||
q.ResourceLong.DailyLast.Value(now),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return core.NewServErr("更新套餐使用记录失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存通道和分配记录
|
|
||||||
err = q.Channel.
|
|
||||||
Omit(field.AssociationFields).
|
|
||||||
Create(channels...)
|
|
||||||
if err != nil {
|
|
||||||
return core.NewServErr("保存通道失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = q.LogsUserUsage.Create(actions...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("保存用户使用记录失败", err)
|
return core.NewServErr("保存用户使用记录失败", err)
|
||||||
}
|
}
|
||||||
@@ -240,118 +226,100 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交异步任务关闭通道
|
|
||||||
_, err = g.Asynq.Enqueue(
|
|
||||||
e.NewRemoveChannel(e.RemoveChannelData{
|
|
||||||
Batch: batch,
|
|
||||||
IDs: core.GetIDs(channels),
|
|
||||||
}),
|
|
||||||
asynq.ProcessAt(expire),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, core.NewServErr("提交关闭通道任务失败", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交配置
|
// 提交配置
|
||||||
for proxy, chanels := range groups {
|
|
||||||
secret := strings.Split(u.Z(proxy.Secret), ":")
|
secret := strings.Split(u.Z(proxy.Secret), ":")
|
||||||
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
|
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
|
||||||
|
|
||||||
configs := make([]g.PortConfigsReq, len(chanels))
|
|
||||||
for i, channel := range chanels {
|
|
||||||
configs[i] = g.PortConfigsReq{
|
|
||||||
Port: int(channel.Port),
|
|
||||||
Status: true,
|
|
||||||
AutoEdgeConfig: &g.AutoEdgeConfig{
|
|
||||||
Isp: channel.FilterISP.String(),
|
|
||||||
Province: u.Z(channel.FilterProv),
|
|
||||||
City: u.Z(channel.FilterCity),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if authWhitelist {
|
|
||||||
configs[i].Whitelist = &whitelistIPs
|
|
||||||
}
|
|
||||||
if authPassword {
|
|
||||||
configs[i].Userpass = u.P(fmt.Sprintf("%s:%s", *channel.Username, *channel.Password))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if env.DebugExternalChange {
|
if env.DebugExternalChange {
|
||||||
err := gateway.GatewayPortConfigs(configs)
|
|
||||||
|
// 连接节点到网关
|
||||||
|
err = g.Cloud.CloudConnect(&g.CloudConnectReq{
|
||||||
|
Uuid: proxy.Mac,
|
||||||
|
Edge: &edgeConfigs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, core.NewServErr("连接云平台失败", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用网关代理通道
|
||||||
|
err = gateway.GatewayPortConfigs(chanConfigs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
|
return nil, core.NewServErr(fmt.Sprintf("配置代理 %s 端口失败", proxy.IP.String()), err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bytes, _ := json.Marshal(configs)
|
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String())
|
||||||
slog.Debug("提交代理端口配置", "proxy", proxy.IP.String(), "config", string(bytes))
|
for _, item := range chanConfigs {
|
||||||
|
str, _ := json.Marshal(item)
|
||||||
|
fmt.Println(string(str))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return channels, nil
|
return channels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *channelBaiyinService) RemoveChannels(batch string, ids []int32) error {
|
func (s *channelBaiyinProvider) RemoveChannels(batch string) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
// 获取连接数据
|
// 获取连接数据
|
||||||
channels, err := q.Channel.
|
channels, err := q.Channel.Where(q.Channel.BatchNo.Eq(batch)).Find()
|
||||||
Preload(q.Channel.Proxy).
|
|
||||||
Where(q.Channel.ID.In(ids...)).
|
|
||||||
Find()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("获取通道数据失败", err)
|
return core.NewServErr(fmt.Sprintf("获取通道数据失败,batch:%s", batch), err)
|
||||||
}
|
}
|
||||||
if len(channels) != len(ids) {
|
if len(channels) == 0 {
|
||||||
return core.NewServErr("获取通道数据不完整", err)
|
slog.Warn(fmt.Sprintf("未找到通道数据,batch:%s", batch))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
proxies := make(map[string]*m.Proxy, len(channels))
|
proxy, err := q.Proxy.Where(q.Proxy.ID.Eq(channels[0].ProxyID)).Take()
|
||||||
groups := make(map[string][]*m.Channel, len(channels))
|
|
||||||
chans := make([]string, len(channels))
|
|
||||||
for i, channel := range channels {
|
|
||||||
ip := channel.Proxy.IP.String()
|
|
||||||
groups[ip] = append(groups[ip], channel)
|
|
||||||
proxies[ip] = &channel.Proxy
|
|
||||||
chans[i] = fmt.Sprintf("%s:%d", ip, channel.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs := make([]netip.AddrPort, len(channels))
|
|
||||||
for i, channel := range channels {
|
|
||||||
addrs[i] = netip.AddrPortFrom(channel.Proxy.IP.Addr, channel.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放端口
|
|
||||||
err = freeChans(batch, chans)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return core.NewServErr(fmt.Sprintf("获取代理数据失败,batch:%s", batch), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空配置
|
// 准备配置数据
|
||||||
for ip, channels := range groups {
|
edgeConfigs := make([]string, len(channels))
|
||||||
proxy := proxies[ip]
|
configs := make([]*g.PortConfigsReq, len(channels))
|
||||||
secret := strings.Split(*proxy.Secret, ":")
|
|
||||||
gateway := g.NewGateway(ip, secret[0], secret[1])
|
|
||||||
|
|
||||||
configs := make([]g.PortConfigsReq, len(channels))
|
|
||||||
for i, channel := range channels {
|
for i, channel := range channels {
|
||||||
configs[i] = g.PortConfigsReq{
|
if channel.EdgeRef != nil {
|
||||||
|
edgeConfigs[i] = *channel.EdgeRef
|
||||||
|
} else {
|
||||||
|
slog.Warn(fmt.Sprintf("通道 %d 没有保存节点引用", channel.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
configs[i] = &g.PortConfigsReq{
|
||||||
Status: false,
|
Status: false,
|
||||||
Port: int(channel.Port),
|
Port: int(channel.Port),
|
||||||
Edge: &[]string{},
|
Edge: &[]string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提交配置
|
||||||
if env.DebugExternalChange {
|
if env.DebugExternalChange {
|
||||||
|
// 断开节点连接
|
||||||
|
g.Cloud.CloudDisconnect(&g.CloudDisconnectReq{
|
||||||
|
Uuid: proxy.Mac,
|
||||||
|
Edge: &edgeConfigs,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清空通道配置
|
||||||
|
secret := strings.Split(*proxy.Secret, ":")
|
||||||
|
gateway := g.NewGateway(proxy.IP.String(), secret[0], secret[1])
|
||||||
err := gateway.GatewayPortConfigs(configs)
|
err := gateway.GatewayPortConfigs(configs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
return core.NewServErr(fmt.Sprintf("清空代理 %s 端口配置失败", proxy.IP.String()), err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bytes, _ := json.Marshal(configs)
|
slog.Debug("清除代理端口配置", "proxy", proxy.IP)
|
||||||
slog.Debug("清除代理端口配置", "proxy", ip, "config", string(bytes))
|
for _, item := range configs {
|
||||||
|
str, _ := json.Marshal(item)
|
||||||
|
fmt.Println(string(str))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 释放端口
|
||||||
|
err = freeChans(proxy.ID, batch)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
slog.Debug("清除代理端口配置", "time", time.Since(start).String())
|
slog.Debug("清除代理端口配置", "time", time.Since(start).String())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,48 +44,28 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *IdService) GenSerial() (string, error) {
|
func (s *IdService) GenSerial() (string, error) {
|
||||||
var ctx = context.Background()
|
|
||||||
|
|
||||||
// 构造Redis键
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
key := idSerialKey(now)
|
|
||||||
|
|
||||||
// 使用Redis事务确保原子操作
|
// 脚本实现原子操作
|
||||||
var sequence int64
|
script := redis.NewScript(`
|
||||||
err := g.Redis.Watch(ctx, func(tx *redis.Tx) error {
|
local current = tonumber(redis.call('GET', KEYS[1])) or 0
|
||||||
|
if current >= tonumber(ARGV[1]) then
|
||||||
|
return redis.error_reply('sequence overflow')
|
||||||
|
end
|
||||||
|
|
||||||
// 获取当前序列号
|
local sequence = current + 1
|
||||||
currentVal, err := tx.Get(ctx, key).Int64()
|
redis.call('SET', KEYS[1], sequence, 'EX', ARGV[2])
|
||||||
if err != nil && !errors.Is(err, redis.Nil) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, redis.Nil) {
|
return sequence
|
||||||
currentVal = 0
|
`)
|
||||||
}
|
sequence, err := script.Run(context.Background(), g.Redis, []string{idSerialKey(now)}, maxSequence, redisTTL).Int64()
|
||||||
sequence = currentVal + 1
|
|
||||||
|
|
||||||
// 检查序列号是否溢出
|
|
||||||
if sequence > maxSequence {
|
|
||||||
return ErrSequenceOverflow
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将更新后的序列号保存回Redis,设置5秒过期时间
|
|
||||||
pipe := tx.Pipeline()
|
|
||||||
pipe.Set(ctx, key, sequence, redisTTL*time.Second)
|
|
||||||
_, err = pipe.Exec(ctx)
|
|
||||||
return err
|
|
||||||
}, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组装最终ID
|
// 组装最终ID
|
||||||
id := uint64((now << timestampShift) | sequence)
|
id := uint64((now << timestampShift) | sequence)
|
||||||
|
return strconv.FormatUint(id, 10), nil
|
||||||
idStr := strconv.FormatUint(id, 10)
|
|
||||||
|
|
||||||
return idStr, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseSerial 解析ID,返回其组成部分
|
// ParseSerial 解析ID,返回其组成部分
|
||||||
@@ -98,7 +78,7 @@ func (s *IdService) ParseSerial(id uint64) (timestamp int64, sequence int64) {
|
|||||||
|
|
||||||
// idSerialKey 根据时间戳生成Redis键
|
// idSerialKey 根据时间戳生成Redis键
|
||||||
func idSerialKey(timestamp int64) string {
|
func idSerialKey(timestamp int64) string {
|
||||||
return fmt.Sprintf("global:id:serial:%d", timestamp)
|
return fmt.Sprintf("id:serial:%d", timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|||||||
@@ -31,28 +31,29 @@ func (s *proxyService) AllProxies(proxyType m.ProxyType, channels bool) ([]*m.Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RegisterBaiyin 注册新代理服务
|
// RegisterBaiyin 注册新代理服务
|
||||||
func (s *proxyService) RegisterBaiyin(Mac string, IP netip.Addr, username, password string) error {
|
func (s *proxyService) RegisterBaiyin(Name string, IP netip.Addr, username, password string) error {
|
||||||
|
|
||||||
|
// 保存代理信息
|
||||||
|
proxy := &m.Proxy{
|
||||||
|
Version: 0,
|
||||||
|
Mac: Name,
|
||||||
|
IP: orm.Inet{Addr: IP},
|
||||||
|
Secret: u.P(fmt.Sprintf("%s:%s", username, password)),
|
||||||
|
Type: m.ProxyTypeBaiYin,
|
||||||
|
Status: m.ProxyStatusOnline,
|
||||||
|
}
|
||||||
|
if err := q.Proxy.Create(proxy); err != nil {
|
||||||
|
return core.NewServErr("保存通道数据失败")
|
||||||
|
}
|
||||||
|
|
||||||
// 添加可用通道到 redis
|
// 添加可用通道到 redis
|
||||||
chans := make([]netip.AddrPort, 10000)
|
chans := make([]netip.AddrPort, 10000)
|
||||||
for i := range 10000 {
|
for i := range 10000 {
|
||||||
chans[i] = netip.AddrPortFrom(IP, uint16(i+10000))
|
chans[i] = netip.AddrPortFrom(IP, uint16(i+10000))
|
||||||
}
|
}
|
||||||
err := addChans(chans)
|
err := regChans(proxy.ID, chans)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("添加通道失败")
|
return core.NewServErr("添加通道失败", err)
|
||||||
}
|
|
||||||
|
|
||||||
// 保存代理信息
|
|
||||||
if err := q.Proxy.Create(&m.Proxy{
|
|
||||||
Version: 0,
|
|
||||||
Mac: Mac,
|
|
||||||
IP: orm.Inet{Addr: IP},
|
|
||||||
Secret: u.P(fmt.Sprintf("%s:%s", username, password)),
|
|
||||||
Type: m.ProxyTypeBaiYin,
|
|
||||||
Status: m.ProxyStatusOnline,
|
|
||||||
}); err != nil {
|
|
||||||
return core.NewServErr("保存通道数据失败")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"platform/pkg/u"
|
"platform/pkg/u"
|
||||||
"platform/web/core"
|
"platform/web/core"
|
||||||
g "platform/web/globals"
|
|
||||||
m "platform/web/models"
|
m "platform/web/models"
|
||||||
q "platform/web/queries"
|
q "platform/web/queries"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,8 +18,7 @@ var Resource = &resourceService{}
|
|||||||
type resourceService struct{}
|
type resourceService struct{}
|
||||||
|
|
||||||
func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data *CreateResourceData) error {
|
func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data *CreateResourceData) error {
|
||||||
return g.Redsync.WithLock(userBalanceKey(uid), func() error {
|
|
||||||
return q.Q.Transaction(func(q *q.Query) error {
|
|
||||||
// 找到用户
|
// 找到用户
|
||||||
user, err := q.User.
|
user, err := q.User.
|
||||||
Where(q.User.ID.Eq(uid)).
|
Where(q.User.ID.Eq(uid)).
|
||||||
@@ -29,15 +28,24 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查余额
|
// 检查余额
|
||||||
var amount = user.Balance.Sub(data.GetAmount())
|
amount, err := data.GetAmount()
|
||||||
if amount.IsNegative() {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newBalance := user.Balance.Sub(amount)
|
||||||
|
if newBalance.IsNegative() {
|
||||||
return ErrBalanceNotEnough
|
return ErrBalanceNotEnough
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return q.Q.Transaction(func(q *q.Query) error {
|
||||||
|
|
||||||
// 更新用户余额
|
// 更新用户余额
|
||||||
_, err = q.User.
|
_, err = q.User.
|
||||||
Where(q.User.ID.Eq(uid), q.User.Balance.Eq(user.Balance)).
|
Where(
|
||||||
UpdateSimple(q.User.Balance.Value(amount))
|
q.User.ID.Eq(uid),
|
||||||
|
q.User.Balance.Eq(user.Balance),
|
||||||
|
).
|
||||||
|
UpdateSimple(q.User.Balance.Value(newBalance))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("更新用户余额失败", err)
|
return core.NewServErr("更新用户余额失败", err)
|
||||||
}
|
}
|
||||||
@@ -49,17 +57,27 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成账单
|
// 生成账单
|
||||||
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource))
|
subject, err := data.GetSubject()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("生成账单失败", err)
|
return core.NewServErr("生成账单失败", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *CreateResourceData, trade *m.Trade) error {
|
func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *CreateResourceData, trade *m.Trade) error { // 检查交易
|
||||||
|
if trade == nil {
|
||||||
|
return core.NewBizErr("交易数据不能为空")
|
||||||
|
}
|
||||||
|
if trade.Status != m.TradeStatusSuccess {
|
||||||
|
return core.NewBizErr("交易状态不正确")
|
||||||
|
}
|
||||||
|
|
||||||
return q.Q.Transaction(func(q *q.Query) error {
|
return q.Q.Transaction(func(q *q.Query) error {
|
||||||
|
|
||||||
// 保存套餐
|
// 保存套餐
|
||||||
@@ -69,7 +87,15 @@ func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成账单
|
// 生成账单
|
||||||
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource, trade))
|
subject, err := data.GetSubject()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
amount, err := data.GetAmount()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource, trade))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("生成账单失败", err)
|
return core.NewServErr("生成账单失败", err)
|
||||||
}
|
}
|
||||||
@@ -95,13 +121,17 @@ func createResource(q *q.Query, uid int32, now time.Time, data *CreateResourceDa
|
|||||||
if short == nil {
|
if short == nil {
|
||||||
return nil, core.NewBizErr("短效套餐数据不能为空")
|
return nil, core.NewBizErr("短效套餐数据不能为空")
|
||||||
}
|
}
|
||||||
var duration = time.Duration(short.Expire) * 24 * time.Hour
|
|
||||||
resource.Short = &m.ResourceShort{
|
resource.Short = &m.ResourceShort{
|
||||||
Type: short.Mode,
|
|
||||||
Live: short.Live,
|
Live: short.Live,
|
||||||
Quota: &short.Quota,
|
Type: short.Mode,
|
||||||
Expire: u.P(now.Add(duration)),
|
Quota: short.Quota,
|
||||||
DailyLimit: short.DailyLimit,
|
}
|
||||||
|
if short.Mode == m.ResourceModeTime {
|
||||||
|
if short.Expire == nil {
|
||||||
|
return nil, core.NewBizErr("包时套餐过期时间不能为空")
|
||||||
|
}
|
||||||
|
var duration = time.Duration(*short.Expire) * 24 * time.Hour
|
||||||
|
resource.Short.ExpireAt = u.P(now.Add(duration))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 长效套餐
|
// 长效套餐
|
||||||
@@ -110,13 +140,17 @@ func createResource(q *q.Query, uid int32, now time.Time, data *CreateResourceDa
|
|||||||
if long == nil {
|
if long == nil {
|
||||||
return nil, core.NewBizErr("长效套餐数据不能为空")
|
return nil, core.NewBizErr("长效套餐数据不能为空")
|
||||||
}
|
}
|
||||||
var duration = time.Duration(long.Expire) * 24 * time.Hour
|
|
||||||
resource.Long = &m.ResourceLong{
|
resource.Long = &m.ResourceLong{
|
||||||
Type: long.Mode,
|
|
||||||
Live: long.Live,
|
Live: long.Live,
|
||||||
Quota: &long.Quota,
|
Type: long.Mode,
|
||||||
Expire: u.P(now.Add(duration)),
|
Quota: long.Quota,
|
||||||
DailyLimit: long.DailyLimit,
|
}
|
||||||
|
if long.Mode == m.ResourceModeTime {
|
||||||
|
if long.Expire == nil {
|
||||||
|
return nil, core.NewBizErr("包时套餐过期时间不能为空")
|
||||||
|
}
|
||||||
|
var duration = time.Duration(*long.Expire) * 24 * time.Hour
|
||||||
|
resource.Long.ExpireAt = u.P(now.Add(duration))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, core.NewBizErr("不支持的套餐类型")
|
return nil, core.NewBizErr("不支持的套餐类型")
|
||||||
@@ -139,20 +173,18 @@ type CreateResourceData struct {
|
|||||||
type CreateShortResourceData struct {
|
type CreateShortResourceData struct {
|
||||||
Live int32 `json:"live" validate:"required,min=180"`
|
Live int32 `json:"live" validate:"required,min=180"`
|
||||||
Mode m.ResourceMode `json:"mode" validate:"required"`
|
Mode m.ResourceMode `json:"mode" validate:"required"`
|
||||||
Expire int32 `json:"expire"`
|
Quota int32 `json:"quota" validate:"required"`
|
||||||
DailyLimit int32 `json:"daily_limit" validate:"min=2000"`
|
Expire *int32 `json:"expire"`
|
||||||
Quota int32 `json:"quota" validate:"min=10000"`
|
|
||||||
|
|
||||||
name string
|
name string
|
||||||
price *decimal.Decimal
|
price *decimal.Decimal
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateLongResourceData struct {
|
type CreateLongResourceData struct {
|
||||||
Live int32 `json:"live" validate:"required,oneof=1 4 8 12 24"`
|
Live int32 `json:"live" validate:"required"`
|
||||||
Mode m.ResourceMode `json:"mode" validate:"required,oneof=1 2"`
|
Mode m.ResourceMode `json:"mode" validate:"required"`
|
||||||
Expire int32 `json:"expire"`
|
Quota int32 `json:"quota" validate:"required"`
|
||||||
DailyLimit int32 `json:"daily_limit" validate:"min=100"`
|
Expire *int32 `json:"expire"`
|
||||||
Quota int32 `json:"quota" validate:"min=500"`
|
|
||||||
|
|
||||||
name string
|
name string
|
||||||
price *decimal.Decimal
|
price *decimal.Decimal
|
||||||
@@ -162,24 +194,28 @@ func (c *CreateResourceData) GetType() m.TradeType {
|
|||||||
return m.TradeTypePurchase
|
return m.TradeTypePurchase
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CreateResourceData) GetSubject() string {
|
func (c *CreateResourceData) GetSubject() (string, error) {
|
||||||
switch c.Type {
|
switch {
|
||||||
case m.ResourceTypeShort:
|
default:
|
||||||
|
return "", errors.New("无效的套餐类型")
|
||||||
|
|
||||||
|
case c.Type == m.ResourceTypeShort && c.Short != nil:
|
||||||
return c.Short.GetSubject()
|
return c.Short.GetSubject()
|
||||||
case m.ResourceTypeLong:
|
case c.Type == m.ResourceTypeLong && c.Long != nil:
|
||||||
return c.Long.GetSubject()
|
return c.Long.GetSubject()
|
||||||
}
|
}
|
||||||
panic("类型对应的数据为空")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CreateResourceData) GetAmount() decimal.Decimal {
|
func (c *CreateResourceData) GetAmount() (decimal.Decimal, error) {
|
||||||
switch c.Type {
|
switch {
|
||||||
case m.ResourceTypeShort:
|
default:
|
||||||
|
return decimal.Zero, errors.New("无效的套餐类型")
|
||||||
|
|
||||||
|
case c.Type == m.ResourceTypeShort && c.Short != nil:
|
||||||
return c.Short.GetAmount()
|
return c.Short.GetAmount()
|
||||||
case m.ResourceTypeLong:
|
case c.Type == m.ResourceTypeLong && c.Long != nil:
|
||||||
return c.Long.GetAmount()
|
return c.Long.GetAmount()
|
||||||
}
|
}
|
||||||
panic("类型对应的数据为空")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CreateResourceData) Serialize() (string, error) {
|
func (c *CreateResourceData) Serialize() (string, error) {
|
||||||
@@ -191,27 +227,37 @@ func (c *CreateResourceData) Deserialize(str string) error {
|
|||||||
return json.Unmarshal([]byte(str), c)
|
return json.Unmarshal([]byte(str), c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *CreateShortResourceData) GetSubject() string {
|
func (data *CreateShortResourceData) GetSubject() (string, error) {
|
||||||
if data.name == "" {
|
if data.name == "" {
|
||||||
var mode string
|
var mode string
|
||||||
switch data.Mode {
|
switch data.Mode {
|
||||||
case 1:
|
default:
|
||||||
|
return "", errors.New("无效的套餐模式")
|
||||||
|
|
||||||
|
case m.ResourceModeTime:
|
||||||
mode = "包时"
|
mode = "包时"
|
||||||
case 2:
|
case m.ResourceModeQuota:
|
||||||
mode = "包量"
|
mode = "包量"
|
||||||
}
|
}
|
||||||
|
|
||||||
data.name = fmt.Sprintf("短效动态%s %v 分钟", mode, data.Live/60)
|
data.name = fmt.Sprintf("短效动态%s %v 分钟", mode, data.Live/60)
|
||||||
}
|
}
|
||||||
return data.name
|
return data.name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *CreateShortResourceData) GetAmount() decimal.Decimal {
|
func (data *CreateShortResourceData) GetAmount() (decimal.Decimal, error) {
|
||||||
if data.price == nil {
|
if data.price == nil {
|
||||||
var factor int32
|
var factor int32
|
||||||
switch data.Mode {
|
switch data.Mode {
|
||||||
case 1:
|
default:
|
||||||
factor = data.DailyLimit * data.Expire
|
return decimal.Zero, errors.New("无效的套餐模式")
|
||||||
case 2:
|
|
||||||
|
case m.ResourceModeTime:
|
||||||
|
if data.Expire == nil {
|
||||||
|
return decimal.Zero, errors.New("包时套餐过期时间不能为空")
|
||||||
|
}
|
||||||
|
factor = data.Quota * *data.Expire
|
||||||
|
case m.ResourceModeQuota:
|
||||||
factor = data.Quota
|
factor = data.Quota
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,38 +269,53 @@ func (data *CreateShortResourceData) GetAmount() decimal.Decimal {
|
|||||||
var dec = decimal.Decimal{}.
|
var dec = decimal.Decimal{}.
|
||||||
Add(decimal.NewFromInt32(base * factor)).
|
Add(decimal.NewFromInt32(base * factor)).
|
||||||
Div(decimal.NewFromInt(30000))
|
Div(decimal.NewFromInt(30000))
|
||||||
|
if dec.IsZero() {
|
||||||
|
return decimal.Zero, errors.New("计算金额错误")
|
||||||
|
}
|
||||||
|
|
||||||
data.price = &dec
|
data.price = &dec
|
||||||
}
|
}
|
||||||
return *data.price
|
return *data.price, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *CreateLongResourceData) GetSubject() string {
|
func (data *CreateLongResourceData) GetSubject() (string, error) {
|
||||||
if data.name == "" {
|
if data.name == "" {
|
||||||
var mode string
|
var mode string
|
||||||
switch data.Mode {
|
switch data.Mode {
|
||||||
case 1:
|
default:
|
||||||
|
return "", errors.New("无效的套餐模式")
|
||||||
|
|
||||||
|
case m.ResourceModeTime:
|
||||||
mode = "包时"
|
mode = "包时"
|
||||||
case 2:
|
case m.ResourceModeQuota:
|
||||||
mode = "包量"
|
mode = "包量"
|
||||||
}
|
}
|
||||||
|
|
||||||
data.name = fmt.Sprintf("长效动态%s %d 小时", mode, data.Live)
|
data.name = fmt.Sprintf("长效动态%s %d 小时", mode, data.Live)
|
||||||
}
|
}
|
||||||
return data.name
|
return data.name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *CreateLongResourceData) GetAmount() decimal.Decimal {
|
func (data *CreateLongResourceData) GetAmount() (decimal.Decimal, error) {
|
||||||
if data.price == nil {
|
if data.price == nil {
|
||||||
var factor int32 = 0
|
var factor int32 = 0
|
||||||
switch data.Mode {
|
switch data.Mode {
|
||||||
|
default:
|
||||||
|
return decimal.Zero, errors.New("无效的套餐模式")
|
||||||
|
|
||||||
case m.ResourceModeTime:
|
case m.ResourceModeTime:
|
||||||
factor = data.Expire * data.DailyLimit
|
if data.Expire == nil {
|
||||||
|
return decimal.Zero, errors.New("包时套餐过期时间不能为空")
|
||||||
|
}
|
||||||
|
factor = *data.Expire * data.Quota
|
||||||
case m.ResourceModeQuota:
|
case m.ResourceModeQuota:
|
||||||
factor = data.Quota
|
factor = data.Quota
|
||||||
}
|
}
|
||||||
|
|
||||||
var base int32
|
var base int32
|
||||||
switch data.Live {
|
switch data.Live {
|
||||||
|
default:
|
||||||
|
return decimal.Zero, errors.New("无效的套餐时长")
|
||||||
case 1:
|
case 1:
|
||||||
base = 30
|
base = 30
|
||||||
case 4:
|
case 4:
|
||||||
@@ -271,11 +332,16 @@ func (data *CreateLongResourceData) GetAmount() decimal.Decimal {
|
|||||||
var dec = decimal.Decimal{}.
|
var dec = decimal.Decimal{}.
|
||||||
Add(decimal.NewFromInt32(base * factor)).
|
Add(decimal.NewFromInt32(base * factor)).
|
||||||
Div(decimal.NewFromInt(100))
|
Div(decimal.NewFromInt(100))
|
||||||
|
if dec.IsZero() {
|
||||||
|
return decimal.Zero, errors.New("计算金额错误")
|
||||||
|
}
|
||||||
|
|
||||||
data.price = &dec
|
data.price = &dec
|
||||||
}
|
}
|
||||||
return *data.price
|
return *data.price, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 交易后创建套餐
|
||||||
type ResourceOnTradeComplete struct{}
|
type ResourceOnTradeComplete struct{}
|
||||||
|
|
||||||
func (r ResourceOnTradeComplete) Check(t m.TradeType) (ProductInfo, bool) {
|
func (r ResourceOnTradeComplete) Check(t m.TradeType) (ProductInfo, bool) {
|
||||||
@@ -289,6 +355,7 @@ func (r ResourceOnTradeComplete) OnTradeComplete(info ProductInfo, trade *m.Trad
|
|||||||
return Resource.CreateResourceByTrade(trade.UserID, time.Time(*trade.CompletedAt), info.(*CreateResourceData), trade)
|
return Resource.CreateResourceByTrade(trade.UserID, time.Time(*trade.CompletedAt), info.(*CreateResourceData), trade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 服务错误
|
||||||
type ResourceServiceErr string
|
type ResourceServiceErr string
|
||||||
|
|
||||||
func (e ResourceServiceErr) Error() string {
|
func (e ResourceServiceErr) Error() string {
|
||||||
|
|||||||
@@ -35,12 +35,18 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
|
|||||||
platform := data.Platform
|
platform := data.Platform
|
||||||
method := data.Method
|
method := data.Method
|
||||||
tType := data.Product.GetType()
|
tType := data.Product.GetType()
|
||||||
subject := data.Product.GetSubject()
|
|
||||||
amount := data.Product.GetAmount()
|
|
||||||
expire := time.Now().Add(30 * time.Minute)
|
expire := time.Now().Add(30 * time.Minute)
|
||||||
|
subject, err := data.Product.GetSubject()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
amount, err := data.Product.GetAmount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// 实际支付金额,只在创建真实订单时使用
|
// 实际支付金额,只在创建真实订单时使用
|
||||||
var amountReal = data.Product.GetAmount()
|
amountReal := amount
|
||||||
if env.RunMode == env.RunModeDev {
|
if env.RunMode == env.RunModeDev {
|
||||||
amountReal = decimal.NewFromFloat(0.01)
|
amountReal = decimal.NewFromFloat(0.01)
|
||||||
}
|
}
|
||||||
@@ -60,7 +66,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var expireAt = time.Time(u.Z(coupon.ExpireAt))
|
expireAt := time.Time(u.Z(coupon.ExpireAt))
|
||||||
if !expireAt.IsZero() && expireAt.Before(now) {
|
if !expireAt.IsZero() && expireAt.Before(now) {
|
||||||
_, err = q.Coupon.
|
_, err = q.Coupon.
|
||||||
Where(q.Coupon.ID.Eq(coupon.ID)).
|
Where(q.Coupon.ID.Eq(coupon.ID)).
|
||||||
@@ -99,7 +105,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成订单号
|
// 生成订单号
|
||||||
var tradeNo, err = ID.GenSerial()
|
tradeNo, err := ID.GenSerial()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, core.NewServErr("生成订单号失败", err)
|
return nil, core.NewServErr("生成订单号失败", err)
|
||||||
}
|
}
|
||||||
@@ -692,8 +698,8 @@ type OnTradeCompletedData struct {
|
|||||||
|
|
||||||
type ProductInfo interface {
|
type ProductInfo interface {
|
||||||
GetType() m.TradeType
|
GetType() m.TradeType
|
||||||
GetSubject() string
|
GetSubject() (string, error)
|
||||||
GetAmount() decimal.Decimal
|
GetAmount() (decimal.Decimal, error)
|
||||||
Serialize() (string, error)
|
Serialize() (string, error)
|
||||||
Deserialize(str string) error
|
Deserialize(str string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,15 @@ func (s *userService) UpdateBalanceByTrade(uid int32, info *RechargeProductInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成账单
|
// 生成账单
|
||||||
err = q.Bill.Create(newForRecharge(uid, Bill.GenNo(), info.GetSubject(), info.GetAmount(), trade))
|
subject, err := info.GetSubject()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
amount, err := info.GetAmount()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = q.Bill.Create(newForRecharge(uid, Bill.GenNo(), subject, amount, trade))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("生成账单失败", err)
|
return core.NewServErr("生成账单失败", err)
|
||||||
}
|
}
|
||||||
@@ -39,23 +47,25 @@ func (s *userService) UpdateBalanceByTrade(uid int32, info *RechargeProductInfo,
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func updateBalance(q *q.Query, uid int32, info *RechargeProductInfo) (err error) {
|
func updateBalance(q *q.Query, uid int32, info *RechargeProductInfo) error {
|
||||||
|
|
||||||
// 更新余额
|
|
||||||
user, err := q.User.
|
user, err := q.User.
|
||||||
Where(q.User.ID.Eq(uid)).Take()
|
Where(q.User.ID.Eq(uid)).Take()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("查询用户失败", err)
|
return core.NewServErr("查询用户失败", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var amount = user.Balance.Add(info.GetAmount())
|
amount, err := info.GetAmount()
|
||||||
if amount.IsNegative() {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
balance := user.Balance.Add(amount)
|
||||||
|
if balance.IsNegative() {
|
||||||
return core.NewServErr("用户余额不足")
|
return core.NewServErr("用户余额不足")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = q.User.
|
_, err = q.User.
|
||||||
Where(q.User.ID.Eq(user.ID)).
|
Where(q.User.ID.Eq(user.ID)).
|
||||||
UpdateSimple(q.User.Balance.Value(amount))
|
UpdateSimple(q.User.Balance.Value(balance))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return core.NewServErr("更新用户余额失败", err)
|
return core.NewServErr("更新用户余额失败", err)
|
||||||
}
|
}
|
||||||
@@ -75,12 +85,13 @@ func (r *RechargeProductInfo) GetType() m.TradeType {
|
|||||||
return m.TradeTypeRecharge
|
return m.TradeTypeRecharge
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RechargeProductInfo) GetSubject() string {
|
func (r *RechargeProductInfo) GetSubject() (string, error) {
|
||||||
return fmt.Sprintf("账户充值 - %s元", r.GetAmount().StringFixed(2))
|
amount, _ := r.GetAmount()
|
||||||
|
return fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RechargeProductInfo) GetAmount() decimal.Decimal {
|
func (r *RechargeProductInfo) GetAmount() (decimal.Decimal, error) {
|
||||||
return decimal.NewFromInt(int64(r.Amount)).Div(decimal.NewFromInt(100))
|
return decimal.NewFromInt(int64(r.Amount)).Div(decimal.NewFromInt(100)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RechargeProductInfo) Serialize() (string, error) {
|
func (r *RechargeProductInfo) Serialize() (string, error) {
|
||||||
|
|||||||
@@ -5,19 +5,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"platform/pkg/env"
|
|
||||||
"platform/pkg/u"
|
|
||||||
"platform/web/events"
|
"platform/web/events"
|
||||||
e "platform/web/events"
|
|
||||||
g "platform/web/globals"
|
|
||||||
m "platform/web/models"
|
|
||||||
q "platform/web/queries"
|
|
||||||
s "platform/web/services"
|
s "platform/web/services"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
"gorm.io/datatypes"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleCompleteTrade(_ context.Context, task *asynq.Task) (err error) {
|
func HandleCompleteTrade(_ context.Context, task *asynq.Task) (err error) {
|
||||||
@@ -45,112 +37,10 @@ func HandleCompleteTrade(_ context.Context, task *asynq.Task) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
|
func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
|
||||||
data := new(e.RemoveChannelData)
|
batch := string(task.Payload())
|
||||||
err = json.Unmarshal(task.Payload(), data)
|
err = s.Channel.RemoveChannels(batch)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("解析任务参数失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Channel.RemoveChannels(data.Batch, data.IDs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("删除通道失败: %w", err)
|
return fmt.Errorf("删除通道失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleFlushGateway(_ context.Context, task *asynq.Task) error {
|
|
||||||
start := time.Now()
|
|
||||||
defer func() {
|
|
||||||
duration := time.Since(start)
|
|
||||||
if duration > time.Second {
|
|
||||||
slog.Warn("更新代理后备配置耗时过长", "time", duration.String())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 获取所有网关:配置组
|
|
||||||
proxies, err := s.Proxy.AllProxies(m.ProxyTypeBaiYin, true)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取网关失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, proxy := range proxies {
|
|
||||||
|
|
||||||
// 获取当前后备配置
|
|
||||||
locals := map[string]int{}
|
|
||||||
for _, channel := range proxy.Channels {
|
|
||||||
isp := channel.FilterISP.String()
|
|
||||||
prov := u.Z(channel.FilterProv)
|
|
||||||
city := u.Z(channel.FilterCity)
|
|
||||||
locals[fmt.Sprintf("%s:%s:%s", isp, prov, city)]++
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取之前的后备配置
|
|
||||||
remotes := map[string]int{}
|
|
||||||
if proxy.Meta != nil {
|
|
||||||
meta, ok := proxy.Meta.Data().([]any)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("解析网关数据失败: %T", proxy.Meta.Data())
|
|
||||||
}
|
|
||||||
for _, rawM := range meta {
|
|
||||||
m, ok := rawM.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("解析网关数据失败: %T", rawM)
|
|
||||||
}
|
|
||||||
remotes[fmt.Sprintf("%s:%s:%s", m["isp"], m["province"], m["city"])] = int(m["count"].(float64))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要更新
|
|
||||||
pass := true
|
|
||||||
for k, local := range locals {
|
|
||||||
remote, ok := remotes[k]
|
|
||||||
if !ok {
|
|
||||||
pass = false
|
|
||||||
} else {
|
|
||||||
local, remote := float64(local), float64(remote)
|
|
||||||
if remote < local*1.5 || remote > local*3 {
|
|
||||||
pass = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pass {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新后备配置
|
|
||||||
configs := make([]g.AutoConfig, 0)
|
|
||||||
for k, local := range locals {
|
|
||||||
arr := strings.Split(k, ":")
|
|
||||||
isp, prov, city := arr[0], arr[1], arr[2]
|
|
||||||
configs = append(configs, g.AutoConfig{
|
|
||||||
Isp: isp,
|
|
||||||
Province: prov,
|
|
||||||
City: city,
|
|
||||||
Count: local * 2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if env.DebugExternalChange {
|
|
||||||
err := g.Cloud.CloudConnect(g.CloudConnectReq{
|
|
||||||
Uuid: proxy.Mac,
|
|
||||||
AutoConfig: configs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("提交代理后备配置失败", "error", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bytes, _ := json.Marshal(configs)
|
|
||||||
slog.Debug("更新代理后备配置", "proxy", proxy.IP.String(), "config", string(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := q.Proxy.
|
|
||||||
Where(q.Proxy.ID.Eq(proxy.ID)).
|
|
||||||
UpdateSimple(q.Proxy.Meta.Value(datatypes.NewJSONType(configs)))
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("更新代理后备配置失败", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
36
web/web.go
36
web/web.go
@@ -6,7 +6,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"platform/web/events"
|
"platform/web/events"
|
||||||
base "platform/web/globals"
|
deps "platform/web/globals"
|
||||||
"platform/web/tasks"
|
"platform/web/tasks"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -19,12 +19,12 @@ func RunApp(pCtx context.Context) error {
|
|||||||
g, ctx := errgroup.WithContext(pCtx)
|
g, ctx := errgroup.WithContext(pCtx)
|
||||||
|
|
||||||
// 初始化依赖
|
// 初始化依赖
|
||||||
err := base.Init(ctx)
|
err := deps.Init(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("初始化依赖失败: %w", err)
|
return fmt.Errorf("初始化依赖失败: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
err := base.Close()
|
err := deps.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("关闭依赖失败", "error", err)
|
slog.Error("关闭依赖失败", "error", err)
|
||||||
}
|
}
|
||||||
@@ -39,10 +39,6 @@ func RunApp(pCtx context.Context) error {
|
|||||||
return RunTask(ctx)
|
return RunTask(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
g.Go(func() error {
|
|
||||||
return RunSchedule(ctx)
|
|
||||||
})
|
|
||||||
|
|
||||||
return g.Wait()
|
return g.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,30 +72,9 @@ func RunWeb(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunSchedule(ctx context.Context) error {
|
|
||||||
var scheduler = asynq.NewSchedulerFromRedisClient(base.Redis, &asynq.SchedulerOpts{
|
|
||||||
Location: time.Local,
|
|
||||||
})
|
|
||||||
|
|
||||||
scheduler.Register("@every 5s", events.NewFlushGateway(5*time.Second))
|
|
||||||
|
|
||||||
// 停止服务
|
|
||||||
go func() {
|
|
||||||
<-ctx.Done()
|
|
||||||
scheduler.Shutdown()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 启动服务
|
|
||||||
err := scheduler.Run()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("调度服务运行失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunTask(ctx context.Context) error {
|
func RunTask(ctx context.Context) error {
|
||||||
var server = asynq.NewServerFromRedisClient(base.Redis, asynq.Config{
|
var server = asynq.NewServerFromRedisClient(deps.Redis, asynq.Config{
|
||||||
|
ShutdownTimeout: 5 * time.Second,
|
||||||
ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) {
|
ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) {
|
||||||
slog.Error("任务执行失败", "task", task.Type(), "error", err)
|
slog.Error("任务执行失败", "task", task.Type(), "error", err)
|
||||||
}),
|
}),
|
||||||
@@ -108,7 +83,6 @@ func RunTask(ctx context.Context) error {
|
|||||||
var mux = asynq.NewServeMux()
|
var mux = asynq.NewServeMux()
|
||||||
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
|
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
|
||||||
mux.HandleFunc(events.CompleteTrade, tasks.HandleCompleteTrade)
|
mux.HandleFunc(events.CompleteTrade, tasks.HandleCompleteTrade)
|
||||||
mux.HandleFunc(events.FlushGateway, tasks.HandleFlushGateway)
|
|
||||||
|
|
||||||
// 停止服务
|
// 停止服务
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
Reference in New Issue
Block a user